Skip to content

Viktor45/geons

Repository files navigation

GeoNS - Lightweight DNS Server for IP Geolocation

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.

Features

  • 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

Requirements

  • Go 1.25 or higher
  • One or more MaxMind database files GeoIP2 and GeoLite2 City and Country, GeoLite2 ASN Databases

Installation

  1. Clone the repository:
git clone https://github.com/viktor45/geons.git
cd geons
  1. Install dependencies:
go mod download
  1. Download one or more GeoIP2/GeoLite2 .mmdb files from MaxMind and place them in the project directory.

Configuration

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.

Containerized environments

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:latest

Docker run example:

docker run -d \
  -p 5300:5300/udp \
  -v $(pwd)/data:/data:ro \
  ghcr.io/viktor45/geons:latest

NOTE: Make sure the ./data folder containing the configuration config.yaml and 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:latest

Podman 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:latest

NOTE: Make sure the ./data folder containing the configuration config.yaml and database with mmdb files exists.

Example Configuration with All Three Zones

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"

Minimal Configuration (Single Zone)

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"

Configuration Options

server

  • 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

zones

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 .mmdb file
  • database.type - Database type: country, city, or asn
  • response.separator - String to separate field values in TXT response
  • response.fields - List of fields to extract from the GeoIP2 database (dot-notation paths)

Available Fields

The fields option supports any field from the corresponding GeoIP2 structure. Use dot-notation paths to access nested fields.

GeoLite2/GeoIP2-Country (type: country)

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 English
  • Country.Names.ru - Country name in Russian
  • Country.Names.* - Country name in any available language
  • Continent.Code - Continent code (e.g., "NA", "EU", "AS")
  • Continent.Names.en - Continent name in English
  • RegisteredCountry.IsoCode - Registered country ISO code
  • RepresentedCountry.IsoCode - Represented country ISO code

GeoLite2/GeoIP2-City (type: city)

Based on the geoip2.City structure:

  • City.Names.en - City name in English
  • City.Names.ru - City name in Russian
  • City.GeoNameId - City GeoName ID
  • Subdivisions[0].IsoCode - First subdivision (state/region) ISO code
  • Subdivisions[0].Names.en - First subdivision name in English
  • Subdivisions[1].Names.en - Second subdivision name (if available)
  • Country.IsoCode - Country ISO code
  • Country.Names.en - Country name in English
  • Location.Latitude - Latitude coordinate
  • Location.Longitude - Longitude coordinate
  • Location.TimeZone - IANA time zone (e.g., "America/Los_Angeles")
  • Location.MetroCode - Metro code (US only)
  • Location.AccuracyRadius - Accuracy radius in kilometers
  • Postal.Code - Postal/ZIP code
  • Continent.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.

GeoLite2-ASN (type: asn)

Based on the geoip2.ASN structure:

  • AutonomousSystemNumber - ASN number (e.g., 15169)
  • AutonomousSystemOrganization - Organization name (e.g., "Google LLC")

Usage

Running the Server

go run main.go

Or build and run:

go build -o geons
./geons

Querying the Server

Use 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.

Country zone queries (.geons)

# 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

City zone queries (.geocity)

# 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"

ASN zone queries (.asn)

# 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"

Using host Command

Note: You cannot specify port number for host command on macos, use dig command 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.1

Hot Reload Configuration

Send 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:

  1. Re-read config.yaml
  2. Re-open all configured MaxMind database files
  3. Apply new settings atomically

This is useful when you update the GeoIP2/GeoLite2 database or change configuration.

Graceful Shutdown

Send SIGINT or SIGTERM to stop the server gracefully:

# Using Ctrl+C
# or
kill -INT <PID>
# or
kill -TERM <PID>

The server will:

  1. Stop accepting new connections
  2. Finish processing current requests
  3. Close all resources
  4. Exit cleanly

Architecture

Request Flow

  1. DNS query arrives at the server
  2. Client IP is checked against allowed_clients whitelist
  3. Domain name is parsed to determine the zone (by suffix) and extract IP address
  4. IP is looked up in the zone's configured MaxMind database (Country/City/ASN)
  5. Configured fields are extracted using reflection and dot-notation paths
  6. TXT record is returned with field values separated by configured separator

Multi-Zone Support

  • 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

Thread Safety

  • 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

Performance

  • Lightweight and fast with minimal overhead
  • MaxMind MMDB format provides fast memory-mapped lookups
  • Concurrent query processing with goroutines
  • Low memory footprint

Example Use Cases

  • 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

Troubleshooting

Server won't start

  • Check if port is already in use
  • Verify config.yaml syntax
  • Ensure at least one zone is configured
  • Verify all .mmdb files exist and are readable
  • Verify database.type is set to one of: country, city, asn

Access denied errors

  • Check if client IP is in allowed_clients list
  • Verify CIDR format is correct (e.g., 192.168.1.0/24)

Empty or incorrect responses

  • Verify database file matches the configured database.type (e.g., don't use a Country database with type: city)
  • Check field names in response.fields match the corresponding GeoIP2 structure
  • For array fields like Subdivisions, ensure the index exists (use [0] for safety)
  • Enable debug logging to see detailed errors

Field path not found

  • Ensure you're using the correct structure for your database type
  • Check capitalization - field names are case-sensitive (e.g., Country.IsoCode, not country.isocode)
  • For map fields like Names, use the language code as key (e.g., Country.Names.en)

Zone not responding

  • Verify the zone name in config matches your query suffix (e.g., .geons for queries like 8.8.8.8.geons)
  • Check that the zone is properly configured in the zones array
  • Verify the database file path is correct

License

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.

Acknowledgments

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For issues and questions, please open an issue on GitHub.