40 Commits
0.1.0 ... 0.3.0

Author SHA1 Message Date
1e29f6bfbf docs: update changelog for release 2021-09-27 19:55:58 +01:00
038aeaaafd Merge branch 'update-changelog' into 'main'
Update changelog

See merge request hectorjsmith/fail2ban-prometheus-exporter!36
2021-09-26 11:39:49 +00:00
94ee6cac4e docs: update changelog 2021-09-26 12:32:48 +01:00
51c1157d21 Merge branch 'render-basic-root-html-page-with-link-to-metrics' into 'main'
Render basic root html page with link to metrics

See merge request hectorjsmith/fail2ban-prometheus-exporter!35
2021-09-25 21:23:29 +00:00
84b9d02068 feat: render basic html page at root url
Add a new request handler for the root URL (`/`) to render a simple HTML
page with a link to the metrics page. This follows the convention of other
metric exporters.
2021-09-25 21:23:28 +00:00
a1a0aa03a4 Merge branch 'print-ready-message-with-host-and-port-on-startup' into 'main'
Print ready message with host and port on startup

See merge request hectorjsmith/fail2ban-prometheus-exporter!34
2021-09-22 12:34:39 +00:00
22a165da3e feat: improve startup logging
Update the exporter startup to add more log messages. The server address,
port, and metrics path are now logged on startup.
A "ready" log message is printed when the server is up and running.
2021-09-21 09:34:23 +01:00
964fbfd0f8 Merge branch 'split-export-functions-into-separate-package' into 'main'
Split export functions into separate package

See merge request hectorjsmith/fail2ban-prometheus-exporter!33
2021-09-14 20:28:15 +00:00
03f5084020 refactor: move exporter code to new package
Split out all the code to define exporter functions and collect data into
a new package. The new package is responsible for all exporter related
activity. This makes the code easier to read.
Split the code for collecting metrics from the database and from the socket
into different files to make the separation more obvious.
2021-09-13 20:25:54 +01:00
911736cee4 Merge branch '12-export-metric-with-fail2ban-server-and-exporter-versions' into 'main'
Resolve "Export metric with fail2ban server and exporter versions"

Closes #12

See merge request hectorjsmith/fail2ban-prometheus-exporter!32
2021-09-10 06:13:56 +00:00
fba9ee2809 feat: export new version metric (#12)
Add a new `f2b_version` metric that includes the version of the fail2ban
server and the exporter.
Add a new socket command to get back the fail2ban server version.
2021-09-10 06:13:56 +00:00
d9f1ee33c8 Merge branch 'docs/update-changelog-for-release' into 'main'
Docs/update changelog for release

See merge request hectorjsmith/fail2ban-prometheus-exporter!31
2021-08-31 13:07:54 +00:00
e4aa5edaa0 docs: update changelog for release
Update the CHANGELOG file for the v0.2.0 release.
2021-08-31 13:04:51 +00:00
e4cf21fdf1 Merge branch 'docs/update-readme' into 'main'
Docs/update readme

See merge request hectorjsmith/fail2ban-prometheus-exporter!30
2021-08-31 12:44:59 +00:00
062abe561c docs: update project readme file
Re-write the project README file based on the new features available in the
exporter and the new socket-based metric collection.
2021-08-31 12:44:58 +00:00
920cf08619 Merge branch 'docs/update-changelog' into 'main'
Docs/update changelog

See merge request hectorjsmith/fail2ban-prometheus-exporter!29
2021-08-30 17:15:08 +00:00
742019a025 docs: update changelog
Update the project changelog.
Refactor the changelog structure to no longer be fully automated. It now
includes some manual tweaks.
Update the Makefile command to generate a new `CHANGELOG_gen.md` file
instead of overwriting the existing Changelog file.
2021-08-30 18:07:20 +01:00
92fcae5cda Merge branch 'refactor/deprecate-old-db-based-metrics' into 'main'
Refactor/deprecate old db based metrics

See merge request hectorjsmith/fail2ban-prometheus-exporter!28
2021-08-30 16:38:33 +00:00
5b62670e9d refactor: deprecate database metrics
Update all old database-based metrics to include the `deprecated` text.
Add a warning on startup if connecting to the fail2ban database to state
that this functionality will be removed in a future release.
Rename deprecated methods and variables.
2021-08-30 16:38:33 +00:00
737a86b6fd Merge branch 'feat/export-metric-with-connection-errors' into 'main'
Feat/export-metric-with-connection-errors

See merge request hectorjsmith/fail2ban-prometheus-exporter!27
2021-08-30 07:19:11 +00:00
4da46f3c4a feat: export metrics with socket errors
Add new metric to collect the number of errors found when connecting to the
fail2ban server socket. Errors are split into two categories: connection
errors (e.g. socket file not found), and request errors (e.g. invalid
response received from server).
Update the `up` metric to return `0` if the socket connection fails.
Improve error logging.
2021-08-30 07:19:11 +00:00
828b67cdd9 Merge branch 'fix/recover-from-fail2ban-restarts' into 'main'
Fix/recover from fail2ban restarts

See merge request hectorjsmith/fail2ban-prometheus-exporter!26
2021-08-30 06:39:06 +00:00
acb40a94bd fix: recover from fail2ban server restarts
Update the code collecting metrics to open a new socket connection each
time metrics are collected. This ensures that a new socket connection is
used each time and avoids errors caused by fail2ban being restarted.
2021-08-30 07:36:15 +01:00
aef73df3fa Merge branch 'feat/update-docker-container-for-socket-based-metrics' into 'main'
Feat/update docker container for socket based metrics

See merge request hectorjsmith/fail2ban-prometheus-exporter!25
2021-08-29 17:42:47 +00:00
2ab1f7dc52 feat: support reading fail2ban socket in docker
Update the docker container to support mounting the fail2ban server socket
and pointing the exporter at it. This allows the exporter to interact with
the socket from within the container.
The entire `/var/run` folder is mounted instead of just the socket file to
correctly handle fail2ban restarts (where the file will be deleted).
2021-08-29 18:36:27 +01:00
82a7bbe1e0 Merge branch 'feat/read-metrics-from-fail2ban-server-socket' into 'main'
Feat/read metrics from fail2ban server socket

See merge request hectorjsmith/fail2ban-prometheus-exporter!22
2021-08-29 16:54:20 +00:00
1964dde273 feat: export metrics for failed/banned counts
Add new metric to track the total number of jails configured in fail2ban.
Add new metrics for the current and total number of filter failures for
each jail, as well as the current/total number of banned IPs per jail.
The new metrics are collected by sending the `status [jail]` command to the
fail2ban server and parsing the response data.
2021-08-29 16:54:20 +00:00
617d711ecf Merge branch 'fix/read-socket-response-in-chunks' into 'main'
Fix/read socket response in chunks

See merge request hectorjsmith/fail2ban-prometheus-exporter!24
2021-08-29 15:05:39 +00:00
e5714b7485 fix: read socket response data in chunks
Read the response data from the socket in chunks to prevent errors when
processing large payloads. The initial implementation solved large payloads
by just defining a very large buffer, but this is not a solution. The new
code reads the socket data in a loop until a terminator is found and
appends all the data into a single byte array.
Reduce the buffer size to `1024` bytes.
2021-08-29 16:02:31 +01:00
e083b48461 Merge branch 'feat/ping-fail2ban-server-over-socket' into 'main'
Feat/ping fail2ban server over socket

See merge request hectorjsmith/fail2ban-prometheus-exporter!23
2021-08-29 11:50:53 +00:00
39133d0a76 feat: collect new up metric from fail2ban socket
Add support for connecting the exporter directly to the fail2ban server's
socket to send requests and receive data. The path to the socket file is
optional and specified on startup.
Export a new metric based on the response of the `ping` command sent to the
fail2ban server. The metric is set to 1 if the server responds with `pong`
and 0 in any other case. This metric is only shown if the path to the
socket file was provided on startup.
2021-08-29 11:50:53 +00:00
9d6b35c59a Merge branch 'fix/update-banned-metric-to-exclude-expired-bans' into 'main'
Fix/update banned metric to exclude expired bans

See merge request hectorjsmith/fail2ban-prometheus-exporter!21
2021-08-27 15:34:21 +00:00
526b1c7272 fix: update banned metrics to exclude expired bans
Update the database query counting the number of banned IPs to filter out
any bans that have already expired. An expired ban is defined as a ban
where the "time of ban" plus the "duration of ban" is less than the
current time.
This is necessary because bans that have expired are not automatically
removed from the database and will cause metrics to diverge from the counts
reported by `fail2ban-client`.
2021-08-27 16:29:01 +01:00
a5e1ae4495 Merge branch 'feat/db-error-count-metric' into 'main'
Feat/db error count metric

See merge request hectorjsmith/fail2ban-prometheus-exporter!20
2021-04-07 20:50:23 +00:00
8726afcd6b feat: new metric to track error counts
Add a new metric to count the number of database errors that have been
found since startup. This complements the `up` metric to have better
visibility into occasional database errors.
2021-04-07 21:46:41 +01:00
a406e019e2 Merge branch 'feat/base-up-metric-on-errors' into 'main'
Feat/base up metric on errors

See merge request hectorjsmith/fail2ban-prometheus-exporter!19
2021-04-07 20:35:39 +00:00
bd841c3a35 feat: set up metric to 0 if errors found
The `up` metric is now based on whether an error was found while reading
data from the database to build other metrics. Note that there is a chance
the `up` metric will not be correctly set if the last metric to be built
before the `up` metric does not throw an error.
2021-04-07 21:32:49 +01:00
a9e41188f6 Merge branch '1-export-metrics-on-enabled-disabled-jails' into 'main'
Resolve "Export metrics on enabled/disabled jails"

Closes #1

See merge request hectorjsmith/fail2ban-prometheus-exporter!18
2021-04-07 18:00:26 +00:00
1282d635eb feat: new metric for enabled jails (#1)
Add a new prometheus metric to track which jails are currently enabled.
Add a new database query to read the jail name and enabled status from the
database.
Add new metric to readme file.
2021-04-07 18:55:34 +01:00
5f9085aa5a Merge branch 'release/0.1.0' into 'main'
Release/0.1.0

See merge request hectorjsmith/fail2ban-prometheus-exporter!17
2021-03-28 16:26:45 +00:00
16 changed files with 828 additions and 133 deletions

View File

@ -4,7 +4,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning]. The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning].
## [Unreleased]## [0.1.0] - 2021-03-28 ## [Unreleased]
*Nothing yet*
## [0.3.0] - 2021-09-27
*Export new version metrics ([#12](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/issues/12))*
### Added
- (3c9a005) feat: render basic html page at root url
- (22a165d) feat: improve startup logging
- (fba9ee2) feat: export new version metric ([#12](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/issues/12))
## [0.2.0] - 2021-08-31
*Collect metrics through fail2ban socket - based on [#11](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/issues/11)*
### Added
- (39133d0) feat: collect new up metric from fail2ban socket
- (4da46f3) feat: export metrics with socket errors
- (bd841c3) feat: set up metric to 0 if errors found
- (1964dde) feat: export metrics for failed/banned counts
- (2ab1f7d) feat: support reading fail2ban socket in docker
- (1282d63) feat: new metric for enabled jails ([#1](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/issues/1))
### Fixed
- (526b1c7) fix: update banned metrics to exclude expired bans ([#11](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/issues/11))
### Deprecated
- Use of the fail2ban database has been deprecated. The exporter now collects metrics through the fail2ban socket file. See [#11](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/issues/11) for more details.
## [0.1.0] - 2021-03-28
*Initial release*
### Added ### Added
- (6355c9e) feat: fail on startup if database file does not exist ([#8](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/issues/8)) - (6355c9e) feat: fail on startup if database file does not exist ([#8](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/issues/8))
@ -18,13 +47,14 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
### Fixed ### Fixed
- (0842419) fix: compile tool without cgo_enabled flag - (0842419) fix: compile tool without cgo_enabled flag
## 0.0.0 - 2021-02-05
*Repository creation*
--- ---
*This changelog is automatically generated by [git-chglog]*
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html [Semantic Versioning]: https://semver.org/spec/v2.0.0.html
[git-chglog]: https://github.com/git-chglog/git-chglog
[Unreleased]: https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/compare/0.1.0...main [Unreleased]: https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/compare/0.1.0...main
[0.1.0]: https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/compare/0.0.0...0.1.0 [0.1.0]: https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/compare/0.0.0...0.1.0
[0.2.0]: https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/compare/0.1.0...0.2.0
[0.3.0]: https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/compare/0.2.0...0.3.0

View File

@ -15,7 +15,7 @@ format:
cd src/ && go vet $(go list ./... | grep -v /vendor/) cd src/ && go vet $(go list ./... | grep -v /vendor/)
generateChangelog: generateChangelog:
./tools/git-chglog_linux_amd64 --config tools/chglog/config.yml 0.0.0.. > CHANGELOG.md ./tools/git-chglog_linux_amd64 --config tools/chglog/config.yml 0.0.0.. > CHANGELOG_gen.md
build/snapshot: build/snapshot:
./tools/goreleaser_linux_amd64 --snapshot --rm-dist --skip-publish ./tools/goreleaser_linux_amd64 --snapshot --rm-dist --skip-publish

219
README.md
View File

@ -3,38 +3,75 @@
Go tool to collect and export metrics on Fail2Ban Go tool to collect and export metrics on Fail2Ban
## Table of Contents ## Table of Contents
1. How to use 1. Introduction
2. Docker 2. Running the Exporter
1. Volumes 3. Running in Docker
2. Docker run
3. Docker compose
3. CLI usage
4. Metrics 4. Metrics
## 1. How to use ## 1. Introduction
The exporter can collect metrics from 2 locations: the fail2ban server socket and the fail2ban server database.
Run the exporter by providing it with a fail2ban database to read data from.
Read access to the database is required.
Once the exporter is running, metrics are available at `localhost:9191/metrics`. Once the exporter is running, metrics are available at `localhost:9191/metrics`.
The default port is `9191`, but this can be modified with the `-port` flag. (The default port is `9191` but can be modified with the `-port` flag)
**Note:** By default fail2ban stores the database file at: `/var/lib/fail2ban/fail2ban.sqlite3` ### 1.1. Socket
The recommended way to run the exporter is to point it at the fail2ban server socket.
This allows the exporter to communicate with the server the same way `fail2ban-client` does and ensures the metrics it collects align with the values reported by `fail2ban-client status <jail>`.
**Fail2Ban Jails** The default path to the socket is: `/var/run/fail2ban/fail2ban.sock`
fail2ban can be configured to process different log files and use different rules for each one. ### 1.2. Deprecated: Database
These separate configurations are referred to as *jails*. The original way to collect metrics is to read them from the fail2ban database.
This has now been deprecated in favour of using the socket.
The reason being that database metrics do not always align with the output of `fail2ban-client status <jail>` and cause confusion.
See [#11](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/issues/11) for more details.
For example, fail2ban can be configured to watch the system logs for failed SSH connections and Nextcloud logs for failed logins. If necessary, these metrics can still be exported by providing the database path to the exporter.
In this configuration, there will be two jails - one for IPs banned from the SSH logs, and one for IPs banned from the Nextcloud logs.
This tool exports several metrics *per jail*, meaning that it is possible to track how many IPs are being banned in each jail as well as the overall total. The default path to the fail2ban database is: `/var/lib/fail2ban/fail2ban.sqlite3`
This can be useful to track what services are seeing more failed logins.
## 2. Docker ## 2. Running the Exporter
An official docker image is available on the Gitlab container registry. The exporter is compiled and released as a single binary.
This makes it very easy to run in any environment.
No additional runtime dependencies are required.
Compiled binaries for various platforms are provided in each release.
See the [releases page](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/releases) for more information.
**Usage**
```
$ fail2ban-prometheus-exporter -h
-db string
path to the fail2ban sqlite database (deprecated)
-port int
port to use for the metrics server (default 9191)
-socket string
path to the fail2ban server socket
-version
show version info and exit
```
**Example**
```
fail2ban-prometheus-exporter -socket /var/run/fail2ban/fail2ban.sock -port 9191
```
Note that the exporter will need read access to the fail2ban socket or database.
### 2.1. Compile from Source
The code can be compiled from source by running `go build` inside the `src/` folder.
Go version `1.15` or greater is required.
Run `go mod download` to download all necessary dependencies before running the build.
## 3. Running in Docker
If use of docker is desired, an official docker image is available on the Gitlab container registry.
Use it by pulling the following image: Use it by pulling the following image:
``` ```
@ -44,26 +81,32 @@ registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
Use the `:latest` tag to get the most up to date code (less stable) or use one of the version tagged images to use a specific release. Use the `:latest` tag to get the most up to date code (less stable) or use one of the version tagged images to use a specific release.
See the [registry page](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/container_registry) for all available tags. See the [registry page](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/container_registry) for all available tags.
### 2.1. Volumes ### 3.1. Volumes
The docker image is designed to run by mounting the fail2ban sqlite3 database. The docker image is designed to run by mounting either the fail2ban sqlite3 database of the fail2ban run folder.
The database should be mounted at: `/app/fail2ban.sqlite3` - The database should be mounted at: `/app/fail2ban.sqlite3`
- The run folder should be mounted at: `/var/run/fail2ban`
The database can be mounted with read-only permissions. Both paths can be mounted with readonly (`ro`) permissions.
### 2.2. Docker run **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.
Use the following command to run the forwarder as a docker container. ### 3.2. Docker run
Use the following command to run the exporter as a docker container.
``` ```
docker run -d \ docker run -d \
--name "fail2ban-exporter" \ --name "fail2ban-exporter" \
-v /var/lib/fail2ban/fail2ban.sqlite3:/app/fail2ban.sqlite3:ro \ -v /var/lib/fail2ban/fail2ban.sqlite3:/app/fail2ban.sqlite3:ro \
-p "9191:9191" -v /var/run/fail2ban:/var/run/fail2ban:ro \
-p "9191:9191" \
registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
``` ```
### 2.3. Docker compose ### 3.3. Docker compose
The following is a simple docker-compose file to run the exporter. The following is a simple docker-compose file to run the exporter.
@ -74,31 +117,100 @@ services:
image: registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest image: registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
volumes: volumes:
- /var/lib/fail2ban/fail2ban.sqlite3:/app/fail2ban.sqlite3:ro - /var/lib/fail2ban/fail2ban.sqlite3:/app/fail2ban.sqlite3:ro
- /var/run/fail2ban/:/var/run/fail2ban:ro
ports: ports:
- "9191:9191" - "9191:9191"
``` ```
## 3. CLI usage
```
$ fail2ban-prometheus-exporter -h
-db string
path to the fail2ban sqlite database
-port int
port to use for the metrics server (default 9191)
-version
show version info and exit
```
## 4. Metrics ## 4. Metrics
Access exported metrics at `/metrics` (on the provided port). Access exported metrics at the `/metrics` path on the configured port.
**Note:** All metric names include the `fail2ban_` prefix to make sure they are unique and easier to find. **Note on Fail2Ban Jails**
fail2ban can be configured to process different log files and use different rules for each one.
These separate configurations are referred to as *jails*.
For example, fail2ban can be configured to watch the system logs for failed SSH connections and Nextcloud logs for failed logins.
In this configuration, there will be two jails - one for IPs banned from the SSH logs, and one for IPs banned from the Nextcloud logs.
This tool exports several metrics *per jail*, meaning that it is possible to track how many IPs are being banned in each jail as well as the overall total.
This can be useful to track what services are seeing more failed logins.
### 4.1. Socket-based Metrics
These are the metrics exported by reading data from the fail2ban server socket.
All metrics are prefixed with `f2b_`.
Exposed metrics:
* `up` - Returns 1 if the fail2ban server is up and connection succeeds
* `errors` - Number of errors since startup
* `db` - Errors connecting to the database
* `socket_conn` - Errors connecting to the fail2ban socket (e.g. connection refused)
* `socket_req` - Errors sending requests to the fail2ban server (e.g. invalid responses)
* `jail_count` - Number of jails configured in fail2ban
* `jail_banned_current` (per jail) - Number of IPs currently banned
* `jail_banned_total` (per jail) - Total number of banned IPs since fail2ban startup (includes expired bans)
* `jail_failed_current` (per jail) - Number of current failures
* `jail_failed_total` (per jail) - Total number of failures since fail2ban startup
**Sample**
```
# HELP f2b_errors Number of errors found since startup
# TYPE f2b_errors counter
f2b_errors{type="db"} 0
f2b_errors{type="socket_conn"} 0
f2b_errors{type="socket_req"} 0
# HELP f2b_jail_banned_current Number of IPs currently banned in this jail
# TYPE f2b_jail_banned_current gauge
f2b_jail_banned_current{jail="recidive"} 5
f2b_jail_banned_current{jail="sshd"} 15
# HELP f2b_jail_banned_total Total number of IPs banned by this jail (includes expired bans)
# TYPE f2b_jail_banned_total gauge
f2b_jail_banned_total{jail="recidive"} 6
f2b_jail_banned_total{jail="sshd"} 31
# HELP f2b_jail_count Number of defined jails
# TYPE f2b_jail_count gauge
f2b_jail_count 2
# HELP f2b_jail_failed_current Number of current failures on this jail's filter
# TYPE f2b_jail_failed_current gauge
f2b_jail_failed_current{jail="recidive"} 5
f2b_jail_failed_current{jail="sshd"} 6
# HELP f2b_jail_failed_total Number of total failures on this jail's filter
# TYPE f2b_jail_failed_total gauge
f2b_jail_failed_total{jail="recidive"} 7
f2b_jail_failed_total{jail="sshd"} 125
# HELP f2b_up Check if the fail2ban server is up
# TYPE f2b_up gauge
f2b_up 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: ...
```
### 4.2. Database Metrics (deprecated)
These are the original metrics exported by the initial release of the exporter.
They are all based on the data stored in the fail2ban sqlite3 database.
*These metrics are deprecated and will be removed in a future release.*
All metrics are prefixed with `fail2ban_`.
Exposed metrics: Exposed metrics:
* `up` - Returns 1 if the service is up * `up` - Returns 1 if the service is up
* `errors` - Returns the number of errors found since startup
* `enabled_jails` - Returns 1 for each jail that is enabled, 0 if disabled.
* `bad_ips` (per jail) * `bad_ips` (per jail)
* A *bad IP* is defined as an IP that has been banned at least once in the past * A *bad IP* is defined as an IP that has been banned at least once in the past
* Bad IPs are counted per jail * Bad IPs are counted per jail
@ -109,15 +221,22 @@ Exposed metrics:
**Sample** **Sample**
``` ```
# HELP fail2ban_bad_ips Number of bad IPs stored in the database (per jail). # HELP fail2ban_bad_ips (Deprecated) Number of bad IPs stored in the database (per jail).
# TYPE fail2ban_bad_ips gauge # TYPE fail2ban_bad_ips gauge
fail2ban_bad_ips{jail="jail1"} 6 fail2ban_bad_ips{jail="recidive"} 0
fail2ban_bad_ips{jail="jail2"} 8 fail2ban_bad_ips{jail="sshd"} 0
# HELP fail2ban_banned_ips Number of banned IPs stored in the database (per jail). # HELP fail2ban_banned_ips (Deprecated) Number of banned IPs stored in the database (per jail).
# TYPE fail2ban_banned_ips gauge # TYPE fail2ban_banned_ips gauge
fail2ban_banned_ips{jail="jail1"} 3 fail2ban_banned_ips{jail="recidive"} 0
fail2ban_banned_ips{jail="jail2"} 2 fail2ban_banned_ips{jail="sshd"} 0
# HELP fail2ban_up Was the last fail2ban query successful. # HELP fail2ban_enabled_jails (Deprecated) Enabled jails.
# TYPE fail2ban_enabled_jails gauge
fail2ban_enabled_jails{jail="recidive"} 1
fail2ban_enabled_jails{jail="sshd"} 1
# HELP fail2ban_errors (Deprecated) Number of errors found since startup.
# TYPE fail2ban_errors counter
fail2ban_errors{type="db"} 0
# HELP fail2ban_up (Deprecated) Was the last fail2ban query successful.
# TYPE fail2ban_up gauge # TYPE fail2ban_up gauge
fail2ban_up 1 fail2ban_up 1
``` ```

View File

@ -3,7 +3,19 @@
# Print version to logs for debugging purposes # Print version to logs for debugging purposes
/app/fail2ban-prometheus-exporter -version /app/fail2ban-prometheus-exporter -version
db_path=/app/fail2ban.sqlite3
socket_path=/var/run/fail2ban/fail2ban.sock
# Blank out the file paths if they do not exist - a hacky way to only use these files if they were mounted into the container.
if [ ! -f "$db_path" ]; then
db_path=""
fi
if [ ! -S "$socket_path" ]; then
socket_path=""
fi
# Start the exporter (use exec to support graceful shutdown) # Start the exporter (use exec to support graceful shutdown)
# Inspired by: https://akomljen.com/stopping-docker-containers-gracefully/ # Inspired by: https://akomljen.com/stopping-docker-containers-gracefully/
exec /app/fail2ban-prometheus-exporter \ exec /app/fail2ban-prometheus-exporter \
-db /app/fail2ban.sqlite3 -db "$db_path" \
-socket "$socket_path"

View File

@ -15,13 +15,15 @@ type AppSettings struct {
VersionMode bool VersionMode bool
MetricsPort int MetricsPort int
Fail2BanDbPath string Fail2BanDbPath string
Fail2BanSocketPath string
} }
func Parse() *AppSettings { func Parse() *AppSettings {
appSettings := &AppSettings{} appSettings := &AppSettings{}
flag.BoolVar(&appSettings.VersionMode, "version", false, "show version info and exit") flag.BoolVar(&appSettings.VersionMode, "version", false, "show version info and exit")
flag.IntVar(&appSettings.MetricsPort, "port", 9191, "port to use for the metrics server") flag.IntVar(&appSettings.MetricsPort, "port", 9191, "port to use for the metrics server")
flag.StringVar(&appSettings.Fail2BanDbPath, "db", "", "path to the fail2ban sqlite database") flag.StringVar(&appSettings.Fail2BanDbPath, "db", "", "path to the fail2ban sqlite database (deprecated)")
flag.StringVar(&appSettings.Fail2BanSocketPath, "socket", "", "path to the fail2ban server socket")
flag.Parse() flag.Parse()
appSettings.validateFlags() appSettings.validateFlags()
@ -31,8 +33,8 @@ func Parse() *AppSettings {
func (settings *AppSettings) validateFlags() { func (settings *AppSettings) validateFlags() {
var flagsValid = true var flagsValid = true
if !settings.VersionMode { if !settings.VersionMode {
if settings.Fail2BanDbPath == "" { if settings.Fail2BanDbPath == "" && settings.Fail2BanSocketPath == "" {
fmt.Println("missing flag 'db'") fmt.Println("at least one of the following flags must be provided: 'db', 'socket'")
flagsValid = false flagsValid = false
} }
if settings.MetricsPort < minServerPort || settings.MetricsPort > maxServerPort { if settings.MetricsPort < minServerPort || settings.MetricsPort > maxServerPort {

View File

@ -7,7 +7,8 @@ import (
) )
const queryBadIpsPerJail = "SELECT j.name, (SELECT COUNT(1) FROM bips b WHERE j.name = b.jail) FROM jails j" const queryBadIpsPerJail = "SELECT j.name, (SELECT COUNT(1) FROM bips b WHERE j.name = b.jail) FROM jails j"
const queryBannedIpsPerJail = "SELECT j.name, (SELECT COUNT(1) FROM bans b WHERE j.name = b.jail) FROM jails j" const queryBannedIpsPerJail = "SELECT j.name, (SELECT COUNT(1) FROM bans b WHERE j.name = b.jail AND b.timeofban + b.bantime >= strftime('%s','now') + 0) FROM jails j"
const queryJailNameToEnabled = "SELECT j.name, j.enabled FROM jails j"
type Fail2BanDB struct { type Fail2BanDB struct {
DatabasePath string DatabasePath string
@ -36,6 +37,10 @@ func (db *Fail2BanDB) CountBadIpsPerJail() (map[string]int, error) {
return db.RunJailNameToCountQuery(queryBadIpsPerJail) return db.RunJailNameToCountQuery(queryBadIpsPerJail)
} }
func (db *Fail2BanDB) JailNameToEnabledValue() (map[string]int, error) {
return db.RunJailNameToCountQuery(queryJailNameToEnabled)
}
func (db *Fail2BanDB) RunJailNameToCountQuery(query string) (map[string]int, error) { func (db *Fail2BanDB) RunJailNameToCountQuery(query string) (map[string]int, error) {
stmt, err := db.sqliteDB.Prepare(query) stmt, err := db.sqliteDB.Prepare(query)
defer db.mustCloseStatement(stmt) defer db.mustCloseStatement(stmt)

25
src/export/build.go Normal file
View File

@ -0,0 +1,25 @@
package export
import (
"fail2ban-prometheus-exporter/cfg"
fail2banDb "fail2ban-prometheus-exporter/db"
"log"
)
func NewExporter(appSettings *cfg.AppSettings, exporterVersion string) *Exporter {
exporter := &Exporter{
exporterVersion: exporterVersion,
lastError: nil,
dbErrorCount: 0,
socketConnectionErrorCount: 0,
socketRequestErrorCount: 0,
}
if appSettings.Fail2BanDbPath != "" {
log.Print("database-based metrics have been deprecated and will be removed in a future release")
exporter.db = fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath)
}
if appSettings.Fail2BanSocketPath != "" {
exporter.socketPath = appSettings.Fail2BanSocketPath
}
return exporter
}

102
src/export/database.go Normal file
View File

@ -0,0 +1,102 @@
package export
import (
"github.com/prometheus/client_golang/prometheus"
"log"
)
const (
deprecatedNamespace = "fail2ban"
)
var (
deprecatedMetricUp = prometheus.NewDesc(
prometheus.BuildFQName(deprecatedNamespace, "", "up"),
"(Deprecated) Was the last fail2ban query successful.",
nil, nil,
)
deprecatedMetricBannedIpsPerJail = prometheus.NewDesc(
prometheus.BuildFQName(deprecatedNamespace, "", "banned_ips"),
"(Deprecated) Number of banned IPs stored in the database (per jail).",
[]string{"jail"}, nil,
)
deprecatedMetricBadIpsPerJail = prometheus.NewDesc(
prometheus.BuildFQName(deprecatedNamespace, "", "bad_ips"),
"(Deprecated) Number of bad IPs stored in the database (per jail).",
[]string{"jail"}, nil,
)
deprecatedMetricEnabledJails = prometheus.NewDesc(
prometheus.BuildFQName(deprecatedNamespace, "", "enabled_jails"),
"(Deprecated) Enabled jails.",
[]string{"jail"}, nil,
)
deprecatedMetricErrorCount = prometheus.NewDesc(
prometheus.BuildFQName(deprecatedNamespace, "", "errors"),
"(Deprecated) Number of errors found since startup.",
[]string{"type"}, nil,
)
)
func (e *Exporter) collectDeprecatedUpMetric(ch chan<- prometheus.Metric) {
var upMetricValue float64 = 1
if e.lastError != nil {
upMetricValue = 0
}
ch <- prometheus.MustNewConstMetric(
deprecatedMetricUp, prometheus.GaugeValue, upMetricValue,
)
}
func (e *Exporter) collectDeprecatedErrorCountMetric(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
deprecatedMetricErrorCount, prometheus.CounterValue, float64(e.dbErrorCount), "db",
)
}
func (e *Exporter) collectDeprecatedBadIpsPerJailMetrics(ch chan<- prometheus.Metric) {
jailNameToCountMap, err := e.db.CountBadIpsPerJail()
e.lastError = err
if err != nil {
e.dbErrorCount++
log.Print(err)
}
for jailName, count := range jailNameToCountMap {
ch <- prometheus.MustNewConstMetric(
deprecatedMetricBadIpsPerJail, prometheus.GaugeValue, float64(count), jailName,
)
}
}
func (e *Exporter) collectDeprecatedBannedIpsPerJailMetrics(ch chan<- prometheus.Metric) {
jailNameToCountMap, err := e.db.CountBannedIpsPerJail()
e.lastError = err
if err != nil {
e.dbErrorCount++
log.Print(err)
}
for jailName, count := range jailNameToCountMap {
ch <- prometheus.MustNewConstMetric(
deprecatedMetricBannedIpsPerJail, prometheus.GaugeValue, float64(count), jailName,
)
}
}
func (e *Exporter) collectDeprecatedEnabledJailMetrics(ch chan<- prometheus.Metric) {
jailNameToEnabledMap, err := e.db.JailNameToEnabledValue()
e.lastError = err
if err != nil {
e.dbErrorCount++
log.Print(err)
}
for jailName, count := range jailNameToEnabledMap {
ch <- prometheus.MustNewConstMetric(
deprecatedMetricEnabledJails, prometheus.GaugeValue, float64(count), jailName,
)
}
}

64
src/export/exporter.go Normal file
View File

@ -0,0 +1,64 @@
package export
import (
fail2banDb "fail2ban-prometheus-exporter/db"
"fail2ban-prometheus-exporter/socket"
"github.com/prometheus/client_golang/prometheus"
"log"
)
type Exporter struct {
db *fail2banDb.Fail2BanDB
socketPath string
exporterVersion string
lastError error
dbErrorCount int
socketConnectionErrorCount int
socketRequestErrorCount int
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
if e.db != nil {
ch <- deprecatedMetricUp
ch <- deprecatedMetricBadIpsPerJail
ch <- deprecatedMetricBannedIpsPerJail
ch <- deprecatedMetricEnabledJails
ch <- deprecatedMetricErrorCount
}
if e.socketPath != "" {
ch <- metricServerUp
ch <- metricJailCount
ch <- metricJailFailedCurrent
ch <- metricJailFailedTotal
ch <- metricJailBannedCurrent
ch <- metricJailBannedTotal
}
ch <- metricErrorCount
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
if e.db != nil {
e.collectDeprecatedBadIpsPerJailMetrics(ch)
e.collectDeprecatedBannedIpsPerJailMetrics(ch)
e.collectDeprecatedEnabledJailMetrics(ch)
e.collectDeprecatedUpMetric(ch)
e.collectDeprecatedErrorCountMetric(ch)
}
if e.socketPath != "" {
s, err := socket.ConnectToSocket(e.socketPath)
if err != nil {
log.Printf("error opening socket: %v", err)
e.socketConnectionErrorCount++
} else {
defer s.Close()
}
e.collectServerUpMetric(ch, s)
if err == nil && s != nil {
e.collectJailMetrics(ch, s)
}
e.collectVersionMetric(ch, s)
} else {
e.collectVersionMetric(ch, nil)
}
e.collectErrorCountMetric(ch)
}

140
src/export/socket.go Normal file
View File

@ -0,0 +1,140 @@
package export
import (
"fail2ban-prometheus-exporter/socket"
"github.com/prometheus/client_golang/prometheus"
"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,
)
metricVersionInfo = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "version"),
"Version of the exporter and fail2ban server",
[]string{"exporter", "fail2ban"}, nil,
)
)
func (e *Exporter) collectErrorCountMetric(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
metricErrorCount, prometheus.CounterValue, float64(e.dbErrorCount), "db",
)
ch <- prometheus.MustNewConstMetric(
metricErrorCount, prometheus.CounterValue, float64(e.socketConnectionErrorCount), "socket_conn",
)
ch <- prometheus.MustNewConstMetric(
metricErrorCount, prometheus.CounterValue, float64(e.socketRequestErrorCount), "socket_req",
)
}
func (e *Exporter) collectServerUpMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
var serverUp float64 = 0
if s != nil {
pingSuccess, err := s.Ping()
if err != nil {
e.socketRequestErrorCount++
log.Print(err)
}
if err == nil && pingSuccess {
serverUp = 1
}
}
ch <- prometheus.MustNewConstMetric(
metricServerUp, prometheus.GaugeValue, serverUp,
)
}
func (e *Exporter) collectJailMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
jails, err := s.GetJails()
var count float64 = 0
if err != nil {
e.socketRequestErrorCount++
log.Print(err)
}
if err == nil {
count = float64(len(jails))
}
ch <- prometheus.MustNewConstMetric(
metricJailCount, prometheus.GaugeValue, count,
)
for i := range jails {
e.collectJailStatsMetric(ch, s, jails[i])
}
}
func (e *Exporter) collectJailStatsMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) {
stats, err := s.GetJailStats(jail)
if err != nil {
e.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 (e *Exporter) collectVersionMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
var err error
var fail2banVersion = ""
if s != nil {
fail2banVersion, err = s.GetServerVersion()
if err != nil {
e.socketRequestErrorCount++
log.Printf("failed to get fail2ban server version: %v", err)
}
}
ch <- prometheus.MustNewConstMetric(
metricVersionInfo, prometheus.GaugeValue, float64(1), e.exporterVersion, fail2banVersion,
)
}

View File

@ -2,102 +2,70 @@ package main
import ( import (
"fail2ban-prometheus-exporter/cfg" "fail2ban-prometheus-exporter/cfg"
fail2banDb "fail2ban-prometheus-exporter/db" "fail2ban-prometheus-exporter/export"
"fmt" "fmt"
"log"
"net/http"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
) )
const namespace = "fail2ban" const (
metricsPath = "/metrics"
)
var ( var (
version = "dev" version = "dev"
commit = "none" commit = "none"
date = "unknown" date = "unknown"
builtBy = "unknown" builtBy = "unknown"
metricUp = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"Was the last fail2ban query successful.",
nil, nil,
) )
metricBannedIpsPerJail = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "banned_ips"),
"Number of banned IPs stored in the database (per jail).",
[]string{"jail"}, nil,
)
metricBadIpsPerJail = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "bad_ips"),
"Number of bad IPs stored in the database (per jail).",
[]string{"jail"}, nil,
)
)
type Exporter struct {
db *fail2banDb.Fail2BanDB
}
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- metricUp
ch <- metricBadIpsPerJail
ch <- metricBannedIpsPerJail
}
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
metricUp, prometheus.GaugeValue, 1,
)
e.collectBadIpsPerJailMetrics(ch)
e.collectBannedIpsPerJailMetrics(ch)
}
func (e *Exporter) collectBadIpsPerJailMetrics(ch chan<- prometheus.Metric) {
jailNameToCountMap, err := e.db.CountBadIpsPerJail()
if err != nil {
log.Print(err)
}
for jailName, count := range jailNameToCountMap {
ch <- prometheus.MustNewConstMetric(
metricBadIpsPerJail, prometheus.GaugeValue, float64(count), jailName,
)
}
}
func (e *Exporter) collectBannedIpsPerJailMetrics(ch chan<- prometheus.Metric) {
jailNameToCountMap, err := e.db.CountBannedIpsPerJail()
if err != nil {
log.Print(err)
}
for jailName, count := range jailNameToCountMap {
ch <- prometheus.MustNewConstMetric(
metricBannedIpsPerJail, prometheus.GaugeValue, float64(count), jailName,
)
}
}
func printAppVersion() { func printAppVersion() {
fmt.Println(version) fmt.Println(version)
fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy) fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy)
} }
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 main() { func main() {
appSettings := cfg.Parse() appSettings := cfg.Parse()
if appSettings.VersionMode { if appSettings.VersionMode {
printAppVersion() printAppVersion()
} else { } else {
log.Print("starting fail2ban exporter") addr := fmt.Sprintf("0.0.0.0:%d", appSettings.MetricsPort)
exporter := &Exporter{ log.Printf("starting fail2ban exporter at %s", addr)
db: fail2banDb.MustConnectToDb(appSettings.Fail2BanDbPath),
} exporter := export.NewExporter(appSettings, version)
prometheus.MustRegister(exporter) prometheus.MustRegister(exporter)
http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/", rootHtmlHandler)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", appSettings.MetricsPort), nil)) http.Handle(metricsPath, promhttp.Handler())
log.Printf("metrics available at '%s'", metricsPath)
svrErr := make(chan error)
go func() {
svrErr <- http.ListenAndServe(addr, nil)
}()
log.Print("ready")
err := <-svrErr
log.Print(err)
} }
} }

View File

@ -3,6 +3,8 @@ module fail2ban-prometheus-exporter
go 1.15 go 1.15
require ( require (
github.com/kisielk/og-rek v1.1.0
github.com/mattn/go-sqlite3 v1.14.6 github.com/mattn/go-sqlite3 v1.14.6
github.com/nlpodyssey/gopickle v0.1.0
github.com/prometheus/client_golang v1.9.0 github.com/prometheus/client_golang v1.9.0
) )

View File

@ -137,6 +137,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/og-rek v1.1.0 h1:u10TvQbPtrlY/6H4+BiFsBywwSVTGFsx0YOVtpx3IbI=
github.com/kisielk/og-rek v1.1.0/go.mod h1:6ihsOSzSAxR/65S3Bn9zNihoEqRquhDQZ2c6I2+MG3c=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@ -175,6 +177,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nlpodyssey/gopickle v0.1.0 h1:9wjwRqXsOSYWZl4c4ko472b6RW+VB1I441ZcfFg1r5g=
github.com/nlpodyssey/gopickle v0.1.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=

8
src/socket/decoder.go Normal file
View File

@ -0,0 +1,8 @@
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

@ -0,0 +1,148 @@
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) 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)
}
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
}

66
src/socket/protocol.go Normal file
View File

@ -0,0 +1,66 @@
package socket
import (
"bufio"
"bytes"
"fmt"
"github.com/nlpodyssey/gopickle/pickle"
)
const (
commandTerminator = "<F2B_END_COMMAND>"
pingCommand = "ping"
statusCommand = "status"
versionCommand = "version"
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" && name == "str" {
return &Py_builtins_str{}, nil
}
return nil, fmt.Errorf("class not found: " + module + " : " + name)
}
return unpickler.Load()
}