From c8924c9b00153c4f1183d93e619654d6fbc147d6 Mon Sep 17 00:00:00 2001 From: Zsombor Welker <fedora@zdeqb.com> Date: Sun, 11 Dec 2022 11:59:35 +0100 Subject: [PATCH] Add support for resolving IPv6 container hostnames --- .../dockerdnsconnector.py | 7 +- src/systemd_resolved_docker/dockerwatcher.py | 22 ++++-- src/systemd_resolved_docker/utils.py | 7 +- test/integration/functions.sh | 7 ++ test/integration/test_ipv6.sh | 76 +++++++++++++++++++ test/integration/test_proxy.sh | 22 +++--- 6 files changed, 116 insertions(+), 25 deletions(-) create mode 100755 test/integration/test_ipv6.sh diff --git a/src/systemd_resolved_docker/dockerdnsconnector.py b/src/systemd_resolved_docker/dockerdnsconnector.py index c25530f..3c7b252 100644 --- a/src/systemd_resolved_docker/dockerdnsconnector.py +++ b/src/systemd_resolved_docker/dockerdnsconnector.py @@ -2,7 +2,7 @@ import ipaddress import threading from typing import List -from dnslib import A, CLASS, DNSLabel, QTYPE, RR +from dnslib import A, AAAA, CLASS, DNSLabel, QTYPE, RR from dnslib.proxy import ProxyResolver from dnslib.server import DNSServer @@ -72,7 +72,10 @@ class DockerDNSConnector: hn = self.as_allowed_hostname(host_name) mh.host_names.append(hn) - rr = RR(hn, QTYPE.A, CLASS.IN, 1, A(host.ip)) + if isinstance(host.ip, ipaddress.IPv4Address): + rr = RR(hn, QTYPE.A, CLASS.IN, 1, A(host.ip.exploded)) + else: + rr = RR(hn, QTYPE.AAAA, CLASS.IN, 1, AAAA(host.ip.exploded)) zone.append(rr) host_names.append(hn) diff --git a/src/systemd_resolved_docker/dockerwatcher.py b/src/systemd_resolved_docker/dockerwatcher.py index a12691f..5517574 100644 --- a/src/systemd_resolved_docker/dockerwatcher.py +++ b/src/systemd_resolved_docker/dockerwatcher.py @@ -1,10 +1,13 @@ +import ipaddress +from typing import List, Union + import docker from threading import Thread class DockerHost: - def __init__(self, host_names, ip, interface=None): + def __init__(self, host_names: List[str], ip: Union[ipaddress.IPv4Address, ipaddress.IPv6Address], interface=None): self.host_names = host_names self.ip = ip self.interface = interface @@ -85,10 +88,11 @@ class DockerWatcher(Thread): name = c.attrs['Name'][1:] settings = c.attrs['NetworkSettings'] for netname, network in settings.get('Networks', {}).items(): - ip = network.get('IPAddress', False) - if not ip or ip == "": + ips = [network[field] for field in ['IPAddress', 'GlobalIPv6Address'] if + field in network and network[field] != ""] + if not ips: if netname == 'host': - ip = self.default_host_ip + ips = [self.default_host_ip] else: continue @@ -96,11 +100,13 @@ class DockerWatcher(Thread): # eg. container is named "foo", and network is "demo", # so create "foo.demo" domain name # (avoiding default network named "bridge") - record = domain_records.get(ip, [*common_hostnames]) - if netname != "bridge": - record.append('%s.%s' % (name, netname)) + for ip in ips: + ipr = ipaddress.ip_address(ip) + record = domain_records.get(ipr, [*common_hostnames]) + if netname != "bridge": + record.append('%s.%s' % (name, netname)) - domain_records[ip] = record + domain_records[ipr] = record for ip, hosts in domain_records.items(): domain_records[ip] = list(filter(lambda h: h not in duplicate_hostnames, hosts)) diff --git a/src/systemd_resolved_docker/utils.py b/src/systemd_resolved_docker/utils.py index e9ed3f6..b5f8b1d 100644 --- a/src/systemd_resolved_docker/utils.py +++ b/src/systemd_resolved_docker/utils.py @@ -1,14 +1,11 @@ import ipaddress import urllib.parse from pyroute2 import NDB -from typing import List +from typing import List, Union class IpAndPort: - ip: ipaddress.ip_address - port: int - - def __init__(self, ip: ipaddress.ip_address, port: int): + def __init__(self, ip: Union[ipaddress.IPv4Address, ipaddress.IPv6Address], port: int): self.ip = ip self.port = port diff --git a/test/integration/functions.sh b/test/integration/functions.sh index 18dfc84..6d3b1b3 100644 --- a/test/integration/functions.sh +++ b/test/integration/functions.sh @@ -44,6 +44,13 @@ docker_ip() { docker inspect --format '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $container_id } +docker_ipv6() { + local container_id=$1 + shift; + + docker inspect --format '{{range.NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}' $container_id +} + docker_name() { local container_id=$1 shift; diff --git a/test/integration/test_ipv6.sh b/test/integration/test_ipv6.sh new file mode 100755 index 0000000..01a4e3c --- /dev/null +++ b/test/integration/test_ipv6.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +. ./functions.sh + +exec 10<<EOF +version: "2.1" +services: + webserver: + image: nginx + labels: + - $TEST_LABEL + networks: + - network + broker: + image: redis + labels: + - $TEST_LABEL + networks: + - network + +networks: + network: + driver: bridge + enable_ipv6: true + labels: + - $TEST_LABEL + ipam: + driver: default + config: + - subnet: 2001:db8:a::/64 + gateway: 2001:db8:a::1 +EOF + +exec 20<<EOF +version: "2.1" +services: + broker: + image: redis + labels: + - $TEST_LABEL + networks: + - network + +networks: + network: + driver: bridge + enable_ipv6: true + labels: + - $TEST_LABEL + ipam: + driver: default + config: + - subnet: 2001:db8:b::/64 + gateway: 2001:db8:b::1 +EOF + +ALLOWED_DOMAINS=.docker,.$TEST_PREFIX start_systemd_resolved_docker + +docker-compose --file /dev/fd/10 --project-name $TEST_PREFIX up --detach --scale webserver=2 + +broker1_ip=$(docker_ipv6 ${TEST_PREFIX}_broker_1) +webserver1_ip=$(docker_ipv6 ${TEST_PREFIX}_webserver_1) +webserver2_ip=$(docker_ipv6 ${TEST_PREFIX}_webserver_2) + +query_ok broker.$TEST_PREFIX $broker1_ip +query_ok 1.broker.$TEST_PREFIX $broker1_ip + +query_ok webserver.$TEST_PREFIX $webserver1_ip +query_ok webserver.$TEST_PREFIX $webserver2_ip +query_ok 1.webserver.$TEST_PREFIX $webserver1_ip +query_ok 2.webserver.$TEST_PREFIX $webserver2_ip + +query_ok broker.docker $broker1_ip + +docker-compose --file /dev/fd/20 --project-name ${TEST_PREFIX}_2 up --detach +query_fail broker.docker diff --git a/test/integration/test_proxy.sh b/test/integration/test_proxy.sh index be3baf4..c6405f1 100755 --- a/test/integration/test_proxy.sh +++ b/test/integration/test_proxy.sh @@ -10,13 +10,15 @@ docker network create --label $TEST_LABEL $NETWORK > /dev/null container_id=$(docker_run resolvetest1 --hostname resolvetest1) container_ip=$(docker_ip ${container_id}) -dns_ip=$(docker network inspect bridge --format '{{ range .IPAM.Config }}{{ .Gateway }}{{ end }}') - -query_ok resolvetest1.docker $container_ip - -# Case 1: generated domains are resolved in containers on the default network -# The DNS server is provided explicitly, since it was not provided to the daemon -docker run --dns $dns_ip --rm alpine sh -c "apk add bind-tools && host resolvetest1.docker" - -# Case 2: generated domains are resolved in containers on other networks -docker run --network $NETWORK --rm alpine sh -c "apk add bind-tools && host resolvetest1.docker" +# The default bridge may have multiple ips/gateways, for example if IPv6 is enabled +for gateway_ip in $(docker network inspect bridge --format '{{ range .IPAM.Config }}{{ .Gateway }} {{ end }}'); +do + query_ok resolvetest1.docker $container_ip + + # Case 1: generated domains are resolved in containers on the default network + # The DNS server is provided explicitly, since it was not provided to the daemon + docker run --dns $gateway_ip --rm alpine sh -c "apk add bind-tools && host resolvetest1.docker" + + # Case 2: generated domains are resolved in containers on other networks + docker run --network $NETWORK --rm alpine sh -c "apk add bind-tools && host resolvetest1.docker" +done \ No newline at end of file -- GitLab