Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ c63ad0e2

History | View | Annotate | Download (25.1 kB)

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

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

    
22
import os
23
import re
24
import glob
25
import time
26
import logging
27
import logging.handlers
28
import threading
29
import subprocess
30

    
31
import daemon
32
import nfqueue
33
import pyinotify
34

    
35
import IPy
36
import socket
37
from select import select
38
from socket import AF_INET, AF_INET6
39

    
40
from scapy.layers.l2 import Ether
41
from scapy.layers.inet import IP, UDP
42
from scapy.layers.inet6 import *
43
from scapy.layers.dhcp import BOOTP, DHCP
44
from scapy.sendrecv import sendp
45

    
46
DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
47
DEFAULT_PATH = "/var/run/ganeti-dhcpd"
48
DEFAULT_USER = "nobody"
49
DEFAULT_LEASE_LIFETIME = 604800 # 1 week
50
DEFAULT_LEASE_RENEWAL = 600  # 10 min
51
DEFAULT_RA_PERIOD = 300 # seconds
52
DHCP_DUMMY_SERVER_IP = "1.2.3.4"
53

    
54
LOG_FILENAME = "nfdhcpd.log"
55

    
56
SYSFS_NET = "/sys/class/net"
57

    
58
LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
59

    
60
# Configuration file specification (see configobj documentation)
61
CONFIG_SPEC = """
62
[general]
63
pidfile = string()
64
datapath = string()
65
logdir = string()
66
user = string()
67

    
68
[dhcp]
69
enable_dhcp = boolean(default=True)
70
lease_lifetime = integer(min=0, max=4294967295)
71
lease_renewal = integer(min=0, max=4294967295)
72
server_ip = ip_addr()
73
dhcp_queue = integer(min=0, max=65535)
74
nameservers = ip_addr_list(family=4)
75

    
76
[ipv6]
77
enable_ipv6 = boolean(default=True)
78
ra_period = integer(min=1, max=4294967295)
79
rs_queue = integer(min=0, max=65535)
80
ns_queue = integer(min=0, max=65535)
81
nameservers = ip_addr_list(family=6)
82
"""
83

    
84

    
85
DHCPDISCOVER = 1
86
DHCPOFFER = 2
87
DHCPREQUEST = 3
88
DHCPDECLINE = 4
89
DHCPACK = 5
90
DHCPNAK = 6
91
DHCPRELEASE = 7
92
DHCPINFORM = 8
93

    
94
DHCP_TYPES = {
95
    DHCPDISCOVER: "DHCPDISCOVER",
96
    DHCPOFFER: "DHCPOFFER",
97
    DHCPREQUEST: "DHCPREQUEST",
98
    DHCPDECLINE: "DHCPDECLINE",
99
    DHCPACK: "DHCPACK",
100
    DHCPNAK: "DHCPNAK",
101
    DHCPRELEASE: "DHCPRELEASE",
102
    DHCPINFORM: "DHCPINFORM",
103
}
104

    
105
DHCP_REQRESP = {
106
    DHCPDISCOVER: DHCPOFFER,
107
    DHCPREQUEST: DHCPACK,
108
    DHCPINFORM: DHCPACK,
109
    }
110

    
111

    
112
class ClientFileHandler(pyinotify.ProcessEvent):
113
    def __init__(self, server):
114
        pyinotify.ProcessEvent.__init__(self)
115
        self.server = server
116

    
117
    def process_IN_DELETE(self, event):
118
        self.server.remove_iface(event.name)
119

    
120
    def process_IN_CLOSE_WRITE(self, event):
121
        self.server.add_iface(os.path.join(event.path, event.name))
122

    
123

    
124
class Client(object):
125
    def __init__(self, mac=None, ips=None, link=None, hostname=None):
126
        self.mac = mac
127
        self.ips = ips
128
        self.hostname = hostname
129
        self.link = link
130
        self.iface = None
131

    
132
    @property
133
    def ip(self):
134
        return self.ips[0]
135

    
136
    def is_valid(self):
137
        return self.mac is not None and self.ips is not None\
138
               and self.hostname is not None
139

    
140

    
141
class Subnet(object):
142
    def __init__(self, net=None, gw=None, dev=None):
143
        if isinstance(net, str):
144
            self.net = IPy.IP(net)
145
        else:
146
            self.net = net
147
        self.gw = gw
148
        self.dev = dev
149

    
150
    @property
151
    def netmask(self):
152
        return str(self.net.netmask())
153

    
154
    @property
155
    def broadcast(self):
156
        return str(self.net.broadcast())
157

    
158
    @property
159
    def prefix(self):
160
        return self.net.net()
161

    
162
    @property
163
    def prefixlen(self):
164
        return self.net.prefixlen()
165

    
166
    @staticmethod
167
    def _make_eui64(net, mac):
168
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
169

    
170
        """
171
        comp = mac.split(":")
172
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
173
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
174
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
175
        for l in range(0, len(eui64), 2):
176
            prefix += ["".join(eui64[l:l+2])]
177
        return IPy.IP(":".join(prefix))
178

    
179
    def make_eui64(self, mac):
180
        return self._make_eui64(self.net, mac)
181

    
182
    def make_ll64(self, mac):
183
        return self._make_eui64("fe80::", mac)
184

    
185

    
186
class VMNetProxy(object):
187
    def __init__(self, data_path, dhcp_queue_num=None,
188
                 rs_queue_num=None, ns_queue_num=None,
189
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
190
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
191
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers = [],
192
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers = []):
193

    
194
        self.data_path = data_path
195
        self.lease_lifetime = dhcp_lease_lifetime
196
        self.lease_renewal = dhcp_lease_renewal
197
        self.dhcp_server_ip = dhcp_server_ip
198
        self.ra_period = ra_period
199
        self.dhcp_nameservers = dhcp_nameservers
200
        self.ipv6_nameservers = ipv6_nameservers
201
        self.ipv6_enabled = False
202

    
203
        self.clients = {}
204
        self.subnets = {}
205
        self.ifaces = {}
206
        self.v6nets = {}
207
        self.nfq = {}
208

    
209
        # Inotify setup
210
        self.wm = pyinotify.WatchManager()
211
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
212
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
213
        handler = ClientFileHandler(self)
214
        self.notifier = pyinotify.Notifier(self.wm, handler)
215
        self.wm.add_watch(self.data_path, mask, rec=True)
216

    
217
        # NFQUEUE setup
218
        if dhcp_queue_num is not None:
219
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
220

    
221
        if rs_queue_num is not None:
222
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
223
            self.ipv6_enabled = True
224

    
225
        if ns_queue_num is not None:
226
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
227
            self.ipv6_enabled = True
228

    
229
    def _setup_nfqueue(self, queue_num, family, callback):
230
        logging.debug("Setting up NFQUEUE for queue %d, AF %s" %
231
                      (queue_num, family))
232
        q = nfqueue.queue()
233
        q.set_callback(callback)
234
        q.fast_open(queue_num, family)
235
        q.set_queue_maxlen(5000)
236
        # This is mandatory for the queue to operate
237
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
238
        self.nfq[q.get_fd()] = q
239

    
240
    def build_config(self):
241
        self.clients.clear()
242
        self.subnets.clear()
243

    
244
        for file in glob.glob(os.path.join(self.data_path, "*")):
245
            self.add_iface(file)
246

    
247
    def get_ifindex(self, iface):
248
        """ Get the interface index from sysfs
249

    
250
        """
251
        file = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
252
        if not file.startswith(SYSFS_NET):
253
            return None
254

    
255
        ifindex = None
256

    
257
        try:
258
            f = open(file, 'r')
259
        except EnvironmentError:
260
            logging.debug("%s is probably down, removing" % iface)
261
            self.remove_iface(iface)
262

    
263
            return ifindex
264

    
265
        try:
266
            ifindex = f.readline().strip()
267
            try:
268
                ifindex = int(ifindex)
269
            except ValueError, e:
270
                logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
271
                             " output '%s'" % (iface, ifindex))
272
        except EnvironmentError, e:
273
            logging.warn("Error reading %s's ifindex from sysfs: %s" %
274
                         (iface, str(e)))
275
            self.remove_iface(iface)
276
        finally:
277
            f.close()
278

    
279
        return ifindex
280

    
281

    
282
    def get_iface_hw_addr(self, iface):
283
        """ Get the interface hardware address from sysfs
284

    
285
        """
286
        file = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
287
        if not file.startswith(SYSFS_NET):
288
            return None
289

    
290
        addr = None
291
        try:
292
            f = open(file, 'r')
293
        except EnvironmentError:
294
            logging.debug("%s is probably down, removing" % iface)
295
            self.remove_iface(iface)
296
            return addr
297

    
298
        try:
299
            addr = f.readline().strip()
300
        except EnvironmentError, e:
301
            logging.warn("Failed to read hw address for %s from sysfs: %s" %
302
                         (iface, str(e)))
303
        finally:
304
            f.close()
305

    
306
        return addr
307

    
308
    def parse_routing_table(self, table="main", family=4):
309
        """ Parse the given routing table to get connected route, gateway and
310
        default device.
311

    
312
        """
313
        ipro = subprocess.Popen(["ip", "-%d" % family, "ro", "ls",
314
                                 "table", table], stdout=subprocess.PIPE)
315
        routes = ipro.stdout.readlines()
316

    
317
        def_gw = None
318
        def_dev = None
319
        def_net = None
320

    
321
        for route in routes:
322
            match = re.match(r'^default.*via ([^\s]+).*dev ([^\s]+)', route)
323
            if match:
324
                def_gw, def_dev = match.groups()
325
                break
326

    
327
        for route in routes:
328
            # Find the least-specific connected route
329
            m = re.match("^([^\\s]+) dev %s" % def_dev, route)
330
            if not m:
331
                continue
332
            def_net = m.groups(1)
333

    
334
            try:
335
                def_net = IPy.IP(def_net)
336
            except ValueError, e:
337
                logging.warn("Unable to parse default route entry %s: %s" %
338
                             (def_net, str(e)))
339

    
340
        return Subnet(net=def_net, gw=def_gw, dev=def_dev)
341

    
342
    def parse_binding_file(self, path):
343
        """ Read a client configuration from a tap file
344

    
345
        """
346
        try:
347
            iffile = open(path, 'r')
348
        except EnvironmentError, e:
349
            logging.warn("Unable to open binding file %s: %s" % (path, str(e)))
350
            return (None, None, None, None)
351

    
352
        mac = None
353
        ips = None
354
        link = None
355
        hostname = None
356

    
357
        for line in iffile:
358
            if line.startswith("IP="):
359
                ip = line.strip().split("=")[1]
360
                ips = ip.split()
361
            elif line.startswith("MAC="):
362
                mac = line.strip().split("=")[1]
363
            elif line.startswith("LINK="):
364
                link = line.strip().split("=")[1]
365
            elif line.startswith("HOSTNAME="):
366
                hostname = line.strip().split("=")[1]
367

    
368
        return Client(mac=mac, ips=ips, link=link, hostname=hostname)
369

    
370
    def add_iface(self, path):
371
        """ Add an interface to monitor
372

    
373
        """
374
        iface = os.path.basename(path)
375

    
376
        logging.debug("Updating configuration for %s" % iface)
377
        binding = self.parse_binding_file(path)
378
        ifindex = self.get_ifindex(iface)
379

    
380
        if ifindex is None:
381
            logging.warn("Stale configuration for %s found" % iface)
382
        else:
383
            if binding.is_valid():
384
                binding.iface = iface
385
                self.clients[binding.mac] = binding
386
                self.subnets[binding.link] = self.parse_routing_table(
387
                                                binding.link)
388
                logging.debug("Added client %s on %s" %
389
                              (binding.hostname, iface))
390
                self.ifaces[ifindex] = iface
391
                self.v6nets[iface] = self.parse_routing_table(binding.link, 6)
392

    
393
    def remove_iface(self, iface):
394
        """ Cleanup clients on a removed interface
395

    
396
        """
397
        if iface in self.v6nets:
398
            del self.v6nets[iface]
399

    
400
        for mac in self.clients.keys():
401
            if self.clients[mac].iface == iface:
402
                del self.clients[mac]
403

    
404
        for ifindex in self.ifaces.keys():
405
            if self.ifaces[ifindex] == iface:
406
                del self.ifaces[ifindex]
407

    
408
        logging.debug("Removed interface %s" % iface)
409

    
410
    def dhcp_response(self, i, payload):
411
        """ Generate a reply to a BOOTP/DHCP request
412

    
413
        """
414
        # Decode the response - NFQUEUE relays IP packets
415
        pkt = IP(payload.get_data())
416

    
417
        # Get the actual interface from the ifindex
418
        iface = self.ifaces[payload.get_indev()]
419

    
420
        # Signal the kernel that it shouldn't further process the packet
421
        payload.set_verdict(nfqueue.NF_DROP)
422

    
423
        # Get the client MAC address
424
        resp = pkt.getlayer(BOOTP).copy()
425
        hlen = resp.hlen
426
        mac = resp.chaddr[:hlen].encode("hex")
427
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
428

    
429
        # Server responses are always BOOTREPLYs
430
        resp.op = "BOOTREPLY"
431
        del resp.payload
432

    
433
        try:
434
            binding = self.clients[mac]
435
        except KeyError:
436
            logging.warn("Invalid client %s on %s" % (mac, iface))
437
            return
438

    
439
        if iface != binding.iface:
440
            logging.warn("Received spoofed DHCP request for %s from interface"
441
                         " %s instead of %s" %
442
                         (mac, iface, binding.iface))
443
            return
444

    
445
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
446
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
447
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
448
        subnet = self.subnets[binding.link]
449

    
450
        if not DHCP in pkt:
451
            logging.warn("Invalid request from %s on %s, no DHCP"
452
                         " payload found" % (binding.mac, iface))
453
            return
454

    
455
        dhcp_options = []
456
        requested_addr = binding.ip
457
        for opt in pkt[DHCP].options:
458
            if type(opt) is tuple and opt[0] == "message-type":
459
                req_type = opt[1]
460
            if type(opt) is tuple and opt[0] == "requested_addr":
461
                requested_addr = opt[1]
462

    
463
        logging.info("%s from %s on %s" %
464
                    (DHCP_TYPES.get(req_type, "UNKNOWN"), binding.mac, iface))
465

    
466
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
467
            resp_type = DHCPNAK
468
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
469
                         " instead of %s" %
470
                         (binding.mac, iface, requested_addr, binding.ip))
471

    
472
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
473
            resp_type = DHCP_REQRESP[req_type]
474
            resp.yiaddr = self.clients[mac].ip
475
            dhcp_options += [
476
                 ("hostname", binding.hostname),
477
                 ("domain", binding.hostname.split('.', 1)[-1]),
478
                 ("router", subnet.gw),
479
                 ("broadcast_address", str(subnet.broadcast)),
480
                 ("subnet_mask", str(subnet.netmask)),
481
                 ("renewal_time", self.lease_renewal),
482
                 ("lease_time", self.lease_lifetime),
483
            ]
484
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
485

    
486
        elif req_type == DHCPINFORM:
487
            resp_type = DHCP_REQRESP[req_type]
488
            dhcp_options += [
489
                 ("hostname", binding.hostname),
490
                 ("domain", binding.hostname.split('.', 1)[-1]),
491
            ]
492
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
493

    
494
        elif req_type == DHCPRELEASE:
495
            # Log and ignore
496
            logging.info("DHCPRELEASE from %s on %s" %
497
                         (binding.mac, iface))
498
            return
499

    
500
        # Finally, always add the server identifier and end options
501
        dhcp_options += [
502
            ("message-type", resp_type),
503
            ("server_id", DHCP_DUMMY_SERVER_IP),
504
            "end"
505
        ]
506
        resp /= DHCP(options=dhcp_options)
507

    
508
        logging.info("%s to %s (%s) on %s" %
509
                      (DHCP_TYPES[resp_type], mac, binding.ip, iface))
510
        sendp(resp, iface=iface, verbose=False)
511

    
512
    def rs_response(self, i, payload):
513
        """ Generate a reply to a BOOTP/DHCP request
514

    
515
        """
516
        # Get the actual interface from the ifindex
517
        iface = self.ifaces[payload.get_indev()]
518
        ifmac = self.get_iface_hw_addr(iface)
519
        subnet = self.v6nets[iface]
520
        ifll = subnet.make_ll64(ifmac)
521

    
522
        # Signal the kernel that it shouldn't further process the packet
523
        payload.set_verdict(nfqueue.NF_DROP)
524

    
525
        resp = Ether(src=self.get_iface_hw_addr(iface))/\
526
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
527
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
528
                                     prefixlen=subnet.prefixlen)
529

    
530
        if self.ipv6_nameservers:
531
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
532
                                     lifetime=self.ra_period * 3)
533

    
534
        logging.info("RA on %s for %s" % (iface, subnet.net))
535
        sendp(resp, iface=iface, verbose=False)
536

    
537
    def ns_response(self, i, payload):
538
        """ Generate a reply to an ICMPv6 neighbor solicitation
539

    
540
        """
541
        # Get the actual interface from the ifindex
542
        iface = self.ifaces[payload.get_indev()]
543
        ifmac = self.get_iface_hw_addr(iface)
544
        subnet = self.v6nets[iface]
545
        ifll = subnet.make_ll64(ifmac)
546

    
547
        ns = IPv6(payload.get_data())
548

    
549
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
550
            logging.debug("Received NS for a non-routable IP (%s)" % ns.tgt)
551
            payload.set_verdict(nfqueue.NF_ACCEPT)
552
            return 1
553

    
554
        payload.set_verdict(nfqueue.NF_DROP)
555

    
556
        try:
557
            client_lladdr = ns.lladdr
558
        except AttributeError:
559
            return 1
560

    
561
        resp = Ether(src=ifmac, dst=client_lladdr)/\
562
               IPv6(src=str(ifll), dst=ns.src)/\
563
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
564
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
565

    
566
        logging.info("NA on %s for %s" % (iface, ns.tgt))
567
        sendp(resp, iface=iface, verbose=False)
568
        return 1
569

    
570
    def send_periodic_ra(self):
571
        # Use a separate thread as this may take a _long_ time with
572
        # many interfaces and we want to be responsive in the mean time
573
        threading.Thread(target=self._send_periodic_ra).start()
574

    
575
    def _send_periodic_ra(self):
576
        logging.debug("Sending out periodic RAs")
577
        start = time.time()
578
        i = 0
579
        for client in self.clients.values():
580
            iface = client.iface
581
            ifmac = self.get_iface_hw_addr(iface)
582
            if not ifmac:
583
                continue
584

    
585
            subnet = self.v6nets[iface]
586
            ifll = subnet.make_ll64(ifmac)
587
            resp = Ether(src=ifmac)/\
588
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
589
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
590
                                         prefixlen=subnet.prefixlen)
591
            if self.ipv6_nameservers:
592
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
593
                                         lifetime=self.ra_period * 3)
594
            try:
595
                sendp(resp, iface=iface, verbose=False)
596
            except socket.error, e:
597
                logging.warn("Periodic RA on %s failed: %s" % (iface, str(e)))
598
            except Exception, e:
599
                logging.warn("Unkown error during periodic RA on %s: %s" %
600
                             (iface, str(e)))
601
            i += 1
602
        logging.debug("Sent %d RAs in %.2f seconds" % (i, time.time() - start))
603

    
604
    def serve(self):
605
        """ Loop forever, serving DHCP requests
606

    
607
        """
608
        self.build_config()
609

    
610
        iwfd = self.notifier._fd
611

    
612
        start = time.time()
613
        if self.ipv6_enabled:
614
            timeout = self.ra_period
615
            self.send_periodic_ra()
616
        else:
617
            timeout = None
618

    
619
        while True:
620
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
621
            if xlist:
622
                logging.warn("Warning: Exception on %s" %
623
                             ", ".join([ str(fd) for fd in xlist]))
624

    
625
            if rlist:
626
                if iwfd in rlist:
627
                # First check if there are any inotify (= configuration change)
628
                # events
629
                    self.notifier.read_events()
630
                    self.notifier.process_events()
631
                    rlist.remove(iwfd)
632

    
633
                for fd in rlist:
634
                    try:
635
                        self.nfq[fd].process_pending()
636
                    except Exception, e:
637
                        logging.warn("Error processing fd %d: %s" %
638
                                     (fd, str(e)))
639

    
640
            if self.ipv6_enabled:
641
                # Calculate the new timeout
642
                timeout = self.ra_period - (time.time() - start)
643

    
644
                if timeout <= 0:
645
                    start = time.time()
646
                    self.send_periodic_ra()
647
                    timeout = self.ra_period - (time.time() - start)
648

    
649

    
650
if __name__ == "__main__":
651
    import optparse
652
    from cStringIO import StringIO
653
    from capng import *
654
    from pwd import getpwnam, getpwuid
655
    from configobj import ConfigObj, ConfigObjError, flatten_errors
656

    
657
    import validate
658

    
659
    validator = validate.Validator()
660

    
661
    def is_ip_list(value, family=4):
662
        try:
663
            family = int(family)
664
        except ValueError:
665
            raise validate.VdtParamError(family)
666
        if isinstance(value, (str, unicode)):
667
            value = [value]
668
        if not isinstance(value, list):
669
            raise validate.VdtTypeError(value)
670

    
671
        for entry in value:
672
            try:
673
                ip = IPy.IP(entry)
674
            except ValueError:
675
                raise validate.VdtValueError(entry)
676

    
677
            if ip.version() != family:
678
                raise validate.VdtValueError(entry)
679
        return value
680

    
681
    validator.functions["ip_addr_list"] = is_ip_list
682
    config_spec = StringIO(CONFIG_SPEC)
683

    
684

    
685
    parser = optparse.OptionParser()
686
    parser.add_option("-c", "--config", dest="config_file",
687
                      help="The location of the data files", metavar="FILE",
688
                      default=DEFAULT_CONFIG)
689
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
690
                      help="Turn on debugging messages")
691
    parser.add_option("-f", "--foreground", action="store_false",
692
                      dest="daemonize", default=True,
693
                      help="Do not daemonize, stay in the foreground")
694

    
695

    
696
    opts, args = parser.parse_args()
697

    
698
    if opts.daemonize:
699
        d = daemon.DaemonContext()
700
        d.umask = 0022
701
        d.open()
702

    
703
    try:
704
        config = ConfigObj(opts.config_file, configspec=config_spec)
705
    except ConfigObjError, e:
706
        sys.stderr.write("Failed to parse config file %s: %s" %
707
                         (opts.config_file, str(e)))
708
        sys.exit(1)
709

    
710
    results = config.validate(validator)
711
    if results != True:
712
        logging.fatal("Configuration file validation failed! See errors below:")
713
        for (section_list, key, _) in flatten_errors(config, results):
714
            if key is not None:
715
                logging.fatal(" '%s' in section '%s' failed validation" %
716
                              (key, ", ".join(section_list)))
717
            else:
718
                logging.fatal(" Section '%s' is missing" %
719
                              ", ".join(section_list))
720
        sys.exit(1)
721

    
722
    pidfile = open(config["general"]["pidfile"], "w")
723
    pidfile.write("%s" % os.getpid())
724
    pidfile.close()
725

    
726
    logger = logging.getLogger()
727
    if opts.debug:
728
        logger.setLevel(logging.DEBUG)
729
    else:
730
        logger.setLevel(logging.INFO)
731

    
732
    logging.info("Starting up")
733

    
734
    proxy_opts = {}
735
    if config["dhcp"].as_bool("enable_dhcp"):
736
        proxy_opts.update({
737
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
738
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
739
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
740
            "dhcp_server_ip": config["dhcp"]["server_ip"],
741
            "dhcp_nameservers": config["dhcp"]["nameservers"],
742
        })
743

    
744
    if config["ipv6"].as_bool("enable_ipv6"):
745
        proxy_opts.update({
746
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
747
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
748
            "ra_period": config["ipv6"].as_int("ra_period"),
749
            "ipv6_nameservers": config["ipv6"]["nameservers"],
750
        })
751

    
752
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
753

    
754
    # Drop all capabilities except CAP_NET_RAW and change uid
755
    try:
756
        uid = getpwuid(config["general"].as_int("user"))
757
    except ValueError:
758
        uid = getpwnam(config["general"]["user"])
759

    
760
    logging.debug("Setting capabilities and changing uid")
761
    logging.debug("User: %s, uid: %d, gid: %d" %
762
                  (config["general"]["user"], uid.pw_uid, uid.pw_gid))
763
    capng_clear(CAPNG_SELECT_BOTH)
764
    capng_update(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_NET_RAW)
765
    capng_change_id(uid.pw_uid, uid.pw_gid,
766
                    CAPNG_DROP_SUPP_GRP | CAPNG_CLEAR_BOUNDING)
767

    
768
    if opts.daemonize:
769
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
770
        handler = logging.handlers.RotatingFileHandler(logfile,
771
                                                       maxBytes=2097152)
772
    else:
773
        handler = logging.StreamHandler()
774

    
775
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
776
    logger.addHandler(handler)
777

    
778
    logging.info("Ready to serve requests")
779
    proxy.serve()
780

    
781

    
782
# vim: set ts=4 sts=4 sw=4 et :