Add extension enumeration detection and comprehensive SIP protection
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
This commit is contained in:
parent
0b0fb53c9c
commit
c73fa9d3d1
19 changed files with 4630 additions and 544 deletions
196
geoip.go
Normal file
196
geoip.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue