Cleanup the fal2ban restart trigger functions, and improove error handling

This commit is contained in:
2025-12-17 19:16:20 +01:00
parent d44f827845
commit b9d8f1b39a
12 changed files with 208 additions and 39 deletions

View File

@@ -80,8 +80,10 @@ func ReloadFail2ban() error {
return conn.Reload(context.Background()) return conn.Reload(context.Background())
} }
// RestartFail2ban restarts the Fail2ban service using the provided server or default connector. // RestartFail2ban restarts (or reloads) the Fail2ban service using the
func RestartFail2ban(serverID string) error { // provided server or default connector and returns a mode string describing
// what actually happened ("restart" or "reload").
func RestartFail2ban(serverID string) (string, error) {
manager := GetManager() manager := GetManager()
var ( var (
conn Connector conn Connector
@@ -93,7 +95,17 @@ func RestartFail2ban(serverID string) error {
conn, err = manager.DefaultConnector() conn, err = manager.DefaultConnector()
} }
if err != nil { if err != nil {
return err return "", err
} }
return conn.Restart(context.Background()) // If the connector supports a detailed restart mode, use it. Otherwise
// fall back to a plain Restart() and assume "restart".
if withMode, ok := conn.(interface {
RestartWithMode(ctx context.Context) (string, error)
}); ok {
return withMode.RestartWithMode(context.Background())
}
if err := conn.Restart(context.Background()); err != nil {
return "", err
}
return "restart", nil
} }

View File

@@ -109,6 +109,15 @@ func (ac *AgentConnector) Restart(ctx context.Context) error {
return ac.post(ctx, "/v1/actions/restart", nil, nil) return ac.post(ctx, "/v1/actions/restart", nil, nil)
} }
// RestartWithMode restarts the remote agent-managed Fail2ban service and
// always reports mode "restart". Any error is propagated to the caller.
func (ac *AgentConnector) RestartWithMode(ctx context.Context) (string, error) {
if err := ac.Restart(ctx); err != nil {
return "restart", err
}
return "restart", nil
}
func (ac *AgentConnector) GetFilterConfig(ctx context.Context, jail string) (string, error) { func (ac *AgentConnector) GetFilterConfig(ctx context.Context, jail string) (string, error) {
var resp struct { var resp struct {
Config string `json:"config"` Config string `json:"config"`

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"sort" "sort"
"strings" "strings"
@@ -163,17 +162,40 @@ func (lc *LocalConnector) Reload(ctx context.Context) error {
return nil return nil
} }
// RestartWithMode restarts (or reloads) the local Fail2ban instance and returns
// a mode string describing what happened:
// - "restart": systemd service was restarted and health check passed
// - "reload": configuration was reloaded via fail2ban-client and pong check passed
func (lc *LocalConnector) RestartWithMode(ctx context.Context) (string, error) {
// 1) Try systemd restart if systemctl is available.
if _, err := exec.LookPath("systemctl"); err == nil {
cmd := "systemctl restart fail2ban"
out, err := executeShellCommand(ctx, cmd)
if err != nil {
return "restart", fmt.Errorf("failed to restart fail2ban via systemd: %w - output: %s",
err, strings.TrimSpace(out))
}
if err := lc.checkFail2banHealthy(ctx); err != nil {
return "restart", fmt.Errorf("fail2ban health check after systemd restart failed: %w", err)
}
return "restart", nil
}
// 2) Fallback: no systemctl in PATH (container image without systemd, or
// non-systemd environment). Use fail2ban-client reload + ping.
if err := lc.Reload(ctx); err != nil {
return "reload", fmt.Errorf("failed to reload fail2ban via fail2ban-client (systemctl not available): %w", err)
}
if err := lc.checkFail2banHealthy(ctx); err != nil {
return "reload", fmt.Errorf("fail2ban health check after reload failed: %w", err)
}
return "reload", nil
}
// Restart implements Connector. // Restart implements Connector.
func (lc *LocalConnector) Restart(ctx context.Context) error { func (lc *LocalConnector) Restart(ctx context.Context) error {
if _, container := os.LookupEnv("CONTAINER"); container { _, err := lc.RestartWithMode(ctx)
return fmt.Errorf("restart not supported inside container; please restart fail2ban on the host") return err
}
cmd := "systemctl restart fail2ban"
out, err := executeShellCommand(ctx, cmd)
if err != nil {
return fmt.Errorf("failed to restart fail2ban: %w - output: %s", err, out)
}
return nil
} }
// GetFilterConfig implements Connector. // GetFilterConfig implements Connector.
@@ -247,6 +269,22 @@ func (lc *LocalConnector) buildFail2banArgs(args ...string) []string {
return append(base, args...) return append(base, args...)
} }
// checkFail2banHealthy runs a quick `fail2ban-client ping` via the existing
// runFail2banClient helper and expects a successful pong reply.
func (lc *LocalConnector) checkFail2banHealthy(ctx context.Context) error {
out, err := lc.runFail2banClient(ctx, "ping")
trimmed := strings.TrimSpace(out)
if err != nil {
return fmt.Errorf("fail2ban ping error: %w (output: %s)", err, trimmed)
}
// Typical output is e.g. "Server replied: pong" accept anything that
// contains "pong" case-insensitively.
if !strings.Contains(strings.ToLower(trimmed), "pong") {
return fmt.Errorf("unexpected fail2ban ping output: %s", trimmed)
}
return nil
}
// GetAllJails implements Connector. // GetAllJails implements Connector.
func (lc *LocalConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) { func (lc *LocalConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
return GetAllJails() return GetAllJails()

View File

@@ -174,11 +174,43 @@ func (sc *SSHConnector) Reload(ctx context.Context) error {
return err return err
} }
// RestartWithMode restarts (or reloads) the remote Fail2ban instance over SSH
// and returns a mode string describing what happened:
// - "restart": systemd service was restarted and health check passed
// - "reload": configuration was reloaded via fail2ban-client and pong check passed
func (sc *SSHConnector) Restart(ctx context.Context) error { func (sc *SSHConnector) Restart(ctx context.Context) error {
_, err := sc.runRemoteCommand(ctx, []string{"sudo", "systemctl", "restart", "fail2ban"}) _, err := sc.RestartWithMode(ctx)
return err return err
} }
// RestartWithMode implements the detailed restart logic for SSH connectors.
func (sc *SSHConnector) RestartWithMode(ctx context.Context) (string, error) {
// First, we try systemd restart on the remote host
out, err := sc.runRemoteCommand(ctx, []string{"sudo", "systemctl", "restart", "fail2ban"})
if err == nil {
if err := sc.checkFail2banHealthyRemote(ctx); err != nil {
return "restart", fmt.Errorf("remote fail2ban health check after systemd restart failed: %w", err)
}
return "restart", nil
}
// Then, if systemd is not available, we fall back to fail2ban-client.
if sc.isSystemctlUnavailable(out, err) {
reloadOut, reloadErr := sc.runFail2banCommand(ctx, "reload")
if reloadErr != nil {
return "reload", fmt.Errorf("failed to reload fail2ban via fail2ban-client on remote: %w (output: %s)",
reloadErr, strings.TrimSpace(reloadOut))
}
if err := sc.checkFail2banHealthyRemote(ctx); err != nil {
return "reload", fmt.Errorf("remote fail2ban health check after reload failed: %w", err)
}
return "reload", nil
}
// systemctl exists but restart failed for some other reason, we surface it.
return "restart", fmt.Errorf("failed to restart fail2ban via systemd on remote: %w (output: %s)", err, out)
}
func (sc *SSHConnector) GetFilterConfig(ctx context.Context, jail string) (string, error) { func (sc *SSHConnector) GetFilterConfig(ctx context.Context, jail string) (string, error) {
// Validate filter name // Validate filter name
jail = strings.TrimSpace(jail) jail = strings.TrimSpace(jail)
@@ -308,6 +340,30 @@ func (sc *SSHConnector) runFail2banCommand(ctx context.Context, args ...string)
return sc.runRemoteCommand(ctx, cmdArgs) return sc.runRemoteCommand(ctx, cmdArgs)
} }
// isSystemctlUnavailable tries to detect “no systemd” situations on the remote host.
func (sc *SSHConnector) isSystemctlUnavailable(output string, err error) bool {
msg := strings.ToLower(output + " " + err.Error())
return strings.Contains(msg, "command not found") ||
strings.Contains(msg, "system has not been booted with systemd") ||
strings.Contains(msg, "failed to connect to bus")
}
// checkFail2banHealthyRemote runs `sudo fail2ban-client ping` on the remote host
// and expects a successful pong reply.
func (sc *SSHConnector) checkFail2banHealthyRemote(ctx context.Context) error {
out, err := sc.runFail2banCommand(ctx, "ping")
trimmed := strings.TrimSpace(out)
if err != nil {
return fmt.Errorf("remote fail2ban ping error: %w (output: %s)", err, trimmed)
}
// Typical output is e.g. "Server replied: pong" accept anything that
// contains "pong" case-insensitively.
if !strings.Contains(strings.ToLower(trimmed), "pong") {
return fmt.Errorf("unexpected remote fail2ban ping output: %s", trimmed)
}
return nil
}
func (sc *SSHConnector) buildFail2banArgs(args ...string) []string { func (sc *SSHConnector) buildFail2banArgs(args ...string) []string {
if sc.server.SocketPath == "" { if sc.server.SocketPath == "" {
return args return args
@@ -1147,12 +1203,13 @@ action = %(action_mwlg)s
// Escape single quotes for safe use in a single-quoted heredoc // Escape single quotes for safe use in a single-quoted heredoc
escaped := strings.ReplaceAll(content, "'", "'\"'\"'") escaped := strings.ReplaceAll(content, "'", "'\"'\"'")
// IMPORTANT: Run migration FIRST before ensuring structure // IMPORTANT: Run migration FIRST before ensuring structure.
// This is because EnsureJailLocalStructure may overwrite jail.local, // This is because EnsureJailLocalStructure may overwrite jail.local,
// which would destroy any jail sections that need to be migrated. // which would destroy any jail sections that need to be migrated.
// If migration fails for any reason, we SHOULD NOT overwrite jail.local,
// otherwise legacy jails would be lost.
if err := sc.MigrateJailsFromJailLocalRemote(ctx); err != nil { if err := sc.MigrateJailsFromJailLocalRemote(ctx); err != nil {
config.DebugLog("Warning: No migration done (may be normal if no jails to migrate): %v", err) return fmt.Errorf("failed to migrate legacy jails from jail.local on remote server %s: %w", sc.server.Name, err)
// Don't fail - continue with ensuring structure
} }
// Write the rebuilt content via heredoc over SSH // Write the rebuilt content via heredoc over SSH
@@ -1232,8 +1289,8 @@ func (sc *SSHConnector) MigrateJailsFromJailLocalRemote(ctx context.Context) err
writeScript := fmt.Sprintf(`cat > %s <<'JAILEOF' writeScript := fmt.Sprintf(`cat > %s <<'JAILEOF'
%s %s
JAILEOF JAILEOF
`, jailFilePath, escapedContent) '`, jailFilePath, escapedContent)
if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-c", writeScript}); err != nil { if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", writeScript}); err != nil {
return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err) return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err)
} }
config.DebugLog("Migrated jail %s to %s on remote system", jailName, jailFilePath) config.DebugLog("Migrated jail %s to %s on remote system", jailName, jailFilePath)
@@ -1248,8 +1305,8 @@ JAILEOF
writeLocalScript := fmt.Sprintf(`cat > %s <<'LOCALEOF' writeLocalScript := fmt.Sprintf(`cat > %s <<'LOCALEOF'
%s %s
LOCALEOF LOCALEOF
`, jailLocalPath, escapedDefault) '`, jailLocalPath, escapedDefault)
if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-c", writeLocalScript}); err != nil { if _, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", writeLocalScript}); err != nil {
return fmt.Errorf("failed to rewrite jail.local: %w", err) return fmt.Errorf("failed to rewrite jail.local: %w", err)
} }
config.DebugLog("Migration completed on remote system: moved %d jails to jail.d/", migratedCount) config.DebugLog("Migration completed on remote system: moved %d jails to jail.d/", migratedCount)

View File

@@ -5,6 +5,8 @@
"nav.settings": "Einstellungen", "nav.settings": "Einstellungen",
"restart_banner.message": "Fail2ban Konfiguration geändert. Um Änderungen zu übernehmen bitte ", "restart_banner.message": "Fail2ban Konfiguration geändert. Um Änderungen zu übernehmen bitte ",
"restart_banner.button": "Service neu starten", "restart_banner.button": "Service neu starten",
"restart_banner.restart_success": "Fail2ban-Dienst wurde neu gestartet und der Healthcheck war erfolgreich",
"restart_banner.reload_success": "Fail2ban-Konfiguration wurde erfolgreich neu geladen (kein Systemd neustart)",
"dashboard.title": "Dashboard", "dashboard.title": "Dashboard",
"dashboard.overview": "Aktive Jails und Blocks Übersicht", "dashboard.overview": "Aktive Jails und Blocks Übersicht",
"dashboard.overview_hint": "Verwende die Suche, um gesperrte IPs zu filtern, und klicke auf ein Jail, um dessen Konfiguration zu bearbeiten.", "dashboard.overview_hint": "Verwende die Suche, um gesperrte IPs zu filtern, und klicke auf ein Jail, um dessen Konfiguration zu bearbeiten.",
@@ -245,6 +247,7 @@
"servers.badge.default": "Standard", "servers.badge.default": "Standard",
"servers.badge.enabled": "Aktiv", "servers.badge.enabled": "Aktiv",
"servers.badge.disabled": "Deaktiviert", "servers.badge.disabled": "Deaktiviert",
"servers.badge.restart_needed": "Neustart erforderlich",
"servers.actions.edit": "Bearbeiten", "servers.actions.edit": "Bearbeiten",
"servers.actions.set_default": "Als Standard setzen", "servers.actions.set_default": "Als Standard setzen",
"servers.actions.enable": "Aktivieren", "servers.actions.enable": "Aktivieren",

View File

@@ -5,6 +5,8 @@
"nav.settings": "Istellige", "nav.settings": "Istellige",
"restart_banner.message": "Fail2ban Konfiguration gänderet! Für z'übernehä, bitte: ", "restart_banner.message": "Fail2ban Konfiguration gänderet! Für z'übernehä, bitte: ",
"restart_banner.button": "Service neu starte", "restart_banner.button": "Service neu starte",
"restart_banner.restart_success": "Fail2ban-Dienst isch neu gestartet worde und dr Healthcheck het klappet",
"restart_banner.reload_success": "Fail2ban-Konfiguration isch erfolgreich neu gelade worde (Systemd neustart müglech)",
"dashboard.title": "Dashboard", "dashboard.title": "Dashboard",
"dashboard.overview": "Übersicht vo de aktive Jails und Blocks", "dashboard.overview": "Übersicht vo de aktive Jails und Blocks",
"dashboard.overview_hint": "Bruch d Suechi zum g'sperrti IPs filtere und klick ufneä Jail, zum Filter bearbeite.", "dashboard.overview_hint": "Bruch d Suechi zum g'sperrti IPs filtere und klick ufneä Jail, zum Filter bearbeite.",
@@ -245,6 +247,7 @@
"servers.badge.default": "Standard", "servers.badge.default": "Standard",
"servers.badge.enabled": "Aktiv", "servers.badge.enabled": "Aktiv",
"servers.badge.disabled": "Deaktiviert", "servers.badge.disabled": "Deaktiviert",
"servers.badge.restart_needed": "Brucht ä Neustart",
"servers.actions.edit": "Bearbeite", "servers.actions.edit": "Bearbeite",
"servers.actions.set_default": "Als Standard setze", "servers.actions.set_default": "Als Standard setze",
"servers.actions.enable": "Aktivierä", "servers.actions.enable": "Aktivierä",

View File

@@ -5,6 +5,8 @@
"nav.settings": "Settings", "nav.settings": "Settings",
"restart_banner.message": "Fail2ban configuration changed. To apply the changes, please ", "restart_banner.message": "Fail2ban configuration changed. To apply the changes, please ",
"restart_banner.button": "Restart Service", "restart_banner.button": "Restart Service",
"restart_banner.restart_success": "Fail2ban service restarted and passed health check",
"restart_banner.reload_success": "Fail2ban configuration reloaded successfully (no systemd service restart)",
"dashboard.title": "Dashboard", "dashboard.title": "Dashboard",
"dashboard.overview": "Overview active Jails and Blocks", "dashboard.overview": "Overview active Jails and Blocks",
"dashboard.overview_hint": "Use the search to filter banned IPs and click a jail to edit its configuration.", "dashboard.overview_hint": "Use the search to filter banned IPs and click a jail to edit its configuration.",
@@ -245,6 +247,7 @@
"servers.badge.default": "Default", "servers.badge.default": "Default",
"servers.badge.enabled": "Enabled", "servers.badge.enabled": "Enabled",
"servers.badge.disabled": "Disabled", "servers.badge.disabled": "Disabled",
"servers.badge.restart_needed": "Restart required",
"servers.actions.edit": "Edit", "servers.actions.edit": "Edit",
"servers.actions.set_default": "Set default", "servers.actions.set_default": "Set default",
"servers.actions.enable": "Enable", "servers.actions.enable": "Enable",

View File

@@ -5,6 +5,8 @@
"nav.settings": "Configuración", "nav.settings": "Configuración",
"restart_banner.message": "¡Configuración de Fail2ban modificada. Para aplicar los cambios, por favor ", "restart_banner.message": "¡Configuración de Fail2ban modificada. Para aplicar los cambios, por favor ",
"restart_banner.button": "Reiniciar servicio", "restart_banner.button": "Reiniciar servicio",
"restart_banner.restart_success": "El servicio Fail2ban se reinició correctamente y superó la comprobación de estado",
"restart_banner.reload_success": "La configuración de Fail2ban se recargó correctamente (sin reinicio del servicio Systemd)",
"dashboard.title": "Panel de control", "dashboard.title": "Panel de control",
"dashboard.overview": "Resumen de Jails y Bloqueos activos", "dashboard.overview": "Resumen de Jails y Bloqueos activos",
"dashboard.overview_hint": "Usa la búsqueda para filtrar IPs bloqueadas y haz clic en un jail para editar su configuración.", "dashboard.overview_hint": "Usa la búsqueda para filtrar IPs bloqueadas y haz clic en un jail para editar su configuración.",
@@ -245,6 +247,7 @@
"servers.badge.default": "Predeterminado", "servers.badge.default": "Predeterminado",
"servers.badge.enabled": "Habilitado", "servers.badge.enabled": "Habilitado",
"servers.badge.disabled": "Deshabilitado", "servers.badge.disabled": "Deshabilitado",
"servers.badge.restart_needed": "Reinicio requerido",
"servers.actions.edit": "Editar", "servers.actions.edit": "Editar",
"servers.actions.set_default": "Establecer predeterminado", "servers.actions.set_default": "Establecer predeterminado",
"servers.actions.enable": "Habilitar", "servers.actions.enable": "Habilitar",

View File

@@ -5,6 +5,8 @@
"nav.settings": "Paramètres", "nav.settings": "Paramètres",
"restart_banner.message": "Configuration Fail2ban modifiée. Pour appliquer les changements, veuillez ", "restart_banner.message": "Configuration Fail2ban modifiée. Pour appliquer les changements, veuillez ",
"restart_banner.button": "Redémarrer le service", "restart_banner.button": "Redémarrer le service",
"restart_banner.restart_success": "Le service Fail2ban a été redémarré et le contrôle d'état a réussi",
"restart_banner.reload_success": "La configuration Fail2ban a été rechargée avec succès (pas de redémarrage du service Systemd)",
"dashboard.title": "Tableau de bord", "dashboard.title": "Tableau de bord",
"dashboard.overview": "Vue d'ensemble des jails et blocages actifs", "dashboard.overview": "Vue d'ensemble des jails et blocages actifs",
"dashboard.overview_hint": "Utilisez la recherche pour filtrer les IP bloquées et cliquez sur un jail pour modifier sa configuration.", "dashboard.overview_hint": "Utilisez la recherche pour filtrer les IP bloquées et cliquez sur un jail pour modifier sa configuration.",
@@ -245,6 +247,7 @@
"servers.badge.default": "Par défaut", "servers.badge.default": "Par défaut",
"servers.badge.enabled": "Activé", "servers.badge.enabled": "Activé",
"servers.badge.disabled": "Désactivé", "servers.badge.disabled": "Désactivé",
"servers.badge.restart_needed": "Redémarrage requis",
"servers.actions.edit": "Modifier", "servers.actions.edit": "Modifier",
"servers.actions.set_default": "Définir par défaut", "servers.actions.set_default": "Définir par défaut",
"servers.actions.enable": "Activer", "servers.actions.enable": "Activer",

View File

@@ -5,6 +5,8 @@
"nav.settings": "Impostazioni", "nav.settings": "Impostazioni",
"restart_banner.message": "Configurazione di Fail2ban modificata. Per applicare le modifiche, per favore ", "restart_banner.message": "Configurazione di Fail2ban modificata. Per applicare le modifiche, per favore ",
"restart_banner.button": "Riavvia il servizio", "restart_banner.button": "Riavvia il servizio",
"restart_banner.restart_success": "Il servizio Fail2ban è stato riavviato e il controllo di stato è riuscito",
"restart_banner.reload_success": "La configurazione di Fail2ban è stata ricaricata correttamente (senza riavvio del servizio Systemd)",
"dashboard.title": "Cruscotto", "dashboard.title": "Cruscotto",
"dashboard.overview": "Panoramica dei jail e dei blocchi attivi", "dashboard.overview": "Panoramica dei jail e dei blocchi attivi",
"dashboard.overview_hint": "Usa la ricerca per filtrare le IP bloccate e fai clic su un jail per modificarne la configurazione.", "dashboard.overview_hint": "Usa la ricerca per filtrare le IP bloccate e fai clic su un jail per modificarne la configurazione.",
@@ -245,6 +247,7 @@
"servers.badge.default": "Predefinito", "servers.badge.default": "Predefinito",
"servers.badge.enabled": "Abilitato", "servers.badge.enabled": "Abilitato",
"servers.badge.disabled": "Disabilitato", "servers.badge.disabled": "Disabilitato",
"servers.badge.restart_needed": "Riavvio richiesto",
"servers.actions.edit": "Modifica", "servers.actions.edit": "Modifica",
"servers.actions.set_default": "Imposta predefinito", "servers.actions.set_default": "Imposta predefinito",
"servers.actions.enable": "Abilita", "servers.actions.enable": "Abilita",

View File

@@ -551,6 +551,28 @@ func UpsertServerHandler(c *gin.Context) {
// Don't fail the request, just log the warning // Don't fail the request, just log the warning
} else { } else {
config.DebugLog("Successfully ensured jail.local structure for server %s", server.Name) config.DebugLog("Successfully ensured jail.local structure for server %s", server.Name)
// If the server was just enabled, try to restart fail2ban and perform a basic health check.
if justEnabled {
if err := conn.Restart(c.Request.Context()); err != nil {
// Surface restart failures to the UI so the user sees that the service did not restart.
msg := fmt.Sprintf("failed to restart fail2ban for server %s: %v", server.Name, err)
config.DebugLog("Warning: %s", msg)
c.JSON(http.StatusInternalServerError, gin.H{
"error": msg,
"server": server,
})
return
} else {
// Basic health check: attempt to fetch jail infos, which runs fail2ban-client status.
if _, err := conn.GetJailInfos(c.Request.Context()); err != nil {
config.DebugLog("Warning: fail2ban appears unhealthy on server %s after restart: %v", server.Name, err)
// Again, we log instead of failing the request to avoid breaking existing flows.
} else {
config.DebugLog("Fail2ban service appears healthy on server %s after restart", server.Name)
}
}
}
} }
} }
} }
@@ -1905,27 +1927,28 @@ func RestartFail2banHandler(c *gin.Context) {
server := conn.Server() server := conn.Server()
// Attempt to restart the fail2ban service. // Attempt to restart the fail2ban service via the connector.
restartErr := fail2ban.RestartFail2ban(server.ID) // Any error here means the service was not restarted, so we surface it to the UI.
if restartErr != nil { mode, err := fail2ban.RestartFail2ban(server.ID)
// Check if running inside a container. if err != nil {
if _, container := os.LookupEnv("CONTAINER"); container && server.Type == "local" { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
// In a container, the restart command may fail (since fail2ban runs on the host). return
// Log the error and continue, so we can mark the restart as done.
log.Printf("Warning: restart failed inside container (expected behavior): %v", restartErr)
} else {
// On the host, a restart error is not acceptable.
c.JSON(http.StatusInternalServerError, gin.H{"error": restartErr.Error()})
return
}
} }
// Only call MarkRestartDone if we either successfully restarted the service or we are in a container. // Only call MarkRestartDone if we successfully restarted the service.
if err := config.MarkRestartDone(server.ID); err != nil { if err := config.MarkRestartDone(server.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "Fail2ban restarted successfully"}) msg := "Fail2ban service restarted successfully"
if mode == "reload" {
msg = "Fail2ban configuration reloaded successfully (no systemd service restart)"
}
c.JSON(http.StatusOK, gin.H{
"message": msg,
"mode": mode,
"server": server,
})
} }
// loadLocale loads a locale JSON file and returns a map of translations // loadLocale loads a locale JSON file and returns a map of translations

View File

@@ -157,6 +157,9 @@ function renderServerManagerList() {
var defaultBadge = server.isDefault var defaultBadge = server.isDefault
? '<span class="ml-2 text-xs font-semibold text-blue-600" data-i18n="servers.badge.default">Default</span>' ? '<span class="ml-2 text-xs font-semibold text-blue-600" data-i18n="servers.badge.default">Default</span>'
: ''; : '';
var restartBadge = server.restartNeeded
? '<span class="ml-2 text-xs font-semibold text-yellow-600" data-i18n="servers.badge.restart_needed">Restart required</span>'
: '';
var descriptor = []; var descriptor = [];
if (server.type) { if (server.type) {
descriptor.push(server.type.toUpperCase()); descriptor.push(server.type.toUpperCase());
@@ -178,7 +181,7 @@ function renderServerManagerList() {
+ '<div class="border border-gray-200 rounded-lg p-4 overflow-x-auto bg-gray-50">' + '<div class="border border-gray-200 rounded-lg p-4 overflow-x-auto bg-gray-50">'
+ ' <div class="flex items-center justify-between">' + ' <div class="flex items-center justify-between">'
+ ' <div>' + ' <div>'
+ ' <p class="font-semibold text-gray-800 flex items-center">' + escapeHtml(server.name || server.id) + defaultBadge + statusBadge + '</p>' + ' <p class="font-semibold text-gray-800 flex items-center">' + escapeHtml(server.name || server.id) + defaultBadge + statusBadge + restartBadge + '</p>'
+ ' <p class="text-sm text-gray-500">' + escapeHtml(meta || server.id) + '</p>' + ' <p class="text-sm text-gray-500">' + escapeHtml(meta || server.id) + '</p>'
+ tags + tags
+ ' </div>' + ' </div>'
@@ -540,9 +543,18 @@ function restartFail2banServer(serverId) {
showToast("Failed to restart Fail2ban: " + data.error, 'error'); showToast("Failed to restart Fail2ban: " + data.error, 'error');
return; return;
} }
var mode = data.mode || 'restart';
var key, fallback;
if (mode === 'reload') {
key = 'restart_banner.reload_success';
fallback = 'Fail2ban configuration reloaded successfully';
} else {
key = 'restart_banner.restart_success';
fallback = 'Fail2ban service restarted and passed health check';
}
return loadServers().then(function() { return loadServers().then(function() {
updateRestartBanner(); updateRestartBanner();
showToast(t('restart_banner.success', 'Fail2ban restart triggered'), 'success'); showToast(t(key, fallback), 'success');
return refreshData({ silent: true }); return refreshData({ silent: true });
}); });
}) })