Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ a8434bc6

History | View | Annotate | Download (29 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
    tap = os.path.basename(path)
130
    indev = None
131
    mac = None
132
    ip = None
133
    hostname = None
134
    subnet = None
135
    gateway = None
136
    subnet6 = None
137
    gateway6 = None
138
    eui64 = None
139

    
140
    def get_value(line):
141
        v = line.strip().split('=')[1]
142
        if v == '':
143
          return None
144
        return v
145

    
146
    for line in iffile:
147
        if line.startswith("IP="):
148
            ip = get_value(line)
149
        elif line.startswith("MAC="):
150
            mac = get_value(line)
151
        elif line.startswith("HOSTNAME="):
152
            hostname = get_value(line)
153
        elif line.startswith("INDEV="):
154
            indev = get_value(line)
155
        elif line.startswith("SUBNET="):
156
            subnet = get_value(line)
157
        elif line.startswith("GATEWAY="):
158
            gateway = get_value(line)
159
        elif line.startswith("SUBNET6="):
160
            subnet6 = get_value(line)
161
        elif line.startswith("GATEWAY6="):
162
            gateway6 = get_value(line)
163
        elif line.startswith("EUI64="):
164
            eui64 = get_value(line)
165

    
166
    return Client(tap=tap, mac=mac, ip=ip,
167
                  hostname=hostname, indev=indev, subnet=subnet,
168
                  gateway=gateway, subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
169

    
170
class ClientFileHandler(pyinotify.ProcessEvent):
171
    def __init__(self, server):
172
        pyinotify.ProcessEvent.__init__(self)
173
        self.server = server
174

    
175
    def process_IN_DELETE(self, event): # pylint: disable=C0103
176
        """ Delete file handler
177

    
178
        Currently this removes an interface from the watch list
179

    
180
        """
181
        self.server.remove_tap(event.name)
182

    
183
    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
184
        """ Add file handler
185

    
186
        Currently this adds an interface to the watch list
187

    
188
        """
189
        self.server.add_tap(os.path.join(event.path, event.name))
190

    
191

    
192
class Client(object):
193
    def __init__(self, tap=None, indev=None, mac=None, ip=None, hostname=None,
194
                 subnet=None, gateway=None, subnet6=None, gateway6=None, eui64=None ):
195
        self.mac = mac
196
        self.ip = ip
197
        self.hostname = hostname
198
        self.indev = indev
199
        self.tap = tap
200
        self.subnet = subnet
201
        self.gateway = gateway
202
        self.net = Subnet(net=subnet, gw=gateway, dev=tap)
203
        self.subnet6 = subnet6
204
        self.gateway6 = gateway6
205
        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
206
        self.eui64 = eui64
207

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

    
212

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
276

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

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

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

    
300
        self.ipv6_enabled = False
301

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

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

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

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

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

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

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

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

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

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

    
347
        logging.info("Cleanup finished")
348

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

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

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

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

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

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

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

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

    
389
        ifindex = None
390

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

    
397
            return ifindex
398

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

    
413
        return ifindex
414

    
415

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

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

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

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

    
440
        return addr
441

    
442
    def add_tap(self, path):
443
        """ Add an interface to monitor
444

    
445
        """
446
        tap = os.path.basename(path)
447

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

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

    
462
    def remove_tap(self, tap):
463
        """ Cleanup clients on a removed interface
464

    
465
        """
466
        for b in self.clients.values():
467
            if b.tap == tap:
468
                del b
469

    
470
        logging.debug("Removed interface %s", tap)
471

    
472
    def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
473
        """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
474

    
475
        """
476
        # Decode the response - NFQUEUE relays IP packets
477
        pkt = IP(payload.get_data())
478
        logging.debug("IN DHCP RESPONCE")
479
        logging.debug(pkt.show())
480

    
481
        # Get the client MAC address
482
        resp = pkt.getlayer(BOOTP).copy()
483
        hlen = resp.hlen
484
        mac = resp.chaddr[:hlen].encode("hex")
485
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
486

    
487
        # Server responses are always BOOTREPLYs
488
        resp.op = "BOOTREPLY"
489
        del resp.payload
490

    
491
        try:
492
            binding = self.clients[mac]
493
        except KeyError:
494
            logging.warn("Invalid client for mac %s ", mac)
495
            payload.set_verdict(nfqueue.NF_ACCEPT)
496
            return
497

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

    
501
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
502
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
503
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
504
        subnet = binding.net
505

    
506
        if not DHCP in pkt:
507
            logging.warn("Invalid request from %s on %s, no DHCP"
508
                         " payload found", binding.mac, binding.tap)
509
            return
510

    
511
        dhcp_options = []
512
        requested_addr = binding.ip
513
        for opt in pkt[DHCP].options:
514
            if type(opt) is tuple and opt[0] == "message-type":
515
                req_type = opt[1]
516
            if type(opt) is tuple and opt[0] == "requested_addr":
517
                requested_addr = opt[1]
518

    
519
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
520
                     binding.mac, binding.tap)
521

    
522
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
523
            resp_type = DHCPNAK
524
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
525
                         " instead of %s", binding.mac, binding.tap, requested_addr,
526
                         binding.ip)
527

    
528
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
529
            resp_type = DHCP_REQRESP[req_type]
530
            resp.yiaddr = self.clients[mac].ip
531
            dhcp_options += [
532
                 ("hostname", binding.hostname),
533
                 ("domain", binding.hostname.split('.', 1)[-1]),
534
                 ("broadcast_address", str(subnet.broadcast)),
535
                 ("subnet_mask", str(subnet.netmask)),
536
                 ("renewal_time", self.lease_renewal),
537
                 ("lease_time", self.lease_lifetime),
538
            ]
539
            if subnet.gw:
540
              dhcp_options += [("router", subnet.gw)]
541
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
542

    
543
        elif req_type == DHCPINFORM:
544
            resp_type = DHCP_REQRESP[req_type]
545
            dhcp_options += [
546
                 ("hostname", binding.hostname),
547
                 ("domain", binding.hostname.split('.', 1)[-1]),
548
            ]
549
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
550

    
551
        elif req_type == DHCPRELEASE:
552
            # Log and ignore
553
            logging.info("DHCPRELEASE from %s on %s", binding.mac, binding.tap )
554
            return
555

    
556
        # Finally, always add the server identifier and end options
557
        dhcp_options += [
558
            ("message-type", resp_type),
559
            ("server_id", DHCP_DUMMY_SERVER_IP),
560
            "end"
561
        ]
562
        resp /= DHCP(options=dhcp_options)
563

    
564
        if payload.get_indev() != self.get_ifindex(binding.indev):
565
            logging.warn("Received spoofed DHCP request for %s from interface"
566
                         " %s instead of %s", mac, payload.get_indev(), binding.indev)
567
            return
568

    
569
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
570
                     binding.ip, binding.tap)
571
        self.sendp(resp, binding.indev)
572

    
573
    def rs_response(self, i, payload): # pylint: disable=W0613
574
        """ Generate a reply to a BOOTP/DHCP request
575

    
576
        """
577
        pkt = IPv6(payload.get_data())
578
        logging.debug("IN RS RESPONCE")
579
        logging.debug(pkt.show())
580
        mac = pkt.lladdr
581
        logging.debug("rs for mac %s", mac)
582
        try:
583
          binding = self.clients[mac]
584
        except KeyError:
585
            logging.debug("Ignoring router solicitation on"
586
                          " for mac %s", mac)
587
            # We don't know what to do with this packet, so let the kernel
588
            # handle it
589
            payload.set_verdict(nfqueue.NF_ACCEPT)
590
            return
591

    
592
        # Signal the kernel that it shouldn't further process the packet
593
        payload.set_verdict(nfqueue.NF_DROP)
594

    
595
        subnet = binding.net6
596

    
597
        if subnet.net is None:
598
          logging.debug("No IPv6 network assigned for the interface")
599
          return
600

    
601
        ifmac = self.get_iface_hw_addr(binding.indev)
602
        ifll = subnet.make_ll64(ifmac)
603

    
604

    
605
        resp = Ether(src=ifmac)/\
606
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
607
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
608
                                     prefixlen=subnet.prefixlen)
609

    
610
        if self.ipv6_nameservers:
611
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
612
                                     lifetime=self.ra_period * 3)
613

    
614
        logging.info("RA on %s for %s", binding.indev, subnet.net)
615
        self.sendp(resp, binding.indev)
616

    
617
    def ns_response(self, i, payload): # pylint: disable=W0613
618
        """ Generate a reply to an ICMPv6 neighbor solicitation
619

    
620
        """
621
        ns = IPv6(payload.get_data())
622
        logging.debug("IN NS RESPONCE")
623
        logging.debug(ns.show())
624

    
625
        logging.debug("dst %s  tgt %s" , ns.dst, ns.tgt)
626

    
627
        try:
628
          binding = self.clients[ns.lladdr]
629
        except:
630
          logging.debug("Ignoring neighbour solicitation for eui64 %s", ns.tgt)
631
          # We don't know what to do with this packet, so let the kernel
632
          # handle it
633
          payload.set_verdict(nfqueue.NF_ACCEPT)
634
          return
635

    
636
        subnet = binding.net6
637
        if subnet.net is None:
638
          logging.debug("No IPv6 network assigned for the interface")
639
          payload.set_verdict(nfqueue.NF_ACCEPT)
640
          return
641

    
642
        payload.set_verdict(nfqueue.NF_DROP)
643

    
644
        indevmac = self.get_iface_hw_addr(binding.indev)
645

    
646
        ifll = subnet.make_ll64(indevmac)
647

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

    
653
        logging.debug("na ether %s %s", binding.mac, ns.src)
654
        resp = Ether(src=indevmac, dst=binding.mac)/\
655
               IPv6(src=str(ifll), dst=ns.src)/\
656
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
657
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
658

    
659
        logging.info("NA on %s for %s", binding.indev, ns.tgt)
660
        self.sendp(resp, binding.indev)
661

    
662
    def send_periodic_ra(self):
663
        # Use a separate thread as this may take a _long_ time with
664
        # many interfaces and we want to be responsive in the mean time
665
        threading.Thread(target=self._send_periodic_ra).start()
666

    
667
    def _send_periodic_ra(self):
668
        logging.debug("Sending out periodic RAs")
669
        start = time.time()
670
        i = 0
671
        for binding in self.clients.values():
672
            tap = binding.tap
673
            indev = binding.indev
674
            mac = binding.mac
675
            subnet = binding.net6
676
            if subnet.net is None:
677
                logging.debug("Skipping periodic RA on interface %s,"
678
                              " as it is not IPv6-connected", tap)
679
                continue
680
            indevmac = self.get_iface_hw_addr(indev)
681
            ifll = subnet.make_ll64(indevmac)
682
            resp = Ether(src=indevmac)/\
683
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
684
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
685
                                         prefixlen=subnet.prefixlen)
686
            if self.ipv6_nameservers:
687
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
688
                                         lifetime=self.ra_period * 3)
689
            try:
690
                self.sendp(resp, indev)
691
            except socket.error, e:
692
                logging.warn("Periodic RA on %s failed: %s", tap, str(e))
693
            except Exception, e:
694
                logging.warn("Unkown error during periodic RA on %s: %s",
695
                             tap, str(e))
696
            i += 1
697
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
698

    
699
    def serve(self):
700
        """ Safely perform the main loop, freeing all resources upon exit
701

    
702
        """
703
        try:
704
            self._serve()
705
        finally:
706
            self._cleanup()
707

    
708
    def _serve(self):
709
        """ Loop forever, serving DHCP requests
710

    
711
        """
712
        self.build_config()
713

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

    
718
        start = time.time()
719
        if self.ipv6_enabled:
720
            timeout = self.ra_period
721
            self.send_periodic_ra()
722
        else:
723
            timeout = None
724

    
725
        while True:
726
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
727
            if xlist:
728
                logging.warn("Warning: Exception on %s",
729
                             ", ".join([ str(fd) for fd in xlist]))
730

    
731
            if rlist:
732
                if iwfd in rlist:
733
                # First check if there are any inotify (= configuration change)
734
                # events
735
                    self.notifier.read_events()
736
                    self.notifier.process_events()
737
                    rlist.remove(iwfd)
738

    
739
                for fd in rlist:
740
                    try:
741
                        self.nfq[fd].process_pending()
742
                    except RuntimeError, e:
743
                        logging.warn("Error processing fd %d: %s", fd, str(e))
744
                    except Exception, e:
745
                        logging.warn("Unknown error processing fd %d: %s",
746
                                     fd, str(e))
747

    
748
            if self.ipv6_enabled:
749
                # Calculate the new timeout
750
                timeout = self.ra_period - (time.time() - start)
751

    
752
                if timeout <= 0:
753
                    start = time.time()
754
                    self.send_periodic_ra()
755
                    timeout = self.ra_period - (time.time() - start)
756

    
757

    
758
if __name__ == "__main__":
759
    import capng
760
    import optparse
761
    from cStringIO import StringIO
762
    from pwd import getpwnam, getpwuid
763
    from configobj import ConfigObj, ConfigObjError, flatten_errors
764

    
765
    import validate
766

    
767
    validator = validate.Validator()
768

    
769
    def is_ip_list(value, family=4):
770
        try:
771
            family = int(family)
772
        except ValueError:
773
            raise validate.VdtParamError(family)
774
        if isinstance(value, (str, unicode)):
775
            value = [value]
776
        if not isinstance(value, list):
777
            raise validate.VdtTypeError(value)
778

    
779
        for entry in value:
780
            try:
781
                ip = IPy.IP(entry)
782
            except ValueError:
783
                raise validate.VdtValueError(entry)
784

    
785
            if ip.version() != family:
786
                raise validate.VdtValueError(entry)
787
        return value
788

    
789
    validator.functions["ip_addr_list"] = is_ip_list
790
    config_spec = StringIO(CONFIG_SPEC)
791

    
792

    
793
    parser = optparse.OptionParser()
794
    parser.add_option("-c", "--config", dest="config_file",
795
                      help="The location of the data files", metavar="FILE",
796
                      default=DEFAULT_CONFIG)
797
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
798
                      help="Turn on debugging messages")
799
    parser.add_option("-f", "--foreground", action="store_false",
800
                      dest="daemonize", default=True,
801
                      help="Do not daemonize, stay in the foreground")
802

    
803

    
804
    opts, args = parser.parse_args()
805

    
806
    try:
807
        config = ConfigObj(opts.config_file, configspec=config_spec)
808
    except ConfigObjError, err:
809
        sys.stderr.write("Failed to parse config file %s: %s" %
810
                         (opts.config_file, str(err)))
811
        sys.exit(1)
812

    
813
    results = config.validate(validator)
814
    if results != True:
815
        logging.fatal("Configuration file validation failed! See errors below:")
816
        for (section_list, key, unused) in flatten_errors(config, results):
817
            if key is not None:
818
                logging.fatal(" '%s' in section '%s' failed validation",
819
                              key, ", ".join(section_list))
820
            else:
821
                logging.fatal(" Section '%s' is missing",
822
                              ", ".join(section_list))
823
        sys.exit(1)
824

    
825
    logger = logging.getLogger()
826
    if opts.debug:
827
        logger.setLevel(logging.DEBUG)
828
    else:
829
        logger.setLevel(logging.INFO)
830

    
831
    if opts.daemonize:
832
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
833
        handler = logging.handlers.RotatingFileHandler(logfile,
834
                                                       maxBytes=2097152)
835
    else:
836
        handler = logging.StreamHandler()
837

    
838
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
839
    logger.addHandler(handler)
840

    
841
    if opts.daemonize:
842
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
843
            config["general"]["pidfile"], 10)
844

    
845
        d = daemon.DaemonContext(pidfile=pidfile,
846
                                 stdout=handler.stream,
847
                                 stderr=handler.stream,
848
                                 files_preserve=[handler.stream])
849
        d.umask = 0022
850
        d.open()
851

    
852
    logging.info("Starting up")
853

    
854
    proxy_opts = {}
855
    if config["dhcp"].as_bool("enable_dhcp"):
856
        proxy_opts.update({
857
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
858
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
859
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
860
            "dhcp_server_ip": config["dhcp"]["server_ip"],
861
            "dhcp_nameservers": config["dhcp"]["nameservers"],
862
        })
863

    
864
    if config["ipv6"].as_bool("enable_ipv6"):
865
        proxy_opts.update({
866
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
867
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
868
            "ra_period": config["ipv6"].as_int("ra_period"),
869
            "ipv6_nameservers": config["ipv6"]["nameservers"],
870
        })
871

    
872
    # pylint: disable=W0142
873
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
874

    
875
    # Drop all capabilities except CAP_NET_RAW and change uid
876
    try:
877
        uid = getpwuid(config["general"].as_int("user"))
878
    except ValueError:
879
        uid = getpwnam(config["general"]["user"])
880

    
881
    logging.debug("Setting capabilities and changing uid")
882
    logging.debug("User: %s, uid: %d, gid: %d",
883
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
884

    
885
    # Keep only the capabilities we need
886
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
887
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
888
    capng.capng_update(capng.CAPNG_ADD,
889
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
890
                       capng.CAP_NET_ADMIN)
891
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
892
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
893

    
894
    logging.info("Ready to serve requests")
895
    try:
896
        proxy.serve()
897
    except Exception:
898
        if opts.daemonize:
899
            exc = "".join(traceback.format_exception(*sys.exc_info()))
900
            logging.critical(exc)
901
        raise
902

    
903

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