mirror of
https://github.com/ben-grande/qusal.git
synced 2024-10-01 02:35:49 -04:00
224312ed42
Make shell a little bit safer with: - add-default-case - check-extra-masked-returns - check-set-e-suppressed - quote-safe-variables - check-unassigned-uppercase Although there are some stylistic decisions for uniformity: - avoid-nullary-conditions - deprecated-which - require-variable-braces
378 lines
10 KiB
Bash
Executable File
378 lines
10 KiB
Bash
Executable File
#!/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}"
|
|
}
|
|
|
|
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;"
|
|
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;;
|
|
*) 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}"
|