swg

package module
v0.0.0-...-4140114 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 14, 2026 License: MIT Imports: 63 Imported by: 0

README

SWG - Secure Web Gateway

Go Reference CI codecov Go Report Card

An HTTPS man-in-the-middle (MITM) proxy for content filtering written in Go. SWG intercepts HTTPS traffic by dynamically generating certificates, allowing inspection and filtering of encrypted connections.

Features

  • SSL/TLS Interception: Decrypt HTTPS traffic using dynamically generated certificates
  • Content Filtering: Block requests based on domain names, URL prefixes, and regex patterns
  • Custom Block Pages: Fully customizable HTML block pages with template support
  • PAC File Generation: Generate Proxy Auto-Configuration files for client setup
  • Prometheus Metrics: Built-in instrumentation for monitoring and alerting
  • Auto-Reloading Rules: Load blocklists from CSV, HTTP endpoints, or databases with periodic refresh
  • Configuration Files: YAML/JSON/TOML config with environment variable overrides
  • Health Check Endpoints: /healthz and /readyz probes for Kubernetes and load balancers
  • Structured Access Log: JSON access log with request metadata, timing, and filter decisions
  • SIGHUP Reload: Reload config and filter rules without restarting (kill -HUP <pid>)
  • Policy Engine: Lifecycle hooks for request/response interception with pluggable identity, group policies, and scanning
  • Allow-List Mode: Deny-by-default filtering for kiosk and restricted environments
  • Time-Based Rules: Schedule filter activation by hour-of-day and day-of-week with timezone support
  • Per-User/Group Policies: Apply different filters based on client identity resolved from IP, CIDR, or custom resolvers
  • Content-Type Filtering: Block responses by MIME type (e.g. executable downloads)
  • Response Body Scanning: Pluggable AV/DLP scanners with allow, block, and replace verdicts
  • Upstream Proxy Chaining: Forward through a parent proxy with CONNECT tunnel and PROXY protocol support
  • Connection Pooling: Configurable transport pool with HTTP/2 support and connection statistics
  • Rate Limiting: Per-client token-bucket rate limiter with automatic stale bucket cleanup
  • Admin API: REST endpoints for runtime rule CRUD, status inspection, and filter reloads via chi
  • mTLS Client Auth: Mutual TLS authentication requiring client certificates with identity/group extraction
  • Bypass Token: Allow authorized clients to skip filtering for debugging via header token or identity
  • Certificate Rotation: Hot-swap CA certificates at runtime without proxy restart
  • OpenTelemetry Tracing: Distributed tracing with W3C Trace Context propagation and OTLP exporters
  • Cross-Platform: Runs on Linux, macOS, and Windows

Installation

Homebrew (macOS/Linux)
brew install acmacalister/tap/swg
APT (Debian/Ubuntu)
echo "deb [trusted=yes] https://apt.fury.io/acmacalister/ /" | sudo tee /etc/apt/sources.list.d/swg.list
sudo apt update
sudo apt install swg
APK (Alpine Linux)
# Download the latest .apk from releases
sudo apk add --allow-untrusted swg_*.apk
Pacman (Arch Linux)
# Using yay
yay -S swg-bin

# Or download from releases
sudo pacman -U swg_*.pkg.tar.zst
RPM (Fedora/RHEL/CentOS)
sudo rpm -i swg_*.rpm
Go Install
go install github.com/acmacalister/swg/cmd@latest
From Source
git clone https://github.com/acmacalister/swg.git
cd swg
go build -o swg ./cmd
Docker
docker pull ghcr.io/acmacalister/swg:latest
docker run -p 8080:8080 -v $(pwd)/certs:/certs ghcr.io/acmacalister/swg:latest \
  -ca-cert /certs/ca.crt -ca-key /certs/ca.key

Quick Start

1. Generate CA Certificate
swg -gen-ca

This creates ca.crt and ca.key in the current directory.

2. Trust the CA Certificate

macOS:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt

Linux (Debian/Ubuntu):

sudo cp ca.crt /usr/local/share/ca-certificates/swg-ca.crt
sudo update-ca-certificates

Windows:

Import-Certificate -FilePath ca.crt -CertStoreLocation Cert:\LocalMachine\Root
3. Start the Proxy
swg -addr :8080 -block "ads.example.com,*.tracking.com" -v
4. Configure System Proxy

Set your system or browser to use localhost:8080 as the HTTP/HTTPS proxy.

CLI Usage

Usage of swg:
  -addr string
        proxy listen address (default ":8080")
  -block string
        comma-separated list of domains to block
  -block-page-file string
        path to custom block page HTML template
  -block-page-url string
        URL to redirect blocked requests to
  -ca-cert string
        path to CA certificate (default "ca.crt")
  -ca-key string
        path to CA private key (default "ca.key")
  -ca-org string
        organization name for generated CA (default "SWG Proxy")
  -config string
        path to config file (default: search ./swg.yaml, ~/.swg/config.yaml, /etc/swg/config.yaml)
  -gen-ca
        generate a new CA certificate and exit
  -gen-config
        generate example config file and exit
  -gen-pac
        generate a PAC file and exit
  -metrics
        enable Prometheus metrics endpoint
  -pac-bypass string
        comma-separated domains to bypass proxy in PAC file
  -print-block-page
        print default block page template and exit
  -v    verbose logging
  -access-log string
        access log output: stdout, stderr, or file path (disabled if empty)
  -healthz
        enable /healthz and /readyz health endpoints
Examples
# Basic usage with domain blocking
swg -block "facebook.com,twitter.com,*.ads.com"

# Using a config file
swg -config /etc/swg/swg.yaml

# Generate example config file
swg -gen-config

# Custom block page redirect
swg -block "malware.com" -block-page-url "https://internal.company.com/blocked"

# Custom block page template
swg -block "restricted.com" -block-page-file ./my-block-page.html

# Export default block page template for customization
swg -print-block-page > custom-block.html

# Generate a PAC file for client auto-configuration
swg -gen-pac -pac-bypass "internal.company.com,*.local"

# Enable Prometheus metrics on /metrics
swg -block "ads.com" -metrics -v

# Enable health check endpoints
swg -block "ads.com" -healthz

# Enable structured JSON access log to file
swg -block "ads.com" -access-log /var/log/swg/access.log

# Access log to stdout (useful in containers)
swg -block "ads.com" -access-log stdout

# Reload config/rules without restart
kill -HUP $(pidof swg)
Configuration File

SWG supports YAML, JSON, and TOML configuration files. Generate an example config:

swg -gen-config

Example swg.yaml:

server:
  addr: ":8080"
  read_timeout: 30s
  write_timeout: 30s

tls:
  ca_cert: "ca.crt"
  ca_key: "ca.key"
  organization: "SWG Proxy"

filter:
  enabled: true
  domains:
    - "ads.example.com"
    - "*.tracking.com"
  reload_interval: 5m

logging:
  level: "info"
  format: "json"

Config file search paths (in order):

  1. Explicit path via -config
  2. ./swg.yaml
  3. $HOME/.swg/config.yaml
  4. /etc/swg/config.yaml

Environment variables override config file values with SWG_ prefix:

  • SWG_SERVER_ADDR=:9090
  • SWG_TLS_ORGANIZATION="My Org"
  • SWG_FILTER_ENABLED=false

Library API

SWG can be used as a Go library for building custom proxy solutions.

Basic Proxy
package main

import (
    "log"
    "github.com/acmacalister/swg"
)

func main() {
    // Load CA certificate
    cm, err := swg.NewCertManager("ca.crt", "ca.key")
    if err != nil {
        log.Fatal(err)
    }

    // Create proxy
    proxy := swg.NewProxy(":8080", cm)

    // Start proxy
    log.Fatal(proxy.ListenAndServe())
}
Domain Filtering
// Create domain filter
filter := swg.NewDomainFilter()
filter.AddDomain("blocked.com")
filter.AddDomain("*.ads.example.com")  // Wildcard support
filter.AddDomains([]string{"evil.com", "malware.org"})

proxy.Filter = filter
Advanced Filtering with RuleSet

RuleSet supports domains, URLs, and regex patterns with categories:

// Create a rule set
rs := swg.NewRuleSet()

// Add domain rules
rs.AddDomain("blocked.com")
rs.AddDomain("*.ads.example.com")

// Add URL prefix rules
rs.AddURL("https://evil.com/malware")

// Add regex patterns
rs.AddRegex(`.*\.tracking\..*`)

// Add rules with full metadata
rs.AddRule(swg.Rule{
    Type:     "domain",
    Pattern:  "malware.com",
    Reason:   "known malware host",
    Category: "security",
})

proxy.Filter = rs
Loading Rules from CSV
// Create CSV loader
loader := swg.NewCSVLoader("blocklist.csv")
loader.HasHeader = true
loader.DefaultReason = "blocked by policy"

// Create reloadable filter
filter := swg.NewReloadableFilter(loader)

// Set up callbacks
filter.OnReload = func(count int) {
    log.Printf("Loaded %d rules", count)
}
filter.OnError = func(err error) {
    log.Printf("Reload error: %v", err)
}

// Initial load
ctx := context.Background()
filter.Load(ctx)

// Start auto-reload every 5 minutes
cancel := filter.StartAutoReload(ctx, 5*time.Minute)
defer cancel()

proxy.Filter = filter

CSV format: type,pattern,reason,category

type,pattern,reason,category
domain,ads.example.com,advertising,ads
domain,*.tracking.com,user tracking,analytics
url,https://phishing.com/login,phishing attempt,security
regex,.*\.doubleclick\.net.*,ad tracker,ads
Loading Rules from PostgreSQL

See _examples/postgres/ for a complete example using sqlx:

// Implement RuleLoader interface
type PostgresLoader struct {
    DB *sqlx.DB
}

func (l *PostgresLoader) Load(ctx context.Context) ([]swg.Rule, error) {
    var rules []swg.Rule
    err := l.DB.SelectContext(ctx, &rules, 
        `SELECT rule_type as type, pattern, reason, category 
         FROM blocklist WHERE enabled = true`)
    return rules, err
}

// Use with ReloadableFilter
loader := &PostgresLoader{DB: db}
filter := swg.NewReloadableFilter(loader)
filter.Load(ctx)
Combining Multiple Sources
// Load from multiple sources
csvLoader := swg.NewCSVLoader("local-rules.csv")
urlLoader := swg.NewURLLoader("https://blocklist.example.com/rules.csv")
staticLoader := swg.NewStaticLoader(
    swg.Rule{Type: "domain", Pattern: "always-blocked.com"},
)

multiLoader := swg.NewMultiLoader(csvLoader, urlLoader, staticLoader)
filter := swg.NewReloadableFilter(multiLoader)
Custom Filter
// Implement the Filter interface
type MyFilter struct{}

func (f *MyFilter) ShouldBlock(req *http.Request) (bool, string) {
    // Block requests with specific paths
    if strings.Contains(req.URL.Path, "/api/tracking") {
        return true, "tracking endpoint blocked"
    }
    return false, ""
}

proxy.Filter = &MyFilter{}

// Or use FilterFunc for simple cases
proxy.Filter = swg.FilterFunc(func(req *http.Request) (bool, string) {
    if req.Host == "blocked.com" {
        return true, "domain blocked"
    }
    return false, ""
})
Custom Block Page
// Use built-in styled block page
proxy.BlockPage = swg.NewBlockPage()

// Or load from file
blockPage, err := swg.NewBlockPageFromFile("block.html")
if err != nil {
    log.Fatal(err)
}
proxy.BlockPage = blockPage

// Or from template string
tmpl := `<html><body>Blocked: {{.URL}} - {{.Reason}}</body></html>`
blockPage, err := swg.NewBlockPageFromTemplate(tmpl)
proxy.BlockPage = blockPage
Block Page Template Variables
Variable Description
{{.URL}} Full blocked URL
{{.Host}} Hostname of blocked request
{{.Path}} Path of blocked request
{{.Reason}} Reason for blocking
{{.Timestamp}} Time of block (RFC1123 format)
Generate CA Programmatically
certPEM, keyPEM, err := swg.GenerateCA("My Organization", 10) // 10 year validity
if err != nil {
    log.Fatal(err)
}

// Save to files
os.WriteFile("ca.crt", certPEM, 0644)
os.WriteFile("ca.key", keyPEM, 0600)

// Or use directly
cm, err := swg.NewCertManagerFromPEM(certPEM, keyPEM)
Health Check Endpoints
health := swg.NewHealthChecker()
proxy.HealthChecker = health

// Mark alive/ready at appropriate lifecycle points
health.SetAlive(true)
health.SetReady(true)

// Add custom readiness checks
health.ReadinessChecks = append(health.ReadinessChecks, func() error {
    if !databaseIsReachable() {
        return errors.New("database unavailable")
    }
    return nil
})

Endpoints return JSON:

  • GET /healthz{"status":"ok","uptime":"1h30m0s"}
  • GET /readyz{"status":"ok","uptime":"1h30m0s"} or {"status":"not ready","details":[...]}
Structured Access Log
// Create a JSON access logger writing to a file
f, _ := os.OpenFile("access.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
alLogger := slog.New(slog.NewJSONHandler(f, nil))
proxy.AccessLog = swg.NewAccessLogger(alLogger)

Each request produces a JSON log entry with method, host, path, scheme, status code, duration, bytes written, client address, blocked/reason, and user agent.

SIGHUP Reload
reloader := swg.WatchSIGHUP(proxy, func(ctx context.Context) (swg.Filter, error) {
    // Rebuild filter from config, database, etc.
    cfg, err := swg.LoadConfig("swg.yaml")
    if err != nil {
        return nil, err
    }
    loader, _ := cfg.BuildRuleLoader()
    filter := swg.NewReloadableFilter(loader)
    filter.Load(ctx)
    return filter, nil
}, logger)
defer reloader.Cancel()

Send SIGHUP to the process to trigger a filter reload without downtime.

Policy Engine

The policy engine provides lifecycle hooks for the full request/response pipeline:

policy := swg.NewPolicyEngine()

// Resolve client identity from IP ranges
resolver := swg.NewIPIdentityResolver()
resolver.AddIP("10.0.0.50", "alice", []string{"engineering"})
resolver.AddCIDR("192.168.1.0/24", "guest", []string{"guests"})
policy.IdentityResolver = resolver

// Add request hooks (run before filtering)
policy.RequestHooks = []swg.RequestHook{
    swg.RequestHookFunc(func(ctx context.Context, req *http.Request, rc *swg.RequestContext) *http.Response {
        log.Printf("request from %s (%s)", rc.Identity, rc.ClientIP)
        rc.Tags["inspected"] = "true"
        return nil // return non-nil *http.Response to short-circuit
    }),
}

proxy.Policy = policy
Per-User/Group Policies
groupFilter := swg.NewGroupPolicyFilter()

// Engineering: minimal blocking
engFilter := swg.NewDomainFilter()
engFilter.AddDomain("malware.example.com")
groupFilter.SetPolicy("engineering", engFilter)

// Guests: allow-list mode (deny everything not explicitly permitted)
guestFilter := swg.NewAllowListFilter()
guestFilter.AddDomains([]string{"docs.google.com", "*.wikipedia.org"})
groupFilter.SetPolicy("guests", guestFilter)

// Fallback for unrecognized users
groupFilter.Default = swg.NewDomainFilter()

proxy.Filter = groupFilter
Allow-List Mode
// Deny-by-default: only listed domains are allowed
allow := swg.NewAllowListFilter()
allow.AddDomains([]string{
    "docs.google.com",
    "*.golang.org",
    "pkg.go.dev",
})
allow.Reason = "domain not on approved list"

proxy.Filter = allow
Time-Based Rules
// Block social media Mon-Fri 9am-5pm US Eastern
eastern, _ := time.LoadLocation("America/New_York")
socialBlock := swg.NewDomainFilter()
socialBlock.AddDomains([]string{"twitter.com", "facebook.com", "reddit.com"})

proxy.Filter = &swg.TimeRule{
    Inner:     socialBlock,
    StartHour: 9,
    EndHour:   17,
    Weekdays:  []time.Weekday{time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday},
    Location:  eastern,
}
Composing Filters
// Chain multiple filters — first block wins
proxy.Filter = &swg.ChainFilter{
    Filters: []swg.Filter{socialTimeRule, afterHoursBlock, malwareFilter},
}
Content-Type Filtering
// Block executable downloads via ResponseHook
ctFilter := swg.NewContentTypeFilter()
ctFilter.Block("application/x-executable", "executable downloads blocked")
ctFilter.Block("application/x-msdownload", "Windows executables blocked")

policy := swg.NewPolicyEngine()
policy.ResponseHooks = []swg.ResponseHook{ctFilter}
proxy.Policy = policy
Response Body Scanning
// Implement ResponseBodyScanner for AV/DLP integration
type AVScanner struct{}

func (s *AVScanner) Scan(ctx context.Context, body []byte, req *http.Request, resp *http.Response) (swg.ScanResult, error) {
    if isMalware(body) {
        return swg.ScanResult{Verdict: swg.VerdictBlock, Reason: "malware detected"}, nil
    }
    return swg.ScanResult{Verdict: swg.VerdictAllow}, nil
}

policy := swg.NewPolicyEngine()
policy.BodyScanners = []swg.ResponseBodyScanner{&AVScanner{}}
policy.ScanContentTypes = []string{"text/html", "application/json"} // empty = scan all
policy.MaxScanSize = 10 << 20 // 10 MiB (default)
proxy.Policy = policy

Scanners return one of three verdicts:

Verdict Behavior
VerdictAllow Content passes through unmodified
VerdictBlock Client receives 403 with the reason
VerdictReplace Scanner provides a replacement body (e.g. DLP redaction)
Graceful Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := proxy.Shutdown(ctx); err != nil {
    log.Printf("shutdown error: %v", err)
}
PAC File Generation
pac := swg.NewPACGenerator("proxy.example.com:8080")
pac.AddBypassDomain("internal.company.com")
pac.AddBypassNetwork("10.0.0.0/8")

// Serve as HTTP handler
http.Handle("/proxy.pac", pac)

// Or generate to file
pac.WriteFile("proxy.pac")
Admin API

The Admin API provides REST endpoints for runtime rule management:

admin := swg.NewAdminAPI(proxy)
admin.Logger = logger

// Optional: configure reload from your source
admin.ReloadFunc = func(ctx context.Context) error {
    return filter.Load(ctx)
}

proxy.Admin = admin

Endpoints (default prefix /api):

Method Path Description
GET /api/status Proxy status, rule count, uptime, filter type
GET /api/rules List all active rules
POST /api/rules Add a rule ({"type":"domain","pattern":"evil.com"})
DELETE /api/rules Remove a rule ({"type":"domain","pattern":"evil.com"})
POST /api/reload Reload rules from source
# Check status
curl http://localhost:8080/api/status

# List rules
curl http://localhost:8080/api/rules

# Add a rule
curl -X POST http://localhost:8080/api/rules \
  -d '{"type":"domain","pattern":"ads.com","reason":"advertising"}'

# Remove a rule
curl -X DELETE http://localhost:8080/api/rules \
  -d '{"type":"domain","pattern":"ads.com"}'

# Trigger reload
curl -X POST http://localhost:8080/api/reload

Rule mutations require the filter to be a *RuleSet or *ReloadableFilter. Other filter types report status and rules as read-only.

mTLS Client Authentication

Require client certificates to connect to the proxy, limiting access to managed devices:

// Load CA that signed client certificates
clientAuth, err := swg.NewClientAuthFromFile("client-ca.pem")
if err != nil {
    log.Fatal(err)
}

// Optional: allow unauthenticated clients (gradual rollout)
// clientAuth.SetPolicy(tls.VerifyClientCertIfGiven)

proxy.ClientAuth = clientAuth

When enabled, the proxy listener is wrapped with TLS requiring client certificates. The cert's Subject fields are automatically mapped to identity:

  • CommonNameRequestContext.Identity
  • OrganizationRequestContext.Groups
  • Tag auth=mtls is set on the request context

Generate client certificates for testing (same package or using the CA PEM directly):

// Parse CA cert from PEM for signing
block, _ := pem.Decode(caCertPEM)
caCert, _ := x509.ParseCertificate(block.Bytes)

certPEM, keyPEM, err := swg.GenerateClientCert(
    caCert, caKeyPEM,
    "alice",                       // CommonName (identity)
    []string{"engineering", "ops"}, // Organizations (groups)
    1,                              // Valid for 1 year
)
Bypass Token

Allow authorized clients to skip content filtering for debugging:

bypass := swg.NewBypass()
bypass.AddToken("debug-token-abc123")

// Or generate a cryptographically random token
tok, _ := bypass.GenerateToken()
fmt.Println("Generated token:", tok)

// Grant bypass by identity (e.g. from mTLS cert CN)
bypass.Identities["admin-user"] = true

proxy.Bypass = bypass

Clients set the bypass header to skip filtering:

curl -H "X-SWG-Bypass: debug-token-abc123" -x http://proxy:8080 http://blocked-site.com

Tokens are compared using constant-time comparison. The bypass header is stripped before forwarding to upstream.

Prometheus Metrics
metrics := swg.NewMetrics()
http.Handle("/metrics", metrics.Handler())

// Record proxy events
metrics.RecordRequest("GET", "https")
metrics.RecordBlocked("ads")
metrics.RecordRequestDuration("GET", 200, duration)

Architecture

┌─────────┐     ┌───────────────┐     ┌──────────────┐
│ Client  │────▶│   SWG Proxy   │────▶│ Origin Server│
└─────────┘     └───────────────┘     └──────────────┘
                       │
                ┌──────┴──────┐
                ▼             ▼
         ┌────────────┐ ┌──────────────┐
         │CertManager │ │ PolicyEngine │
         │(Dynamic TLS)│ │  (Lifecycle) │
         └────────────┘ └──────┬───────┘
                               │
           ┌───────────────────┼───────────────────┐
           ▼                   ▼                   ▼
    ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
    │  Identity    │   │   Filter     │   │ Body Scanner │
    │  Resolver    │   │ (Block/Allow)│   │  (AV / DLP)  │
    └──────────────┘   └──────────────┘   └──────────────┘
Request Lifecycle
  1. Client sends CONNECT request to proxy
  2. Proxy responds with 200 Connection Established
  3. Proxy performs TLS handshake with client using dynamically generated certificate
  4. Policy request hooks run: identity resolution, access control, tagging
  5. Filter checks: domain, URL, regex, allow-list, time-based, group-based
  6. If allowed, proxy forwards request to origin server
  7. Policy response hooks run: content-type filtering
  8. Body scanners run: AV, DLP, keyword detection
  9. Response is returned to client through the TLS tunnel

Security Considerations

  • CA Private Key: The CA private key (ca.key) should be kept secure. Anyone with access can intercept traffic.
  • Trust Scope: Only install the CA certificate on systems you control.
  • Network Position: The proxy must be in the network path to intercept traffic.
  • Certificate Pinning: Applications using certificate pinning will fail through this proxy.

Kubernetes Deployment

SWG includes Kubernetes manifests and a Helm chart for cluster deployment.

Using kubectl
# Create CA secret first
kubectl create namespace swg
kubectl create secret generic swg-ca-cert \
  --from-file=ca.crt=ca.crt \
  --from-file=ca.key=ca.key \
  -n swg

# Apply manifests
kubectl apply -f deploy/kubernetes/

See deploy/kubernetes/README.md for details.

Using Helm
helm install swg ./deploy/helm/swg -n swg

See deploy/helm/swg/README.md for configuration options.

Performance

SWG is designed for high throughput with minimal overhead. Benchmarks run on Apple M4 Pro:

Operation Performance Notes
HTTP proxy throughput ~14,000 req/s Plain HTTP forwarding
Certificate generation ~43ms Per-host, first request only
Certificate cache hit ~6ns Subsequent requests
Domain filter (100K rules) ~40ns O(1) map lookup
Wildcard filter (10K rules) ~71µs Linear scan
Rate limiter check ~47ns Per-client token bucket
Gzip compression 4 GB/s ~50KB response body
Brotli compression 292 MB/s Better ratio, slower
Comparison with Other Proxies

Direct benchmarking across MITM proxies is difficult due to differing test methodologies, hardware, and feature sets. For rough reference:

Proxy Language Approx. Throughput Notes
SWG Go ~14,000 req/s This project
mitmproxy Python ~1,400 req/s Single-threaded
Traefik Go ~26,000 req/s Reverse proxy (not MITM)
Caddy Go ~18,000 req/s Reverse proxy (not MITM)

Note: mitmproxy numbers from Fluxzy benchmarks. Traefik/Caddy are reverse proxies without MITM interception, included for Go baseline reference.

Run benchmarks locally:

go test -bench=. -benchmem ./...

License

MIT License - see LICENSE for details.

Documentation

Overview

Package swg provides an HTTPS man-in-the-middle (MITM) proxy for content filtering. It intercepts HTTPS connections by dynamically generating TLS certificates signed by a trusted CA, allowing inspection and filtering of encrypted traffic.

Architecture

The proxy handles both HTTP and HTTPS (CONNECT) requests. For HTTPS, it performs a TLS handshake with the client using a dynamically generated certificate for the requested host, then forwards the decrypted request to the origin server. Filters can inspect and block requests at any point.

Basic Proxy

Create a proxy with certificate management and start serving:

cm, err := swg.NewCertManager("ca.crt", "ca.key")
if err != nil {
    log.Fatal(err)
}

proxy := swg.NewProxy(":8080", cm)
log.Fatal(proxy.ListenAndServe())

Domain Filtering

Block requests by domain name with optional wildcard support:

filter := swg.NewDomainFilter()
filter.AddDomain("blocked.com")
filter.AddDomain("*.ads.example.com")
proxy.Filter = filter

Advanced Filtering with RuleSet

RuleSet supports domain, URL prefix, and regex pattern matching with metadata such as reason and category:

rs := swg.NewRuleSet()
rs.AddDomain("ads.example.com")
rs.AddURL("https://evil.com/malware")
rs.AddRegex(`.*\.tracking\..*`)

rs.AddRule(swg.Rule{
    Type:     "domain",
    Pattern:  "malware.com",
    Reason:   "known malware host",
    Category: "security",
})

proxy.Filter = rs

Reloadable Filters

Load rules from external sources (CSV files, HTTP endpoints, databases) with automatic periodic reloading:

loader := swg.NewCSVLoader("blocklist.csv")
loader.HasHeader = true

filter := swg.NewReloadableFilter(loader)
filter.OnReload = func(count int) {
    log.Printf("Loaded %d rules", count)
}

ctx := context.Background()
filter.Load(ctx)

cancel := filter.StartAutoReload(ctx, 5*time.Minute)
defer cancel()

proxy.Filter = filter

Multiple sources can be combined:

multi := swg.NewMultiLoader(
    swg.NewCSVLoader("local.csv"),
    swg.NewURLLoader("https://blocklist.example.com/rules.csv"),
    swg.NewStaticLoader(swg.Rule{Type: "domain", Pattern: "always-blocked.com"}),
)
filter := swg.NewReloadableFilter(multi)

Custom Filters

Implement the Filter interface or use FilterFunc for simple cases:

proxy.Filter = swg.FilterFunc(func(req *http.Request) (bool, string) {
    if req.Host == "blocked.com" {
        return true, "domain blocked"
    }
    return false, ""
})

Block Pages

Display a customizable HTML page when requests are blocked:

proxy.BlockPage = swg.NewBlockPage()

// Or from a custom template file
bp, err := swg.NewBlockPageFromFile("block.html")
proxy.BlockPage = bp

Template variables available in block pages: {{.URL}}, {{.Host}}, {{.Path}}, {{.Reason}}, and {{.Timestamp}}.

PAC File Generation

Generate Proxy Auto-Configuration files for client setup:

pac := swg.NewPACGenerator("proxy.example.com:8080")
pac.AddBypassDomain("internal.company.com")
pac.AddBypassNetwork("10.0.0.0/8")

// Serve as HTTP handler
http.Handle("/proxy.pac", pac)

// Or write to file
pac.WriteFile("proxy.pac")

Prometheus Metrics

Instrument the proxy with Prometheus metrics for monitoring:

metrics := swg.NewMetrics()
http.Handle("/metrics", metrics.Handler())

The Metrics type provides methods for recording requests, blocked connections, certificate cache statistics, filter reloads, and more.

Health Check Endpoints

Expose /healthz and /readyz endpoints for Kubernetes and load balancers:

health := swg.NewHealthChecker()
proxy.HealthChecker = health

health.SetAlive(true)
health.SetReady(true)

Custom readiness checks verify downstream dependencies:

health.ReadinessChecks = append(health.ReadinessChecks, func() error {
    if !dbPing() {
        return errors.New("database unreachable")
    }
    return nil
})

Structured Access Log

Write JSON access log entries for every proxied request:

f, _ := os.OpenFile("access.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
alLogger := slog.New(slog.NewJSONHandler(f, nil))
proxy.AccessLog = swg.NewAccessLogger(alLogger)

Each entry includes method, host, path, scheme, status code, duration, bytes written, client address, blocked/reason, and user agent.

SIGHUP Reload

Reload filter rules on SIGHUP without restarting the proxy:

reloader := swg.WatchSIGHUP(proxy, func(ctx context.Context) (swg.Filter, error) {
    cfg, _ := swg.LoadConfig("swg.yaml")
    loader, _ := cfg.BuildRuleLoader()
    filter := swg.NewReloadableFilter(loader)
    filter.Load(ctx)
    return filter, nil
}, logger)
defer reloader.Cancel()

Configuration

Load configuration from YAML, JSON, or TOML files with environment variable overrides (SWG_ prefix):

cfg, err := swg.LoadConfig("swg.yaml")
if err != nil {
    log.Fatal(err)
}

rs, err := cfg.BuildRuleSet()
proxy.Filter = rs

ACME / Let's Encrypt Certificates

Use ACMECertManager to obtain and automatically renew TLS certificates from Let's Encrypt or any RFC 8555-compliant CA, eliminating the need to manage a self-signed CA for the proxy's own listener certificate:

acm, err := swg.NewACMECertManager(swg.ACMEConfig{
    Email:     "admin@example.com",
    Domains:   []string{"proxy.example.com"},
    AcceptTOS: true,
    CA:        swg.LetsEncryptStaging,
})
if err != nil {
    log.Fatal(err)
}
defer acm.Close()

ctx := context.Background()
if err := acm.Initialize(ctx); err != nil {
    log.Fatal(err)
}
if err := acm.ObtainCertificates(ctx); err != nil {
    log.Fatal(err)
}
acm.StartAutoRenewal(12 * time.Hour)

The manager persists account data and certificates to disk so restarts do not re-issue certificates. Combine with a self-signed CertManager for MITM per-host certificates while using ACME for the proxy's own listener TLS:

srv := &http.Server{
    TLSConfig: &tls.Config{GetCertificate: acm.GetCertificate},
}

See ACMEConfig for the full set of configuration fields including challenge ports, key types, External Account Binding, and renewal timing.

CA Certificate Generation

Generate a new CA certificate and key pair programmatically:

certPEM, keyPEM, err := swg.GenerateCA("My Organization", 10)
cm, err := swg.NewCertManagerFromPEM(certPEM, keyPEM)

Graceful Shutdown

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := proxy.Shutdown(ctx); err != nil {
    log.Printf("shutdown error: %v", err)
}

Index

Constants

View Source
const (
	LetsEncryptProduction = lego.LEDirectoryProduction
	LetsEncryptStaging    = lego.LEDirectoryStaging
)

ACME CA directory URLs for use with ACMEConfig.CA.

LetsEncryptProduction is the default when CA is empty. Use LetsEncryptStaging during development and testing to avoid Let's Encrypt production rate limits (5 duplicate certificates per week, 50 certificates per registered domain per week).

View Source
const (
	KB = 1024
	MB = 1024 * KB
	GB = 1024 * MB
)

Common body size constants for convenience.

View Source
const (
	EncodingGzip    = "gzip"
	EncodingZstd    = "zstd"
	EncodingBrotli  = "br"
	EncodingDeflate = "deflate"
)

Compression encoding constants.

View Source
const DefaultBlockPageHTML = `` /* 4263-byte string literal not displayed */

DefaultBlockPageHTML is the default block page template.

View Source
const DefaultBypassHeader = "X-SWG-Bypass"

DefaultBypassHeader is the default HTTP header used to carry a bypass token.

Variables

View Source
var ErrBodyTooLarge = errors.New("request body too large")

ErrBodyTooLarge is returned when the request body exceeds the configured limit.

Functions

func CompressBytes

func CompressBytes(data []byte, encoding string) ([]byte, error)

CompressBytes compresses data with the specified encoding.

func GenerateCA

func GenerateCA(org string, validYears int) (certPEM, keyPEM []byte, err error)

GenerateCA generates a new CA certificate and private key. Returns PEM-encoded certificate and key.

func GenerateClientCert

func GenerateClientCert(caCert *x509.Certificate, caKeyPEM []byte, cn string, orgs []string, validYears int) (certPEM, keyPEM []byte, err error)

GenerateClientCert generates a client certificate signed by the given CA. The certificate includes the x509.ExtKeyUsageClientAuth extended key usage and is valid for the specified number of years.

This is a convenience function for testing and development. Production deployments should use a proper PKI or certificate authority.

func LimitRequestBody

func LimitRequestBody(maxSize int64, next http.Handler) http.Handler

LimitRequestBody is a convenience function that wraps an http.Handler with body size limiting middleware.

func WithRequestContext

func WithRequestContext(ctx context.Context, rc *RequestContext) context.Context

WithRequestContext attaches a RequestContext to the given context.

func WriteExampleConfig

func WriteExampleConfig(path string) error

WriteExampleConfig writes an example configuration file.

Types

type ACMECertManager

type ACMECertManager struct {

	// OnCertObtained is called after a new certificate is obtained for a
	// domain. It is called from the goroutine that performed the obtain.
	OnCertObtained func(domain string)

	// OnCertRenewed is called after an existing certificate is renewed.
	OnCertRenewed func(domain string)

	// OnError is called when obtaining or renewing a certificate fails.
	OnError func(domain string, err error)
	// contains filtered or unexported fields
}

ACMECertManager obtains and renews TLS certificates from an ACME CA such as Let's Encrypt. It implements the same GetCertificate / GetCertificateForHost surface as CertManager, so it can be used with tls.Config.GetCertificate or anywhere a per-host certificate provider is needed.

Lifecycle

The typical usage follows four steps:

  1. Create — NewACMECertManager validates the config and creates the on-disk storage directory.
  2. Initialize — ACMECertManager.Initialize registers (or loads) the ACME account, configures challenge solvers, and loads any previously obtained certificates from disk.
  3. Obtain — ACMECertManager.ObtainCertificates contacts the CA and obtains certificates for every domain in the config.
  4. Renew — ACMECertManager.StartAutoRenewal spawns a background goroutine that periodically checks certificate expiration and renews before the RenewBefore window.

Call ACMECertManager.Close to stop the renewal goroutine and release resources.

Callbacks

Three optional callbacks are available for observability:

  • OnCertObtained — fired after a certificate is successfully obtained.
  • OnCertRenewed — fired after a certificate is successfully renewed.
  • OnError — fired when obtaining or renewing a certificate fails.

Thread Safety

All public methods are safe for concurrent use. The certificate cache is protected by an internal sync.RWMutex.

Example

acm, err := swg.NewACMECertManager(swg.ACMEConfig{
    Email:     "admin@example.com",
    Domains:   []string{"proxy.example.com"},
    AcceptTOS: true,
    CA:        swg.LetsEncryptStaging, // use staging for testing
})
if err != nil {
    log.Fatal(err)
}
defer acm.Close()

if err := acm.Initialize(ctx); err != nil {
    log.Fatal(err)
}
if err := acm.ObtainCertificates(ctx); err != nil {
    log.Fatal(err)
}
acm.StartAutoRenewal(12 * time.Hour)

srv := &http.Server{
    TLSConfig: &tls.Config{GetCertificate: acm.GetCertificate},
}

func NewACMECertManager

func NewACMECertManager(cfg ACMEConfig) (*ACMECertManager, error)

NewACMECertManager validates cfg and returns a new ACMECertManager. It creates the storage directory specified by [ACMEConfig.StoragePath] but does not contact the CA — call ACMECertManager.Initialize next.

Returns an error if Email is empty, Domains is empty, or AcceptTOS is false.

func (*ACMECertManager) CacheSize

func (acm *ACMECertManager) CacheSize() int

CacheSize returns the number of certificates currently held in the in-memory cache.

func (*ACMECertManager) Close

func (acm *ACMECertManager) Close() error

Close stops the auto-renewal goroutine (if running) and waits for it to exit. It is safe to call Close multiple times.

func (*ACMECertManager) GetCertificate

func (acm *ACMECertManager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate returns a TLS certificate for the SNI host name in hello. It is intended for use as tls.Config.GetCertificate:

srv := &http.Server{
    TLSConfig: &tls.Config{
        GetCertificate: acm.GetCertificate,
    },
}

Returns an error if the ClientHelloInfo contains no SNI server name.

func (*ACMECertManager) GetCertificateForHost

func (acm *ACMECertManager) GetCertificateForHost(host string) (*tls.Certificate, error)

GetCertificateForHost returns the cached TLS certificate for host. If the certificate is not in the cache but the host is one of the configured [ACMEConfig.Domains], an on-demand obtain is attempted.

Returns an error if the host is not in the configured domain list.

func (*ACMECertManager) Initialize

func (acm *ACMECertManager) Initialize(ctx context.Context) error

Initialize creates the lego ACME client, configures the HTTP-01 and TLS-ALPN-01 challenge providers, and either loads an existing account from disk or registers a new one with the CA.

On first run the account private key is generated and persisted at <StoragePath>/account.json. Subsequent calls load the existing key.

If [ACMEConfig.EABKeyID] and [ACMEConfig.EABMACKey] are set, External Account Binding is used during registration.

Any certificates previously stored on disk are loaded into the in-memory cache so they are available immediately without contacting the CA.

func (*ACMECertManager) ObtainCertificates

func (acm *ACMECertManager) ObtainCertificates(ctx context.Context) error

ObtainCertificates iterates over every domain in [ACMEConfig.Domains] and obtains a certificate from the CA. If a valid certificate already exists in the cache and is not within the RenewBefore window, the domain is skipped.

Certificates are persisted to disk under <StoragePath>/certificates/<domain>/. The [ACMECertManager.OnCertObtained] callback is invoked for each newly obtained certificate.

Returns the first error encountered; remaining domains are not attempted.

func (*ACMECertManager) SetLogger

func (acm *ACMECertManager) SetLogger(logger *slog.Logger)

SetLogger replaces the default slog.Default logger used by the ACMECertManager. Call this before ACMECertManager.Initialize to capture all log output.

func (*ACMECertManager) StartAutoRenewal

func (acm *ACMECertManager) StartAutoRenewal(checkInterval time.Duration)

StartAutoRenewal spawns a background goroutine that checks all cached certificates at the given interval and renews any that will expire within the [ACMEConfig.RenewBefore] window.

If checkInterval is zero it defaults to 12 hours. A typical production value is 12*time.Hour, which balances CA load against timely renewal.

The goroutine is stopped when ACMECertManager.Close is called.

type ACMEConfig

type ACMEConfig struct {
	// Email is the address registered with the ACME account. The CA sends
	// certificate expiration warnings here. Required.
	Email string `mapstructure:"email"`

	// CA is the ACME directory URL. Defaults to [LetsEncryptProduction].
	// Use [LetsEncryptStaging] during development to avoid rate limits.
	// Any RFC 8555-compliant CA directory URL is accepted (e.g. ZeroSSL,
	// Buypass, Google Trust Services).
	CA string `mapstructure:"ca"`

	// KeyType selects the private key algorithm for issued certificates.
	// Supported values:
	//
	//   - "ec256"   — ECDSA P-256 (default, recommended)
	//   - "ec384"   — ECDSA P-384
	//   - "rsa2048" — RSA 2048-bit
	//   - "rsa4096" — RSA 4096-bit
	//   - "rsa8192" — RSA 8192-bit
	//
	// ECDSA keys produce smaller certificates and faster TLS handshakes.
	KeyType string `mapstructure:"key_type"`

	// StoragePath is the directory where account data and certificates are
	// persisted. The directory is created automatically with mode 0700.
	// Defaults to "./acme".
	StoragePath string `mapstructure:"storage_path"`

	// HTTPPort is the listen port for HTTP-01 ACME challenges.
	// Defaults to 80. Set to 0 to disable the HTTP-01 challenge solver.
	HTTPPort int `mapstructure:"http_port"`

	// TLSPort is the listen port for TLS-ALPN-01 ACME challenges.
	// Defaults to 443. Set to 0 to disable the TLS-ALPN-01 challenge solver.
	TLSPort int `mapstructure:"tls_port"`

	// RenewBefore is how far in advance of expiration the certificate is
	// renewed during auto-renewal. Defaults to 30 days. Let's Encrypt
	// certificates are valid for 90 days, so 30 days gives two retry
	// windows.
	RenewBefore time.Duration `mapstructure:"renew_before"`

	// Domains lists the fully-qualified domain names for which
	// certificates will be obtained. At least one is required. Each
	// domain receives its own certificate (no SANs across entries).
	Domains []string `mapstructure:"domains"`

	// AcceptTOS must be set to true to indicate acceptance of the CA's
	// Terms of Service. [NewACMECertManager] returns an error if false.
	AcceptTOS bool `mapstructure:"accept_tos"`

	// EABKeyID is the External Account Binding key identifier.
	// Required only for CAs that mandate EAB (e.g. ZeroSSL).
	EABKeyID string `mapstructure:"eab_key_id"`

	// EABMACKey is the base64url-encoded HMAC key for EAB.
	// Required only for CAs that mandate EAB (e.g. ZeroSSL).
	EABMACKey string `mapstructure:"eab_mac_key"`
}

ACMEConfig holds the configuration for obtaining and renewing TLS certificates via the ACME protocol (RFC 8555). It is the sole input to NewACMECertManager.

At minimum you must set [ACMEConfig.Email], [ACMEConfig.Domains], and [ACMEConfig.AcceptTOS]. All other fields have sensible defaults provided by DefaultACMEConfig.

Challenge Types

The ACME protocol verifies domain ownership through challenge types. ACMEConfig supports two:

  • HTTP-01 — The CA makes an HTTP request to port 80 on the domain. Controlled by [ACMEConfig.HTTPPort]. Set to 0 to disable.
  • TLS-ALPN-01 — The CA performs a TLS handshake on port 443 using a special ALPN protocol. Controlled by [ACMEConfig.TLSPort]. Set to 0 to disable.

At least one challenge type must remain enabled. Both ports must be reachable from the public internet for the challenge to succeed.

External Account Binding (EAB)

Some CAs (ZeroSSL, Google Trust Services, Buypass Go) require External Account Binding. Set [ACMEConfig.EABKeyID] and [ACMEConfig.EABMACKey] to the values provided by the CA's dashboard.

Storage Layout

Certificates, private keys, and account data are persisted under [ACMEConfig.StoragePath] (default "./acme"):

<StoragePath>/
├── account.json                     # ACME account + private key
└── certificates/
    └── <domain>/
        ├── certificate.pem          # Leaf + intermediates
        ├── private_key.pem          # Certificate private key
        ├── issuer.pem               # Issuer certificate
        └── metadata.json            # Domain, URL, timestamp

All files are created with mode 0600/0700 so only the process owner can read them.

func DefaultACMEConfig

func DefaultACMEConfig() ACMEConfig

DefaultACMEConfig returns an ACMEConfig populated with production-ready defaults. The caller must still set Email, Domains, and AcceptTOS before passing the config to NewACMECertManager.

cfg := swg.DefaultACMEConfig()
cfg.Email     = "admin@example.com"
cfg.Domains   = []string{"proxy.example.com"}
cfg.AcceptTOS = true

type AccessLogEntry

type AccessLogEntry struct {
	// Timestamp when the request was received.
	Timestamp time.Time

	// Method is the HTTP method (GET, POST, CONNECT, etc.).
	Method string

	// Host is the target hostname.
	Host string

	// Path is the request URL path.
	Path string

	// Scheme is "http" or "https".
	Scheme string

	// StatusCode is the upstream response status code. Zero if blocked or errored.
	StatusCode int

	// Duration is the time to process the request.
	Duration time.Duration

	// BytesWritten is the response body size.
	BytesWritten int64

	// ClientAddr is the client's remote address.
	ClientAddr string

	// Blocked is true if the request was blocked by a filter.
	Blocked bool

	// BlockReason is the reason the request was blocked (if Blocked is true).
	BlockReason string

	// Error is a description of any error that occurred.
	Error string

	// UserAgent is the client's User-Agent header.
	UserAgent string
}

AccessLogEntry contains all fields for a single access log record.

type AccessLogger

type AccessLogger struct {
	// contains filtered or unexported fields
}

AccessLogger writes structured access log entries for each proxied request. It uses slog.LogAttrs for low-allocation logging on the hot path.

func NewAccessLogger

func NewAccessLogger(logger *slog.Logger) *AccessLogger

NewAccessLogger creates a new AccessLogger that writes to the given slog.Logger. For best performance, pass a logger configured with slog.NewJSONHandler.

func (*AccessLogger) Log

func (al *AccessLogger) Log(e AccessLogEntry)

Log writes an access log entry using slog.LogAttrs to minimize allocations.

type AdminAPI

type AdminAPI struct {
	// Proxy is the proxy instance to manage.
	Proxy *Proxy

	// Logger for admin API events.
	Logger *slog.Logger

	// PathPrefix is the URL path prefix for admin routes (default "/api").
	PathPrefix string

	// ReloadFunc is called when POST /api/reload is invoked. It should
	// rebuild the filter from its source (e.g. config file, database).
	// If nil, the reload endpoint returns 501 Not Implemented.
	ReloadFunc func(ctx context.Context) error
	// contains filtered or unexported fields
}

AdminAPI provides REST endpoints for managing the proxy at runtime. It exposes routes for listing, adding, and removing filter rules, viewing proxy status, and triggering filter reloads.

The API is mounted at a configurable path prefix (default "/api") and uses chi for routing.

All endpoints return JSON responses with appropriate status codes. Mutations require the filter to be a *RuleSet or a *ReloadableFilter that exposes a RuleSet.

func NewAdminAPI

func NewAdminAPI(proxy *Proxy) *AdminAPI

NewAdminAPI creates an AdminAPI wired to the given proxy.

func (*AdminAPI) Handler

func (a *AdminAPI) Handler() http.Handler

Handler returns an http.Handler for the admin API routes. Mount this on the proxy or a separate listener.

func (*AdminAPI) ServeHTTP

func (a *AdminAPI) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler by delegating to the internal chi router after stripping the path prefix.

type AllowListFilter

type AllowListFilter struct {
	Reason string // block reason for denied requests
	// contains filtered or unexported fields
}

AllowListFilter implements Filter with a deny-by-default policy. Only requests matching allowed domains pass through; everything else is blocked.

func NewAllowListFilter

func NewAllowListFilter() *AllowListFilter

NewAllowListFilter creates a deny-by-default filter.

func (*AllowListFilter) AddDomain

func (f *AllowListFilter) AddDomain(domain string)

AddDomain adds a domain to the allow list. Supports wildcards: "*.example.com" allows all subdomains.

func (*AllowListFilter) AddDomains

func (f *AllowListFilter) AddDomains(domains []string)

AddDomains adds multiple domains to the allow list.

func (*AllowListFilter) ShouldBlock

func (f *AllowListFilter) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock implements Filter. Returns true for domains NOT in the allow list.

type BlockPage

type BlockPage struct {
	// contains filtered or unexported fields
}

BlockPage represents a customizable block page.

func NewBlockPage

func NewBlockPage() *BlockPage

NewBlockPage creates a new BlockPage with the default template.

func NewBlockPageFromFile

func NewBlockPageFromFile(path string) (*BlockPage, error)

NewBlockPageFromFile creates a BlockPage from a template file.

func NewBlockPageFromTemplate

func NewBlockPageFromTemplate(templateStr string) (*BlockPage, error)

NewBlockPageFromTemplate creates a BlockPage from a custom template string.

func (*BlockPage) Render

func (bp *BlockPage) Render(w io.Writer, data BlockPageData) error

Render writes the block page to the given writer.

func (*BlockPage) RenderString

func (bp *BlockPage) RenderString(data BlockPageData) (string, error)

RenderString returns the block page as a string.

func (*BlockPage) ServeHTTP

func (bp *BlockPage) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler for serving the block page directly.

type BlockPageConfig

type BlockPageConfig struct {
	// Enabled determines if custom block page is used
	Enabled bool `mapstructure:"enabled"`

	// RedirectURL to redirect blocked requests (optional)
	RedirectURL string `mapstructure:"redirect_url"`

	// TemplatePath to custom block page template
	TemplatePath string `mapstructure:"template_path"`

	// TemplateInline is inline template content
	TemplateInline string `mapstructure:"template_inline"`
}

BlockPageConfig contains block page settings.

type BlockPageData

type BlockPageData struct {
	URL       string
	Host      string
	Path      string
	Reason    string
	Timestamp string
}

BlockPageData contains the data passed to the block page template.

type BodyLimitConfig

type BodyLimitConfig struct {
	// MaxSize is the maximum allowed request body size in bytes.
	// Zero means no limit.
	MaxSize int64

	// StreamCheck enables early rejection by checking Content-Length header
	// before reading the body. If Content-Length exceeds MaxSize, the request
	// is rejected immediately without buffering.
	StreamCheck bool

	// RejectResponse is an optional custom response to send when the limit is
	// exceeded. If nil, a default 413 Payload Too Large response is sent.
	RejectResponse *http.Response

	// SkipPaths is a list of URL path prefixes to skip body limit checks.
	// Useful for upload endpoints that need larger limits.
	SkipPaths []string

	// SkipMethods is a list of HTTP methods to skip body limit checks.
	// By default, GET, HEAD, OPTIONS, and TRACE are skipped as they
	// typically don't have request bodies.
	SkipMethods []string
}

BodyLimitConfig configures request body size limits.

func DefaultBodyLimitConfig

func DefaultBodyLimitConfig() BodyLimitConfig

DefaultBodyLimitConfig returns a configuration with sensible defaults.

type BodyLimiter

type BodyLimiter struct {
	Config BodyLimitConfig
	// contains filtered or unexported fields
}

BodyLimiter enforces request body size limits.

func NewBodyLimiter

func NewBodyLimiter(maxSize int64) *BodyLimiter

NewBodyLimiter creates a new BodyLimiter with the given maximum size.

func NewBodyLimiterWithConfig

func NewBodyLimiterWithConfig(cfg BodyLimitConfig) *BodyLimiter

NewBodyLimiterWithConfig creates a BodyLimiter with custom configuration.

func (*BodyLimiter) Check

func (bl *BodyLimiter) Check(req *http.Request) error

Check validates the request body size against configured limits. Returns ErrBodyTooLarge if the body exceeds the limit. If StreamCheck is enabled and Content-Length is set, validation happens without reading the body. Otherwise, the body is wrapped with a limiting reader.

func (*BodyLimiter) GetPathLimit

func (bl *BodyLimiter) GetPathLimit(path string) int64

GetPathLimit returns the effective limit for a given path. Returns the path-specific limit if set, otherwise the global MaxSize.

func (*BodyLimiter) HandleRequest

func (bl *BodyLimiter) HandleRequest(ctx context.Context, req *http.Request, rc *RequestContext) *http.Response

HandleRequest implements RequestHook for integration with PolicyEngine. Returns a 413 response if the body size limit is exceeded.

func (*BodyLimiter) Middleware

func (bl *BodyLimiter) Middleware(next http.Handler) http.Handler

Middleware returns an http.Handler middleware that enforces body size limits.

func (*BodyLimiter) SetPathLimit

func (bl *BodyLimiter) SetPathLimit(pathPrefix string, limit int64)

SetPathLimit sets a custom body size limit for a specific path prefix. This overrides the global MaxSize for requests matching the prefix. Set limit to 0 to disable limits for this path, or -1 to use the global limit.

type Bypass

type Bypass struct {
	// Header is the HTTP header name that carries the bypass token.
	// Defaults to [DefaultBypassHeader] ("X-SWG-Bypass").
	Header string

	// Identities is a set of [RequestContext] identity values (e.g.
	// usernames from mTLS certificates) that are granted bypass.
	// Identity matching is case-sensitive and checked after token
	// matching.
	Identities map[string]bool

	// Logger for bypass events. If nil, bypass is silent.
	Logger *slog.Logger
	// contains filtered or unexported fields
}

Bypass allows authorized clients to skip content filtering. Clients present a secret token via an HTTP header or are identified by their RequestContext identity. When a request is granted bypass, filtering and policy hooks are skipped and the request is forwarded directly.

Tokens are compared using constant-time comparison to prevent timing side-channels.

Usage:

b := swg.NewBypass()
b.AddToken("debug-token-abc123")
proxy.Bypass = b

Clients then set the header:

curl -H "X-SWG-Bypass: debug-token-abc123" -x http://proxy:8080 http://example.com

func NewBypass

func NewBypass() *Bypass

NewBypass creates a Bypass with the default header name and no tokens. Use Bypass.AddToken or Bypass.GenerateToken to register tokens.

func (*Bypass) AddToken

func (b *Bypass) AddToken(token string)

AddToken registers a bypass token. Duplicate tokens are ignored. AddToken is safe for concurrent use.

func (*Bypass) GenerateToken

func (b *Bypass) GenerateToken() (string, error)

GenerateToken creates a cryptographically random 32-byte hex token, registers it, and returns the token string. The returned token is suitable for use in HTTP headers.

func (*Bypass) RemoveToken

func (b *Bypass) RemoveToken(token string)

RemoveToken revokes a previously registered bypass token. RemoveToken is safe for concurrent use.

func (*Bypass) RevokeAll

func (b *Bypass) RevokeAll()

RevokeAll removes all registered bypass tokens. RevokeAll is safe for concurrent use.

func (*Bypass) ShouldBypass

func (b *Bypass) ShouldBypass(req *http.Request) bool

ShouldBypass reports whether the request should skip content filtering. It checks the bypass header for a valid token using constant-time comparison, then falls back to identity matching via RequestContext. Returns true if bypass is granted.

func (*Bypass) TokenCount

func (b *Bypass) TokenCount() int

TokenCount returns the number of registered bypass tokens. TokenCount is safe for concurrent use.

type CSVLoader

type CSVLoader struct {
	// Path to the CSV file
	Path string

	// HasHeader indicates if the first row is a header (skipped)
	HasHeader bool

	// DefaultReason is used when the reason column is empty
	DefaultReason string

	// DefaultCategory is used when the category column is empty
	DefaultCategory string
}

CSVLoader loads rules from a CSV file. Expected CSV format: type,pattern,reason,category Where type is one of: domain, url, regex

func NewCSVLoader

func NewCSVLoader(path string) *CSVLoader

NewCSVLoader creates a new CSV loader for the given file path.

func (*CSVLoader) Load

func (l *CSVLoader) Load(ctx context.Context) ([]Rule, error)

Load implements RuleLoader.

func (*CSVLoader) LoadFromReader

func (l *CSVLoader) LoadFromReader(ctx context.Context, r io.Reader) ([]Rule, error)

LoadFromReader loads rules from an io.Reader (useful for testing).

type CertManager

type CertManager struct {
	// contains filtered or unexported fields
}

CertManager manages CA and per-host certificate generation for MITM proxying.

func NewCertManager

func NewCertManager(caCertPath, caKeyPath string) (*CertManager, error)

NewCertManager creates a CertManager from existing CA certificate and key files.

func NewCertManagerFromPEM

func NewCertManagerFromPEM(caCertPEM, caKeyPEM []byte) (*CertManager, error)

NewCertManagerFromPEM creates a CertManager from PEM-encoded CA cert and key.

func (*CertManager) GetCertificate

func (cm *CertManager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate returns a TLS certificate for the given host, generating one if needed. This is suitable for use as tls.Config.GetCertificate.

func (*CertManager) GetCertificateForHost

func (cm *CertManager) GetCertificateForHost(host string) (*tls.Certificate, error)

GetCertificateForHost returns a TLS certificate for the given hostname.

type CertRotator

type CertRotator struct {

	// OnRotate is called after a successful rotation with the new CA subject.
	OnRotate func(subject string)

	// OnError is called when a rotation attempt fails.
	OnError func(err error)
	// contains filtered or unexported fields
}

CertRotator wraps a CertManager and adds the ability to atomically swap the underlying CA certificate and key at runtime, e.g. from a SIGHUP handler or periodic refresh. All in-flight TLS handshakes continue using the old CA; new connections pick up the rotated CA immediately.

Host-certificate caches are flushed on every rotation because the old certs were signed by the previous CA.

func NewCertRotator

func NewCertRotator(cm *CertManager, certPath, keyPath string) *CertRotator

NewCertRotator creates a CertRotator that can reload the CA from disk.

func (*CertRotator) CACert

func (cr *CertRotator) CACert() *x509.Certificate

CACert returns the current CA certificate.

func (*CertRotator) CAKey

func (cr *CertRotator) CAKey() *rsa.PrivateKey

CAKey returns the current CA private key.

func (*CertRotator) CacheSize

func (cr *CertRotator) CacheSize() int

CacheSize returns the number of cached host certificates.

func (*CertRotator) CertManager

func (cr *CertRotator) CertManager() *CertManager

CertManager returns the current CertManager. The caller must not hold a reference across a rotation boundary — call this each time you need it.

func (*CertRotator) GetCertificate

func (cr *CertRotator) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate implements the tls.Config.GetCertificate callback, delegating to the current CertManager. This should be used instead of cm.GetCertificate when certificate rotation is enabled.

func (*CertRotator) GetCertificateForHost

func (cr *CertRotator) GetCertificateForHost(host string) (*tls.Certificate, error)

GetCertificateForHost generates (or retrieves from cache) a host certificate signed by the current CA.

func (*CertRotator) Rotate

func (cr *CertRotator) Rotate() (*CertManager, error)

Rotate reloads the CA certificate and key from the paths configured at creation time. On success the internal CertManager is swapped atomically and the host-cert cache is cleared. Returns the new CertManager.

func (*CertRotator) RotateFromPEM

func (cr *CertRotator) RotateFromPEM(certPEM, keyPEM []byte) (*CertManager, error)

RotateFromPEM reloads the CA from in-memory PEM bytes.

func (*CertRotator) WatchCAFiles

func (cr *CertRotator) WatchCAFiles(interval func() <-chan time.Time) func()

WatchCAFiles watches the CA cert and key files for changes and automatically rotates when they are modified. Returns a cancel function. This uses a simple polling approach; for production use consider fsnotify or similar.

type ChainFilter

type ChainFilter struct {
	Filters []Filter
}

ChainFilter composes multiple Filters into one. Filters are checked in order; the first one that blocks wins.

func (*ChainFilter) ShouldBlock

func (cf *ChainFilter) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock implements Filter.

type ClientAuth

type ClientAuth struct {

	// IdentityFromCert controls whether the client certificate's subject
	// is used for identity resolution. When true, the certificate's
	// CommonName populates [RequestContext.Identity] and the Organization
	// fields populate [RequestContext.Groups]. This overrides the
	// PolicyEngine's IdentityResolver for mTLS-authenticated clients.
	IdentityFromCert bool

	// Logger for client auth events.
	Logger *slog.Logger
	// contains filtered or unexported fields
}

ClientAuth configures mutual TLS (mTLS) client certificate authentication for the proxy listener. When enabled, clients must present a valid TLS certificate signed by one of the trusted client CAs to use the proxy.

The proxy listener is wrapped with TLS so that the initial connection requires a client certificate. The mTLS handshake happens before any HTTP traffic, meaning unauthenticated clients cannot even send a CONNECT request.

ClientAuth integrates with the PolicyEngine identity system: when [ClientAuth.IdentityFromCert] is true, the certificate's Common Name is injected as the client identity and the certificate's Organization fields are used as group memberships. This populates [RequestContext.Identity] and [RequestContext.Groups] for downstream policy decisions.

func NewClientAuth

func NewClientAuth(pool *x509.CertPool) *ClientAuth

NewClientAuth creates a ClientAuth that requires and verifies client certificates against the provided CA certificate pool.

func NewClientAuthFromFile

func NewClientAuthFromFile(path string) (*ClientAuth, error)

NewClientAuthFromFile creates a ClientAuth by loading a PEM-encoded CA certificate bundle from the given file path.

func NewClientAuthFromPEM

func NewClientAuthFromPEM(pemData []byte) (*ClientAuth, error)

NewClientAuthFromPEM creates a ClientAuth from PEM-encoded CA certificates. Multiple certificates may be concatenated in the PEM data.

func (*ClientAuth) AddCACert

func (ca *ClientAuth) AddCACert(cert *x509.Certificate)

AddCACert adds a CA certificate to the trusted pool. This is safe for concurrent use.

func (*ClientAuth) AddCAPEM

func (ca *ClientAuth) AddCAPEM(pemData []byte) error

AddCAPEM appends PEM-encoded CA certificates to the trusted pool. Returns an error if no valid certificates are found.

func (*ClientAuth) IdentityFromConn

func (ca *ClientAuth) IdentityFromConn(conn *tls.Conn) (identity string, groups []string)

IdentityFromConn extracts client identity from a TLS connection's peer certificate. Returns the CommonName as identity and the Organization fields as groups. If the connection has no verified peer certificates, returns empty strings.

func (*ClientAuth) Policy

func (ca *ClientAuth) Policy() tls.ClientAuthType

Policy returns the current TLS client auth policy.

func (*ClientAuth) SetPolicy

func (ca *ClientAuth) SetPolicy(policy tls.ClientAuthType)

SetPolicy sets the TLS client auth policy. The default is tls.RequireAndVerifyClientCert. Use tls.VerifyClientCertIfGiven for optional mTLS where unauthenticated clients are still allowed.

func (*ClientAuth) TLSConfig

func (ca *ClientAuth) TLSConfig() *tls.Config

TLSConfig returns a tls.Config suitable for wrapping the proxy listener. The returned config requires client certificates verified against the trusted CA pool.

func (*ClientAuth) VerifyPeerCertificate

func (ca *ClientAuth) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error

VerifyPeerCertificate returns a function suitable for tls.Config.VerifyPeerCertificate that checks the raw client certificate against the trusted CA pool. This is useful when integrating with custom TLS configurations.

func (*ClientAuth) WrapListener

func (ca *ClientAuth) WrapListener(inner net.Listener, serverCert tls.Certificate) net.Listener

WrapListener wraps a net.Listener with TLS using the ClientAuth configuration. The returned listener performs TLS handshakes with client certificate verification on every accepted connection.

The serverCert is the proxy's own TLS certificate presented to clients. For self-signed proxy deployments, generate one with GenerateCA or use the proxy's existing CertManager CA.

type CompressHandler

type CompressHandler struct {
	Handler http.Handler
	Config  CompressionConfig
}

CompressHandler wraps an http.Handler with response compression.

func NewCompressHandler

func NewCompressHandler(h http.Handler) *CompressHandler

NewCompressHandler creates a compression middleware with default config.

func (*CompressHandler) ServeHTTP

func (c *CompressHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler with transparent response compression.

type CompressionConfig

type CompressionConfig struct {
	// MinSize is the minimum response size to compress (default: 256 bytes).
	// Responses smaller than this are sent uncompressed.
	MinSize int

	// Level is the compression level (1-9 for gzip, 1-22 for brotli, 1-4 for zstd).
	// 0 uses the default level for each algorithm.
	Level int

	// ContentTypes is a list of content-type prefixes to compress.
	// Empty means compress common text types (text/*, application/json, etc.).
	ContentTypes []string

	// PreferOrder is the preferred encoding order when client accepts multiple.
	// Default: ["br", "zstd", "gzip"]
	PreferOrder []string
}

CompressionConfig controls response compression behavior.

func DefaultCompressionConfig

func DefaultCompressionConfig() CompressionConfig

DefaultCompressionConfig returns a CompressionConfig with sensible defaults.

type Config

type Config struct {
	// Server configuration
	Server ServerConfig `mapstructure:"server"`

	// TLS/CA configuration
	TLS TLSConfig `mapstructure:"tls"`

	// Filtering configuration
	Filter FilterConfig `mapstructure:"filter"`

	// Block page configuration
	BlockPage BlockPageConfig `mapstructure:"block_page"`

	// Logging configuration
	Logging LoggingConfig `mapstructure:"logging"`
}

Config represents the complete proxy configuration.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with sensible defaults.

func LoadConfig

func LoadConfig(configPath string) (*Config, error)

LoadConfig loads configuration from file, environment, and defaults. It searches for config files in the following order: 1. Explicit path (if provided) 2. ./swg.yaml, ./swg.yml, ./swg.json, ./swg.toml 3. $HOME/.swg/config.yaml 4. /etc/swg/config.yaml

func LoadConfigFromReader

func LoadConfigFromReader(configType string, data []byte) (*Config, error)

LoadConfigFromReader loads configuration from a reader. Useful for testing or embedded configs.

func (*Config) BuildRuleLoader

func (c *Config) BuildRuleLoader() (RuleLoader, error)

BuildRuleLoader creates a RuleLoader from the filter sources configuration.

func (*Config) BuildRuleSet

func (c *Config) BuildRuleSet() (*RuleSet, error)

BuildRuleSet creates a RuleSet from the filter configuration.

type ContentTypeFilter

type ContentTypeFilter struct {
	// contains filtered or unexported fields
}

ContentTypeFilter blocks responses based on Content-Type. It is used as a ResponseHook to inspect the upstream response headers.

func NewContentTypeFilter

func NewContentTypeFilter() *ContentTypeFilter

NewContentTypeFilter creates a content-type response filter.

func (*ContentTypeFilter) Block

func (f *ContentTypeFilter) Block(contentTypePrefix, reason string)

Block adds a content-type prefix to the block list. For example, "application/x-executable" blocks that exact type, while "application/" blocks all application/* types.

func (*ContentTypeFilter) HandleResponse

func (f *ContentTypeFilter) HandleResponse(_ context.Context, _ *http.Request, resp *http.Response, _ *RequestContext) *http.Response

HandleResponse implements ResponseHook.

type DomainFilter

type DomainFilter struct {
	// contains filtered or unexported fields
}

DomainFilter is a simple filter that blocks requests to specific domains.

func NewDomainFilter

func NewDomainFilter() *DomainFilter

NewDomainFilter creates a new domain-based filter.

func (*DomainFilter) AddDomain

func (f *DomainFilter) AddDomain(domain string)

AddDomain adds a domain to the blocklist. Supports wildcards: "*.example.com" blocks all subdomains.

func (*DomainFilter) AddDomains

func (f *DomainFilter) AddDomains(domains []string)

AddDomains adds multiple domains to the blocklist.

func (*DomainFilter) ShouldBlock

func (f *DomainFilter) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock implements Filter.

type ErrorResponse

type ErrorResponse struct {
	Error string `json:"error"`
}

ErrorResponse is returned for error conditions.

type ExporterType

type ExporterType string

ExporterType defines the type of trace exporter to use.

const (
	// ExporterOTLPHTTP exports traces via OTLP over HTTP.
	ExporterOTLPHTTP ExporterType = "otlp-http"
	// ExporterOTLPGRPC exports traces via OTLP over gRPC.
	ExporterOTLPGRPC ExporterType = "otlp-grpc"
)

type Filter

type Filter interface {
	// ShouldBlock returns true if the request should be blocked, along with a reason.
	ShouldBlock(req *http.Request) (blocked bool, reason string)
}

Filter determines whether a request should be blocked.

type FilterConfig

type FilterConfig struct {
	// Enabled determines if filtering is active
	Enabled bool `mapstructure:"enabled"`

	// Domains is a list of domains to block
	Domains []string `mapstructure:"domains"`

	// URLs is a list of URL prefixes to block
	URLs []string `mapstructure:"urls"`

	// Regex is a list of regex patterns to block
	Regex []string `mapstructure:"regex"`

	// Rules is a list of full rule definitions
	Rules []RuleConfig `mapstructure:"rules"`

	// Sources defines external rule sources
	Sources []SourceConfig `mapstructure:"sources"`

	// ReloadInterval for external sources (0 = no auto-reload)
	ReloadInterval time.Duration `mapstructure:"reload_interval"`
}

FilterConfig contains filtering settings.

type FilterFunc

type FilterFunc func(req *http.Request) (blocked bool, reason string)

FilterFunc is a function adapter for Filter.

func (FilterFunc) ShouldBlock

func (f FilterFunc) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock calls the underlying function to determine if a request should be blocked.

type GroupPolicyFilter

type GroupPolicyFilter struct {
	Default Filter // applied when no group matches
	// contains filtered or unexported fields
}

GroupPolicyFilter applies different filters based on the client's resolved group membership. It reads groups from the RequestContext.

func NewGroupPolicyFilter

func NewGroupPolicyFilter() *GroupPolicyFilter

NewGroupPolicyFilter creates a group-based policy filter.

func (*GroupPolicyFilter) SetPolicy

func (gf *GroupPolicyFilter) SetPolicy(group string, filter Filter)

SetPolicy assigns a filter to a group name.

func (*GroupPolicyFilter) ShouldBlock

func (gf *GroupPolicyFilter) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock implements Filter. Checks the RequestContext for group membership and applies the first matching group policy.

type HealthChecker

type HealthChecker struct {

	// ReadinessChecks are optional functions that must all return nil
	// for the readiness probe to pass. If empty, readiness follows liveness.
	ReadinessChecks []ReadinessCheck
	// contains filtered or unexported fields
}

HealthChecker provides liveness and readiness probes for the proxy. It tracks whether the proxy has started successfully and can optionally run custom readiness checks (e.g., verifying filter rules are loaded).

func NewHealthChecker

func NewHealthChecker() *HealthChecker

NewHealthChecker creates a new HealthChecker.

func (*HealthChecker) HandleHealthz

func (h *HealthChecker) HandleHealthz(w http.ResponseWriter, _ *http.Request)

HandleHealthz handles the /healthz liveness probe endpoint.

func (*HealthChecker) HandleReadyz

func (h *HealthChecker) HandleReadyz(w http.ResponseWriter, _ *http.Request)

HandleReadyz handles the /readyz readiness probe endpoint.

func (*HealthChecker) IsAlive

func (h *HealthChecker) IsAlive() bool

IsAlive returns true if the proxy is alive.

func (*HealthChecker) IsReady

func (h *HealthChecker) IsReady() bool

IsReady returns true if the proxy is ready to serve traffic. If ReadinessChecks are configured, all must pass. Otherwise, readiness follows the explicitly set ready state.

func (*HealthChecker) SetAlive

func (h *HealthChecker) SetAlive(alive bool)

SetAlive marks the proxy as alive (liveness probe passes).

func (*HealthChecker) SetReady

func (h *HealthChecker) SetReady(ready bool)

SetReady marks the proxy as ready (readiness probe passes).

type HealthResponse

type HealthResponse struct {
	Status  string   `json:"status"`
	Uptime  string   `json:"uptime,omitempty"`
	Reason  string   `json:"reason,omitempty"`
	Details []string `json:"details,omitempty"`
}

HealthResponse is the JSON body returned by health endpoints.

type IPIdentityResolver

type IPIdentityResolver struct {
	// contains filtered or unexported fields
}

IPIdentityResolver maps client IPs to identity/groups.

func NewIPIdentityResolver

func NewIPIdentityResolver() *IPIdentityResolver

NewIPIdentityResolver creates an empty IP-based identity resolver.

func (*IPIdentityResolver) AddCIDR

func (r *IPIdentityResolver) AddCIDR(cidr, identity string, groups []string) error

AddCIDR maps a CIDR range to an identity and groups.

func (*IPIdentityResolver) AddIP

func (r *IPIdentityResolver) AddIP(ip, identity string, groups []string)

AddIP maps a single IP to an identity and groups.

func (*IPIdentityResolver) Resolve

func (r *IPIdentityResolver) Resolve(req *http.Request) (string, []string, error)

Resolve implements IdentityResolver.

type IdentityResolver

type IdentityResolver interface {
	Resolve(req *http.Request) (identity string, groups []string, err error)
}

IdentityResolver determines who a client is from the request. This drives per-user/group policy decisions. Implementations might use client certificates, Proxy-Authorization headers, IP-to-user mappings, or external auth services.

type IdentityResolverFunc

type IdentityResolverFunc func(req *http.Request) (string, []string, error)

IdentityResolverFunc is a function adapter for IdentityResolver.

func (IdentityResolverFunc) Resolve

func (f IdentityResolverFunc) Resolve(req *http.Request) (string, []string, error)

type LoggingConfig

type LoggingConfig struct {
	// Level is the log level: debug, info, warn, error
	Level string `mapstructure:"level"`

	// Format is the log format: text, json
	Format string `mapstructure:"format"`

	// Output is where to write logs: stdout, stderr, or file path
	Output string `mapstructure:"output"`
}

LoggingConfig contains logging settings.

type MessageResponse

type MessageResponse struct {
	Message string `json:"message"`
}

MessageResponse is returned for successful mutations.

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics holds all Prometheus metrics for the proxy.

func NewMetrics

func NewMetrics() *Metrics

NewMetrics creates a new Metrics instance with all collectors registered.

func (*Metrics) DecActiveConns

func (m *Metrics) DecActiveConns()

DecActiveConns decrements the active connection gauge.

func (*Metrics) Handler

func (m *Metrics) Handler() http.Handler

Handler returns an http.Handler that serves the /metrics endpoint.

func (*Metrics) IncActiveConns

func (m *Metrics) IncActiveConns()

IncActiveConns increments the active connection gauge.

func (*Metrics) RecordBlocked

func (m *Metrics) RecordBlocked(reason string)

RecordBlocked records a blocked request.

func (*Metrics) RecordCertCacheHit

func (m *Metrics) RecordCertCacheHit()

RecordCertCacheHit records a certificate cache hit.

func (*Metrics) RecordCertCacheMiss

func (m *Metrics) RecordCertCacheMiss()

RecordCertCacheMiss records a certificate cache miss.

func (*Metrics) RecordFilterReload

func (m *Metrics) RecordFilterReload()

RecordFilterReload records a successful filter reload.

func (*Metrics) RecordFilterReloadError

func (m *Metrics) RecordFilterReloadError()

RecordFilterReloadError records a failed filter reload.

func (*Metrics) RecordRequest

func (m *Metrics) RecordRequest(method, scheme string)

RecordRequest records a processed request.

func (*Metrics) RecordRequestDuration

func (m *Metrics) RecordRequestDuration(method string, statusCode int, duration time.Duration)

RecordRequestDuration records the duration of a request.

func (*Metrics) RecordTLSHandshakeError

func (m *Metrics) RecordTLSHandshakeError()

RecordTLSHandshakeError records a TLS handshake failure.

func (*Metrics) RecordUpstreamError

func (m *Metrics) RecordUpstreamError(host string)

RecordUpstreamError records an upstream connection error.

func (*Metrics) SetCertCacheSize

func (m *Metrics) SetCertCacheSize(size int)

SetCertCacheSize sets the certificate cache size gauge.

func (*Metrics) SetFilterRuleCount

func (m *Metrics) SetFilterRuleCount(count int)

SetFilterRuleCount sets the current filter rule count.

type MultiLoader

type MultiLoader struct {
	Loaders []RuleLoader
}

MultiLoader combines multiple loaders into one.

func NewMultiLoader

func NewMultiLoader(loaders ...RuleLoader) *MultiLoader

NewMultiLoader creates a loader that combines rules from multiple sources.

func (*MultiLoader) Load

func (m *MultiLoader) Load(ctx context.Context) ([]Rule, error)

Load implements RuleLoader by loading from all configured loaders.

type PACGenerator

type PACGenerator struct {
	// ProxyAddr is the proxy address in host:port format (e.g., "proxy.local:8080").
	ProxyAddr string

	// BypassDomains are domains that should bypass the proxy (direct connection).
	BypassDomains []string

	// BypassNetworks are CIDR networks that should bypass the proxy.
	BypassNetworks []string

	// FallbackDirect determines whether to fall back to direct connection
	// if the proxy is unreachable.
	FallbackDirect bool
}

PACGenerator generates Proxy Auto-Configuration (PAC) files.

func NewPACGenerator

func NewPACGenerator(proxyAddr string) *PACGenerator

NewPACGenerator creates a PACGenerator for the given proxy address.

func (*PACGenerator) AddBypassDomain

func (g *PACGenerator) AddBypassDomain(domain string)

AddBypassDomain adds a domain to the bypass list. Supports wildcards: ".example.com" bypasses all subdomains.

func (*PACGenerator) AddBypassNetwork

func (g *PACGenerator) AddBypassNetwork(cidr string)

AddBypassNetwork adds a CIDR network to the bypass list.

func (*PACGenerator) Generate

func (g *PACGenerator) Generate(w io.Writer) error

Generate writes the PAC file content to the given writer.

func (*PACGenerator) GenerateString

func (g *PACGenerator) GenerateString() (string, error)

GenerateString returns the PAC file content as a string.

func (*PACGenerator) ServeHTTP

func (g *PACGenerator) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler, serving the PAC file with the correct content type.

func (*PACGenerator) WriteFile

func (g *PACGenerator) WriteFile(path string) error

WriteFile writes the PAC file to the given path.

type PolicyEngine

type PolicyEngine struct {
	// RequestHooks are called in order when a request arrives.
	// Any hook may short-circuit by returning a response.
	RequestHooks []RequestHook

	// ResponseHooks are called in order after the upstream response
	// is received. Any hook may replace the response.
	ResponseHooks []ResponseHook

	// IdentityResolver resolves client identity before hooks run.
	IdentityResolver IdentityResolver

	// BodyScanners inspect response bodies. They run after
	// ResponseHooks, only for responses whose Content-Type matches
	// ScanContentTypes (or all responses if ScanContentTypes is empty).
	BodyScanners []ResponseBodyScanner

	// ScanContentTypes limits body scanning to responses with matching
	// Content-Type prefixes (e.g. "application/octet-stream",
	// "application/zip"). Empty means scan all responses.
	ScanContentTypes []string

	// MaxScanSize is the maximum number of bytes to buffer for body
	// scanning. Responses larger than this are passed through without
	// scanning. Default is 10 MiB.
	MaxScanSize int64
}

PolicyEngine manages the request/response pipeline with hooks, identity resolution, and body scanning. It is set on Proxy.Policy.

func NewPolicyEngine

func NewPolicyEngine() *PolicyEngine

NewPolicyEngine creates a PolicyEngine with sensible defaults.

func (*PolicyEngine) ProcessRequest

func (pe *PolicyEngine) ProcessRequest(ctx context.Context, req *http.Request) (*RequestContext, *http.Response)

ProcessRequest runs the request-side pipeline: identity resolution then request hooks. Returns a non-nil response to short-circuit.

func (*PolicyEngine) ProcessResponse

func (pe *PolicyEngine) ProcessResponse(ctx context.Context, req *http.Request, resp *http.Response, rc *RequestContext) (*http.Response, error)

ProcessResponse runs the response-side pipeline: response hooks then body scanners. Returns the (possibly replaced) response.

type Proxy

type Proxy struct {
	// Addr is the address to listen on (e.g., ":8080")
	Addr string

	// CertManager handles dynamic certificate generation
	CertManager *CertManager

	// Filter determines whether requests should be blocked
	Filter Filter

	// BlockPageURL is the URL to redirect blocked requests to (optional)
	BlockPageURL string

	// BlockPage is a custom block page template (optional, uses default if nil)
	BlockPage *BlockPage

	// Logger for proxy events
	Logger *slog.Logger

	// Transport for outbound requests (optional, uses default if nil)
	Transport http.RoundTripper

	// Metrics collects Prometheus metrics (optional)
	Metrics *Metrics

	// PACHandler serves PAC files at /proxy.pac (optional)
	PACHandler *PACGenerator

	// HealthChecker provides /healthz and /readyz endpoints (optional)
	HealthChecker *HealthChecker

	// AccessLog writes structured access log entries for each request (optional)
	AccessLog *AccessLogger

	// UpstreamProxy forwards requests through a parent proxy (optional).
	// When set, CONNECT tunnels are established via the upstream proxy
	// and plain HTTP requests are forwarded through it.
	UpstreamProxy *UpstreamProxy

	// RateLimiter provides per-client request throttling (optional).
	// When set, requests exceeding the rate limit receive 429 responses.
	RateLimiter *RateLimiter

	// TransportPool provides a connection-pooled transport with HTTP/2
	// support (optional). When set, its Transport() is used as the base
	// transport instead of the Transport field.
	TransportPool *TransportPool

	// Policy provides lifecycle hooks, identity resolution, and response
	// body scanning (optional). When set, request hooks run before
	// filtering and response hooks run after the upstream response is
	// received. This enables pluggable AV scanning, DLP, content-type
	// blocking, per-group policies, and more.
	Policy *PolicyEngine

	// Admin provides REST endpoints for runtime rule management,
	// status inspection, and filter reloads (optional). When set,
	// requests matching the AdminAPI.PathPrefix are routed to the
	// admin handler instead of being proxied.
	Admin *AdminAPI

	// ClientAuth enables mutual TLS (mTLS) on the proxy listener.
	// When set, clients must present a valid certificate signed by
	// a trusted CA to connect. See [ClientAuth] for configuration.
	ClientAuth *ClientAuth

	// Bypass allows authorized clients to skip content filtering.
	// When set, requests carrying a valid bypass token in an HTTP
	// header or originating from a whitelisted identity skip the
	// filter and policy hooks. See [Bypass] for configuration.
	Bypass *Bypass
	// contains filtered or unexported fields
}

Proxy is an HTTPS MITM proxy that intercepts TLS traffic for content filtering.

func NewProxy

func NewProxy(addr string, cm *CertManager) *Proxy

NewProxy creates a new HTTPS MITM proxy.

func (*Proxy) ListenAndServe

func (p *Proxy) ListenAndServe() error

ListenAndServe starts the proxy server. When [Proxy.ClientAuth] is set, the listener is wrapped with TLS to enforce mutual TLS authentication before any HTTP traffic.

func (*Proxy) ServeHTTP

func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles incoming proxy requests.

func (*Proxy) Shutdown

func (p *Proxy) Shutdown(ctx context.Context) error

Shutdown gracefully stops the proxy.

type ProxyTracer

type ProxyTracer struct {
	// contains filtered or unexported fields
}

ProxyTracer provides tracing specifically for proxy operations.

func NewProxyTracer

func NewProxyTracer(t *Tracer) *ProxyTracer

NewProxyTracer creates a ProxyTracer wrapping a Tracer.

func (*ProxyTracer) AddEvent

func (pt *ProxyTracer) AddEvent(ctx context.Context, name string, attrs ...attribute.KeyValue)

AddEvent adds an event to the current span.

func (*ProxyTracer) Enabled

func (pt *ProxyTracer) Enabled() bool

Enabled returns whether tracing is enabled.

func (*ProxyTracer) RecordError

func (pt *ProxyTracer) RecordError(ctx context.Context, err error)

RecordError records an error on the current span.

func (*ProxyTracer) SetAllowed

func (pt *ProxyTracer) SetAllowed(ctx context.Context)

SetAllowed marks the request as allowed through the filter.

func (*ProxyTracer) SetBlocked

func (pt *ProxyTracer) SetBlocked(ctx context.Context, reason string)

SetBlocked marks the request as blocked by the filter.

func (*ProxyTracer) SetIdentity

func (pt *ProxyTracer) SetIdentity(ctx context.Context, identity string, groups []string)

SetIdentity sets identity information on the span.

func (*ProxyTracer) SetUpstreamResponse

func (pt *ProxyTracer) SetUpstreamResponse(ctx context.Context, statusCode int, contentLength int64)

SetUpstreamResponse records upstream response details.

func (*ProxyTracer) StartBodyScan

func (pt *ProxyTracer) StartBodyScan(ctx context.Context, contentType string, size int64) (context.Context, trace.Span)

StartBodyScan starts a span for response body scanning.

func (*ProxyTracer) StartCertGeneration

func (pt *ProxyTracer) StartCertGeneration(ctx context.Context, host string) (context.Context, trace.Span)

StartCertGeneration starts a span for certificate generation.

func (*ProxyTracer) StartConnect

func (pt *ProxyTracer) StartConnect(ctx context.Context, host string, clientAddr string) (context.Context, trace.Span)

StartConnect starts a span for a CONNECT tunnel establishment.

func (*ProxyTracer) StartFilter

func (pt *ProxyTracer) StartFilter(ctx context.Context, host string) (context.Context, trace.Span)

StartFilter starts a span for filter evaluation.

func (*ProxyTracer) StartRequest

func (pt *ProxyTracer) StartRequest(ctx context.Context, r *http.Request) (context.Context, trace.Span)

StartRequest starts a span for an incoming proxy request.

func (*ProxyTracer) StartTLSHandshake

func (pt *ProxyTracer) StartTLSHandshake(ctx context.Context, host string) (context.Context, trace.Span)

StartTLSHandshake starts a span for TLS handshake with client.

func (*ProxyTracer) StartUpstream

func (pt *ProxyTracer) StartUpstream(ctx context.Context, r *http.Request) (context.Context, trace.Span)

StartUpstream starts a span for upstream request.

type RateLimiter

type RateLimiter struct {

	// Rate is the number of requests permitted per second per client.
	Rate float64

	// Burst is the maximum number of requests a client can make in a
	// single burst before being throttled.
	Burst int

	// CleanupInterval controls how often stale buckets are removed.
	// Defaults to 1 minute.
	CleanupInterval time.Duration
	// contains filtered or unexported fields
}

RateLimiter provides per-client request throttling using a token-bucket algorithm. Each client IP gets an independent bucket that refills at a steady rate up to a configurable burst size.

func NewRateLimiter

func NewRateLimiter(rate float64, burst int) *RateLimiter

NewRateLimiter creates a new per-client rate limiter. rate is requests/second, burst is the max tokens a client can accumulate.

func (*RateLimiter) Allow

func (rl *RateLimiter) Allow(addr string) bool

Allow returns true if the request from the given client address is permitted under the rate limit.

func (*RateLimiter) AllowHTTP

func (rl *RateLimiter) AllowHTTP(w http.ResponseWriter, r *http.Request) bool

AllowHTTP checks the rate limit for the given HTTP request and writes a 429 Too Many Requests response if the client is throttled. Returns true if the request is allowed.

func (*RateLimiter) ClientCount

func (rl *RateLimiter) ClientCount() int

ClientCount returns the number of tracked clients.

func (*RateLimiter) Close

func (rl *RateLimiter) Close()

Close stops the background cleanup goroutine.

type ReadinessCheck

type ReadinessCheck func() error

ReadinessCheck is a function that returns nil if the component is ready, or an error describing why it is not.

type ReloadFunc

type ReloadFunc func(ctx context.Context) (Filter, error)

ReloadFunc is called on each SIGHUP. It should reload configuration and return the new Filter (or nil to keep the current one) and any error.

type ReloadableFilter

type ReloadableFilter struct {

	// OnReload is called after successful reload with the rule count
	OnReload func(count int)

	// OnError is called when reload fails
	OnError func(err error)
	// contains filtered or unexported fields
}

ReloadableFilter wraps a RuleSet with automatic reloading from a RuleLoader.

func NewReloadableFilter

func NewReloadableFilter(loader RuleLoader) *ReloadableFilter

NewReloadableFilter creates a new filter that can reload rules from a loader.

func (*ReloadableFilter) Count

func (rf *ReloadableFilter) Count() int

Count returns the current number of rules.

func (*ReloadableFilter) Load

func (rf *ReloadableFilter) Load(ctx context.Context) error

Load loads rules from the configured loader, replacing existing rules.

func (*ReloadableFilter) RuleSet

func (rf *ReloadableFilter) RuleSet() *RuleSet

RuleSet returns the underlying RuleSet for direct rule manipulation. The returned RuleSet is thread-safe for concurrent reads and writes.

func (*ReloadableFilter) ShouldBlock

func (rf *ReloadableFilter) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock implements the Filter interface.

func (*ReloadableFilter) StartAutoReload

func (rf *ReloadableFilter) StartAutoReload(ctx context.Context, interval time.Duration) context.CancelFunc

StartAutoReload starts a goroutine that reloads rules at the specified interval. Returns a cancel function to stop the reload goroutine.

type RequestContext

type RequestContext struct {
	// ClientIP is the connecting client's IP (without port).
	ClientIP string

	// Identity is resolved by an IdentityResolver (optional).
	// May represent a username, group, device ID, or any string.
	Identity string

	// Groups the client belongs to (resolved by IdentityResolver).
	Groups []string

	// Tags are arbitrary key-value metadata set by hooks.
	Tags map[string]string

	// Blocked is set to true when any stage decides to block the request.
	Blocked bool

	// BlockReason is the human-readable reason for blocking.
	BlockReason string

	// StartTime is when the request was first received.
	StartTime time.Time
}

RequestContext carries metadata through the proxy request lifecycle. Hooks and middleware attach information here for downstream stages.

func GetRequestContext

func GetRequestContext(ctx context.Context) *RequestContext

GetRequestContext retrieves the RequestContext from the context, or nil.

type RequestHook

type RequestHook interface {
	HandleRequest(ctx context.Context, req *http.Request, rc *RequestContext) *http.Response
}

RequestHook is called when a request is first received, before any filtering. Hooks may inspect, modify, or block the request. They may also resolve identity, attach tags, or perform early access control.

Returning a non-nil *http.Response short-circuits the pipeline: that response is sent to the client and no further hooks or forwarding occur.

type RequestHookFunc

type RequestHookFunc func(ctx context.Context, req *http.Request, rc *RequestContext) *http.Response

RequestHookFunc is a function adapter for RequestHook.

func (RequestHookFunc) HandleRequest

func (f RequestHookFunc) HandleRequest(ctx context.Context, req *http.Request, rc *RequestContext) *http.Response

type ResponseBodyScanner

type ResponseBodyScanner interface {
	Scan(ctx context.Context, body []byte, req *http.Request, resp *http.Response) (ScanResult, error)
}

ResponseBodyScanner inspects response bodies for threats or policy violations. Implementations can wrap AV engines, DLP scanners, keyword detectors, or any content analysis tool.

Scan receives the full body as a byte slice (up to the configured MaxScanSize) plus the request and response for context. Returning an error causes the proxy to serve a 502 error to the client.

type ResponseBodyScannerFunc

type ResponseBodyScannerFunc func(ctx context.Context, body []byte, req *http.Request, resp *http.Response) (ScanResult, error)

ResponseBodyScannerFunc is a function adapter for ResponseBodyScanner.

func (ResponseBodyScannerFunc) Scan

func (f ResponseBodyScannerFunc) Scan(ctx context.Context, body []byte, req *http.Request, resp *http.Response) (ScanResult, error)

type ResponseHook

type ResponseHook interface {
	HandleResponse(ctx context.Context, req *http.Request, resp *http.Response, rc *RequestContext) *http.Response
}

ResponseHook is called after the upstream response is received but before it is sent back to the client. Hooks may inspect headers, content-type, or the response body. They may replace the response entirely (e.g. with a block page) by returning a non-nil *http.Response.

The original response body is readable (and should be closed by the hook if it replaces the response). For body inspection, use ResponseBodyScanner which handles buffering and streaming.

type ResponseHookFunc

type ResponseHookFunc func(ctx context.Context, req *http.Request, resp *http.Response, rc *RequestContext) *http.Response

ResponseHookFunc is a function adapter for ResponseHook.

func (ResponseHookFunc) HandleResponse

func (f ResponseHookFunc) HandleResponse(ctx context.Context, req *http.Request, resp *http.Response, rc *RequestContext) *http.Response

type Rule

type Rule struct {
	// Type of rule: "domain", "url", "regex"
	Type string

	// Pattern is the matching pattern (domain, URL prefix, or regex)
	Pattern string

	// Reason for blocking (shown to user)
	Reason string

	// Category for grouping/reporting (optional)
	Category string
	// contains filtered or unexported fields
}

Rule represents a blocking rule that can match domains, URLs, or patterns.

func ParseDomainList

func ParseDomainList(r io.Reader) ([]Rule, error)

ParseDomainList parses a list of domains (one per line) into rules. Supports comments (lines starting with #) and empty lines.

type RuleConfig

type RuleConfig struct {
	Type     string `mapstructure:"type"`
	Pattern  string `mapstructure:"pattern"`
	Reason   string `mapstructure:"reason"`
	Category string `mapstructure:"category"`
}

RuleConfig represents a single blocking rule in config.

type RuleLoader

type RuleLoader interface {
	// Load reads rules from the source and returns them.
	Load(ctx context.Context) ([]Rule, error)
}

RuleLoader defines the interface for loading rules from various sources.

type RuleLoaderFunc

type RuleLoaderFunc func(ctx context.Context) ([]Rule, error)

RuleLoaderFunc is a function adapter for RuleLoader.

func (RuleLoaderFunc) Load

func (f RuleLoaderFunc) Load(ctx context.Context) ([]Rule, error)

Load calls the underlying function to load rules.

type RuleRequest

type RuleRequest struct {
	Type     string `json:"type"`
	Pattern  string `json:"pattern"`
	Reason   string `json:"reason,omitempty"`
	Category string `json:"category,omitempty"`
}

RuleRequest is the body for POST /api/rules and DELETE /api/rules.

type RuleSet

type RuleSet struct {
	// contains filtered or unexported fields
}

RuleSet is a collection of blocking rules with efficient lookup.

func NewRuleSet

func NewRuleSet() *RuleSet

NewRuleSet creates a new empty RuleSet.

func (*RuleSet) AddDomain

func (rs *RuleSet) AddDomain(domain string)

AddDomain is a convenience method to add a domain blocking rule.

func (*RuleSet) AddRegex

func (rs *RuleSet) AddRegex(pattern string) error

AddRegex is a convenience method to add a regex blocking rule.

func (*RuleSet) AddRule

func (rs *RuleSet) AddRule(r Rule) error

AddRule adds a rule to the set.

func (*RuleSet) AddURL

func (rs *RuleSet) AddURL(urlPrefix string)

AddURL is a convenience method to add a URL prefix blocking rule.

func (*RuleSet) Clear

func (rs *RuleSet) Clear()

Clear removes all rules from the set.

func (*RuleSet) Count

func (rs *RuleSet) Count() int

Count returns the total number of rules in the set.

func (*RuleSet) Match

func (rs *RuleSet) Match(req *http.Request) (*Rule, bool)

Match checks if a request matches any rule in the set. Returns the matching rule and true if blocked, nil and false otherwise.

func (*RuleSet) RemoveRule

func (rs *RuleSet) RemoveRule(ruleType, pattern string) bool

RemoveRule removes the first rule matching the given type and pattern. Returns true if a rule was removed.

func (*RuleSet) Rules

func (rs *RuleSet) Rules() []Rule

Rules returns a snapshot of all rules in the set.

func (*RuleSet) ShouldBlock

func (rs *RuleSet) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock implements the Filter interface.

type RulesResponse

type RulesResponse struct {
	Count int    `json:"count"`
	Rules []Rule `json:"rules"`
}

RulesResponse is returned by GET /api/rules.

type SIGHUPReloader

type SIGHUPReloader struct {
	// contains filtered or unexported fields
}

SIGHUPReloader watches for SIGHUP signals and reloads the proxy filter. Call Cancel to stop watching.

func WatchSIGHUP

func WatchSIGHUP(proxy *Proxy, reload ReloadFunc, logger *slog.Logger) *SIGHUPReloader

WatchSIGHUP starts a goroutine that listens for SIGHUP signals and calls the reload function. If reload returns a non-nil Filter, it is assigned to the proxy. The returned SIGHUPReloader can be used to stop watching.

func (*SIGHUPReloader) Cancel

func (r *SIGHUPReloader) Cancel()

Cancel stops the SIGHUP watcher.

type ScanResult

type ScanResult struct {
	Verdict ScanVerdict

	// Reason is set when Verdict is VerdictBlock.
	Reason string

	// ReplacementBody is set when Verdict is VerdictReplace. The caller
	// is responsible for closing it.
	ReplacementBody io.ReadCloser

	// ReplacementContentType overrides Content-Type when replacing.
	ReplacementContentType string
}

ScanResult holds the outcome of a body scan.

type ScanVerdict

type ScanVerdict int

ScanVerdict is the result of a ResponseBodyScanner inspection.

const (
	// VerdictAllow means the content is clean.
	VerdictAllow ScanVerdict = iota

	// VerdictBlock means the content should be blocked.
	VerdictBlock

	// VerdictReplace means the scanner is providing a replacement body.
	VerdictReplace
)

type ServerConfig

type ServerConfig struct {
	// Address to listen on (e.g., ":8080", "0.0.0.0:8080")
	Addr string `mapstructure:"addr"`

	// ReadTimeout for incoming connections
	ReadTimeout time.Duration `mapstructure:"read_timeout"`

	// WriteTimeout for outgoing responses
	WriteTimeout time.Duration `mapstructure:"write_timeout"`

	// IdleTimeout for keep-alive connections
	IdleTimeout time.Duration `mapstructure:"idle_timeout"`
}

ServerConfig contains server-related settings.

type SourceConfig

type SourceConfig struct {
	// Type of source: "csv", "url"
	Type string `mapstructure:"type"`

	// Path for file-based sources
	Path string `mapstructure:"path"`

	// URL for remote sources
	URL string `mapstructure:"url"`

	// HasHeader indicates if CSV has a header row
	HasHeader bool `mapstructure:"has_header"`
}

SourceConfig defines an external rule source.

type StaticLoader

type StaticLoader struct {
	Rules []Rule
}

StaticLoader returns a fixed set of rules. Useful for testing or combining with other loaders.

func NewStaticLoader

func NewStaticLoader(rules ...Rule) *StaticLoader

NewStaticLoader creates a loader with a fixed set of rules.

func (*StaticLoader) Load

func (l *StaticLoader) Load(ctx context.Context) ([]Rule, error)

Load implements RuleLoader.

type StatusResponse

type StatusResponse struct {
	Status    string `json:"status"`
	RuleCount int    `json:"rule_count"`
	Uptime    string `json:"uptime,omitempty"`
	Filter    string `json:"filter_type"`
}

StatusResponse is returned by GET /api/status.

type TLSConfig

type TLSConfig struct {
	// CACert is the path to the CA certificate file (for self-signed mode)
	CACert string `mapstructure:"ca_cert"`

	// CAKey is the path to the CA private key file (for self-signed mode)
	CAKey string `mapstructure:"ca_key"`

	// Organization name for generated certificates
	Organization string `mapstructure:"organization"`

	// CertValidityDays for generated host certificates
	CertValidityDays int `mapstructure:"cert_validity_days"`

	// ACME configuration for Let's Encrypt certificates (optional)
	// When enabled, certificates are obtained from Let's Encrypt instead of self-signed.
	ACME *ACMEConfig `mapstructure:"acme"`
}

TLSConfig contains TLS/certificate settings.

type TimeRule

type TimeRule struct {
	// Inner is the filter to apply during the active window.
	Inner Filter

	// StartHour is the hour (0-23) when the rule becomes active.
	StartHour int

	// EndHour is the hour (0-23) when the rule becomes inactive.
	// If EndHour < StartHour, the window wraps past midnight.
	EndHour int

	// Weekdays limits the rule to specific days. Empty means every day.
	Weekdays []time.Weekday

	// Location for time evaluation. Defaults to UTC.
	Location *time.Location

	// NowFunc returns the current time. Defaults to time.Now.
	// Exposed for testing.
	NowFunc func() time.Time
}

TimeRule wraps a Filter and only activates it during specific time windows. Outside the window, the inner filter is bypassed.

func (*TimeRule) ShouldBlock

func (tr *TimeRule) ShouldBlock(req *http.Request) (bool, string)

ShouldBlock implements Filter. Delegates to Inner only during the active time window.

type Tracer

type Tracer struct {
	// contains filtered or unexported fields
}

Tracer provides OpenTelemetry tracing for the proxy.

func NewTracer

func NewTracer(cfg TracingConfig) (*Tracer, error)

NewTracer creates a new Tracer with the given configuration.

func (*Tracer) Enabled

func (t *Tracer) Enabled() bool

Enabled returns whether tracing is enabled.

func (*Tracer) Extract

func (t *Tracer) Extract(ctx context.Context, headers http.Header) context.Context

Extract extracts trace context from HTTP headers.

func (*Tracer) Inject

func (t *Tracer) Inject(ctx context.Context, headers http.Header)

Inject injects trace context into HTTP headers.

func (*Tracer) Shutdown

func (t *Tracer) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the tracer provider.

func (*Tracer) SpanFromContext

func (t *Tracer) SpanFromContext(ctx context.Context) trace.Span

SpanFromContext returns the current span from the context.

func (*Tracer) StartSpan

func (t *Tracer) StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span)

StartSpan starts a new span with the given name.

func (*Tracer) TracerProvider

func (t *Tracer) TracerProvider() *sdktrace.TracerProvider

TracerProvider returns the underlying TracerProvider.

type TracingConfig

type TracingConfig struct {
	// Enabled enables or disables tracing.
	Enabled bool

	// ServiceName is the name of the service reported in traces.
	// Defaults to "swg-proxy".
	ServiceName string

	// ServiceVersion is the version of the service reported in traces.
	ServiceVersion string

	// Exporter specifies which exporter to use.
	// Defaults to ExporterOTLPHTTP.
	Exporter ExporterType

	// Endpoint is the OTLP collector endpoint.
	// For OTLP HTTP: defaults to "localhost:4318"
	// For OTLP gRPC: defaults to "localhost:4317"
	Endpoint string

	// Insecure disables TLS for the exporter connection.
	Insecure bool

	// Headers are additional headers to send with OTLP requests.
	Headers map[string]string

	// SampleRate is the sampling rate (0.0 to 1.0).
	// 1.0 means sample everything, 0.0 means sample nothing.
	// Defaults to 1.0.
	SampleRate float64

	// BatchTimeout is the maximum time to wait before exporting a batch.
	// Defaults to 5 seconds.
	BatchTimeout time.Duration

	// MaxExportBatchSize is the maximum number of spans per batch.
	// Defaults to 512.
	MaxExportBatchSize int

	// MaxQueueSize is the maximum number of spans to queue.
	// Defaults to 2048.
	MaxQueueSize int

	// ResourceAttributes are additional attributes to add to the resource.
	ResourceAttributes map[string]string
}

TracingConfig configures OpenTelemetry tracing for the proxy.

func DefaultTracingConfig

func DefaultTracingConfig() TracingConfig

DefaultTracingConfig returns a TracingConfig with sensible defaults.

type TracingMiddleware

type TracingMiddleware struct {
	// contains filtered or unexported fields
}

TracingMiddleware wraps an http.Handler with tracing.

func NewTracingMiddleware

func NewTracingMiddleware(tracer *Tracer, next http.Handler, opts TracingMiddlewareOptions) *TracingMiddleware

NewTracingMiddleware creates a new tracing middleware.

func (*TracingMiddleware) ServeHTTP

func (m *TracingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

type TracingMiddlewareOptions

type TracingMiddlewareOptions struct {
	// IncludeHeaders includes request/response headers in span attributes.
	IncludeHeaders bool

	// HeadersToCapture specifies which headers to capture (if IncludeHeaders is true).
	// If empty, captures all headers.
	HeadersToCapture []string

	// IncludeQuery includes the query string in span attributes.
	IncludeQuery bool

	// SkipPaths are URL paths to skip tracing for.
	SkipPaths []string
}

TracingMiddlewareOptions configures the tracing middleware.

type TransportPool

type TransportPool struct {
	// MaxIdleConns is the total maximum number of idle connections
	// across all hosts. Zero means the default (100).
	MaxIdleConns int

	// MaxIdleConnsPerHost is the maximum number of idle connections
	// per host. Zero means the default (2 per host).
	MaxIdleConnsPerHost int

	// MaxConnsPerHost limits the total number of connections per host,
	// including connections in the dialing, active, and idle states.
	// Zero means no limit.
	MaxConnsPerHost int

	// IdleConnTimeout is how long an idle connection remains in the
	// pool before being closed. Zero means the default (90 seconds).
	IdleConnTimeout time.Duration

	// DialTimeout is the maximum time to wait for a TCP dial to complete.
	// Zero means the default (30 seconds).
	DialTimeout time.Duration

	// TLSHandshakeTimeout is the maximum time to wait for a TLS
	// handshake. Zero means the default (10 seconds).
	TLSHandshakeTimeout time.Duration

	// ResponseHeaderTimeout is the maximum time to wait for a server's
	// response headers after the request has been fully written.
	// Zero means no timeout.
	ResponseHeaderTimeout time.Duration

	// EnableHTTP2 enables HTTP/2 negotiation with upstream servers.
	// When true, the transport will attempt h2 via ALPN during TLS.
	EnableHTTP2 bool

	// TLSConfig provides custom TLS settings for upstream connections.
	// If nil, a default configuration is used.
	TLSConfig *tls.Config

	// DisableKeepAlives disables HTTP keep-alives; each request will
	// use a fresh connection. This overrides connection pool settings.
	DisableKeepAlives bool

	// WriteBufferSize specifies the size of the write buffer used
	// when writing to the transport. Zero uses the default.
	WriteBufferSize int

	// ReadBufferSize specifies the size of the read buffer used
	// when reading from the transport. Zero uses the default.
	ReadBufferSize int
	// contains filtered or unexported fields
}

TransportPool provides a configurable HTTP transport with connection pooling and optional HTTP/2 support. It wraps http.Transport with sensible defaults for a forward proxy workload and exposes connection pool statistics for monitoring.

func NewTransportPool

func NewTransportPool() *TransportPool

NewTransportPool creates a TransportPool with sensible proxy defaults.

func (*TransportPool) Build

func (tp *TransportPool) Build() *http.Transport

Build creates the underlying http.Transport. Call this after setting all configuration fields. It is safe to call multiple times; each call creates a fresh transport and closes idle connections on the previous one.

func (*TransportPool) CloseIdleConnections

func (tp *TransportPool) CloseIdleConnections()

CloseIdleConnections closes all idle connections in the pool.

func (*TransportPool) Stats

func (tp *TransportPool) Stats() TransportPoolStats

Stats returns a snapshot of transport statistics.

func (*TransportPool) Transport

func (tp *TransportPool) Transport() http.RoundTripper

Transport returns an http.RoundTripper that wraps the pooled transport with request counting. If [Build] has not been called, it is called automatically.

type TransportPoolStats

type TransportPoolStats struct {
	TotalRequests  int64
	ActiveRequests int64
}

TransportPoolStats holds a snapshot of connection pool statistics.

type URLLoader

type URLLoader struct {
	// URL to fetch rules from
	URL string

	// Client for HTTP requests (uses http.DefaultClient if nil)
	Client *http.Client

	// HasHeader indicates if the first row is a header
	HasHeader bool

	// DefaultReason is used when the reason column is empty
	DefaultReason string
}

URLLoader loads rules from an HTTP endpoint. Expects the same CSV format as CSVLoader.

func NewURLLoader

func NewURLLoader(endpoint string) *URLLoader

NewURLLoader creates a loader that fetches rules from a URL.

func (*URLLoader) Load

func (l *URLLoader) Load(ctx context.Context) ([]Rule, error)

Load implements RuleLoader.

type UpstreamAuth

type UpstreamAuth struct {
	Username string
	Password string
}

UpstreamAuth holds basic-auth credentials for an upstream proxy.

type UpstreamProxy

type UpstreamProxy struct {
	// URL is the upstream proxy address (e.g., "http://proxy.corp:3128").
	URL *url.URL

	// Auth is optional basic-auth credentials for the upstream proxy.
	Auth *UpstreamAuth

	// TLSConfig for connecting to TLS-enabled upstream proxies (optional).
	TLSConfig *tls.Config

	// DialTimeout is the timeout for establishing a connection to the upstream proxy.
	// Defaults to 10 seconds.
	DialTimeout time.Duration

	// ProxyProtocol enables sending a PROXY protocol header (v1 or v2) when
	// connecting to the upstream proxy. This preserves the original client address.
	// 0 = disabled, 1 = v1 (text), 2 = v2 (binary).
	ProxyProtocol int
}

UpstreamProxy configures forwarding through a parent proxy. Both HTTP CONNECT proxies and SOCKS-style chaining are supported via the standard CONNECT tunnel method.

func NewUpstreamProxy

func NewUpstreamProxy(rawURL string) (*UpstreamProxy, error)

NewUpstreamProxy creates an UpstreamProxy from a URL string.

func (*UpstreamProxy) DialConnect

func (up *UpstreamProxy) DialConnect(ctx context.Context, network, addr string, clientAddr net.Addr) (net.Conn, error)

DialConnect establishes a CONNECT tunnel through the upstream proxy to the given target address. This is used for HTTPS proxy chaining where the downstream proxy needs a raw TCP tunnel to the target.

func (*UpstreamProxy) Transport

func (up *UpstreamProxy) Transport(base http.RoundTripper) http.RoundTripper

Transport returns an http.RoundTripper that forwards requests through the upstream proxy. For HTTPS requests, it establishes a CONNECT tunnel.

Directories

Path Synopsis
_examples
acme command
Example: SWG proxy with ACME / Let's Encrypt certificates
Example: SWG proxy with ACME / Let's Encrypt certificates
admin command
Example: Admin API for runtime rule management
Example: Admin API for runtime rule management
allowlist command
Example: Allow-list mode with time-based rules
Example: Allow-list mode with time-based rules
bypass command
Example: Bypass token for debugging
Example: Bypass token for debugging
config command
Example: Using SWG with a configuration file
Example: Using SWG with a configuration file
csv command
Example: Loading blocklist rules from a CSV file
Example: Loading blocklist rules from a CSV file
mtls command
Example: mTLS client certificate authentication
Example: mTLS client certificate authentication
policy command
Example: Policy engine with lifecycle hooks, identity, and body scanning
Example: Policy engine with lifecycle hooks, identity, and body scanning
postgres command
Example: Loading blocklist rules from PostgreSQL using sqlx
Example: Loading blocklist rules from PostgreSQL using sqlx
scanner command
Example: Response body scanning with a pluggable AV scanner
Example: Response body scanning with a pluggable AV scanner

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL