WHOIS is dead, long live RDAP
The whois protocol is dead. For decades, it was a fundamental tool for network reconnaissance, but its time has passed. The protocol was officially sunset for all generic top-level domains in early 2025, replaced by the more modern, web-based protocol, RDAP.

So why talk about WHOIS now? To pay our respects. Because the WHOIS protocol is so simple, it makes a perfect case study for basic network programming and a window into an earlier era of the internet. To help memorialize this piece of internet history, we will build a tiny implementation from scratch and understand why its death was necessary in the process.
What is WHOIS?
Think of WHOIS as the internet’s public directory. Its primary job is to resolve a domain name into a set of administrative details.
When you query example.com, you aren’t asking for the website content; you are asking for the paper trail. A standard response returns the Registrar (the vendor, such as Namecheap or GoDaddy), the Name Servers (which direct traffic), and key dates regarding the domain’s creation and expiration. In the early days of the web, this output also listed the owner’s full name, address, and phone number. Today, privacy regulations like GDPR have largely forced that personal information behind generic “Redacted for Privacy” placeholders.
Who provides this data?
WHOIS data is a requirement enforced by ICANN (Internet Corporation for Assigned Names and Numbers). ICANN sets the rules, Registries (like Verisign for .com) manage the master lists for their TLDs, and Registrars (like Google Domains or Namecheap) sell the names to you. Domain registrars are contractually obligated to maintain this registration data and make it available to the public.
Why do we need it?
While often used by developers to check if a cool side project name is taken, WHOIS is critical infrastructure for maintaining the internet’s health.
Despite these redactions, the protocol remains vital for security. ICANN mandates that every domain record must publicly display an abuse contact email and phone number. This provides a direct line for network operators to report domains hosting malware, phishing schemes, or spam.
Security researchers have also pivoted their tactics. instead of looking for a specific person, they look for digital fingerprints. If a cluster of 500 suspicious domains appears on the network, registered simultaneously via the same obscure Name Server and Registrar, it strongly suggests a coordinated botnet. You don’t need to know the name of the attacker to know the assets are connected.
Investigative journalists use historical WHOIS data to map state-sponsored disinformation campaigns. For modern investigations, RDAP introduces “tiered access,” theoretically allowing vetted professionals to request unredacted data for legitimate purposes, though this process is still maturing. Also, a new initiative called RDRS aims to standardize access to nonpublic registration data for legitimate purposes.
How WHOIS Works
The WHOIS protocol, defined in RFC 3912, is a simple exchange over a TCP connection.
- CONNECT: The client opens a TCP socket to a WHOIS server (on port 43).
- ASK: The client sends the query: a single line of text like
example.com, terminated by a carriage return and line feed (<CR><LF>). - RESPONSE: The server streams back the registration data as plain text.
- DISCONNECT: The server kills the connection.
There are no headers, no authentication, and no complex data formats (more on that later). It is quite literally one of the simplest protocols imaginable. This simplicity makes it a good candidate for a small project to demonstrate basic networking concepts.
Building a WHOIS Server
Let’s turn theory into code. Because the protocol is so trivial, we can implement a functional server in Go using a few lines of code and the Go standard library. We can then verify that it works using the tools already installed on your machine, like telnet or the whois command itself.
WHOIS Server Implementation
We’ll add a records map to hold fake domain data and implement a handleConnection function to process queries and send back the corresponding record.
whois-server/main.go (click to expand)View on GitHub
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
// whoisData now holds a single, static WHOIS record for debugging purposes.
var whoisData = map[string]string{
"google.com": `Domain Name: google.com
Registrar: My Go Server
Creation Date: 2025-12-15T00:00:00Z
`,
"example.com": `Domain Name: example.com
Registrar: My Go Server
Creation Date: 2025-12-15T00:00:00Z
`,
}
func handleConnection(conn net.Conn) {
log.Printf("new connection from %s", conn.RemoteAddr())
defer log.Printf("connection to %s closed", conn.RemoteAddr())
defer conn.Close()
scanner := bufio.NewScanner(conn)
var clientQuery string
// Read the first line from the client.
if scanner.Scan() {
clientQuery = strings.TrimSpace(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Printf("Error reading from client: %v", err)
return
}
if clientQuery == "" {
log.Printf("Client disconnected without sending a query or sent an empty query.")
// Send a minimal response in case the client expects *something*
_, err := fmt.Fprint(conn, "No query provided.\r\n")
if err != nil {
log.Printf("Error writing empty query response: %v", err)
}
return
}
log.Printf("Received query: %q", clientQuery)
var response string
if data, ok := whoisData[strings.ToLower(clientQuery)]; ok {
response = strings.ReplaceAll(data, "\n", "\r\n")
} else {
response = fmt.Sprintf("No match for %s\r\n", clientQuery)
}
// Write the response and close the connection.
_, err := fmt.Fprint(conn, response)
if err != nil {
log.Printf("Error writing response to client: %v", err)
}
}
func main() {
addr := ":43"
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Error listening: %v", err)
}
defer listener.Close()
log.Printf("Static WHOIS server listening on %s", addr)
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Error accepting connection: %v", err)
continue
}
go handleConnection(conn)
}
}
To run the server, execute:
go run ./whois-server
With the server running, you can now test it with telnet and whois:
telnet localhost 43
# Trying ::1...
# Connected to localhost.
# Escape character is '^]'.
# example.com
# Domain Name: example.com
# Registrar: My Go Server
# Creation Date: 2025-12-15T00:00:00Z
# Connection closed by foreign host.
whois -h localhost google.com
# Domain Name: google.com
# Registrar: My Go Server
# Creation Date: 2025-12-15T00:00:00Z
Real-World Complications
Our server works for the domains stored in its local records map, but the real WHOIS system is a distributed, federated system of registries and registrars, not a single database.
This leads to concepts like “thin” and “thick” lookups. A “thick” registry (like .org) holds all the data, and one query is enough. A “thin” registry (like .com) only knows which registrar manages a domain (e.g., GoDaddy, Namecheap). A whois client querying a “thin” registry gets a referral and must make a second query to the correct registrar’s WHOIS server to get the full details.
This system is brittle, relying on parsing unstructured text to find the referral server. Classic whois clients, such as the rfc1036/whois, handle this by scanning each line of text for known referral markers using functions like find_referral_server_iana. This approach works, but it is fragile because every registry formats output differently. The brittleness of parsing free-form text was a key driver to replace it with a modern protocol that uses structured data like JSON.
RDAP: The Modern Successor
The push to replace it began back in 2013, when an ICANN Expert Working Group recommended that the WHOIS protocol should be tossed out. They proposed a system that would keep information secret from most users, disclosing data only for specific “permissible purposes” like legal actions or trademark enforcement. Notably, journalism was excluded from this list, despite WHOIS historically being a key tool for investigative reporting.
After years of debate and voting, the transition became official. On January 28, 2025, WHOIS was officially sunset for generic Top-Level Domains (gTLDs). Registries are no longer required to support it, and the industry has shifted its focus to RDAP (Registration Data Access Protocol).
RDAP performs the same function as WHOIS but it uses HTTPS and returns JSON instead of plain text.
| Feature | WHOIS | RDAP |
|---|---|---|
| Transport | TCP Port 43 | HTTP/HTTPS (Port 80/443) |
| Format | Unstructured Plain Text | Structured JSON (machine-readable) |
| Security | None | Standard Web Security (TLS, Auth) |
| Discovery | Brittle, text-based referrals | Standardized discovery. |
You can try it with curl:
curl -L https://rdap.verisign.com/com/v1/domain/google.com
Output (click to expand)
{
"objectClassName": "domain",
"handle": "2138514_DOMAIN_COM-VRSN",
"ldhName": "GOOGLE.COM",
"links": [
{
"value": "https://rdap.verisign.com/com/v1/domain/GOOGLE.COM",
"rel": "self",
"href": "https://rdap.verisign.com/com/v1/domain/GOOGLE.COM",
"type": "application/rdap+json"
},
{
"value": "https://rdap.markmonitor.com/rdap/domain/GOOGLE.COM",
"rel": "related",
"href": "https://rdap.markmonitor.com/rdap/domain/GOOGLE.COM",
"type": "application/rdap+json"
}
],
"status": [
"client delete prohibited",
"client transfer prohibited",
"client update prohibited",
"server delete prohibited",
"server transfer prohibited",
"server update prohibited"
],
"entities": [
{
"objectClassName": "entity",
"handle": "292",
"roles": [
"registrar"
],
"links": [
{
"href": "http://www.markmonitor.com",
"type": "text/html",
"value": "https://rdap.markmonitor.com/rdap/",
"rel": "about"
}
],
"publicIds": [
{
"type": "IANA Registrar ID",
"identifier": "292"
}
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
"MarkMonitor Inc."
]
]
],
"entities": [
{
"objectClassName": "entity",
"roles": [
"abuse"
],
"vcardArray": [
"vcard",
[
[
"version",
{},
"text",
"4.0"
],
[
"fn",
{},
"text",
""
],
[
"tel",
{
"type": "voice"
},
"uri",
"tel:+1.2086851750"
],
[
"email",
{},
"text",
"[email protected]"
]
]
]
}
]
}
],
"events": [
{
"eventAction": "registration",
"eventDate": "1997-09-15T04:00:00Z"
},
{
"eventAction": "expiration",
"eventDate": "2028-09-14T04:00:00Z"
},
{
"eventAction": "last changed",
"eventDate": "2019-09-09T15:39:04Z"
},
{
"eventAction": "last update of RDAP database",
"eventDate": "2025-12-16T20:15:07Z"
}
],
"secureDNS": {
"delegationSigned": false
},
"nameservers": [
{
"objectClassName": "nameserver",
"ldhName": "NS1.GOOGLE.COM"
},
{
"objectClassName": "nameserver",
"ldhName": "NS2.GOOGLE.COM"
},
{
"objectClassName": "nameserver",
"ldhName": "NS3.GOOGLE.COM"
},
{
"objectClassName": "nameserver",
"ldhName": "NS4.GOOGLE.COM"
}
],
"rdapConformance": [
"rdap_level_0",
"icann_rdap_technical_implementation_guide_1",
"icann_rdap_response_profile_1"
],
"notices": [
{
"title": "Terms of Service",
"description": [
"Service subject to Terms of Use."
],
"links": [
{
"href": "https://www.verisign.com/domain-names/registration-data-access-protocol/terms-service/index.xhtml",
"type": "text/html",
"value": "https://rdap.verisign.com/com/v1/domain/google.com",
"rel": "terms-of-service"
}
]
},
{
"title": "Status Codes",
"description": [
"For more information on domain status codes, please visit https://icann.org/epp"
],
"links": [
{
"href": "https://icann.org/epp",
"type": "text/html"
}
]
},
{
"title": "RDDS Inaccuracy Complaint Form",
"description": [
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"
],
"links": [
{
"href": "https://icann.org/wicf",
"type": "text/html",
"value": "https://rdap.verisign.com/com/v1/domain/google.com",
"rel": "help"
}
]
}
]
}
The response is a structured JSON object that is far easier to parse than the free-form text of WHOIS.
Why .dev domains don’t work
If you try to run a legacy WHOIS lookup against a modern TLD like .dev, you will likely hit a dead end. Google, along with many newer registries, has effectively deprecated port 43. They are not required to support the old text-based protocol, so they don’t.
Instead, querying a .dev domain via the command line often returns a generic placeholder from IANA. It tells you who manages the .dev registry, but it won’t tell you anything about the specific domain you asked for (like kmcd.dev).
$ whois kmcd.dev
# % IANA WHOIS server
# % for more information on IANA, visit http://www.iana.org
# % This query returned 1 object
# domain: DEV
# organisation: Charleston Road Registry Inc.
# ...
# remarks: Registration information: https://www.registry.google
# source: IANA
To get the actual data, you are supposed to use rdap. As shown below, the rdap command retrieves the full registration details you would expect:
rdap kmcd.dev
Output (click to expand)
Domain:
Domain Name: kmcd.dev
Handle: E04E36511-DEV
Status: client transfer prohibited
Conformance: rdap_level_0
Conformance: icann_rdap_response_profile_1
Conformance: icann_rdap_technical_implementation_guide_1
Notice:
Title: RDAP Terms of Service
Description: By querying our Domain Database as part of the RDAP pilot program (RDAP Domain Database), you are agreeing to comply with these terms and acknowledging that your information will be used in accordance with Charleston Road Registry's Privacy Policy (https://www.registry.google/about/privacy.html), so please read the terms and Privacy Policy carefully.
Description: Any information provided is 'as is' without any guarantee of accuracy.
Description: Please do not misuse the RDAP Domain Database. It is intended solely for query-based access on an experimental basis and should not be used for or relied upon for any other purpose.
Description: Don't use the RDAP Domain Database to allow, enable, or otherwise support the transmission of mass unsolicited, commercial advertising or solicitations.
Description: Don't access our RDAP Domain Database through the use of high volume, automated electronic processes that send queries or data to the systems of Charleston Road Registry or any ICANN-accredited registrar.
Description: You may only use the information contained in the RDAP Domain Database for lawful purposes.
Description: Do not compile, repackage, disseminate, or otherwise use the information contained in the RDAP Domain Database in its entirety, or in any substantial portion, without our prior written permission.
Description: We may retain certain details about queries to our RDAP Domain Database for the purposes of detecting and preventing misuse.
Description: We reserve the right to restrict or deny your access to the RDAP Domain Database if we suspect that you have failed to comply with these terms.
Description: We reserve the right to modify or discontinue our participation in the RDAP pilot program and suspend or terminate access to the RDAP Domain Database at any time and for any reason in our sole discretion.
Description: Reminder that underlying Registrant data may be requested via ICANN's RDRS service (https://rdrs.icann.org/).
Description: We reserve the right to modify this agreement at any time.
Link: https://pubapi.registry.google/rdap/help/tos
Link: https://www.registry.google/policies/rdap-terms/
Notice:
Title: Status Codes
Description: For more information on domain status codes, please visit https://icann.org/epp
Link: https://icann.org/epp
Notice:
Title: RDDS Inaccuracy Complaint Form
Description: URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf
Link: https://icann.org/wicf
Link: https://pubapi.registry.google/rdap/domain/kmcd.dev
Link: https://rdap.cloudflare.com/rdap/v1/domain/kmcd.dev
Event:
Action: registration
Actor: cloudflare
Date: 2024-05-18T19:33:01.182Z
Event:
Action: expiration
Date: 2034-05-18T19:33:01.182Z
Event:
Action: last update of RDAP database
Date: 2025-12-16T22:15:17.386Z
Event:
Action: last changed
Date: 2025-10-08T17:07:04.569Z
Secure DNS:
Zone Signed: true
Delegation Signed: true
DSData:
Key Tag: 2371
Algorithm: 13
Digest: 321F88BA26AB76AE885C06671798645ADF4D06448F52D67D627641FA28392AF7
DigestType: 2
Entity:
Handle: 1910
Public ID:
Type: IANA Registrar ID
Identifier: 1910
Remark:
Title: Incomplete Data
Type: object truncated due to unexplainable reasons
Description: Summary data only. For complete data, send a specific query for the object.
Link: https://pubapi.registry.google/rdap/entity/1910
Link: None
Role: registrar
vCard version: 4.0
vCard fn: CloudFlare, Inc.
Entity:
Status: active
Role: abuse
vCard version: 4.0
vCard fn: Abuse Team
vCard tel: tel:+1.4153197517
vCard email: [email protected]
Nameserver:
Nameserver: chuck.ns.cloudflare.com
Handle: 13F5B5F1_HOW-GOOGLE
Remark:
Title: Incomplete Data
Type: object truncated due to unexplainable reasons
Description: Summary data only. For complete data, send a specific query for the object.
Link: https://pubapi.registry.google/rdap/nameserver/chuck.ns.cloudflare.com
Nameserver:
Nameserver: oaklyn.ns.cloudflare.com
Handle: 40ADE79A0-GOOGLE
Remark:
Title: Incomplete Data
Type: object truncated due to unexplainable reasons
Description: Summary data only. For complete data, send a specific query for the object.
Link: https://pubapi.registry.google/rdap/nameserver/oaklyn.ns.cloudflare.com
Even though rdap works, it isn’t installed by default on most systems. Many people are probably going to forget to install rdap or will just default to whois out of habit. I figured that one way to get the old whois command working again is by making a proxy that speaks the WHOIS protocol to the client and will fetch the data using RDAP.
Building a WHOIS-to-RDAP Proxy
Now, I will walk you through a WHOIS server that acts as a proxy to other RDAP servers. It will listen for WHOIS queries on port 43 and when it receives a query, it will make an HTTPS request to the appropriate RDAP server, parse the JSON response, format the important details into a human-readable text format, and send that text back to the original WHOIS client. Simple, no?
This approach makes RDAP-only domains accessible to legacy tools that only speak the classic WHOIS protocol. Although, let’s be honest, you should probably just use the existing rdap command for anything serious. This is just a toy. But it was fun to make.

Here is the implementation of our new WHOIS->RDAP proxy server:
whois-server-proxy/main.go (click to expand)View on GitHub
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"text/template"
"time"
_ "embed"
)
//go:embed rdap.template
var rdapTemplateContent string
// tldRdapServers provides a direct mapping for common TLDs to their RDAP servers.
var tldRdapServers = map[string]string{
"com": "https://rdap.verisign.com/com/v1/domain/",
"net": "https://rdap.verisign.com/net/v1/domain/",
"org": "https://rdap.publicinterestregistry.org/rdap/domain/",
"dev": "https://pubapi.registry.google/rdap/domain/",
}
// --- RDAP Data Structures ---
type RDAPLink struct {
Rel string `json:"rel"`
Href string `json:"href"`
Type string `json:"type"`
}
type RDAPEvent struct {
Action string `json:"eventAction"`
Actor string `json:"eventActor"`
Date time.Time `json:"eventDate"`
}
type RDAPNameserver struct {
LDHName string `json:"ldhName"`
Handle string `json:"handle"`
Remarks []struct {
Title string `json:"title"`
Type string `json:"type"`
Description []string `json:"description"`
} `json:"remarks"`
Links []RDAPLink `json:"links"`
}
type RDAPEntity struct {
VCardArray VCard `json:"vcardArray"`
Roles []string `json:"roles"`
Entities []RDAPEntity `json:"entities"`
Handle string `json:"handle"`
PublicIDs []struct {
Type string `json:"type"`
Identifier string `json:"identifier"`
} `json:"publicIds"`
Remarks []struct {
Title string `json:"title"`
Type string `json:"type"`
Description []string `json:"description"`
} `json:"remarks"`
Links []RDAPLink `json:"links"`
Status []string `json:"status"`
}
type VCard []interface{}
func (vc VCard) GetField(key string) string {
if len(vc) < 2 {
return ""
}
properties, ok := vc[1].([]interface{})
if !ok {
return ""
}
for _, prop := range properties {
propertyArray, ok := prop.([]interface{})
if !ok || len(propertyArray) < 4 {
continue
}
propKey, ok := propertyArray[0].(string)
if !ok || propKey != key {
continue
}
val, ok := propertyArray[3].(string)
if ok {
return val
}
}
return ""
}
type SecureDNSData struct {
Algorithm int `json:"algorithm"`
Digest string `json:"digest"`
DigestType int `json:"digestType"`
KeyTag int `json:"keyTag"`
}
type RDAPResponse struct {
LDHName string `json:"ldhName"`
Handle string `json:"handle"`
Nameservers []RDAPNameserver `json:"nameservers"`
Events []RDAPEvent `json:"events"`
Entities []RDAPEntity `json:"entities"`
Links []RDAPLink `json:"links"`
Status []string `json:"status"`
Conformance []string `json:"rdapConformance"`
Notices []struct {
Title string `json:"title"`
Description []string `json:"description"`
Links []RDAPLink `json:"links"`
} `json:"notices"`
SecureDNS struct {
ZoneSigned bool `json:"zoneSigned"`
DelegationSigned bool `json:"delegationSigned"`
DSData []SecureDNSData `json:"dsData"`
} `json:"secureDNS"`
Remarks []struct {
Title string `json:"title"`
Description []string `json:"description"`
} `json:"remarks"`
}
func (r *RDAPResponse) getReferralURL() string {
for _, link := range r.Links {
if link.Rel == "related" && link.Type == "application/rdap+json" {
return link.Href
}
}
return ""
}
// Server holds the dependencies for the WHOIS server.
type Server struct {
rdapTemplate *template.Template
}
// NewServer creates a new server and parses the RDAP template.
func NewServer() (*Server, error) {
tmpl, err := template.New("rdap").Parse(rdapTemplateContent)
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
return &Server{rdapTemplate: tmpl}, nil
}
// queryRDAP performs the RDAP lookup, following one level of referral if necessary.
func queryRDAP(domain string) (*RDAPResponse, error) {
parts := strings.Split(domain, ".")
var url string
if len(parts) > 1 {
tld := parts[len(parts)-1]
if baseUrl, ok := tldRdapServers[tld]; ok {
url = baseUrl + domain
log.Printf("Found direct RDAP server for TLD .%s, using: %s", tld, url)
}
}
if url == "" {
url = "https://rdap.iana.org/domain/" + domain
log.Printf("Using IANA bootstrap RDAP endpoint: %s", url)
}
// Perform the initial query
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("RDAP request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("RDAP server returned status %d: %s", resp.StatusCode, string(body))
}
var initialResponse RDAPResponse
if err := json.NewDecoder(resp.Body).Decode(&initialResponse); err != nil {
return nil, fmt.Errorf("failed to decode initial RDAP JSON: %w", err)
}
resp.Body.Close() // Close the body of the first response now.
// Check for a referral and follow it
if referralURL := initialResponse.getReferralURL(); referralURL != "" {
log.Printf("Following RDAP referral to: %s", referralURL)
resp, err = http.Get(referralURL)
if err != nil {
return nil, fmt.Errorf("RDAP referral request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("RDAP referral server returned status %d: %s", resp.StatusCode, string(body))
}
var finalResponse RDAPResponse
if err := json.NewDecoder(resp.Body).Decode(&finalResponse); err != nil {
return nil, fmt.Errorf("failed to decode final RDAP JSON: %w", err)
}
return &finalResponse, nil
}
return &initialResponse, nil
}
func (s *Server) handleConnection(conn net.Conn) {
log.Printf("new connection from %s", conn.RemoteAddr())
defer log.Printf("connection to %s closed", conn.RemoteAddr())
defer conn.Close()
scanner := bufio.NewScanner(conn)
var clientQuery string
if scanner.Scan() {
clientQuery = strings.TrimSpace(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Printf("Error reading from client: %v", err)
return
}
if clientQuery == "" {
_, _ = fmt.Fprint(conn, "Please provide a domain name.\r\n")
return
}
log.Printf("Received query for: %q", clientQuery)
rdapData, err := queryRDAP(clientQuery)
if err != nil {
log.Printf("RDAP query for %q failed: %v", clientQuery, err)
_, _ = fmt.Fprintf(conn, "Error performing RDAP lookup: %v\r\n", err)
return
}
var responseBuilder strings.Builder
if err := s.rdapTemplate.Execute(&responseBuilder, rdapData); err != nil {
log.Printf("Internal error: failed to execute template: %v", err)
_, _ = fmt.Fprint(conn, "Internal server error.\r\n")
return
}
_, err = fmt.Fprint(conn, responseBuilder.String())
if err != nil {
log.Printf("Error writing response: %v", err)
}
}
func main() {
port := ":43"
server, err := NewServer()
if err != nil {
log.Fatalf("Error creating server: %v", err)
}
listener, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("Error listening on port %s: %v", port, err)
}
defer listener.Close()
log.Printf("RDAP Proxy WHOIS server listening on %s", port)
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Error accepting connection: %v", err)
continue
}
go server.handleConnection(conn)
}
}
The output is formatted using a Go template to create a classic WHOIS-style report from the RDAP JSON data:whois-server-proxy/rdap.template (click to expand)View on GitHub
Domain:
Domain Name: {{.LDHName}}
Handle: {{.Handle}}
{{- range .Status}}
Status: {{.}}
{{- end}}
{{- range .Conformance}}
Conformance: {{.}}
{{- end}}
{{- range .Notices}}
Notice:
Title: {{.Title}}
{{- range .Description}}
Description: {{.}}
{{- end}}
{{- range .Links}}
Link: {{.Href}}
{{- end}}
{{- end}}
{{- range .Links}}
Link: {{.Href}}
{{- end}}
{{- range .Events}}
Event:
Action: {{.Action}}
{{- if .Actor}}
Actor: {{.Actor}}
{{- end}}
Date: {{.Date.Format "2006-01-02T15:04:05.000Z"}}
{{- end}}
{{- with .SecureDNS}}
Secure DNS:
Zone Signed: {{.ZoneSigned}}
Delegation Signed: {{.DelegationSigned}}
{{- range .DSData}}
DSData:
Key Tag: {{.KeyTag}}
Algorithm: {{.Algorithm}}
Digest: {{.Digest}}
DigestType: {{.DigestType}}
{{- end}}
{{- end}}
{{- range .Entities}}
Entity:
Handle: {{.Handle}}
{{- range .PublicIDs}}
Public ID:
Type: {{.Type}}
Identifier: {{.Identifier}}
{{- end}}
{{- range .Remarks}}
Remark:
Title: {{.Title}}
Type: {{.Type}}
{{- range .Description}}
Description: {{.}}
{{- end}}
{{- end}}
{{- range .Links}}
Link: {{.Href}}
{{- end}}
Role: {{range $i, $role := .Roles}}{{if $i}}, {{end}}{{$role}}{{end}}
{{- if .VCardArray.GetField "fn"}}
vCard version: 4.0
vCard fn: {{.VCardArray.GetField "fn"}}
{{- end}}
{{- range .Entities}}
Entity:
{{- range .Status}}
Status: {{.}}
{{- end}}
Role: {{range $i, $role := .Roles}}{{if $i}}, {{end}}{{$role}}{{end}}
vCard version: 4.0
vCard fn: {{.VCardArray.GetField "fn"}}
vCard tel: {{.VCardArray.GetField "tel"}}
vCard email: {{.VCardArray.GetField "email"}}
{{- end}}
{{- end}}
{{- range .Nameservers}}
Nameserver:
Nameserver: {{.LDHName}}
Handle: {{.Handle}}
{{- range .Remarks}}
Remark:
Title: {{.Title}}
Type: {{.Type}}
{{- range .Description}}
Description: {{.}}
{{- end}}
{{- end}}
{{- range .Links}}
Link: {{.Href}}
{{- end}}
{{- end}}
With the proxy running, we can query it for kmcd.dev and get a complete and useful response using a standard whois client:
# Run the proxy in one terminal
go run ./whois-server-proxy
# Query it from another
whois -h localhost kmcd.dev
Output (click to expand)
Domain:
Domain Name: kmcd.dev
Handle: E04E36511-DEV
Status: client transfer prohibited
Conformance: rdap_level_0
Conformance: icann_rdap_technical_implementation_guide_0
Conformance: icann_rdap_response_profile_0
Notice:
Title: Cloudflare Registrar
Description: Cloudflare provides more than 13 million domains with the tools to give their global users a faster, more secure, and more reliable internet experience.
Description: Register your domain name at https://www.cloudflare.com/registrar/
Link: https://www.cloudflare.com/registrar/
Notice:
Title: Terms of Use
Description: Data in the Cloudflare Registrar WHOIS database is provided to you by Cloudflare under the terms and conditions at https://www.cloudflare.com/domain-registration-agreement/.
Description: By submitting this query, you agree to abide by these terms.
Link: https://www.cloudflare.com/domain-registration-agreement/
Notice:
Title: EPP Status Codes
Description: For more information on domain status codes, please visit https://icann.org/epp.
Link: https://icann.icann.org/epp
Notice:
Title: Whois Inaccuracy Complaint Form
Description: URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf.
Link: https://www.icann.org/wicf
Event:
Action: registration
Date: 2024-05-18T19:33:01.000Z
Event:
Action: last changed
Date: 2024-05-23T20:08:26.329Z
Event:
Action: expiration
Date: 2034-05-18T19:33:01.000Z
Event:
Action: registrar expiration
Date: 2034-05-18T19:33:01.000Z
Secure DNS:
Zone Signed: false
Delegation Signed: true
DSData:
Key Tag: 2371
Algorithm: 13
Digest: 321F88BA26AB76AE885C06671798645ADF4D06448F52D67D627641FA28392AF7
DigestType: 2
Entity:
Handle: 1910
Public ID:
Type: IANA Registrar ID
Identifier: 1910
Role: registrar
vCard version: 4.0
vCard fn: Cloudflare, Inc.
Entity:
Role: abuse
vCard version: 4.0
vCard fn: Cloudflare Registrar Abuse
vCard tel: tel:+1.4153197517
vCard email: [email protected]
Entity:
Handle:
Remark:
Title: DATA REDACTED
Type: object redacted due to authorization
Description: Some of the data in this object has been removed
Role: registrant
vCard version: 4.0
vCard fn: DATA REDACTED
Entity:
Handle:
Remark:
Title: DATA REDACTED
Type: object redacted due to authorization
Description: Some of the data in this object has been removed
Role: administrative
vCard version: 4.0
vCard fn: DATA REDACTED
Entity:
Handle:
Remark:
Title: DATA REDACTED
Type: object redacted due to authorization
Description: Some of the data in this object has been removed
Role: technical
vCard version: 4.0
vCard fn: DATA REDACTED
Entity:
Handle:
Remark:
Title: DATA REDACTED
Type: object redacted due to authorization
Description: Some of the data in this object has been removed
Role: billing
vCard version: 4.0
vCard fn: DATA REDACTED
Nameserver:
Nameserver: chuck.ns.cloudflare.com
Handle:
Nameserver:
Nameserver: oaklyn.ns.cloudflare.com
Handle:
Closing Thoughts
WHOIS is simple and approachable, but it belongs to a smaller and more trusting Internet. It relies on unstructured text, inconsistent formatting, informal conventions, and an unencrypted transport. It is out-of-place in the modern Internet.
RDAP is the natural evolution of WHOIS. It fixes the exact problems that made WHOIS brittle: structure, discovery and security.
By wrapping the new standard in the old interface, we bridged the gap between the past and present. With the WHOIS-to-RDAP proxy, we get the structured power of RDAP without losing the muscle-memory and intuitive naming of the whois command.
This was a toy project made to learn about both WHOIS and RDAP, but this acts as a useful lens on how internet protocols evolve and illustrates many features of modern web APIs that we take for granted today.
