Simplify the connector and jail.local cunstruction with a unified function for all connectors

This commit is contained in:
2026-02-10 15:50:32 +01:00
parent 8f9399196e
commit 337d199143
4 changed files with 46 additions and 734 deletions

View File

@@ -1064,214 +1064,8 @@ func ExtractFilterFromJailConfig(jailContent string) string {
return ""
}
// UpdateDefaultSettingsLocal updates specific keys in the [DEFAULT] section of /etc/fail2ban/jail.local
// with the provided settings, preserving all other content including the ui-custom-action section.
// Removes commented lines (starting with #) before applying updates.
// UpdateDefaultSettingsLocal rewrites /etc/fail2ban/jail.local with the current settings.
func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
config.DebugLog("UpdateDefaultSettingsLocal called")
localPath := "/etc/fail2ban/jail.local"
// Check jail.local integrity first
var existingContent string
fileExists := false
if content, err := os.ReadFile(localPath); err == nil {
existingContent = string(content)
fileExists = len(strings.TrimSpace(existingContent)) > 0
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to read jail.local: %w", err)
}
hasUIAction := strings.Contains(existingContent, "ui-custom-action")
if fileExists && !hasUIAction {
// File belongs to the user never overwrite
return fmt.Errorf("jail.local is not managed by Fail2ban-UI - skipping settings update (please migrate your jail.local manually)")
}
if !fileExists {
// File was deleted (e.g. user finished migration); create a fresh managed file
config.DebugLog("jail.local does not exist - initializing fresh managed file")
if err := config.EnsureJailLocalStructure(); err != nil {
return fmt.Errorf("failed to initialize jail.local: %w", err)
}
// Re-read the freshly created file
if content, err := os.ReadFile(localPath); err == nil {
existingContent = string(content)
}
}
// Remove commented lines (lines starting with #) but preserve:
// - Banner lines (containing "Fail2Ban-UI" or "fail2ban-ui")
// - action_mwlg and action override lines
lines := strings.Split(existingContent, "\n")
var uncommentedLines []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// Keep empty lines, banner lines, action_mwlg lines, action override lines, and lines that don't start with #
isBanner := strings.Contains(line, "Fail2Ban-UI") || strings.Contains(line, "fail2ban-ui")
isActionMwlg := strings.Contains(trimmed, "action_mwlg")
isActionOverride := strings.Contains(trimmed, "action = %(action_mwlg)s")
if trimmed == "" || !strings.HasPrefix(trimmed, "#") || isBanner || isActionMwlg || isActionOverride {
uncommentedLines = append(uncommentedLines, line)
}
}
existingContent = strings.Join(uncommentedLines, "\n")
// Convert IgnoreIPs array to space-separated string
ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
if ignoreIPStr == "" {
ignoreIPStr = "127.0.0.1/8 ::1"
}
// Set default banaction values if not set
banaction := settings.Banaction
if banaction == "" {
banaction = "nftables-multiport"
}
banactionAllports := settings.BanactionAllports
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),
"bantime.increment": fmt.Sprintf("bantime.increment = %t", settings.BantimeIncrement),
"ignoreip": fmt.Sprintf("ignoreip = %s", ignoreIPStr),
"bantime": fmt.Sprintf("bantime = %s", settings.Bantime),
"findtime": fmt.Sprintf("findtime = %s", settings.Findtime),
"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
keysUpdated := make(map[string]bool)
// Parse existing content and update only specific keys in DEFAULT section
if existingContent == "" {
// File doesn't exist, create new one with banner and DEFAULT section
var newLines []string
newLines = append(newLines, strings.Split(strings.TrimRight(config.JailLocalBanner(), "\n"), "\n")...)
newLines = append(newLines, "[DEFAULT]")
for _, key := range defaultKeysOrder {
newLines = append(newLines, keysToUpdate[key])
}
newLines = append(newLines, "")
newContent := strings.Join(newLines, "\n")
if err := os.WriteFile(localPath, []byte(newContent), 0644); err != nil {
return fmt.Errorf("failed to write jail.local: %w", err)
}
config.DebugLog("Created new jail.local with banner and DEFAULT section")
return nil
}
// Parse and update only specific keys in DEFAULT section
lines = strings.Split(existingContent, "\n")
var outputLines []string
inDefault := false
defaultSectionFound := false
// Always add the full banner at the start
outputLines = append(outputLines, strings.Split(strings.TrimRight(config.JailLocalBanner(), "\n"), "\n")...)
// Skip everything before [DEFAULT] section (old banner, comments, empty lines)
foundSection := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
// Found a section - stop skipping and process this line
foundSection = true
}
if !foundSection {
// Skip lines before any section (old banner, comments, empty lines)
continue
}
// Process lines after we found a section
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
sectionName := strings.Trim(trimmed, "[]")
if sectionName == "DEFAULT" {
// Start of DEFAULT section
inDefault = true
defaultSectionFound = true
outputLines = append(outputLines, line)
} else {
// Other section - stop DEFAULT mode
inDefault = false
outputLines = append(outputLines, line)
}
} else if inDefault {
// We're in DEFAULT section - check if this line is a key we need to update
keyUpdated := false
// 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
// 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 {
// Keep the line as-is (might be other DEFAULT settings or action_mwlg)
outputLines = append(outputLines, line)
}
} else {
// Keep lines outside DEFAULT section (preserves ui-custom-action and other content)
outputLines = append(outputLines, line)
}
}
// If DEFAULT section wasn't found, create it at the beginning
if !defaultSectionFound {
defaultLines := []string{"[DEFAULT]"}
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 defaultKeysOrder {
if !keysUpdated[key] {
// Find the DEFAULT section and insert after it
for i, line := range outputLines {
if strings.TrimSpace(line) == "[DEFAULT]" {
// Insert after [DEFAULT] header
outputLines = append(outputLines[:i+1], append([]string{keysToUpdate[key]}, outputLines[i+1:]...)...)
break
}
}
}
}
}
newContent := strings.Join(outputLines, "\n")
if err := os.WriteFile(localPath, []byte(newContent), 0644); err != nil {
return fmt.Errorf("failed to write jail.local: %w", err)
}
config.DebugLog("Updated specific keys in DEFAULT section of jail.local")
return nil
return config.EnsureJailLocalStructure()
}