diff options
-rw-r--r-- | changes/bug-5592_harcode_openvpn_path_for_bundle | 1 | ||||
-rw-r--r-- | changes/bug-5595-increase-polkit-wait-time | 1 | ||||
-rw-r--r-- | changes/bug-avoid-soft-restart | 1 | ||||
-rw-r--r-- | changes/feature_linux-firewall | 1 | ||||
-rw-r--r-- | pkg/linux/README | 4 | ||||
-rw-r--r-- | pkg/linux/README.rst | 10 | ||||
-rwxr-xr-x | pkg/linux/bitmask-root | 829 | ||||
-rw-r--r-- | pkg/linux/polkit/se.leap.bitmask.policy (renamed from pkg/linux/polkit/net.openvpn.gui.leap.policy) | 12 | ||||
-rwxr-xr-x | pkg/linux/update-resolv-conf | 58 | ||||
-rw-r--r-- | src/leap/bitmask/backend.py | 43 | ||||
-rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 19 | ||||
-rw-r--r-- | src/leap/bitmask/gui/twisted_main.py | 1 | ||||
-rw-r--r-- | src/leap/bitmask/platform_init/initializers.py | 9 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/linuxvpnlauncher.py | 113 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/vpnlauncher.py | 124 | ||||
-rw-r--r-- | src/leap/bitmask/services/eip/vpnprocess.py | 88 | ||||
-rw-r--r-- | src/leap/bitmask/util/privilege_policies.py | 82 |
17 files changed, 1166 insertions, 230 deletions
diff --git a/changes/bug-5592_harcode_openvpn_path_for_bundle b/changes/bug-5592_harcode_openvpn_path_for_bundle new file mode 100644 index 00000000..67f4b533 --- /dev/null +++ b/changes/bug-5592_harcode_openvpn_path_for_bundle @@ -0,0 +1 @@ +- Hardcode paths for openvpn if STANDALONE=True. Related: #5592 diff --git a/changes/bug-5595-increase-polkit-wait-time b/changes/bug-5595-increase-polkit-wait-time new file mode 100644 index 00000000..5662e249 --- /dev/null +++ b/changes/bug-5595-increase-polkit-wait-time @@ -0,0 +1 @@ +- Increase waiting time to wait for polkit agent to be up. Closes: #5595 diff --git a/changes/bug-avoid-soft-restart b/changes/bug-avoid-soft-restart new file mode 100644 index 00000000..36795ad7 --- /dev/null +++ b/changes/bug-avoid-soft-restart @@ -0,0 +1 @@ +- Use openvpn hard restart. Closes: #5669 diff --git a/changes/feature_linux-firewall b/changes/feature_linux-firewall new file mode 100644 index 00000000..2a48da07 --- /dev/null +++ b/changes/feature_linux-firewall @@ -0,0 +1 @@ +- Use iptables firewall. Closes: #5588 diff --git a/pkg/linux/README b/pkg/linux/README deleted file mode 100644 index 7410789b..00000000 --- a/pkg/linux/README +++ /dev/null @@ -1,4 +0,0 @@ -= Files = -In GNU/Linux, we expect these files to be in place: - -resolv-update -> /etc/leap/resolv-update diff --git a/pkg/linux/README.rst b/pkg/linux/README.rst new file mode 100644 index 00000000..220565ff --- /dev/null +++ b/pkg/linux/README.rst @@ -0,0 +1,10 @@ +Files +===== + +In GNU/Linux, we expect these files to be in place:: + + update-resolv-conf -> /etc/leap/update-resolv-conf + resolv-update -> /etc/leap/resolv-update + + bitmask-root -> /usr/sbin/bitmask-root + polkit/se.leap.bitmask.policy -> /usr/share/polkit-1/actions/se.leap.bitmask.policy diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root new file mode 100755 index 00000000..136fd6a4 --- /dev/null +++ b/pkg/linux/bitmask-root @@ -0,0 +1,829 @@ +#!/usr/bin/python +# -*- 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 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. +""" +# TODO should be tested with python3, which can be the default on some distro. +from __future__ import print_function +import atexit +import os +import re +import signal +import socket +import subprocess +import sys +import time +import traceback + + +cmdcheck = subprocess.check_output + +## +## CONSTANTS +## + +SCRIPT = "bitmask-root" +NAMESERVER = "10.42.0.1" +BITMASK_CHAIN = "bitmask" + +IP = "/bin/ip" +IPTABLES = "/sbin/iptables" +IP6TABLES = "/sbin/ip6tables" + +RESOLVCONF_SYSTEM_BIN = "/sbin/resolvconf" +RESOLVCONF_LEAP_BIN = "/usr/local/sbin/leap-resolvconf" + +OPENVPN_USER = "nobody" +OPENVPN_GROUP = "nogroup" +LEAPOPENVPN = "LEAPOPENVPN" +OPENVPN_SYSTEM_BIN = "/usr/sbin/openvpn" # Debian location +OPENVPN_LEAP_BIN = "/usr/sbin/leap-openvpn" # installed by bundle + + +""" +The path to the script to update resolv.conf +""" +# XXX We have to check if we have a valid resolvconf, and use +# the old resolv-update if not. +LEAP_UPDATE_RESOLVCONF_FILE = "/etc/leap/update-resolv-conf" +LEAP_RESOLV_UPDATE = "/etc/leap/resolv-update" + +FIXED_FLAGS = [ + "--setenv", "LEAPOPENVPN", "1", + "--nobind", + "--client", + "--dev", "tun", + "--tls-client", + "--remote-cert-tls", "server", + "--management-signal", + "--script-security", "1", + "--user", "nobody", + "--group", "nogroup", + "--remap-usr1", "SIGTERM", +] + +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"] +} + +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" +} + + +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) + +## +## 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: + print("%s: ERROR: MALFORMED IP: %s!" % (SCRIPT, value)) + return False + + +def has_system_resolvconf(): + """ + Return True if resolvconf is found in the system. + + :rtype: bool + """ + return os.path.isfile(RESOLVCONF) + + +def has_valid_update_resolvconf(): + """ + Return True if a valid update-resolv-conf script is found in the system. + + :rtype: bool + """ + return os.path.isfile(LEAP_UPDATE_RESOLVCONF_FILE) + + +def has_valid_leap_resolv_update(): + """ + Return True if a valid resolv-update script is found in the system. + + :rtype: bool + """ + return os.path.isfile(LEAP_RESOLV_UPDATE) + + +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) + + +class Daemon(object): + """ + A generic daemon class. + """ + def __init__(self, pidfile, stdin='/dev/null', + stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + Do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError, e: + sys.stderr.write( + "fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError, e: + sys.stderr.write( + "fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file(self.stdin, 'r') + so = file(self.stdout, 'a+') + se = file(self.stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + file(self.pidfile, 'w+').write("%s\n" % pid) + + def delpid(self): + """ + Delete the pidfile. + """ + os.remove(self.pidfile) + + def start(self, *args): + """ + Start the daemon. + """ + # Check for a pidfile to see if the daemon already runs + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + message = "pidfile %s already exist. Daemon already running?\n" + sys.stderr.write(message % self.pidfile) + sys.exit(1) + + # Start the daemon + self.daemonize() + self.run(args) + + def stop(self): + """ + Stop the daemon. + """ + # Get the pid from the pidfile + try: + pf = file(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if not pid: + message = "pidfile %s does not exist. Daemon not running?\n" + sys.stderr.write(message % self.pidfile) + return # not an error in a restart + + # Try killing the daemon process + try: + while 1: + os.kill(pid, signal.SIGTERM) + time.sleep(0.1) + except OSError, err: + err = str(err) + if err.find("No such process") > 0: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + print(str(err)) + sys.exit(1) + + def restart(self): + """ + Restart the daemon. + """ + self.stop() + self.start() + + def run(self): + """ + This should be overridden by derived classes. + """ + + +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. + """ + parts = [command] + parts.extend(args) + if TEST or DEBUG: + print("%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) + + if not _check or _detach or _input: + if _input: + return subprocess.Popen(parts, stdin=subprocess.PIPE) + else: + # XXX ok with return None ?? + subprocess.Popen(parts) + else: + try: + devnull = open('/dev/null', 'w') + subprocess.check_call(parts, stdout=devnull, stderr=devnull) + return 0 + except subprocess.CalledProcessError as exc: + if DEBUG: + logger.exception(exc) + if _exitcode: + return exc.returncode + else: + bail("ERROR: Could not run %s: %s" % (exc.cmd, exc.output), + exception=exc) + + +def bail(msg=None, exception=None): + """ + Abnormal exit. + + :param msg: optional error message. + :type msg: str + """ + if msg is not None: + print("%s: %s" % (SCRIPT, msg)) + if exception is not None: + traceback.print_exc() + 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): + print("%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: + print("%s: ERROR: Bad argument %s" % + (SCRIPT, param)) + return None + else: + print("WARNING: unrecognized openvpn flag %s" % flag_name) + return result + except Exception as exc: + print("%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: + print("%s: running openvpn with flags:" % (SCRIPT,)) + print(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) + +## +## DNS +## + + +def get_resolvconf_bin(): + """ + Return the path for either the system resolvconf or the one the + bundle has put there. + """ + if os.path.isfile(RESOLVCONF_SYSTEM_BIN): + return RESOLVCONF_SYSTEM_BIN + + # the bundle option should be removed from the debian package. + if os.path.isfile(RESOLVCONF_LEAP_BIN): + return RESOLVCONF_LEAP_BIN + +RESOLVCONF = get_resolvconf_bin() + + +class NameserverSetter(Daemon): + """ + A daemon that will add leap nameserver inside the tunnel + to the system `resolv.conf` + """ + + def run(self, *args): + """ + Run when daemonized. + """ + if args: + ip_address = args[0] + self.set_dns_nameserver(ip_address) + + def set_dns_nameserver(self, ip_address): + """ + Add the tunnel DNS server to `resolv.conf` + + :param ip_address: the ip to add to `resolv.conf` + :type ip_address: str + """ + if os.path.isfile(RESOLVCONF): + process = run(RESOLVCONF, "-a", "bitmask", input=True) + process.communicate("nameserver %s\n" % ip_address) + else: + bail("ERROR: package openresolv or resolvconf not installed.") + +nameserver_setter = NameserverSetter('/tmp/leap-dns-up.pid') + + +class NameserverRestorer(Daemon): + """ + A daemon that will restore the previous nameservers. + """ + + def run(self): + """ + Run when daemonized. + """ + self.restore_dns_nameserver() + + def restore_dns_nameserver(self): + """ + Remove tunnel DNS server from `resolv.conf` + """ + if os.path.isfile(RESOLVCONF): + run(RESOLVCONF, "-d", "bitmask") + else: + print("%s: ERROR: package openresolv " + "or resolvconf not installed." % + (SCRIPT,)) + +nameserver_restorer = NameserverRestorer('/tmp/leap-dns-down.pid') + + +## +## 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.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.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.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 --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 "--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 ipv4_chain_exists(table): + """ + Check if a given chain exists. + + :param table: the table to check against + :type table: str + :rtype: bool + """ + code = run(IPTABLES, "--list", table, "--numeric", exitcode=True) + return code == 0 + + +def ipv6_chain_exists(table): + """ + Check if a given chain exists. + + :param table: the table to check against + :type table: str + :rtype: bool + """ + code = run(IP6TABLES, "--list", table, "--numeric", exitcode=True) + return code == 0 + + +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" + if not ipv4_chain_exists(BITMASK_CHAIN): + ip4tables("--new-chain", BITMASK_CHAIN) + if not ipv6_chain_exists(BITMASK_CHAIN): + ip6tables("--new-chain", BITMASK_CHAIN) + iptables("--insert", "OUTPUT", "--jump", BITMASK_CHAIN) + + # reject everything + iptables("--insert", BITMASK_CHAIN, "-o", default_device, + "--jump", "REJECT") + + # allow traffic to gateways + for gateway in gateways: + ip4tables("--insert", BITMASK_CHAIN, "--destination", gateway, + "-o", default_device, "--jump", "ACCEPT") + + # allow traffic to IPs on local network + if local_network_ipv4: + ip4tables("--insert", BITMASK_CHAIN, + "--destination", local_network_ipv4, "-o", default_device, + "--jump", "ACCEPT") + if local_network_ipv6: + ip6tables("--insert", BITMASK_CHAIN, + "--destination", local_network_ipv6, "-o", default_device, + "--jump", "ACCEPT") + + # block DNS requests to anyone but the service provider or localhost + # when we actually route ipv6, we will need dns rules for it too + ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp", "--dport", "53", + "--jump", "REJECT") + + for allowed_dns in [NAMESERVER, "127.0.0.1", "127.0.1.1"]: + ip4tables("--insert", BITMASK_CHAIN, "--protocol", "udp", + "--dport", "53", "--destination", allowed_dns, + "--jump", "ACCEPT") + + +def firewall_stop(): + """ + Stop the firewall. + """ + iptables("--delete", "OUTPUT", "--jump", BITMASK_CHAIN) + if ipv4_chain_exists(BITMASK_CHAIN): + ip4tables("--flush", BITMASK_CHAIN) + ip4tables("--delete-chain", BITMASK_CHAIN) + if ipv6_chain_exists(BITMASK_CHAIN): + ip6tables("--flush", BITMASK_CHAIN) + ip6tables("--delete-chain", BITMASK_CHAIN) + +## +## MAIN +## + + +def main(): + if len(sys.argv) >= 3: + command = "_".join(sys.argv[1:3]) + args = sys.argv[3:] + + if command == "openvpn_start": + openvpn_start(args) + + elif command == "openvpn_stop": + openvpn_stop(args) + + elif command == "firewall_start": + try: + firewall_start(args) + nameserver_setter.start(NAMESERVER) + except Exception as ex: + nameserver_restorer.start() + firewall_stop() + bail("ERROR: could not start firewall", ex) + + elif command == "firewall_stop": + try: + firewall_stop() + nameserver_restorer.start() + except Exception as ex: + bail("ERROR: could not stop firewall", ex) + + elif command == "firewall_isup": + if ipv4_chain_exists(BITMASK_CHAIN): + print("%s: INFO: bitmask firewall is up" % (SCRIPT,)) + else: + bail("INFO: bitmask firewall is down") + + else: + bail("ERROR: No such command") + else: + bail("ERROR: No such command") + +if __name__ == "__main__": + if DEBUG: + logger.debug(" ".join(sys.argv)) + main() + print("%s: done" % (SCRIPT,)) + exit(0) diff --git a/pkg/linux/polkit/net.openvpn.gui.leap.policy b/pkg/linux/polkit/se.leap.bitmask.policy index 50f991a3..c66f4701 100644 --- a/pkg/linux/polkit/net.openvpn.gui.leap.policy +++ b/pkg/linux/polkit/se.leap.bitmask.policy @@ -7,17 +7,17 @@ <vendor>LEAP Project</vendor> <vendor_url>http://leap.se/</vendor_url> - <action id="net.openvpn,gui.leap.run-openvpn"> - <description>Runs the openvpn binary</description> - <description xml:lang="es">Ejecuta el binario openvpn</description> - <message>OpenVPN needs that you authenticate to start</message> - <message xml:lang="es">OpenVPN necesita autorizacion para comenzar</message> + <action id="se.leap.bitmask.policy"> + <description>Runs bitmask helper to launch firewall and openvpn</description> + <description xml:lang="es">Ejecuta el asistente de bitmask para lanzar el firewall y openvpn</description> + <message>Bitmask needs that you authenticate to start</message> + <message xml:lang="es">Bitmask necesita autorizacion para comenzar</message> <icon_name>package-x-generic</icon_name> <defaults> <allow_any>yes</allow_any> <allow_inactive>yes</allow_inactive> <allow_active>yes</allow_active> </defaults> - <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/openvpn</annotate> + <annotate key="org.freedesktop.policykit.exec.path">/usr/sbin/bitmask-root</annotate> </action> </policyconfig> diff --git a/pkg/linux/update-resolv-conf b/pkg/linux/update-resolv-conf new file mode 100755 index 00000000..76c69413 --- /dev/null +++ b/pkg/linux/update-resolv-conf @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Parses DHCP options from openvpn to update resolv.conf +# To use set as 'up' and 'down' script in your openvpn *.conf: +# up /etc/leap/update-resolv-conf +# down /etc/leap/update-resolv-conf +# +# Used snippets of resolvconf script by Thomas Hood and Chris Hanson. +# Licensed under the GNU GPL. See /usr/share/common-licenses/GPL. +# +# Example envs set from openvpn: +# +# foreign_option_1='dhcp-option DNS 193.43.27.132' +# foreign_option_2='dhcp-option DNS 193.43.27.133' +# foreign_option_3='dhcp-option DOMAIN be.bnc.ch' +# + +[ -x /sbin/resolvconf ] || exit 0 +[ "$script_type" ] || exit 0 +[ "$dev" ] || exit 0 + +split_into_parts() +{ + part1="$1" + part2="$2" + part3="$3" +} + +case "$script_type" in + up) + NMSRVRS="" + SRCHS="" + for optionvarname in ${!foreign_option_*} ; do + option="${!optionvarname}" + echo "$option" + split_into_parts $option + if [ "$part1" = "dhcp-option" ] ; then + if [ "$part2" = "DNS" ] ; then + NMSRVRS="${NMSRVRS:+$NMSRVRS }$part3" + elif [ "$part2" = "DOMAIN" ] ; then + SRCHS="${SRCHS:+$SRCHS }$part3" + fi + fi + done + R="" + [ "$SRCHS" ] && R="search $SRCHS +" + for NS in $NMSRVRS ; do + R="${R}nameserver $NS +" + done + echo -n "$R" | /sbin/resolvconf -a "${dev}.openvpn" + ;; + down) + /sbin/resolvconf -d "${dev}.openvpn" + ;; +esac + diff --git a/src/leap/bitmask/backend.py b/src/leap/bitmask/backend.py index 8dd9f799..a2df465d 100644 --- a/src/leap/bitmask/backend.py +++ b/src/leap/bitmask/backend.py @@ -17,12 +17,15 @@ """ Backend for everything """ +import commands import logging import os +import time from functools import partial from Queue import Queue, Empty +from twisted.internet import reactor from twisted.internet import threads, defer from twisted.internet.task import LoopingCall from twisted.python import log @@ -32,6 +35,7 @@ import zope.interface from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.crypto.srpauth import SRPAuth from leap.bitmask.crypto.srpregister import SRPRegister +from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.provider import get_provider_path from leap.bitmask.provider.providerbootstrapper import ProviderBootstrapper from leap.bitmask.services.eip import eipconfig @@ -374,6 +378,34 @@ class EIP(object): Stop the service. """ self._vpn.terminate(shutdown) + if IS_LINUX: + self._wait_for_firewall_down() + + def _wait_for_firewall_down(self): + """ + Wait for the firewall to come down. + """ + # Due to how we delay the resolvconf action in linux. + # XXX this *has* to wait for a reasonable lapse, since we have some + # delay in vpn.terminate. + # For a better solution it should be signaled from backend that + # everything is clear to proceed, or a timeout happened. + MAX_FW_WAIT_RETRIES = 25 + FW_WAIT_STEP = 0.5 + + retry = 0 + + fw_up_cmd = "pkexec /usr/sbin/bitmask-root firewall isup" + fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256 + + while retry < MAX_FW_WAIT_RETRIES: + if fw_is_down(): + return + else: + time.sleep(FW_WAIT_STEP) + retry += 1 + logger.warning("After waiting, firewall is not down... " + "You might experience lack of connectivity") def terminate(self): """ @@ -927,9 +959,17 @@ class Backend(object): """ Stops the looping call and tries to cancel all the defers. """ + reactor.callLater(2, self._stop) + + def _stop(self): + """ + Delayed stopping of worker. Called from `stop`. + """ log.msg("Stopping worker...") if self._lc.running: self._lc.stop() + else: + logger.warning("Looping call is not running, cannot stop") while len(self._ongoing_defers) > 0: d = self._ongoing_defers.pop() d.cancel() @@ -1111,6 +1151,9 @@ class Backend(object): def stop_eip(self, shutdown=False): """ Stop the EIP service. + + :param shutdown: + :type shutdown: bool """ self._call_queue.put(("eip", "stop", None, shutdown)) diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 6e270de1..e3848c46 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -19,6 +19,7 @@ Main window for Bitmask. """ import logging import socket +import time from threading import Condition from datetime import datetime @@ -2109,12 +2110,25 @@ class MainWindow(QtGui.QMainWindow): logger.debug('Terminating vpn') self._backend.stop_eip(shutdown=True) + # We need to give some time to the ongoing signals for shutdown + # to come into action. This needs to be solved using + # back-communication from backend. + QtCore.QTimer.singleShot(3000, self._shutdown) + + def _shutdown(self): + """ + Actually shutdown. + """ self._cancel_ongoing_defers() # TODO missing any more cancels? logger.debug('Cleaning pidfiles') self._cleanup_pidfiles() + if self._quit_callback: + self._quit_callback() + + logger.debug('Bye.') def quit(self): """ @@ -2150,8 +2164,3 @@ class MainWindow(QtGui.QMainWindow): self._logger_window.close() self.close() - - if self._quit_callback: - self._quit_callback() - - logger.debug('Bye.') diff --git a/src/leap/bitmask/gui/twisted_main.py b/src/leap/bitmask/gui/twisted_main.py index f39d0bbe..dfd69033 100644 --- a/src/leap/bitmask/gui/twisted_main.py +++ b/src/leap/bitmask/gui/twisted_main.py @@ -26,7 +26,6 @@ logger = logging.getLogger(__name__) def stop(): - logger.debug("Really stoping all the things...") QtCore.QCoreApplication.sendPostedEvents() QtCore.QCoreApplication.flush() try: diff --git a/src/leap/bitmask/platform_init/initializers.py b/src/leap/bitmask/platform_init/initializers.py index d93efbc6..f2710c58 100644 --- a/src/leap/bitmask/platform_init/initializers.py +++ b/src/leap/bitmask/platform_init/initializers.py @@ -366,15 +366,8 @@ def _linux_install_missing_scripts(badexec, notfound): fd, tempscript = tempfile.mkstemp(prefix="leap_installer-") polfd, pol_tempfile = tempfile.mkstemp(prefix="leap_installer-") try: - path = launcher.OPENVPN_BIN_PATH - policy_contents = privilege_policies.get_policy_contents(path) - - with os.fdopen(polfd, 'w') as f: - f.write(policy_contents) - pkexec = first(launcher.maybe_pkexec()) - scriptlines = launcher.cmd_for_missing_scripts(installer_path, - pol_tempfile) + scriptlines = launcher.cmd_for_missing_scripts(installer_path) with os.fdopen(fd, 'w') as f: f.write(scriptlines) diff --git a/src/leap/bitmask/services/eip/linuxvpnlauncher.py b/src/leap/bitmask/services/eip/linuxvpnlauncher.py index 8747daa6..1f0813e0 100644 --- a/src/leap/bitmask/services/eip/linuxvpnlauncher.py +++ b/src/leap/bitmask/services/eip/linuxvpnlauncher.py @@ -25,7 +25,6 @@ import sys import time from leap.bitmask.config import flags -from leap.bitmask.util import privilege_policies from leap.bitmask.util.privilege_policies import LinuxPolicyChecker from leap.common.files import which from leap.bitmask.services.eip.vpnlauncher import VPNLauncher @@ -36,6 +35,8 @@ from leap.bitmask.util import first logger = logging.getLogger(__name__) +COM = commands + class EIPNoPolkitAuthAgentAvailable(VPNLauncherException): pass @@ -64,10 +65,10 @@ def _is_auth_agent_running(): """ # the [x] thing is to avoid grep match itself polkit_options = [ - 'ps aux | grep polkit-[g]nome-authentication-agent-1', - 'ps aux | grep polkit-[k]de-authentication-agent-1', - 'ps aux | grep polkit-[m]ate-authentication-agent-1', - 'ps aux | grep [l]xpolkit' + 'ps aux | grep "polkit-[g]nome-authentication-agent-1"', + 'ps aux | grep "polkit-[k]de-authentication-agent-1"', + 'ps aux | grep "polkit-[m]ate-authentication-agent-1"', + 'ps aux | grep "[l]xpolkit"' ] is_running = [commands.getoutput(cmd) for cmd in polkit_options] return any(is_running) @@ -85,35 +86,39 @@ def _try_to_launch_agent(): # will do "sh -c 'foo'", so if we do not quoute it we'll end # up with a invocation to the python interpreter. And that # is bad. + logger.debug("Trying to launch polkit agent") subprocess.call(["python -m leap.bitmask.util.polkit_agent"], shell=True, env=env) except Exception as exc: logger.exception(exc) +SYSTEM_CONFIG = "/etc/leap" +leapfile = lambda f: "%s/%s" % (SYSTEM_CONFIG, f) + + class LinuxVPNLauncher(VPNLauncher): PKEXEC_BIN = 'pkexec' - OPENVPN_BIN = 'openvpn' - OPENVPN_BIN_PATH = os.path.join( - get_path_prefix(), "..", "apps", "eip", OPENVPN_BIN) - - SYSTEM_CONFIG = "/etc/leap" - UP_DOWN_FILE = "resolv-update" - UP_DOWN_PATH = "%s/%s" % (SYSTEM_CONFIG, UP_DOWN_FILE) + BITMASK_ROOT = "/usr/sbin/bitmask-root" # We assume this is there by our openvpn dependency, and # we will put it there on the bundle too. - # TODO adapt to the bundle path. - OPENVPN_DOWN_ROOT_BASE = "/usr/lib/openvpn/" - OPENVPN_DOWN_ROOT_FILE = "openvpn-plugin-down-root.so" - OPENVPN_DOWN_ROOT_PATH = "%s/%s" % ( - OPENVPN_DOWN_ROOT_BASE, - OPENVPN_DOWN_ROOT_FILE) - - UP_SCRIPT = DOWN_SCRIPT = UP_DOWN_PATH - UPDOWN_FILES = (UP_DOWN_PATH,) + if flags.STANDALONE: + OPENVPN_BIN_PATH = "/usr/sbin/leap-openvpn" + else: + OPENVPN_BIN_PATH = "/usr/sbin/openvpn" + POLKIT_PATH = LinuxPolicyChecker.get_polkit_path() - OTHER_FILES = (POLKIT_PATH, ) + + if flags.STANDALONE: + RESOLVCONF_BIN_PATH = "/usr/local/sbin/leap-resolvconf" + else: + # this only will work with debian/ubuntu distros. + RESOLVCONF_BIN_PATH = "/sbin/resolvconf" + + # XXX openvpn binary TOO + OTHER_FILES = (POLKIT_PATH, BITMASK_ROOT, OPENVPN_BIN_PATH, + RESOLVCONF_BIN_PATH) @classmethod def maybe_pkexec(kls): @@ -131,7 +136,7 @@ class LinuxVPNLauncher(VPNLauncher): if _is_pkexec_in_system(): if not _is_auth_agent_running(): _try_to_launch_agent() - time.sleep(0.5) + time.sleep(2) if _is_auth_agent_running(): pkexec_possibilities = which(kls.PKEXEC_BIN) leap_assert(len(pkexec_possibilities) > 0, @@ -146,28 +151,6 @@ class LinuxVPNLauncher(VPNLauncher): raise EIPNoPkexecAvailable() @classmethod - def missing_other_files(kls): - """ - 'Extend' the VPNLauncher's missing_other_files to check if the polkit - files is outdated, in the case of an standalone bundle. - If the polkit file that is in OTHER_FILES exists but is not up to date, - it is added to the missing list. - - :returns: a list of missing files - :rtype: list of str - """ - # we use `super` in order to send the class to use - missing = super(LinuxVPNLauncher, kls).missing_other_files() - - if flags.STANDALONE: - polkit_file = LinuxPolicyChecker.get_polkit_path() - if polkit_file not in missing: - if privilege_policies.is_policy_outdated(kls.OPENVPN_BIN_PATH): - missing.append(polkit_file) - - return missing - - @classmethod def get_vpn_command(kls, eipconfig, providerconfig, socket_host, socket_port="unix", openvpn_verb=1): """ @@ -198,6 +181,10 @@ class LinuxVPNLauncher(VPNLauncher): command = super(LinuxVPNLauncher, kls).get_vpn_command( eipconfig, providerconfig, socket_host, socket_port, openvpn_verb) + command.insert(0, kls.BITMASK_ROOT) + command.insert(1, "openvpn") + command.insert(2, "start") + pkexec = kls.maybe_pkexec() if pkexec: command.insert(0, first(pkexec)) @@ -205,26 +192,44 @@ class LinuxVPNLauncher(VPNLauncher): return command @classmethod - def cmd_for_missing_scripts(kls, frompath, pol_file): + def cmd_for_missing_scripts(kls, frompath): """ Returns a sh script that can copy the missing files. - :param frompath: The path where the up/down scripts live + :param frompath: The path where the helper files live :type frompath: str - :param pol_file: The path where the dynamically generated - policy file lives - :type pol_file: str :rtype: str """ - to = kls.SYSTEM_CONFIG + # no system config for now + # sys_config = kls.SYSTEM_CONFIG + (polkit_file, openvpn_bin_file, + bitmask_root_file, resolvconf_bin_file) = map( + lambda p: os.path.split(p)[-1], + (kls.POLKIT_PATH, kls.OPENVPN_BIN_PATH, + kls.BITMASK_ROOT, kls.RESOLVCONF_BIN_PATH)) cmd = '#!/bin/sh\n' - cmd += 'mkdir -p "%s"\n' % (to, ) - cmd += 'cp "%s/%s" "%s"\n' % (frompath, kls.UP_DOWN_FILE, to) - cmd += 'cp "%s" "%s"\n' % (pol_file, kls.POLKIT_PATH) + cmd += 'mkdir -p /usr/local/sbin\n' + + cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, polkit_file), + kls.POLKIT_PATH) cmd += 'chmod 644 "%s"\n' % (kls.POLKIT_PATH, ) + cmd += 'cp "%s" "%s"\n' % (os.path.join(frompath, bitmask_root_file), + kls.BITMASK_ROOT) + cmd += 'chmod 744 "%s"\n' % (kls.BITMASK_ROOT, ) + + if flags.STANDALONE: + cmd += 'cp "%s" "%s"\n' % ( + os.path.join(frompath, openvpn_bin_file), + kls.OPENVPN_BIN_PATH) + cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) + + cmd += 'cp "%s" "%s"\n' % ( + os.path.join(frompath, resolvconf_bin_file), + kls.RESOLVCONF_BIN_PATH) + cmd += 'chmod 744 "%s"\n' % (kls.POLKIT_PATH, ) return cmd @classmethod diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py index 99cae7f9..dcb48e8a 100644 --- a/src/leap/bitmask/services/eip/vpnlauncher.py +++ b/src/leap/bitmask/services/eip/vpnlauncher.py @@ -25,14 +25,12 @@ import stat from abc import ABCMeta, abstractmethod from functools import partial -from leap.bitmask.config import flags from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.platform_init import IS_LINUX from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector -from leap.bitmask.util import first -from leap.bitmask.util import get_path_prefix from leap.common.check import leap_assert, leap_assert_type -from leap.common.files import which + logger = logging.getLogger(__name__) @@ -107,10 +105,43 @@ class VPNLauncher(object): @classmethod @abstractmethod + def get_gateways(kls, eipconfig, providerconfig): + """ + Return the selected gateways for a given provider, looking at the EIP + config file. + + :param eipconfig: eip configuration object + :type eipconfig: EIPConfig + + :param providerconfig: provider specific configuration + :type providerconfig: ProviderConfig + + :rtype: list + """ + gateways = [] + leap_settings = LeapSettings() + domain = providerconfig.get_domain() + gateway_conf = leap_settings.get_selected_gateway(domain) + + if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: + gateway_selector = VPNGatewaySelector(eipconfig) + gateways = gateway_selector.get_gateways() + else: + gateways = [gateway_conf] + + if not gateways: + logger.error('No gateway was found!') + raise VPNLauncherException('No gateway was found!') + + logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) + return gateways + + @classmethod + @abstractmethod def get_vpn_command(kls, eipconfig, providerconfig, socket_host, socket_port, openvpn_verb=1): """ - Returns the platform dependant vpn launching command + Return the platform-dependant vpn command for launching openvpn. Might raise: OpenVPNNotFoundException, @@ -134,16 +165,19 @@ class VPNLauncher(object): leap_assert_type(eipconfig, EIPConfig) leap_assert_type(providerconfig, ProviderConfig) - kwargs = {} - if flags.STANDALONE: - kwargs['path_extension'] = os.path.join( - get_path_prefix(), "..", "apps", "eip") - - openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) - if len(openvpn_possibilities) == 0: + # XXX this still has to be changed on osx and windows accordingly + #kwargs = {} + #openvpn_possibilities = which(kls.OPENVPN_BIN, **kwargs) + #if not openvpn_possibilities: + #raise OpenVPNNotFoundException() + #openvpn = first(openvpn_possibilities) + # ----------------------------------------- + if not os.path.isfile(kls.OPENVPN_BIN_PATH): + logger.warning("Could not find openvpn bin in path %s" % ( + kls.OPENVPN_BIN_PATH)) raise OpenVPNNotFoundException() - openvpn = first(openvpn_possibilities) + openvpn = kls.OPENVPN_BIN_PATH args = [] args += [ @@ -154,22 +188,7 @@ class VPNLauncher(object): if openvpn_verb is not None: args += ['--verb', '%d' % (openvpn_verb,)] - gateways = [] - leap_settings = LeapSettings() - domain = providerconfig.get_domain() - gateway_conf = leap_settings.get_selected_gateway(domain) - - if gateway_conf == leap_settings.GATEWAY_AUTOMATIC: - gateway_selector = VPNGatewaySelector(eipconfig) - gateways = gateway_selector.get_gateways() - else: - gateways = [gateway_conf] - - if not gateways: - logger.error('No gateway was found!') - raise VPNLauncherException('No gateway was found!') - - logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) + gateways = kls.get_gateways(eipconfig, providerconfig) for gw in gateways: args += ['--remote', gw, '1194', 'udp'] @@ -177,11 +196,6 @@ class VPNLauncher(object): args += [ '--client', '--dev', 'tun', - ############################################################## - # persist-tun makes ping-restart fail because it leaves a - # broken routing table - ############################################################## - # '--persist-tun', '--persist-key', '--tls-client', '--remote-cert-tls', @@ -194,15 +208,6 @@ class VPNLauncher(object): user = getpass.getuser() - ############################################################## - # The down-root plugin fails in some situations, so we don't - # drop privs for the time being - ############################################################## - # args += [ - # '--user', user, - # '--group', grp.getgrgid(os.getgroups()[-1]).gr_name - # ] - if socket_port == "unix": # that's always the case for linux args += [ '--management-client-user', user @@ -226,20 +231,6 @@ class VPNLauncher(object): '--down', '\"%s\"' % (kls.DOWN_SCRIPT,) ] - ########################################################### - # For the time being we are disabling the usage of the - # down-root plugin, because it doesn't quite work as - # expected (i.e. it doesn't run route -del as root - # when finishing, so it fails to properly - # restart/quit) - ########################################################### - # if _has_updown_scripts(kls.OPENVPN_DOWN_PLUGIN): - # args += [ - # '--plugin', kls.OPENVPN_DOWN_ROOT, - # '\'%s\'' % kls.DOWN_SCRIPT # for OSX - # '\'script_type=down %s\'' % kls.DOWN_SCRIPT # for Linux - # ] - args += [ '--cert', eipconfig.get_client_cert_path(providerconfig), '--key', eipconfig.get_client_cert_path(providerconfig), @@ -271,13 +262,18 @@ class VPNLauncher(object): :rtype: list """ - leap_assert(kls.UPDOWN_FILES is not None, - "Need to define UPDOWN_FILES for this particular " - "launcher before calling this method") - file_exist = partial(_has_updown_scripts, warn=False) - zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) - missing = filter(lambda (path, exists): exists is False, zipped) - return [path for path, exists in missing] + # FIXME + # XXX remove method when we ditch UPDOWN in osx and win too + if IS_LINUX: + return [] + else: + leap_assert(kls.UPDOWN_FILES is not None, + "Need to define UPDOWN_FILES for this particular " + "launcher before calling this method") + file_exist = partial(_has_updown_scripts, warn=False) + zipped = zip(kls.UPDOWN_FILES, map(file_exist, kls.UPDOWN_FILES)) + missing = filter(lambda (path, exists): exists is False, zipped) + return [path for path, exists in missing] @classmethod def missing_other_files(kls): diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py index c7b8071c..1559ea8b 100644 --- a/src/leap/bitmask/services/eip/vpnprocess.py +++ b/src/leap/bitmask/services/eip/vpnprocess.py @@ -21,6 +21,7 @@ import logging import os import shutil import socket +import subprocess import sys from itertools import chain, repeat @@ -36,10 +37,11 @@ except ImportError: from leap.bitmask.config import flags from leap.bitmask.config.providerconfig import ProviderConfig from leap.bitmask.services.eip import get_vpn_launcher +from leap.bitmask.services.eip import linuxvpnlauncher from leap.bitmask.services.eip.eipconfig import EIPConfig from leap.bitmask.services.eip.udstelnet import UDSTelnet from leap.bitmask.util import first -from leap.bitmask.platform_init import IS_MAC +from leap.bitmask.platform_init import IS_MAC, IS_LINUX from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) @@ -66,9 +68,8 @@ class VPNObserver(object): 'Network is unreachable (code=101)',), 'PROCESS_RESTART_TLS': ( "SIGUSR1[soft,tls-error]",), - # Let ping-restart work as it should - # 'PROCESS_RESTART_PING': ( - # "SIGUSR1[soft,ping-restart]",), + 'PROCESS_RESTART_PING': ( + "SIGTERM[soft,ping-restart]",), 'INITIALIZATION_COMPLETED': ( "Initialization Sequence Completed",), } @@ -159,6 +160,8 @@ class VPN(object): self._signaler = kwargs['signaler'] self._openvpn_verb = flags.OPENVPN_VERBOSITY + self._user_stopped = False + def start(self, *args, **kwargs): """ Starts the openvpn subprocess. @@ -170,6 +173,7 @@ class VPN(object): :type kwargs: dict """ logger.debug('VPN: start') + self._user_stopped = False self._stop_pollers() kwargs['openvpn_verb'] = self._openvpn_verb kwargs['signaler'] = self._signaler @@ -181,6 +185,15 @@ class VPN(object): logger.info("Another vpn process is running. Will try to stop it.") vpnproc.stop_if_already_running() + # we try to bring the firewall up + if IS_LINUX: + gateways = vpnproc.getGateways() + firewall_up = self._launch_firewall(gateways) + if not firewall_up: + logger.error("Could not bring firewall up, " + "aborting openvpn launch.") + return + cmd = vpnproc.getCommand() env = os.environ for key, val in vpnproc.vpn_env.items(): @@ -198,9 +211,37 @@ class VPN(object): self._pollers.extend(poll_list) self._start_pollers() + def _launch_firewall(self, gateways): + """ + Launch the firewall using the privileged wrapper. + + :param gateways: + :type gateways: list + + :returns: True if the exitcode of calling the root helper in a + subprocess is 0. + :rtype: bool + """ + # XXX could check for wrapper existence, check it's root owned etc. + # XXX could check that the iptables rules are in place. + + BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT + exitCode = subprocess.call(["pkexec", + BM_ROOT, "firewall", "start"] + gateways) + return True if exitCode is 0 else False + + def _tear_down_firewall(self): + """ + Tear the firewall down using the privileged wrapper. + """ + BM_ROOT = linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT + exitCode = subprocess.call(["pkexec", + BM_ROOT, "firewall", "stop"]) + return True if exitCode is 0 else False + def _kill_if_left_alive(self, tries=0): """ - Check if the process is still alive, and sends a + Check if the process is still alive, and send a SIGKILL after a timeout period. :param tries: counter of tries, used in recursion @@ -210,6 +251,15 @@ class VPN(object): while tries < self.TERMINATE_MAXTRIES: if self._vpnproc.transport.pid is None: logger.debug("Process has been happily terminated.") + + # we try to tear the firewall down + if IS_LINUX and self._user_stopped: + firewall_down = self._tear_down_firewall() + if firewall_down: + logger.debug("Firewall down") + else: + logger.warning("Could not tear firewall down") + return else: logger.debug("Process did not die, waiting...") @@ -246,6 +296,10 @@ class VPN(object): from twisted.internet import reactor self._stop_pollers() + # We assume that the only valid shutodowns are initiated + # by an user action. + self._user_stopped = shutdown + # First we try to be polite and send a SIGTERM... if self._vpnproc: self._sentterm = True @@ -253,12 +307,17 @@ class VPN(object): # ...but we also trigger a countdown to be unpolite # if strictly needed. - - # XXX Watch out! This will fail NOW since we are running - # openvpn as root as a workaround for some connection issues. reactor.callLater( self.TERMINATE_WAIT, self._kill_if_left_alive) + if shutdown: + if IS_LINUX and self._user_stopped: + firewall_down = self._tear_down_firewall() + if firewall_down: + logger.debug("Firewall down") + else: + logger.warning("Could not tear firewall down") + def _start_pollers(self): """ Iterate through the registered observers @@ -830,9 +889,20 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager): if not isinstance(c, str): command[i] = c.encode(encoding) - logger.debug("Running VPN with command: {0}".format(command)) + logger.debug("Running VPN with command: ") + logger.debug("{0}".format(" ".join(command))) return command + def getGateways(self): + """ + Get the gateways from the appropiate launcher. + + :rtype: list + """ + gateways = self._launcher.get_gateways( + self._eipconfig, self._providerconfig) + return gateways + # shutdown def killProcess(self): diff --git a/src/leap/bitmask/util/privilege_policies.py b/src/leap/bitmask/util/privilege_policies.py index 72442553..9d1e2c9a 100644 --- a/src/leap/bitmask/util/privilege_policies.py +++ b/src/leap/bitmask/util/privilege_policies.py @@ -27,35 +27,6 @@ from abc import ABCMeta, abstractmethod logger = logging.getLogger(__name__) -POLICY_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE policyconfig PUBLIC - "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" - "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> -<policyconfig> - - <vendor>LEAP Project</vendor> - <vendor_url>https://leap.se/</vendor_url> - - <action id="net.openvpn.gui.leap.run-openvpn"> - <description>Runs the openvpn binary</description> - <description xml:lang="es">Ejecuta el binario openvpn</description> - <message>OpenVPN needs that you authenticate to start</message> - <message xml:lang="es"> - OpenVPN necesita autorizacion para comenzar - </message> - <icon_name>package-x-generic</icon_name> - <defaults> - <allow_any>yes</allow_any> - <allow_inactive>yes</allow_inactive> - <allow_active>yes</allow_active> - </defaults> - <annotate key="org.freedesktop.policykit.exec.path">{path}</annotate> - <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> - </action> -</policyconfig> -""" - - def is_missing_policy_permissions(): """ Returns True if we do not have implemented a policy checker for this @@ -76,36 +47,6 @@ def is_missing_policy_permissions(): return policy_checker().is_missing_policy_permissions() -def get_policy_contents(openvpn_path): - """ - Returns the contents that the policy file should have. - - :param openvpn_path: the openvpn path to use in the polkit file - :type openvpn_path: str - :rtype: str - """ - return POLICY_TEMPLATE.format(path=openvpn_path) - - -def is_policy_outdated(path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - _system = platform.system() - platform_checker = _system + "PolicyChecker" - policy_checker = globals().get(platform_checker, None) - if policy_checker is None: - logger.debug("we could not find a policy checker implementation " - "for %s" % (_system,)) - return False - return policy_checker().is_outdated(path) - - class PolicyChecker: """ Abstract PolicyChecker class @@ -129,7 +70,7 @@ class LinuxPolicyChecker(PolicyChecker): PolicyChecker for Linux """ LINUX_POLKIT_FILE = ("/usr/share/polkit-1/actions/" - "net.openvpn.gui.leap.policy") + "se.leap.bitmask.policy") @classmethod def get_polkit_path(self): @@ -141,6 +82,8 @@ class LinuxPolicyChecker(PolicyChecker): return self.LINUX_POLKIT_FILE def is_missing_policy_permissions(self): + # FIXME this name is quite confusing, it does not have anything to do with + # file permissions. """ Returns True if we could not find the appropriate policykit file in place @@ -148,22 +91,3 @@ class LinuxPolicyChecker(PolicyChecker): :rtype: bool """ return not os.path.isfile(self.LINUX_POLKIT_FILE) - - def is_outdated(self, path): - """ - Returns if the existing polkit file is outdated, comparing if the path - is correct. - - :param path: the path that should have the polkit file. - :type path: str. - :rtype: bool - """ - polkit = None - try: - with open(self.LINUX_POLKIT_FILE) as f: - polkit = f.read() - except IOError, e: - logger.error("Error reading polkit file(%s): %r" % ( - self.LINUX_POLKIT_FILE, e)) - - return get_policy_contents(path) != polkit |