From 97e67f721c8e43e0d652e79e3c35e120a859cd77 Mon Sep 17 00:00:00 2001 From: Zsombor Welker <fedora@zdeqb.com> Date: Fri, 17 Jun 2022 12:01:25 +0200 Subject: [PATCH] Allow specifying both the listen IPs and ports The configuration is also split to allow explicitly specifying the listen address to use with systemd-resolved and the "other" listen addresses. --- README.md | 29 ++++++------ src/systemd_resolved_docker/cli.py | 45 ++++++++++--------- .../dockerdnsconnector.py | 25 +++++------ src/systemd_resolved_docker/dockerwatcher.py | 3 ++ .../resolvedconnector.py | 30 ++++++++----- src/systemd_resolved_docker/utils.py | 29 +++++++++++- systemd-resolved-docker.sysconfig | 12 ++--- 7 files changed, 106 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 0c6f328..8aa7d8b 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,21 @@ Host test.docker not found: 3(NXDOMAIN) If there are link-local, VPN or other DNS servers configured then those will also work within containers. +## Configuration + +`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. + +| 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` | +| DOCKER_INTERFACE | Docker interface name | The first docker network's interface | `docker0` | +| 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` | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------- | + ## Install ### Fedora / COPR @@ -155,20 +170,6 @@ For Fedora and RPM based systems [COPR](https://copr.fedorainfracloud.org/coprs/ unmanaged-devices=interface-name:docker0 ``` -### Configuration - -`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. - -| 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` | -| DOCKER_INTERFACE | Docker interface name | The first docker network's interface | `docker0` | -| LISTEN_ADDRESS | IPs to listen on for queries from systemd-resolved and docker containers. | _ip of the default docker bridge_, often `172.17.0.1` | `172.17.0.1,127.0.0.153` | -| LISTEN_PORT | Port to listen on for queries from systemd-resolved and docker containers. | `53` | `1053` | -| 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` | - ## Build `setup.py` may be used to create a python package. diff --git a/src/systemd_resolved_docker/cli.py b/src/systemd_resolved_docker/cli.py index 6076612..d662766 100644 --- a/src/systemd_resolved_docker/cli.py +++ b/src/systemd_resolved_docker/cli.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 import os -import docker import signal -from systemd import daemon, journal -from systemd_resolved_docker.resolvedconnector import SystemdResolvedConnector +import docker +from systemd import daemon + from .dockerdnsconnector import DockerDNSConnector -from .utils import find_default_docker_bridge_gateway, find_docker_dns_servers +from .resolvedconnector import SystemdResolvedConnector +from .utils import find_default_docker_bridge_gateway, parse_ip_port, parse_listen_address class Handler: @@ -16,8 +17,10 @@ class Handler: self.log("Started daemon") def on_update(self, hosts): - message = "Refreshed - %d items (%s)" % ( - len(hosts), ' '.join(["%s/%s" % (host.ip, ','.join(host.host_names)) for host in hosts])) + if len(hosts) > 0: + message = "Refreshed - %d items\n\t%s" % (len(hosts), '\n\t'.join(map(lambda x: str(x), hosts))) + else: + message = "Refreshed - no running containers" self.log(message) @@ -29,10 +32,10 @@ class Handler: def main(): - dns_server = os.environ.get("DNS_SERVER", "127.0.0.53") + systemd_resolved_listen_address = os.environ.get("SYSTEMD_RESOLVED_LISTEN_ADDRESS", None) + docker_listen_address = os.environ.get("DOCKER_LISTEN_ADDRESS", None) + dns_server = parse_ip_port(os.environ.get("UPSTREAM_DNS_SERVER", "127.0.0.53")) default_domain = os.environ.get("DEFAULT_DOMAIN", "docker") - listen_port = int(os.environ.get("LISTEN_PORT", "53")) - listen_address = os.environ.get("LISTEN_ADDRESS", None) tld = os.environ.get('ALLOWED_DOMAINS', None) if tld is None or len(tld.strip()) == 0: @@ -41,25 +44,25 @@ def main(): domains = [item.strip() for item in tld.split(',')] cli = docker.from_env() - docker_dns_servers = find_docker_dns_servers(cli) docker_gateway = find_default_docker_bridge_gateway(cli) - if listen_address is None or len(listen_address) < 1: - listen_addresses = [entry['gateway'] for entry in docker_gateway] - else: - listen_addresses = listen_address.split(",") - - interface = os.environ.get('DOCKER_INTERFACE', None) - if interface is None or len(interface) < 1: - interface = docker_gateway[0]['interface'] + systemd_resolved_interface = os.environ.get('DOCKER_INTERFACE', None) + if systemd_resolved_interface is None or len(systemd_resolved_interface) < 1: + systemd_resolved_interface = docker_gateway[0]['interface'] handler = Handler() handler.log("Default domain: %s, allowed domains: %s" % (default_domain, ", ".join(domains))) - resolved = SystemdResolvedConnector(interface, listen_addresses, listen_port, domains) + 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 + docker_gateway]) + + resolved = SystemdResolvedConnector(systemd_resolved_interface, systemd_resolved_listen_addresses, domains, handler) - dns_connector = DockerDNSConnector(listen_addresses, listen_port, dns_server, domains, default_domain, interface, - handler, cli) + dns_connector = DockerDNSConnector(systemd_resolved_listen_addresses + docker_listen_addresses, dns_server, domains, + default_domain, handler, cli) dns_connector.start() resolved.register() diff --git a/src/systemd_resolved_docker/dockerdnsconnector.py b/src/systemd_resolved_docker/dockerdnsconnector.py index d850384..b40877f 100644 --- a/src/systemd_resolved_docker/dockerdnsconnector.py +++ b/src/systemd_resolved_docker/dockerdnsconnector.py @@ -1,4 +1,5 @@ import threading +from typing import List from dnslib import A, CLASS, DNSLabel, QTYPE, RR from dnslib.proxy import ProxyResolver @@ -6,19 +7,16 @@ from dnslib.server import DNSServer from .dockerwatcher import DockerWatcher, DockerHost from .interceptresolver import InterceptResolver +from .utils import IpAndPort from .zoneresolver import ZoneResolver class DockerDNSConnector: - def __init__(self, listen_addresses, listen_port, upstream_dns_server, dns_domains, default_domain, - docker_interface, handler, cli): + def __init__(self, listen_addresses: List[IpAndPort], upstream_dns_server: IpAndPort, dns_domains, default_domain, + handler, cli): super().__init__() - self.listen_addresses = listen_addresses - self.upstream_dns_server = upstream_dns_server self.default_domain = default_domain - self.dns_domains = dns_domains - self.docker_interface = docker_interface self.handler = handler self.dns_domains_globs = ['*%s' % domain if domain.startswith('.') else domain for domain in dns_domains] @@ -27,14 +25,15 @@ class DockerDNSConnector: self.servers = [] resolver = InterceptResolver(self.dns_domains_globs, self.resolver, - ProxyResolver(upstream_dns_server, port=53, timeout=5)) - self.handler.log("Unhandled DNS requests will be resolved using %s:53" % upstream_dns_server) - - for address in listen_addresses: - server = DNSServer(resolver, address=address, port=listen_port) - server.thread_name = "%s:%s" % (address, listen_port) + 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))) + + 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 %s:%s" % (address, listen_port)) self.watcher = DockerWatcher(self, cli) diff --git a/src/systemd_resolved_docker/dockerwatcher.py b/src/systemd_resolved_docker/dockerwatcher.py index 02ee5ef..95f3063 100644 --- a/src/systemd_resolved_docker/dockerwatcher.py +++ b/src/systemd_resolved_docker/dockerwatcher.py @@ -9,6 +9,9 @@ class DockerHost: self.ip = ip self.interface = interface + def __str__(self): + return "%s/%s" % (self.ip, ','.join(self.host_names)) + class DockerWatcher(Thread): """ diff --git a/src/systemd_resolved_docker/resolvedconnector.py b/src/systemd_resolved_docker/resolvedconnector.py index d3eb11b..96e51ba 100644 --- a/src/systemd_resolved_docker/resolvedconnector.py +++ b/src/systemd_resolved_docker/resolvedconnector.py @@ -1,28 +1,31 @@ import ipaddress from socket import AF_INET, AF_INET6 +from typing import List import dbus from pyroute2 import IPRoute +from .utils import IpAndPort + class SystemdResolvedConnector: - def __init__(self, docker_interface, listen_addresses, listen_port, dns_domains): + def __init__(self, interface, listen_addresses: List[IpAndPort], dns_domains, handler): super().__init__() - self.docker_interface = docker_interface + self.interface = interface self.listen_addresses = listen_addresses - self.listen_port = listen_port self.dns_domains = dns_domains + self.handler = handler - self.ifindex = self.resolve_ifindex(docker_interface) + self.ifindex = self.resolve_ifindex(interface) @staticmethod - def resolve_ifindex(docker_interface): + def resolve_ifindex(interface): with IPRoute() as ipr: - ifi = ipr.link_lookup(ifname=docker_interface) + ifi = ipr.link_lookup(ifname=interface) if not ifi: - raise ValueError("Unknown interface '%s'" % docker_interface) + raise ValueError("Unknown interface '%s'" % interface) return ifi[0] @@ -33,15 +36,18 @@ class SystemdResolvedConnector: return dbus.Interface(proxy, 'org.freedesktop.resolve1.Manager') def register(self): + self.handler.log("Registering with systemd-resolved - interface: %s, domains: %s, dns server: %s" % ( + self.interface, self.dns_domains, ", ".join(map(lambda x: str(x), self.listen_addresses)))) + domains = [[domain.strip("."), True] for domain in self.dns_domains] ips = [ [ - AF_INET if isinstance(ip, ipaddress.IPv4Address) else AF_INET6, - ip.packed, - self.listen_port, + AF_INET if isinstance(ip_port.ip, ipaddress.IPv4Address) else AF_INET6, + ip_port.ip.packed, + ip_port.port, "", ] - for ip in [ipaddress.ip_address(ip) for ip in self.listen_addresses] + for ip_port in self.listen_addresses ] manager = self.if_manager() @@ -50,5 +56,7 @@ class SystemdResolvedConnector: manager.SetLinkDomains(self.ifindex, domains) def unregister(self): + self.handler.log("Unregistering with systemd-resolved: %s" % self.interface) + manager = self.if_manager() manager.RevertLink(self.ifindex) diff --git a/src/systemd_resolved_docker/utils.py b/src/systemd_resolved_docker/utils.py index 323a6de..2ce72d5 100644 --- a/src/systemd_resolved_docker/utils.py +++ b/src/systemd_resolved_docker/utils.py @@ -1,5 +1,30 @@ -def find_docker_dns_servers(cli): - return [] +import ipaddress +import urllib.parse +from typing import List + + +class IpAndPort: + ip: ipaddress.ip_address + port: int + + def __init__(self, ip: ipaddress.ip_address, port: int): + self.ip = ip + self.port = port + + def __str__(self): + return "%s:%s" % (self.ip.compressed, self.port) + + +def parse_ip_port(entry, default_port=53) -> IpAndPort: + result = urllib.parse.urlsplit('//' + entry) + return IpAndPort(ip=ipaddress.ip_address(result.hostname), port=result.port or default_port) + + +def parse_listen_address(listen_addresses, default_value) -> List[IpAndPort]: + if listen_addresses is not None and len(listen_addresses) > 1: + return [parse_ip_port(item) for item in listen_addresses.split(",")] + else: + return default_value() def find_default_docker_bridge_gateway(cli): diff --git a/systemd-resolved-docker.sysconfig b/systemd-resolved-docker.sysconfig index 51806a8..c6afc32 100644 --- a/systemd-resolved-docker.sysconfig +++ b/systemd-resolved-docker.sysconfig @@ -6,13 +6,13 @@ ## default: first docker network's interface # DOCKER_INTERFACE=docker0 -## IPs to listen on for queries from systemd-resolved and docker containers -## default: ip address of each defined docker network -# LISTEN_ADDRESS=172.17.0.1 +## IPs (+port) to listen on for queries from systemd-resolved. +## default: 127.0.0.153 +# SYSTEMD_RESOLVED_LISTEN_ADDRESS=127.0.0.153:53 -## Port to listen on for queries from systemd-resolved and docker containers. -## default: 53 -# LISTEN_PORT=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 -- GitLab