Statistics
| Branch: | Tag: | Revision:

root / vncauthproxy / proxy.py @ 86d1202e

History | View | Annotate | Download (24.2 kB)

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

    
23
DEFAULT_CTRL_SOCKET = "/var/run/vncauthproxy/ctrl.sock"
24
DEFAULT_LOG_FILE = "/var/log/vncauthproxy/vncauthproxy.log"
25
DEFAULT_PID_FILE = "/var/run/vncauthproxy/vncauthproxy.pid"
26
DEFAULT_CONNECT_TIMEOUT = 30
27
DEFAULT_CONNECT_RETRIES = 3
28
DEFAULT_RETRY_WAIT = 0.1
29
DEFAULT_BACKLOG = 256
30
DEFAULT_SOCK_TIMEOUT = 60.0
31
# We must take care not to fall into the ephemeral port range,
32
# this can lead to transient failures to bind a chosen port.
33
#
34
# By default, Linux uses 32768 to 61000, see:
35
# http://www.ncftp.com/ncftpd/doc/misc/ephemeral_ports.html#Linux
36
# so 25000-30000 seems to be a sensible default.
37
DEFAULT_MIN_PORT = 25000
38
DEFAULT_MAX_PORT = 30000
39

    
40
import os
41
import sys
42
import logging
43
import gevent
44
import gevent.event
45
import daemon
46
import random
47
import daemon.runner
48

    
49
import rfb
50

    
51
try:
52
    import simplejson as json
53
except ImportError:
54
    import json
55

    
56
from gevent import socket
57
from signal import SIGINT, SIGTERM
58
from gevent.select import select
59

    
60
from lockfile import LockTimeout, AlreadyLocked
61
# Take care of differences between python-daemon versions.
62
try:
63
    from daemon import pidfile as pidlockfile
64
except:
65
    from daemon import pidlockfile
66

    
67

    
68
logger = None
69

    
70

    
71
# Currently, gevent uses libevent-dns for asynchronous DNS resolution,
72
# which opens a socket upon initialization time. Since we can't get the fd
73
# reliably, We have to maintain all file descriptors open (which won't harm
74
# anyway)
75
class AllFilesDaemonContext(daemon.DaemonContext):
76
    """DaemonContext class keeping all file descriptors open"""
77
    def _get_exclude_file_descriptors(self):
78
        class All:
79
            def __contains__(self, value):
80
                return True
81
        return All()
82

    
83

    
84
class VncAuthProxy(gevent.Greenlet):
85
    """
86
    Simple class implementing a VNC Forwarder with MITM authentication as a
87
    Greenlet
88

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

94
    It is primarily intended for use in virtualization environments, as a VNC
95
    ``switch''.
96

97
    """
98
    id = 1
99

    
100
    def __init__(self, logger, listeners, pool, daddr, dport, server, password,
101
                 connect_timeout):
102
        """
103
        @type logger: logging.Logger
104
        @param logger: the logger to use
105
        @type listeners: list
106
        @param listeners: list of listening sockets to use for clients
107
        @type pool: list
108
        @param pool: if not None, return the client number into this port pool
109
        @type daddr: str
110
        @param daddr: destination address (IPv4, IPv6 or hostname)
111
        @type dport: int
112
        @param dport: destination port
113
        @type server: socket
114
        @param server: VNC server socket
115
        @type password: str
116
        @param password: password to request from the client
117
        @type connect_timeout: int
118
        @param connect_timeout: how long to wait for client connections
119
                                (seconds)
120

121
        """
122
        gevent.Greenlet.__init__(self)
123
        self.id = VncAuthProxy.id
124
        VncAuthProxy.id += 1
125
        self.log = logger
126
        self.listeners = listeners
127
        # A list of worker/forwarder greenlets, one for each direction
128
        self.workers = []
129
        # All listening sockets are assumed to be on the same port
130
        self.sport = listeners[0].getsockname()[1]
131
        self.pool = pool
132
        self.daddr = daddr
133
        self.dport = dport
134
        self.server = server
135
        self.password = password
136
        self.client = None
137
        self.timeout = connect_timeout
138

    
139
    def _cleanup(self):
140
        """Cleanup everything: workers, sockets, ports
141

142
        Kill all remaining forwarder greenlets, close all active sockets,
143
        return the source port to the pool if applicable, then exit
144
        gracefully.
145

146
        """
147
        # Make sure all greenlets are dead, then clean them up
148
        self.debug("Cleaning up %d workers", len(self.workers))
149
        for g in self.workers:
150
            g.kill()
151
        gevent.joinall(self.workers)
152
        del self.workers
153

    
154
        self.debug("Cleaning up sockets")
155
        while self.listeners:
156
            self.listeners.pop().close()
157
        if self.server:
158
            self.server.close()
159
        if self.client:
160
            self.client.close()
161

    
162
        # Reintroduce the port number of the client socket in
163
        # the port pool, if applicable.
164
        if not self.pool is None:
165
            self.pool.append(self.sport)
166
            self.debug("Returned port %d to port pool, contains %d ports",
167
                       self.sport, len(self.pool))
168

    
169
        self.info("Cleaned up connection, all done")
170
        raise gevent.GreenletExit
171

    
172
    def __str__(self):
173
        return "VncAuthProxy: %d -> %s:%d" % (self.sport, self.daddr,
174
                                              self.dport)
175

    
176
    def _forward(self, source, dest):
177
        """
178
        Forward traffic from source to dest
179

180
        @type source: socket
181
        @param source: source socket
182
        @type dest: socket
183
        @param dest: destination socket
184

185
        """
186

    
187
        while True:
188
            d = source.recv(16384)
189
            if d == '':
190
                if source == self.client:
191
                    self.info("Client connection closed")
192
                else:
193
                    self.info("Server connection closed")
194
                break
195
            dest.sendall(d)
196
        # No need to close the source and dest sockets here.
197
        # They are owned by and will be closed by the original greenlet.
198

    
199
    def _client_handshake(self):
200
        """
201
        Perform handshake/authentication with a connecting client
202

203
        Outline:
204
        1. Client connects
205
        2. We fake RFB 3.8 protocol and require VNC authentication
206
           [processing also supports RFB 3.3]
207
        3. Client accepts authentication method
208
        4. We send an authentication challenge
209
        5. Client sends the authentication response
210
        6. We check the authentication
211

212
        Upon return, self.client socket is connected to the client.
213

214
        """
215
        self.client.send(rfb.RFB_VERSION_3_8 + "\n")
216
        client_version_str = self.client.recv(1024)
217
        client_version = rfb.check_version(client_version_str)
218
        if not client_version:
219
            self.error("Invalid version: %s", client_version_str)
220
            raise gevent.GreenletExit
221

    
222
        # Both for RFB 3.3 and 3.8
223
        self.debug("Requesting authentication")
224
        auth_request = rfb.make_auth_request(rfb.RFB_AUTHTYPE_VNC,
225
                                             version=client_version)
226
        self.client.send(auth_request)
227

    
228
        # The client gets to propose an authtype only for RFB 3.8
229
        if client_version == rfb.RFB_VERSION_3_8:
230
            res = self.client.recv(1024)
231
            type = rfb.parse_client_authtype(res)
232
            if type == rfb.RFB_AUTHTYPE_ERROR:
233
                self.warn("Client refused authentication: %s", res[1:])
234
            else:
235
                self.debug("Client requested authtype %x", type)
236

    
237
            if type != rfb.RFB_AUTHTYPE_VNC:
238
                self.error("Wrong auth type: %d", type)
239
                self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
240
                raise gevent.GreenletExit
241

    
242
        # Generate the challenge
243
        challenge = os.urandom(16)
244
        self.client.send(challenge)
245
        response = self.client.recv(1024)
246
        if len(response) != 16:
247
            self.error("Wrong response length %d, should be 16", len(response))
248
            raise gevent.GreenletExit
249

    
250
        if rfb.check_password(challenge, response, self.password):
251
            self.debug("Authentication successful")
252
        else:
253
            self.warn("Authentication failed")
254
            self.client.send(rfb.to_u32(rfb.RFB_AUTH_ERROR))
255
            raise gevent.GreenletExit
256

    
257
        # Accept the authentication
258
        self.client.send(rfb.to_u32(rfb.RFB_AUTH_SUCCESS))
259

    
260
    def _run(self):
261
        try:
262
            self.info("Waiting for a client to connect at %s",
263
                      ", ".join(["%s:%d" % s.getsockname()[:2]
264
                                 for s in self.listeners]))
265
            rlist, _, _ = select(self.listeners, [], [], timeout=self.timeout)
266

    
267
            if not rlist:
268
                self.info("Timed out, no connection after %d sec",
269
                          self.timeout)
270
                raise gevent.GreenletExit
271

    
272
            for sock in rlist:
273
                self.client, addrinfo = sock.accept()
274
                self.info("Connection from %s:%d", *addrinfo[:2])
275

    
276
                # Close all listening sockets, we only want a one-shot
277
                # connection from a single client.
278
                while self.listeners:
279
                    self.listeners.pop().close()
280
                break
281

    
282
            # Perform RFB handshake with the client.
283
            self._client_handshake()
284

    
285
            # Bridge both connections through two "forwarder" greenlets.
286
            # This greenlet will wait until any of the workers dies.
287
            # Final cleanup will take place in _cleanup().
288
            dead = gevent.event.Event()
289
            dead.clear()
290

    
291
            # This callback will get called if any of the two workers dies.
292
            def callback(g):
293
                self.debug("Worker %d/%d died", self.workers.index(g),
294
                           len(self.workers))
295
                dead.set()
296

    
297
            self.workers.append(gevent.spawn(self._forward,
298
                                             self.client, self.server))
299
            self.workers.append(gevent.spawn(self._forward,
300
                                             self.server, self.client))
301
            for g in self.workers:
302
                g.link(callback)
303

    
304
            # Wait until any of the workers dies
305
            self.debug("Waiting for any of %d workers to die",
306
                       len(self.workers))
307
            dead.wait()
308

    
309
            # We can go now, _cleanup() will take care of
310
            # all worker, socket and port cleanup
311
            self.debug("A forwarder died, our work here is done")
312
            raise gevent.GreenletExit
313
        except Exception, e:
314
            # Any unhandled exception in the previous block
315
            # is an error and must be logged accordingly
316
            if not isinstance(e, gevent.GreenletExit):
317
                self.exception(e)
318
            raise e
319
        finally:
320
            self._cleanup()
321

    
322
# Logging support inside VncAuthproxy
323
# Wrap all common logging functions in logging-specific methods
324
for funcname in ["info", "debug", "warn", "error", "critical",
325
                 "exception"]:
326

    
327
    def gen(funcname):
328
        def wrapped_log_func(self, *args, **kwargs):
329
            func = getattr(self.log, funcname)
330
            func("[C%d] %s" % (self.id, args[0]), *args[1:], **kwargs)
331
        return wrapped_log_func
332
    setattr(VncAuthProxy, funcname, gen(funcname))
333

    
334

    
335
def fatal_signal_handler(signame):
336
    logger.info("Caught %s, will raise SystemExit", signame)
337
    raise SystemExit
338

    
339

    
340
def get_listening_sockets(sport):
341
    sockets = []
342

    
343
    # Use two sockets, one for IPv4, one for IPv6. IPv4-to-IPv6 mapped
344
    # addresses do not work reliably everywhere (under linux it may have
345
    # been disabled in /proc/sys/net/ipv6/bind_ipv6_only).
346
    for res in socket.getaddrinfo(None, sport, socket.AF_UNSPEC,
347
                                  socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
348
        af, socktype, proto, canonname, sa = res
349
        try:
350
            s = None
351
            s = socket.socket(af, socktype, proto)
352
            if af == socket.AF_INET6:
353
                # Bind v6 only when AF_INET6, otherwise either v4 or v6 bind
354
                # will fail.
355
                s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
356
            s.bind(sa)
357
            s.listen(1)
358
            sockets.append(s)
359
            logger.debug("Listening on %s:%d", *sa[:2])
360
        except socket.error, msg:
361
            logger.error("Error binding to %s:%d: %s", sa[0], sa[1], msg[1])
362
            if s:
363
                s.close()
364
            while sockets:
365
                sockets.pop().close()
366

    
367
            # Make sure we fail immediately if we cannot get a socket
368
            raise msg
369

    
370
    return sockets
371

    
372

    
373
def perform_server_handshake(daddr, dport, tries, retry_wait, sock_timeout):
374
    """
375
    Initiate a connection with the backend server and perform basic
376
    RFB 3.8 handshake with it.
377

378
    Return a socket connected to the backend server.
379

380
    """
381
    server = None
382

    
383
    while tries:
384
        tries -= 1
385

    
386
        # Initiate server connection
387
        for res in socket.getaddrinfo(daddr, dport, socket.AF_UNSPEC,
388
                                      socket.SOCK_STREAM, 0,
389
                                      socket.AI_PASSIVE):
390
            af, socktype, proto, canonname, sa = res
391
            try:
392
                server = socket.socket(af, socktype, proto)
393
            except socket.error:
394
                server = None
395
                continue
396

    
397
            # Set socket timeout for the initial handshake
398
            server.settimeout(sock_timeout)
399

    
400
            try:
401
                logger.debug("Connecting to %s:%s", *sa[:2])
402
                server.connect(sa)
403
                logger.debug("Connection to %s:%s successful", *sa[:2])
404
            except socket.error:
405
                server.close()
406
                server = None
407
                continue
408

    
409
            # We succesfully connected to the server
410
            tries = 0
411
            break
412

    
413
        # Wait and retry
414
        gevent.sleep(retry_wait)
415

    
416
    if server is None:
417
        raise Exception("Failed to connect to server")
418

    
419
    version = server.recv(1024)
420
    if not rfb.check_version(version):
421
        raise Exception("Unsupported RFB version: %s" % version.strip())
422

    
423
    server.send(rfb.RFB_VERSION_3_8 + "\n")
424

    
425
    res = server.recv(1024)
426
    types = rfb.parse_auth_request(res)
427
    if not types:
428
        raise Exception("Error handshaking with the server")
429

    
430
    else:
431
        logger.debug("Supported authentication types: %s",
432
                     " ".join([str(x) for x in types]))
433

    
434
    if rfb.RFB_AUTHTYPE_NONE not in types:
435
        raise Exception("Error, server demands authentication")
436

    
437
    server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))
438

    
439
    # Check authentication response
440
    res = server.recv(4)
441
    res = rfb.from_u32(res)
442

    
443
    if res != 0:
444
        raise Exception("Authentication error")
445

    
446
    # Reset the timeout for the rest of the session
447
    server.settimeout(None)
448

    
449
    return server
450

    
451

    
452
def parse_arguments(args):
453
    from optparse import OptionParser
454

    
455
    parser = OptionParser()
456
    parser.add_option("-s", "--socket", dest="ctrl_socket",
457
                      default=DEFAULT_CTRL_SOCKET,
458
                      metavar="PATH",
459
                      help=("UNIX socket for control connections (default: "
460
                            "%s" % DEFAULT_CTRL_SOCKET))
461
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
462
                      help="Enable debugging information")
463
    parser.add_option("-l", "--log", dest="log_file",
464
                      default=DEFAULT_LOG_FILE,
465
                      metavar="FILE",
466
                      help=("Write log to FILE instead of %s" %
467
                            DEFAULT_LOG_FILE))
468
    parser.add_option('--pid-file', dest="pid_file",
469
                      default=DEFAULT_PID_FILE,
470
                      metavar='PIDFILE',
471
                      help=("Save PID to file (default: %s)" %
472
                            DEFAULT_PID_FILE))
473
    parser.add_option("-t", "--connect-timeout", dest="connect_timeout",
474
                      default=DEFAULT_CONNECT_TIMEOUT, type="int",
475
                      metavar="SECONDS", help=("Wait SECONDS sec for a client "
476
                                               "to connect"))
477
    parser.add_option("-r", "--connect-retries", dest="connect_retries",
478
                      default=DEFAULT_CONNECT_RETRIES, type="int",
479
                      metavar="RETRIES",
480
                      help="How many times to try to connect to the server")
481
    parser.add_option("-w", "--retry-wait", dest="retry_wait",
482
                      default=DEFAULT_RETRY_WAIT, type="float",
483
                      metavar="SECONDS", help=("Retry connection to server "
484
                                               "every SECONDS sec"))
485
    parser.add_option("-p", "--min-port", dest="min_port",
486
                      default=DEFAULT_MIN_PORT, type="int", metavar="MIN_PORT",
487
                      help=("The minimum port number to use for automatically-"
488
                            "allocated ephemeral ports"))
489
    parser.add_option("-P", "--max-port", dest="max_port",
490
                      default=DEFAULT_MAX_PORT, type="int", metavar="MAX_PORT",
491
                      help=("The maximum port number to use for automatically-"
492
                            "allocated ephemeral ports"))
493
    parser.add_option("-b", "--backlog", dest="backlog",
494
                      default=DEFAULT_BACKLOG, type="int", metavar="BACKLOG",
495
                      help=("Length of the backlog queue for the control"
496
                            "connection socket"))
497
    parser.add_option("--socket-timeout", dest="sock_timeout",
498
                      default=DEFAULT_SOCK_TIMEOUT, type="float",
499
                      metavar="SOCK_TIMEOUT",
500
                      help=("Socket timeout for the server handshake"))
501

    
502
    return parser.parse_args(args)
503

    
504

    
505
def establish_connection(client, addr, ports, opts):
506
    # Receive and parse a client request.
507
    response = {
508
        "source_port": 0,
509
        "status": "FAILED",
510
    }
511
    try:
512
        # TODO: support multiple forwardings in the same message?
513
        #
514
        # Control request, in JSON:
515
        #
516
        # {
517
        #     "source_port":
518
        #         <source port or 0 for automatic allocation>,
519
        #     "destination_address":
520
        #         <destination address of backend server>,
521
        #     "destination_port":
522
        #         <destination port>
523
        #     "password":
524
        #         <the password to use to authenticate clients>
525
        # }
526
        #
527
        # The <password> is used for MITM authentication of clients
528
        # connecting to <source_port>, who will subsequently be
529
        # forwarded to a VNC server listening at
530
        # <destination_address>:<destination_port>
531
        #
532
        # Control reply, in JSON:
533
        # {
534
        #     "source_port": <the allocated source port>
535
        #     "status": <one of "OK" or "FAILED">
536
        # }
537
        #
538
        buf = client.recv(1024)
539
        req = json.loads(buf)
540

    
541
        sport_orig = int(req['source_port'])
542
        daddr = req['destination_address']
543
        dport = int(req['destination_port'])
544
        password = req['password']
545
    except Exception, e:
546
        logger.warn("Malformed request: %s", buf)
547
        client.send(json.dumps(response))
548
        client.close()
549

    
550
    # Spawn a new Greenlet to service the request.
551
    server = None
552
    try:
553
        # If the client has so indicated, pick an ephemeral source port
554
        # randomly, and remove it from the port pool.
555
        if sport_orig == 0:
556
            while True:
557
                try:
558
                    sport = random.choice(ports)
559
                    ports.remove(sport)
560
                    break
561
                except ValueError:
562
                    logger.debug("Port %d already taken", sport)
563

    
564
            logger.debug("Got port %d from pool, %d remaining",
565
                         sport, len(ports))
566
            pool = ports
567
        else:
568
            sport = sport_orig
569
            pool = None
570

    
571
        listeners = get_listening_sockets(sport)
572
        server = perform_server_handshake(daddr, dport,
573
                                          opts.connect_retries,
574
                                          opts.retry_wait, opts.sock_timeout)
575

    
576
        VncAuthProxy.spawn(logger, listeners, pool, daddr, dport,
577
                           server, password, opts.connect_timeout)
578

    
579
        logger.info("New forwarding: %d (client req'd: %d) -> %s:%d",
580
                    sport, sport_orig, daddr, dport)
581
        response = {"source_port": sport,
582
                    "status": "OK"}
583
    except IndexError:
584
        logger.error(("FAILED forwarding, out of ports for [req'd by "
585
                      "client: %d -> %s:%d]"),
586
                     sport_orig, daddr, dport)
587
    except Exception, msg:
588
        logger.error(msg)
589
        logger.error(("FAILED forwarding: %d (client req'd: %d) -> "
590
                      "%s:%d"), sport, sport_orig, daddr, dport)
591
        if not pool is None:
592
            pool.append(sport)
593
            logger.debug("Returned port %d to pool, %d remanining",
594
                         sport, len(pool))
595
        if not server is None:
596
            server.close()
597
    finally:
598
        client.send(json.dumps(response))
599
        client.close()
600

    
601

    
602
def main():
603
    """Run the daemon from the command line"""
604

    
605
    (opts, args) = parse_arguments(sys.argv[1:])
606

    
607
    # Create pidfile
608
    pidf = pidlockfile.TimeoutPIDLockFile(opts.pid_file, 10)
609

    
610
    # Initialize logger
611
    lvl = logging.DEBUG if opts.debug else logging.INFO
612

    
613
    global logger
614
    logger = logging.getLogger("vncauthproxy")
615
    logger.setLevel(lvl)
616
    formatter = logging.Formatter(("%(asctime)s %(module)s[%(process)d] "
617
                                   " %(levelname)s: %(message)s"),
618
                                  "%Y-%m-%d %H:%M:%S")
619
    handler = logging.FileHandler(opts.log_file)
620
    handler.setFormatter(formatter)
621
    logger.addHandler(handler)
622

    
623
    # Become a daemon:
624
    # Redirect stdout and stderr to handler.stream to catch
625
    # early errors in the daemonization process [e.g., pidfile creation]
626
    # which will otherwise go to /dev/null.
627
    daemon_context = AllFilesDaemonContext(
628
        pidfile=pidf,
629
        umask=0022,
630
        stdout=handler.stream,
631
        stderr=handler.stream,
632
        files_preserve=[handler.stream])
633

    
634
    # Remove any stale PID files, left behind by previous invocations
635
    if daemon.runner.is_pidfile_stale(pidf):
636
        logger.warning("Removing stale PID lock file %s", pidf.path)
637
        pidf.break_lock()
638

    
639
    try:
640
        daemon_context.open()
641
    except (AlreadyLocked, LockTimeout):
642
        logger.critical(("Failed to lock PID file %s, another instance "
643
                         "running?"), pidf.path)
644
        sys.exit(1)
645
    logger.info("Became a daemon")
646

    
647
    # A fork() has occured while daemonizing,
648
    # we *must* reinit gevent
649
    gevent.reinit()
650

    
651
    if os.path.exists(opts.ctrl_socket):
652
        logger.critical("Socket '%s' already exists", opts.ctrl_socket)
653
        sys.exit(1)
654

    
655
    # TODO: make this tunable? chgrp as well?
656
    old_umask = os.umask(0007)
657

    
658
    ctrl = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
659
    ctrl.bind(opts.ctrl_socket)
660

    
661
    os.umask(old_umask)
662

    
663
    ctrl.listen(opts.backlog)
664
    logger.info("Initialized, waiting for control connections at %s",
665
                opts.ctrl_socket)
666

    
667
    # Catch signals to ensure graceful shutdown,
668
    # e.g., to make sure the control socket gets unlink()ed.
669
    #
670
    # Uses gevent.signal so the handler fires even during
671
    # gevent.socket.accept()
672
    gevent.signal(SIGINT, fatal_signal_handler, "SIGINT")
673
    gevent.signal(SIGTERM, fatal_signal_handler, "SIGTERM")
674

    
675
    # Init ephemeral port pool
676
    ports = range(opts.min_port, opts.max_port + 1)
677

    
678
    while True:
679
        try:
680
            client, addr = ctrl.accept()
681
            logger.info("New control connection")
682

    
683
            gevent.Greenlet.spawn(establish_connection, client, addr,
684
                                  ports, opts)
685
        except Exception, e:
686
            logger.exception(e)
687
            continue
688
        except SystemExit:
689
            break
690

    
691
    logger.info("Unlinking control socket at %s", opts.ctrl_socket)
692
    os.unlink(opts.ctrl_socket)
693
    daemon_context.close()
694
    sys.exit(0)