Add default chain and default BantimeRndtime settings and make configurable through the settings page

This commit is contained in:
2026-02-08 19:43:34 +01:00
parent 5f14da5934
commit 1a2df7b252
15 changed files with 330 additions and 44 deletions

5
.gitignore vendored
View File

@@ -33,4 +33,7 @@ fail2ban-ui.db*
# Node.js / Tailwind CSS build
node_modules/
package-lock.json
.tailwind-build/
.tailwind-build/
# Server specific test-files
server

View File

@@ -74,6 +74,8 @@ type AppSettings struct {
Destemail string `json:"destemail"`
Banaction string `json:"banaction"` // Default banning action
BanactionAllports string `json:"banactionAllports"` // Allports banning action
Chain string `json:"chain"` // Default iptables/nftables chain (INPUT, DOCKER-USER, FORWARD)
BantimeRndtime string `json:"bantimeRndtime"` // Optional: bantime.rndtime in seconds (e.g. 2048) for bantime increment formula
//Sender string `json:"sender"`
// GeoIP and Whois settings
@@ -414,6 +416,12 @@ func applyAppSettingsRecordLocked(rec storage.AppSettingsRecord) {
currentSettings.Destemail = rec.DestEmail
currentSettings.Banaction = rec.Banaction
currentSettings.BanactionAllports = rec.BanactionAllports
if rec.Chain != "" {
currentSettings.Chain = rec.Chain
} else {
currentSettings.Chain = "INPUT"
}
currentSettings.BantimeRndtime = rec.BantimeRndtime
currentSettings.SMTP = SMTPSettings{
Host: rec.SMTPHost,
Port: rec.SMTPPort,
@@ -527,6 +535,8 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) {
DestEmail: currentSettings.Destemail,
Banaction: currentSettings.Banaction,
BanactionAllports: currentSettings.BanactionAllports,
Chain: currentSettings.Chain,
BantimeRndtime: currentSettings.BantimeRndtime,
// Advanced features
AdvancedActionsJSON: string(advancedBytes),
GeoIPProvider: currentSettings.GeoIPProvider,
@@ -672,6 +682,9 @@ func setDefaultsLocked() {
if currentSettings.BanactionAllports == "" {
currentSettings.BanactionAllports = "nftables-allports"
}
if currentSettings.Chain == "" {
currentSettings.Chain = "INPUT"
}
if currentSettings.GeoIPProvider == "" {
currentSettings.GeoIPProvider = "builtin"
}
@@ -739,6 +752,12 @@ func initializeFromJailFile() error {
if val, ok := settings["banaction_allports"]; ok {
currentSettings.BanactionAllports = val
}
if val, ok := settings["chain"]; ok && val != "" {
currentSettings.Chain = val
}
if val, ok := settings["bantime.rndtime"]; ok && val != "" {
currentSettings.BantimeRndtime = val
}
/*if val, ok := settings["destemail"]; ok {
currentSettings.Destemail = val
}*/
@@ -911,6 +930,10 @@ func ensureJailLocalStructure() error {
if banactionAllports == "" {
banactionAllports = "nftables-allports"
}
chain := settings.Chain
if chain == "" {
chain = "INPUT"
}
defaultSection := fmt.Sprintf(`[DEFAULT]
enabled = %t
bantime.increment = %t
@@ -920,8 +943,13 @@ findtime = %s
maxretry = %d
banaction = %s
banaction_allports = %s
chain = %s
`, settings.DefaultJailEnable, settings.BantimeIncrement, ignoreIPStr, settings.Bantime, settings.Findtime, settings.Maxretry, banaction, banactionAllports)
`, settings.DefaultJailEnable, settings.BantimeIncrement, ignoreIPStr, settings.Bantime, settings.Findtime, settings.Maxretry, banaction, banactionAllports, chain)
if settings.BantimeRndtime != "" {
defaultSection += fmt.Sprintf("bantime.rndtime = %s\n", settings.BantimeRndtime)
}
defaultSection += "\n"
// Build action_mwlg configuration
// Note: action_mwlg depends on action_ which depends on banaction (now defined above)
@@ -979,6 +1007,10 @@ func updateJailLocalDefaultSection(settings AppSettings) error {
if banactionAllports == "" {
banactionAllports = "nftables-allports"
}
chain := settings.Chain
if chain == "" {
chain = "INPUT"
}
// Keys to update
keysToUpdate := map[string]string{
"enabled": fmt.Sprintf("enabled = %t", settings.DefaultJailEnable),
@@ -989,6 +1021,14 @@ func updateJailLocalDefaultSection(settings AppSettings) error {
"maxretry": fmt.Sprintf("maxretry = %d", settings.Maxretry),
"banaction": fmt.Sprintf("banaction = %s", banaction),
"banaction_allports": fmt.Sprintf("banaction_allports = %s", banactionAllports),
"chain": fmt.Sprintf("chain = %s", chain),
}
if settings.BantimeRndtime != "" {
keysToUpdate["bantime.rndtime"] = fmt.Sprintf("bantime.rndtime = %s", settings.BantimeRndtime)
}
defaultKeysOrder := []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports", "chain"}
if settings.BantimeRndtime != "" {
defaultKeysOrder = append(defaultKeysOrder, "bantime.rndtime")
}
keysUpdated := make(map[string]bool)
@@ -1021,14 +1061,24 @@ func updateJailLocalDefaultSection(settings AppSettings) error {
} else if inDefault {
// Check if this line is a key we need to update
keyUpdated := false
for key, newValue := range keysToUpdate {
keyPattern := "^\\s*" + regexp.QuoteMeta(key) + "\\s*="
if matched, _ := regexp.MatchString(keyPattern, trimmed); matched {
outputLines = append(outputLines, newValue)
keysUpdated[key] = true
// When user cleared bantime.rndtime, remove the line from config instead of keeping old value
if settings.BantimeRndtime == "" {
if matched, _ := regexp.MatchString(`^\s*bantime\.rndtime\s*=`, trimmed); matched {
keyUpdated = true
defaultUpdated = true
break
// don't append: line is removed
}
}
if !keyUpdated {
for key, newValue := range keysToUpdate {
keyPattern := "^\\s*" + regexp.QuoteMeta(key) + "\\s*="
if matched, _ := regexp.MatchString(keyPattern, trimmed); matched {
outputLines = append(outputLines, newValue)
keysUpdated[key] = true
keyUpdated = true
defaultUpdated = true
break
}
}
}
if !keyUpdated {
@@ -1043,9 +1093,8 @@ func updateJailLocalDefaultSection(settings AppSettings) error {
// Add any missing keys to the DEFAULT section
if inDefault {
for key, newValue := range keysToUpdate {
if !keysUpdated[key] {
// Find the DEFAULT section and insert after it
for _, key := range defaultKeysOrder {
if newValue, ok := keysToUpdate[key]; ok && !keysUpdated[key] {
for i, outputLine := range outputLines {
if strings.TrimSpace(outputLine) == "[DEFAULT]" {
outputLines = append(outputLines[:i+1], append([]string{newValue}, outputLines[i+1:]...)...)

View File

@@ -424,6 +424,10 @@ func (ac *AgentConnector) UpdateDefaultSettings(ctx context.Context, settings co
if banactionAllports == "" {
banactionAllports = "nftables-allports"
}
chain := settings.Chain
if chain == "" {
chain = "INPUT"
}
payload := map[string]interface{}{
"bantimeIncrement": settings.BantimeIncrement,
"defaultJailEnable": settings.DefaultJailEnable,
@@ -433,6 +437,8 @@ func (ac *AgentConnector) UpdateDefaultSettings(ctx context.Context, settings co
"maxretry": settings.Maxretry,
"banaction": banaction,
"banactionAllports": banactionAllports,
"chain": chain,
"bantimeRndtime": settings.BantimeRndtime,
}
return ac.put(ctx, "/v1/jails/default-settings", payload, nil)
}

View File

@@ -1618,6 +1618,10 @@ func (sc *SSHConnector) UpdateDefaultSettings(ctx context.Context, settings conf
if banactionAllportsVal == "" {
banactionAllportsVal = "nftables-allports"
}
chainVal := settings.Chain
if chainVal == "" {
chainVal = "INPUT"
}
// Define the keys we want to update
keysToUpdate := map[string]string{
"enabled": fmt.Sprintf("enabled = %t", settings.DefaultJailEnable),
@@ -1628,13 +1632,21 @@ func (sc *SSHConnector) UpdateDefaultSettings(ctx context.Context, settings conf
"maxretry": fmt.Sprintf("maxretry = %d", settings.Maxretry),
"banaction": fmt.Sprintf("banaction = %s", banactionVal),
"banaction_allports": fmt.Sprintf("banaction_allports = %s", banactionAllportsVal),
"chain": fmt.Sprintf("chain = %s", chainVal),
}
if settings.BantimeRndtime != "" {
keysToUpdate["bantime.rndtime"] = fmt.Sprintf("bantime.rndtime = %s", settings.BantimeRndtime)
}
defaultKeysOrder := []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports", "chain"}
if settings.BantimeRndtime != "" {
defaultKeysOrder = append(defaultKeysOrder, "bantime.rndtime")
}
// Parse existing content and update only specific keys in DEFAULT section
if existingContent == "" {
// File doesn't exist, create new one with DEFAULT section
defaultLines := []string{"[DEFAULT]"}
for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"} {
for _, key := range defaultKeysOrder {
defaultLines = append(defaultLines, keysToUpdate[key])
}
defaultLines = append(defaultLines, "")
@@ -1662,6 +1674,11 @@ func (sc *SSHConnector) UpdateDefaultSettings(ctx context.Context, settings conf
bantimeIncrementPython = "True"
}
chainValEsc := escapeForShell(chainVal)
bantimeRndtimeEsc := ""
if settings.BantimeRndtime != "" {
bantimeRndtimeEsc = escapeForShell(settings.BantimeRndtime)
}
updateScript := fmt.Sprintf(`python3 <<'PY'
import re
@@ -1674,6 +1691,8 @@ bantime_increment_val = %s
bantime_val = '%s'
findtime_val = '%s'
maxretry_val = %d
chain_val = '%s'
bantime_rndtime_val = '%s'
keys_to_update = {
'enabled': 'enabled = ' + str(default_jail_enable_val).lower(),
'bantime.increment': 'bantime.increment = ' + str(bantime_increment_val).lower(),
@@ -1682,8 +1701,14 @@ keys_to_update = {
'findtime': 'findtime = ' + findtime_val,
'maxretry': 'maxretry = ' + str(maxretry_val),
'banaction': 'banaction = ' + banaction_val,
'banaction_allports': 'banaction_allports = ' + banaction_allports_val
'banaction_allports': 'banaction_allports = ' + banaction_allports_val,
'chain': 'chain = ' + chain_val
}
if bantime_rndtime_val:
keys_to_update['bantime.rndtime'] = 'bantime.rndtime = ' + bantime_rndtime_val
keys_order = ['enabled', 'bantime.increment', 'ignoreip', 'bantime', 'findtime', 'maxretry', 'banaction', 'banaction_allports', 'chain']
if bantime_rndtime_val:
keys_order.append('bantime.rndtime')
try:
with open(jail_file, 'r') as f:
@@ -1716,13 +1741,18 @@ for line in lines:
elif in_default:
# Check if this line is a key we need to update
key_updated = False
for key, new_value in keys_to_update.items():
pattern = r'^\s*' + re.escape(key) + r'\s*='
if re.match(pattern, stripped):
output_lines.append(new_value + '\n')
keys_updated.add(key)
key_updated = True
break
# When user cleared bantime.rndtime, remove the line from config instead of keeping old value
if not bantime_rndtime_val and re.match(r'^\s*bantime\.rndtime\s*=', stripped):
key_updated = True
# don't append: line is removed
if not key_updated:
for key, new_value in keys_to_update.items():
pattern = r'^\s*' + re.escape(key) + r'\s*='
if re.match(pattern, stripped):
output_lines.append(new_value + '\n')
keys_updated.add(key)
key_updated = True
break
if not key_updated:
# Keep the line as-is (might be action_mwlg or other DEFAULT settings)
output_lines.append(line)
@@ -1733,13 +1763,13 @@ for line in lines:
# If DEFAULT section wasn't found, create it at the beginning
if not default_section_found:
default_lines = ["[DEFAULT]\n"]
for key in ["enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"]:
for key in keys_order:
default_lines.append(keys_to_update[key] + "\n")
default_lines.append("\n")
output_lines = default_lines + output_lines
else:
# Add any missing keys to the DEFAULT section
for key in ["enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"]:
for key in keys_order:
if key not in keys_updated:
# Find the DEFAULT section and insert after it
for i, line in enumerate(output_lines):
@@ -1749,7 +1779,7 @@ else:
with open(jail_file, 'w') as f:
f.writelines(output_lines)
PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(banactionVal), escapeForShell(banactionAllportsVal), defaultJailEnablePython, bantimeIncrementPython, escapeForShell(settings.Bantime), escapeForShell(settings.Findtime), settings.Maxretry)
PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(banactionVal), escapeForShell(banactionAllportsVal), defaultJailEnablePython, bantimeIncrementPython, escapeForShell(settings.Bantime), escapeForShell(settings.Findtime), settings.Maxretry, chainValEsc, bantimeRndtimeEsc)
_, err = sc.runRemoteCommand(ctx, []string{updateScript})
return err
@@ -1779,6 +1809,10 @@ func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
if banactionAllportsVal == "" {
banactionAllportsVal = "nftables-allports"
}
chainVal := settings.Chain
if chainVal == "" {
chainVal = "INPUT"
}
// Build the new jail.local content in Go (mirrors local ensureJailLocalStructure)
banner := config.JailLocalBanner()
@@ -1792,6 +1826,7 @@ findtime = %s
maxretry = %d
banaction = %s
banaction_allports = %s
chain = %s
`,
settings.DefaultJailEnable,
@@ -1802,7 +1837,12 @@ banaction_allports = %s
settings.Maxretry,
banactionVal,
banactionAllportsVal,
chainVal,
)
if settings.BantimeRndtime != "" {
defaultSection += fmt.Sprintf("bantime.rndtime = %s\n", settings.BantimeRndtime)
}
defaultSection += "\n"
actionMwlgConfig := `# Custom Fail2Ban action for UI callbacks
action_mwlg = %(action_)s

View File

@@ -1236,6 +1236,10 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
if banactionAllports == "" {
banactionAllports = "nftables-allports"
}
chain := settings.Chain
if chain == "" {
chain = "INPUT"
}
// Define the keys we want to update
keysToUpdate := map[string]string{
"enabled": fmt.Sprintf("enabled = %t", settings.DefaultJailEnable),
@@ -1246,6 +1250,14 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
"maxretry": fmt.Sprintf("maxretry = %d", settings.Maxretry),
"banaction": fmt.Sprintf("banaction = %s", banaction),
"banaction_allports": fmt.Sprintf("banaction_allports = %s", banactionAllports),
"chain": fmt.Sprintf("chain = %s", chain),
}
if settings.BantimeRndtime != "" {
keysToUpdate["bantime.rndtime"] = fmt.Sprintf("bantime.rndtime = %s", settings.BantimeRndtime)
}
defaultKeysOrder := []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports", "chain"}
if settings.BantimeRndtime != "" {
defaultKeysOrder = append(defaultKeysOrder, "bantime.rndtime")
}
// Track which keys we've updated
@@ -1257,7 +1269,7 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
var newLines []string
newLines = append(newLines, strings.Split(strings.TrimRight(config.JailLocalBanner(), "\n"), "\n")...)
newLines = append(newLines, "[DEFAULT]")
for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"} {
for _, key := range defaultKeysOrder {
newLines = append(newLines, keysToUpdate[key])
}
newLines = append(newLines, "")
@@ -1307,14 +1319,23 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
} else if inDefault {
// We're in DEFAULT section - check if this line is a key we need to update
keyUpdated := false
for key, newValue := range keysToUpdate {
// Check if this line contains the key (with or without spaces around =)
keyPattern := "^\\s*" + regexp.QuoteMeta(key) + "\\s*="
if matched, _ := regexp.MatchString(keyPattern, trimmed); matched {
outputLines = append(outputLines, newValue)
keysUpdated[key] = true
// When user cleared bantime.rndtime, remove the line from config instead of keeping old value
if settings.BantimeRndtime == "" {
if matched, _ := regexp.MatchString(`^\s*bantime\.rndtime\s*=`, trimmed); matched {
keyUpdated = true
break
// don't append: line is removed
}
}
if !keyUpdated {
for key, newValue := range keysToUpdate {
// Check if this line contains the key (with or without spaces around =)
keyPattern := "^\\s*" + regexp.QuoteMeta(key) + "\\s*="
if matched, _ := regexp.MatchString(keyPattern, trimmed); matched {
outputLines = append(outputLines, newValue)
keysUpdated[key] = true
keyUpdated = true
break
}
}
}
if !keyUpdated {
@@ -1330,14 +1351,14 @@ func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
// If DEFAULT section wasn't found, create it at the beginning
if !defaultSectionFound {
defaultLines := []string{"[DEFAULT]"}
for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"} {
for _, key := range defaultKeysOrder {
defaultLines = append(defaultLines, keysToUpdate[key])
}
defaultLines = append(defaultLines, "")
outputLines = append(defaultLines, outputLines...)
} else {
// Add any missing keys to the DEFAULT section
for _, key := range []string{"enabled", "bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "banaction", "banaction_allports"} {
for _, key := range defaultKeysOrder {
if !keysUpdated[key] {
// Find the DEFAULT section and insert after it
for i, line := range outputLines {

View File

@@ -171,6 +171,18 @@
"settings.default_bantime": "Standard-Bantime",
"settings.default_bantime.description": "Die Anzahl der Sekunden, für die ein Host gesperrt wird. Zeitformat: 1m = 1 Minute, 1h = 1 Stunde, 1d = 1 Tag, 1w = 1 Woche, 1mo = 1 Monat, 1y = 1 Jahr.",
"settings.default_bantime_placeholder": "z.B. 48h",
"settings.bantime_rndtime": "Bantime Rndtime",
"settings.bantime_rndtime.description": "Optional. Maximale Zufallssekunden in der Bantime-Inkrement-Formel (z.B. 2048). Leer lassen für Fail2ban-Standard.",
"settings.bantime_rndtime_placeholder": "z.B. 2048",
"settings.default_chain": "Standard-Chain",
"settings.default_chain.description": "iptables/nftables-Chain für Bans (z.B. INPUT für Host, DOCKER-USER für Docker-Container).",
"settings.chain_help_title": "Standard-Chain",
"settings.chain_docker_user": "DOCKER-USER",
"settings.chain_help_docker_user": "Für Anwendungen in Docker. Bans gelten für alle Container auf dem Host.",
"settings.chain_input": "INPUT",
"settings.chain_help_input": "Für Anwendungen auf dem Host. Bans gelten nur für das Host-Netzwerk, nicht für Docker-Netze.",
"settings.chain_forward": "FORWARD",
"settings.chain_help_forward": "Nur bei älteren Docker-Setups, in denen DOCKER-USER nicht verfügbar ist.",
"settings.banaction": "Banaction",
"settings.banaction.description": "Standard-Sperraktion (z.B. nftables-multiport, nftables-allports, firewallcmd-rich-rules, etc). Wird verwendet, um action_* Variablen zu definieren.",
"settings.banaction_allports": "Banaction Allports",

View File

@@ -171,6 +171,18 @@
"settings.default_bantime": "Standard-Bantime",
"settings.default_bantime.description": "D Aazahl vo de Sekunde, wo ä Host gsperrt wird. Zytformat: 1m = 1 Minute, 1h = 1 Stund, 1d = 1 Tag, 1w = 1 Woche, 1mo = 1 Monet, 1y = 1 Jahr.",
"settings.default_bantime_placeholder": "z.B. 48h",
"settings.bantime_rndtime": "Bantime Rndtime",
"settings.bantime_rndtime.description": "Optionau. Maximali Zufallssekunde i dr Bantime-Inkrement-Formle (z.B. 2048). Läär la füre Fail2ban-Standard.",
"settings.bantime_rndtime_placeholder": "z.B. 2048",
"settings.default_chain": "Standard-Chain",
"settings.default_chain.description": "iptables/nftables-Chain für Bans (z.B. INPUT für Host, DOCKER-USER für Docker-Container).",
"settings.chain_help_title": "Standard-Chain",
"settings.chain_docker_user": "DOCKER-USER",
"settings.chain_help_docker_user": "Für Aawändige i Docker. Bans gäute für alli Container uf em Host.",
"settings.chain_input": "INPUT",
"settings.chain_help_input": "Für Aawändige uf em Host. Bans gäute nur für s Host-Netz, nid für Docker-Netz.",
"settings.chain_forward": "FORWARD",
"settings.chain_help_forward": "Nur bi äutere Docker-Setups, wo DOCKER-USER nid verfüegbar isch.",
"settings.banaction": "Banaction",
"settings.banaction.description": "Standard-Sperraktione (z.B. nftables-multiport, nftables-allports, firewallcmd-rich-rules, etc). Wird brucht, zum action_* Variablen z definiere.",
"settings.banaction_allports": "Banaction Allports",

View File

@@ -171,6 +171,18 @@
"settings.default_bantime": "Default Bantime",
"settings.default_bantime.description": "The number of seconds that a host is banned. Time format: 1m = 1 minutes, 1h = 1 hour, 1d = 1 day, 1w = 1 week, 1mo = 1 month, 1y = 1 year.",
"settings.default_bantime_placeholder": "e.g., 48h",
"settings.bantime_rndtime": "Bantime Rndtime",
"settings.bantime_rndtime.description": "Optional. Max random seconds added in bantime increment formula (e.g. 2048). Leave empty to use Fail2ban default.",
"settings.bantime_rndtime_placeholder": "e.g., 2048",
"settings.default_chain": "Default Chain",
"settings.default_chain.description": "iptables/nftables chain used for banning (e.g. INPUT for host, DOCKER-USER for Docker containers).",
"settings.chain_help_title": "Default chain",
"settings.chain_docker_user": "DOCKER-USER",
"settings.chain_help_docker_user": "Use for apps running in Docker. Bans apply to all containers on the host.",
"settings.chain_input": "INPUT",
"settings.chain_help_input": "Use for apps on the host. Bans apply to the host network only, not Docker networks.",
"settings.chain_forward": "FORWARD",
"settings.chain_help_forward": "Only for older Docker setups where DOCKER-USER is not available.",
"settings.banaction": "Banaction",
"settings.banaction.description": "Default banning action (e.g. nftables-multiport, nftables-allports, firewallcmd-rich-rules, etc). It is used to define action_* variables.",
"settings.banaction_allports": "Banaction Allports",

View File

@@ -171,6 +171,18 @@
"settings.default_bantime": "Bantime por defecto",
"settings.default_bantime.description": "El número de segundos que un host está bloqueado. Formato de tiempo: 1m = 1 minutos, 1h = 1 horas, 1d = 1 días, 1w = 1 semana, 1mo = 1 mes, 1y = 1 año.",
"settings.default_bantime_placeholder": "p.ej., 48h",
"settings.bantime_rndtime": "Bantime Rndtime",
"settings.bantime_rndtime.description": "Opcional. Segundos aleatorios máximos en la fórmula de incremento de bantime (p.ej. 2048). Dejar vacío para usar el valor por defecto de Fail2ban.",
"settings.bantime_rndtime_placeholder": "p.ej., 2048",
"settings.default_chain": "Chain por defecto",
"settings.default_chain.description": "Chain de iptables/nftables para baneos (p.ej. INPUT para host, DOCKER-USER para contenedores Docker).",
"settings.chain_help_title": "Chain por defecto",
"settings.chain_docker_user": "DOCKER-USER",
"settings.chain_help_docker_user": "Para aplicaciones en Docker. Los bans se aplican a todos los contenedores del host.",
"settings.chain_input": "INPUT",
"settings.chain_help_input": "Para aplicaciones en el host. Los bans solo afectan a la red del host, no a las redes Docker.",
"settings.chain_forward": "FORWARD",
"settings.chain_help_forward": "Solo en instalaciones Docker antiguas donde DOCKER-USER no existe.",
"settings.banaction": "Banaction",
"settings.banaction.description": "Acción de bloqueo por defecto (p.ej. nftables-multiport, nftables-allports, firewallcmd-rich-rules, etc). Se utiliza para definir las variables action_*.",
"settings.banaction_allports": "Banaction Allports",

View File

@@ -171,6 +171,18 @@
"settings.default_bantime": "Bantime par défaut",
"settings.default_bantime.description": "Le nombre de secondes pendant lesquelles un hôte est banni. Format de temps : 1m = 1 minutes, 1h = 1 heures, 1d = 1 jours, 1w = 1 semaines, 1mo = 1 mois, 1y = 1 années.",
"settings.default_bantime_placeholder": "par exemple, 48h",
"settings.bantime_rndtime": "Bantime Rndtime",
"settings.bantime_rndtime.description": "Optionnel. Nombre maximal de secondes aléatoires dans la formule d'incrément de bantime (ex. 2048). Laisser vide pour utiliser la valeur par défaut de Fail2ban.",
"settings.bantime_rndtime_placeholder": "par ex., 2048",
"settings.default_chain": "Chain par défaut",
"settings.default_chain.description": "Chain iptables/nftables pour les bannissements (ex. INPUT pour l'hôte, DOCKER-USER pour les conteneurs Docker).",
"settings.chain_help_title": "Chain par défaut",
"settings.chain_docker_user": "DOCKER-USER",
"settings.chain_help_docker_user": "Pour les applications dans Docker. Les bans s'appliquent à tous les conteneurs de l'hôte.",
"settings.chain_input": "INPUT",
"settings.chain_help_input": "Pour les applications sur l'hôte. Les bans ne concernent que le réseau de l'hôte, pas les réseaux Docker.",
"settings.chain_forward": "FORWARD",
"settings.chain_help_forward": "Uniquement pour les anciennes installations Docker sans DOCKER-USER.",
"settings.banaction": "Banaction",
"settings.banaction.description": "Action de bannissement par défaut (par ex. nftables-multiport, nftables-allports, firewallcmd-rich-rules, etc). Elle est utilisée pour définir les variables action_*.",
"settings.banaction_allports": "Banaction Allports",

View File

@@ -171,6 +171,18 @@
"settings.default_bantime": "Bantime predefinito",
"settings.default_bantime.description": "Il numero di secondi per cui un host viene bannato. Formato tempo: 1m = 1 minuti, 1h = 1 ore, 1d = 1 giorni, 1w = 1 settimane, 1mo = 1 mesi, 1y = 1 anni.",
"settings.default_bantime_placeholder": "es. 48h",
"settings.bantime_rndtime": "Bantime Rndtime",
"settings.bantime_rndtime.description": "Opzionale. Secondi casuali massimi nella formula di incremento bantime (es. 2048). Lasciare vuoto per usare il default di Fail2ban.",
"settings.bantime_rndtime_placeholder": "es. 2048",
"settings.default_chain": "Chain predefinita",
"settings.default_chain.description": "Chain iptables/nftables per i ban (es. INPUT per l'host, DOCKER-USER per i container Docker).",
"settings.chain_help_title": "Chain predefinita",
"settings.chain_docker_user": "DOCKER-USER",
"settings.chain_help_docker_user": "Per applicazioni in Docker. I ban si applicano a tutti i container sull'host.",
"settings.chain_input": "INPUT",
"settings.chain_help_input": "Per applicazioni sull'host. I ban riguardano solo la rete dell'host, non le reti Docker.",
"settings.chain_forward": "FORWARD",
"settings.chain_help_forward": "Solo per installazioni Docker vecchie dove DOCKER-USER non è disponibile.",
"settings.banaction": "Banaction",
"settings.banaction.description": "Azione di ban predefinita (es. nftables-multiport, nftables-allports, firewallcmd-rich-rules, ecc). Viene utilizzata per definire le variabili action_*.",
"settings.banaction_allports": "Banaction Allports",

View File

@@ -81,6 +81,8 @@ type AppSettingsRecord struct {
DestEmail string
Banaction string
BanactionAllports string
Chain string
BantimeRndtime string
// Advanced features
AdvancedActionsJSON string
GeoIPProvider string
@@ -194,17 +196,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) {
}
row := db.QueryRowContext(ctx, `
SELECT language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, 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, console_output, smtp_insecure_skip_verify, smtp_auth_method
SELECT language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, 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, console_output, smtp_insecure_skip_verify, smtp_auth_method, chain, bantime_rndtime
FROM app_settings
WHERE id = 1`)
var (
lang, callback, callbackSecret, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath, smtpAuthMethod sql.NullString
port, smtpPort, maxretry, maxLogLines sql.NullInt64
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn, emailAlertsForBans, emailAlertsForUnbans, consoleOutput, smtpInsecureSkipVerify sql.NullInt64
lang, callback, callbackSecret, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, chain, bantimeRndtime, advancedActions, geoipProvider, geoipDatabasePath, smtpAuthMethod sql.NullString
port, smtpPort, maxretry, maxLogLines sql.NullInt64
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn, emailAlertsForBans, emailAlertsForUnbans, consoleOutput, smtpInsecureSkipVerify sql.NullInt64
)
err := row.Scan(&lang, &port, &debug, &restartNeeded, &callback, &callbackSecret, &alerts, &emailAlertsForBans, &emailAlertsForUnbans, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines, &consoleOutput, &smtpInsecureSkipVerify, &smtpAuthMethod)
err := row.Scan(&lang, &port, &debug, &restartNeeded, &callback, &callbackSecret, &alerts, &emailAlertsForBans, &emailAlertsForUnbans, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines, &consoleOutput, &smtpInsecureSkipVerify, &smtpAuthMethod, &chain, &bantimeRndtime)
if errors.Is(err, sql.ErrNoRows) {
return AppSettingsRecord{}, false, nil
}
@@ -244,6 +246,8 @@ WHERE id = 1`)
DestEmail: stringFromNull(destemail),
Banaction: stringFromNull(banaction),
BanactionAllports: stringFromNull(banactionAllports),
Chain: stringFromNull(chain),
BantimeRndtime: stringFromNull(bantimeRndtime),
// Advanced features
AdvancedActionsJSON: stringFromNull(advancedActions),
GeoIPProvider: stringFromNull(geoipProvider),
@@ -262,9 +266,9 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error {
}
_, err := db.ExecContext(ctx, `
INSERT INTO app_settings (
id, language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, 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, console_output, smtp_insecure_skip_verify, smtp_auth_method
id, language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, 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, console_output, smtp_insecure_skip_verify, smtp_auth_method, chain, bantime_rndtime
) VALUES (
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) ON CONFLICT(id) DO UPDATE SET
language = excluded.language,
port = excluded.port,
@@ -296,7 +300,9 @@ INSERT INTO app_settings (
max_log_lines = excluded.max_log_lines,
console_output = excluded.console_output,
smtp_insecure_skip_verify = excluded.smtp_insecure_skip_verify,
smtp_auth_method = excluded.smtp_auth_method
smtp_auth_method = excluded.smtp_auth_method,
chain = excluded.chain,
bantime_rndtime = excluded.bantime_rndtime
`, rec.Language,
rec.Port,
boolToInt(rec.Debug),
@@ -327,7 +333,9 @@ INSERT INTO app_settings (
rec.MaxLogLines,
boolToInt(rec.ConsoleOutput),
boolToInt(rec.SMTPInsecureSkipVerify),
rec.SMTPAuthMethod)
rec.SMTPAuthMethod,
rec.Chain,
rec.BantimeRndtime)
return err
}
@@ -1101,6 +1109,16 @@ CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status);
return err
}
}
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN chain TEXT DEFAULT 'INPUT'`); err != nil {
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
return err
}
}
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN bantime_rndtime TEXT DEFAULT ''`); err != nil {
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
return err
}
}
_ = strings.Contains // Keep strings import for migration example above
return nil

View File

@@ -2129,10 +2129,12 @@ func UpdateSettingsHandler(c *gin.Context) {
oldSettings.DefaultJailEnable != newSettings.DefaultJailEnable ||
ignoreIPsChanged ||
oldSettings.Bantime != newSettings.Bantime ||
oldSettings.BantimeRndtime != newSettings.BantimeRndtime ||
oldSettings.Findtime != newSettings.Findtime ||
oldSettings.Maxretry != newSettings.Maxretry ||
oldSettings.Banaction != newSettings.Banaction ||
oldSettings.BanactionAllports != newSettings.BanactionAllports
oldSettings.BanactionAllports != newSettings.BanactionAllports ||
oldSettings.Chain != newSettings.Chain
if defaultSettingsChanged {
config.DebugLog("Fail2Ban DEFAULT settings changed, pushing to all enabled servers")
@@ -2348,6 +2350,10 @@ func ApplyFail2banSettings(jailLocalPath string) error {
// TODO: -> maybe we store [DEFAULT] block in memory, replace lines
// or do a line-based approach. Example is simplistic:
chain := s.Chain
if chain == "" {
chain = "INPUT"
}
newLines := []string{
"[DEFAULT]",
fmt.Sprintf("enabled = %t", s.DefaultJailEnable),
@@ -2358,8 +2364,12 @@ func ApplyFail2banSettings(jailLocalPath string) error {
fmt.Sprintf("maxretry = %d", s.Maxretry),
fmt.Sprintf("banaction = %s", s.Banaction),
fmt.Sprintf("banaction_allports = %s", s.BanactionAllports),
"",
fmt.Sprintf("chain = %s", chain),
}
if s.BantimeRndtime != "" {
newLines = append(newLines, fmt.Sprintf("bantime.rndtime = %s", s.BantimeRndtime))
}
newLines = append(newLines, "")
content := strings.Join(newLines, "\n")
return os.WriteFile(jailLocalPath, []byte(content), 0644)

View File

@@ -160,8 +160,10 @@ function loadSettings() {
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('bantimeRndtime').value = data.bantimeRndtime || '';
document.getElementById('findTime').value = data.findtime || '';
document.getElementById('maxRetry').value = data.maxretry || '';
document.getElementById('defaultChain').value = data.chain || 'INPUT';
// Load IgnoreIPs as array
const ignoreIPs = data.ignoreips || [];
renderIgnoreIPsTags(ignoreIPs);
@@ -233,11 +235,13 @@ function saveSettings(event) {
bantimeIncrement: document.getElementById('bantimeIncrement').checked,
defaultJailEnable: document.getElementById('defaultJailEnable').checked,
bantime: document.getElementById('banTime').value.trim(),
bantimeRndtime: document.getElementById('bantimeRndtime').value.trim(),
findtime: document.getElementById('findTime').value.trim(),
maxretry: parseInt(document.getElementById('maxRetry').value, 10) || 3,
ignoreips: getIgnoreIPsArray(),
banaction: document.getElementById('banaction').value,
banactionAllports: document.getElementById('banactionAllports').value,
chain: document.getElementById('defaultChain').value || 'INPUT',
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,

View File

@@ -833,6 +833,14 @@
<p class="text-xs text-red-600 mt-1 hidden" id="banTimeError"></p>
</div>
<!-- Bantime Rndtime (optional, for bantime increment formula) -->
<div class="mb-4">
<label for="bantimeRndtime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.bantime_rndtime">Bantime Rndtime</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.bantime_rndtime.description">Optional. Max random seconds added in bantime increment formula (e.g. 2048). Leave empty to use Fail2ban default.</p>
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="bantimeRndtime"
data-i18n-placeholder="settings.bantime_rndtime_placeholder" placeholder="e.g., 2048" />
</div>
<!-- Banaction -->
<div class="mb-4">
<label for="banaction" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.banaction">Banaction</label>
@@ -907,6 +915,20 @@
</select>
</div>
<!-- Default Chain -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-2">
<label for="defaultChain" class="block text-sm font-medium text-gray-700" data-i18n="settings.default_chain">Default Chain</label>
<button type="button" onclick="document.getElementById('chainHelpModal').classList.remove('hidden')" class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 text-gray-600 hover:bg-gray-300 text-xs font-medium" aria-label="Chain help" title="Chains help">?</button>
</div>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.default_chain.description">iptables/nftables chain used for banning (e.g. INPUT for host, DOCKER-USER for Docker containers).</p>
<select id="defaultChain" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="INPUT" data-i18n="settings.chain_input">INPUT</option>
<option value="DOCKER-USER" data-i18n="settings.chain_docker_user">DOCKER-USER</option>
<option value="FORWARD" data-i18n="settings.chain_forward">FORWARD</option>
</select>
</div>
<!-- Findtime -->
<div class="mb-4">
<label for="findTime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_findtime">Default Findtime</label>
@@ -1478,6 +1500,47 @@
</div>
</div>
<!-- Chain Help Modal -->
<div id="chainHelpModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 560px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<div class="flex items-center justify-between">
<h3 class="text-lg leading-6 font-medium text-gray-900" data-i18n="settings.chain_help_title">Default chain</h3>
<button type="button" onclick="closeModal('chainHelpModal')" class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600" aria-label="Close">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<dl class="mt-4 space-y-4 text-sm">
<div>
<dt class="font-medium text-gray-900" data-i18n="settings.chain_docker_user">DOCKER-USER</dt>
<dd class="text-gray-600 mt-1" data-i18n="settings.chain_help_docker_user">Use for apps running in Docker. Bans apply to all containers on the host.</dd>
</div>
<div>
<dt class="font-medium text-gray-900" data-i18n="settings.chain_input">INPUT</dt>
<dd class="text-gray-600 mt-1" data-i18n="settings.chain_help_input">Use for apps on the host. Bans apply to the host network only, not Docker networks.</dd>
</div>
<div>
<dt class="font-medium text-gray-900" data-i18n="settings.chain_forward">FORWARD</dt>
<dd class="text-gray-600 mt-1" data-i18n="settings.chain_help_forward">Only for older Docker setups where DOCKER-USER is not available.</dd>
</div>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm" onclick="closeModal('chainHelpModal')" data-i18n="modal.close">Close</button>
</div>
</div>
</div>
</div>
<!-- ********************** Modal Templates END ************************ -->
<!-- jQuery (used by Select2) -->