From d9772c3ef75ce77a5cb1061aa79146c27a44ff6e Mon Sep 17 00:00:00 2001 From: Jack O'Sullivan Date: Mon, 26 Aug 2019 00:02:28 +0100 Subject: [PATCH] Improved DHCP client messaging --- Dockerfile | 4 +++- net-dhcp/network.py | 28 +++++++++++++++++++++-- net-dhcp/udhcpc.py | 46 ++++++++++++++++---------------------- net-dhcp/udhcpc_handler.py | 12 +++++++--- requirements.txt | 2 ++ 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index f5739b3..528307b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ FROM python:3-alpine COPY requirements.txt /opt/ -RUN pip install -r /opt/requirements.txt +RUN apk --no-cache add gcc musl-dev && \ + pip install -r /opt/requirements.txt && \ + apk --no-cache del gcc musl-dev RUN mkdir -p /opt/plugin /run/docker/plugins /var/run/docker/netns COPY net-dhcp/ /opt/plugin/net_dhcp diff --git a/net-dhcp/network.py b/net-dhcp/network.py index d8ea7db..c4f8344 100644 --- a/net-dhcp/network.py +++ b/net-dhcp/network.py @@ -222,10 +222,11 @@ def join(): ipv6 = ipv6_enabled(req['NetworkID']) for route in bridge.routes: - if route['type'] != rtypes['RTN_UNICAST'] or (route['family'] == socket.AF_INET6 and not ipv6): + if route['type'] != rtypes['RTN_UNICAST'] or \ + (route['family'] == socket.AF_INET6 and not ipv6): continue - if route['dst'] == '' or route['dst'] == '/0': + if route['dst'] in ('', '/0'): if route['family'] == socket.AF_INET and 'Gateway' not in res: logger.info('Adding IPv4 gateway %s', route['gateway']) res['Gateway'] = route['gateway'] @@ -253,15 +254,38 @@ class ContainerDHCPManager: def __init__(self, network, endpoint): self.network = network self.endpoint = endpoint + self.ipv6 = ipv6_enabled(network) self._thread = threading.Thread(target=self.run) self._thread.start() + def _on_event(self, dhcp, event_type, _args): + if event_type != udhcpc.EventType.RENEW: + return + + for route in dhcp.iface.routes: + if route['type'] != rtypes['RTN_UNICAST'] or \ + (route['family'] == socket.AF_INET6 and not self.ipv6) or \ + route['dst'] not in ('', '/0'): + continue + + # Needed because Route.remove() doesn't like a blank destination + logger.info('Removing default route via %s', route['gateway']) + route['dst'] = '::' if route['family'] == socket.AF_INET6 else '0.0.0.0' + (route + .remove() + .commit()) + + logger.info('Adding default route via %s', dhcp.gateway) + (dhcp.iface.routes.add({'gateway': dhcp.gateway}) + .commit()) + def run(self): iface = endpoint_container_iface(self.network, self.endpoint) self.dhcp = udhcpc.DHCPClient(iface) logger.info('Starting DHCP client on %s in container namespace %s', iface['ifname'], \ self.dhcp.netns) + def stop(self): logger.info('Shutting down DHCP client on %s in container namespace %s', \ self.dhcp.iface['ifname'], self.dhcp.netns) diff --git a/net-dhcp/udhcpc.py b/net-dhcp/udhcpc.py index 813fa24..7741788 100644 --- a/net-dhcp/udhcpc.py +++ b/net-dhcp/udhcpc.py @@ -2,15 +2,15 @@ from enum import Enum import ipaddress import os from os import path -import fcntl -import time +from select import select import threading import subprocess import logging +from eventfd import EventFD +import posix_ipc from pyroute2.netns.process.proxy import NSPopen -EVENT_PREFIX = '__event' HANDLER_SCRIPT = path.join(path.dirname(__file__), 'udhcpc_handler.py') AWAIT_INTERVAL = 0.1 @@ -26,13 +26,7 @@ class DHCPClientError(Exception): pass def _nspopen_wrapper(netns): - def _wrapper(*args, **kwargs): - # We have to set O_NONBLOCK on stdout since NSPopen uses a global lock - # on the object (e.g. deadlock if we try to readline() and terminate()) - proc = NSPopen(netns, *args, **kwargs) - proc.stdout.fcntl(fcntl.F_SETFL, os.O_NONBLOCK) - return proc - return _wrapper + return lambda *args, **kwargs: NSPopen(netns, *args, **kwargs) class DHCPClient: def __init__(self, iface, once=False, event_listener=None): self.iface = iface @@ -49,14 +43,17 @@ class DHCPClient: Popen = _nspopen_wrapper(self.netns) if self.netns else subprocess.Popen cmdline = ['/sbin/udhcpc', '-s', HANDLER_SCRIPT, '-i', iface['ifname'], '-f'] cmdline.append('-q' if once else '-R') - self.proc = Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8') + + self._event_queue = posix_ipc.MessageQueue(f'/udhcpc_{iface["address"].replace(":", "_")}', \ + flags=os.O_CREAT | os.O_EXCL) + self.proc = Popen(cmdline, env={'EVENT_QUEUE': self._event_queue.name}) self._has_lease = threading.Event() self.ip = None self.gateway = None self.domain = None - self._running = True + self._shutdown_event = EventFD() self._event_thread = threading.Thread(target=self._read_events) self._event_thread.start() @@ -73,21 +70,13 @@ class DHCPClient: self.domain = None def _read_events(self): - while self._running: - line = self.proc.stdout.readline().strip() - if not line: - # stdout will be O_NONBLOCK if udhcpc is in a netns - # We can't use select() since the file descriptor is from - # the NSPopen proxy - if self.netns and self._running: - time.sleep(0.1) - continue + while True: + r, _w, _e = select([self._shutdown_event, self._event_queue.mqd], [], []) + if self._shutdown_event in r: + break - if not line.startswith(EVENT_PREFIX): - logger.debug('[udhcpc#%d] %s', self.proc.pid, line) - continue - - args = line.split(' ')[1:] + msg, _priority = self._event_queue.receive() + args = msg.decode('utf-8').split(' ') try: event_type = EventType(args[0]) except ValueError: @@ -114,7 +103,10 @@ class DHCPClient: raise DHCPClientError(f'udhcpc exited with non-zero exit code {self.proc.returncode}') if self.netns: self.proc.release() - self._running = False + + self._shutdown_event.set() self._event_thread.join() + self._event_queue.close() + self._event_queue.unlink() return self.ip diff --git a/net-dhcp/udhcpc_handler.py b/net-dhcp/udhcpc_handler.py index 4dbc215..ab740f6 100755 --- a/net-dhcp/udhcpc_handler.py +++ b/net-dhcp/udhcpc_handler.py @@ -2,7 +2,7 @@ import sys from os import environ as env -EVENT_PREFIX = '__event' +import posix_ipc if __name__ != '__main__': print('You shouldn\'t be importing this script!') @@ -10,9 +10,15 @@ if __name__ != '__main__': event_type = sys.argv[1] if event_type in ('bound', 'renew'): - print(f'{EVENT_PREFIX} {event_type} {env["ip"]}/{env["mask"]} {env["router"]} {env["domain"]}') + event = f'{event_type} {env["ip"]}/{env["mask"]} {env["router"]} {env["domain"]}' elif event_type in ('deconfig', 'leasefail', 'nak'): - print(f'{EVENT_PREFIX} {event_type}') + event = event_type else: print(f'unknown udhcpc event "{event_type}"') sys.exit(1) + +#with open(env['EVENT_FILE'], 'a') as event_file: +# event_file.write(event + '\n') +queue = posix_ipc.MessageQueue(env['EVENT_QUEUE']) +queue.send(event) +queue.close() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cf9d969..c10901b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ flask==1.1.1 gunicorn==19.9.0 pyroute2==0.5.6 docker==4.0.2 +eventfd==0.2 +posix_ipc==1.0.4