From b5e8324762908aca1991dc6277ea74c5a8c02037 Mon Sep 17 00:00:00 2001 From: Michael Reber Date: Wed, 7 Jan 2026 16:14:48 +0100 Subject: [PATCH] Added manual block section and BanIP method to for all connectors, like the UnbanIP functionallity --- internal/fail2ban/connector_agent.go | 5 + internal/fail2ban/connector_local.go | 9 ++ internal/fail2ban/connector_ssh.go | 5 + internal/fail2ban/manager.go | 1 + internal/locales/de.json | 14 +++ internal/locales/de_ch.json | 14 +++ internal/locales/en.json | 14 +++ internal/locales/es.json | 14 +++ internal/locales/fr.json | 14 +++ internal/locales/it.json | 14 +++ pkg/web/handlers.go | 23 +++++ pkg/web/routes.go | 1 + pkg/web/static/js/dashboard.js | 137 +++++++++++++++++++++++++++ 13 files changed, 265 insertions(+) diff --git a/internal/fail2ban/connector_agent.go b/internal/fail2ban/connector_agent.go index 5ca1b0c..d8568b7 100644 --- a/internal/fail2ban/connector_agent.go +++ b/internal/fail2ban/connector_agent.go @@ -101,6 +101,11 @@ func (ac *AgentConnector) UnbanIP(ctx context.Context, jail, ip string) error { return ac.post(ctx, fmt.Sprintf("/v1/jails/%s/unban", url.PathEscape(jail)), payload, nil) } +func (ac *AgentConnector) BanIP(ctx context.Context, jail, ip string) error { + payload := map[string]string{"ip": ip} + return ac.post(ctx, fmt.Sprintf("/v1/jails/%s/ban", url.PathEscape(jail)), payload, nil) +} + func (ac *AgentConnector) Reload(ctx context.Context) error { return ac.post(ctx, "/v1/actions/reload", nil, nil) } diff --git a/internal/fail2ban/connector_local.go b/internal/fail2ban/connector_local.go index 8a042fa..3790e3e 100644 --- a/internal/fail2ban/connector_local.go +++ b/internal/fail2ban/connector_local.go @@ -140,6 +140,15 @@ func (lc *LocalConnector) UnbanIP(ctx context.Context, jail, ip string) error { return nil } +// BanIP implements Connector. +func (lc *LocalConnector) BanIP(ctx context.Context, jail, ip string) error { + args := []string{"set", jail, "banip", ip} + if _, err := lc.runFail2banClient(ctx, args...); err != nil { + return fmt.Errorf("error banning IP %s in jail %s: %w", ip, jail, err) + } + return nil +} + // Reload implements Connector. func (lc *LocalConnector) Reload(ctx context.Context) error { out, err := lc.runFail2banClient(ctx, "reload") diff --git a/internal/fail2ban/connector_ssh.go b/internal/fail2ban/connector_ssh.go index fdd9690..7808dfe 100644 --- a/internal/fail2ban/connector_ssh.go +++ b/internal/fail2ban/connector_ssh.go @@ -187,6 +187,11 @@ func (sc *SSHConnector) UnbanIP(ctx context.Context, jail, ip string) error { return err } +func (sc *SSHConnector) BanIP(ctx context.Context, jail, ip string) error { + _, err := sc.runFail2banCommand(ctx, "set", jail, "banip", ip) + return err +} + func (sc *SSHConnector) Reload(ctx context.Context) error { _, err := sc.runFail2banCommand(ctx, "reload") return err diff --git a/internal/fail2ban/manager.go b/internal/fail2ban/manager.go index 4acdf83..cc02f4e 100644 --- a/internal/fail2ban/manager.go +++ b/internal/fail2ban/manager.go @@ -16,6 +16,7 @@ type Connector interface { GetJailInfos(ctx context.Context) ([]JailInfo, error) GetBannedIPs(ctx context.Context, jail string) ([]string, error) UnbanIP(ctx context.Context, jail, ip string) error + BanIP(ctx context.Context, jail, ip string) error Reload(ctx context.Context) error Restart(ctx context.Context) error GetFilterConfig(ctx context.Context, jail string) (string, string, error) // Returns (config, filePath, error) diff --git a/internal/locales/de.json b/internal/locales/de.json index 5984bb6..c24f11a 100644 --- a/internal/locales/de.json +++ b/internal/locales/de.json @@ -38,6 +38,20 @@ "dashboard.table.log_line": "Logzeile", "dashboard.no_banned_ips": "Keine gesperrten IPs", "dashboard.unban": "Entsperren", + "dashboard.manual_block.title": "Manuelle IP-Sperre", + "dashboard.manual_block.subtitle": "Manuell eine IP-Adresse in einem bestimmten Jail sperren.", + "dashboard.manual_block.expand_hint": "Klicken Sie, um zu erweitern und eine IP-Adresse zu sperren", + "dashboard.manual_block.jail_label": "Jail auswählen", + "dashboard.manual_block.jail_placeholder": "Jail auswählen...", + "dashboard.manual_block.ip_label": "IP-Adresse", + "dashboard.manual_block.ip_placeholder": "z.B. 88.76.21.123", + "dashboard.manual_block.button": "IP sperren", + "dashboard.manual_block.confirm": "IP {ip} im Jail {jail} sperren?", + "dashboard.manual_block.success": "IP erfolgreich gesperrt", + "dashboard.manual_block.error": "Fehler beim Sperren der IP", + "dashboard.manual_block.jail_required": "Bitte wählen Sie ein Jail aus", + "dashboard.manual_block.ip_required": "Bitte geben Sie eine IP-Adresse ein", + "dashboard.manual_block.invalid_ip": "Bitte geben Sie eine gültige IP-Adresse ein", "dashboard.banned.show_more": "Mehr anzeigen", "dashboard.banned.show_less": "Weniger anzeigen", "logs.overview.title": "Interne Log-Übersicht", diff --git a/internal/locales/de_ch.json b/internal/locales/de_ch.json index f1a0c42..7053ca8 100644 --- a/internal/locales/de_ch.json +++ b/internal/locales/de_ch.json @@ -38,6 +38,20 @@ "dashboard.table.log_line": "Log-Zile", "dashboard.no_banned_ips": "Ke g'sperrti IPs", "dashboard.unban": "Entsperre", + "dashboard.manual_block.title": "Manuelli IP-Sperri", + "dashboard.manual_block.subtitle": "Manuell e IP-Adrässe inme bestimmte Jail sperre.", + "dashboard.manual_block.expand_hint": "Klick zum Ufklappe und e IP-Adrässe z sperre", + "dashboard.manual_block.jail_label": "Jail uswähle", + "dashboard.manual_block.jail_placeholder": "Jail uswähle...", + "dashboard.manual_block.ip_label": "IP-Adrässe", + "dashboard.manual_block.ip_placeholder": "z.B. 88.76.21.123", + "dashboard.manual_block.button": "IP sperre", + "dashboard.manual_block.confirm": "IP {ip} im Jail {jail} sperre?", + "dashboard.manual_block.success": "IP erfolgriich g'sperrt", + "dashboard.manual_block.error": "Fehler bim Sperre vo dr IP", + "dashboard.manual_block.jail_required": "Bitte wähl es Jail us", + "dashboard.manual_block.ip_required": "Bitte gib e IP-Adrässe ii", + "dashboard.manual_block.invalid_ip": "Bitte gib e gültigi IP-Adrässe ii", "dashboard.banned.show_more": "Meh azeige", "dashboard.banned.show_less": "Weniger azeige", "logs.overview.title": "Generelli Log-Übersicht", diff --git a/internal/locales/en.json b/internal/locales/en.json index d39b1ae..7c2ab99 100644 --- a/internal/locales/en.json +++ b/internal/locales/en.json @@ -38,6 +38,20 @@ "dashboard.table.log_line": "Log Line", "dashboard.no_banned_ips": "No banned IPs", "dashboard.unban": "Unban", + "dashboard.manual_block.title": "Manual Block IP", + "dashboard.manual_block.subtitle": "Manually block an IP address in a specific jail.", + "dashboard.manual_block.expand_hint": "Click to expand and block an IP address", + "dashboard.manual_block.jail_label": "Select Jail", + "dashboard.manual_block.jail_placeholder": "Choose a jail...", + "dashboard.manual_block.ip_label": "IP Address", + "dashboard.manual_block.ip_placeholder": "e.g., 88.76.21.123", + "dashboard.manual_block.button": "Block IP", + "dashboard.manual_block.confirm": "Block IP {ip} in jail {jail}?", + "dashboard.manual_block.success": "IP blocked successfully", + "dashboard.manual_block.error": "Error blocking IP", + "dashboard.manual_block.jail_required": "Please select a jail", + "dashboard.manual_block.ip_required": "Please enter an IP address", + "dashboard.manual_block.invalid_ip": "Please enter a valid IP address", "dashboard.banned.show_more": "Show more", "dashboard.banned.show_less": "Hide extra", "logs.overview.title": "Internal Log Overview", diff --git a/internal/locales/es.json b/internal/locales/es.json index 16bd448..6109362 100644 --- a/internal/locales/es.json +++ b/internal/locales/es.json @@ -38,6 +38,20 @@ "dashboard.table.log_line": "Línea de log", "dashboard.no_banned_ips": "No hay IP bloqueadas", "dashboard.unban": "Desbloquear", + "dashboard.manual_block.title": "Bloqueo manual de IP", + "dashboard.manual_block.subtitle": "Bloquear manualmente una dirección IP en una cárcel específica.", + "dashboard.manual_block.expand_hint": "Haga clic para expandir y bloquear una dirección IP", + "dashboard.manual_block.jail_label": "Seleccionar cárcel", + "dashboard.manual_block.jail_placeholder": "Elegir una cárcel...", + "dashboard.manual_block.ip_label": "Dirección IP", + "dashboard.manual_block.ip_placeholder": "ej. 88.76.21.123", + "dashboard.manual_block.button": "Bloquear IP", + "dashboard.manual_block.confirm": "¿Bloquear IP {ip} en la cárcel {jail}?", + "dashboard.manual_block.success": "IP bloqueada exitosamente", + "dashboard.manual_block.error": "Error al bloquear la IP", + "dashboard.manual_block.jail_required": "Por favor seleccione una cárcel", + "dashboard.manual_block.ip_required": "Por favor ingrese una dirección IP", + "dashboard.manual_block.invalid_ip": "Por favor ingrese una dirección IP válida", "dashboard.banned.show_more": "Mostrar más", "dashboard.banned.show_less": "Mostrar menos", "logs.overview.title": "Resumen interno de registros", diff --git a/internal/locales/fr.json b/internal/locales/fr.json index 69bcd90..5ce9b12 100644 --- a/internal/locales/fr.json +++ b/internal/locales/fr.json @@ -38,6 +38,20 @@ "dashboard.table.log_line": "Ligne de log", "dashboard.no_banned_ips": "Aucune IP bloquée", "dashboard.unban": "Débloquer", + "dashboard.manual_block.title": "Blocage manuel d'IP", + "dashboard.manual_block.subtitle": "Bloquer manuellement une adresse IP dans une prison spécifique.", + "dashboard.manual_block.expand_hint": "Cliquez pour développer et bloquer une adresse IP", + "dashboard.manual_block.jail_label": "Sélectionner une prison", + "dashboard.manual_block.jail_placeholder": "Choisir une prison...", + "dashboard.manual_block.ip_label": "Adresse IP", + "dashboard.manual_block.ip_placeholder": "ex. 88.76.21.123", + "dashboard.manual_block.button": "Bloquer l'IP", + "dashboard.manual_block.confirm": "Bloquer l'IP {ip} dans la prison {jail}?", + "dashboard.manual_block.success": "IP bloquée avec succès", + "dashboard.manual_block.error": "Erreur lors du blocage de l'IP", + "dashboard.manual_block.jail_required": "Veuillez sélectionner une prison", + "dashboard.manual_block.ip_required": "Veuillez entrer une adresse IP", + "dashboard.manual_block.invalid_ip": "Veuillez entrer une adresse IP valide", "dashboard.banned.show_more": "Afficher plus", "dashboard.banned.show_less": "Afficher moins", "logs.overview.title": "Vue d'ensemble interne des journaux", diff --git a/internal/locales/it.json b/internal/locales/it.json index c733bc4..7c533aa 100644 --- a/internal/locales/it.json +++ b/internal/locales/it.json @@ -38,6 +38,20 @@ "dashboard.table.log_line": "Riga di log", "dashboard.no_banned_ips": "Nessuna IP bloccata", "dashboard.unban": "Sblocca", + "dashboard.manual_block.title": "Blocco manuale IP", + "dashboard.manual_block.subtitle": "Bloccare manualmente un indirizzo IP in una prigione specifica.", + "dashboard.manual_block.expand_hint": "Fare clic per espandere e bloccare un indirizzo IP", + "dashboard.manual_block.jail_label": "Seleziona prigione", + "dashboard.manual_block.jail_placeholder": "Scegli una prigione...", + "dashboard.manual_block.ip_label": "Indirizzo IP", + "dashboard.manual_block.ip_placeholder": "es. 88.76.21.123", + "dashboard.manual_block.button": "Blocca IP", + "dashboard.manual_block.confirm": "Bloccare IP {ip} nella prigione {jail}?", + "dashboard.manual_block.success": "IP bloccato con successo", + "dashboard.manual_block.error": "Errore nel bloccare l'IP", + "dashboard.manual_block.jail_required": "Si prega di selezionare una prigione", + "dashboard.manual_block.ip_required": "Si prega di inserire un indirizzo IP", + "dashboard.manual_block.invalid_ip": "Si prega di inserire un indirizzo IP valido", "dashboard.banned.show_more": "Mostra di più", "dashboard.banned.show_less": "Mostra meno", "logs.overview.title": "Panoramica interna dei log", diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index 92f6cce..3c8e8b0 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -170,6 +170,29 @@ func UnbanIPHandler(c *gin.Context) { }) } +// BanIPHandler bans a given IP in a specific jail. +func BanIPHandler(c *gin.Context) { + config.DebugLog("----------------------------") + config.DebugLog("BanIPHandler called (handlers.go)") // entry point + jail := c.Param("jail") + ip := c.Param("ip") + + conn, err := resolveConnector(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := conn.BanIP(c.Request.Context(), jail, ip); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + fmt.Println(ip + " in jail " + jail + " banned successfully.") + c.JSON(http.StatusOK, gin.H{ + "message": "IP banned successfully", + }) +} + // BanNotificationHandler processes incoming ban notifications from Fail2Ban. func BanNotificationHandler(c *gin.Context) { // Validate callback secret diff --git a/pkg/web/routes.go b/pkg/web/routes.go index 2f5d1af..29fd876 100644 --- a/pkg/web/routes.go +++ b/pkg/web/routes.go @@ -32,6 +32,7 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) { { api.GET("/summary", SummaryHandler) api.POST("/jails/:jail/unban/:ip", UnbanIPHandler) + api.POST("/jails/:jail/ban/:ip", BanIPHandler) // Routes for jail-filter management (TODO: rename API-call) api.GET("/jails/:jail/config", GetJailFilterConfigHandler) diff --git a/pkg/web/static/js/dashboard.js b/pkg/web/static/js/dashboard.js index 785dcdc..71d7097 100644 --- a/pkg/web/static/js/dashboard.js +++ b/pkg/web/static/js/dashboard.js @@ -441,6 +441,24 @@ function toggleBannedList(hiddenId, buttonId) { } } +function toggleManualBlockSection() { + var container = document.getElementById('manualBlockFormContainer'); + var icon = document.getElementById('manualBlockToggleIcon'); + if (!container || !icon) { + return; + } + var isHidden = container.classList.contains("hidden"); + if (isHidden) { + container.classList.remove("hidden"); + icon.classList.remove("fa-chevron-down"); + icon.classList.add("fa-chevron-up"); + } else { + container.classList.add("hidden"); + icon.classList.remove("fa-chevron-up"); + icon.classList.add("fa-chevron-down"); + } +} + function unbanIP(jail, ip) { const confirmMsg = isLOTRModeActive ? 'Restore ' + ip + ' to the realm from ' + jail + '?' @@ -470,6 +488,75 @@ function unbanIP(jail, ip) { }); } +function banIP(jail, ip) { + const confirmMsg = isLOTRModeActive + ? 'Banish ' + ip + ' from the realm in ' + jail + '?' + : 'Block IP ' + ip + ' in jail ' + jail + '?'; + if (!confirm(confirmMsg)) { + return; + } + showLoading(true); + var url = '/api/jails/' + encodeURIComponent(jail) + '/ban/' + encodeURIComponent(ip); + fetch(withServerParam(url), { + method: 'POST', + headers: serverHeaders() + }) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (data.error) { + showToast("Error blocking IP: " + data.error, 'error'); + } else { + showToast(t('dashboard.manual_block.success', 'IP blocked successfully'), 'success'); + return refreshData({ silent: true }); + } + }) + .catch(function(err) { + showToast("Error: " + err, 'error'); + }) + .finally(function() { + showLoading(false); + }); +} + +function handleManualBlock() { + var jailSelect = document.getElementById('blockJailSelect'); + var ipInput = document.getElementById('blockIPInput'); + + if (!jailSelect || !ipInput) { + return; + } + + var jail = jailSelect.value; + var ip = ipInput.value.trim(); + + if (!jail) { + showToast(t('dashboard.manual_block.jail_required', 'Please select a jail'), 'error'); + jailSelect.focus(); + return; + } + + if (!ip) { + showToast(t('dashboard.manual_block.ip_required', 'Please enter an IP address'), 'error'); + ipInput.focus(); + return; + } + + // Basic IP validation + var ipv4Pattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/; + var ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; + if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) { + showToast(t('dashboard.manual_block.invalid_ip', 'Please enter a valid IP address'), 'error'); + ipInput.focus(); + return; + } + + banIP(jail, ip); + + // Clear form after submission + ipInput.value = ''; + jailSelect.value = ''; +} + function renderDashboard() { var container = document.getElementById('dashboard'); if (!container) return; @@ -590,6 +677,56 @@ function renderDashboard() { html += ''; // close overview card } + // Manual Block IP Section + if (summary && summary.jails && summary.jails.length > 0) { + var enabledJails = summary.jails.filter(function(j) { return j.enabled !== false; }); + if (enabledJails.length > 0) { + html += '' + + '
' + + '
' + + '
' + + '
' + + '

Manual Block IP

' + + '

Manually block an IP address in a specific jail.

' + + '

Click to expand and block an IP address

' + + '
' + + '
' + + ' ' + + '
' + + '
' + + '
' + + ' ' + + '
'; + } + } + html += '
' + renderLogOverviewContent() + '
'; container.innerHTML = html;