Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ cf51ea5b

History | View | Annotate | Download (29.8 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_routing_table(table="main", family=4):
120
    """ Parse the given routing table to get connected route, gateway and
121
    default device.
122

    
123
    """
124
    ipro = subprocess.Popen(["ip", "-%d" % family, "ro", "ls",
125
                             "table", table], stdout=subprocess.PIPE)
126
    routes = ipro.stdout.readlines()
127

    
128
    def_gw = None
129
    def_dev = None
130
    def_net = None
131

    
132
    for route in routes:
133
        match = re.match(r'^default.*via ([^\s]+).*dev ([^\s]+)', route)
134
        if match:
135
            def_gw, def_dev = match.groups()
136
            break
137

    
138
    for route in routes:
139
        # Find the least-specific connected route
140
        m = re.match("^([^\\s]+) dev %s" % def_dev, route)
141
        if not m:
142
            continue
143

    
144
        if family == 6 and m.group(1).startswith("fe80:"):
145
            # Skip link-local declarations in "main" table
146
            continue
147

    
148
        def_net = m.group(1)
149

    
150
        try:
151
            def_net = IPy.IP(def_net)
152
        except ValueError, e:
153
            logging.warn("Unable to parse default route entry %s: %s",
154
                         def_net, str(e))
155

    
156
    return Subnet(net=def_net, gw=def_gw, dev=def_dev)
157

    
158

    
159
def parse_binding_file(path):
160
    """ Read a client configuration from a tap file
161

    
162
    """
163
    try:
164
        iffile = open(path, 'r')
165
    except EnvironmentError, e:
166
        logging.warn("Unable to open binding file %s: %s", path, str(e))
167
        return None
168

    
169
    ifname = os.path.basename(path)
170
    mac = None
171
    ips = None
172
    link = None
173
    hostname = None
174

    
175
    for line in iffile:
176
        if line.startswith("IP="):
177
            ip = line.strip().split("=")[1]
178
            ips = ip.split()
179
        elif line.startswith("MAC="):
180
            mac = line.strip().split("=")[1]
181
        elif line.startswith("LINK="):
182
            link = line.strip().split("=")[1]
183
        elif line.startswith("HOSTNAME="):
184
            hostname = line.strip().split("=")[1]
185
        elif line.startswith("IFACE="):
186
            iface = line.strip().split("=")[1]
187

    
188
    return Client(ifname=ifname, mac=mac, ips=ips, link=link, hostname=hostname, iface=iface)
189

    
190

    
191
class ClientFileHandler(pyinotify.ProcessEvent):
192
    def __init__(self, server):
193
        pyinotify.ProcessEvent.__init__(self)
194
        self.server = server
195

    
196
    def process_IN_DELETE(self, event): # pylint: disable=C0103
197
        """ Delete file handler
198

    
199
        Currently this removes an interface from the watch list
200

    
201
        """
202
        self.server.remove_iface(event.name)
203

    
204
    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
205
        """ Add file handler
206

    
207
        Currently this adds an interface to the watch list
208

    
209
        """
210
        self.server.add_iface(os.path.join(event.path, event.name))
211

    
212

    
213
class Client(object):
214
    def __init__(self, ifname=None, mac=None, ips=None, link=None, hostname=None, iface=None):
215
        self.mac = mac
216
        self.ips = ips
217
        self.hostname = hostname
218
        self.link = link
219
        self.iface = iface
220
        self.ifname = ifname
221

    
222
    @property
223
    def ip(self):
224
        return self.ips[0]
225

    
226
    def is_valid(self):
227
        return self.mac is not None and self.ips is not None\
228
               and self.hostname is not None
229

    
230

    
231
class Subnet(object):
232
    def __init__(self, net=None, gw=None, dev=None):
233
        if isinstance(net, str):
234
            self.net = IPy.IP(net)
235
        else:
236
            self.net = net
237
        self.gw = gw
238
        self.dev = dev
239

    
240
    @property
241
    def netmask(self):
242
        """ Return the netmask in textual representation
243

    
244
        """
245
        return str(self.net.netmask())
246

    
247
    @property
248
    def broadcast(self):
249
        """ Return the broadcast address in textual representation
250

    
251
        """
252
        return str(self.net.broadcast())
253

    
254
    @property
255
    def prefix(self):
256
        """ Return the network as an IPy.IP
257

    
258
        """
259
        return self.net.net()
260

    
261
    @property
262
    def prefixlen(self):
263
        """ Return the prefix length as an integer
264

    
265
        """
266
        return self.net.prefixlen()
267

    
268
    @staticmethod
269
    def _make_eui64(net, mac):
270
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
271

    
272
        """
273
        comp = mac.split(":")
274
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
275
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
276
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
277
        for l in range(0, len(eui64), 2):
278
            prefix += ["".join(eui64[l:l+2])]
279
        return IPy.IP(":".join(prefix))
280

    
281
    def make_eui64(self, mac):
282
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
283
        subnet.
284

    
285
        """
286
        return self._make_eui64(self.net, mac)
287

    
288
    def make_ll64(self, mac):
289
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
290

    
291
        """
292
        return self._make_eui64("fe80::", mac)
293

    
294

    
295
class VMNetProxy(object): # pylint: disable=R0902
296
    def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
297
                 rs_queue_num=None, ns_queue_num=None,
298
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
299
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
300
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
301
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
302

    
303
        self.data_path = data_path
304
        self.lease_lifetime = dhcp_lease_lifetime
305
        self.lease_renewal = dhcp_lease_renewal
306
        self.dhcp_server_ip = dhcp_server_ip
307
        self.ra_period = ra_period
308
        if dhcp_nameservers is None:
309
            self.dhcp_nameserver = []
310
        else:
311
            self.dhcp_nameservers = dhcp_nameservers
312

    
313
        if ipv6_nameservers is None:
314
            self.ipv6_nameservers = []
315
        else:
316
            self.ipv6_nameservers = ipv6_nameservers
317

    
318
        self.ipv6_enabled = False
319

    
320
        self.clients = {}
321
        self.subnets = {}
322
        self.ifaces = {}
323
        self.v6nets = {}
324
        self.nfq = {}
325
        self.l2socket = socket.socket(socket.AF_PACKET,
326
                                      socket.SOCK_RAW, ETH_P_ALL)
327
        self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
328

    
329
        # Inotify setup
330
        self.wm = pyinotify.WatchManager()
331
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
332
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
333
        inotify_handler = ClientFileHandler(self)
334
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
335
        self.wm.add_watch(self.data_path, mask, rec=True)
336

    
337
        # NFQUEUE setup
338
        if dhcp_queue_num is not None:
339
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
340

    
341
        if rs_queue_num is not None:
342
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
343
            self.ipv6_enabled = True
344

    
345
        if ns_queue_num is not None:
346
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
347
            self.ipv6_enabled = True
348

    
349
    def _cleanup(self):
350
        """ Free all resources for a graceful exit
351

    
352
        """
353
        logging.info("Cleaning up")
354

    
355
        logging.debug("Closing netfilter queues")
356
        for q in self.nfq.values():
357
            q.close()
358

    
359
        logging.debug("Closing socket")
360
        self.l2socket.close()
361

    
362
        logging.debug("Stopping inotify watches")
363
        self.notifier.stop()
364

    
365
        logging.info("Cleanup finished")
366

    
367
    def _setup_nfqueue(self, queue_num, family, callback):
368
        logging.debug("Setting up NFQUEUE for queue %d, AF %s",
369
                      queue_num, family)
370
        q = nfqueue.queue()
371
        q.set_callback(callback)
372
        q.fast_open(queue_num, family)
373
        q.set_queue_maxlen(5000)
374
        # This is mandatory for the queue to operate
375
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
376
        self.nfq[q.get_fd()] = q
377

    
378
    def sendp(self, data, iface):
379
        """ Send a raw packet using a layer-2 socket
380

    
381
        """
382
        if isinstance(data, BasePacket):
383
            data = str(data)
384

    
385
        self.l2socket.bind((iface, ETH_P_ALL))
386
        count = self.l2socket.send(data)
387
        ldata = len(data)
388
        if count != ldata:
389
            logging.warn("Truncated send on %s (%d/%d bytes sent)",
390
                         iface, count, ldata)
391

    
392
    def build_config(self):
393
        self.clients.clear()
394
        self.subnets.clear()
395

    
396
        for path in glob.glob(os.path.join(self.data_path, "*")):
397
            self.add_iface(path)
398

    
399
    def get_ifindex(self, iface):
400
        """ Get the interface index from sysfs
401

    
402
        """
403
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
404
        if not path.startswith(SYSFS_NET):
405
            return None
406

    
407
        ifindex = None
408

    
409
        try:
410
            f = open(path, 'r')
411
        except EnvironmentError:
412
            logging.debug("%s is probably down, removing", iface)
413
            self.remove_iface(iface)
414

    
415
            return ifindex
416

    
417
        try:
418
            ifindex = f.readline().strip()
419
            try:
420
                ifindex = int(ifindex)
421
            except ValueError, e:
422
                logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
423
                             " output '%s'", iface, ifindex)
424
        except EnvironmentError, e:
425
            logging.warn("Error reading %s's ifindex from sysfs: %s",
426
                         iface, str(e))
427
            self.remove_iface(iface)
428
        finally:
429
            f.close()
430

    
431
        return ifindex
432

    
433

    
434
    def get_iface_hw_addr(self, iface):
435
        """ Get the interface hardware address from sysfs
436

    
437
        """
438
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
439
        if not path.startswith(SYSFS_NET):
440
            return None
441

    
442
        addr = None
443
        try:
444
            f = open(path, 'r')
445
        except EnvironmentError:
446
            logging.debug("%s is probably down, removing", iface)
447
            self.remove_iface(iface)
448
            return addr
449

    
450
        try:
451
            addr = f.readline().strip()
452
        except EnvironmentError, e:
453
            logging.warn("Failed to read hw address for %s from sysfs: %s",
454
                         iface, str(e))
455
        finally:
456
            f.close()
457

    
458
        return addr
459

    
460
    def add_iface(self, path):
461
        """ Add an interface to monitor
462

    
463
        """
464
        iface = os.path.basename(path)
465

    
466
        logging.debug("Updating configuration for %s", iface)
467
        binding = parse_binding_file(path)
468
        if binding is None:
469
            return
470
        ifindex = self.get_ifindex(binding.iface)
471

    
472
        if ifindex is None:
473
            logging.warn("Stale configuration for %s found", iface)
474
        else:
475
            if binding.is_valid():
476
                self.clients[binding.mac] = binding
477
                self.subnets[binding.link] = parse_routing_table(binding.link)
478
                logging.debug("Added client %s on %s", binding.hostname, iface)
479
                self.ifaces[ifindex] = binding.iface
480
                self.v6nets[iface] = parse_routing_table(binding.link, 6)
481

    
482
    def remove_iface(self, ifname):
483
        """ Cleanup clients on a removed interface
484

    
485
        """
486
        if ifname in self.v6nets:
487
            del self.v6nets[ifname]
488

    
489
        for mac in self.clients.keys():
490
            if self.clients[mac].ifname == ifname:
491
                iface = self.client[mac].iface
492
                del self.clients[mac]
493

    
494
        for ifindex in self.ifaces.keys():
495
            if self.ifaces[ifindex] == ifname == iface:
496
                del self.ifaces[ifindex]
497

    
498
        logging.debug("Removed interface %s", ifname)
499

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

    
503
        """
504
        logging.info("%s",payload)
505
        indev = payload.get_indev()
506
        try:
507
            # Get the actual interface from the ifindex
508
            iface = self.ifaces[indev]
509
        except KeyError:
510
            # We don't know anything about this interface, so accept the packet
511
            # and return
512
            logging.debug("Ignoring DHCP request on unknown iface %d", indev)
513
            # We don't know what to do with this packet, so let the kernel
514
            # handle it
515
            payload.set_verdict(nfqueue.NF_ACCEPT)
516
            return
517

    
518
        # Decode the response - NFQUEUE relays IP packets
519
        pkt = IP(payload.get_data())
520

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

    
524
        # Get the client MAC address
525
        resp = pkt.getlayer(BOOTP).copy()
526
        hlen = resp.hlen
527
        mac = resp.chaddr[:hlen].encode("hex")
528
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
529

    
530
	logging.info("%s %s %s ", resp, hlen, mac)
531
        # Server responses are always BOOTREPLYs
532
        resp.op = "BOOTREPLY"
533
        del resp.payload
534

    
535
        try:
536
            binding = self.clients[mac]
537
        except KeyError:
538
            logging.warn("Invalid client %s on %s", mac, iface)
539
            return
540

    
541
        if iface != binding.iface:
542
            logging.warn("Received spoofed DHCP request for %s from interface"
543
                         " %s instead of %s", mac, iface, binding.iface)
544
            return
545

    
546
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
547
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
548
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
549
        subnet = self.subnets[binding.link]
550

    
551
        if not DHCP in pkt:
552
            logging.warn("Invalid request from %s on %s, no DHCP"
553
                         " payload found", binding.mac, iface)
554
            return
555

    
556
        dhcp_options = []
557
        requested_addr = binding.ip
558
        for opt in pkt[DHCP].options:
559
            if type(opt) is tuple and opt[0] == "message-type":
560
                req_type = opt[1]
561
            if type(opt) is tuple and opt[0] == "requested_addr":
562
                requested_addr = opt[1]
563

    
564
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
565
                     binding.mac, iface)
566

    
567
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
568
            resp_type = DHCPNAK
569
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
570
                         " instead of %s", binding.mac, iface, requested_addr,
571
                         binding.ip)
572

    
573
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
574
            resp_type = DHCP_REQRESP[req_type]
575
            resp.yiaddr = self.clients[mac].ip
576
            dhcp_options += [
577
                 ("hostname", binding.hostname),
578
                 ("domain", binding.hostname.split('.', 1)[-1]),
579
                 ("router", subnet.gw),
580
                 ("broadcast_address", str(subnet.broadcast)),
581
                 ("subnet_mask", str(subnet.netmask)),
582
                 ("renewal_time", self.lease_renewal),
583
                 ("lease_time", self.lease_lifetime),
584
            ]
585
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
586

    
587
        elif req_type == DHCPINFORM:
588
            resp_type = DHCP_REQRESP[req_type]
589
            dhcp_options += [
590
                 ("hostname", binding.hostname),
591
                 ("domain", binding.hostname.split('.', 1)[-1]),
592
            ]
593
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
594

    
595
        elif req_type == DHCPRELEASE:
596
            # Log and ignore
597
            logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
598
            return
599

    
600
        # Finally, always add the server identifier and end options
601
        dhcp_options += [
602
            ("message-type", resp_type),
603
            ("server_id", DHCP_DUMMY_SERVER_IP),
604
            "end"
605
        ]
606
        resp /= DHCP(options=dhcp_options)
607

    
608
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
609
                     binding.ip, iface)
610
        self.sendp(resp, iface)
611

    
612
    def rs_response(self, i, payload): # pylint: disable=W0613
613
        """ Generate a reply to a BOOTP/DHCP request
614

    
615
        """
616
        indev = payload.get_indev()
617
        try:
618
            # Get the actual interface from the ifindex
619
            iface = self.ifaces[indev]
620
        except KeyError:
621
            logging.debug("Ignoring router solicitation on"
622
                          " unknown interface %d", indev)
623
            # We don't know what to do with this packet, so let the kernel
624
            # handle it
625
            payload.set_verdict(nfqueue.NF_ACCEPT)
626
            return
627

    
628
        ifmac = self.get_iface_hw_addr(iface)
629
        subnet = self.v6nets[iface]
630
        ifll = subnet.make_ll64(ifmac)
631

    
632
        # Signal the kernel that it shouldn't further process the packet
633
        payload.set_verdict(nfqueue.NF_DROP)
634

    
635
        resp = Ether(src=self.get_iface_hw_addr(iface))/\
636
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
637
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
638
                                     prefixlen=subnet.prefixlen)
639

    
640
        if self.ipv6_nameservers:
641
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
642
                                     lifetime=self.ra_period * 3)
643

    
644
        logging.info("RA on %s for %s", iface, subnet.net)
645
        self.sendp(resp, iface)
646

    
647
    def ns_response(self, i, payload): # pylint: disable=W0613
648
        """ Generate a reply to an ICMPv6 neighbor solicitation
649

    
650
        """
651
        indev = payload.get_indev()
652
        try:
653
            # Get the actual interface from the ifindex
654
            iface = self.ifaces[indev]
655
        except KeyError:
656
            logging.debug("Ignoring neighbour solicitation on"
657
                          " unknown interface %d", indev)
658
            # We don't know what to do with this packet, so let the kernel
659
            # handle it
660
            payload.set_verdict(nfqueue.NF_ACCEPT)
661
            return
662

    
663
        ifmac = self.get_iface_hw_addr(iface)
664
        subnet = self.v6nets[iface]
665
        ifll = subnet.make_ll64(ifmac)
666

    
667
        ns = IPv6(payload.get_data())
668

    
669
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
670
            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
671
            payload.set_verdict(nfqueue.NF_ACCEPT)
672
            return 1
673

    
674
        payload.set_verdict(nfqueue.NF_DROP)
675

    
676
        try:
677
            client_lladdr = ns.lladdr
678
        except AttributeError:
679
            return 1
680

    
681
        resp = Ether(src=ifmac, dst=client_lladdr)/\
682
               IPv6(src=str(ifll), dst=ns.src)/\
683
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
684
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
685

    
686
        logging.info("NA on %s for %s", iface, ns.tgt)
687
        self.sendp(resp, iface)
688
        return 1
689

    
690
    def send_periodic_ra(self):
691
        # Use a separate thread as this may take a _long_ time with
692
        # many interfaces and we want to be responsive in the mean time
693
        threading.Thread(target=self._send_periodic_ra).start()
694

    
695
    def _send_periodic_ra(self):
696
        logging.debug("Sending out periodic RAs")
697
        start = time.time()
698
        i = 0
699
        for client in self.clients.values():
700
            iface = client.iface
701
            ifmac = self.get_iface_hw_addr(iface)
702
            if not ifmac:
703
                continue
704

    
705
            subnet = self.v6nets[iface]
706
            if subnet.net is None:
707
                logging.debug("Skipping periodic RA on interface %s,"
708
                              " as it is not IPv6-connected", iface)
709
                continue
710

    
711
            ifll = subnet.make_ll64(ifmac)
712
            resp = Ether(src=ifmac)/\
713
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
714
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
715
                                         prefixlen=subnet.prefixlen)
716
            if self.ipv6_nameservers:
717
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
718
                                         lifetime=self.ra_period * 3)
719
            try:
720
                self.sendp(resp, iface)
721
            except socket.error, e:
722
                logging.warn("Periodic RA on %s failed: %s", iface, str(e))
723
            except Exception, e:
724
                logging.warn("Unkown error during periodic RA on %s: %s",
725
                             iface, str(e))
726
            i += 1
727
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
728

    
729
    def serve(self):
730
        """ Safely perform the main loop, freeing all resources upon exit
731

    
732
        """
733
        try:
734
            self._serve()
735
        finally:
736
            self._cleanup()
737

    
738
    def _serve(self):
739
        """ Loop forever, serving DHCP requests
740

    
741
        """
742
        self.build_config()
743

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

    
748
        start = time.time()
749
        if self.ipv6_enabled:
750
            timeout = self.ra_period
751
            self.send_periodic_ra()
752
        else:
753
            timeout = None
754

    
755
        while True:
756
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
757
            if xlist:
758
                logging.warn("Warning: Exception on %s",
759
                             ", ".join([ str(fd) for fd in xlist]))
760

    
761
            if rlist:
762
                if iwfd in rlist:
763
                # First check if there are any inotify (= configuration change)
764
                # events
765
                    self.notifier.read_events()
766
                    self.notifier.process_events()
767
                    rlist.remove(iwfd)
768

    
769
                for fd in rlist:
770
                    try:
771
                        self.nfq[fd].process_pending()
772
                    except RuntimeError, e:
773
                        logging.warn("Error processing fd %d: %s", fd, str(e))
774
                    except Exception, e:
775
                        logging.warn("Unknown error processing fd %d: %s",
776
                                     fd, str(e))
777

    
778
            if self.ipv6_enabled:
779
                # Calculate the new timeout
780
                timeout = self.ra_period - (time.time() - start)
781

    
782
                if timeout <= 0:
783
                    start = time.time()
784
                    self.send_periodic_ra()
785
                    timeout = self.ra_period - (time.time() - start)
786

    
787

    
788
if __name__ == "__main__":
789
    import capng
790
    import optparse
791
    from cStringIO import StringIO
792
    from pwd import getpwnam, getpwuid
793
    from configobj import ConfigObj, ConfigObjError, flatten_errors
794

    
795
    import validate
796

    
797
    validator = validate.Validator()
798

    
799
    def is_ip_list(value, family=4):
800
        try:
801
            family = int(family)
802
        except ValueError:
803
            raise validate.VdtParamError(family)
804
        if isinstance(value, (str, unicode)):
805
            value = [value]
806
        if not isinstance(value, list):
807
            raise validate.VdtTypeError(value)
808

    
809
        for entry in value:
810
            try:
811
                ip = IPy.IP(entry)
812
            except ValueError:
813
                raise validate.VdtValueError(entry)
814

    
815
            if ip.version() != family:
816
                raise validate.VdtValueError(entry)
817
        return value
818

    
819
    validator.functions["ip_addr_list"] = is_ip_list
820
    config_spec = StringIO(CONFIG_SPEC)
821

    
822

    
823
    parser = optparse.OptionParser()
824
    parser.add_option("-c", "--config", dest="config_file",
825
                      help="The location of the data files", metavar="FILE",
826
                      default=DEFAULT_CONFIG)
827
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
828
                      help="Turn on debugging messages")
829
    parser.add_option("-f", "--foreground", action="store_false",
830
                      dest="daemonize", default=True,
831
                      help="Do not daemonize, stay in the foreground")
832

    
833

    
834
    opts, args = parser.parse_args()
835

    
836
    try:
837
        config = ConfigObj(opts.config_file, configspec=config_spec)
838
    except ConfigObjError, err:
839
        sys.stderr.write("Failed to parse config file %s: %s" %
840
                         (opts.config_file, str(err)))
841
        sys.exit(1)
842

    
843
    results = config.validate(validator)
844
    if results != True:
845
        logging.fatal("Configuration file validation failed! See errors below:")
846
        for (section_list, key, unused) in flatten_errors(config, results):
847
            if key is not None:
848
                logging.fatal(" '%s' in section '%s' failed validation",
849
                              key, ", ".join(section_list))
850
            else:
851
                logging.fatal(" Section '%s' is missing",
852
                              ", ".join(section_list))
853
        sys.exit(1)
854

    
855
    logger = logging.getLogger()
856
    if opts.debug:
857
        logger.setLevel(logging.DEBUG)
858
    else:
859
        logger.setLevel(logging.INFO)
860

    
861
    if opts.daemonize:
862
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
863
        handler = logging.handlers.RotatingFileHandler(logfile,
864
                                                       maxBytes=2097152)
865
    else:
866
        handler = logging.StreamHandler()
867

    
868
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
869
    logger.addHandler(handler)
870

    
871
    if opts.daemonize:
872
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
873
            config["general"]["pidfile"], 10)
874

    
875
        d = daemon.DaemonContext(pidfile=pidfile,
876
                                 stdout=handler.stream,
877
                                 stderr=handler.stream,
878
                                 files_preserve=[handler.stream])
879
        d.umask = 0022
880
        d.open()
881

    
882
    logging.info("Starting up")
883

    
884
    proxy_opts = {}
885
    if config["dhcp"].as_bool("enable_dhcp"):
886
        proxy_opts.update({
887
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
888
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
889
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
890
            "dhcp_server_ip": config["dhcp"]["server_ip"],
891
            "dhcp_nameservers": config["dhcp"]["nameservers"],
892
        })
893

    
894
    if config["ipv6"].as_bool("enable_ipv6"):
895
        proxy_opts.update({
896
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
897
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
898
            "ra_period": config["ipv6"].as_int("ra_period"),
899
            "ipv6_nameservers": config["ipv6"]["nameservers"],
900
        })
901

    
902
    # pylint: disable=W0142
903
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
904

    
905
    # Drop all capabilities except CAP_NET_RAW and change uid
906
    try:
907
        uid = getpwuid(config["general"].as_int("user"))
908
    except ValueError:
909
        uid = getpwnam(config["general"]["user"])
910

    
911
    logging.debug("Setting capabilities and changing uid")
912
    logging.debug("User: %s, uid: %d, gid: %d",
913
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
914

    
915
    # Keep only the capabilities we need
916
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
917
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
918
    capng.capng_update(capng.CAPNG_ADD,
919
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
920
                       capng.CAP_NET_ADMIN)
921
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
922
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
923

    
924
    logging.info("Ready to serve requests")
925
    try:
926
        proxy.serve()
927
    except Exception:
928
        if opts.daemonize:
929
            exc = "".join(traceback.format_exception(*sys.exc_info()))
930
            logging.critical(exc)
931
        raise
932

    
933

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