Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ ed7f0f2a

History | View | Annotate | Download (29.4 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 sys
25
import glob
26
import time
27
import logging
28
import logging.handlers
29
import threading
30
import traceback
31
import subprocess
32

    
33
import daemon
34
import daemon.pidlockfile
35
import nfqueue
36
import pyinotify
37

    
38
import IPy
39
import socket
40
from select import select
41
from socket import AF_INET, AF_INET6
42

    
43
from scapy.data import ETH_P_ALL
44
from scapy.packet import BasePacket
45
from scapy.layers.l2 import Ether
46
from scapy.layers.inet import IP, UDP
47
from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
48
                               ICMPv6NDOptDstLLAddr, \
49
                               ICMPv6NDOptPrefixInfo, \
50
                               ICMPv6NDOptRDNSS
51
from scapy.layers.dhcp import BOOTP, DHCP
52

    
53
DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
54
DEFAULT_PATH = "/var/run/ganeti-dhcpd"
55
DEFAULT_USER = "nobody"
56
DEFAULT_LEASE_LIFETIME = 604800 # 1 week
57
DEFAULT_LEASE_RENEWAL = 600  # 10 min
58
DEFAULT_RA_PERIOD = 300 # seconds
59
DHCP_DUMMY_SERVER_IP = "1.2.3.4"
60

    
61
LOG_FILENAME = "nfdhcpd.log"
62

    
63
SYSFS_NET = "/sys/class/net"
64

    
65
LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
66

    
67
# Configuration file specification (see configobj documentation)
68
CONFIG_SPEC = """
69
[general]
70
pidfile = string()
71
datapath = string()
72
logdir = string()
73
user = string()
74

    
75
[dhcp]
76
enable_dhcp = boolean(default=True)
77
lease_lifetime = integer(min=0, max=4294967295)
78
lease_renewal = integer(min=0, max=4294967295)
79
server_ip = ip_addr()
80
dhcp_queue = integer(min=0, max=65535)
81
nameservers = ip_addr_list(family=4)
82

    
83
[ipv6]
84
enable_ipv6 = boolean(default=True)
85
ra_period = integer(min=1, max=4294967295)
86
rs_queue = integer(min=0, max=65535)
87
ns_queue = integer(min=0, max=65535)
88
nameservers = ip_addr_list(family=6)
89
"""
90

    
91

    
92
DHCPDISCOVER = 1
93
DHCPOFFER = 2
94
DHCPREQUEST = 3
95
DHCPDECLINE = 4
96
DHCPACK = 5
97
DHCPNAK = 6
98
DHCPRELEASE = 7
99
DHCPINFORM = 8
100

    
101
DHCP_TYPES = {
102
    DHCPDISCOVER: "DHCPDISCOVER",
103
    DHCPOFFER: "DHCPOFFER",
104
    DHCPREQUEST: "DHCPREQUEST",
105
    DHCPDECLINE: "DHCPDECLINE",
106
    DHCPACK: "DHCPACK",
107
    DHCPNAK: "DHCPNAK",
108
    DHCPRELEASE: "DHCPRELEASE",
109
    DHCPINFORM: "DHCPINFORM",
110
}
111

    
112
DHCP_REQRESP = {
113
    DHCPDISCOVER: DHCPOFFER,
114
    DHCPREQUEST: DHCPACK,
115
    DHCPINFORM: DHCPACK,
116
    }
117

    
118

    
119
def parse_binding_file(path):
120
    """ Read a client configuration from a tap file
121

    
122
    """
123
    try:
124
        iffile = open(path, 'r')
125
    except EnvironmentError, e:
126
        logging.warn("Unable to open binding file %s: %s", path, str(e))
127
        return None
128

    
129
    ifname = os.path.basename(path)
130
    mac = None
131
    ips = None
132
    link = None
133
    hostname = None
134
    subnet = None
135
    gateway = None
136
    subnet6 = None
137
    gateway6 = None
138

    
139
    for line in iffile:
140
        if line.startswith("IP="):
141
            ip = line.strip().split("=")[1]
142
            ips = ip.split()
143
        elif line.startswith("MAC="):
144
            mac = line.strip().split("=")[1]
145
        elif line.startswith("LINK="):
146
            link = line.strip().split("=")[1]
147
        elif line.startswith("HOSTNAME="):
148
            hostname = line.strip().split("=")[1]
149
        elif line.startswith("IFACE="):
150
            iface = line.strip().split("=")[1]
151
        elif line.startswith("SUBNET="):
152
            subnet = line.strip().split("=")[1]
153
        elif line.startswith("GATEWAY="):
154
            gateway = line.strip().split("=")[1]
155
        elif line.startswith("SUBNET6="):
156
            subnet6 = line.strip().split("=")[1]
157
        elif line.startswith("GATEWAY6="):
158
            gatewa6 = line.strip().split("=")[1]
159

    
160
    return Client(ifname=ifname, mac=mac, ips=ips, link=link,
161
                  hostname=hostname,iface=iface, subnet=subnet,
162
                  gateway=gateway, subnet6=subnet6, gateway6=gateway6 )
163

    
164
class ClientFileHandler(pyinotify.ProcessEvent):
165
    def __init__(self, server):
166
        pyinotify.ProcessEvent.__init__(self)
167
        self.server = server
168

    
169
    def process_IN_DELETE(self, event): # pylint: disable=C0103
170
        """ Delete file handler
171

    
172
        Currently this removes an interface from the watch list
173

    
174
        """
175
        self.server.remove_iface(event.name)
176

    
177
    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
178
        """ Add file handler
179

    
180
        Currently this adds an interface to the watch list
181

    
182
        """
183
        self.server.add_iface(os.path.join(event.path, event.name))
184

    
185

    
186
class Client(object):
187
    def __init__(self, ifname=None, mac=None, ips=None, link=None,
188
                 hostname=None, iface=None, subnet=None, gateway=None,
189
                 subnet6=None, gateway6=None ):
190
        self.mac = mac
191
        self.ips = ips
192
        self.hostname = hostname
193
        self.link = link
194
        self.iface = iface
195
        self.ifname = ifname
196
        self.subnet = subnet
197
        self.gateway = gateway
198
        self.net = Subnet(net=subnet, gw=gateway, dev=ifname)
199
        self.subnet6 = subnet6
200
        self.gateway6 = gateway6
201
        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=ifname)
202

    
203
    @property
204
    def ip(self):
205
        return self.ips[0]
206

    
207
    def is_valid(self):
208
        return self.mac is not None and self.ips is not None\
209
               and self.hostname is not None
210

    
211

    
212
class Subnet(object):
213
    def __init__(self, net=None, gw=None, dev=None):
214
        if isinstance(net, str):
215
            self.net = IPy.IP(net)
216
        else:
217
            self.net = net
218
        self.gw = gw
219
        self.dev = dev
220

    
221
    @property
222
    def netmask(self):
223
        """ Return the netmask in textual representation
224

    
225
        """
226
        return str(self.net.netmask())
227

    
228
    @property
229
    def broadcast(self):
230
        """ Return the broadcast address in textual representation
231

    
232
        """
233
        return str(self.net.broadcast())
234

    
235
    @property
236
    def prefix(self):
237
        """ Return the network as an IPy.IP
238

    
239
        """
240
        return self.net.net()
241

    
242
    @property
243
    def prefixlen(self):
244
        """ Return the prefix length as an integer
245

    
246
        """
247
        return self.net.prefixlen()
248

    
249
    @staticmethod
250
    def _make_eui64(net, mac):
251
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
252

    
253
        """
254
        comp = mac.split(":")
255
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
256
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
257
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
258
        for l in range(0, len(eui64), 2):
259
            prefix += ["".join(eui64[l:l+2])]
260
        return IPy.IP(":".join(prefix))
261

    
262
    def make_eui64(self, mac):
263
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
264
        subnet.
265

    
266
        """
267
        return self._make_eui64(self.net, mac)
268

    
269
    def make_ll64(self, mac):
270
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
271

    
272
        """
273
        return self._make_eui64("fe80::", mac)
274

    
275

    
276
class VMNetProxy(object): # pylint: disable=R0902
277
    def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
278
                 rs_queue_num=None, ns_queue_num=None,
279
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
280
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
281
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
282
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
283

    
284
        self.data_path = data_path
285
        self.lease_lifetime = dhcp_lease_lifetime
286
        self.lease_renewal = dhcp_lease_renewal
287
        self.dhcp_server_ip = dhcp_server_ip
288
        self.ra_period = ra_period
289
        if dhcp_nameservers is None:
290
            self.dhcp_nameserver = []
291
        else:
292
            self.dhcp_nameservers = dhcp_nameservers
293

    
294
        if ipv6_nameservers is None:
295
            self.ipv6_nameservers = []
296
        else:
297
            self.ipv6_nameservers = ipv6_nameservers
298

    
299
        self.ipv6_enabled = False
300

    
301
        self.clients = {}
302
        #self.subnets = {}
303
        self.ifaces = {}
304
        #self.v6nets = {}
305
        self.nfq = {}
306
        self.l2socket = socket.socket(socket.AF_PACKET,
307
                                      socket.SOCK_RAW, ETH_P_ALL)
308
        self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
309

    
310
        # Inotify setup
311
        self.wm = pyinotify.WatchManager()
312
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
313
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
314
        inotify_handler = ClientFileHandler(self)
315
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
316
        self.wm.add_watch(self.data_path, mask, rec=True)
317

    
318
        # NFQUEUE setup
319
        if dhcp_queue_num is not None:
320
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
321

    
322
        if rs_queue_num is not None:
323
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
324
            self.ipv6_enabled = True
325

    
326
        if ns_queue_num is not None:
327
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
328
            self.ipv6_enabled = True
329

    
330
    def _cleanup(self):
331
        """ Free all resources for a graceful exit
332

    
333
        """
334
        logging.info("Cleaning up")
335

    
336
        logging.debug("Closing netfilter queues")
337
        for q in self.nfq.values():
338
            q.close()
339

    
340
        logging.debug("Closing socket")
341
        self.l2socket.close()
342

    
343
        logging.debug("Stopping inotify watches")
344
        self.notifier.stop()
345

    
346
        logging.info("Cleanup finished")
347

    
348
    def _setup_nfqueue(self, queue_num, family, callback):
349
        logging.debug("Setting up NFQUEUE for queue %d, AF %s",
350
                      queue_num, family)
351
        q = nfqueue.queue()
352
        q.set_callback(callback)
353
        q.fast_open(queue_num, family)
354
        q.set_queue_maxlen(5000)
355
        # This is mandatory for the queue to operate
356
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
357
        self.nfq[q.get_fd()] = q
358

    
359
    def sendp(self, data, iface):
360
        """ Send a raw packet using a layer-2 socket
361

    
362
        """
363
        logging.debug("%s", data)
364
        if isinstance(data, BasePacket):
365
            data = str(data)
366

    
367
        self.l2socket.bind((iface, ETH_P_ALL))
368
        count = self.l2socket.send(data)
369
        ldata = len(data)
370
        if count != ldata:
371
            logging.warn("Truncated send on %s (%d/%d bytes sent)",
372
                         iface, count, ldata)
373

    
374
    def build_config(self):
375
        self.clients.clear()
376

    
377
        for path in glob.glob(os.path.join(self.data_path, "*")):
378
            self.add_iface(path)
379

    
380
    def get_ifindex(self, iface):
381
        """ Get the interface index from sysfs
382

    
383
        """
384
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
385
        if not path.startswith(SYSFS_NET):
386
            return None
387

    
388
        ifindex = None
389

    
390
        try:
391
            f = open(path, 'r')
392
        except EnvironmentError:
393
            logging.debug("%s is probably down, removing", iface)
394
            self.remove_iface(iface)
395

    
396
            return ifindex
397

    
398
        try:
399
            ifindex = f.readline().strip()
400
            try:
401
                ifindex = int(ifindex)
402
            except ValueError, e:
403
                logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
404
                             " output '%s'", iface, ifindex)
405
        except EnvironmentError, e:
406
            logging.warn("Error reading %s's ifindex from sysfs: %s",
407
                         iface, str(e))
408
            self.remove_iface(iface)
409
        finally:
410
            f.close()
411

    
412
        return ifindex
413

    
414

    
415
    def get_iface_hw_addr(self, iface):
416
        """ Get the interface hardware address from sysfs
417

    
418
        """
419
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
420
        if not path.startswith(SYSFS_NET):
421
            return None
422

    
423
        addr = None
424
        try:
425
            f = open(path, 'r')
426
        except EnvironmentError:
427
            logging.debug("%s is probably down, removing", iface)
428
            self.remove_iface(iface)
429
            return addr
430

    
431
        try:
432
            addr = f.readline().strip()
433
        except EnvironmentError, e:
434
            logging.warn("Failed to read hw address for %s from sysfs: %s",
435
                         iface, str(e))
436
        finally:
437
            f.close()
438

    
439
        return addr
440

    
441
    def add_iface(self, path):
442
        """ Add an interface to monitor
443

    
444
        """
445
        iface = os.path.basename(path)
446

    
447
        logging.debug("Updating configuration for %s", iface)
448
        binding = parse_binding_file(path)
449
        if binding is None:
450
            return
451
        ifindex = self.get_ifindex(binding.iface)
452

    
453
        if ifindex is None:
454
            logging.warn("Stale configuration for %s found", iface)
455
        else:
456
            if binding.is_valid():
457
                self.clients[binding.mac] = binding
458
                logging.debug("Added client %s on %s", binding.hostname, iface)
459
                self.ifaces[ifindex] = binding.iface
460

    
461
    def remove_iface(self, ifname):
462
        """ Cleanup clients on a removed interface
463

    
464
        """
465
        for mac in self.clients.keys():
466
            if self.clients[mac].ifname == ifname:
467
                iface = self.client[mac].iface
468
                del self.clients[mac]
469

    
470
        for ifindex in self.ifaces.keys():
471
            if self.ifaces[ifindex] == ifname == iface:
472
                del self.ifaces[ifindex]
473

    
474
        logging.debug("Removed interface %s", ifname)
475

    
476
    def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
477
        """ Generate a reply to a BOOTP/DHCP request
478

    
479
        """
480
        logging.info("%s",payload)
481
        indev = payload.get_indev()
482
        try:
483
            # Get the actual interface from the ifindex
484
            iface = self.ifaces[indev]
485
        except KeyError:
486
            # We don't know anything about this interface, so accept the packet
487
            # and return
488
            logging.debug("Ignoring DHCP request on unknown iface %d", indev)
489
            # We don't know what to do with this packet, so let the kernel
490
            # handle it
491
            payload.set_verdict(nfqueue.NF_ACCEPT)
492
            return
493

    
494
        # Decode the response - NFQUEUE relays IP packets
495
        pkt = IP(payload.get_data())
496

    
497
        # Signal the kernel that it shouldn't further process the packet
498
        payload.set_verdict(nfqueue.NF_DROP)
499

    
500
        # Get the client MAC address
501
        resp = pkt.getlayer(BOOTP).copy()
502
        hlen = resp.hlen
503
        mac = resp.chaddr[:hlen].encode("hex")
504
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
505

    
506
        # Server responses are always BOOTREPLYs
507
        resp.op = "BOOTREPLY"
508
        del resp.payload
509

    
510
        try:
511
            binding = self.clients[mac]
512
        except KeyError:
513
            logging.warn("Invalid client %s on %s", mac, iface)
514
            return
515

    
516
        if iface != binding.iface:
517
            logging.warn("Received spoofed DHCP request for %s from interface"
518
                         " %s instead of %s", mac, iface, binding.iface)
519
            return
520

    
521
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
522
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
523
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
524
        subnet = binding.net
525

    
526
        if not DHCP in pkt:
527
            logging.warn("Invalid request from %s on %s, no DHCP"
528
                         " payload found", binding.mac, iface)
529
            return
530

    
531
        dhcp_options = []
532
        requested_addr = binding.ip
533
        for opt in pkt[DHCP].options:
534
            if type(opt) is tuple and opt[0] == "message-type":
535
                req_type = opt[1]
536
            if type(opt) is tuple and opt[0] == "requested_addr":
537
                requested_addr = opt[1]
538

    
539
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
540
                     binding.mac, iface)
541

    
542
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
543
            resp_type = DHCPNAK
544
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
545
                         " instead of %s", binding.mac, iface, requested_addr,
546
                         binding.ip)
547

    
548
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
549
            resp_type = DHCP_REQRESP[req_type]
550
            resp.yiaddr = self.clients[mac].ip
551
            dhcp_options += [
552
                 ("hostname", binding.hostname),
553
                 ("domain", binding.hostname.split('.', 1)[-1]),
554
                 ("broadcast_address", str(subnet.broadcast)),
555
                 ("subnet_mask", str(subnet.netmask)),
556
                 ("renewal_time", self.lease_renewal),
557
                 ("lease_time", self.lease_lifetime),
558
            ]
559
            if subnet.gw:
560
              dhcp_options += [("router", subnet.gw)]
561
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
562

    
563
        elif req_type == DHCPINFORM:
564
            resp_type = DHCP_REQRESP[req_type]
565
            dhcp_options += [
566
                 ("hostname", binding.hostname),
567
                 ("domain", binding.hostname.split('.', 1)[-1]),
568
            ]
569
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
570

    
571
        elif req_type == DHCPRELEASE:
572
            # Log and ignore
573
            logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
574
            return
575

    
576
        # Finally, always add the server identifier and end options
577
        dhcp_options += [
578
            ("message-type", resp_type),
579
            ("server_id", DHCP_DUMMY_SERVER_IP),
580
            "end"
581
        ]
582
        resp /= DHCP(options=dhcp_options)
583

    
584
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
585
                     binding.ip, iface)
586
        self.sendp(resp, iface)
587

    
588
    def rs_response(self, i, payload): # pylint: disable=W0613
589
        """ Generate a reply to a BOOTP/DHCP request
590

    
591
        """
592
        indev = payload.get_indev()
593
        try:
594
            # Get the actual interface from the ifindex
595
            iface = self.ifaces[indev]
596
        except KeyError:
597
            logging.debug("Ignoring router solicitation on"
598
                          " unknown interface %d", indev)
599
            # We don't know what to do with this packet, so let the kernel
600
            # handle it
601
            payload.set_verdict(nfqueue.NF_ACCEPT)
602
            return
603

    
604
        ifmac = self.get_iface_hw_addr(iface)
605
        binding = self.clients[ifmac]
606
        subnet = binding.net6
607
        ifll = subnet.make_ll64(ifmac)
608

    
609
        # Signal the kernel that it shouldn't further process the packet
610
        payload.set_verdict(nfqueue.NF_DROP)
611

    
612
        resp = Ether(src=self.get_iface_hw_addr(iface))/\
613
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
614
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
615
                                     prefixlen=subnet.prefixlen)
616

    
617
        if self.ipv6_nameservers:
618
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
619
                                     lifetime=self.ra_period * 3)
620

    
621
        logging.info("RA on %s for %s", iface, subnet.net)
622
        self.sendp(resp, iface)
623

    
624
    def ns_response(self, i, payload): # pylint: disable=W0613
625
        """ Generate a reply to an ICMPv6 neighbor solicitation
626

    
627
        """
628
        indev = payload.get_indev()
629
        try:
630
            # Get the actual interface from the ifindex
631
            iface = self.ifaces[indev]
632
        except KeyError:
633
            logging.debug("Ignoring neighbour solicitation on"
634
                          " unknown interface %d", indev)
635
            # We don't know what to do with this packet, so let the kernel
636
            # handle it
637
            payload.set_verdict(nfqueue.NF_ACCEPT)
638
            return
639

    
640
        ifmac = self.get_iface_hw_addr(iface)
641
        binding = self.clients[ifmac]
642
        subnet = binding.net6
643
        ifll = subnet.make_ll64(ifmac)
644

    
645
        ns = IPv6(payload.get_data())
646

    
647
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
648
            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
649
            payload.set_verdict(nfqueue.NF_ACCEPT)
650
            return 1
651

    
652
        payload.set_verdict(nfqueue.NF_DROP)
653

    
654
        try:
655
            client_lladdr = ns.lladdr
656
        except AttributeError:
657
            return 1
658

    
659
        resp = Ether(src=ifmac, dst=client_lladdr)/\
660
               IPv6(src=str(ifll), dst=ns.src)/\
661
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
662
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
663

    
664
        logging.info("NA on %s for %s", iface, ns.tgt)
665
        self.sendp(resp, iface)
666
        return 1
667

    
668
    def send_periodic_ra(self):
669
        # Use a separate thread as this may take a _long_ time with
670
        # many interfaces and we want to be responsive in the mean time
671
        threading.Thread(target=self._send_periodic_ra).start()
672

    
673
    def _send_periodic_ra(self):
674
        logging.debug("Sending out periodic RAs")
675
        start = time.time()
676
        i = 0
677
        for client in self.clients.values():
678
            iface = client.iface
679
            ifmac = self.get_iface_hw_addr(iface)
680
            if not ifmac:
681
                continue
682

    
683
            binding = self.clients[ifmac]
684
            subnet = binding.net6
685
            if subnet.net is None:
686
                logging.debug("Skipping periodic RA on interface %s,"
687
                              " as it is not IPv6-connected", iface)
688
                continue
689

    
690
            ifll = subnet.make_ll64(ifmac)
691
            resp = Ether(src=ifmac)/\
692
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
693
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
694
                                         prefixlen=subnet.prefixlen)
695
            if self.ipv6_nameservers:
696
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
697
                                         lifetime=self.ra_period * 3)
698
            try:
699
                self.sendp(resp, iface)
700
            except socket.error, e:
701
                logging.warn("Periodic RA on %s failed: %s", iface, str(e))
702
            except Exception, e:
703
                logging.warn("Unkown error during periodic RA on %s: %s",
704
                             iface, str(e))
705
            i += 1
706
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
707

    
708
    def serve(self):
709
        """ Safely perform the main loop, freeing all resources upon exit
710

    
711
        """
712
        try:
713
            self._serve()
714
        finally:
715
            self._cleanup()
716

    
717
    def _serve(self):
718
        """ Loop forever, serving DHCP requests
719

    
720
        """
721
        self.build_config()
722

    
723
        # Yes, we are accessing _fd directly, but it's the only way to have a
724
        # single select() loop ;-)
725
        iwfd = self.notifier._fd # pylint: disable=W0212
726

    
727
        start = time.time()
728
        if self.ipv6_enabled:
729
            timeout = self.ra_period
730
            self.send_periodic_ra()
731
        else:
732
            timeout = None
733

    
734
        while True:
735
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
736
            if xlist:
737
                logging.warn("Warning: Exception on %s",
738
                             ", ".join([ str(fd) for fd in xlist]))
739

    
740
            if rlist:
741
                if iwfd in rlist:
742
                # First check if there are any inotify (= configuration change)
743
                # events
744
                    self.notifier.read_events()
745
                    self.notifier.process_events()
746
                    rlist.remove(iwfd)
747

    
748
                for fd in rlist:
749
                    try:
750
                        self.nfq[fd].process_pending()
751
                    except RuntimeError, e:
752
                        logging.warn("Error processing fd %d: %s", fd, str(e))
753
                    except Exception, e:
754
                        logging.warn("Unknown error processing fd %d: %s",
755
                                     fd, str(e))
756

    
757
            if self.ipv6_enabled:
758
                # Calculate the new timeout
759
                timeout = self.ra_period - (time.time() - start)
760

    
761
                if timeout <= 0:
762
                    start = time.time()
763
                    self.send_periodic_ra()
764
                    timeout = self.ra_period - (time.time() - start)
765

    
766

    
767
if __name__ == "__main__":
768
    import capng
769
    import optparse
770
    from cStringIO import StringIO
771
    from pwd import getpwnam, getpwuid
772
    from configobj import ConfigObj, ConfigObjError, flatten_errors
773

    
774
    import validate
775

    
776
    validator = validate.Validator()
777

    
778
    def is_ip_list(value, family=4):
779
        try:
780
            family = int(family)
781
        except ValueError:
782
            raise validate.VdtParamError(family)
783
        if isinstance(value, (str, unicode)):
784
            value = [value]
785
        if not isinstance(value, list):
786
            raise validate.VdtTypeError(value)
787

    
788
        for entry in value:
789
            try:
790
                ip = IPy.IP(entry)
791
            except ValueError:
792
                raise validate.VdtValueError(entry)
793

    
794
            if ip.version() != family:
795
                raise validate.VdtValueError(entry)
796
        return value
797

    
798
    validator.functions["ip_addr_list"] = is_ip_list
799
    config_spec = StringIO(CONFIG_SPEC)
800

    
801

    
802
    parser = optparse.OptionParser()
803
    parser.add_option("-c", "--config", dest="config_file",
804
                      help="The location of the data files", metavar="FILE",
805
                      default=DEFAULT_CONFIG)
806
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
807
                      help="Turn on debugging messages")
808
    parser.add_option("-f", "--foreground", action="store_false",
809
                      dest="daemonize", default=True,
810
                      help="Do not daemonize, stay in the foreground")
811

    
812

    
813
    opts, args = parser.parse_args()
814

    
815
    try:
816
        config = ConfigObj(opts.config_file, configspec=config_spec)
817
    except ConfigObjError, err:
818
        sys.stderr.write("Failed to parse config file %s: %s" %
819
                         (opts.config_file, str(err)))
820
        sys.exit(1)
821

    
822
    results = config.validate(validator)
823
    if results != True:
824
        logging.fatal("Configuration file validation failed! See errors below:")
825
        for (section_list, key, unused) in flatten_errors(config, results):
826
            if key is not None:
827
                logging.fatal(" '%s' in section '%s' failed validation",
828
                              key, ", ".join(section_list))
829
            else:
830
                logging.fatal(" Section '%s' is missing",
831
                              ", ".join(section_list))
832
        sys.exit(1)
833

    
834
    logger = logging.getLogger()
835
    if opts.debug:
836
        logger.setLevel(logging.DEBUG)
837
    else:
838
        logger.setLevel(logging.INFO)
839

    
840
    if opts.daemonize:
841
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
842
        handler = logging.handlers.RotatingFileHandler(logfile,
843
                                                       maxBytes=2097152)
844
    else:
845
        handler = logging.StreamHandler()
846

    
847
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
848
    logger.addHandler(handler)
849

    
850
    if opts.daemonize:
851
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
852
            config["general"]["pidfile"], 10)
853

    
854
        d = daemon.DaemonContext(pidfile=pidfile,
855
                                 stdout=handler.stream,
856
                                 stderr=handler.stream,
857
                                 files_preserve=[handler.stream])
858
        d.umask = 0022
859
        d.open()
860

    
861
    logging.info("Starting up")
862

    
863
    proxy_opts = {}
864
    if config["dhcp"].as_bool("enable_dhcp"):
865
        proxy_opts.update({
866
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
867
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
868
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
869
            "dhcp_server_ip": config["dhcp"]["server_ip"],
870
            "dhcp_nameservers": config["dhcp"]["nameservers"],
871
        })
872

    
873
    if config["ipv6"].as_bool("enable_ipv6"):
874
        proxy_opts.update({
875
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
876
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
877
            "ra_period": config["ipv6"].as_int("ra_period"),
878
            "ipv6_nameservers": config["ipv6"]["nameservers"],
879
        })
880

    
881
    # pylint: disable=W0142
882
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
883

    
884
    # Drop all capabilities except CAP_NET_RAW and change uid
885
    try:
886
        uid = getpwuid(config["general"].as_int("user"))
887
    except ValueError:
888
        uid = getpwnam(config["general"]["user"])
889

    
890
    logging.debug("Setting capabilities and changing uid")
891
    logging.debug("User: %s, uid: %d, gid: %d",
892
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
893

    
894
    # Keep only the capabilities we need
895
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
896
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
897
    capng.capng_update(capng.CAPNG_ADD,
898
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
899
                       capng.CAP_NET_ADMIN)
900
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
901
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
902

    
903
    logging.info("Ready to serve requests")
904
    try:
905
        proxy.serve()
906
    except Exception:
907
        if opts.daemonize:
908
            exc = "".join(traceback.format_exception(*sys.exc_info()))
909
            logging.critical(exc)
910
        raise
911

    
912

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