From 80638d64b5d280587c473e487a34135824ccce4c Mon Sep 17 00:00:00 2001 From: Ben Grande <ben.grande.b@gmail.com> Date: Tue, 16 Jan 2024 00:15:29 +0100 Subject: [PATCH] feat: port forwarder If persistent rules are chosen, it can deal with disposable sys-net, but not with disposable sys-firewall, as the qube ip will change, the rule won't work. Applying the rule to the disposable template is a "try it all", but it's usage is discouraged. --- README.md | 7 - salt/sys-syncthing/README.md | 20 +- salt/sys-syncthing/create.sls | 9 + salt/sys-syncthing/files/admin/firewall/in.sh | 312 ------------------ .../files/admin/firewall/qvm-port-forward | 266 +++++++++++++++ 5 files changed, 285 insertions(+), 329 deletions(-) delete mode 100644 salt/sys-syncthing/files/admin/firewall/in.sh create mode 100644 salt/sys-syncthing/files/admin/firewall/qvm-port-forward diff --git a/README.md b/README.md index 03d9dc0..cdada5e 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,6 @@ Salt Formulas for Qubes OS. **Warning**: Not ready for production, development only. Breaking changes can and will be introduced in the meantime. You've been warned. -The following projects are unfinished (not a complete list): - -- sys-syncthing: broken firewall script due to nftables and disposable netvm - -Other projects might also have drastic changes, the above are simply not ready -at all. - ## Table of Contents * [Description](#description) diff --git a/salt/sys-syncthing/README.md b/salt/sys-syncthing/README.md index 87f9bac..0523397 100644 --- a/salt/sys-syncthing/README.md +++ b/salt/sys-syncthing/README.md @@ -29,8 +29,8 @@ qubesctl top.enable sys-syncthing browser qubesctl --targets=tpl-browser,sys-syncthing-browser,tpl-sys-syncthing,sys-syncthing state.apply qubesctl top.disable sys-syncthing browser qubesctl state.apply sys-syncthing.appmenus -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh -a -p add sys-syncthing tcp 22000 -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh -a -p add sys-syncthing udp 22000 +qvm-port-forward -a add -q sys-syncthing -n tcp -p 22000 +qvm-port-forward -a add -q sys-syncthing -n udp -p 22000 ``` - State: @@ -42,8 +42,8 @@ qubesctl --skip-dom0 --targets=tpl-sys-syncthing state.apply sys-syncthing.insta qubesctl --skip-dom0 --targets=sys-syncthing state.apply sys-syncthing.configure qubesctl --skip-dom0 --targets=sys-syncthing-browser state.apply sys-syncthing.configure-browser qubesctl state.apply sys-syncthing.appmenus -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh -a -p add sys-syncthing tcp 22000 -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh -a -p add sys-syncthing udp 22000 +qvm-port-forward -a add -q sys-syncthing -n tcp -p 22000 +qvm-port-forward -a add -q sys-syncthing -n udp -p 22000 ``` <!-- pkg:end:post-install --> @@ -99,10 +99,10 @@ If sys-net has more than one network card the first external interface will be used by default. If this is incorrect, you must change it manually. In Dom0 run: ```sh -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh delete sys-syncthing tcp 22000 -a -p -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh delete sys-syncthing udp 22000 -a -p -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh add sys-syncthing tcp 22000 -p -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh add sys-syncthing udp 22000 -p +qvm-port-forward -a del -q sys-syncthing -n udp -p 22000 +qvm-port-forward -a del -q sys-syncthing -n tcp -p 22000 +qvm-port-forward -a add -q sys-syncthing -n udp -p 22000 +qvm-port-forward -a add -q sys-syncthing -n tcp -p 22000 ``` This will let you choose the NIC. @@ -117,8 +117,8 @@ Syncthing between qubes. Uninstallation procedure: <!-- pkg:begin:preun-uninstall --> ```sh -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh -a -p delete sys-syncthing tcp 22000 -/srv/salt/qusal/sys-syncthing/files/admin/firewall/in.sh -a -p delete sys-syncthing udp 22000 +qvm-port-forward -a del -q sys-syncthing -n tcp -p 22000 +qvm-port-forward -a del -q sys-syncthing -n udp -p 22000 qubesctl --skip-dom0 --targets=sys-syncthing state.apply sys-syncthing.cancel qubesctl state.apply sys-syncthing.clean ``` diff --git a/salt/sys-syncthing/create.sls b/salt/sys-syncthing/create.sls index 4efa1fd..71a6015 100644 --- a/salt/sys-syncthing/create.sls +++ b/salt/sys-syncthing/create.sls @@ -97,3 +97,12 @@ features: {% from 'utils/macros/policy.sls' import policy_set with context -%} {{ policy_set(sls_path, '80') }} + +"{{ slsdotpath }}-qvm-port-forward": + file.managed: + - name: /usr/local/bin/qvm-port-forward + - source: salt://{{ slsdotpath }}/files/admin/firewall/qvm-port-forward + - user: root + - group: root + - mode: '0755' + - makedirs: True diff --git a/salt/sys-syncthing/files/admin/firewall/in.sh b/salt/sys-syncthing/files/admin/firewall/in.sh deleted file mode 100644 index d2bd2fa..0000000 --- a/salt/sys-syncthing/files/admin/firewall/in.sh +++ /dev/null @@ -1,312 +0,0 @@ -#!/usr/bin/env bash - -# SPDX-FileCopyrightText: 2022 unman <unman@thirdeyesecurity.org> -# SPDX-FileCopyrightText: 2023 Benjamin Grande M. S. <ben.grande.b@gmail.com> -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -## Credits: https://github.com/unman/shaker/blob/main/i2p/in.sh -## Recursively open ports through the firewall to allow remote access to a qube. - -## TODO: remove iptables in favor of nft. It doesn't work if the upstream net -## qubes are disposables, instead, the rule should be applied on the -## disposable template. This would work well if users used our project that -## creates a template per service, but if user is using a default diposable -## template for that, such as debian-XX-dvm, the firewall would allow many -## qubes to be exposed. - -me="${0##*/}" -rc="/rw/config/rc.local.d/50-port-forwarder.rc" - -usage(){ -cat <<HERE -Usage: ${me} [-h|a|p] [add|delete] [target] [tcp|udp] [port number|service] [external port] - -Options: - h print this help - a auto mode, a port will be opened on the first external interface - p permanent rules, takes effect in each qube start up - Action add, delete - Protocol tcp, udp - Target Port port number or service name (e.g. ssh) - External Port port number or service name (e.g. ssh) (default: target port) - -Example: - ${me} OPTIONS ACTION TARGET_QUBE PROTOCOL TARGET_PORT EXTERNAL_PORT - ${me} add QUBE tcp 80 80 - ${me} add QUBE tcp ssh ssh - ${me} delete QUBE tcp https https - -DO NOT use this script for qubes behind a Tor or VPN proxy. -At a minimum you risk breaking the security of those proxies. -HERE - exit 1 -} - - -## Check input port -check_port(){ - if test "$2" != "$2";then - status=1 - else - if test "$2" -lt 65536; then - status=0 - portnum="$2" - else - status=1 - fi - fi - if [ "$status" -ne 0 ]; then - if ! grep -q -w "^$2 " /etc/services; then - echo "Specify usable port number or service name" - exit 1 - else - portnum="$(getent services "$2" | awk '{split($2,a,"/");print a[1]}')" - if test -z "$portnum"; then - echo "Specify usable port number or service name" - exit 1 - fi - fi - fi - echo "$portnum" -} - - -get_handle(){ - local my_handle - my_handle="$(qvm-run -q -u root -p "$1" -- "nft -a list table $2 | awk 'BEGIN{c=0} /$3/{c++; if (c==$4) print \$NF}'")" - echo "$my_handle" -} - - -## Tunnel through netvms -tunnel(){ - declare -a my_netvms=("${!1}") - declare -a my_ips=("${!2}") - declare -i numhops - numhops="${#my_ips[@]}" - lasthop=$((numhops-1)) - local i=1 - iface="eth0" - if qvm-run -q -u root "${my_netvms[$lasthop]}" " nft list table nat|grep ' $proto dport $portnum dnat to ${my_ips[$numhops-1]}'" - then - echo "Are rules already set?" - exit 1 - fi - while test "$i" != "$numhops"; do - if test "$i" = "1"; then - portnum_used=$external_portnum - portnum_target=$portnum - else - portnum_used=$external_portnum - portnum_target=$external_portnum - fi - echo "${my_netvms[$i]} $portnum_used" - if [ $i -eq $lasthop ]; then - iface=$external_iface - fi - # Is it nft or iptables? - local found - found="$(qvm-run -p -q -u root "${my_netvms[$i]}" -- nft list table nat 2>/dev/null)" - if test -z "$found"; then - qvm-run -q -u root "${my_netvms[$i]}" -- "iptables -I QBS-FORWARD -i $iface -p $proto --dport $portnum_target -d ${my_ips[$i-1]} -j ACCEPT" - qvm-run -q -u root "${my_netvms[$i]}" -- "iptables -t nat -I PR-QBS-SERVICES -i $iface -p $proto --dport $portnum_used -j DNAT --to-destination ${my_ips[$i-1]}:$portnum_target" - if test "$permanent" = "1"; then - qvm-run -q -u root "${my_netvms[$i]}" -- "echo iptables -I QBS-FORWARD -i $iface -p $proto --dport $portnum_target -d ${my_ips[$i-1]} -j ACCEPT >> ${rc}" - qvm-run -q -u root "${my_netvms[$i]}" -- "echo iptables -t nat -I PR-QBS-SERVICES -i $iface -p $proto --dport $portnum_used -j DNAT --to-destination ${my_ips[$i-1]}:$portnum_target >> ${rc}" - fi - else - qvm-run -q -u root "${my_netvms[$i]}" -- nft insert rule nat PR-QBS-SERVICES meta iifname "$iface" "$proto" dport "$portnum_used" dnat to "${my_ips[$i-1]}:$portnum_target" - qvm-run -q -u root "${my_netvms[$i]}" -- nft insert rule filter QBS-FORWARD meta iifname "$iface" ip daddr "${my_ips[$i-1]}" "$proto" dport "$portnum_target" ct state new accept - if test "$permanent" = "1"; then - qvm-run -q -u root "${my_netvms[$i]}" -- "echo nft insert rule nat PR-QBS-SERVICES meta iifname $iface $proto dport $portnum_used dnat to ${my_ips[$i-1]}:$portnum_target >> ${rc}" - qvm-run -q -u root "${my_netvms[$i]}" -- "echo nft insert rule filter QBS-FORWARD meta iifname $iface ip daddr ${my_ips[$i-1]} $proto dport $portnum_target ct state new accept >> ${rc}" - fi - fi - ((i++)) - done -} - - -## Teardown from top netvm down -teardown(){ - declare -a my_netvms=("${!1}") - declare -a my_ips=("${!2}") - declare -i numhops - numhops=${#my_ips[@]} - numhops=$((numhops-1)) - local i=$numhops - iface="eth0" - echo "Removing firewall rules" - while [ $i -gt 0 ]; do - if [ $i -eq 1 ]; then - portnum_used=$external_portnum - portnum_target=$portnum - else - portnum_used=$external_portnum - portnum_target=$external_portnum - fi - # Is it nft or iptables? - echo "${my_netvms[$i]}" - local found - found="$( qvm-run -p -q -u root "${my_netvms[$i]}" -- "nft list table nat 2>/dev/null" )" - if test -z "$found"; then - qvm-run -q -u root "${my_netvms[$i]}" -- "iptables -D QBS-FORWARD -i $iface -p $proto --dport $portnum_target -d ${my_ips[$i-1]} -j ACCEPT" - qvm-run -q -u root "${my_netvms[$i]}" -- "iptables -t nat -D PR-QBS-SERVICES -i $iface -p $proto --dport $external_portnum -j DNAT --to-destination ${my_ips[$i-1]}:$portnum_target" - if [ "$permanent" -eq 1 ]; then - qvm-run -q -u root "${my_netvms[$i]}" -- "sed -i '/iptables -D QBS-FORWARD -i $iface -p $proto --dport $portnum_target -d ${my_ips[$i-1]} -j ACCEPT/d' ${rc}" - qvm-run -q -u root "${my_netvms[$i]}" -- "sed -i '/iptables -t nat -D PR-QBS-SERVICES -i $iface -p $proto --dport $external_portnum -j DNAT --to-destination ${my_ips[$i-1]}:$portnum_target/d' ${rc}" - fi - else - local handle - handle="$( get_handle "${my_netvms[$i]}" nat "dport $external_portnum " 1 )" - qvm-run -q -u root "${my_netvms[$i]}" -- "nft delete rule nat PR-QBS-SERVICES handle $handle" - local handle - handle="$( get_handle "${my_netvms[$i]}" filter "dport $external_portnum " 1 )" - qvm-run -q -u root "${my_netvms[$i]}" -- "nft delete rule filter QBS-FORWARD handle $handle" - if [ "$permanent" -eq 1 ]; then - qvm-run -q -u root "${my_netvms[$i]}" -- "sed -i '/nft insert rule nat PR-QBS-SERVICES meta iifname $iface $proto dport $portnum_used dnat to ${my_ips[$i-1]}:$portnum_target/d' ${rc}" - qvm-run -q -u root "${my_netvms[$i]}" -- "sed -i '/nft insert rule filter QBS-FORWARD meta iifname $iface ip daddr ${my_ips[$i-1]} $proto dport $portnum_target ct state new accept/d' ${rc}" - fi - fi - ((i--)) - done - local found - found="$( qvm-run -p -q -u root "${my_netvms[$i]}" -- nft list table nat 2>/dev/null )" - if test -z "$found"; then - qvm-run -q -u root "${my_netvms[$i]}" " iptables -D INPUT -p $proto --dport $external_portnum -j ACCEPT" - else - handle=$( get_handle "${my_netvms[$i]}" filter "dport $portnum " 1 ) - qvm-run -q -u root "${my_netvms[$i]}" -- nft delete rule filter INPUT handle "$handle" - fi - exit -} - - -list(){ - return -} - - -## Defaults -auto=0 -permanent=0 - -## Get options -optstring=":hap" -while getopts ${optstring} option ; do - case $option in - h) usage;; - a) auto=1;; - p) permanent=1;; - ?) usage;; - esac -done -shift $((OPTIND -1)) - -## Check inputs -test "$#" -lt 4 && usage -if ! qvm-check -q "$2" 2>/dev/null; then - echo "$2 is not the name of any qube" - exit 1 -fi -qube_name="$2" -if test "$3" != "tcp" && test "$3" != "udp"; then - echo "Specify tcp or udp" - exit -fi -proto="$3" -portnum="$(check_port "$3" "$4")" - -if [ $# -eq 5 ]; then - external_portnum="$(check_port "$3" "$5")" -else - external_portnum=$portnum -fi - -## Get all netvms -declare -a netvms -declare -a ips -declare -a external_ips -hop=0 -# shellcheck disable=SC2004 -netvms[${hop}]="$qube_name" -IFS='|' read -r netvms[$hop+1] ips[$hop] <<< "$(qvm-ls "$qube_name" --raw-data -O netvm,IP)" -while [ "${netvms[hop+1]}" != "-" ] -do - ((hop++)) - IFS='|' read -r netvms[$hop+1] ips[$hop] <<< "$(qvm-ls "${netvms[$hop]}" --raw-data -O netvm,IP)" -done - -if test "$1" = "delete"; then - teardown netvms[@] ips[@] -elif test "$1" = "add"; then - if [ "$hop" -eq 0 ]; then - echo "$qube_name is not network connected" - echo "Cannot set up a tunnel" - exit - fi - - # Check last hop has external IP address - readarray -t external_ips < <( qvm-run -p "${netvms[$hop]}" "ip -4 -o a|grep -wv 'lo\|vif[0-9]*.*'"|awk '{print $2,$4}') - #readarray -t external_ips < <( qvm-run -p ${netvms[$hop]} "ip -4 -o a|grep -wv 'vif[0-9]'"|awk '{print $2,$4}') - num_ifs=${#external_ips[@]} - if [ "$num_ifs" -eq 1 ]; then - interface=0 - elif [ $auto -eq 1 ]; then - interface=0 - elif [ "$num_ifs" -gt 1 ]; then - echo "${netvms[$hop]} has more than 1 external interface" - echo "Which one do you want to use?" - for i in $(seq "$num_ifs"); do - echo "$i. ${external_ips[$i-1]}" - done - read -r interface - if ! [ "$interface" -eq "$interface" ] 2> /dev/null; then - echo "No such interface" - exit - elif [ "$interface" -gt "$num_ifs" ] || [ "$interface" -lt 1 ]; then - echo "No such interface" - exit - fi - ((interface--)) - else - echo "${netvms[$hop]} does not have an external interface" - echo "Cannot set up a tunnel" - exit - fi - external_ip="${external_ips[$interface]}" - external_iface="${external_ip%[[:space:]]*}" - ip="${external_ip#*[0-9]}" - ip="${ip%%/*}" - # shellcheck disable=SC2004,SC2034 - ips[$hop]="$ip" - - # Create tunnel - found="$(qvm-run -p -q -u root "$qube_name" -- nft list table nat 2>/dev/null)" - if test -z "$found"; then - found=$(qvm-run -p -u root "$qube_name" "iptables -L -nv | grep -c '.*ACCEPT.*$proto dpt:$portnum' ") - if [ "$found" -gt 0 ]; then - echo "Input rule in $qube_name already exists" - echo "Please check configuration - exiting now." - exit - else - qvm-run -q -u root "$qube_name" "iptables -I INPUT -p $proto --dport $portnum -j ACCEPT " - fi - else - if qvm-run -q -u root "$qube_name" "nft list table filter | grep '$proto dport $portnum accept' " - then - echo "Input rule in $qube_name already exists" - echo "Please check configuration - exiting now." - exit - else - handle="$(get_handle "$qube_name" filter related,established 1)" - qvm-run -q -u root "$qube_name" -- nft add rule filter INPUT position "$handle" iifname eth0 "$proto" dport "$portnum" accept - fi - fi - if ! tunnel netvms[@] ips[@]; then - teardown netvms[@] ips[@] - fi -else - usage -fi diff --git a/salt/sys-syncthing/files/admin/firewall/qvm-port-forward b/salt/sys-syncthing/files/admin/firewall/qvm-port-forward new file mode 100644 index 0000000..62f6441 --- /dev/null +++ b/salt/sys-syncthing/files/admin/firewall/qvm-port-forward @@ -0,0 +1,266 @@ +#!/bin/sh + +# SPDX-FileCopyrightText: 2017 Jean-Philippe Ouellet <jpo@vt.edu> +# SPDX-FileCopyrightText: 2022 daktak <daktak@gmail.com> +# SPDX-FileCopyrightText: 2023 Frederic Pierret <frederic.pierret@qubes-os.org> +# SPDX-FileCopyrightText: 2024 Benjamin Grande M. S. <ben.grande.b@gmail.com> +# +# SPDX-License-Identifier: MIT +# +# Credits: https://gist.github.com/daktak/f887352d564b54f9e529404cc0eb60d5 +# Credits: https://gist.github.com/jpouellet/d8cd0eb8589a5b9bf0c53a28fc530369 +# Credits: https://gist.github.com/fepitre/941d7161ae1150d90e15f778027e3248 + +set -eu + +run_qube(){ + qube="${1}" + shift + qvm-run --pass-io --user=root "${qube}" -- "${@}" +} + +create_net_dir(){ + qube="${1}" + run_qube "${qube}" mkdir -p "${hook_dir}" +} + +get_rule_handle(){ + qube="${1}" + chain="${2}" + rule="${3}" + run_qube "${qube}" "nft --handle --stateless list chain ip qubes ${chain} | tr -d '\"' | grep '^\s\+${rule} # handle ' | awk '{print \$NF}' | tr '\n' ' '" 2>/dev/null +} + +delete_rule_handle(){ + qube="${1}" + chain="${2}" + handle="${3}" + run_qube "${qube}" "nft delete rule ip qubes ${chain} handle ${handle}" +} + +delete_rule(){ + qube="${1}" + chain="${2}" + rule="${3}" + handle="$(get_rule_handle "${qube}" "${chain}" "${rule}")" + if test -n "${handle}"; then + for h in ${handle}; do + delete_rule_handle "${qube}" "${chain}" "${h}" + done + fi +} + +forward() { + from_qube="${1}" + to_qube="${2}" + create_net_dir "${from_qube}" + + ## TODO: Handle multiple interfaces in upstream. + dev="$(run_qube "${from_qube}" ip -4 r \ + | awk '/^default via /{print $5}' | head -1)" + from_ip="$(run_qube "${from_qube}" ip -4 -o a show dev "${dev}" \ + | awk '{print $4}' | cut -d "/" -f 1)" + to_ip="$(qvm-prefs --get -- "${to_qube}" ip)" + to_ip_escaped="$(echo "${to_ip}" | tr "." "-")" + hook="${hook_prefix}${to_ip}-${proto}-${port}.sh" + + if test "${from_ip}" = "None"; then + from_ip="" + fi + + dnat_chain="custom-pf-${to_ip_escaped}" + dnat_rule="iifname ${dev} ip saddr ${lan_ip} ${proto} dport ${port} ct state established,related,new counter dnat to ${to_ip}" + forward_chain="custom-forward" + forward_rule="iifname ${dev} ip saddr ${lan_ip} ip daddr ${to_ip} ${proto} dport ${port} ct state established,related,new counter accept" + full_rule="nft 'add chain ip qubes ${dnat_chain} { type nat hook prerouting priority filter +1; policy accept; } +add rule ip qubes ${dnat_chain} ${dnat_rule} +add rule ip qubes ${forward_chain} ${forward_rule}'" + + delete_rule "${from_qube}" "${forward_chain}" "${forward_rule}" + delete_rule "${from_qube}" "${dnat_chain}" "${dnat_rule}" + if test "${action}" = "del"; then + echo "info: ${from_qube}: deleting rules" >&2 + run_qube "${from_qube}" "rm -f ${hook}" + else + echo "info: ${from_qube}: adding forward rule dev ${dev} saddr ${lan_ip} daddr ${to_ip}" >&2 + run_qube "${from_qube}" "${full_rule}" + + if test "${persistent}" = "1"; then + if test "$(qvm-prefs --get -- "${from_qube}" klass)" = "DispVM"; then + from_qube="$(qvm-prefs --get -- "${from_qube}" template)" + fi + + full_rule="#!/bin/sh +get_handle(){ + chain=\\\${1} + rule=\\\${2} + nft --handle --stateless list chain ip qubes \\\${chain} | tr -d '\\\"' | grep '^\\\s\\\+\\\${rule} \\# handle ' | awk '{print \\\$NF}' | tr \\\"\\\n\\\" \\\" \\\" +} + +forward_handle=\\\$(get_handle ${forward_chain} \\\"${forward_rule}\\\") +if test -n \\\"\\\${forward_handle:-}\\\"; then + for h in \\\${forward_handle}; do + nft delete rule ip qubes ${forward_chain} handle \\\${h} + done +fi + +dnat_handle=\\\$(get_handle ${dnat_chain} \\\"${dnat_rule}\\\") +if test -n \\\"\\\${dnat_handle:-}\\\"; then + for h in \\\${dnat_handle}; do + nft delete rule ip qubes ${dnat_chain} handle \\\${h} + done +fi + +${full_rule}" + + create_net_dir "${from_qube}" + run_qube "${from_qube}" "echo \"${full_rule}\" | tee \"${hook}\" >/dev/null" + run_qube "${from_qube}" "chmod +x ${hook}" + fi + fi +} + +input() { + qube="${1}" + to_ip="$(qvm-prefs --get -- "${qube}" ip)" + hook="${hook_prefix}${to_ip}-${proto}-${port}.sh" + create_net_dir "${qube}" + + custom_input_rule="${proto} dport ${port} ip daddr ${to_ip} ct state new counter accept" + input_rule="nft add rule ip qubes custom-input ${custom_input_rule}" + + delete_rule "${qube}" "custom-input" "${custom_input_rule}" + if test "${action}" = "del"; then + echo "info: ${qube}: deleting rules" >&2 + run_qube "${qube}" "rm -f ${hook}" + else + echo "info: ${qube}: adding input rule daddr ${to_ip}" >&2 + run_qube "${qube}" "${input_rule}" + if test "${persistent}" = "1"; then + input_rule="#!/bin/sh +get_handle(){ + chain=\\\${1} + rule=\\\${2} + nft --handle --stateless list chain ip qubes \\\${chain} | tr -d '\\\"' | grep '^\\\s\\\+\\\${rule} \\# handle ' | awk '{print \\\$NF}' | tr \\\"\\\n\\\" \\\" \\\" +} + +input_handle=\\\$(get_handle custom-input \\\"${custom_input_rule}\\\") +if test -n \\\"\\\${input_handle:-}\\\"; then + for h in \\\${input_handle}; do + nft delete rule ip qubes custom-input handle \\\${h} + done +fi + +${input_rule}" + + run_qube "${qube}" "echo \"${input_rule}\" | tee \"${hook}\" >/dev/null" + run_qube "${qube}" "chmod +x ${hook}" + fi + fi +} + +get_lan(){ + qube="${1}" + ## TODO: Handle multiple interfaces in upstream. + dev="$(run_qube "${qube}" ip -4 route \ + | awk '/^default via /{print $5}' | head -1)" + if test -z "${dev}"; then + echo "error: ${qube}: could not find any device that is up" >&2 + exit 1 + fi + lan_ip="$(run_qube "${qube}" ip -4 r show dev "${dev}" prot kernel \ + | cut -d " " -f 1)" + if test -z "${lan_ip}"; then + echo "error: ${qube}: could not find LAN from device ${dev}" >&2 + exit 1 + fi +} + +recurse_netvms() { + cmd="${1}" + rec_qube="${2}" + rec_netvm="$(qvm-prefs --get -- "${rec_qube}" netvm)" + if test -n "${rec_netvm}" && test "${rec_netvm}" != "None"; then + case "${cmd}" in + show-upstream);; + apply-rules) forward "${rec_netvm}" "${rec_qube}";; + esac + recurse_netvms "${cmd}" "${rec_netvm}" + fi + case "${cmd}" in + show-upstream) get_lan "${rec_qube}";; + apply-rules) ;; + esac +} + +usage() { + echo "Usage: ${0##*/} --action ACTION --qube QUBE --port PORT --proto PROTO --persistent +Example: + ${0##*/} --action add --qube work --port 22 --proto tcp + ${0##*/} --action add --qube work --port 444 --proto udp --persistent + ${0##*/} --action del --qube work --port 22 --proto tcp + ${0##*/} --action del --qube work --port 444 --proto udp +Note: Defaults to temporary rules +Warn: If persistent is and and a netvm is disposable, the rule will be saved in the disposable template" >&2 + exit 1 +} + +check_opt(){ + case "${action:-}" in + add|del);; + *) echo "error: action must be either 'add' or 'del'" >&2; exit 1;; + esac + + case "${proto:-}" in + tcp|udp);; + *) echo "error: protocol must be only 'tcp' or 'udp'" >&2; exit 1;; + esac + + case "${port:-}" in + ""|*[!0-9]*) echo "error: port must be only numbers" >&2; exit 1;; + *) + esac + + if test "${port}" -ge 1 && test "${port}" -le 65535; then + true + else + echo "error: port must be in range 1-65535" >&2 + exit 1 + fi + + if test -z "${target_qube:-}"; then + echo "error: qube name not provided" >&2 + exit 1 + fi + + if ! qvm-check "${target_qube}" >/dev/null 2>&1; then + echo "error: qube '${target_qube}' not found." >&2 + exit 1 + fi +} + +hook_dir="/rw/config/network-hooks.d" +hook_prefix="${hook_dir}/90-port-forward-" +persistent="" + +if ! OPTS=$(getopt -o a:q:p:n:s --long action:,qube:,port:,proto:,persistent -n "${0}" -- "${@}"); then + echo "An error occurred while parsing options." >&2 + exit 1 +fi + +eval set -- "${OPTS}" +while test "${#}" -gt "0"; do + case "${1}" in + -a|--action) action="${2}"; shift;; + -q|--qube) target_qube="${2}"; shift;; + -p|--port) port="${2}"; shift;; + -n|--proto) proto="${2}"; shift;; + -s|--persistent) persistent=1; shift;; + esac + shift +done + +check_opt +recurse_netvms show-upstream "${target_qube}" +input "${target_qube}" +recurse_netvms apply-rules "${target_qube}"