Skip to content

Instantly share code, notes, and snippets.

@neowutran
Forked from fepitre/qvm-portfwd-iptables
Last active July 28, 2025 16:29
Show Gist options
  • Save neowutran/e93ce542ba1e94a5ecbf1a38eef85485 to your computer and use it in GitHub Desktop.
Save neowutran/e93ce542ba1e94a5ecbf1a38eef85485 to your computer and use it in GitHub Desktop.
Qubes-os port forwarding to allow external connections
#!/bin/bash
# Neowutran <[email protected]>
# 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"
@imme-emosol
Copy link

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
Author

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

@IOZZYS
Copy link

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
Author

thanks, updated with your suggestion

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