Skip to content

πŸ—„ A DNS server using the high-speed Solana blockchain as the backing datastore. (WIP)

License

Notifications You must be signed in to change notification settings

Monadical-SAS/solana-dns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Logo

Solana DNS


A simple DNS server using the high-performance Solana blockchain to store signed records.
(Implemented in JavaScript and Solana BPF Rust, w/ an optional REST API + Web UI)
a.k.a "who-dat over very-fast-brick-string"

Why Solana? | How does DNS work normally? | Quickstart | Documentation | Manual Setup | Architecture | Configuration | Usage



(Based on code from hbouvier/dns and solana-labs/example-messagefeed, but not affiliated with either project officially)

WARNING: this project is not actually implemented yet! Follow the TODOs for progress and check back soon for runnable code!

Why Solana?

It allows us to replace centralized root authorizy servers like .com with a high-speed, decentralized system while retaining full compatibility with all regular DNS clients and the rest of the DNS ecosystem.

  • Provable history by design.
    Records are cryptographically provable to have existed at a given time.

  • Inherently immutable.
    Past history can never be modified, whether by accident or maliciously.

  • Inherently distributed and fault-tolerant.
    A globally-distributed network of nodes serve as validators, data replicas, and leaders in the event of a fail-over.

  • Inherently authenticatable.
    All records are signed with your wallet private key, and can be verified against your public key.
    Remember though, for true authentication signing alone is meaningless unless your public keys are provably linked to your identity (e.g. via public PGP signed message, Keybase.io, etc).
    Solana is not an identity provider, but Solana can link accounts to other identity providers like Google Auth.

  • It's fast as hell.
    The network leader can theoretically handle 710,000 TPS, and this library additionally caches records locally up to the signed TTL.
    Solana ain't your average blockchain, it pulls all this off with no sharding or validation delays.
    See the Solana Architecture section for more info.

  • DNS on the Blockchain makes sense.
    Given all the properties above, having DNS as the authenticated source-of-truth for DNS is not a bad idea (whether to augment DNSSEC or replace it entirely).
    Caching layers that improve backwards-compatibility and speed can always be built on top, but everything gets easier when the underlying store is fast, immutable, globally-consistent, and authenticated.

WARNING: These are all big claims with little proof to back them up. This project is still in idea-phase, so read these as goals, not guarantees!


How does DNS work normally?

(click sections to expand)

This is a simplified example of a standard DNS setup process (without Solana DNS or DNSSEC):
  1. You register a domain example.com on the Namecheap Registrar

  2. You set your authoritative nameservers in the NameCheap control panel to ns1.digitalocean.com

  3. NameCheap pushes the change to the root dns servers for your TLD, in this case the root servers for .com (this is handled outside of the DNS system, usually using a protocol called the Extensible Provisioning Protocol (EPP))

  4. The root DNS servers update their NS records to point to your new authoritative name server ns1.digitalocean.com

  5. In your DigitalOcean account you configure the domain's DNS and set the example.com A record to 123.123.123.123

Then a user comes along and makes a DNS request for your domain `example.com` using their configured DNS upstream server `1.1.1.1`:
  1. User's device sends a DNS query to 1.1.1.1:53/udp asking for the example.com A record (in plain text, unencrypted)

  2. Assuming 1.1.1.1 doesn't have the record cached, it asks the root domain servers "what nameserver can I use to look up this record?"

  3. The .com root domain server responds with ns1.digitalocean.com

  4. 1.1.1.1 then sends a query to ns1.digitalocean.com asking for the example.com A record

  5. ns1.digitalocean.com responds with 123.123.123.123 to 1.1.1.1

  6. 1.1.1.1 caches this record and passes along the response back to the user

  7. The user receives back an unencrypted, unsigned, plaintext response containing 123.123.123.123, stating that 1.1.1.1 was the server that answered the request

Unfortunately this system has a number of major security flaws! Can you spot some of them?
  • there are many layers of implicit trust with no authentication or verification mechanism. You have to trust that

    • ns1.digitalocean.com,
    • 1.1.1.1,
    • the .com root servers,
    • and every middle-box between you and them
      are all non-malicious, free of bugs, and are otherwise functioning perfectly.
  • the requests and responses are entirely in plaintext, making it trivial for middle-boxes to intercept, record, and modify query content

  • the responses are unsigned and unauthenticated, meaning the user has no idea if the you were the one who created that DNS record, or if a malicious/buggy/hacked middle-box just decided to return a different value randomly.
    With no signature or public key to verify against, there's no way to know whether the response value has been tampered with.

Enter DNSSEC & DNS-over-HTTPS... two separate technologies aiming to solve two of the biggest issues.
  • DNSSEC partially fixes the authentication issue by allowing people to pin a public key along with their authoritative name servers at the registrar level (NameCheap) / root DNS level (.com root servers). The user can then use the corresponding private key (that only they have access to), to sign all records added to their nameserver (ns1.digitalocean.com). DNSSEC-compatible clients can then receive signed DNS query responses, and can verify the signatures are valid against the public key published at the root level.

  • DNS-over-HTTPS (DOH) doesn't add any kind of authentication to the individual records, but it does allow users to form a direct, encrypted connection to their upstream DNS servers, and authenticate the server's identity using their public SSL certificate. This makes one link in the chain trusted, but it does nothing for the links upstream from the user's DNS server unless they're also using DNS over HTTPS or an equivalent encrypted transit method (which luckily many major providers do use).

So what does Solana add to the equation?

It removes the root DNS servers from the trust equation (e.g. the .com TLD servers). Remember that they have ultimate control over all record authentication because they could choose to ignore or maliciously change the DNSSEC public key that you published via the registrar. This implicit trust can be removed in Solana DNS because the public key is the user's wallet public key, which is intrinsically tied to their identity on the platform. Solana has no power to change the public key because the chain forms an immutable record, and only later statements signed with a revocation key are recognized by clients as authorized to change the public key in control of a given domain.

This isn't just a case of "slap a blockchain on it and hope it gets better", this is a true novel distributed database-style solution that hasn't been feasible with traditional blockchains. Running usable DNS on blockchains is now feasible with the advent of fast proof-of-history chains, because it enables updates to happen within DNS TTL windows like 30sec, which are well below the 10min verification time that a chain like Bitcoin would require. Because changes can also be synchronously applied to appear to everyone across the world at the same time, we can also lose the "eventual consistency" and propagation delay issues that plagued standard DNS systems in the past.

Solana DNS can also retain the nice distributed properties of past DNS systems. Because all records are signed all the way up to the root using the domain owner's public key, any node can serve up records without users having to implicitly trust them. The query results will always arrive signed with a key that can be verified to ensure malicous middle-boxes cant get away with modifying records.

Potential problems?
  • Solana may be fast, but the RPC communication with the Solana chain may be significantly slower than DNS over UDP
  • If RPC communication becomes the bottleneck, we end up having to implement time-synchronized caching servers with fairly complex validation/staking mechanics to penalize clock drift and inadherence to signed record TTL expiry times. This problem becomes slightly simpler if we only allow caching on localhost, with no middleboxes between the local Solana DNS server and the Solana chain API.
  • It may not be necessary to replace DNSSEC entirely, it's possible to just sign the same root public key used on the TLD in Solana, and use DNSSEC from there on down the chain (checking against the Solana root key instead of the implicitly trusted root-server-published key)
  • The mechanics around domain ownership verification need to be figured out (or whether it's even necessary with Solana DNS)
  • The mechanics of pinning identities to keys needs to be figured out (whether via staking, 3rd party identity providers, or something else)
  • The mechanics of key issuing, rotating, and revocation need to be figured out (proper revocation is haaard, we dont want domains being lost forever to the ether because someone lost a private key)

Given the power of an immutable, globally sychronized database, with key-based identity baked in, many of the distributed-systems and authentication problems that have plagued DNS in the past become drastically easier to solve. Unfortunately, designing DNS system to seamlessly augment or entirely replace existing DNS authentication mechanisms is incredibly complex. Though our ambitions are big, this project will likely have to make some decisions early on to narrow the scope and pick a few core features to focus on as a proof-of-concept.


Quickstart

This installs all the dependencies, creates a free Solana account on the beta testnet, uploads the on-chain code, and runs a local dns server on 127.0.0.1:5300 using Solana as the backing datastore.

git clone https://github.com/pirate/solana-dns
cd solana-dns

./bin/setup
./bin/server --bind-dns=127.0.0.1:5300 --bind-http=127.0.0.1:5380

dig @127.0.0.1 -p 5300 google.com

If you don't like helper scripts installing packages and want to understand/fine-tune the setup, or if you encounter any issues, follow the Manual Setup steps below.


Documentation

Manual Setup | Architecture | Configuration | Usage


Manual Setup

(click sections to expand)

1. Install the language dependencies...
2. Clone the repo & install the project dependencies...
git clone https://github.com/pirate/solana-dns
cd solana-dns

npm install
3. Create an account on your desired Solana network...

Running code on Solana requires an "account"/wallet with tokens that will be used to run the on-chain part of the DNS server.
(Similar to how running Ethereum DAPPs on-chain requires spending some tokens in exchange for CPU time)
Choose which Solana network you want to store your records in:

  • Using the public beta testnet (easiest, all records publicly accessible, free):
    You automatically get free air-dropped tokens to run code on the beta net.

    ./bin/signup --net=beta --save-config=./secrets.env  # or use --net=edge
  • Using a local testnet (harder, no data accessible off your local machine, free):
    You get infinite free tokens on your local test net because you own it!

    ./bin/localnet > localnet.log &
    ./bin/signup --net=localnet --save-config=./secrets.env
  • Using the public mainnet (hardest, all records publicly accessible, real $ needed):
    You have to purchase SOL tokens via an exchange to run code on the main net.

    # Not available yet, check https://solana.com/tds/ for updates
4. Upload the on-chain side of the program using your account...

Build and upload the Rust BPF program that runs on the Solana net to handle requests from your local solana-dns server.

./bin/build
./bin/upload --config=./secrets.env
5. Run the solana-dns server on localhost...
./bin/server --bind-dns=127.0.0.1:5300 --bind-http=127.0.0.1:5380 --upstream=1.1.1.1,8.8.8.8
6. You're done! Your `solana-dns` server should be accessible via the bound ports... βœ…
  • To query it via DNS:

    dig @127.0.0.1 -p 5300 google.com
  • To query it via the REST API:

    curl http://127.0.0.1:5380/dns/api/v1/name/google.com
  • To view the Web UI:
    Open http://127.0.0.1:5380


Architecture

Data Flow | Execution Flow | Solana Architecture

Data flow

The data flows through the stack like this: (click to expand)
  • β¬‡οΈπŸ‘©β€πŸ’»πŸ“ƒ User
    Makes requests via DNS or HTTP.
    ./ui/index.js (runs in-browser) / direct request via DNS or REST API

  • ⬇️πŸ–₯⬆️ Local Node Server
    Main JavaScript logic handles CLI commands, DNS queries, and HTTP requests.
    ./server/server.js (runs locally) / ./server/dns-client.js (runs locally, resolves any records not found in Solana)

  • β¬‡οΈπŸŒβ¬†οΈ Solana Network API
    Calls out via Solana w3 JSON RPC API to the Solana network.
    ./network/api.js (runs locally, calls configured Solana Network's endpoint, e.g. https://beta.testnet.solana.com:8443)

  • ⬇️⛓⬆️ Solana BPF Rust Program On-Chain
    The uploaded BPF Rust program spends your account tokens to run on the Solana network and perform reads/writes of stored data.
    ./network/kvstore.rs (gets uploaded and runs on-chain)

  • β¬‡οΈβž‘οΈβ¬†οΈ Solana Blockchain
    Solana provides the data storage layer to hold the records.
    <Solana internals> (stored as text blobs on-chain)

Execution Flow

The code execution flow looks like this: (click to expand)
  1. New Solana account gets created with some free air-dropped tokens (needed to run code on-chain)
    ./bin/signup -> ./network/signup.js
    (only works on testnet/localnet, you're not getting free tokens on mainnet that easy πŸ˜‰)

  2. BPF Rust program gets uploaded to the Solana chain under new account
    ./bin/upload -> ./bin/build ./network/kvstore.rs -> ./network/upload.js ./network/kvstore.rs BPF Loader runs to push program to chain via configured Solana network's endpoint, e.g. https://beta.testnet.solana.com:8443

  3. REST API / DNS queries get handled by local node server
    ./bin/server -> ./server/server.js -> ./server/http-server.js,./server/dns-server.js

  4. Local node server calls out to Solana network via Solana Web3 JSON RPC API
    ./network/api.js -> https://beta.testnet.solana.com:8443

  5. BPF Rust program runs on Solana network to handle record read/write requests
    ./network/kvstore.rs -> <solana internal API>
    (Spends account tokens in exchange for the CPU time)

  6. Solana blockchain handles storage requests
    <solana internal API> -> <solana blob storage>

  7. If record is found, results are returned back up the stack, if not, they're resolved via the upstream DNS servers ./server/dns-client.js -> <upstream DNS servers>

Solana Architecture

More info on Solana's novel proof-of-history design, architecture, and available APIs can be found here:

Intro

Example Code

Source Code & API Reference


Configuration

Config options can be passed to Solana DNS commands (e.g. ./bin/server) in a few different ways:

  1. Configuration File passed via --config=path/to/file.env
  2. Environment Variables (which override any existing options in the config file)
  3. CLI Parameters (which override both env variables and config file params)

CLI Parameters

Click to expand CLI parameter docs

--config=path/to/file.conf

Default: --config=./secrets.env
Example: --config=/etc/solana/credentials.conf

Specify the path to the file containing your network config and account credentials used to connect to a Solana network.

For more info on the config file and options available within, see the Configuration File section.

--bind-dns=[host]:[port]

Default: --bind-dns=127.0.0.1:5300
Example: --bind-dns=0.0.0.0:53

Specify the local [ip]:[port] to bind the DNS server to. It must be 0.0.0.0:53 to act as a standard public DNS server that can accept requests from any client (instead of just localhost).

See the instructions below if you want to bind to port 53 instead.

--bind-http=[host]:[port]

Default: --bind-http=off
Example: --bind-http=127.0.0.1:5380

Specify the local [ip]:[port] to bind the web UI server to. It must be 0.0.0.0:[port] in order to accept HTTP requests from any client (instead of just localhost).

--upstream=[host]:[port],[host2]:[port2],...

Default: --upstream=off
Example: --upstream=1.1.1.1,8.8.8.8,208.67.222.222,dns.example.com:5353

Specify which upstream DNS servers to send requests to when the query cannot be resolved via Solana DNS.
The default is off, meaning it will return "no result found" if the record is not in the Solana store.

To make it a usable DNS server for all queries, and not just records stored in Solana, it's recommended to run with a few upstream servers capable of resolving normal internet-level DNS records.

Configuration File

Click to expand configuration file docs

When running the server, the path to the config file should be specified via the --config=path/to/file.env CLI param.

The config file is initially generated during setup when running:

./bin/signup --net=beta --save-config=./secrets.env

It can also be modified after the initial signup to change the credentials or include some additional options.

The config file must be in Docker/Bash compatible .env format, and can contain the following parameters:

SOLANA_NETWORK_NAME=beta
SOLANA_NETWORL_ENDPOINT=https://beta.testnet.solana.com:8443

SOLANA_USER_ID=[unique user id here]
SOLANA_USER_PUBLIC_KEY=[public key here]
SOLANA_USER_PRIVATE_KEY=[private key here]
SOLANA_USER_AUTH_GOOGLE=[optional Google email address here]

# Optionally specify additional DNS server config here
# these options are equivalent to ther respective CLI params
SOLANA_DNS_BIND_DNS=127.0.0.1:5300
SOLANA_DNS_BIND_HTTP=127.0.0.1:5380
SOLANA_DNS_UPSTREAM=1.1.1.1,8.8.8.8,208.67.222.222

Environment Variables

Click to expand environment variable docs

The options in the config file can also be passed as environment varaibles using the same format. e.g.:

env SOLANA_DNS_BIND_DNS=127.0.0.1:5300 ./bin/server ...

(This works well to pass config when running inside a Docker container)


Usage

CLI | REST API | DNS over HTTPS API | JSON API | Web UI


CLI

Click to expand CLI usage docs

Start the server

See the Configuration section for a list of the options available.

./bin/server [options]

To bind to any ports below 1000, most systems require running the program as root.

sudo ./bin/server [options]

Start the server on port 53

By default the DNS server listens on a custom UDP port 5300 in order to avoid requiring sudo or conflicting with any existing local DNS server. To bind to the the standard DNS port (UDP 53) instead, follow the steps below.

  1. Check to see if a DNS server is already running on 127.0.0.1:53

    sudo nc -ulp 53 || echo "port already in use"
  2. On Ubuntu: you may have to stop systemd-resolvd (the system DNS resolver that binds to :53 by default)

    # to stop it immediately, run:
    systemctl stop systemd-resolvd
    
    # then, if you want to prevent it from starting the next time you reboot, run:
    systemctl disable systemd-resolvd
    
    # make sure /ets/resolv.conf exists and has at least one non-local upstream DNS server
    echo "nameserver 1.1.1.1" >> /etc/resolv.conf
  3. Once the port is confirmed open, you can bind Solana-DNS to :53

    # listening on ports below 1000 requires sudo on most systems
    
    # to run a server that responds to DNS queries from localhost only, run:
    sudo ./bin/server --bind-dns=127.0.0.1:53
    
    # to run a server that responds to DNS queries from your local network only, run:
    sudo ./bin/server --bind-dns=x.x.x.x:53  # replace x.x.x.x with your LAN IP
    
    # to run a public server that responds to DNS queries from any location, run:
    sudo ./bin/server --bind-dns=0.0.0.0:53
  4. Optional: tell your system to use the local DNS server for all queries:

    # edit /etc/resolv.conf and put this line *above* any other "nameserver x.x.x.x" lines
    nameserver 127.0.0.1

    On macOS you can set this under:

    System Preferences > Network > Advanced > DNS > [+]
    127.0.0.1
    (drag it to the top of the list if there are other entries)
    

Add/Modify/Delete Records

TODO


Rest API

Click to expand Rest API docs

GET /api/dns/A/{domain}

Return the IP address for the given given domain.

PUT /api/dns/A/{domain}

Create or Modify the IP address for the given domain.

DELETE /api/dns/A/{domain}

Forget the IP address record for the given domain.

GET /api/dns/A/.

List all host to IP address mappings that the local server knows about.

DELETE /api/dns/A/?force=true

Forget all records.

GET /api/dns/zone

Return the DNS ZONE.

GET /api/dns/status

Return the DNS server status.


DNS over HTTPS API

TODO: implement a Google/Cloudflare-compatible DNS over HTTPS API


JSON API

TODO: implement a Google/Cloudflare-compatible DNS over HTTPS JSON API

Web UI

TODO: document Web UI features.


TODO

Core Implementation

Lower Priority

  • Add support for more DNS record types besides A records (e.g. CNAME, TXT, etc.)
  • Implement ./server/http-server.js to bind to a TCP port and serve HTTP Web UI and REST API
  • Implement ./server/dns-client.js to fetch records from upstream servers when not found on Solana
  • Add support for adding/removing/modifying DNS records via CLI
  • Add config file / environment variable support for hardcoded DNS records
  • Add support for DNS-over-HTTPS API & JSON API
  • Finish documentation for the Web UI and REST, DNS-over-HTTP, and JSON APIs