mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Fix the manage jails functions over ssh, also improve the speed or remote connections
This commit is contained in:
@@ -249,3 +249,45 @@ func (ac *AgentConnector) do(req *http.Request, out any) error {
|
|||||||
}
|
}
|
||||||
return json.Unmarshal(data, out)
|
return json.Unmarshal(data, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllJails implements Connector.
|
||||||
|
func (ac *AgentConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
||||||
|
var resp struct {
|
||||||
|
Jails []JailInfo `json:"jails"`
|
||||||
|
}
|
||||||
|
if err := ac.get(ctx, "/v1/jails/all", &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Jails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJailEnabledStates implements Connector.
|
||||||
|
func (ac *AgentConnector) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error {
|
||||||
|
return ac.post(ctx, "/v1/jails/update-enabled", updates, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilters implements Connector.
|
||||||
|
func (ac *AgentConnector) GetFilters(ctx context.Context) ([]string, error) {
|
||||||
|
var resp struct {
|
||||||
|
Filters []string `json:"filters"`
|
||||||
|
}
|
||||||
|
if err := ac.get(ctx, "/v1/filters", &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilter implements Connector.
|
||||||
|
func (ac *AgentConnector) TestFilter(ctx context.Context, filterName string, logLines []string) ([]string, error) {
|
||||||
|
payload := map[string]any{
|
||||||
|
"filterName": filterName,
|
||||||
|
"logLines": logLines,
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Matches []string `json:"matches"`
|
||||||
|
}
|
||||||
|
if err := ac.post(ctx, "/v1/filters/test", payload, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Matches, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||||
@@ -51,37 +52,61 @@ func (lc *LocalConnector) GetJailInfos(ctx context.Context) ([]JailInfo, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
oneHourAgo := time.Now().Add(-1 * time.Hour)
|
oneHourAgo := time.Now().Add(-1 * time.Hour)
|
||||||
var results []JailInfo
|
|
||||||
for _, jail := range jails {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
bannedIPs, err := lc.GetBannedIPs(ctx, jail)
|
// Use parallel execution for better performance
|
||||||
if err != nil {
|
type jailResult struct {
|
||||||
continue
|
jail JailInfo
|
||||||
}
|
err error
|
||||||
newInLastHour := 0
|
}
|
||||||
if events, ok := banHistory[jail]; ok {
|
results := make(chan jailResult, len(jails))
|
||||||
for _, e := range events {
|
var wg sync.WaitGroup
|
||||||
if e.Time.After(oneHourAgo) {
|
|
||||||
newInLastHour++
|
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{
|
||||||
results = append(results, JailInfo{
|
JailName: j,
|
||||||
JailName: jail,
|
TotalBanned: len(bannedIPs),
|
||||||
TotalBanned: len(bannedIPs),
|
NewInLastHour: newInLastHour,
|
||||||
NewInLastHour: newInLastHour,
|
BannedIPs: bannedIPs,
|
||||||
BannedIPs: bannedIPs,
|
Enabled: true,
|
||||||
Enabled: true,
|
},
|
||||||
})
|
}
|
||||||
|
}(jail)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBannedIPs implements Connector.
|
// GetBannedIPs implements Connector.
|
||||||
@@ -207,6 +232,26 @@ func (lc *LocalConnector) buildFail2banArgs(args ...string) []string {
|
|||||||
return append(base, args...)
|
return append(base, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllJails implements Connector.
|
||||||
|
func (lc *LocalConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
||||||
|
return GetAllJails()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJailEnabledStates implements Connector.
|
||||||
|
func (lc *LocalConnector) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error {
|
||||||
|
return UpdateJailEnabledStates(updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilters implements Connector.
|
||||||
|
func (lc *LocalConnector) GetFilters(ctx context.Context) ([]string, error) {
|
||||||
|
return GetFiltersLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilter implements Connector.
|
||||||
|
func (lc *LocalConnector) TestFilter(ctx context.Context, filterName string, logLines []string) ([]string, error) {
|
||||||
|
return TestFilterLocal(filterName, logLines)
|
||||||
|
}
|
||||||
|
|
||||||
func executeShellCommand(ctx context.Context, command string) (string, error) {
|
func executeShellCommand(ctx context.Context, command string) (string, error) {
|
||||||
parts := strings.Fields(command)
|
parts := strings.Fields(command)
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package fail2ban
|
package fail2ban
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,11 +9,12 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sshEnsureActionScript = `sudo python3 - <<'PY'
|
const sshEnsureActionScript = `python3 - <<'PY'
|
||||||
import base64
|
import base64
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
@@ -90,24 +92,46 @@ func (sc *SSHConnector) GetJailInfos(ctx context.Context) ([]JailInfo, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var infos []JailInfo
|
// Use parallel execution for better performance
|
||||||
|
type jailResult struct {
|
||||||
|
jail JailInfo
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
results := make(chan jailResult, len(jails))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, jail := range jails {
|
for _, jail := range jails {
|
||||||
select {
|
wg.Add(1)
|
||||||
case <-ctx.Done():
|
go func(j string) {
|
||||||
return nil, ctx.Err()
|
defer wg.Done()
|
||||||
default:
|
ips, err := sc.GetBannedIPs(ctx, j)
|
||||||
}
|
if err != nil {
|
||||||
ips, err := sc.GetBannedIPs(ctx, jail)
|
results <- jailResult{err: err}
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
infos = append(infos, JailInfo{
|
infos = append(infos, result.jail)
|
||||||
JailName: jail,
|
|
||||||
TotalBanned: len(ips),
|
|
||||||
NewInLastHour: 0,
|
|
||||||
BannedIPs: ips,
|
|
||||||
Enabled: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.SliceStable(infos, func(i, j int) bool {
|
sort.SliceStable(infos, func(i, j int) bool {
|
||||||
@@ -177,7 +201,10 @@ func (sc *SSHConnector) ensureAction(ctx context.Context) error {
|
|||||||
actionConfig := config.BuildFail2banActionConfig(callbackURL)
|
actionConfig := config.BuildFail2banActionConfig(callbackURL)
|
||||||
payload := base64.StdEncoding.EncodeToString([]byte(actionConfig))
|
payload := base64.StdEncoding.EncodeToString([]byte(actionConfig))
|
||||||
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
|
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
|
||||||
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", script})
|
// Base64 encode the entire script to avoid shell escaping issues
|
||||||
|
scriptB64 := base64.StdEncoding.EncodeToString([]byte(script))
|
||||||
|
cmd := fmt.Sprintf("echo %s | base64 -d | sudo bash", scriptB64)
|
||||||
|
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,3 +281,176 @@ func (sc *SSHConnector) buildSSHArgs(command []string) []string {
|
|||||||
args = append(args, command...)
|
args = append(args, command...)
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllJails implements Connector.
|
||||||
|
func (sc *SSHConnector) GetAllJails(ctx context.Context) ([]JailInfo, error) {
|
||||||
|
// Read jail.local and jail.d files remotely
|
||||||
|
var allJails []JailInfo
|
||||||
|
|
||||||
|
// Parse jail.local
|
||||||
|
jailLocalContent, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", "/etc/fail2ban/jail.local"})
|
||||||
|
if err == nil {
|
||||||
|
jails := parseJailConfigContent(jailLocalContent)
|
||||||
|
allJails = append(allJails, jails...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse jail.d directory
|
||||||
|
jailDList, err := sc.runRemoteCommand(ctx, []string{"sudo", "bash", "-c", "for f in /etc/fail2ban/jail.d/*.conf; do [ -f \"$f\" ] && echo \"$f\"; done"})
|
||||||
|
if err == nil && jailDList != "" {
|
||||||
|
for _, file := range strings.Split(jailDList, "\n") {
|
||||||
|
file = strings.TrimSpace(file)
|
||||||
|
if file == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", file})
|
||||||
|
if err == nil {
|
||||||
|
jails := parseJailConfigContent(content)
|
||||||
|
allJails = append(allJails, jails...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allJails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJailEnabledStates implements Connector.
|
||||||
|
func (sc *SSHConnector) UpdateJailEnabledStates(ctx context.Context, updates map[string]bool) error {
|
||||||
|
// Read current jail.local
|
||||||
|
content, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", "/etc/fail2ban/jail.local"})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read jail.local: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update enabled states
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
var outputLines []string
|
||||||
|
var currentJail string
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||||
|
currentJail = strings.Trim(trimmed, "[]")
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
} else if strings.HasPrefix(trimmed, "enabled") {
|
||||||
|
if val, ok := updates[currentJail]; ok {
|
||||||
|
outputLines = append(outputLines, fmt.Sprintf("enabled = %t", val))
|
||||||
|
delete(updates, currentJail)
|
||||||
|
} else {
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back
|
||||||
|
newContent := strings.Join(outputLines, "\n")
|
||||||
|
cmd := fmt.Sprintf("cat <<'EOF' | sudo tee /etc/fail2ban/jail.local >/dev/null\n%s\nEOF", newContent)
|
||||||
|
_, err = sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilters implements Connector.
|
||||||
|
func (sc *SSHConnector) GetFilters(ctx context.Context) ([]string, error) {
|
||||||
|
list, err := sc.runRemoteCommand(ctx, []string{"sudo", "bash", "-c", "for f in /etc/fail2ban/filter.d/*.conf; do [ -f \"$f\" ] && basename \"$f\" .conf; done"})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list filters: %w", err)
|
||||||
|
}
|
||||||
|
var filters []string
|
||||||
|
for _, line := range strings.Split(list, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
filters = append(filters, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilter implements Connector.
|
||||||
|
func (sc *SSHConnector) TestFilter(ctx context.Context, filterName string, logLines []string) ([]string, error) {
|
||||||
|
if len(logLines) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read filter config remotely
|
||||||
|
filterPath := fmt.Sprintf("/etc/fail2ban/filter.d/%s.conf", filterName)
|
||||||
|
content, err := sc.runRemoteCommand(ctx, []string{"sudo", "cat", filterPath})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("filter %s not found: %w", filterName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract failregex
|
||||||
|
var failregex string
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||||
|
inFailregex := false
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "[Definition]") {
|
||||||
|
inFailregex = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inFailregex && strings.HasPrefix(line, "failregex") {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
failregex = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if inFailregex && strings.HasPrefix(line, "[") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failregex == "" {
|
||||||
|
return nil, fmt.Errorf("no failregex found in filter %s", filterName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test each log line remotely
|
||||||
|
var matches []string
|
||||||
|
for _, logLine := range logLines {
|
||||||
|
if logLine == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Escape the log line and regex for shell
|
||||||
|
escapedLine := strconv.Quote(logLine)
|
||||||
|
escapedRegex := strconv.Quote(failregex)
|
||||||
|
cmd := fmt.Sprintf("echo %s | sudo fail2ban-regex - %s", escapedLine, escapedRegex)
|
||||||
|
out, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||||
|
if err == nil && strings.Contains(out, "Success") {
|
||||||
|
matches = append(matches, logLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseJailConfigContent parses jail configuration content and returns JailInfo slice.
|
||||||
|
func parseJailConfigContent(content string) []JailInfo {
|
||||||
|
var jails []JailInfo
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||||
|
var currentJail string
|
||||||
|
enabled := true
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
if currentJail != "" && currentJail != "DEFAULT" {
|
||||||
|
jails = append(jails, JailInfo{
|
||||||
|
JailName: currentJail,
|
||||||
|
Enabled: enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
currentJail = strings.Trim(line, "[]")
|
||||||
|
enabled = true
|
||||||
|
} else if strings.HasPrefix(strings.ToLower(line), "enabled") {
|
||||||
|
parts := strings.Split(line, "=")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
enabled = strings.EqualFold(value, "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if currentJail != "" && currentJail != "DEFAULT" {
|
||||||
|
jails = append(jails, JailInfo{
|
||||||
|
JailName: currentJail,
|
||||||
|
Enabled: enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return jails
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,10 +17,13 @@
|
|||||||
package fail2ban
|
package fail2ban
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFilterConfig returns the filter configuration using the default connector.
|
// GetFilterConfig returns the filter configuration using the default connector.
|
||||||
@@ -59,3 +62,73 @@ func SetFilterConfigLocal(jail, newContent string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFiltersLocal returns a list of filter names from /etc/fail2ban/filter.d
|
||||||
|
func GetFiltersLocal() ([]string, error) {
|
||||||
|
dir := "/etc/fail2ban/filter.d"
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read filter directory: %w", err)
|
||||||
|
}
|
||||||
|
var filters []string
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".conf") {
|
||||||
|
name := strings.TrimSuffix(entry.Name(), ".conf")
|
||||||
|
filters = append(filters, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilterLocal tests a filter against log lines using fail2ban-regex
|
||||||
|
func TestFilterLocal(filterName string, logLines []string) ([]string, error) {
|
||||||
|
if len(logLines) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
filterPath := filepath.Join("/etc/fail2ban/filter.d", filterName+".conf")
|
||||||
|
if _, err := os.Stat(filterPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("filter %s not found: %w", filterName, err)
|
||||||
|
}
|
||||||
|
// Read the filter config to extract the failregex
|
||||||
|
content, err := os.ReadFile(filterPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read filter config: %w", err)
|
||||||
|
}
|
||||||
|
// Extract failregex from the config
|
||||||
|
var failregex string
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(string(content)))
|
||||||
|
inFailregex := false
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "[Definition]") {
|
||||||
|
inFailregex = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inFailregex && strings.HasPrefix(line, "failregex") {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
failregex = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if inFailregex && strings.HasPrefix(line, "[") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failregex == "" {
|
||||||
|
return nil, fmt.Errorf("no failregex found in filter %s", filterName)
|
||||||
|
}
|
||||||
|
// Use fail2ban-regex to test
|
||||||
|
var matches []string
|
||||||
|
for _, logLine := range logLines {
|
||||||
|
if logLine == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd := exec.Command("fail2ban-regex", logLine, failregex)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err == nil && strings.Contains(string(out), "Success") {
|
||||||
|
matches = append(matches, logLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ type Connector interface {
|
|||||||
GetFilterConfig(ctx context.Context, jail string) (string, error)
|
GetFilterConfig(ctx context.Context, jail string) (string, error)
|
||||||
SetFilterConfig(ctx context.Context, jail, content string) error
|
SetFilterConfig(ctx context.Context, jail, content string) error
|
||||||
FetchBanEvents(ctx context.Context, limit int) ([]BanEvent, 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) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager orchestrates all connectors for configured Fail2ban servers.
|
// Manager orchestrates all connectors for configured Fail2ban servers.
|
||||||
|
|||||||
@@ -578,9 +578,12 @@ func SetJailFilterConfigHandler(c *gin.Context) {
|
|||||||
func ManageJailsHandler(c *gin.Context) {
|
func ManageJailsHandler(c *gin.Context) {
|
||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
config.DebugLog("ManageJailsHandler called (handlers.go)") // entry point
|
config.DebugLog("ManageJailsHandler called (handlers.go)") // entry point
|
||||||
// Get all jails from jail.local and jail.d directories.
|
conn, err := resolveConnector(c)
|
||||||
// This helper should parse both files and return []fail2ban.JailInfo.
|
if err != nil {
|
||||||
jails, err := fail2ban.GetAllJails()
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jails, err := conn.GetAllJails(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load jails: " + err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load jails: " + err.Error()})
|
||||||
return
|
return
|
||||||
@@ -594,21 +597,21 @@ func ManageJailsHandler(c *gin.Context) {
|
|||||||
func UpdateJailManagementHandler(c *gin.Context) {
|
func UpdateJailManagementHandler(c *gin.Context) {
|
||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
config.DebugLog("UpdateJailManagementHandler called (handlers.go)") // entry point
|
config.DebugLog("UpdateJailManagementHandler called (handlers.go)") // entry point
|
||||||
|
conn, err := resolveConnector(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
var updates map[string]bool
|
var updates map[string]bool
|
||||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON: " + err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Update jail configuration file(s) with the new enabled states.
|
// Update jail configuration file(s) with the new enabled states.
|
||||||
if err := fail2ban.UpdateJailEnabledStates(updates); err != nil {
|
if err := conn.UpdateJailEnabledStates(c.Request.Context(), updates); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update jail settings: " + err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update jail settings: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Restart the Fail2ban service.
|
|
||||||
//if err := fail2ban.RestartFail2ban(); err != nil {
|
|
||||||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload fail2ban: " + err.Error()})
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
if err := config.MarkRestartNeeded(); err != nil {
|
if err := config.MarkRestartNeeded(); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -669,43 +672,35 @@ func ListFiltersHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
server := conn.Server()
|
server := conn.Server()
|
||||||
if server.Type != "local" {
|
if server.Type == "local" {
|
||||||
c.JSON(http.StatusOK, gin.H{"filters": []string{}, "messageKey": "filter_debug.not_available"})
|
// For local, check if directory exists first
|
||||||
return
|
dir := "/etc/fail2ban/filter.d"
|
||||||
}
|
if _, statErr := os.Stat(dir); statErr != nil {
|
||||||
|
if os.IsNotExist(statErr) {
|
||||||
dir := "/etc/fail2ban/filter.d"
|
c.JSON(http.StatusOK, gin.H{"filters": []string{}, "messageKey": "filter_debug.local_missing"})
|
||||||
if _, statErr := os.Stat(dir); statErr != nil {
|
return
|
||||||
if os.IsNotExist(statErr) {
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"filters": []string{}, "messageKey": "filter_debug.local_missing"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read filter directory: " + statErr.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read filter directory: " + statErr.Error()})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := os.ReadDir(dir)
|
filters, err := conn.GetFilters(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list filters: " + err.Error()})
|
||||||
"error": "Failed to read filter directory: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var filters []string
|
|
||||||
for _, f := range files {
|
|
||||||
if !f.IsDir() && strings.HasSuffix(f.Name(), ".conf") {
|
|
||||||
name := strings.TrimSuffix(f.Name(), ".conf")
|
|
||||||
filters = append(filters, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"filters": filters})
|
c.JSON(http.StatusOK, gin.H{"filters": filters})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterHandler(c *gin.Context) {
|
func TestFilterHandler(c *gin.Context) {
|
||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
config.DebugLog("TestFilterHandler called (handlers.go)") // entry point
|
config.DebugLog("TestFilterHandler called (handlers.go)") // entry point
|
||||||
|
conn, err := resolveConnector(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
var req struct {
|
var req struct {
|
||||||
FilterName string `json:"filterName"`
|
FilterName string `json:"filterName"`
|
||||||
LogLines []string `json:"logLines"`
|
LogLines []string `json:"logLines"`
|
||||||
@@ -715,8 +710,12 @@ func TestFilterHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, just pretend nothing matches
|
matches, err := conn.TestFilter(c.Request.Context(), req.FilterName, req.LogLines)
|
||||||
c.JSON(http.StatusOK, gin.H{"matches": []string{}})
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test filter: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"matches": matches})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyFail2banSettings updates /etc/fail2ban/jail.local [DEFAULT] with our JSON
|
// ApplyFail2banSettings updates /etc/fail2ban/jail.local [DEFAULT] with our JSON
|
||||||
|
|||||||
Reference in New Issue
Block a user