DNScrypt-proxy qube. Encrypted anon DNS with relay routing (like Tor)

This guide describes how to create a dedicated sys-dns qube running dnscrypt-proxy to provide encrypted, anonymized DNS resolution for downstream VMs in Qubes OS.

This specific configuration prioritizes DNSCrypt with anonymized DNS relays rather than plain DNS-over-HTTPS (DoH). Anonymized DNS works by routing your queries through two independent relays before they reach the resolver, similar to how Tor uses entry and exit nodes. This means no single relay knows both who you are and what you are querying. The config enforces strict policies - DNSSEC validation, no-logging, and no-filtering resolvers only - while disabling DoH and Oblivious DoH to avoid centralized infrastructure and browser-vendor dependencies. The result is a privacy-first, censorship-resistant DNS setup that does not rely on any single provider or protocol.


:gear: Install:

You can create a new fedora-minimal template and perform all the configuration there. I will instead show a simple setup using /rw/config/rc.local to avoid maintaining multiple templates.

1. Install dnscrypt-proxy in the fedora-xfce template:

sudo dnf install -y dnscrypt-proxy
sudo systemctl enable dnscrypt-proxy

2. Add dnscrypt-proxy configuration in fedora-xfce template:

This setup enables DNSCrypt with anonymized DNS (via relays), enforces DNSSEC, no-logging, and no-filter policies, and disables DoH and Oblivious DoH:

sudo tee /etc/dnscrypt-proxy/dnscrypt-proxy.toml << 'EOF'
server_names = []
listen_addresses = ['127.0.0.1:53', '0.0.0.0:53', '[::1]:53']
max_clients = 250
ipv4_servers = true
ipv6_servers = false
dnscrypt_servers = true
doh_servers = false
odoh_servers = false
require_dnssec = true
require_nolog = true
require_nofilter = true
disabled_server_names = ['scaleway', 'scaleway-ams']
force_tcp = false
timeout = 5000
keepalive = 30
cert_refresh_delay = 240
bootstrap_resolvers = ['9.9.9.11:53', '8.8.8.8:53']
ignore_system_dns = true
log_files_max_size = 10
log_files_max_age = 7
log_files_max_backups = 1
block_ipv6 = false
block_unqualified = true
block_undelegated = true
reject_ttl = 10
cache = true
cache_size = 4096
cache_min_ttl = 2400
cache_max_ttl = 86400
cache_neg_min_ttl = 60
cache_neg_max_ttl = 600
lb_strategy = 'wp2'
lb_estimator = true

[sources.public-resolvers]
urls = [
    'https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md',
    'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md',
    'https://cdn.jsdelivr.net/gh/DNSCrypt/dnscrypt-resolvers@master/v3/public-resolvers.md'
]
cache_file = 'public-resolvers.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
refresh_delay = 73
prefix = ''

[sources.relays]
urls = [
    'https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md',
    'https://download.dnscrypt.info/resolvers-list/v3/relays.md',
    'https://cdn.jsdelivr.net/gh/DNSCrypt/dnscrypt-resolvers@master/v3/relays.md'
]
cache_file = 'relays.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
refresh_delay = 73
prefix = ''

[anonymized_dns]
routes = [
    { server_name='*', via=['anon-scaleway', 'anon-scaleway-ams', 'anon-kama', 'anon-tiarap'] }
]
skip_incompatible = true
EOF

Note: The 10.139.1.1 and 10.139.1.2 addresses are the standard Qubes OS DNS IPs assigned to downstream VMs. Binding to 0.0.0.0:53 ensures dnscrypt-proxy can accept queries on these interfaces.


3. Create and configure sys-dns qube:

Create fedora-based AppVM sys-dns and add option Provides network in Qube Manager (advansed settings)

The next commands are executed in the sys-dns terminal.

4. Disable systemd-resolved in sys-dns:

sudo tee /rw/config/rc.local << 'EOF'
#!/bin/bash

# Disable systemd-resolved to free up port 53
systemctl stop systemd-resolved 2>/dev/null
systemctl disable systemd-resolved 2>/dev/null
systemctl mask systemd-resolved 2>/dev/null

systemctl stop systemd-resolved-varlink.socket 2>/dev/null
systemctl disable systemd-resolved-varlink.socket 2>/dev/null
systemctl mask systemd-resolved-varlink.socket 2>/dev/null

systemctl stop systemd-resolved-monitor.socket 2>/dev/null
systemctl disable systemd-resolved-monitor.socket 2>/dev/null
systemctl mask systemd-resolved-monitor.socket 2>/dev/null

# Assign Qubes standard DNS IPs to eth0 for downstream VMs
# These IPs are dynamically provided to downstream qubes via Qubes mechanisms
ip addr add 10.139.1.1/32 dev eth0 2>/dev/null
ip addr add 10.139.1.2/32 dev eth0 2>/dev/null

# start dnscrypt-proxy (let's add it just in case)
systemctl start dnscrypt-proxy
EOF

5. Configure nftables (Accept Rules for DNS) in sys-dns:

sudo tee /rw/config/qubes-firewall-user-script << 'EOF'
#!/bin/bash

# Allow DNS queries from downstream qubes to 10.139.1.1/2
# vif* are interfaces for downstream qubes, group 2
nft add rule ip qubes custom-input iifgroup 2 ip daddr { 10.139.1.1, 10.139.1.2 } udp dport 53 accept 2>/dev/null
nft add rule ip qubes custom-input iifgroup 2 ip daddr { 10.139.1.1, 10.139.1.2 } tcp dport 53 accept 2>/dev/null
EOF

Restart sys-dns to apply all changes.

6. Test with test-vm:

Create a test-vm, set Net qube to sys-dns and visit dnsleaktest.com to confirm you are using DNScrypt resolvers and not your ISP’s DNS. You should see this:

If you stop dnscrypt-proxy service in sys-dns, internet will stop working in test-vm:


Also see this guide:
Installation of Amnezia VPN and Amnezia WG: effective tools against internet blocks via DPI for China, Russia, Belarus, Turkmenistan, Iran. VPN with Vless XRay reality. Best obfuscation for WireGuard. Easy self‑hosted VPN.

4 Likes

Cool guide! I use a very similar setup. One thing that pairs nicely with dnscrypt-proxy is to download Spam/Malware/Whatever IP/Domain lists and use dnscrypt-proxy’s blacklist feature to ban those. Similar to what pi-hole does, this is a good point in the net proxyvm chain to block trackers and alike.

I usually have dnscrypt-proxy setup in the VM that also provides the VPN. Here should be said that for example mullvad has a switch via its mullvad cli or via the gui to disable themselves managing DNS - otherwise one can comment the DNS lines in a wireguard config - DNS then “is caught” by dnscrypt-proxy (via the nftables rules above), and dnscrypt-proxy’s requests are going via the VPN.

Finally, you may want to have sys-dns be a named disposable qube. In this case you can save yourself some failing systemd unit hassle in the template if you add a line in dnscypt-proxys systemd unit (as in have an override for it) where you only start it if the VM type is ProxyVM (bcs the startup resolvers are only reachable if the VM has internet access).

And as always githuuuuuuub :wink: :stuck_out_tongue: Or at least build yourself a small website or so. You can create a website with hugo, put it on github and have cloudflare pick up that website from github, this way it doesn’t cost any money to host it. If you ever want to work in IT security (if there still is IT security in two years lol) then having a website + a github repo with your projects is a massive plus for applying to companies.

2 Likes

Cooool! I tried setting up dnscrypt-proxy a year ago, but it didn’t work. And I never understood how to configure these anonymous relays. Thank you very much!

@kuhbs Thanks for the interesting comment! I will look into it.

Please write how to add it

1 Like

I was hoping @linuxuser1 would :wink:

Here is an old script I don’t maintain anymore that might push you in the right direction - its YEARS old:

# Generate blocked domains list for dnscrypt-proxy
# This should run during setup and as an update hook I think, so it stays fresh
#
# TODO also process https://github.com/mullvad/dns-blocklists


# Download and process blocklists for dnscrypt-proxy
process_blocklist() {
    list_category=$1
    mkdir -p /etc/dnscrypt-proxy/blocked-names/$list_category/
    list_url=$2
    list_filename=$(echo $list_url | tr '[:upper:]' '[:lower:]' | sed -e 's@https://@@g' -e 's@/@.@g' -e 's/\%/./g' -e 's/-/./g')
    curl $list_url \
    | sed 's/0.0.0.0//g' \
    | sed 's/127.0.0.1//g' \
    | sed 's/^[[:space:]]\+//g' \
    | sed '/^#/d' \
    | sed 's/#.*//g' \
    | sed 's/ /\n/g' \
    | sed '/./!d' \
    | egrep -v 'localhost|::1|^-$|^--$|::|\[|\]|\||</b></td>' \
    >> /etc/dnscrypt-proxy/blocked-names/$list_category/$list_filename
}

# Custom blacklist
mkdir -p /etc/dnscrypt-proxy/blocked-names/custom/
echo "*google*
*facebook*
*bing*
*instagram*" >> /etc/dnscrypt-proxy/blocked-names/custom/custom.txt

# https://www.github.developerdan.com/hosts
process_blocklist ads      https://www.github.developerdan.com/hosts/lists/ads-and-tracking-extended.txt
process_blocklist social   https://www.github.developerdan.com/hosts/lists/facebook-extended.txt
process_blocklist google   https://www.github.developerdan.com/hosts/lists/amp-hosts-extended.txt
process_blocklist hate     https://www.github.developerdan.com/hosts/lists/hate-and-junk-extended.txt
process_blocklist tracking https://www.github.developerdan.com/hosts/lists/tracking-aggressive-extended.txt

# https://firebog.net - Suspicious Lists
process_blocklist malware  https://raw.githubusercontent.com/PolishFiltersTeam/KADhosts/master/KADhosts.txt
process_blocklist malware  https://v.firebog.net/hosts/static/w3kbl.txt

# https://firebog.net - Advertising Lists
process_blocklist ads      https://v.firebog.net/hosts/AdguardDNS.txt
process_blocklist ads      https://v.firebog.net/hosts/Admiral.txt
process_blocklist ads      https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt
process_blocklist ads      https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
process_blocklist ads      https://v.firebog.net/hosts/Easylist.txt
process_blocklist ads      https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext
process_blocklist ads      https://raw.githubusercontent.com/FadeMind/hosts.extras/master/UncheckyAds/hosts
process_blocklist ads      https://raw.githubusercontent.com/bigdargon/hostsVN/master/hosts

# https://firebog.net - Malicious Lists
process_blocklist malware  https://raw.githubusercontent.com/DandelionSprout/adfilt/master/Alternate%20versions%20Anti-Malware%20List/AntiMalwareHosts.txt
process_blocklist malware  https://osint.digitalside.it/Threat-Intel/lists/latestdomains.txt
process_blocklist malware  https://v.firebog.net/hosts/Prigent-Crypto.txt
process_blocklist malware  https://bitbucket.org/ethanr/dns-blacklists/raw/8575c9f96e5b4a1308f2f12394abd86d0927a4a0/bad_lists/Mandiant_APT1_Report_Appendix_D.txt
process_blocklist malware  https://phishing.army/download/phishing_army_blocklist_extended.txt
process_blocklist malware  https://gitlab.com/quidsup/notrack-blocklists/raw/master/notrack-malware.txt
process_blocklist malware  https://v.firebog.net/hosts/Shalla-mal.txt
process_blocklist malware  https://raw.githubusercontent.com/Spam404/lists/master/main-blocked-names.txt
process_blocklist malware  https://raw.githubusercontent.com/FadeMind/hosts.extras/master/add.Risk/hosts
process_blocklist malware  https://urlhaus.abuse.ch/downloads/hostfile/

# https://firebog.net - Other Lists
process_blocklist crypto   https://zerodot1.gitlab.io/CoinBlockerLists/hosts_browser


# Combine blocklists
cat /etc/dnscrypt-proxy/blocked-names/*/* > /etc/dnscrypt-proxy/blocked-names.txt.tmp

# Clean up combined blocklists
sort --unique /etc/dnscrypt-proxy/blocked-names.txt.tmp > /etc/dnscrypt-proxy/blocked-names.txt
rm -v /etc/dnscrypt-proxy/blocked-names.txt.tmp
chown _dnscrypt-proxy:root /etc/dnscrypt-proxy/blocked-names.txt

I haven’t used this in ages, expect it to fail really hard. One way to do this better would be to tell a LLM to download and read the source of things like adblock plus or so, and see where they get their sources from. The game is essentially to find lots of highly trustworthy blocklists, download them, parse them into sth dnscrypt-proxy can read and then use them.
I want to do some self-marketing here and say that GitHub - kuhbs/qubes-snitch: qubes-snitch is a firewall that runs in a Qubes OS NetVM, and is designed similar to Little Snitch/OpenSnitch. It uses nftables to force VM traffic through userspace prompting, with separate handling for DNS queries and connection decisions, and saves allow/reject rules per source and destination. · GitHub , a snitch like firewall management thingy for Qubes OS, does you select which DNS is allowed, provided your app asks for dns over udp/53. This way you can build an allow-list, which is better than a deny-list.

Btw, @linuxuser1 , I ran this post through ChatGPT 5.6-sol and compared it with my solution (my “kuhb” that has dnscrypt-proxy) and it found a few flaws in both your and my approach (following content LLM generated)


- listen_addresses
Do not combine 127.0.0.1:53 with 0.0.0.0:53. The wildcard already includes loopback and can cause a duplicate TCP bind failure. Bind only the required address(es).

  • disabled_server_names
    Change scaleway to scaleway-fr. Names are matched exactly; his current value does not prevent the Scaleway relay and Scaleway resolver from being used together.

  • [anonymized_dns].routes
    His via=[…] list selects one alternative relay—it does not create a two-relay chain. The guide should describe this correctly.

  • server_names = []
    Use a small, audited resolver list with explicit resolver-to-relay pairings. Selecting every matching resolver makes it harder to guarantee that resolver and relay operators are independent.

  • cache_file
    Use absolute paths such as /var/cache/dnscrypt-proxy/public-resolvers.md. Relative paths depend on the service’s working directory.

  • bootstrap_resolvers
    If decentralization is the goal, avoid Google (8.8.8.8) as a bootstrap provider. Bootstrap resolvers do not receive normal user queries, but they can observe startup/source-list lookups. (I’d keep quad-nine (9.9.9.9) and maybe mullvad’s dns servers here or so, just my personal recommendation (me the human)).


I read but did not check those for accuracy against the dnscrypt-proxy documentation.

Okay, I will take a look it. I use browsers with uBlock Origin, so I haven’t had any problems with ads and trackers.
As for GPT - that’s a scary word) I feel like I could write a duress-nuke-guide called “How to quickly kill your system” with just one rule: try configuring your system using GPT) In my opinion, Kimi or MiniMax are much better.

Its not just browsers that do this crap. Try opening thunderbird with a wireshark in front of it - its fucking madness. Blocking those morons centrally is a REALLY good idea imho.

As for which LLM - yeah, I’m sure there are differences. I’m using the 200 usd overlord version of ChatGPT rn and I’m kiiiiiinda ok with it, though its ability to understand my 5-6k lines of py in kuhbs are not ideal atm. I’m looking for alternatives tbh.

Okay, I will take a look it

Check MiniMax and Kimi. Chinese open source)

I wonder if CDNs like Cloudflare and AWS should be excluded.

My understanding is that if my IP is in Europe and the final DNS server receiving the request is in Asia, the DNS server will resolve to the CDN server closest to it, not the closest to my IP. And the CDN server will forward every request to the CDN closest to my IP, adding a delay.

I wonder if the delay is usually negligible or if it will make most browsing much slower, like using Tor.

I don’t notice any delays. Even if delay occurs, it’s still faster than Tor anyway.

True. It will certainly not make it faster.