summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/bug-5592_harcode_openvpn_path_for_bundle1
-rw-r--r--changes/bug-5595-increase-polkit-wait-time1
-rw-r--r--changes/bug-avoid-soft-restart1
-rw-r--r--changes/feature_linux-firewall1
-rw-r--r--pkg/linux/README4
-rw-r--r--pkg/linux/README.rst10
-rwxr-xr-xpkg/linux/bitmask-root829
-rw-r--r--pkg/linux/polkit/se.leap.bitmask.policy (renamed from pkg/linux/polkit/net.openvpn.gui.leap.policy)12
-rwxr-xr-xpkg/linux/update-resolv-conf58
-rw-r--r--src/leap/bitmask/backend.py43
-rw-r--r--src/leap/bitmask/gui/mainwindow.py19
-rw-r--r--src/leap/bitmask/gui/twisted_main.py1
-rw-r--r--src/leap/bitmask/platform_init/initializers.py9
-rw-r--r--src/leap/bitmask/services/eip/linuxvpnlauncher.py113
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py124
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py88
-rw-r--r--src/leap/bitmask/util/privilege_policies.py82
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