summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/vpn/_management.py
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2017-02-01 00:00:20 +0100
committerKali Kaneko (leap communications) <kali@leap.se>2017-02-23 00:37:29 +0100
commit6d1d18faec5caa60c26b8245f0ab17c63d0b80d8 (patch)
tree08dbfd0e257bea0434543df6505bcc9e12acbf97 /src/leap/bitmask/vpn/_management.py
parent8fb76310e14a5893397894c6155ac7aa4f28e483 (diff)
[refactor] split vpn control into some modules
Diffstat (limited to 'src/leap/bitmask/vpn/_management.py')
-rw-r--r--src/leap/bitmask/vpn/_management.py422
1 files changed, 422 insertions, 0 deletions
diff --git a/src/leap/bitmask/vpn/_management.py b/src/leap/bitmask/vpn/_management.py
new file mode 100644
index 0000000..0eb37b7
--- /dev/null
+++ b/src/leap/bitmask/vpn/_management.py
@@ -0,0 +1,422 @@
+class OpenVPNAlreadyRunning(Exception):
+ message = ("Another openvpn instance is already running, and could "
+ "not be stopped.")
+
+
+class AlienOpenVPNAlreadyRunning(Exception):
+ message = ("Another openvpn instance is already running, and could "
+ "not be stopped because it was not launched by LEAP.")
+
+
+class VPNManagement(object):
+ """
+ This is a mixin that we use in the VPNProcess class.
+ Here we get together all methods related with the openvpn management
+ interface.
+
+ For more info about management methods::
+
+ zcat `dpkg -L openvpn | grep management`
+ """
+
+ # Timers, in secs
+ # NOTE: We need to set a bigger poll time in OSX because it seems
+ # openvpn malfunctions when you ask it a lot of things in a short
+ # amount of time.
+ POLL_TIME = 2.5 if IS_MAC else 1.0
+ CONNECTION_RETRY_TIME = 1
+
+ def __init__(self, signaler=None):
+ """
+ Initializes the VPNManager.
+
+ :param signaler: Signaler object used to send notifications to the
+ backend
+ :type signaler: backend.Signaler
+ """
+ self._tn = None
+ self._signaler = signaler
+ self._aborted = False
+
+ @property
+ def aborted(self):
+ return self._aborted
+
+ @aborted.setter
+ def aborted(self, value):
+ self._aborted = value
+
+ def _seek_to_eof(self):
+ """
+ Read as much as available. Position seek pointer to end of stream
+ """
+ try:
+ self._tn.read_eager()
+ except EOFError:
+ logger.debug("Could not read from socket. Assuming it died.")
+ return
+
+ def _send_command(self, command, until=b"END"):
+ """
+ Sends a command to the telnet connection and reads until END
+ is reached.
+
+ :param command: command to send
+ :type command: str
+
+ :param until: byte delimiter string for reading command output
+ :type until: byte str
+
+ :return: response read
+ :rtype: list
+ """
+ # leap_assert(self._tn, "We need a tn connection!")
+
+ try:
+ self._tn.write("%s\n" % (command,))
+ buf = self._tn.read_until(until, 2)
+ self._seek_to_eof()
+ blist = buf.split('\r\n')
+ if blist[-1].startswith(until):
+ del blist[-1]
+ return blist
+ else:
+ return []
+
+ except socket.error:
+ # XXX should get a counter and repeat only
+ # after mod X times.
+ logger.warning('socket error (command was: "%s")' % (command,))
+ self._close_management_socket(announce=False)
+ logger.debug('trying to connect to management again')
+ self.try_to_connect_to_management(max_retries=5)
+ return []
+
+ # XXX should move this to a errBack!
+ except Exception as e:
+ logger.warning("Error sending command %s: %r" %
+ (command, e))
+ return []
+
+ def _close_management_socket(self, announce=True):
+ """
+ Close connection to openvpn management interface.
+ """
+ logger.debug('closing socket')
+ if announce:
+ self._tn.write("quit\n")
+ self._tn.read_all()
+ self._tn.get_socket().close()
+ self._tn = None
+
+ def _connect_management(self, socket_host, socket_port):
+ """
+ Connects to the management interface on the specified
+ socket_host socket_port.
+
+ :param socket_host: either socket path (unix) or socket IP
+ :type socket_host: str
+
+ :param socket_port: either string "unix" if it's a unix
+ socket, or port otherwise
+ :type socket_port: str
+ """
+ if self.is_connected():
+ self._close_management_socket()
+
+ try:
+ self._tn = UDSTelnet(socket_host, socket_port)
+
+ # XXX make password optional
+ # specially for win. we should generate
+ # the pass on the fly when invoking manager
+ # from conductor
+
+ # self.tn.read_until('ENTER PASSWORD:', 2)
+ # self.tn.write(self.password + '\n')
+ # self.tn.read_until('SUCCESS:', 2)
+ if self._tn:
+ self._tn.read_eager()
+
+ # XXX move this to the Errback
+ except Exception as e:
+ logger.warning("Could not connect to OpenVPN yet: %r" % (e,))
+ self._tn = None
+
+ def _connectCb(self, *args):
+ """
+ Callback for connection.
+
+ :param args: not used
+ """
+ if self._tn:
+ logger.info('Connected to management')
+ else:
+ logger.debug('Cannot connect to management...')
+
+ def _connectErr(self, failure):
+ """
+ Errorback for connection.
+
+ :param failure: Failure
+ """
+ logger.warning(failure)
+
+ def connect_to_management(self, host, port):
+ """
+ Connect to a management interface.
+
+ :param host: the host of the management interface
+ :type host: str
+
+ :param port: the port of the management interface
+ :type port: str
+
+ :returns: a deferred
+ """
+ self.connectd = defer.maybeDeferred(
+ self._connect_management, host, port)
+ self.connectd.addCallbacks(self._connectCb, self._connectErr)
+ return self.connectd
+
+ def is_connected(self):
+ """
+ Returns the status of the management interface.
+
+ :returns: True if connected, False otherwise
+ :rtype: bool
+ """
+ return True if self._tn else False
+
+ def try_to_connect_to_management(self, retry=0, max_retries=None):
+ """
+ Attempts to connect to a management interface, and retries
+ after CONNECTION_RETRY_TIME if not successful.
+
+ :param retry: number of the retry
+ :type retry: int
+ """
+ if max_retries and retry > max_retries:
+ logger.warning("Max retries reached while attempting to connect "
+ "to management. Aborting.")
+ self.aborted = True
+ return
+
+ # _alive flag is set in the VPNProcess class.
+ if not self._alive:
+ logger.debug('Tried to connect to management but process is '
+ 'not alive.')
+ return
+ logger.debug('trying to connect to management')
+ if not self.aborted and not self.is_connected():
+ self.connect_to_management(self._socket_host, self._socket_port)
+ reactor.callLater(
+ self.CONNECTION_RETRY_TIME,
+ self.try_to_connect_to_management, retry + 1)
+
+ def _parse_state_and_notify(self, output):
+ """
+ Parses the output of the state command and emits state_changed
+ signal when the state changes.
+
+ :param output: list of lines that the state command printed as
+ its output
+ :type output: list
+ """
+ for line in output:
+ stripped = line.strip()
+ if stripped == "END":
+ continue
+ parts = stripped.split(",")
+ if len(parts) < 5:
+ continue
+ ts, status_step, ok, ip, remote = parts
+
+ state = status_step
+ if state != self._last_state:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.eip_state_changed, state)
+ self._last_state = state
+
+ def _parse_status_and_notify(self, output):
+ """
+ Parses the output of the status command and emits
+ status_changed signal when the status changes.
+
+ :param output: list of lines that the status command printed
+ as its output
+ :type output: list
+ """
+ tun_tap_read = ""
+ tun_tap_write = ""
+
+ for line in output:
+ stripped = line.strip()
+ if stripped.endswith("STATISTICS") or stripped == "END":
+ continue
+ parts = stripped.split(",")
+ if len(parts) < 2:
+ continue
+
+ text, value = parts
+ # text can be:
+ # "TUN/TAP read bytes"
+ # "TUN/TAP write bytes"
+ # "TCP/UDP read bytes"
+ # "TCP/UDP write bytes"
+ # "Auth read bytes"
+
+ if text == "TUN/TAP read bytes":
+ tun_tap_read = value # download
+ elif text == "TUN/TAP write bytes":
+ tun_tap_write = value # upload
+
+ status = (tun_tap_read, tun_tap_write)
+ if status != self._last_status:
+ if self._signaler is not None:
+ self._signaler.signal(self._signaler.eip_status_changed, status)
+ self._last_status = status
+
+ def get_state(self):
+ """
+ Notifies the gui of the output of the state command over
+ the openvpn management interface.
+ """
+ if self.is_connected():
+ return self._parse_state_and_notify(self._send_command("state"))
+
+ def get_status(self):
+ """
+ Notifies the gui of the output of the status command over
+ the openvpn management interface.
+ """
+ if self.is_connected():
+ return self._parse_status_and_notify(self._send_command("status"))
+
+ @property
+ def vpn_env(self):
+ """
+ Return a dict containing the vpn environment to be used.
+ """
+ return self._launcher.get_vpn_env()
+
+ def terminate_openvpn(self, shutdown=False):
+ """
+ Attempts to terminate openvpn by sending a SIGTERM.
+ """
+ if self.is_connected():
+ self._send_command("signal SIGTERM")
+ if shutdown:
+ self._cleanup_tempfiles()
+
+ def _cleanup_tempfiles(self):
+ """
+ Remove all temporal files we might have left behind.
+
+ Iif self.port is 'unix', we have created a temporal socket path that,
+ under normal circumstances, we should be able to delete.
+ """
+ if self._socket_port == "unix":
+ logger.debug('cleaning socket file temp folder')
+ tempfolder = first(os.path.split(self._socket_host))
+ if tempfolder and os.path.isdir(tempfolder):
+ try:
+ shutil.rmtree(tempfolder)
+ except OSError:
+ logger.error('could not delete tmpfolder %s' % tempfolder)
+
+ def get_openvpn_process(self):
+ """
+ Looks for openvpn instances running.
+
+ :rtype: process
+ """
+ openvpn_process = None
+ for p in psutil.process_iter():
+ try:
+ # XXX Not exact!
+ # Will give false positives.
+ # we should check that cmdline BEGINS
+ # with openvpn or with our wrapper
+ # (pkexec / osascript / whatever)
+
+ # This needs more work, see #3268, but for the moment
+ # we need to be able to filter out arguments in the form
+ # --openvpn-foo, since otherwise we are shooting ourselves
+ # in the feet.
+
+ if PSUTIL_2:
+ cmdline = p.cmdline()
+ else:
+ cmdline = p.cmdline
+ if any(map(lambda s: s.find(
+ "LEAPOPENVPN") != -1, cmdline)):
+ openvpn_process = p
+ break
+ except psutil_AccessDenied:
+ pass
+ return openvpn_process
+
+ def stop_if_already_running(self):
+ """
+ Checks if VPN is already running and tries to stop it.
+
+ Might raise OpenVPNAlreadyRunning.
+
+ :return: True if stopped, False otherwise
+
+ """
+ process = self.get_openvpn_process()
+ if not process:
+ logger.debug('Could not find openvpn process while '
+ 'trying to stop it.')
+ return
+
+ logger.debug("OpenVPN is already running, trying to stop it...")
+ cmdline = process.cmdline
+
+ manag_flag = "--management"
+ if isinstance(cmdline, list) and manag_flag in cmdline:
+ # we know that our invocation has this distinctive fragment, so
+ # we use this fingerprint to tell other invocations apart.
+ # this might break if we change the configuration path in the
+ # launchers
+ smellslikeleap = lambda s: "leap" in s and "providers" in s
+
+ if not any(map(smellslikeleap, cmdline)):
+ logger.debug("We cannot stop this instance since we do not "
+ "recognise it as a leap invocation.")
+ raise AlienOpenVPNAlreadyRunning
+
+ try:
+ index = cmdline.index(manag_flag)
+ host = cmdline[index + 1]
+ port = cmdline[index + 2]
+ logger.debug("Trying to connect to %s:%s"
+ % (host, port))
+ self.connect_to_management(host, port)
+
+ # XXX this has a problem with connections to different
+ # remotes. So the reconnection will only work when we are
+ # terminating instances left running for the same provider.
+ # If we are killing an openvpn instance configured for another
+ # provider, we will get:
+ # TLS Error: local/remote TLS keys are out of sync
+ # However, that should be a rare case right now.
+ self._send_command("signal SIGTERM")
+ self._close_management_socket(announce=True)
+ except (Exception, AssertionError) as e:
+ logger.warning("Problem trying to terminate OpenVPN: %r"
+ % (e,))
+ else:
+ logger.debug("Could not find the expected openvpn command line.")
+
+ process = self.get_openvpn_process()
+ if process is None:
+ logger.debug("Successfully finished already running "
+ "openvpn process.")
+ return True
+ else:
+ logger.warning("Unable to terminate OpenVPN")
+ raise OpenVPNAlreadyRunning
+