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

911 lines
27 KiB
Go

package fail2ban
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/swissmakers/fail2ban-ui/internal/config"
)
func ensureJailLocalFile(jailName string) error {
jailName = strings.TrimSpace(jailName)
if jailName == "" {
return fmt.Errorf("jail name cannot be empty")
}
jailDPath := "/etc/fail2ban/jail.d"
localPath := filepath.Join(jailDPath, jailName+".local")
confPath := filepath.Join(jailDPath, jailName+".conf")
if _, err := os.Stat(localPath); err == nil {
config.DebugLog("Jail .local file already exists: %s", localPath)
return nil
}
if _, err := os.Stat(confPath); err == nil {
config.DebugLog("Copying jail config from .conf to .local: %s -> %s", confPath, localPath)
content, err := os.ReadFile(confPath)
if err != nil {
return fmt.Errorf("failed to read jail .conf file %s: %w", confPath, err)
}
if err := os.WriteFile(localPath, content, 0644); err != nil {
return fmt.Errorf("failed to write jail .local file %s: %w", localPath, err)
}
config.DebugLog("Successfully copied jail config to .local file")
return nil
}
config.DebugLog("Creating minimal jail .local file: %s", localPath)
if err := os.MkdirAll(jailDPath, 0755); err != nil {
return fmt.Errorf("failed to create jail.d directory: %w", err)
}
minimalContent := fmt.Sprintf("[%s]\n", jailName)
if err := os.WriteFile(localPath, []byte(minimalContent), 0644); err != nil {
return fmt.Errorf("failed to create jail .local file %s: %w", localPath, err)
}
config.DebugLog("Successfully created minimal jail .local file")
return nil
}
// =========================================================================
// Config Read/Write
// =========================================================================
func readJailConfigWithFallback(jailName string) (string, string, error) {
jailName = strings.TrimSpace(jailName)
if jailName == "" {
return "", "", fmt.Errorf("jail name cannot be empty")
}
jailDPath := "/etc/fail2ban/jail.d"
localPath := filepath.Join(jailDPath, jailName+".local")
confPath := filepath.Join(jailDPath, jailName+".conf")
if content, err := os.ReadFile(localPath); err == nil {
config.DebugLog("Reading jail config from .local: %s", localPath)
return string(content), localPath, nil
}
if content, err := os.ReadFile(confPath); err == nil {
config.DebugLog("Reading jail config from .conf: %s", confPath)
return string(content), confPath, nil
}
config.DebugLog("Neither .local nor .conf exists for jail %s, returning empty section", jailName)
return fmt.Sprintf("[%s]\n", jailName), localPath, nil
}
// =========================================================================
// Validation
// =========================================================================
func ValidateJailName(name string) error {
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("jail name cannot be empty")
}
reservedNames := map[string]bool{
"DEFAULT": true,
"INCLUDES": true,
}
if reservedNames[strings.ToUpper(name)] {
return fmt.Errorf("jail name '%s' is reserved and cannot be used", name)
}
// Check for invalid characters (only alphanumeric, dash, underscore allowed)
invalidChars := regexp.MustCompile(`[^a-zA-Z0-9_-]`)
if invalidChars.MatchString(name) {
return fmt.Errorf("jail name '%s' contains invalid characters. Only alphanumeric characters, dashes, and underscores are allowed", name)
}
return nil
}
// =========================================================================
// Jail Discovery
// =========================================================================
func ListJailFiles(directory string) ([]string, error) {
var files []string
entries, err := os.ReadDir(directory)
if err != nil {
return nil, fmt.Errorf("failed to read jail directory %s: %w", directory, err)
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if strings.HasPrefix(name, ".") {
continue
}
// Only include .local and .conf files
if strings.HasSuffix(name, ".local") || strings.HasSuffix(name, ".conf") {
fullPath := filepath.Join(directory, name)
files = append(files, fullPath)
}
}
return files, nil
}
// Returns all jails from /etc/fail2ban/jail.d directory.
func DiscoverJailsFromFiles() ([]JailInfo, error) {
jailDPath := "/etc/fail2ban/jail.d"
if _, err := os.Stat(jailDPath); os.IsNotExist(err) {
return []JailInfo{}, nil
}
files, err := ListJailFiles(jailDPath)
if err != nil {
return nil, err
}
var allJails []JailInfo
processedFiles := make(map[string]bool)
processedJails := make(map[string]bool)
// Parse .local files
for _, filePath := range files {
if !strings.HasSuffix(filePath, ".local") {
continue
}
filename := filepath.Base(filePath)
baseName := strings.TrimSuffix(filename, ".local")
if baseName == "" {
continue
}
if processedFiles[baseName] {
continue
}
processedFiles[baseName] = true
jails, err := parseJailConfigFile(filePath)
if err != nil {
config.DebugLog("Failed to parse jail file %s: %v", filePath, err)
continue
}
for _, jail := range jails {
if jail.JailName != "" && jail.JailName != "DEFAULT" && !processedJails[jail.JailName] {
allJails = append(allJails, jail)
processedJails[jail.JailName] = true
}
}
}
for _, filePath := range files {
if !strings.HasSuffix(filePath, ".conf") {
continue
}
filename := filepath.Base(filePath)
baseName := strings.TrimSuffix(filename, ".conf")
if baseName == "" {
continue
}
if processedFiles[baseName] {
continue
}
processedFiles[baseName] = true
jails, err := parseJailConfigFile(filePath)
if err != nil {
config.DebugLog("Failed to parse jail file %s: %v", filePath, err)
continue
}
for _, jail := range jails {
if jail.JailName != "" && jail.JailName != "DEFAULT" && !processedJails[jail.JailName] {
allJails = append(allJails, jail)
processedJails[jail.JailName] = true
}
}
}
return allJails, nil
}
// =========================================================================
// Jail Creation
// =========================================================================
func CreateJail(jailName, content string) error {
if err := ValidateJailName(jailName); err != nil {
return err
}
jailDPath := "/etc/fail2ban/jail.d"
localPath := filepath.Join(jailDPath, jailName+".local")
if err := os.MkdirAll(jailDPath, 0755); err != nil {
return fmt.Errorf("failed to create jail.d directory: %w", err)
}
trimmed := strings.TrimSpace(content)
expectedSection := fmt.Sprintf("[%s]", jailName)
if !strings.HasPrefix(trimmed, expectedSection) {
content = expectedSection + "\n" + content
}
if err := os.WriteFile(localPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to create jail file %s: %w", localPath, err)
}
config.DebugLog("Created jail file: %s", localPath)
return nil
}
// =========================================================================
//
// Jail Deletion
//
// =========================================================================
func DeleteJail(jailName string) error {
if err := ValidateJailName(jailName); err != nil {
return err
}
jailDPath := "/etc/fail2ban/jail.d"
localPath := filepath.Join(jailDPath, jailName+".local")
confPath := filepath.Join(jailDPath, jailName+".conf")
var deletedFiles []string
var lastErr error
if _, err := os.Stat(localPath); err == nil {
if err := os.Remove(localPath); err != nil {
lastErr = fmt.Errorf("failed to delete jail file %s: %w", localPath, err)
} else {
deletedFiles = append(deletedFiles, localPath)
config.DebugLog("Deleted jail file: %s", localPath)
}
}
if _, err := os.Stat(confPath); err == nil {
if err := os.Remove(confPath); err != nil {
lastErr = fmt.Errorf("failed to delete jail file %s: %w", confPath, err)
} else {
deletedFiles = append(deletedFiles, confPath)
config.DebugLog("Deleted jail file: %s", confPath)
}
}
if len(deletedFiles) == 0 && lastErr == nil {
return fmt.Errorf("jail file %s or %s does not exist", localPath, confPath)
}
if lastErr != nil {
return lastErr
}
return nil
}
// Returns all jails.
func GetAllJails() ([]JailInfo, error) {
// Run migration once if enabled (experimental, off by default)
if isJailAutoMigrationEnabled() {
migrationOnce.Do(func() {
config.DebugLog("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration")
if err := MigrateJailsFromJailLocal(); err != nil {
config.DebugLog("Migration warning: %v", err)
}
})
}
jails, err := DiscoverJailsFromFiles()
if err != nil {
return nil, fmt.Errorf("failed to discover jails from files: %w", err)
}
return jails, nil
}
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
ignoredSections := map[string]bool{
"DEFAULT": true,
"INCLUDES": true,
}
enabled := true
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
if currentJail != "" && !ignoredSections[currentJail] {
jails = append(jails, JailInfo{
JailName: currentJail,
Enabled: enabled,
})
}
currentJail = strings.TrimSpace(strings.Trim(line, "[]"))
if currentJail == "" {
currentJail = ""
enabled = true
continue
}
enabled = true
} else if strings.HasPrefix(strings.ToLower(line), "enabled") {
if currentJail != "" {
parts := strings.Split(line, "=")
if len(parts) == 2 {
value := strings.TrimSpace(parts[1])
enabled = strings.EqualFold(value, "true")
}
}
}
}
if currentJail != "" && !ignoredSections[currentJail] {
jails = append(jails, JailInfo{
JailName: currentJail,
Enabled: enabled,
})
}
return jails, scanner.Err()
}
// =========================================================================
// Jail Enabled from "Manage Jails"
// =========================================================================
func UpdateJailEnabledStates(updates map[string]bool) error {
config.DebugLog("UpdateJailEnabledStates called with %d updates: %+v", len(updates), updates)
jailDPath := "/etc/fail2ban/jail.d"
for jailName, enabled := range updates {
jailName = strings.TrimSpace(jailName)
if jailName == "" {
config.DebugLog("Skipping empty jail name in updates map")
continue
}
config.DebugLog("Processing jail: %s, enabled: %t", jailName, enabled)
// Ensure .local file exists
if err := ensureJailLocalFile(jailName); err != nil {
return fmt.Errorf("failed to ensure .local file for jail %s: %w", jailName, err)
}
jailFilePath := filepath.Join(jailDPath, jailName+".local")
config.DebugLog("Jail file path: %s", jailFilePath)
content, err := os.ReadFile(jailFilePath)
if err != nil {
return fmt.Errorf("failed to read jail .local file %s: %w", jailFilePath, err)
}
var lines []string
if len(content) > 0 {
lines = strings.Split(string(content), "\n")
} else {
lines = []string{fmt.Sprintf("[%s]", jailName)}
}
var outputLines []string
var foundEnabled bool
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(strings.ToLower(trimmed), "enabled") {
if currentJail == jailName {
outputLines = append(outputLines, fmt.Sprintf("enabled = %t", enabled))
foundEnabled = true
} else {
outputLines = append(outputLines, line)
}
} else {
outputLines = append(outputLines, line)
}
}
if !foundEnabled {
var newLines []string
for i, line := range outputLines {
newLines = append(newLines, line)
if strings.TrimSpace(line) == fmt.Sprintf("[%s]", jailName) {
// Insert enabled line after the section header
newLines = append(newLines, fmt.Sprintf("enabled = %t", enabled))
if i+1 < len(outputLines) {
newLines = append(newLines, outputLines[i+1:]...)
}
break
}
}
if len(newLines) > len(outputLines) {
outputLines = newLines
} else {
outputLines = append(outputLines, fmt.Sprintf("enabled = %t", enabled))
}
}
newContent := strings.Join(outputLines, "\n")
if !strings.HasSuffix(newContent, "\n") {
newContent += "\n"
}
if err := os.WriteFile(jailFilePath, []byte(newContent), 0644); err != nil {
return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err)
}
config.DebugLog("Updated jail %s: enabled = %t (file: %s)", jailName, enabled, jailFilePath)
}
return nil
}
// Returns the full jail configuration from /etc/fail2ban/jail.d/{jailName}.local
func GetJailConfig(jailName string) (string, string, error) {
jailName = strings.TrimSpace(jailName)
if jailName == "" {
return "", "", fmt.Errorf("jail name cannot be empty")
}
config.DebugLog("GetJailConfig called for jail: %s", jailName)
content, filePath, err := readJailConfigWithFallback(jailName)
if err != nil {
config.DebugLog("Failed to read jail config: %v", err)
return "", "", fmt.Errorf("failed to read jail config for %s: %w", jailName, err)
}
config.DebugLog("Jail config read successfully, length: %d, file: %s", len(content), filePath)
return content, filePath, nil
}
// Extracts the filter name from the jail configuration.
func ExtractFilterFromJailConfig(jailContent string) string {
scanner := bufio.NewScanner(strings.NewReader(jailContent))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(strings.ToLower(line), "filter") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
filterValue := strings.TrimSpace(parts[1])
if idx := strings.Index(filterValue, "["); idx >= 0 {
filterValue = filterValue[:idx]
}
return strings.TrimSpace(filterValue)
}
}
}
return ""
}
// Writes the full jail configuration to /etc/fail2ban/jail.d/{jailName}.local
func SetJailConfig(jailName, content string) error {
jailName = strings.TrimSpace(jailName)
if jailName == "" {
return fmt.Errorf("jail name cannot be empty")
}
config.DebugLog("SetJailConfig called for jail: %s, content length: %d", jailName, len(content))
jailDPath := "/etc/fail2ban/jail.d"
if err := ensureJailLocalFile(jailName); err != nil {
return fmt.Errorf("failed to ensure .local file for jail %s: %w", jailName, err)
}
trimmed := strings.TrimSpace(content)
if trimmed == "" {
config.DebugLog("Content is empty, creating minimal jail config")
content = fmt.Sprintf("[%s]\n", jailName)
} else {
expectedSection := fmt.Sprintf("[%s]", jailName)
lines := strings.Split(content, "\n")
sectionFound := false
sectionIndex := -1
var sectionIndices []int
// Find all section headers in the content
for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "[") && strings.HasSuffix(trimmedLine, "]") {
sectionIndices = append(sectionIndices, i)
if trimmedLine == expectedSection {
if !sectionFound {
sectionIndex = i
sectionFound = true
config.DebugLog("Correct section header found at line %d", i)
} else {
config.DebugLog("Duplicate correct section header found at line %d, will remove", i)
}
} else {
config.DebugLog("Incorrect section header found at line %d: %s (expected %s)", i, trimmedLine, expectedSection)
if sectionIndex == -1 {
sectionIndex = i
}
}
}
}
if len(sectionIndices) > 1 {
config.DebugLog("Found %d section headers, removing duplicates", len(sectionIndices))
var newLines []string
keptFirst := false
for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
isSectionHeader := strings.HasPrefix(trimmedLine, "[") && strings.HasSuffix(trimmedLine, "]")
if isSectionHeader {
if !keptFirst && trimmedLine == expectedSection {
newLines = append(newLines, expectedSection)
keptFirst = true
config.DebugLog("Keeping section header at line %d", i)
} else {
config.DebugLog("Removing duplicate/incorrect section header at line %d: %s", i, trimmedLine)
continue
}
} else {
newLines = append(newLines, line)
}
}
lines = newLines
}
if !sectionFound {
if sectionIndex >= 0 {
config.DebugLog("Replacing incorrect section header at line %d", sectionIndex)
lines[sectionIndex] = expectedSection
} else {
config.DebugLog("No section header found, prepending %s", expectedSection)
lines = append([]string{expectedSection}, lines...)
}
content = strings.Join(lines, "\n")
} else {
content = strings.Join(lines, "\n")
}
}
jailFilePath := filepath.Join(jailDPath, jailName+".local")
config.DebugLog("Writing jail config to: %s", jailFilePath)
if err := os.WriteFile(jailFilePath, []byte(content), 0644); err != nil {
config.DebugLog("Failed to write jail config: %v", err)
return fmt.Errorf("failed to write jail config for %s: %w", jailName, err)
}
config.DebugLog("Jail config written successfully to .local file")
return nil
}
// =========================================================================
// Logpath Operations
// =========================================================================
func TestLogpath(logpath string) ([]string, error) {
if logpath == "" {
return []string{}, nil
}
logpath = strings.TrimSpace(logpath)
hasWildcard := strings.ContainsAny(logpath, "*?[")
var matches []string
if hasWildcard {
matched, err := filepath.Glob(logpath)
if err != nil {
return nil, fmt.Errorf("invalid glob pattern: %w", err)
}
matches = matched
} else {
info, err := os.Stat(logpath)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, fmt.Errorf("failed to stat path: %w", err)
}
if info.IsDir() {
entries, err := os.ReadDir(logpath)
if err != nil {
return nil, fmt.Errorf("failed to read directory: %w", err)
}
for _, entry := range entries {
if !entry.IsDir() {
fullPath := filepath.Join(logpath, entry.Name())
matches = append(matches, fullPath)
}
}
} else {
matches = []string{logpath}
}
}
return matches, nil
}
// Resolves variables in logpath and tests the resolved path.
func TestLogpathWithResolution(logpath string) (originalPath, resolvedPath string, files []string, err error) {
originalPath = strings.TrimSpace(logpath)
if originalPath == "" {
return originalPath, "", []string{}, nil
}
resolvedPath, err = ResolveLogpathVariables(originalPath)
if err != nil {
return originalPath, "", nil, fmt.Errorf("failed to resolve logpath variables: %w", err)
}
if resolvedPath == "" {
resolvedPath = originalPath
}
files, err = TestLogpath(resolvedPath)
if err != nil {
return originalPath, resolvedPath, nil, fmt.Errorf("failed to test logpath: %w", err)
}
return originalPath, resolvedPath, files, nil
}
// Extracts the logpath from the jail configuration.
func ExtractLogpathFromJailConfig(jailContent string) string {
var logpaths []string
scanner := bufio.NewScanner(strings.NewReader(jailContent))
inLogpathLine := false
currentLogpath := ""
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#") {
if inLogpathLine && currentLogpath != "" {
paths := strings.Fields(currentLogpath)
logpaths = append(logpaths, paths...)
currentLogpath = ""
inLogpathLine = false
}
continue
}
if strings.HasPrefix(strings.ToLower(line), "logpath") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
logpathValue := strings.TrimSpace(parts[1])
if logpathValue != "" {
currentLogpath = logpathValue
inLogpathLine = true
}
}
} else if inLogpathLine {
if line != "" && !strings.Contains(line, "=") {
currentLogpath += " " + line
} else {
if currentLogpath != "" {
paths := strings.Fields(currentLogpath)
logpaths = append(logpaths, paths...)
currentLogpath = ""
}
inLogpathLine = false
}
} else if inLogpathLine && line == "" {
if currentLogpath != "" {
paths := strings.Fields(currentLogpath)
logpaths = append(logpaths, paths...)
currentLogpath = ""
}
inLogpathLine = false
}
}
if currentLogpath != "" {
paths := strings.Fields(currentLogpath)
logpaths = append(logpaths, paths...)
}
return strings.Join(logpaths, "\n")
}
// Updates /etc/fail2ban/jail.local with the current settings.
func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
config.DebugLog("UpdateDefaultSettingsLocal called")
return config.EnsureJailLocalStructure()
}
// =========================================================================
// Jail Auto Migration (EXPERIMENTAL, runs only when JAIL_AUTOMIGRATION=true)
// =========================================================================
var (
migrationOnce sync.Once
)
func isJailAutoMigrationEnabled() bool {
return strings.EqualFold(os.Getenv("JAIL_AUTOMIGRATION"), "true")
}
// Migrates jail.local to jail.d/*.local.
func MigrateJailsFromJailLocal() error {
localPath := "/etc/fail2ban/jail.local"
jailDPath := "/etc/fail2ban/jail.d"
if _, err := os.Stat(localPath); os.IsNotExist(err) {
return nil
}
content, err := os.ReadFile(localPath)
if err != nil {
return fmt.Errorf("failed to read jail.local: %w", err)
}
sections, defaultContent, err := parseJailSectionsUncommented(string(content))
if err != nil {
return fmt.Errorf("failed to parse jail.local: %w", err)
}
if len(sections) == 0 {
config.DebugLog("No jails to migrate from jail.local")
return nil
}
backupPath := localPath + ".backup." + fmt.Sprintf("%d", time.Now().Unix())
if err := os.WriteFile(backupPath, content, 0644); err != nil {
return fmt.Errorf("failed to create backup: %w", err)
}
config.DebugLog("Created backup of jail.local at %s", backupPath)
if err := os.MkdirAll(jailDPath, 0755); err != nil {
return fmt.Errorf("failed to create jail.d directory: %w", err)
}
migratedCount := 0
for jailName, jailContent := range sections {
if jailName == "" {
continue
}
jailFilePath := filepath.Join(jailDPath, jailName+".local")
if _, err := os.Stat(jailFilePath); err == nil {
config.DebugLog("Skipping migration for jail %s: .local file already exists", jailName)
continue
}
enabledSet := strings.Contains(jailContent, "enabled") || strings.Contains(jailContent, "Enabled")
if !enabledSet {
lines := strings.Split(jailContent, "\n")
modifiedContent := ""
for i, line := range lines {
modifiedContent += line + "\n"
if i == 0 && strings.HasPrefix(strings.TrimSpace(line), "[") && strings.HasSuffix(strings.TrimSpace(line), "]") {
modifiedContent += "enabled = false\n"
}
}
jailContent = modifiedContent
} else {
jailContent = regexp.MustCompile(`(?m)^\s*enabled\s*=\s*true\s*$`).ReplaceAllString(jailContent, "enabled = false")
}
if err := os.WriteFile(jailFilePath, []byte(jailContent), 0644); err != nil {
return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err)
}
config.DebugLog("Migrated jail %s to %s (enabled = false)", jailName, jailFilePath)
migratedCount++
}
if migratedCount > 0 {
newLocalContent := defaultContent
scanner := bufio.NewScanner(strings.NewReader(string(content)))
var inCommentedJail bool
var commentedJailContent strings.Builder
var commentedJailName string
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
originalLine := strings.TrimSpace(line)
if strings.HasPrefix(originalLine, "#[") {
if inCommentedJail && commentedJailName != "" {
newLocalContent += commentedJailContent.String()
}
inCommentedJail = true
commentedJailContent.Reset()
commentedJailName = strings.Trim(trimmed, "[]")
if strings.HasPrefix(commentedJailName, "#") {
commentedJailName = strings.TrimSpace(strings.TrimPrefix(commentedJailName, "#"))
}
commentedJailContent.WriteString(line)
commentedJailContent.WriteString("\n")
} else {
if inCommentedJail && commentedJailName != "" {
newLocalContent += commentedJailContent.String()
inCommentedJail = false
commentedJailContent.Reset()
}
}
} else if inCommentedJail {
commentedJailContent.WriteString(line)
commentedJailContent.WriteString("\n")
}
}
if inCommentedJail && commentedJailName != "" {
newLocalContent += commentedJailContent.String()
}
if !strings.HasSuffix(newLocalContent, "\n") {
newLocalContent += "\n"
}
if err := os.WriteFile(localPath, []byte(newLocalContent), 0644); err != nil {
return fmt.Errorf("failed to rewrite jail.local: %w", err)
}
config.DebugLog("Migration completed: moved %d jails to jail.d/", migratedCount)
}
return nil
}
// Parses an existing jail configuration and returns all jail sections from the file.
func parseJailSectionsUncommented(content string) (map[string]string, string, error) {
sections := make(map[string]string)
var defaultContent strings.Builder
ignoredSections := map[string]bool{
"DEFAULT": true,
"INCLUDES": true,
}
scanner := bufio.NewScanner(strings.NewReader(content))
var currentSection string
var currentContent strings.Builder
inDefault := false
sectionIsCommented := false
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
originalLine := strings.TrimSpace(line)
isCommented := strings.HasPrefix(originalLine, "#")
if currentSection != "" {
sectionContent := strings.TrimSpace(currentContent.String())
if inDefault {
defaultContent.WriteString(sectionContent)
if !strings.HasSuffix(sectionContent, "\n") {
defaultContent.WriteString("\n")
}
} else if !ignoredSections[currentSection] && !sectionIsCommented {
sections[currentSection] = sectionContent
}
}
if isCommented {
sectionName := strings.Trim(trimmed, "[]")
if strings.HasPrefix(sectionName, "#") {
sectionName = strings.TrimSpace(strings.TrimPrefix(sectionName, "#"))
}
currentSection = sectionName
sectionIsCommented = true
} else {
currentSection = strings.Trim(trimmed, "[]")
sectionIsCommented = false
}
currentContent.Reset()
currentContent.WriteString(line)
currentContent.WriteString("\n")
inDefault = (currentSection == "DEFAULT")
} else {
currentContent.WriteString(line)
currentContent.WriteString("\n")
}
}
if currentSection != "" {
sectionContent := strings.TrimSpace(currentContent.String())
if inDefault {
defaultContent.WriteString(sectionContent)
} else if !ignoredSections[currentSection] && !sectionIsCommented {
sections[currentSection] = sectionContent
}
}
return sections, defaultContent.String(), scanner.Err()
}