Run a private DNS over HTTPS service

Table of contents

Why 🍩?

DNS over HTTPS (or DoH) is a protocol that makes browsing more private. Browsers typically resolve domain names with an unencrypted protocol, allowing nosy neighbors and internet providers to snoop on some internet activity. DoH creates an encrypted connection between browsers and the DNS resolver to make it difficult to even see what domains a user is loading. Firefox now supports DNS over HTTPS, and Chrome will soon.

It’s useful to be able to run private services for protocols like DoH. If you pipe all your encrypted DNS requests to one shared provider, you’re essentially just swapping one nosy ISP for an even bigger internet provider. Fly is an especially good place to run DNS services because we deploy apps globally and the internet is faster when DNS is close by. And if you want to, you can even manage your own TLS.

Prerequisites

Install Fly CLI

If you’re on a Mac, you can install the CLI with Homebrew:

brew install superfly/tap/flyctl

For other systems, use the install script:

curl https://get.fly.io/flyctl.sh | sh

Log in to Fly

Log in to Fly with flyctl auth login:

➜  flyctl auth login
Opening browser to url https://fly.io/app/auth/cli/token-gibberish
Waiting for session...⣽ 

This will open a browser window you can use to login. If you haven’t yet registered with Fly, you can do that now!

Create a Fly app

Pick a name for your new DNS over HTTPS service. Then create a Fly app:

➜  flyctl apps create
? App Name (leave blank to use an auto-generated name)
> [your-app-name]

✔ New app created
  Name    = [your-app-name]
  Owner   = fly
  Version = v0
  Status  =

Created fly.toml

Two-minute easy mode

We’ve published a Docker image you can use to get started.

1. Deploy flyio/doh-proxy from Docker Hub

➜  flyctl deploy -i flyio/doh-proxy:0.1.19
--> done
  Version     = v0
  Reason      = Deploy image
  Description =
  User        = flydev@fly.local

2. Use https://[your-app-name].fly.dev/dns-query

Your new Fly app includes dedicated IP addresses and a [your-app-name].fly.dev hostname with a valid certificate. You can see these by running flyctl info:

➜  flyctl info
App
  Name     = doh-proxy
  Owner    = fly
  Version  = 10
  Status   = pending
  Hostname = https://[your-app-name].fly.dev

Services
  TASK   PROTOCOL   PORT   INTERNAL PORT   HANDLERS
  app    tcp        80     8080            http
  app    tcp        443    8080            tls http

IP Addresses
  ADDRESS                                TYPE
  77.83.140.25                           v4
  2a09:8280:1:68a1:90f6:e320:d275:70f7   v6

The URL for DNS queries is: https://[your-app-name].fly.dev/dns-query, you can use it in any app that supports DNS over HTTPS.

You can also try it out with curl:

➜  curl -D - -sS "https://doh-proxy.fly.dev/dns-query?ct&dns=q80BAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE" | strings
HTTP/2 200
content-length: 56
content-type: application/dns-message
x-padding: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
cache-control: max-age=43200
date: Wed, 11 Sep 2019 15:08:54 GMT
server: Fly/a04ef97 (Wed, 11 Sep 2019 11:18:30 +0000)
example

Hard mode (maybe 10 minutes)

1. Write a custom Dockerfile

We use Docker to build and package applications. For custom builds, you’ll need a Dockerfile in your working directory — here’s a good one for doh-proxy:

# ------------------------------------------------------------------------------
# Cargo Build Stage
# ------------------------------------------------------------------------------

FROM rust:latest as cargo-build

ARG VERSION=0.1.19

RUN apt-get update

RUN apt-get install musl-tools -y

RUN rustup target add x86_64-unknown-linux-musl

RUN RUSTFLAGS=-Clinker=musl-gcc cargo install doh-proxy --version $VERSION --root /usr/local/ --target=x86_64-unknown-linux-musl

# ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------

FROM alpine:3.10

RUN apk add --no-cache libgcc runit shadow curl

COPY --from=cargo-build /usr/local/bin /usr/local/bin

RUN useradd doh-proxy

USER doh-proxy

ENV LISTEN=0.0.0.0:8080
ENV RESOLVER=9.9.9.9:53

RUN /usr/local/bin/doh-proxy --version

CMD ["sh", "-c", "/usr/local/bin/doh-proxy --listen-address $LISTEN --server-address $RESOLVER"]

2. Deploy working directory with flyctl

The Fly CLI will build + deploy a Docker image in one step. Just run flyctl deploy in your working directory. If Docker is running locally, we’ll do a local build. If you don’t have docker installed, we copy the contents of your working directory and perform a build on our servers.

➜  flyctl deploy
Deploy source directory '.'
Docker daemon available, performing local build...
Using Dockerfile from project: Dockerfile
Step 1/15 : FROM rust:latest as cargo-build
...
Step 6/15 : RUN RUSTFLAGS=-Clinker=musl-gcc cargo install doh-proxy --version $VERSION --root /usr/local/ --target=x86_64-unknown-linux-musl
 ---> 3e9b7ff35fb4
Step 7/15 : FROM alpine:3.10
...
Step 9/15 : COPY --from=cargo-build /usr/local/bin /usr/local/bin
...
Step 15/15 : CMD ["sh", "-c", "/usr/local/bin/doh-proxy --listen-address $LISTEN --server-address $RESOLVER"]
 ---> 112c09922fb3
Successfully built 112c09922fb3
Successfully tagged registry.fly.io/[your-app-name]:deployment-1568224602
Image size: 18 MB
==> Pushing image
The push refers to repository [registry.fly.io/[your-app-name]]
6398c5b9614b: Layer already exists
2d62a6f9eb35: Layer already exists
79d3694c6de3: Layer already exists
03901b4a2ea8: Layer already exists
deployment-1568224602: digest: sha256:03b75cb1249658e3d6cf110ccfb7935d749280d4f61662ee96e10cf6725a461c size: 1158
--> done
==> Deploying Image
--> done
  Version     = v0
  Reason      = Deploy image
  Description =
  User        = flydev@fly.local

This could take a while on the first one. Subsequent runs will reuse image layers and finish quite a bit faster.

Optional configuration

Use a different resolver

The doh-proxy project defaults to using the Quad9* DNS resolver at 9.9.9.9:53. To change this, you can add an environment variable:

➜  flyctl secrets set RESOLVER=8.8.8.8:53
  VERSION   REASON            DESCRIPTION   USER               DATE
  v10       Secrets updated                 flydev@fly.local   0s ago

Change hostnames

Fly apps support an unlimited number of hostnames and SSL certificates. To set one up:

  1. Add a certificate to your fly app:

    ➜  flyctl certs create example.com -a doh-proxy
    
    Hostname              = example.com
    Configured            = false
    Certificate Authority = lets_encrypt
    DNS Provider          = namesystem
    DNS Validation Target = example.com.q8o1.flydns.net
    Source                = fly
    
  2. Create a validation CNAME entry:
    • Name: _acme-challenge.example.com.
    • Target: example.com.q8o1.flydns.net
       
  3. Make a DNS entry pointing example.com to your Fly app IP

Handle your own TLS

Fly can take care of TLS + HTTPS for you, but it doesn’t have to. If you’d prefer to terminate your own TLS, you can generate your own certificates, store them as encrypted Fly secrets, and tweak the builder to pull them from your app environment. We’ll happily forwarded encrypted TCP to your app and let you worry about the rest.