Files
f2b/docs/api.md
Ismo Vuorinen 98b53d84b5 fix: repair Renovate config, convert Makefile to go run, update GitHub Actions (#120)
* fix: repair Renovate config and convert Makefile to go run (#117)

- Remove non-existent `github>renovatebot/presets:golang` preset that
  broke Renovate PR creation
- Replace deprecated `fileMatch` with `managerFilePatterns` in
  customManagers
- Rewrite regex to match new Makefile pattern (renovate comment above
  version variable assignment)
- Fix `matchFileNames` glob pattern (`*.mk` -> `**/*.mk`)
- Convert all tool invocations from `go install` + global binary to
  `go run tool@version` for reproducible builds
- Convert npm global tools to `npx --yes` invocations
- Remove `dev-deps` and `check-deps` targets (tools auto-download)
- Add mdformat pre-commit hook with GFM support and config
- Add `fmt-md` Makefile target for manual markdown formatting
- Update local golangci-lint pre-commit hook to use `go run`
- Apply golangci-lint v2.10.1 auto-fixes (fmt.Fprintf optimization)
- Add nolint:gosec annotations for legitimate exec.Command usage
- Exclude .serena/ from mdformat and megalinter
- Add markdown indent_size=unset in .editorconfig for CommonMark compat

* chore(deps): update GitHub Actions to latest versions

- anthropics/claude-code-action: v1.0.34 -> v1.0.64
- actions/setup-go: v6.2.0 -> v6.3.0
- actions/upload-artifact: v6.0.0 -> v7.0.0
- goreleaser/goreleaser-action: v6.4.0 -> v7.0.0
- docker/login-action: v3.6.0 -> v3.7.0
- ivuorinen/actions: v2026.01.21 -> v2026.02.24

* fix: address code review feedback

- Fix issue template YAML frontmatter (replace underscore separators
  with proper --- delimiters); exclude templates from mdformat
- Replace string(rune(n)) with strconv.Itoa(n) in test files to produce
  deterministic numeric directory names instead of Unicode characters
- Remove stale `make dev-deps` reference in README, replace with
  `make dev-setup`
- Extract ban/unban format strings into shared.MetricsFmtBanOperations
  and shared.MetricsFmtUnbanOperations constants
- Replace hardcoded coverage percentages in README with evergreen
  phrasing

* fix: address round 2 code review feedback for PR #120

- Fix corrupted path traversal example in docs/security.md
- Fix Renovate .mk regex to match nested paths (.*\.mk$)
- Update checkmake pre-commit hook to v0.3.2 to match Makefile
- Add sync.WaitGroup to unsynchronized goroutines in security tests
- Fix fmt-md target to use pre-commit run mdformat
- Pin markdownlint-cli2 to v0.21.0 in lint-md target
- Standardize //nolint:gosec to // #nosec annotations for gosec CLI

* fix(ci): install PyYAML dependency for PR lint workflow

The pr-lint workflow uses ivuorinen/actions/pr-lint which internally
calls validate-inputs running a Python script that imports yaml.
Python was set up but PyYAML was never installed, causing
ModuleNotFoundError at runtime.

* fix: address round 3 code review feedback for PR #120

- Wrap Windows-style path traversal example in backtick code span so
  backslashes render literally in docs/security.md
- Add Renovate-managed MARKDOWNLINT_CLI2_VERSION variable in Makefile
  to match the pattern used by all other tool versions
2026-03-01 19:09:17 +02:00

14 KiB

f2b Internal API Documentation

This document provides comprehensive documentation for the internal APIs and interfaces used in the f2b project. This is intended for developers who want to contribute to the project or integrate with its components.

Table of Contents

Core Interfaces

fail2ban.Client Interface

The core interface for interacting with fail2ban operations.

type Client interface {
    // Basic operations
    BanIP(ip, jail string) (int, error)
    UnbanIP(ip, jail string) (int, error)
    BanIPWithContext(ctx context.Context, ip, jail string) (int, error)
    UnbanIPWithContext(ctx context.Context, ip, jail string) (int, error)

    // Information retrieval
    ListJails() ([]string, error)
    ListJailsWithContext(ctx context.Context) ([]string, error)
    StatusAll() (string, error)
    StatusJail(jail string) (string, error)
    GetBanRecords(jails []string) ([]BanRecord, error)
    BannedIn(ip string) ([]string, error)

    // Filter operations
    ListFilters() ([]string, error)
    TestFilter(filter, logfile string, verbose bool) ([]string, error)
}

Implementation Examples

Using the Client Interface

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/ivuorinen/f2b/fail2ban"
)

func banIPExample() error {
    // Create a client
    client, err := fail2ban.NewClient()
    if err != nil {
        return fmt.Errorf("failed to create client: %w", err)
    }

    // Ban an IP with timeout context
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    code, err := client.BanIPWithContext(ctx, "192.168.1.100", "sshd")
    if err != nil {
        return fmt.Errorf("ban operation failed: %w", err)
    }

    fmt.Printf("Ban operation completed with code: %d\n", code)
    return nil
}

Client Package

Real Client

The RealClient struct implements the Client interface for actual fail2ban operations.

type RealClient struct {
    path          string        // Path to fail2ban-client binary
    timeout       time.Duration // Default timeout for operations
    sudoChecker   SudoChecker   // Interface for sudo privilege checking
    runner        Runner        // Interface for command execution
}

Configure RealClient

// Create a new client with custom timeout
client, err := fail2ban.NewClientWithTimeout(45 * time.Second)
if err != nil {
    return err
}

// Create a client with custom sudo checker
customSudoChecker := &MyCustomSudoChecker{}
client, err := fail2ban.NewClientWithSudo(customSudoChecker)

Mock Client

For testing purposes, use the MockClient:

func TestMyFunction(t *testing.T) {
    mock := fail2ban.NewMockClient()

    // Configure mock responses
    mock.Jails = map[string]struct{}{
        "sshd": {},
        "apache": {},
    }

    mock.Banned = map[string]map[string]time.Time{
        "sshd": {"192.168.1.100": time.Now()},
    }

    // Use mock in your test
    result, err := myFunction(mock)
    // ... assertions
}

Command Package

Config Structure

The central configuration structure for the CLI:

type Config struct {
    LogDir          string        // Path to Fail2Ban log directory
    FilterDir       string        // Path to Fail2Ban filter directory
    Format          string        // Output format: "plain" or "json"
    CommandTimeout  time.Duration // Timeout for individual fail2ban commands
    FileTimeout     time.Duration // Timeout for file operations
    ParallelTimeout time.Duration // Timeout for parallel operations
}

Configuration Validation

func validateConfig() error {
    config := cmd.NewConfigFromEnv()

    // Validate the configuration
    if err := config.ValidateConfig(); err != nil {
        return fmt.Errorf("invalid configuration: %w", err)
    }

    return nil
}

Command Helpers

The cmd package provides several helper functions for command creation and validation:

// Command creation
func NewCommand(
  use,
  short string,
  aliases []string,
  runE func(*cobra.Command, []string) error
) *cobra.Command

// Validation helpers
func ValidateIPArgument(args []string) (string, error)
func ValidateServiceAction(action string) error

// Jail operations
func GetJailsFromArgsWithContext(
  ctx context.Context,
  client fail2ban.Client,
  args []string,
  startIndex int
) ([]string, error)

// Error handling
func HandleClientError(err error) error
func PrintErrorAndReturn(err error) error

Error Handling

Contextual Errors

The f2b project uses enhanced error handling with context and remediation hints:

type ContextualError struct {
    Message     string
    Category    ErrorCategory
    Remediation string
    Cause       error
}

// Create contextual errors
validationErr := fail2ban.NewValidationError(
    "invalid IP address: 192.168.1.999",
    "Provide a valid IPv4 or IPv6 address",
)

systemErr := fail2ban.NewSystemError(
    "fail2ban service not running",
    "Start the service with: sudo systemctl start fail2ban",
    originalError,
)

Error Categories

const (
    ErrorCategoryValidation  ErrorCategory = "validation"
    ErrorCategoryNetwork     ErrorCategory = "network"
    ErrorCategoryPermission  ErrorCategory = "permission"
    ErrorCategorySystem      ErrorCategory = "system"
    ErrorCategoryConfig      ErrorCategory = "config"
)

Configuration

Environment Variables

The configuration system supports the following environment variables:

Variable Description Default
F2B_LOG_DIR Log directory path /var/log
F2B_FILTER_DIR Filter directory path /etc/fail2ban/filter.d
F2B_LOG_LEVEL Log level info
F2B_COMMAND_TIMEOUT Command timeout 30s
F2B_FILE_TIMEOUT File operation timeout 10s
F2B_PARALLEL_TIMEOUT Parallel operation timeout 60s

Path Security

All configuration paths undergo comprehensive validation:

func validateConfigPath(path, pathType string) (string, error)

This function:

  • Checks for path traversal attempts
  • Validates against null byte injection
  • Ensures paths are within reasonable system locations
  • Resolves to absolute paths
  • Enforces length limits

Logging and Metrics

Contextual Logging

The logging system supports structured logging with context propagation:

// Create a contextual logger
logger := cmd.NewContextualLogger()

// Add context to logging
ctx := cmd.WithOperation(context.Background(), "ban_ip")
ctx = cmd.WithIP(ctx, "192.168.1.100")
ctx = cmd.WithJail(ctx, "sshd")

// Log with context
logger.WithContext(ctx).Info("Starting ban operation")

// Log operations with timing
err := logger.LogOperation(ctx, "ban_operation", func() error {
    return client.BanIP("192.168.1.100", "sshd")
})

Performance Metrics

The metrics system provides comprehensive performance monitoring:

// Get global metrics
metrics := cmd.GetGlobalMetrics()

// Record operations
metrics.RecordCommandExecution("ban", duration, success)
metrics.RecordBanOperation("ban", duration, success)
metrics.RecordValidationCacheHit()

// Get metrics snapshot
snapshot := metrics.GetSnapshot()
fmt.Printf("Command executions: %d\n", snapshot.CommandExecutions)
fmt.Printf("Average latency: %.2fms\n", snapshot.CommandLatencyBuckets["ban"].GetAverageLatency())

Timed Operations

Use timed operations for automatic instrumentation:

func performBanOperation(ctx context.Context, ip, jail string) error {
    metrics := cmd.GetGlobalMetrics()
    timer := cmd.NewTimedOperation(ctx, metrics, "ban", "ban_ip")

    // Perform the operation
    err := client.BanIP(ip, jail)

    // Record timing and success/failure
    timer.Finish(err == nil)

    return err
}

Testing Framework

Modern Test Framework

The f2b project includes a fluent testing framework for command testing:

func TestBanCommand(t *testing.T) {
    cmd.NewCommandTest(t, "ban").
        WithArgs("192.168.1.100", "sshd").
        WithSetup(func(mock *fail2ban.MockClient) {
            setMockJails(mock, []string{"sshd"})
        }).
        ExpectSuccess().
        ExpectOutput("Banned 192.168.1.100 in sshd").
        Run()
}

Mock Client Builder

For complex mock scenarios:

func TestComplexScenario(t *testing.T) {
    mockBuilder := cmd.NewMockClientBuilder().
        WithJails("sshd", "apache").
        WithBannedIP("192.168.1.100", "sshd").
        WithBanRecord("sshd", "192.168.1.100", "01:30:00").
        WithStatusResponse("sshd", "Status: active")

    cmd.NewCommandTest(t, "status").
        WithArgs("sshd").
        WithMockBuilder(mockBuilder).
        ExpectSuccess().
        Run()
}

Environment Setup

Use standardized environment setup:

func TestWithMockEnvironment(t *testing.T) {
    _, cleanup := fail2ban.SetupMockEnvironmentWithSudo(t, true)
    defer cleanup()

    // Test implementation with mocked environment
}

Examples

Complete Command Implementation

Here's how to implement a new command following f2b patterns:

package cmd

import (
    "context"
    "fmt"

    "github.com/spf13/cobra"
    "github.com/ivuorinen/f2b/fail2ban"
)

// MyCmd implements a new command with full context and error handling
func MyCmd(client fail2ban.Client, config *Config) *cobra.Command {
    return NewCommand("mycmd <arg>", "My command description", []string{"mc"},
        func(cmd *cobra.Command, args []string) error {
            // Create timeout context
            ctx, cancel := context.WithTimeout(context.Background(), config.CommandTimeout)
            defer cancel()

            // Validate arguments
            if len(args) < 1 {
                return PrintErrorAndReturn(fail2ban.ErrActionRequiredError)
            }

            // Add context for logging
            ctx = WithOperation(ctx, "my_operation")
            ctx = WithCommand(ctx, "mycmd")

            // Log operation with timing
            logger := GetContextualLogger()
            return logger.LogOperation(ctx, "my_command", func() error {
                // Perform operation with client
                result, err := client.SomeOperation(args[0])
                if err != nil {
                    return HandleClientError(err)
                }

                // Output results
                OutputResults(cmd, result, config)
                return nil
            })
        })
}

Custom Client Implementation

package main

import (
    "context"
    "time"

    "github.com/ivuorinen/f2b/fail2ban"
)

// CustomClient implements the Client interface with custom logic
type CustomClient struct {
    baseClient fail2ban.Client
    customLogic string
}

func (c *CustomClient) BanIP(ip, jail string) (int, error) {
    // Custom pre-processing
    if err := c.preprocessBan(ip, jail); err != nil {
        return 0, err
    }

    // Delegate to base client
    return c.baseClient.BanIP(ip, jail)
}

func (c *CustomClient) BanIPWithContext(ctx context.Context, ip, jail string) (int, error) {
    // Context-aware implementation
    select {
    case <-ctx.Done():
        return 0, ctx.Err()
    default:
        return c.BanIP(ip, jail)
    }
}

// Implement other Client interface methods...

func (c *CustomClient) preprocessBan(ip, jail string) error {
    // Custom validation or processing logic
    return nil
}

Integration with External Systems

package integration

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/ivuorinen/f2b/fail2ban"
    "github.com/ivuorinen/f2b/cmd"
)

// HTTPHandler provides HTTP API integration
type HTTPHandler struct {
    client fail2ban.Client
    logger *cmd.ContextualLogger
}

func (h *HTTPHandler) BanHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Extract parameters
    ip := r.URL.Query().Get("ip")
    jail := r.URL.Query().Get("jail")

    // Validate
    if err := fail2ban.ValidateIP(ip); err != nil {
        h.writeError(w, http.StatusBadRequest, err)
        return
    }

    // Perform operation with logging
    err := h.logger.LogOperation(ctx, "http_ban", func() error {
        _, err := h.client.BanIPWithContext(ctx, ip, jail)
        return err
    })

    if err != nil {
        h.writeError(w, http.StatusInternalServerError, err)
        return
    }

    // Success response
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "status": "success",
        "ip": ip,
        "jail": jail,
    })
}

func (h *HTTPHandler) writeError(w http.ResponseWriter, code int, err error) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)

    response := map[string]string{"error": err.Error()}

    // Add remediation hint if available
    if contextErr, ok := err.(*fail2ban.ContextualError); ok {
        response["hint"] = contextErr.GetRemediation()
        response["category"] = string(contextErr.GetCategory())
    }

    json.NewEncoder(w).Encode(response)
}

Best Practices

Error Handling Best Practices

  1. Always use contextual errors for user-facing messages
  2. Provide remediation hints where possible
  3. Log errors with appropriate context
  4. Use error categories for systematic handling

Context Usage

  1. Always use context for operations that can timeout
  2. Propagate context through the call chain
  3. Add relevant context values for logging
  4. Use context cancellation for cleanup

Testing

  1. Use the fluent testing framework for command tests
  2. Always use mock environments for integration tests
  3. Test both success and failure scenarios
  4. Include timeout testing for long-running operations

Performance

  1. Use the metrics system to monitor performance
  2. Implement proper caching where appropriate
  3. Use object pooling for frequently allocated objects
  4. Profile and optimize hot paths

This documentation provides a comprehensive overview of the f2b internal APIs and patterns. For specific implementation details, refer to the source code and inline documentation.