summaryrefslogtreecommitdiff
path: root/helpers/bitmask-root
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2019-08-16 12:50:45 +0200
committerKali Kaneko (leap communications) <kali@leap.se>2019-08-16 12:58:00 +0200
commit3120b855ac31158a52c9263f522615b4c3a0aa20 (patch)
tree624837ece395e563286a54cbb050f4f21ae48cd2 /helpers/bitmask-root
parent4aa6fd4109aa9e92a1ade72885d7c99effcd7c76 (diff)
[pkg] add helpers to repo, generate hooks in snap
Diffstat (limited to 'helpers/bitmask-root')
-rw-r--r--helpers/bitmask-root1091
1 files changed, 1091 insertions, 0 deletions
diff --git a/helpers/bitmask-root b/helpers/bitmask-root
new file mode 100644
index 0000000..42a46bd
--- /dev/null
+++ b/helpers/bitmask-root
@@ -0,0 +1,1091 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014-2019 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+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 ...
+ bitmask-root fw-email stop
+ bitmask-root fw-email start uid
+
+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 os
+import re
+import signal
+import socket
+import syslog
+import subprocess
+import sys
+import stat
+import traceback
+
+cmdcheck = subprocess.check_output
+
+#
+# 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 tostr(s):
+ return s.decode('utf-8')
+
+VERSION = "11"
+SCRIPT = "bitmask-root"
+NAMESERVER_TCP = "10.41.0.1"
+NAMESERVER_UDP = "10.42.0.1"
+# for the time being, we're hardcoding tcp on connection params.
+NAMESERVER = NAMESERVER_TCP
+BITMASK_CHAIN = "bitmask"
+BITMASK_CHAIN_NAT_OUT = "bitmask"
+BITMASK_CHAIN_NAT_POST = "bitmask_postrouting"
+BITMASK_CHAIN_EMAIL = "bitmask_email"
+BITMASK_CHAIN_EMAIL_OUT = "bitmask_email_output"
+LOCAL_INTERFACE = "lo"
+IMAP_PORT = "1984"
+SMTP_PORT = "2013"
+
+IP = "/sbin/ip"
+IPTABLES = "/sbin/iptables"
+IP6TABLES = "/sbin/ip6tables"
+SYSCTL = "/sbin/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",
+ "--persist-key",
+ "--persist-local-ip",
+ "--tls-version-min", "1.0",
+]
+
+if OPENVPN_GROUP is not None:
+ FIXED_FLAGS.extend(["--group", OPENVPN_GROUP])
+
+ALLOWED_FLAGS = {
+ "--remote": ["IP", "NUMBER", "PROTO"],
+ "--tls-cipher": ["CIPHER"],
+ "--cipher": ["CIPHER"],
+ "--auth": ["CIPHER"],
+ "--management": ["DIR||IP", "UNIXSOCKET||NUMBER"],
+ "--management-client-user": ["USER"],
+ "--cert": ["FILE"],
+ "--key": ["FILE"],
+ "--ca": ["FILE"],
+ "--fragment": ["NUMBER"],
+ "--keepalive": ["NUMBER", "NUMBER"],
+ "--verb": ["NUMBER"],
+ "--management-client": [],
+ "--tun-ipv6": [],
+}
+
+PARAM_FORMATS = {
+ "NUMBER": lambda s: re.match("^\d+$", s),
+ "PROTO": lambda s: re.match("^(tcp|udp|tcp4|udp4)$", s),
+ "IP": lambda s: is_valid_address(s),
+ "CIPHER": lambda s: re.match("^[A-Z0-9-]+$", s),
+ "USER": lambda s: re.match(
+ "^[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",
+ "UID": lambda s: re.match("^[a-zA-Z0-9]+$", s)
+}
+
+# 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)
+
+#
+# UTILITY
+#
+
+
+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 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)
+
+#
+# OPENVPN
+#
+
+
+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
+
+#
+# FIREWALL
+#
+
+
+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 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])
+
+
+#
+# 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.
+#
+
+
+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)
+
+ # 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")
+ 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:
+ # allow dns to localhost
+ ip4tables("-t", "nat", "--append", BITMASK_CHAIN, "--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
+ if local_network_ipv4:
+ # allow local network destinations
+ 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")
+ if local_network_ipv6:
+ 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")
+
+ # log rejected packets to syslog
+ if DEBUG:
+ iptables("--append", BITMASK_CHAIN, "-o", default_device,
+ "--jump", "LOG", "--log-prefix", "iptables denied: ",
+ "--log-level", "7")
+
+ # 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", "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.")
+
+
+def fw_email_start(args):
+ """
+ Bring up the email firewall.
+
+ :param args: the user uid of the bitmask process
+ :type args: list
+ """
+ # add custom chain "bitmask_email" to front of INPUT chain
+ if not ipv4_chain_exists(BITMASK_CHAIN_EMAIL):
+ ip4tables("--new-chain", BITMASK_CHAIN_EMAIL)
+ if not ipv6_chain_exists(BITMASK_CHAIN_EMAIL):
+ ip6tables("--new-chain", BITMASK_CHAIN_EMAIL)
+ iptables("--insert", "INPUT", "--jump", BITMASK_CHAIN_EMAIL)
+
+ # add custom chain "bitmask_email_output" to front of OUTPUT chain
+ if not ipv4_chain_exists(BITMASK_CHAIN_EMAIL_OUT):
+ ip4tables("--new-chain", BITMASK_CHAIN_EMAIL_OUT)
+ if not ipv6_chain_exists(BITMASK_CHAIN_EMAIL_OUT):
+ ip6tables("--new-chain", BITMASK_CHAIN_EMAIL_OUT)
+ iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN_EMAIL_OUT)
+
+ # Disable the access to imap and smtp from outside
+ iptables("--append", BITMASK_CHAIN_EMAIL,
+ "--in-interface", LOCAL_INTERFACE, "--protocol", "tcp",
+ "--dport", IMAP_PORT, "--jump", "ACCEPT")
+ iptables("--append", BITMASK_CHAIN_EMAIL,
+ "--in-interface", LOCAL_INTERFACE, "--protocol", "tcp",
+ "--dport", SMTP_PORT, "--jump", "ACCEPT")
+ iptables("--append", BITMASK_CHAIN_EMAIL,
+ "--protocol", "tcp", "--dport", IMAP_PORT, "--jump", "REJECT")
+ iptables("--append", BITMASK_CHAIN_EMAIL,
+ "--protocol", "tcp", "--dport", SMTP_PORT, "--jump", "REJECT")
+
+ if not args or not PARAM_FORMATS["UID"](args[0]):
+ raise Exception("No uid given")
+ uid = args[0]
+
+ # Only the unix 'uid' have access to the email imap and smtp ports
+ iptables("--append", BITMASK_CHAIN_EMAIL_OUT,
+ "--out-interface", LOCAL_INTERFACE,
+ "--match", "owner", "--uid-owner", uid, "--protocol", "tcp",
+ "--dport", IMAP_PORT, "--jump", "ACCEPT")
+ iptables("--append", BITMASK_CHAIN_EMAIL_OUT,
+ "--out-interface", LOCAL_INTERFACE,
+ "--match", "owner", "--uid-owner", uid, "--protocol", "tcp",
+ "--dport", SMTP_PORT, "--jump", "ACCEPT")
+ iptables("--append", BITMASK_CHAIN_EMAIL_OUT,
+ "--out-interface", LOCAL_INTERFACE,
+ "--protocol", "tcp", "--dport", IMAP_PORT, "--jump", "REJECT")
+ iptables("--append", BITMASK_CHAIN_EMAIL_OUT,
+ "--out-interface", LOCAL_INTERFACE,
+ "--protocol", "tcp", "--dport", SMTP_PORT, "--jump", "REJECT")
+
+
+def fw_email_stop():
+ """
+ Stop the email firewall.
+ """
+ ok = True
+
+ try:
+ iptables("--delete", "INPUT", "--jump", BITMASK_CHAIN_EMAIL,
+ throw=True)
+ except subprocess.CalledProcessError as exc:
+ debug("INFO: not able to remove bitmask email firewall from INPUT "
+ "chain (maybe it is already removed?)", exc)
+ ok = False
+
+ try:
+ iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN_EMAIL_OUT,
+ throw=True)
+ except subprocess.CalledProcessError as exc:
+ debug("INFO: not able to remove bitmask email firewall from OUTPUT "
+ "chain (maybe it is already removed?)", exc)
+ ok = False
+
+ try:
+ ip4tables("--flush", BITMASK_CHAIN_EMAIL, throw=True)
+ ip4tables("--delete-chain", BITMASK_CHAIN_EMAIL, throw=True)
+ except subprocess.CalledProcessError as exc:
+ debug("INFO: not able to flush and delete bitmask ipv4 email firewall "
+ "chain (maybe it is already destroyed?)", exc)
+ ok = False
+
+ try:
+ ip6tables("--flush", BITMASK_CHAIN_EMAIL, throw=True)
+ ip6tables("--delete-chain", BITMASK_CHAIN_EMAIL, throw=True)
+ except subprocess.CalledProcessError as exc:
+ debug("INFO: not able to flush and delete bitmask ipv6 email firewall "
+ "chain (maybe it is already destroyed?)", exc)
+ ok = False
+
+ try:
+ ip4tables("--flush", BITMASK_CHAIN_EMAIL_OUT, throw=True)
+ ip4tables("--delete-chain", BITMASK_CHAIN_EMAIL_OUT, throw=True)
+ except subprocess.CalledProcessError as exc:
+ debug("INFO: not able to flush and delete bitmask ipv4 email firewall "
+ "chain (maybe it is already destroyed?)", exc)
+ ok = False
+
+ try:
+ ip6tables("--flush", BITMASK_CHAIN_EMAIL_OUT, throw=True)
+ ip6tables("--delete-chain", BITMASK_CHAIN_EMAIL_OUT, throw=True)
+ except subprocess.CalledProcessError as exc:
+ debug("INFO: not able to flush and delete bitmask ipv6 email firewall "
+ "chain (maybe it is already destroyed?)", exc)
+ ok = False
+
+ if not (ok or ipv4_chain_exists or ipv6_chain_exists):
+ raise Exception("email firewall might still be left up. "
+ "Please try `fw-email stop` again.")
+
+
+#
+# MAIN
+#
+
+USAGE = """
+This is bitmask-root version {VERSION}
+
+This program manipulates the Bitmask firewall. It is *not* intented to be used
+manually.
+
+Commands:
+
+{SCRIPT} version
+{SCRIPT} restart
+{SCRIPT} openvpn start <args>
+{SCRIPT} openvpn stop
+{SCRIPT} firewall start <args>
+{SCRIPT} firewall stop
+{SCRIPT} firewall isup
+{SCRIPT} fw-email start
+{SCRIPT} fw-email stop
+{SCRIPT} fw-email isup
+""".format(SCRIPT=SCRIPT, VERSION=VERSION)
+
+
+def main():
+ """
+ Entry point for cmdline execution.
+ """
+ # TODO use argparse instead please.
+
+ if len(sys.argv) >= 2:
+ command = "_".join(sys.argv[1:3])
+ args = sys.argv[3:]
+
+ is_restart = False
+
+ if command == 'help' or command == '-h':
+ print(USAGE)
+ exit(0)
+
+ if args and args[0] == 'restart':
+ is_restart = True
+ args.remove('restart')
+
+ if command == "version":
+ print(VERSION)
+ exit(0)
+
+ if os.getuid() != 0:
+ bail("ERROR: must be run as root")
+
+ if command == "openvpn_start":
+ openvpn_start(args)
+
+ elif command == "openvpn_stop":
+ openvpn_stop(args)
+
+ 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)
+
+ elif command == "firewall_stop":
+ try:
+ firewall_stop()
+ except Exception as ex:
+ bail("ERROR: could not stop firewall", ex)
+
+ elif command == "firewall_isup":
+ if ipv4_chain_exists(BITMASK_CHAIN):
+ # too verbose since bitmask polls this
+ pass
+ else:
+ bail("INFO: bitmask firewall is down")
+
+ elif command == "fw-email_start":
+ try:
+ fw_email_start(args)
+ except Exception as ex:
+ if not is_restart:
+ fw_email_stop()
+ bail("ERROR: could not start email firewall", ex)
+
+ elif command == "fw-email_stop":
+ try:
+ fw_email_stop()
+ except Exception as ex:
+ bail("ERROR: could not stop email firewall", ex)
+
+ elif command == "fw-email_isup":
+ if ipv4_chain_exists(BITMASK_CHAIN_EMAIL):
+ log("%s: INFO: bitmask email firewall is up" % (SCRIPT,))
+ else:
+ bail("INFO: bitmask email firewall is down")
+
+ else:
+ bail("ERROR: No such command. Try bitmask-root help")
+ else:
+ bail("ERROR: No such command. Try bitmask-root help")
+
+
+if __name__ == "__main__":
+ debug(" ".join(sys.argv))
+ main()
+ exit(0)