caddy-sip-guardian/geoip.go

197 lines
5 KiB
Go
Raw Normal View History

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
}