Skip to main content

DNS Over TLS Using BIND And Nginx

Shenzhen, China

Test Run

Deploying a Simple DoT-DNS Gateway

The NGINX Stream (TCP/UDP) module supports SSL termination, and so it’s actually really simple to set up a DoT service. You can create a simple DoT gateway in just a few lines of NGINX configuration. You need an upstream block for your DNS servers - I am using the default Google DNS 8.8.8.8 for now - and a server block for TLS termination.

I am using a modified version of the Docker NGINX Ingress that already has a self-signed certificate I can use:

nginx.conf

user  nginx;
worker_processes auto;
worker_rlimit_nofile 15000;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;

events {
worker_connections 2048;
multi_accept on;
use epoll;
}

stream {
# DNS upstream pool
upstream dns {
zone dns 64k;
server 8.8.8.8:53;
}

# DoT server for decryption
server {
listen 853 ssl;
# ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;
# ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;
proxy_pass dns;

include self-signed.conf;
include ssl-params.conf;
}
}

self-signed.conf

ssl_certificate /etc/nginx/conf.d/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/conf.d/ssl/private/nginx-selfsigned.key;

ssl-params.conf

ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/conf.d/ssl/private/dhparam.pem;
ssl_ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM;
ssl_conf_command Options PrioritizeChaCha;
ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9

Gateway Container

Start the docker container with:

docker network create --subnet=172.24.0.0/16 instar-net
docker pull nginx:alpine

docker run -d \
--rm \
--name=gateway \
--net=instar-net \
--ip=172.24.0.16 \
-p 853:853 \
-v /opt/bind9/gateway/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/bind9/gateway/self-signed.conf:/etc/nginx/self-signed.conf \
-v /opt/bind9/gateway/ssl-params.conf:/etc/nginx/ssl-params.conf \
-v /opt/bind9/gateway/ssl:/etc/nginx/conf.d/ssl \
nginx:alpine

Testing

Verify that the TLS service is available - the IPv4 address 16.132.12.32, in the following command is the IP address of my Docker host server that is running the gateway:

openssl s_client -host 16.132.12.32 -port 853 -showcerts

CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = 16.132.12.32
verify error:num=18:self signed certificate
verify return:1
depth=0 C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = 16.132.12.32
verify return:1

...

I am going to use the getDNS Utilities to test the Gateway:

apt install getdns-utils

First I try a run without TLS - which cannot work. The IPv4 address 16.132.12.32, in the following command is the IP address of my Docker host server that is running the gateway:

getdns_query @16.132.12.32 -s -a -A www.instar.com

{
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for www.instar.com.>,
"just_address_answers": [],
"replies_full": [],
"replies_tree": [],
"status": GETDNS_RESPSTATUS_ALL_TIMEOUT
}
An error occurred: The callback got a callback_type of 702. Exiting.
Error : 'The requested action timed out; response is filled in with empty structures'

And now to TLS:

getdns_query @16.132.12.32 -s -a -A  -l L www.instar.com

{
"answer_ipv4_address": <bindata for 16.132.12.32>,
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for www.instar.com.>,
"just_address_answers":
[
{
"address_data": <bindata for 49.12.0.118>,
"address_type": <bindata of "IPv4">
}
],

...

"status": GETDNS_RESPSTATUS_GOOD
}

The IP address 49.12.0.118 is successfully resolved for www.instar.com.

Bind9 in Docker

I am going to use my Bind9 Docker Container and run it inside the same Docker network instar-net:

docker network create --subnet=172.24.0.0/16 instar-net
docker run -d --rm --name=ddns-master --net=instar-net --ip=172.24.0.2 ddns-master

Gateway Configuration

Now I can replace the 8.8.8.8 Google DNS Server and replace it with the Bind9 Docker container IP 172.24.0.2 or the container name ddns-master. I am also adding a stream for port 53 so I don't have to expose any ports from the Bind9 container:

user  nginx;
worker_processes auto;
worker_rlimit_nofile 15000;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;


events {
worker_connections 2048;
multi_accept on;
use epoll;
}

stream {
# DNS Upstream
upstream dns {
zone dns 64k;
server ddns-master:53;
}

# DNS Ingress
server {
listen 53 udp reuseport;
proxy_timeout 20s;
proxy_pass dns;
}

# DoT Ingress TLS Endpoint
server {
listen 853 ssl;
# ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;
# ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;
proxy_timeout 20s;
proxy_pass dns;

include self-signed.conf;
include ssl-params.conf;
}
}
docker run -d \
--rm \
--name=gateway \
--net=instar-net \
--ip=172.24.0.16 \
-p 853:853/tcp \
-p 53:53/udp \
-v /opt/bind9/gateway/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/bind9/gateway/self-signed.conf:/etc/nginx/self-signed.conf \
-v /opt/bind9/gateway/ssl-params.conf:/etc/nginx/ssl-params.conf \
-v /opt/bind9/gateway/ssl:/etc/nginx/conf.d/ssl \
nginx:alpine

Testing

Without TLS:

getdns_query @16.132.12.32 -s -a -A service2.instar-net.io                                                            
{
"answer_ipv4_address": <bindata for 16.132.12.32>,
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for service2.instar-net.io.>,
"just_address_answers":
[
{
"address_data": <bindata for 172.24.0.4>,
"address_type": <bindata of "IPv4">
}
],
...

"status": GETDNS_RESPSTATUS_GOOD
}

But I am seeing an error message when switching to TLS:

getdns_query @16.132.12.32 -s -a -A  -l L service2.instar-net.io

{
"answer_ipv4_address": <bindata for 16.132.12.32>,
"answer_type": GETDNS_NAMETYPE_DNS,
"canonical_name": <bindata for service2.instar-net.io.>,
"just_address_answers": [],

...

"status": GETDNS_RESPSTATUS_NO_NAME
}
An error occurred: The callback got a callback_type of 702. Exiting.
Error : 'The requested action timed out; response is filled in with empty structures'

DNSUtils

I am able to resolve the IP of my service as defined in Bind9 using nslookup:

apt-get install dnsutils

nslookup service2.instar-net.io 16.132.12.32

Server: 16.132.12.32
Address: 16.132.12.32#53

Name: service2.instar-net.io
Address: 172.24.0.4

But there is no way to check the TLS connecting through the gateway proxy.

Knot DNSUtils

That is where kdig comes in:

apt-get install knot-dnsutils

Without TLS:

kdig service1.instar-net.io -t A @16.132.12.32

;; TLS session (TLS1.3)-(ECDHE-SECP384R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 17361
;; Flags: qr aa rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1

;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR

;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 16017
;; Flags: qr aa rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 0

;; QUESTION SECTION:
;; service2.instar-net.io. IN A

;; ANSWER SECTION:
service2.instar-net.io. 86400 IN A 172.24.0.4

;; Received 56 B
;; Time 2022-03-10 08:31:07 CET
;; From 1.2.3.4@53(UDP) in 0.0 ms

And here it also seems to be working over TLS - I am getting the 172.24.0.3 resolution for Service1, as expected:

kdig +tcp +tls -p 853 service1.instar-net.io -t A @16.132.12.32

;; TLS session (TLS1.3)-(ECDHE-SECP384R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 57694
;; Flags: qr aa rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1

;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR

;; QUESTION SECTION:
;; service1.instar-net.io. IN A

;; ANSWER SECTION:
service1.instar-net.io. 86400 IN A 172.24.0.3

;; Received 67 B
;; Time 2022-03-09 13:33:15 CET
;; From 16.132.12.32@853(TCP) in 1.4 ms

And the forwarder is working as well:

kdig +tcp +tls -p 853 www.instar.com -t A @16.132.12.32

;; TLS session (TLS1.3)-(ECDHE-SECP384R1)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM)
;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 22289
;; Flags: qr rd ra; QUERY: 1; ANSWER: 1; AUTHORITY: 0; ADDITIONAL: 1

;; EDNS PSEUDOSECTION:
;; Version: 0; flags: ; UDP size: 1232 B; ext-rcode: NOERROR

;; QUESTION SECTION:
;; www.instar.com. IN A

;; ANSWER SECTION:
www.instar.com. 600 IN A 49.12.0.118

;; Received 59 B
;; Time 2022-03-10 04:34:23 CET
;; From 16.132.12.32@853(TCP) in 41.4 ms

Docker-Compose

Wrapping everything up into a compose file:

version: "3.9"

services:
bind:
build:
context: .
dockerfile: Dockerfile
image: dns-master:latest
networks:
bindnet:
ipv4_address: 172.24.0.2


gateway:
image: nginx:alpine
ports:
- "53:53/udp"
- "853:853/tcp"
networks:
bindnet:
ipv4_address: 172.24.0.16
volumes:
- /opt/bind9/gateway/nginx.conf:/etc/nginx/nginx.conf
- /opt/bind9/gateway/self-signed.conf:/etc/nginx/self-signed.conf
- /opt/bind9/gateway/ssl-params.conf:/etc/nginx/ssl-params.conf
- /opt/bind9/gateway/ssl:/etc/nginx/conf.d/ssl

networks:
bindnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.24.0.0/16

To execute the compose file first run docker-compose build to build the Bind9 Docker container - the Dockerfile and all dependencies have to be in the same directory as the compose file:

FROM internetsystemsconsortium/bind9:9.18

RUN apt update \
&& apt install -y \
bind9-doc \
dnsutils \
geoip-bin \
mariadb-server \
net-tools

# Copy configuration files
COPY configuration/named.conf.options /etc/bind/
COPY configuration/named.conf.local /etc/bind/
COPY configuration/db.instar-net.io /etc/bind/zones/

# Expose Ports
EXPOSE 53/tcp
EXPOSE 53/udp
EXPOSE 953/tcp

# Start the Name Service
CMD ["/usr/sbin/named", "-g", "-c", "/etc/bind/named.conf", "-u", "bind"]