DNS over TLS (DOT) for Android 9+

If you guys would like to run your own Pihole with DoT, so you can block ads on your Android, here is sample script you can follow:

Note: Use at your own risk.



# ===============================================================================
sudo su

apt-get update && apt-get upgrade -y && apt-get autoremove -y

# Install necessary packages
apt-get install vim nginx certbot resolvconf python-certbot-nginx python3-certbot-dns-cloudflare dialog php7.2-fpm php7.2-zip php-sqlite3 iptables-persistent -y

# (Optional) Install Amazon EC2 Instant Connect
apt-get install ec2-instance-connect

# ===============================================================================
# Create NGINX server block for pihole web interface
# Replace pi.domain.com with your domain

echo "server {
        listen 80;
        listen [::]:80;
        root /var/www/html;
        server_name pi.domain.com;
        autoindex off;
        index pihole/index.php index.php index.html index.htm;
        location / {
                return 301 \$scheme://\$host/admin;
        }
        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.2-fpm.sock;
        }
        location /*.js {
                index pihole/index.js;
        }
        location /admin {
                root /var/www/html;
                index index.php index.html index.htm;
        }
        location ~ /\.ht {
                deny all;
        }
}" >> /etc/nginx/sites-available/pihole
# ===============================================================================
# Create Symbolic link for the site

ln -s /etc/nginx/sites-available/pihole /etc/nginx/sites-enabled/pihole 
# ===============================================================================
# Verify configuration and reload NGINX

systemctl reload nginx
# ===============================================================================
# Cloudflare API credentials for certificate verification

echo "dns_cloudflare_email = XXXXXXXXX@XXXXX.XXXXX
dns_cloudflare_api_key = XXXXXXXXXXXXXXXXXX" >> ./cloudflare.ini
# ===============================================================================
# Change permission for cloudfare credentials file

chmod 400 ./cloudflare.ini
# ===============================================================================
# Generate certificate request using Certbot

certbot -i nginx -m admin@domain.com --dns-cloudflare-credentials ./cloudflare.ini --agree-tos --no-eff-email -d *.domain.com
# ===============================================================================
# Install Pihole Server

curl -sSL https://install.pi-hole.net | bash
# ===============================================================================
# Create NGINX Streams directory

mkdir /etc/nginx/streams/
# ===============================================================================
# Create new server block for DNS-Over-TLS Server Block
# We will be requesting the cert later in the stage

echo "upstream dns-servers {
  server [::1]:53;
  server 127.0.0.1:53;
}
server {
  listen 853 ssl;
  listen [::]:853 ssl;
  ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/domain.com/privkey.pem;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
  ssl_protocols            TLSv1.2 TLSv1.3;
  ssl_ciphers              HIGH:!aNULL:!MD5;
  ssl_handshake_timeout    10s;
  ssl_session_cache        shared:SSL:20m;
  ssl_session_timeout      4h;
  proxy_ssl off;
  #proxy_bind \$remote_addr transparent;
  proxy_pass dns-servers;
}


" >> /etc/nginx/streams/dns-over-tls
# ===============================================================================
# Enable SSL Stapling

echo "ssl_stapling on;
ssl_stapling_verify on;
resolver [::1] 127.0.0.1;" >> /etc/nginx/conf.d/stapling.conf
# ===============================================================================
# Create new NGINX streams directory

echo "stream {
        include /etc/nginx/streams/*;
}" >> /etc/nginx/nginx.conf
# ===============================================================================
# Enable NXDOMAIN response for blocked domains

echo "BLOCKINGMODE=NXDOMAIN" >> /etc/pihole/pihole-FTL.conf
# ===============================================================================
# Restart NGINX and Pihole Server

service nginx restart
systemctl restart pihole-FTL
# ===============================================================================
# Reset Pihole Web Interface password

pihole -a -p
# ===============================================================================
# Verify if DNS-Over-TLS is working or not

# ===============================================================================
# ===============================================================================
# Enabling NGINX Transparency Proxy
# https://www.nginx.com/blog/ip-transparency-direct-server-return-nginx-plus-transparent-proxy/
# ===============================================================================
# ===============================================================================
# Edit DNS-Over-TLS server block at /etc/nginx/streams/dns-over-tls

# Replace upstream IP with Server's IPv6 and IPv4 Interface IP
  server [XXXX:XXXX::XXXX]:53;
  server X.X.X.X:53;

# Add the NGINX Proxy Bind to the server block
  proxy_bind $remote_addr transparent;
# ===============================================================================
# Run NGINX as Root, instead of www-data

sed -i 's/www-data/root/' /etc/nginx/nginx.conf
# ===============================================================================
# Add IPtables marking rules to tag DNS response packets

ip6tables -t mangle -A OUTPUT -p tcp --sport 53 -j MARK --set-xmark 7
iptables -t mangle -A OUTPUT -p tcp --sport 53 -j MARK --set-xmark 7
# ===============================================================================
# Add IP rules to divert DNS response packets to NGINX

# IPv6
ip -6 rule add fwmark 7 lookup 99
ip -6 route add local ::/0 dev lo table 99

# IPv4
ip rule add fwmark 7 lookup 99
ip route add local 0.0.0.0/0 dev lo table 99
# ===============================================================================
# Restart NGINX service

service nginx restart
# ===============================================================================
# Save IP6tables & IPtables rules

ip6tables-save > /etc/iptables/rules.v6
iptables-save > /etc/iptables/rules.v4
# ===============================================================================
# Ignore localhost queries

echo "IGNORE_LOCALHOST=yes" >> /etc/pihole/pihole-FTL.conf
service pihole-FTL restart
# ===============================================================================
# Verify DNS-Over-TLS is working

# ===============================================================================
# Troubleshooting Commands

netstat -tulpane                              # Connection Tracking
pihole -r                                     # Reset/Reconfigure Pihole Server after IP Change
pihole tail                                   # Pihole logs
ip6tables -t mangle                           # IPtables IPv6 Mangle Table
ip6tables -t mangle                           # IPtables IPv4 Mangle Table
tcpdump -n -i any tcp port 853 -w dump.pcap   # TCPDump command to verify DNS-Over-TLS packets
# ===============================================================================

I recommend running this in your VPC. And, don't bother on VPNs. VPNs are useless and cause extra battery drain.

Mod Edit: External link removed and script contents added to post.

Fair warning to any users considering running the above script:

It looks ok from a quick glance, but just make sure you understand what it is doing before running it

2 Likes

The script is wrong in some places.

# ===============================================================================
# Enable NXDOMAIN response for blocked domains

echo "BLOCKINGMODE=NXDOMAIN" | sudo tee /etc/pihole/pihole-FTL.conf

That will add an additional line to the config file and that is not correct. If you want to change the mode then you need to sed in changes to the existing mode configuration line. If it doesn't exist then you add it.

Same as with

echo "IGNORE_LOCALHOST=yes" >> /etc/pihole/pihole-FTL.conf

Another good guide:

I loosely followed this when I set one up for access from my phone when out and about. Though I ended up reimplementing it with traefik, I found it really good for helping me understand what I needed to do in order to achieve it.

Thanks. I added a note as well.

I corrected my post. Thanks for the advise.

@PromoFaux Could you share your Traefik configuration please? I have the below, which openssl s_client -connect says it has the correct certificate, but the Android phone fails to connect to it with traefik logging a level=error msg="Error during connection: readfrom tcp 172.25.0.254:33418->172.25.0.6:53: remote error: tls: expired certificate" where 172.25.0.6:53 is the pihole container.

      - traefik.tcp.services.pihole-dotservice.loadbalancer.server.port=53
      - traefik.tcp.routers.pihole-dot.entrypoints=dot
      - traefik.tcp.routers.pihole-dot.rule=HostSNI(`dot.adyanth.site`)
      - traefik.tcp.routers.pihole-dot.tls
      - traefik.tcp.routers.pihole.service=pihole-dotservice

This sounds like it might be the problem where Android's DoT software gets confused by Let's Encrypt's old expired CA X3 certificate. Here is a long blog entry about the problem, with solutions.

The short answer is to make sure you request your certificates with

preferred-chain = "ISRG Root X1"

in the cli.ini file, or with --preferred-chain="ISRG Root X1" on the command line.

If you are constructing a chain.pem file or something similar for Traefik, then you have to make sure the ISRG Root X1 certificate is the only one you're adding. So you're chain.pem should be something like

cat my-new-cert.pem ISRG_Root_X1.pem > DoT-cert.pem

It's not. I had gone through all the probable causes. Finally, from packet captures, found that the DoT implementation does not support SNI extension for TLS, which made Traefik serve the default certificate rather than the LetsEncrypt certificate based on the domain name.

Here is the original issues/PRs for Traefik:

Is it a modern android phone? I know there were some issues with letsencrypt certs towards the end of last year

I have this in my labels underneath my Pi-hole container:

    labels:
      - traefik.enable=true
      - traefik.docker.network=home
  ##### tcp
    ### services
      # backend port
      - traefik.tcp.services.svc_PiholeDns.loadbalancer.server.port=53
    ### routers
      # DoT forward
      - traefik.tcp.routers.rou_PiholeDot.entrypoints=dot
      - traefik.tcp.routers.rou_PiholeDot.rule=HostSNI(`my.dot.domain.com`)
      - traefik.tcp.routers.rou_PiholeDot.tls=true
      - traefik.tcp.routers.rou_PiholeDot.tls.certresolver=le
      - traefik.tcp.routers.rou_PiholeDot.tls.options=default
      - traefik.tcp.routers.rou_PiholeDot.service=svc_PiholeDns

I also have a file in /etc/traefik/traefik.conf.d/ named tls.toml:

########################################################################################################################
# TLS configuration
########################################################################################################################

[tls.options]
  [tls.options.default]
    minVersion = "VersionTLS12"
    cipherSuites = [
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    ]

To be honest, it's been so long since I set it up, I don't really remember which parts of the system actually make it work - I should have taken notes when I first did it (I keep threatening to do a full write up on it...)

Thanks! These mostly look identical. I will try with the cipherSuites, maybe they force to use SNIs? Idk. From the packet captures, the issue I saw was that the traefik does not send the needed certificate because DoT implementations do not support SNI which traefik uses to determine the certificate. I'm still wondering how yours is working :slight_smile:

Yup, it is. OnePlus 7T running Android 11. And the problem is that it is not receiving the letsencrypt cert but the TRAEFIK_DEFAULT_CERT.

I'm probably not the best person to ask there :sweat_smile: (Unless you accept the answer "because I want it to work")

Go to f-droid webpage in browser, or app, and download

RethinkDNS which will allow this, (DNSSEC) if that's the same, but even if not, download it anyway and set your pihole dns and android dns wrong and then connect to wireless.

Ooen the app (rethink DNS) and go to the network log and let your jaw hit the floor, it updates every few seconds so just scroll up to see new entries, it won't stop! You cam block ANY service you want without root, even on android oreo!

I haven't gotten the on-device blocklist to properly download yet but I cam just pre-block it with a local network before I connect to the cellular internet.

Also look into, or rather go install the unbound server, following the very simple (half of which you can skip) instructions.

Pi-hole has a half pager of instructions and you'll learn about dns security.

download the single file using the provided wget command.

Set Pi-Hole DNS in the webgui to not use any upstream dns providers, and use

127.0.0.1#5335

for the ipv4 DNS server (which also still sends and requests ipv6 AAAA records.)

You can self-host this system and it only needs to run when you turn on your computer really, unless you torrent files all night and day, it reduces the need for third-party cloud-based dns providers.