Create a WHOIS server from scratch

The WHOIS protocol is commonly used to obtain information about IP addresses and domain names from regional Internet registries and registrar databases.

For example one might use the whois command to find registration information about an IP address:

❯ whois 1.1.1.1
% [whois.apnic.net]
% Whois data copyright terms    http://www.apnic.net/db/dbcopyright.html

inetnum:        1.1.1.0 - 1.1.1.255
netname:        APNIC-LABS
descr:          APNIC and Cloudflare DNS Resolver project
descr:          Routed globally by AS13335/Cloudflare
descr:          Research prefix for APNIC Labs
country:        AU

[...]

However, WHOIS is not limited to “official” RIR and registrar servers. Other services might provide a WHOIS interface. For example bgp.tools, which we can query specifically by providing the -h or --host flag:

❯ whois -h bgp.tools -- 1.1.1.1           
AS      | IP               | BGP Prefix          | CC | Registry | Allocated  | AS Name
13335   | 1.1.1.1          | 1.1.1.0/24          | US | ARIN     | 0001-01-01 | Cloudflare, Inc.%

In this post we will create our own WHOIS service that returns ASN and geolocation information from the free Country & ASN database:

❯ whois -h localhost -- 1.1.1.1
as-domain:       cloudflare.com
as-name:         Cloudflare, Inc.
asn:             AS13335
continent:       OC
continent-name:  Oceania
country:         AU
country-name:    Australia

A simple echo server

The WHOIS protocol is defined in RFC3912. It is extremely simple:

A WHOIS server listens on TCP port 43 for requests from WHOIS clients.

The WHOIS client makes a text request to the WHOIS server, then the WHOIS server replies with text content. All requests are terminated with ASCII CR and then ASCII LF.

The response might contain more than one line of text, so the presence of ASCII CR or ASCII LF characters does not indicate the end of the response.

The WHOIS server closes its connection as soon as the output is finished.
The closed TCP connection is the indication to the client that the response has been received.

Let’s write a WHOIS server that echoes back the query to the client in Python. We will use Python’s asyncio module that makes it very easy to write TCP servers.

Put the following in a file named server.py:

import asyncio

SEPARATOR = b"\r\n"

async def callback(reader, writer):
    data = await reader.readuntil(SEPARATOR)

    # The query is everything before the first \r\n (CRLF).
    query = data.split(SEPARATOR)[0]

    # Echo back the query and add the mandatory \r\n line ending.
    writer.write(query + SEPARATOR)
    await writer.drain()

    # Close the connection.
    writer.close()
    await writer.wait_closed()

async def main():
    # WHOIS uses port 43 but since this is a privileged port we use
    # 8043 for development so that we don't need to run our script as root.
    server = await asyncio.start_server(callback, "localhost", 8043)
    await server.serve_forever()

if __name__ == "__main__":
    asyncio.run(main())

Run the server in one terminal:

python3 server.py

And query it in another:

whois -h 127.0.0.1 -p 8043 -- hello world

You should get the following output:

hello world

Serving MMDB files

Let’s make our server more useful by returning ASN and geolocation information. For that we will use IPinfo’s free Country & ASN database, and the maxminddb Python module to read MMDB files.

Start by creating a virtual environment and install the maxminddb package in it:

python3 -m venv .venv
source .venv/bin/activate
pip install maxminddb 

Then replace the content of server.py with the following:

import argparse
import asyncio
import maxminddb

SEPARATOR = b"\r\n"

class Handler:
    def __init__(self, database):
        self.reader = maxminddb.open_database(database)

    async def handle(self, reader, writer):
        data = await reader.readuntil(SEPARATOR)

        # Convert bytes to UTF-8 and lookup data in the MMDB.
        query = data.split(SEPARATOR)[0].decode()
        info = self.reader.get(query)

        # Output one key/value pair per line.
        for k, v in info.items():
            writer.write(f"{k}: {v}".encode() + SEPARATOR)
        await writer.drain()

        # Close the connection.
        writer.close()
        await writer.wait_closed()

async def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("database")
    parser.add_argument("--host", default="localhost")
    parser.add_argument("--port", default=43, type=int)
    args = parser.parse_args()

    handler = Handler(args.database)
    server = await asyncio.start_server(handler.handle, args.host, args.port)
    await server.serve_forever()

if __name__ == "__main__":
    asyncio.run(main())

Download the database:

ipinfo download country-asn -f mmdb

Run the server:

python3 server.py --port 8043 country_asn.mmdb

Query it:

whois -h 127.0.0.1 -p 8043 -- 1.1.1.1

And tada :tada:

as_domain: cloudflare.com
as_name: Cloudflare, Inc.
asn: AS13335
continent: OC
continent_name: Oceania
country: AU
country_name: Australia

Cosmetic touches

The RIR WHOIS servers use dashes instead of underscores in attribute names, and align values. We can replicate this behaviour by using the following logic in the handle function:

# Compute the maximum length of a key.
width = max(len(k) for k in info) + 2

# Output one key/value pair per line.
for k, v in info.items():
    k = k.replace("_", "-") + ":"
    writer.write(f"{k:{width}} {v}".encode() + SEPARATOR)
await writer.drain()

Output:

as-domain:       cloudflare.com
as-name:         Cloudflare, Inc.
asn:             AS13335
continent:       OC
continent-name:  Oceania
country:         AU
country-name:    Australia

Conclusion

In this post we’ve shown how to implement a WHOIS server in Python.

A production-ready version of the MMDB WHOIS server is available on GitHub: maxmouchet/mmdb-whois-server.

As a bonus I have hosted a public instance of this server over IPv4 and IPv6 at whois.dscp.dev. It serves the free Country & ASN database, updated daily:

❯ whois -h whois.dscp.dev -- 1.1.1.1
as-domain:       cloudflare.com
as-name:         Cloudflare, Inc.
asn:             AS13335
continent:       OC
continent-name:  Oceania
country:         AU
country-name:    Australia

% IP address data provided by https://ipinfo.io
4 Likes