Major features: - Extension enumeration detection with 3 detection algorithms: - Max unique extensions threshold (default: 20 in 5 min) - Sequential pattern detection (e.g., 100,101,102...) - Rapid-fire detection (many extensions in short window) - Prometheus metrics for all SIP Guardian operations - SQLite persistent storage for bans and attack history - Webhook notifications for ban/unban/suspicious events - GeoIP-based country blocking with continent shortcuts - Per-method rate limiting with token bucket algorithm Bug fixes: - Fix whitelist count always reporting zero in stats - Fix whitelisted connections metric never incrementing - Fix Caddyfile config not being applied to shared guardian New files: - enumeration.go: Extension enumeration detector - enumeration_test.go: 14 comprehensive unit tests - metrics.go: Prometheus metrics handler - storage.go: SQLite persistence layer - webhooks.go: Webhook notification system - geoip.go: MaxMind GeoIP integration - ratelimit.go: Per-method rate limiting Testing: - sandbox/ contains complete Docker Compose test environment - All 14 enumeration tests pass
197 lines
5.0 KiB
Go
197 lines
5.0 KiB
Go
package sipguardian
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
|
|
"github.com/oschwald/maxminddb-golang"
|
|
)
|
|
|
|
// GeoIPLookup provides IP to country lookup using MaxMind databases
|
|
type GeoIPLookup struct {
|
|
db *maxminddb.Reader
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// GeoIPRecord represents the data we extract from MaxMind database
|
|
type GeoIPRecord struct {
|
|
Country struct {
|
|
ISOCode string `maxminddb:"iso_code"`
|
|
Names map[string]string `maxminddb:"names"`
|
|
} `maxminddb:"country"`
|
|
City struct {
|
|
Names map[string]string `maxminddb:"names"`
|
|
} `maxminddb:"city"`
|
|
Continent struct {
|
|
Code string `maxminddb:"code"`
|
|
Names map[string]string `maxminddb:"names"`
|
|
} `maxminddb:"continent"`
|
|
}
|
|
|
|
// NewGeoIPLookup creates a new GeoIP lookup from a MaxMind database file
|
|
// Supports both GeoLite2-Country and GeoLite2-City databases
|
|
func NewGeoIPLookup(path string) (*GeoIPLookup, error) {
|
|
db, err := maxminddb.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open GeoIP database: %w", err)
|
|
}
|
|
|
|
return &GeoIPLookup{
|
|
db: db,
|
|
}, nil
|
|
}
|
|
|
|
// LookupCountry returns the ISO country code for an IP address
|
|
func (g *GeoIPLookup) LookupCountry(ipStr string) (string, error) {
|
|
g.mu.RLock()
|
|
defer g.mu.RUnlock()
|
|
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
return "", fmt.Errorf("invalid IP address: %s", ipStr)
|
|
}
|
|
|
|
var record GeoIPRecord
|
|
err := g.db.Lookup(ip, &record)
|
|
if err != nil {
|
|
return "", fmt.Errorf("lookup failed: %w", err)
|
|
}
|
|
|
|
return record.Country.ISOCode, nil
|
|
}
|
|
|
|
// LookupFull returns full GeoIP information for an IP address
|
|
func (g *GeoIPLookup) LookupFull(ipStr string) (*GeoIPRecord, error) {
|
|
g.mu.RLock()
|
|
defer g.mu.RUnlock()
|
|
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("invalid IP address: %s", ipStr)
|
|
}
|
|
|
|
var record GeoIPRecord
|
|
err := g.db.Lookup(ip, &record)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lookup failed: %w", err)
|
|
}
|
|
|
|
return &record, nil
|
|
}
|
|
|
|
// Close closes the database
|
|
func (g *GeoIPLookup) Close() error {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
return g.db.Close()
|
|
}
|
|
|
|
// CountryName returns the English name for a country code
|
|
func (g *GeoIPLookup) CountryName(ipStr string) (string, error) {
|
|
record, err := g.LookupFull(ipStr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if name, ok := record.Country.Names["en"]; ok {
|
|
return name, nil
|
|
}
|
|
|
|
return record.Country.ISOCode, nil
|
|
}
|
|
|
|
// IsPrivate checks if an IP is in a private/reserved range
|
|
func IsPrivateIP(ipStr string) bool {
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
// Private IPv4 ranges
|
|
privateBlocks := []string{
|
|
"10.0.0.0/8",
|
|
"172.16.0.0/12",
|
|
"192.168.0.0/16",
|
|
"127.0.0.0/8",
|
|
"169.254.0.0/16", // Link-local
|
|
"100.64.0.0/10", // Carrier-grade NAT
|
|
}
|
|
|
|
// Private IPv6 ranges
|
|
if ip.To4() == nil {
|
|
privateBlocks = append(privateBlocks,
|
|
"::1/128", // Loopback
|
|
"fc00::/7", // Unique local
|
|
"fe80::/10", // Link-local
|
|
)
|
|
}
|
|
|
|
for _, block := range privateBlocks {
|
|
_, cidr, err := net.ParseCIDR(block)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if cidr.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Continent codes for grouping countries
|
|
var ContinentCountries = map[string][]string{
|
|
"AF": { // Africa
|
|
"AO", "BF", "BI", "BJ", "BW", "CD", "CF", "CG", "CI", "CM",
|
|
"CV", "DJ", "DZ", "EG", "EH", "ER", "ET", "GA", "GH", "GM",
|
|
"GN", "GQ", "GW", "KE", "KM", "LR", "LS", "LY", "MA", "MG",
|
|
"ML", "MR", "MU", "MW", "MZ", "NA", "NE", "NG", "RE", "RW",
|
|
"SC", "SD", "SH", "SL", "SN", "SO", "SS", "ST", "SZ", "TD",
|
|
"TG", "TN", "TZ", "UG", "YT", "ZA", "ZM", "ZW",
|
|
},
|
|
"AS": { // Asia
|
|
"AE", "AF", "AM", "AZ", "BD", "BH", "BN", "BT", "CN", "CY",
|
|
"GE", "HK", "ID", "IL", "IN", "IQ", "IR", "JO", "JP", "KG",
|
|
"KH", "KP", "KR", "KW", "KZ", "LA", "LB", "LK", "MM", "MN",
|
|
"MO", "MV", "MY", "NP", "OM", "PH", "PK", "PS", "QA", "SA",
|
|
"SG", "SY", "TH", "TJ", "TL", "TM", "TR", "TW", "UZ", "VN",
|
|
"YE",
|
|
},
|
|
"EU": { // Europe
|
|
"AD", "AL", "AT", "AX", "BA", "BE", "BG", "BY", "CH", "CZ",
|
|
"DE", "DK", "EE", "ES", "FI", "FO", "FR", "GB", "GG", "GI",
|
|
"GR", "HR", "HU", "IE", "IM", "IS", "IT", "JE", "LI", "LT",
|
|
"LU", "LV", "MC", "MD", "ME", "MK", "MT", "NL", "NO", "PL",
|
|
"PT", "RO", "RS", "RU", "SE", "SI", "SJ", "SK", "SM", "UA",
|
|
"VA", "XK",
|
|
},
|
|
"NA": { // North America
|
|
"AG", "AI", "AW", "BB", "BL", "BM", "BQ", "BS", "BZ", "CA",
|
|
"CR", "CU", "CW", "DM", "DO", "GD", "GL", "GP", "GT", "HN",
|
|
"HT", "JM", "KN", "KY", "LC", "MF", "MQ", "MS", "MX", "NI",
|
|
"PA", "PM", "PR", "SV", "SX", "TC", "TT", "US", "VC", "VG",
|
|
"VI",
|
|
},
|
|
"SA": { // South America
|
|
"AR", "BO", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PE",
|
|
"PY", "SR", "UY", "VE",
|
|
},
|
|
"OC": { // Oceania
|
|
"AS", "AU", "CK", "FJ", "FM", "GU", "KI", "MH", "MP", "NC",
|
|
"NF", "NR", "NU", "NZ", "PF", "PG", "PN", "PW", "SB", "TK",
|
|
"TO", "TV", "VU", "WF", "WS",
|
|
},
|
|
"AN": { // Antarctica
|
|
"AQ", "GS", "HM", "TF",
|
|
},
|
|
}
|
|
|
|
// ExpandContinentCode expands a continent code to its country codes
|
|
func ExpandContinentCode(code string) []string {
|
|
if countries, ok := ContinentCountries[code]; ok {
|
|
return countries
|
|
}
|
|
return nil
|
|
}
|