package fail2ban import ( "context" "fmt" "sync" "github.com/swissmakers/fail2ban-ui/internal/config" ) // ========================================================================= // Connector Interface // ========================================================================= // Connector is the communication backend for a Fail2ban server. type Connector interface { ID() string Server() config.Fail2banServer GetJailInfos(ctx context.Context) ([]JailInfo, error) GetBannedIPs(ctx context.Context, jail string) ([]string, error) UnbanIP(ctx context.Context, jail, ip string) error BanIP(ctx context.Context, jail, ip string) error Reload(ctx context.Context) error Restart(ctx context.Context) error GetFilterConfig(ctx context.Context, jail string) (string, string, error) SetFilterConfig(ctx context.Context, jail, content string) error FetchBanEvents(ctx context.Context, limit int) ([]BanEvent, error) // Jail management GetAllJails(ctx context.Context) ([]JailInfo, error) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error // Filter operations GetFilters(ctx context.Context) ([]string, error) TestFilter(ctx context.Context, filterName string, logLines []string, filterContent string) (output string, filterPath string, err error) // Jail configuration operations GetJailConfig(ctx context.Context, jail string) (string, string, error) SetJailConfig(ctx context.Context, jail, content string) error TestLogpath(ctx context.Context, logpath string) ([]string, error) TestLogpathWithResolution(ctx context.Context, logpath string) (originalPath, resolvedPath string, files []string, err error) // Default settings operations UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error // Jail local structure management EnsureJailLocalStructure(ctx context.Context) error // CheckJailLocalIntegrity checks whether jail.local exists and contains the // ui-custom-action marker, which indicates it is managed by Fail2ban-UI. CheckJailLocalIntegrity(ctx context.Context) (bool, bool, error) // Jail and filter creation/deletion CreateJail(ctx context.Context, jailName, content string) error DeleteJail(ctx context.Context, jailName string) error CreateFilter(ctx context.Context, filterName, content string) error DeleteFilter(ctx context.Context, filterName string) error } // ========================================================================= // Manager // ========================================================================= // Manager holds connectors for all configured Fail2ban servers. type Manager struct { mu sync.RWMutex connectors map[string]Connector } var ( managerOnce sync.Once managerInst *Manager ) func GetManager() *Manager { managerOnce.Do(func() { managerInst = &Manager{ connectors: make(map[string]Connector), } }) return managerInst } func (m *Manager) ReloadFromSettings(settings config.AppSettings) error { m.mu.Lock() defer m.mu.Unlock() connectors := make(map[string]Connector) for _, srv := range settings.Servers { if !srv.Enabled { continue } conn, err := newConnectorForServer(srv) if err != nil { return fmt.Errorf("failed to initialise connector for %s (%s): %w", srv.Name, srv.ID, err) } connectors[srv.ID] = conn } m.connectors = connectors return nil } // Returns the connector for the specified server ID. func (m *Manager) Connector(serverID string) (Connector, error) { m.mu.RLock() defer m.mu.RUnlock() if serverID == "" { return nil, fmt.Errorf("server id must be provided") } conn, ok := m.connectors[serverID] if !ok { return nil, fmt.Errorf("connector for server %s not found or not enabled", serverID) } return conn, nil } // Returns the default connector as defined in settings. func (m *Manager) DefaultConnector() (Connector, error) { server := config.GetDefaultServer() if server.ID == "" { return nil, fmt.Errorf("no active fail2ban server configured") } return m.Connector(server.ID) } // Returns all connectors. func (m *Manager) Connectors() []Connector { m.mu.RLock() defer m.mu.RUnlock() result := make([]Connector, 0, len(m.connectors)) for _, conn := range m.connectors { result = append(result, conn) } return result } // ========================================================================= // Action File Management // ========================================================================= // Updates action files for all active remote connectors (SSH and Agent). func (m *Manager) UpdateActionFiles(ctx context.Context) error { m.mu.RLock() connectors := make([]Connector, 0, len(m.connectors)) for _, conn := range m.connectors { server := conn.Server() // Only update remote servers (SSH and Agent), not local if server.Type == "ssh" || server.Type == "agent" { connectors = append(connectors, conn) } } m.mu.RUnlock() var lastErr error for _, conn := range connectors { if err := updateConnectorAction(ctx, conn); err != nil { fmt.Printf("warning: failed to update action file for server %s: %v\n", conn.Server().Name, err) lastErr = err } } return lastErr } // Updates the action file for a single server. func (m *Manager) UpdateActionFileForServer(ctx context.Context, serverID string) error { m.mu.RLock() conn, ok := m.connectors[serverID] m.mu.RUnlock() if !ok { return fmt.Errorf("connector for server %s not found or not enabled", serverID) } return updateConnectorAction(ctx, conn) } func updateConnectorAction(ctx context.Context, conn Connector) error { switch c := conn.(type) { case *SSHConnector: return c.ensureAction(ctx) case *AgentConnector: return c.ensureAction(ctx) default: return nil } } // ========================================================================= // Connector Factory // ========================================================================= func newConnectorForServer(server config.Fail2banServer) (Connector, error) { switch server.Type { case "local": // Run migration before ensuring structure, but only when the experimental JAIL_AUTOMIGRATION=true env var is set. if isJailAutoMigrationEnabled() { config.DebugLog("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration for local server %s", server.Name) if err := MigrateJailsFromJailLocal(); err != nil { return nil, fmt.Errorf("failed to initialise local fail2ban connector for %s: %w", server.Name, err) } } if err := config.EnsureLocalFail2banAction(server); err != nil { return nil, fmt.Errorf("failed to ensure local fail2ban action for %s: %w", server.Name, err) } return NewLocalConnector(server), nil case "ssh": return NewSSHConnector(server) case "agent": return NewAgentConnector(server) default: return nil, fmt.Errorf("unsupported server type %s", server.Type) } }