summaryrefslogtreecommitdiff
path: root/src/leap/services/eip/vpn.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/services/eip/vpn.py')
-rw-r--r--src/leap/services/eip/vpn.py359
1 files changed, 359 insertions, 0 deletions
diff --git a/src/leap/services/eip/vpn.py b/src/leap/services/eip/vpn.py
new file mode 100644
index 00000000..f117cdbc
--- /dev/null
+++ b/src/leap/services/eip/vpn.py
@@ -0,0 +1,359 @@
+# -*- coding: utf-8 -*-
+# vpn.py
+# Copyright (C) 2013 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/>.
+
+"""
+VPN launcher and watcher thread
+"""
+import logging
+import sys
+
+from PySide import QtCore, QtGui
+from subprocess import Popen, PIPE
+from functools import partial
+
+from leap.config.providerconfig import ProviderConfig
+from leap.services.eip.vpnlaunchers import get_platform_launcher
+from leap.services.eip.eipconfig import EIPConfig
+from leap.services.eip.udstelnet import UDSTelnet
+
+logger = logging.getLogger(__name__)
+ON_POSIX = 'posix' in sys.builtin_module_names
+
+
+# TODO: abstract the thread that can be asked to quit to another
+# generic class that Fetcher and VPN inherit from
+class VPN(QtCore.QThread):
+ """
+ VPN launcher and watcher thread. It will emit signals based on
+ different events caught by the management interface
+ """
+
+ state_changed = QtCore.Signal(dict)
+ status_changed = QtCore.Signal(dict)
+
+ CONNECTION_RETRY_TIME = 1000
+ POLL_TIME = 100
+
+ TS_KEY = "ts"
+ STATUS_STEP_KEY = "status_step"
+ OK_KEY = "ok"
+ IP_KEY = "ip"
+ REMOTE_KEY = "remote"
+
+ TUNTAP_READ_KEY = "tun_tap_read"
+ TUNTAP_WRITE_KEY = "tun_tap_write"
+ TCPUDP_READ_KEY = "tcp_udp_read"
+ TCPUDP_WRITE_KEY = "tcp_udp_write"
+ AUTH_READ_KEY = "auth_read"
+
+ def __init__(self):
+ QtCore.QThread.__init__(self)
+
+ self._should_quit = False
+ self._should_quit_lock = QtCore.QMutex()
+
+ self._launcher = get_platform_launcher()
+ self._subp = None
+ self._started = False
+
+ self._tn = None
+ self._host = None
+ self._port = None
+
+ self._last_state = None
+ self._last_status = None
+
+ def get_should_quit(self):
+ """
+ Returns wether this thread should quit
+
+ @rtype: bool
+ @return: True if the thread should terminate itself, Flase otherwise
+ """
+ QtCore.QMutexLocker(self._should_quit_lock)
+ return self._should_quit
+
+ def set_should_quit(self):
+ """
+ Sets the should_quit flag to True so that this thread
+ terminates the first chance it gets.
+ Also terminates the VPN process and the connection to it
+ """
+ QtCore.QMutexLocker(self._should_quit_lock)
+ self._should_quit = True
+ if self._tn is None or self._subp is None:
+ return
+
+ try:
+ self._disconnect()
+ self._subp.terminate()
+ except Exception as e:
+ logger.debug("Could not terminate process, trying command " +
+ "signal SIGNINT: %r" % (e,))
+ self._send_command("signal SIGINT")
+ self._subp.wait()
+ self.wait()
+ self._started = False
+
+ def start(self, eipconfig, providerconfig, socket_host, socket_port):
+ """
+ Launches OpenVPN and starts the thread to watch its output
+
+ @param eipconfig: eip configuration object
+ @type eipconfig: EIPConfig
+ @param providerconfig: provider specific configuration
+ @type providerconfig: ProviderConfig
+ @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
+ """
+ assert eipconfig, "We need an eip config"
+ assert isinstance(eipconfig, EIPConfig), "Expected EIPConfig " + \
+ "object instead of %s" % (type(eipconfig),)
+ assert providerconfig, "We need a provider config"
+ assert isinstance(providerconfig, ProviderConfig), "Expected " + \
+ "ProviderConfig object instead of %s" % (type(providerconfig),)
+ assert not self._started, "Starting process more than once!"
+
+ logger.debug("Starting VPN...")
+
+ with QtCore.QMutexLocker(self._should_quit_lock):
+ self._should_quit = False
+
+ command = self._launcher.get_vpn_command(eipconfig=eipconfig,
+ providerconfig=providerconfig,
+ socket_host=socket_host,
+ socket_port=socket_port)
+ try:
+ self._subp = Popen(command, stdout=PIPE, stderr=PIPE,
+ bufsize=1, close_fds=ON_POSIX)
+
+ self._host = socket_host
+ self._port = socket_port
+
+ self._started = True
+
+ QtCore.QThread.start(self)
+ except Exception as e:
+ logger.warning("Something went wrong while starting OpenVPN: %r" %
+ (e,))
+
+ def _connect(self, socket_host, socket_port):
+ """
+ Connects to 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
+ """
+ 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()
+ except Exception as e:
+ logger.warning("Could not connect to OpenVPN yet: %r" % (e,))
+ self._tn = None
+
+ def _disconnect(self):
+ """
+ Disconnects the telnet connection to the openvpn process
+ """
+ logger.debug('Closing socket')
+ self._tn.write("quit\n")
+ self._tn.read_all()
+ self._tn.close()
+ self._tn = None
+
+ 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
+ """
+ assert self._tn, "We need a tn connection!"
+ try:
+ self._tn.write("%s\n" % (command,))
+ buf = self._tn.read_until(until, 2)
+ self._tn.read_eager()
+ lines = buf.split("\n")
+ return lines
+ except Exception as e:
+ logger.warning("Error sending command %s: %r" %
+ (command, e))
+ return []
+
+ 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_dict = {
+ self.TS_KEY: ts,
+ self.STATUS_STEP_KEY: status_step,
+ self.OK_KEY: ok,
+ self.IP_KEY: ip,
+ self.REMOTE_KEY: remote
+ }
+
+ if state_dict != self._last_state:
+ self.state_changed.emit(state_dict)
+ self._last_state = state_dict
+
+ 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 = ""
+ tcp_udp_read = ""
+ tcp_udp_write = ""
+ auth_read = ""
+ for line in output:
+ stripped = line.strip()
+ if stripped.endswith("STATISTICS") or stripped == "END":
+ continue
+ parts = stripped.split(",")
+ if len(parts) < 2:
+ continue
+ if parts[0].strip() == "TUN/TAP read bytes":
+ tun_tap_read = parts[1]
+ elif parts[0].strip() == "TUN/TAP write bytes":
+ tun_tap_write = parts[1]
+ elif parts[0].strip() == "TCP/UDP read bytes":
+ tcp_udp_read = parts[1]
+ elif parts[0].strip() == "TCP/UDP write bytes":
+ tcp_udp_write = parts[1]
+ elif parts[0].strip() == "Auth read bytes":
+ auth_read = parts[1]
+
+ status_dict = {
+ self.TUNTAP_READ_KEY: tun_tap_read,
+ self.TUNTAP_WRITE_KEY: tun_tap_write,
+ self.TCPUDP_READ_KEY: tcp_udp_read,
+ self.TCPUDP_WRITE_KEY: tcp_udp_write,
+ self.AUTH_READ_KEY: auth_read
+ }
+
+ if status_dict != self._last_status:
+ self.status_changed.emit(status_dict)
+ self._last_status = status_dict
+
+ def run(self):
+ """
+ Main run loop for this thread
+ """
+ while True:
+ if self.get_should_quit():
+ logger.debug("Quitting VPN thread")
+ return
+
+ if self._tn is None:
+ self._connect(self._host, self._port)
+ QtCore.QThread.msleep(self.CONNECTION_RETRY_TIME)
+ else:
+ self._parse_state_and_notify(self._send_command("state"))
+ self._parse_status_and_notify(self._send_command("status"))
+ QtCore.QThread.msleep(self.POLL_TIME)
+
+
+if __name__ == "__main__":
+ app = QtGui.QApplication(sys.argv)
+
+ import signal
+
+ def sigint_handler(*args, **kwargs):
+ logger.debug('SIGINT catched. shutting down...')
+ vpn_thread = args[0]
+ vpn_thread.set_should_quit()
+ QtGui.QApplication.quit()
+
+ def signal_tester(d):
+ print d
+
+ logger = logging.getLogger(name='leap')
+ logger.setLevel(logging.DEBUG)
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ formatter = logging.Formatter(
+ '%(asctime)s '
+ '- %(name)s - %(levelname)s - %(message)s')
+ console.setFormatter(formatter)
+ logger.addHandler(console)
+
+ vpn_thread = VPN()
+
+ sigint = partial(sigint_handler, vpn_thread)
+ signal.signal(signal.SIGINT, sigint)
+
+ eipconfig = EIPConfig()
+ if eipconfig.load("leap/providers/bitmask.net/eip-service.json"):
+ provider = ProviderConfig()
+ if provider.load("leap/providers/bitmask.net/provider.json"):
+ vpn_thread.start(eipconfig=eipconfig,
+ providerconfig=provider,
+ socket_host="/home/chiiph/vpnsock",
+ socket_port="unix")
+
+ timer = QtCore.QTimer()
+ timer.start(500)
+ timer.timeout.connect(lambda: None)
+ app.connect(app, QtCore.SIGNAL("aboutToQuit()"),
+ vpn_thread.set_should_quit)
+ w = QtGui.QWidget()
+ w.resize(100, 100)
+ w.show()
+
+ vpn_thread.state_changed.connect(signal_tester)
+ vpn_thread.status_changed.connect(signal_tester)
+
+ sys.exit(app.exec_())