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)