Name #
whoisd — One Ring multi-RIR WHOIS server.
Synopsis #
whoisd [-c file | --config file]
[-s | --stats] [-r | --reload]
[-v | --version] [-h | --help]
Description #
whoisd is a multi-core WHOIS server (RFC 3912) that downloads, merges, and serves the bulk RPSL dumps of all five Regional Internet Registries (RIPE, ARIN, APNIC, LACNIC, AFRINIC) from a single TCP endpoint. One query reaches every registry regardless of which one owns the address space.
The server keeps the merged database in memory, indexed by primary key,
NIC handle, domain name, and a 128-bit binary radix trie of IP prefixes
(longest-prefix-match returns the most specific allocation). Each upstream
is polled on a configurable cadence with conditional
If-Modified-Since requests, and the in-memory store is rebuilt
only when at least one source actually changed. A UTC time-of-day anchor
guarantees at least one fresh attempt around the RIRs' daily publish
window.
A live stats subsystem tracks every in-flight connection (source, query, state, duration, bytes transferred) and dumps a JSON snapshot to disk on a configurable cadence (sub-second supported). An optional embedded HTTP listener serves both a single-file dashboard and the JSON for external consumers.
Options #
- -c file, --config file
- Path to the YAML configuration file. Default:
/etc/whoisd.yaml. - -s, --stats
- Fetch the live status JSON from the running server's stats HTTP
listener (as configured by
stats.http_bind) and print a pretty, multi-column summary to standard output. The endpoint URL is discovered from the same configuration file. Exits without starting the server. - -r, --reload
- Signal the running whoisd to reload its configuration.
Looks up the pid file (see Files) and sends
SIGHUP. Exits 0 on a successful signal delivery, 1 if no pid file is found or the signal cannot be delivered. Does not wait for the reload to complete on the daemon side — observe the result via the daemon's log output orwhoisd --stats. - -v, --version
- Print the binary's version string and exit. The string uses a
two-number (MAJOR.MINOR) scheme and can be stamped at build
time via
go build -ldflags "-X main.Version=1.2". - -h, --help
- Show the built-in usage summary and exit.
Configuration file #
The configuration file is YAML. All fields have sensible defaults; an
empty file is a valid configuration but you must enable at least one
source under dbase.sources. The top-level keys are
server, dbase, acl,
rate_limit, stats, and log.
server #
- bind
- TCP listen address for the WHOIS protocol, in
host:portform. Default:0.0.0.0:43. Binding to port 43 requires either root orsetcap cap_net_bind_service=+ep ./whoisd. - read_timeout
- Maximum time to wait for the client's query line.
Default:
5s. - write_timeout
- Maximum time spent writing the response.
Default:
30s. - max_concurrent
- Upper bound on concurrent in-flight queries. Excess clients receive
%% server busy, try again. Default:1024. - max_query_bytes
- Caps the query line length to defeat abusive clients.
Default:
512. - pid_file
- Path the daemon writes its pid to on startup, and that
whoisd --reloadreads to locate the running instance. The parent directory is created (0755) if missing. Set to the empty string to disable pid-file management entirely (e.g. when running under a supervisor that owns the pid via other means);--reloadthen has nothing to signal. Default:/run/whoisd.pid.
dbase #
- cache_dir
- Local directory where downloaded
*.gzRIR dumps are cached. One subdirectory per source. Default:./var/dbase. - poll_interval
- How often to ask each upstream "has anything changed?". Polls are
cheap because of conditional GETs.
Default:
1h. Minimum:30s. - daily_at
- UTC time-of-day (
HH:MM) at which to schedule one guaranteed refresh attempt per day. Picks up the RIRs' daily publishes promptly even ifpoll_intervalis long. Set to the empty string ("") to disable the anchor. Default:02:00. - retry_on_failure
- Backoff applied between attempts after a refresh fails.
Default:
5m. - sources
- List of upstream databases to fetch and merge. Each entry is an
object with the following fields:
- name — symbolic name used as the per-source
cache subdirectory and in stats output (e.g.
ripe,arin). - enabled — whether to fetch this source. Setting
any source to
falseskips it. - format —
split|single.splitfetches one.gzper RPSL class from${url}/${db_name}.db.${class}.gz(RIPE, APNIC).singlefetches one large.gzdirectly fromurl(ARIN, LACNIC, AFRINIC). - url — directory URL for
split, or the full file URL forsingle. - db_name — file-name prefix used by the
splitformat. Defaults tonameif omitted. - classes — list of RPSL classes to fetch (only
meaningful for
split). Typical full set: inetnum, inet6num, route, route6, aut-num, as-block, as-set, domain, mntner, organisation, person, role, irt, key-cert, filter-set, inet-rtr, peering-set, route-set, rtr-set.
- name — symbolic name used as the per-source
cache subdirectory and in stats output (e.g.
acl #
The ACL is a Binary Encoded Radix Tree (BERT) of CIDR prefixes covering both IPv4 and IPv6 in one structure. The longest matching prefix wins.
- default
- Policy applied when no explicit allow/deny rule matches:
allow|deny. Default:allow. - allow
- List of CIDRs (or bare IPs interpreted as
/32//128) that are permitted. Crucially, sources matched by an entry inalloware also exempt from the rate limiter (seerate_limit). Sources permitted only via the default policy are still rate-limited. - deny
- List of CIDRs that are explicitly rejected with
%% access denied by policy.
The loopback prefixes 127.0.0.0/8 and ::1/128
are always implicitly allowed (and rate-limit-exempt) so operators can
always reach the server locally.
rate_limit #
Per-source token-bucket limiter. Source addresses are aggregated to a configurable prefix length to prevent scanners from hiding in address space.
- enabled
- Master switch. Default:
false. - rps
- Sustained queries per second per aggregated source. Required when
enabledis true. - burst
- Token-bucket size above
rpsbefore throttling kicks in. Required whenenabledis true. - v4_prefix
- Prefix length to aggregate IPv4 sources by (0–32).
Default:
32(per-IP). Most operators want24to lump abusive subnets together. - v6_prefix
- Prefix length to aggregate IPv6 sources by (0–128).
Default:
128. Typical:48. - idle_ttl
- How long an idle per-source bucket lives in memory before garbage
collection. Default:
10m.
stats #
- enabled
- Master switch for the stats subsystem (counters, live connection
registry, JSON dumps, optional HTTP listener).
Default:
false. - dump_path
- Path of the JSON file rewritten atomically every
dump_interval. Empty disables the disk dump. The HTTP listener can still serve in-memory snapshots even with no disk dump configured. - dump_interval
- Cadence at which the snapshot is regenerated and (optionally) flushed
to disk. Sub-second values are allowed (
"200ms","500ms"). Minimum:50ms. Default:1s. - http_bind
- TCP listen address for the embedded HTTP dashboard. Serves
GET /(single-file HTML dashboard) andGET /stats.json(current snapshot). Empty disables the listener.
log #
- level
- Verbosity. One of
debug,info,warn,error, ornone(also acceptsofforsilent). Each level includes everything above it. Default:info.
A small set of lifecycle events (server starting with version, each
listener bind, each (re)load completion) is logged at a custom
NOTICE level above error, so it is emitted
regardless of the configured filter except none.
Signals #
- SIGHUP
- Re-read the configuration file and apply the
reloadable subset:
acl.*,rate_limit.*,log.level,stats.dump_interval, andserver.bind. Each sub-step is independent — a failure in one is logged and skipped, the others still apply. Changes to fields outside this subset (notablydbase.*, theserverdeadlines, andserver.max_concurrent) require a full restart. The same effect is available from the CLI viawhoisd --reload. - SIGINT, SIGTERM
- Initiate a graceful shutdown: stops accepting new connections, waits for in-flight queries to finish, flushes the final stats snapshot, and exits.
Files #
/etc/whoisd.yaml- Default configuration file location (override with
-c). - cache_dir/source/source.db.class.gz
- Cached RIR dumps. Each enabled source has its own subdirectory; the file naming mirrors the upstream layout (or just source.db.gz for single-file formats).
- dump_path
- The current stats JSON snapshot, atomically rewritten every
dump_interval. - server.pid_file
- Configurable pid file path. Written on startup and read back by
whoisd --reload. Default:/run/whoisd.pid.
Exit status #
- 0
- Clean shutdown after SIGINT/SIGTERM, or a successful one-shot
operation (
--version,--stats,--reload). - 1
- Runtime failure: initial database load failed, the listener died, or
the server otherwise stopped on an error. Also returned by
--statsand--reloadwhen they cannot reach the running instance. - 2
- Startup failure: configuration could not be loaded, parsed, validated, or the ACL could not be built.
Examples #
Run the server with a custom config:
whoisd --config /etc/whoisd.yaml
Query the running server (default WHOIS client, port from
server.bind):
whois -h 127.0.0.1 -p 4343 8.8.8.8
whois -h 127.0.0.1 -p 4343 AS3333
whois -h 127.0.0.1 -p 4343 -- '-m 193.0.0.0/16'
Inspect the live status without disturbing the server:
whoisd --stats
Apply changes from the config file without a restart:
whoisd --reload # or: kill -HUP $(pidof whoisd)
Bind to the privileged port 43 without running as root:
setcap cap_net_bind_service=+ep /usr/local/bin/whoisd
Notes #
Memory footprint scales with the size of the merged store. With all five RIRs loaded, expect roughly 18–20 GiB resident. Startup time is dominated by the initial download and parse; subsequent starts reuse the on-disk cache and are bound by parse time alone.
During a reload-triggered re-index the resident set briefly spikes to roughly 2× steady-state, because the new in-memory store is built alongside the old one before an atomic swap.
whoisd is a single static binary with no runtime dependencies beyond the host operating system. It does not require a database server, message broker, or external state store.
See also #
whois(1), yaml(5)
RIPE Database split-format documentation: https://ftp.ripe.net/ripe/dbase/split/
Project home: https://github.com/hackman/One_Ring