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 lookupinternal/logging/accesslog- Access logginginternal/notif- Notificationsinternal/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 startlog_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)