From 26d51eab13272f08fc9017744548b38fcf886214 Mon Sep 17 00:00:00 2001 From: Samuel <36420837+Samueru-sama@users.noreply.github.com> Date: Sun, 2 Aug 2026 05:57:04 -0400 Subject: [PATCH] rewrite `bitmask-root` in `bash` --- pkg/pickle/helpers/bitmask-root | 1721 +++++++++++++------------------ 1 file changed, 727 insertions(+), 994 deletions(-) diff --git a/pkg/pickle/helpers/bitmask-root b/pkg/pickle/helpers/bitmask-root index ce0050c7..781a1090 100755 --- a/pkg/pickle/helpers/bitmask-root +++ b/pkg/pickle/helpers/bitmask-root @@ -1,5 +1,4 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +#!/usr/bin/env bash # # Copyright (C) 2014-2019 LEAP # @@ -16,1035 +15,769 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -""" -This is a privileged helper script for safely running certain commands as root. -It should only be called by the Bitmask application. - -Expected paths: - - When installed by distro path: - /usr/sbin/bitmask-root - - When installed by bundle or from git: - /usr/local/sbin/bitmask-root - - When installed by snap: - /snap/bin/riseup-vpn.bitmask-root - -USAGE: - bitmask-root firewall stop - bitmask-root firewall start [restart] GATEWAY1 GATEWAY2 ... - bitmask-root openvpn stop - bitmask-root openvpn start CONFIG1 CONFIG1 ... - -All actions return exit code 0 for success, non-zero otherwise. - -The `openvpn start` action is special: it calls exec on openvpn and replaces -the current process. If the `restart` parameter is passed, the firewall will -not be teared down in the case of an error during launch. -""" -import ipaddress -import os -import re -import signal -import socket -import syslog -import subprocess -import sys -import stat -import traceback -import tempfile - -cmdcheck = subprocess.check_output - +# bash translation of bitmask-root +# +# USAGE: +# bitmask-root firewall stop +# bitmask-root firewall start [restart] GATEWAY1 GATEWAY2 ... +# bitmask-root openvpn stop +# bitmask-root openvpn start CONFIG1 CONFIG1 ... +# +# All actions return exit code 0 for success, non-zero otherwise. # -# CONSTANTS - -def get_no_group_name(): - """ - Return the right group name to use for the current OS. - Examples: - - Ubuntu: nogroup - - Arch: nobody - - :rtype: str or None - """ - import grp - try: - grp.getgrnam('nobody') - return 'nobody' - except KeyError: - try: - grp.getgrnam('nogroup') - return 'nogroup' - except KeyError: - return None - -def is_ipv6_disabled(): - """ - Return True if ipv6 support is disabled by the kernel. - """ - code = os.system("sysctl -a 2>/dev/null | grep all.disable_ipv6 | grep 1") - return code == 0 - -def tostr(s): - return s.decode('utf-8') - -VERSION = "19" -SCRIPT = "bitmask-root" -NAMESERVER_TCP = "10.41.0.1" -NAMESERVER_UDP = "10.42.0.1" - -if os.getenv("UDP") == "1": - NAMESERVER = NAMESERVER_UDP -else: - NAMESERVER = NAMESERVER_TCP -BITMASK_CHAIN = "bitmask" -BITMASK_CHAIN_NAT_OUT = "bitmask" -BITMASK_CHAIN_NAT_POST = "bitmask_postrouting" -LOCAL_INTERFACE = "lo" - -def swhich(binary): - """ - Find the path to binary in sbin - - :rtype: str - """ - for folder in ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/local/sbin"]: - path = os.path.join(folder, binary) - if os.path.isfile(path): - return path - - raise Exception("Can't find %s" % (binary,)) - -IP = swhich("ip") -IPTABLES = swhich("iptables") -IP6TABLES = swhich("ip6tables") -SYSCTL = swhich("sysctl") - -OPENVPN_USER = "nobody" -OPENVPN_GROUP = get_no_group_name() -LEAPOPENVPN = "LEAPOPENVPN" -OPENVPN_SYSTEM_BIN = "/usr/sbin/openvpn" # Debian location -OPENVPN_LEAP_BIN = "/usr/local/sbin/leap-openvpn" # installed by bundle -OPENVPN_SNAP_BIN = "/snap/bin/riseup-vpn.openvpn" # installed by snap - -FIXED_FLAGS = [ - "--setenv", "LEAPOPENVPN", "1", - "--nobind", - "--client", - "--dev", "tun", - "--tls-client", - "--remote-cert-tls", "server", - "--management-signal", - "--script-security", "1", - "--user", "nobody", - "--auth-nocache", - "--tls-version-min", "1.2", -] - -if OPENVPN_GROUP is not None: - FIXED_FLAGS.extend(["--group", OPENVPN_GROUP]) - -if is_ipv6_disabled(): - FIXED_FLAGS.extend([ - "--pull-filter", "ignore", "ifconfig-ipv6", - "--pull-filter", "ignore", "route-ipv6"]) - - -ALLOWED_FLAGS = { - "--remote": ["IP", "NUMBER", "PROTO"], - "--tls-cipher": ["CIPHER"], - "--cipher": ["CIPHER"], - "--auth": ["CIPHER"], - "--management": ["DIR||IP", "UNIXSOCKET||NUMBER", "FILE"], - "--management-client-user": ["USER"], - "--route": ["IP", "IP", "NETGW"], - "--cert": ["FILE"], - "--key": ["FILE"], - "--ca": ["FILE"], - "--fragment": ["NUMBER"], - "--keepalive": ["NUMBER", "NUMBER"], - "--verb": ["NUMBER"], - "--management-client": [], - "--tun-ipv6": [], - "--log": ["LOGFILE"], - "--pull-filter": ["ignore", "route"], - "--socks-proxy": ["IP", "NUMBER"], +# The `openvpn start` action is special: it calls exec on openvpn and replaces +# the current process. If the `restart` parameter is passed, the firewall will +# not be teared down in the case of an error during launch. + +set -e + +if [ -n "${DEBUG}${TEST}" ]; then + set -x +fi + +VERSION=19 +SCRIPT=${0##*/} + +IFS=$' \t\n' +REAL_IFS=$IFS # copy to restore later when changing IFS +if [ "$UDP" = "1" ]; then + NAMESERVER=10.42.0.1 +else + NAMESERVER=10.41.0.1 +fi +CHAIN=bitmask +CHAIN_NAT=bitmask +CHAIN_POST=bitmask_postrouting + +LEAPOPENVPN=LEAPOPENVPN +QUBES_PROXY=false +USER_ID=$(id -u) +OPENVPN_GROUP="" +for g in nobody nogroup; do + if getent group "$g" >/dev/null 2>&1; then + OPENVPN_GROUP=$g + break + fi +done + +# Are these needed at all? +OPENVPN_USER=nobody +LOCAL_INTERFACE=lo + +log_msg() { + >&2 echo "$SCRIPT: $1" + logger -p "daemon.${2:-info}" -t "$SCRIPT" -- "$1" 2>/dev/null || : } -PARAM_FORMATS = { - "NUMBER": lambda s: re.match(r"^\d+$", s), - "PROTO": lambda s: re.match("^(tcp|udp|tcp4|udp4)$", s), - "IP": lambda s: is_valid_address(s), - "CIPHER": lambda s: re.match(r"^[A-Z0-9-\:]+$", s), - "USER": lambda s: re.match( - r"^[a-zA-Z0-9_\.\@][a-zA-Z0-9_\-\.\@]*\$?$", s), # IEEE Std 1003.1-2001 - "FILE": lambda s: os.path.isfile(s), - "DIR": lambda s: os.path.isdir(os.path.split(s)[0]), - "UNIXSOCKET": lambda s: s == "unix", - "NETGW": lambda s: s == "net_gateway", - "UID": lambda s: re.match("^[a-zA-Z0-9]+$", s), - "LOGFILE": lambda s: s == f"{tempfile.gettempdir()}/leap-vpn.log", - "ignore": lambda s: s == "ignore", - "route": lambda s: s == "route", +bail() { + log_msg "$1" err + exit 1 } -# Determine Qubes OS version, if any -QUBES_PROXY = os.path.exists("/var/run/qubes/this-is-proxyvm") -if os.path.isdir("/etc/qubes"): - QUBES_CFG = "/rw/config/" - QUBES_IPHOOK = QUBES_CFG + "qubes-ip-change-hook" - QUBES_FW_SCRIPT = QUBES_CFG + "qubes-firewall-user-script" - if subprocess.call([IPTABLES, "--list", "QBS-FORWARD"]) == 0: - QUBES_VER = 4 - else: - QUBES_VER = 3 -else: - # not a Qubes system - QUBES_VER = 0 - - -DEBUG = os.getenv("DEBUG") -TEST = os.getenv("TEST") - -if DEBUG: - import logging - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s") - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) - ch.setFormatter(formatter) - logger = logging.getLogger(__name__) - logger.setLevel(logging.DEBUG) - logger.addHandler(ch) - -syslog.openlog(SCRIPT) +# the python script checks for a literal file in "/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/local/sbin" +# the order is flipped, normally you want to check in "/usr/local" "/usr" "/" instead +# also checking for a literal file is not correct, you need to make sure it has +# executable permission, the command -v builtin of shell does all of this for us +swhich() { + p=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin + PATH=$p command -v "$1" || bail "Can't find $1" +} -# -# UTILITY -# +IP=$(swhich ip) +IPTABLES=$(swhich iptables) +IP6TABLES=$(swhich ip6tables) +SYSCTL=$(swhich sysctl) + +QUBES_VER=0 +if [ -f /var/run/qubes/this-is-proxyvm ] && [ -d /etc/qubes ]; then + QUBES_CFG=/rw/config/ + QUBES_IPHOOK=${QUBES_CFG}qubes-ip-change-hook + QUBES_FW_SCRIPT=${QUBES_CFG}qubes-firewall-user-script + if "$IPTABLES" --list QBS-FORWARD >/dev/null 2>&1; then + QUBES_VER=4 + else + QUBES_VER=3 + fi +fi + +usage() { + cat <<-EOF + This is $SCRIPT version $VERSION + + This program manipulates the Bitmask firewall. It is *NOT* intented to be used manually. + + Commands: + + $SCRIPT version + $SCRIPT restart + $SCRIPT openvpn start + $SCRIPT openvpn stop + $SCRIPT firewall start + $SCRIPT firewall stop + $SCRIPT firewall isup + EOF + exit 0 +} -def is_valid_address(value): - """ - Validate that the passed ip is a valid IP address. - - :param value: the value to be validated - :type value: str - :rtype: bool - """ - try: - socket.inet_aton(value) - return True - except Exception: - log("%s: ERROR: MALFORMED IP: %s!" % (SCRIPT, value)) - return False - - -def split_list(_list, regex): - """ - Split a list based on a regex: - e.g. split_list(["xx", "yy", "x1", "zz"], "^x") => [["xx", "yy"], ["x1", - "zz"]] - - :param _list: the list to be split. - :type _list: list - :param regex: the regex expression to filter with. - :type regex: str - - :rtype: list - """ - if not hasattr(regex, "match"): - regex = re.compile(regex) - result = [] - i = 0 - if not _list: - return result - while True: - if regex.match(_list[i]): - result.append([]) - while True: - result[-1].append(_list[i]) - i += 1 - if i >= len(_list) or regex.match(_list[i]): - break - else: - i += 1 - if i >= len(_list): - break - return result - - -def get_process_list(): - """ - Get a process list by reading `/proc` filesystem. - - :return: a list of tuples, each containing pid and command string. - :rtype: tuple if lists - """ - res = [] - pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] - - for pid in pids: - try: - res.append((pid, open( - os.path.join( - '/proc', pid, 'cmdline'), 'rb').read())) - except IOError: # proc has already terminated - continue - return filter(None, res) - - -def getIPv4AllowAddresses(): - lines = [] - try: - with open("/etc/bitmask/ipv4.allow", 'r') as f: - lines = [l.strip() for l in f.readlines()] - except FileNotFoundError: - return lines - - lines = filter(lambda x: ipaddress.ip_address(x).version == 4, lines) - return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) - -def getIPv6AllowAddresses(): - lines = [] - try: - with open("/etc/bitmask/ipv6.allow", 'r') as f: - lines = [l.strip() for l in f.readlines()] - except FileNotFoundError: - return lines - - lines = filter(lambda x: ipaddress.ip_address(x).version == 6, lines) - return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) - - -def run(command, *args, **options): - """ - Run an external command. - - Options: - - `check`: If True, check the command's output. bail if non-zero. (the - default is true unless detach or input is true) - `exitcode`: like `check`, but return exitcode instead of bailing. - `detach`: If True, run in detached process. - `input`: If True, open command for writing stream to, returning the Popen - object. - `throw`: If True, raise an exception if there is an error instead - of bailing. - """ - parts = [command] - parts.extend(args) - debug("%s run: %s " % (SCRIPT, " ".join(parts))) - - _check = options.get("check", True) - _detach = options.get("detach", False) - _input = options.get("input", False) - _exitcode = options.get("exitcode", False) - _throw = options.get("throw", False) - - if not (_check or _throw) or _detach or _input: - if _input: - return subprocess.Popen(parts, stdin=subprocess.PIPE) - else: - subprocess.Popen(parts) - return None - else: - try: - devnull = open('/dev/null', 'w') - subprocess.check_call(parts, stdout=devnull, stderr=devnull) - return 0 - except subprocess.CalledProcessError as exc: - if _exitcode: - if exc.returncode != 1: - # 0 or 1 is to be expected, but anything else - # should be logged. - debug("ERROR: Could not run %s: %s" % - (exc.cmd, exc.output), exception=exc) - return exc.returncode - elif _throw: - raise exc - else: - bail("ERROR: Could not run %s: %s" % (exc.cmd, exc.output), - exception=exc) - - -def log(msg=None, exception=None, priority=syslog.LOG_INFO): - """ - print and log warning message or exception. - - :param msg: optional error message. - :type msg: str - :param msg: optional exception. - :type msg: Exception - :param msg: syslog level - :type msg: one of LOG_EMERG, LOG_ALERT, LOG_CRIT, LOG_ERR, - LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG - """ - if msg is not None: - print("%s: %s" % (SCRIPT, msg)) - syslog.syslog(priority, msg) - if exception is not None: - if TEST or DEBUG: - traceback.print_exc() - syslog.syslog(priority, traceback.format_exc()) - - -def debug(msg=None, exception=None): - """ - Just like log, but is skipped unless DEBUG. Use syslog.LOG_INFO - even for debug messages (we don't want to miss them). - """ - if TEST or DEBUG: - log(msg, exception) - - -def bail(msg=None, exception=None): - """ - abnormal exit. like log(), but exits with error status code. - """ - log(msg, exception) - exit(1) +check_root() { + [ "$USER_ID" = 0 ] || bail "ERROR: must be run as root" +} +# The python script uses sysctl -a 2>/dev/null | grep all.disable_ipv6 | grep 1 +# which is flawed. the sysctl setting can only exists when the IPv6 stack was initialized +# so if you are running a kernel with ipv6 just not compiled in or with the ipv6.disable=1 +# kernel parameter, the old method will return a false positive! # -# OPENVPN -# +# Instead, lets check for the presence of the sysctl key and read it +# This is also faster than shelling out sysctl | grep | grep +is_ipv6_disabled() { + f=/proc/sys/net/ipv6/conf/all/disable_ipv6 + if [ ! -f "$f" ]; then return 0 # disabled at kernel level + elif read -r val < "$f"; then [ "$val" = 1 ] # disabled by sysctl + else return 1 # should never be reached + fi +} +# python uses socket.inet_aton, here we have to shell out grep -E technically a +# bunch of case statements with regexes would remove grep but that becomes unreadable mess +is_valid_address() { + printf '%s' "$1" | grep -qEx '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' || return 1 + # make sure no number is greater than 255 + IFS=. + set -- $1 + IFS=$REAL_IFS + for octet do + if [ "$octet" -gt 255 ]; then + return 1 + fi + done + return 0 +} -def get_openvpn_bin(): - """ - Return the path for either the system openvpn or the one the - bundle has put there. - """ - if os.environ.get('SNAP') and os.path.isfile(OPENVPN_SNAP_BIN): - # the snap option should be removed from the debian package. - return OPENVPN_SNAP_BIN - - if os.path.isfile(OPENVPN_SYSTEM_BIN): - return OPENVPN_SYSTEM_BIN - - # the bundle option should also be removed from the debian. - if os.path.isfile(OPENVPN_LEAP_BIN): - return OPENVPN_LEAP_BIN - - -def parse_openvpn_flags(args): - """ - Take argument list from the command line and parse it, only allowing some - configuration flags. - - :type args: list - """ - result = [] - try: - for flag in split_list(args, "^--"): - flag_name = flag[0] - if flag_name in ALLOWED_FLAGS: - result.append(flag_name) - required_params = ALLOWED_FLAGS[flag_name] - if required_params: - # flatten if separated by spaces - flag_params = [i for sublist in map( - lambda s: s.split(), flag[1:]) for i in sublist] - if len(flag_params) != len(required_params): - log("%s: ERROR: not enough params for %s" % - (SCRIPT, flag_name)) - return None - for param, param_type in zip(flag_params, required_params): - for tpe in param_type.split("||"): - if PARAM_FORMATS[tpe](param): - result.append(param) - break - else: - log("%s: ERROR: Bad argument %s" % - (SCRIPT, param)) - return None - else: - log("WARNING: unrecognized openvpn flag %s" % flag_name) - return result - except Exception as exc: - log("%s: ERROR PARSING FLAGS: %s" % (SCRIPT, exc)) - if DEBUG: - logger.exception(exc) - return None - - -def openvpn_start(args): - """ - Launch openvpn, sanitizing input, and replacing the current process with - the openvpn process. - - :param args: arguments to be passed to openvpn - :type args: list - """ - openvpn_flags = parse_openvpn_flags(args) - if openvpn_flags: - OPENVPN = get_openvpn_bin() - flags = [OPENVPN] + FIXED_FLAGS + openvpn_flags - if DEBUG: - log("%s: running openvpn with flags:" % (SCRIPT,)) - log(flags) - # note: first argument to command is ignored, but customarily set to - # the command. - os.execv(OPENVPN, flags) - else: - bail('ERROR: could not parse openvpn options') - - -def openvpn_stop(args): - """ - Stop the openvpn that has likely been launched by bitmask. - - :param args: arguments to openvpn - :type args: list - """ - plist = get_process_list() - for pid, proc in plist: - if bytes("openvpn", 'utf-8') in proc and bytes(LEAPOPENVPN, 'utf-8') in proc: - os.kill(int(pid), signal.SIGTERM) - break +# python uses ipaddress to validate which is much simpler +getIPv4AllowAddresses() { + # 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 27.0.0.0/8 + # `|| :` so a missing file yields empty instead of aborting on + # awk's non-zero exit when the file is absent (python tolerates it) + awk '/^10\./ || /^172\.(1[6-9]|2[0-9]|3[01])\./ || /^192\.168\./ || /^127\./' /etc/bitmask/ipv4.allow 2>/dev/null || : +} -# -# FIREWALL -# +getIPv6AllowAddresses() { + # fc00::/7 fe80::/10 ::1 + awk '/^[[:space:]]*(fc|fd)/ || /^[[:space:]]*::1/ || /^[[:space:]]*[fF][eE][89aAbB]/' /etc/bitmask/ipv6.allow 2>/dev/null || : +} +run_iptable_with_check_flag() { + mode="" + for a do + case "$a" in + --append|--insert) mode=add;; + --delete) mode=del;; + esac + done + + if [ "$mode" = "" ]; then + "$@" + return $? + fi + + set +e + # check the entire array and swap --{append,insert,delete} for --check + # before executing with it, needs to be done in a subshell + # since we need to preserve the original array for later + ( + for a do + case "$a" in + --append|--insert|--delete) a=--check;; + esac + set -- "$@" "$a" + shift + done + "$@" >/dev/null 2>&1 + ) + status=$? + set -e + + if [ "$mode" = add ] && [ "$status" != 0 ]; then "$@" + elif [ "$mode" = del ] && [ "$status" = 0 ]; then "$@" + fi +} -def get_gateways(gateways): - """ - Filter a passed sequence of gateways, returning only the valid ones. - - :param gateways: a sequence of gateways to filter. - :type gateways: iterable - :rtype: iterable - """ - result = filter(is_valid_address, gateways) - if not result: - bail("ERROR: No valid gateways specified") - else: - return list(result) - - -def get_default_device(): - """ - Retrieve the current default network device. - - :rtype: str - """ - routes = subprocess.check_output([IP, "route", "show"]) - match = re.search(rb"^default .*dev ([^\s]*) .*$", routes, flags=re.M) - if match and match.groups(): - return tostr(match.group(1)) - else: - bail("Could not find default device") - - -def get_local_network_ipv4(device): - """ - Get the local ipv4 addres for a given device. - - :param device: - :type device: str - """ - addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) - match = re.search(rb"^.*inet ([^ ]*) .*$", addresses, flags=re.M) - if match and match.groups(): - return tostr(match.group(1)) - else: - return None - - -def get_local_network_ipv6(device): - """ - Get the local ipv6 addres for a given device. - - :param device: - :type device: str - """ - addresses = cmdcheck([IP, "-o", "address", "show", "dev", device]) - match = re.search(rb"^.*inet6 ([^ ]*) .*$", addresses, flags=re.M) - if match and match.groups(): - return tostr(match.group(1)) - else: - return None - - -def run_iptable_with_check(cmd, *args, **options): - """ - Run an iptables command checking to see if it should: - for --append: run only if rule does not already exist. - for --insert: run only if rule does not already exist. - for --delete: run only if rule does exist. - other commands are run normally. - """ - if "--insert" in args: - check_args = [arg.replace("--insert", "--check") for arg in args] - check_code = run(cmd, *check_args, exitcode=True) - if check_code != 0: - run(cmd, *args, **options) - elif "--append" in args: - check_args = [arg.replace("--append", "--check") for arg in args] - check_code = run(cmd, *check_args, exitcode=True) - if check_code != 0: - run(cmd, *args, **options) - elif "--delete" in args: - check_args = [arg.replace("--delete", "--check") for arg in args] - check_code = run(cmd, *check_args, exitcode=True) - if check_code == 0: - run(cmd, *args, **options) - else: - run(cmd, *args, **options) - - -def iptables(*args, **options): - """ - Run iptables4 and iptables6. - """ - ip4tables(*args, **options) - ip6tables(*args, **options) - - -def ip4tables(*args, **options): - """ - Run iptables4 with checks. - """ - run_iptable_with_check(IPTABLES, *args, **options) - - -def ip6tables(*args, **options): - """ - Run iptables6 with checks. - """ - run_iptable_with_check(IP6TABLES, *args, **options) - - -def toggle_ipv6(status='disable'): - if status == 'disable': - arg = 1 - elif status == 'enable': - arg = 0 - else: - return - cmdcheck([SYSCTL, '-w', 'net.ipv6.conf.all.disable_ipv6=%s' % arg]) +iptables_both() { + # Run on both ipv4 and ipv6 iptables. + ip4tables "$@" + ip6tables "$@" +} +ip4tables() { + run_iptable_with_check_flag "$IPTABLES" "$@" +} -# -# NOTE: these tests to see if a chain exists might incorrectly return false. -# This happens when there is an error in calling `iptables --list bitmask`. -# -# For this reason, when stopping the firewall, we do not trust the -# output of ipvx_chain_exists() but instead always attempt to delete -# the chain. -# +ip6tables() { + run_iptable_with_check_flag "$IP6TABLES" "$@" +} + +ipv4_chain_exists() ( + chain="$1" table="$2" + set +e + if [ -n "$table" ]; then + "$IPTABLES" -t "$table" --list "$chain" --numeric >/dev/null 2>&1 + else + "$IPTABLES" --list "$chain" --numeric >/dev/null 2>&1 + fi + status=$? + set -e + case $status in + 0) return 0;; + 1) return 1;; + *) log_msg "ERROR: Could not determine state of iptable chain" + return 1 + ;; + esac +) + +ipv6_chain_exists() ( + chain="$1" + set +e + "$IP6TABLES" --list "$chain" --numeric >/dev/null 2>&1 + status=$? + set -e + case $status in + 0) return 0;; + 1) return 1;; + *) log_msg "ERROR: Could not determine state of ip6table chain" + return 1 + ;; + esac +) + +get_openvpn_bin() { + # /snap/bin/riseup-vpn.openvpn no longer searched since the snap was dropped right? + set -- \ + /usr/sbin/openvpn \ + /usr/local/sbin/leap-openvpn + + for bin do + if command -v "$bin"; then + return 0 + fi + done + + bail "Could not find openvpn binary" +} +_validate_openvpn_param() { + passed=0 + type_spec=$1 + value=$2 + IFS='|' + for type_part in $type_spec; do + case "$type_part" in + '') + continue + ;; + NUMBER) + case "$value" in + ''|*[!0-9]*) passed=0;; + *) passed=1;; + esac + ;; + PROTO) + case "$value" in + tcp|udp|tcp4|udp4) passed=1;; + esac + ;; + IP) + if is_valid_address "$value"; then + passed=1 + fi + ;; + CIPHER) + case "$value" in + *[!A-Za-z0-9:\-]*) passed=0;; + *) passed=1;; + esac + ;; + USER) + case "$value" in + [a-zA-Z0-9_.@][a-zA-Z0-9_\-\.@]*\$|[a-zA-Z0-9_.@][a-zA-Z0-9_\-\.@]*) + passed=1 + ;; + esac + ;; + FILE) + if [ -f "$value" ]; then + passed=1 + fi + ;; + DIR) + if [ -d "${value%/*}" ]; then + passed=1 + fi + ;; + UNIXSOCKET) + if [ "$value" = unix ]; then + passed=1 + fi + ;; + NETGW) + if [ "$value" = net_gateway ]; then + passed=1 + fi + ;; + UID) + case "$value" in + *[!a-zA-Z0-9]*) passed=0;; + *) passed=1;; + esac + ;; + LOGFILE) + if [ "$value" = /tmp/leap-vpn.log ]; then + passed=1 + fi + ;; + ignore) + if [ "$value" = ignore ]; then + passed=1 + fi + ;; + route) + if [ "$value" = route ]; then + passed=1 + fi + ;; + esac + # if one alternative matched, no need to check the rest + if [ "$passed" = 1 ]; then + break + fi + done + + IFS=$REAL_IFS + if [ "$passed" = 1 ]; then + return 0 + fi + return 1 +} -def ipv4_chain_exists(chain, table=None): - """ - Check if a given chain exists. Only returns true if it actually exists, - but might return false if it exists and iptables failed to run. - - :param chain: the chain to check against - :type chain: str - :rtype: bool - """ - if table is not None: - code = run(IPTABLES, "-t", table, - "--list", chain, "--numeric", exitcode=True) - else: - code = run(IPTABLES, "--list", chain, "--numeric", exitcode=True) - if code == 0: - return True - elif code == 1: - return False - else: - log("ERROR: Could not determine state of iptable chain") - return False - - -def ipv6_chain_exists(chain): - """ - see ipv4_chain_exists() - - :param chain: the chain to check against - :type chain: str - :rtype: bool - """ - code = run(IP6TABLES, "--list", chain, "--numeric", exitcode=True) - if code == 0: - return True - elif code == 1: - return False - else: - log("ERROR: Could not determine state of iptable chain") - return False - - -def enable_ip_forwarding(): - """ - ip_fowarding must be enabled for the firewall to work. - """ - with open('/proc/sys/net/ipv4/ip_forward', 'w') as f: - f.write('1\n') - - -def firewall_start(args): - """ - Bring up the firewall. - - :param args: list of gateways, to be sanitized. - :type args: list - """ - default_device = get_default_device() - local_network_ipv4 = get_local_network_ipv4(default_device) - local_network_ipv6 = get_local_network_ipv6(default_device) - gateways = get_gateways(args) - - # allow local address in listed exception list - # this will allow all ports and both tcp and udp. - def allow4(ip): - ip4tables("--append", BITMASK_CHAIN, "--destination", ip, - "-o", default_device, "--jump", "ACCEPT") - - def allow6(ip): - ip6tables("--append", BITMASK_CHAIN, "--destination", ip, - "-o", default_device, "--jump", "ACCEPT") - - # add custom chain "bitmask" to front of OUTPUT chain for both - # the 'filter' and the 'nat' tables. - if not ipv4_chain_exists(BITMASK_CHAIN): - ip4tables("--new-chain", BITMASK_CHAIN) - if not ipv4_chain_exists(BITMASK_CHAIN_NAT_OUT, 'nat'): - ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_OUT) - if not ipv4_chain_exists(BITMASK_CHAIN_NAT_POST, 'nat'): - ip4tables("--table", "nat", "--new-chain", BITMASK_CHAIN_NAT_POST) - if not ipv6_chain_exists(BITMASK_CHAIN): - ip6tables("--new-chain", BITMASK_CHAIN) - ip4tables("--table", "nat", "--insert", "OUTPUT", - "--jump", BITMASK_CHAIN_NAT_OUT) - ip4tables("--table", "nat", "--insert", "POSTROUTING", - "--jump", BITMASK_CHAIN_NAT_POST) - iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN) - - # route all ipv4 DNS over VPN - # (note: NAT does not work with ipv6 until kernel 3.7) - enable_ip_forwarding() - if QUBES_PROXY and QUBES_VER >= 3: - # rewrite DNS packets for VPN DNS; Qubes preconfigures masquerade - ip4tables("-t", "nat", "--flush", "PR-QBS") - for gateway in gateways: - ip4tables("-t", "nat", "--append", "PR-QBS", "--destination", gateway, - "--jump", "RETURN") - ip4tables("-t", "nat", "--append", "PR-QBS", "-p", "udp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - ip4tables("-t", "nat", "--append", "PR-QBS", "-p", "tcp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - else: - # As we may have OpenVPN running on port 53, we don't want to redirect that - for gateway in gateways: - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "--destination", - gateway, "--jump", "RETURN") - # allow dns to localhost - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "--protocol", "udp", - "--dest", "127.0.1.1,127.0.0.1,127.0.0.53", "--dport", "53", - "--jump", "ACCEPT") - # rewrite all outgoing packets to use VPN DNS server - # (DNS does sometimes use TCP!) - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "udp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_OUT, "-p", "tcp", - "--dport", "53", "--jump", "DNAT", "--to", - NAMESERVER + ":53") - # enable masquerading, so that DNS packets rewritten by DNAT will - # have the correct source IPs. Apply masquerade only to the NAMESERVER, - # we don't want to apply it to the localhost dns resolver. - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, - "--dest", NAMESERVER, - "--protocol", "udp", "--dport", "53", "--jump", "MASQUERADE") - ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, - "--dest", NAMESERVER, - "--protocol", "tcp", "--dport", "53", "--jump", "MASQUERADE") - - # allow local network traffic - - ipv4_exceptions = getIPv4AllowAddresses() - if local_network_ipv4: - if len(ipv4_exceptions) == 0: - # allow all local network destinations if no explicit allow rules defined - ip4tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv4, "-o", default_device, - "--jump", "ACCEPT") - # allow local network sources for DNS - # (required to allow local network DNS that gets rewritten by NAT - # to get passed through so that MASQUERADE can set correct source IP) - ip4tables("--append", BITMASK_CHAIN, - "--source", local_network_ipv4, "-o", default_device, - "-p", "udp", "--dport", "53", "--jump", "ACCEPT") - ip4tables("--append", BITMASK_CHAIN, - "--source", local_network_ipv4, "-o", default_device, - "-p", "tcp", "--dport", "53", "--jump", "ACCEPT") - # allow multicast Simple Service Discovery Protocol - ip4tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "239.255.255.250", "--dport", "1900", - "-o", default_device, "--jump", "RETURN") - # allow multicast Bonjour/mDNS - ip4tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "224.0.0.251", "--dport", "5353", - "-o", default_device, "--jump", "RETURN") - - - ipv6_exceptions = getIPv6AllowAddresses() - if local_network_ipv6: - if len(ipv6_exceptions) == 0: - # allow all local network destinations if no explicit allow rules defined - ip6tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv6, "-o", default_device, - "--jump", "ACCEPT") - # allow multicast Simple Service Discovery Protocol - ip6tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "FF05::C", "--dport", "1900", - "-o", default_device, "--jump", "RETURN") - # allow multicast Bonjour/mDNS - ip6tables("--append", BITMASK_CHAIN, - "--protocol", "udp", - "--destination", "FF02::FB", "--dport", "5353", - "-o", default_device, "--jump", "RETURN") - - # allow ipv4 traffic to gateways - for gateway in gateways: - ip4tables("--append", BITMASK_CHAIN, "--destination", gateway, - "-o", default_device, "--jump", "ACCEPT") - - # TODO allow ipv6 traffic to gws too - - # log rejected packets to syslog - if DEBUG: - iptables("--append", BITMASK_CHAIN, "-o", default_device, - "--jump", "LOG", "--log-prefix", "iptables denied: ", - "--log-level", "7") - - # allow explicit private exceptions - if len(ipv4_exceptions) != 0: - for ip in ipv4_exceptions: - allow4(ip) - ip4tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv4, "-o", default_device, - "--jump", "REJECT") - - if len(ipv6_exceptions) != 0: - for ip in ipv6_exceptions: - allow6(ip) - ip6tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv6, "-o", default_device, - "--jump", "REJECT") - - # for now, ensure all other ipv6 packets get rejected (regardless of - # device). not sure why, but "-p any" doesn't work. - ip6tables("--append", BITMASK_CHAIN, "-p", "tcp", "--jump", "REJECT") - ip6tables("--append", BITMASK_CHAIN, "-p", "udp", "--jump", "REJECT") - - # reject all other ipv4 sent over the default device - ip4tables("--append", BITMASK_CHAIN, "-o", - default_device, "--jump", "REJECT") - - - # On Qubes OS, add anti-leak rules for proxyVM qubes-firewall.service - # Must stay on 'top' of chain! - if QUBES_PROXY and QUBES_VER >= 3 and run("grep", r"installed\ by\ " + - SCRIPT, QUBES_FW_SCRIPT, - exitcode=True) != 0: - with open(QUBES_FW_SCRIPT, mode="w") as qfile: - qfile.write("#!/bin/sh\n") - qfile.write("# Anti-leak rules installed by " + SCRIPT + " " - + VERSION + "\n") - qfile.write("iptables --insert FORWARD -i eth0 -j DROP\n") - qfile.write("iptables --insert FORWARD -o eth0 -j DROP\n") - qfile.write("ip6tables --insert FORWARD -i eth0 -j DROP\n") - qfile.write("ip6tables --insert FORWARD -o eth0 -j DROP\n") - qfile.write("iptables --insert INPUT -i tun+ -j DROP\n") - qfile.write("ip6tables --insert INPUT -i tun+ -j DROP\n") - os.chmod(QUBES_FW_SCRIPT, stat.S_IRWXU) - if not os.path.exists(QUBES_IPHOOK): - os.symlink(QUBES_FW_SCRIPT, QUBES_IPHOOK) - if QUBES_VER == 4: - run(QUBES_FW_SCRIPT) - elif QUBES_VER == 3: - run("systemctl", "restart", "qubes-firewall.service") - - # toggle_ipv6('disable') - - -def firewall_stop(): - """ - Stop the firewall. Because we really really always want the firewall to - be stopped if at all possible, this function is cautious and contains a - lot of trys and excepts. - - If there were any problems, we raise an exception at the end. This allows - the calling code to retry stopping the firewall. Stopping the firewall - can fail if iptables is being run by another process (only one iptables - command can be run at a time). - """ - ok = True - - # -t filter -D OUTPUT -j bitmask - try: - iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to remove bitmask firewall from OUTPUT chain " - "(maybe it is already removed?)", exc) - ok = False - - # -t nat -D OUTPUT -j bitmask - try: - ip4tables("-t", "nat", "--delete", "OUTPUT", - "--jump", BITMASK_CHAIN_NAT_OUT, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to remove bitmask firewall from OUTPUT chain " - "in 'nat' table (maybe it is already removed?)", exc) - ok = False - - # -t nat -D POSTROUTING -j bitmask_postrouting - try: - ip4tables("-t", "nat", "--delete", "POSTROUTING", - "--jump", BITMASK_CHAIN_NAT_POST, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to remove bitmask firewall from POSTROUTING " - "chain in 'nat' table (maybe it is already removed?)", exc) - ok = False - - # -t filter --delete-chain bitmask - try: - ip4tables("--flush", BITMASK_CHAIN, throw=True) - ip4tables("--delete-chain", BITMASK_CHAIN, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv4 firewall " - "chain (maybe it is already destroyed?)", exc) - ok = False - - # -t nat --delete-chain bitmask - try: - ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_OUT, throw=True) - ip4tables("-t", "nat", "--delete-chain", - BITMASK_CHAIN_NAT_OUT, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv4 firewall " - "chain in 'nat' table (maybe it is already destroyed?)", exc) - ok = False - - # -t nat --delete-chain bitmask_postrouting - try: - ip4tables("-t", "nat", "--flush", BITMASK_CHAIN_NAT_POST, throw=True) - ip4tables("-t", "nat", "--delete-chain", - BITMASK_CHAIN_NAT_POST, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv4 firewall " - "chain in 'nat' table (maybe it is already destroyed?)", exc) - ok = False - - # -t filter --delete-chain bitmask (ipv6) - try: - ip6tables("--flush", BITMASK_CHAIN, throw=True) - ip6tables("--delete-chain", BITMASK_CHAIN, throw=True) - except subprocess.CalledProcessError as exc: - debug("INFO: not able to flush and delete bitmask ipv6 firewall " - "chain (maybe it is already destroyed?)", exc) - ok = False - - # toggle_ipv6('enable') - - if not (ok or ipv4_chain_exists or ipv6_chain_exists): - raise Exception("firewall might still be left up. " - "Please try `firewall stop` again.") +# echo the required params for a given flag name, or "__NONE__" if no params. +_get_allowed_flag_info() { + case "$1" in + --remote) echo "IP NUMBER PROTO";; + --tls-cipher) echo "CIPHER";; + --cipher) echo "CIPHER";; + --auth) echo "CIPHER";; + --management) echo "DIR||IP UNIXSOCKET||NUMBER FILE";; + --management-client-user) echo "USER";; + --route) echo "IP IP NETGW";; + --cert) echo "FILE";; + --key) echo "FILE";; + --ca) echo "FILE";; + --fragment) echo "NUMBER";; + --keepalive) echo "NUMBER NUMBER";; + --verb) echo "NUMBER";; + --management-client) echo "__NONE__";; + --tun-ipv6) echo "__NONE__";; + --log) echo "LOGFILE";; + --pull-filter) echo "ignore route";; + --socks-proxy) echo "IP NUMBER";; + *) return 1;; + esac +} +parse_openvpn_flags() { + _flags=() + while :; do + [ $# -eq 0 ] && break + flag=$1 + shift + + case "$flag" in + --*) :;; + *) continue;; + esac + + if ! allowed=$(_get_allowed_flag_info "$flag"); then + log_msg "WARNING: unrecognized openvpn flag $flag" + continue + fi + + _params=() + while [ $# -gt 0 ]; do + case "$1" in + --*) break;; + esac + _params+=("$1") + shift + done + + if [ "$allowed" = __NONE__ ]; then + if [ ${#_params[@]} != 0 ]; then + log_msg "ERROR: $flag takes no params" + return 1 + fi + else + _specs=($allowed) + if [ ${#_specs[@]} != ${#_params[@]} ]; then + log_msg "ERROR: wrong param count for $flag" + return 1 + fi + + for i in "${!_specs[@]}"; do + if ! _validate_openvpn_param "${_specs[i]}" "${_params[i]}"; then + log_msg "ERROR: Bad argument ${_params[i]}" + return 1 + fi + done + fi + + _flags+=("$flag" "${_params[@]}") + done + # emit one arg per line so the caller can re-assemble them as an + # array. joining them with spaces and word-splitting later would let + # spaces/globs in a file or address leak into multiple argv entries. + printf '%s\n' "${_flags[@]}" +} -# -# MAIN -# +openvpn_start() { + if ! safe_flags=$(parse_openvpn_flags "$@") || [ -z "$safe_flags" ]; then + bail "ERROR: could not parse openvpn options" + fi + readarray -t safe_flags <<<"$safe_flags" -USAGE = """ -This is bitmask-root version {VERSION} + OPENVPN=$(get_openvpn_bin) -This program manipulates the Bitmask firewall. It is *not* intented to be used -manually. + set -- \ + --setenv LEAPOPENVPN 1 --nobind --client --dev tun \ + --tls-client --remote-cert-tls server --management-signal \ + --script-security 1 --user nobody --auth-nocache --tls-version-min 1.2 -Commands: + if [ -n "$OPENVPN_GROUP" ]; then + set -- "$@" --group "$OPENVPN_GROUP" + fi -{SCRIPT} version -{SCRIPT} restart -{SCRIPT} openvpn start -{SCRIPT} openvpn stop -{SCRIPT} firewall start -{SCRIPT} firewall stop -{SCRIPT} firewall isup -""".format(SCRIPT=SCRIPT, VERSION=VERSION) + if is_ipv6_disabled; then + set -- "$@" \ + --pull-filter ignore ifconfig-ipv6 --pull-filter ignore route-ipv6 + fi + exec "$OPENVPN" "$@" "${safe_flags[@]}" +} -def main(): - """ - Entry point for cmdline execution. - """ - # TODO use argparse instead please. +# the python script manually iterates over /proc/*/cmdline +# we could do the same with a for loop but +# then we would have to mess with null bytes in shell and that's a mess +# +# this also does not need to be a function, made it for reference to the python script +get_process_list() { + ps -eo pid=,args= +} - if len(sys.argv) >= 2: - command = "_".join(sys.argv[1:3]) - args = sys.argv[3:] +openvpn_stop() { + # Stop the openvpn that has likely been launched by bitmask. + get_process_list | while read -r pid cmdline; do + case "$cmdline" in + *openvpn*"$LEAPOPENVPN"*|*"$LEAPOPENVPN"*openvpn*) + kill -TERM "$pid" 2>/dev/null || : + break + ;; + esac + done +} - is_restart = False +############################################################################### +# FIREWALL HELPERS +############################################################################### + +get_gateways() { + # Filter a passed list of gateways, returning only the valid ones. + # Outputs valid gateways one per line. + found=0 + for gw in "$@"; do + if is_valid_address "$gw"; then + echo "$gw" + found=1 + fi + done + [ "$found" != 0 ] || return 1 + return 0 +} - if command == 'help' or command == '-h': - print(USAGE) - exit(0) +get_default_device() { + # Retrieve the current default network device. + routes="$("$IP" route show 2>/dev/null)" || return 1 + device="${routes#*default*dev }" + device="${device%% *}" + [ -n "$device" ] || return 1 + echo "$device" +} - if args and args[0] == 'restart': - is_restart = True - args.remove('restart') +get_local_network_ipv4() { + device=$1 + addresses=$("$IP" -o address show dev "$device" 2>/dev/null) || return 1 + case "$addresses" in + *'inet '*) + addr=${addresses#*inet } + addr=${addr%% *} + ;; + *) + return 1 + ;; + esac + echo "$addr" +} - if command == "version": - print(VERSION) - exit(0) +get_local_network_ipv6() { + device=$1 + addresses=$("$IP" -o address show dev "$device" 2>/dev/null) || return 1 + case "$addresses" in + *'inet6 '*) + addr=${addresses#*inet6 } + addr=${addr%% *} + ;; + *) + return 1 + ;; + esac + echo "$addr" +} - if os.getuid() != 0: - bail("ERROR: must be run as root") +enable_ip_forwarding() { + echo "1" > /proc/sys/net/ipv4/ip_forward 2>/dev/null || : +} - if command == "openvpn_start": - openvpn_start(args) +_firewall_start_commands() { + default_device="$(get_default_device)" || bail "Could not find default device" + local_network_ipv4=$(get_local_network_ipv4 "$default_device") || : + local_network_ipv6=$(get_local_network_ipv6 "$default_device") || : + gateways=$(get_gateways "$@") || bail "ERROR: No valid gateways specified" + + # Create custom chains + ipv4_chain_exists "$CHAIN" || ip4tables --new-chain "$CHAIN" + ipv4_chain_exists "$CHAIN_NAT" nat || ip4tables --table nat --new-chain "$CHAIN_NAT" + ipv4_chain_exists "$CHAIN_POST" nat || ip4tables --table nat --new-chain "$CHAIN_POST" + ipv6_chain_exists "$CHAIN" || ip6tables --new-chain "$CHAIN" + + ip4tables --table nat --insert OUTPUT --jump "$CHAIN_NAT" + ip4tables --table nat --insert POSTROUTING --jump "$CHAIN_POST" + iptables_both --insert OUTPUT --jump "$CHAIN" +} - elif command == "openvpn_stop": - openvpn_stop(args) +_firewall_start_ipv4() { + ipv4_exceptions=$(getIPv4AllowAddresses) + [ -n "$local_network_ipv4" ] || return 0 + + if [ -z "$ipv4_exceptions" ]; then + # Allow all network destinations if no explicit allow list + ip4tables --append "$CHAIN" \ + --destination "$local_network_ipv4" -o "$default_device" --jump ACCEPT + fi + # Allow network sources for DNS + ip4tables --append "$CHAIN" \ + --source "$local_network_ipv4" -o "$default_device" -p udp --dport 53 --jump ACCEPT + ip4tables --append "$CHAIN" \ + --source "$local_network_ipv4" -o "$default_device" -p tcp --dport 53 --jump ACCEPT + # Allow multicast SSDP + ip4tables --append "$CHAIN" \ + --protocol udp --destination 239.255.255.250 --dport 1900 \ + -o "$default_device" --jump RETURN + # Allow multicast mDNS + ip4tables --append "$CHAIN" \ + --protocol udp --destination 224.0.0.251 --dport 5353 \ + -o "$default_device" --jump RETURN +} - elif command == "firewall_start": - try: - firewall_start(args) - except Exception as ex: - if not is_restart: - firewall_stop() - bail("ERROR: could not start firewall", ex) +_firewall_start_ipv6() { + ipv6_exceptions="$(getIPv6AllowAddresses)" + [ -n "$local_network_ipv6" ] || return 0 + if [ -z "$ipv6_exceptions" ]; then + ip6tables --append "$CHAIN" \ + --destination "$local_network_ipv6" -o "$default_device" --jump ACCEPT + fi + ip6tables --append "$CHAIN" --protocol udp --destination "FF05::C" \ + --dport 1900 -o "$default_device" --jump RETURN + ip6tables --append "$CHAIN" --protocol udp --destination "FF02::FB" \ + --dport 5353 -o "$default_device" --jump RETURN +} - elif command == "firewall_stop": - try: - firewall_stop() - except Exception as ex: - bail("ERROR: could not stop firewall", ex) +_firewall_start_qubes() { + [ "$QUBES_VER" -ge 3 ] || return 0 + if ! grep -q "installed by $SCRIPT" "$QUBES_FW_SCRIPT" 2>/dev/null; then + cat > "$QUBES_FW_SCRIPT" <<-EOF + #!/bin/sh + # Anti-leak rules installed by $SCRIPT $VERSION + iptables --insert FORWARD -i eth0 -j DROP + iptables --insert FORWARD -o eth0 -j DROP + ip6tables --insert FORWARD -i eth0 -j DROP + ip6tables --insert FORWARD -o eth0 -j DROP + iptables --insert INPUT -i tun+ -j DROP + ip6tables --insert INPUT -i tun+ -j DROP + EOF + chmod 0700 "$QUBES_FW_SCRIPT" + if [ ! -e "$QUBES_IPHOOK" ]; then + ln -s "$QUBES_FW_SCRIPT" "$QUBES_IPHOOK" 2>/dev/null || : + fi + if [ "$QUBES_VER" = 4 ]; then + "$QUBES_FW_SCRIPT" + elif [ "$QUBES_VER" = 3 ]; then + systemctl restart qubes-firewall.service >/dev/null 2>&1 || : + fi + fi +} - elif command == "firewall_isup": - if ipv4_chain_exists(BITMASK_CHAIN): - # too verbose since bitmask polls this - pass - else: - bail("INFO: bitmask firewall is down") +firewall_start() { + # Bring up the firewall. + # Args: [restart] gateway1 gateway2 ... + # + # If "restart" is passed, the firewall is not torn down on error + # so it can be retried without losing existing rules. + + _RESTART=0 + for arg do + case "$arg" in + restart) _RESTART=1; shift;; + *) set -- "$@" "$arg"; shift;; + esac + done + trap '[ "$_RESTART" -eq 0 ] && firewall_stop || :' EXIT + + _firewall_start_commands "$@" + + # Route all ipv4 DNS over VPN + enable_ip_forwarding + + if [ "$QUBES_VER" -ge 3 ]; then + # Qubes rewrites DNS + ip4tables -t nat --flush PR-QBS + echo "$gateways" | while IFS= read -r gw; do + [ -n "$gw" ] || continue + ip4tables -t nat --append PR-QBS --destination "$gw" --jump RETURN + done + ip4tables -t nat --append PR-QBS -p udp --dport 53 --jump DNAT --to "${NAMESERVER}:53" + ip4tables -t nat --append PR-QBS -p tcp --dport 53 --jump DNAT --to "${NAMESERVER}:53" + else + # Normal DNS rewrite + echo "$gateways" | while IFS= read -r gw; do + [ -n "$gw" ] || continue + ip4tables -t nat --append "$CHAIN_NAT" --destination "$gw" --jump RETURN + done + + ip4tables -t nat --append "$CHAIN_NAT" --protocol udp \ + --dest "127.0.1.1,127.0.0.1,127.0.0.53" --dport 53 --jump ACCEPT + + ip4tables -t nat --append "$CHAIN_NAT" -p udp --dport 53 \ + --jump DNAT --to "${NAMESERVER}:53" + ip4tables -t nat --append "$CHAIN_NAT" -p tcp --dport 53 \ + --jump DNAT --to "${NAMESERVER}:53" + + # Masquerade for rewritten DNS packets + ip4tables -t nat --append "$CHAIN_POST" \ + --dest "$NAMESERVER" --protocol udp --dport 53 --jump MASQUERADE + ip4tables -t nat --append "$CHAIN_POST" \ + --dest "$NAMESERVER" --protocol tcp --dport 53 --jump MASQUERADE + fi + + _firewall_start_ipv4 + _firewall_start_ipv6 + + # ---- Allow traffic to gateways ---- + echo "$gateways" | while IFS= read -r gw; do + [ -n "$gw" ] || continue + ip4tables --append "$CHAIN" --destination "$gw" -o "$default_device" --jump ACCEPT + done + + # ---- Debug logging ---- + if [ -n "$DEBUG" ]; then + iptables_both --append "$CHAIN" -o "$default_device" \ + --jump LOG --log-prefix "iptables denied: " --log-level 7 + fi + + # ---- Explicit private exceptions ---- + if [ -n "$ipv4_exceptions" ]; then + echo "$ipv4_exceptions" | while IFS= read -r ip; do + [ -n "$ip" ] || continue + ip4tables --append "$CHAIN" --destination "$ip" -o "$default_device" --jump ACCEPT + done + ip4tables --append "$CHAIN" \ + --destination "$local_network_ipv4" -o "$default_device" --jump REJECT + fi + + if [ -n "$ipv6_exceptions" ]; then + echo "$ipv6_exceptions" | while IFS= read -r ip; do + [ -n "$ip" ] || continue + ip6tables --append "$CHAIN" --destination "$ip" -o "$default_device" --jump ACCEPT + done + ip6tables --append "$CHAIN" \ + --destination "$local_network_ipv6" -o "$default_device" --jump REJECT + fi + + # ---- Reject all other IPv6 ---- + ip6tables --append "$CHAIN" -p tcp --jump REJECT + ip6tables --append "$CHAIN" -p udp --jump REJECT + + # ---- Reject all other IPv4 over default device ---- + ip4tables --append "$CHAIN" -o "$default_device" --jump REJECT + + # ---- Qubes anti-leak rules ---- + _firewall_start_qubes + + trap - EXIT +} - else: - bail("ERROR: No such command. Try bitmask-root help") - else: - bail("ERROR: No such command. Try bitmask-root help") +_firewall_stop_commands() { + iptables_both --delete OUTPUT --jump "$CHAIN" || status=$? + ip4tables -t nat --delete OUTPUT --jump "$CHAIN_NAT" || status=$? + ip4tables -t nat --delete POSTROUTING --jump "$CHAIN_POST" || status=$? + ip4tables --flush "$CHAIN" && ip4tables --delete-chain "$CHAIN" || status=$? + ip4tables -t nat --flush "$CHAIN_NAT" && ip4tables -t nat --delete-chain "$CHAIN_NAT" || status=$? + ip4tables -t nat --flush "$CHAIN_POST" && ip4tables -t nat --delete-chain "$CHAIN_POST" || status=$? + ip6tables --flush "$CHAIN" && ip6tables --delete-chain "$CHAIN" || status=$? +} + +firewall_stop() { + status=0 + _firewall_stop_commands + + # NOTE: the python script has a bug here: + # it checks `if not (ok or ipv4_chain_exists or ipv6_chain_exists)` + # without`()` on the function calls, + # so the exception is never raised and failures are silently ignored. + if [ "$status" = 0 ]; then + return 0 + elif ipv4_chain_exists "$CHAIN" || ipv6_chain_exists "$CHAIN"; then + log_msg "firewall might still be left up. Please try 'firewall stop' again." err + fi + return 1 +} -if __name__ == "__main__": - debug(" ".join(sys.argv)) - main() - exit(0) +case "$1" in + --help|help|-h) + usage + ;; + --version|version) + echo "$VERSION" + exit 0 + ;; + openvpn) + shift + check_root + case "$1" in + start) shift; openvpn_start "$@";; + stop) shift; openvpn_stop;; + *) bail "ERROR: No such command. Try $SCRIPT help";; + esac + ;; + firewall) + shift + check_root + case "$1" in + start) shift; firewall_start "$@";; + stop) shift; firewall_stop || bail "ERROR: could not stop firewall";; + isup) shift; ipv4_chain_exists "$CHAIN" || bail "INFO: bitmask firewall is down";; + *) bail "ERROR: No such command. Try $SCRIPT help";; + esac;; + *) + bail "ERROR: No such command. Try $SCRIPT help" + ;; +esac