summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/vpn/_control.py
blob: 6e942f484b7e9400b9b40942effc2a9dd5f578dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213

import os

from twisted.internet.task import LoopingCall
from twisted.internet import reactor
from twisted.logger import Logger

from .process import VPNProcess
from .constants import IS_MAC

logger = Logger()

# 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


class VPNControl(object):
    """
    This is the high-level object that the service is dealing with.
    It exposes the start and terminate methods.

    On start, it spawns a VPNProcess instance that will use a vpnlauncher
    suited for the running platform and connect to the management interface
    opened by the openvpn process, executing commands over that interface on
    demand.
    """
    TERMINATE_MAXTRIES = 10
    TERMINATE_WAIT = 1  # secs

    OPENVPN_VERB = "openvpn_verb"

    def __init__(self, **kwargs):
        # TODO what the fuck this is doing that is different from
        # the manager?
        self._vpnproc = None
        self._pollers = []

        self._signaler = kwargs['signaler']
        # self._openvpn_verb = flags.OPENVPN_VERBOSITY
        self._openvpn_verb = None

        self._user_stopped = False
        self._remotes = kwargs['remotes']

    def start(self, *args, **kwargs):
        """
        Starts the openvpn subprocess.

        :param args: args to be passed to the VPNProcess
        :type args: tuple

        :param kwargs: kwargs to be passed to the VPNProcess
        :type kwargs: dict
        """
        logger.debug('VPN: start')
        self._user_stopped = False
        self._stop_pollers()
        kwargs['openvpn_verb'] = self._openvpn_verb
        kwargs['signaler'] = self._signaler
        kwargs['remotes'] = self._remotes

        # 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()

        # FIXME it would be good to document where the
        # errors here are catched, since we currently handle them
        # at the frontend layer. This *should* move to be handled entirely
        # in the backend.
        # exception is indeed technically catched in backend, then converted
        # into a signal, that is catched in the eip_status widget in the
        # frontend, and converted into a signal to abort the connection that is
        # sent to the backend again.

        # the whole exception catching should be done in the backend, without
        # 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:
            cmd = vpnproc.getCommand()
        except Exception as e:
            logger.error("Error while getting vpn command... {0!r}".format(e))
            raise

        env = os.environ
        for key, val in vpnproc.vpn_env.items():
            env[key] = val

        reactor.spawnProcess(vpnproc, cmd[0], cmd, env)
        self._vpnproc = vpnproc

        # add pollers for status and state
        # this could be extended to a collection of
        # generic watchers

        poll_list = [LoopingCall(vpnproc.pollStatus),
                     LoopingCall(vpnproc.pollState)]
        self._pollers.extend(poll_list)
        self._start_pollers()


    # TODO -- rename to stop ??
    def terminate(self, shutdown=False, restart=False):
        """
        Stops the openvpn subprocess.

        Attempts to send a SIGTERM first, and after a timeout
        it sends a SIGKILL.

        :param shutdown: whether this is the final shutdown
        :type shutdown: bool
        :param restart: whether this stop is part of a hard restart.
        :type restart: bool
        """
        self._stop_pollers()

        # First we try to be polite and send a SIGTERM...
        if self._vpnproc is not None:
            # We assume that the only valid stops are initiated
            # by an user action, not hard restarts
            self._user_stopped = not restart
            self._vpnproc.is_restart = restart

            self._sentterm = True
            self._vpnproc.terminate_openvpn(shutdown=shutdown)

            # ...but we also trigger a countdown to be unpolite
            # if strictly needed.
            reactor.callLater(
                self.TERMINATE_WAIT, self._kill_if_left_alive)
        else:
            logger.debug("VPN is not running.")


    # TODO should this be public??
    def killit(self):
        """
        Sends a kill signal to the process.
        """
        self._stop_pollers()
        if self._vpnproc is None:
            logger.debug("There's no vpn process running to kill.")
        else:
            self._vpnproc.aborted = True
            self._vpnproc.killProcess()


    def bitmask_root_vpn_down(self):
        """
        Bring openvpn down using the privileged wrapper.
        """
        if IS_MAC:
            # We don't support Mac so far
            return True
        BM_ROOT = force_eval(linux.LinuxVPNLauncher.BITMASK_ROOT)

        # FIXME -- port to processProtocol
        exitCode = subprocess.call(["pkexec",
                                    BM_ROOT, "openvpn", "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 send a
        SIGKILL after a timeout period.

        :param tries: counter of tries, used in recursion
        :type tries: int
        """
        while tries < self.TERMINATE_MAXTRIES:
            if self._vpnproc.transport.pid is None:
                logger.debug("Process has been happily terminated.")
                return
            else:
                logger.debug("Process did not die, waiting...")

            tries += 1
            reactor.callLater(self.TERMINATE_WAIT,
                              self._kill_if_left_alive, tries)
            return

        # after running out of patience, we try a killProcess
        logger.debug("Process did not died. Sending a SIGKILL.")
        try:
            self.killit()
        except OSError:
            logger.error("Could not kill process!")


    def _start_pollers(self):
        """
        Iterate through the registered observers
        and start the looping call for them.
        """
        for poller in self._pollers:
            poller.start(POLL_TIME)


    def _stop_pollers(self):
        """
        Iterate through the registered observers
        and stop the looping calls if they are running.
        """
        for poller in self._pollers:
            if poller.running:
                poller.stop()
        self._pollers = []