Files

ACL (Access Control List)

Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.

Overview

The ACL package provides network-level access control by wrapping TCP listeners and validating incoming connections against configurable allow/deny rules. It integrates with MaxMind GeoIP for geographic-based filtering and supports access logging with notification batching.

Primary consumers

  • internal/entrypoint - Wraps the main TCP listener for connection filtering
  • Operators - Configure rules via YAML configuration

Non-goals

  • HTTP request-level filtering (handled by middleware)
  • Authentication or authorization (see internal/auth)
  • VPN or tunnel integration

Stability

Stable internal package. The public API is the Config struct and its methods.

Public API

Exported types

type Config struct {
    Default    string                     // "allow" or "deny" (default: "allow")
    AllowLocal *bool                      // Allow private/loopback IPs (default: true)
    Allow      Matchers                   // Allow rules
    Deny       Matchers                   // Deny rules
    Log        *accesslog.ACLLoggerConfig // Access logging configuration

    Notify struct {
        To             []string      // Notification providers
        Interval       time.Duration // Notification frequency (default: 1m)
        IncludeAllowed *bool         // Include allowed in notifications (default: false)
    }
}
type Matcher struct {
    match MatcherFunc
}
type Matchers []Matcher

Exported functions and methods

func (c *Config) Validate() gperr.Error

Validates configuration and sets defaults. Must be called before Start.

func (c *Config) Start(parent task.Parent) gperr.Error

Initializes the ACL, starts the logger and notification goroutines.

func (c *Config) IPAllowed(ip net.IP) bool

Returns true if the IP is allowed based on configured rules. Performs caching and GeoIP lookup if needed.

func (c *Config) WrapTCP(lis net.Listener) net.Listener

Wraps a net.Listener to filter connections by IP.

func (matcher *Matcher) Parse(s string) error

Parses a matcher string in the format {type}:{value}. Supported types: ip, cidr, tz, country.

Architecture

Core components

graph TD
    A[TCP Listener] --> B[TCPListener Wrapper]
    B --> C{IP Allowed?}
    C -->|Yes| D[Accept Connection]
    C -->|No| E[Close Connection]

    F[Config] --> G[Validate]
    G --> H[Start]
    H --> I[Matcher Evaluation]
    I --> C

    J[MaxMind] -.-> K[IP Lookup]
    K -.-> I

    L[Access Logger] -.-> M[Log & Notify]
    M -.-> B

Connection filtering flow

sequenceDiagram
    participant Client
    participant TCPListener
    participant Config
    participant MaxMind
    participant Logger

    Client->>TCPListener: Connection Request
    TCPListener->>Config: IPAllowed(clientIP)

    alt Loopback IP
        Config-->>TCPListener: true
    else Private IP (allow_local)
        Config-->>TCPListener: true
    else Cached Result
        Config-->>TCPListener: Cached Result
    else Evaluate Allow Rules
        Config->>Config: Check Allow list
        alt Matches
            Config->>Config: Cache true
            Config-->>TCPListener: Allowed
        else Evaluate Deny Rules
            Config->>Config: Check Deny list
            alt Matches
                Config->>Config: Cache false
                Config-->>TCPListener: Denied
            else Default Action
                Config->>MaxMind: Lookup GeoIP
                MaxMind-->>Config: IPInfo
                Config->>Config: Apply default rule
                Config->>Config: Cache result
                Config-->>TCPListener: Result
            end
        end
    end

    alt Logging enabled
        Config->>Logger: Log access attempt
    end

Matcher types

Type Format Example
IP ip:address ip:192.168.1.1
CIDR cidr:network cidr:192.168.0.0/16
TimeZone tz:timezone tz:Asia/Shanghai
Country country:ISOCode country:GB

Configuration Surface

Config sources

Configuration is loaded from config/config.yml under the acl key.

Schema

acl:
  default: "allow"              # "allow" or "deny"
  allow_local: true             # Allow private/loopback IPs
  log:
    log_allowed: false          # Log allowed connections
  notify:
    to: ["gotify"]              # Notification providers
    interval: "1m"              # Notification interval
    include_allowed: false      # Include allowed in notifications

Hot-reloading

Configuration requires restart. The ACL does not support dynamic rule updates.

Dependency and Integration Map

Internal dependencies

  • internal/maxmind - IP geolocation lookup
  • internal/logging/accesslog - Access logging
  • internal/notif - Notifications
  • internal/task/task.go - Lifetime management

Integration points

// Entrypoint uses ACL to wrap the TCP listener
aclListener := config.ACL.WrapTCP(listener)
http.Server.Serve(aclListener, entrypoint)

Observability

Logs

  • ACL started - Configuration summary on start
  • log_notify_loop - Access attempts (allowed/denied)

Log levels: Info for startup, Debug for client closure.

Metrics

No metrics are currently exposed.

Security Considerations

  • Loopback and private IPs are always allowed unless explicitly denied
  • Cache TTL is 1 minute to limit memory usage
  • Notification channel has a buffer of 100 to prevent blocking
  • Failed connections are immediately closed without response

Failure Modes and Recovery

Failure Behavior Recovery
Invalid matcher syntax Validation fails on startup Fix configuration syntax
MaxMind database unavailable GeoIP lookups return unknown location Default action applies; cache hit still works
Notification provider unavailable Notification dropped Error logged, continues operation
Cache full No eviction, uses Go map No action needed

Usage Examples

Basic configuration

aclConfig := &acl.Config{
    Default:    "allow",
    AllowLocal: ptr(true),
    Allow: acl.Matchers{
        {match: matchIP(net.ParseIP("192.168.1.0/24"))},
    },
    Deny: acl.Matchers{
        {match: matchISOCode("CN")},
    },
}
if err := aclConfig.Validate(); err != nil {
    log.Fatal(err)
}
if err := aclConfig.Start(parent); err != nil {
    log.Fatal(err)
}

Wrapping a TCP listener

listener, err := net.Listen("tcp", ":443")
if err != nil {
    log.Fatal(err)
}

// Wrap with ACL
aclListener := aclConfig.WrapTCP(listener)

// Use with HTTP server
server := &http.Server{}
server.Serve(aclListener)

Creating custom matchers

matcher := &acl.Matcher{}
err := matcher.Parse("country:US")
if err != nil {
    log.Fatal(err)
}

// Use the matcher
allowed := matcher.match(ipInfo)