leshy 0.3.1

DNS-driven split-tunnel router. Resolves domains, installs kernel routes — only the traffic that needs a VPN goes through a VPN.
Documentation
# Leshy DNS Server Configuration Example

[server]
# Address to listen on for DNS queries
listen_address = "127.0.0.1:15353"

# Default upstream DNS servers (used when no zone matches)
default_upstream = ["8.8.8.8:53", "8.8.4.4:53"]

# What to do when route addition fails:
# - "servfail": Return SERVFAIL to client
# - "fallback": Continue and return DNS response (default, recommended)
route_failure_mode = "fallback"

# Enable automatic config reload when this file changes
# When enabled, Leshy will:
# - Watch this config file for changes
# - Reload configuration automatically
# - Remove routes for deleted zones
# - Start tracking new zones
auto_reload = true

# DNS response cache settings (global defaults)
# cache_size: max entries, 0 = disabled (default: 1000)
# cache_min_ttl: minimum TTL in seconds (default: 60)
# cache_max_ttl: maximum TTL in seconds (default: 3600)
# cache_negative_ttl: TTL for NXDOMAIN / empty responses in seconds (default: 30)
cache_size = 1000
cache_min_ttl = 60
cache_max_ttl = 3600
cache_negative_ttl = 30

# Route aggregation: group DNS-resolved IPs into wider CIDR prefixes
# to reduce kernel routing table size. Value is the prefix length (e.g. 24 = /24).
# Unset or 32 = disabled (each IP gets its own /32 route).
# Recommended: 22 (1024 IPs per aggregate) or 24 (256 IPs per aggregate).
# route_aggregation_prefix = 24

# Example Zone 1: Corporate VPN with device-based routing
# Routes traffic through a VPN tunnel device that may connect/disconnect
[[zones]]
name = "corporate"
route_type = "dev"                               # Route via network device
route_target = "/run/vpn/corporate.dev"          # File containing device name (e.g., "tun0")
domains = ["internal.company.com", "jira.company.com"]
patterns = ["corp"]  # Regex: matches any domain containing "corp"

# Per-zone cache TTL overrides (optional, falls back to [server] defaults)
cache_min_ttl = 30
cache_max_ttl = 600

# Rich dns_servers format — per-server cache TTL overrides:
[[zones.dns_servers]]
address = "10.44.2.2:53"
cache_min_ttl = 10
cache_max_ttl = 300

[[zones.dns_servers]]
address = "10.44.2.4:53"
# inherits zone → global defaults

# Example Zone 2: EU VPN with static gateway
# Routes traffic through a fixed gateway (always-on VPN)
[[zones]]
name = "eu"
dns_servers = []  # Empty = use default_upstream
route_type = "via"
route_target = "192.168.169.1"  # Static VPN gateway IP
domains = ["chatgpt.com", "github.com"]
patterns = ["openai", "anthropic"]

# Example Zone 3: Office network
# Simple dns_servers format still works:
[[zones]]
name = "office"
dns_servers = ["192.168.1.1:53"]
route_type = "via"
route_target = "192.168.1.254"
domains = ["office.local", "printer.local"]
patterns = []

# Example Zone 4: Exclusive VPN catch-all
# Routes ALL traffic through VPN except excluded domains/patterns.
# Zone mode:
#   "inclusive" (default) — only match listed domains/patterns
#   "exclusive" — match everything EXCEPT listed domains/patterns
# Patterns are raw regex: \.ru$, ^corp, \.internal\.
[[zones]]
name = "vpn-catchall"
mode = "exclusive"
dns_servers = []
route_type = "via"
route_target = "10.8.0.1"
# These domains/patterns are EXCLUDED from the VPN (accessed directly):
domains = ["local.network"]
patterns = ['\.ru$', '\.local$']