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