mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
153 lines
3.5 KiB
Go
153 lines
3.5 KiB
Go
// Package geolocation provides IP-to-country lookups using MaxMind GeoLite2 databases.
|
|
package geolocation
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/oschwald/maxminddb-golang"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
// EnvDisable disables geolocation lookups entirely when set to a truthy value.
|
|
EnvDisable = "NB_PROXY_DISABLE_GEOLOCATION"
|
|
|
|
mmdbGlob = "GeoLite2-City_*.mmdb"
|
|
)
|
|
|
|
type record struct {
|
|
Country struct {
|
|
ISOCode string `maxminddb:"iso_code"`
|
|
} `maxminddb:"country"`
|
|
City struct {
|
|
Names struct {
|
|
En string `maxminddb:"en"`
|
|
} `maxminddb:"names"`
|
|
} `maxminddb:"city"`
|
|
Subdivisions []struct {
|
|
ISOCode string `maxminddb:"iso_code"`
|
|
Names struct {
|
|
En string `maxminddb:"en"`
|
|
} `maxminddb:"names"`
|
|
} `maxminddb:"subdivisions"`
|
|
}
|
|
|
|
// Result holds the outcome of a geo lookup.
|
|
type Result struct {
|
|
CountryCode string
|
|
CityName string
|
|
SubdivisionCode string
|
|
SubdivisionName string
|
|
}
|
|
|
|
// Lookup provides IP geolocation lookups.
|
|
type Lookup struct {
|
|
mu sync.RWMutex
|
|
db *maxminddb.Reader
|
|
logger *log.Logger
|
|
}
|
|
|
|
// NewLookup opens or downloads the GeoLite2-City MMDB in dataDir.
|
|
// Returns nil without error if geolocation is disabled via environment
|
|
// variable, no data directory is configured, or the download fails
|
|
// (graceful degradation: country restrictions will deny all requests).
|
|
func NewLookup(logger *log.Logger, dataDir string) (*Lookup, error) {
|
|
if isDisabledByEnv(logger) {
|
|
logger.Info("geolocation disabled via environment variable")
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
|
|
if dataDir == "" {
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
|
|
mmdbPath, err := ensureMMDB(logger, dataDir)
|
|
if err != nil {
|
|
logger.Warnf("geolocation database unavailable: %v", err)
|
|
logger.Warn("country-based access restrictions will deny all requests until a database is available")
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
|
|
db, err := maxminddb.Open(mmdbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open GeoLite2 database %s: %w", mmdbPath, err)
|
|
}
|
|
|
|
logger.Infof("geolocation database loaded from %s", mmdbPath)
|
|
return &Lookup{db: db, logger: logger}, nil
|
|
}
|
|
|
|
// LookupAddr returns the country ISO code and city name for the given IP.
|
|
// Returns an empty Result if the database is nil or the lookup fails.
|
|
func (l *Lookup) LookupAddr(addr netip.Addr) Result {
|
|
if l == nil {
|
|
return Result{}
|
|
}
|
|
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
if l.db == nil {
|
|
return Result{}
|
|
}
|
|
|
|
addr = addr.Unmap()
|
|
|
|
var rec record
|
|
if err := l.db.Lookup(addr.AsSlice(), &rec); err != nil {
|
|
l.logger.Debugf("geolocation lookup %s: %v", addr, err)
|
|
return Result{}
|
|
}
|
|
r := Result{
|
|
CountryCode: rec.Country.ISOCode,
|
|
CityName: rec.City.Names.En,
|
|
}
|
|
if len(rec.Subdivisions) > 0 {
|
|
r.SubdivisionCode = rec.Subdivisions[0].ISOCode
|
|
r.SubdivisionName = rec.Subdivisions[0].Names.En
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Available reports whether the lookup has a loaded database.
|
|
func (l *Lookup) Available() bool {
|
|
if l == nil {
|
|
return false
|
|
}
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return l.db != nil
|
|
}
|
|
|
|
// Close releases the database resources.
|
|
func (l *Lookup) Close() error {
|
|
if l == nil {
|
|
return nil
|
|
}
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if l.db != nil {
|
|
err := l.db.Close()
|
|
l.db = nil
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isDisabledByEnv(logger *log.Logger) bool {
|
|
val := os.Getenv(EnvDisable)
|
|
if val == "" {
|
|
return false
|
|
}
|
|
disabled, err := strconv.ParseBool(val)
|
|
if err != nil {
|
|
logger.Warnf("parse %s=%q: %v", EnvDisable, val, err)
|
|
return false
|
|
}
|
|
return disabled
|
|
}
|