mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
216 lines
6.7 KiB
Go
216 lines
6.7 KiB
Go
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)
|
|
}
|
|
}
|