Statistics
| Branch: | Tag: | Revision:

root / vncproxy.py @ eeb14dde

History | View | Annotate | Download (12.4 kB)

1
#!/usr/bin/env python
2
#
3

    
4
# Copyright (c) 2010 Apollon Oikonomopoulos
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
import os
23
import sys
24
import logging
25
import gevent
26

    
27
import rfb
28

    
29
from gevent import socket
30
from gevent.select import select
31

    
32
class VncForwarder(gevent.Greenlet):
33
    """
34
    Simple class implementing a VNC Forwarder with MITM authentication as a
35
    Greenlet
36

37
    VncForwarder forwards VNC traffic from a specified port of the local host
38
    to a specified remote host:port. Furthermore, it implements VNC
39
    Authentication, intercepting the client/server handshake and asking the
40
    client for authentication even if the backend requires none.
41

42
    It is primarily intended for use in virtualization environments, as a VNC
43
    ``switch''.
44

45
    """
46
    id = 1
47

    
48
    def __init__(self, sport, daddr, dport, password, connect_timeout=30):
49
        """
50
        @type sport: int
51
        @param sport: source port
52
        @type daddr: str
53
        @param daddr: destination address (IPv4, IPv6 or hostname)
54
        @type dport: int
55
        @param dport: destination port
56
        @type password: str
57
        @param password: password to request from the client
58
        @type connect_timeout: int
59
        @param connect_timeout: how long to wait for client connections
60
                                (seconds)
61

62
        """
63
        gevent.Greenlet.__init__(self)
64
        self.id = VncForwarder.id
65
        VncForwarder.id += 1
66
        self.sport = sport
67
        self.daddr = daddr
68
        self.dport = dport
69
        self.password = password
70
        self.log = logging
71
        self.server = None
72
        self.client = None
73
        self.timeout = connect_timeout
74

    
75
    def _cleanup(self):
76
        """Close all active sockets and exit gracefully"""
77
        if self.server:
78
            self.server.close()
79
        if self.client:
80
            self.client.close()
81
        raise gevent.GreenletExit
82

    
83
    def info(self, msg):
84
        logging.info("[C%d] %s" % (self.id, msg))
85

    
86
    def debug(self, msg):
87
        logging.debug("[C%d] %s" % (self.id, msg))
88

    
89
    def warn(self, msg):
90
        logging.warn("[C%d] %s" % (self.id, msg))
91

    
92
    def error(self, msg):
93
        logging.error("[C%d] %s" % (self.id, msg))
94

    
95
    def critical(self, msg):
96
        logging.critical("[C%d] %s" % (self.id, msg))
97

    
98
    def __str__(self):
99
        return "VncForwarder: %d -> %s:%d" % (self.sport, self.daddr, self.dport)
100

    
101
    def _forward(self, source, dest):
102
        """
103
        Forward traffic from source to dest
104

105
        @type source: socket
106
        @param source: source socket
107
        @type dest: socket
108
        @param dest: destination socket
109

110
        """
111

    
112
        while True:
113
            d = source.recv(8096)
114
            if d == '':
115
                if source == self.client:
116
                    self.info("Client connection closed")
117
                else:
118
                    self.info("Server connection closed")
119
                break
120
            dest.sendall(d)
121
        source.close()
122
        dest.close()
123

    
124

    
125
    def _handshake(self):
126
        """
127
        Perform handshake/authentication with a connecting client
128

129
        Outline:
130
        1. Client connects
131
        2. We fake RFB 3.8 protocol and require VNC authentication
132
        3. Client accepts authentication method
133
        4. We send an authentication challenge
134
        5. Client sends the authentication response
135
        6. We check the authentication
136
        7. We initiate a connection with the backend server and perform basic
137
           RFB 3.8 handshake with it.
138
        8. If the above is successful, "bridge" both connections through two
139
           "fowrarder" greenlets.
140

141
        """
142
        self.client.send(rfb.RFB_VERSION_3_8 + "\n")
143
        client_version = self.client.recv(1024)
144
        if not rfb.check_version(client_version):
145
            self.error("Invalid version: %s" % client_version)
146
            self._cleanup()
147
        self.debug("Requesting authentication")
148
        auth_request = rfb.make_auth_request(rfb.RFB_AUTHTYPE_VNC)
149
        self.client.send(auth_request)
150
        res = self.client.recv(1024)
151
        type = rfb.parse_client_authtype(res)
152
        if type == rfb.RFB_AUTHTYPE_ERROR:
153
            self.warn("Client refused authentication: %s" % res[1:])
154
        else:
155
            self.debug("Client requested authtype %x" % type)
156

    
157
        if type != rfb.RFB_AUTHTYPE_VNC:
158
            self.error("Wrong auth type: %d" % type)
159
            self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
160
            self._cleanup()
161

    
162
        # Generate the challenge
163
        challenge = os.urandom(16)
164
        self.client.send(challenge)
165
        response = self.client.recv(1024)
166
        if len(response) != 16:
167
            self.error("Wrong response length %d, should be 16" % len(response))
168
            self._cleanup()
169

    
170
        if rfb.check_password(challenge, response, password):
171
            self.debug("Authentication successful!")
172
        else:
173
            self.warn("Authentication failed")
174
            self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
175
            self._cleanup()
176

    
177
        # Accept the authentication
178
        self.client.send(rfb.to_u32(rfb.RFB_AUTH_SUCCESS))
179

    
180
        # Initiate server connection
181
        for res in socket.getaddrinfo(self.daddr, self.dport, socket.AF_UNSPEC,
182
                                      socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
183
            af, socktype, proto, canonname, sa = res
184
            try:
185
                self.server = socket.socket(af, socktype, proto)
186
            except socket.error, msg:
187
                self.server = None
188
                continue;
189

    
190
            try:
191
                self.debug("Connecting to %s:%s" % sa[:2])
192
                self.server.connect(sa)
193
                self.debug("Connection to %s:%s successful" % sa[:2])
194
            except socket.error, msg:
195
                self.server.close()
196
                self.server = None
197
                continue;
198

    
199
            break
200

    
201
        if self.server is None:
202
            self.error("Failed to connect to server")
203
            self._cleanup()
204

    
205
        version = self.server.recv(1024)
206
        if not rfb.check_version(version):
207
            self.error("Unsupported RFB version: %s" % version.strip())
208
            self._cleanup()
209

    
210
        self.server.send(rfb.RFB_VERSION_3_8 + "\n")
211

    
212
        res = self.server.recv(1024)
213
        types = rfb.parse_auth_request(res)
214
        if not types:
215
            self.error("Error handshaking with the server")
216
            self._cleanup()
217

    
218
        else:
219
            self.debug("Supported authentication types: %s" %
220
                           " ".join([str(x) for x in types]))
221

    
222
        if rfb.RFB_AUTHTYPE_NONE not in types:
223
            self.error("Error, server demands authentication")
224
            self._cleanup()
225

    
226
        self.server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))
227

    
228
        # Check authentication response
229
        res = self.server.recv(4)
230
        res = rfb.from_u32(res)
231

    
232
        if res != 0:
233
            self.error("Authentication error")
234
            self._cleanup()
235

    
236
        # Bridge client/server connections
237
        self.workers = [gevent.spawn(self._forward, self.client, self.server),
238
                        gevent.spawn(self._forward, self.server, self.client)]
239
        gevent.joinall(self.workers)
240

    
241
        del self.workers
242
        self._cleanup()
243

    
244
    def _run(self):
245
        sockets = []
246

    
247
        # Use two sockets, one for IPv4, one for IPv6. IPv4-to-IPv6 mapped
248
        # addresses do not work reliably everywhere (under linux it may have
249
        # been disabled in /proc/sys/net/ipv6/bind_ipv6_only).
250
        for res in socket.getaddrinfo(None, self.sport, socket.AF_UNSPEC,
251
                                      socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
252
            af, socktype, proto, canonname, sa = res
253
            try:
254
                s = socket.socket(af, socktype, proto)
255
                if af == socket.AF_INET6:
256
                    # Bind v6 only when AF_INET6, otherwise either v4 or v6 bind
257
                    # will fail.
258
                    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
259
            except socket.error, msg:
260
                s = None
261
                continue;
262

    
263
            try:
264
                s.bind(sa)
265
                s.listen(1)
266
                self.debug("Listening on %s:%d" % sa[:2])
267
            except socket.error, msg:
268
                self.error("Error binding to %s:%d: %s" %
269
                               (sa[0], sa[1], msg[1]))
270
                s.close()
271
                s = None
272
                continue
273

    
274
            if s:
275
                sockets.append(s)
276

    
277
        if not sockets:
278
            self.error("Failed to listen for connections")
279
            self._cleanup()
280

    
281
        self.log.debug("Waiting for client to connect")
282
        rlist, _, _ = select(sockets, [], [], timeout=self.timeout)
283

    
284
        if not rlist:
285
            self.info("Timed out, no connection after %d sec" % self.timeout)
286
            self._cleanup()
287

    
288
        for sock in rlist:
289
            self.client, addrinfo = sock.accept()
290
            self.info("Connection from %s:%d" % addrinfo[:2])
291

    
292
            # Close all listening sockets, we only want a one-shot connection
293
            # from a single client.
294
            for listener in sockets:
295
                listener.close()
296
            break
297

    
298
        self._handshake()
299

    
300

    
301
if __name__ == '__main__':
302
    from optparse import OptionParser
303

    
304
    parser = OptionParser()
305
    parser.add_option("-s", "--socket", dest="ctrl_socket",
306
                      help="UNIX socket path for control connections",
307
                      default="/tmp/vncproxy.sock",
308
                      metavar="PATH")
309
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
310
                      help="Enable debugging information")
311
    parser.add_option("-l", "--log", dest="logfile", default=None,
312
                      help="Write log to FILE instead of stdout",
313
                      metavar="FILE")
314
    parser.add_option("-t", "--connect-timeout", dest="connect_timeout",
315
                      default=30, type="int", metavar="SECONDS",
316
                      help="How long to listen for clients to forward")
317

    
318
    (opts, args) = parser.parse_args(sys.argv[1:])
319

    
320
    lvl = logging.DEBUG if opts.debug else logging.INFO
321

    
322
    logging.basicConfig(level=lvl, filename=opts.logfile,
323
                        format="%(asctime)s %(levelname)s: %(message)s",
324
                        datefmt="%m/%d/%Y %H:%M:%S")
325

    
326
    if os.path.exists(opts.ctrl_socket):
327
        logging.critical("Socket '%s' already exists" % opts.ctrl_socket)
328
        sys.exit(1)
329

    
330
    # TODO: make this tunable? chgrp as well?
331
    old_umask = os.umask(0077)
332

    
333
    ctrl = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
334
    ctrl.bind(opts.ctrl_socket)
335

    
336
    os.umask(old_umask)
337

    
338
    ctrl.listen(1)
339
    logging.info("Initalized, waiting for control connections at %s" %
340
                 opts.ctrl_socket)
341

    
342
    while True:
343
        try:
344
            client, addr = ctrl.accept()
345
        except KeyboardInterrupt:
346
            break
347

    
348
        logging.info("New control connection")
349
        line = client.recv(1024).strip()
350
        try:
351
            # Control message format:
352
            # TODO: make this json-based?
353
            # TODO: support multiple forwardings in the same message?
354
            # <source_port>:<destination_address>:<destination_port>:<password>
355
            # <password> will be used for MITM authentication of clients
356
            # connecting to <source_port>, who will subsequently be forwarded
357
            # to a VNC server at <destination_address>:<destination_port>
358
            sport, daddr, dport, password = line.split(':', 3)
359
            logging.info("New forwarding [%d -> %s:%d]" %
360
                         (int(sport), daddr, int(dport)))
361
        except:
362
            logging.warn("Malformed request: %s" % line)
363
            client.send("FAILED\n")
364
            client.close()
365
            continue
366

    
367
        client.send("OK\n")
368
        VncForwarder.spawn(sport, daddr, dport, password, opts.connect_timeout)
369
        client.close()
370

    
371
    os.unlink(opts.ctrl_socket)
372
    sys.exit(0)