Add sections to integrations

This commit is contained in:
2026-02-17 00:05:46 +01:00
parent 0d551ede53
commit 1cd2439cea
4 changed files with 58 additions and 38 deletions

View File

@@ -17,6 +17,10 @@ func init() {
Register(&mikrotikIntegration{}) Register(&mikrotikIntegration{})
} }
// =========================================================================
// Interface Implementation
// =========================================================================
func (m *mikrotikIntegration) ID() string { func (m *mikrotikIntegration) ID() string {
return "mikrotik" return "mikrotik"
} }
@@ -41,6 +45,10 @@ func (m *mikrotikIntegration) Validate(cfg config.AdvancedActionsConfig) error {
return nil return nil
} }
// =========================================================================
// Block/Unblock
// =========================================================================
func (m *mikrotikIntegration) BlockIP(req Request) error { func (m *mikrotikIntegration) BlockIP(req Request) error {
if err := m.Validate(req.Config); err != nil { if err := m.Validate(req.Config); err != nil {
return err return err
@@ -59,6 +67,10 @@ func (m *mikrotikIntegration) UnblockIP(req Request) error {
return m.runCommand(req, cmd) return m.runCommand(req, cmd)
} }
// =========================================================================
// SSH Communication
// =========================================================================
func (m *mikrotikIntegration) runCommand(req Request, command string) error { func (m *mikrotikIntegration) runCommand(req Request, command string) error {
cfg := req.Config.Mikrotik cfg := req.Config.Mikrotik
@@ -97,7 +109,6 @@ func (m *mikrotikIntegration) runCommand(req Request, command string) error {
address := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", port)) address := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", port))
client, err := ssh.Dial("tcp", address, clientCfg) client, err := ssh.Dial("tcp", address, clientCfg)
if err != nil { if err != nil {
// Provide more specific error messages for common connection issues
if netErr, ok := err.(net.Error); ok { if netErr, ok := err.(net.Error); ok {
if netErr.Timeout() { if netErr.Timeout() {
return fmt.Errorf("connection to mikrotik at %s timed out: %w", address, err) return fmt.Errorf("connection to mikrotik at %s timed out: %w", address, err)

View File

@@ -20,6 +20,10 @@ func init() {
Register(&opnsenseIntegration{}) Register(&opnsenseIntegration{})
} }
// =========================================================================
// Interface Implementation
// =========================================================================
func (o *opnsenseIntegration) ID() string { func (o *opnsenseIntegration) ID() string {
return "opnsense" return "opnsense"
} }
@@ -41,6 +45,10 @@ func (o *opnsenseIntegration) Validate(cfg config.AdvancedActionsConfig) error {
return nil return nil
} }
// =========================================================================
// Block/Unblock
// =========================================================================
func (o *opnsenseIntegration) BlockIP(req Request) error { func (o *opnsenseIntegration) BlockIP(req Request) error {
if err := o.Validate(req.Config); err != nil { if err := o.Validate(req.Config); err != nil {
return err return err
@@ -55,6 +63,10 @@ func (o *opnsenseIntegration) UnblockIP(req Request) error {
return o.callAPI(req, "delete", req.IP) return o.callAPI(req, "delete", req.IP)
} }
// =========================================================================
// OPNsense API
// =========================================================================
func (o *opnsenseIntegration) callAPI(req Request, action, ip string) error { func (o *opnsenseIntegration) callAPI(req Request, action, ip string) error {
cfg := req.Config.OPNsense cfg := req.Config.OPNsense
apiURL := strings.TrimSuffix(cfg.BaseURL, "/") + fmt.Sprintf("/api/firewall/alias_util/%s/%s", action, cfg.Alias) apiURL := strings.TrimSuffix(cfg.BaseURL, "/") + fmt.Sprintf("/api/firewall/alias_util/%s/%s", action, cfg.Alias)

View File

@@ -16,12 +16,14 @@ import (
type pfSenseIntegration struct{} type pfSenseIntegration struct{}
// FirewallAliasResponse represents the response structure from pfSense API // =========================================================================
// Types
// =========================================================================
type FirewallAliasResponse struct { type FirewallAliasResponse struct {
Data FirewallAlias `json:"data"` Data FirewallAlias `json:"data"`
} }
// FirewallAlias represents a firewall alias in pfSense
type FirewallAlias struct { type FirewallAlias struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -31,6 +33,10 @@ type FirewallAlias struct {
Detail []string `json:"detail"` Detail []string `json:"detail"`
} }
// =========================================================================
// Interface Implementation
// =========================================================================
func init() { func init() {
Register(&pfSenseIntegration{}) Register(&pfSenseIntegration{})
} }
@@ -56,6 +62,10 @@ func (p *pfSenseIntegration) Validate(cfg config.AdvancedActionsConfig) error {
return nil return nil
} }
// =========================================================================
// Block/Unblock
// =========================================================================
func (p *pfSenseIntegration) BlockIP(req Request) error { func (p *pfSenseIntegration) BlockIP(req Request) error {
if err := p.Validate(req.Config); err != nil { if err := p.Validate(req.Config); err != nil {
return err return err
@@ -70,12 +80,14 @@ func (p *pfSenseIntegration) UnblockIP(req Request) error {
return p.modifyAliasIP(req, req.IP, "", false) return p.modifyAliasIP(req, req.IP, "", false)
} }
// modifyAliasIP implements the GET-modify-PATCH pattern for pfSense alias management // =========================================================================
// pfSense API
// =========================================================================
func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string, add bool) error { func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string, add bool) error {
cfg := req.Config.PfSense cfg := req.Config.PfSense
baseURL := strings.TrimSuffix(cfg.BaseURL, "/") baseURL := strings.TrimSuffix(cfg.BaseURL, "/")
// Create HTTP client
httpClient := &http.Client{ httpClient := &http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
} }
@@ -93,7 +105,6 @@ func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string,
if req.Logger != nil { if req.Logger != nil {
req.Logger("Alias %s not found, creating it automatically", cfg.Alias) req.Logger("Alias %s not found, creating it automatically", cfg.Alias)
} }
// Create a new alias with default values
newAlias := &FirewallAlias{ newAlias := &FirewallAlias{
Name: cfg.Alias, Name: cfg.Alias,
Type: "host", Type: "host",
@@ -111,9 +122,7 @@ func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string,
} }
} }
// Modify the address array
if add { if add {
// Check if IP already exists
ipExists := false ipExists := false
for _, addr := range alias.Address { for _, addr := range alias.Address {
if addr == ip { if addr == ip {
@@ -124,24 +133,21 @@ func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string,
if !ipExists { if !ipExists {
alias.Address = append(alias.Address, ip) alias.Address = append(alias.Address, ip)
if description != "" { if description != "" {
// Add description to detail array, matching the address array length
alias.Detail = append(alias.Detail, description) alias.Detail = append(alias.Detail, description)
} }
} else { } else {
if req.Logger != nil { if req.Logger != nil {
req.Logger("IP %s already exists in alias %s", ip, cfg.Alias) req.Logger("IP %s already exists in alias %s", ip, cfg.Alias)
} }
return nil // IP already blocked, consider it success return nil
} }
} else { } else {
// Remove IP from address array
found := false found := false
newAddress := make([]string, 0, len(alias.Address)) newAddress := make([]string, 0, len(alias.Address))
newDetail := make([]string, 0, len(alias.Detail)) newDetail := make([]string, 0, len(alias.Detail))
for i, addr := range alias.Address { for i, addr := range alias.Address {
if addr != ip { if addr != ip {
newAddress = append(newAddress, addr) newAddress = append(newAddress, addr)
// Keep corresponding detail if it exists
if i < len(alias.Detail) { if i < len(alias.Detail) {
newDetail = append(newDetail, alias.Detail[i]) newDetail = append(newDetail, alias.Detail[i])
} }
@@ -153,20 +159,17 @@ func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string,
if req.Logger != nil { if req.Logger != nil {
req.Logger("IP %s not found in alias %s", ip, cfg.Alias) req.Logger("IP %s not found in alias %s", ip, cfg.Alias)
} }
return nil // IP not in alias, consider it success return nil
} }
alias.Address = newAddress alias.Address = newAddress
alias.Detail = newDetail alias.Detail = newDetail
} }
// PATCH the alias with updated configuration
if err := p.updateAlias(httpClient, baseURL, cfg.APIToken, alias, req.Logger); err != nil { if err := p.updateAlias(httpClient, baseURL, cfg.APIToken, alias, req.Logger); err != nil {
return fmt.Errorf("failed to update alias %s: %w", cfg.Alias, err) return fmt.Errorf("failed to update alias %s: %w", cfg.Alias, err)
} }
// Apply firewall changes
if err := p.applyFirewallChanges(httpClient, baseURL, cfg.APIToken, req.Logger); err != nil { if err := p.applyFirewallChanges(httpClient, baseURL, cfg.APIToken, req.Logger); err != nil {
// Log warning but don't fail - the alias was updated successfully
if req.Logger != nil { if req.Logger != nil {
req.Logger("Warning: failed to apply firewall changes: %v", err) req.Logger("Warning: failed to apply firewall changes: %v", err)
} }
@@ -183,11 +186,9 @@ func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string,
return nil return nil
} }
// getAliasByName retrieves a firewall alias by name using GET /api/v2/firewall/aliases
func (p *pfSenseIntegration) getAliasByName(client *http.Client, baseURL, apiToken, aliasName string, logger func(string, ...interface{})) (*FirewallAlias, error) { func (p *pfSenseIntegration) getAliasByName(client *http.Client, baseURL, apiToken, aliasName string, logger func(string, ...interface{})) (*FirewallAlias, error) {
apiURL := baseURL + "/api/v2/firewall/aliases" apiURL := baseURL + "/api/v2/firewall/aliases"
// Add query parameter for alias name filtering
u, err := url.Parse(apiURL) u, err := url.Parse(apiURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err) return nil, fmt.Errorf("failed to parse URL: %w", err)
@@ -226,7 +227,6 @@ func (p *pfSenseIntegration) getAliasByName(client *http.Client, baseURL, apiTok
return nil, fmt.Errorf("pfSense API GET failed: status %s, response: %s", resp.Status, bodyStr) return nil, fmt.Errorf("pfSense API GET failed: status %s, response: %s", resp.Status, bodyStr)
} }
// The plural endpoint returns an array of aliases in the data field
var listResp struct { var listResp struct {
Data []FirewallAlias `json:"data"` Data []FirewallAlias `json:"data"`
} }
@@ -234,7 +234,6 @@ func (p *pfSenseIntegration) getAliasByName(client *http.Client, baseURL, apiTok
return nil, fmt.Errorf("failed to decode pfSense alias response: %w", err) return nil, fmt.Errorf("failed to decode pfSense alias response: %w", err)
} }
// Find the alias with matching name (query parameter may return multiple results)
for i := range listResp.Data { for i := range listResp.Data {
if listResp.Data[i].Name == aliasName { if listResp.Data[i].Name == aliasName {
return &listResp.Data[i], nil return &listResp.Data[i], nil
@@ -244,11 +243,9 @@ func (p *pfSenseIntegration) getAliasByName(client *http.Client, baseURL, apiTok
return nil, fmt.Errorf("alias %s not found", aliasName) return nil, fmt.Errorf("alias %s not found", aliasName)
} }
// createAlias creates a new firewall alias using POST /api/v2/firewall/alias
func (p *pfSenseIntegration) createAlias(client *http.Client, baseURL, apiToken string, alias *FirewallAlias, logger func(string, ...interface{})) (*FirewallAlias, error) { func (p *pfSenseIntegration) createAlias(client *http.Client, baseURL, apiToken string, alias *FirewallAlias, logger func(string, ...interface{})) (*FirewallAlias, error) {
apiURL := baseURL + "/api/v2/firewall/alias" apiURL := baseURL + "/api/v2/firewall/alias"
// Prepare POST payload - exclude ID as it will be generated by pfSense
postPayload := map[string]interface{}{ postPayload := map[string]interface{}{
"name": alias.Name, "name": alias.Name,
"type": alias.Type, "type": alias.Type,
@@ -294,7 +291,6 @@ func (p *pfSenseIntegration) createAlias(client *http.Client, baseURL, apiToken
return nil, fmt.Errorf("pfSense API POST failed: status %s, response: %s", resp.Status, bodyStr) return nil, fmt.Errorf("pfSense API POST failed: status %s, response: %s", resp.Status, bodyStr)
} }
// Parse the response to get the created alias with its ID
var createResp FirewallAliasResponse var createResp FirewallAliasResponse
if err := json.Unmarshal(bodyBytes, &createResp); err != nil { if err := json.Unmarshal(bodyBytes, &createResp); err != nil {
return nil, fmt.Errorf("failed to decode pfSense alias creation response: %w", err) return nil, fmt.Errorf("failed to decode pfSense alias creation response: %w", err)
@@ -307,20 +303,13 @@ func (p *pfSenseIntegration) createAlias(client *http.Client, baseURL, apiToken
return &createResp.Data, nil return &createResp.Data, nil
} }
// updateAlias updates a firewall alias using PATCH /api/v2/firewall/alias
// The id must be included in the request body, not in the URL path
func (p *pfSenseIntegration) updateAlias(client *http.Client, baseURL, apiToken string, alias *FirewallAlias, logger func(string, ...interface{})) error { func (p *pfSenseIntegration) updateAlias(client *http.Client, baseURL, apiToken string, alias *FirewallAlias, logger func(string, ...interface{})) error {
apiURL := baseURL + "/api/v2/firewall/alias" apiURL := baseURL + "/api/v2/firewall/alias"
// Prepare PATCH payload - include id in the request body
// pfSense requires that detail cannot have more items than address
// Always include detail array to ensure it matches address length
detailToSend := alias.Detail detailToSend := alias.Detail
if len(detailToSend) > len(alias.Address) { if len(detailToSend) > len(alias.Address) {
// Truncate detail to match address length
detailToSend = detailToSend[:len(alias.Address)] detailToSend = detailToSend[:len(alias.Address)]
} }
// If address is empty, detail must also be empty
if len(alias.Address) == 0 { if len(alias.Address) == 0 {
detailToSend = []string{} detailToSend = []string{}
} }
@@ -331,7 +320,7 @@ func (p *pfSenseIntegration) updateAlias(client *http.Client, baseURL, apiToken
"type": alias.Type, "type": alias.Type,
"descr": alias.Descr, "descr": alias.Descr,
"address": alias.Address, "address": alias.Address,
"detail": detailToSend, // Always include detail to ensure it's cleared when address is empty "detail": detailToSend,
} }
data, err := json.Marshal(patchPayload) data, err := json.Marshal(patchPayload)
@@ -376,7 +365,7 @@ func (p *pfSenseIntegration) updateAlias(client *http.Client, baseURL, apiToken
return nil return nil
} }
// applyFirewallChanges applies firewall changes using POST /api/v2/firewall/apply // Applies firewall changes
func (p *pfSenseIntegration) applyFirewallChanges(client *http.Client, baseURL, apiToken string, logger func(string, ...interface{})) error { func (p *pfSenseIntegration) applyFirewallChanges(client *http.Client, baseURL, apiToken string, logger func(string, ...interface{})) error {
apiURL := baseURL + "/api/v2/firewall/apply" apiURL := baseURL + "/api/v2/firewall/apply"

View File

@@ -7,7 +7,11 @@ import (
"github.com/swissmakers/fail2ban-ui/internal/config" "github.com/swissmakers/fail2ban-ui/internal/config"
) )
// Request represents a block/unblock request for an integration plugin. // =========================================================================
// Types
// =========================================================================
// Block/Unblock request for an integration.
type Request struct { type Request struct {
Context context.Context Context context.Context
IP string IP string
@@ -17,7 +21,7 @@ type Request struct {
Logger func(format string, args ...interface{}) Logger func(format string, args ...interface{})
} }
// Integration exposes functionality required by an external firewall vendor. // Exposes functionality required by an external firewall vendor.
type Integration interface { type Integration interface {
ID() string ID() string
DisplayName() string DisplayName() string
@@ -28,7 +32,11 @@ type Integration interface {
var registry = map[string]Integration{} var registry = map[string]Integration{}
// Register adds an integration to the global registry. // =========================================================================
// Registry
// =========================================================================
// Adds an integration to the registry.
func Register(integration Integration) { func Register(integration Integration) {
if integration == nil { if integration == nil {
return return
@@ -36,13 +44,13 @@ func Register(integration Integration) {
registry[integration.ID()] = integration registry[integration.ID()] = integration
} }
// Get returns the integration by id. // Returns the integration by id.
func Get(id string) (Integration, bool) { func Get(id string) (Integration, bool) {
integration, ok := registry[id] integration, ok := registry[id]
return integration, ok return integration, ok
} }
// MustGet obtains the integration or panics used during init. // Returns the integration or panics.
func MustGet(id string) Integration { func MustGet(id string) Integration {
integration, ok := Get(id) integration, ok := Get(id)
if !ok { if !ok {
@@ -51,7 +59,7 @@ func MustGet(id string) Integration {
return integration return integration
} }
// Supported returns ids of all registered integrations. // Returns all registered integration ids.
func Supported() []string { func Supported() []string {
keys := make([]string, 0, len(registry)) keys := make([]string, 0, len(registry))
for id := range registry { for id := range registry {