mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
restructure jail.local default config functions, make banactions configurable
This commit is contained in:
@@ -24,7 +24,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -61,12 +60,14 @@ type AppSettings struct {
|
|||||||
Servers []Fail2banServer `json:"servers"`
|
Servers []Fail2banServer `json:"servers"`
|
||||||
|
|
||||||
// Fail2Ban [DEFAULT] section values from jail.local
|
// Fail2Ban [DEFAULT] section values from jail.local
|
||||||
BantimeIncrement bool `json:"bantimeIncrement"`
|
BantimeIncrement bool `json:"bantimeIncrement"`
|
||||||
IgnoreIP string `json:"ignoreip"`
|
IgnoreIPs []string `json:"ignoreips"` // Changed from string to []string for individual IP management
|
||||||
Bantime string `json:"bantime"`
|
Bantime string `json:"bantime"`
|
||||||
Findtime string `json:"findtime"`
|
Findtime string `json:"findtime"`
|
||||||
Maxretry int `json:"maxretry"`
|
Maxretry int `json:"maxretry"`
|
||||||
Destemail string `json:"destemail"`
|
Destemail string `json:"destemail"`
|
||||||
|
Banaction string `json:"banaction"` // Default banning action
|
||||||
|
BanactionAllports string `json:"banactionAllports"` // Allports banning action
|
||||||
//Sender string `json:"sender"`
|
//Sender string `json:"sender"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +333,12 @@ func applyAppSettingsRecordLocked(rec storage.AppSettingsRecord) {
|
|||||||
currentSettings.CallbackURL = rec.CallbackURL
|
currentSettings.CallbackURL = rec.CallbackURL
|
||||||
currentSettings.RestartNeeded = rec.RestartNeeded
|
currentSettings.RestartNeeded = rec.RestartNeeded
|
||||||
currentSettings.BantimeIncrement = rec.BantimeIncrement
|
currentSettings.BantimeIncrement = rec.BantimeIncrement
|
||||||
currentSettings.IgnoreIP = rec.IgnoreIP
|
// Convert IgnoreIP string to array (backward compatibility)
|
||||||
|
if rec.IgnoreIP != "" {
|
||||||
|
currentSettings.IgnoreIPs = strings.Fields(rec.IgnoreIP)
|
||||||
|
} else {
|
||||||
|
currentSettings.IgnoreIPs = []string{}
|
||||||
|
}
|
||||||
currentSettings.Bantime = rec.Bantime
|
currentSettings.Bantime = rec.Bantime
|
||||||
currentSettings.Findtime = rec.Findtime
|
currentSettings.Findtime = rec.Findtime
|
||||||
currentSettings.Maxretry = rec.MaxRetry
|
currentSettings.Maxretry = rec.MaxRetry
|
||||||
@@ -409,24 +415,27 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return storage.AppSettingsRecord{
|
return storage.AppSettingsRecord{
|
||||||
Language: currentSettings.Language,
|
Language: currentSettings.Language,
|
||||||
Port: currentSettings.Port,
|
Port: currentSettings.Port,
|
||||||
Debug: currentSettings.Debug,
|
Debug: currentSettings.Debug,
|
||||||
CallbackURL: currentSettings.CallbackURL,
|
CallbackURL: currentSettings.CallbackURL,
|
||||||
RestartNeeded: currentSettings.RestartNeeded,
|
RestartNeeded: currentSettings.RestartNeeded,
|
||||||
AlertCountriesJSON: string(countryBytes),
|
AlertCountriesJSON: string(countryBytes),
|
||||||
SMTPHost: currentSettings.SMTP.Host,
|
SMTPHost: currentSettings.SMTP.Host,
|
||||||
SMTPPort: currentSettings.SMTP.Port,
|
SMTPPort: currentSettings.SMTP.Port,
|
||||||
SMTPUsername: currentSettings.SMTP.Username,
|
SMTPUsername: currentSettings.SMTP.Username,
|
||||||
SMTPPassword: currentSettings.SMTP.Password,
|
SMTPPassword: currentSettings.SMTP.Password,
|
||||||
SMTPFrom: currentSettings.SMTP.From,
|
SMTPFrom: currentSettings.SMTP.From,
|
||||||
SMTPUseTLS: currentSettings.SMTP.UseTLS,
|
SMTPUseTLS: currentSettings.SMTP.UseTLS,
|
||||||
BantimeIncrement: currentSettings.BantimeIncrement,
|
BantimeIncrement: currentSettings.BantimeIncrement,
|
||||||
IgnoreIP: currentSettings.IgnoreIP,
|
// Convert IgnoreIPs array to space-separated string for storage
|
||||||
|
IgnoreIP: strings.Join(currentSettings.IgnoreIPs, " "),
|
||||||
Bantime: currentSettings.Bantime,
|
Bantime: currentSettings.Bantime,
|
||||||
Findtime: currentSettings.Findtime,
|
Findtime: currentSettings.Findtime,
|
||||||
MaxRetry: currentSettings.Maxretry,
|
MaxRetry: currentSettings.Maxretry,
|
||||||
DestEmail: currentSettings.Destemail,
|
DestEmail: currentSettings.Destemail,
|
||||||
|
Banaction: currentSettings.Banaction,
|
||||||
|
BanactionAllports: currentSettings.BanactionAllports,
|
||||||
AdvancedActionsJSON: string(advancedBytes),
|
AdvancedActionsJSON: string(advancedBytes),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -538,8 +547,14 @@ func setDefaultsLocked() {
|
|||||||
if !currentSettings.SMTP.UseTLS {
|
if !currentSettings.SMTP.UseTLS {
|
||||||
currentSettings.SMTP.UseTLS = true
|
currentSettings.SMTP.UseTLS = true
|
||||||
}
|
}
|
||||||
if currentSettings.IgnoreIP == "" {
|
if len(currentSettings.IgnoreIPs) == 0 {
|
||||||
currentSettings.IgnoreIP = "127.0.0.1/8 ::1"
|
currentSettings.IgnoreIPs = []string{"127.0.0.1/8", "::1"}
|
||||||
|
}
|
||||||
|
if currentSettings.Banaction == "" {
|
||||||
|
currentSettings.Banaction = "iptables-multiport"
|
||||||
|
}
|
||||||
|
if currentSettings.BanactionAllports == "" {
|
||||||
|
currentSettings.BanactionAllports = "iptables-allports"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSettings.AdvancedActions == AdvancedActionsConfig{}) {
|
if (currentSettings.AdvancedActions == AdvancedActionsConfig{}) {
|
||||||
@@ -586,7 +601,18 @@ func initializeFromJailFile() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if val, ok := settings["ignoreip"]; ok {
|
if val, ok := settings["ignoreip"]; ok {
|
||||||
currentSettings.IgnoreIP = val
|
// Convert space-separated string to array
|
||||||
|
if val != "" {
|
||||||
|
currentSettings.IgnoreIPs = strings.Fields(val)
|
||||||
|
} else {
|
||||||
|
currentSettings.IgnoreIPs = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val, ok := settings["banaction"]; ok {
|
||||||
|
currentSettings.Banaction = val
|
||||||
|
}
|
||||||
|
if val, ok := settings["banaction_allports"]; ok {
|
||||||
|
currentSettings.BanactionAllports = val
|
||||||
}
|
}
|
||||||
if val, ok := settings["destemail"]; ok {
|
if val, ok := settings["destemail"]; ok {
|
||||||
currentSettings.Destemail = val
|
currentSettings.Destemail = val
|
||||||
@@ -695,124 +721,203 @@ func ensureFail2banActionFiles(callbackURL, serverID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setupGeoCustomAction(); err != nil {
|
// Ensure jail.local has proper structure (banner, DEFAULT, action_mwlg, action override)
|
||||||
return err
|
if err := ensureJailLocalStructure(); err != nil {
|
||||||
}
|
|
||||||
if err := ensureJailDConfig(); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeFail2banAction(callbackURL, serverID)
|
return writeFail2banAction(callbackURL, serverID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupGeoCustomAction checks and replaces the default action in jail.local with our from fail2ban-UI
|
// EnsureJailLocalStructure creates or updates jail.local with proper structure:
|
||||||
func setupGeoCustomAction() error {
|
// This is exported so connectors can call it.
|
||||||
DebugLog("Running initial setupGeoCustomAction()") // entry point
|
func EnsureJailLocalStructure() error {
|
||||||
if err := os.MkdirAll(filepath.Dir(jailFile), 0o755); err != nil {
|
return ensureJailLocalStructure()
|
||||||
return fmt.Errorf("failed to ensure jail.local directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(jailFile)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
if _, statErr := os.Stat(defaultJailFile); os.IsNotExist(statErr) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if copyErr := copyFile(defaultJailFile, jailFile); copyErr != nil {
|
|
||||||
return fmt.Errorf("failed to copy default jail.conf to jail.local: %w", copyErr)
|
|
||||||
}
|
|
||||||
file, err = os.Open(jailFile)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var lines []string
|
|
||||||
actionPattern := regexp.MustCompile(`^\s*action\s*=\s*%(.*?)\s*$`)
|
|
||||||
alreadyModified := false
|
|
||||||
actionFound := false
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
|
|
||||||
// Check if we already modified the file (prevent duplicate modifications)
|
|
||||||
if strings.Contains(line, "# Custom Fail2Ban action applied") {
|
|
||||||
alreadyModified = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for an existing action definition
|
|
||||||
if actionPattern.MatchString(line) && !alreadyModified {
|
|
||||||
actionFound = true
|
|
||||||
|
|
||||||
// Comment out the existing action line
|
|
||||||
lines = append(lines, "# "+line)
|
|
||||||
|
|
||||||
// Add our replacement action with a comment marker
|
|
||||||
lines = append(lines, "# Custom Fail2Ban action applied by fail2ban-ui")
|
|
||||||
lines = append(lines, "action = %(action_mwlg)s")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the original line
|
|
||||||
lines = append(lines, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no action was found, no need to modify the file
|
|
||||||
if !actionFound || alreadyModified {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write back the modified lines
|
|
||||||
output := strings.Join(lines, "\n")
|
|
||||||
return os.WriteFile(jailFile, []byte(output), 0644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyFile copies a file from src to dst. If the destination file does not exist, it will be created.
|
// ensureJailLocalStructure creates or updates jail.local with proper structure:
|
||||||
func copyFile(src, dst string) error {
|
// 1. Banner at top warning users not to edit manually
|
||||||
sourceFile, err := os.Open(src)
|
// 2. [DEFAULT] section with current UI settings
|
||||||
if err != nil {
|
// 3. action_mwlg configuration
|
||||||
return err
|
// 4. action = %(action_mwlg)s at the end
|
||||||
}
|
func ensureJailLocalStructure() error {
|
||||||
defer sourceFile.Close()
|
DebugLog("Running ensureJailLocalStructure()") // entry point
|
||||||
|
|
||||||
destFile, err := os.Create(dst)
|
// Check if /etc/fail2ban directory exists (fail2ban must be installed)
|
||||||
if err != nil {
|
if _, err := os.Stat(filepath.Dir(jailFile)); os.IsNotExist(err) {
|
||||||
return err
|
return fmt.Errorf("fail2ban is not installed: /etc/fail2ban directory does not exist. Please install fail2ban package first")
|
||||||
}
|
|
||||||
defer destFile.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(destFile, sourceFile)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureJailDConfig checks if the jail.d file exists and creates it if necessary
|
|
||||||
func ensureJailDConfig() error {
|
|
||||||
DebugLog("Running initial ensureJailDConfig()") // entry point
|
|
||||||
// Check if the file already exists
|
|
||||||
if _, err := os.Stat(jailDFile); err == nil {
|
|
||||||
// File already exists, do nothing
|
|
||||||
DebugLog("Custom jail.d configuration already exists.")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(jailDFile), 0o755); err != nil {
|
// Get current settings
|
||||||
return fmt.Errorf("failed to ensure jail.d directory: %v", err)
|
settings := GetSettings()
|
||||||
|
|
||||||
|
// Read existing jail.local content if it exists
|
||||||
|
var existingContent string
|
||||||
|
if content, err := os.ReadFile(jailFile); err == nil {
|
||||||
|
existingContent = string(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the content for the custom jail.d configuration
|
// Check if file already has our banner (indicating it's already structured)
|
||||||
jailDConfig := `[DEFAULT]
|
hasBanner := strings.Contains(existingContent, "Fail2Ban-UI") || strings.Contains(existingContent, "fail2ban-ui")
|
||||||
# Custom Fail2Ban action using geo-filter for email alerts
|
hasActionMwlg := strings.Contains(existingContent, "action_mwlg") && strings.Contains(existingContent, "ui-custom-action")
|
||||||
|
hasActionOverride := strings.Contains(existingContent, "action = %(action_mwlg)s")
|
||||||
|
|
||||||
|
// If file is already properly structured, just ensure DEFAULT section is up to date
|
||||||
|
if hasBanner && hasActionMwlg && hasActionOverride {
|
||||||
|
DebugLog("jail.local already has proper structure, updating DEFAULT section if needed")
|
||||||
|
// Update DEFAULT section values without changing structure
|
||||||
|
return updateJailLocalDefaultSection(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the banner
|
||||||
|
banner := `################################################################################
|
||||||
|
# Fail2Ban-UI Managed Configuration
|
||||||
|
#
|
||||||
|
# WARNING: This file is automatically managed by Fail2Ban-UI.
|
||||||
|
# DO NOT EDIT THIS FILE MANUALLY - your changes will be overwritten.
|
||||||
|
#
|
||||||
|
# This file overrides settings from /etc/fail2ban/jail.conf
|
||||||
|
# Custom jail configurations should be placed in /etc/fail2ban/jail.d/
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// Build [DEFAULT] section
|
||||||
|
// Convert IgnoreIPs array to space-separated string
|
||||||
|
ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
|
||||||
|
if ignoreIPStr == "" {
|
||||||
|
ignoreIPStr = "127.0.0.1/8 ::1"
|
||||||
|
}
|
||||||
|
// Set default banaction values if not set
|
||||||
|
banaction := settings.Banaction
|
||||||
|
if banaction == "" {
|
||||||
|
banaction = "iptables-multiport"
|
||||||
|
}
|
||||||
|
banactionAllports := settings.BanactionAllports
|
||||||
|
if banactionAllports == "" {
|
||||||
|
banactionAllports = "iptables-allports"
|
||||||
|
}
|
||||||
|
defaultSection := fmt.Sprintf(`[DEFAULT]
|
||||||
|
bantime.increment = %t
|
||||||
|
ignoreip = %s
|
||||||
|
bantime = %s
|
||||||
|
findtime = %s
|
||||||
|
maxretry = %d
|
||||||
|
destemail = %s
|
||||||
|
banaction = %s
|
||||||
|
banaction_allports = %s
|
||||||
|
|
||||||
|
`, settings.BantimeIncrement, ignoreIPStr, settings.Bantime, settings.Findtime, settings.Maxretry, settings.Destemail, banaction, banactionAllports)
|
||||||
|
|
||||||
|
// Build action_mwlg configuration
|
||||||
|
// Note: action_mwlg depends on action_ which depends on banaction (now defined above)
|
||||||
|
// The multi-line format uses indentation for continuation
|
||||||
|
actionMwlgConfig := `# Custom Fail2Ban action using geo-filter for email alerts
|
||||||
action_mwlg = %(action_)s
|
action_mwlg = %(action_)s
|
||||||
ui-custom-action[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
|
ui-custom-action[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
|
||||||
|
|
||||||
`
|
`
|
||||||
// Write the new configuration file
|
|
||||||
err := os.WriteFile(jailDFile, []byte(jailDConfig), 0644)
|
// Build action override (at the end as per user requirements)
|
||||||
|
actionOverride := `# Custom Fail2Ban action applied by fail2ban-ui
|
||||||
|
action = %(action_mwlg)s
|
||||||
|
`
|
||||||
|
|
||||||
|
// Combine all parts
|
||||||
|
newContent := banner + defaultSection + actionMwlgConfig + actionOverride
|
||||||
|
|
||||||
|
// Write the new content
|
||||||
|
err := os.WriteFile(jailFile, []byte(newContent), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write jail.d config: %v", err)
|
return fmt.Errorf("failed to write jail.local: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLog("Created/updated jail.local with proper structure")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateJailLocalDefaultSection updates only the [DEFAULT] section values in jail.local
|
||||||
|
// while preserving the banner, action_mwlg, and action override
|
||||||
|
func updateJailLocalDefaultSection(settings AppSettings) error {
|
||||||
|
content, err := os.ReadFile(jailFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read jail.local: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
var outputLines []string
|
||||||
|
inDefault := false
|
||||||
|
defaultUpdated := false
|
||||||
|
|
||||||
|
// Keys to update
|
||||||
|
keysToUpdate := map[string]string{
|
||||||
|
"bantime.increment": fmt.Sprintf("bantime.increment = %t", settings.BantimeIncrement),
|
||||||
|
"ignoreip": fmt.Sprintf("ignoreip = %s", strings.Join(settings.IgnoreIPs, " ")),
|
||||||
|
"bantime": fmt.Sprintf("bantime = %s", settings.Bantime),
|
||||||
|
"findtime": fmt.Sprintf("findtime = %s", settings.Findtime),
|
||||||
|
"maxretry": fmt.Sprintf("maxretry = %d", settings.Maxretry),
|
||||||
|
"destemail": fmt.Sprintf("destemail = %s", settings.Destemail),
|
||||||
|
}
|
||||||
|
keysUpdated := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||||
|
sectionName := strings.Trim(trimmed, "[]")
|
||||||
|
if sectionName == "DEFAULT" {
|
||||||
|
inDefault = true
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
} else {
|
||||||
|
inDefault = false
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
} else if inDefault {
|
||||||
|
// Check if this line is a key we need to update
|
||||||
|
keyUpdated := false
|
||||||
|
for key, newValue := range keysToUpdate {
|
||||||
|
keyPattern := "^\\s*" + regexp.QuoteMeta(key) + "\\s*="
|
||||||
|
if matched, _ := regexp.MatchString(keyPattern, trimmed); matched {
|
||||||
|
outputLines = append(outputLines, newValue)
|
||||||
|
keysUpdated[key] = true
|
||||||
|
keyUpdated = true
|
||||||
|
defaultUpdated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !keyUpdated {
|
||||||
|
// Keep the line as-is
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep lines outside DEFAULT section
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any missing keys to the DEFAULT section
|
||||||
|
if inDefault {
|
||||||
|
for key, newValue := range keysToUpdate {
|
||||||
|
if !keysUpdated[key] {
|
||||||
|
// Find the DEFAULT section and insert after it
|
||||||
|
for i, outputLine := range outputLines {
|
||||||
|
if strings.TrimSpace(outputLine) == "[DEFAULT]" {
|
||||||
|
outputLines = append(outputLines[:i+1], append([]string{newValue}, outputLines[i+1:]...)...)
|
||||||
|
defaultUpdated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultUpdated {
|
||||||
|
newContent := strings.Join(outputLines, "\n")
|
||||||
|
if err := os.WriteFile(jailFile, []byte(newContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write jail.local: %w", err)
|
||||||
|
}
|
||||||
|
DebugLog("Updated DEFAULT section in jail.local")
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugLog("Created custom jail.d configuration at: %v", jailDFile)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,8 +925,10 @@ action_mwlg = %(action_)s
|
|||||||
func writeFail2banAction(callbackURL, serverID string) error {
|
func writeFail2banAction(callbackURL, serverID string) error {
|
||||||
DebugLog("Running initial writeFail2banAction()") // entry point
|
DebugLog("Running initial writeFail2banAction()") // entry point
|
||||||
DebugLog("----------------------------")
|
DebugLog("----------------------------")
|
||||||
if err := os.MkdirAll(filepath.Dir(actionFile), 0o755); err != nil {
|
|
||||||
return fmt.Errorf("failed to ensure action.d directory: %w", err)
|
// Check if /etc/fail2ban/action.d directory exists (fail2ban must be installed)
|
||||||
|
if _, err := os.Stat(filepath.Dir(actionFile)); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("fail2ban is not installed: /etc/fail2ban/action.d directory does not exist. Please install fail2ban package first")
|
||||||
}
|
}
|
||||||
|
|
||||||
actionConfig := BuildFail2banActionConfig(callbackURL, serverID)
|
actionConfig := BuildFail2banActionConfig(callbackURL, serverID)
|
||||||
@@ -1185,8 +1292,20 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
|
|||||||
old := currentSettings
|
old := currentSettings
|
||||||
|
|
||||||
// If certain fields change, we mark reload needed
|
// If certain fields change, we mark reload needed
|
||||||
|
// Compare IgnoreIPs arrays
|
||||||
|
ignoreIPsChanged := false
|
||||||
|
if len(old.IgnoreIPs) != len(new.IgnoreIPs) {
|
||||||
|
ignoreIPsChanged = true
|
||||||
|
} else {
|
||||||
|
for i := range old.IgnoreIPs {
|
||||||
|
if old.IgnoreIPs[i] != new.IgnoreIPs[i] {
|
||||||
|
ignoreIPsChanged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
restartTriggered := old.BantimeIncrement != new.BantimeIncrement ||
|
restartTriggered := old.BantimeIncrement != new.BantimeIncrement ||
|
||||||
old.IgnoreIP != new.IgnoreIP ||
|
ignoreIPsChanged ||
|
||||||
old.Bantime != new.Bantime ||
|
old.Bantime != new.Bantime ||
|
||||||
old.Findtime != new.Findtime ||
|
old.Findtime != new.Findtime ||
|
||||||
old.Maxretry != new.Maxretry
|
old.Maxretry != new.Maxretry
|
||||||
|
|||||||
@@ -320,3 +320,40 @@ func (ac *AgentConnector) TestLogpath(ctx context.Context, logpath string) ([]st
|
|||||||
}
|
}
|
||||||
return resp.Files, nil
|
return resp.Files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDefaultSettings implements Connector.
|
||||||
|
func (ac *AgentConnector) UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error {
|
||||||
|
// Convert IgnoreIPs array to space-separated string
|
||||||
|
ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
|
||||||
|
if ignoreIPStr == "" {
|
||||||
|
ignoreIPStr = "127.0.0.1/8 ::1"
|
||||||
|
}
|
||||||
|
// Set default banaction values if not set
|
||||||
|
banaction := settings.Banaction
|
||||||
|
if banaction == "" {
|
||||||
|
banaction = "iptables-multiport"
|
||||||
|
}
|
||||||
|
banactionAllports := settings.BanactionAllports
|
||||||
|
if banactionAllports == "" {
|
||||||
|
banactionAllports = "iptables-allports"
|
||||||
|
}
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"bantimeIncrement": settings.BantimeIncrement,
|
||||||
|
"ignoreip": ignoreIPStr,
|
||||||
|
"bantime": settings.Bantime,
|
||||||
|
"findtime": settings.Findtime,
|
||||||
|
"maxretry": settings.Maxretry,
|
||||||
|
"destemail": settings.Destemail,
|
||||||
|
"banaction": banaction,
|
||||||
|
"banactionAllports": banactionAllports,
|
||||||
|
}
|
||||||
|
return ac.put(ctx, "/v1/jails/default-settings", payload, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureJailLocalStructure implements Connector.
|
||||||
|
func (ac *AgentConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||||
|
// Call agent API endpoint to ensure jail.local structure
|
||||||
|
// If the endpoint doesn't exist, we'll need to implement it on the agent side
|
||||||
|
// For now, we'll try calling it and handle the error gracefully
|
||||||
|
return ac.post(ctx, "/v1/jails/ensure-structure", nil, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,8 +142,14 @@ func (lc *LocalConnector) UnbanIP(ctx context.Context, jail, ip string) error {
|
|||||||
|
|
||||||
// Reload implements Connector.
|
// Reload implements Connector.
|
||||||
func (lc *LocalConnector) Reload(ctx context.Context) error {
|
func (lc *LocalConnector) Reload(ctx context.Context) error {
|
||||||
if _, err := lc.runFail2banClient(ctx, "reload"); err != nil {
|
out, err := lc.runFail2banClient(ctx, "reload")
|
||||||
return fmt.Errorf("fail2ban reload error: %w", err)
|
if err != nil {
|
||||||
|
// Include the output in the error message for better debugging
|
||||||
|
return fmt.Errorf("fail2ban reload error: %w (output: %s)", err, strings.TrimSpace(out))
|
||||||
|
}
|
||||||
|
// Check if output indicates success (fail2ban-client returns "OK" on success)
|
||||||
|
if strings.TrimSpace(out) != "OK" && strings.TrimSpace(out) != "" {
|
||||||
|
config.DebugLog("fail2ban reload output: %s", out)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -267,6 +273,16 @@ func (lc *LocalConnector) TestLogpath(ctx context.Context, logpath string) ([]st
|
|||||||
return TestLogpath(logpath)
|
return TestLogpath(logpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDefaultSettings implements Connector.
|
||||||
|
func (lc *LocalConnector) UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error {
|
||||||
|
return UpdateDefaultSettingsLocal(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureJailLocalStructure implements Connector.
|
||||||
|
func (lc *LocalConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||||
|
return config.EnsureJailLocalStructure()
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -591,6 +591,333 @@ fi
|
|||||||
return matches, nil
|
return matches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDefaultSettings implements Connector.
|
||||||
|
func (sc *SSHConnector) UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error {
|
||||||
|
jailLocalPath := "/etc/fail2ban/jail.local"
|
||||||
|
|
||||||
|
// Read existing file if it exists
|
||||||
|
existingContent, err := sc.runRemoteCommand(ctx, []string{"cat", jailLocalPath})
|
||||||
|
if err != nil {
|
||||||
|
// File doesn't exist, create new one
|
||||||
|
existingContent = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove commented lines (lines starting with #) using sed
|
||||||
|
if existingContent != "" {
|
||||||
|
// Use sed to remove lines starting with # (but preserve empty lines)
|
||||||
|
removeCommentsCmd := fmt.Sprintf("sed '/^[[:space:]]*#/d' %s", jailLocalPath)
|
||||||
|
uncommentedContent, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", removeCommentsCmd})
|
||||||
|
if err == nil {
|
||||||
|
existingContent = uncommentedContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert IgnoreIPs array to space-separated string
|
||||||
|
ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
|
||||||
|
if ignoreIPStr == "" {
|
||||||
|
ignoreIPStr = "127.0.0.1/8 ::1"
|
||||||
|
}
|
||||||
|
// Set default banaction values if not set
|
||||||
|
banactionVal := settings.Banaction
|
||||||
|
if banactionVal == "" {
|
||||||
|
banactionVal = "iptables-multiport"
|
||||||
|
}
|
||||||
|
banactionAllportsVal := settings.BanactionAllports
|
||||||
|
if banactionAllportsVal == "" {
|
||||||
|
banactionAllportsVal = "iptables-allports"
|
||||||
|
}
|
||||||
|
// Define the keys we want to update
|
||||||
|
keysToUpdate := map[string]string{
|
||||||
|
"bantime.increment": fmt.Sprintf("bantime.increment = %t", settings.BantimeIncrement),
|
||||||
|
"ignoreip": fmt.Sprintf("ignoreip = %s", ignoreIPStr),
|
||||||
|
"bantime": fmt.Sprintf("bantime = %s", settings.Bantime),
|
||||||
|
"findtime": fmt.Sprintf("findtime = %s", settings.Findtime),
|
||||||
|
"maxretry": fmt.Sprintf("maxretry = %d", settings.Maxretry),
|
||||||
|
"destemail": fmt.Sprintf("destemail = %s", settings.Destemail),
|
||||||
|
"banaction": fmt.Sprintf("banaction = %s", banactionVal),
|
||||||
|
"banaction_allports": fmt.Sprintf("banaction_allports = %s", banactionAllportsVal),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse existing content and update only specific keys in DEFAULT section
|
||||||
|
if existingContent == "" {
|
||||||
|
// File doesn't exist, create new one with DEFAULT section
|
||||||
|
defaultLines := []string{"[DEFAULT]"}
|
||||||
|
for _, key := range []string{"bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail", "banaction", "banaction_allports"} {
|
||||||
|
defaultLines = append(defaultLines, keysToUpdate[key])
|
||||||
|
}
|
||||||
|
defaultLines = append(defaultLines, "")
|
||||||
|
newContent := strings.Join(defaultLines, "\n")
|
||||||
|
cmd := fmt.Sprintf("cat <<'EOF' | tee %s >/dev/null\n%s\nEOF", jailLocalPath, newContent)
|
||||||
|
_, err = sc.runRemoteCommand(ctx, []string{"bash", "-lc", cmd})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Python script to update only specific keys in DEFAULT section
|
||||||
|
// Preserves banner, action_mwlg, and action override sections
|
||||||
|
// Escape values for shell/Python
|
||||||
|
escapeForShell := func(s string) string {
|
||||||
|
// Escape single quotes for shell
|
||||||
|
return strings.ReplaceAll(s, "'", "'\"'\"'")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScript := fmt.Sprintf(`python3 <<'PY'
|
||||||
|
import re
|
||||||
|
|
||||||
|
jail_file = '%s'
|
||||||
|
ignore_ip_str = '%s'
|
||||||
|
banaction_val = '%s'
|
||||||
|
banaction_allports_val = '%s'
|
||||||
|
bantime_increment_val = %t
|
||||||
|
keys_to_update = {
|
||||||
|
'bantime.increment': 'bantime.increment = ' + str(bantime_increment_val),
|
||||||
|
'ignoreip': 'ignoreip = ' + ignore_ip_str,
|
||||||
|
'bantime': 'bantime = %s',
|
||||||
|
'findtime': 'findtime = %s',
|
||||||
|
'maxretry': 'maxretry = %d',
|
||||||
|
'destemail': 'destemail = %s',
|
||||||
|
'banaction': 'banaction = ' + banaction_val,
|
||||||
|
'banaction_allports': 'banaction_allports = ' + banaction_allports_val
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(jail_file, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
output_lines = []
|
||||||
|
in_default = False
|
||||||
|
default_section_found = False
|
||||||
|
keys_updated = set()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# Preserve banner lines, action_mwlg lines, and action override lines
|
||||||
|
is_banner = 'Fail2Ban-UI' in line or 'fail2ban-ui' in line
|
||||||
|
is_action_mwlg = 'action_mwlg' in stripped
|
||||||
|
is_action_override = 'action = %%(action_mwlg)s' in stripped
|
||||||
|
|
||||||
|
if stripped.startswith('[') and stripped.endswith(']'):
|
||||||
|
section_name = stripped.strip('[]')
|
||||||
|
if section_name == "DEFAULT":
|
||||||
|
in_default = True
|
||||||
|
default_section_found = True
|
||||||
|
output_lines.append(line)
|
||||||
|
else:
|
||||||
|
in_default = False
|
||||||
|
output_lines.append(line)
|
||||||
|
elif in_default:
|
||||||
|
# Check if this line is a key we need to update
|
||||||
|
key_updated = False
|
||||||
|
for key, new_value in keys_to_update.items():
|
||||||
|
pattern = r'^\s*' + re.escape(key) + r'\s*='
|
||||||
|
if re.match(pattern, stripped):
|
||||||
|
output_lines.append(new_value + '\n')
|
||||||
|
keys_updated.add(key)
|
||||||
|
key_updated = True
|
||||||
|
break
|
||||||
|
if not key_updated:
|
||||||
|
# Keep the line as-is (might be action_mwlg or other DEFAULT settings)
|
||||||
|
output_lines.append(line)
|
||||||
|
else:
|
||||||
|
# Keep lines outside DEFAULT section (preserves banner, action_mwlg, action override)
|
||||||
|
output_lines.append(line)
|
||||||
|
|
||||||
|
# If DEFAULT section wasn't found, create it at the beginning
|
||||||
|
if not default_section_found:
|
||||||
|
default_lines = ["[DEFAULT]\n"]
|
||||||
|
for key in ["bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail"]:
|
||||||
|
default_lines.append(keys_to_update[key] + "\n")
|
||||||
|
default_lines.append("\n")
|
||||||
|
output_lines = default_lines + output_lines
|
||||||
|
else:
|
||||||
|
# Add any missing keys to the DEFAULT section
|
||||||
|
for key in ["bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail"]:
|
||||||
|
if key not in keys_updated:
|
||||||
|
# Find the DEFAULT section and insert after it
|
||||||
|
for i, line in enumerate(output_lines):
|
||||||
|
if line.strip() == "[DEFAULT]":
|
||||||
|
output_lines.insert(i + 1, keys_to_update[key] + "\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(jail_file, 'w') as f:
|
||||||
|
f.writelines(output_lines)
|
||||||
|
PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(banactionVal), escapeForShell(banactionAllportsVal), settings.BantimeIncrement, escapeForShell(settings.Bantime), escapeForShell(settings.Findtime), settings.Maxretry, escapeForShell(settings.Destemail))
|
||||||
|
|
||||||
|
_, err = sc.runRemoteCommand(ctx, []string{"bash", "-lc", updateScript})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureJailLocalStructure implements Connector.
|
||||||
|
func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error {
|
||||||
|
jailLocalPath := "/etc/fail2ban/jail.local"
|
||||||
|
settings := config.GetSettings()
|
||||||
|
|
||||||
|
// Convert IgnoreIPs array to space-separated string
|
||||||
|
ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
|
||||||
|
if ignoreIPStr == "" {
|
||||||
|
ignoreIPStr = "127.0.0.1/8 ::1"
|
||||||
|
}
|
||||||
|
// Set default banaction values if not set
|
||||||
|
banactionVal := settings.Banaction
|
||||||
|
if banactionVal == "" {
|
||||||
|
banactionVal = "iptables-multiport"
|
||||||
|
}
|
||||||
|
banactionAllportsVal := settings.BanactionAllports
|
||||||
|
if banactionAllportsVal == "" {
|
||||||
|
banactionAllportsVal = "iptables-allports"
|
||||||
|
}
|
||||||
|
// Escape values for shell/Python
|
||||||
|
escapeForShell := func(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", "'\"'\"'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the structure using Python script
|
||||||
|
ensureScript := fmt.Sprintf(`python3 <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
jail_file = '%s'
|
||||||
|
ignore_ip_str = '%s'
|
||||||
|
banaction_val = '%s'
|
||||||
|
banaction_allports_val = '%s'
|
||||||
|
settings = {
|
||||||
|
'bantime_increment': %t,
|
||||||
|
'ignoreip': ignore_ip_str,
|
||||||
|
'bantime': '%s',
|
||||||
|
'findtime': '%s',
|
||||||
|
'maxretry': %d,
|
||||||
|
'destemail': '%s',
|
||||||
|
'banaction': banaction_val,
|
||||||
|
'banaction_allports': banaction_allports_val
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if file already has our banner
|
||||||
|
has_banner = False
|
||||||
|
has_action_mwlg = False
|
||||||
|
has_action_override = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(jail_file, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
has_banner = 'Fail2Ban-UI' in content or 'fail2ban-ui' in content
|
||||||
|
has_action_mwlg = 'action_mwlg' in content and 'ui-custom-action' in content
|
||||||
|
has_action_override = 'action = %%(action_mwlg)s' in content
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If already properly structured, just update DEFAULT section
|
||||||
|
if has_banner and has_action_mwlg and has_action_override:
|
||||||
|
try:
|
||||||
|
with open(jail_file, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
output_lines = []
|
||||||
|
in_default = False
|
||||||
|
keys_updated = set()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if stripped.startswith('[') and stripped.endswith(']'):
|
||||||
|
section_name = stripped.strip('[]')
|
||||||
|
if section_name == "DEFAULT":
|
||||||
|
in_default = True
|
||||||
|
output_lines.append(line)
|
||||||
|
else:
|
||||||
|
in_default = False
|
||||||
|
output_lines.append(line)
|
||||||
|
elif in_default:
|
||||||
|
key_updated = False
|
||||||
|
for key, new_value in [
|
||||||
|
('bantime.increment', 'bantime.increment = ' + str(settings['bantime_increment'])),
|
||||||
|
('ignoreip', 'ignoreip = ' + settings['ignoreip']),
|
||||||
|
('bantime', 'bantime = ' + settings['bantime']),
|
||||||
|
('findtime', 'findtime = ' + settings['findtime']),
|
||||||
|
('maxretry', 'maxretry = ' + str(settings['maxretry'])),
|
||||||
|
('destemail', 'destemail = ' + settings['destemail']),
|
||||||
|
('banaction', 'banaction = ' + settings['banaction']),
|
||||||
|
('banaction_allports', 'banaction_allports = ' + settings['banaction_allports']),
|
||||||
|
]:
|
||||||
|
pattern = r'^\s*' + re.escape(key) + r'\s*='
|
||||||
|
if re.match(pattern, stripped):
|
||||||
|
output_lines.append(new_value + '\n')
|
||||||
|
keys_updated.add(key)
|
||||||
|
key_updated = True
|
||||||
|
break
|
||||||
|
if not key_updated:
|
||||||
|
output_lines.append(line)
|
||||||
|
else:
|
||||||
|
output_lines.append(line)
|
||||||
|
|
||||||
|
# Add missing keys
|
||||||
|
if in_default:
|
||||||
|
for key, new_value in [
|
||||||
|
('bantime.increment', 'bantime.increment = ' + str(settings['bantime_increment'])),
|
||||||
|
('ignoreip', 'ignoreip = ' + settings['ignoreip']),
|
||||||
|
('bantime', 'bantime = ' + settings['bantime']),
|
||||||
|
('findtime', 'findtime = ' + settings['findtime']),
|
||||||
|
('maxretry', 'maxretry = ' + str(settings['maxretry'])),
|
||||||
|
('destemail', 'destemail = ' + settings['destemail']),
|
||||||
|
]:
|
||||||
|
if key not in keys_updated:
|
||||||
|
for i, output_line in enumerate(output_lines):
|
||||||
|
if output_line.strip() == "[DEFAULT]":
|
||||||
|
output_lines.insert(i + 1, new_value + '\n')
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(jail_file, 'w') as f:
|
||||||
|
f.writelines(output_lines)
|
||||||
|
else:
|
||||||
|
# Create new structure
|
||||||
|
banner = """################################################################################
|
||||||
|
# Fail2Ban-UI Managed Configuration
|
||||||
|
#
|
||||||
|
# WARNING: This file is automatically managed by Fail2Ban-UI.
|
||||||
|
# DO NOT EDIT THIS FILE MANUALLY - your changes will be overwritten.
|
||||||
|
#
|
||||||
|
# This file overrides settings from /etc/fail2ban/jail.conf
|
||||||
|
# Custom jail configurations should be placed in /etc/fail2ban/jail.d/
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
default_section = """[DEFAULT]
|
||||||
|
bantime.increment = """ + str(settings['bantime_increment']) + """
|
||||||
|
ignoreip = """ + settings['ignoreip'] + """
|
||||||
|
bantime = """ + settings['bantime'] + """
|
||||||
|
findtime = """ + settings['findtime'] + """
|
||||||
|
maxretry = """ + str(settings['maxretry']) + """
|
||||||
|
destemail = """ + settings['destemail'] + """
|
||||||
|
banaction = """ + settings['banaction'] + """
|
||||||
|
banaction_allports = """ + settings['banaction_allports'] + """
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
action_mwlg_config = """# Custom Fail2Ban action using geo-filter for email alerts
|
||||||
|
action_mwlg = %%(action_)s
|
||||||
|
ui-custom-action[sender="%%(sender)s", dest="%%(destemail)s", logpath="%%(logpath)s", chain="%%(chain)s"]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
action_override = """# Custom Fail2Ban action applied by fail2ban-ui
|
||||||
|
action = %%(action_mwlg)s
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_content = banner + default_section + action_mwlg_config + action_override
|
||||||
|
|
||||||
|
with open(jail_file, 'w') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
PY`, escapeForShell(jailLocalPath), escapeForShell(ignoreIPStr), escapeForShell(banactionVal), escapeForShell(banactionAllportsVal), settings.BantimeIncrement,
|
||||||
|
escapeForShell(settings.Bantime), escapeForShell(settings.Findtime), settings.Maxretry, escapeForShell(settings.Destemail))
|
||||||
|
|
||||||
|
_, err := sc.runRemoteCommand(ctx, []string{"bash", "-lc", ensureScript})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// parseJailConfigContent parses jail configuration content and returns JailInfo slice.
|
// parseJailConfigContent parses jail configuration content and returns JailInfo slice.
|
||||||
func parseJailConfigContent(content string) []JailInfo {
|
func parseJailConfigContent(content string) []JailInfo {
|
||||||
var jails []JailInfo
|
var jails []JailInfo
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -562,3 +563,157 @@ func ExtractFilterFromJailConfig(jailContent string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateDefaultSettingsLocal updates specific keys in the [DEFAULT] section of /etc/fail2ban/jail.local
|
||||||
|
// with the provided settings, preserving all other content including the ui-custom-action section.
|
||||||
|
// Removes commented lines (starting with #) before applying updates.
|
||||||
|
func UpdateDefaultSettingsLocal(settings config.AppSettings) error {
|
||||||
|
config.DebugLog("UpdateDefaultSettingsLocal called")
|
||||||
|
localPath := "/etc/fail2ban/jail.local"
|
||||||
|
|
||||||
|
// Read existing file if it exists
|
||||||
|
var existingContent string
|
||||||
|
if content, err := os.ReadFile(localPath); err == nil {
|
||||||
|
existingContent = string(content)
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to read jail.local: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove commented lines (lines starting with #) but preserve:
|
||||||
|
// - Banner lines (containing "Fail2Ban-UI" or "fail2ban-ui")
|
||||||
|
// - action_mwlg and action override lines
|
||||||
|
lines := strings.Split(existingContent, "\n")
|
||||||
|
var uncommentedLines []string
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
// Keep empty lines, banner lines, action_mwlg lines, action override lines, and lines that don't start with #
|
||||||
|
isBanner := strings.Contains(line, "Fail2Ban-UI") || strings.Contains(line, "fail2ban-ui")
|
||||||
|
isActionMwlg := strings.Contains(trimmed, "action_mwlg")
|
||||||
|
isActionOverride := strings.Contains(trimmed, "action = %(action_mwlg)s")
|
||||||
|
if trimmed == "" || !strings.HasPrefix(trimmed, "#") || isBanner || isActionMwlg || isActionOverride {
|
||||||
|
uncommentedLines = append(uncommentedLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existingContent = strings.Join(uncommentedLines, "\n")
|
||||||
|
|
||||||
|
// Convert IgnoreIPs array to space-separated string
|
||||||
|
ignoreIPStr := strings.Join(settings.IgnoreIPs, " ")
|
||||||
|
if ignoreIPStr == "" {
|
||||||
|
ignoreIPStr = "127.0.0.1/8 ::1"
|
||||||
|
}
|
||||||
|
// Set default banaction values if not set
|
||||||
|
banaction := settings.Banaction
|
||||||
|
if banaction == "" {
|
||||||
|
banaction = "iptables-multiport"
|
||||||
|
}
|
||||||
|
banactionAllports := settings.BanactionAllports
|
||||||
|
if banactionAllports == "" {
|
||||||
|
banactionAllports = "iptables-allports"
|
||||||
|
}
|
||||||
|
// Define the keys we want to update
|
||||||
|
keysToUpdate := map[string]string{
|
||||||
|
"bantime.increment": fmt.Sprintf("bantime.increment = %t", settings.BantimeIncrement),
|
||||||
|
"ignoreip": fmt.Sprintf("ignoreip = %s", ignoreIPStr),
|
||||||
|
"bantime": fmt.Sprintf("bantime = %s", settings.Bantime),
|
||||||
|
"findtime": fmt.Sprintf("findtime = %s", settings.Findtime),
|
||||||
|
"maxretry": fmt.Sprintf("maxretry = %d", settings.Maxretry),
|
||||||
|
"destemail": fmt.Sprintf("destemail = %s", settings.Destemail),
|
||||||
|
"banaction": fmt.Sprintf("banaction = %s", banaction),
|
||||||
|
"banaction_allports": fmt.Sprintf("banaction_allports = %s", banactionAllports),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which keys we've updated
|
||||||
|
keysUpdated := make(map[string]bool)
|
||||||
|
|
||||||
|
// Parse existing content and update only specific keys in DEFAULT section
|
||||||
|
if existingContent == "" {
|
||||||
|
// File doesn't exist, create new one with DEFAULT section
|
||||||
|
defaultLines := []string{"[DEFAULT]"}
|
||||||
|
for _, key := range []string{"bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail"} {
|
||||||
|
defaultLines = append(defaultLines, keysToUpdate[key])
|
||||||
|
}
|
||||||
|
defaultLines = append(defaultLines, "")
|
||||||
|
newContent := strings.Join(defaultLines, "\n")
|
||||||
|
if err := os.WriteFile(localPath, []byte(newContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write jail.local: %w", err)
|
||||||
|
}
|
||||||
|
config.DebugLog("Created new jail.local with DEFAULT section")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and update only specific keys in DEFAULT section
|
||||||
|
lines = strings.Split(existingContent, "\n")
|
||||||
|
var outputLines []string
|
||||||
|
inDefault := false
|
||||||
|
defaultSectionFound := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||||
|
sectionName := strings.Trim(trimmed, "[]")
|
||||||
|
if sectionName == "DEFAULT" {
|
||||||
|
// Start of DEFAULT section
|
||||||
|
inDefault = true
|
||||||
|
defaultSectionFound = true
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
} else {
|
||||||
|
// Other section - stop DEFAULT mode
|
||||||
|
inDefault = false
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
} else if inDefault {
|
||||||
|
// We're in DEFAULT section - check if this line is a key we need to update
|
||||||
|
keyUpdated := false
|
||||||
|
for key, newValue := range keysToUpdate {
|
||||||
|
// Check if this line contains the key (with or without spaces around =)
|
||||||
|
keyPattern := "^\\s*" + regexp.QuoteMeta(key) + "\\s*="
|
||||||
|
if matched, _ := regexp.MatchString(keyPattern, trimmed); matched {
|
||||||
|
outputLines = append(outputLines, newValue)
|
||||||
|
keysUpdated[key] = true
|
||||||
|
keyUpdated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !keyUpdated {
|
||||||
|
// Keep the line as-is (might be other DEFAULT settings or action_mwlg)
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep lines outside DEFAULT section (preserves ui-custom-action and other content)
|
||||||
|
outputLines = append(outputLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If DEFAULT section wasn't found, create it at the beginning
|
||||||
|
if !defaultSectionFound {
|
||||||
|
defaultLines := []string{"[DEFAULT]"}
|
||||||
|
for _, key := range []string{"bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail"} {
|
||||||
|
defaultLines = append(defaultLines, keysToUpdate[key])
|
||||||
|
}
|
||||||
|
defaultLines = append(defaultLines, "")
|
||||||
|
outputLines = append(defaultLines, outputLines...)
|
||||||
|
} else {
|
||||||
|
// Add any missing keys to the DEFAULT section
|
||||||
|
for _, key := range []string{"bantime.increment", "ignoreip", "bantime", "findtime", "maxretry", "destemail"} {
|
||||||
|
if !keysUpdated[key] {
|
||||||
|
// Find the DEFAULT section and insert after it
|
||||||
|
for i, line := range outputLines {
|
||||||
|
if strings.TrimSpace(line) == "[DEFAULT]" {
|
||||||
|
// Insert after [DEFAULT] header
|
||||||
|
outputLines = append(outputLines[:i+1], append([]string{keysToUpdate[key]}, outputLines[i+1:]...)...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newContent := strings.Join(outputLines, "\n")
|
||||||
|
if err := os.WriteFile(localPath, []byte(newContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write jail.local: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.DebugLog("Updated specific keys in DEFAULT section of jail.local")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ type Connector interface {
|
|||||||
GetJailConfig(ctx context.Context, jail string) (string, error)
|
GetJailConfig(ctx context.Context, jail string) (string, error)
|
||||||
SetJailConfig(ctx context.Context, jail, content string) error
|
SetJailConfig(ctx context.Context, jail, content string) error
|
||||||
TestLogpath(ctx context.Context, logpath string) ([]string, error)
|
TestLogpath(ctx context.Context, logpath string) ([]string, error)
|
||||||
|
|
||||||
|
// Default settings operations
|
||||||
|
UpdateDefaultSettings(ctx context.Context, settings config.AppSettings) error
|
||||||
|
|
||||||
|
// Jail local structure management
|
||||||
|
EnsureJailLocalStructure(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager orchestrates all connectors for configured Fail2ban servers.
|
// Manager orchestrates all connectors for configured Fail2ban servers.
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
||||||
"settings.smtp_tls": "TLS verwenden (empfohlen)",
|
"settings.smtp_tls": "TLS verwenden (empfohlen)",
|
||||||
"settings.send_test_email": "Test-E-Mail senden",
|
"settings.send_test_email": "Test-E-Mail senden",
|
||||||
"settings.fail2ban": "Fail2Ban-Konfiguration",
|
"settings.fail2ban": "Globale Standard-Fail2Ban-Konfigurationen",
|
||||||
|
"settings.fail2ban.description": "Diese Einstellungen werden auf allen aktivierten Fail2Ban-Servern angewendet und in deren jail.local [DEFAULT]-Abschnitt gespeichert.",
|
||||||
"settings.enable_bantime_increment": "Bantime-Inkrement aktivieren",
|
"settings.enable_bantime_increment": "Bantime-Inkrement aktivieren",
|
||||||
"settings.default_bantime": "Standard-Bantime",
|
"settings.default_bantime": "Standard-Bantime",
|
||||||
"settings.default_bantime_placeholder": "z.B. 48h",
|
"settings.default_bantime_placeholder": "z.B. 48h",
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
||||||
"settings.smtp_tls": "TLS bruuche (empfohlen)",
|
"settings.smtp_tls": "TLS bruuche (empfohlen)",
|
||||||
"settings.send_test_email": "Test-Email schicke",
|
"settings.send_test_email": "Test-Email schicke",
|
||||||
"settings.fail2ban": "Fail2Ban-Konfiguration",
|
"settings.fail2ban": "Globale Standard-Fail2Ban-Konfiguratione",
|
||||||
|
"settings.fail2ban.description": "Die Einstellige werde uf alli aktivierte Fail2Ban-Server aagwändet und i däre jail.local [DEFAULT]-Abschnitt gspeicheret.",
|
||||||
"settings.enable_bantime_increment": "Bantime-Inkrement aktivierä",
|
"settings.enable_bantime_increment": "Bantime-Inkrement aktivierä",
|
||||||
"settings.default_bantime": "Standard-Bantime",
|
"settings.default_bantime": "Standard-Bantime",
|
||||||
"settings.default_bantime_placeholder": "z.B. 48h",
|
"settings.default_bantime_placeholder": "z.B. 48h",
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
||||||
"settings.smtp_tls": "Use TLS (Recommended)",
|
"settings.smtp_tls": "Use TLS (Recommended)",
|
||||||
"settings.send_test_email": "Send Test Email",
|
"settings.send_test_email": "Send Test Email",
|
||||||
"settings.fail2ban": "Fail2Ban Configuration",
|
"settings.fail2ban": "Global Default Fail2Ban Configurations",
|
||||||
|
"settings.fail2ban.description": "These settings will be applied to all enabled Fail2Ban servers and stored in their jail.local [DEFAULT] section.",
|
||||||
"settings.enable_bantime_increment": "Enable Bantime Increment",
|
"settings.enable_bantime_increment": "Enable Bantime Increment",
|
||||||
"settings.default_bantime": "Default Bantime",
|
"settings.default_bantime": "Default Bantime",
|
||||||
"settings.default_bantime_placeholder": "e.g., 48h",
|
"settings.default_bantime_placeholder": "e.g., 48h",
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
||||||
"settings.smtp_tls": "Usar TLS (recomendado)",
|
"settings.smtp_tls": "Usar TLS (recomendado)",
|
||||||
"settings.send_test_email": "Enviar correo de prueba",
|
"settings.send_test_email": "Enviar correo de prueba",
|
||||||
"settings.fail2ban": "Configuración de Fail2Ban",
|
"settings.fail2ban": "Configuraciones Globales Predeterminadas de Fail2Ban",
|
||||||
|
"settings.fail2ban.description": "Estas configuraciones se aplicarán a todos los servidores Fail2Ban habilitados y se almacenarán en su sección [DEFAULT] de jail.local.",
|
||||||
"settings.enable_bantime_increment": "Habilitar incremento de Bantime",
|
"settings.enable_bantime_increment": "Habilitar incremento de Bantime",
|
||||||
"settings.default_bantime": "Bantime por defecto",
|
"settings.default_bantime": "Bantime por defecto",
|
||||||
"settings.default_bantime_placeholder": "p.ej., 48h",
|
"settings.default_bantime_placeholder": "p.ej., 48h",
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
||||||
"settings.smtp_tls": "Utiliser TLS (recommandé)",
|
"settings.smtp_tls": "Utiliser TLS (recommandé)",
|
||||||
"settings.send_test_email": "Envoyer un email de test",
|
"settings.send_test_email": "Envoyer un email de test",
|
||||||
"settings.fail2ban": "Configuration Fail2Ban",
|
"settings.fail2ban": "Configurations Globales par Défaut de Fail2Ban",
|
||||||
|
"settings.fail2ban.description": "Ces paramètres seront appliqués à tous les serveurs Fail2Ban activés et stockés dans leur section [DEFAULT] de jail.local.",
|
||||||
"settings.enable_bantime_increment": "Activer l'incrémentation du Bantime",
|
"settings.enable_bantime_increment": "Activer l'incrémentation du Bantime",
|
||||||
"settings.default_bantime": "Bantime par défaut",
|
"settings.default_bantime": "Bantime par défaut",
|
||||||
"settings.default_bantime_placeholder": "par exemple, 48h",
|
"settings.default_bantime_placeholder": "par exemple, 48h",
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
|
||||||
"settings.smtp_tls": "Usa TLS (raccomandato)",
|
"settings.smtp_tls": "Usa TLS (raccomandato)",
|
||||||
"settings.send_test_email": "Invia email di test",
|
"settings.send_test_email": "Invia email di test",
|
||||||
"settings.fail2ban": "Configurazione Fail2Ban",
|
"settings.fail2ban": "Configurazioni Globali Predefinite di Fail2Ban",
|
||||||
|
"settings.fail2ban.description": "Queste impostazioni verranno applicate a tutti i server Fail2Ban abilitati e memorizzate nella loro sezione [DEFAULT] di jail.local.",
|
||||||
"settings.enable_bantime_increment": "Abilita incremento del Bantime",
|
"settings.enable_bantime_increment": "Abilita incremento del Bantime",
|
||||||
"settings.default_bantime": "Bantime predefinito",
|
"settings.default_bantime": "Bantime predefinito",
|
||||||
"settings.default_bantime_placeholder": "es. 48h",
|
"settings.default_bantime_placeholder": "es. 48h",
|
||||||
|
|||||||
@@ -61,11 +61,13 @@ type AppSettingsRecord struct {
|
|||||||
SMTPFrom string
|
SMTPFrom string
|
||||||
SMTPUseTLS bool
|
SMTPUseTLS bool
|
||||||
BantimeIncrement bool
|
BantimeIncrement bool
|
||||||
IgnoreIP string
|
IgnoreIP string // Stored as space-separated string, converted to array in AppSettings
|
||||||
Bantime string
|
Bantime string
|
||||||
Findtime string
|
Findtime string
|
||||||
MaxRetry int
|
MaxRetry int
|
||||||
DestEmail string
|
DestEmail string
|
||||||
|
Banaction string
|
||||||
|
BanactionAllports string
|
||||||
AdvancedActionsJSON string
|
AdvancedActionsJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,17 +170,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
row := db.QueryRowContext(ctx, `
|
row := db.QueryRowContext(ctx, `
|
||||||
SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail, advanced_actions
|
SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions
|
||||||
FROM app_settings
|
FROM app_settings
|
||||||
WHERE id = 1`)
|
WHERE id = 1`)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, advancedActions sql.NullString
|
lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions sql.NullString
|
||||||
port, smtpPort, maxretry sql.NullInt64
|
port, smtpPort, maxretry sql.NullInt64
|
||||||
debug, restartNeeded, smtpTLS, bantimeInc sql.NullInt64
|
debug, restartNeeded, smtpTLS, bantimeInc sql.NullInt64
|
||||||
)
|
)
|
||||||
|
|
||||||
err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &advancedActions)
|
err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return AppSettingsRecord{}, false, nil
|
return AppSettingsRecord{}, false, nil
|
||||||
}
|
}
|
||||||
@@ -205,6 +207,8 @@ WHERE id = 1`)
|
|||||||
Findtime: stringFromNull(findtime),
|
Findtime: stringFromNull(findtime),
|
||||||
MaxRetry: intFromNull(maxretry),
|
MaxRetry: intFromNull(maxretry),
|
||||||
DestEmail: stringFromNull(destemail),
|
DestEmail: stringFromNull(destemail),
|
||||||
|
Banaction: stringFromNull(banaction),
|
||||||
|
BanactionAllports: stringFromNull(banactionAllports),
|
||||||
AdvancedActionsJSON: stringFromNull(advancedActions),
|
AdvancedActionsJSON: stringFromNull(advancedActions),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,9 +221,9 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error {
|
|||||||
}
|
}
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.ExecContext(ctx, `
|
||||||
INSERT INTO app_settings (
|
INSERT INTO app_settings (
|
||||||
id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail, advanced_actions
|
id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions
|
||||||
) VALUES (
|
) VALUES (
|
||||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
) ON CONFLICT(id) DO UPDATE SET
|
) ON CONFLICT(id) DO UPDATE SET
|
||||||
language = excluded.language,
|
language = excluded.language,
|
||||||
port = excluded.port,
|
port = excluded.port,
|
||||||
@@ -239,6 +243,8 @@ INSERT INTO app_settings (
|
|||||||
findtime = excluded.findtime,
|
findtime = excluded.findtime,
|
||||||
maxretry = excluded.maxretry,
|
maxretry = excluded.maxretry,
|
||||||
destemail = excluded.destemail,
|
destemail = excluded.destemail,
|
||||||
|
banaction = excluded.banaction,
|
||||||
|
banaction_allports = excluded.banaction_allports,
|
||||||
advanced_actions = excluded.advanced_actions
|
advanced_actions = excluded.advanced_actions
|
||||||
`, rec.Language,
|
`, rec.Language,
|
||||||
rec.Port,
|
rec.Port,
|
||||||
@@ -258,6 +264,8 @@ INSERT INTO app_settings (
|
|||||||
rec.Findtime,
|
rec.Findtime,
|
||||||
rec.MaxRetry,
|
rec.MaxRetry,
|
||||||
rec.DestEmail,
|
rec.DestEmail,
|
||||||
|
rec.Banaction,
|
||||||
|
rec.BanactionAllports,
|
||||||
rec.AdvancedActionsJSON,
|
rec.AdvancedActionsJSON,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
@@ -772,6 +780,8 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
|||||||
findtime TEXT,
|
findtime TEXT,
|
||||||
maxretry INTEGER,
|
maxretry INTEGER,
|
||||||
destemail TEXT,
|
destemail TEXT,
|
||||||
|
banaction TEXT,
|
||||||
|
banaction_allports TEXT,
|
||||||
advanced_actions TEXT
|
advanced_actions TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -841,6 +851,18 @@ CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backfill banaction columns for existing databases that predate them.
|
||||||
|
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN banaction TEXT`); err != nil {
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN banaction_allports TEXT`); err != nil {
|
||||||
|
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN advanced_actions TEXT`); err != nil {
|
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN advanced_actions TEXT`); err != nil {
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -433,6 +433,19 @@ func UpsertServerHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure jail.local structure is properly initialized for newly enabled/added servers
|
||||||
|
if justEnabled || !wasEnabled {
|
||||||
|
conn, err := fail2ban.GetManager().Connector(server.ID)
|
||||||
|
if err == nil {
|
||||||
|
if err := conn.EnsureJailLocalStructure(c.Request.Context()); err != nil {
|
||||||
|
config.DebugLog("Warning: failed to ensure jail.local structure for server %s: %v", server.Name, err)
|
||||||
|
// Don't fail the request, just log the warning
|
||||||
|
} else {
|
||||||
|
config.DebugLog("Successfully ensured jail.local structure for server %s", server.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"server": server})
|
c.JSON(http.StatusOK, gin.H{"server": server})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,6 +846,19 @@ func min(a, b int) int {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// equalStringSlices compares two string slices for equality
|
||||||
|
func equalStringSlices(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// TestLogpathHandler tests a logpath and returns matching files
|
// TestLogpathHandler tests a logpath and returns matching files
|
||||||
func TestLogpathHandler(c *gin.Context) {
|
func TestLogpathHandler(c *gin.Context) {
|
||||||
config.DebugLog("----------------------------")
|
config.DebugLog("----------------------------")
|
||||||
@@ -979,7 +1005,7 @@ func AdvancedActionsTestHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// UpdateJailManagementHandler updates the enabled state for each jail.
|
// UpdateJailManagementHandler updates the enabled state for each jail.
|
||||||
// Expected JSON format: { "JailName1": true, "JailName2": false, ... }
|
// Expected JSON format: { "JailName1": true, "JailName2": false, ... }
|
||||||
// After updating, the Fail2ban service is restarted.
|
// After updating, fail2ban is reloaded to apply the changes.
|
||||||
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
|
||||||
@@ -998,11 +1024,17 @@ func UpdateJailManagementHandler(c *gin.Context) {
|
|||||||
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
|
||||||
}
|
}
|
||||||
if err := config.MarkRestartNeeded(conn.Server().ID); err != nil {
|
// Reload fail2ban to apply the changes (reload is sufficient for jail enable/disable)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
if err := conn.Reload(c.Request.Context()); err != nil {
|
||||||
|
config.DebugLog("Warning: failed to reload fail2ban after updating jail settings: %v", err)
|
||||||
|
// Still return success but warn about reload failure
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Jail settings updated successfully, but fail2ban reload failed",
|
||||||
|
"warning": "Please reload fail2ban manually: " + err.Error(),
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Jail settings updated successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Jail settings updated and fail2ban reloaded successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSettingsHandler returns the entire AppSettings struct as JSON
|
// GetSettingsHandler returns the entire AppSettings struct as JSON
|
||||||
@@ -1078,6 +1110,49 @@ func UpdateSettingsHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if Fail2Ban DEFAULT settings changed and push to all enabled servers
|
||||||
|
// Compare IgnoreIPs arrays
|
||||||
|
ignoreIPsChanged := !equalStringSlices(oldSettings.IgnoreIPs, newSettings.IgnoreIPs)
|
||||||
|
defaultSettingsChanged := oldSettings.BantimeIncrement != newSettings.BantimeIncrement ||
|
||||||
|
ignoreIPsChanged ||
|
||||||
|
oldSettings.Bantime != newSettings.Bantime ||
|
||||||
|
oldSettings.Findtime != newSettings.Findtime ||
|
||||||
|
oldSettings.Maxretry != newSettings.Maxretry ||
|
||||||
|
oldSettings.Destemail != newSettings.Destemail ||
|
||||||
|
oldSettings.Banaction != newSettings.Banaction ||
|
||||||
|
oldSettings.BanactionAllports != newSettings.BanactionAllports
|
||||||
|
|
||||||
|
if defaultSettingsChanged {
|
||||||
|
config.DebugLog("Fail2Ban DEFAULT settings changed, pushing to all enabled servers")
|
||||||
|
connectors := fail2ban.GetManager().Connectors()
|
||||||
|
var errors []string
|
||||||
|
for _, conn := range connectors {
|
||||||
|
server := conn.Server()
|
||||||
|
config.DebugLog("Updating DEFAULT settings on server: %s (type: %s)", server.Name, server.Type)
|
||||||
|
if err := conn.UpdateDefaultSettings(c.Request.Context(), newSettings); err != nil {
|
||||||
|
errorMsg := fmt.Sprintf("Failed to update DEFAULT settings on %s: %v", server.Name, err)
|
||||||
|
config.DebugLog("Error: %s", errorMsg)
|
||||||
|
errors = append(errors, errorMsg)
|
||||||
|
} else {
|
||||||
|
config.DebugLog("Successfully updated DEFAULT settings on %s", server.Name)
|
||||||
|
// Mark server as needing restart
|
||||||
|
if err := config.MarkRestartNeeded(server.ID); err != nil {
|
||||||
|
config.DebugLog("Warning: failed to mark restart needed for %s: %v", server.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errors) > 0 {
|
||||||
|
config.DebugLog("Some servers failed to update DEFAULT settings: %v", errors)
|
||||||
|
// Don't fail the request, but include warnings in response
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Settings updated",
|
||||||
|
"restartNeeded": newSettings.RestartNeeded,
|
||||||
|
"warnings": errors,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Settings updated",
|
"message": "Settings updated",
|
||||||
"restartNeeded": newSettings.RestartNeeded,
|
"restartNeeded": newSettings.RestartNeeded,
|
||||||
@@ -1154,7 +1229,7 @@ func ApplyFail2banSettings(jailLocalPath string) error {
|
|||||||
newLines := []string{
|
newLines := []string{
|
||||||
"[DEFAULT]",
|
"[DEFAULT]",
|
||||||
fmt.Sprintf("bantime.increment = %t", s.BantimeIncrement),
|
fmt.Sprintf("bantime.increment = %t", s.BantimeIncrement),
|
||||||
fmt.Sprintf("ignoreip = %s", s.IgnoreIP),
|
fmt.Sprintf("ignoreip = %s", strings.Join(s.IgnoreIPs, " ")),
|
||||||
fmt.Sprintf("bantime = %s", s.Bantime),
|
fmt.Sprintf("bantime = %s", s.Bantime),
|
||||||
fmt.Sprintf("findtime = %s", s.Findtime),
|
fmt.Sprintf("findtime = %s", s.Findtime),
|
||||||
fmt.Sprintf("maxretry = %d", s.Maxretry),
|
fmt.Sprintf("maxretry = %d", s.Maxretry),
|
||||||
|
|||||||
@@ -569,36 +569,127 @@
|
|||||||
|
|
||||||
<!-- Fail2Ban Configuration Group -->
|
<!-- Fail2Ban Configuration Group -->
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.fail2ban">Fail2Ban Configuration</h3>
|
<h3 class="text-lg font-medium text-gray-900 mb-4" data-i18n="settings.fail2ban">Global Default Fail2Ban Configurations</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4" data-i18n="settings.fail2ban.description">These settings will be applied to all enabled Fail2Ban servers and stored in their jail.local [DEFAULT] section.</p>
|
||||||
|
|
||||||
<!-- Bantime Increment -->
|
<!-- Bantime Increment -->
|
||||||
<div class="flex items-center mb-4">
|
<div class="mb-4">
|
||||||
<input type="checkbox" id="bantimeIncrement" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out" />
|
<div class="flex items-center mb-2">
|
||||||
<label for="bantimeIncrement" class="ml-2 block text-sm text-gray-700" data-i18n="settings.enable_bantime_increment">Enable Bantime Increment</label>
|
<input type="checkbox" id="bantimeIncrement" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out" />
|
||||||
|
<label for="bantimeIncrement" class="ml-2 block text-sm font-medium text-gray-700" data-i18n="settings.enable_bantime_increment">Enable Bantime Increment</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 ml-9" data-i18n="settings.enable_bantime_increment.description">If set to true, the bantime will be calculated using the formula: bantime = findtime * (number of failures / maxretry) * (1 + bantime.rndtime).</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bantime -->
|
<!-- Bantime -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="banTime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_bantime">Default Bantime</label>
|
<label for="banTime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_bantime">Default Bantime</label>
|
||||||
|
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.default_bantime.description">The number of seconds that a host is banned. Time format: 1h = 1 hour, 1d = 1 day, 1w = 1 week, 1m = 1 month, 1y = 1 year.</p>
|
||||||
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="banTime"
|
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="banTime"
|
||||||
data-i18n-placeholder="settings.default_bantime_placeholder" placeholder="e.g., 48h" />
|
data-i18n-placeholder="settings.default_bantime_placeholder" placeholder="e.g., 48h" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Banaction -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="banaction" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.banaction">Banaction</label>
|
||||||
|
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.banaction.description">Default banning action (e.g. iptables-multiport, iptables-allports, firewallcmd-multiport, etc). It is used to define action_* variables.</p>
|
||||||
|
<select id="banaction" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="iptables-multiport">iptables-multiport</option>
|
||||||
|
<option value="iptables-allports">iptables-allports</option>
|
||||||
|
<option value="iptables-new">iptables-new</option>
|
||||||
|
<option value="iptables-ipset">iptables-ipset</option>
|
||||||
|
<option value="iptables-ipset-proto4">iptables-ipset-proto4</option>
|
||||||
|
<option value="iptables-ipset-proto6">iptables-ipset-proto6</option>
|
||||||
|
<option value="iptables-ipset-proto6-allports">iptables-ipset-proto6-allports</option>
|
||||||
|
<option value="iptables-multiport-log">iptables-multiport-log</option>
|
||||||
|
<option value="iptables-xt_recent-echo">iptables-xt_recent-echo</option>
|
||||||
|
<option value="firewallcmd-multiport">firewallcmd-multiport</option>
|
||||||
|
<option value="firewallcmd-allports">firewallcmd-allports</option>
|
||||||
|
<option value="firewallcmd-ipset">firewallcmd-ipset</option>
|
||||||
|
<option value="firewallcmd-new">firewallcmd-new</option>
|
||||||
|
<option value="firewallcmd-rich-rules">firewallcmd-rich-rules</option>
|
||||||
|
<option value="firewallcmd-rich-logging">firewallcmd-rich-logging</option>
|
||||||
|
<option value="nftables-multiport">nftables-multiport</option>
|
||||||
|
<option value="nftables-allports">nftables-allports</option>
|
||||||
|
<option value="nftables">nftables</option>
|
||||||
|
<option value="shorewall">shorewall</option>
|
||||||
|
<option value="shorewall-ipset-proto6">shorewall-ipset-proto6</option>
|
||||||
|
<option value="ufw">ufw</option>
|
||||||
|
<option value="pf">pf</option>
|
||||||
|
<option value="bsd-ipfw">bsd-ipfw</option>
|
||||||
|
<option value="ipfw">ipfw</option>
|
||||||
|
<option value="ipfilter">ipfilter</option>
|
||||||
|
<option value="npf">npf</option>
|
||||||
|
<option value="osx-ipfw">osx-ipfw</option>
|
||||||
|
<option value="osx-afctl">osx-afctl</option>
|
||||||
|
<option value="apf">apf</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Banaction Allports -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="banactionAllports" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.banaction_allports">Banaction Allports</label>
|
||||||
|
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.banaction_allports.description">Banning action for all ports (e.g. iptables-allports, firewallcmd-allports, etc). Used when a jail needs to ban all ports instead of specific ones.</p>
|
||||||
|
<select id="banactionAllports" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="iptables-allports">iptables-allports</option>
|
||||||
|
<option value="iptables-multiport">iptables-multiport</option>
|
||||||
|
<option value="iptables-new">iptables-new</option>
|
||||||
|
<option value="iptables-ipset">iptables-ipset</option>
|
||||||
|
<option value="iptables-ipset-proto4">iptables-ipset-proto4</option>
|
||||||
|
<option value="iptables-ipset-proto6">iptables-ipset-proto6</option>
|
||||||
|
<option value="iptables-ipset-proto6-allports">iptables-ipset-proto6-allports</option>
|
||||||
|
<option value="iptables-multiport-log">iptables-multiport-log</option>
|
||||||
|
<option value="iptables-xt_recent-echo">iptables-xt_recent-echo</option>
|
||||||
|
<option value="firewallcmd-allports">firewallcmd-allports</option>
|
||||||
|
<option value="firewallcmd-multiport">firewallcmd-multiport</option>
|
||||||
|
<option value="firewallcmd-ipset">firewallcmd-ipset</option>
|
||||||
|
<option value="firewallcmd-new">firewallcmd-new</option>
|
||||||
|
<option value="firewallcmd-rich-rules">firewallcmd-rich-rules</option>
|
||||||
|
<option value="firewallcmd-rich-logging">firewallcmd-rich-logging</option>
|
||||||
|
<option value="nftables-allports">nftables-allports</option>
|
||||||
|
<option value="nftables-multiport">nftables-multiport</option>
|
||||||
|
<option value="nftables">nftables</option>
|
||||||
|
<option value="shorewall">shorewall</option>
|
||||||
|
<option value="shorewall-ipset-proto6">shorewall-ipset-proto6</option>
|
||||||
|
<option value="ufw">ufw</option>
|
||||||
|
<option value="pf">pf</option>
|
||||||
|
<option value="bsd-ipfw">bsd-ipfw</option>
|
||||||
|
<option value="ipfw">ipfw</option>
|
||||||
|
<option value="ipfilter">ipfilter</option>
|
||||||
|
<option value="npf">npf</option>
|
||||||
|
<option value="osx-ipfw">osx-ipfw</option>
|
||||||
|
<option value="osx-afctl">osx-afctl</option>
|
||||||
|
<option value="apf">apf</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Findtime -->
|
<!-- Findtime -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="findTime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_findtime">Default Findtime</label>
|
<label for="findTime" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_findtime">Default Findtime</label>
|
||||||
|
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.default_findtime.description">A host is banned if it has generated 'maxretry' failures during the last 'findtime' seconds. Time format: 1h = 1 hour, 1d = 1 day, 1w = 1 week, 1m = 1 month, 1y = 1 year.</p>
|
||||||
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="findTime"
|
<input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="findTime"
|
||||||
data-i18n-placeholder="settings.default_findtime_placeholder" placeholder="e.g., 30m" />
|
data-i18n-placeholder="settings.default_findtime_placeholder" placeholder="e.g., 30m" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Max Retry -->
|
<!-- Max Retry -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="maxRetry" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_max_retry">Default Max Retry</label>
|
<label for="maxRetry" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.default_max_retry">Default Max Retry</label>
|
||||||
|
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.default_max_retry.description">Number of failures before a host gets banned.</p>
|
||||||
<input type="number" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="maxRetry"
|
<input type="number" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="maxRetry"
|
||||||
data-i18n-placeholder="settings.default_max_retry_placeholder" placeholder="Enter maximum retries" />
|
data-i18n-placeholder="settings.default_max_retry_placeholder" placeholder="Enter maximum retries" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ignore IPs -->
|
<!-- Ignore IPs -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="ignoreIP" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.ignore_ips">Ignore IPs</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.ignore_ips">Ignore IPs</label>
|
||||||
<textarea class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" id="ignoreIP" rows="2"
|
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.ignore_ips.description">Space separated list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which matches an address in this list.</p>
|
||||||
data-i18n-placeholder="settings.ignore_ips_placeholder" placeholder="IPs to ignore, separated by spaces"></textarea>
|
<div class="border border-gray-300 rounded-md p-2 min-h-[60px] bg-gray-50" id="ignoreIPsContainer">
|
||||||
|
<div id="ignoreIPsTags" class="flex flex-wrap gap-2 mb-2"></div>
|
||||||
|
<input type="text" id="ignoreIPInput" class="w-full border-0 bg-transparent focus:outline-none focus:ring-0 text-sm"
|
||||||
|
data-i18n-placeholder="settings.ignore_ips_placeholder" placeholder="Enter IP address and press Enter" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors" data-i18n="settings.save">Save</button>
|
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors" data-i18n="settings.save">Save</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -3127,7 +3218,7 @@
|
|||||||
showToast("Error saving jail settings: " + data.error, 'error');
|
showToast("Error saving jail settings: " + data.error, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showToast(t('jails.manage.save_success', 'Jail settings saved. Please restart Fail2ban.'), 'info');
|
showToast(t('jails.manage.save_success', 'Jail settings saved and fail2ban reloaded.'), 'info');
|
||||||
return loadServers().then(function() {
|
return loadServers().then(function() {
|
||||||
updateRestartBanner();
|
updateRestartBanner();
|
||||||
return refreshData({ silent: true });
|
return refreshData({ silent: true });
|
||||||
@@ -3395,7 +3486,13 @@
|
|||||||
document.getElementById('banTime').value = data.bantime || '';
|
document.getElementById('banTime').value = data.bantime || '';
|
||||||
document.getElementById('findTime').value = data.findtime || '';
|
document.getElementById('findTime').value = data.findtime || '';
|
||||||
document.getElementById('maxRetry').value = data.maxretry || '';
|
document.getElementById('maxRetry').value = data.maxretry || '';
|
||||||
document.getElementById('ignoreIP').value = data.ignoreip || '';
|
// Load IgnoreIPs as array
|
||||||
|
const ignoreIPs = data.ignoreips || [];
|
||||||
|
renderIgnoreIPsTags(ignoreIPs);
|
||||||
|
|
||||||
|
// Load banaction settings
|
||||||
|
document.getElementById('banaction').value = data.banaction || 'iptables-multiport';
|
||||||
|
document.getElementById('banactionAllports').value = data.banactionAllports || 'iptables-allports';
|
||||||
|
|
||||||
applyAdvancedActionsSettings(data.advancedActions || {});
|
applyAdvancedActionsSettings(data.advancedActions || {});
|
||||||
loadPermanentBlockLog();
|
loadPermanentBlockLog();
|
||||||
@@ -3445,7 +3542,9 @@
|
|||||||
bantime: document.getElementById('banTime').value.trim(),
|
bantime: document.getElementById('banTime').value.trim(),
|
||||||
findtime: document.getElementById('findTime').value.trim(),
|
findtime: document.getElementById('findTime').value.trim(),
|
||||||
maxretry: parseInt(document.getElementById('maxRetry').value, 10) || 3,
|
maxretry: parseInt(document.getElementById('maxRetry').value, 10) || 3,
|
||||||
ignoreip: document.getElementById('ignoreIP').value.trim(),
|
ignoreips: getIgnoreIPsArray(),
|
||||||
|
banaction: document.getElementById('banaction').value,
|
||||||
|
banactionAllports: document.getElementById('banactionAllports').value,
|
||||||
smtp: smtpSettings,
|
smtp: smtpSettings,
|
||||||
advancedActions: collectAdvancedActionsSettings()
|
advancedActions: collectAdvancedActionsSettings()
|
||||||
};
|
};
|
||||||
@@ -3903,7 +4002,92 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup IgnoreIPs tag input
|
||||||
|
setupIgnoreIPsInput();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//*******************************************************************
|
||||||
|
//* IgnoreIPs Tag Management Functions : *
|
||||||
|
//*******************************************************************
|
||||||
|
|
||||||
|
function renderIgnoreIPsTags(ips) {
|
||||||
|
const container = document.getElementById('ignoreIPsTags');
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (ips && ips.length > 0) {
|
||||||
|
ips.forEach(function(ip) {
|
||||||
|
if (ip && ip.trim()) {
|
||||||
|
addIgnoreIPTag(ip.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIgnoreIPTag(ip) {
|
||||||
|
if (!ip || !ip.trim()) return;
|
||||||
|
|
||||||
|
const container = document.getElementById('ignoreIPsTags');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const existingTags = Array.from(container.querySelectorAll('.ignore-ip-tag')).map(tag => tag.dataset.ip);
|
||||||
|
if (existingTags.includes(ip.trim())) {
|
||||||
|
return; // Already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.className = 'ignore-ip-tag inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800';
|
||||||
|
tag.dataset.ip = ip.trim();
|
||||||
|
const escapedIP = escapeHtml(ip.trim());
|
||||||
|
tag.innerHTML = escapedIP + ' <button type="button" class="ml-1 text-blue-600 hover:text-blue-800 focus:outline-none" onclick="removeIgnoreIPTag(\'' + escapedIP.replace(/'/g, "\\'") + '\')">×</button>';
|
||||||
|
container.appendChild(tag);
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
const input = document.getElementById('ignoreIPInput');
|
||||||
|
if (input) input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIgnoreIPTag(ip) {
|
||||||
|
const container = document.getElementById('ignoreIPsTags');
|
||||||
|
if (!container) return;
|
||||||
|
const escapedIP = escapeHtml(ip);
|
||||||
|
const tag = container.querySelector('.ignore-ip-tag[data-ip="' + escapedIP.replace(/"/g, '"') + '"]');
|
||||||
|
if (tag) {
|
||||||
|
tag.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIgnoreIPsArray() {
|
||||||
|
const container = document.getElementById('ignoreIPsTags');
|
||||||
|
if (!container) return [];
|
||||||
|
const tags = container.querySelectorAll('.ignore-ip-tag');
|
||||||
|
return Array.from(tags).map(tag => tag.dataset.ip).filter(ip => ip && ip.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupIgnoreIPsInput() {
|
||||||
|
const input = document.getElementById('ignoreIPInput');
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
input.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' || e.key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
// Support space or comma separated IPs
|
||||||
|
const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
|
||||||
|
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function(e) {
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value) {
|
||||||
|
const ips = value.split(/[,\s]+/).filter(ip => ip.trim());
|
||||||
|
ips.forEach(ip => addIgnoreIPTag(ip.trim()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//*******************************************************************
|
//*******************************************************************
|
||||||
//* Translation Related Functions : *
|
//* Translation Related Functions : *
|
||||||
|
|||||||
Reference in New Issue
Block a user