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 tap = os.path.basename(path)
141 v = line.strip().split('=')[1]
147 if line.startswith("IP="):
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)
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 )
170 class ClientFileHandler(pyinotify.ProcessEvent):
171 def __init__(self, server):
172 pyinotify.ProcessEvent.__init__(self)
175 def process_IN_DELETE(self, event): # pylint: disable=C0103
176 """ Delete file handler
178 Currently this removes an interface from the watch list
181 self.server.remove_tap(event.name)
183 def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
186 Currently this adds an interface to the watch list
189 self.server.add_tap(os.path.join(event.path, event.name))
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 ):
197 self.hostname = hostname
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)
209 return self.mac is not None and self.ip is not None\
210 and self.hostname is not None
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)
224 """ Return the netmask in textual representation
227 return str(self.net.netmask())
231 """ Return the broadcast address in textual representation
234 return str(self.net.broadcast())
238 """ Return the network as an IPy.IP
241 return self.net.net()
245 """ Return the prefix length as an integer
248 return self.net.prefixlen()
251 def _make_eui64(net, mac):
252 """ Compute an EUI-64 address from an EUI-48 (MAC) address
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))
263 def make_eui64(self, mac):
264 """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
268 return self._make_eui64(self.net, mac)
270 def make_ll64(self, mac):
271 """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
274 return self._make_eui64("fe80::", mac)
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):
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 = []
293 self.dhcp_nameservers = dhcp_nameservers
295 if ipv6_nameservers is None:
296 self.ipv6_nameservers = []
298 self.ipv6_nameservers = ipv6_nameservers
300 self.ipv6_enabled = False
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)
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)
320 if dhcp_queue_num is not None:
321 self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
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
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
332 """ Free all resources for a graceful exit
335 logging.info("Cleaning up")
337 logging.debug("Closing netfilter queues")
338 for q in self.nfq.values():
341 logging.debug("Closing socket")
342 self.l2socket.close()
344 logging.debug("Stopping inotify watches")
347 logging.info("Cleanup finished")
349 def _setup_nfqueue(self, queue_num, family, callback):
350 logging.debug("Setting up NFQUEUE for queue %d, AF %s",
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
360 def sendp(self, data, dev):
361 """ Send a raw packet using a layer-2 socket
364 logging.debug("%s", data)
365 if isinstance(data, BasePacket):
368 self.l2socket.bind((dev, ETH_P_ALL))
369 count = self.l2socket.send(data)
372 logging.warn("Truncated send on %s (%d/%d bytes sent)",
375 def build_config(self):
378 for path in glob.glob(os.path.join(self.data_path, "*")):
381 def get_ifindex(self, iface):
382 """ Get the interface index from sysfs
385 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
386 if not path.startswith(SYSFS_NET):
393 except EnvironmentError:
394 logging.debug("%s is probably down, removing", iface)
395 self.remove_tap(iface)
400 ifindex = f.readline().strip()
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",
409 self.remove_tap(iface)
416 def get_iface_hw_addr(self, iface):
417 """ Get the interface hardware address from sysfs
420 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
421 if not path.startswith(SYSFS_NET):
427 except EnvironmentError:
428 logging.debug("%s is probably down, removing", iface)
429 self.remove_tap(iface)
433 addr = f.readline().strip()
434 except EnvironmentError, e:
435 logging.warn("Failed to read hw address for %s from sysfs: %s",
442 def add_tap(self, path):
443 """ Add an interface to monitor
446 tap = os.path.basename(path)
448 logging.debug("Updating configuration for %s", tap)
449 binding = parse_binding_file(path)
452 ifindex = self.get_ifindex(binding.tap)
455 logging.warn("Stale configuration for %s found", tap)
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())
462 def remove_tap(self, tap):
463 """ Cleanup clients on a removed interface
466 for b in self.clients.values():
470 logging.debug("Removed interface %s", tap)
472 def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
473 """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
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())
481 # Get the client MAC address
482 resp = pkt.getlayer(BOOTP).copy()
484 mac = resp.chaddr[:hlen].encode("hex")
485 mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
487 # Server responses are always BOOTREPLYs
488 resp.op = "BOOTREPLY"
492 binding = self.clients[mac]
494 logging.warn("Invalid client for mac %s ", mac)
495 payload.set_verdict(nfqueue.NF_ACCEPT)
498 # Signal the kernel that it shouldn't further process the packet
499 payload.set_verdict(nfqueue.NF_DROP)
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
507 logging.warn("Invalid request from %s on %s, no DHCP"
508 " payload found", binding.mac, binding.tap)
512 requested_addr = binding.ip
513 for opt in pkt[DHCP].options:
514 if type(opt) is tuple and opt[0] == "message-type":
516 if type(opt) is tuple and opt[0] == "requested_addr":
517 requested_addr = opt[1]
519 logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
520 binding.mac, binding.tap)
522 if req_type == DHCPREQUEST and requested_addr != binding.ip:
524 logging.info("Sending DHCPNAK to %s on %s: requested %s"
525 " instead of %s", binding.mac, binding.tap, requested_addr,
528 elif req_type in (DHCPDISCOVER, DHCPREQUEST):
529 resp_type = DHCP_REQRESP[req_type]
530 resp.yiaddr = self.clients[mac].ip
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),
540 dhcp_options += [("router", subnet.gw)]
541 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
543 elif req_type == DHCPINFORM:
544 resp_type = DHCP_REQRESP[req_type]
546 ("hostname", binding.hostname),
547 ("domain", binding.hostname.split('.', 1)[-1]),
549 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
551 elif req_type == DHCPRELEASE:
553 logging.info("DHCPRELEASE from %s on %s", binding.mac, binding.tap )
556 # Finally, always add the server identifier and end options
558 ("message-type", resp_type),
559 ("server_id", DHCP_DUMMY_SERVER_IP),
562 resp /= DHCP(options=dhcp_options)
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)
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)
573 def rs_response(self, i, payload): # pylint: disable=W0613
574 """ Generate a reply to a BOOTP/DHCP request
577 pkt = IPv6(payload.get_data())
578 logging.debug("IN RS RESPONCE")
579 logging.debug(pkt.show())
581 logging.debug("rs for mac %s", mac)
583 binding = self.clients[mac]
585 logging.debug("Ignoring router solicitation on"
587 # We don't know what to do with this packet, so let the kernel
589 payload.set_verdict(nfqueue.NF_ACCEPT)
592 # Signal the kernel that it shouldn't further process the packet
593 payload.set_verdict(nfqueue.NF_DROP)
595 subnet = binding.net6
597 if subnet.net is None:
598 logging.debug("No IPv6 network assigned for the interface")
601 ifmac = self.get_iface_hw_addr(binding.indev)
602 ifll = subnet.make_ll64(ifmac)
605 resp = Ether(src=ifmac)/\
606 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
607 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
608 prefixlen=subnet.prefixlen)
610 if self.ipv6_nameservers:
611 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
612 lifetime=self.ra_period * 3)
614 logging.info("RA on %s for %s", binding.indev, subnet.net)
615 self.sendp(resp, binding.indev)
617 def ns_response(self, i, payload): # pylint: disable=W0613
618 """ Generate a reply to an ICMPv6 neighbor solicitation
621 ns = IPv6(payload.get_data())
622 logging.debug("IN NS RESPONCE")
623 logging.debug(ns.show())
625 logging.debug("dst %s tgt %s" , ns.dst, ns.tgt)
628 binding = self.clients[ns.lladdr]
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
633 payload.set_verdict(nfqueue.NF_ACCEPT)
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)
642 payload.set_verdict(nfqueue.NF_DROP)
644 indevmac = self.get_iface_hw_addr(binding.indev)
646 ifll = subnet.make_ll64(indevmac)
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)
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)
659 logging.info("NA on %s for %s", binding.indev, ns.tgt)
660 self.sendp(resp, binding.indev)
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()
667 def _send_periodic_ra(self):
668 logging.debug("Sending out periodic RAs")
671 for binding in self.clients.values():
673 indev = binding.indev
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)
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)
690 self.sendp(resp, indev)
691 except socket.error, e:
692 logging.warn("Periodic RA on %s failed: %s", tap, str(e))
694 logging.warn("Unkown error during periodic RA on %s: %s",
697 logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
700 """ Safely perform the main loop, freeing all resources upon exit
709 """ Loop forever, serving DHCP requests
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
719 if self.ipv6_enabled:
720 timeout = self.ra_period
721 self.send_periodic_ra()
726 rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
728 logging.warn("Warning: Exception on %s",
729 ", ".join([ str(fd) for fd in xlist]))
733 # First check if there are any inotify (= configuration change)
735 self.notifier.read_events()
736 self.notifier.process_events()
741 self.nfq[fd].process_pending()
742 except RuntimeError, e:
743 logging.warn("Error processing fd %d: %s", fd, str(e))
745 logging.warn("Unknown error processing fd %d: %s",
748 if self.ipv6_enabled:
749 # Calculate the new timeout
750 timeout = self.ra_period - (time.time() - start)
754 self.send_periodic_ra()
755 timeout = self.ra_period - (time.time() - start)
758 if __name__ == "__main__":
761 from cStringIO import StringIO
762 from pwd import getpwnam, getpwuid
763 from configobj import ConfigObj, ConfigObjError, flatten_errors
767 validator = validate.Validator()
769 def is_ip_list(value, family=4):
773 raise validate.VdtParamError(family)
774 if isinstance(value, (str, unicode)):
776 if not isinstance(value, list):
777 raise validate.VdtTypeError(value)
783 raise validate.VdtValueError(entry)
785 if ip.version() != family:
786 raise validate.VdtValueError(entry)
789 validator.functions["ip_addr_list"] = is_ip_list
790 config_spec = StringIO(CONFIG_SPEC)
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")
804 opts, args = parser.parse_args()
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)))
813 results = config.validate(validator)
815 logging.fatal("Configuration file validation failed! See errors below:")
816 for (section_list, key, unused) in flatten_errors(config, results):
818 logging.fatal(" '%s' in section '%s' failed validation",
819 key, ", ".join(section_list))
821 logging.fatal(" Section '%s' is missing",
822 ", ".join(section_list))
825 logger = logging.getLogger()
827 logger.setLevel(logging.DEBUG)
829 logger.setLevel(logging.INFO)
832 logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
833 handler = logging.handlers.RotatingFileHandler(logfile,
836 handler = logging.StreamHandler()
838 handler.setFormatter(logging.Formatter(LOG_FORMAT))
839 logger.addHandler(handler)
842 pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
843 config["general"]["pidfile"], 10)
845 d = daemon.DaemonContext(pidfile=pidfile,
846 stdout=handler.stream,
847 stderr=handler.stream,
848 files_preserve=[handler.stream])
852 logging.info("Starting up")
855 if config["dhcp"].as_bool("enable_dhcp"):
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"],
864 if config["ipv6"].as_bool("enable_ipv6"):
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"],
872 # pylint: disable=W0142
873 proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
875 # Drop all capabilities except CAP_NET_RAW and change uid
877 uid = getpwuid(config["general"].as_int("user"))
879 uid = getpwnam(config["general"]["user"])
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)
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,
891 capng.capng_change_id(uid.pw_uid, uid.pw_gid,
892 capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
894 logging.info("Ready to serve requests")
899 exc = "".join(traceback.format_exception(*sys.exc_info()))
900 logging.critical(exc)
904 # vim: set ts=4 sts=4 sw=4 et :