summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/services
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2016-01-29 13:18:36 -0800
committerKali Kaneko <kali@leap.se>2016-04-18 16:15:21 -0400
commite9e9abc4ec26be29b3a6b09e6a0b67786269183b (patch)
tree698ffd51104e6d391957ba25e31e88a2bbced38a /src/leap/bitmask/services
parent0bd65c1d3e6c5ee1d861122ec2cd617ad026de43 (diff)
[feature] privileged bitmask helper
This is still quite untested, and a bit hacky, but the main idea behind let us have a daemonized bitmask helper, that should be installed by the Bitmask installer. Its responsibilities are to launch the vpn process as a privileged user, and start/stop the firewall.
Diffstat (limited to 'src/leap/bitmask/services')
-rw-r--r--src/leap/bitmask/services/eip/darwinvpnlauncher.py63
-rw-r--r--src/leap/bitmask/services/eip/vpnlauncher.py8
-rw-r--r--src/leap/bitmask/services/eip/vpnprocess.py188
3 files changed, 186 insertions, 73 deletions
diff --git a/src/leap/bitmask/services/eip/darwinvpnlauncher.py b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
index 17fc11c2..42d9576b 100644
--- a/src/leap/bitmask/services/eip/darwinvpnlauncher.py
+++ b/src/leap/bitmask/services/eip/darwinvpnlauncher.py
@@ -20,6 +20,7 @@ Darwin VPN launcher implementation.
import commands
import getpass
import os
+import socket
import sys
from leap.bitmask.logs.utils import get_logger
@@ -34,17 +35,47 @@ class EIPNoTunKextLoaded(VPNLauncherException):
pass
+class DarwinHelperCommand(object):
+
+ SOCKET_ADDR = '/tmp/bitmask-helper.socket'
+
+ def __init__(self):
+ pass
+
+ def _connect(self):
+ self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ self._sock.connect(self.SOCKET_ADDR)
+ except socket.error, msg:
+ raise RuntimeError(msg)
+
+ def send(self, cmd, args=''):
+ # TODO check cmd is in allowed list
+ self._connect()
+ sock = self._sock
+ data = ""
+
+ command = cmd + ' ' + args + '/CMD'
+
+ try:
+ sock.sendall(command)
+ while '\n' not in data:
+ data += sock.recv(32)
+ finally:
+ sock.close()
+
+ return data
+
+
class DarwinVPNLauncher(VPNLauncher):
"""
VPN launcher for the Darwin Platform
"""
- COCOASUDO = "cocoasudo"
- # XXX need the good old magic translate for these strings
- # (look for magic in 0.2.0 release)
- SUDO_MSG = ("Bitmask needs administrative privileges to run "
- "Encrypted Internet.")
- INSTALL_MSG = ("\"Bitmask needs administrative privileges to install "
- "missing scripts and fix permissions.\"")
+ UP_SCRIPT = None
+ DOWN_SCRIPT = None
+
+ # TODO -- move this to bitmask-helper
+
# Hardcode the installation path for OSX for security, openvpn is
# run as root
@@ -56,14 +87,9 @@ class DarwinVPNLauncher(VPNLauncher):
INSTALL_PATH_ESCAPED,)
OPENVPN_BIN_PATH = "%s/Contents/Resources/%s" % (INSTALL_PATH,
OPENVPN_BIN)
-
- UP_SCRIPT = "%s/client.up.sh" % (OPENVPN_PATH,)
- DOWN_SCRIPT = "%s/client.down.sh" % (OPENVPN_PATH,)
- OPENVPN_DOWN_PLUGIN = '%s/openvpn-down-root.so' % (OPENVPN_PATH,)
-
- UPDOWN_FILES = (UP_SCRIPT, DOWN_SCRIPT, OPENVPN_DOWN_PLUGIN)
OTHER_FILES = []
+ # TODO deprecate ------------------------------------------------
@classmethod
def cmd_for_missing_scripts(kls, frompath):
"""
@@ -87,7 +113,7 @@ class DarwinVPNLauncher(VPNLauncher):
:returns: True if kext is loaded, False otherwise.
:rtype: bool
"""
- return bool(commands.getoutput('kextstat | grep "leap.tun"'))
+ return bool(commands.getoutput('kextstat | grep "foo.tun"'))
@classmethod
def _get_icon_path(kls):
@@ -101,6 +127,8 @@ class DarwinVPNLauncher(VPNLauncher):
return os.path.join(resources_path, "bitmask.tiff")
+
+ # TODO deprecate ---------------------------------------------------------
@classmethod
def get_cocoasudo_ovpn_cmd(kls):
"""
@@ -120,6 +148,7 @@ class DarwinVPNLauncher(VPNLauncher):
return kls.COCOASUDO, args
+ # TODO deprecate ---------------------------------------------------------
@classmethod
def get_cocoasudo_installmissing_cmd(kls):
"""
@@ -171,12 +200,6 @@ class DarwinVPNLauncher(VPNLauncher):
# we use `super` in order to send the class to use
command = super(DarwinVPNLauncher, kls).get_vpn_command(
eipconfig, providerconfig, socket_host, socket_port, openvpn_verb)
-
- cocoa, cargs = kls.get_cocoasudo_ovpn_cmd()
- cargs.extend(command)
- command = cargs
- command.insert(0, cocoa)
-
command.extend(['--setenv', "LEAPUSER", getpass.getuser()])
return command
diff --git a/src/leap/bitmask/services/eip/vpnlauncher.py b/src/leap/bitmask/services/eip/vpnlauncher.py
index c48f857c..16dfd9cf 100644
--- a/src/leap/bitmask/services/eip/vpnlauncher.py
+++ b/src/leap/bitmask/services/eip/vpnlauncher.py
@@ -29,7 +29,7 @@ from leap.bitmask.config import flags
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.backend.settings import Settings, GATEWAY_AUTOMATIC
from leap.bitmask.config.providerconfig import ProviderConfig
-from leap.bitmask.platform_init import IS_LINUX
+from leap.bitmask.platform_init import IS_LINUX, IS_MAC
from leap.bitmask.services.eip.eipconfig import EIPConfig, VPNGatewaySelector
from leap.bitmask.util import force_eval
from leap.common.check import leap_assert, leap_assert_type
@@ -286,8 +286,8 @@ class VPNLauncher(object):
:rtype: list
"""
# FIXME
- # XXX remove method when we ditch UPDOWN in osx and win too
- if IS_LINUX:
+ # XXX remove method when we ditch UPDOWN in win too
+ if IS_LINUX or IS_MAC:
return []
else:
leap_assert(kls.UPDOWN_FILES is not None,
@@ -308,7 +308,7 @@ class VPNLauncher(object):
"""
leap_assert(kls.OTHER_FILES is not None,
"Need to define OTHER_FILES for this particular "
- "auncher before calling this method")
+ "launcher before calling this method")
other = force_eval(kls.OTHER_FILES)
file_exist = partial(_has_other_files, warn=False)
diff --git a/src/leap/bitmask/services/eip/vpnprocess.py b/src/leap/bitmask/services/eip/vpnprocess.py
index 586b50f5..6d18a599 100644
--- a/src/leap/bitmask/services/eip/vpnprocess.py
+++ b/src/leap/bitmask/services/eip/vpnprocess.py
@@ -23,6 +23,7 @@ import shutil
import socket
import subprocess
import sys
+import time
from itertools import chain, repeat
@@ -41,6 +42,7 @@ from leap.bitmask.config.providerconfig import ProviderConfig
from leap.bitmask.logs.utils import get_logger
from leap.bitmask.services.eip import get_vpn_launcher
from leap.bitmask.services.eip import linuxvpnlauncher
+from leap.bitmask.services.eip import darwinvpnlauncher
from leap.bitmask.services.eip.eipconfig import EIPConfig
from leap.bitmask.services.eip.udstelnet import UDSTelnet
from leap.bitmask.util import first, force_eval
@@ -145,7 +147,7 @@ class VPN(object):
demand.
"""
TERMINATE_MAXTRIES = 10
- TERMINATE_WAIT = 1 # secs
+ TERMINATE_WAIT = 2 # secs
OPENVPN_VERB = "openvpn_verb"
@@ -173,7 +175,7 @@ class VPN(object):
:param kwargs: kwargs to be passed to the VPNProcess
:type kwargs: dict
"""
- logger.debug('VPN: start')
+ logger.debug('VPN: start ---------------------------------------------------')
self._user_stopped = False
self._stop_pollers()
kwargs['openvpn_verb'] = self._openvpn_verb
@@ -181,22 +183,6 @@ class VPN(object):
restart = kwargs.pop('restart', False)
- # start the main vpn subprocess
- vpnproc = VPNProcess(*args, **kwargs)
-
- if vpnproc.get_openvpn_process():
- 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,
- restart=restart)
- if not restart and not firewall_up:
- logger.error("Could not bring firewall up, "
- "aborting openvpn launch.")
- return
# FIXME it would be good to document where the
# errors here are catched, since we currently handle them
@@ -211,18 +197,56 @@ class VPN(object):
# the ping-pong to the frontend, and without adding any logical checks
# in the frontend. We should just communicate UI changes to frontend,
# and abstract us away from anything else.
- try:
+
+ # TODO factor this out to the platform-launchers
+
+ if IS_LINUX:
+ # start the main vpn subprocess
+ vpnproc = VPNProcess(*args, **kwargs)
cmd = vpnproc.getCommand()
- except Exception as e:
- logger.error("Error while getting vpn command... {0!r}".format(e))
- raise
+
+ if vpnproc.get_openvpn_process():
+ logger.info("Another vpn process is running. Will try to stop it.")
+ vpnproc.stop_if_already_running()
+
+ # we try to bring the firewall up
+ gateways = vpnproc.getGateways()
+ firewall_up = self._launch_firewall_linux(
+ gateways, restart=restart)
+ if not restart and not firewall_up:
+ logger.error("Could not bring firewall up, "
+ "aborting openvpn launch.")
+ return
+
+ if IS_MAC:
+ # start the main vpn subprocess
+ vpnproc = VPNCanary(*args, **kwargs)
+
+ # we try to bring the firewall up
+ gateways = vpnproc.getGateways()
+ firewall_up = self._launch_firewall_osx(
+ gateways, restart=restart)
+ if not restart and not firewall_up:
+ logger.error("Could not bring firewall up, "
+ "aborting openvpn launch.")
+ return
+
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ cmd = vpnproc.getVPNCommand()
+ result = helper.send('openvpn_start %s' % ' '.join(cmd))
+
+ # TODO Windows version -- should be similar to osx.
env = os.environ
for key, val in vpnproc.vpn_env.items():
env[key] = val
- reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
+ cmd = vpnproc.getCommand()
+ running_proc = reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
+ vpnproc.pid = running_proc.pid
self._vpnproc = vpnproc
+
+
# add pollers for status and state
# this could be extended to a collection of
@@ -233,9 +257,9 @@ class VPN(object):
self._pollers.extend(poll_list)
self._start_pollers()
- def _launch_firewall(self, gateways, restart=False):
+ def _launch_firewall_linux(self, gateways, restart=False):
"""
- Launch the firewall using the privileged wrapper.
+ Launch the firewall using the privileged wrapper (linux).
:param gateways:
:type gateways: list
@@ -254,40 +278,63 @@ class VPN(object):
exitCode = subprocess.call(cmd + gateways)
return True if exitCode is 0 else False
+ def _launch_firewall_osx(self, gateways, restart=False):
+ cmd = 'firewall_start %s' % ' '.join(gateways)
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
+ return True
+
+ # TODO -- write LINUX/OSX VERSION too ------------------------------------
def is_fw_down(self):
"""
Return whether the firewall is down or not.
:rtype: bool
"""
- BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
- fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT)
- fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256
- return fw_is_down()
+ if IS_LINUX:
+ BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
+ fw_up_cmd = "pkexec {0} firewall isup".format(BM_ROOT)
+ fw_is_down = lambda: commands.getstatusoutput(fw_up_cmd)[0] == 256
+ return fw_is_down()
+
+ if IS_MAC:
+ cmd = 'firewall_isup'
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
+ return True
+
def tear_down_firewall(self):
"""
Tear the firewall down using the privileged wrapper.
"""
if IS_MAC:
- # We don't support Mac so far
+ cmd = 'firewall_stop'
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
return True
- BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
- exitCode = subprocess.call(["pkexec",
- BM_ROOT, "firewall", "stop"])
- return True if exitCode is 0 else False
+
+ if IS_LINUX:
+ BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
+ exitCode = subprocess.call(["pkexec",
+ BM_ROOT, "firewall", "stop"])
+ return True if exitCode is 0 else False
def bitmask_root_vpn_down(self):
"""
Bring openvpn down using the privileged wrapper.
"""
if IS_MAC:
- # We don't support Mac so far
+ cmd = 'openvpn_stop'
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ result = helper.send(cmd)
return True
- BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
- exitCode = subprocess.call(["pkexec",
- BM_ROOT, "openvpn", "stop"])
- return True if exitCode is 0 else False
+
+ if IS_LINUX:
+ BM_ROOT = force_eval(linuxvpnlauncher.LinuxVPNLauncher.BITMASK_ROOT)
+ exitCode = subprocess.call(["pkexec",
+ BM_ROOT, "openvpn", "stop"])
+ return True if exitCode is 0 else False
def _kill_if_left_alive(self, tries=0):
"""
@@ -297,18 +344,18 @@ class VPN(object):
:param tries: counter of tries, used in recursion
:type tries: int
"""
+ # we try to tear the firewall down
+ if (IS_LINUX or IS_MAC) and self._user_stopped:
+ logger.debug('trying to bring firewall down...')
+ firewall_down = self.tear_down_firewall()
+ if firewall_down:
+ logger.debug("Firewall down")
+ else:
+ logger.warning("Could not tear firewall down")
+
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...")
@@ -813,6 +860,8 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
programmatically.
"""
+ pid = None
+
def __init__(self, eipconfig, providerconfig, socket_host, socket_port,
signaler, openvpn_verb):
"""
@@ -861,7 +910,7 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self._vpn_observer = VPNObserver(signaler)
self.is_restart = False
- # processProtocol methods
+ # ProcessProtocol methods
def connectionMade(self):
"""
@@ -893,8 +942,11 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
.. seeAlso: `http://twistedmatrix.com/documents/13.0.0/api/twisted.internet.protocol.ProcessProtocol.html` # noqa
"""
exit_code = reason.value.exitCode
+
if isinstance(exit_code, int):
logger.debug("processExited, status %d" % (exit_code,))
+ else:
+ exit_code = 0
self._signaler.signal(
self._signaler.eip_process_finished, exit_code)
self._alive = False
@@ -976,3 +1028,41 @@ class VPNProcess(protocol.ProcessProtocol, VPNManager):
self.transport.signalProcess('KILL')
except internet_error.ProcessExitedAlready:
logger.debug('Process Exited Already')
+
+
+class VPNCanary(VPNProcess):
+
+ """
+ This is a Canary Process that does not run openvpn itself, but it's
+ notified by the privileged process when the process dies.
+
+ This is an ugly workaround to allow the qt signals and the processprotocol
+ to live happily together until we refactor EIP out of the qt model
+ completely.
+ """
+
+ def connectionMade(self):
+ VPNProcess.connectionMade(self)
+ reactor.callLater(2, self.registerPID)
+
+ def registerPID(self):
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ cmd = 'openvpn_set_watcher %s' % self.pid
+ result = helper.send(cmd)
+
+ def killProcess(self):
+ helper = darwinvpnlauncher.DarwinHelperCommand()
+ cmd = 'openvpn_force_stop'
+ result = helper.send(cmd)
+
+ def getVPNCommand(self):
+ return VPNProcess.getCommand(self)
+
+ def getCommand(self):
+ canary = '''import sys, signal, time
+def receive_signal(signum, stack):
+ sys.exit()
+signal.signal(signal.SIGTERM, receive_signal)
+while True:
+ time.sleep(60)'''
+ return ['python', '-c', '%s' % canary]