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

@@ -863,70 +863,22 @@ func ensureFail2banActionFiles(callbackURL, serverID string) error {
}
// Ensure jail.local has proper structure (banner, DEFAULT, action_mwlg, action override)
if err := ensureJailLocalStructure(); err != nil {
if err := EnsureJailLocalStructure(); err != nil {
return err
}
return writeFail2banAction(callbackURL, serverID)
}
// EnsureJailLocalStructure creates or updates jail.local with proper structure:
// This is exported so connectors can call it.
func EnsureJailLocalStructure() error {
return ensureJailLocalStructure()
}
// ensureJailLocalStructure creates or updates jail.local with proper structure:
func ensureJailLocalStructure() error {
DebugLog("Running ensureJailLocalStructure()") // entry point
// Check if /etc/fail2ban directory exists (fail2ban must be installed)
if _, err := os.Stat(filepath.Dir(jailFile)); os.IsNotExist(err) {
return fmt.Errorf("fail2ban is not installed: /etc/fail2ban directory does not exist. Please install fail2ban package first")
}
// Get current settings
// BuildJailLocalContent builds the complete managed jail.local file content
// from the current settings. This is the single source of truth for the file
// format, shared by all connectors (local, SSH, agent).
func BuildJailLocalContent() string {
settings := GetSettings()
// Read existing jail.local content if it exists
var existingContent string
fileExists := false
if content, err := os.ReadFile(jailFile); err == nil {
existingContent = string(content)
fileExists = len(strings.TrimSpace(existingContent)) > 0
}
// If jail.local exists but is NOT managed by Fail2ban-UI,
// it belongs to the user; never overwrite it.
hasUIAction := strings.Contains(existingContent, "ui-custom-action")
if fileExists && !hasUIAction {
DebugLog("jail.local exists but is not managed by Fail2ban-UI - skipping overwrite")
return nil
}
// Check if file already has our full banner; indicating it's already properly structured
hasFullBanner := strings.Contains(existingContent, "################################################################################") &&
strings.Contains(existingContent, "Fail2Ban-UI Managed Configuration") &&
strings.Contains(existingContent, "DO NOT EDIT THIS FILE MANUALLY")
hasActionMwlg := strings.Contains(existingContent, "action_mwlg") && hasUIAction
hasActionOverride := strings.Contains(existingContent, "action = %(action_mwlg)s")
// If file is already properly structured, just ensure DEFAULT section is up to date
if hasFullBanner && hasActionMwlg && hasActionOverride {
DebugLog("jail.local already has proper structure, updating DEFAULT section if needed")
// Update DEFAULT section values without changing structure
return updateJailLocalDefaultSection(settings)
}
// Use the standard banner
banner := jailLocalBanner
// Build [DEFAULT] section
// 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"
@@ -949,34 +901,52 @@ maxretry = %d
banaction = %s
banaction_allports = %s
chain = %s
`, settings.DefaultJailEnable, settings.BantimeIncrement, ignoreIPStr, settings.Bantime, settings.Findtime, settings.Maxretry, banaction, banactionAllports, chain)
`, 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
// The multi-line format uses indentation for continuation
// ui-custom-action only needs logpath and chain
actionMwlgConfig := `# Custom Fail2Ban action for UI callbacks
action_mwlg = %(action_)s
ui-custom-action[logpath="%(logpath)s", chain="%(chain)s"]
`
// Build action override (at the end as per user requirements)
actionOverride := `# Custom Fail2Ban action applied by fail2ban-ui
action = %(action_mwlg)s
`
// Combine all parts
newContent := banner + defaultSection + actionMwlgConfig + actionOverride
return jailLocalBanner + defaultSection + actionMwlgConfig + actionOverride
}
// Write the new content
err := os.WriteFile(jailFile, []byte(newContent), 0644)
if err != nil {
// EnsureJailLocalStructure writes a complete managed jail.local to the local filesystem.
func EnsureJailLocalStructure() error {
DebugLog("Running EnsureJailLocalStructure()")
// Check if /etc/fail2ban directory exists (fail2ban must be installed)
if _, err := os.Stat(filepath.Dir(jailFile)); os.IsNotExist(err) {
return fmt.Errorf("fail2ban is not installed: /etc/fail2ban directory does not exist. Please install fail2ban package first")
}
// Read existing jail.local content if it exists
var existingContent string
fileExists := false
if content, err := os.ReadFile(jailFile); err == nil {
existingContent = string(content)
fileExists = len(strings.TrimSpace(existingContent)) > 0
}
// If jail.local exists but is NOT managed by Fail2ban-UI,
// it belongs to the user — never overwrite it.
if fileExists && !strings.Contains(existingContent, "ui-custom-action") {
DebugLog("jail.local exists but is not managed by Fail2ban-UI - skipping overwrite")
return nil
}
// Full rewrite from current settings (self-healing, no stale keys)
if err := os.WriteFile(jailFile, []byte(BuildJailLocalContent()), 0644); err != nil {
return fmt.Errorf("failed to write jail.local: %v", err)
}
@@ -984,144 +954,6 @@ action = %(action_mwlg)s
return nil
}
// updateJailLocalDefaultSection updates only the [DEFAULT] section values in jail.local
// while preserving the banner, action_mwlg, and action override
func updateJailLocalDefaultSection(settings AppSettings) error {
content, err := os.ReadFile(jailFile)
if err != nil {
return fmt.Errorf("failed to read jail.local: %w", err)
}
contentStr := string(content)
lines := strings.Split(contentStr, "\n")
var outputLines []string
inDefault := false
defaultUpdated := false
// 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"
}
// Keys 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")
}
keysUpdated := make(map[string]bool)
// Always add the full banner at the start
outputLines = append(outputLines, strings.Split(strings.TrimRight(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" {
inDefault = true
outputLines = append(outputLines, line)
} else {
inDefault = false
outputLines = append(outputLines, line)
}
} else if inDefault {
// 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
defaultUpdated = true
// 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 {
// Keep the line as-is
outputLines = append(outputLines, line)
}
} else {
// Keep lines outside DEFAULT section
outputLines = append(outputLines, line)
}
}
// Add any missing keys to the DEFAULT section
if inDefault {
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:]...)...)
defaultUpdated = true
break
}
}
}
}
}
if defaultUpdated {
newContent := strings.Join(outputLines, "\n")
if err := os.WriteFile(jailFile, []byte(newContent), 0644); err != nil {
return fmt.Errorf("failed to write jail.local: %w", err)
}
DebugLog("Updated DEFAULT section in jail.local")
}
return nil
}
// writeFail2banAction creates or updates the action file with the AlertCountries.
func writeFail2banAction(callbackURL, serverID string) error {
DebugLog("Running initial writeFail2banAction()") // entry point