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 }