monovm-whois-rust
Domain WHOIS and RDAP lookups for Rust, with availability detection you can audit.
A ground-up Rust design: RDAP, structured record parsing, referral chasing, rate limiting, caching, and verdicts that explain themselves.
use WhoisClient;
let client = new?;
let lookup = client.lookup?;
println!;
if let Some = &lookup.record
# Ok::
Install
[]
= "1"
The library is imported as monovm_whois. Async instead of blocking:
= { = "1", = false, = ["async", "rdap", "parser"] }
The command line tool (installs the monovm-whois binary):
The problem this crate is about
WHOIS has no status codes. A registry saying this domain is free, one saying you are querying too fast, and one saying I do not serve that suffix all send prose over the same socket — and every one of them can contain the word "available".
Libraries in this space overwhelmingly resolve that ambiguity the same way: anything not recognisably a record is treated as availability. The consequence is that a rate-limited registry reports its entire zone as free to register, and a stale server mapping that lands a query on a regional IP registry does the same.
This crate never reports an unanswered query as an answer:
use ;
#
There is deliberately no Availability::Unknown. An uncertain answer that renders
as "available" is the one outcome a caller must not be handed.
What you get
| Coverage | 873 curated suffixes plus IANA's RDAP bootstrap registry — over 1600 in total |
| Two protocols | Port 43 (RFC 3912) and RDAP (RFC 7482/9082/9083), with automatic fallback |
| Structured records | Typed dates, EPP statuses, name servers, DNSSEC, contacts — not raw text |
| Referral chasing | Thin registries point at the registrar; this follows the pointer and merges both records |
| Auditable verdicts | Every answer names the rule that produced it, with a confidence level |
| Rate limiting | Per-host pacing, because registries punish speed with silence |
| Caching | Pluggable, with a bundled in-memory TTL + LRU implementation |
| Retries | Exponential backoff, applied only to failures worth retrying |
| Both runtimes | WhoisClient and AsyncWhoisClient |
No unsafe |
#![forbid(unsafe_code)] |
Examples
Bulk availability, and bare names
A name with no suffix is checked under a list of popular ones — the "is this brand free?" search:
use ;
let checker = new
.with_popular_tlds?;
let report = checker.check;
println!;
println!;
// Failures are entries, not silence: a lookup that could not be answered is
// reported as such rather than folded into "taken".
for in report.failures
# Ok::
Concurrent lookups
use AsyncWhoisClient;
# async
Why did it say that?
#
example.com — registered (high confidence, registered: status field matched "status[\s._\-]*:\s*active")
consulted: whois.verisign-grs.com, whois.example-registrar.com
verdict: registered (high confidence, registered: status field matched …)
response: 2214 bytes
wrong-server abstained
refusal abstained
rdap abstained
registry-marker abstained
withheld abstained
registered registered [high] status field matched …
not-found abstained
tld-pattern abstained
recordless abstained
Configuring it
use Duration;
use ;
use WhoisClient;
let client = builder
// RDAP first: its 404 makes availability a fact rather than an inference.
.prefer
.referrals
.memory_cache
.throttle_per_host
.connect_timeout
.build?;
# Ok::
Architecture
Six layers, each with one job and no knowledge of the others. Every one is a trait with a bundled implementation, so any of them can be replaced without forking:
┌──────────────────────────────────────────┐
│ client — composes the lookup sequence │
└────┬─────────┬──────────┬────────┬───────┘
│ │ │ │
┌──────────▼──┐ ┌───▼──────┐ ┌─▼─────┐ ┌▼────────┐
│ registry │ │transport │ │detect │ │ parser │
│ who serves │ │ the only │ │ what │ │ record │
│ this suffix │ │ I/O │ │it said│ │ as data │
└──────┬──────┘ └────┬─────┘ └───────┘ └─────────┘
│ │
RegistryProvider Transport ← cache, retry, throttle (decorators)
│
┌──────▼──────────────────────────────────────────┐
│ domain — validated values: DomainName, Tld │
└─────────────────────────────────────────────────┘
Patterns used, and what each one buys:
| Pattern | Where | Why |
|---|---|---|
| Chain of Responsibility | detect::AvailabilityRule |
Detection is ~10 competing heuristics whose order is the design. A chain makes the priority explicit and each rule independently testable. |
| Decorator | RetryTransport, ThrottleTransport, CachingTransport |
Retrying, pacing and caching are orthogonal to talking. Composing them lets a caller order them meaningfully — caching outside throttling serves repeats without waiting. |
| Strategy | RecordParser, Transport, RegistryProvider |
One interface per varying algorithm, so adding a protocol or a record format is a new type rather than a new match arm. |
| Composite | LayeredRegistry, Router, CompositeParser |
Several providers behave as one, so the client holds a single collaborator regardless of how many sources back it. |
| Builder | WhoisClientBuilder, RegistryBuilder |
Both have many optional fields with sensible defaults; a builder keeps the common case to one line and adding a field non-breaking. |
| Value Object | DomainName, Tld, Availability |
Validated once at the boundary. A DomainName that exists is queryable, so no layer below re-checks. |
| Null Object | NullCache |
"No caching" is a value, not a branch, so the caching layer is always present and always called the same way. |
| Template Method | RegistryProvider::resolve |
Suffix resolution is one algorithm over a primitive each provider supplies — doing it in both places would make the result depend on which provider answered first. |
SOLID, concretely:
- S —
Whois43Transportopens sockets and nothing else; deciding what the bytes mean isdetect's job, and parsing them isparser's. - O — a new detection rule, transport, cache or registry source is a new type. No existing file changes.
- L — every
Transportis substitutable,MockTransportincluded; that is what makes the test suite run without a network. - I —
ResponseCachehas three methods,RecordParsertwo. Nothing implements what it does not need. - D —
WhoisClientdepends ondyn RegistryProvider/dyn Transport, never on a concrete type.
Features
| Feature | Default | Gives you |
|---|---|---|
blocking |
✓ | WhoisClient, synchronous transports |
rdap |
✓ | RDAP over HTTPS, typed RFC 9083 model |
parser |
✓ | WhoisRecord and record parsing |
async |
AsyncWhoisClient, Tokio transports |
|
iana-bootstrap |
Refresh the RDAP registry from IANA at runtime | |
cli |
The monovm-whois binary |
|
mock |
MockTransport, for your own tests |
Command line
Exit status reflects whether the questions were answered. A domain that turns out to be taken exits zero; a registry that refused to answer exits non-zero.
Registry data
Two bundled sources, layered:
data/registries.json— 289 curated registry definitions covering 873 suffixes: WHOIS hosts, the exact wording each server uses for an unregistered name, and per-registry quirks. Regenerate from an upstream definition list withpython data/build_registries.py path/to/dist.whois.json; that script also applies this crate's corrections to hosts such a list has left behind and adds registries it never covered, so a regeneration does not lose them.data/rdap-bootstrap.json— a snapshot of IANA's RDAP bootstrap registry, covering about 1200 suffixes.
Point at your own file, or stack it over the bundled data:
use ;
use WhoisClient;
let registry = new
.shared_layer
.layer;
let client = builder.registry.build?;
# Ok::
With the iana-bootstrap feature, refresh the RDAP list from IANA rather than
shipping a snapshot that ages.
Testing
The suite runs offline: MockTransport scripts responses, and fixtures hold real
recorded records from a range of registries.
The network tests are #[ignore] on purpose. A test that fails because a registry
is having a bad afternoon teaches nobody anything.
They are still worth running occasionally: survey_the_popular_suffixes sweeps 40
suffixes and prints which answered and how. Its last run answered 35 and got none of
them wrong; the wordings it found that no table had — AFNIC's %% NOT FOUND, auDA's
bare Available, JPRS's bracketed field keys — are in the CHANGELOG and pinned by
fixtures.
License
MIT. See LICENSE.