[Help needed] Using zapret (a DPI bypass tool) and DNSCrypt

Help needed. Read this first

After doing all the steps described below, I can’t access the internet. The packets stop at sys-net:vif* and aren’t getting forwarded to ens6 (confirmed with wireshark). If in 4.15 I choose to leave the interfaces at their default values and skip 4.16, then zapret only applies in sys-net, but not in connected qubes. Please help me resolve this issue. (For the record I’m using tpws in transparent mode)

I plan to turn this post into a guide when the issue is solved, which is why it’s formatted like that

Below is the output of different commands when the internet is broken:

nft list tables
table ip qubes
table ip6 qubes
table ip qubes-firewall
table ip6 qubes-firewall
table inet zapret
table inet qubes-nat-accel
nft list table ip qubes
table ip qubes {
	set downstream {
		type ipv4_addr
		elements = { 10.138.39.29 }
	}

	set allowed {
		type ifname . ipv4_addr
		elements = { "vif162.0" . 10.138.39.29 }
	}

	chain prerouting {
		type filter hook prerouting priority raw; policy accept;
		iifgroup 2 goto antispoof
		ip saddr @downstream counter packets 0 bytes 0 drop
	}

	chain antispoof {
		iifname . ip saddr @allowed accept
		counter packets 0 bytes 0 drop
	}

	chain postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		oifgroup 2 accept
		oif "lo" accept
		masquerade
	}

	chain input {
		type filter hook input priority filter; policy drop;
		jump custom-input
		ct state invalid counter packets 64 bytes 5185 drop
		iifgroup 2 udp dport 68 counter packets 0 bytes 0 drop
		ct state established,related accept
		iifgroup 2 meta l4proto icmp accept
		iif "lo" accept
		iifgroup 2 counter packets 0 bytes 0 reject with icmp host-prohibited
		counter packets 755 bytes 88626
	}

	chain forward {
		type filter hook forward priority filter; policy accept;
		jump custom-forward
		ct state invalid counter packets 49 bytes 3136 drop
		ct state established,related accept
		oifgroup 2 counter packets 0 bytes 0 drop
	}

	chain custom-input {
	}

	chain custom-forward {
	}

	chain dnat-dns {
		type nat hook prerouting priority dstnat; policy accept;
		ip daddr 10.139.1.1 udp dport 53 dnat to 8.8.8.8
		ip daddr 10.139.1.1 tcp dport 53 dnat to 8.8.8.8
		ip daddr 10.139.1.2 udp dport 53 dnat to 8.8.8.8
		ip daddr 10.139.1.2 tcp dport 53 dnat to 8.8.8.8
	}
}
nft list table ip6 qubes
table ip6 qubes {
	set downstream {
		type ipv6_addr
	}

	set allowed {
		type ifname . ipv6_addr
	}

	chain antispoof {
		iifname . ip6 saddr @allowed accept
		counter packets 70 bytes 4596 drop
	}

	chain prerouting {
		type filter hook prerouting priority raw; policy accept;
		iifgroup 2 goto antispoof
		ip6 saddr @downstream counter packets 0 bytes 0 drop
	}

	chain postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		oifgroup 2 accept
		oif "lo" accept
		masquerade
	}

	chain _icmpv6 {
		meta l4proto != ipv6-icmp counter packets 0 bytes 0 reject with icmpv6 admin-prohibited
		icmpv6 type { nd-router-advert, nd-redirect } counter packets 0 bytes 0 drop
		accept
	}

	chain input {
		type filter hook input priority filter; policy drop;
		jump custom-input
		ct state invalid counter packets 0 bytes 0 drop
		ct state established,related accept
		iifgroup 2 goto _icmpv6
		iif "lo" accept
		ip6 saddr fe80::/64 ip6 daddr fe80::/64 udp dport 546 accept
		meta l4proto ipv6-icmp accept
		counter packets 0 bytes 0
	}

	chain forward {
		type filter hook forward priority filter; policy accept;
		jump custom-forward
		ct state invalid counter packets 0 bytes 0 drop
		ct state established,related accept
		oifgroup 2 counter packets 0 bytes 0 drop
	}

	chain custom-input {
	}

	chain custom-forward {
	}
}

nft list table ip qubes-firewall
table ip qubes-firewall {
	chain forward {
		type filter hook forward priority filter; policy drop;
		ct state established,related accept
		iifname != "vif*" accept
		ip saddr 10.138.39.29 jump qbs-10-138-39-29
		ip saddr 10.138.5.194 jump qbs-10-138-5-194
	}

	chain prerouting {
		type filter hook prerouting priority raw; policy accept;
		iifname != "vif*" ip saddr 10.138.39.29 drop
	}

	chain postrouting {
		type filter hook postrouting priority raw; policy accept;
		oifname != "vif*" ip daddr 10.138.39.29 drop
	}

	chain qbs-10-138-39-29 {
		accept
		reject with icmp admin-prohibited
	}

	chain qbs-10-138-5-194 {
		accept
		reject with icmp admin-prohibited
	}
}
nft list table ip6 qubes-firewall
table ip6 qubes-firewall {
	chain forward {
		type filter hook forward priority filter; policy drop;
		ct state established,related accept
		iifname != "vif*" accept
	}

	chain prerouting {
		type filter hook prerouting priority raw; policy accept;
	}

	chain postrouting {
		type filter hook postrouting priority raw; policy accept;
	}
}
nft list table inet zapret
table inet zapret {
	set zapret {
		type ipv4_addr
		policy memory
		size 522288
		flags interval
		auto-merge
	}

	set ipban {
		type ipv4_addr
		policy memory
		size 522288
		flags interval
		auto-merge
	}

	set nozapret {
		type ipv4_addr
		policy memory
		size 65536
		flags interval
		auto-merge
		elements = { 10.0.0.0/8, 127.0.0.0/8,
			     169.254.0.0/16, 172.16.0.0/12,
			     192.168.0.0/16 }
	}

	set lanif {
		type ifname
		elements = { "vif162.0" }
	}

	set wanif {
		type ifname
		elements = { "ens6" }
	}

	set wanif6 {
		type ifname
	}

	map link_local {
		type ifname : ipv6_addr
	}

	chain dnat_output {
		type nat hook output priority dstnat - 1; policy accept;
		meta skuid != 981 oifname @wanif tcp dport { 80, 443 } ip daddr != @nozapret ip daddr != @ipban dnat ip to 127.0.0.127:988
	}

	chain dnat_pre {
		type nat hook prerouting priority dstnat - 1; policy accept;
		iifname @lanif tcp dport { 80, 443 } ip daddr != @nozapret ip daddr != @ipban dnat ip to 127.0.0.127:988
	}

	chain forward {
		type filter hook forward priority filter - 1; policy accept;
	}

	chain input {
		type filter hook input priority filter - 1; policy accept;
		iif != "lo" jump localnet_protect
	}

	chain flow_offload {
	}

	chain localnet_protect {
		ip daddr 127.0.0.127 return comment "route_localnet allow access to tpws"
		ip daddr 127.0.0.0/8 drop comment "route_localnet remote access protection"
	}

	chain postrouting {
	}

	chain postrouting_hook {
		type filter hook postrouting priority srcnat - 1; policy accept;
		meta mark & 0x40000000 == 0x00000000 jump postrouting
	}

	chain postnat {
	}

	chain postnat_hook {
		type filter hook postrouting priority srcnat + 1; policy accept;
		meta mark & 0x40000000 == 0x00000000 jump postnat
	}

	chain prerouting {
		type filter hook prerouting priority dstnat + 1; policy accept;
		icmp type time-exceeded ct state invalid drop
		icmp type time-exceeded ct mark & 0x40000000 != 0x00000000 drop comment "nfqws related : prevent ttl expired socket errors"
	}

	chain prenat {
		type filter hook prerouting priority dstnat - 1; policy accept;
	}

	chain predefrag {
		type filter hook output priority -401; policy accept;
		meta mark & 0x40000000 != 0x00000000 jump predefrag_nfqws comment "nfqws generated : avoid drop by INVALID conntrack state"
	}

	chain predefrag_nfqws {
		meta mark & 0x20000000 != 0x00000000 notrack comment "postnat traffic"
		ip frag-off & 0x1fff != 0x0 notrack comment "ipfrag"
		exthdr frag exists notrack comment "ipfrag"
		tcp flags ! syn,rst,ack notrack comment "datanoack"
	}
}
nft list table inet qubes-nat-accel
table inet qubes-nat-accel {
	flowtable qubes-accel {
		hook ingress priority filter
		devices = { ens6, lo, vif162.0 }
	}

	chain qubes-accel {
		type filter hook forward priority filter + 5; policy accept;
		meta l4proto { tcp, udp } iifgroup 2 oifgroup 1 flow add @qubes-accel
		counter packets 12281 bytes 8701803
	}
}

And when the internet works, but zapret does not apply:

nft list table inet zapret
table inet zapret {
	set zapret {
		type ipv4_addr
		policy memory
		size 522288
		flags interval
		auto-merge
	}

	set ipban {
		type ipv4_addr
		policy memory
		size 522288
		flags interval
		auto-merge
	}

	set nozapret {
		type ipv4_addr
		policy memory
		size 65536
		flags interval
		auto-merge
		elements = { 10.0.0.0/8, 127.0.0.0/8,
			     169.254.0.0/16, 172.16.0.0/12,
			     192.168.0.0/16 }
	}

	set lanif {
		type ifname
	}

	set wanif {
		type ifname
	}

	set wanif6 {
		type ifname
	}

	map link_local {
		type ifname : ipv6_addr
	}

	chain dnat_output {
		type nat hook output priority dstnat - 1; policy accept;
		meta skuid != 981 tcp dport { 80, 443 } ip daddr != @nozapret ip daddr != @ipban dnat ip to 127.0.0.127:988
	}

	chain dnat_pre {
		type nat hook prerouting priority dstnat - 1; policy accept;
		iifname @lanif tcp dport { 80, 443 } ip daddr != @nozapret ip daddr != @ipban dnat ip to 127.0.0.127:988
	}

	chain forward {
		type filter hook forward priority filter - 1; policy accept;
	}

	chain input {
		type filter hook input priority filter - 1; policy accept;
		iif != "lo" jump localnet_protect
	}

	chain flow_offload {
	}

	chain localnet_protect {
		ip daddr 127.0.0.127 return comment "route_localnet allow access to tpws"
		ip daddr 127.0.0.0/8 drop comment "route_localnet remote access protection"
	}

	chain postrouting {
	}

	chain postrouting_hook {
		type filter hook postrouting priority srcnat - 1; policy accept;
		meta mark & 0x40000000 == 0x00000000 jump postrouting
	}

	chain postnat {
	}

	chain postnat_hook {
		type filter hook postrouting priority srcnat + 1; policy accept;
		meta mark & 0x40000000 == 0x00000000 jump postnat
	}

	chain prerouting {
		type filter hook prerouting priority dstnat + 1; policy accept;
		icmp type time-exceeded ct state invalid drop
		icmp type time-exceeded ct mark & 0x40000000 != 0x00000000 drop comment "nfqws related : prevent ttl expired socket errors"
	}

	chain prenat {
		type filter hook prerouting priority dstnat - 1; policy accept;
	}

	chain predefrag {
		type filter hook output priority -401; policy accept;
		meta mark & 0x40000000 != 0x00000000 jump predefrag_nfqws comment "nfqws generated : avoid drop by INVALID conntrack state"
	}

	chain predefrag_nfqws {
		meta mark & 0x20000000 != 0x00000000 notrack comment "postnat traffic"
		ip frag-off & 0x1fff != 0x0 notrack comment "ipfrag"
		exthdr frag exists notrack comment "ipfrag"
		tcp flags ! syn,rst,ack notrack comment "datanoack"
	}
}

The guide that needs fixing:

This is a guide for using [zapret | github.com] together with DNSCrypt on QubesOS.

Notes

  • This guide involves giving a TemplateVM (sys-net-tmpl) network access. This means that the TemplateVM should be considered compromised, which is fine, as the only qube based on the template (sys-net) is untrusted
  • We have to run both zapret and blockcheck in a PCI-passthrough qubes, because QubesOS’ networking interferes with both
  • This guide assumes that sys-net is not disposable

1. Installing the necessary packages

  1. Create a new TemplateVM using the “Create New Qube” tool called sys-net-tmpl (red) (based on the same template as sys-net, in my case: fedora-41-xfce)
  2. Start sys-net-tmpl
  3. sys-net-tmpl: sudo dnf install nslookup
  4. sys-net-tmpl: sudo dnf install dnscrypt-proxy
  5. sys-net-tmpl: Configure dnscrypt-proxy if needed in /etc/dnscrypt-proxy/dnscrypt-proxy.toml (see [Dnscrypt-proxy#Configuration | wiki.archlinux.org] for details) (this is a temporary configuration that will only be used in the blockcheck)

3. Downloading zapret

  1. Download the latest tarball from [zapret releases | github.com]
  2. Copy it using qvm-copy to sys-net-tmpl
  3. sys-net-tmpl: Unpack the tarball

4. Installing zapret

  1. Shut down sys-net-tmpl
  2. Set the NetVM of sys-firewall (and all other qubes connected to sys-net) to (none)
  3. Shut down sys-net
  4. Change the virtualization mode of sys-net-tmpl to HVM
  5. Add your ethernet card to sys-net-tmpl
  6. Start sys-net-tmpl
  7. sys-net-tmpl: sudo systemctl start dnscrypt-proxy
  8. sys-net-tmpl: sudo touch /var/run/qubes-service/network-manager
  9. sys-net-tmpl: sudo systemctl start NetworkManager
  10. If your network doesn’t use DHCP, you’ll have to set the ip manually here
  11. Wait for NetworkManager to connect
  12. sys-net-tmpl: Clear the contents of /etc/resolv.conf and add nameserver 127.0.0.1
  13. sys-net-tmpl: sudo systemctl restart systemd-resolved
  14. sys-net-tmpl: Go to zapret’s directory and run ./install_bin.sh and then ./blockcheck.sh (see [zapret docs | github.com] for details)
  15. sys-net-tmpl: ./install_easy.sh
    • Answer yes when prompted to copy to /opt/zapret
    • If you enable tpws enable it with “transparent mode”
    • Set the LAN interface to lo (we’ll change it later)
    • Set the WAN interface to your actual ethernet card
  16. Edit the zapret config in /opt/zapret/config, find IFACE_LAN=lo and change it to IFACE_LAN="$(ls /sys/class/net/ | grep vif* | tr '\n' ' ')"
  17. sys-net-tmpl: Test the DPI bypass by curling some blocked websites
  18. Shut down sys-net-tmpl

5. Setting up sys-net

  1. Change the template of sys-net to sys-net-tmpl
  2. Start sys-net
  3. Revert the NetVM of all qubes you changed in 4.2 to sys-net

6. Setting up encrypted DNS

This part is based on [this guide | forum.qubes-os.org] by qubist

  1. Install the fedora-<latest>-minimal TemplateVM (from now on called just fedora-minimal, see [QubesOS docs | qubes-os.org] for details)
  2. Create a new TemplateVM using the “Create New Qube” tool called sys-dns-tmpl (yellow), cloned from fedora-minimal
  3. Start sys-dns-tmpl
  4. dom0: qvm-run -u root sys-dns-tmpl xterm
  5. sys-dns-tmpl: dnf install qubes-core-agent-networking dnscrypt-proxy vim-minimal iptables-nft sysctl
  6. sys-dns-tmpl: systemctl disable dnscrypt-proxy
  7. Create the dnscrypt user and group: (sys-dns-tmpl)
groupadd --system dnscrypt
useradd --system --home /run/dnscrypt-proxy --shell /bin/false --gid dnscrypt dnscrypt
usermod --lock dnscrypt
  1. sys-dns-tmpl: mkdir -p /run/dnscrypt-proxy
  2. Set proper ownership and permissions: (sys-dns-tmpl)
chown dnscrypt:dnscrypt /run/dnscrypt-proxy
chmod go-rwx /run/dnscrypt-proxy
chown -R dnscrypt:dnscrypt /etc/dnscrypt-proxy
chmod -R go-rwx /etc/dnscrypt-proxy
  1. sys-dns-tmpl: Edit /etc/dnscrypt-proxy/dnscrypt-proxy.toml and set user_name = 'dnscrypt'
  2. Shut down sys-dns-tmpl
  3. Create the following VM:
    • Name: sys-dns-dvm (yellow)
    • Type: “AppVM”
    • Template: sys-dns-tmpl
    • Settings > Advanced > Disposable Template: On
  4. Start sys-dns-dvm
  5. dom0: qvm-run -u root sys-dns-dvm xterm
  6. sys-dns-dvm: mv /etc/dnscrypt-proxy /rw/
  7. sys-dns-dvm: Create /rw/config/setup-dns.sh
File /rw/config/setup-dns.sh
nft='/usr/sbin/nft'

# allow redirects to localhost
/usr/sbin/sysctl -w net.ipv4.conf.all.route_localnet=1
"${nft}" add rule ip qubes custom-input meta l4proto { tcp, udp } iifgroup 2 ip daddr 127.0.0.1 th dport 53 accept

# block connections to other DNS servers
"${nft}" add rule ip qubes custom-forward meta l4proto { tcp, udp } iifgroup 2 ip daddr != 127.0.0.1 th dport 53 drop

"${nft}" flush chain ip qubes dnat-dns

"${nft}" add rule ip qubes dnat-dns meta l4proto { tcp, udp } th dport 53 counter dnat to 127.0.0.1

echo 'nameserver 127.0.0.1' > /etc/resolv.conf
# https://github.com/DNSCrypt/dnscrypt-proxy/wiki/Installation-linux
# https://wiki.archlinux.org/title/Dnscrypt-proxy#Enable_EDNS0
echo 'options edns0' >> /etc/resolv.conf

ln -s /rw/dnscrypt-proxy /etc/dnscrypt-proxy
/usr/bin/systemctl start dnscrypt-proxy.service
  1. sys-dns-dvm: Create /rw/config/watch-dns.sh
File /rw/config/watch-dns.sh
while true; do
    if cat /etc/resolv.conf | grep 127.0.0.1 > /dev/null; then
        sleep 1
    else
        sleep 5
        /rw/config/setup-dns.sh
    fi
done
  1. sys-dns-dvm: Add the following to /rw/config/rc.local
File /rw/config/rc.local
/rw/config/watch-dns.sh &
  1. sys-dns-dvm: chmod +x /rw/config/*.sh
  2. Shut down sys-dns-dvm
  3. Start sys-dns-tmpl
  4. dom0: qvm-run -u root sys-dns-tmpl xterm
  5. sys-dns-tmpl: rm -rf /etc/dnscrypt-proxy
  6. Shut down sys-dns-tmpl
  7. Create the following VM:
    • Name: sys-dns (yellow)
    • Type: “DisposableVM”
    • Template: sys-dns-dvm
    • Networking: sys-net
    • Settings > Basic > Start qube automatically on boot: On
    • Settings > Advanced > Provides network: On
  8. Start sys-dns
  9. Change the NetVM of sys-firewall to sys-dns
1 Like

The Qubes OS firewall is blocking all incoming connections not coming from lo by default:

Maybe this rule will help:

nft add rule ip qubes custom-input ip daddr 127.0.0.127 accept

Also, instead of:
IFACE_LAN="$(ls /sys/class/net/ | grep vif* | tr '\n' ' ')"
You can use this:
IFACE_LAN="vif*"

Hi! I hope everything works out for you and that you’ll be able to write the guide - it would be very useful for totalitarian countries. You could also consider it GitHub - Verity-Freedom/Tor-Portable: The portable Tor based on Tor Expert Bundle and ZeroOmega. in addition to zapret

I tried that, it applies, but it doesn’t help

The couters indicate that ip qubes custom-input upd the added rule doesn’t get triggered, and neither does ip qubes input

inet zapret dnat_pre also never gets triggered upd the counters report that the dnat succedes, upd but tpws (when ran with --debug=syslog --debug-level=2) does not

upd even after flushing the ruleset and adding only this table, tpws still doesn’t receive connections

table inet zapret {
	chain input {
		type filter hook input priority filter - 1; policy accept;
		accept
	}

	chain dnat_pre {
		type nat hook prerouting priority dstnat - 1; policy accept;
		tcp dport { 80, 443 } counter packets 3238 bytes 168376 dnat ip to 127.0.0.127:988
	}
}

But tpws logs connections when using nc 127.0.0.127 988

zapret itself fails if this is used

Use tcpdump/wireshark to check where the connections are going.
You can also add log to firewall rules to check how are they processed.

Seems like it’s doing something else with IFACE_LAN, except using it in firewall rule.
I think you’ll have problem with using this:
IFACE_LAN="$(ls /sys/class/net/ | grep vif* | tr '\n' ' ')"
Since it’s probably evaluated only a single time at zapret start and you’ll need to restart zapret after new qubes connect to zapret qube and create a new vif interfaces.