Compare commits

..

No commits in common. "main" and "0.0.0" have entirely different histories.
main ... 0.0.0

35 changed files with 23 additions and 2594 deletions

2
.gitignore vendored
View file

@ -4,5 +4,3 @@ vendor/
build/ build/
dist/ dist/
fail2ban_exporter

View file

@ -1,64 +1,22 @@
image: golang:latest
before_script:
- make install-deps
stages: stages:
- test - test
- build
- release
.go_template:
image: golang:latest
sast:
stage: test
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
format: format:
extends: .go_template
stage: test stage: test
script: script:
- make check/fmt - make format
vet: dependencies:
extends: .go_template
stage: test stage: test
allow_failure: true
script: script:
- make vet - make go-mod-tidy
test: test:
extends: .go_template
stage: test stage: test
script: script:
- make test - make test
build:
extends: .go_template
stage: build
script:
- make build
artifacts:
paths:
- fail2ban_exporter
expire_in: 1 day
release:
stage: release
image: docker:stable
services:
- docker:dind
variables:
DOCKER_REGISTRY: $CI_REGISTRY
DOCKER_USERNAME: $CI_REGISTRY_USER
DOCKER_PASSWORD: $CI_REGISTRY_PASSWORD
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_TAG =~ /^v.*$/
script: |
docker run --rm --privileged \
-v $PWD:/go/src/gitlab.com/hectorjsmith/fail2ban-prometheus-exporter \
-w /go/src/gitlab.com/hectorjsmith/fail2ban-prometheus-exporter \
-v /var/run/docker.sock:/var/run/docker.sock \
-e DOCKER_USERNAME -e DOCKER_PASSWORD -e DOCKER_REGISTRY \
-e GITLAB_TOKEN \
goreleaser/goreleaser release --clean

View file

@ -1,97 +0,0 @@
project_name: fail2ban_exporter
builds:
- env: [CGO_ENABLED=0]
binary: fail2ban_exporter
goos:
- linux
- darwin
goarch:
- amd64
- "386"
- arm
- arm64
goarm:
- "6"
- "7"
dockers:
- image_templates:
- "registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}-amd64"
- "registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}.{{ .Minor }}-amd64"
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Version }}-amd64'
- "registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest-amd64"
extra_files:
- health
use: buildx
dockerfile: Dockerfile.goreleaser
build_flag_templates:
- "--pull"
- "--platform=linux/amd64"
- --label=org.opencontainers.image.title={{ .ProjectName }}
- --label=org.opencontainers.image.description={{ .ProjectName }}
- --label=org.opencontainers.image.url=https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter
- --label=org.opencontainers.image.source=https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=MIT
- image_templates:
- "registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}-arm64"
- "registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}.{{ .Minor }}-arm64"
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Version }}-arm64'
- "registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest-arm64"
extra_files:
- health
use: buildx
dockerfile: Dockerfile.goreleaser
build_flag_templates:
- "--pull"
- "--platform=linux/arm64"
- --label=org.opencontainers.image.title={{ .ProjectName }}
- --label=org.opencontainers.image.description={{ .ProjectName }}
- --label=org.opencontainers.image.url=https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter
- --label=org.opencontainers.image.source=https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=MIT
goarch: arm64
docker_manifests:
- name_template: 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}'
image_templates:
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}-amd64'
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}-arm64'
- name_template: 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}.{{ .Minor }}'
image_templates:
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}.{{ .Minor }}-amd64'
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Major }}.{{ .Minor }}-arm64'
- name_template: 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Version }}'
image_templates:
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Version }}-amd64'
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:{{ .Version }}-arm64'
- name_template: 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest'
image_templates:
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest-amd64'
- 'registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest-arm64'
changelog:
groups:
- title: "⛔ Breaking Changes"
regexp: '^.*?!:.+$'
order: 0
- title: "🎉 Features"
regexp: '^.*?feat(\(\w+\))??:.+$'
order: 1
- title: "🐛 Fixes"
regexp: '^.*?fix(\(\w+\))??:.+$'
order: 2
- title: "📑 Other"
order: 999
filters:
exclude:
- "^Merge"
- "^merge"

View file

@ -1,30 +0,0 @@
FROM golang:1.20-buster AS build
# Create build workspace folder
WORKDIR /workspace
ADD . /workspace
# Install updates and build tools
RUN apt update --yes && \
apt install --yes build-essential
# Build the actual binary
RUN make build
# -- -- -- -- -- --
# Set up image to run the tool
FROM alpine
# Create main app folder to run from
WORKDIR /app
# Copy built binary from build image
COPY --from=build /workspace/fail2ban_exporter /app
# Setup a healthcheck
COPY health /app/health
RUN apk add curl
HEALTHCHECK --interval=10s --timeout=4s --retries=3 CMD /app/health
ENTRYPOINT ["/app/fail2ban_exporter"]

View file

@ -1,15 +0,0 @@
FROM alpine
# Create main app folder to run from
WORKDIR /app
# Copy compiled binary to release image
# (must build the binary before running docker build)
COPY fail2ban_exporter /app/fail2ban_exporter
# Setup a healthcheck
COPY health /app/health
RUN apk add curl
HEALTHCHECK --interval=10s --timeout=4s --retries=3 CMD /app/health
ENTRYPOINT ["/app/fail2ban_exporter"]

View file

@ -1,55 +1,15 @@
# List make commands install-deps:
.PHONY: ls
ls:
cat Makefile | grep "^[a-zA-Z#].*" | cut -d ":" -f 1 | sed s';#;\n#;'g
# Download dependencies
.PHONY: download
download:
go mod download go mod download
# Update project dependencies # Standard go test
.PHONY: update test:
update:
go get -u
go mod download
go mod tidy
# Run project tests
.PHONY: test
test: download
go test ./... -v -race go test ./... -v -race
# Look for "suspicious constructs" in source code # Make sure no unnecessary dependencies are present
.PHONY: vet go-mod-tidy:
vet: download go mod tidy -v
go vet ./... git diff-index --quiet HEAD
# Format code format:
.PHONY: fmt go fmt $(go list ./... | grep -v /vendor/)
fmt: download go vet $(go list ./... | grep -v /vendor/)
go mod tidy
go fmt ./...
# Check for unformatted go code
.PHONY: check/fmt
check/fmt: download
test -z $(shell gofmt -l .)
# Build project
.PHONY: build
build:
CGO_ENABLED=0 go build \
-ldflags "\
-X main.version=${shell git describe --tags} \
-X main.commit=${shell git rev-parse HEAD} \
-X main.date=${shell date --iso-8601=seconds} \
-X main.builtBy=manual \
" \
-o fail2ban_exporter \
exporter.go
# Build project docker container
.PHONY: build/docker
build/docker:
docker build -t fail2ban-prometheus-exporter .

238
README.md
View file

@ -1,239 +1,3 @@
# Fail2Ban Prometheus Exporter # Fail2Ban Prometheus Exporter
[![Pipeline](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/badges/main/pipeline.svg)](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter) Go tool to collect and export metrics on Fail2Ban
[![Go Report Card](https://goreportcard.com/badge/gitlab.com/hectorjsmith/fail2ban-prometheus-exporter)](https://goreportcard.com/report/gitlab.com/hectorjsmith/fail2ban-prometheus-exporter)
Collect metrics from a running fail2ban instance.
## Table of Contents
1. Quick Start
2. Metrics
3. Configuration
4. Building from source
5. Textfile metrics
6. Troubleshooting
## 1. Quick Start
The exporter can be run as a standalone binary or a docker container.
### 1.1. Standalone
The following command will start collecting metrics from the `/var/run/fail2ban/fail2ban.sock` file and expose them on port `9191`.
```
$ fail2ban_exporter --collector.f2b.socket=/var/run/fail2ban/fail2ban.sock --web.listen-address=":9191"
2022/02/20 09:54:06 fail2ban exporter version 0.8.1
2022/02/20 09:54:06 starting server at :9191
2022/02/20 09:54:06 reading metrics from fail2ban socket: /var/run/fail2ban/fail2ban.sock
2022/02/20 09:54:06 metrics available at '/metrics'
2022/02/20 09:54:06 ready
```
Binary files for each release can be found on the [releases](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/releases) page.
There is also an [example systemd service file](/_examples/systemd/fail2ban_exporter.service) included in the repository.
This is a starting point to run the exporter as a service.
### 1.2. Docker
**Docker run**
```
docker run -d \
--name "fail2ban-exporter" \
-v /var/run/fail2ban:/var/run/fail2ban:ro \
-p "9191:9191" \
registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
```
**Docker compose**
```
version: "2"
services:
exporter:
image: registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
volumes:
- /var/run/fail2ban/:/var/run/fail2ban:ro
ports:
- "9191:9191"
```
Use the `:latest` tag to get the latest stable release.
See the [registry page](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/container_registry) for all available tags.
**NOTE:** While it is possible to mount the `fail2ban.sock` file directly, it is recommended to mount the parent folder instead.
The `.sock` file is deleted by fail2ban on shutdown and re-created on startup and this causes problems for the docker mount.
See [this reply](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/issues/11#note_665003499) for more details.
## 2. Metrics
The exporter exposes the following metrics:
*All metric names are prefixed with `f2b_`*
| Metric | Description | Example |
|------------------------------|------------------------------------------------------------------------------------|-----------------------------------------------------|
| `up` | Returns 1 if the exporter is up and running | `f2b_up 1` |
| `errors` | Count the number of errors since startup by type | |
| `errors{type="socket_conn"}` | Errors connecting to the fail2ban socket (e.g. connection refused) | `f2b_errors{type="socket_conn"} 0` |
| `errors{type="socket_req"}` | Errors sending requests to the fail2ban server (e.g. invalid responses) | `f2b_errors{type="socket_req"} 0` |
| `jail_count` | Number of jails configured in fail2ban | `f2b_jail_count 2` |
| `jail_banned_current` | Number of IPs currently banned per jail | `f2b_jail_banned_current{jail="sshd"} 15` |
| `jail_banned_total` | Total number of banned IPs since fail2ban startup per jail (includes expired bans) | `f2b_jail_banned_total{jail="sshd"} 31` |
| `jail_failed_current` | Number of current failures per jail | `f2b_jail_failed_current{jail="sshd"} 6` |
| `jail_failed_total` | Total number of failures since fail2ban startup per jail | `f2b_jail_failed_total{jail="sshd"} 125` |
| `jail_config_ban_time` | How long an IP is banned for in this jail (in seconds) | `f2b_config_jail_ban_time{jail="sshd"} 600` |
| `jail_config_find_time` | How far back the filter will look for failures in this jail (in seconds) | `f2b_config_jail_find_time{jail="sshd"} 600` |
| `jail_config_max_retry` | The max number of failures allowed before banning an IP in this jail | `f2b_config_jail_max_retries{jail="sshd"} 5` |
| `version` | Version string of the exporter and fail2ban | `f2b_version{exporter="0.5.0",fail2ban="0.11.1"} 1` |
The metrics above correspond to the matching fields in the `fail2ban-client status <jail>` command:
```
Status for the jail: sshd
|- Filter
| |- Currently failed: 6
| |- Total failed: 125
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 15
|- Total banned: 31
`- Banned IP list: ...
```
### 2.1. Grafana
The metrics exported by this tool are compatible with Prometheus and Grafana.
A sample grafana dashboard can be found in the [grafana.json](/_examples/grafana/dashboard.json) file.
Just import the contents of this file into a new Grafana dashboard to get started.
The dashboard supports displaying data from multiple exporters. Use the `instance` dashboard variable to select which ones to display.
*(Sample dashboard is compatible with Grafana `9.1.8` and above)*
## 3. Configuration
The exporter is configured with CLI flags and environment variables.
There are no configuration files.
**CLI flags**
```
🚀 Collect prometheus metrics from a running Fail2Ban instance
Flags:
-h, --help Show context-sensitive help.
-v, --version Show version info and exit
--dry-run Attempt to connect to the fail2ban socket then exit
before starting the server
--web.listen-address=":9191" Address to use for the metrics server
($F2B_WEB_LISTEN_ADDRESS)
--collector.f2b.socket="/var/run/fail2ban/fail2ban.sock"
Path to the fail2ban server socket
($F2B_COLLECTOR_SOCKET)
--collector.f2b.exit-on-socket-connection-error
When set to true the exporter will immediately
exit on a fail2ban socket connection error
($F2B_EXIT_ON_SOCKET_CONN_ERROR)
--collector.textfile.directory=STRING
Directory to read text files with metrics from
($F2B_COLLECTOR_TEXT_PATH)
--web.basic-auth.username=STRING
Username to use to protect endpoints with basic auth
($F2B_WEB_BASICAUTH_USER)
--web.basic-auth.password=STRING
Password to use to protect endpoints with basic auth
($F2B_WEB_BASICAUTH_PASS)
```
**Environment variables**
Each environment variable corresponds to a CLI flag.
If both are specified, the CLI flag takes precedence.
| Environment variable | Corresponding CLI flag |
|---------------------------------|---------------------------------------------------|
| `F2B_COLLECTOR_SOCKET` | `--collector.f2b.socket` |
| `F2B_COLLECTOR_TEXT_PATH` | `--collector.textfile.directory` |
| `F2B_WEB_LISTEN_ADDRESS` | `--web.listen-address` |
| `F2B_WEB_BASICAUTH_USER` | `--web.basic-auth.username` |
| `F2B_WEB_BASICAUTH_PASS` | `--web.basic-auth.password` |
| `F2B_EXIT_ON_SOCKET_CONN_ERROR` | `--collector.f2b.exit-on-socket-connection-error` |
## 4. Building from source
Building from source has the following dependencies:
- Go v1.20
- Make
From there, simply run `make build`
This will download the necessary dependencies and build a `fail2ban_exporter` binary in the root of the project.
## 5. Textfile metrics
For more flexibility the exporter also allows exporting metrics collected from a text file.
To enable textfile metrics provide the directory to read files from with the `--collector.textfile.directory` flag.
Metrics collected from these files will be exposed directly alongside the other metrics without any additional processing.
This means that it is the responsibility of the file creator to ensure the format is correct.
By exporting textfile metrics an extra metric is also exported with an error count for each file:
```
# HELP textfile_error Checks for errors while reading text files
# TYPE textfile_error gauge
textfile_error{path="file.prom"} 0
```
**NOTE:** Any file not ending with `.prom` will be ignored.
**Running in Docker**
To collect textfile metrics inside a docker container, a couple of things need to be done:
1. Mount the folder with the metrics files
2. Set the `F2B_COLLECTOR_TEXT_PATH` environment variable
*For example:*
```
docker run -d \
--name "fail2ban-exporter" \
-v /var/run/fail2ban:/var/run/fail2ban:ro \
-v /path/to/metrics:/app/metrics/:ro \
-e F2B_COLLECTOR_TEXT_PATH=/app/metrics \
-p "9191:9191" \
registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
```
## 6. Troubleshooting
### 6.1. "no such file or directory"
```
error opening socket: dial unix /var/run/fail2ban/fail2ban.sock: connect: no such file or directory
```
There are a couple of potential causes for the error above.
**File not found**
The first is that the file does not exist, so first check that the file path shown in the error actually exists on the system running the exporter.
The fail2ban server may be storing the socket file in another location on your machine.
If you are using docker, make sure the correct host folder was mounted to the correct location.
If the file is not in the expected location, you can run the exporter with the corresponding CLI flag or environment variable to use a different file path.
**Permissions**
If the file does exist, the likely cause are file permissions.
By default, the fail2ban server runs as the `root` user and the socket file can only be accessed by the same user.
If you are running the exporter as a non-root user, it will not be able to open the socket file to read/write commands to the server, leading to the error above.
In this case there are a few solutions:
1. Run the exporter as the same user as fail2ban (usually `root`)
2. Update the fail2ban server config to run as a non-root user, then run the exporter as the same user
3. Update the socket file permissions to be less restrictive
I would recommend option `1.` since it is the simplest. Option `2.` is a bit more complex, check the [fail2ban server documentation](https://coderwall.com/p/haj28a/running-rootless-fail2ban-on-debian) for more details. And option `3.` is just a temporary fix. The socket file gets re-created each time the fail2ban server is restarted and the original permissions will be restored, so you will need to update the permissions every time the server restarts.

View file

@ -1,877 +0,0 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__elements": {},
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "9.1.8"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "table",
"name": "Table",
"version": ""
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 2,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": ".*Time"
},
"properties": [
{
"id": "unit",
"value": "s"
}
]
}
]
},
"gridPos": {
"h": 6,
"w": 24,
"x": 0,
"y": 0
},
"id": 206,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "9.1.8",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "f2b_config_jail_max_retries{instance=~\"$instance\"}",
"format": "table",
"instant": true,
"interval": "",
"legendFormat": "{{jail}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "f2b_config_jail_ban_time{instance=~\"$instance\"}",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "{{jail}}",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "f2b_config_jail_find_time{instance=~\"$instance\"}",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "{{jail}}",
"refId": "C"
}
],
"title": "F2B Config",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "groupBy",
"options": {
"fields": {
"Value #A": {
"aggregations": [
"lastNotNull"
],
"operation": "aggregate"
},
"Value #B": {
"aggregations": [
"lastNotNull"
],
"operation": "aggregate"
},
"Value #C": {
"aggregations": [
"lastNotNull"
],
"operation": "aggregate"
},
"instance": {
"aggregations": [],
"operation": "groupby"
},
"jail": {
"aggregations": [],
"operation": "groupby"
}
}
}
},
{
"id": "organize",
"options": {
"excludeByName": {},
"indexByName": {},
"renameByName": {
"Value #A (lastNotNull)": "Max Retries",
"Value #B (lastNotNull)": "Ban Time",
"Value #C (lastNotNull)": "Find Time",
"jail": "Jail"
}
}
}
],
"transparent": true,
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 6
},
"id": 190,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_failed_total{instance=~\"$instance\"}",
"hide": false,
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Failures (Total)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 6
},
"id": 191,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_banned_total{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Bans (Total)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 14
},
"id": 208,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_failed_current{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Failures (Current)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 14
},
"id": 209,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_banned_current{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Bans (Current)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 1,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 22
},
"id": 203,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_up{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "Up ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Up",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 1,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 22
},
"id": 204,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_errors{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{type}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Exporter Errors",
"transparent": true,
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 37,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"label": "Data Source",
"multi": false,
"name": "DataSource",
"options": [],
"query": "prometheus",
"queryValue": "",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"definition": "f2b_up",
"description": "Select which instance(s) to show",
"hide": 0,
"includeAll": false,
"label": "Instance",
"multi": true,
"name": "instance",
"options": [],
"query": {
"query": "f2b_up",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "/.*instance=\"([^\"]+)\"/",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "F2B",
"uid": "cTkH9AT7z",
"version": 3,
"weekStart": ""
}

View file

@ -1,7 +0,0 @@
# Systemd
The `.service` file in this directory should be copied to the `/etc/systemd/system/` folder.
- It expects the binary file to be installed at `/usr/sbin/fail2ban_exporter`.
- It expects a user named `fail2ban_exporter` to exist. This user should not have a shell or any special privileges aside from read-access to the fail2ban socket file.
The `ExecStart` line can be modified to add any custom CLI flags.

View file

@ -1,19 +0,0 @@
[Unit]
Description=Fail2ban metric exporter for Prometheus
Documentation=https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/blob/main/README.md
Requires=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/sbin/fail2ban_exporter
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
# Currently need to run the exporter as root to ensure it has read/write access to the
# fail2ban socket file.
User=root
Group=root
[Install]
WantedBy=multi-user.target

View file

@ -1,29 +0,0 @@
package auth
import (
"fmt"
"net/http"
)
func NewBasicAuthProvider(username, password string) AuthProvider {
return &basicAuthProvider{
hashedAuth: encodeBasicAuth(username, password),
}
}
type basicAuthProvider struct {
hashedAuth string
}
func (p *basicAuthProvider) IsAllowed(request *http.Request) bool {
username, password, ok := request.BasicAuth()
if !ok {
return false
}
requestAuth := encodeBasicAuth(username, password)
return p.hashedAuth == requestAuth
}
func encodeBasicAuth(username, password string) string {
return HashString(fmt.Sprintf("%s:%s", username, password))
}

View file

@ -1,53 +0,0 @@
package auth
import (
"net/http"
"net/http/httptest"
"testing"
)
func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithCorrectCreds_THEN_TrueReturned(t *testing.T) {
// assemble
username := "u1"
password := HashString("abc")
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
request.SetBasicAuth(username, password)
provider := NewBasicAuthProvider(username, password)
// act
result := provider.IsAllowed(request)
// assert
if !result {
t.Errorf("expected request to be allowed, but failed")
}
}
func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithoutCreds_THEN_FalseReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
provider := NewBasicAuthProvider("u1", "p1")
// act
result := provider.IsAllowed(request)
// assert
if result {
t.Errorf("expected request to be denied, but was allowed")
}
}
func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithWrongCreds_THEN_FalseReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
request.SetBasicAuth("wrong", "pw")
provider := NewBasicAuthProvider("u1", "p1")
// act
result := provider.IsAllowed(request)
// assert
if result {
t.Errorf("expected request to be denied, but was allowed")
}
}

View file

@ -1,14 +0,0 @@
package auth
import "net/http"
func NewEmptyAuthProvider() AuthProvider {
return &emptyAuthProvider{}
}
type emptyAuthProvider struct {
}
func (p *emptyAuthProvider) IsAllowed(request *http.Request) bool {
return true
}

View file

@ -1,36 +0,0 @@
package auth
import (
"net/http"
"net/http/httptest"
"testing"
)
func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithoutAuth_THEN_TrueReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
provider := NewEmptyAuthProvider()
// act
response := provider.IsAllowed(request)
// assert
if !response {
t.Errorf("expected request to be allowed, but failed")
}
}
func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithAuth_THEN_TrueReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
request.SetBasicAuth("user", "pass")
provider := NewEmptyAuthProvider()
// act
response := provider.IsAllowed(request)
// assert
if !response {
t.Errorf("expected request to be allowed, but failed")
}
}

View file

@ -1,18 +0,0 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
)
func hash(data []byte) []byte {
if len(data) == 0 {
return []byte{}
}
b := sha256.Sum256(data)
return b[:]
}
func HashString(data string) string {
return hex.EncodeToString(hash([]byte(data)))
}

View file

@ -1,26 +0,0 @@
package auth
import (
"reflect"
"testing"
)
func TestHashString(t *testing.T) {
tests := []struct {
name string
args string
want string
}{
{"Happy path #1", "123", "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"},
{"Happy path #2", "hello world", "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"},
{"Happy path #3", "H3Ll0_W0RLD", "d58a27fe9a6e73a1d8a67189fb8acace047e7a1a795276a0056d3717ad61bd0e"},
{"Blank string", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HashString(tt.args); !reflect.DeepEqual(got, tt.want) {
t.Errorf("HashString() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,9 +0,0 @@
package auth
import (
"net/http"
)
type AuthProvider interface {
IsAllowed(*http.Request) bool
}

View file

@ -1,84 +0,0 @@
package cfg
import (
"fmt"
"log"
"os"
"github.com/alecthomas/kong"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth"
)
var cliStruct struct {
VersionMode bool `name:"version" short:"v" help:"Show version info and exit"`
DryRunMode bool `name:"dry-run" help:"Attempt to connect to the fail2ban socket then exit before starting the server"`
ServerAddress string `name:"web.listen-address" env:"F2B_WEB_LISTEN_ADDRESS" help:"Address to use for the metrics server" default:"${default_address}"`
F2bSocketPath string `name:"collector.f2b.socket" env:"F2B_COLLECTOR_SOCKET" help:"Path to the fail2ban server socket" default:"${default_socket}"`
ExitOnSocketError bool `name:"collector.f2b.exit-on-socket-connection-error" env:"F2B_EXIT_ON_SOCKET_CONN_ERROR" help:"When set to true the exporter will immediately exit on a fail2ban socket connection error"`
TextFileExporterPath string `name:"collector.textfile.directory" env:"F2B_COLLECTOR_TEXT_PATH" help:"Directory to read text files with metrics from"`
BasicAuthUser string `name:"web.basic-auth.username" env:"F2B_WEB_BASICAUTH_USER" help:"Username to use to protect endpoints with basic auth"`
BasicAuthPass string `name:"web.basic-auth.password" env:"F2B_WEB_BASICAUTH_PASS" help:"Password to use to protect endpoints with basic auth"`
}
func Parse() *AppSettings {
ctx := kong.Parse(
&cliStruct,
kong.Vars{
"default_socket": "/var/run/fail2ban/fail2ban.sock",
"default_address": ":9191",
},
kong.Name("fail2ban_exporter"),
kong.Description("🚀 Export prometheus metrics from a running Fail2Ban instance"),
kong.UsageOnError(),
)
validateFlags(ctx)
settings := &AppSettings{
VersionMode: cliStruct.VersionMode,
DryRunMode: cliStruct.DryRunMode,
MetricsAddress: cliStruct.ServerAddress,
Fail2BanSocketPath: cliStruct.F2bSocketPath,
FileCollectorPath: cliStruct.TextFileExporterPath,
ExitOnSocketConnError: cliStruct.ExitOnSocketError,
AuthProvider: createAuthProvider(),
}
return settings
}
func createAuthProvider() auth.AuthProvider {
username := cliStruct.BasicAuthUser
password := cliStruct.BasicAuthPass
if len(username) == 0 && len(password) == 0 {
return auth.NewEmptyAuthProvider()
}
log.Print("basic auth enabled")
return auth.NewBasicAuthProvider(username, password)
}
func validateFlags(cliCtx *kong.Context) {
var flagsValid = true
var messages = []string{}
if !cliStruct.VersionMode {
if cliStruct.F2bSocketPath == "" {
messages = append(messages, "error: fail2ban socket path must not be blank")
flagsValid = false
}
if cliStruct.ServerAddress == "" {
messages = append(messages, "error: invalid server address, must not be blank")
flagsValid = false
}
if (len(cliStruct.BasicAuthUser) > 0) != (len(cliStruct.BasicAuthPass) > 0) {
messages = append(messages, "error: to enable basic auth both the username and the password must be provided")
flagsValid = false
}
}
if !flagsValid {
cliCtx.PrintUsage(false)
fmt.Println()
for i := 0; i < len(messages); i++ {
fmt.Println(messages[i])
}
os.Exit(1)
}
}

View file

@ -1,13 +0,0 @@
package cfg
import "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth"
type AppSettings struct {
VersionMode bool
DryRunMode bool
MetricsAddress string
Fail2BanSocketPath string
FileCollectorPath string
AuthProvider auth.AuthProvider
ExitOnSocketConnError bool
}

View file

@ -1,92 +0,0 @@
package f2b
import (
"log"
"os"
"github.com/prometheus/client_golang/prometheus"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/cfg"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/socket"
)
type Collector struct {
socketPath string
exporterVersion string
lastError error
socketConnectionErrorCount int
socketRequestErrorCount int
exitOnSocketConnError bool
}
func NewExporter(appSettings *cfg.AppSettings, exporterVersion string) *Collector {
log.Printf("reading fail2ban metrics from socket file: %s", appSettings.Fail2BanSocketPath)
printFail2BanServerVersion(appSettings.Fail2BanSocketPath)
return &Collector{
socketPath: appSettings.Fail2BanSocketPath,
exporterVersion: exporterVersion,
lastError: nil,
socketConnectionErrorCount: 0,
socketRequestErrorCount: 0,
exitOnSocketConnError: appSettings.ExitOnSocketConnError,
}
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- metricServerUp
ch <- metricJailCount
ch <- metricJailFailedCurrent
ch <- metricJailFailedTotal
ch <- metricJailBannedCurrent
ch <- metricJailBannedTotal
ch <- metricErrorCount
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
s, err := socket.ConnectToSocket(c.socketPath)
if err != nil {
log.Printf("error opening socket: %v", err)
c.socketConnectionErrorCount++
if c.exitOnSocketConnError {
os.Exit(1)
}
} else {
defer s.Close()
}
c.collectServerUpMetric(ch, s)
if err == nil && s != nil {
c.collectJailMetrics(ch, s)
c.collectVersionMetric(ch, s)
}
c.collectErrorCountMetric(ch)
}
func (c *Collector) IsHealthy() bool {
s, err := socket.ConnectToSocket(c.socketPath)
if err != nil {
log.Printf("error opening socket: %v", err)
c.socketConnectionErrorCount++
return false
}
pingSuccess, err := s.Ping()
if err != nil {
log.Printf("error pinging fail2ban server: %v", err)
c.socketRequestErrorCount++
return false
}
return pingSuccess
}
func printFail2BanServerVersion(socketPath string) {
s, err := socket.ConnectToSocket(socketPath)
if err != nil {
log.Printf("error connecting to socket: %v", err)
} else {
version, err := s.GetServerVersion()
if err != nil {
log.Printf("error interacting with socket: %v", err)
} else {
log.Printf("successfully connected to fail2ban socket! fail2ban version: %s", version)
}
}
}

View file

@ -1,179 +0,0 @@
package f2b
import (
"github.com/prometheus/client_golang/prometheus"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/socket"
"log"
)
const (
namespace = "f2b"
)
var (
metricErrorCount = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "errors"),
"Number of errors found since startup",
[]string{"type"}, nil,
)
metricServerUp = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"Check if the fail2ban server is up",
nil, nil,
)
metricJailCount = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_count"),
"Number of defined jails",
nil, nil,
)
metricJailFailedCurrent = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_failed_current"),
"Number of current failures on this jail's filter",
[]string{"jail"}, nil,
)
metricJailFailedTotal = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_failed_total"),
"Number of total failures on this jail's filter",
[]string{"jail"}, nil,
)
metricJailBannedCurrent = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_banned_current"),
"Number of IPs currently banned in this jail",
[]string{"jail"}, nil,
)
metricJailBannedTotal = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_banned_total"),
"Total number of IPs banned by this jail (includes expired bans)",
[]string{"jail"}, nil,
)
metricJailBanTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "config", "jail_ban_time"),
"How long an IP is banned for in this jail (in seconds)",
[]string{"jail"}, nil,
)
metricJailFindTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "config", "jail_find_time"),
"How far back will the filter look for failures in this jail (in seconds)",
[]string{"jail"}, nil,
)
metricJailMaxRetry = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "config", "jail_max_retries"),
"The number of failures allowed until the IP is banned by this jail",
[]string{"jail"}, nil,
)
metricVersionInfo = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "version"),
"Version of the exporter and fail2ban server",
[]string{"exporter", "fail2ban"}, nil,
)
)
func (c *Collector) collectErrorCountMetric(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
metricErrorCount, prometheus.CounterValue, float64(c.socketConnectionErrorCount), "socket_conn",
)
ch <- prometheus.MustNewConstMetric(
metricErrorCount, prometheus.CounterValue, float64(c.socketRequestErrorCount), "socket_req",
)
}
func (c *Collector) collectServerUpMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
var serverUp float64 = 0
if s != nil {
pingSuccess, err := s.Ping()
if err != nil {
c.socketRequestErrorCount++
log.Print(err)
}
if err == nil && pingSuccess {
serverUp = 1
}
}
ch <- prometheus.MustNewConstMetric(
metricServerUp, prometheus.GaugeValue, serverUp,
)
}
func (c *Collector) collectJailMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
jails, err := s.GetJails()
var count float64 = 0
if err != nil {
c.socketRequestErrorCount++
log.Print(err)
}
if err == nil {
count = float64(len(jails))
}
ch <- prometheus.MustNewConstMetric(
metricJailCount, prometheus.GaugeValue, count,
)
for i := range jails {
c.collectJailStatsMetric(ch, s, jails[i])
c.collectJailConfigMetrics(ch, s, jails[i])
}
}
func (c *Collector) collectJailStatsMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) {
stats, err := s.GetJailStats(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get stats for jail %s: %v", jail, err)
return
}
ch <- prometheus.MustNewConstMetric(
metricJailFailedCurrent, prometheus.GaugeValue, float64(stats.FailedCurrent), jail,
)
ch <- prometheus.MustNewConstMetric(
metricJailFailedTotal, prometheus.GaugeValue, float64(stats.FailedTotal), jail,
)
ch <- prometheus.MustNewConstMetric(
metricJailBannedCurrent, prometheus.GaugeValue, float64(stats.BannedCurrent), jail,
)
ch <- prometheus.MustNewConstMetric(
metricJailBannedTotal, prometheus.GaugeValue, float64(stats.BannedTotal), jail,
)
}
func (c *Collector) collectJailConfigMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) {
banTime, err := s.GetJailBanTime(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get ban time for jail %s: %v", jail, err)
} else {
ch <- prometheus.MustNewConstMetric(
metricJailBanTime, prometheus.GaugeValue, float64(banTime), jail,
)
}
findTime, err := s.GetJailFindTime(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get find time for jail %s: %v", jail, err)
} else {
ch <- prometheus.MustNewConstMetric(
metricJailFindTime, prometheus.GaugeValue, float64(findTime), jail,
)
}
maxRetry, err := s.GetJailMaxRetries(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get max retries for jail %s: %v", jail, err)
} else {
ch <- prometheus.MustNewConstMetric(
metricJailMaxRetry, prometheus.GaugeValue, float64(maxRetry), jail,
)
}
}
func (c *Collector) collectVersionMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
fail2banVersion, err := s.GetServerVersion()
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get fail2ban server version: %v", err)
}
ch <- prometheus.MustNewConstMetric(
metricVersionInfo, prometheus.GaugeValue, float64(1), c.exporterVersion, fail2banVersion,
)
}

View file

@ -1,48 +0,0 @@
package textfile
import (
"github.com/prometheus/client_golang/prometheus"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/cfg"
"log"
)
type Collector struct {
enabled bool
folderPath string
fileMap map[string]*fileData
}
type fileData struct {
readErrors int
fileName string
fileContents []byte
}
func NewCollector(appSettings *cfg.AppSettings) *Collector {
collector := &Collector{
enabled: appSettings.FileCollectorPath != "",
folderPath: appSettings.FileCollectorPath,
fileMap: make(map[string]*fileData),
}
if collector.enabled {
log.Printf("reading textfile metrics from: %s", collector.folderPath)
}
return collector
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
if c.enabled {
ch <- metricReadError
}
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
if c.enabled {
c.collectFileContents()
c.collectFileErrors(ch)
}
}
func (c *Collector) appendErrorForPath(path string) {
c.fileMap[path].readErrors++
}

View file

@ -1,56 +0,0 @@
package textfile
import (
"log"
"os"
"path/filepath"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
const namespace = "textfile"
var (
metricReadError = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "error"),
"Checks for errors while reading text files",
[]string{"path"}, nil,
)
)
func (c *Collector) collectFileContents() {
files, err := os.ReadDir(c.folderPath)
if err != nil {
log.Printf("error reading directory '%s': %v", c.folderPath, err)
return
}
for _, file := range files {
fileName := file.Name()
if !strings.HasSuffix(strings.ToLower(fileName), ".prom") {
continue
}
c.fileMap[fileName] = &fileData{
readErrors: 0,
fileName: fileName,
}
fullPath := filepath.Join(c.folderPath, fileName)
content, err := os.ReadFile(fullPath)
if err != nil {
c.appendErrorForPath(fileName)
log.Printf("error reading contents of file '%s': %v", fileName, err)
}
c.fileMap[fileName].fileContents = content
}
}
func (c *Collector) collectFileErrors(ch chan<- prometheus.Metric) {
for _, f := range c.fileMap {
ch <- prometheus.MustNewConstMetric(
metricReadError, prometheus.GaugeValue, float64(f.readErrors), f.fileName,
)
}
}

View file

@ -1,20 +0,0 @@
package textfile
import (
"log"
"net/http"
)
func (c *Collector) WriteTextFileMetrics(w http.ResponseWriter, r *http.Request) {
if !c.enabled {
return
}
for _, f := range c.fileMap {
_, err := w.Write(f.fileContents)
if err != nil {
c.appendErrorForPath(f.fileName)
log.Printf("error writing file contents to response writer '%s': %v", f.fileName, err)
}
}
}

View file

@ -1,66 +1,7 @@
package main package main
import ( import "log"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/prometheus/client_golang/prometheus"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/cfg"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/f2b"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/textfile"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/server"
)
var (
version = "dev"
commit = "none"
date = "unknown"
builtBy = "unknown"
)
func printAppVersion() {
fmt.Println(version)
fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy)
}
func main() { func main() {
appSettings := cfg.Parse() log.Println("Not yet implemented")
if appSettings.VersionMode {
printAppVersion()
return
}
handleGracefulShutdown()
log.Printf("fail2ban exporter version %s", version)
log.Printf("starting server at %s", appSettings.MetricsAddress)
f2bCollector := f2b.NewExporter(appSettings, version)
prometheus.MustRegister(f2bCollector)
textFileCollector := textfile.NewCollector(appSettings)
prometheus.MustRegister(textFileCollector)
if !appSettings.DryRunMode {
svrErr := server.StartServer(appSettings, f2bCollector, textFileCollector)
err := <-svrErr
log.Fatal(err)
} else {
log.Print("running in dry-run mode - exiting")
}
}
func handleGracefulShutdown() {
var signals = make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM)
signal.Notify(signals, syscall.SIGINT)
go func() {
sig := <-signals
log.Printf("caught signal: %+v", sig)
os.Exit(0)
}()
} }

22
go.mod
View file

@ -1,21 +1,3 @@
module gitlab.com/hectorjsmith/fail2ban-prometheus-exporter module fail2ban-prometheus-exporter
go 1.20 go 1.15
require (
github.com/alecthomas/kong v0.8.1
github.com/kisielk/og-rek v1.2.0
github.com/nlpodyssey/gopickle v0.2.0
github.com/prometheus/client_golang v1.17.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)

33
go.sum
View file

@ -1,33 +0,0 @@
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/kisielk/og-rek v1.2.0 h1:CTvDIin+YnetsSQAYbe+QNAxXU3B50C5hseEz8xEoJw=
github.com/kisielk/og-rek v1.2.0/go.mod h1:6ihsOSzSAxR/65S3Bn9zNihoEqRquhDQZ2c6I2+MG3c=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/nlpodyssey/gopickle v0.2.0 h1:4naD2DVylYJupQLbCQFdwo6yiXEmPyp+0xf5MVlrBDY=
github.com/nlpodyssey/gopickle v0.2.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

8
health
View file

@ -1,8 +0,0 @@
#!/bin/sh
port=9191
if [ ! -z $F2B_WEB_LISTEN_ADDRESS ]; then
port=`echo $F2B_WEB_LISTEN_ADDRESS | cut -d ":" -f 2 -`
fi
curl --fail localhost:$port/health || exit 1

View file

@ -1,17 +0,0 @@
package server
import (
"net/http"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth"
)
func AuthMiddleware(handlerFunc http.HandlerFunc, authProvider auth.AuthProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if authProvider.IsAllowed(r) {
handlerFunc.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
}

View file

@ -1,46 +0,0 @@
package server
import (
"net/http"
"net/http/httptest"
"testing"
)
type testAuthProvider struct {
match bool
}
func (p testAuthProvider) IsAllowed(request *http.Request) bool {
return p.match
}
func newTestRequest() *http.Request {
return httptest.NewRequest(http.MethodGet, "http://example.com", nil)
}
func executeAuthMiddlewareTest(t *testing.T, authMatches bool, expectedCode int, expectedCallCount int) {
callCount := 0
testHandler := func(w http.ResponseWriter, r *http.Request) {
callCount++
}
handler := AuthMiddleware(testHandler, testAuthProvider{match: authMatches})
recorder := httptest.NewRecorder()
request := newTestRequest()
handler.ServeHTTP(recorder, request)
if recorder.Code != expectedCode {
t.Errorf("statusCode = %v, want %v", recorder.Code, expectedCode)
}
if callCount != expectedCallCount {
t.Errorf("callCount = %v, want %v", callCount, expectedCallCount)
}
}
func Test_GIVEN_MatchingBasicAuth_WHEN_MethodCalled_THEN_RequestProcessed(t *testing.T) {
executeAuthMiddlewareTest(t, true, http.StatusOK, 1)
}
func Test_GIVEN_NonMatchingBasicAuth_WHEN_MethodCalled_THEN_RequestRejected(t *testing.T) {
executeAuthMiddlewareTest(t, false, http.StatusUnauthorized, 0)
}

View file

@ -1,44 +0,0 @@
package server
import (
"log"
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/f2b"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/textfile"
)
const (
metricsPath = "/metrics"
)
func rootHtmlHandler(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(
`<html>
<head><title>Fail2Ban Exporter</title></head>
<body>
<h1>Fail2Ban Exporter</h1>
<p><a href="` + metricsPath + `">Metrics</a></p>
</body>
</html>`))
if err != nil {
log.Printf("error handling root url: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func metricHandler(w http.ResponseWriter, r *http.Request, collector *textfile.Collector) {
promhttp.Handler().ServeHTTP(w, r)
collector.WriteTextFileMetrics(w, r)
}
func healthHandler(w http.ResponseWriter, r *http.Request, collector *f2b.Collector) {
if collector.IsHealthy() {
w.WriteHeader(http.StatusOK)
w.Write([]byte("{\"healthy\":true}"))
} else {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("{\"healthy\":false}"))
}
}

View file

@ -1,48 +0,0 @@
package server
import (
"log"
"net/http"
"time"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/cfg"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/f2b"
"gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/textfile"
)
func StartServer(
appSettings *cfg.AppSettings,
f2bCollector *f2b.Collector,
textFileCollector *textfile.Collector,
) chan error {
http.HandleFunc("/", AuthMiddleware(
rootHtmlHandler,
appSettings.AuthProvider,
))
http.HandleFunc(metricsPath, AuthMiddleware(
func(w http.ResponseWriter, r *http.Request) {
metricHandler(w, r, textFileCollector)
},
appSettings.AuthProvider,
))
http.HandleFunc("/health",
func(w http.ResponseWriter, r *http.Request) {
healthHandler(w, r, f2bCollector)
},
)
log.Printf("metrics available at '%s'", metricsPath)
svrErr := make(chan error)
go func() {
httpServer := &http.Server{
Addr: appSettings.MetricsAddress,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
svrErr <- httpServer.ListenAndServe()
}()
log.Print("ready")
return svrErr
}

View file

@ -1,8 +0,0 @@
package socket
// Py_builtins_str is used by the pickle decoder to parse the server response into a format Go can understand
type Py_builtins_str struct{}
func (c Py_builtins_str) Call(args ...interface{}) (interface{}, error) {
return args[0], nil
}

View file

@ -1,179 +0,0 @@
package socket
import (
"fmt"
"github.com/kisielk/og-rek"
"github.com/nlpodyssey/gopickle/types"
"net"
"strings"
)
type Fail2BanSocket struct {
socket net.Conn
encoder *ogórek.Encoder
}
type JailStats struct {
FailedCurrent int
FailedTotal int
BannedCurrent int
BannedTotal int
}
func ConnectToSocket(path string) (*Fail2BanSocket, error) {
c, err := net.Dial("unix", path)
if err != nil {
return nil, err
}
return &Fail2BanSocket{
socket: c,
encoder: ogórek.NewEncoder(c),
}, nil
}
func (s *Fail2BanSocket) Close() error {
return s.socket.Close()
}
func (s *Fail2BanSocket) Ping() (bool, error) {
response, err := s.sendCommand([]string{pingCommand, "100"})
if err != nil {
return false, newConnectionError(pingCommand, err)
}
if t, ok := response.(*types.Tuple); ok {
if (*t)[1] == "pong" {
return true, nil
}
return false, fmt.Errorf("unexpected response data (expecting 'pong'): %s", (*t)[1])
}
return false, newBadFormatError(pingCommand, response)
}
func (s *Fail2BanSocket) GetJails() ([]string, error) {
response, err := s.sendCommand([]string{statusCommand})
if err != nil {
return nil, err
}
if lvl1, ok := response.(*types.Tuple); ok {
if lvl2, ok := lvl1.Get(1).(*types.List); ok {
if lvl3, ok := lvl2.Get(1).(*types.Tuple); ok {
if lvl4, ok := lvl3.Get(1).(string); ok {
splitJails := strings.Split(lvl4, ",")
return trimSpaceForAll(splitJails), nil
}
}
}
}
return nil, newBadFormatError(statusCommand, response)
}
func (s *Fail2BanSocket) GetJailStats(jail string) (JailStats, error) {
response, err := s.sendCommand([]string{statusCommand, jail})
if err != nil {
return JailStats{}, err
}
stats := JailStats{
FailedCurrent: -1,
FailedTotal: -1,
BannedCurrent: -1,
BannedTotal: -1,
}
if lvl1, ok := response.(*types.Tuple); ok {
if lvl2, ok := lvl1.Get(1).(*types.List); ok {
if filter, ok := lvl2.Get(0).(*types.Tuple); ok {
if filterLvl1, ok := filter.Get(1).(*types.List); ok {
if filterCurrentTuple, ok := filterLvl1.Get(0).(*types.Tuple); ok {
if filterCurrent, ok := filterCurrentTuple.Get(1).(int); ok {
stats.FailedCurrent = filterCurrent
}
}
if filterTotalTuple, ok := filterLvl1.Get(1).(*types.Tuple); ok {
if filterTotal, ok := filterTotalTuple.Get(1).(int); ok {
stats.FailedTotal = filterTotal
}
}
}
}
if actions, ok := lvl2.Get(1).(*types.Tuple); ok {
if actionsLvl1, ok := actions.Get(1).(*types.List); ok {
if actionsCurrentTuple, ok := actionsLvl1.Get(0).(*types.Tuple); ok {
if actionsCurrent, ok := actionsCurrentTuple.Get(1).(int); ok {
stats.BannedCurrent = actionsCurrent
}
}
if actionsTotalTuple, ok := actionsLvl1.Get(1).(*types.Tuple); ok {
if actionsTotal, ok := actionsTotalTuple.Get(1).(int); ok {
stats.BannedTotal = actionsTotal
}
}
}
}
return stats, nil
}
}
return stats, newBadFormatError(statusCommand, response)
}
func (s *Fail2BanSocket) GetJailBanTime(jail string) (int, error) {
command := fmt.Sprintf(banTimeCommandFmt, jail)
return s.sendSimpleIntCommand(command)
}
func (s *Fail2BanSocket) GetJailFindTime(jail string) (int, error) {
command := fmt.Sprintf(findTimeCommandFmt, jail)
return s.sendSimpleIntCommand(command)
}
func (s *Fail2BanSocket) GetJailMaxRetries(jail string) (int, error) {
command := fmt.Sprintf(maxRetriesCommandFmt, jail)
return s.sendSimpleIntCommand(command)
}
func (s *Fail2BanSocket) GetServerVersion() (string, error) {
response, err := s.sendCommand([]string{versionCommand})
if err != nil {
return "", err
}
if lvl1, ok := response.(*types.Tuple); ok {
if versionStr, ok := lvl1.Get(1).(string); ok {
return versionStr, nil
}
}
return "", newBadFormatError(versionCommand, response)
}
// sendSimpleIntCommand sends a command to the fail2ban socket and parses the response to extract an int.
// This command assumes that the response data is in the format of `(d, d)` where `d` is a number.
func (s *Fail2BanSocket) sendSimpleIntCommand(command string) (int, error) {
response, err := s.sendCommand(strings.Split(command, " "))
if err != nil {
return -1, err
}
if lvl1, ok := response.(*types.Tuple); ok {
if banTime, ok := lvl1.Get(1).(int); ok {
return banTime, nil
}
}
return -1, newBadFormatError(command, response)
}
func newBadFormatError(command string, data interface{}) error {
return fmt.Errorf("(%s) unexpected response format - cannot parse: %v", command, data)
}
func newConnectionError(command string, err error) error {
return fmt.Errorf("(%s) failed to send command through socket: %v", command, err)
}
func trimSpaceForAll(slice []string) []string {
for i := range slice {
slice[i] = strings.TrimSpace(slice[i])
}
return slice
}

View file

@ -1,69 +0,0 @@
package socket
import (
"bufio"
"bytes"
"fmt"
"github.com/nlpodyssey/gopickle/pickle"
)
const (
commandTerminator = "<F2B_END_COMMAND>"
pingCommand = "ping"
statusCommand = "status"
versionCommand = "version"
banTimeCommandFmt = "get %s bantime"
findTimeCommandFmt = "get %s findtime"
maxRetriesCommandFmt = "get %s maxretry"
socketReadBufferSize = 1024
)
func (s *Fail2BanSocket) sendCommand(command []string) (interface{}, error) {
err := s.write(command)
if err != nil {
return nil, err
}
return s.read()
}
func (s *Fail2BanSocket) write(command []string) error {
err := s.encoder.Encode(command)
if err != nil {
return err
}
_, err = s.socket.Write([]byte(commandTerminator))
if err != nil {
return err
}
return nil
}
func (s *Fail2BanSocket) read() (interface{}, error) {
reader := bufio.NewReader(s.socket)
data := []byte{}
for {
buf := make([]byte, socketReadBufferSize)
_, err := reader.Read(buf)
if err != nil {
return nil, err
}
data = append(data, buf...)
containsTerminator := bytes.Contains(data, []byte(commandTerminator))
if containsTerminator {
break
}
}
bufReader := bytes.NewReader(data)
unpickler := pickle.NewUnpickler(bufReader)
unpickler.FindClass = func(module, name string) (interface{}, error) {
if (module == "builtins" || module == "__builtin__") && name == "str" {
return &Py_builtins_str{}, nil
}
return nil, fmt.Errorf("class not found: " + module + " : " + name)
}
return unpickler.Load()
}