#!/bin/sh # SPDX-FileCopyrightText: 2017 Jean-Philippe Ouellet # SPDX-FileCopyrightText: 2022 daktak # SPDX-FileCopyrightText: 2023 Frederic Pierret # SPDX-FileCopyrightText: 2024 Benjamin Grande M. S. # # 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}" } validate_handle(){ qube="${1}" untrusted_handle="${2}" case "${untrusted_handle}" in ""|*[!0-9]*) echo "error: ${qube}: invalid handle" >&2 exit 1 ;; *) ;; esac } validate_ipv4(){ qube="${1}" untrusted_ip="${2}" case "${untrusted_ip}" in ""|*[!0-9./]*) echo "error: ${qube}: invalid IPv4 address" >&2 exit 1 ;; *) ;; esac } validate_ipv6(){ qube="${1}" untrusted_ip="${2}" case "${untrusted_ip}" in ""|*[!0-9a-f:/]*) echo "error: ${qube}: invalid IPv6 address" >&2 exit 1 ;; *) ;; esac } validate_dev(){ qube="${1}" untrusted_dev="${2}" case "${untrusted_dev}" in ""|*[!0-9A-Za-z]*) echo "error: ${qube}: invalid device name" >&2 exit 1 ;; *) ;; esac } 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}" untrusted_handle_list="$(get_rule_handle "${qube}" "${chain}" "${rule}")" if test -n "${untrusted_handle_list}"; then for untrusted_handle in ${untrusted_handle_list}; do unset handle validate_handle "${qube}" "${untrusted_handle}" handle="${untrusted_handle}" delete_rule_handle "${qube}" "${chain}" "${handle}" done fi } forward() { from_qube="${1}" to_qube="${2}" create_net_dir "${from_qube}" unset dev ## TODO: Handle multiple interfaces in upstream. untrusted_dev="$(run_qube "${from_qube}" ip -4 route | \ awk '/^default via /{print $5}' | head -1)" validate_dev "${from_qube}" "${untrusted_dev}" dev="${untrusted_dev}" unset from_ip untrusted_from_ip="$(run_qube "${from_qube}" ip -4 -o addr show dev \ "${dev}" | awk '{print $4}' | cut -d "/" -f 1)" validate_ipv4 "${from_qube}" "${untrusted_from_ip}" from_ip="${untrusted_from_ip}" 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" dnat_rule="${dnat_rule} state established,related,new counter dnat to" dnat_rule="${dnat_rule} ${to_ip}" forward_chain="custom-forward" forward_rule="iifname ${dev} ip saddr ${lan_ip} ip daddr ${to_ip} ${proto}" forward_rule="${forward_rule} dport ${port} ct state" forward_rule="${forward_rule} established,related,new counter accept" dnat_policy="type nat hook prerouting priority filter +1; policy accept;" dnat_policy="{ ${dnat_policy} }" full_rule="nft 'add chain ip qubes ${dnat_chain} ${dnat_policy} 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 msg="adding forward rule dev ${dev} saddr ${lan_ip} daddr ${to_ip}" echo "info: ${from_qube}: ${msg}" >&2 run_qube "${from_qube}" "${full_rule}" if test "${persistent}" = "1"; then class="$(qvm-prefs --get -- "${from_qube}" klass)" if test "${class}" = "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" custom_input_rule="${custom_input_rule} 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}" unset dev ## TODO: Handle multiple interfaces in upstream. untrusted_dev="$(run_qube "${qube}" ip -4 route | \ awk '/^default via /{print $5}' | head -1)" validate_dev "${qube}" "${untrusted_dev}" dev="${untrusted_dev}" if test -z "${dev}"; then echo "error: ${qube}: could not find any device that is up" >&2 exit 1 fi unset lan_ip untrusted_lan_ip="$(run_qube "${qube}" ip -4 route show dev "${dev}" \ prot kernel | cut -d " " -f 1)" validate_ipv4 "${qube}" "${untrusted_lan_ip}" lan_ip="${untrusted_lan_ip}" if test -z "${lan_ip}"; then echo "error: ${qube}: could not find LAN from device ${dev}" >&2 exit 1 fi } test_qvm_run(){ qube="${1}" # shellcheck disable=SC2310 if ! run_qube "${qube}" echo "Test QUBESRPC" >/dev/null 2>&1; then echo "error: ${qube}: RPC qubes.VMShell failed, use a different qube" >&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) test_qvm_run "${rec_qube}";; apply-rules) forward "${rec_netvm}" "${rec_qube}";; *) echo "Unsupported command passed to recurse_netvms()" >&2; exit 1;; esac recurse_netvms "${cmd}" "${rec_netvm}" fi case "${cmd}" in show-upstream) get_lan "${rec_qube}";; apply-rules) ;; *) echo "Unsupported command passed to recurse_netvms()" >&2; exit 1;; esac } usage() { echo "Usage: ${0##*/} OPTIONS Option syntax: --action ACTION --qube QUBE --port PORT --proto PROTO [--persistent] Options: -a, --action ACTION add or delete a rule (add, del) -q, --qube QUBE qube name which holds the service to be exposed -p, --port PORT port number to be exposed -n, --proto PROTO protocol the service uses (tcp, udp) -s, --persistent persist rules across reboots 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: Persistent rules of disposable netvm are saved to its 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 h,a:q:p:n:s \ --long help,action:,qube:,port:,proto:,persistent -n "${0}" -- "${@}") then echo "An error occurred while parsing options." >&2 exit 1 fi eval set -- "${OPTS}" if test "${OPTS}" = " --"; then usage fi 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;; -h|--help) usage;; --) break;; *) echo "Unsupported option" >&2; exit 1;; esac shift done check_opt recurse_netvms show-upstream "${target_qube}" input "${target_qube}" recurse_netvms apply-rules "${target_qube}"