Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ master

History | View | Annotate | Download (23.8 kB)

1 138d0e8b Faidon Liambotis
#!/usr/bin/env python
2 138d0e8b Faidon Liambotis
"""
3 138d0e8b Faidon Liambotis
vncauthproxy - a VNC authentication proxy
4 138d0e8b Faidon Liambotis
"""
5 138d0e8b Faidon Liambotis
#
6 31965126 Vangelis Koukis
# Copyright (c) 2010-2013 Greek Research and Technology Network S.A.
7 138d0e8b Faidon Liambotis
#
8 138d0e8b Faidon Liambotis
# This program is free software; you can redistribute it and/or modify
9 138d0e8b Faidon Liambotis
# it under the terms of the GNU General Public License as published by
10 138d0e8b Faidon Liambotis
# the Free Software Foundation; either version 2 of the License, or
11 138d0e8b Faidon Liambotis
# (at your option) any later version.
12 138d0e8b Faidon Liambotis
#
13 138d0e8b Faidon Liambotis
# This program is distributed in the hope that it will be useful, but
14 138d0e8b Faidon Liambotis
# WITHOUT ANY WARRANTY; without even the implied warranty of
15 138d0e8b Faidon Liambotis
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 138d0e8b Faidon Liambotis
# General Public License for more details.
17 138d0e8b Faidon Liambotis
#
18 138d0e8b Faidon Liambotis
# You should have received a copy of the GNU General Public License
19 138d0e8b Faidon Liambotis
# along with this program; if not, write to the Free Software
20 138d0e8b Faidon Liambotis
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 138d0e8b Faidon Liambotis
# 02110-1301, USA.
22 138d0e8b Faidon Liambotis
23 138d0e8b Faidon Liambotis
DEFAULT_CTRL_SOCKET = "/var/run/vncauthproxy/ctrl.sock"
24 138d0e8b Faidon Liambotis
DEFAULT_LOG_FILE = "/var/log/vncauthproxy/vncauthproxy.log"
25 138d0e8b Faidon Liambotis
DEFAULT_PID_FILE = "/var/run/vncauthproxy/vncauthproxy.pid"
26 138d0e8b Faidon Liambotis
DEFAULT_CONNECT_TIMEOUT = 30
27 7eb27319 Stratos Psomadakis
DEFAULT_CONNECT_RETRIES = 3
28 7eb27319 Stratos Psomadakis
DEFAULT_RETRY_WAIT = 0.1
29 1e3d1c7d Vangelis Koukis
# We must take care not to fall into the ephemeral port range,
30 1e3d1c7d Vangelis Koukis
# this can lead to transient failures to bind a chosen port.
31 1e3d1c7d Vangelis Koukis
#
32 1e3d1c7d Vangelis Koukis
# By default, Linux uses 32768 to 61000, see:
33 1e3d1c7d Vangelis Koukis
# http://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html#Linux
34 1e3d1c7d Vangelis Koukis
# so 25000-30000 seems to be a sensible default.
35 1e3d1c7d Vangelis Koukis
DEFAULT_MIN_PORT = 25000
36 1e3d1c7d Vangelis Koukis
DEFAULT_MAX_PORT = 30000
37 138d0e8b Faidon Liambotis
38 138d0e8b Faidon Liambotis
import os
39 138d0e8b Faidon Liambotis
import sys
40 138d0e8b Faidon Liambotis
import logging
41 138d0e8b Faidon Liambotis
import gevent
42 020f4a9e Vangelis Koukis
import gevent.event
43 138d0e8b Faidon Liambotis
import daemon
44 138d0e8b Faidon Liambotis
import random
45 39840bd3 Vangelis Koukis
import daemon.runner
46 138d0e8b Faidon Liambotis
47 138d0e8b Faidon Liambotis
import rfb
48 39840bd3 Vangelis Koukis
49 138d0e8b Faidon Liambotis
try:
50 138d0e8b Faidon Liambotis
    import simplejson as json
51 138d0e8b Faidon Liambotis
except ImportError:
52 138d0e8b Faidon Liambotis
    import json
53 138d0e8b Faidon Liambotis
54 138d0e8b Faidon Liambotis
from gevent import socket
55 138d0e8b Faidon Liambotis
from signal import SIGINT, SIGTERM
56 138d0e8b Faidon Liambotis
from gevent.select import select
57 138d0e8b Faidon Liambotis
58 180a750f Vangelis Koukis
from lockfile import LockTimeout, AlreadyLocked
59 180a750f Vangelis Koukis
# Take care of differences between python-daemon versions.
60 180a750f Vangelis Koukis
try:
61 180a750f Vangelis Koukis
    from daemon import pidfile as pidlockfile
62 180a750f Vangelis Koukis
except:
63 180a750f Vangelis Koukis
    from daemon import pidlockfile
64 180a750f Vangelis Koukis
65 180a750f Vangelis Koukis
66 88420a63 Faidon Liambotis
logger = None
67 88420a63 Faidon Liambotis
68 31965126 Vangelis Koukis
69 fe5fc466 Vangelis Koukis
# Currently, gevent uses libevent-dns for asynchronous DNS resolution,
70 376a8634 Vangelis Koukis
# which opens a socket upon initialization time. Since we can't get the fd
71 376a8634 Vangelis Koukis
# reliably, We have to maintain all file descriptors open (which won't harm
72 376a8634 Vangelis Koukis
# anyway)
73 376a8634 Vangelis Koukis
class AllFilesDaemonContext(daemon.DaemonContext):
74 376a8634 Vangelis Koukis
    """DaemonContext class keeping all file descriptors open"""
75 376a8634 Vangelis Koukis
    def _get_exclude_file_descriptors(self):
76 376a8634 Vangelis Koukis
        class All:
77 376a8634 Vangelis Koukis
            def __contains__(self, value):
78 376a8634 Vangelis Koukis
                return True
79 376a8634 Vangelis Koukis
        return All()
80 376a8634 Vangelis Koukis
81 376a8634 Vangelis Koukis
82 138d0e8b Faidon Liambotis
class VncAuthProxy(gevent.Greenlet):
83 138d0e8b Faidon Liambotis
    """
84 138d0e8b Faidon Liambotis
    Simple class implementing a VNC Forwarder with MITM authentication as a
85 138d0e8b Faidon Liambotis
    Greenlet
86 138d0e8b Faidon Liambotis

87 138d0e8b Faidon Liambotis
    VncAuthProxy forwards VNC traffic from a specified port of the local host
88 138d0e8b Faidon Liambotis
    to a specified remote host:port. Furthermore, it implements VNC
89 138d0e8b Faidon Liambotis
    Authentication, intercepting the client/server handshake and asking the
90 138d0e8b Faidon Liambotis
    client for authentication even if the backend requires none.
91 138d0e8b Faidon Liambotis

92 138d0e8b Faidon Liambotis
    It is primarily intended for use in virtualization environments, as a VNC
93 138d0e8b Faidon Liambotis
    ``switch''.
94 138d0e8b Faidon Liambotis

95 138d0e8b Faidon Liambotis
    """
96 138d0e8b Faidon Liambotis
    id = 1
97 138d0e8b Faidon Liambotis
98 31965126 Vangelis Koukis
    def __init__(self, logger, listeners, pool, daddr, dport, server, password,
99 31965126 Vangelis Koukis
                 connect_timeout):
100 138d0e8b Faidon Liambotis
        """
101 138d0e8b Faidon Liambotis
        @type logger: logging.Logger
102 138d0e8b Faidon Liambotis
        @param logger: the logger to use
103 138d0e8b Faidon Liambotis
        @type listeners: list
104 31965126 Vangelis Koukis
        @param listeners: list of listening sockets to use for clients
105 138d0e8b Faidon Liambotis
        @type pool: list
106 31965126 Vangelis Koukis
        @param pool: if not None, return the client number into this port pool
107 138d0e8b Faidon Liambotis
        @type daddr: str
108 138d0e8b Faidon Liambotis
        @param daddr: destination address (IPv4, IPv6 or hostname)
109 138d0e8b Faidon Liambotis
        @type dport: int
110 138d0e8b Faidon Liambotis
        @param dport: destination port
111 512c571e Stratos Psomadakis
        @type server: socket
112 512c571e Stratos Psomadakis
        @param server: VNC server socket
113 138d0e8b Faidon Liambotis
        @type password: str
114 138d0e8b Faidon Liambotis
        @param password: password to request from the client
115 138d0e8b Faidon Liambotis
        @type connect_timeout: int
116 138d0e8b Faidon Liambotis
        @param connect_timeout: how long to wait for client connections
117 138d0e8b Faidon Liambotis
                                (seconds)
118 138d0e8b Faidon Liambotis

119 138d0e8b Faidon Liambotis
        """
120 138d0e8b Faidon Liambotis
        gevent.Greenlet.__init__(self)
121 138d0e8b Faidon Liambotis
        self.id = VncAuthProxy.id
122 138d0e8b Faidon Liambotis
        VncAuthProxy.id += 1
123 138d0e8b Faidon Liambotis
        self.log = logger
124 138d0e8b Faidon Liambotis
        self.listeners = listeners
125 020f4a9e Vangelis Koukis
        # A list of worker/forwarder greenlets, one for each direction
126 020f4a9e Vangelis Koukis
        self.workers = []
127 138d0e8b Faidon Liambotis
        # All listening sockets are assumed to be on the same port
128 138d0e8b Faidon Liambotis
        self.sport = listeners[0].getsockname()[1]
129 138d0e8b Faidon Liambotis
        self.pool = pool
130 138d0e8b Faidon Liambotis
        self.daddr = daddr
131 138d0e8b Faidon Liambotis
        self.dport = dport
132 512c571e Stratos Psomadakis
        self.server = server
133 138d0e8b Faidon Liambotis
        self.password = password
134 138d0e8b Faidon Liambotis
        self.client = None
135 138d0e8b Faidon Liambotis
        self.timeout = connect_timeout
136 138d0e8b Faidon Liambotis
137 138d0e8b Faidon Liambotis
    def _cleanup(self):
138 020f4a9e Vangelis Koukis
        """Cleanup everything: workers, sockets, ports
139 020f4a9e Vangelis Koukis

140 020f4a9e Vangelis Koukis
        Kill all remaining forwarder greenlets, close all active sockets,
141 020f4a9e Vangelis Koukis
        return the source port to the pool if applicable, then exit
142 020f4a9e Vangelis Koukis
        gracefully.
143 020f4a9e Vangelis Koukis

144 020f4a9e Vangelis Koukis
        """
145 020f4a9e Vangelis Koukis
        # Make sure all greenlets are dead, then clean them up
146 020f4a9e Vangelis Koukis
        self.debug("Cleaning up %d workers", len(self.workers))
147 020f4a9e Vangelis Koukis
        for g in self.workers:
148 020f4a9e Vangelis Koukis
            g.kill()
149 020f4a9e Vangelis Koukis
        gevent.joinall(self.workers)
150 020f4a9e Vangelis Koukis
        del self.workers
151 020f4a9e Vangelis Koukis
152 020f4a9e Vangelis Koukis
        self.debug("Cleaning up sockets")
153 138d0e8b Faidon Liambotis
        while self.listeners:
154 138d0e8b Faidon Liambotis
            self.listeners.pop().close()
155 138d0e8b Faidon Liambotis
        if self.server:
156 138d0e8b Faidon Liambotis
            self.server.close()
157 138d0e8b Faidon Liambotis
        if self.client:
158 138d0e8b Faidon Liambotis
            self.client.close()
159 138d0e8b Faidon Liambotis
160 f6eb1be8 Vangelis Koukis
        # Reintroduce the port number of the client socket in
161 f6eb1be8 Vangelis Koukis
        # the port pool, if applicable.
162 f6eb1be8 Vangelis Koukis
        if not self.pool is None:
163 f6eb1be8 Vangelis Koukis
            self.pool.append(self.sport)
164 f6eb1be8 Vangelis Koukis
            self.debug("Returned port %d to port pool, contains %d ports",
165 f6eb1be8 Vangelis Koukis
                       self.sport, len(self.pool))
166 f6eb1be8 Vangelis Koukis
167 020f4a9e Vangelis Koukis
        self.info("Cleaned up connection, all done")
168 138d0e8b Faidon Liambotis
        raise gevent.GreenletExit
169 138d0e8b Faidon Liambotis
170 138d0e8b Faidon Liambotis
    def __str__(self):
171 31965126 Vangelis Koukis
        return "VncAuthProxy: %d -> %s:%d" % (self.sport, self.daddr,
172 31965126 Vangelis Koukis
                                              self.dport)
173 138d0e8b Faidon Liambotis
174 138d0e8b Faidon Liambotis
    def _forward(self, source, dest):
175 138d0e8b Faidon Liambotis
        """
176 138d0e8b Faidon Liambotis
        Forward traffic from source to dest
177 138d0e8b Faidon Liambotis

178 138d0e8b Faidon Liambotis
        @type source: socket
179 138d0e8b Faidon Liambotis
        @param source: source socket
180 138d0e8b Faidon Liambotis
        @type dest: socket
181 138d0e8b Faidon Liambotis
        @param dest: destination socket
182 138d0e8b Faidon Liambotis

183 138d0e8b Faidon Liambotis
        """
184 138d0e8b Faidon Liambotis
185 138d0e8b Faidon Liambotis
        while True:
186 138d0e8b Faidon Liambotis
            d = source.recv(16384)
187 138d0e8b Faidon Liambotis
            if d == '':
188 138d0e8b Faidon Liambotis
                if source == self.client:
189 138d0e8b Faidon Liambotis
                    self.info("Client connection closed")
190 138d0e8b Faidon Liambotis
                else:
191 138d0e8b Faidon Liambotis
                    self.info("Server connection closed")
192 138d0e8b Faidon Liambotis
                break
193 138d0e8b Faidon Liambotis
            dest.sendall(d)
194 138d0e8b Faidon Liambotis
        # No need to close the source and dest sockets here.
195 138d0e8b Faidon Liambotis
        # They are owned by and will be closed by the original greenlet.
196 138d0e8b Faidon Liambotis
197 512c571e Stratos Psomadakis
    def _client_handshake(self):
198 138d0e8b Faidon Liambotis
        """
199 138d0e8b Faidon Liambotis
        Perform handshake/authentication with a connecting client
200 138d0e8b Faidon Liambotis

201 138d0e8b Faidon Liambotis
        Outline:
202 138d0e8b Faidon Liambotis
        1. Client connects
203 31965126 Vangelis Koukis
        2. We fake RFB 3.8 protocol and require VNC authentication
204 31965126 Vangelis Koukis
           [processing also supports RFB 3.3]
205 138d0e8b Faidon Liambotis
        3. Client accepts authentication method
206 138d0e8b Faidon Liambotis
        4. We send an authentication challenge
207 138d0e8b Faidon Liambotis
        5. Client sends the authentication response
208 138d0e8b Faidon Liambotis
        6. We check the authentication
209 138d0e8b Faidon Liambotis

210 512c571e Stratos Psomadakis
        Upon return, self.client socket is connected to the client.
211 138d0e8b Faidon Liambotis

212 138d0e8b Faidon Liambotis
        """
213 138d0e8b Faidon Liambotis
        self.client.send(rfb.RFB_VERSION_3_8 + "\n")
214 138d0e8b Faidon Liambotis
        client_version_str = self.client.recv(1024)
215 138d0e8b Faidon Liambotis
        client_version = rfb.check_version(client_version_str)
216 138d0e8b Faidon Liambotis
        if not client_version:
217 c87d99e9 Vangelis Koukis
            self.error("Invalid version: %s", client_version_str)
218 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
219 138d0e8b Faidon Liambotis
220 138d0e8b Faidon Liambotis
        # Both for RFB 3.3 and 3.8
221 138d0e8b Faidon Liambotis
        self.debug("Requesting authentication")
222 138d0e8b Faidon Liambotis
        auth_request = rfb.make_auth_request(rfb.RFB_AUTHTYPE_VNC,
223 31965126 Vangelis Koukis
                                             version=client_version)
224 138d0e8b Faidon Liambotis
        self.client.send(auth_request)
225 138d0e8b Faidon Liambotis
226 138d0e8b Faidon Liambotis
        # The client gets to propose an authtype only for RFB 3.8
227 138d0e8b Faidon Liambotis
        if client_version == rfb.RFB_VERSION_3_8:
228 138d0e8b Faidon Liambotis
            res = self.client.recv(1024)
229 138d0e8b Faidon Liambotis
            type = rfb.parse_client_authtype(res)
230 138d0e8b Faidon Liambotis
            if type == rfb.RFB_AUTHTYPE_ERROR:
231 c87d99e9 Vangelis Koukis
                self.warn("Client refused authentication: %s", res[1:])
232 138d0e8b Faidon Liambotis
            else:
233 c87d99e9 Vangelis Koukis
                self.debug("Client requested authtype %x", type)
234 138d0e8b Faidon Liambotis
235 138d0e8b Faidon Liambotis
            if type != rfb.RFB_AUTHTYPE_VNC:
236 c87d99e9 Vangelis Koukis
                self.error("Wrong auth type: %d", type)
237 138d0e8b Faidon Liambotis
                self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
238 138d0e8b Faidon Liambotis
                raise gevent.GreenletExit
239 39840bd3 Vangelis Koukis
240 138d0e8b Faidon Liambotis
        # Generate the challenge
241 138d0e8b Faidon Liambotis
        challenge = os.urandom(16)
242 138d0e8b Faidon Liambotis
        self.client.send(challenge)
243 138d0e8b Faidon Liambotis
        response = self.client.recv(1024)
244 138d0e8b Faidon Liambotis
        if len(response) != 16:
245 c87d99e9 Vangelis Koukis
            self.error("Wrong response length %d, should be 16", len(response))
246 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
247 138d0e8b Faidon Liambotis
248 5a196d84 Vangelis Koukis
        if rfb.check_password(challenge, response, self.password):
249 c87d99e9 Vangelis Koukis
            self.debug("Authentication successful")
250 138d0e8b Faidon Liambotis
        else:
251 138d0e8b Faidon Liambotis
            self.warn("Authentication failed")
252 138d0e8b Faidon Liambotis
            self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
253 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
254 138d0e8b Faidon Liambotis
255 138d0e8b Faidon Liambotis
        # Accept the authentication
256 138d0e8b Faidon Liambotis
        self.client.send(rfb.to_u32(rfb.RFB_AUTH_SUCCESS))
257 39840bd3 Vangelis Koukis
258 138d0e8b Faidon Liambotis
    def _run(self):
259 138d0e8b Faidon Liambotis
        try:
260 c87d99e9 Vangelis Koukis
            self.info("Waiting for a client to connect at %s",
261 c87d99e9 Vangelis Koukis
                      ", ".join(["%s:%d" % s.getsockname()[:2]
262 c87d99e9 Vangelis Koukis
                                 for s in self.listeners]))
263 5a196d84 Vangelis Koukis
            rlist, _, _ = select(self.listeners, [], [], timeout=self.timeout)
264 138d0e8b Faidon Liambotis
265 138d0e8b Faidon Liambotis
            if not rlist:
266 c87d99e9 Vangelis Koukis
                self.info("Timed out, no connection after %d sec",
267 c87d99e9 Vangelis Koukis
                          self.timeout)
268 138d0e8b Faidon Liambotis
                raise gevent.GreenletExit
269 138d0e8b Faidon Liambotis
270 138d0e8b Faidon Liambotis
            for sock in rlist:
271 138d0e8b Faidon Liambotis
                self.client, addrinfo = sock.accept()
272 d5705e2c Vangelis Koukis
                self.info("Connection from %s:%d", *addrinfo[:2])
273 138d0e8b Faidon Liambotis
274 31965126 Vangelis Koukis
                # Close all listening sockets, we only want a one-shot
275 31965126 Vangelis Koukis
                # connection from a single client.
276 138d0e8b Faidon Liambotis
                while self.listeners:
277 138d0e8b Faidon Liambotis
                    self.listeners.pop().close()
278 138d0e8b Faidon Liambotis
                break
279 39840bd3 Vangelis Koukis
280 512c571e Stratos Psomadakis
            # Perform RFB handshake with the client.
281 512c571e Stratos Psomadakis
            self._client_handshake()
282 138d0e8b Faidon Liambotis
283 138d0e8b Faidon Liambotis
            # Bridge both connections through two "forwarder" greenlets.
284 020f4a9e Vangelis Koukis
            # This greenlet will wait until any of the workers dies.
285 020f4a9e Vangelis Koukis
            # Final cleanup will take place in _cleanup().
286 020f4a9e Vangelis Koukis
            dead = gevent.event.Event()
287 020f4a9e Vangelis Koukis
            dead.clear()
288 020f4a9e Vangelis Koukis
289 020f4a9e Vangelis Koukis
            # This callback will get called if any of the two workers dies.
290 020f4a9e Vangelis Koukis
            def callback(g):
291 020f4a9e Vangelis Koukis
                self.debug("Worker %d/%d died", self.workers.index(g),
292 020f4a9e Vangelis Koukis
                           len(self.workers))
293 020f4a9e Vangelis Koukis
                dead.set()
294 020f4a9e Vangelis Koukis
295 020f4a9e Vangelis Koukis
            self.workers.append(gevent.spawn(self._forward,
296 020f4a9e Vangelis Koukis
                                             self.client, self.server))
297 020f4a9e Vangelis Koukis
            self.workers.append(gevent.spawn(self._forward,
298 020f4a9e Vangelis Koukis
                                             self.server, self.client))
299 020f4a9e Vangelis Koukis
            for g in self.workers:
300 020f4a9e Vangelis Koukis
                g.link(callback)
301 020f4a9e Vangelis Koukis
302 020f4a9e Vangelis Koukis
            # Wait until any of the workers dies
303 020f4a9e Vangelis Koukis
            self.debug("Waiting for any of %d workers to die",
304 020f4a9e Vangelis Koukis
                       len(self.workers))
305 020f4a9e Vangelis Koukis
            dead.wait()
306 020f4a9e Vangelis Koukis
307 020f4a9e Vangelis Koukis
            # We can go now, _cleanup() will take care of
308 020f4a9e Vangelis Koukis
            # all worker, socket and port cleanup
309 020f4a9e Vangelis Koukis
            self.debug("A forwarder died, our work here is done")
310 138d0e8b Faidon Liambotis
            raise gevent.GreenletExit
311 138d0e8b Faidon Liambotis
        except Exception, e:
312 138d0e8b Faidon Liambotis
            # Any unhandled exception in the previous block
313 138d0e8b Faidon Liambotis
            # is an error and must be logged accordingly
314 138d0e8b Faidon Liambotis
            if not isinstance(e, gevent.GreenletExit):
315 c87d99e9 Vangelis Koukis
                self.exception(e)
316 138d0e8b Faidon Liambotis
            raise e
317 138d0e8b Faidon Liambotis
        finally:
318 138d0e8b Faidon Liambotis
            self._cleanup()
319 138d0e8b Faidon Liambotis
320 c87d99e9 Vangelis Koukis
# Logging support inside VncAuthproxy
321 c87d99e9 Vangelis Koukis
# Wrap all common logging functions in logging-specific methods
322 c87d99e9 Vangelis Koukis
for funcname in ["info", "debug", "warn", "error", "critical",
323 c87d99e9 Vangelis Koukis
                 "exception"]:
324 c87d99e9 Vangelis Koukis
    def gen(funcname):
325 c87d99e9 Vangelis Koukis
        def wrapped_log_func(self, *args, **kwargs):
326 c87d99e9 Vangelis Koukis
            func = getattr(self.log, funcname)
327 c87d99e9 Vangelis Koukis
            func("[C%d] %s" % (self.id, args[0]), *args[1:], **kwargs)
328 c87d99e9 Vangelis Koukis
        return wrapped_log_func
329 c87d99e9 Vangelis Koukis
    setattr(VncAuthProxy, funcname, gen(funcname))
330 c87d99e9 Vangelis Koukis
331 138d0e8b Faidon Liambotis
332 138d0e8b Faidon Liambotis
def fatal_signal_handler(signame):
333 c87d99e9 Vangelis Koukis
    logger.info("Caught %s, will raise SystemExit", signame)
334 138d0e8b Faidon Liambotis
    raise SystemExit
335 138d0e8b Faidon Liambotis
336 31965126 Vangelis Koukis
337 138d0e8b Faidon Liambotis
def get_listening_sockets(sport):
338 138d0e8b Faidon Liambotis
    sockets = []
339 138d0e8b Faidon Liambotis
340 138d0e8b Faidon Liambotis
    # Use two sockets, one for IPv4, one for IPv6. IPv4-to-IPv6 mapped
341 138d0e8b Faidon Liambotis
    # addresses do not work reliably everywhere (under linux it may have
342 138d0e8b Faidon Liambotis
    # been disabled in /proc/sys/net/ipv6/bind_ipv6_only).
343 138d0e8b Faidon Liambotis
    for res in socket.getaddrinfo(None, sport, socket.AF_UNSPEC,
344 138d0e8b Faidon Liambotis
                                  socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
345 138d0e8b Faidon Liambotis
        af, socktype, proto, canonname, sa = res
346 138d0e8b Faidon Liambotis
        try:
347 138d0e8b Faidon Liambotis
            s = None
348 138d0e8b Faidon Liambotis
            s = socket.socket(af, socktype, proto)
349 138d0e8b Faidon Liambotis
            if af == socket.AF_INET6:
350 138d0e8b Faidon Liambotis
                # Bind v6 only when AF_INET6, otherwise either v4 or v6 bind
351 138d0e8b Faidon Liambotis
                # will fail.
352 138d0e8b Faidon Liambotis
                s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
353 138d0e8b Faidon Liambotis
            s.bind(sa)
354 138d0e8b Faidon Liambotis
            s.listen(1)
355 138d0e8b Faidon Liambotis
            sockets.append(s)
356 d5705e2c Vangelis Koukis
            logger.debug("Listening on %s:%d", *sa[:2])
357 138d0e8b Faidon Liambotis
        except socket.error, msg:
358 c87d99e9 Vangelis Koukis
            logger.error("Error binding to %s:%d: %s", sa[0], sa[1], msg[1])
359 138d0e8b Faidon Liambotis
            if s:
360 138d0e8b Faidon Liambotis
                s.close()
361 138d0e8b Faidon Liambotis
            while sockets:
362 138d0e8b Faidon Liambotis
                sockets.pop().close()
363 39840bd3 Vangelis Koukis
364 138d0e8b Faidon Liambotis
            # Make sure we fail immediately if we cannot get a socket
365 138d0e8b Faidon Liambotis
            raise msg
366 39840bd3 Vangelis Koukis
367 138d0e8b Faidon Liambotis
    return sockets
368 138d0e8b Faidon Liambotis
369 31965126 Vangelis Koukis
370 7eb27319 Stratos Psomadakis
def perform_server_handshake(daddr, dport, tries, retry_wait):
371 512c571e Stratos Psomadakis
    """
372 512c571e Stratos Psomadakis
    Initiate a connection with the backend server and perform basic
373 512c571e Stratos Psomadakis
    RFB 3.8 handshake with it.
374 512c571e Stratos Psomadakis

375 31965126 Vangelis Koukis
    Return a socket connected to the backend server.
376 512c571e Stratos Psomadakis

377 512c571e Stratos Psomadakis
    """
378 512c571e Stratos Psomadakis
    server = None
379 512c571e Stratos Psomadakis
380 512c571e Stratos Psomadakis
    while tries:
381 512c571e Stratos Psomadakis
        tries -= 1
382 512c571e Stratos Psomadakis
383 512c571e Stratos Psomadakis
        # Initiate server connection
384 512c571e Stratos Psomadakis
        for res in socket.getaddrinfo(daddr, dport, socket.AF_UNSPEC,
385 31965126 Vangelis Koukis
                                      socket.SOCK_STREAM, 0,
386 31965126 Vangelis Koukis
                                      socket.AI_PASSIVE):
387 512c571e Stratos Psomadakis
            af, socktype, proto, canonname, sa = res
388 512c571e Stratos Psomadakis
            try:
389 512c571e Stratos Psomadakis
                server = socket.socket(af, socktype, proto)
390 31965126 Vangelis Koukis
            except socket.error:
391 512c571e Stratos Psomadakis
                server = None
392 512c571e Stratos Psomadakis
                continue
393 512c571e Stratos Psomadakis
394 512c571e Stratos Psomadakis
            try:
395 d5705e2c Vangelis Koukis
                logger.debug("Connecting to %s:%s", *sa[:2])
396 512c571e Stratos Psomadakis
                server.connect(sa)
397 d5705e2c Vangelis Koukis
                logger.debug("Connection to %s:%s successful", *sa[:2])
398 31965126 Vangelis Koukis
            except socket.error:
399 512c571e Stratos Psomadakis
                server.close()
400 512c571e Stratos Psomadakis
                server = None
401 512c571e Stratos Psomadakis
                continue
402 512c571e Stratos Psomadakis
403 512c571e Stratos Psomadakis
            # We succesfully connected to the server
404 512c571e Stratos Psomadakis
            tries = 0
405 512c571e Stratos Psomadakis
            break
406 512c571e Stratos Psomadakis
407 512c571e Stratos Psomadakis
        # Wait and retry
408 0423d976 Vangelis Koukis
        gevent.sleep(retry_wait)
409 512c571e Stratos Psomadakis
410 512c571e Stratos Psomadakis
    if server is None:
411 512c571e Stratos Psomadakis
        raise Exception("Failed to connect to server")
412 512c571e Stratos Psomadakis
413 512c571e Stratos Psomadakis
    version = server.recv(1024)
414 512c571e Stratos Psomadakis
    if not rfb.check_version(version):
415 512c571e Stratos Psomadakis
        raise Exception("Unsupported RFB version: %s" % version.strip())
416 512c571e Stratos Psomadakis
417 512c571e Stratos Psomadakis
    server.send(rfb.RFB_VERSION_3_8 + "\n")
418 512c571e Stratos Psomadakis
419 512c571e Stratos Psomadakis
    res = server.recv(1024)
420 512c571e Stratos Psomadakis
    types = rfb.parse_auth_request(res)
421 512c571e Stratos Psomadakis
    if not types:
422 512c571e Stratos Psomadakis
        raise Exception("Error handshaking with the server")
423 512c571e Stratos Psomadakis
424 512c571e Stratos Psomadakis
    else:
425 c87d99e9 Vangelis Koukis
        logger.debug("Supported authentication types: %s",
426 c87d99e9 Vangelis Koukis
                     " ".join([str(x) for x in types]))
427 512c571e Stratos Psomadakis
428 512c571e Stratos Psomadakis
    if rfb.RFB_AUTHTYPE_NONE not in types:
429 512c571e Stratos Psomadakis
        raise Exception("Error, server demands authentication")
430 512c571e Stratos Psomadakis
431 512c571e Stratos Psomadakis
    server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))
432 512c571e Stratos Psomadakis
433 512c571e Stratos Psomadakis
    # Check authentication response
434 512c571e Stratos Psomadakis
    res = server.recv(4)
435 512c571e Stratos Psomadakis
    res = rfb.from_u32(res)
436 512c571e Stratos Psomadakis
437 512c571e Stratos Psomadakis
    if res != 0:
438 512c571e Stratos Psomadakis
        raise Exception("Authentication error")
439 512c571e Stratos Psomadakis
440 512c571e Stratos Psomadakis
    return server
441 512c571e Stratos Psomadakis
442 31965126 Vangelis Koukis
443 138d0e8b Faidon Liambotis
def parse_arguments(args):
444 138d0e8b Faidon Liambotis
    from optparse import OptionParser
445 138d0e8b Faidon Liambotis
446 138d0e8b Faidon Liambotis
    parser = OptionParser()
447 138d0e8b Faidon Liambotis
    parser.add_option("-s", "--socket", dest="ctrl_socket",
448 138d0e8b Faidon Liambotis
                      default=DEFAULT_CTRL_SOCKET,
449 138d0e8b Faidon Liambotis
                      metavar="PATH",
450 31965126 Vangelis Koukis
                      help=("UNIX socket for control connections (default: "
451 31965126 Vangelis Koukis
                            "%s" % DEFAULT_CTRL_SOCKET))
452 138d0e8b Faidon Liambotis
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
453 138d0e8b Faidon Liambotis
                      help="Enable debugging information")
454 138d0e8b Faidon Liambotis
    parser.add_option("-l", "--log", dest="log_file",
455 138d0e8b Faidon Liambotis
                      default=DEFAULT_LOG_FILE,
456 138d0e8b Faidon Liambotis
                      metavar="FILE",
457 31965126 Vangelis Koukis
                      help=("Write log to FILE instead of %s" %
458 31965126 Vangelis Koukis
                            DEFAULT_LOG_FILE))
459 138d0e8b Faidon Liambotis
    parser.add_option('--pid-file', dest="pid_file",
460 138d0e8b Faidon Liambotis
                      default=DEFAULT_PID_FILE,
461 138d0e8b Faidon Liambotis
                      metavar='PIDFILE',
462 31965126 Vangelis Koukis
                      help=("Save PID to file (default: %s)" %
463 31965126 Vangelis Koukis
                            DEFAULT_PID_FILE))
464 138d0e8b Faidon Liambotis
    parser.add_option("-t", "--connect-timeout", dest="connect_timeout",
465 31965126 Vangelis Koukis
                      default=DEFAULT_CONNECT_TIMEOUT, type="int",
466 31965126 Vangelis Koukis
                      metavar="SECONDS", help=("Wait SECONDS sec for a client "
467 31965126 Vangelis Koukis
                                               "to connect"))
468 7eb27319 Stratos Psomadakis
    parser.add_option("-r", "--connect-retries", dest="connect_retries",
469 7eb27319 Stratos Psomadakis
                      default=DEFAULT_CONNECT_RETRIES, type="int",
470 7eb27319 Stratos Psomadakis
                      metavar="RETRIES",
471 7eb27319 Stratos Psomadakis
                      help="How many times to try to connect to the server")
472 7eb27319 Stratos Psomadakis
    parser.add_option("-w", "--retry-wait", dest="retry_wait",
473 31965126 Vangelis Koukis
                      default=DEFAULT_RETRY_WAIT, type="float",
474 31965126 Vangelis Koukis
                      metavar="SECONDS", help=("Retry connection to server "
475 31965126 Vangelis Koukis
                                               "every SECONDS sec"))
476 138d0e8b Faidon Liambotis
    parser.add_option("-p", "--min-port", dest="min_port",
477 138d0e8b Faidon Liambotis
                      default=DEFAULT_MIN_PORT, type="int", metavar="MIN_PORT",
478 31965126 Vangelis Koukis
                      help=("The minimum port number to use for automatically-"
479 31965126 Vangelis Koukis
                            "allocated ephemeral ports"))
480 138d0e8b Faidon Liambotis
    parser.add_option("-P", "--max-port", dest="max_port",
481 138d0e8b Faidon Liambotis
                      default=DEFAULT_MAX_PORT, type="int", metavar="MAX_PORT",
482 31965126 Vangelis Koukis
                      help=("The maximum port number to use for automatically-"
483 31965126 Vangelis Koukis
                            "allocated ephemeral ports"))
484 138d0e8b Faidon Liambotis
485 138d0e8b Faidon Liambotis
    return parser.parse_args(args)
486 138d0e8b Faidon Liambotis
487 138d0e8b Faidon Liambotis
488 138d0e8b Faidon Liambotis
def main():
489 d5705e2c Vangelis Koukis
    """Run the daemon from the command line"""
490 138d0e8b Faidon Liambotis
491 138d0e8b Faidon Liambotis
    (opts, args) = parse_arguments(sys.argv[1:])
492 138d0e8b Faidon Liambotis
493 138d0e8b Faidon Liambotis
    # Create pidfile
494 180a750f Vangelis Koukis
    pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
495 39840bd3 Vangelis Koukis
496 138d0e8b Faidon Liambotis
    # Initialize logger
497 138d0e8b Faidon Liambotis
    lvl = logging.DEBUG if opts.debug else logging.INFO
498 88420a63 Faidon Liambotis
499 88420a63 Faidon Liambotis
    global logger
500 138d0e8b Faidon Liambotis
    logger = logging.getLogger("vncauthproxy")
501 138d0e8b Faidon Liambotis
    logger.setLevel(lvl)
502 31965126 Vangelis Koukis
    formatter = logging.Formatter(("%(asctime)s %(module)s[%(process)d] "
503 31965126 Vangelis Koukis
                                   " %(levelname)s: %(message)s"),
504 31965126 Vangelis Koukis
                                  "%Y-%m-%d %H:%M:%S")
505 138d0e8b Faidon Liambotis
    handler = logging.FileHandler(opts.log_file)
506 138d0e8b Faidon Liambotis
    handler.setFormatter(formatter)
507 138d0e8b Faidon Liambotis
    logger.addHandler(handler)
508 138d0e8b Faidon Liambotis
509 138d0e8b Faidon Liambotis
    # Become a daemon:
510 138d0e8b Faidon Liambotis
    # Redirect stdout and stderr to handler.stream to catch
511 138d0e8b Faidon Liambotis
    # early errors in the daemonization process [e.g., pidfile creation]
512 138d0e8b Faidon Liambotis
    # which will otherwise go to /dev/null.
513 376a8634 Vangelis Koukis
    daemon_context = AllFilesDaemonContext(
514 138d0e8b Faidon Liambotis
        pidfile=pidf,
515 138d0e8b Faidon Liambotis
        umask=0022,
516 138d0e8b Faidon Liambotis
        stdout=handler.stream,
517 138d0e8b Faidon Liambotis
        stderr=handler.stream,
518 138d0e8b Faidon Liambotis
        files_preserve=[handler.stream])
519 39840bd3 Vangelis Koukis
520 39840bd3 Vangelis Koukis
    # Remove any stale PID files, left behind by previous invocations
521 39840bd3 Vangelis Koukis
    if daemon.runner.is_pidfile_stale(pidf):
522 75eed2cf Vangelis Koukis
        logger.warning("Removing stale PID lock file %s", pidf.path)
523 39840bd3 Vangelis Koukis
        pidf.break_lock()
524 39840bd3 Vangelis Koukis
525 39840bd3 Vangelis Koukis
    try:
526 39840bd3 Vangelis Koukis
        daemon_context.open()
527 180a750f Vangelis Koukis
    except (AlreadyLocked, LockTimeout):
528 31965126 Vangelis Koukis
        logger.critical(("Failed to lock PID file %s, another instance "
529 31965126 Vangelis Koukis
                         "running?"), pidf.path)
530 39840bd3 Vangelis Koukis
        sys.exit(1)
531 138d0e8b Faidon Liambotis
    logger.info("Became a daemon")
532 138d0e8b Faidon Liambotis
533 138d0e8b Faidon Liambotis
    # A fork() has occured while daemonizing,
534 138d0e8b Faidon Liambotis
    # we *must* reinit gevent
535 138d0e8b Faidon Liambotis
    gevent.reinit()
536 138d0e8b Faidon Liambotis
537 138d0e8b Faidon Liambotis
    if os.path.exists(opts.ctrl_socket):
538 d5705e2c Vangelis Koukis
        logger.critical("Socket '%s' already exists", opts.ctrl_socket)
539 138d0e8b Faidon Liambotis
        sys.exit(1)
540 138d0e8b Faidon Liambotis
541 138d0e8b Faidon Liambotis
    # TODO: make this tunable? chgrp as well?
542 1c241b27 Faidon Liambotis
    old_umask = os.umask(0007)
543 138d0e8b Faidon Liambotis
544 138d0e8b Faidon Liambotis
    ctrl = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
545 138d0e8b Faidon Liambotis
    ctrl.bind(opts.ctrl_socket)
546 138d0e8b Faidon Liambotis
547 138d0e8b Faidon Liambotis
    os.umask(old_umask)
548 138d0e8b Faidon Liambotis
549 138d0e8b Faidon Liambotis
    ctrl.listen(1)
550 d5705e2c Vangelis Koukis
    logger.info("Initialized, waiting for control connections at %s",
551 d5705e2c Vangelis Koukis
                opts.ctrl_socket)
552 138d0e8b Faidon Liambotis
553 138d0e8b Faidon Liambotis
    # Catch signals to ensure graceful shutdown,
554 138d0e8b Faidon Liambotis
    # e.g., to make sure the control socket gets unlink()ed.
555 138d0e8b Faidon Liambotis
    #
556 138d0e8b Faidon Liambotis
    # Uses gevent.signal so the handler fires even during
557 138d0e8b Faidon Liambotis
    # gevent.socket.accept()
558 138d0e8b Faidon Liambotis
    gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
559 138d0e8b Faidon Liambotis
    gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
560 138d0e8b Faidon Liambotis
561 138d0e8b Faidon Liambotis
    # Init ephemeral port pool
562 39840bd3 Vangelis Koukis
    ports = range(opts.min_port, opts.max_port + 1)
563 138d0e8b Faidon Liambotis
564 138d0e8b Faidon Liambotis
    while True:
565 138d0e8b Faidon Liambotis
        try:
566 138d0e8b Faidon Liambotis
            client, addr = ctrl.accept()
567 138d0e8b Faidon Liambotis
            logger.info("New control connection")
568 39840bd3 Vangelis Koukis
569 138d0e8b Faidon Liambotis
            # Receive and parse a client request.
570 138d0e8b Faidon Liambotis
            response = {
571 138d0e8b Faidon Liambotis
                "source_port": 0,
572 138d0e8b Faidon Liambotis
                "status": "FAILED"
573 138d0e8b Faidon Liambotis
            }
574 138d0e8b Faidon Liambotis
            try:
575 138d0e8b Faidon Liambotis
                # TODO: support multiple forwardings in the same message?
576 39840bd3 Vangelis Koukis
                #
577 138d0e8b Faidon Liambotis
                # Control request, in JSON:
578 138d0e8b Faidon Liambotis
                #
579 138d0e8b Faidon Liambotis
                # {
580 31965126 Vangelis Koukis
                #     "source_port":
581 31965126 Vangelis Koukis
                #         <source port or 0 for automatic allocation>,
582 31965126 Vangelis Koukis
                #     "destination_address":
583 31965126 Vangelis Koukis
                #         <destination address of backend server>,
584 31965126 Vangelis Koukis
                #     "destination_port":
585 31965126 Vangelis Koukis
                #         <destination port>
586 31965126 Vangelis Koukis
                #     "password":
587 31965126 Vangelis Koukis
                #         <the password to use to authenticate clients>
588 138d0e8b Faidon Liambotis
                # }
589 39840bd3 Vangelis Koukis
                #
590 138d0e8b Faidon Liambotis
                # The <password> is used for MITM authentication of clients
591 31965126 Vangelis Koukis
                # connecting to <source_port>, who will subsequently be
592 31965126 Vangelis Koukis
                # forwarded to a VNC server listening at
593 31965126 Vangelis Koukis
                # <destination_address>:<destination_port>
594 138d0e8b Faidon Liambotis
                #
595 138d0e8b Faidon Liambotis
                # Control reply, in JSON:
596 138d0e8b Faidon Liambotis
                # {
597 138d0e8b Faidon Liambotis
                #     "source_port": <the allocated source port>
598 138d0e8b Faidon Liambotis
                #     "status": <one of "OK" or "FAILED">
599 138d0e8b Faidon Liambotis
                # }
600 31965126 Vangelis Koukis
                #
601 138d0e8b Faidon Liambotis
                buf = client.recv(1024)
602 138d0e8b Faidon Liambotis
                req = json.loads(buf)
603 39840bd3 Vangelis Koukis
604 138d0e8b Faidon Liambotis
                sport_orig = int(req['source_port'])
605 138d0e8b Faidon Liambotis
                daddr = req['destination_address']
606 138d0e8b Faidon Liambotis
                dport = int(req['destination_port'])
607 138d0e8b Faidon Liambotis
                password = req['password']
608 138d0e8b Faidon Liambotis
            except Exception, e:
609 d5705e2c Vangelis Koukis
                logger.warn("Malformed request: %s", buf)
610 138d0e8b Faidon Liambotis
                client.send(json.dumps(response))
611 138d0e8b Faidon Liambotis
                client.close()
612 138d0e8b Faidon Liambotis
                continue
613 39840bd3 Vangelis Koukis
614 138d0e8b Faidon Liambotis
            # Spawn a new Greenlet to service the request.
615 512c571e Stratos Psomadakis
            server = None
616 138d0e8b Faidon Liambotis
            try:
617 138d0e8b Faidon Liambotis
                # If the client has so indicated, pick an ephemeral source port
618 138d0e8b Faidon Liambotis
                # randomly, and remove it from the port pool.
619 138d0e8b Faidon Liambotis
                if sport_orig == 0:
620 138d0e8b Faidon Liambotis
                    sport = random.choice(ports)
621 138d0e8b Faidon Liambotis
                    ports.remove(sport)
622 d5705e2c Vangelis Koukis
                    logger.debug("Got port %d from pool, %d remaining",
623 d5705e2c Vangelis Koukis
                                 sport, len(ports))
624 138d0e8b Faidon Liambotis
                    pool = ports
625 138d0e8b Faidon Liambotis
                else:
626 138d0e8b Faidon Liambotis
                    sport = sport_orig
627 138d0e8b Faidon Liambotis
                    pool = None
628 512c571e Stratos Psomadakis
629 138d0e8b Faidon Liambotis
                listeners = get_listening_sockets(sport)
630 7eb27319 Stratos Psomadakis
                server = perform_server_handshake(daddr, dport,
631 31965126 Vangelis Koukis
                                                  opts.connect_retries,
632 31965126 Vangelis Koukis
                                                  opts.retry_wait)
633 512c571e Stratos Psomadakis
634 138d0e8b Faidon Liambotis
                VncAuthProxy.spawn(logger, listeners, pool, daddr, dport,
635 31965126 Vangelis Koukis
                                   server, password, opts.connect_timeout)
636 31965126 Vangelis Koukis
637 d5705e2c Vangelis Koukis
                logger.info("New forwarding: %d (client req'd: %d) -> %s:%d",
638 d5705e2c Vangelis Koukis
                            sport, sport_orig, daddr, dport)
639 31965126 Vangelis Koukis
                response = {"source_port": sport,
640 31965126 Vangelis Koukis
                            "status": "OK"}
641 138d0e8b Faidon Liambotis
            except IndexError:
642 31965126 Vangelis Koukis
                logger.error(("FAILED forwarding, out of ports for [req'd by "
643 d5705e2c Vangelis Koukis
                              "client: %d -> %s:%d]"),
644 d5705e2c Vangelis Koukis
                             sport_orig, daddr, dport)
645 512c571e Stratos Psomadakis
            except Exception, msg:
646 512c571e Stratos Psomadakis
                logger.error(msg)
647 31965126 Vangelis Koukis
                logger.error(("FAILED forwarding: %d (client req'd: %d) -> "
648 d5705e2c Vangelis Koukis
                              "%s:%d"), sport, sport_orig, daddr, dport)
649 138d0e8b Faidon Liambotis
                if not pool is None:
650 138d0e8b Faidon Liambotis
                    pool.append(sport)
651 d5705e2c Vangelis Koukis
                    logger.debug("Returned port %d to pool, %d remanining",
652 d5705e2c Vangelis Koukis
                                 sport, len(pool))
653 512c571e Stratos Psomadakis
                if not server is None:
654 512c571e Stratos Psomadakis
                    server.close()
655 138d0e8b Faidon Liambotis
            finally:
656 138d0e8b Faidon Liambotis
                client.send(json.dumps(response))
657 138d0e8b Faidon Liambotis
                client.close()
658 138d0e8b Faidon Liambotis
        except Exception, e:
659 138d0e8b Faidon Liambotis
            logger.exception(e)
660 138d0e8b Faidon Liambotis
            continue
661 138d0e8b Faidon Liambotis
        except SystemExit:
662 138d0e8b Faidon Liambotis
            break
663 39840bd3 Vangelis Koukis
664 d5705e2c Vangelis Koukis
    logger.info("Unlinking control socket at %s", opts.ctrl_socket)
665 138d0e8b Faidon Liambotis
    os.unlink(opts.ctrl_socket)
666 138d0e8b Faidon Liambotis
    daemon_context.close()
667 138d0e8b Faidon Liambotis
    sys.exit(0)