package integrations import ( "bytes" "crypto/tls" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/swissmakers/fail2ban-ui/internal/config" ) type pfSenseIntegration struct{} func init() { Register(&pfSenseIntegration{}) } func (p *pfSenseIntegration) ID() string { return "pfsense" } func (p *pfSenseIntegration) DisplayName() string { return "pfSense" } func (p *pfSenseIntegration) Validate(cfg config.AdvancedActionsConfig) error { if cfg.PfSense.BaseURL == "" { return fmt.Errorf("pfSense base URL is required") } if cfg.PfSense.APIToken == "" { return fmt.Errorf("pfSense API key is required") } if cfg.PfSense.Alias == "" { return fmt.Errorf("pfSense alias is required") } return nil } func (p *pfSenseIntegration) BlockIP(req Request) error { if err := p.Validate(req.Config); err != nil { return err } payload := map[string]any{ "alias": req.Config.PfSense.Alias, "ip": req.IP, "descr": "Fail2ban-UI permanent block", } return p.callAPI(req, "add", payload) } func (p *pfSenseIntegration) UnblockIP(req Request) error { if err := p.Validate(req.Config); err != nil { return err } payload := map[string]any{ "alias": req.Config.PfSense.Alias, "ip": req.IP, } return p.callAPI(req, "delete", payload) } func (p *pfSenseIntegration) callAPI(req Request, action string, payload map[string]any) error { cfg := req.Config.PfSense payload["action"] = action data, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to encode pfSense payload: %w", err) } apiURL := strings.TrimSuffix(cfg.BaseURL, "/") + "/api/v2/firewall/alias/ip" httpClient := &http.Client{ Timeout: 10 * time.Second, } if cfg.SkipTLSVerify { httpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402 - user controlled } } reqLogger := "pfSense" if req.Logger != nil { req.Logger("Calling pfSense API %s action=%s payload=%s", apiURL, action, string(data)) } httpReq, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(data)) if err != nil { return fmt.Errorf("failed to create pfSense request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") // pfSense REST API v2 uses KeyAuth with x-api-key header only httpReq.Header.Set("x-api-key", cfg.APIToken) resp, err := httpClient.Do(httpReq) if err != nil { // Provide more specific error messages for connection issues if netErr, ok := err.(interface { Timeout() bool Error() string }); ok && netErr.Timeout() { return fmt.Errorf("pfSense API request to %s timed out: %w", apiURL, err) } return fmt.Errorf("pfSense API request to %s failed: %w (check base URL, network connectivity, and API credentials)", apiURL, err) } defer resp.Body.Close() // Read response body for better error messages bodyBytes, _ := io.ReadAll(resp.Body) bodyStr := strings.TrimSpace(string(bodyBytes)) if resp.StatusCode >= 300 { if bodyStr != "" { return fmt.Errorf("pfSense API request failed: status %s, response: %s", resp.Status, bodyStr) } return fmt.Errorf("pfSense API request failed: status %s (check API credentials and alias name)", resp.Status) } if req.Logger != nil { req.Logger("%s API call succeeded", reqLogger) if bodyStr != "" { req.Logger("%s API response: %s", reqLogger, bodyStr) } } return nil }