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:
parent
46a47ce2c6
commit
5cf34eb3c0
8 changed files with 2383 additions and 11 deletions
116
sipguardian.go
116
sipguardian.go
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue