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 monovm_whois::WhoisClient;
let client = WhoisClient::new()?;
let lookup = client.lookup("example.com")?;
println!("{} is {}", lookup.domain, lookup.availability());
if let Some(record) = &lookup.record {
println!("registrar: {:?}", record.registrar);
println!("expires: {:?}", record.expires);
println!("locked: {}", record.is_transfer_locked());
}
# Ok::<(), monovm_whois::Error>(())[dependencies]
monovm-whois-rust = "1"The library is imported as monovm_whois. Async instead of blocking:
monovm-whois-rust = { version = "1", default-features = false, features = ["async", "rdap", "parser"] }The command line tool (installs the monovm-whois binary):
cargo install monovm-whois-rust --features cliWHOIS 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 monovm_whois::{Error, Refusal};
# fn demo(client: &monovm_whois::WhoisClient) {
match client.lookup("example.com") {
Ok(lookup) => { /* a real verdict */ }
Err(Error::Refused { reason: Refusal::RateLimited, .. }) => { /* back off */ }
Err(Error::Inconclusive { detail, .. }) => { /* unparseable */ }
Err(other) => { /* network, input, … */ }
}
# }There is deliberately no Availability::Unknown. An uncertain answer that renders
as "available" is the one outcome a caller must not be handed.
| 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)] |
A name with no suffix is checked under a list of popular ones — the "is this brand free?" search:
use monovm_whois::{Checker, WhoisClient};
let checker = Checker::new(WhoisClient::new()?)
.with_popular_tlds(["com", "net", "io", "co.uk"])?;
let report = checker.check(["monovm", "example.com"]);
println!("{report}");
println!("free: {:?}", report.available());
// Failures are entries, not silence: a lookup that could not be answered is
// reported as such rather than folded into "taken".
for (domain, error) in report.failures() {
eprintln!("{domain}: {error}");
}
# Ok::<(), monovm_whois::Error>(())use monovm_whois::AsyncWhoisClient;
# async fn demo() -> Result<(), monovm_whois::Error> {
let client = AsyncWhoisClient::builder()
.concurrency(32)
.build()?;
for (domain, outcome) in client.lookup_many(["example.com", "example.net"]).await {
match outcome {
Ok(lookup) => println!("{domain}: {}", lookup.availability()),
Err(error) => eprintln!("{domain}: {error}"),
}
}
# Ok(())
# }# fn demo(client: &monovm_whois::WhoisClient) -> Result<(), monovm_whois::Error> {
println!("{}", client.explain("example.com")?);
# Ok(())
# }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
use std::time::Duration;
use monovm_whois::client::{Preference, ReferralPolicy};
use monovm_whois::WhoisClient;
let client = WhoisClient::builder()
// RDAP first: its 404 makes availability a fact rather than an inference.
.prefer(Preference::Rdap)
.referrals(ReferralPolicy::eager(2))
.memory_cache(Duration::from_secs(600))
.throttle_per_host(Duration::from_millis(500))
.connect_timeout(Duration::from_secs(3))
.build()?;
# Ok::<(), monovm_whois::Error>(())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.
| 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 |
monovm-whois example.com # availability
monovm-whois monovm --tlds com,net,io # a bare name across several suffixes
monovm-whois example.com --record # the parsed registration
monovm-whois example.com --raw # the server's own text
monovm-whois example.com --explain # why the verdict is what it is
monovm-whois example.com --json # machine-readable
monovm-whois --list-tlds # every suffix this build supportsExit 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.
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 monovm_whois::registry::{JsonRegistry, LayerStrategy, LayeredRegistry, default_provider};
use monovm_whois::WhoisClient;
let registry = LayeredRegistry::new(LayerStrategy::Override)
.shared_layer(default_provider())
.layer(JsonRegistry::from_path("my-overrides.json")?);
let client = WhoisClient::builder().registry(registry).build()?;
# Ok::<(), monovm_whois::Error>(())With the iana-bootstrap feature, refresh the RDAP list from IANA rather than
shipping a snapshot that ages.
The suite runs offline: MockTransport scripts responses, and fixtures hold real
recorded records from a range of registries.
cargo test # offline, the default
cargo test --all-features
cargo test -- --ignored # the live-network tests, opt-inThe 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.
MIT. See LICENSE.