GeoIP and GeoSite Databases
sing-box supports two legacy geographic database formats for IP and domain-based routing: GeoIP (MaxMind MMDB format) and GeoSite (custom binary format). These are being replaced by the newer rule set system but remain supported for backward compatibility.
Source: common/geoip/, common/geosite/, option/route.go
GeoIP (MaxMind MMDB)
Overview
GeoIP uses a modified MaxMind MMDB database with the database type identifier sing-geoip (not the standard MaxMind GeoLite2-Country). This is a purpose-built database that maps IP addresses directly to country codes.
Reader Implementation
type Reader struct {
reader *maxminddb.Reader
}
func Open(path string) (*Reader, []string, error) {
database, err := maxminddb.Open(path)
if err != nil {
return nil, nil, err
}
if database.Metadata.DatabaseType != "sing-geoip" {
database.Close()
return nil, nil, E.New("incorrect database type, expected sing-geoip, got ",
database.Metadata.DatabaseType)
}
return &Reader{database}, database.Metadata.Languages, nil
}Key points:
- Database type validation: Only accepts databases with type
sing-geoip, rejecting standard MaxMind databases - Language codes: Returns the list of available country codes via
database.Metadata.Languages - Lookup: Maps a
netip.Addrdirectly to a country code string; returns"unknown"if not found
func (r *Reader) Lookup(addr netip.Addr) string {
var code string
_ = r.reader.Lookup(addr.AsSlice(), &code)
if code != "" {
return code
}
return "unknown"
}MMDB Format
The MMDB format is a binary trie structure designed for efficient IP prefix lookup. The maxminddb-golang library handles parsing. For the sing-geoip variant:
- The data section stores simple string values (country codes) rather than nested structures
- The metadata section uses
Languagesto store the list of available country codes - IPv4 and IPv6 addresses are both supported through the trie structure
Configuration
{
"route": {
"geoip": {
"path": "geoip.db",
"download_url": "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db",
"download_detour": "proxy"
}
}
}GeoSite (Custom Binary Format)
Overview
GeoSite is a custom binary database format that maps category codes (like google, category-ads-all) to lists of domain rules. Each domain rule has a type (exact, suffix, keyword, regex) and a value.
File Structure
[uint8: version (0)]
[uvarint: entry_count]
[uvarint: code_length] [bytes: code_string]
[uvarint: byte_offset]
[uvarint: item_count]
...
[domain items data...]The file has two sections:
- Metadata section: An index mapping category codes to byte offsets and item counts
- Data section: The actual domain items, stored contiguously
Item Types
type ItemType = uint8
const (
RuleTypeDomain ItemType = 0 // Exact domain match
RuleTypeDomainSuffix ItemType = 1 // Domain suffix match (e.g., ".google.com")
RuleTypeDomainKeyword ItemType = 2 // Domain contains keyword
RuleTypeDomainRegex ItemType = 3 // Regular expression match
)
type Item struct {
Type ItemType
Value string
}Reader Implementation
type Reader struct {
access sync.Mutex
reader io.ReadSeeker
bufferedReader *bufio.Reader
metadataIndex int64 // byte offset where data section starts
domainIndex map[string]int // code -> byte offset in data section
domainLength map[string]int // code -> number of items
}The reader operates in two phases:
readMetadata(): Reads the entire index on open, building maps from code to offset/lengthRead(code): Seeks to the code's offset in the data section and reads items on demand
func (r *Reader) Read(code string) ([]Item, error) {
index, exists := r.domainIndex[code]
if !exists {
return nil, E.New("code ", code, " not exists!")
}
_, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart)
// ... read items
}Each item in the data section is stored as:
[uint8: item_type]
[uvarint: value_length] [bytes: value_string]Writer Implementation
The writer builds the data section in memory first to calculate offsets:
func Write(writer varbin.Writer, domains map[string][]Item) error {
// 1. Sort codes alphabetically
// 2. Write all items to a buffer, recording byte offsets per code
// 3. Write version byte (0)
// 4. Write entry count
// 5. For each code: write code string, byte offset, item count
// 6. Write the buffered item data
}Compiling to Rules
GeoSite items are compiled into sing-box rule options:
func Compile(code []Item) option.DefaultRule {
// Maps each ItemType to the corresponding rule field:
// RuleTypeDomain -> rule.Domain
// RuleTypeDomainSuffix -> rule.DomainSuffix
// RuleTypeDomainKeyword -> rule.DomainKeyword
// RuleTypeDomainRegex -> rule.DomainRegex
}Multiple compiled rules can be merged with Merge(), which concatenates all domain lists.
Configuration
{
"route": {
"geosite": {
"path": "geosite.db",
"download_url": "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db",
"download_detour": "proxy"
}
}
}How They Are Used in Rules
In routing rules, GeoIP and GeoSite references are used like:
{
"route": {
"rules": [
{
"geoip": ["cn", "private"],
"outbound": "direct"
},
{
"geosite": ["category-ads-all"],
"outbound": "block"
}
]
}
}The router loads the database on startup, reads the requested codes, and compiles them into in-memory rule matchers. GeoIP codes produce IP CIDR rules; GeoSite codes produce domain/keyword/regex rules.
Migration to Rule Sets
GeoIP and GeoSite are considered legacy. The recommended migration path is to use SRS rule sets, which:
- Support more rule types (ports, processes, network conditions)
- Have better update mechanisms (HTTP with ETag, file watching)
- Allow inline definitions without external files
- Use a more compact binary format with zlib compression
Reimplementation Notes
- GeoIP: Depends on
github.com/oschwald/maxminddb-golangfor MMDB parsing. The MMDB format is well-documented and has implementations in many languages. The only sing-box-specific aspect is thesing-geoipdatabase type check - GeoSite: Uses a custom binary format with uvarint encoding. The format is straightforward to implement: read the index, seek to the right offset, read items
- Thread safety: The GeoSite reader uses a mutex because it shares a seekable reader and buffered reader across calls. A reimplementation could use per-call readers if the data is small enough to cache in memory
- Byte offset tracking: The
readCounterwrapper tracks how many bytes the metadata section consumed, accounting for buffered reader lookahead viareader.Buffered()