Revision 6765a36f

b/nfdhcpd
21 21

  
22 22
import os
23 23
import re
24
import sys
24 25
import glob
25 26
import time
26 27
import logging
......
39 40

  
40 41
from scapy.layers.l2 import Ether
41 42
from scapy.layers.inet import IP, UDP
42
from scapy.layers.inet6 import *
43
from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
44
                               ICMPv6NDOptDstLLAddr, \
45
                               ICMPv6NDOptPrefixInfo, \
46
                               ICMPv6NDOptRDNSS
43 47
from scapy.layers.dhcp import BOOTP, DHCP
44 48
from scapy.sendrecv import sendp
45 49

  
......
109 113
    }
110 114

  
111 115

  
116
def parse_routing_table(table="main", family=4):
117
    """ Parse the given routing table to get connected route, gateway and
118
    default device.
119

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

  
125
    def_gw = None
126
    def_dev = None
127
    def_net = None
128

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

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

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

  
148
    return Subnet(net=def_net, gw=def_gw, dev=def_dev)
149

  
150

  
151
def parse_binding_file(path):
152
    """ Read a client configuration from a tap file
153

  
154
    """
155
    try:
156
        iffile = open(path, 'r')
157
    except EnvironmentError, e:
158
        logging.warn("Unable to open binding file %s: %s", path, str(e))
159
        return (None, None, None, None)
160

  
161
    mac = None
162
    ips = None
163
    link = None
164
    hostname = None
165

  
166
    for line in iffile:
167
        if line.startswith("IP="):
168
            ip = line.strip().split("=")[1]
169
            ips = ip.split()
170
        elif line.startswith("MAC="):
171
            mac = line.strip().split("=")[1]
172
        elif line.startswith("LINK="):
173
            link = line.strip().split("=")[1]
174
        elif line.startswith("HOSTNAME="):
175
            hostname = line.strip().split("=")[1]
176

  
177
    return Client(mac=mac, ips=ips, link=link, hostname=hostname)
178

  
179

  
112 180
class ClientFileHandler(pyinotify.ProcessEvent):
113 181
    def __init__(self, server):
114 182
        pyinotify.ProcessEvent.__init__(self)
115 183
        self.server = server
116 184

  
117 185
    def process_IN_DELETE(self, event):
186
        """ Delete file handler
187

  
188
        Currently this removes an interface from the watch list
189

  
190
        """
118 191
        self.server.remove_iface(event.name)
119 192

  
120 193
    def process_IN_CLOSE_WRITE(self, event):
194
        """ Add file handler
195

  
196
        Currently this adds an interface to the watch list
197

  
198
        """
121 199
        self.server.add_iface(os.path.join(event.path, event.name))
122 200

  
123 201

  
......
149 227

  
150 228
    @property
151 229
    def netmask(self):
230
        """ Return the netmask in textual representation
231

  
232
        """
152 233
        return str(self.net.netmask())
153 234

  
154 235
    @property
155 236
    def broadcast(self):
237
        """ Return the broadcast address in textual representation
238

  
239
        """
156 240
        return str(self.net.broadcast())
157 241

  
158 242
    @property
159 243
    def prefix(self):
244
        """ Return the network as an IPy.IP
245

  
246
        """
160 247
        return self.net.net()
161 248

  
162 249
    @property
163 250
    def prefixlen(self):
251
        """ Return the prefix length as an integer
252

  
253
        """
164 254
        return self.net.prefixlen()
165 255

  
166 256
    @staticmethod
......
177 267
        return IPy.IP(":".join(prefix))
178 268

  
179 269
    def make_eui64(self, mac):
270
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
271
        subnet.
272

  
273
        """
180 274
        return self._make_eui64(self.net, mac)
181 275

  
182 276
    def make_ll64(self, mac):
277
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
278

  
279
        """
183 280
        return self._make_eui64("fe80::", mac)
184 281

  
185 282

  
186
class VMNetProxy(object):
187
    def __init__(self, data_path, dhcp_queue_num=None,
283
class VMNetProxy(object): # pylint: disable=R0902
284
    def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
188 285
                 rs_queue_num=None, ns_queue_num=None,
189 286
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
190 287
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
191
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers = [],
192
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers = []):
288
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
289
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
193 290

  
194 291
        self.data_path = data_path
195 292
        self.lease_lifetime = dhcp_lease_lifetime
196 293
        self.lease_renewal = dhcp_lease_renewal
197 294
        self.dhcp_server_ip = dhcp_server_ip
198 295
        self.ra_period = ra_period
199
        self.dhcp_nameservers = dhcp_nameservers
200
        self.ipv6_nameservers = ipv6_nameservers
296
        if dhcp_nameservers is None:
297
            self.dhcp_nameserver = []
298
        else:
299
            self.dhcp_nameservers = dhcp_nameservers
300

  
301
        if ipv6_nameservers is None:
302
            self.ipv6_nameservers = []
303
        else:
304
            self.ipv6_nameservers = ipv6_nameservers
305

  
201 306
        self.ipv6_enabled = False
202 307

  
203 308
        self.clients = {}
......
210 315
        self.wm = pyinotify.WatchManager()
211 316
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
212 317
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
213
        handler = ClientFileHandler(self)
214
        self.notifier = pyinotify.Notifier(self.wm, handler)
318
        inotify_handler = ClientFileHandler(self)
319
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
215 320
        self.wm.add_watch(self.data_path, mask, rec=True)
216 321

  
217 322
        # NFQUEUE setup
......
227 332
            self.ipv6_enabled = True
228 333

  
229 334
    def _setup_nfqueue(self, queue_num, family, callback):
230
        logging.debug("Setting up NFQUEUE for queue %d, AF %s" %
231
                      (queue_num, family))
335
        logging.debug("Setting up NFQUEUE for queue %d, AF %s",
336
                      queue_num, family)
232 337
        q = nfqueue.queue()
233 338
        q.set_callback(callback)
234 339
        q.fast_open(queue_num, family)
......
241 346
        self.clients.clear()
242 347
        self.subnets.clear()
243 348

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

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

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

  
255 360
        ifindex = None
256 361

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

  
263 368
            return ifindex
......
268 373
                ifindex = int(ifindex)
269 374
            except ValueError, e:
270 375
                logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
271
                             " output '%s'" % (iface, ifindex))
376
                             " output '%s'", iface, ifindex)
272 377
        except EnvironmentError, e:
273
            logging.warn("Error reading %s's ifindex from sysfs: %s" %
274
                         (iface, str(e)))
378
            logging.warn("Error reading %s's ifindex from sysfs: %s",
379
                         iface, str(e))
275 380
            self.remove_iface(iface)
276 381
        finally:
277 382
            f.close()
......
283 388
        """ Get the interface hardware address from sysfs
284 389

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

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

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

  
306 411
        return addr
307 412

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  
393 434
    def remove_iface(self, iface):
394 435
        """ Cleanup clients on a removed interface
......
405 446
            if self.ifaces[ifindex] == iface:
406 447
                del self.ifaces[ifindex]
407 448

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

  
410
    def dhcp_response(self, i, payload):
451
    def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
411 452
        """ Generate a reply to a BOOTP/DHCP request
412 453

  
413 454
        """
......
433 474
        try:
434 475
            binding = self.clients[mac]
435 476
        except KeyError:
436
            logging.warn("Invalid client %s on %s" % (mac, iface))
477
            logging.warn("Invalid client %s on %s", mac, iface)
437 478
            return
438 479

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

  
445 485
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
......
449 489

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

  
455 495
        dhcp_options = []
......
460 500
            if type(opt) is tuple and opt[0] == "requested_addr":
461 501
                requested_addr = opt[1]
462 502

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

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

  
472 512
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
473 513
            resp_type = DHCP_REQRESP[req_type]
......
493 533

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

  
500 539
        # Finally, always add the server identifier and end options
......
505 544
        ]
506 545
        resp /= DHCP(options=dhcp_options)
507 546

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

  
512
    def rs_response(self, i, payload):
551
    def rs_response(self, i, payload): # pylint: disable=W0613
513 552
        """ Generate a reply to a BOOTP/DHCP request
514 553

  
515 554
        """
......
531 570
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
532 571
                                     lifetime=self.ra_period * 3)
533 572

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

  
537
    def ns_response(self, i, payload):
576
    def ns_response(self, i, payload): # pylint: disable=W0613
538 577
        """ Generate a reply to an ICMPv6 neighbor solicitation
539 578

  
540 579
        """
......
547 586
        ns = IPv6(payload.get_data())
548 587

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

  
......
563 602
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
564 603
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
565 604

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

  
......
594 633
            try:
595 634
                sendp(resp, iface=iface, verbose=False)
596 635
            except socket.error, e:
597
                logging.warn("Periodic RA on %s failed: %s" % (iface, str(e)))
636
                logging.warn("Periodic RA on %s failed: %s", iface, str(e))
598 637
            except Exception, e:
599
                logging.warn("Unkown error during periodic RA on %s: %s" %
600
                             (iface, str(e)))
638
                logging.warn("Unkown error during periodic RA on %s: %s",
639
                             iface, str(e))
601 640
            i += 1
602
        logging.debug("Sent %d RAs in %.2f seconds" % (i, time.time() - start))
641
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
603 642

  
604 643
    def serve(self):
605 644
        """ Loop forever, serving DHCP requests
......
607 646
        """
608 647
        self.build_config()
609 648

  
610
        iwfd = self.notifier._fd
649
        # Yes, we are accessing _fd directly, but it's the only way to have a 
650
        # single select() loop ;-)
651
        iwfd = self.notifier._fd # pylint: disable=W0212
611 652

  
612 653
        start = time.time()
613 654
        if self.ipv6_enabled:
......
619 660
        while True:
620 661
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
621 662
            if xlist:
622
                logging.warn("Warning: Exception on %s" %
663
                logging.warn("Warning: Exception on %s",
623 664
                             ", ".join([ str(fd) for fd in xlist]))
624 665

  
625 666
            if rlist:
......
633 674
                for fd in rlist:
634 675
                    try:
635 676
                        self.nfq[fd].process_pending()
677
                    except RuntimeError, e:
678
                        logging.warn("Error processing fd %d: %s", fd, str(e))
636 679
                    except Exception, e:
637
                        logging.warn("Error processing fd %d: %s" %
638
                                     (fd, str(e)))
680
                        logging.warn("Unknown error processing fd %d: %s",
681
                                     fd, str(e))
639 682

  
640 683
            if self.ipv6_enabled:
641 684
                # Calculate the new timeout
......
648 691

  
649 692

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

  
......
702 745

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

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

  
......
749 792
            "ipv6_nameservers": config["ipv6"]["nameservers"],
750 793
        })
751 794

  
795
    # pylint: disable=W0142
752 796
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
753 797

  
754 798
    # Drop all capabilities except CAP_NET_RAW and change uid
......
758 802
        uid = getpwnam(config["general"]["user"])
759 803

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

  
768 814
    if opts.daemonize:
769 815
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)

Also available in: Unified diff