Check jail.local state and warn user if it is not fail2ban-UI managed, disable automatic jail.local migration because it is only testing

This commit is contained in:
2026-02-09 19:56:43 +01:00
parent e8592d17e6
commit 90b287f409
18 changed files with 232 additions and 244 deletions

View File

@@ -17,6 +17,12 @@ var (
migrationOnce sync.Once
)
// Auto-migration of an existing jail.local into jail.d/ is experimental and disabled by default;
// it is always best to migrate a pre-existing jail.local by hand.
func isJailAutoMigrationEnabled() bool {
return strings.EqualFold(os.Getenv("JAIL_AUTOMIGRATION"), "true")
}
// ensureJailLocalFile ensures that a .local file exists for the given jail.
// If .local doesn't exist, it copies from .conf if available, or creates a minimal section.
func ensureJailLocalFile(jailName string) error {
@@ -324,16 +330,17 @@ func DeleteJail(jailName string) error {
return nil
}
// GetAllJails reads jails from /etc/fail2ban/jail.local (DEFAULT only) and /etc/fail2ban/jail.d directory.
// Automatically migrates legacy jails from jail.local to jail.d on first call.
// Now uses DiscoverJailsFromFiles() for file-based discovery.
// GetAllJails reads jails from /etc/fail2ban/jail.d directory.
func GetAllJails() ([]JailInfo, error) {
// Run migration once if needed
migrationOnce.Do(func() {
if err := MigrateJailsToJailD(); err != nil {
config.DebugLog("Migration warning: %v", err)
}
})
// 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)
}
})
}
// Discover jails from filesystem
jails, err := DiscoverJailsFromFiles()
@@ -510,142 +517,9 @@ func UpdateJailEnabledStates(updates map[string]bool) error {
return nil
}
// MigrateJailsToJailD migrates all non-DEFAULT jails from jail.local to individual files in jail.d/.
// Creates a backup of jail.local before migration. If a jail already exists in jail.d, jail.local takes precedence.
func MigrateJailsToJailD() error {
localPath := "/etc/fail2ban/jail.local"
jailDPath := "/etc/fail2ban/jail.d"
// Check if jail.local exists
if _, err := os.Stat(localPath); os.IsNotExist(err) {
return nil // Nothing to migrate
}
// Read jail.local content
content, err := os.ReadFile(localPath)
if err != nil {
return fmt.Errorf("failed to read jail.local: %w", err)
}
// Parse content to extract sections
sections, defaultContent, err := parseJailSections(string(content))
if err != nil {
return fmt.Errorf("failed to parse jail.local: %w", err)
}
// If no non-DEFAULT jails found, nothing to migrate
if len(sections) == 0 {
return nil
}
// Create backup of jail.local
backupPath := localPath + ".backup." + fmt.Sprintf("%d", os.Getpid())
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)
// Ensure jail.d directory exists
if err := os.MkdirAll(jailDPath, 0755); err != nil {
return fmt.Errorf("failed to create jail.d directory: %w", err)
}
// Write each jail to its own file in jail.d/
for jailName, jailContent := range sections {
jailFilePath := filepath.Join(jailDPath, jailName+".conf")
// Check if file already exists
if _, err := os.Stat(jailFilePath); err == nil {
// File exists - jail.local takes precedence, so overwrite
config.DebugLog("Overwriting existing jail file %s with content from jail.local", jailFilePath)
}
// Write jail content to file
if err := os.WriteFile(jailFilePath, []byte(jailContent), 0644); err != nil {
return fmt.Errorf("failed to write jail file %s: %w", jailFilePath, err)
}
}
// Rewrite jail.local with only DEFAULT section
newLocalContent := defaultContent
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/", len(sections))
return nil
}
// parseJailSections parses jail.local content and returns:
// - map of jail name to jail content (excluding DEFAULT and INCLUDES)
// - DEFAULT section content
func parseJailSections(content string) (map[string]string, string, error) {
sections := make(map[string]string)
var defaultContent strings.Builder
// Sections that should be ignored (not jails)
ignoredSections := map[string]bool{
"DEFAULT": true,
"INCLUDES": true,
}
scanner := bufio.NewScanner(strings.NewReader(content))
var currentSection string
var currentContent strings.Builder
inDefault := false
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
// Save previous section
if currentSection != "" {
sectionContent := strings.TrimSpace(currentContent.String())
if inDefault {
defaultContent.WriteString(sectionContent)
if !strings.HasSuffix(sectionContent, "\n") {
defaultContent.WriteString("\n")
}
} else if !ignoredSections[currentSection] {
// Only save if it's not an ignored section
sections[currentSection] = sectionContent
}
}
// Start new section
currentSection = strings.Trim(trimmed, "[]")
currentContent.Reset()
currentContent.WriteString(line)
currentContent.WriteString("\n")
inDefault = (currentSection == "DEFAULT")
} else {
currentContent.WriteString(line)
currentContent.WriteString("\n")
}
}
// Save final section
if currentSection != "" {
sectionContent := strings.TrimSpace(currentContent.String())
if inDefault {
defaultContent.WriteString(sectionContent)
} else if !ignoredSections[currentSection] {
// Only save if it's not an ignored section
sections[currentSection] = sectionContent
}
}
return sections, defaultContent.String(), scanner.Err()
}
// parseJailSectionsUncommented parses jail.local content and returns:
// - map of jail name to jail content (excluding DEFAULT, INCLUDES, and commented sections)
// - DEFAULT section content (including commented lines)
// Only extracts non-commented jail sections
func parseJailSectionsUncommented(content string) (map[string]string, string, error) {
sections := make(map[string]string)
var defaultContent strings.Builder
@@ -725,7 +599,7 @@ func parseJailSectionsUncommented(content string) (map[string]string, string, er
}
// MigrateJailsFromJailLocal migrates non-commented jail sections from jail.local to jail.d/*.local files.
// This should be called when a server is added or enabled to migrate legacy jails.
// EXPERIMENTAL: Only called when JAIL_AUTOMIGRATION=true. It is always best to migrate a pre-existing jail.local by hand.
func MigrateJailsFromJailLocal() error {
localPath := "/etc/fail2ban/jail.local"
jailDPath := "/etc/fail2ban/jail.d"