Implement basic ban-API call back to Go-application for handling

This commit is contained in:
Michael Reber
2025-01-29 23:48:06 +01:00
parent 689f3fadd8
commit 3ffd2457ed
5 changed files with 165 additions and 17 deletions

7
go.mod
View File

@@ -2,7 +2,10 @@ module github.com/swissmakers/fail2ban-ui
go 1.22.9 go 1.22.9
require github.com/gin-gonic/gin v1.10.0 require (
github.com/gin-gonic/gin v1.10.0
github.com/oschwald/maxminddb-golang v1.13.1
)
require ( require (
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.11.6 // indirect
@@ -27,7 +30,7 @@ require (
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

6
go.sum
View File

@@ -43,6 +43,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -72,8 +74,8 @@ golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=

View File

@@ -169,7 +169,7 @@ func initializeFail2banAction() error {
fmt.Println("Error setting up jail.d configuration:", err) fmt.Println("Error setting up jail.d configuration:", err)
} }
// Write the fail2ban action file // Write the fail2ban action file
return writeFail2banAction(currentSettings.AlertCountries) return writeFail2banAction()
} }
// setupGeoCustomAction checks and replaces the default action in jail.local with our from fail2ban-UI // setupGeoCustomAction checks and replaces the default action in jail.local with our from fail2ban-UI
@@ -248,13 +248,9 @@ action_mwlg = %(action_)s
} }
// writeFail2banAction creates or updates the action file with the AlertCountries. // writeFail2banAction creates or updates the action file with the AlertCountries.
func writeFail2banAction(alertCountries []string) error { func writeFail2banAction() error {
// Join the alertCountries into a comma-separated string
countriesFormatted := strings.Join(alertCountries, ",")
// Define the Fail2Ban action file content // Define the Fail2Ban action file content
actionConfig := fmt.Sprintf(`[INCLUDES] actionConfig := `[INCLUDES]
before = sendmail-common.conf before = sendmail-common.conf
mail-whois-common.conf mail-whois-common.conf
@@ -266,9 +262,16 @@ before = sendmail-common.conf
norestored = 1 norestored = 1
# Option: actionban # Option: actionban
# This executes the Python script with <ip> and the list of allowed countries. # This executes a cURL request to notify our API when an IP is banned.
# If the country matches the allowed list, it sends the email.
actionban = /etc/fail2ban/action.d/geoip_notify.py <ip> "%s" 'name=<name>;ip=<ip>;fq-hostname=<fq-hostname>;sendername=<sendername>;sender=<sender>;dest=<dest>;failures=<failures>;_whois_command=%%(_whois_command)s;_grep_logs=%%(_grep_logs)s;grepmax=<grepmax>;mailcmd=<mailcmd>' actionban = /usr/bin/curl -X POST http://127.0.0.1:8080/api/ban \
-H "Content-Type: application/json" \
-d "{\"ip\": \"<ip>\", \
\"jail\": \"<name>\", \
\"hostname\": \"<fq-hostname>\", \
\"failures\": \"<failures>\", \
\"whois\": \"%%(_whois_command)s\", \
\"logs\": \"%%(_grep_logs)s\"}"
[Init] [Init]
@@ -280,8 +283,7 @@ logpath = /dev/null
# Number of log lines to include in the email # Number of log lines to include in the email
# grepmax = 1000 # grepmax = 1000
# grepopts = -m <grepmax> # grepopts = -m <grepmax>`
`, countriesFormatted)
// Write the action file // Write the action file
err := os.WriteFile(actionFile, []byte(actionConfig), 0644) err := os.WriteFile(actionFile, []byte(actionConfig), 0644)
@@ -335,7 +337,7 @@ func saveSettings() error {
log.Println("Settings saved successfully!") // Debug log.Println("Settings saved successfully!") // Debug
} }
// Update the Fail2ban action file // Update the Fail2ban action file
return writeFail2banAction(currentSettings.AlertCountries) return writeFail2banAction()
} }
// GetSettings returns a copy of the current settings // GetSettings returns a copy of the current settings

View File

@@ -17,13 +17,18 @@
package web package web
import ( import (
"bytes"
"fmt" "fmt"
"log"
"net"
"net/http" "net/http"
"os" "os"
"os/exec"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/oschwald/maxminddb-golang"
"github.com/swissmakers/fail2ban-ui/internal/config" "github.com/swissmakers/fail2ban-ui/internal/config"
"github.com/swissmakers/fail2ban-ui/internal/fail2ban" "github.com/swissmakers/fail2ban-ui/internal/fail2ban"
) )
@@ -91,6 +96,139 @@ func UnbanIPHandler(c *gin.Context) {
}) })
} }
// BanNotificationHandler processes incoming ban notifications from Fail2Ban.
func BanNotificationHandler(c *gin.Context) {
var request struct {
IP string `json:"ip" binding:"required"`
Jail string `json:"jail" binding:"required"`
Hostname string `json:"hostname"`
Failures string `json:"failures"`
Whois string `json:"whois"`
Logs string `json:"logs"`
}
// Parse JSON request body
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
return
}
// Handle the Fail2Ban notification
if err := HandleBanNotification(request.IP, request.Jail, request.Hostname, request.Failures, request.Whois, request.Logs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process ban notification: " + err.Error()})
return
}
// Respond with success
c.JSON(http.StatusOK, gin.H{"message": "Ban notification processed successfully"})
}
// HandleBanNotification processes Fail2Ban notifications, checks geo-location, and sends alerts.
func HandleBanNotification(ip, jail, hostname, failures, whois, logs string) error {
// Load settings to get alert countries
settings := config.GetSettings()
// Lookup the country for the given IP
country, err := lookupCountry(ip)
if err != nil {
log.Printf("⚠️ GeoIP lookup failed for IP %s: %v", ip, err)
return err
}
// Check if country is in alert list
if !shouldAlertForCountry(country, settings.AlertCountries) {
log.Printf("❌ IP %s belongs to %s, which is NOT in alert countries (%v). No alert sent.", ip, country, settings.AlertCountries)
return nil
}
// Send email notification
if err := sendBanAlert(ip, jail, hostname, failures, whois, logs, country, settings); err != nil {
log.Printf("❌ Failed to send alert email: %v", err)
return err
}
log.Printf("✅ Email alert sent for banned IP %s (%s)", ip, country)
return nil
}
// lookupCountry finds the country ISO code for a given IP using MaxMind GeoLite2 database.
func lookupCountry(ip string) (string, error) {
// Convert the IP string to net.IP
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return "", fmt.Errorf("invalid IP address: %s", ip)
}
// Open the GeoIP database
db, err := maxminddb.Open("/usr/share/GeoIP/GeoLite2-Country.mmdb")
if err != nil {
return "", fmt.Errorf("failed to open GeoIP database: %w", err)
}
defer db.Close()
// Define the structure to store the lookup result
var record struct {
Country struct {
ISOCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
}
// Perform the lookup using net.IP type
if err := db.Lookup(parsedIP, &record); err != nil {
return "", fmt.Errorf("GeoIP lookup error: %w", err)
}
// Return the country code
return record.Country.ISOCode, nil
}
// shouldAlertForCountry checks if an IPs country is in the allowed alert list.
func shouldAlertForCountry(country string, alertCountries []string) bool {
if len(alertCountries) == 0 || strings.Contains(strings.Join(alertCountries, ","), "ALL") {
return true // If "ALL" is selected, alert for all bans
}
for _, c := range alertCountries {
if strings.EqualFold(country, c) {
return true
}
}
return false
}
// sendBanAlert sends an email notification for a banned IP.
func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, settings config.AppSettings) error {
// Construct email content
emailBody := fmt.Sprintf(`Subject: [Fail2Ban] %s: banned %s from %s
Date: `+"`LC_ALL=C date +\"%%a, %%d %%h %%Y %%T %%z\"`"+`
From: %s <%s>
To: %s
Hi,
The IP %s has just been banned by Fail2Ban after %s attempts against %s.
📍 Country: %s
🔍 Whois Info:
%s
📄 Log Entries:
%s
Best Regards,
Fail2Ban
`, jail, ip, hostname, settings.Sender, settings.Sender, settings.Destemail, ip, failures, jail, country, whois, logs)
// Use msmtp or sendmail to send email
cmd := exec.Command("/usr/sbin/sendmail", "-f", settings.Sender, settings.Destemail)
cmd.Stdin = bytes.NewBufferString(emailBody)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to send email: %w", err)
}
return nil
}
func sortByTimeDesc(events []fail2ban.BanEvent) { func sortByTimeDesc(events []fail2ban.BanEvent) {
for i := 0; i < len(events); i++ { for i := 0; i < len(events); i++ {
for j := i + 1; j < len(events); j++ { for j := i + 1; j < len(events); j++ {

View File

@@ -46,5 +46,8 @@ func RegisterRoutes(r *gin.Engine) {
// Reload endpoint // Reload endpoint
api.POST("/fail2ban/reload", ReloadFail2banHandler) api.POST("/fail2ban/reload", ReloadFail2banHandler)
// Handle Fail2Ban notifications
api.POST("/ban", BanNotificationHandler)
} }
} }