4 # nfdcpd: A promiscuous, NFQUEUE-based DHCP server for virtual machine hosting
5 # Copyright (c) 2010 GRNET SA
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.
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.
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.
28 import logging.handlers
34 import daemon.pidlockfile
40 from select import select
41 from socket import AF_INET, AF_INET6
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, \
51 from scapy.layers.dhcp import BOOTP, DHCP
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"
61 LOG_FILENAME = "nfdhcpd.log"
63 SYSFS_NET = "/sys/class/net"
65 LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
67 # Configuration file specification (see configobj documentation)
76 enable_dhcp = boolean(default=True)
77 lease_lifetime = integer(min=0, max=4294967295)
78 lease_renewal = integer(min=0, max=4294967295)
80 dhcp_queue = integer(min=0, max=65535)
81 nameservers = ip_addr_list(family=4)
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)
102 DHCPDISCOVER: "DHCPDISCOVER",
103 DHCPOFFER: "DHCPOFFER",
104 DHCPREQUEST: "DHCPREQUEST",
105 DHCPDECLINE: "DHCPDECLINE",
108 DHCPRELEASE: "DHCPRELEASE",
109 DHCPINFORM: "DHCPINFORM",
113 DHCPDISCOVER: DHCPOFFER,
114 DHCPREQUEST: DHCPACK,
119 def parse_routing_table(table="main", family=4):
120 """ Parse the given routing table to get connected route, gateway and
124 ipro = subprocess.Popen(["ip", "-%d" % family, "ro", "ls",
125 "table", table], stdout=subprocess.PIPE)
126 routes = ipro.stdout.readlines()
133 match = re.match(r'^default.*via ([^\s]+).*dev ([^\s]+)', route)
135 def_gw, def_dev = match.groups()
139 # Find the least-specific connected route
140 m = re.match("^([^\\s]+) dev %s" % def_dev, route)
144 if family == 6 and m.group(1).startswith("fe80:"):
145 # Skip link-local declarations in "main" table
151 def_net = IPy.IP(def_net)
152 except ValueError, e:
153 logging.warn("Unable to parse default route entry %s: %s",
156 return Subnet(net=def_net, gw=def_gw, dev=def_dev)
159 def parse_binding_file(path):
160 """ Read a client configuration from a tap file
164 iffile = open(path, 'r')
165 except EnvironmentError, e:
166 logging.warn("Unable to open binding file %s: %s", path, str(e))
175 if line.startswith("IP="):
176 ip = line.strip().split("=")[1]
178 elif line.startswith("MAC="):
179 mac = line.strip().split("=")[1]
180 elif line.startswith("LINK="):
181 link = line.strip().split("=")[1]
182 elif line.startswith("HOSTNAME="):
183 hostname = line.strip().split("=")[1]
185 return Client(mac=mac, ips=ips, link=link, hostname=hostname)
188 class ClientFileHandler(pyinotify.ProcessEvent):
189 def __init__(self, server):
190 pyinotify.ProcessEvent.__init__(self)
193 def process_IN_DELETE(self, event): # pylint: disable=C0103
194 """ Delete file handler
196 Currently this removes an interface from the watch list
199 self.server.remove_iface(event.name)
201 def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
204 Currently this adds an interface to the watch list
207 self.server.add_iface(os.path.join(event.path, event.name))
210 class Client(object):
211 def __init__(self, mac=None, ips=None, link=None, hostname=None):
214 self.hostname = hostname
223 return self.mac is not None and self.ips is not None\
224 and self.hostname is not None
227 class Subnet(object):
228 def __init__(self, net=None, gw=None, dev=None):
229 if isinstance(net, str):
230 self.net = IPy.IP(net)
238 """ Return the netmask in textual representation
241 return str(self.net.netmask())
245 """ Return the broadcast address in textual representation
248 return str(self.net.broadcast())
252 """ Return the network as an IPy.IP
255 return self.net.net()
259 """ Return the prefix length as an integer
262 return self.net.prefixlen()
265 def _make_eui64(net, mac):
266 """ Compute an EUI-64 address from an EUI-48 (MAC) address
269 comp = mac.split(":")
270 prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
271 eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
272 eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
273 for l in range(0, len(eui64), 2):
274 prefix += ["".join(eui64[l:l+2])]
275 return IPy.IP(":".join(prefix))
277 def make_eui64(self, mac):
278 """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
282 return self._make_eui64(self.net, mac)
284 def make_ll64(self, mac):
285 """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
288 return self._make_eui64("fe80::", mac)
291 class VMNetProxy(object): # pylint: disable=R0902
292 def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
293 rs_queue_num=None, ns_queue_num=None,
294 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
295 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
296 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
297 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
299 self.data_path = data_path
300 self.lease_lifetime = dhcp_lease_lifetime
301 self.lease_renewal = dhcp_lease_renewal
302 self.dhcp_server_ip = dhcp_server_ip
303 self.ra_period = ra_period
304 if dhcp_nameservers is None:
305 self.dhcp_nameserver = []
307 self.dhcp_nameservers = dhcp_nameservers
309 if ipv6_nameservers is None:
310 self.ipv6_nameservers = []
312 self.ipv6_nameservers = ipv6_nameservers
314 self.ipv6_enabled = False
321 self.l2socket = socket.socket(socket.AF_PACKET,
322 socket.SOCK_RAW, ETH_P_ALL)
323 self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
326 self.wm = pyinotify.WatchManager()
327 mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
328 mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
329 inotify_handler = ClientFileHandler(self)
330 self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
331 self.wm.add_watch(self.data_path, mask, rec=True)
334 if dhcp_queue_num is not None:
335 self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
337 if rs_queue_num is not None:
338 self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
339 self.ipv6_enabled = True
341 if ns_queue_num is not None:
342 self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
343 self.ipv6_enabled = True
346 """ Free all resources for a graceful exit
349 logging.info("Cleaning up")
351 logging.debug("Closing netfilter queues")
352 for q in self.nfq.values():
355 logging.debug("Closing socket")
356 self.l2socket.close()
358 logging.debug("Stopping inotify watches")
361 logging.info("Cleanup finished")
363 def _setup_nfqueue(self, queue_num, family, callback):
364 logging.debug("Setting up NFQUEUE for queue %d, AF %s",
367 q.set_callback(callback)
368 q.fast_open(queue_num, family)
369 q.set_queue_maxlen(5000)
370 # This is mandatory for the queue to operate
371 q.set_mode(nfqueue.NFQNL_COPY_PACKET)
372 self.nfq[q.get_fd()] = q
374 def sendp(self, data, iface):
375 """ Send a raw packet using a layer-2 socket
378 if isinstance(data, BasePacket):
381 self.l2socket.bind((iface, ETH_P_ALL))
382 count = self.l2socket.send(data)
385 logging.warn("Truncated send on %s (%d/%d bytes sent)",
388 def build_config(self):
392 for path in glob.glob(os.path.join(self.data_path, "*")):
395 def get_ifindex(self, iface):
396 """ Get the interface index from sysfs
399 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
400 if not path.startswith(SYSFS_NET):
407 except EnvironmentError:
408 logging.debug("%s is probably down, removing", iface)
409 self.remove_iface(iface)
414 ifindex = f.readline().strip()
416 ifindex = int(ifindex)
417 except ValueError, e:
418 logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
419 " output '%s'", iface, ifindex)
420 except EnvironmentError, e:
421 logging.warn("Error reading %s's ifindex from sysfs: %s",
423 self.remove_iface(iface)
430 def get_iface_hw_addr(self, iface):
431 """ Get the interface hardware address from sysfs
434 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
435 if not path.startswith(SYSFS_NET):
441 except EnvironmentError:
442 logging.debug("%s is probably down, removing", iface)
443 self.remove_iface(iface)
447 addr = f.readline().strip()
448 except EnvironmentError, e:
449 logging.warn("Failed to read hw address for %s from sysfs: %s",
456 def add_iface(self, path):
457 """ Add an interface to monitor
460 iface = os.path.basename(path)
462 logging.debug("Updating configuration for %s", iface)
463 binding = parse_binding_file(path)
466 ifindex = self.get_ifindex(iface)
469 logging.warn("Stale configuration for %s found", iface)
471 if binding.is_valid():
472 binding.iface = iface
473 self.clients[binding.mac] = binding
474 self.subnets[binding.link] = parse_routing_table(binding.link)
475 logging.debug("Added client %s on %s", binding.hostname, iface)
476 self.ifaces[ifindex] = iface
477 self.v6nets[iface] = parse_routing_table(binding.link, 6)
479 def remove_iface(self, iface):
480 """ Cleanup clients on a removed interface
483 if iface in self.v6nets:
484 del self.v6nets[iface]
486 for mac in self.clients.keys():
487 if self.clients[mac].iface == iface:
488 del self.clients[mac]
490 for ifindex in self.ifaces.keys():
491 if self.ifaces[ifindex] == iface:
492 del self.ifaces[ifindex]
494 logging.debug("Removed interface %s", iface)
496 def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
497 """ Generate a reply to a BOOTP/DHCP request
500 indev = payload.get_indev()
502 # Get the actual interface from the ifindex
503 iface = self.ifaces[indev]
505 # We don't know anything about this interface, so accept the packet
507 logging.debug("Ignoring DHCP request on unknown iface %d", indev)
508 # We don't know what to do with this packet, so let the kernel
510 payload.set_verdict(nfqueue.NF_ACCEPT)
513 # Decode the response - NFQUEUE relays IP packets
514 pkt = IP(payload.get_data())
516 # Signal the kernel that it shouldn't further process the packet
517 payload.set_verdict(nfqueue.NF_DROP)
519 # Get the client MAC address
520 resp = pkt.getlayer(BOOTP).copy()
522 mac = resp.chaddr[:hlen].encode("hex")
523 mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
525 # Server responses are always BOOTREPLYs
526 resp.op = "BOOTREPLY"
530 binding = self.clients[mac]
532 logging.warn("Invalid client %s on %s", mac, iface)
535 if iface != binding.iface:
536 logging.warn("Received spoofed DHCP request for %s from interface"
537 " %s instead of %s", mac, iface, binding.iface)
540 resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
541 IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
542 UDP(sport=pkt.dport, dport=pkt.sport)/resp
543 subnet = self.subnets[binding.link]
546 logging.warn("Invalid request from %s on %s, no DHCP"
547 " payload found", binding.mac, iface)
551 requested_addr = binding.ip
552 for opt in pkt[DHCP].options:
553 if type(opt) is tuple and opt[0] == "message-type":
555 if type(opt) is tuple and opt[0] == "requested_addr":
556 requested_addr = opt[1]
558 logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
561 if req_type == DHCPREQUEST and requested_addr != binding.ip:
563 logging.info("Sending DHCPNAK to %s on %s: requested %s"
564 " instead of %s", binding.mac, iface, requested_addr,
567 elif req_type in (DHCPDISCOVER, DHCPREQUEST):
568 resp_type = DHCP_REQRESP[req_type]
569 resp.yiaddr = self.clients[mac].ip
571 ("hostname", binding.hostname),
572 ("domain", binding.hostname.split('.', 1)[-1]),
573 ("router", subnet.gw),
574 ("broadcast_address", str(subnet.broadcast)),
575 ("subnet_mask", str(subnet.netmask)),
576 ("renewal_time", self.lease_renewal),
577 ("lease_time", self.lease_lifetime),
579 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
581 elif req_type == DHCPINFORM:
582 resp_type = DHCP_REQRESP[req_type]
584 ("hostname", binding.hostname),
585 ("domain", binding.hostname.split('.', 1)[-1]),
587 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
589 elif req_type == DHCPRELEASE:
591 logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
594 # Finally, always add the server identifier and end options
596 ("message-type", resp_type),
597 ("server_id", DHCP_DUMMY_SERVER_IP),
600 resp /= DHCP(options=dhcp_options)
602 logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
604 self.sendp(resp, iface)
606 def rs_response(self, i, payload): # pylint: disable=W0613
607 """ Generate a reply to a BOOTP/DHCP request
610 indev = payload.get_indev()
612 # Get the actual interface from the ifindex
613 iface = self.ifaces[indev]
615 logging.debug("Ignoring router solicitation on"
616 " unknown interface %d", indev)
617 # We don't know what to do with this packet, so let the kernel
619 payload.set_verdict(nfqueue.NF_ACCEPT)
622 ifmac = self.get_iface_hw_addr(iface)
623 subnet = self.v6nets[iface]
624 ifll = subnet.make_ll64(ifmac)
626 # Signal the kernel that it shouldn't further process the packet
627 payload.set_verdict(nfqueue.NF_DROP)
629 resp = Ether(src=self.get_iface_hw_addr(iface))/\
630 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
631 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
632 prefixlen=subnet.prefixlen)
634 if self.ipv6_nameservers:
635 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
636 lifetime=self.ra_period * 3)
638 logging.info("RA on %s for %s", iface, subnet.net)
639 self.sendp(resp, iface)
641 def ns_response(self, i, payload): # pylint: disable=W0613
642 """ Generate a reply to an ICMPv6 neighbor solicitation
645 indev = payload.get_indev()
647 # Get the actual interface from the ifindex
648 iface = self.ifaces[indev]
650 logging.debug("Ignoring neighbour solicitation on"
651 " unknown interface %d", indev)
652 # We don't know what to do with this packet, so let the kernel
654 payload.set_verdict(nfqueue.NF_ACCEPT)
657 ifmac = self.get_iface_hw_addr(iface)
658 subnet = self.v6nets[iface]
659 ifll = subnet.make_ll64(ifmac)
661 ns = IPv6(payload.get_data())
663 if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
664 logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
665 payload.set_verdict(nfqueue.NF_ACCEPT)
668 payload.set_verdict(nfqueue.NF_DROP)
671 client_lladdr = ns.lladdr
672 except AttributeError:
675 resp = Ether(src=ifmac, dst=client_lladdr)/\
676 IPv6(src=str(ifll), dst=ns.src)/\
677 ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
678 ICMPv6NDOptDstLLAddr(lladdr=ifmac)
680 logging.info("NA on %s for %s", iface, ns.tgt)
681 self.sendp(resp, iface)
684 def send_periodic_ra(self):
685 # Use a separate thread as this may take a _long_ time with
686 # many interfaces and we want to be responsive in the mean time
687 threading.Thread(target=self._send_periodic_ra).start()
689 def _send_periodic_ra(self):
690 logging.debug("Sending out periodic RAs")
693 for client in self.clients.values():
695 ifmac = self.get_iface_hw_addr(iface)
699 subnet = self.v6nets[iface]
700 if subnet.net is None:
701 logging.debug("Skipping periodic RA on interface %s,"
702 " as it is not IPv6-connected", iface)
705 ifll = subnet.make_ll64(ifmac)
706 resp = Ether(src=ifmac)/\
707 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
708 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
709 prefixlen=subnet.prefixlen)
710 if self.ipv6_nameservers:
711 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
712 lifetime=self.ra_period * 3)
714 self.sendp(resp, iface)
715 except socket.error, e:
716 logging.warn("Periodic RA on %s failed: %s", iface, str(e))
718 logging.warn("Unkown error during periodic RA on %s: %s",
721 logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
724 """ Safely perform the main loop, freeing all resources upon exit
733 """ Loop forever, serving DHCP requests
738 # Yes, we are accessing _fd directly, but it's the only way to have a
739 # single select() loop ;-)
740 iwfd = self.notifier._fd # pylint: disable=W0212
743 if self.ipv6_enabled:
744 timeout = self.ra_period
745 self.send_periodic_ra()
750 rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
752 logging.warn("Warning: Exception on %s",
753 ", ".join([ str(fd) for fd in xlist]))
757 # First check if there are any inotify (= configuration change)
759 self.notifier.read_events()
760 self.notifier.process_events()
765 self.nfq[fd].process_pending()
766 except RuntimeError, e:
767 logging.warn("Error processing fd %d: %s", fd, str(e))
769 logging.warn("Unknown error processing fd %d: %s",
772 if self.ipv6_enabled:
773 # Calculate the new timeout
774 timeout = self.ra_period - (time.time() - start)
778 self.send_periodic_ra()
779 timeout = self.ra_period - (time.time() - start)
782 if __name__ == "__main__":
785 from cStringIO import StringIO
786 from pwd import getpwnam, getpwuid
787 from configobj import ConfigObj, ConfigObjError, flatten_errors
791 validator = validate.Validator()
793 def is_ip_list(value, family=4):
797 raise validate.VdtParamError(family)
798 if isinstance(value, (str, unicode)):
800 if not isinstance(value, list):
801 raise validate.VdtTypeError(value)
807 raise validate.VdtValueError(entry)
809 if ip.version() != family:
810 raise validate.VdtValueError(entry)
813 validator.functions["ip_addr_list"] = is_ip_list
814 config_spec = StringIO(CONFIG_SPEC)
817 parser = optparse.OptionParser()
818 parser.add_option("-c", "--config", dest="config_file",
819 help="The location of the data files", metavar="FILE",
820 default=DEFAULT_CONFIG)
821 parser.add_option("-d", "--debug", action="store_true", dest="debug",
822 help="Turn on debugging messages")
823 parser.add_option("-f", "--foreground", action="store_false",
824 dest="daemonize", default=True,
825 help="Do not daemonize, stay in the foreground")
828 opts, args = parser.parse_args()
831 config = ConfigObj(opts.config_file, configspec=config_spec)
832 except ConfigObjError, err:
833 sys.stderr.write("Failed to parse config file %s: %s" %
834 (opts.config_file, str(err)))
837 results = config.validate(validator)
839 logging.fatal("Configuration file validation failed! See errors below:")
840 for (section_list, key, unused) in flatten_errors(config, results):
842 logging.fatal(" '%s' in section '%s' failed validation",
843 key, ", ".join(section_list))
845 logging.fatal(" Section '%s' is missing",
846 ", ".join(section_list))
849 logger = logging.getLogger()
851 logger.setLevel(logging.DEBUG)
853 logger.setLevel(logging.INFO)
856 logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
857 handler = logging.handlers.RotatingFileHandler(logfile,
860 handler = logging.StreamHandler()
862 handler.setFormatter(logging.Formatter(LOG_FORMAT))
863 logger.addHandler(handler)
866 pidfile = daemon.pidlockfile.TimeoutPIDLockFile
867 config["general"]["pidfile"], 10)
869 d = daemon.DaemonContext(pidfile=pidfile,
870 stdout=handler.stream,
871 stderr=handler.stream,
872 files_preserve=[handler.stream])
876 logging.info("Starting up")
879 if config["dhcp"].as_bool("enable_dhcp"):
881 "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
882 "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
883 "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
884 "dhcp_server_ip": config["dhcp"]["server_ip"],
885 "dhcp_nameservers": config["dhcp"]["nameservers"],
888 if config["ipv6"].as_bool("enable_ipv6"):
890 "rs_queue_num": config["ipv6"].as_int("rs_queue"),
891 "ns_queue_num": config["ipv6"].as_int("ns_queue"),
892 "ra_period": config["ipv6"].as_int("ra_period"),
893 "ipv6_nameservers": config["ipv6"]["nameservers"],
896 # pylint: disable=W0142
897 proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
899 # Drop all capabilities except CAP_NET_RAW and change uid
901 uid = getpwuid(config["general"].as_int("user"))
903 uid = getpwnam(config["general"]["user"])
905 logging.debug("Setting capabilities and changing uid")
906 logging.debug("User: %s, uid: %d, gid: %d",
907 config["general"]["user"], uid.pw_uid, uid.pw_gid)
909 # Keep only the capabilities we need
910 # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
911 capng.capng_clear(capng.CAPNG_SELECT_BOTH)
912 capng.capng_update(capng.CAPNG_ADD,
913 capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
915 capng.capng_change_id(uid.pw_uid, uid.pw_gid,
916 capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
918 logging.info("Ready to serve requests")
923 exc = "".join(traceback.format_exception(*sys.exc_info()))
924 logging.critical(exc)
928 # vim: set ts=4 sts=4 sw=4 et :