Revision 5da701ba

b/vapclient.py
1
#!/usr/bin/env python
2

  
3
import sys
4
import socket
5

  
6
CTRL_SOCKET = "/tmp/vncproxy.sock"
7

  
8
def request_forwarding(sport, daddr, dport, password):
9
    sport = str(int(sport))
10
    dport = str(int(dport))
11
    assert(len(password) > 0)
12

  
13
    request = ":".join([sport, daddr, dport, password])
14

  
15
    ctrl = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
16

  
17
    ctrl.connect(CTRL_SOCKET)
18
    ctrl.send(request)
19
    response = ctrl.recv(1024)
20
    if response == "OK":
21
        return True
22
    else:
23
        return False
24

  
25
if __name__ == '__main__':
26
    request_forwarding(*sys.argv[1:])
b/vncauthproxy.py
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 VncAuthProxy(gevent.Greenlet):
33
    """
34
    Simple class implementing a VNC Forwarder with MITM authentication as a
35
    Greenlet
36

  
37
    VncAuthProxy 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 = VncAuthProxy.id
65
        VncAuthProxy.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 "VncAuthProxy: %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
        VncAuthProxy.spawn(sport, daddr, dport, password, opts.connect_timeout)
369
        client.close()
370

  
371
    os.unlink(opts.ctrl_socket)
372
    sys.exit(0)
/dev/null
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)
/dev/null
1
#!/usr/bin/env python
2

  
3
import sys
4
import socket
5

  
6
CTRL_SOCKET = "/tmp/vncproxy.sock"
7

  
8
def request_forwarding(sport, daddr, dport, password):
9
    sport = str(int(sport))
10
    dport = str(int(dport))
11
    assert(len(password) > 0)
12

  
13
    request = ":".join([sport, daddr, dport, password])
14

  
15
    ctrl = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
16

  
17
    ctrl.connect(CTRL_SOCKET)
18
    ctrl.send(request)
19
    response = ctrl.recv(1024)
20
    if response == "OK":
21
        return True
22
    else:
23
        return False
24

  
25
if __name__ == '__main__':
26
    request_forwarding(*sys.argv[1:])

Also available in: Unified diff