A simple, fast, and configurable DNS server-like microservice that returns geolocation information for IP addresses using MaxMind GeoLite2 and MaxMind City and Country GeoIP2 databases. Supports Country, City, and ASN databases with multi-zone configuration.
Essentially, it's a database access interface via the DNS protocol, designed for high-load systems; it eliminates the need for such projects to implement dependencies for database updates and read interfaces.
- GeoNS - Lightweight DNS Server for IP Geolocation
- Lightweight DNS microservice with minimal dependencies
- Multi-database support - Country, City, and ASN GeoIP2Lite/GeoIP2 databases
- Multi-zone configuration - serve multiple databases on different domain suffixes
- Configurable response fields - extract any field from the GeoIP2Lite/GeoIP2 database structure using dot-notation paths
- Array indexing support - access slice elements like
Subdivisions[0].Names.en - Access Control Lists (ACL) - restrict access to specific CIDR ranges
- Hot reload - reload configuration and database without downtime (
SIGHUP) - Graceful shutdown - clean server termination (
SIGINT/SIGTERM) - IPv4 and IPv6 support - automatic IP version detection
- Flexible TXT record format - customizable separators and field selection
- Go 1.25 or higher
- One or more MaxMind database files GeoIP2 and GeoLite2 City and Country, GeoLite2 ASN Databases
- Clone the repository:
git clone https://github.com/viktor45/geons.git
cd geons- Install dependencies:
go mod download- Download one or more GeoIP2/GeoLite2
.mmdbfiles from MaxMind and place them in the project directory.
Server uses config.yaml as configuration file. The server supports multiple zones, each with its own database and response configuration.
An example data/config.yaml file is available here.
For containerized environments, there is a GEONS_CONFIG variable that specifies the location of the configuration file.
For Docker details, see Dockerfile.
Docker pull example:
docker pull ghcr.io/viktor45/geons:latestDocker run example:
docker run -d \
-p 5300:5300/udp \
-v $(pwd)/data:/data:ro \
ghcr.io/viktor45/geons:latestNOTE: Make sure the
./datafolder containing the configurationconfig.yamland database with mmdb files exists.
For Docker compose file, see compose.yaml
For Podman systemd quadlet, see geons.container.
Podman pull example:
podman pull ghcr.io/viktor45/geons:latestPodman run example:
podman run --name geons --replace --rm --cgroups=split --sdnotify=conmon -d \
-v $(pwd)/data:/data:ro,z,shared \
-p 5300:5300/udp \
ghcr.io/viktor45/geons:latestNOTE: Make sure the
./datafolder containing the configurationconfig.yamland database with mmdb files exists.
server:
port: 5300
bind_address: "127.0.0.1" # "0.0.0.0"
# Whitelist of networks (CIDR) that are allowed to make requests
allowed_clients:
- "127.0.0.0/8"
# - "192.168.0.0/16"
# - "10.0.0.0/8"
# Zones configuration - only configured zones will be loaded
zones:
# Country zone
- name: ".geons"
database:
path: "GeoLite2-Country.mmdb" # "GeoIP2-Country.mmdb"
type: "country"
response:
separator: "|"
fields:
- "Country.IsoCode"
- "Country.Names.en"
# ASN zone
- name: ".asn"
database:
path: "GeoLite2-ASN.mmdb"
type: "asn"
response:
separator: "|"
fields:
- "AutonomousSystemNumber"
- "AutonomousSystemOrganization"
# City zone
- name: ".geocity"
database:
path: "GeoLite2-City.mmdb" # "GeoIP2-City.mmdb"
type: "city"
response:
separator: "|"
fields:
- "City.Names.en"
- "Subdivisions[0].Names.en"
- "Country.IsoCode"
- "Location.Latitude"
- "Location.Longitude"
- "Location.TimeZone"server:
port: 5300
bind_address: "127.0.0.1" # "0.0.0.0"
allowed_clients:
- "127.0.0.0/8"
zones:
- name: ".geo"
database:
path: "GeoLite2-Country.mmdb" # "GeoIP2-Country.mmdb"
type: "country"
response:
separator: "|"
fields:
- "Country.IsoCode"port- UDP port to listen on (required)bind_address- IP address to bind the UDP listener to (default:127.0.0.1)allowed_clients- List of CIDR ranges allowed to make queries
Array of zone configurations. Each zone has:
name- Domain suffix for this zone (e.g.,.geons,.city,.asn)database.path- Path to MaxMind GeoLite2/GeoIP2.mmdbfiledatabase.type- Database type:country,city, orasnresponse.separator- String to separate field values in TXT responseresponse.fields- List of fields to extract from the GeoIP2 database (dot-notation paths)
The fields option supports any field from the corresponding GeoIP2 structure. Use dot-notation paths to access nested fields.
Based on the geoip2.Country structure:
Country.IsoCode- ISO 3166-1 alpha-2 country code (e.g., "US", "DE")Country.Names.en- Country name in EnglishCountry.Names.ru- Country name in RussianCountry.Names.*- Country name in any available languageContinent.Code- Continent code (e.g., "NA", "EU", "AS")Continent.Names.en- Continent name in EnglishRegisteredCountry.IsoCode- Registered country ISO codeRepresentedCountry.IsoCode- Represented country ISO code
Based on the geoip2.City structure:
City.Names.en- City name in EnglishCity.Names.ru- City name in RussianCity.GeoNameId- City GeoName IDSubdivisions[0].IsoCode- First subdivision (state/region) ISO codeSubdivisions[0].Names.en- First subdivision name in EnglishSubdivisions[1].Names.en- Second subdivision name (if available)Country.IsoCode- Country ISO codeCountry.Names.en- Country name in EnglishLocation.Latitude- Latitude coordinateLocation.Longitude- Longitude coordinateLocation.TimeZone- IANA time zone (e.g., "America/Los_Angeles")Location.MetroCode- Metro code (US only)Location.AccuracyRadius- Accuracy radius in kilometersPostal.Code- Postal/ZIP codeContinent.Code- Continent code
Note: Array indexing is supported using
[N]syntax (e.g.,Subdivisions[0]). If the index is out of bounds, an empty string is returned.
Based on the geoip2.ASN structure:
AutonomousSystemNumber- ASN number (e.g.,15169)AutonomousSystemOrganization- Organization name (e.g., "Google LLC")
go run main.goOr build and run:
go build -o geons
./geonsUse dig, host, or any DNS client to query the server.
And, of course, you can retrieve microservice data using any programming language by querying the TXT DNS-record of the appropriate synthetic domain.
# Query country info for Google DNS (8.8.8.8)
dig TXT 8.8.8.8.geons @127.0.0.1 -p 5300
# Expected response:
# ;; ANSWER SECTION:
# 8.8.8.8.geons. 60 IN TXT "US|United States"
# Query for Cloudflare DNS (1.1.1.1)
dig TXT 1.1.1.1.geons @127.0.0.1 -p 5300
# 1.1.1.1.geons. 60 IN TXT "AU|Australia"
# IPv6 query
dig TXT "2001:4860:4860::8888.geons" @127.0.0.1 -p 5300# Query detailed location info
dig TXT 8.8.8.8.geocity @127.0.0.1 -p 5300
# Expected response:
# 8.8.8.8.geocity. 60 IN TXT "Mountain View|California|US|37.4056|-122.0775|America/Los_Angeles"
# Query for Yandex DNS
dig TXT 77.88.8.8.geocity @127.0.0.1 -p 5300
# 77.88.8.8.geocity. 60 IN TXT "Moscow|Moscow|RU|55.7527|37.6175|Europe/Moscow"# Query ASN info for Google DNS
dig TXT 8.8.8.8.asn @127.0.0.1 -p 5300
# Expected response:
# 8.8.8.8.asn. 60 IN TXT "15169|Google LLC"
# Query for Cloudflare DNS
dig TXT 1.1.1.1.asn @127.0.0.1 -p 5300
# 1.1.1.1.asn. 60 IN TXT "13335|Cloudflare, Inc."
# Query for Yandex DNS
dig TXT 77.88.8.8.asn @127.0.0.1 -p 5300
# 77.88.8.8.asn. 60 IN TXT "13238|Yandex LLC"Note: You cannot specify port number for
hostcommand on macos, usedigcommand instead.
host -p 5300 -t txt 8.8.8.8.geons 127.0.0.1
host -p 5300 -t txt 8.8.8.8.geocity 127.0.0.1
host -p 5300 -t txt 8.8.8.8.asn 127.0.0.1Send SIGHUP signal to reload configuration and database without restarting:
# Find server PID
ps aux | grep geons
# Send reload signal
kill -HUP <PID>The server will:
- Re-read
config.yaml - Re-open all configured MaxMind database files
- Apply new settings atomically
This is useful when you update the GeoIP2/GeoLite2 database or change configuration.
Send SIGINT or SIGTERM to stop the server gracefully:
# Using Ctrl+C
# or
kill -INT <PID>
# or
kill -TERM <PID>The server will:
- Stop accepting new connections
- Finish processing current requests
- Close all resources
- Exit cleanly
- DNS query arrives at the server
- Client IP is checked against
allowed_clientswhitelist - Domain name is parsed to determine the zone (by suffix) and extract IP address
- IP is looked up in the zone's configured MaxMind database (Country/City/ASN)
- Configured fields are extracted using reflection and dot-notation paths
- TXT record is returned with field values separated by configured separator
- Each zone operates independently with its own database and configuration
- Zones are matched by domain suffix (e.g.,
.geons,.city,.asn) - Only configured zones are loaded into memory
- Hot reload updates all zones atomically
- Configuration and databases are protected by
sync.RWMutex - Multiple concurrent queries can read configuration simultaneously
- Hot reload acquires write lock to atomically update configuration
- No request blocking during reload
- Lightweight and fast with minimal overhead
- MaxMind MMDB format provides fast memory-mapped lookups
- Concurrent query processing with goroutines
- Low memory footprint
- Geolocation API - Quick IP-to-country/city lookup via DNS
- ASN lookup - Identify the ISP/organization behind an IP
- Network diagnostics - Identify geographic location of IPs
- Content filtering - Block or allow traffic based on country or ASN
- Analytics - Track geographic and ISP distribution of traffic
- Load balancing - Route traffic based on geographic location
- Fraud detection - Detect mismatches between user location and IP
- Check if port is already in use
- Verify
config.yamlsyntax - Ensure at least one zone is configured
- Verify all
.mmdbfiles exist and are readable - Verify
database.typeis set to one of:country,city,asn
- Check if client IP is in
allowed_clientslist - Verify CIDR format is correct (e.g.,
192.168.1.0/24)
- Verify database file matches the configured
database.type(e.g., don't use a Country database withtype: city) - Check field names in
response.fieldsmatch the corresponding GeoIP2 structure - For array fields like
Subdivisions, ensure the index exists (use[0]for safety) - Enable debug logging to see detailed errors
- Ensure you're using the correct structure for your database type
- Check capitalization - field names are case-sensitive (e.g.,
Country.IsoCode, notcountry.isocode) - For map fields like
Names, use the language code as key (e.g.,Country.Names.en)
- Verify the zone name in config matches your query suffix (e.g.,
.geonsfor queries like8.8.8.8.geons) - Check that the zone is properly configured in the
zonesarray - Verify the database file path is correct
This project is licensed under the MIT License - see the LICENSE file for details.
All trademarks are the property of their respective owners.
Database Copyright (c) MaxMind, Inc.
- GeoLite2 End User License Agreement
- GeoIP2 End User License Agreement
- Creative Commons Corporation Attribution-ShareAlike 4.0 International License
- miekg/dns - DNS library for Go
- oschwald/geoip2-golang - MaxMind GeoIP2 Reader
- MaxMind - GeoIP2/GeoLite2 geolocation database
Contributions are welcome! Please feel free to submit a Pull Request.
For issues and questions, please open an issue on GitHub.