Overview
ResolveDB transforms DNS infrastructure into a data distribution platform. Data is encoded in DNS queries and responses, leveraging global DNS caching for sub-millisecond access.
Explicit Parameters Design
ResolveDB uses explicit parameters for all context-dependent queries. The server never infers client identity, location, or preferences from the source IP address. This design provides predictability, privacy, cache efficiency, and auditability.
Core Principles
| Principle | Implementation |
|---|---|
| Deterministic responses | Identical queries MUST return identical responses regardless of source |
| Explicit parameters only | Location, IP, and context MUST be provided as query parameters |
| No source IP inference | Server MUST NOT use the querier's IP for any business logic |
| Proxy-transparent | Queries through VPNs, DoH, or proxies work identically to direct queries |
Benefits
| Benefit | Description |
|---|---|
| Predictability | Same query = same result. Debug from anywhere, test from CI, results never surprise you. |
| Cache efficiency | No ECS scope fragmentation. One cached response serves all users worldwide. |
| Privacy | Server never learns client's real IP or location. You control what data is shared. |
| Auditability | Inspect any query string to see exactly what data the server receives. |
| Compatibility | Works through DoH/DoT resolvers, VPNs, corporate proxies, Tor—all correctly. |
Why Traditional GeoDNS Breaks
Traditional DNS-based services infer client location from source IP, causing:
- Unpredictable results - Same query returns different data from different networks
- Proxy/VPN breakage - Queries return data for the proxy's location, not yours
- Cache fragmentation - ECS-scoped responses create thousands of cache entries per /24 subnet
- Privacy leakage - Server logs reveal your approximate location
- Testing difficulty - Can't reproduce production behavior in CI/staging
Explicit Parameter Pattern
# CORRECT: Client explicitly provides context
get.ip-8-8-8-8.geoip.v1.resolvedb.net # GeoIP for specific IP
get.city-newyork.weather.v1.resolvedb.net # Weather for named location
get.lat-40d7128.lon--74d0060.weather.v1.resolvedb.net # Weather for coordinates
get.region-eu.config.myapp.v1.resolvedb.net # EU-specific configuration
# WRONG: Implicit context (rejected or undefined behavior)
get.weather.v1.resolvedb.net # ← What location? Rejected!
geoip.self.v1.resolvedb.net # ← Rejected with FormErrPrivacy Best Practices
For applications handling sensitive data, ResolveDB supports multiple layers of protection:
| Layer | Feature | Description |
|---|---|---|
| Transport | DoH/DoT | Query authoritative servers via DNS-over-HTTPS to encrypt queries in transit |
| Authentication | JWT tokens | Use auth-<token> prefix for authenticated queries |
| Payload encryption | AES-256-GCM | Client-side encrypt data before storing; server never sees plaintext |
| Token privacy | Hash references | Use h-<hash> prefix to avoid exposing tokens in DNS logs |
| Namespace isolation | Private namespaces | Use organization namespaces (myorg.resolvedb.net) for access control |
Client-side encryption example:
// Encrypt before storing - server never sees plaintext
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
// Store encrypted blob
await resolvedb.put('secrets.api-key.myapp.v1', base64(iv + encrypted));
// Retrieve and decrypt client-side
const blob = await resolvedb.get('secrets.api-key.myapp.v1');
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: blob.iv }, key, blob.ciphertext);Client DNS Resolver ResolveDB Authoritative
| | |
|-- get.city-nyc.weather.v1.resolvedb.net ---------------->|
| | |
|<-- TXT "v=rdb1;s=ok;d={"temp":72}" ---------------------|
| | |
|-- (cached globally) ----->| |Write Operations
Important: Write operations (put, delete) are handled via the HTTP API at api.resolvedb.io, not via DNS queries. DNS is a read-optimized protocol; writes flow through the API.
Why API for Writes?
- DNS queries are limited to 253 characters (FQDN limit)
- DNS lacks reliable delivery guarantees for mutations
- Authentication is simpler over HTTPS
- Write confirmation requires bidirectional communication
API Examples
# Create/update a resource
curl -X PUT https://api.resolvedb.io/v1/namespaces/myapp/resources/config \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"theme": "dark", "locale": "en-US"}'
# Delete a resource
curl -X DELETE https://api.resolvedb.io/v1/namespaces/myapp/resources/config \
-H "Authorization: Bearer <token>"
# List resources
curl https://api.resolvedb.io/v1/namespaces/myapp/resources \
-H "Authorization: Bearer <token>"After a write, the data is immediately available via DNS queries:
dig TXT get.config.myapp.v1.resolvedb.net +short
# "v=rdb1;s=ok;t=data;f=json;d={\"theme\":\"dark\",\"locale\":\"en-US\"}"Namespace Architecture
Hostname Scoping Protocol (HSP)
resolvedb.<tld> # Root domain (.com/.net/.org/.io)
├── public.resolvedb.<tld> # Public data services
├── user.resolvedb.<tld> # User-owned namespaces
├── system.resolvedb.<tld> # System operations
└── registry.resolvedb.<tld> # Service registryPublic Namespace (public.resolvedb.<tld>)
Globally accessible data through standardized interfaces. Supports pluggable providers including MCPs.
<query>.<parameters>.<service>.<version>.public.resolvedb.<tld>
Examples:
current.celsius.newyork.weather.v1.public.resolvedb.net
price.aapl.nasdaq.stock.v1.public.resolvedb.net
latest.headlines.tech.news.v1.public.resolvedb.netService Registration:
_service.weather.v1.public.resolvedb.net TXT "v=rdb1;type=mcp;endpoint=weather-mcp://api;schema=v1.2.0"
_caps.weather.v1.public.resolvedb.net TXT "methods=current,forecast;formats=json,xml;auth=none"
_schema.weather.v1.public.resolvedb.net URI "https://schemas.resolvedb.net/weather/v1/openapi.json"User Namespace (user.resolvedb.<tld>)
Isolated data storage for individuals and organizations.
<data>.<collection>.<user-id>.user.resolvedb.<tld>
<data>.<app>.<org-identifier>.user.resolvedb.<tld>
Examples:
profile.settings.alice.user.resolvedb.net
config.myapp.prod.acme-corp.user.resolvedb.netDual Namespace Addressing
Organizations receive both a human-readable vanity name and a stable hash ID:
| Type | Format | Example |
|---|---|---|
| Vanity name | <org-name> (1-32 chars) | acme-corp |
| Hash ID | 16 character hex (64 bits minimum) | a7f3b2c4e8d9f012 |
Both resolve to identical data:
config.myapp.acme-corp.user.resolvedb.net # vanity (default)
config.myapp.a7f3b2c4e8d9f012.user.resolvedb.net # hash (privacy mode)Use cases:
- Vanity name: Default for readability, documentation, sharing
- Hash ID: Privacy-sensitive contexts where org name shouldn't leak in DNS traffic; stable identifier that survives org renames
Behavior:
- Vanity names are aliases pointing to the underlying hash ID
- Hash IDs are immutable and assigned at namespace creation
- Org renames update the vanity alias without changing the hash ID
- API responses include both identifiers (see Claiming a Namespace)
Hash ID Security Requirements:
| Requirement | Value | Rationale |
|---|---|---|
| Minimum length | 16 hex chars (64 bits) | Birthday bound collision resistance |
| Derivation | Deterministic | SHA256(namespace_name || creation_timestamp || server_salt)[0:16] |
| Collision check | REQUIRED | Server MUST verify uniqueness before assignment |
Security Rationale:
6-8 hex characters (24-32 bits) provides collision at only ~4K-16K namespaces (birthday bound). With 64 bits, collision requires ~2^32 namespaces, providing adequate safety margin.
Namespace Claim:
_claim.alice.user.resolvedb.net TXT "v=rdb1;pubkey=<ed25519-base64>;sig=<signature-base64>;exp=1735689600"
_claim.acme-corp.user.resolvedb.net TXT "v=rdb1;id=a7f3b2c4e8d9f012;org=acme-corp;admin=admin@acme.com;verified=true"Claim Signature Verification
Signature Scope:
The signature in claim records MUST cover a canonicalized representation:
signed_data = SHA256(
namespace_name_lowercase || # "alice" or "acme-corp"
":" ||
pubkey_bytes || # 32-byte Ed25519 public key
":" ||
exp_timestamp # Big-endian 64-bit Unix timestamp
)
signature = Ed25519.sign(owner_private_key, signed_data)Verification Process:
def verify_claim(claim_record):
# 1. Parse claim fields
namespace = claim_record.name.split('.')[1] # Extract from _claim.<namespace>...
pubkey = base64_decode(claim_record['pubkey'])
sig = base64_decode(claim_record['sig'])
exp = int(claim_record['exp'])
# 2. Check expiration
if exp < current_unix_timestamp():
return ClaimResult.EXPIRED
# 3. Reconstruct signed data
signed_data = sha256(
namespace.lower().encode() + b':' +
pubkey + b':' +
exp.to_bytes(8, 'big')
)
# 4. Verify signature
if not ed25519_verify(pubkey, signed_data, sig):
return ClaimResult.INVALID_SIGNATURE
return ClaimResult.VALIDKey Rotation:
To rotate keys, the owner signs with the OLD key authorizing the NEW key:
_claim.<namespace>.user.resolvedb.net TXT "v=rdb1;pubkey=<new-key>;prev_pubkey=<old-key>;sig=<signed-by-old>;exp=..."
The signature for rotation covers: new_pubkey || ":" || prev_pubkey || ":" || exp
Revocation:
Compromised keys are revoked by publishing a revocation record signed by any valid key in the chain:
_revoke.<key-fingerprint>.<namespace>.user.resolvedb.net TXT "v=rdb1;reason=compromised;revoked_at=1704067200;sig=<signature>"
System Namespace (system.resolvedb.<tld>)
Reserved for health, metrics, and status.
_health.system.resolvedb.net TXT "v=rdb1;status=healthy;tld=net;region=global"
Namespace Registration
Naming Rules
| Rule | Constraint |
|---|---|
| Length | 1-32 characters |
| Characters | a-z, 0-9, - (hyphen) |
| Start/End | Must start and end with alphanumeric |
| Case | Case-insensitive (stored lowercase) |
Reserved Namespaces
The following namespaces cannot be claimed by users:
| Category | Namespaces | Purpose |
|---|---|---|
| System | public, system, registry, admin, root | Core platform operations |
| Infrastructure | api, www, cdn, dns | Infrastructure confusion prevention |
| Nameservers | ns*, ns01, ns02, ns03 (pattern) | Nameserver confusion |
mail, email, smtp, imap, mx | Email infrastructure | |
| Protocol | http, https, ftp, ssh, sftp | Protocol confusion |
| Brand | resolvedb, rdb | Brand protection |
| DNS convention | _* (underscore prefix) | RFC compliance |
| Short names | All 2-character names | ISO country code conflicts |
Pattern Matching:
Namespaces matching these patterns are also reserved:
ns[0-9]*- Any nameserver-like patternv[0-9]+- Version-like patterns (conflicts with protocol version)test*,dev*,staging*- Reserved for platform testing
Validation Implementation:
fn is_reserved_namespace(name: &str) -> bool {
let lower = name.to_lowercase();
// Exact matches
const RESERVED: &[&str] = &[
"public", "system", "registry", "admin", "root",
"api", "www", "cdn", "dns", "mail", "email", "smtp", "imap", "mx",
"http", "https", "ftp", "ssh", "sftp", "resolvedb", "rdb"
];
if RESERVED.contains(&lower.as_str()) { return true; }
// Pattern matches
if lower.starts_with('_') { return true; } // DNS convention
if lower.len() <= 2 { return true; } // Too short
if lower.starts_with("ns") && lower[2..].chars().all(|c| c.is_ascii_digit()) { return true; }
if lower.starts_with('v') && lower[1..].chars().all(|c| c.is_ascii_digit()) { return true; }
false
}Claiming a Namespace
Namespaces are claimed via the API:
curl -X POST https://api.resolvedb.io/v1/namespaces \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "myapp", "public_key": "<ed25519-pubkey>"}'Response includes both identifiers:
{
"name": "myapp",
"id": "a7f3b2c4e8d9f012",
"owner": "user-12345",
"created": 1704067200
}The claim record is then queryable via either identifier:
_claim.myapp.user.resolvedb.net TXT "v=rdb1;id=a7f3b2c4e8d9f012;pubkey=<ed25519>;owner=<user-id>;created=1704067200"
_claim.a7f3b2c4e8d9f012.user.resolvedb.net TXT "v=rdb1;id=a7f3b2c4e8d9f012;name=myapp;pubkey=<ed25519>;owner=<user-id>;created=1704067200"Renaming a namespace:
curl -X PATCH https://api.resolvedb.io/v1/namespaces/myapp \
-H "Authorization: Bearer <token>" \
-d '{"name": "myapp-v2"}'The hash ID (a7f3b2c4e8d9f012) remains stable; existing queries using the hash continue to work. Queries using the old vanity name return notfound after rename.
Access Control Model
| Permission | Description |
|---|---|
read | Query resources in namespace |
write | Create/update resources |
delete | Remove resources |
list | Enumerate resources |
admin:grant | Grant permissions to others |
admin:revoke | Revoke permissions |
admin:transfer | Transfer namespace ownership |
Cross-Namespace Access:
public.*namespaces: readable by all, writable by registered providersuser.*namespaces: private by default, owner controls access- Access grants are stored in
_acl.<namespace>.user.resolvedb.<tld>
Query Format
Structure
<operation>.<params>.<resource>.<namespace>.<version>.resolvedb.<tld>
| Component | Required | Description |
|---|---|---|
| operation | Yes | Action to perform |
| params | No | Encoded parameters |
| resource | Yes | Data resource name |
| namespace | Yes | Scope (public, user, system) |
| version | Yes | Protocol version (v1) |
| resolvedb | Yes | Protocol marker |
| tld | Yes | .com, .net, .org, .io |
Formal Grammar (ABNF)
; Query structure
query = operation "." [params "."] resource "." namespace "." version ".resolvedb." tld
operation = "get" / "put" / "delete" / "list" / "search" / "info" / "health" / "geoip" / "watch"
params = encoded-param *("." encoded-param)
; Parameter encodings (all use hyphen separators, NOT colons)
encoded-param = plain-param / b64-param / b32-param / hex-param / auth-param
/ chunk-param / hash-param / geo-param / cursor-param
/ ts-param / nonce-param / limit-param / offset-param
; Plain parameters: alphanumeric, cannot start/end with hyphen (RFC 1035)
plain-param = ALPHANUM *61(ALPHA / DIGIT / "-") [ALPHANUM]
ALPHANUM = ALPHA / DIGIT
; Encoding prefixes
b64-param = "b64-" 1*base64url
b32-param = "b32-" 1*base32 ; Case-insensitive encoding
hex-param = "hex-" 1*HEXDIG
auth-param = "auth-" (jwt-token / hash-ref) ; Full JWT or hash reference
hash-ref = "h-" 32*64HEXDIG ; HMAC-SHA256 reference (128+ bits)
chunk-param = "chunk-" index "-" total "-" hash
hash-param = "h-" 32*64HEXDIG ; Content hash (128+ bits minimum)
; GeoIP: lat/lon with 'd' as decimal separator (colons invalid in DNS)
geo-param = "geo-" coordinate "-" coordinate
coordinate = ["-"] 1*3DIGIT ["d" 1*6DIGIT] ; e.g., 40d7128 for 40.7128
; Pagination
cursor-param = "cursor-" (1*43base64url / hash-ref) ; Max 63 chars per label
limit-param = "limit-" 1*4DIGIT ; 1-1000
offset-param = "offset-" 1*10DIGIT
; Replay protection (required for auth queries)
ts-param = "ts-" 10DIGIT ; Unix timestamp
nonce-param = "nonce-" 8*16ALPHANUM ; Random, unique per request
; Structural elements
resource = label
namespace = label *("." label)
version = "v" 1*DIGIT
tld = "com" / "net" / "org" / "io"
; Labels per RFC 1035: alphanumeric start/end, max 63 chars
label = ALPHANUM *61(ALPHA / DIGIT / "-") [ALPHANUM]
; Character classes
base64url = ALPHA / DIGIT / "-" / "_" ; URL-safe, no padding
base32 = %x41-5A / "2" / "3" / "4" / "5" / "6" / "7" ; A-Z, 2-7
jwt-token = 3*(base64url ".") base64url ; header.payload.signature format
index = 1*DIGIT
total = 1*DIGIT
hash = 16*64HEXDIG ; Minimum 64 bits for collision resistanceGrammar Notes:
- All prefixes use hyphens (
-), never colons (:) - colons are invalid in DNS labels per RFC 1035 - Labels MUST start and end with alphanumeric characters (RFC 1035 Section 2.3.1)
- JWT tokens in
auth-SHOULD use hash references (auth-h-<hash>) for tokens exceeding 63 chars - Hash references (
h-) require minimum 32 hex chars (128 bits) for brute-force resistance
Operations
| Operation | Description | Auth Required | Transport |
|---|---|---|---|
get | Retrieve data | No (public) / Yes (user) | DNS |
put | Store data | Yes | HTTP API only |
delete | Remove data | Yes | HTTP API only |
list | List resources | Depends | DNS |
search | Search resources | Depends | DNS |
watch | Subscribe to changes | Yes | DNS (returns WebSocket URL) |
info | Resource metadata | No | DNS |
health | System health | No | DNS |
geoip | Client IP geolocation | No | DNS |
Parameter Encoding
Parameters requiring special characters are encoded using DNS-safe prefixes. All prefixes use hyphens (-) as separators since colons are not valid in DNS labels per RFC 1035.
| Prefix | Encoding | Use Case |
|---|---|---|
| (none) | Plain alphanumeric | Simple keys |
b64- | Base64 URL-safe | JSON, binary, complex params |
b32- | Base32 | Case-insensitive |
hex- | Hexadecimal | Binary hashes |
auth- | JWT token | Authentication |
chunk- | Chunk reference | Large data |
h- | Hash reference | Content-addressed |
bdt- | Blind Device Token | IoT device identity |
ctp- | Cohort Token | User targeting |
sig- | Namespace Signature | Multi-tenant auth |
Security Token Prefixes
For use cases requiring enhanced security guarantees beyond basic authentication, UQRP defines three specialized token prefixes. These patterns provide cryptographic guarantees that prevent enumeration, privacy leakage, and cross-tenant access.
Blind Device Token (bdt-)
Provides device identity without exposing device IDs in queries. Used for IoT and industrial configurations.
Token Derivation:
device_secret = HKDF-SHA256(
ikm = factory_master_secret,
salt = device_id,
info = "resolvedb-bdt-v1"
)
blind_token = hex(SHA256(device_secret || factory_id || epoch_week)[0:16])Query Format:
get.bdt-<32-hex-chars>.config.<factory-namespace>.v1.resolvedb.<tld>
Example:
get.bdt-a7f3b2c4e8d9f012a7f3b2c4e8d9f012.config.acme-factory.v1.resolvedb.net
Validation:
- Server maintains index of
blind_token → device_idmappings - Accepts tokens for current week AND previous week (seamless rotation)
- Returns
E018(bdtinvalid) for unknown tokens
Response Encryption: Responses MAY be encrypted with the device's derived secret:
v=rdb1;s=ok;t=data;e=aes;f=json;ttl=300;d=<AES-256-GCM(device_secret, config)>
Security Properties:
| Property | Guarantee |
|---|---|
| Device enumeration resistance | 2^128 token space |
| Identity privacy | Device ID never in query |
| Rotation | Automatic weekly (epoch_week) |
| Factory isolation | Token bound to factory_id |
Cohort Token Pattern (ctp-)
Enables server-side user targeting without exposing user identity or targeting rules in queries.
Token Structure:
cohort_token = base64url(AES-256-GCM(
key = app_secret,
nonce = random(12),
data = CBOR({
"u": SHA256(user_id)[0:8], // 8-byte user hash
"s": segment_bitmap, // 4-byte bitmap (32 targeting bits)
"t": floor(unix_time / 300) // 5-minute bucket
})
))Segment Bitmap (32 bits):
Bit 0: is_premium Bit 16: experiment_a
Bit 1: is_beta_user Bit 17: experiment_b
Bit 2: is_internal Bit 18: experiment_c
Bit 3: (reserved) Bit 19: experiment_d
Bit 4: platform_ios Bit 20-23: (reserved)
Bit 5: platform_android Bit 24: locale_en
Bit 6: platform_web Bit 25: locale_es
Bit 7: platform_desktop Bit 26: locale_fr
Bit 8: region_na Bit 27: locale_de
Bit 9: region_eu Bit 28: locale_ja
Bit 10: region_apac Bit 29: locale_zh
Bit 11: region_latam Bit 30: locale_pt
Bit 12-15: tier (0-15) Bit 31: custom_flagQuery Format:
get.ctp-<base64url-token>.<resource>.<namespace>.v1.resolvedb.<tld>
Example:
get.ctp-dGVzdHRva2VuMTIzNDU2Nzg5MGFiY2RlZg.dark-mode.flags.myapp.v1.resolvedb.net
Validation:
- Server decrypts token with app's registered secret
- Validates timestamp (reject if >5 minutes old)
- Evaluates targeting rules against segment bitmap
- Returns evaluated flag values, NOT targeting rules
Security Properties:
| Property | Guarantee |
|---|---|
| User identity privacy | Only 8-byte hash in encrypted token |
| Targeting rule privacy | Rules evaluated server-side |
| Cache efficiency | Same cohort (bitmap) = same cache entry |
| Replay window | 5-minute token expiry |
Error Codes:
| Code | Status | Description |
|---|---|---|
E019 | secviol | CTP token decryption failed |
E020 | secviol | CTP token expired (>5 min) |
Namespace-Bound Signature (sig-)
Cryptographically binds queries to a specific tenant namespace, preventing cross-tenant access even with stolen tokens.
Signature Derivation:
timestamp = unix_epoch_seconds()
material = UTF8(operation + "." + resource + "." + namespace + ".v1|" + timestamp + "|" + tenant_id)
signature = hex(HMAC-SHA256(tenant_query_key, material)[0:8])Query Format:
get.sig-<16-hex-chars>-t-<unix-timestamp>.<resource>.<namespace>.v1.resolvedb.<tld>
Example:
get.sig-a3f2e8c1d4b5a678-t-1704067200.config.acme-corp.v1.resolvedb.net
Validation:
- Extract namespace from query FQDN
- Look up tenant's
tenant_query_keyby namespace - Recompute expected signature using extracted timestamp
- Constant-time compare signatures
- Verify timestamp within 5-minute window
- Return
E018(siginvalid) for any failure
Combined with JWT (Defense in Depth): For maximum security, combine signature validation with JWT:
get.sig-<sig>-t-<ts>.auth-h-<jwt-hash>.<resource>.<namespace>.v1.resolvedb.net
Server verifies:
- JWT claims contain matching
tenantfield - Query namespace matches JWT tenant
- Signature is valid for query namespace
Security Properties:
| Property | Guarantee |
|---|---|
| Cross-tenant prevention | Signature cryptographically bound to namespace |
| Token theft resistance | Attacker needs query_key, not just JWT |
| Replay window | 5-minute timestamp validation |
| Bug immunity | Works even if authorization code has bugs |
Error Codes:
| Code | Status | Description |
|---|---|---|
E018 | secviol | Signature validation failed |
E021 | secviol | Timestamp outside valid window |
E022 | secviol | Namespace mismatch (JWT vs query) |
Security Token Summary
| Prefix | Use Case | Key Derivation | Expiry | Error Codes |
|---|---|---|---|---|
bdt- | IoT device identity | HKDF from factory secret | Weekly rotation | E018 |
ctp- | User targeting | AES-256-GCM with app secret | 5 minutes | E019, E020 |
sig- | Multi-tenant auth | HMAC-SHA256 with tenant key | 5 minutes | E018, E021, E022 |
RFC Conformance
All security token prefixes conform to:
- RFC 1035: Labels ≤63 chars, FQDN ≤253 chars, valid chars
[a-z0-9-] - RFC 4648: Base64url encoding for CTP tokens
- RFC 5869: HKDF key derivation for BDT
- RFC 5116: AEAD (AES-256-GCM) for CTP encryption
- RFC 2104: HMAC for NBA signatures
Query Examples
# Simple (no params)
get.weather.public.v1.resolvedb.net
# With location param (plain alphanumeric)
get.newyork.weather.public.v1.resolvedb.net
# Base64 encoded params (JSON with special chars)
# {"lat":40.7128} -> eyJsYXQiOjQwLjcxMjh9
get.b64-eyJsYXQiOjQwLjcxMjh9.location.public.v1.resolvedb.net
# Authenticated (JWT token)
get.auth-eyJhbGciOiJFZDI1NTE5In0.profile.acme.v1.resolvedb.net
# Chunked data (index-total-hash format)
get.chunk-0-10-abc123.largedata.enterprise.v1.resolvedb.net
# Hash-addressed (for long params)
get.h-a1b2c3d4e5f6g7h8.cache.public.v1.resolvedb.net
# Conditional
get.if-modified-since-1704067200.data.user.v1.resolvedb.net
# GeoIP lookup (explicit IP required)
get.ip-8-8-8-8.geoip.v1.resolvedb.netResponse Format
TXT Record Response (v1)
v=rdb1;s=<status>;t=<type>;e=<encoding>;f=<format>;c=<chunks>;h=<hash>;ttl=<seconds>;sig=<signature>;seq=<sequence>;ts=<timestamp>;err=<error-code>;retry=<seconds>;d=<data>
| Field | Description | Values |
|---|---|---|
v | Protocol version | rdb1 |
s | Status code | See status codes |
t | Response type | data, url, multi, stream, encrypted |
e | Encoding | plain, b64, b32, hex, compressed, encrypted |
f | Format | json, xml, protobuf, msgpack, text, binary |
c | Chunk info | current/total (e.g., 1/3) |
h | SHA-256 hash | First 16+ chars |
ttl | Cache delegation duration | Seconds (see TTL Cache Delegation) |
sig | Ed25519 signature | Base64 encoded |
seq | Sequence number | For ordering multi-part |
ts | Timestamp | Unix epoch |
err | Error code | Machine-readable error (e.g., E001) |
retry | Retry after | Seconds until retry is appropriate |
d | Data payload | Encoded data |
Status Codes
| Code | HTTP Equiv | Description |
|---|---|---|
ok | 200 | Success |
partial | 206 | Partial content (chunked response) |
redirect | 301 | See URL in data |
notfound | 404 | Resource not found |
auth | 401 | Authentication required |
forbidden | 403 | Access denied |
ratelimit | 429 | Too many requests |
invalid | 400 | Malformed query |
toolarge | 413 | Response exceeds limits |
secviol | 400 | Security violation (signature invalid, replay detected) |
error | 500 | Server error |
unavail | 503 | Service unavailable |
Error Codes
Machine-readable error codes for programmatic handling:
| Code | Status | Description | Retryable | Recovery Strategy |
|---|---|---|---|---|
E001 | invalid | Malformed query syntax | No | Fix query format |
E002 | invalid | Unknown operation | No | Use valid operation |
E003 | invalid | Invalid encoding prefix | No | Use b64-, hex-, etc. |
E004 | notfound | Resource does not exist | No | Check resource path |
E005 | notfound | Namespace does not exist | No | Register namespace first |
E006 | auth | Missing authentication | No | Include auth-<token> |
E007 | auth | Token expired | No | Refresh token |
E008 | auth | Token invalid | No | Check token format/signature |
E009 | forbidden | Insufficient permissions | No | Request access grant |
E010 | ratelimit | Rate limit exceeded | Yes | Wait for retry seconds |
E011 | toolarge | Payload exceeds 64KB | No | Use chunking protocol |
E012 | error | Internal server error | Yes | Retry with backoff |
E013 | unavail | Service temporarily unavailable | Yes | Retry with backoff |
Error Response Example:
v=rdb1;s=ratelimit;err=E010;retry=60;d=Rate limit exceeded
Error Privacy Mode
For privacy-sensitive namespaces, error responses may leak information about resource existence:
| Error | Information Leaked |
|---|---|
| E004 (Resource not found) | Namespace exists, specific resource doesn't |
| E005 (Namespace not found) | Namespace doesn't exist |
| E009 (Forbidden) | Resource exists, user lacks permission |
Privacy Mode Behavior:
When privacy mode is enabled for a namespace, servers SHOULD return unified error responses:
# Standard mode (informative):
v=rdb1;s=notfound;err=E004;d=Resource not found
v=rdb1;s=notfound;err=E005;d=Namespace not found
v=rdb1;s=forbidden;err=E009;d=Access denied
# Privacy mode (uniform):
v=rdb1;s=notfound;err=E004;d=Not found or access deniedConfiguration:
Privacy mode is set per-namespace via claim record:
_claim.<namespace>.user.resolvedb.net TXT "v=rdb1;...;privacy=high"
| Privacy Level | Behavior |
|---|---|
normal | Distinct errors (E004, E005, E009) |
high | Unified error (always E004 for not-found/forbidden) |
Trade-offs:
- Normal: Better debugging, faster issue resolution
- High: Prevents enumeration, hides internal structure
Response Examples
# Success with JSON
v=rdb1;s=ok;t=data;e=plain;f=json;h=a1b2c3d4e5f6g7h8;ttl=300;d={"temp":72,"unit":"F"}
# URL redirect (large data)
v=rdb1;s=redirect;t=url;ttl=3600;d=https://cdn.resolvedb.cloud/blob/abc123
# Multi-record (chunked)
v=rdb1;s=ok;t=multi;c=1/3;seq=1;h=abc123;d=<chunk1>
v=rdb1;s=ok;t=multi;c=2/3;seq=2;h=abc123;d=<chunk2>
v=rdb1;s=ok;t=multi;c=3/3;seq=3;h=abc123;d=<chunk3>
# Streaming endpoint (see WebSocket Session Security below)
v=rdb1;s=ok;t=stream;d=wss://stream.resolvedb.net/session/<session-token>
# Rate limited with retry hint
v=rdb1;s=ratelimit;err=E010;retry=60;ttl=1;d=Rate limit exceeded. Retry after 60s
# Encrypted
v=rdb1;s=ok;t=encrypted;e=aes256gcm;k=<ephemeral-pubkey>;d=<ciphertext>
# GeoIP response
v=rdb1;s=ok;t=data;f=json;ttl=300;d={"country":"US","region":"NY","city":"New York","lat":40.7128,"lon":-74.006}GeoIP Operation
The geoip operation returns geographic location data for a specified IP address. Following the Privacy by Design principle, the IP address MUST be provided as an explicit parameter.
Query Format
get.ip-<encoded-ip>.geoip.v1.resolvedb.net
IP Encoding:
- IPv4: Replace dots with hyphens (e.g.,
8.8.8.8→ip-8-8-8-8) - IPv6: Replace colons with hyphens,
::becomes--(e.g.,2001:4860:4860::8888→ip-2001-4860-4860--8888)
Response
{
"ip": "8.8.8.8",
"country": "US",
"country_name": "United States",
"region": "CA",
"region_name": "California",
"city": "Mountain View",
"postal": "94035",
"lat": 37.386,
"lon": -122.084,
"timezone": "America/Los_Angeles",
"asn": 15169,
"org": "Google LLC"
}Examples
# Lookup a specific IPv4 address
dig TXT get.ip-8-8-8-8.geoip.v1.resolvedb.net +short
# "v=rdb1;s=ok;t=data;f=json;ttl=300;d={\"ip\":\"8.8.8.8\",\"country\":\"US\",...}"
# Lookup a specific IPv6 address
dig TXT get.ip-2001-4860-4860--8888.geoip.v1.resolvedb.net +short
# Lookup Cloudflare DNS
dig TXT get.ip-1-1-1-1.geoip.v1.resolvedb.net +shortPrivacy Note
The server does NOT use the querier's source IP for GeoIP lookups. The client must explicitly provide the IP address they want to look up. This ensures:
- Consistent results regardless of where the query originates
- Correct behavior through DoH/DoT resolvers, VPNs, and proxies
- No privacy leakage of the client's actual IP
- Cacheable responses (same query = same result)
WebSocket Session Security (CRITICAL)
The watch operation returns a WebSocket URL for real-time updates. Session security is critical.
Session Token Format
Session tokens MUST be cryptographically secure and short-lived:
session_token = Base64URL(HMAC-SHA256(server_secret,
tenant_id || resource_path || created_timestamp || client_ip_hash
))[0:32] # 256-bit truncated to 32 chars| Component | Purpose |
|---|---|
tenant_id | Binds session to authenticated user |
resource_path | Binds to specific watched resource |
created_timestamp | Enables expiration check |
client_ip_hash | Optional IP binding for added security |
Connection Security
Handshake Requirements:
| Step | Requirement |
|---|---|
| 1 | Client connects with Origin header matching allowed origins |
| 2 | Server validates session token (MUST be < 5 minutes old) |
| 3 | Server validates client IP matches token creation IP (optional) |
| 4 | Server sends initial resource state |
| 5 | Bidirectional communication established |
Rate Limiting:
- Maximum 10 WebSocket connections per tenant per minute
- Maximum 100 concurrent connections per tenant
Session Timeouts:
| Timeout | Duration | Action |
|---|---|---|
| Idle | 30 minutes | Disconnect with close code 1000 |
| Maximum | 24 hours | Force reconnection with new token |
| Token validity | 5 minutes | Reject if token older |
Reconnection Protocol
On disconnect, clients MUST:
- Obtain new session token via fresh
watchDNS query - Connect with new token (old tokens are single-use)
- Server sends full state, not just delta
Token Single-Use Enforcement:
Session tokens are consumed on first use. Reusing a token returns:
WebSocket close code: 4401
Reason: "Session token already used"URL Security Concerns
Session tokens in WebSocket URLs are visible in:
- Server access logs
- Browser history
- Referrer headers (if page navigates)
Mitigations:
- Short token validity (5 minutes)
- Single-use tokens
- Consider passing token via WebSocket subprotocol header:
Sec-WebSocket-Protocol: resolvedb-v1, token-<session_token>
Error Codes
| Close Code | Meaning |
|---|---|
| 4400 | Invalid session token |
| 4401 | Session token already used |
| 4403 | Access denied to resource |
| 4429 | Rate limit exceeded |
Pagination
For list and search operations that return multiple results, pagination is supported via cursor-based navigation.
Query Parameters
| Parameter | Format | Description |
|---|---|---|
limit-N | limit-50 | Maximum results per page (1-1000, default 100) |
offset-N | offset-200 | Skip N results (for simple pagination) |
cursor-TOKEN | cursor-abc123 | Opaque cursor for next page |
Response Fields
{
"items": [...],
"cursor": "eyJsYXN0X2lkIjoiMTIzIn0",
"hasMore": true,
"total": 523
}| Field | Description |
|---|---|
items | Array of results for current page |
cursor | Opaque token for next page (Base64-encoded, URL-safe, HMAC-signed) |
hasMore | Boolean indicating more results exist |
total | Total count (approximate for large sets, omit for privacy-sensitive namespaces) |
Cursor Integrity (CRITICAL)
Cursors MUST be cryptographically signed to prevent manipulation attacks.
Cursor Format:
cursor = Base64URL(cursor_data) + "." + Base64URL(signature)
cursor_data = JSON({
"last_id": "<last_item_id>",
"tenant": "<tenant_id>",
"query_hash": "<sha256_of_original_query_params>",
"created": <unix_timestamp>
})
signature = HMAC-SHA256(server_secret, cursor_data)[0:16] # 128-bit truncatedValidation Requirements:
Servers MUST:
- Verify HMAC signature before using cursor
- Reject cursors older than 1 hour (prevents stale enumeration)
- Verify
tenantmatches authenticated user (if applicable) - Verify
query_hashmatches current query parameters (prevents cross-query cursor reuse)
Attack Prevention:
| Attack | Mitigation |
|---|---|
| Cursor tampering | HMAC signature verification |
| Cross-user cursor theft | Tenant binding in cursor data |
| Cross-query cursor reuse | Query hash binding |
| Stale cursor enumeration | 1-hour expiration |
Error Response:
Invalid cursors return:
v=rdb1;s=invalid;err=E017;d=Invalid or expired cursor
| Code | Status | Description |
|---|---|---|
E017 | invalid | Cursor validation failed |
Privacy Considerations
For privacy-sensitive namespaces:
totalfield SHOULD be omitted or return approximate value- Consider capping display at "100+" to prevent exact enumeration
Example
# First page
list.limit-50.resources.myapp.v1.resolvedb.net
-> {"items":[...],"cursor":"eyJsYXN0IjoiZm9vIn0","hasMore":true,"total":150}
# Next page (cursor must fit in 63-char DNS label)
list.cursor-eyJsYXN0IjoiZm9vIn0.resources.myapp.v1.resolvedb.net
-> {"items":[...],"cursor":"eyJsYXN0IjoiYmFyIn0","hasMore":true,"total":150}
# Last page
list.cursor-eyJsYXN0IjoiYmFyIn0.resources.myapp.v1.resolvedb.net
-> {"items":[...],"hasMore":false,"total":150}DNS Label Constraints
Cursors must fit within DNS label limits:
- Maximum 63 characters per label
- URL-safe Base64 encoding (no
+,/, or=) - For long cursors, use hash reference:
cursor-h-<hash>where hash points to stored cursor state
Large Data (NULL Records)
For data >4KB, use NULL record type (up to 64KB per record):
get.bigdata.tenant.v1.resolvedb.net TYPE=NULL
Amplification Attack Mitigation (CRITICAL)
NULL records present significant DDoS amplification risk:
- Minimum query size: ~32 bytes
- Maximum response size: 65,536 bytes
- Amplification factor: 2,048x
Mandatory Mitigations (per RFC 5358):
| Mitigation | Requirement | Implementation |
|---|---|---|
| Response Rate Limiting (RRL) | REQUIRED | Max 10 NULL responses/second per source /24 |
| TCP Fallback | REQUIRED | Responses >4KB MUST use TC bit, require TCP |
| Authentication | REQUIRED | NULL records require auth- token or API key |
| Source Validation | RECOMMENDED | BCP 38/84 ingress filtering |
Protocol Behavior:
# UDP query for large data:
Query: get.bigdata.tenant.v1.resolvedb.net TYPE=NULL (UDP)
Response: v=rdb1;s=toolarge;err=E015;d=Use TCP for responses >4KB;tc=1
# New error code:
E015 | toolarge | Response requires TCP | Yes | Retry over TCPSize Limits by Transport:
| Transport | Max Response | Behavior |
|---|---|---|
| UDP | 4,096 bytes | TC bit set if exceeded |
| TCP | 65,536 bytes | Full response allowed |
| DoH | 65,536 bytes | Full response allowed |
Rate Limits for NULL Records:
| Tier | NULL Records/sec | Burst | Notes |
|---|---|---|---|
| Unauthenticated | 0 | 0 | NULL requires auth |
| Free | 1 | 5 | Strict limit |
| Pro | 10 | 50 | Standard |
| Enterprise | 100 | 500 | High volume |
Chunking Protocol
# 1. Get manifest (includes per-chunk hashes for integrity)
get.manifest.bigfile.tenant.v1.resolvedb.net
-> {"chunks":5,"size":320000,"hash":"abc123def456789012345678901234567890123456789012345678901234","chunk_hashes":["hash0","hash1","hash2","hash3","hash4"]}
# 2. Retrieve chunks (can be parallel, format: chunk-index-total-hash)
# Hash reference MUST be at least 16 hex chars (64 bits)
get.chunk-0-5-abc123def4567890.bigfile.tenant.v1.resolvedb.net TYPE=NULL
get.chunk-1-5-abc123def4567890.bigfile.tenant.v1.resolvedb.net TYPE=NULL
...
# 3. Verify each chunk hash, then verify full content hash after reassemblyChunk Integrity Verification
Clients MUST:
- Verify each chunk's SHA-256 hash matches
chunk_hashes[index]before storing - Verify reassembled content SHA-256 matches manifest
hash - Reject chunks with mismatched hashes (do not retry automatically - may indicate MITM)
- Complete all chunks within 5 minutes or restart (prevents resource exhaustion)
Encryption Wire Format
For encrypted responses (t=encrypted), the following wire format is used.
AES-256-GCM Structure
┌─────────────────────────────────────────────────────────────┐
│ Encrypted Response │
├─────────────────────────────────────────────────────────────┤
│ Nonce (12 bytes) │ Ciphertext (variable) │ Tag (16 bytes) │
└─────────────────────────────────────────────────────────────┘| Component | Size | Description |
|---|---|---|
| Nonce | 12 bytes | Unique per encryption (random or counter-based) |
| Ciphertext | Variable | Encrypted payload |
| Auth Tag | 16 bytes | GCM authentication tag |
Key Derivation (CRITICAL)
Keys are derived using HKDF-SHA256 with mandatory context binding:
shared_secret = X25519(client_private, server_ephemeral_public)
OR X25519(server_private, client_public)
encryption_key = HKDF-SHA256(
ikm = shared_secret,
salt = "resolvedb-v1-encryption",
info = context_info, # MANDATORY - see below
len = 32
)Context Binding Requirements (MANDATORY):
The context_info field MUST include all of the following to prevent key reuse attacks:
| Component | Format | Purpose |
|---|---|---|
| Query FQDN | UTF-8 bytes | Prevents cross-query key reuse |
| Client ephemeral pubkey | 32 bytes | Binds to specific client |
| Server ephemeral pubkey | 32 bytes | Binds to specific response |
| Timestamp | 8 bytes (big-endian Unix epoch) | Prevents replay |
| Nonce | 8 bytes (random) | Additional entropy |
Context Construction:
context_info = concat(
length_prefix(query_fqdn), # 2-byte length + UTF-8 FQDN
client_ephemeral_pubkey, # 32 bytes
server_ephemeral_pubkey, # 32 bytes
timestamp_be64, # 8 bytes (Unix timestamp, big-endian)
random_nonce # 8 bytes (cryptographically random)
)Security Rationale:
Without complete context binding:
- Same FQDN from different clients could derive same key
- Responses could be replayed to different sessions
- Keys could be precomputed for known FQDNs
Implementation Check:
# CORRECT: Full context binding
context = (
len(fqdn).to_bytes(2, 'big') + fqdn.encode() +
client_pubkey + # 32 bytes
server_pubkey + # 32 bytes
timestamp_bytes + # 8 bytes
random_nonce # 8 bytes
)
# WRONG: Incomplete binding
context = fqdn.encode() # Missing keys, timestamp, nonceEphemeral Key Format
The k field in encrypted responses contains the server's ephemeral X25519 public key:
v=rdb1;s=ok;t=encrypted;e=aes256gcm;k=<base64-ephemeral-pubkey>;d=<base64-encrypted-payload>
| Field | Format | Description |
|---|---|---|
k | Base64 (32 bytes decoded) | Server ephemeral X25519 public key |
d | Base64 | Nonce + Ciphertext + Tag concatenated |
Complete Example
Response:
v=rdb1;s=ok;t=encrypted;e=aes256gcm;k=MCowBQYDK2VuAyEAe8RB0...;d=dGVzdCBub25jZQAAAA...
Decoding d:
Base64 decode -> raw_bytes
nonce = raw_bytes[0:12] # 12 bytes
ciphertext = raw_bytes[12:-16] # variable length
tag = raw_bytes[-16:] # 16 bytesDecryption:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# Derive shared secret from client private key and server ephemeral public
shared = x25519(client_private_key, server_ephemeral_public)
key = hkdf_sha256(shared, salt=b"resolvedb-v1-encryption", info=query_fqdn, length=32)
# Decrypt
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(nonce, ciphertext + tag, associated_data=None)Multi-TLD Root Server Redundancy
All four TLDs serve as active authoritative nameservers with identical data:
| TLD | Primary | Cross-Backup |
|---|---|---|
.com | ns1/ns2.resolvedb.com | ns-backup.resolvedb.net |
.net | ns1/ns2.resolvedb.net | ns-backup.resolvedb.org |
.org | ns1/ns2.resolvedb.org | ns-backup.resolvedb.io |
.io | ns1/ns2.resolvedb.io | ns-backup.resolvedb.com |
Client Failover
ROOT_SERVERS = ['resolvedb.com', 'resolvedb.net', 'resolvedb.org', 'resolvedb.io']
def query_with_redundancy(resource):
# Sort by health/latency
for tld in sorted_by_health(ROOT_SERVERS):
try:
result = dns_query(f"{resource}.{tld}")
mark_healthy(tld)
return result
except DNSError:
mark_unhealthy(tld)
continue
raise AllTLDsFailedError()Benefits
- TLD-level failure protection
- DDoS mitigation (attack one TLD, others continue)
- Load distribution across infrastructures
- Regulatory compliance (different jurisdictions)
- Performance optimization (clients choose fastest)
Security Protocol (RDBSP)
Layer 1: DNSSEC Foundation
- ECDSA P-256 (Algorithm 13) for all zones
- Automatic key rotation (ZSK: 30 days, KSK: 365 days)
- NSEC3 with salt for authenticated denial (see parameters below)
- Clients SHOULD verify AD flag
NSEC3 Parameters (RFC 5155, RFC 9276)
| Parameter | Value | Rationale |
|---|---|---|
| Hash Algorithm | SHA-1 (1) | Required by RFC 5155 |
| Iterations | 0-10 | Per RFC 9276 guidance (low for online signing) |
| Salt Length | 0-8 bytes | Random salt, rotate with ZSK |
| Opt-Out | Disabled | All names authenticated |
NSEC3PARAM Record:
resolvedb.net. NSEC3PARAM 1 0 10 <random-salt-hex>
Salt Rotation:
- Rotate salt with each ZSK rotation (30 days)
- Use cryptographically random salt (minimum 64 bits)
- Zero-length salt acceptable per RFC 9276
Iteration Count Guidance (RFC 9276):
- Online signing: 0-10 iterations (performance)
- Offline signing: Up to 100 iterations acceptable
- Higher iterations provide minimal security benefit but significant CPU cost
Layer 2: Content Integrity
- SHA-256 hash verification (minimum 16 chars, full recommended)
- Ed25519 signatures for authenticity
- Unix timestamps for replay protection (5-second max tolerance)
- Cryptographic nonces (MANDATORY for authenticated requests)
Replay Protection Requirements (CRITICAL)
Timestamp Tolerance:
| Context | Max Tolerance | Rationale |
|---|---|---|
| Authenticated requests | 5 seconds | Limits replay window |
| Unsigned public queries | 30 seconds | Allows for clock skew |
| Encrypted responses | 5 seconds | Bound to ephemeral keys |
Nonce Requirements:
For authenticated requests (auth-* prefix), clients MUST include a nonce:
get.auth-<jwt>.ts-<unix_timestamp>.nonce-<8-random-chars>.resource.namespace.v1.resolvedb.net
| Field | Format | Requirements |
|---|---|---|
ts- | Unix timestamp | Within 5 seconds of server time |
nonce- | 8 alphanumeric chars | Cryptographically random, unique per request |
Server-Side Tracking:
Servers MUST:
- Reject requests with
tsmore than 5 seconds from server time - Track
(nonce, ts)pairs for 10 seconds (2x tolerance window) - Reject duplicate
(nonce, ts)pairs withsecviolstatus - Use constant-time comparison for nonce matching
New Error Code:
| Code | Status | Description | Retryable | Recovery |
|---|---|---|---|---|
E016 | secviol | Replay attack detected | No | Generate new nonce |
Clock Synchronization:
Clients SHOULD:
- Use NTP or similar for time synchronization
- Include RTT estimate in tolerance calculations
- Retry with fresh timestamp on
E016(but not same nonce)
Layer 3: Encryption Modes
Public (Integrity Only):
- Plaintext data
- SHA-256 hash
- Ed25519 signature
- DNSSEC transportSymmetric (Shared Secret):
- AES-256-GCM encryption
- Pre-shared keys (out-of-band)
- Argon2id key derivation
- AEADAsymmetric (Public Key):
- X25519 key exchange
- ChaCha20-Poly1305 encryption
- Ephemeral keys (PFS)
- Public keys in TLSA recordsLayer 4: Query Privacy (CRITICAL)
Transport Security Requirements:
| Query Type | Transport Requirement | Enforcement |
|---|---|---|
auth-* prefix | DoH/DoT REQUIRED | Server MUST return secviol for plaintext |
user.* namespace | DoH/DoT REQUIRED | Server MUST return secviol for plaintext |
public.* namespace | DoH/DoT RECOMMENDED | Warning logged, query processed |
system.* namespace | Plaintext ALLOWED | Health checks allowed over UDP |
Server Enforcement:
Servers MUST detect transport type and enforce requirements:
# Plaintext query to protected namespace:
v=rdb1;s=secviol;err=E014;d=Encrypted transport required (DoH/DoT)New Error Code:
| Code | Status | Description | Retryable | Recovery |
|---|---|---|---|---|
E014 | secviol | Encrypted transport required | Yes | Retry over DoH/DoT |
Query Privacy Measures:
- Query pattern obfuscation via QNAME minimization (RFC 9156)
- Decoy queries for statistical privacy (implementation-specific)
- Padding to fixed sizes to prevent length-based analysis
Plaintext DNS Exposure Warning:
Queries over plaintext DNS expose to all network observers:
- User identifiers in namespace paths
- Resource names being accessed
- Access timing patterns
- Query frequency
Even with hash IDs (a7f3b2 instead of acme-corp), traffic analysis can correlate patterns.
Authentication
# JWT in auth- parameter (hyphen prefix, not colon)
get.auth-<jwt>.resource.namespace.v1.resolvedb.netAlgorithm Requirements (CRITICAL)
Allowed Algorithms:
| Algorithm | Use Case | Status |
|---|---|---|
EdDSA (Ed25519) | Primary signing algorithm | REQUIRED |
ES256 | ECDSA P-256 (legacy compatibility) | ALLOWED |
RS256 | RSA 2048+ (legacy compatibility) | ALLOWED |
Forbidden Algorithms:
| Algorithm | Reason | Action |
|---|---|---|
none | No signature | MUST reject with secviol |
HS256, HS384, HS512 | Symmetric key confusion risk | MUST reject |
PS256, PS384, PS512 | Implementation complexity | SHOULD reject |
Algorithm Confusion Prevention:
Implementations MUST:
- Explicit allowlist: Only process tokens with algorithms from the allowed list above
- Pre-parse validation: Check
algheader BEFORE any signature verification - Reject before decode: If
algis forbidden, reject immediately without attempting verification - Case-sensitive matching:
"alg": "None"and"alg": "NONE"MUST also be rejected - Key type binding: RSA keys MUST only verify RS*/PS* algorithms; EC keys MUST only verify ES* algorithms
Implementation Pattern:
# BEFORE any JWT library processing:
header = base64url_decode(token.split('.')[0])
if header.get('alg') in ['none', 'None', 'NONE', 'HS256', 'HS384', 'HS512']:
return error('secviol', 'E008', 'Forbidden algorithm')JWT Claims Specification
Required Claims:
| Claim | Type | Description |
|---|---|---|
sub | string | Subject (user ID or service ID) |
iss | string | Issuer (resolvedb.io or tenant issuer) |
aud | string | Audience (must include resolvedb.io) |
exp | integer | Expiration time (Unix timestamp) |
iat | integer | Issued at (Unix timestamp) |
nbf | integer | Not before (Unix timestamp) |
jti | string | JWT ID (unique token identifier for revocation) |
tenant | string | Namespace/tenant identifier |
scopes | array | Permission scopes (e.g., ["read", "write"]) |
Optional Claims:
| Claim | Type | Description |
|---|---|---|
rate_limit_tier | string | Override tier: free, pro, enterprise |
metadata | object | Arbitrary key-value metadata |
nonce | string | Replay protection nonce |
Example JWT Payload:
{
"sub": "user-12345",
"iss": "resolvedb.io",
"aud": "resolvedb.io",
"exp": 1704153600,
"iat": 1704067200,
"nbf": 1704067200,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tenant": "myapp",
"scopes": ["read", "write", "list"],
"rate_limit_tier": "pro"
}Token Transport Constraints
DNS labels are limited to 63 characters. JWT tokens typically exceed this limit.
Solutions:
-
Token Hash Reference (REQUIRED): Store token server-side, reference by cryptographic hash
get.auth-h-<32-hex-chars>.resource.namespace.v1.resolvedb.netSecurity Requirements:
- Token references MUST use HMAC-SHA256 with a server-side secret key
- Reference MUST be at least 128 bits (32 hex characters) to prevent brute-force
- Format:
auth-h-<first-32-hex-chars-of-HMAC-SHA256(server_secret, token)> - Server MUST maintain token-to-reference mapping with TTL matching token's
expclaim - References MUST be invalidated when corresponding token is revoked
- Server SHOULD rate-limit
auth-h-queries to prevent enumeration attacks
-
Short-Lived Tokens: Use compact tokens with minimal claims (max 5 minutes validity)
-
Multi-Label Split: Spread token across labels - NOT RECOMMENDED due to:
- Increased attack surface (multiple labels to intercept)
- Complex reassembly logic prone to implementation errors
- No integrity protection across labels
Recommended Pattern: Use the HTTP API to exchange a full JWT for a cryptographically-signed token reference, then use that reference in DNS queries. The reference exchange endpoint MUST require TLS 1.3+.
DNS Compliance (RFC 1035/1123)
Absolute Limits
- Max FQDN: 253 characters (excluding trailing dot, per RFC 1035 Section 2.3.4)
- Max label: 63 characters
- Max labels: 127 levels
- Valid chars:
a-z,A-Z,0-9,-(hyphen, not at label edges)
Important: Colons (:) are NOT valid DNS characters. UQRP uses hyphens (-) for encoding prefixes (e.g., b64- not b64:).
Case Normalization (CRITICAL)
Per RFC 1035 Section 2.3.3, DNS names are case-insensitive. Implementations MUST normalize consistently to prevent security issues.
Normalization Requirements:
| Component | Normalization Point | Rule |
|---|---|---|
| Query FQDN | Immediately at parse | Lowercase before ANY processing |
| Namespace | Immediately at extraction | Lowercase before authorization check |
| Cache key | After normalization | Use normalized form only |
| Auth comparison | All comparisons | Case-insensitive or pre-normalized |
Security Rationale:
Without consistent normalization, attackers can exploit case differences:
# Attack: Cache poisoning via case confusion
1. Victim caches response for: get.data.VICTIM.v1.resolvedb.net
2. Cache key uses: get.data.victim.v1.resolvedb.net (normalized)
3. Attacker queries with different case: get.data.Victim.v1.resolvedb.net
4. If parser extracts "Victim" but cache uses "victim", cross-user data leak
# Defense: Normalize BEFORE any extraction
namespace = extracted_namespace.to_lowercase() # FIRSTImplementation Pattern:
// CORRECT: Normalize immediately at parse
fn parse_query(qname: &str) -> Result<ParsedQuery> {
let normalized = qname.to_lowercase(); // FIRST OPERATION
let parts: Vec<&str> = normalized.split('.').collect();
// All subsequent operations use normalized form
}
// WRONG: Normalize only at cache time
fn get_cache_key(qname: &str) -> String {
qname.to_lowercase() // TOO LATE - parser may have used original case
}Length Budget
Base domain: resolvedb.net (12 chars)
Tenant: org1 (4-10 chars)
Version: v1 (2 chars)
Separators: (3-10 chars)
Safety margin: (10 chars)
─────────────────────────────────────────────────────
Reserved: (~35 chars)
Available for data: (~218 chars)Fallback Strategies
- Hash Reference: Store full data, query by hash
- Multi-Query: Split across queries
- Compression: Dictionary for common patterns
- Indirect: Short reference to full data
Rate Limits
| Tier | QPS/IP | QPS/Tenant | NULL Records |
|---|---|---|---|
| Free | 100 | N/A | 10/s |
| Pro | 1,000 | 10,000 | 100/s |
| Enterprise | 10,000 | 100,000 | 1,000/s |
Rate Limit Normalization (CRITICAL)
Rate limiting MUST use normalized queries to prevent bypass via case variations.
Normalization Before Rate Check:
# These MUST count as the SAME query for rate limiting:
get.data.VICTIM.v1.resolvedb.net
get.data.victim.v1.resolvedb.net
get.data.Victim.v1.resolvedb.net
GET.data.victim.v1.resolvedb.net # Operation caseRate Limit Key Construction:
rate_key = hash(
source_ip_prefix, # /24 for IPv4, /48 for IPv6
normalized_qname.to_lowercase(),
qtype,
qclass
)Separate Buckets (Do NOT Combine):
| Bucket | Purpose | Rationale |
|---|---|---|
| Standard queries | Normal operations | Base rate |
| NULL record queries | Large data | Amplification risk |
| Authenticated queries | Per-tenant | Different limits |
| Health checks | System monitoring | Higher allowance |
Bypass Prevention:
Attackers may attempt bypass via:
- Case variations - Mitigated by normalization
- Multiple query types - Separate buckets prevent mixing
- Distributed sources - /24 aggregation limits effectiveness
- Authenticated token rotation - Per-tenant limits apply
Rate Limit Response:
v=rdb1;s=ratelimit;err=E010;retry=60;ttl=1;d=Rate limit exceeded
Pluggable Provider Protocol (PPP)
Services can be implemented via MCPs or custom backends:
class ResolveDBProvider:
name: str
version: str
capabilities: ProviderCapabilities
def can_handle(self, query: DNSQuery) -> bool
def execute(self, query: DNSQuery) -> DNSResponse
def health_check(self) -> HealthStatusService Discovery:
_services.registry.resolvedb.net TXT "weather.v1,stock.v1,news.v1"
_meta.weather.v1.registry TXT "provider=OpenWeather;sla=99.9"
_health.weather.v1.registry TXT "status=healthy;latency=15ms"Location-Based Queries
Following the Privacy by Design principle, location-based queries REQUIRE explicit location parameters. The server does NOT infer location from the client's IP address.
Location Parameter Formats
# Named location (city, region)
get.city-newyork.weather.public.v1.resolvedb.net
# Coordinates via geo- prefix (decimals use 'd' separator)
# Format: lat-<lat>.lon-<lon> where decimals use 'd' separator
get.lat-40d7128.lon--74d0060.weather.public.v1.resolvedb.net
# ^^ double hyphen for negative longitude
# Coordinates via Base64-encoded JSON
# {"lat":40.7128,"lon":-74.0060} -> eyJsYXQiOjQwLjcxMjgsImxvbiI6LTc0LjAwNjB9
get.b64-eyJsYXQiOjQwLjcxMjgsImxvbiI6LTc0LjAwNjB9.weather.public.v1.resolvedb.netWhy Explicit Location?
| Implicit (WRONG) | Explicit (CORRECT) |
|---|---|
| Server infers from source IP | Client provides location |
| Breaks through VPNs/proxies | Works everywhere |
| Different results from different networks | Same query = same result |
| Privacy leak | Privacy preserved |
| Cache fragmentation (ECS scopes) | Fully cacheable |
Note: Colons (:) are not valid in DNS labels per RFC 1035. All parameters requiring special characters must use Base64 encoding or DNS-safe character substitution.
EDNS Client Subnet (ECS) Handling (RFC 7871)
Privacy Implications:
ECS exposes client subnet information to authoritative servers. This creates privacy concerns:
- Client location disclosed without explicit consent
- Cached responses may leak location to subsequent queries
- Third-party observers can correlate IP ranges to locations
Server Requirements:
| Requirement | Implementation | RFC Reference |
|---|---|---|
| Scope Prefix Handling | REQUIRED | RFC 7871 Section 7.3 |
| Privacy Mode Support | REQUIRED | RFC 7871 Section 12.3 |
| Opt-Out Mechanism | REQUIRED | Client can omit ECS |
Scope Prefix Behavior:
Servers MUST include SCOPE PREFIX-LENGTH in ECS responses to indicate caching granularity:
# Query includes ECS with /24 prefix
# Server responds with /16 scope (less specific = broader caching)
Client: ECS 192.0.2.0/24
Server: ECS 192.0.0.0/16 SCOPE 16
# Cached response valid for all 192.0.x.x clientsPrivacy Mode (ECS=0):
Clients MAY send ECS with SOURCE PREFIX-LENGTH=0 to indicate privacy preference:
- Server MUST NOT use client subnet for response
- Server MUST respond with SCOPE PREFIX-LENGTH=0
- Response is cached globally (no location variance)
# Privacy mode query
Client: ECS 0.0.0.0/0 (SOURCE=0)
Server: ECS 0.0.0.0/0 SCOPE=0GeoIP Privacy Considerations:
geoipoperation responses MUST use short TTL (30-60 seconds)- GeoIP should NOT be served via shared recursive resolvers
- For accurate, privacy-preserving geolocation, clients should query authoritative directly over DoH
- The
geoipoperation SHOULD NOT cache at intermediate resolvers
Cache Scope Pollution Prevention:
Implementations MUST separate cache entries by ECS scope:
Cache Key = (QNAME, QTYPE, QCLASS, ECS_SCOPE_PREFIX)
# Different cache entries:
weather.public.v1.resolvedb.net:TXT:IN:192.0.0.0/16
weather.public.v1.resolvedb.net:TXT:IN:198.51.0.0/16
weather.public.v1.resolvedb.net:TXT:IN:GLOBAL # ECS=0 responseTTL Cache Delegation
ResolveDB's core scalability advantage: leveraging the global DNS caching infrastructure to achieve massive query reduction.
RFC References: TTL semantics per RFC 1035 Section 3.2.1, negative caching per RFC 2308, stale serving per RFC 8767.
The Caching Multiplier
When ResolveDB returns a response with ttl=3600, every resolver in the DNS hierarchy caches that response independently:
┌─────────────────────────────────────────────────────────────────┐
│ DNS CACHING HIERARCHY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Browser │ TTL respected per response │
│ │ DNS Cache │ (Chrome, Firefox, Safari cache DNS) │
│ └──────┬───────┘ │
│ │ cache miss │
│ ▼ │
│ ┌──────────────┐ │
│ │ OS DNS │ System resolver cache │
│ │ Cache │ (macOS, Windows, Linux systemd-resolved) │
│ └──────┬───────┘ │
│ │ cache miss │
│ ▼ │
│ ┌──────────────┐ │
│ │ Corporate │ Serves entire organization │
│ │ DNS Server │ (1000s of users share this cache) │
│ └──────┬───────┘ │
│ │ cache miss │
│ ▼ │
│ ┌──────────────┐ │
│ │ ISP │ Serves millions of subscribers │
│ │ Recursive │ (Comcast, AT&T, regional ISPs) │
│ └──────┬───────┘ │
│ │ cache miss │
│ ▼ │
│ ┌──────────────┐ │
│ │ Public DNS │ Global scale (8.8.8.8, 1.1.1.1) │
│ │ (optional) │ Serves tens of millions │
│ └──────┬───────┘ │
│ │ cache miss │
│ ▼ │
│ ┌──────────────┐ │
│ │ ResolveDB │ <--- ONLY SEES CACHE MISSES │
│ │ Authoritative│ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘Example: If 10,000 users behind a corporate DNS query weather.public.v1.resolvedb.net with ttl=3600:
- Traditional API: 10,000 requests/hour to origin
- ResolveDB: 1 request/hour (all others served from corporate DNS cache)
Key insight: ResolveDB query volume scales with:
(Number of UNIQUE queries) x (1 / TTL)
NOT with total number of clients or total request volume.
TTL Classes
Server assigns TTL based on data volatility classification:
| Class | TTL | Use Case |
|---|---|---|
immutable | 604800 (7 days) | Hash-addressed content (h-<hash>), versioned data |
stable | 86400 (24 hours) | Reference data, public datasets, documentation |
standard | 3600 (1 hour) | Default for most data, weather forecasts, news |
dynamic | 300 (5 min) | User data, frequently changing content |
volatile | 30-60 sec | Real-time status, live feeds, health checks |
nocache | 0 | Write confirmations, errors, rate limit responses |
Operation TTL Defaults
| Operation | Default TTL | Notes |
|---|---|---|
get | 3600 | Per-resource override based on data classification |
put | 0 | Write confirmation, not cacheable |
delete | 0 | Delete confirmation, not cacheable |
info | 3600 | Metadata is stable |
list | 300 | Listings change with additions |
search | 60 | Results may be contextual/personalized |
health | 30 | Must reflect current state |
geoip | 300 | Location data is relatively stable |
watch | 0 | Streaming endpoint, not DNS-cacheable |
TTL Selection Guidelines
For API Consumers:
- Static configuration: Use
stable(24h) or include version in query path - User data: Use
dynamic(5min) - accept predictable staleness window - Real-time feeds: Use
volatile(30-60s) or switch to streaming (watch) - Content-addressed: Use
immutable(7 days) forh-<hash>queries
Special Cases:
| Case | TTL Behavior |
|---|---|
Authenticated (auth- prefix) | Short TTL (60-300s) - prevents stale sessions in shared caches |
notfound (404) | SOA MINIMUM (3600s) - negative cache prevents repeated queries |
ratelimit (429) | TTL=1 - signal immediate retry |
error (500) | TTL=0 - don't cache server failures |
unavail (503) | TTL=0 - transient, retry immediately |
Chunked data (chunk- prefix) | Same TTL across all chunks - prevents partial staleness |
Authenticated Query Cache Exclusion (CRITICAL)
Authenticated queries MUST NOT be cached to prevent cross-user data leakage.
Detection Requirements:
Implementations MUST detect authenticated queries through BOTH methods:
| Detection Method | Coverage | Fallback |
|---|---|---|
| Full query parsing | Primary - extracts auth_token from parsed query | Required |
| String matching | Secondary - checks for .auth- in QNAME | Backup only |
Fail-Secure Behavior:
fn is_authenticated_query(query: &DNSQuery) -> bool {
// PRIMARY: Parse the query structure
match parse_resolvedb_query(query) {
Ok(parsed) => parsed.auth_token.is_some(),
Err(_) => true, // FAIL SECURE: If parsing fails, assume authenticated
}
}Security Rationale:
Without strict auth detection:
- Encoded auth tokens (
b64-<token-with-auth-inside>) bypass string matching - Malformed queries may cache and serve to unauthorized users
- Cross-user cache poisoning becomes possible
Cache Key Requirements for Authenticated Queries:
If authenticated responses MUST be cached (e.g., for performance):
- Cache key MUST include full token hash or user identifier
- Cache entries MUST be isolated per authenticated session
- TTL MUST NOT exceed token expiration minus clock skew margin
Negative Caching (RFC 2308)
ResolveDB returns negative responses in two forms:
| Response | RCODE | Meaning | TTL Source |
|---|---|---|---|
| NXDOMAIN | 3 | Name does not exist | min(SOA TTL, SOA MINIMUM) = 3600s |
| NODATA | 0 (empty answer) | Name exists, but not for queried type | Same as NXDOMAIN |
Both responses include the SOA record in the authority section (required for caching per RFC 2308 Section 3). Negative caching prevents repeated queries for non-existent resources and defends against DNS water torture attacks.
TTL=0 Behavior Notes
TTL=0 signals "do not cache" per RFC 1035, but resolver implementations vary:
| Resolver | TTL=0 Behavior |
|---|---|
| BIND | Caches briefly (~1 second) for loop prevention |
| Unbound | Respects TTL=0, no cache |
| Cloudflare (1.1.1.1) | Respects TTL=0 |
| Google (8.8.8.8) | May serve stale data (RFC 8767) |
| Windows DNS Client | Minimum 1-second cache |
| Browser caches | Often apply minimum TTL regardless |
For truly uncacheable operations (put, delete, errors), TTL=0 is correct but clients should not assume zero latency for repeated queries. Write confirmations should use response signatures for verification regardless of caching.
Resolver TTL Capping
Some public resolvers cap maximum TTL values:
| Resolver | Max TTL |
|---|---|
| Google Public DNS | 86400 (1 day) |
| Cloudflare | 604800 (7 days) |
| Most ISP resolvers | 86400-604800 |
The immutable class (7 days) may be capped to 1 day by some resolvers. For hash-addressed content (h-<hash>), this is acceptable since re-queries return identical data.
GeoIP/ECS Considerations
When responses vary by client location using EDNS Client Subnet (RFC 7871), caching becomes scope-limited. A response served to NYC clients is not cacheable for LA clients, reducing the caching multiplier but maintaining correctness.
Implementation Status
| Feature | Status | Notes |
|---|---|---|
| TTL in response format | Implemented | ttl=<seconds> in TXT metadata |
| SOA MINIMUM for negative cache | Implemented | 3600s default |
| Cache respects response TTL | Implemented | Extracts from first answer, clamps to ttl_min/ttl_max |
| Per-operation TTL defaults | Implemented | ttl_for_operation() in constants.rs, determine_ttl() in authority.rs |
| TTL class constants | Implemented | TTL_IMMUTABLE (7d), TTL_STABLE (24h), TTL_STANDARD (1h), TTL_DYNAMIC (5m), TTL_VOLATILE (30-60s) |
| Error-specific TTL (0 for failures) | Implemented | determine_ttl() returns TTL_NOCACHE for errors |
| Auth query TTL reduction | Implemented | determine_ttl() returns TTL_AUTH_MAX (300s), cache excludes auth- queries |
| Rate limit TTL | Implemented | TTL_RATELIMIT (1s) via RateAction::suggested_ttl() |
Protocol Evolution
Version Negotiation
Negotiation Process:
- Client queries with preferred version:
get.weather.public.v2.resolvedb.net - If server doesn't support v2, respond with redirect:
v=rdb1;s=redirect;supported=v1;d=get.weather.public.v1.resolvedb.net - Client retries with supported version
Anti-Downgrade Protection (CRITICAL):
Attackers may force clients to use older, vulnerable protocol versions:
| Protection | Implementation |
|---|---|
| Maximum downgrade | Clients MUST NOT downgrade more than one major version |
| Minimum version | Servers advertise min_version in health endpoint |
| Version pinning | Clients MAY pin to specific versions for security-critical operations |
Server Health Includes Version Info:
_health.system.resolvedb.net TXT "v=rdb1;status=healthy;versions=v1,v2;min_version=v1;recommended=v2"
Client Behavior:
def negotiate_version(preferred: str, server_supported: list) -> str:
# Check minimum version requirement
if server_min_version > client_minimum_acceptable:
raise VersionError("Server requires newer client")
# Only downgrade one major version
pref_major = int(preferred[1:])
for v in sorted(server_supported, reverse=True):
v_major = int(v[1:])
if v_major >= pref_major - 1: # Max 1 version downgrade
return v
raise VersionError("No acceptable version available")Backward Compatibility Rules
| Change Type | Allowed | Requires |
|---|---|---|
| Adding optional fields | Yes | Minor version bump |
| Adding new status codes | Yes | Clients treat unknown as error |
| Adding new operations | Yes | Clients return E002 for unknown |
| Adding new encoding prefixes | Yes | Clients reject unknown prefixes |
| Removing required fields | NO | New major version |
| Changing field semantics | NO | New major version |
| Changing status code meanings | NO | New major version |
| Changing encoding prefix format | NO | New major version |
Deprecation
_deprecation.old-service.v1.public.resolvedb.net TXT "deprecated=true;sunset=2025-12-31;migrate=new-service.v2"
Deprecation Timeline:
- Deprecation announcement: 6 months before sunset
- Warning responses: 3 months before sunset (include
deprecation=truein responses) - Sunset: Return
redirectto new version
Domain Portfolio
| Domain | Purpose | Status |
|---|---|---|
| resolvedb.net | DNS resolver (primary) | Active |
| resolvedb.io | HTTP API & Web | Active |
| resolvedb.com | Root redundancy | Reserved |
| resolvedb.org | Root redundancy | Reserved |
| resolvedb.dev | Developer portal, sandbox | Planned |
| resolvedb.cloud | CDN, blob storage | Planned |
| resolvedb.app | Dashboard, UI | Planned |
| resolvedb.ca | Canadian data residency | Reserved |
Current usage:
- DNS queries:
*.resolvedb.net(all DNS resolution) - HTTP API:
api.resolvedb.io(write operations, management) - Web dashboard:
resolvedb.io(future)