Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ 3a82ff41

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
        # Find the least-specific connected route
134
        m = re.match("^([\S]+/[\S]+) dev ([\S]+)", route)
135
        if not m:
136
            continue
137

    
138
        if family == 6 and m.group(1).startswith("fe80:"):
139
            # Skip link-local declarations in "main" table
140
            continue
141

    
142
        def_net, def_dev = m.groups()
143

    
144
        try:
145
            def_net = IPy.IP(def_net)
146
        except ValueError, e:
147
            logging.warn("Unable to parse default route entry %s: %s",
148
                         def_net, str(e))
149

    
150
    for route in routes:
151
        match = re.match(r'^default.*via ([\S]+).*dev ([\S]+)', route)
152
        if match:
153
            def_gw, def_dev = match.groups()
154
            break
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
        logging.debug("%s", data)
383
        if isinstance(data, BasePacket):
384
            data = str(data)
385

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

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

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

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

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

    
408
        ifindex = None
409

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

    
416
            return ifindex
417

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

    
432
        return ifindex
433

    
434

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

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

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

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

    
459
        return addr
460

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
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
                 ("broadcast_address", str(subnet.broadcast)),
580
                 ("subnet_mask", str(subnet.netmask)),
581
                 ("renewal_time", self.lease_renewal),
582
                 ("lease_time", self.lease_lifetime),
583
            ]
584
            if subnet.gw:
585
              dhcp_options += [("router", subnet.gw)]
586
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
587

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
675
        payload.set_verdict(nfqueue.NF_DROP)
676

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

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

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

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

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

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

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

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

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

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

    
742
        """
743
        self.build_config()
744

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

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

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

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

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

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

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

    
788

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

    
796
    import validate
797

    
798
    validator = validate.Validator()
799

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

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

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

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

    
823

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

    
834

    
835
    opts, args = parser.parse_args()
836

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

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

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

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

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

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

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

    
883
    logging.info("Starting up")
884

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

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

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

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

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

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

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

    
934

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