From 3ffd2457ed20858a59e053c4b62d76faa63e9a53 Mon Sep 17 00:00:00 2001 From: Michael Reber Date: Wed, 29 Jan 2025 23:48:06 +0100 Subject: [PATCH] Implement basic ban-API call back to Go-application for handling --- go.mod | 7 +- go.sum | 6 +- internal/config/settings.go | 28 ++++---- pkg/web/handlers.go | 138 ++++++++++++++++++++++++++++++++++++ pkg/web/routes.go | 3 + 5 files changed, 165 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 8b611af..bcb0fc7 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/swissmakers/fail2ban-ui 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 ( github.com/bytedance/sonic v1.11.6 // indirect @@ -27,7 +30,7 @@ require ( golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.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 google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 7f08abb..451fafe 100644 --- a/go.sum +++ b/go.sum @@ -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/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/internal/config/settings.go b/internal/config/settings.go index 30bf18c..fb3ab0f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -169,7 +169,7 @@ func initializeFail2banAction() error { fmt.Println("Error setting up jail.d configuration:", err) } // 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 @@ -248,13 +248,9 @@ action_mwlg = %(action_)s } // writeFail2banAction creates or updates the action file with the AlertCountries. -func writeFail2banAction(alertCountries []string) error { - - // Join the alertCountries into a comma-separated string - countriesFormatted := strings.Join(alertCountries, ",") - +func writeFail2banAction() error { // Define the Fail2Ban action file content - actionConfig := fmt.Sprintf(`[INCLUDES] + actionConfig := `[INCLUDES] before = sendmail-common.conf mail-whois-common.conf @@ -266,9 +262,16 @@ before = sendmail-common.conf norestored = 1 # Option: actionban -# This executes the Python script with and the list of allowed countries. -# If the country matches the allowed list, it sends the email. -actionban = /etc/fail2ban/action.d/geoip_notify.py "%s" 'name=;ip=;fq-hostname=;sendername=;sender=;dest=;failures=;_whois_command=%%(_whois_command)s;_grep_logs=%%(_grep_logs)s;grepmax=;mailcmd=' +# This executes a cURL request to notify our API when an IP is banned. + +actionban = /usr/bin/curl -X POST http://127.0.0.1:8080/api/ban \ + -H "Content-Type: application/json" \ + -d "{\"ip\": \"\", \ + \"jail\": \"\", \ + \"hostname\": \"\", \ + \"failures\": \"\", \ + \"whois\": \"%%(_whois_command)s\", \ + \"logs\": \"%%(_grep_logs)s\"}" [Init] @@ -280,8 +283,7 @@ logpath = /dev/null # Number of log lines to include in the email # grepmax = 1000 -# grepopts = -m -`, countriesFormatted) +# grepopts = -m ` // Write the action file err := os.WriteFile(actionFile, []byte(actionConfig), 0644) @@ -335,7 +337,7 @@ func saveSettings() error { log.Println("Settings saved successfully!") // Debug } // Update the Fail2ban action file - return writeFail2banAction(currentSettings.AlertCountries) + return writeFail2banAction() } // GetSettings returns a copy of the current settings diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index c4eb0fb..2351b11 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -17,13 +17,18 @@ package web import ( + "bytes" "fmt" + "log" + "net" "net/http" "os" + "os/exec" "strings" "time" "github.com/gin-gonic/gin" + "github.com/oschwald/maxminddb-golang" "github.com/swissmakers/fail2ban-ui/internal/config" "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 IP’s 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) { for i := 0; i < len(events); i++ { for j := i + 1; j < len(events); j++ { diff --git a/pkg/web/routes.go b/pkg/web/routes.go index 674d693..752e9d2 100644 --- a/pkg/web/routes.go +++ b/pkg/web/routes.go @@ -46,5 +46,8 @@ func RegisterRoutes(r *gin.Engine) { // Reload endpoint api.POST("/fail2ban/reload", ReloadFail2banHandler) + + // Handle Fail2Ban notifications + api.POST("/ban", BanNotificationHandler) } }