Consolidate JailInfo function to own connector_global.go and also remove old FetchBanEvents function

This commit is contained in:
2026-02-20 00:02:06 +01:00
parent efd00b2a17
commit a770fccbae
17 changed files with 79 additions and 387 deletions

View File

@@ -9,7 +9,6 @@ import (
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
@@ -157,45 +156,6 @@ func (ac *AgentConnector) SetFilterConfig(ctx context.Context, jail, content str
return ac.put(ctx, fmt.Sprintf("/v1/filters/%s", url.PathEscape(jail)), payload, nil)
}
func (ac *AgentConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEvent, error) {
query := url.Values{}
if limit > 0 {
query.Set("limit", strconv.Itoa(limit))
}
var resp struct {
Events []struct {
IP string `json:"ip"`
Jail string `json:"jail"`
Hostname string `json:"hostname"`
Failures string `json:"failures"`
Whois string `json:"whois"`
Logs string `json:"logs"`
Timestamp string `json:"timestamp"`
} `json:"events"`
}
endpoint := "/v1/events"
if encoded := query.Encode(); encoded != "" {
endpoint += "?" + encoded
}
if err := ac.get(ctx, endpoint, &resp); err != nil {
return nil, err
}
result := make([]BanEvent, 0, len(resp.Events))
for _, evt := range resp.Events {
ts, err := time.Parse(time.RFC3339, evt.Timestamp)
if err != nil {
ts = time.Now()
}
result = append(result, BanEvent{
Time: ts,
Jail: evt.Jail,
IP: evt.IP,
LogLine: fmt.Sprintf("%s %s", evt.Hostname, evt.Failures),
})
}
return result, nil
}
// =========================================================================
// HTTP Helpers
// =========================================================================

View File

@@ -1,6 +1,6 @@
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
//
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch)
//
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
// You may not use this file except in compliance with the License.
@@ -14,14 +14,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Shared types, helpers, and high-level functions used across all connectors.
package fail2ban
import "context"
import (
"context"
"sort"
"sync"
)
// =========================================================================
// Types
// =========================================================================
// JailInfo holds summary data for a single Fail2ban jail.
type JailInfo struct {
JailName string `json:"jailName"`
TotalBanned int `json:"totalBanned"`
@@ -31,55 +37,10 @@ type JailInfo struct {
}
// =========================================================================
// Client Functions
// Service Control
// =========================================================================
// Returns jail names from the default server.
func GetJails() ([]string, error) {
conn, err := GetManager().DefaultConnector()
if err != nil {
return nil, err
}
infos, err := conn.GetJailInfos(context.Background())
if err != nil {
return nil, err
}
names := make([]string, 0, len(infos))
for _, info := range infos {
names = append(names, info.JailName)
}
return names, nil
}
// Returns a slice of currently banned IPs for a specific jail.
func GetBannedIPs(jail string) ([]string, error) {
conn, err := GetManager().DefaultConnector()
if err != nil {
return nil, err
}
return conn.GetBannedIPs(context.Background(), jail)
}
// Unbans an IP from the given jail.
func UnbanIP(jail, ip string) error {
conn, err := GetManager().DefaultConnector()
if err != nil {
return err
}
return conn.UnbanIP(context.Background(), jail, ip)
}
// Returns extended info for each jail on the default server.
func BuildJailInfos(_ string) ([]JailInfo, error) {
conn, err := GetManager().DefaultConnector()
if err != nil {
return nil, err
}
return conn.GetJailInfos(context.Background())
}
// Restarts (or reloads) the Fail2ban service.
// RestartFail2ban restarts (or reloads) the Fail2ban service on the given server.
func RestartFail2ban(serverID string) (string, error) {
manager := GetManager()
var (
@@ -104,3 +65,61 @@ func RestartFail2ban(serverID string) (string, error) {
}
return "restart", nil
}
// =========================================================================
// Jail Info Collection
// =========================================================================
// bannedIPsFn is the signature used by any connector's GetBannedIPs method.
type bannedIPsFn func(ctx context.Context, jail string) ([]string, error)
// collectJailInfos fans out to fetch banned IPs for each jail concurrently,
// then returns the results sorted alphabetically. Both the local and SSH
// connectors delegate to this function from their GetJailInfos methods.
func collectJailInfos(ctx context.Context, jails []string, getBannedIPs bannedIPsFn) ([]JailInfo, error) {
type jailResult struct {
jail JailInfo
err error
}
results := make(chan jailResult, len(jails))
var wg sync.WaitGroup
for _, jail := range jails {
wg.Add(1)
go func(j string) {
defer wg.Done()
ips, err := getBannedIPs(ctx, j)
if err != nil {
results <- jailResult{err: err}
return
}
results <- jailResult{
jail: JailInfo{
JailName: j,
TotalBanned: len(ips),
BannedIPs: ips,
Enabled: true,
},
}
}(jail)
}
go func() {
wg.Wait()
close(results)
}()
var infos []JailInfo
for r := range results {
if r.err != nil {
continue
}
infos = append(infos, r.jail)
}
sort.SliceStable(infos, func(i, j int) bool {
return infos[i].JailName < infos[j].JailName
})
return infos, nil
}

View File

@@ -6,10 +6,7 @@ import (
"fmt"
"os"
"os/exec"
"sort"
"strings"
"sync"
"time"
"github.com/swissmakers/fail2ban-ui/internal/config"
)
@@ -36,71 +33,13 @@ func (lc *LocalConnector) Server() config.Fail2banServer {
return lc.server
}
// Get jail information.
// Collects jail status for every active local jail.
func (lc *LocalConnector) GetJailInfos(ctx context.Context) ([]JailInfo, error) {
jails, err := lc.getJails(ctx)
if err != nil {
return nil, err
}
logPath := lc.server.LogPath // LEGACY, WILL BE REMOVED IN FUTURE VERSIONS.
if logPath == "" {
logPath = "/var/log/fail2ban.log"
}
banHistory, err := ParseBanLog(logPath) // LEGACY, WILL BE REMOVED IN FUTURE VERSIONS.
if err != nil {
banHistory = make(map[string][]BanEvent)
}
oneHourAgo := time.Now().Add(-1 * time.Hour)
type jailResult struct {
jail JailInfo
err error
}
results := make(chan jailResult, len(jails))
var wg sync.WaitGroup
for _, jail := range jails {
wg.Add(1)
go func(j string) {
defer wg.Done()
bannedIPs, err := lc.GetBannedIPs(ctx, j)
if err != nil {
results <- jailResult{err: err}
return
}
newInLastHour := 0
if events, ok := banHistory[j]; ok {
for _, e := range events {
if e.Time.After(oneHourAgo) {
newInLastHour++
}
}
}
results <- jailResult{
jail: JailInfo{
JailName: j,
TotalBanned: len(bannedIPs),
NewInLastHour: newInLastHour,
BannedIPs: bannedIPs,
Enabled: true,
},
}
}(jail)
}
go func() {
wg.Wait()
close(results)
}()
var finalResults []JailInfo
for result := range results {
if result.err != nil {
continue
}
finalResults = append(finalResults, result.jail)
}
sort.SliceStable(finalResults, func(i, j int) bool {
return finalResults[i].JailName < finalResults[j].JailName
})
return finalResults, nil
return collectJailInfos(ctx, jails, lc.GetBannedIPs)
}
// Get banned IPs for a given jail.
@@ -196,29 +135,6 @@ func (lc *LocalConnector) SetFilterConfig(ctx context.Context, jail, content str
return SetFilterConfigLocal(jail, content)
}
// REMOVE THIS FUNCTION
func (lc *LocalConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEvent, error) {
logPath := lc.server.LogPath
if logPath == "" {
logPath = "/var/log/fail2ban.log"
}
eventsByJail, err := ParseBanLog(logPath)
if err != nil {
return nil, err
}
var all []BanEvent
for _, evs := range eventsByJail {
all = append(all, evs...)
}
sort.SliceStable(all, func(i, j int) bool {
return all[i].Time.After(all[j].Time)
})
if limit > 0 && len(all) > limit {
all = all[:limit]
}
return all, nil
}
// Get all jails.
func (lc *LocalConnector) getJails(ctx context.Context) ([]string, error) {
out, err := lc.runFail2banClient(ctx, "status")

View File

@@ -84,55 +84,13 @@ func (sc *SSHConnector) Server() config.Fail2banServer {
return sc.server
}
// Get jail infos for all jails.
// Collects jail status for every active remote jail.
func (sc *SSHConnector) GetJailInfos(ctx context.Context) ([]JailInfo, error) {
jails, err := sc.getJails(ctx)
if err != nil {
return nil, err
}
type jailResult struct {
jail JailInfo
err error
}
results := make(chan jailResult, len(jails))
var wg sync.WaitGroup
for _, jail := range jails {
wg.Add(1)
go func(j string) {
defer wg.Done()
ips, err := sc.GetBannedIPs(ctx, j)
if err != nil {
results <- jailResult{err: err}
return
}
results <- jailResult{
jail: JailInfo{
JailName: j,
TotalBanned: len(ips),
NewInLastHour: 0,
BannedIPs: ips,
Enabled: true,
},
}
}(jail)
}
go func() {
wg.Wait()
close(results)
}()
var infos []JailInfo
for result := range results {
if result.err != nil {
continue
}
infos = append(infos, result.jail)
}
sort.SliceStable(infos, func(i, j int) bool {
return infos[i].JailName < infos[j].JailName
})
return infos, nil
return collectJailInfos(ctx, jails, sc.GetBannedIPs)
}
// Get banned IPs for a given jail.
@@ -252,10 +210,6 @@ func (sc *SSHConnector) SetFilterConfig(ctx context.Context, filterName, content
return nil
}
func (sc *SSHConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEvent, error) {
return []BanEvent{}, nil
}
func (sc *SSHConnector) ensureAction(ctx context.Context) error {
callbackURL := config.GetCallbackURL()
settings := config.GetSettings()

View File

@@ -1,86 +0,0 @@
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
//
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
//
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// LEGACY, WILL BE REMOVED IN FUTURE VERSIONS.
package fail2ban
import (
"bufio"
"fmt"
"os"
"regexp"
"time"
)
// =========================================================================
// Types
// =========================================================================
var logRegex = regexp.MustCompile(`^(\S+\s+\S+) fail2ban\.actions.*?\[\d+\]: NOTICE\s+\[(\S+)\]\s+Ban\s+(\S+)`)
// This is a single ban event from the fail2ban log. REMOVE THIS TYPE.
type BanEvent struct {
Time time.Time
Jail string
IP string
LogLine string
}
// =========================================================================
// Log Parsing
// =========================================================================
// ParseBanLog reads the fail2ban log and returns events grouped by jail.
func ParseBanLog(logPath string) (map[string][]BanEvent, error) {
file, err := os.Open(logPath)
if err != nil {
return nil, fmt.Errorf("failed to open fail2ban log: %v", err)
}
defer file.Close()
eventsByJail := make(map[string][]BanEvent)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := logRegex.FindStringSubmatch(line)
if len(matches) == 4 {
timestampStr := matches[1]
jail := matches[2]
ip := matches[3]
parsedTime, err := time.Parse("2006-01-02 15:04:05,000", timestampStr)
if err != nil {
continue
}
ev := BanEvent{
Time: parsedTime,
Jail: jail,
IP: ip,
LogLine: line,
}
eventsByJail[jail] = append(eventsByJail[jail], ev)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return eventsByJail, nil
}

View File

@@ -25,7 +25,6 @@ type Connector interface {
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)

View File

@@ -309,7 +309,6 @@
"servers.form.socket_path": "Fail2ban-Socket-Pfad",
"servers.form.socket_path_placeholder": "/var/run/fail2ban/fail2ban.sock",
"servers.form.log_path": "Fail2ban-Logpfad",
"servers.form.log_path_placeholder": "/var/log/fail2ban.log",
"servers.form.hostname": "Server-Hostname",
"servers.form.hostname_placeholder": "optional",
"servers.form.ssh_user": "SSH-Benutzer",

View File

@@ -309,7 +309,6 @@
"servers.form.socket_path": "Fail2ban-Socket-Pfad",
"servers.form.socket_path_placeholder": "/var/run/fail2ban/fail2ban.sock",
"servers.form.log_path": "Fail2ban-Logpfad",
"servers.form.log_path_placeholder": "/var/log/fail2ban.log",
"servers.form.hostname": "Server-Hostname",
"servers.form.hostname_placeholder": "optional",
"servers.form.ssh_user": "SSH-Benutzer",

View File

@@ -309,7 +309,6 @@
"servers.form.socket_path": "Fail2ban Socket Path",
"servers.form.socket_path_placeholder": "/var/run/fail2ban/fail2ban.sock",
"servers.form.log_path": "Fail2ban Log Path",
"servers.form.log_path_placeholder": "/var/log/fail2ban.log",
"servers.form.hostname": "Server Hostname",
"servers.form.hostname_placeholder": "optional",
"servers.form.ssh_user": "SSH User",

View File

@@ -309,7 +309,6 @@
"servers.form.socket_path": "Ruta del socket de Fail2ban",
"servers.form.socket_path_placeholder": "/var/run/fail2ban/fail2ban.sock",
"servers.form.log_path": "Ruta del log de Fail2ban",
"servers.form.log_path_placeholder": "/var/log/fail2ban.log",
"servers.form.hostname": "Nombre de host del servidor",
"servers.form.hostname_placeholder": "opcional",
"servers.form.ssh_user": "Usuario SSH",

View File

@@ -309,7 +309,6 @@
"servers.form.socket_path": "Chemin du socket Fail2ban",
"servers.form.socket_path_placeholder": "/var/run/fail2ban/fail2ban.sock",
"servers.form.log_path": "Chemin du log Fail2ban",
"servers.form.log_path_placeholder": "/var/log/fail2ban.log",
"servers.form.hostname": "Nom d'hôte du serveur",
"servers.form.hostname_placeholder": "optionnel",
"servers.form.ssh_user": "Utilisateur SSH",

View File

@@ -309,7 +309,6 @@
"servers.form.socket_path": "Percorso del socket Fail2ban",
"servers.form.socket_path_placeholder": "/var/run/fail2ban/fail2ban.sock",
"servers.form.log_path": "Percorso del log Fail2ban",
"servers.form.log_path_placeholder": "/var/log/fail2ban.log",
"servers.form.hostname": "Nome host del server",
"servers.form.hostname_placeholder": "opzionale",
"servers.form.ssh_user": "Utente SSH",

View File

@@ -102,7 +102,6 @@ type ServerRecord struct {
Host string
Port int
SocketPath string
LogPath string
SSHUser string
SSHKeyPath string
AgentURL string
@@ -344,7 +343,7 @@ func ListServers(ctx context.Context) ([]ServerRecord, error) {
}
rows, err := db.QueryContext(ctx, `
SELECT id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
SELECT id, name, type, host, port, socket_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
FROM servers
ORDER BY created_at`)
if err != nil {
@@ -355,7 +354,7 @@ ORDER BY created_at`)
var records []ServerRecord
for rows.Next() {
var rec ServerRecord
var host, socket, logPath, sshUser, sshKey, agentURL, agentSecret, hostname, tags sql.NullString
var host, socket, sshUser, sshKey, agentURL, agentSecret, hostname, tags sql.NullString
var name, serverType sql.NullString
var created, updated sql.NullString
var port sql.NullInt64
@@ -368,7 +367,6 @@ ORDER BY created_at`)
&host,
&port,
&socket,
&logPath,
&sshUser,
&sshKey,
&agentURL,
@@ -389,7 +387,6 @@ ORDER BY created_at`)
rec.Host = stringFromNull(host)
rec.Port = intFromNull(port)
rec.SocketPath = stringFromNull(socket)
rec.LogPath = stringFromNull(logPath)
rec.SSHUser = stringFromNull(sshUser)
rec.SSHKeyPath = stringFromNull(sshKey)
rec.AgentURL = stringFromNull(agentURL)
@@ -438,9 +435,9 @@ func ReplaceServers(ctx context.Context, servers []ServerRecord) error {
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO servers (
id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
id, name, type, host, port, socket_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)`)
if err != nil {
return err
@@ -463,7 +460,6 @@ INSERT INTO servers (
srv.Host,
srv.Port,
srv.SocketPath,
srv.LogPath,
srv.SSHUser,
srv.SSHKeyPath,
srv.AgentURL,
@@ -1028,7 +1024,6 @@ CREATE TABLE IF NOT EXISTS servers (
host TEXT,
port INTEGER,
socket_path TEXT,
log_path TEXT,
ssh_user TEXT,
ssh_key_path TEXT,
agent_url TEXT,