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_binding_file(path):
120 """ Read a client configuration from a tap file
124 iffile = open(path, 'r')
125 except EnvironmentError, e:
126 logging.warn("Unable to open binding file %s: %s", path, str(e))
129 ifname = os.path.basename(path)
140 if line.startswith("IP="):
141 ip = line.strip().split("=")[1]
143 elif line.startswith("MAC="):
144 mac = line.strip().split("=")[1]
145 elif line.startswith("HOSTNAME="):
146 hostname = line.strip().split("=")[1]
147 elif line.startswith("IFACE="):
148 iface = line.strip().split("=")[1]
149 elif line.startswith("SUBNET="):
150 subnet = line.strip().split("=")[1]
151 elif line.startswith("GATEWAY="):
152 gateway = line.strip().split("=")[1]
153 elif line.startswith("SUBNET6="):
154 subnet6 = line.strip().split("=")[1]
155 elif line.startswith("GATEWAY6="):
156 gateway6 = line.strip().split("=")[1]
158 return Client(ifname=ifname, mac=mac, ips=ips, link=link,
159 hostname=hostname, iface=iface, subnet=subnet,
160 gateway=gateway, subnet6=subnet6, gateway6=gateway6 )
162 class ClientFileHandler(pyinotify.ProcessEvent):
163 def __init__(self, server):
164 pyinotify.ProcessEvent.__init__(self)
167 def process_IN_DELETE(self, event): # pylint: disable=C0103
168 """ Delete file handler
170 Currently this removes an interface from the watch list
173 self.server.remove_iface(event.name)
175 def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
178 Currently this adds an interface to the watch list
181 self.server.add_iface(os.path.join(event.path, event.name))
184 class Client(object):
185 def __init__(self, ifname=None, mac=None, ips=None, link=None,
186 hostname=None, iface=None, subnet=None, gateway=None,
187 subnet6=None, gateway6=None ):
190 self.hostname = hostname
195 self.gateway = gateway
196 self.net = Subnet(net=subnet, gw=gateway, dev=ifname)
197 self.subnet6 = subnet6
198 self.gateway6 = gateway6
199 self.net6 = Subnet(net=subnet6, gw=gateway6, dev=ifname)
206 return self.mac is not None and self.ips is not None\
207 and self.hostname is not None
210 class Subnet(object):
211 def __init__(self, net=None, gw=None, dev=None):
212 if isinstance(net, str):
213 self.net = IPy.IP(net)
221 """ Return the netmask in textual representation
224 return str(self.net.netmask())
228 """ Return the broadcast address in textual representation
231 return str(self.net.broadcast())
235 """ Return the network as an IPy.IP
238 return self.net.net()
242 """ Return the prefix length as an integer
245 return self.net.prefixlen()
248 def _make_eui64(net, mac):
249 """ Compute an EUI-64 address from an EUI-48 (MAC) address
252 comp = mac.split(":")
253 prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
254 eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
255 eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
256 for l in range(0, len(eui64), 2):
257 prefix += ["".join(eui64[l:l+2])]
258 return IPy.IP(":".join(prefix))
260 def make_eui64(self, mac):
261 """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
265 return self._make_eui64(self.net, mac)
267 def make_ll64(self, mac):
268 """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
271 return self._make_eui64("fe80::", mac)
274 class VMNetProxy(object): # pylint: disable=R0902
275 def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
276 rs_queue_num=None, ns_queue_num=None,
277 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
278 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
279 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
280 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
282 self.data_path = data_path
283 self.lease_lifetime = dhcp_lease_lifetime
284 self.lease_renewal = dhcp_lease_renewal
285 self.dhcp_server_ip = dhcp_server_ip
286 self.ra_period = ra_period
287 if dhcp_nameservers is None:
288 self.dhcp_nameserver = []
290 self.dhcp_nameservers = dhcp_nameservers
292 if ipv6_nameservers is None:
293 self.ipv6_nameservers = []
295 self.ipv6_nameservers = ipv6_nameservers
297 self.ipv6_enabled = False
304 self.l2socket = socket.socket(socket.AF_PACKET,
305 socket.SOCK_RAW, ETH_P_ALL)
306 self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
309 self.wm = pyinotify.WatchManager()
310 mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
311 mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
312 inotify_handler = ClientFileHandler(self)
313 self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
314 self.wm.add_watch(self.data_path, mask, rec=True)
317 if dhcp_queue_num is not None:
318 self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
320 if rs_queue_num is not None:
321 self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
322 self.ipv6_enabled = True
324 if ns_queue_num is not None:
325 self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
326 self.ipv6_enabled = True
329 """ Free all resources for a graceful exit
332 logging.info("Cleaning up")
334 logging.debug("Closing netfilter queues")
335 for q in self.nfq.values():
338 logging.debug("Closing socket")
339 self.l2socket.close()
341 logging.debug("Stopping inotify watches")
344 logging.info("Cleanup finished")
346 def _setup_nfqueue(self, queue_num, family, callback):
347 logging.debug("Setting up NFQUEUE for queue %d, AF %s",
350 q.set_callback(callback)
351 q.fast_open(queue_num, family)
352 q.set_queue_maxlen(5000)
353 # This is mandatory for the queue to operate
354 q.set_mode(nfqueue.NFQNL_COPY_PACKET)
355 self.nfq[q.get_fd()] = q
357 def sendp(self, data, iface):
358 """ Send a raw packet using a layer-2 socket
361 logging.debug("%s", data)
362 if isinstance(data, BasePacket):
365 self.l2socket.bind((iface, ETH_P_ALL))
366 count = self.l2socket.send(data)
369 logging.warn("Truncated send on %s (%d/%d bytes sent)",
372 def build_config(self):
375 for path in glob.glob(os.path.join(self.data_path, "*")):
378 def get_ifindex(self, iface):
379 """ Get the interface index from sysfs
382 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
383 if not path.startswith(SYSFS_NET):
390 except EnvironmentError:
391 logging.debug("%s is probably down, removing", iface)
392 self.remove_iface(iface)
397 ifindex = f.readline().strip()
399 ifindex = int(ifindex)
400 except ValueError, e:
401 logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
402 " output '%s'", iface, ifindex)
403 except EnvironmentError, e:
404 logging.warn("Error reading %s's ifindex from sysfs: %s",
406 self.remove_iface(iface)
413 def get_iface_hw_addr(self, iface):
414 """ Get the interface hardware address from sysfs
417 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
418 if not path.startswith(SYSFS_NET):
424 except EnvironmentError:
425 logging.debug("%s is probably down, removing", iface)
426 self.remove_iface(iface)
430 addr = f.readline().strip()
431 except EnvironmentError, e:
432 logging.warn("Failed to read hw address for %s from sysfs: %s",
439 def add_iface(self, path):
440 """ Add an interface to monitor
443 iface = os.path.basename(path)
445 logging.debug("Updating configuration for %s", iface)
446 binding = parse_binding_file(path)
449 ifindex = self.get_ifindex(binding.iface)
452 logging.warn("Stale configuration for %s found", iface)
454 if binding.is_valid():
455 self.clients[binding.mac] = binding
456 logging.debug("Added client %s on %s", binding.hostname, iface)
457 self.ifaces[ifindex] = binding.iface
459 def remove_iface(self, ifname):
460 """ Cleanup clients on a removed interface
463 for mac in self.clients.keys():
464 if self.clients[mac].ifname == ifname:
465 iface = self.client[mac].iface
466 del self.clients[mac]
468 for ifindex in self.ifaces.keys():
469 if self.ifaces[ifindex] == ifname == iface:
470 del self.ifaces[ifindex]
472 logging.debug("Removed interface %s", ifname)
474 def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
475 """ Generate a reply to a BOOTP/DHCP request
478 logging.info("%s",payload)
479 indev = payload.get_indev()
481 # Get the actual interface from the ifindex
482 iface = self.ifaces[indev]
484 # We don't know anything about this interface, so accept the packet
486 logging.debug("Ignoring DHCP request on unknown iface %d", indev)
487 # We don't know what to do with this packet, so let the kernel
489 payload.set_verdict(nfqueue.NF_ACCEPT)
492 # Decode the response - NFQUEUE relays IP packets
493 pkt = IP(payload.get_data())
495 # Signal the kernel that it shouldn't further process the packet
496 payload.set_verdict(nfqueue.NF_DROP)
498 # Get the client MAC address
499 resp = pkt.getlayer(BOOTP).copy()
501 mac = resp.chaddr[:hlen].encode("hex")
502 mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
504 # Server responses are always BOOTREPLYs
505 resp.op = "BOOTREPLY"
509 binding = self.clients[mac]
511 logging.warn("Invalid client %s on %s", mac, iface)
514 if iface != binding.iface:
515 logging.warn("Received spoofed DHCP request for %s from interface"
516 " %s instead of %s", mac, iface, binding.iface)
519 resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
520 IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
521 UDP(sport=pkt.dport, dport=pkt.sport)/resp
525 logging.warn("Invalid request from %s on %s, no DHCP"
526 " payload found", binding.mac, iface)
530 requested_addr = binding.ip
531 for opt in pkt[DHCP].options:
532 if type(opt) is tuple and opt[0] == "message-type":
534 if type(opt) is tuple and opt[0] == "requested_addr":
535 requested_addr = opt[1]
537 logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
540 if req_type == DHCPREQUEST and requested_addr != binding.ip:
542 logging.info("Sending DHCPNAK to %s on %s: requested %s"
543 " instead of %s", binding.mac, iface, requested_addr,
546 elif req_type in (DHCPDISCOVER, DHCPREQUEST):
547 resp_type = DHCP_REQRESP[req_type]
548 resp.yiaddr = self.clients[mac].ip
550 ("hostname", binding.hostname),
551 ("domain", binding.hostname.split('.', 1)[-1]),
552 ("broadcast_address", str(subnet.broadcast)),
553 ("subnet_mask", str(subnet.netmask)),
554 ("renewal_time", self.lease_renewal),
555 ("lease_time", self.lease_lifetime),
558 dhcp_options += [("router", subnet.gw)]
559 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
561 elif req_type == DHCPINFORM:
562 resp_type = DHCP_REQRESP[req_type]
564 ("hostname", binding.hostname),
565 ("domain", binding.hostname.split('.', 1)[-1]),
567 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
569 elif req_type == DHCPRELEASE:
571 logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
574 # Finally, always add the server identifier and end options
576 ("message-type", resp_type),
577 ("server_id", DHCP_DUMMY_SERVER_IP),
580 resp /= DHCP(options=dhcp_options)
582 logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
584 self.sendp(resp, iface)
586 def rs_response(self, i, payload): # pylint: disable=W0613
587 """ Generate a reply to a BOOTP/DHCP request
590 indev = payload.get_indev()
592 # Get the actual interface from the ifindex
593 iface = self.ifaces[indev]
595 logging.debug("Ignoring router solicitation on"
596 " unknown interface %d", indev)
597 # We don't know what to do with this packet, so let the kernel
599 payload.set_verdict(nfqueue.NF_ACCEPT)
602 ifmac = self.get_iface_hw_addr(iface)
603 binding = self.clients[ifmac]
604 subnet = binding.net6
605 ifll = subnet.make_ll64(ifmac)
607 # Signal the kernel that it shouldn't further process the packet
608 payload.set_verdict(nfqueue.NF_DROP)
610 resp = Ether(src=self.get_iface_hw_addr(iface))/\
611 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
612 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
613 prefixlen=subnet.prefixlen)
615 if self.ipv6_nameservers:
616 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
617 lifetime=self.ra_period * 3)
619 logging.info("RA on %s for %s", iface, subnet.net)
620 self.sendp(resp, iface)
622 def ns_response(self, i, payload): # pylint: disable=W0613
623 """ Generate a reply to an ICMPv6 neighbor solicitation
626 indev = payload.get_indev()
628 # Get the actual interface from the ifindex
629 iface = self.ifaces[indev]
631 logging.debug("Ignoring neighbour solicitation on"
632 " unknown interface %d", indev)
633 # We don't know what to do with this packet, so let the kernel
635 payload.set_verdict(nfqueue.NF_ACCEPT)
638 ifmac = self.get_iface_hw_addr(iface)
639 binding = self.clients[ifmac]
640 subnet = binding.net6
641 ifll = subnet.make_ll64(ifmac)
643 ns = IPv6(payload.get_data())
645 if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
646 logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
647 payload.set_verdict(nfqueue.NF_ACCEPT)
650 payload.set_verdict(nfqueue.NF_DROP)
653 client_lladdr = ns.lladdr
654 except AttributeError:
657 resp = Ether(src=ifmac, dst=client_lladdr)/\
658 IPv6(src=str(ifll), dst=ns.src)/\
659 ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
660 ICMPv6NDOptDstLLAddr(lladdr=ifmac)
662 logging.info("NA on %s for %s", iface, ns.tgt)
663 self.sendp(resp, iface)
666 def send_periodic_ra(self):
667 # Use a separate thread as this may take a _long_ time with
668 # many interfaces and we want to be responsive in the mean time
669 threading.Thread(target=self._send_periodic_ra).start()
671 def _send_periodic_ra(self):
672 logging.debug("Sending out periodic RAs")
675 for binding in self.clients.values():
676 iface = binding.ifname
678 subnet = binding.net6
679 if subnet.net is None:
680 logging.debug("Skipping periodic RA on interface %s,"
681 " as it is not IPv6-connected", iface)
684 ifll = subnet.make_ll64(ifmac)
685 resp = Ether(src=ifmac)/\
686 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
687 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
688 prefixlen=subnet.prefixlen)
689 if self.ipv6_nameservers:
690 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
691 lifetime=self.ra_period * 3)
693 self.sendp(resp, iface)
694 except socket.error, e:
695 logging.warn("Periodic RA on %s failed: %s", iface, str(e))
697 logging.warn("Unkown error during periodic RA on %s: %s",
700 logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
703 """ Safely perform the main loop, freeing all resources upon exit
712 """ Loop forever, serving DHCP requests
717 # Yes, we are accessing _fd directly, but it's the only way to have a
718 # single select() loop ;-)
719 iwfd = self.notifier._fd # pylint: disable=W0212
722 if self.ipv6_enabled:
723 timeout = self.ra_period
724 self.send_periodic_ra()
729 rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
731 logging.warn("Warning: Exception on %s",
732 ", ".join([ str(fd) for fd in xlist]))
736 # First check if there are any inotify (= configuration change)
738 self.notifier.read_events()
739 self.notifier.process_events()
744 self.nfq[fd].process_pending()
745 except RuntimeError, e:
746 logging.warn("Error processing fd %d: %s", fd, str(e))
748 logging.warn("Unknown error processing fd %d: %s",
751 if self.ipv6_enabled:
752 # Calculate the new timeout
753 timeout = self.ra_period - (time.time() - start)
757 self.send_periodic_ra()
758 timeout = self.ra_period - (time.time() - start)
761 if __name__ == "__main__":
764 from cStringIO import StringIO
765 from pwd import getpwnam, getpwuid
766 from configobj import ConfigObj, ConfigObjError, flatten_errors
770 validator = validate.Validator()
772 def is_ip_list(value, family=4):
776 raise validate.VdtParamError(family)
777 if isinstance(value, (str, unicode)):
779 if not isinstance(value, list):
780 raise validate.VdtTypeError(value)
786 raise validate.VdtValueError(entry)
788 if ip.version() != family:
789 raise validate.VdtValueError(entry)
792 validator.functions["ip_addr_list"] = is_ip_list
793 config_spec = StringIO(CONFIG_SPEC)
796 parser = optparse.OptionParser()
797 parser.add_option("-c", "--config", dest="config_file",
798 help="The location of the data files", metavar="FILE",
799 default=DEFAULT_CONFIG)
800 parser.add_option("-d", "--debug", action="store_true", dest="debug",
801 help="Turn on debugging messages")
802 parser.add_option("-f", "--foreground", action="store_false",
803 dest="daemonize", default=True,
804 help="Do not daemonize, stay in the foreground")
807 opts, args = parser.parse_args()
810 config = ConfigObj(opts.config_file, configspec=config_spec)
811 except ConfigObjError, err:
812 sys.stderr.write("Failed to parse config file %s: %s" %
813 (opts.config_file, str(err)))
816 results = config.validate(validator)
818 logging.fatal("Configuration file validation failed! See errors below:")
819 for (section_list, key, unused) in flatten_errors(config, results):
821 logging.fatal(" '%s' in section '%s' failed validation",
822 key, ", ".join(section_list))
824 logging.fatal(" Section '%s' is missing",
825 ", ".join(section_list))
828 logger = logging.getLogger()
830 logger.setLevel(logging.DEBUG)
832 logger.setLevel(logging.INFO)
835 logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
836 handler = logging.handlers.RotatingFileHandler(logfile,
839 handler = logging.StreamHandler()
841 handler.setFormatter(logging.Formatter(LOG_FORMAT))
842 logger.addHandler(handler)
845 pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
846 config["general"]["pidfile"], 10)
848 d = daemon.DaemonContext(pidfile=pidfile,
849 stdout=handler.stream,
850 stderr=handler.stream,
851 files_preserve=[handler.stream])
855 logging.info("Starting up")
858 if config["dhcp"].as_bool("enable_dhcp"):
860 "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
861 "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
862 "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
863 "dhcp_server_ip": config["dhcp"]["server_ip"],
864 "dhcp_nameservers": config["dhcp"]["nameservers"],
867 if config["ipv6"].as_bool("enable_ipv6"):
869 "rs_queue_num": config["ipv6"].as_int("rs_queue"),
870 "ns_queue_num": config["ipv6"].as_int("ns_queue"),
871 "ra_period": config["ipv6"].as_int("ra_period"),
872 "ipv6_nameservers": config["ipv6"]["nameservers"],
875 # pylint: disable=W0142
876 proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
878 # Drop all capabilities except CAP_NET_RAW and change uid
880 uid = getpwuid(config["general"].as_int("user"))
882 uid = getpwnam(config["general"]["user"])
884 logging.debug("Setting capabilities and changing uid")
885 logging.debug("User: %s, uid: %d, gid: %d",
886 config["general"]["user"], uid.pw_uid, uid.pw_gid)
888 # Keep only the capabilities we need
889 # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
890 capng.capng_clear(capng.CAPNG_SELECT_BOTH)
891 capng.capng_update(capng.CAPNG_ADD,
892 capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
894 capng.capng_change_id(uid.pw_uid, uid.pw_gid,
895 capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
897 logging.info("Ready to serve requests")
902 exc = "".join(traceback.format_exception(*sys.exc_info()))
903 logging.critical(exc)
907 # vim: set ts=4 sts=4 sw=4 et :