Added manual block section and BanIP method to for all connectors, like the UnbanIP functionallity

This commit is contained in:
2026-01-07 16:14:48 +01:00
parent ad6f86146d
commit b5e8324762
13 changed files with 265 additions and 0 deletions

View File

@@ -101,6 +101,11 @@ func (ac *AgentConnector) UnbanIP(ctx context.Context, jail, ip string) error {
return ac.post(ctx, fmt.Sprintf("/v1/jails/%s/unban", url.PathEscape(jail)), payload, nil)
}
func (ac *AgentConnector) BanIP(ctx context.Context, jail, ip string) error {
payload := map[string]string{"ip": ip}
return ac.post(ctx, fmt.Sprintf("/v1/jails/%s/ban", url.PathEscape(jail)), payload, nil)
}
func (ac *AgentConnector) Reload(ctx context.Context) error {
return ac.post(ctx, "/v1/actions/reload", nil, nil)
}

View File

@@ -140,6 +140,15 @@ func (lc *LocalConnector) UnbanIP(ctx context.Context, jail, ip string) error {
return nil
}
// BanIP implements Connector.
func (lc *LocalConnector) BanIP(ctx context.Context, jail, ip string) error {
args := []string{"set", jail, "banip", ip}
if _, err := lc.runFail2banClient(ctx, args...); err != nil {
return fmt.Errorf("error banning IP %s in jail %s: %w", ip, jail, err)
}
return nil
}
// Reload implements Connector.
func (lc *LocalConnector) Reload(ctx context.Context) error {
out, err := lc.runFail2banClient(ctx, "reload")

View File

@@ -187,6 +187,11 @@ func (sc *SSHConnector) UnbanIP(ctx context.Context, jail, ip string) error {
return err
}
func (sc *SSHConnector) BanIP(ctx context.Context, jail, ip string) error {
_, err := sc.runFail2banCommand(ctx, "set", jail, "banip", ip)
return err
}
func (sc *SSHConnector) Reload(ctx context.Context) error {
_, err := sc.runFail2banCommand(ctx, "reload")
return err

View File

@@ -16,6 +16,7 @@ type Connector interface {
GetJailInfos(ctx context.Context) ([]JailInfo, error)
GetBannedIPs(ctx context.Context, jail string) ([]string, error)
UnbanIP(ctx context.Context, jail, ip string) error
BanIP(ctx context.Context, jail, ip string) error
Reload(ctx context.Context) error
Restart(ctx context.Context) error
GetFilterConfig(ctx context.Context, jail string) (string, string, error) // Returns (config, filePath, error)

View File

@@ -38,6 +38,20 @@
"dashboard.table.log_line": "Logzeile",
"dashboard.no_banned_ips": "Keine gesperrten IPs",
"dashboard.unban": "Entsperren",
"dashboard.manual_block.title": "Manuelle IP-Sperre",
"dashboard.manual_block.subtitle": "Manuell eine IP-Adresse in einem bestimmten Jail sperren.",
"dashboard.manual_block.expand_hint": "Klicken Sie, um zu erweitern und eine IP-Adresse zu sperren",
"dashboard.manual_block.jail_label": "Jail auswählen",
"dashboard.manual_block.jail_placeholder": "Jail auswählen...",
"dashboard.manual_block.ip_label": "IP-Adresse",
"dashboard.manual_block.ip_placeholder": "z.B. 88.76.21.123",
"dashboard.manual_block.button": "IP sperren",
"dashboard.manual_block.confirm": "IP {ip} im Jail {jail} sperren?",
"dashboard.manual_block.success": "IP erfolgreich gesperrt",
"dashboard.manual_block.error": "Fehler beim Sperren der IP",
"dashboard.manual_block.jail_required": "Bitte wählen Sie ein Jail aus",
"dashboard.manual_block.ip_required": "Bitte geben Sie eine IP-Adresse ein",
"dashboard.manual_block.invalid_ip": "Bitte geben Sie eine gültige IP-Adresse ein",
"dashboard.banned.show_more": "Mehr anzeigen",
"dashboard.banned.show_less": "Weniger anzeigen",
"logs.overview.title": "Interne Log-Übersicht",

View File

@@ -38,6 +38,20 @@
"dashboard.table.log_line": "Log-Zile",
"dashboard.no_banned_ips": "Ke g'sperrti IPs",
"dashboard.unban": "Entsperre",
"dashboard.manual_block.title": "Manuelli IP-Sperri",
"dashboard.manual_block.subtitle": "Manuell e IP-Adrässe inme bestimmte Jail sperre.",
"dashboard.manual_block.expand_hint": "Klick zum Ufklappe und e IP-Adrässe z sperre",
"dashboard.manual_block.jail_label": "Jail uswähle",
"dashboard.manual_block.jail_placeholder": "Jail uswähle...",
"dashboard.manual_block.ip_label": "IP-Adrässe",
"dashboard.manual_block.ip_placeholder": "z.B. 88.76.21.123",
"dashboard.manual_block.button": "IP sperre",
"dashboard.manual_block.confirm": "IP {ip} im Jail {jail} sperre?",
"dashboard.manual_block.success": "IP erfolgriich g'sperrt",
"dashboard.manual_block.error": "Fehler bim Sperre vo dr IP",
"dashboard.manual_block.jail_required": "Bitte wähl es Jail us",
"dashboard.manual_block.ip_required": "Bitte gib e IP-Adrässe ii",
"dashboard.manual_block.invalid_ip": "Bitte gib e gültigi IP-Adrässe ii",
"dashboard.banned.show_more": "Meh azeige",
"dashboard.banned.show_less": "Weniger azeige",
"logs.overview.title": "Generelli Log-Übersicht",

View File

@@ -38,6 +38,20 @@
"dashboard.table.log_line": "Log Line",
"dashboard.no_banned_ips": "No banned IPs",
"dashboard.unban": "Unban",
"dashboard.manual_block.title": "Manual Block IP",
"dashboard.manual_block.subtitle": "Manually block an IP address in a specific jail.",
"dashboard.manual_block.expand_hint": "Click to expand and block an IP address",
"dashboard.manual_block.jail_label": "Select Jail",
"dashboard.manual_block.jail_placeholder": "Choose a jail...",
"dashboard.manual_block.ip_label": "IP Address",
"dashboard.manual_block.ip_placeholder": "e.g., 88.76.21.123",
"dashboard.manual_block.button": "Block IP",
"dashboard.manual_block.confirm": "Block IP {ip} in jail {jail}?",
"dashboard.manual_block.success": "IP blocked successfully",
"dashboard.manual_block.error": "Error blocking IP",
"dashboard.manual_block.jail_required": "Please select a jail",
"dashboard.manual_block.ip_required": "Please enter an IP address",
"dashboard.manual_block.invalid_ip": "Please enter a valid IP address",
"dashboard.banned.show_more": "Show more",
"dashboard.banned.show_less": "Hide extra",
"logs.overview.title": "Internal Log Overview",

View File

@@ -38,6 +38,20 @@
"dashboard.table.log_line": "Línea de log",
"dashboard.no_banned_ips": "No hay IP bloqueadas",
"dashboard.unban": "Desbloquear",
"dashboard.manual_block.title": "Bloqueo manual de IP",
"dashboard.manual_block.subtitle": "Bloquear manualmente una dirección IP en una cárcel específica.",
"dashboard.manual_block.expand_hint": "Haga clic para expandir y bloquear una dirección IP",
"dashboard.manual_block.jail_label": "Seleccionar cárcel",
"dashboard.manual_block.jail_placeholder": "Elegir una cárcel...",
"dashboard.manual_block.ip_label": "Dirección IP",
"dashboard.manual_block.ip_placeholder": "ej. 88.76.21.123",
"dashboard.manual_block.button": "Bloquear IP",
"dashboard.manual_block.confirm": "¿Bloquear IP {ip} en la cárcel {jail}?",
"dashboard.manual_block.success": "IP bloqueada exitosamente",
"dashboard.manual_block.error": "Error al bloquear la IP",
"dashboard.manual_block.jail_required": "Por favor seleccione una cárcel",
"dashboard.manual_block.ip_required": "Por favor ingrese una dirección IP",
"dashboard.manual_block.invalid_ip": "Por favor ingrese una dirección IP válida",
"dashboard.banned.show_more": "Mostrar más",
"dashboard.banned.show_less": "Mostrar menos",
"logs.overview.title": "Resumen interno de registros",

View File

@@ -38,6 +38,20 @@
"dashboard.table.log_line": "Ligne de log",
"dashboard.no_banned_ips": "Aucune IP bloquée",
"dashboard.unban": "Débloquer",
"dashboard.manual_block.title": "Blocage manuel d'IP",
"dashboard.manual_block.subtitle": "Bloquer manuellement une adresse IP dans une prison spécifique.",
"dashboard.manual_block.expand_hint": "Cliquez pour développer et bloquer une adresse IP",
"dashboard.manual_block.jail_label": "Sélectionner une prison",
"dashboard.manual_block.jail_placeholder": "Choisir une prison...",
"dashboard.manual_block.ip_label": "Adresse IP",
"dashboard.manual_block.ip_placeholder": "ex. 88.76.21.123",
"dashboard.manual_block.button": "Bloquer l'IP",
"dashboard.manual_block.confirm": "Bloquer l'IP {ip} dans la prison {jail}?",
"dashboard.manual_block.success": "IP bloquée avec succès",
"dashboard.manual_block.error": "Erreur lors du blocage de l'IP",
"dashboard.manual_block.jail_required": "Veuillez sélectionner une prison",
"dashboard.manual_block.ip_required": "Veuillez entrer une adresse IP",
"dashboard.manual_block.invalid_ip": "Veuillez entrer une adresse IP valide",
"dashboard.banned.show_more": "Afficher plus",
"dashboard.banned.show_less": "Afficher moins",
"logs.overview.title": "Vue d'ensemble interne des journaux",

View File

@@ -38,6 +38,20 @@
"dashboard.table.log_line": "Riga di log",
"dashboard.no_banned_ips": "Nessuna IP bloccata",
"dashboard.unban": "Sblocca",
"dashboard.manual_block.title": "Blocco manuale IP",
"dashboard.manual_block.subtitle": "Bloccare manualmente un indirizzo IP in una prigione specifica.",
"dashboard.manual_block.expand_hint": "Fare clic per espandere e bloccare un indirizzo IP",
"dashboard.manual_block.jail_label": "Seleziona prigione",
"dashboard.manual_block.jail_placeholder": "Scegli una prigione...",
"dashboard.manual_block.ip_label": "Indirizzo IP",
"dashboard.manual_block.ip_placeholder": "es. 88.76.21.123",
"dashboard.manual_block.button": "Blocca IP",
"dashboard.manual_block.confirm": "Bloccare IP {ip} nella prigione {jail}?",
"dashboard.manual_block.success": "IP bloccato con successo",
"dashboard.manual_block.error": "Errore nel bloccare l'IP",
"dashboard.manual_block.jail_required": "Si prega di selezionare una prigione",
"dashboard.manual_block.ip_required": "Si prega di inserire un indirizzo IP",
"dashboard.manual_block.invalid_ip": "Si prega di inserire un indirizzo IP valido",
"dashboard.banned.show_more": "Mostra di più",
"dashboard.banned.show_less": "Mostra meno",
"logs.overview.title": "Panoramica interna dei log",

View File

@@ -170,6 +170,29 @@ func UnbanIPHandler(c *gin.Context) {
})
}
// BanIPHandler bans a given IP in a specific jail.
func BanIPHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("BanIPHandler called (handlers.go)") // entry point
jail := c.Param("jail")
ip := c.Param("ip")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := conn.BanIP(c.Request.Context(), jail, ip); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
fmt.Println(ip + " in jail " + jail + " banned successfully.")
c.JSON(http.StatusOK, gin.H{
"message": "IP banned successfully",
})
}
// BanNotificationHandler processes incoming ban notifications from Fail2Ban.
func BanNotificationHandler(c *gin.Context) {
// Validate callback secret

View File

@@ -32,6 +32,7 @@ func RegisterRoutes(r *gin.Engine, hub *Hub) {
{
api.GET("/summary", SummaryHandler)
api.POST("/jails/:jail/unban/:ip", UnbanIPHandler)
api.POST("/jails/:jail/ban/:ip", BanIPHandler)
// Routes for jail-filter management (TODO: rename API-call)
api.GET("/jails/:jail/config", GetJailFilterConfigHandler)

View File

@@ -441,6 +441,24 @@ function toggleBannedList(hiddenId, buttonId) {
}
}
function toggleManualBlockSection() {
var container = document.getElementById('manualBlockFormContainer');
var icon = document.getElementById('manualBlockToggleIcon');
if (!container || !icon) {
return;
}
var isHidden = container.classList.contains("hidden");
if (isHidden) {
container.classList.remove("hidden");
icon.classList.remove("fa-chevron-down");
icon.classList.add("fa-chevron-up");
} else {
container.classList.add("hidden");
icon.classList.remove("fa-chevron-up");
icon.classList.add("fa-chevron-down");
}
}
function unbanIP(jail, ip) {
const confirmMsg = isLOTRModeActive
? 'Restore ' + ip + ' to the realm from ' + jail + '?'
@@ -470,6 +488,75 @@ function unbanIP(jail, ip) {
});
}
function banIP(jail, ip) {
const confirmMsg = isLOTRModeActive
? 'Banish ' + ip + ' from the realm in ' + jail + '?'
: 'Block IP ' + ip + ' in jail ' + jail + '?';
if (!confirm(confirmMsg)) {
return;
}
showLoading(true);
var url = '/api/jails/' + encodeURIComponent(jail) + '/ban/' + encodeURIComponent(ip);
fetch(withServerParam(url), {
method: 'POST',
headers: serverHeaders()
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
showToast("Error blocking IP: " + data.error, 'error');
} else {
showToast(t('dashboard.manual_block.success', 'IP blocked successfully'), 'success');
return refreshData({ silent: true });
}
})
.catch(function(err) {
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
});
}
function handleManualBlock() {
var jailSelect = document.getElementById('blockJailSelect');
var ipInput = document.getElementById('blockIPInput');
if (!jailSelect || !ipInput) {
return;
}
var jail = jailSelect.value;
var ip = ipInput.value.trim();
if (!jail) {
showToast(t('dashboard.manual_block.jail_required', 'Please select a jail'), 'error');
jailSelect.focus();
return;
}
if (!ip) {
showToast(t('dashboard.manual_block.ip_required', 'Please enter an IP address'), 'error');
ipInput.focus();
return;
}
// Basic IP validation
var ipv4Pattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
var ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) {
showToast(t('dashboard.manual_block.invalid_ip', 'Please enter a valid IP address'), 'error');
ipInput.focus();
return;
}
banIP(jail, ip);
// Clear form after submission
ipInput.value = '';
jailSelect.value = '';
}
function renderDashboard() {
var container = document.getElementById('dashboard');
if (!container) return;
@@ -590,6 +677,56 @@ function renderDashboard() {
html += '</div>'; // close overview card
}
// Manual Block IP Section
if (summary && summary.jails && summary.jails.length > 0) {
var enabledJails = summary.jails.filter(function(j) { return j.enabled !== false; });
if (enabledJails.length > 0) {
html += ''
+ '<div class="bg-white rounded-lg shadow p-6 mb-6">'
+ ' <div class="cursor-pointer hover:bg-gray-50 -m-6 p-6 rounded-lg transition-colors" onclick="toggleManualBlockSection()">'
+ ' <div class="flex items-center justify-between">'
+ ' <div class="flex-1">'
+ ' <h3 class="text-lg font-medium text-gray-900 mb-2" data-i18n="dashboard.manual_block.title">Manual Block IP</h3>'
+ ' <p class="text-sm text-gray-500" data-i18n="dashboard.manual_block.subtitle">Manually block an IP address in a specific jail.</p>'
+ ' <p class="text-xs text-gray-400 mt-1" data-i18n="dashboard.manual_block.expand_hint">Click to expand and block an IP address</p>'
+ ' </div>'
+ ' <div class="ml-4">'
+ ' <i id="manualBlockToggleIcon" class="fas fa-chevron-down text-gray-400 transition-transform"></i>'
+ ' </div>'
+ ' </div>'
+ ' </div>'
+ ' <div id="manualBlockFormContainer" class="hidden mt-4">'
+ ' <form id="manualBlockForm" onsubmit="return false;">'
+ ' <div class="grid grid-cols-1 md:grid-cols-3 gap-4">'
+ ' <div>'
+ ' <label for="blockJailSelect" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="dashboard.manual_block.jail_label">Select Jail</label>'
+ ' <select id="blockJailSelect" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required>'
+ ' <option value="" data-i18n="dashboard.manual_block.jail_placeholder">Choose a jail...</option>';
enabledJails.forEach(function(jail) {
html += ' <option value="' + escapeHtml(jail.jailName) + '">' + escapeHtml(jail.jailName) + '</option>';
});
html += ''
+ ' </select>'
+ ' </div>'
+ ' <div>'
+ ' <label for="blockIPInput" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="dashboard.manual_block.ip_label">IP Address</label>'
+ ' <input type="text" id="blockIPInput" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" data-i18n-placeholder="dashboard.manual_block.ip_placeholder" placeholder="e.g., 88.76.21.123" pattern="^([0-9]{1,3}\\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$" required>'
+ ' </div>'
+ ' <div class="flex items-end">'
+ ' <button type="button" onclick="handleManualBlock()" class="w-full bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors flex items-center justify-center gap-2">'
+ ' <i class="fas fa-ban"></i>'
+ ' <span data-i18n="dashboard.manual_block.button">Block IP</span>'
+ ' </button>'
+ ' </div>'
+ ' </div>'
+ ' </form>'
+ ' </div>'
+ '</div>';
}
}
html += '<div id="logOverview">' + renderLogOverviewContent() + '</div>';
container.innerHTML = html;