diff options
author | Kali Kaneko (leap communications) <kali@leap.se> | 2017-01-31 13:31:13 +0100 |
---|---|---|
committer | Kali Kaneko (leap communications) <kali@leap.se> | 2017-02-23 00:37:25 +0100 |
commit | ca0e1c4518749e27bccad817d22ab87afbf8acf7 (patch) | |
tree | 636c1188683c1ea91d70b3aecd2810aafa7cf724 /src/leap/bitmask/vpn/fw | |
parent | ff5ec25029db7669163854886be254fccde90e80 (diff) |
[feature] initial port of legacy vpn code
non functional at the moment, but started doing some cleanup
Diffstat (limited to 'src/leap/bitmask/vpn/fw')
-rw-r--r-- | src/leap/bitmask/vpn/fw/__init__.py | 0 | ||||
-rwxr-xr-x | src/leap/bitmask/vpn/fw/bitmask-root | 971 | ||||
-rw-r--r-- | src/leap/bitmask/vpn/fw/firewall.py | 95 |
3 files changed, 1066 insertions, 0 deletions
diff --git a/src/leap/bitmask/vpn/fw/__init__.py b/src/leap/bitmask/vpn/fw/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/bitmask/vpn/fw/__init__.py diff --git a/src/leap/bitmask/vpn/fw/bitmask-root b/src/leap/bitmask/vpn/fw/bitmask-root new file mode 100755 index 0000000..80ac12e --- /dev/null +++ b/src/leap/bitmask/vpn/fw/bitmask-root @@ -0,0 +1,971 @@ +#!/usr/bin/python2.7 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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. + +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. +""" +# TODO should be tested with python3, which can be the default on some distro. +from __future__ import print_function +import os +import re +import signal +import socket +import syslog +import subprocess +import sys +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 + + +VERSION = "6" +SCRIPT = "bitmask-root" +NAMESERVER = "10.42.0.1" +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" + +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 + +FIXED_FLAGS = [ + "--setenv", "LEAPOPENVPN", "1", + "--nobind", + "--client", + "--dev", "tun", + "--tls-client", + "--remote-cert-tls", "server", + "--management-signal", + "--script-security", "1", + "--user", "nobody", + "--remap-usr1", "SIGTERM", +] + +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", "UNIXSOCKET"], + "--management-client-user": ["USER"], + "--cert": ["FILE"], + "--key": ["FILE"], + "--ca": ["FILE"], + "--fragment": ["NUMBER"] +} + +PARAM_FORMATS = { + "NUMBER": lambda s: re.match("^\d+$", s), + "PROTO": lambda s: re.match("^(tcp|udp)$", 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) +} + + +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.path.isfile(OPENVPN_SYSTEM_BIN): + return OPENVPN_SYSTEM_BIN + + # the bundle option should be removed from the debian package. + 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: + flag_params = flag[1:] + 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): + if PARAM_FORMATS[param_type](param): + result.append(param) + 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() + OPENVPN_BIN = get_openvpn_bin() + found_leap_openvpn = filter( + lambda (p, s): s.startswith(OPENVPN_BIN) and LEAPOPENVPN in s, + plist) + + if found_leap_openvpn: + pid = found_leap_openvpn[0][0] + os.kill(int(pid), signal.SIGTERM) + +# +# 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("^default .*dev ([^\s]*) .*$", routes, flags=re.M) + if match and match.groups(): + return 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("^.*inet ([^ ]*) .*$", addresses, flags=re.M) + if match and match.groups(): + return 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("^.*inet6 ([^ ]*) .*$", addresses, flags=re.M) + if match and match.groups(): + return 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) + +# +# 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() + # allow dns to localhost + ip4tables("-t", "nat", "--append", BITMASK_CHAIN, "--protocol", "udp", + "--dest", "127.0.1.1,127.0.0.1", "--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 + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, + "--protocol", "udp", "--dport", "53", "--jump", "MASQUERADE") + ip4tables("-t", "nat", "--append", BITMASK_CHAIN_NAT_POST, + "--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") + + +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 + + 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 +# + + +def main(): + """ + Entry point for cmdline execution. + """ + # TODO use argparse instead. + + if len(sys.argv) >= 2: + command = "_".join(sys.argv[1:3]) + args = sys.argv[3:] + + is_restart = False + 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): + log("%s: INFO: bitmask firewall is up" % (SCRIPT,)) + 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") + else: + bail("ERROR: No such command") + +if __name__ == "__main__": + debug(" ".join(sys.argv)) + main() + log("done") + exit(0) diff --git a/src/leap/bitmask/vpn/fw/firewall.py b/src/leap/bitmask/vpn/fw/firewall.py new file mode 100644 index 0000000..4335b8e --- /dev/null +++ b/src/leap/bitmask/vpn/fw/firewall.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# manager.py +# Copyright (C) 2015 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/>. + +""" +Firewall Manager +""" + +import commands +import subprocess + +from leap.bitmask.vpn.constants import IS_MAC + + +class FirewallManager(object): + + """ + Firewall manager that blocks/unblocks all the internet traffic with some + exceptions. + This allows us to achieve fail close on a vpn connection. + """ + + # FIXME -- get the path + BITMASK_ROOT = "/usr/local/sbin/bitmask-root" + + def __init__(self, remotes): + """ + Initialize the firewall manager with a set of remotes that we won't + block. + + :param remotes: the gateway(s) that we will allow + :type remotes: list + """ + self._remotes = remotes + + def start(self, restart=False): + """ + Launch the firewall using the privileged wrapper. + + :returns: True if the exitcode of calling the root helper in a + subprocess is 0. + :rtype: bool + """ + gateways = [gateway for gateway, port in self._remotes] + + # XXX check for wrapper existence, check it's root owned etc. + # XXX check that the iptables rules are in place. + + cmd = ["pkexec", self.BITMASK_ROOT, "firewall", "start"] + if restart: + cmd.append("restart") + + # FIXME -- use a processprotocol + exitCode = subprocess.call(cmd + gateways) + return True if exitCode is 0 else False + + # def tear_down_firewall(self): + def stop(self): + """ + Tear the firewall down using the privileged wrapper. + """ + if IS_MAC: + # We don't support Mac so far + return True + + exitCode = subprocess.call(["pkexec", self.BITMASK_ROOT, + "firewall", "stop"]) + return True if exitCode is 0 else False + + # def is_fw_down(self): + def is_up(self): + """ + Return whether the firewall is up or not. + + :rtype: bool + """ + # TODO test this, refactored from is_fw_down + + cmd = "pkexec {0} firewall isup".format(self.BITMASK_ROOT) + output = commands.getstatusoutput(cmd)[0] + + return output != 256 |