diff --git a/internal/integrations/pfsense.go b/internal/integrations/pfsense.go index b80921a..68fd629 100644 --- a/internal/integrations/pfsense.go +++ b/internal/integrations/pfsense.go @@ -88,7 +88,27 @@ func (p *pfSenseIntegration) modifyAliasIP(req Request, ip, description string, // GET the alias by name alias, err := p.getAliasByName(httpClient, baseURL, cfg.APIToken, cfg.Alias, req.Logger) if err != nil { - return fmt.Errorf("failed to get alias %s: %w", cfg.Alias, err) + // If alias doesn't exist, create it automatically + if strings.Contains(err.Error(), "not found") { + if req.Logger != nil { + req.Logger("Alias %s not found, creating it automatically", cfg.Alias) + } + // Create a new alias with default values + newAlias := &FirewallAlias{ + Name: cfg.Alias, + Type: "host", + Descr: "Fail2ban-UI alias", + Address: []string{}, + Detail: []string{}, + } + createdAlias, createErr := p.createAlias(httpClient, baseURL, cfg.APIToken, newAlias, req.Logger) + if createErr != nil { + return fmt.Errorf("failed to create alias %s: %w", cfg.Alias, createErr) + } + alias = createdAlias + } else { + return fmt.Errorf("failed to get alias %s: %w", cfg.Alias, err) + } } // Modify the address array @@ -224,12 +244,77 @@ func (p *pfSenseIntegration) getAliasByName(client *http.Client, baseURL, apiTok return nil, fmt.Errorf("alias %s not found", aliasName) } -// updateAlias updates a firewall alias using PATCH /api/v2/firewall/alias/{id} -func (p *pfSenseIntegration) updateAlias(client *http.Client, baseURL, apiToken string, alias *FirewallAlias, logger func(string, ...interface{})) error { - apiURL := fmt.Sprintf("%s/api/v2/firewall/alias/%d", baseURL, alias.ID) +// 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) { + apiURL := baseURL + "/api/v2/firewall/alias" - // Prepare PATCH payload - only include fields that can be updated + // Prepare POST payload - exclude ID as it will be generated by pfSense + postPayload := map[string]interface{}{ + "name": alias.Name, + "type": alias.Type, + "descr": alias.Descr, + "address": alias.Address, + } + if len(alias.Detail) > 0 { + postPayload["detail"] = alias.Detail + } + + data, err := json.Marshal(postPayload) + if err != nil { + return nil, fmt.Errorf("failed to encode pfSense POST payload: %w", err) + } + + if logger != nil { + logger("Calling pfSense API POST %s payload=%s", apiURL, string(data)) + } + + httpReq, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to create pfSense POST request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("x-api-key", apiToken) + + resp, err := client.Do(httpReq) + if err != nil { + if netErr, ok := err.(interface { + Timeout() bool + Error() string + }); ok && netErr.Timeout() { + return nil, fmt.Errorf("pfSense API request to %s timed out: %w", apiURL, err) + } + return nil, fmt.Errorf("pfSense API request to %s failed: %w", apiURL, err) + } + defer resp.Body.Close() + + bodyBytes, _ := io.ReadAll(resp.Body) + bodyStr := strings.TrimSpace(string(bodyBytes)) + + if resp.StatusCode >= 300 { + 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 + if err := json.Unmarshal(bodyBytes, &createResp); err != nil { + return nil, fmt.Errorf("failed to decode pfSense alias creation response: %w", err) + } + + if logger != nil { + logger("pfSense API POST succeeded: alias %s created with ID %d", createResp.Data.Name, createResp.Data.ID) + } + + 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 { + apiURL := baseURL + "/api/v2/firewall/alias" + + // Prepare PATCH payload - include id in the request body patchPayload := map[string]interface{}{ + "id": alias.ID, "name": alias.Name, "type": alias.Type, "descr": alias.Descr,