Files
fail2ban-ui/internal/fail2ban/jail_management.go

151 lines
4.6 KiB
Go
Raw Normal View History

package fail2ban
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/swissmakers/fail2ban-ui/internal/config"
)
// GetAllJails reads jails from both /etc/fail2ban/jail.local and /etc/fail2ban/jail.d directory.
func GetAllJails() ([]JailInfo, error) {
var jails []JailInfo
// Parse jails from jail.local
localPath := "/etc/fail2ban/jail.local"
localJails, err := parseJailConfigFile(localPath)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", localPath, err)
}
config.DebugLog("############################")
config.DebugLog(fmt.Sprintf("%+v", localJails))
config.DebugLog("############################")
jails = append(jails, localJails...)
// Parse jails from jail.d directory, if it exists
jailDPath := "/etc/fail2ban/jail.d"
files, err := os.ReadDir(jailDPath)
if err == nil {
for _, f := range files {
if !f.IsDir() && filepath.Ext(f.Name()) == ".conf" {
fullPath := filepath.Join(jailDPath, f.Name())
dJails, err := parseJailConfigFile(fullPath)
if err == nil {
jails = append(jails, dJails...)
}
}
}
}
return jails, nil
}
// parseJailConfigFile parses a jail configuration file and returns a slice of JailInfo.
// It assumes each jail section is defined by [JailName] and that an "enabled" line may exist.
func parseJailConfigFile(path string) ([]JailInfo, error) {
var jails []JailInfo
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var currentJail string
// default value is true if "enabled" is missing; we set it for each section.
enabled := true
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
// When a new section starts, save the previous jail if exists.
if currentJail != "" && currentJail != "DEFAULT" {
jails = append(jails, JailInfo{
JailName: currentJail,
Enabled: enabled,
})
}
// Start a new jail section.
currentJail = strings.Trim(line, "[]")
// Reset to default for the new section.
enabled = true
} else if strings.HasPrefix(strings.ToLower(line), "enabled") {
// Expect format: enabled = true/false
parts := strings.Split(line, "=")
if len(parts) == 2 {
value := strings.TrimSpace(parts[1])
enabled = strings.EqualFold(value, "true")
}
}
}
// Add the final jail if one exists.
if currentJail != "" && currentJail != "DEFAULT" {
jails = append(jails, JailInfo{
JailName: currentJail,
Enabled: enabled,
})
}
return jails, scanner.Err()
}
// UpdateJailEnabledStates updates the enabled state for each jail based on the provided updates map.
// It updates /etc/fail2ban/jail.local and attempts to update any jail.d files as well.
func UpdateJailEnabledStates(updates map[string]bool) error {
// Update jail.local file
localPath := "/etc/fail2ban/jail.local"
if err := updateJailConfigFile(localPath, updates); err != nil {
return fmt.Errorf("failed to update %s: %w", localPath, err)
}
// Update jail.d files (if any)
jailDPath := "/etc/fail2ban/jail.d"
files, err := os.ReadDir(jailDPath)
if err == nil {
for _, f := range files {
if !f.IsDir() && filepath.Ext(f.Name()) == ".conf" {
fullPath := filepath.Join(jailDPath, f.Name())
// Ignore error here, as jail.d files might not need to be updated.
_ = updateJailConfigFile(fullPath, updates)
}
}
}
return nil
}
// updateJailConfigFile updates a single jail configuration file with the new enabled states.
func updateJailConfigFile(path string, updates map[string]bool) error {
input, err := os.ReadFile(path)
if err != nil {
return err
}
lines := strings.Split(string(input), "\n")
var outputLines []string
var currentJail string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
currentJail = strings.Trim(trimmed, "[]")
outputLines = append(outputLines, line)
} else if strings.HasPrefix(trimmed, "enabled") {
if val, ok := updates[currentJail]; ok {
outputLines = append(outputLines, fmt.Sprintf("enabled = %t", val))
// Remove the update from map to mark it as processed.
delete(updates, currentJail)
} else {
outputLines = append(outputLines, line)
}
} else {
outputLines = append(outputLines, line)
}
}
// For any jails in updates that did not have an "enabled" line, append it.
for jail, val := range updates {
outputLines = append(outputLines, fmt.Sprintf("[%s]", jail))
outputLines = append(outputLines, fmt.Sprintf("enabled = %t", val))
}
newContent := strings.Join(outputLines, "\n")
return os.WriteFile(path, []byte(newContent), 0644)
}