Support DHCPv6

This commit is contained in:
Jack O'Sullivan 2019-08-26 13:58:56 +01:00
parent ec198aad67
commit 644731410b
3 changed files with 56 additions and 26 deletions

View File

@ -17,6 +17,7 @@ from . import NetDhcpError, udhcpc, app
OPTS_KEY = 'com.docker.network.generic' OPTS_KEY = 'com.docker.network.generic'
OPT_PREFIX = 'devplayer0.net-dhcp' OPT_PREFIX = 'devplayer0.net-dhcp'
OPT_BRIDGE = f'{OPT_PREFIX}.bridge' OPT_BRIDGE = f'{OPT_PREFIX}.bridge'
OPT_IPV6 = f'{OPT_PREFIX}.ipv6'
logger = logging.getLogger('gunicorn.error') logger = logging.getLogger('gunicorn.error')
@ -57,7 +58,9 @@ def get_bridges():
def net_bridge(n): def net_bridge(n):
return ndb.interfaces[client.networks.get(n).attrs['Options'][OPT_BRIDGE]] return ndb.interfaces[client.networks.get(n).attrs['Options'][OPT_BRIDGE]]
def ipv6_enabled(n): def ipv6_enabled(n):
return client.networks.get(n).attrs['EnableIPv6'] options = client.networks.get(n).attrs['Options']
return OPT_IPV6 in options and options[OPT_IPV6] == 'true'
def endpoint_container_iface(n, e): def endpoint_container_iface(n, e):
for cid, info in client.networks.get(n).attrs['Containers'].items(): for cid, info in client.networks.get(n).attrs['Containers'].items():
if info['EndpointID'] == e: if info['EndpointID'] == e:
@ -99,10 +102,15 @@ def net_get_capabilities():
@app.route('/NetworkDriver.CreateNetwork', methods=['POST']) @app.route('/NetworkDriver.CreateNetwork', methods=['POST'])
def create_net(): def create_net():
req = request.get_json(force=True) req = request.get_json(force=True)
if OPT_BRIDGE not in req['Options'][OPTS_KEY]: options = req['Options'][OPTS_KEY]
if OPT_BRIDGE not in options:
return jsonify({'Err': 'No bridge provided'}), 400 return jsonify({'Err': 'No bridge provided'}), 400
# We have to use a custom "enable IPv6" option because Docker's null IPAM driver doesn't support IPv6 and a plugin
# IPAM driver isn't allowed to return an empty address
if OPT_IPV6 in options and options[OPT_IPV6] not in ('', 'true', 'false'):
return jsonify({'Err': 'Invalid boolean value for ipv6'}), 400
desired = req['Options'][OPTS_KEY][OPT_BRIDGE] desired = options[OPT_BRIDGE]
bridges = get_bridges() bridges = get_bridges()
if desired not in bridges: if desired not in bridges:
return jsonify({'Err': f'Bridge "{desired}" not found (or the specified bridge is already used by Docker)'}), 400 return jsonify({'Err': f'Bridge "{desired}" not found (or the specified bridge is already used by Docker)'}), 400
@ -156,14 +164,16 @@ def create_endpoint():
for bridge_addr in bridge_addrs: for bridge_addr in bridge_addrs:
if addr.ip == bridge_addr.ip: if addr.ip == bridge_addr.ip:
raise NetDhcpError(400, f'Address {addr} is already in use on bridge {bridge["ifname"]}') raise NetDhcpError(400, f'Address {addr} is already in use on bridge {bridge["ifname"]}')
elif type_ == 'v4':
dhcp = udhcpc.DHCPClient(if_container, once=True)
addr = dhcp.finish()
res_iface['Address'] = str(addr)
gateway_hints[endpoint_id] = dhcp.gateway
else: else:
raise NetDhcpError(400, f'DHCPv6 is currently unsupported') dhcp = udhcpc.DHCPClient(if_container, v6=type_ == 'v6', once=True)
logger.info('Adding address %s to %s', addr, if_container['ifname']) addr = dhcp.finish()
if not addr:
return
res_iface[k] = str(addr)
if dhcp.gateway:
gateway_hints[endpoint_id] = dhcp.gateway
logger.info('Adding IP%s address %s to %s', type_, addr, if_container['ifname'])
try_addr('v4') try_addr('v4')
if ipv6_enabled(network_id): if ipv6_enabled(network_id):
@ -236,7 +246,8 @@ def join():
}, },
'StaticRoutes': [] 'StaticRoutes': []
} }
if endpoint in gateway_hints and gateway_hints[endpoint]:
if endpoint in gateway_hints:
gateway = gateway_hints[endpoint] gateway = gateway_hints[endpoint]
logger.info('Setting IPv4 gateway from DHCP (%s)', gateway) logger.info('Setting IPv4 gateway from DHCP (%s)', gateway)
res['Gateway'] = str(gateway) res['Gateway'] = str(gateway)
@ -288,6 +299,7 @@ class ContainerDHCPManager:
self.ipv6 = ipv6_enabled(network) self.ipv6 = ipv6_enabled(network)
self.dhcp = None self.dhcp = None
self.dhcp6 = None
self._thread = threading.Thread(target=self.run) self._thread = threading.Thread(target=self.run)
self._thread.start() self._thread.start()
@ -323,9 +335,15 @@ class ContainerDHCPManager:
def run(self): def run(self):
try: try:
iface = await_endpoint_container_iface(self.network, self.endpoint) iface = await_endpoint_container_iface(self.network, self.endpoint)
self.dhcp = udhcpc.DHCPClient(iface, event_listener=self._on_event) self.dhcp = udhcpc.DHCPClient(iface, event_listener=self._on_event)
logger.info('Starting DHCP client on %s in container namespace %s', iface['ifname'], \ logger.info('Starting DHCPv4 client on %s in container namespace %s', iface['ifname'], \
self.dhcp.netns) self.dhcp.netns)
if self.ipv6:
self.dhcp6 = udhcpc.DHCPClient(iface, v6=True, event_listener=self._on_event)
logger.info('Starting DHCPv6 client on %s in container namespace %s', iface['ifname'], \
self.dhcp6.netns)
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
@ -333,8 +351,14 @@ class ContainerDHCPManager:
if not self.dhcp: if not self.dhcp:
return return
logger.info('Shutting down DHCP client on %s in container namespace %s', \ logger.info('Shutting down DHCPv4 client on %s in container namespace %s', \
self.dhcp.iface['ifname'], self.dhcp.netns) self.dhcp.iface['ifname'], self.dhcp.netns)
self.dhcp.finish(timeout=1) self.dhcp.finish(timeout=1)
if self.ipv6:
logger.info('Shutting down DHCPv6 client on %s in container namespace %s', \
self.dhcp6.iface['ifname'], self.dhcp.netns)
self.dhcp6.finish(timeout=1)
ndb.sources.remove(self.dhcp.netns) ndb.sources.remove(self.dhcp.netns)
self._thread.join() self._thread.join()

View File

@ -29,8 +29,9 @@ class DHCPClientError(Exception):
def _nspopen_wrapper(netns): def _nspopen_wrapper(netns):
return lambda *args, **kwargs: NSPopen(netns, *args, **kwargs) return lambda *args, **kwargs: NSPopen(netns, *args, **kwargs)
class DHCPClient: class DHCPClient:
def __init__(self, iface, once=False, event_listener=None): def __init__(self, iface, v6=False, once=False, event_listener=None):
self.iface = iface self.iface = iface
self.v6 = v6
self.once = once self.once = once
self.event_listeners = [DHCPClient._attr_listener] self.event_listeners = [DHCPClient._attr_listener]
if event_listener: if event_listener:
@ -42,10 +43,12 @@ class DHCPClient:
logger.debug('udhcpc using netns %s', self.netns) logger.debug('udhcpc using netns %s', self.netns)
Popen = _nspopen_wrapper(self.netns) if self.netns else subprocess.Popen Popen = _nspopen_wrapper(self.netns) if self.netns else subprocess.Popen
cmdline = ['/sbin/udhcpc', '-s', HANDLER_SCRIPT, '-i', iface['ifname'], '-f'] bin_path = '/usr/bin/udhcpc6' if v6 else '/sbin/udhcpc'
cmdline = [bin_path, '-s', HANDLER_SCRIPT, '-i', iface['ifname'], '-f']
cmdline.append('-q' if once else '-R') cmdline.append('-q' if once else '-R')
self._event_queue = posix_ipc.MessageQueue(f'/udhcpc_{iface["address"].replace(":", "_")}', \ self._suffix = '6' if v6 else ''
self._event_queue = posix_ipc.MessageQueue(f'/udhcpc{self._suffix}_{iface["address"].replace(":", "_")}', \
flags=os.O_CREAT | os.O_EXCL) flags=os.O_CREAT | os.O_EXCL)
self.proc = Popen(cmdline, env={'EVENT_QUEUE': self._event_queue.name}) self.proc = Popen(cmdline, env={'EVENT_QUEUE': self._event_queue.name})
@ -84,10 +87,10 @@ class DHCPClient:
try: try:
event['type'] = EventType(event['type']) event['type'] = EventType(event['type'])
except ValueError: except ValueError:
logger.warning('udhcpc#%d unknown event "%s"', self.proc.pid, event) logger.warning('udhcpc%s#%d unknown event "%s"', self._suffix, self.proc.pid, event)
continue continue
logger.debug('[udhcp#%d event] %s', self.proc.pid, event) logger.debug('[udhcp%s#%d event] %s', self._suffix, self.proc.pid, event)
for listener in self.event_listeners: for listener in self.event_listeners:
try: try:
listener(self, event['type'], event) listener(self, event['type'], event)
@ -96,7 +99,7 @@ class DHCPClient:
def await_ip(self, timeout=10): def await_ip(self, timeout=10):
if not self._has_lease.wait(timeout=timeout): if not self._has_lease.wait(timeout=timeout):
raise DHCPClientError('Timed out waiting for dhcp lease') raise DHCPClientError(f'Timed out waiting for lease from udhcpc{self._suffix}')
return self.ip return self.ip
@ -105,14 +108,14 @@ class DHCPClient:
return return
if self.proc.returncode != None and (not self.once or self.proc.returncode != 0): if self.proc.returncode != None and (not self.once or self.proc.returncode != 0):
raise DHCPClientError(f'udhcpc exited early with code {self.proc.returncode}') raise DHCPClientError(f'udhcpc{self._suffix} exited early with code {self.proc.returncode}')
if self.once: if self.once:
self.await_ip() self.await_ip()
else: else:
self.proc.terminate() self.proc.terminate()
if self.proc.wait(timeout=timeout) != 0: if self.proc.wait(timeout=timeout) != 0:
raise DHCPClientError(f'udhcpc exited with non-zero exit code {self.proc.returncode}') raise DHCPClientError(f'udhcpc{self._suffix} exited with non-zero exit code {self.proc.returncode}')
if self.netns: if self.netns:
self.proc.release() self.proc.release()

View File

@ -11,11 +11,14 @@ if __name__ != '__main__':
event = {'type': sys.argv[1]} event = {'type': sys.argv[1]}
if event['type'] in ('bound', 'renew'): if event['type'] in ('bound', 'renew'):
event['ip'] = f'{env["ip"]}/{env["mask"]}' if 'ipv6' in env:
if 'router' in env: event['ip'] = env['ipv6']
event['gateway'] = env['router'] else:
if 'domain' in env: event['ip'] = f'{env["ip"]}/{env["mask"]}'
event['domain'] = env['domain'] if 'router' in env:
event['gateway'] = env['router']
if 'domain' in env:
event['domain'] = env['domain']
elif event['type'] in ('deconfig', 'leasefail', 'nak'): elif event['type'] in ('deconfig', 'leasefail', 'nak'):
pass pass
else: else: