How can i migrate my vpn-scripts from f38(iptables) f39(nftables)?

I use f38 template to run appvm with vpn, I’m trying to migrate now to f39 but I don’t know how to modify the scripts from iptables to nftables, i would like your help to transform the scripts below so that they can work on f39.

On AppVM(VPN) with fedora-38 template i have 3 files:

  1. /rw/config/rc.local
  2. /rw/config/vpn/qubes-vpn-handler.sh
  3. /rw/config/qubes-firewall-user-script

RC.LOCAL

#!/bin/bash
VPN_CLIENT='openvpn'
VPN_OPTIONS='--cd /rw/config/vpn/ --config openvpn-client.ovpn --daemon'

#    Add the `matrix` group to system, if it doesn't already exist

if ! grep -q "^matrix:" /etc/group ; then
     groupadd -rf matrix
     su - -c 'notify-send "$(hostname) Creating matrix" --icon=network-server' user
     sync
fi
sleep 1s
su - -c 'notify-send -u critical "$(hostname) entering matrix..." --icon=network-server' user

# matrix
sg matrix -c "$VPN_CLIENT $VPN_OPTIONS"

QUBES-VPN-HANDLER.SH

#!/bin/bash
set -e
export PATH="$PATH:/usr/sbin:/sbin"

case "$1" in

up)
# To override DHCP DNS, assign DNS addresses to 'vpn_dns' env variable before calling this script;
# Format is 'X.X.X.X  Y.Y.Y.Y [...]'

if [[ -z "$vpn_dns" ]] ; then

    # Parses DHCP foreign_option_* vars to automatically set DNS address translation:

    for optionname in ${!foreign_option_*} ; do
        option="${!optionname}"
        unset fops; fops=($option)
        if [ ${fops[1]} == "DNS" ] ; then vpn_dns="$vpn_dns ${fops[2]}" ; fi
    done
fi

iptables -t nat -F PR-QBS
if [[ -n "$vpn_dns" ]] ; then

    # Set DNS address translation in firewall:

    for addr in $vpn_dns; do
        iptables -t nat -A PR-QBS -i vif+ -p udp --dport 53 -j DNAT --to $addr
        iptables -t nat -A PR-QBS -i vif+ -p tcp --dport 53 -j DNAT --to $addr
    done
    su - -c 'notify-send -u critical "WELCOME !" "$(hostname): ON." --icon=network-idle' user
else
    su - -c 'notify-send "$(hostname): ON, no DNS!" --icon=dialog-error' user
fi

;;

down)
su - -c 'notify-send -u critical "TANGO DOWN !" "$(hostname): OFF !" --icon=dialog-warning' user

# Restart the VPN automatically

sleep 3s
sudo /rw/config/rc.local

;;

esac

QUBES-FIREWALL-USER-SCRIPT

#!/bin/bash

#    Block forwarding of connections through upstream network device
#    (in case the vpn tunnel breaks):
iptables -I FORWARD -o eth0 -j DROP
iptables -I FORWARD -i eth0 -j DROP
ip6tables -I FORWARD -o eth0 -j DROP
ip6tables -I FORWARD -i eth0 -j DROP

#    Accept traffic to VPN
iptables -P OUTPUT ACCEPT
iptables -F OUTPUT

#    Block non-VPN traffic to clearnet
iptables -I OUTPUT -o eth0 -j DROP

#    Allow traffic from the `matrix` group to the uplink interface (eth0);
#    Our VPN client will run with group `matrix`.
iptables -I OUTPUT -p all -o eth0 -m owner --gid-owner matrix -j ACCEPT

Do you have Qubes OS 4.1 or Qubes OS 4.2?
The switch from iptables to nftables happens when switching the Qubes OS versions, not when switching template versions. So if you’re using Qubes OS 4.1 then both fedora-38 and fedora-39 templates will still have iptables and if you have Qubes OS 4.2 then both templates should have nftables.

so… im using fedora 38 on qubes 4.2.1 and is working… didnt know about that…and i migrated my appvpm using a backup from qubes 4.1 and is working… i was thinking that the problem was from templates… and the f38 template has both… nft and iptables… so i dont need to change nothiing?

Since you’re using Qubes OS 4.2 then you need to switch the iptables to nftables.
And it’s better to use the fresh template installed from Qubes OS repository instead of upgrading your old fedora-38 template in-place since you have iptables installed there and maybe some other legacy things that could break something.
You can check this post as an example:

apparatus i want to do it on fresh template, just need someone to convert those iptables codes to nftable

Check the link in my previous post, the scripts there are the same as yours and converted from iptables to nftables.

apparatus i have done what you said… i used iptables-translate to convert all the iptables and got this error when trying to connect…

user@teste:~$ sudo openvpn --cd /rw/config/vpn/ --config openvpn-client.ovpn
2024-07-20 20:12:21 WARNING: file 'user.key' is group or others accessible
2024-07-20 20:12:21 OpenVPN 2.5.1 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on May 14 2021
2024-07-20 20:12:21 library versions: OpenSSL 1.1.1w  11 Sep 2023, LZO 2.10
2024-07-20 20:12:21 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
2024-07-20 20:12:21 Outgoing Control Channel Encryption: Cipher 'AES-256-CTR' initialized with 256 bit key
2024-07-20 20:12:21 Outgoing Control Channel Encryption: Using 256 bit message hash 'SHA256' for HMAC authentication
2024-07-20 20:12:21 Incoming Control Channel Encryption: Cipher 'AES-256-CTR' initialized with 256 bit key
2024-07-20 20:12:21 Incoming Control Channel Encryption: Using 256 bit message hash 'SHA256' for HMAC authentication
2024-07-20 20:12:21 TCP/UDP: Preserving recently used remote address: [AF_INET]X.X.X.X:YYYY
2024-07-20 20:12:21 Socket Buffers: R=[212992->212992] S=[212992->212992]
2024-07-20 20:12:21 UDP link local: (not bound)
2024-07-20 20:12:21 UDP link remote: [AF_INET]X.X.X.X:YYYY
2024-07-20 20:12:21 TLS: Initial packet from [AF_INET]X.X.X.X:YYYY, sid=b5b493f4 77c7d1e5
2024-07-20 20:12:22 VERIFY OK: depth=1, C=IT, ST=IT, L=Perugia, O=airvpn.org, CN=airvpn.org CA, emailAddress=info@airvpn.org
2024-07-20 20:12:22 VERIFY KU OK
2024-07-20 20:12:22 Validating certificate extended key usage
2024-07-20 20:12:22 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
2024-07-20 20:12:22 VERIFY EKU OK
2024-07-20 20:12:22 VERIFY OK: depth=0, C=IT, ST=IT, L=Perugia, O=airvpn.org, CN=Diphda, emailAddress=info@airvpn.org
2024-07-20 20:12:22 Control Channel: TLSv1.3, cipher TLSv1.3 TLS_CHACHA20_POLY1305_SHA256, 4096 bit RSA
2024-07-20 20:12:22 [Diphda] Peer Connection Initiated with [AF_INET]X.X.X.X:YYYY
2024-07-20 20:12:22 PUSH: Received control message: 'PUSH_REPLY,comp-lzo no,redirect-gateway  def1 bypass-dhcp,dhcp-option DNS X.X.X.X,route-gateway X.X.X.X,topology subnet,ping 10,ping-restart 60,ifconfig X.X.X.X X.X.X.X,peer-id 12,cipher AES-256-GCM'
2024-07-20 20:12:22 OPTIONS IMPORT: timers and/or timeouts modified
2024-07-20 20:12:22 OPTIONS IMPORT: compression parms modified
2024-07-20 20:12:22 OPTIONS IMPORT: --ifconfig/up options modified
2024-07-20 20:12:22 OPTIONS IMPORT: route options modified
2024-07-20 20:12:22 OPTIONS IMPORT: route-related options modified
2024-07-20 20:12:22 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
2024-07-20 20:12:22 OPTIONS IMPORT: peer-id set
2024-07-20 20:12:22 OPTIONS IMPORT: adjusting link_mtu to 1625
2024-07-20 20:12:22 OPTIONS IMPORT: data channel crypto options modified
2024-07-20 20:12:22 Data Channel: using negotiated cipher 'AES-256-GCM'
2024-07-20 20:12:22 Outgoing Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
2024-07-20 20:12:22 Incoming Data Channel: Cipher 'AES-256-GCM' initialized with 256 bit key
2024-07-20 20:12:22 net_route_v4_best_gw query: dst 0.0.0.0
2024-07-20 20:12:22 net_route_v4_best_gw result: via X.X.X.X dev eth0
2024-07-20 20:12:22 ROUTE_GATEWAY X.X.X.X
2024-07-20 20:12:22 TUN/TAP device tun0 opened
2024-07-20 20:12:22 net_iface_mtu_set: mtu 1500 for tun0
2024-07-20 20:12:22 net_iface_up: set tun0 up
2024-07-20 20:12:22 net_addr_v4_add: X.X.X.X/24 dev tun0
2024-07-20 20:12:22 qubes-vpn-handler.sh up tun0 1500 1553 X.X.X.X X.X.X.X init
Error: syntax error, unexpected addr, expecting string
insert rule ip nat dnat-dns iifname vif* udp dport 53 counter dnat to $addr
                                                                       ^^^^
2024-07-20 20:12:22 WARNING: Failed running command (--up/--down): external program exited with error status: 1
2024-07-20 20:12:22 Exiting due to fatal error

Don’t use iptables-translate to convert the rules, they won’t convert properly because Qubes OS is using custom table and chain names.
The linked post already contains two scripts /rw/config/vpn/qubes-vpn-handler.sh and /rw/config/qubes-firewall-user-script that have firewall rules converted from iptables to nftables. Use the firewall rules from there.
Open the link and click on the /rw/config/vpn/qubes-vpn-handler.sh and /rw/config/qubes-firewall-user-script lines to expand the hidden text.

So i did as you said, installed a fresh template fedora-39-minimal, cloned (f39m-vpn) and installed all the packages needed to run the vpn on this, copied your scripts and restart the vpn… open console and started vpn, it gets stucked here:

bash-5.2# sudo openvpn --cd /rw/config/vpn --config openvpn-client.ovpn
2024-07-21 06:50:28 WARNING: file 'user.key' is group or others accessible
2024-07-21 06:50:28 OpenVPN 2.6.9 x86_64-redhat-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] [DCO]
2024-07-21 06:50:28 library versions: OpenSSL 3.1.1 30 May 2023, LZO 2.10
2024-07-21 06:50:28 DCO version: N/A
2024-07-21 06:50:28 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
2024-07-21 06:50:28 TCP/UDP: Preserving recently used remote address: [AF_INET]x.x.x.x:yyyy
2024-07-21 06:50:28 Socket Buffers: R=[212992->212992] S=[212992->212992]
2024-07-21 06:50:28 UDPv4 link local: (not bound)
2024-07-21 06:50:28 UDPv4 link remote: [AF_INET]x.x.x.x:yyyy
2024-07-21 06:50:28 write UDPv4 []: Operation not permitted (fd=3,code=1)
2024-07-21 06:50:30 write UDPv4 []: Operation not permitted (fd=3,code=1)
2024-07-21 06:50:34 write UDPv4 []: Operation not permitted (fd=3,code=1)

i noticed that you have made some changes in qubes-firewall-user-script… perhaps something about that because you didnt test it?

Those are not scripts created by me, they are used in the guide in that link.
That guide assumes that you will run openvpn process with qvpn group permissions:

So the firewall rules in the script are restricting the outbound traffic and only allow the traffic originated from the processes started by qvpn group.

Since you use different group name in your script then change qvpn to matrix or vice versa.
And start the openvpn process from this group and not like this:

yes, observed that and made the modification before running, and i still cant connect.
if i changed drop to accept in this line inside qubes-firewall-user-script i can browser inside the appvm that is running the vpn but if i try to use this appvm as proxy to another qube, i cant get connection

nft 'add chain qubes output { type filter hook output priority 0; policy drop; }'

but if i change it to drop i cant do that…

If you want to test the openvpn connection then run it like this:

sudo sg qvpn -c "openvpn --cd /rw/config/vpn --config openvpn-client.ovpn"

Change qvpn to the group name that you use in the firewall rules.

so i have copied your scripts exactly as you posted here… to vpn connects but no traffic on vms…

/rw/config/qubes-firewall-user-script

#!/bin/bash

#    Block forwarding of connections through upstream network device
#    (in case the vpn tunnel breaks):
# Prevent the qube to forward traffic outside of the VPN
nft insert rule qubes custom-forward oifname eth0 counter drop
nft insert rule ip6 qubes custom-forward oifname eth0 counter drop
nft insert rule qubes custom-forward iifname eth0 counter drop
nft insert rule ip6 qubes custom-forward iifname eth0 counter drop

#    Add the `qvpn` group to system, if it doesn't already exist
if ! grep -q "^qvpn:" /etc/group ; then
     sleep 3s
     groupadd -rf qvpn
     sync
fi
sleep 2s

#    Accept traffic to VPN
nft 'add chain qubes output { type filter hook output priority 0; policy drop; }'
#nft 'add chain qubes output { type filter hook output priority 0; policy accept; }'#iptables -P OUTPUT ACCEPT
#iptables -F OUTPUT

#    Block non-VPN traffic to clearnet
#nft insert rule ip qubes output oifname eth0 counter drop
#iptables -I OUTPUT -o eth0 -j DROP


#    Allow traffic from the `qvpn` group to the uplink interface (eth0);
#    Our VPN client will run with group `qvpn`.
nft insert rule ip qubes output oifname eth0 skgid qvpn accept
#iptables -I OUTPUT -p all -o eth0 -m owner --gid-owner qvpn -j ACCEPT

/rw/config/vpn/qubes-vpn-handler.sh

#!/bin/bash
set -e
export PATH="$PATH:/usr/sbin:/sbin"

case "$1" in

up)
# To override DHCP DNS, assign DNS addresses to 'vpn_dns' env variable before calling this script;
# Format is 'X.X.X.X  Y.Y.Y.Y [...]'
if [[ -z "$vpn_dns" ]] ; then
    # Parses DHCP foreign_option_* vars to automatically set DNS address translation:
    for optionname in ${!foreign_option_*} ; do
        option="${!optionname}"
        unset fops; fops=($option)
        if [ ${fops[1]} == "DNS" ] ; then vpn_dns="$vpn_dns ${fops[2]}" ; fi
    done
fi


nft flush chain ip qubes dnat-dns
#nft add chain qubes nat { type nat hook prerouting priority dstnat\; }
#iptables -t nat -F PR-QBS
if [[ -n "$vpn_dns" ]] ; then
    # Set DNS address translation in firewall:
    for addr in $vpn_dns; do
        nft add rule qubes dnat-dns iifname == "vif*" tcp dport 53 dnat "$addr"
        nft add rule qubes dnat-dns iifname == "vif*" udp dport 53 dnat "$addr"
        #iptables -t nat -A PR-QBS -i vif+ -p udp --dport 53 -j DNAT --to $addr
        #iptables -t nat -A PR-QBS -i vif+ -p tcp --dport 53 -j DNAT --to $addr
    done
    su - -c 'notify-send "$(hostname): LINK IS UP." --icon=network-idle' user
else
    su - -c 'notify-send "$(hostname): LINK UP, NO DNS!" --icon=dialog-error' user
fi

;;
down)
su - -c 'notify-send "$(hostname): LINK IS DOWN !" --icon=dialog-error' user

# Restart the VPN automatically
#sleep 5s
#sudo /rw/config/rc.local
;;
esac

i disable rc.local and start the vm manually… it worked but now the problem is: no traffic insiide when using qubes-firewall-user-script with this line

#    Accept traffic to VPN
nft 'add chain qubes output { type filter hook output priority 0; policy drop; }'

and if i change “drop” to accept" i have traffic inside vpn-vm, but can use it as proxyvm to another qube

It’s intended behavior to prevent VPN leaks and only allow the openvpn connection in the vpn-vm.
If you want to test the network in the vpn-vm then run the commands as qvpn group e.g.:

sudo sg qvpn -c "ping 9.9.9.9"
[user@fedora ~]$ sudo sg qvpn -c "ping 9.9.9.9"
PING 9.9.9.9 (9.9.9.9) 56(84) bytes of data.
64 bytes from 9.9.9.9: icmp_seq=1 ttl=52 time=27.9 ms
64 bytes from 9.9.9.9: icmp_seq=2 ttl=52 time=28.8 ms
64 bytes from 9.9.9.9: icmp_seq=3 ttl=52 time=25.3 ms
64 bytes from 9.9.9.9: icmp_seq=4 ttl=52 time=25.5 ms
64 bytes from 9.9.9.9: icmp_seq=5 ttl=52 time=24.4 ms
^C
--- 9.9.9.9 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4006ms
rtt min/avg/max/mdev = 24.371/26.374/28.776/1.662 ms
[user@fedora ~]$ 

without vpn connection i have ping but if i run vpn and try it fails. get stucked…

vpn

by the way this is how im using it

appvm ↔ vpn_apparatus ↔ sys-firewall-vpn ↔ sys-net-eth

What’s the output of this command in vpn_apparatus qube?

sudo nft list ruleset

Are you sure that your VPN is connecting successfully in vpn_apparatus qube?

here is the output:

table ip qubes {
	set downstream {
		type ipv4_addr
	}

	set allowed {
		type ifname . ipv4_addr
	}

	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 0 bytes 0 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 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 {
		iifname "eth0" counter packets 0 bytes 0 drop
		oifname "eth0" counter packets 0 bytes 0 drop
	}

	chain output {
		type filter hook output priority filter; policy drop;
		oifname "eth0" meta skgid 990 accept
	}

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

	set allowed {
		type ifname . ipv6_addr
	}

	chain antispoof {
		iifname . ip6 saddr @allowed accept
		counter packets 0 bytes 0 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 {
		iifname "eth0" counter packets 0 bytes 0 drop
		oifname "eth0" counter packets 0 bytes 0 drop
	}
}
table ip 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;
		iifname != "vif*" ip saddr { 10.137.0.36, 10.137.0.53 } drop
	}

	chain postrouting {
		type filter hook postrouting priority raw; policy accept;
		oifname != "vif*" ip daddr { 10.137.0.36, 10.137.0.53 } drop
	}
}
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;
	}
}

yes im sure vpn_apparatus is connected

inside client area panel shows that im connected.

so, it looks like the problem was that the service network-manager was enable, i disabled it on qubes-manager “services” window and it is running like a charm. i would like to thank you @apparatus for your attention with this topic, you really helped me with this… theres something i forget to tell that is about the notification-daemon, on vpn template i had to make some conf about notifications.

created a file named:

org.freedesktop.Notifications.service

inside directory:

/usr/share/dbus-1/services/

with this content:

[user@fedora ~]$ cat /usr/share/dbus-1/services/org.freedesktop.Notifications.service 

[D-BUS Service]
Name=org.freedesktop.Notifications
Exec=/usr/libexec/notification-daemon

[user@fedora ~]$