diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 5d04081..938c2f8 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -82,9 +82,17 @@ jobs: fetch-depth: 0 - name: List contents run: git config --global --add safe.directory $(pwd) - - name: Build package + - name: Build SRPM + run: tito build --srpm --offline --test + - name: Save SRPM + uses: actions/upload-artifact@v3 + with: + name: SRPM package + path: | + /tmp/tito/*.src.rpm + - name: Build RPM run: tito build --rpm --offline --test - - name: Save artifacts + - name: Save RPM uses: actions/upload-artifact@v3 with: name: RPM package diff --git a/README.md b/README.md index 9cd5e99..78c9214 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ an exact match is required. If a generated domain address doesn't match the list 1. `.` - All containers will be reachable by their `container_id`: + All containers may be reachable by their `container_id`: ```sh docker run --rm -it alpine # d6d51528ac46.docker docker ps @@ -125,15 +125,17 @@ If there are link-local, VPN or other DNS servers configured then those will als `systemd-resolved-docker` may be configured using environment variables. When installed using the RPM `/etc/sysconfig/systemd-resolved-docker` may also be modified to update the environment variables. +*Note*: IPv6 addresses should be provided in square brackets (`[2001:db8:1::1]` or `[2001:db8:1::1]:1053`). + | Name | Description | Default Value | Example | |-----------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|-----------------------------------| -| DNS_SERVER | DNS server to use when resolving queries from docker containers. | `127.0.0.53` - systemd-resolved DNS server | `127.0.0.53` | -| SYSTEMD_RESOLVED_INTERFACE | Dummy interface name which will be created to interface with systemd-resolved | `srd-dummy` | `srd-dummy` | -| SYSTEMD_RESOLVED_LISTEN_ADDRESS | IPs (+port) to listen on for queries from systemd-resolved. | `127.0.0.153` | `127.0.0.153:1053` | -| DOCKER_LISTEN_ADDRESS | IPs (+port) to listen on for queries from docker containers in the default network. | _ip of the default docker bridge_, often `172.17.0.1` | `172.17.0.1` or `172.17.0.1:53` | | ALLOWED_DOMAINS | Domain which will be handled by the DNS server. If a domain starts with `.` then all subdomains will also be allowed. | `.docker` | `.docker,.local` | | DEFAULT_DOMAIN | Domain to append to hostnames which are not allowed by `ALLOWED_DOMAINS`. | `docker` | `docker` | | DEFAULT_HOST_IP | IP address to use for containers on the host network if the container doesn't contain one. | `127.0.0.1` | `127.0.0.1` | +| DOCKER_LISTEN_ADDRESS | IPs (+port) to listen on for queries from docker containers in the default network. | _ip of the default docker bridge_, often `172.17.0.1` | `172.17.0.1` or `172.17.0.1:53` | +| UPSTREAM_DNS_SERVER | DNS server to use when resolving queries from docker containers. | `127.0.0.53` - systemd-resolved DNS server | `127.0.0.53` | +| SYSTEMD_RESOLVED_INTERFACE | Dummy interface name which will be created to interface with systemd-resolved | `srd-dummy` | `srd-dummy` | +| SYSTEMD_RESOLVED_LISTEN_ADDRESS | IPs (+port) to listen on for queries from systemd-resolved. | `127.0.0.153` | `127.0.0.153:1053` | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------- | ## Install diff --git a/src/systemd_resolved_docker/cli.py b/src/systemd_resolved_docker/cli.py index 6d289ec..c46618e 100644 --- a/src/systemd_resolved_docker/cli.py +++ b/src/systemd_resolved_docker/cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import os import signal @@ -9,7 +8,7 @@ from .dockerdnsconnector import DockerDNSConnector from .resolvedconnector import SystemdResolvedConnector from .utils import find_default_docker_bridge_gateway, parse_ip_port, parse_listen_address, remove_dummy_interface, \ - create_dummy_interface, sanify_domain + create_dummy_interface, sanify_domain, parse_ip class Handler: @@ -58,7 +57,7 @@ def main(): systemd_resolved_listen_addresses = parse_listen_address(systemd_resolved_listen_address, lambda: [parse_ip_port("127.0.0.153:53")]) docker_listen_addresses = parse_listen_address(docker_listen_address, - lambda: [parse_ip_port(entry['gateway']) for entry in + lambda: [parse_ip(entry['gateway']) for entry in docker_gateway]) handler.log("Creating interface %s" % systemd_resolved_interface) diff --git a/src/systemd_resolved_docker/dockerdnsconnector.py b/src/systemd_resolved_docker/dockerdnsconnector.py index 7ffbadf..3c7b252 100644 --- a/src/systemd_resolved_docker/dockerdnsconnector.py +++ b/src/systemd_resolved_docker/dockerdnsconnector.py @@ -1,12 +1,14 @@ +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 from .dockerwatcher import DockerWatcher, DockerHost from .interceptresolver import InterceptResolver +from .udpserver import UDPServer6, UDPServer4 from .utils import IpAndPort from .zoneresolver import ZoneResolver @@ -28,12 +30,14 @@ def __init__(self, listen_addresses: List[IpAndPort], upstream_dns_server: IpAnd ProxyResolver(upstream_dns_server.ip.exploded, port=upstream_dns_server.port, timeout=5)) self.handler.log("Unhandled DNS requests will be resolved using %s" % upstream_dns_server) - self.handler.log("DNS server listening on %s" % ", ".join(map(lambda x: str(x), listen_addresses))) + #self.handler.log("DNS server listening on %s" % ", ".join(map(lambda x: str(x), listen_addresses))) for ip_and_port in listen_addresses: - server = DNSServer(resolver, address=ip_and_port.ip.exploded, port=ip_and_port.port) - server.thread_name = "%s:%s" % (ip_and_port.ip, ip_and_port.port) - self.servers.append(server) + self.handler.log("DNS server listening on " + str(ip_and_port)) + udp_server = UDPServer4 if isinstance(ip_and_port.ip, ipaddress.IPv4Address) else UDPServer6 + dns_server = DNSServer(resolver, address=ip_and_port.ip.exploded, port=ip_and_port.port, server=udp_server) + dns_server.thread_name = "%s:%s" % (ip_and_port.ip, ip_and_port.port) + self.servers.append(dns_server) self.watcher = DockerWatcher(self, default_host_ip, cli) @@ -68,7 +72,10 @@ def handle_hosts(self, hosts): 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 @@ def collect_from_containers(self): 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 @@ def collect_from_containers(self): # 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/udpserver.py b/src/systemd_resolved_docker/udpserver.py new file mode 100644 index 0000000..9e411f4 --- /dev/null +++ b/src/systemd_resolved_docker/udpserver.py @@ -0,0 +1,10 @@ +import dnslib.server +import socket + + +class UDPServer4(dnslib.server.UDPServer): + address_family = socket.AF_INET + + +class UDPServer6(dnslib.server.UDPServer): + address_family = socket.AF_INET6 diff --git a/src/systemd_resolved_docker/utils.py b/src/systemd_resolved_docker/utils.py index 8ecc780..b5f8b1d 100644 --- a/src/systemd_resolved_docker/utils.py +++ b/src/systemd_resolved_docker/utils.py @@ -1,19 +1,23 @@ 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 def __str__(self): - return "%s:%s" % (self.ip.compressed, self.port) + if isinstance(self.ip, ipaddress.IPv4Address): + return "%s:%s" % (self.ip.compressed, self.port) + else: + return "[%s]:%s" % (self.ip.compressed, self.port) + + +def parse_ip(entry, default_port=53) -> IpAndPort: + return IpAndPort(ip=ipaddress.ip_address(entry), port=default_port) def parse_ip_port(entry, default_port=53) -> IpAndPort: diff --git a/systemd-resolved-docker.sysconfig b/systemd-resolved-docker.sysconfig index fde5312..e9b1ad2 100644 --- a/systemd-resolved-docker.sysconfig +++ b/systemd-resolved-docker.sysconfig @@ -1,6 +1,25 @@ +## Domain globs of domains which will be handled by the DNS server. +## A container must be within one of these domains, while all non-matching requests +## will be forwarded to the configured DNS server. +## default: .docker +# ALLOWED_DOMAINS=.docker + +## Domain to append to containers which don't have one set using `--domainname` +## or are not part of a network +## default: .docker +# DEFAULT_DOMAIN=docker + +## IPs (+port) to listen on for queries from docker containers in the default network. +## default: ip of the default docker bridge +# DOCKER_LISTEN_ADDRESS=172.17.0.1:53 + +## IP address to use with host networks when an IP is not specified +## default: 127.0.0.1 +# DEFAULT_HOST_ip=127.0.0.1 + ## DNS server to use when resolving queries from docker containers. ## default: 127.0.0.53 -# DNS_SERVER=127.0.0.53 +# UPSTREAM_DNS_SERVER=127.0.0.53 ## Dummy interface name which will be created to interface with systemd-resolved. ## default: srd-dummy @@ -9,22 +28,3 @@ ## IPs (+port) to listen on for queries from systemd-resolved. ## default: 127.0.0.153 # SYSTEMD_RESOLVED_LISTEN_ADDRESS=127.0.0.153:53 - -## IPs (+port) to listen on for queries from docker containers in the default network. -## default: ip of the default docker bridge -# DOCKER_LISTEN_ADDRESS=172.17.0.1:53 - -## Domain to append to containers which don't have one set using `--domainname` -## or are not part of a network -## default: .docker -# DEFAULT_DOMAIN=docker - -## Domain globs of domains which will be handled by the DNS server. -## A container must be within one of these domains, while all non-matching requests -## will be forwarded to the configured DNS server. -## default: .docker -# ALLOWED_DOMAINS=.docker - -## IP address to use with host networks when an IP is not specified -## default: 127.0.0.1 -# DEFAULT_HOST_ip=127.0.0.1 \ No newline at end of file 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< /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