Add DNS-aware whitelisting feature

Support for whitelisting SIP trunks and providers by hostname or SRV
record with automatic IP resolution and periodic refresh.

Features:
- Hostname resolution via A/AAAA records
- SRV record resolution (e.g., _sip._udp.provider.com)
- Configurable refresh interval (default 5m)
- Stale entry handling when DNS fails
- Admin API endpoints for DNS whitelist management
- Caddyfile directives: whitelist_hosts, whitelist_srv, dns_refresh

This allows whitelisting by provider name rather than tracking
constantly-changing IP addresses.
This commit is contained in:
Ryan Malloy 2025-12-08 00:46:43 -07:00
parent 46a47ce2c6
commit 5cf34eb3c0
8 changed files with 2383 additions and 11 deletions

View file

@ -41,6 +41,11 @@ type SIPGuardian struct {
BanTime caddy.Duration `json:"ban_time,omitempty"`
WhitelistCIDR []string `json:"whitelist_cidr,omitempty"`
// DNS-aware whitelist configuration
WhitelistHosts []string `json:"whitelist_hosts,omitempty"` // Hostnames to resolve (A/AAAA)
WhitelistSRV []string `json:"whitelist_srv,omitempty"` // SRV records to resolve
DNSRefresh caddy.Duration `json:"dns_refresh,omitempty"` // DNS refresh interval (default: 5m)
// Webhook configuration
Webhooks []WebhookConfig `json:"webhooks,omitempty"`
@ -48,7 +53,7 @@ type SIPGuardian struct {
StoragePath string `json:"storage_path,omitempty"`
// GeoIP configuration
GeoIPPath string `json:"geoip_path,omitempty"`
GeoIPPath string `json:"geoip_path,omitempty"`
BlockedCountries []string `json:"blocked_countries,omitempty"`
AllowedCountries []string `json:"allowed_countries,omitempty"`
@ -63,6 +68,7 @@ type SIPGuardian struct {
bannedIPs map[string]*BanEntry
failureCounts map[string]*failureTracker
whitelistNets []*net.IPNet
dnsWhitelist *DNSWhitelist
mu sync.RWMutex
storage *Storage
geoIP *GeoIPLookup
@ -155,6 +161,34 @@ func (g *SIPGuardian) Provision(ctx caddy.Context) error {
}
}
// Initialize DNS whitelist if configured
if len(g.WhitelistHosts) > 0 || len(g.WhitelistSRV) > 0 {
refreshInterval := 5 * time.Minute
if g.DNSRefresh > 0 {
refreshInterval = time.Duration(g.DNSRefresh)
}
g.dnsWhitelist = NewDNSWhitelist(DNSWhitelistConfig{
Hostnames: g.WhitelistHosts,
SRVRecords: g.WhitelistSRV,
RefreshInterval: refreshInterval,
AllowStale: true,
ResolveTimeout: 10 * time.Second,
}, g.logger)
if err := g.dnsWhitelist.Start(); err != nil {
g.logger.Warn("Failed to initialize DNS whitelist",
zap.Error(err),
)
} else {
g.logger.Info("DNS whitelist initialized",
zap.Int("hostnames", len(g.WhitelistHosts)),
zap.Int("srv_records", len(g.WhitelistSRV)),
zap.Duration("refresh_interval", refreshInterval),
)
}
}
// Initialize enumeration detection with config if specified
if g.Enumeration != nil {
SetEnumerationConfig(*g.Enumeration)
@ -216,12 +250,14 @@ func (g *SIPGuardian) loadBansFromStorage() error {
return nil
}
// IsWhitelisted checks if an IP is in the whitelist
// IsWhitelisted checks if an IP is in the whitelist (CIDR or DNS-based)
func (g *SIPGuardian) IsWhitelisted(ip string) bool {
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return false
}
// Check CIDR-based whitelist
for _, network := range g.whitelistNets {
if network.Contains(parsedIP) {
if enableMetrics {
@ -230,6 +266,19 @@ func (g *SIPGuardian) IsWhitelisted(ip string) bool {
return true
}
}
// Check DNS-based whitelist
if g.dnsWhitelist != nil && g.dnsWhitelist.Contains(ip) {
if enableMetrics {
RecordWhitelistedConnection()
}
g.logger.Debug("IP whitelisted via DNS",
zap.String("ip", ip),
zap.String("source", g.dnsWhitelist.GetSource(ip).Source),
)
return true
}
return false
}
@ -448,10 +497,34 @@ func (g *SIPGuardian) GetStats() map[string]interface{} {
}
}
return map[string]interface{}{
"active_bans": activeBans,
"tracked_failures": len(g.failureCounts),
"whitelist_count": len(g.whitelistNets),
stats := map[string]interface{}{
"active_bans": activeBans,
"tracked_failures": len(g.failureCounts),
"whitelist_cidr": len(g.whitelistNets),
"whitelist_hosts": len(g.WhitelistHosts),
"whitelist_srv": len(g.WhitelistSRV),
}
// Add DNS whitelist stats if available
if g.dnsWhitelist != nil {
stats["dns_whitelist"] = g.dnsWhitelist.Stats()
}
return stats
}
// GetDNSWhitelistEntries returns all resolved DNS whitelist entries
func (g *SIPGuardian) GetDNSWhitelistEntries() []ResolvedEntry {
if g.dnsWhitelist == nil {
return nil
}
return g.dnsWhitelist.GetResolvedIPs()
}
// RefreshDNSWhitelist forces an immediate refresh of DNS whitelist entries
func (g *SIPGuardian) RefreshDNSWhitelist() {
if g.dnsWhitelist != nil {
g.dnsWhitelist.ForceRefresh()
}
}
@ -500,8 +573,15 @@ func (g *SIPGuardian) cleanup() {
// max_failures 5
// find_time 10m
// ban_time 1h
//
// # IP/CIDR whitelist (static)
// whitelist 10.0.0.0/8 192.168.0.0/16
//
// # DNS-aware whitelist (dynamic, auto-refreshed)
// whitelist_hosts pbx.example.com trunk.provider.com
// whitelist_srv _sip._udp.provider.com _sip._tcp.carrier.net
// dns_refresh 5m # How often to refresh DNS (default: 5m)
//
// # Persistent storage
// storage /var/lib/sip-guardian/guardian.db
//
@ -552,10 +632,34 @@ func (g *SIPGuardian) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
g.BanTime = caddy.Duration(dur)
case "whitelist":
// Legacy: CIDR-only whitelist
for d.NextArg() {
g.WhitelistCIDR = append(g.WhitelistCIDR, d.Val())
}
case "whitelist_hosts":
// DNS A/AAAA record whitelist (hostnames resolved to IPs)
for d.NextArg() {
g.WhitelistHosts = append(g.WhitelistHosts, d.Val())
}
case "whitelist_srv":
// DNS SRV record whitelist (e.g., _sip._udp.provider.com)
for d.NextArg() {
g.WhitelistSRV = append(g.WhitelistSRV, d.Val())
}
case "dns_refresh":
// Interval for refreshing DNS-based whitelist entries
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("invalid dns_refresh: %v", err)
}
g.DNSRefresh = caddy.Duration(dur)
case "storage":
if !d.NextArg() {
return d.ArgErr()