mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Implement geoIP and whois lookups directly from fail2ban-UI
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# =========================================
|
||||
# STAGE 1: Build Fail2Ban UI Binary
|
||||
# =========================================
|
||||
FROM golang:1.23 AS builder
|
||||
FROM golang:1.24 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
9
go.mod
9
go.mod
@@ -1,6 +1,8 @@
|
||||
module github.com/swissmakers/fail2ban-ui
|
||||
|
||||
go 1.23
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
@@ -26,6 +28,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/likexian/whois v1.15.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -35,9 +38,9 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -46,6 +46,8 @@ github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/likexian/whois v1.15.6 h1:hizngFHJTNQDlhwhU+FEGyPGxy8bRnf25gHDNrSB4Ag=
|
||||
github.com/likexian/whois v1.15.6/go.mod h1:vx3kt3sZ4mx4XFgpaNp3GXQCZQIzAoyrUAkRtJwoM2I=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -85,10 +87,14 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -96,8 +102,11 @@ golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
|
||||
@@ -70,6 +70,11 @@ type AppSettings struct {
|
||||
Banaction string `json:"banaction"` // Default banning action
|
||||
BanactionAllports string `json:"banactionAllports"` // Allports banning action
|
||||
//Sender string `json:"sender"`
|
||||
|
||||
// GeoIP and Whois settings
|
||||
GeoIPProvider string `json:"geoipProvider"` // "maxmind" or "builtin"
|
||||
GeoIPDatabasePath string `json:"geoipDatabasePath"` // Path to MaxMind database (optional)
|
||||
MaxLogLines int `json:"maxLogLines"` // Maximum log lines to include (default: 50)
|
||||
}
|
||||
|
||||
type AdvancedActionsConfig struct {
|
||||
@@ -172,9 +177,8 @@ actionban = /usr/bin/curl -X POST __CALLBACK_URL__/api/ban \
|
||||
--arg jail '<name>' \
|
||||
--arg hostname '<fq-hostname>' \
|
||||
--arg failures '<failures>' \
|
||||
--arg whois "$(whois <ip> || echo 'missing whois program')" \
|
||||
--arg logs "$(tac <logpath> | grep <grepopts> -wF <ip>)" \
|
||||
'{serverId: $serverId, ip: $ip, jail: $jail, hostname: $hostname, failures: $failures, whois: $whois, logs: $logs}')"
|
||||
'{serverId: $serverId, ip: $ip, jail: $jail, hostname: $hostname, failures: $failures, logs: $logs}')"
|
||||
|
||||
[Init]
|
||||
|
||||
@@ -458,6 +462,9 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) {
|
||||
Banaction: currentSettings.Banaction,
|
||||
BanactionAllports: currentSettings.BanactionAllports,
|
||||
AdvancedActionsJSON: string(advancedBytes),
|
||||
GeoIPProvider: currentSettings.GeoIPProvider,
|
||||
GeoIPDatabasePath: currentSettings.GeoIPDatabasePath,
|
||||
MaxLogLines: currentSettings.MaxLogLines,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -577,6 +584,15 @@ func setDefaultsLocked() {
|
||||
if currentSettings.BanactionAllports == "" {
|
||||
currentSettings.BanactionAllports = "iptables-allports"
|
||||
}
|
||||
if currentSettings.GeoIPProvider == "" {
|
||||
currentSettings.GeoIPProvider = "builtin"
|
||||
}
|
||||
if currentSettings.GeoIPDatabasePath == "" {
|
||||
currentSettings.GeoIPDatabasePath = "/usr/share/GeoIP/GeoLite2-Country.mmdb"
|
||||
}
|
||||
if currentSettings.MaxLogLines == 0 {
|
||||
currentSettings.MaxLogLines = 50
|
||||
}
|
||||
|
||||
if (currentSettings.AdvancedActions == AdvancedActionsConfig{}) {
|
||||
currentSettings.AdvancedActions = defaultAdvancedActionsConfig()
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
"settings.default_max_retry": "Standard-Maximalversuche",
|
||||
"settings.default_max_retry.description": "Anzahl der Fehler, bevor ein Host gesperrt wird.",
|
||||
"settings.default_max_retry_placeholder": "Geben Sie die maximale Anzahl der Versuche ein",
|
||||
"settings.geoip_provider": "GeoIP-Anbieter",
|
||||
"settings.geoip_provider.description": "Wählen Sie den GeoIP-Lookup-Anbieter. MaxMind erfordert eine lokale Datenbankdatei, während Built-in eine kostenlose Online-API verwendet.",
|
||||
"settings.geoip_provider.maxmind": "MaxMind (Lokale Datenbank)",
|
||||
"settings.geoip_provider.builtin": "Built-in (ip-api.com)",
|
||||
"settings.geoip_database_path": "GeoIP-Datenbankpfad",
|
||||
"settings.geoip_database_path.description": "Pfad zur MaxMind GeoLite2-Country-Datenbankdatei.",
|
||||
"settings.max_log_lines": "Maximale Log-Zeilen",
|
||||
"settings.max_log_lines.description": "Maximale Anzahl von Log-Zeilen, die in Ban-Benachrichtigungen enthalten sein sollen. Die relevantesten Zeilen werden automatisch ausgewählt.",
|
||||
"settings.ignore_ips": "IP-Adressen ignorieren",
|
||||
"settings.ignore_ips.description": "Durch Leerzeichen getrennte Liste von IP-Adressen, CIDR-Masken oder DNS-Hosts. Fail2ban wird keinen Host sperren, der mit einer Adresse in dieser Liste übereinstimmt.",
|
||||
"settings.ignore_ips_placeholder": "IP-Adressen, getrennt durch Leerzeichen",
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
"settings.default_max_retry": "Standard-Maximalversüech",
|
||||
"settings.default_max_retry.description": "Aazahl vo de Fähler, bevor ä Host gsperrt wird.",
|
||||
"settings.default_max_retry_placeholder": "Gib d'maximal Versüech ii",
|
||||
"settings.geoip_provider": "GeoIP-Aabieter",
|
||||
"settings.geoip_provider.description": "Wähl di GeoIP-Aabieter. MaxMind brucht e lokali Datenbankdatei, während Built-in e gratis Online-API verwendet.",
|
||||
"settings.geoip_provider.maxmind": "MaxMind (Lokali Datenbank)",
|
||||
"settings.geoip_provider.builtin": "Built-in (ip-api.com)",
|
||||
"settings.geoip_database_path": "GeoIP-Datenbankpfad",
|
||||
"settings.geoip_database_path.description": "Pfad zur MaxMind GeoLite2-Country-Datebank.",
|
||||
"settings.max_log_lines": "Maximali Log-Zeile",
|
||||
"settings.max_log_lines.description": "Maximali Aazahl vo Log-Zeile, wo i Ban-Benachrichtigunge enthalte si söll. Di relevanteschte Zeile werdet automatisch usgwählt.",
|
||||
"settings.ignore_ips": "IPs ignorierä",
|
||||
"settings.ignore_ips.description": "Dur Leerzeichä trennti Lischte vo IP-Adrässe, CIDR-Maske oder DNS-Hosts. Fail2ban wird kei Host sperre, wo mit ere Adrässe i dere Lischte übereistimmt.",
|
||||
"settings.ignore_ips_placeholder": "IPs, getrennt dur e Leerzeichä",
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
"settings.default_max_retry": "Default Max Retry",
|
||||
"settings.default_max_retry.description": "Number of failures before a host gets banned.",
|
||||
"settings.default_max_retry_placeholder": "Enter maximum retries",
|
||||
"settings.geoip_provider": "GeoIP Provider",
|
||||
"settings.geoip_provider.description": "Choose the GeoIP lookup provider. MaxMind requires a local database file, while Built-in uses a free online API.",
|
||||
"settings.geoip_provider.maxmind": "MaxMind (Local Database)",
|
||||
"settings.geoip_provider.builtin": "Built-in (ip-api.com)",
|
||||
"settings.geoip_database_path": "GeoIP Database Path",
|
||||
"settings.geoip_database_path.description": "Path to the MaxMind GeoLite2-Country database file.",
|
||||
"settings.max_log_lines": "Maximum Log Lines",
|
||||
"settings.max_log_lines.description": "Maximum number of log lines to include in ban notifications. Most relevant lines are selected automatically.",
|
||||
"settings.ignore_ips": "Ignore IPs",
|
||||
"settings.ignore_ips.description": "Space separated list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which matches an address in this list.",
|
||||
"settings.ignore_ips_placeholder": "IPs to ignore, separated by spaces",
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
"settings.default_max_retry": "Número máximo de reintentos por defecto",
|
||||
"settings.default_max_retry.description": "Número de fallos antes de que un host sea bloqueado.",
|
||||
"settings.default_max_retry_placeholder": "Introduce el número máximo de reintentos",
|
||||
"settings.geoip_provider": "Proveedor de GeoIP",
|
||||
"settings.geoip_provider.description": "Elija el proveedor de consulta GeoIP. MaxMind requiere un archivo de base de datos local, mientras que Built-in utiliza una API en línea gratuita.",
|
||||
"settings.geoip_provider.maxmind": "MaxMind (Base de Datos Local)",
|
||||
"settings.geoip_provider.builtin": "Built-in (ip-api.com)",
|
||||
"settings.geoip_database_path": "Ruta de la Base de Datos GeoIP",
|
||||
"settings.geoip_database_path.description": "Ruta al archivo de base de datos MaxMind GeoLite2-Country.",
|
||||
"settings.max_log_lines": "Líneas de Log Máximas",
|
||||
"settings.max_log_lines.description": "Número máximo de líneas de log a incluir en las notificaciones de bloqueo. Las líneas más relevantes se seleccionan automáticamente.",
|
||||
"settings.ignore_ips": "Ignorar IPs",
|
||||
"settings.ignore_ips.description": "Lista separada por espacios de direcciones IP, máscaras CIDR o hosts DNS. Fail2ban no bloqueará un host que coincida con una dirección en esta lista.",
|
||||
"settings.ignore_ips_placeholder": "IPs a ignorar, separadas por espacios",
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
"settings.default_max_retry": "Nombre maximal de réessais par défaut",
|
||||
"settings.default_max_retry.description": "Nombre d'échecs avant qu'un hôte ne soit banni.",
|
||||
"settings.default_max_retry_placeholder": "Entrez le nombre maximal de réessais",
|
||||
"settings.geoip_provider": "Fournisseur GeoIP",
|
||||
"settings.geoip_provider.description": "Choisissez le fournisseur de recherche GeoIP. MaxMind nécessite un fichier de base de données local, tandis que Built-in utilise une API en ligne gratuite.",
|
||||
"settings.geoip_provider.maxmind": "MaxMind (Base de Données Locale)",
|
||||
"settings.geoip_provider.builtin": "Built-in (ip-api.com)",
|
||||
"settings.geoip_database_path": "Chemin de la Base de Données GeoIP",
|
||||
"settings.geoip_database_path.description": "Chemin vers le fichier de base de données MaxMind GeoLite2-Country.",
|
||||
"settings.max_log_lines": "Lignes de Log Maximales",
|
||||
"settings.max_log_lines.description": "Nombre maximal de lignes de log à inclure dans les notifications de bannissement. Les lignes les plus pertinentes sont sélectionnées automatiquement.",
|
||||
"settings.ignore_ips": "Ignorer les IPs",
|
||||
"settings.ignore_ips.description": "Liste séparée par des espaces d'adresses IP, de masques CIDR ou d'hôtes DNS. Fail2ban ne bannira pas un hôte qui correspond à une adresse de cette liste.",
|
||||
"settings.ignore_ips_placeholder": "IPs à ignorer, séparées par des espaces",
|
||||
|
||||
@@ -138,6 +138,14 @@
|
||||
"settings.default_max_retry": "Numero massimo di tentativi predefinito",
|
||||
"settings.default_max_retry.description": "Numero di errori prima che un host venga bannato.",
|
||||
"settings.default_max_retry_placeholder": "Inserisci il numero massimo di tentativi",
|
||||
"settings.geoip_provider": "Provider GeoIP",
|
||||
"settings.geoip_provider.description": "Scegli il provider di ricerca GeoIP. MaxMind richiede un file di database locale, mentre Built-in utilizza un'API online gratuita.",
|
||||
"settings.geoip_provider.maxmind": "MaxMind (Database Locale)",
|
||||
"settings.geoip_provider.builtin": "Built-in (ip-api.com)",
|
||||
"settings.geoip_database_path": "Percorso Database GeoIP",
|
||||
"settings.geoip_database_path.description": "Percorso al file del database MaxMind GeoLite2-Country.",
|
||||
"settings.max_log_lines": "Righe di Log Massime",
|
||||
"settings.max_log_lines.description": "Numero massimo di righe di log da includere nelle notifiche di ban. Le righe più rilevanti vengono selezionate automaticamente.",
|
||||
"settings.ignore_ips": "Ignora IP",
|
||||
"settings.ignore_ips.description": "Elenco separato da spazi di indirizzi IP, maschere CIDR o host DNS. Fail2ban non bannerà un host che corrisponde a un indirizzo in questo elenco.",
|
||||
"settings.ignore_ips_placeholder": "IP da ignorare, separate da spazi",
|
||||
|
||||
@@ -70,6 +70,9 @@ type AppSettingsRecord struct {
|
||||
Banaction string
|
||||
BanactionAllports string
|
||||
AdvancedActionsJSON string
|
||||
GeoIPProvider string
|
||||
GeoIPDatabasePath string
|
||||
MaxLogLines int
|
||||
}
|
||||
|
||||
type ServerRecord struct {
|
||||
@@ -171,17 +174,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) {
|
||||
}
|
||||
|
||||
row := db.QueryRowContext(ctx, `
|
||||
SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions
|
||||
SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines
|
||||
FROM app_settings
|
||||
WHERE id = 1`)
|
||||
|
||||
var (
|
||||
lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions sql.NullString
|
||||
port, smtpPort, maxretry sql.NullInt64
|
||||
lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath sql.NullString
|
||||
port, smtpPort, maxretry, maxLogLines sql.NullInt64
|
||||
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn sql.NullInt64
|
||||
)
|
||||
|
||||
err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions)
|
||||
err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return AppSettingsRecord{}, false, nil
|
||||
}
|
||||
@@ -212,6 +215,9 @@ WHERE id = 1`)
|
||||
Banaction: stringFromNull(banaction),
|
||||
BanactionAllports: stringFromNull(banactionAllports),
|
||||
AdvancedActionsJSON: stringFromNull(advancedActions),
|
||||
GeoIPProvider: stringFromNull(geoipProvider),
|
||||
GeoIPDatabasePath: stringFromNull(geoipDatabasePath),
|
||||
MaxLogLines: intFromNull(maxLogLines),
|
||||
}
|
||||
|
||||
return rec, true, nil
|
||||
@@ -223,9 +229,9 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error {
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `
|
||||
INSERT INTO app_settings (
|
||||
id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions
|
||||
id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines
|
||||
) VALUES (
|
||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
) ON CONFLICT(id) DO UPDATE SET
|
||||
language = excluded.language,
|
||||
port = excluded.port,
|
||||
@@ -248,7 +254,10 @@ INSERT INTO app_settings (
|
||||
destemail = excluded.destemail,
|
||||
banaction = excluded.banaction,
|
||||
banaction_allports = excluded.banaction_allports,
|
||||
advanced_actions = excluded.advanced_actions
|
||||
advanced_actions = excluded.advanced_actions,
|
||||
geoip_provider = excluded.geoip_provider,
|
||||
geoip_database_path = excluded.geoip_database_path,
|
||||
max_log_lines = excluded.max_log_lines
|
||||
`, rec.Language,
|
||||
rec.Port,
|
||||
boolToInt(rec.Debug),
|
||||
@@ -271,6 +280,9 @@ INSERT INTO app_settings (
|
||||
rec.Banaction,
|
||||
rec.BanactionAllports,
|
||||
rec.AdvancedActionsJSON,
|
||||
rec.GeoIPProvider,
|
||||
rec.GeoIPDatabasePath,
|
||||
rec.MaxLogLines,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -451,7 +463,7 @@ INSERT INTO ban_events (
|
||||
server_id, server_name, jail, ip, country, hostname, failures, whois, logs, occurred_at, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
result, err := db.ExecContext(
|
||||
_, err := db.ExecContext(
|
||||
ctx,
|
||||
query,
|
||||
record.ServerID,
|
||||
@@ -470,12 +482,6 @@ INSERT INTO ban_events (
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the inserted ID
|
||||
id, err := result.LastInsertId()
|
||||
if err == nil {
|
||||
record.ID = id
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -797,7 +803,10 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
||||
destemail TEXT,
|
||||
banaction TEXT,
|
||||
banaction_allports TEXT,
|
||||
advanced_actions TEXT
|
||||
advanced_actions TEXT,
|
||||
geoip_provider TEXT,
|
||||
geoip_database_path TEXT,
|
||||
max_log_lines INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
@@ -884,6 +893,38 @@ CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status);
|
||||
}
|
||||
}
|
||||
|
||||
// Add geoip_provider column
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN geoip_provider TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add geoip_database_path column
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN geoip_database_path TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add max_log_lines column
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN max_log_lines INTEGER`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set default values for new columns if they are NULL
|
||||
if _, err := db.ExecContext(ctx, `UPDATE app_settings SET geoip_provider = 'maxmind' WHERE geoip_provider IS NULL`); err != nil {
|
||||
log.Printf("Warning: Failed to set default value for geoip_provider: %v", err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `UPDATE app_settings SET geoip_database_path = '/usr/share/GeoIP/GeoLite2-Country.mmdb' WHERE geoip_database_path IS NULL`); err != nil {
|
||||
log.Printf("Warning: Failed to set default value for geoip_database_path: %v", err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `UPDATE app_settings SET max_log_lines = 50 WHERE max_log_lines IS NULL OR max_log_lines = 0`); err != nil {
|
||||
log.Printf("Warning: Failed to set default value for max_log_lines: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||
"github.com/swissmakers/fail2ban-ui/internal/integrations"
|
||||
@@ -84,7 +86,7 @@ func runAdvancedIntegrationAction(ctx context.Context, action, ip string, settin
|
||||
"unblock": "unblocked",
|
||||
}[action]
|
||||
|
||||
message := fmt.Sprintf("%s via %s", strings.Title(action), cfg.Integration)
|
||||
message := fmt.Sprintf("%s via %s", cases.Title(language.English).String(action), cfg.Integration)
|
||||
if err != nil && !skipLoggingIfAlreadyBlocked {
|
||||
status = "error"
|
||||
message = err.Error()
|
||||
|
||||
@@ -177,7 +177,7 @@ func BanNotificationHandler(c *gin.Context) {
|
||||
Jail string `json:"jail" binding:"required"`
|
||||
Hostname string `json:"hostname"`
|
||||
Failures string `json:"failures"`
|
||||
Whois string `json:"whois"`
|
||||
Whois string `json:"whois"` // Optional for backward compatibility
|
||||
Logs string `json:"logs"`
|
||||
}
|
||||
|
||||
@@ -585,15 +585,42 @@ func TestServerHandler(c *gin.Context) {
|
||||
|
||||
// HandleBanNotification processes Fail2Ban notifications, checks geo-location, stores the event, and sends alerts.
|
||||
func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip, jail, hostname, failures, whois, logs string) error {
|
||||
// Load settings to get alert countries
|
||||
// Load settings to get alert countries and GeoIP provider
|
||||
settings := config.GetSettings()
|
||||
|
||||
// Lookup the country for the given IP
|
||||
country, err := lookupCountry(ip)
|
||||
// Perform whois lookup if not provided (backward compatibility)
|
||||
var whoisData string
|
||||
var err error
|
||||
if whois == "" || whois == "missing whois program" {
|
||||
log.Printf("Performing whois lookup for IP %s", ip)
|
||||
whoisData, err = lookupWhois(ip)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Whois lookup failed for IP %s: %v", ip, err)
|
||||
whoisData = ""
|
||||
}
|
||||
} else {
|
||||
log.Printf("Using provided whois data for IP %s", ip)
|
||||
whoisData = whois
|
||||
}
|
||||
|
||||
// Filter logs to show relevant lines
|
||||
filteredLogs := filterRelevantLogs(logs, ip, settings.MaxLogLines)
|
||||
|
||||
// Lookup the country for the given IP using configured provider
|
||||
country, err := lookupCountry(ip, settings.GeoIPProvider, settings.GeoIPDatabasePath)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ GeoIP lookup failed for IP %s: %v", ip, err)
|
||||
// Try to extract country from whois as fallback
|
||||
if whoisData != "" {
|
||||
country = extractCountryFromWhois(whoisData)
|
||||
if country != "" {
|
||||
log.Printf("Extracted country %s from whois data for IP %s", country, ip)
|
||||
}
|
||||
}
|
||||
if country == "" {
|
||||
country = ""
|
||||
}
|
||||
}
|
||||
|
||||
event := storage.BanEventRecord{
|
||||
ServerID: server.ID,
|
||||
@@ -603,8 +630,8 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip
|
||||
Country: country,
|
||||
Hostname: hostname,
|
||||
Failures: failures,
|
||||
Whois: whois,
|
||||
Logs: logs,
|
||||
Whois: whoisData,
|
||||
Logs: filteredLogs,
|
||||
OccurredAt: time.Now().UTC(),
|
||||
}
|
||||
if err := storage.RecordBanEvent(ctx, event); err != nil {
|
||||
@@ -630,7 +657,7 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip
|
||||
}
|
||||
|
||||
// Send email notification
|
||||
if err := sendBanAlert(ip, jail, hostname, failures, whois, logs, country, settings); err != nil {
|
||||
if err := sendBanAlert(ip, jail, hostname, failures, whoisData, filteredLogs, country, settings); err != nil {
|
||||
log.Printf("❌ Failed to send alert email: %v", err)
|
||||
return err
|
||||
}
|
||||
@@ -639,8 +666,29 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupCountry finds the country ISO code for a given IP using MaxMind GeoLite2 database.
|
||||
func lookupCountry(ip string) (string, error) {
|
||||
// lookupCountry finds the country ISO code for a given IP using the configured provider.
|
||||
func lookupCountry(ip, provider, dbPath string) (string, error) {
|
||||
switch provider {
|
||||
case "builtin":
|
||||
return lookupCountryBuiltin(ip)
|
||||
case "maxmind", "":
|
||||
// Default to maxmind if empty
|
||||
if dbPath == "" {
|
||||
dbPath = "/usr/share/GeoIP/GeoLite2-Country.mmdb"
|
||||
}
|
||||
return lookupCountryMaxMind(ip, dbPath)
|
||||
default:
|
||||
// Unknown provider, try maxmind as fallback
|
||||
log.Printf("Unknown GeoIP provider '%s', falling back to MaxMind", provider)
|
||||
if dbPath == "" {
|
||||
dbPath = "/usr/share/GeoIP/GeoLite2-Country.mmdb"
|
||||
}
|
||||
return lookupCountryMaxMind(ip, dbPath)
|
||||
}
|
||||
}
|
||||
|
||||
// lookupCountryMaxMind finds the country ISO code using MaxMind GeoLite2 database.
|
||||
func lookupCountryMaxMind(ip, dbPath string) (string, error) {
|
||||
// Convert the IP string to net.IP
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
@@ -648,9 +696,9 @@ func lookupCountry(ip string) (string, error) {
|
||||
}
|
||||
|
||||
// Open the GeoIP database
|
||||
db, err := maxminddb.Open("/usr/share/GeoIP/GeoLite2-Country.mmdb")
|
||||
db, err := maxminddb.Open(dbPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open GeoIP database: %w", err)
|
||||
return "", fmt.Errorf("failed to open GeoIP database at %s: %w", dbPath, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
@@ -670,6 +718,150 @@ func lookupCountry(ip string) (string, error) {
|
||||
return record.Country.ISOCode, nil
|
||||
}
|
||||
|
||||
// lookupCountryBuiltin finds the country ISO code using ip-api.com free API.
|
||||
func lookupCountryBuiltin(ip string) (string, error) {
|
||||
// Convert the IP string to net.IP to validate
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return "", fmt.Errorf("invalid IP address: %s", ip)
|
||||
}
|
||||
|
||||
// Use ip-api.com free API (no account needed, rate limited to 45 requests/minute)
|
||||
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=countryCode", ip)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to query ip-api.com: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
CountryCode string `json:"countryCode"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if result.Status == "fail" {
|
||||
return "", fmt.Errorf("ip-api.com error: %s", result.Message)
|
||||
}
|
||||
|
||||
return result.CountryCode, nil
|
||||
}
|
||||
|
||||
// filterRelevantLogs filters log lines to show the most relevant ones that caused the block.
|
||||
func filterRelevantLogs(logs, ip string, maxLines int) string {
|
||||
if logs == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if maxLines <= 0 {
|
||||
maxLines = 50 // Default
|
||||
}
|
||||
|
||||
lines := strings.Split(logs, "\n")
|
||||
if len(lines) <= maxLines {
|
||||
return logs // Return as-is if within limit
|
||||
}
|
||||
|
||||
// Priority indicators for relevant log lines
|
||||
priorityPatterns := []string{
|
||||
"denied", "deny", "forbidden", "unauthorized", "failed", "failure",
|
||||
"error", "403", "404", "401", "500", "502", "503",
|
||||
"invalid", "rejected", "blocked", "ban",
|
||||
}
|
||||
|
||||
// Score each line based on relevance
|
||||
type scoredLine struct {
|
||||
line string
|
||||
score int
|
||||
index int
|
||||
}
|
||||
|
||||
scored := make([]scoredLine, len(lines))
|
||||
for i, line := range lines {
|
||||
lineLower := strings.ToLower(line)
|
||||
score := 0
|
||||
|
||||
// Check if line contains the IP
|
||||
if strings.Contains(line, ip) {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// Check for priority patterns
|
||||
for _, pattern := range priorityPatterns {
|
||||
if strings.Contains(lineLower, pattern) {
|
||||
score += 5
|
||||
}
|
||||
}
|
||||
|
||||
// Recent lines get higher score (lines at the end are more recent)
|
||||
score += (len(lines) - i) / 10
|
||||
|
||||
scored[i] = scoredLine{
|
||||
line: line,
|
||||
score: score,
|
||||
index: i,
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score (descending)
|
||||
for i := 0; i < len(scored)-1; i++ {
|
||||
for j := i + 1; j < len(scored); j++ {
|
||||
if scored[i].score < scored[j].score {
|
||||
scored[i], scored[j] = scored[j], scored[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Take top N lines and sort by original index to maintain chronological order
|
||||
selected := scored[:maxLines]
|
||||
for i := 0; i < len(selected)-1; i++ {
|
||||
for j := i + 1; j < len(selected); j++ {
|
||||
if selected[i].index > selected[j].index {
|
||||
selected[i], selected[j] = selected[j], selected[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build result
|
||||
result := make([]string, len(selected))
|
||||
for i, s := range selected {
|
||||
result[i] = s.line
|
||||
}
|
||||
|
||||
// Remove duplicate consecutive lines
|
||||
filtered := []string{}
|
||||
lastLine := ""
|
||||
for _, line := range result {
|
||||
if line != lastLine {
|
||||
filtered = append(filtered, line)
|
||||
lastLine = line
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(filtered, "\n")
|
||||
}
|
||||
|
||||
// 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") {
|
||||
|
||||
@@ -180,9 +180,9 @@ mark {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
#backendStatus:hover {
|
||||
/*#backendStatus:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}*/
|
||||
|
||||
#statusDot {
|
||||
width: 0.5rem;
|
||||
|
||||
@@ -63,7 +63,7 @@ function showBanEventToast(event) {
|
||||
+ ' <div class="font-semibold text-sm">New Block Detected</div>'
|
||||
+ ' <div class="text-sm mt-1">'
|
||||
+ ' <span class="font-mono font-semibold">' + escapeHtml(ip) + '</span>'
|
||||
+ ' <span class="text-gray-500"> banned in </span>'
|
||||
+ ' <span> banned in </span>'
|
||||
+ ' <span class="font-semibold">' + escapeHtml(jail) + '</span>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="text-xs text-gray-400 mt-1">'
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
// Settings page functions for Fail2ban UI
|
||||
"use strict";
|
||||
|
||||
// Handle GeoIP provider change
|
||||
function onGeoIPProviderChange(provider) {
|
||||
const dbPathContainer = document.getElementById('geoipDatabasePathContainer');
|
||||
if (dbPathContainer) {
|
||||
if (provider === 'maxmind') {
|
||||
dbPathContainer.style.display = 'block';
|
||||
} else {
|
||||
dbPathContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
showLoading(true);
|
||||
fetch('/api/settings')
|
||||
@@ -83,6 +95,13 @@ function loadSettings() {
|
||||
|
||||
document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false;
|
||||
document.getElementById('defaultJailEnable').checked = data.defaultJailEnable || false;
|
||||
|
||||
// GeoIP settings
|
||||
const geoipProvider = data.geoipProvider || 'builtin';
|
||||
document.getElementById('geoipProvider').value = geoipProvider;
|
||||
onGeoIPProviderChange(geoipProvider);
|
||||
document.getElementById('geoipDatabasePath').value = data.geoipDatabasePath || '/usr/share/GeoIP/GeoLite2-Country.mmdb';
|
||||
document.getElementById('maxLogLines').value = data.maxLogLines || 50;
|
||||
document.getElementById('banTime').value = data.bantime || '';
|
||||
document.getElementById('findTime').value = data.findtime || '';
|
||||
document.getElementById('maxRetry').value = data.maxretry || '';
|
||||
@@ -149,6 +168,9 @@ function saveSettings(event) {
|
||||
ignoreips: getIgnoreIPsArray(),
|
||||
banaction: document.getElementById('banaction').value,
|
||||
banactionAllports: document.getElementById('banactionAllports').value,
|
||||
geoipProvider: document.getElementById('geoipProvider').value || 'builtin',
|
||||
geoipDatabasePath: document.getElementById('geoipDatabasePath').value || '/usr/share/GeoIP/GeoLite2-Country.mmdb',
|
||||
maxLogLines: parseInt(document.getElementById('maxLogLines').value, 10) || 50,
|
||||
smtp: smtpSettings,
|
||||
advancedActions: collectAdvancedActionsSettings()
|
||||
};
|
||||
|
||||
@@ -330,12 +330,38 @@
|
||||
<!-- Alert Settings Group -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.alert">Alert Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="destEmail" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.destination_email">Destination Email (Alerts Receiver)</label>
|
||||
<input type="email" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="destEmail"
|
||||
data-i18n-placeholder="settings.destination_email_placeholder" placeholder="alerts@swissmakers.ch" />
|
||||
<p class="text-xs text-red-600 mt-1 hidden" id="destEmailError"></p>
|
||||
</div>
|
||||
|
||||
<!-- GeoIP Provider -->
|
||||
<div class="mb-4">
|
||||
<label for="geoipProvider" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.geoip_provider">GeoIP Provider</label>
|
||||
<select id="geoipProvider" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" onchange="onGeoIPProviderChange(this.value)">
|
||||
<option value="builtin" data-i18n="settings.geoip_provider.builtin">Built-in (ip-api.com)</option>
|
||||
<option value="maxmind" data-i18n="settings.geoip_provider.maxmind">MaxMind (Local Database)</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.geoip_provider.description">Choose the GeoIP lookup provider. MaxMind requires a local database file, while Built-in uses a free online API.</p>
|
||||
</div>
|
||||
|
||||
<!-- GeoIP Database Path (shown only for MaxMind) -->
|
||||
<div id="geoipDatabasePathContainer" class="mb-4">
|
||||
<label for="geoipDatabasePath" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.geoip_database_path">GeoIP Database Path</label>
|
||||
<input type="text" id="geoipDatabasePath" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="/usr/share/GeoIP/GeoLite2-Country.mmdb">
|
||||
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.geoip_database_path.description">Path to the MaxMind GeoLite2-Country database file.</p>
|
||||
</div>
|
||||
|
||||
<!-- Max Log Lines -->
|
||||
<div class="mb-4">
|
||||
<label for="maxLogLines" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.max_log_lines">Maximum Log Lines</label>
|
||||
<input type="number" id="maxLogLines" min="1" max="500" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="50">
|
||||
<p class="text-xs text-gray-500 mt-1" data-i18n="settings.max_log_lines.description">Maximum number of log lines to include in ban notifications. Most relevant lines are selected automatically.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="alertCountries" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.alert_countries">Alert Countries</label>
|
||||
<p class="text-sm text-gray-500 mb-2" data-i18n="settings.alert_countries_description">
|
||||
|
||||
@@ -37,9 +37,6 @@ const (
|
||||
|
||||
// Send pings to peer with this period (must be less than pongWait)
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size allowed from peer
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
|
||||
125
pkg/web/whois.go
Normal file
125
pkg/web/whois.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||
//
|
||||
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
||||
//
|
||||
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/likexian/whois"
|
||||
)
|
||||
|
||||
var (
|
||||
whoisCache = make(map[string]cachedWhois)
|
||||
whoisCacheMutex sync.RWMutex
|
||||
cacheExpiry = 24 * time.Hour
|
||||
)
|
||||
|
||||
type cachedWhois struct {
|
||||
data string
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// lookupWhois performs a whois lookup for the given IP address.
|
||||
// It uses caching to avoid repeated queries for the same IP.
|
||||
func lookupWhois(ip string) (string, error) {
|
||||
// Check cache first
|
||||
whoisCacheMutex.RLock()
|
||||
if cached, ok := whoisCache[ip]; ok {
|
||||
if time.Since(cached.timestamp) < cacheExpiry {
|
||||
whoisCacheMutex.RUnlock()
|
||||
return cached.data, nil
|
||||
}
|
||||
}
|
||||
whoisCacheMutex.RUnlock()
|
||||
|
||||
// Perform whois lookup with timeout
|
||||
done := make(chan string, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
whoisData, err := whois.Whois(ip)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
done <- whoisData
|
||||
}()
|
||||
|
||||
var whoisData string
|
||||
select {
|
||||
case whoisData = <-done:
|
||||
// Success - cache will be updated below
|
||||
case err := <-errChan:
|
||||
return "", fmt.Errorf("whois lookup failed: %w", err)
|
||||
case <-time.After(10 * time.Second):
|
||||
return "", fmt.Errorf("whois lookup timeout after 10 seconds")
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
whoisCacheMutex.Lock()
|
||||
whoisCache[ip] = cachedWhois{
|
||||
data: whoisData,
|
||||
timestamp: time.Now(),
|
||||
}
|
||||
// Clean old cache entries if cache is getting large
|
||||
if len(whoisCache) > 1000 {
|
||||
now := time.Now()
|
||||
for k, v := range whoisCache {
|
||||
if now.Sub(v.timestamp) > cacheExpiry {
|
||||
delete(whoisCache, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
whoisCacheMutex.Unlock()
|
||||
|
||||
return whoisData, nil
|
||||
}
|
||||
|
||||
// extractCountryFromWhois attempts to extract country code from whois data.
|
||||
// This is a fallback if GeoIP lookup fails.
|
||||
func extractCountryFromWhois(whoisData string) string {
|
||||
lines := strings.Split(whoisData, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
lineLower := strings.ToLower(line)
|
||||
|
||||
// Look for country field
|
||||
if strings.HasPrefix(lineLower, "country:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
country := strings.TrimSpace(parts[1])
|
||||
if len(country) == 2 {
|
||||
return strings.ToUpper(country)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Alternative format
|
||||
if strings.HasPrefix(lineLower, "country code:") {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
country := strings.TrimSpace(parts[1])
|
||||
if len(country) == 2 {
|
||||
return strings.ToUpper(country)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user