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.

  1. CONNECT: The client opens a TCP socket to a WHOIS server (on port 43).
  2. ASK: The client sends the query: a single line of text like example.com, terminated by a carriage return and line feed (<CR><LF>).
  3. RESPONSE: The server streams back the registration data as plain text.
  4. 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.

FeatureWHOISRDAP
TransportTCP Port 43HTTP/HTTPS (Port 80/443)
FormatUnstructured Plain TextStructured JSON (machine-readable)
SecurityNoneStandard Web Security (TLS, Auth)
DiscoveryBrittle, text-based referralsStandardized 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?

D2 Diagram

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.

References