Skip to content

Instantly share code, notes, and snippets.

@neowutran
Forked from fepitre/qvm-portfwd-iptables
Last active November 25, 2025 19:05
Show Gist options
  • Select an option

  • Save neowutran/e93ce542ba1e94a5ecbf1a38eef85485 to your computer and use it in GitHub Desktop.

Select an option

Save neowutran/e93ce542ba1e94a5ecbf1a38eef85485 to your computer and use it in GitHub Desktop.
Qubes-os port forwarding to allow external connections
#!/bin/bash
# Neowutran <github@neowutran.ovh>
# Adapted previous work to support QubesOS v4.2
# Adapted from previous work:
# - https://gist.github.com/fepitre/941d7161ae1150d90e15f778027e3248
# - https://gist.github.com/daktak/f887352d564b54f9e529404cc0eb60d5
# - https://gist.github.com/jpouellet/d8cd0eb8589a5b9bf0c53a28fc530369
# - https://gist.github.com/Joeviocoe/6c4dc0c283f6d6c5b1a3f5af8793292b
[ "$DEBUG" = 1 ] && set -x
forward() {
local action=${1? Action is required}
local from_qube=${2? From Qube is required}
local to_qube=${3? To Qube is required}
local port=${4? Port is required}
local proto=${5? Protocol is required}
local iface from_ip from_ip6 to_ip to_ip6
# TODO: Handle multiple interfaces in sys-net. It currently catches only the first physical interface which is UP.
iface=$(qvm-run -p -u root "$from_qube" "ip link | grep -E '^[0-9]' | grep -E 'state UP' | cut -d ':' -f 2 | cut -d ' ' -f 2 | grep -vE '^(vif|lo)' | head -1")
from_ip=$(qvm-run -p -u root "$from_qube" "ip addr show dev $iface | grep -Eo 'inet [0-9]+(\.[0-9]+){3}' | cut -d ' ' -f 2 | head -1")
from_ip6=$(qvm-run -p -u root "$from_qube" "ip addr show dev $iface | grep inet6 | grep -v deprecated | head -1 | grep -Eo 'inet6 ([^/])*' | cut -d ' ' -f 2")
to_ip=$(qvm-prefs "$to_qube" ip)
to_ip6=$(qvm-prefs "$to_qube" ip6)
qvm-run -p -u root "$from_qube" 'data=$(nft list ruleset | grep -v '"'PortFwd $from_qube $to_qube:$proto$port'"'); nft flush ruleset; echo "$data" | nft -f -'
if [[ "$action" = "create" ]]; then
echo "$from_qube: Forwarding on $iface port $port to $to_qube ($from_ip -> $to_ip)" >&2
qvm-run -p -u root "$from_qube" "nft -- create chain ip qubes-firewall custom-prerouting { type nat hook prerouting priority dstnat -1 \; }"
qvm-run -p -u root "$from_qube" "nft add rule ip qubes-firewall custom-prerouting iifname $iface ip daddr $from_ip $proto dport $port counter dnat to $to_ip comment \\\"PortFwd $from_qube $to_qube:$proto$port \\\" "
qvm-run -p -u root "$from_qube" "nft add rule ip qubes custom-forward iifname $iface ip daddr $to_ip $proto dport $port ct state new counter accept comment \\\"PortFwd $from_qube $to_qube:$proto$port\\\" "
if [[ -n "$from_ip6" ]] && [[ -n "$to_ip6" ]] ; then
echo "$from_qube: Forwarding on $iface port $port to $to_qube ($from_ip6 -> $to_ip6)" >&2
qvm-run -p -u root "$from_qube" "nft -- create chain ip6 qubes-firewall custom-prerouting { type nat hook prerouting priority dstnat -1 \; }"
qvm-run -p -u root "$from_qube" "nft add rule ip6 qubes-firewall custom-prerouting iifname $iface ip6 daddr $from_ip6 $proto dport $port counter dnat to $to_ip6 comment \\\"PortFwd $from_qube $to_qube:$proto$port \\\" "
qvm-run -p -u root "$from_qube" "nft add rule ip6 qubes custom-forward iifname $iface ip6 daddr $to_ip6 $proto dport $port ct state new counter accept comment \\\"PortFwd $from_qube $to_qube:$proto$port\\\" "
fi
fi
}
input() {
local action="$1"
local qube="$2"
local port="$3"
local proto="$4"
qvm-run -p -u root "$qube" 'data=$(nft list ruleset | grep -v '"'PortFwd $qube:$proto$port'"'); nft flush ruleset; echo "$data" | nft -f -'
if [[ "$action" = "create" ]]; then
echo "$qube: Allowing input to port $port" >&2
qvm-run -p -u root "$qube" "nft add rule ip qubes custom-input $proto dport $port ct state new counter accept comment \\\"PortFwd $qube:$proto$port\\\" "
qvm-run -p -u root "$qube" "nft add rule ip6 qubes custom-input $proto dport $port ct state new counter accept comment \\\"PortFwd $qube:$proto$port\\\" "
fi
}
recurse_netvms() {
local action="$1"
local this_qube="$2"
local port="$3"
local proto="$4"
local outer_dom
outer_dom=$(qvm-prefs "$this_qube" netvm)
if [[ -n "$outer_dom" ]]; then
forward "$action" "$outer_dom" "$this_qube" "$port" "$proto"
recurse_netvms "$action" "$outer_dom" "$port" "$proto"
fi
}
usage() {
echo "Usage: ${0##*/} --action ACTION --qube QUBE --port PORT --proto PROTO" >&2
echo "" >&2
echo "Exemple: " >&2
echo " -> ${0##*/} --action create --qube work --port 22" >&2
echo " -> ${0##*/} --action create --qube work --port 444 --proto udp" >&2
echo " -> ${0##*/} --action clear --qube work --port 22" >&2
echo " -> ${0##*/} --action clear --qube work --port 444 --proto udp" >&2
echo "" >&2
echo "Default value for PROTO is 'tcp'"
exit 1
}
if ! OPTS=$(getopt -o a:q:p:n: --long action:,qube:,port:,proto: -n "$0" -- "$@"); then
echo "An error occurred while parsing options." >&2
exit 1
fi
eval set -- "$OPTS"
while [[ $# -gt 0 ]]; do
case "$1" in
-a | --action ) ACTION="$2"; shift ;;
-q | --qube ) QUBE="$2"; shift ;;
-p | --port ) PORT="$2"; shift ;;
-n | --proto ) PROTO="$2"; shift ;;
esac
shift
done
if [ -z "$PROTO" ]; then
PROTO="tcp"
fi
if { [ "$ACTION" != "create" ] || [ "$ACTION" == "clear" ]; } && { [ -z "$QUBE" ] || [ -z "$PORT" ]; }; then
usage
fi
if ! qvm-check "$QUBE" > /dev/null 2>&1; then
echo "Qube '$QUBE' not found." >&2
exit 1
fi
input "$ACTION" "$QUBE" "$PORT" "$PROTO"
recurse_netvms "$ACTION" "$QUBE" "$PORT" "$PROTO"
@IOZZYS
Copy link
Copy Markdown

IOZZYS commented Aug 24, 2023

Time to rip my hair out as the ultimate noob that is going to try and port this to Qubes 4.2

@neowutran
Copy link
Copy Markdown
Author

neowutran commented Aug 24, 2023

my mistake, didn't checked and copied the wrong things ^^', i updated this script in one of my computer to make it work with Qubes 4.2. Will fix this git in one or two days with the correct thing

@IOZZYS
Copy link
Copy Markdown

IOZZYS commented Aug 25, 2023

Thank you so much, you're amazing!
I will try to add interface options and share my work!

@imme-emosol
Copy link
Copy Markdown

I couldn't test, because I'm not on 4.2 // missing the table 'qubes' , but perhaps this revision works : https://gist.github.com/imme-emosol/840ca36b4f098ce3431db43f08288aaa .

@neowutran
Copy link
Copy Markdown
Author

( updated my version, should work correctly on 4.2, but will not work for previous version )

@IOZZYS
Copy link
Copy Markdown

IOZZYS commented Dec 9, 2023

@neowutran Added "| grep -E '^[0-9]' | grep -E 'state UP' | cut -d ':' -f 2 |" in line 37 to ensure only interfaces that are UP are chosen for now.
@imme-emosol I remember trying your version but I don´t remember what went wrong, maybe I'll check again in a sec.

@neowutran
Copy link
Copy Markdown
Author

thanks, updated with your suggestion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment