Skip to content
Snippets Groups Projects
Commit 6e5d5acb authored by Zsombor Welker's avatar Zsombor Welker
Browse files

Clarify domain name generating logic

parent 6be3a300
Branches
Tags
No related merge requests found
......@@ -3,11 +3,99 @@
Provides systemd-resolved and docker DNS integration.
A DNS server is configured to listen on each docker interface's IP address. This is used to:
1. expose the systemd-resolved DNS service (`127.0.0.53`) to docker containers by proxying DNS requests, since the
systems loopback IPs can't be accessed from containers.
2. adds the created DNS servers to the docker interface using systemd-resolved so that docker containers may
be referenced by hostname. This uses `--hostname` and `--domainname`, `--network` or a default of `.docker` to
create the domains.
1. allows containers to be referenced by hostname by adding the created DNS servers to the docker interface using
the systemd-resolved D-Bus API.
1. expose the systemd-resolved DNS service (`127.0.0.53`) to docker containers by proxying DNS requests, which doesn't
work by default due to the differing network namespaces.
## Features
### Container domain addresses
Based on the container's properties multiple domain names may be generated. For this the `default_domain`
(`DEFAULT_DOMAIN`) and _allowed domains_ (`ALLOWED_DOMAINS`) options are used. The list of _allowed domains_ specifies
which domains may be handled. An entry starting with `.` (example: `.docker`) allows all matching subdomains, otherwise
an exact match is required. If a generated domain address doesn't match the list of _allowed domains_, then the
`default_domain` is appended.
1. `<container_id>.<default_domain>`
All containers will be reachable by their `container_id`:
```sh
docker run --rm -it alpine # d6d51528ac46.docker
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d6d51528ac46 alpine "/bin/sh" 8 seconds ago Up 6 seconds relaxed_cartwright
```
1. `<container_hostname>.<default_domain>`, `<container_hostname>.<container_domain>.<default_domain>`, `<container_hostname>.<container_domain>`
If an explicit `--hostname` is provided then that may also be used:
```sh
docker run --rm -it --hostname test alpine # test.docker
```
When the hostname is in the list of _allowed domains_ (`ALLOWED_DOMAINS=.docker,some-host`), then the `default_domain`
will not be appended:
```sh
docker run --rm -it --hostname some-host alpine # some-host
```
If an explicit `--domainname` is provided then that may also be used:
```sh
docker run --rm -it --hostname test --domainname mydomain alpine # test.mydomain.docker
```
When the domain name is in the list of _allowed domains_ (`ALLOWED_DOMAINS=.docker,.local`), then the `default_domain`
will not be appended:
```sh
docker run --rm -it --hostname test --domainname local alpine # test.local
```
1. `<container_name>.<container_network>.<default_domain>`, `<container_name>.<container_network>`
If a non-default network is used (not `bridge` or `host`) then a name will be generated based on the network's name:
```sh
docker run --rm -it --network testnet alpine # zealous_jones.testnet.docker
docker run --rm -it --name db --network testnet alpine # db.testnet.docker
```
When the network's name is in the list of _allowed domains_ (`ALLOWED_DOMAINS=.docker,.somenet`), then the
`default_domain` will not be appended:
```sh
docker run --rm -it --network somenet alpine # zealous_jones.somenet
docker run --rm -it --name db --network somenet alpine # db.somenet.docker
```
If configured correctly then `resolvectl status` should show the configured link-specific DNS server:
$ resolvectl status
...
Link 7 (docker0)
Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 172.17.0.1
DNS Domain: ~docker
...
### 127.0.0.53 / systemd-resolved within containers
If docker is configured to use the provided DNS server then the container domain names may also be resolved within containers:
```
$ docker run --dns 1.1.1.1 --rm -it alpine
/ # apk add bind
/ # host test.docker
Host test.docker not found: 3(NXDOMAIN)
```
```
$ docker run --dns 172.17.0.1 --rm -it alpine
/ # apk add bind
/ # host test.docker
/ # host test.docker
test.docker has address 172.17.0.3
Host test.docker not found: 3(NXDOMAIN)
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.
## Install
......@@ -49,45 +137,8 @@ For Fedora and RPM based systems [COPR](https://copr.fedorainfracloud.org/coprs/
| 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` |
| DEFAULT_DOMAIN | Domain to append to containers which don't have one set using `--domainname` or are not part of a network `--network`. | `.docker` | `.docker` |
| ALLOWED_DOMAINS | Domain globs which will be handled by the DNS server. | `.docker` | `.docker,local` |
## Usage
Start a container with a specified hostname:
`docker run --hostname test python:3.9 python -m http.server 3000`
If configured correctly then `resolvectl status` should show the configured link-specific DNS server, while the url
should load: http://test.docker:3000/
$ resolvectl status
...
Link 7 (docker0)
Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 172.17.0.1
DNS Domain: ~docker
...
If docker is configured to use the provided DNS server then the container domain names may also be resolved within containers:
$ docker run --dns 1.1.1.1 --rm -it alpine
/ # apk add bind
/ # host test.docker
Host test.docker not found: 3(NXDOMAIN)
```
$ docker run --dns 172.17.0.1 --rm -it alpine
/ # apk add bind
/ # host test.docker
/ # host test.docker
test.docker has address 172.17.0.3
Host test.docker not found: 3(NXDOMAIN)
Host test.docker not found: 3(NXDOMAIN)
```
If there are link-local, VPN or other DNS servers configured than those will also work within containers.
| 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
......@@ -95,7 +146,6 @@ If there are link-local, VPN or other DNS servers configured than those will als
`tito` may be used to create RPMs.
## Links
Portions are based on [docker-auto-dnsmasq](https://github.com/metal3d/docker-auto-dnsmasq).
......@@ -18,7 +18,7 @@ class Handler:
os.system('resolvectl flush-caches')
message = "Refreshed - %d items (%s)" % (
len(hosts), ', '.join(["%s/%s" % (host.ip, ','.join(host.host_names)) for host in hosts]))
len(hosts), ' '.join(["%s/%s" % (host.ip, ','.join(host.host_names)) for host in hosts]))
self.log(message)
def on_stop(self):
......@@ -30,15 +30,15 @@ class Handler:
def main():
dns_server = os.environ.get("DNS_SERVER", "127.0.0.53")
default_domain = os.environ.get("DEFAULT_DOMAIN", ".docker")
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:
if tld is None or len(tld.strip()) == 0:
domains = [".docker"]
else:
domains = tld.split(',') if tld and len(tld) > 0 else []
domains = [item.strip() for item in tld.split(',')]
cli = docker.from_env()
docker_dns_servers = find_docker_dns_servers(cli)
......@@ -54,6 +54,7 @@ def main():
interface = docker_gateway[0]['interface']
handler = Handler()
handler.log("Default domain: %s, allowed domains: %s" % (default_domain, ", ".join(domains)))
connector = DockerDNSConnector(listen_addresses, listen_port, dns_server, domains, default_domain, interface,
handler, cli)
......
import ipaddress
import os
import threading
from socket import AF_INET, AF_INET6
import dbus
from dnslib import A, CLASS, QTYPE, RR
from dnslib import A, CLASS, DNSLabel, QTYPE, RR
from dnslib.proxy import ProxyResolver
from dnslib.server import DNSServer
from pyroute2 import IPRoute
from .dockerwatcher import DockerWatcher
from .dockerwatcher import DockerWatcher, DockerHost
from .interceptresolver import InterceptResolver
from .zoneresolver import ZoneResolver
......@@ -21,6 +20,7 @@ class DockerDNSConnector:
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
......@@ -39,7 +39,7 @@ class DockerDNSConnector:
self.servers.append(server)
self.handler.log("DNS server listening on %s:%s" % (address, listen_port))
self.watcher = DockerWatcher(self, self.dns_domains_globs, default_domain, cli)
self.watcher = DockerWatcher(self, cli)
def start(self):
self.watcher.start()
......@@ -91,14 +91,28 @@ class DockerDNSConnector:
def handle_hosts(self, hosts):
zone = []
host_names = []
mapped_hosts = []
for host in hosts:
mh = DockerHost([], host.ip)
mapped_hosts.append(mh)
for host_name in host.host_names:
rr = RR(host_name, QTYPE.A, CLASS.IN, 1, A(host.ip))
hn = self.as_allowed_hostname(host_name)
mh.host_names.append(hn)
rr = RR(hn, QTYPE.A, CLASS.IN, 1, A(host.ip))
zone.append(rr)
host_names.append(host_name)
host_names.append(hn)
self.resolver.update(zone)
self.update_resolved(enabled=len(host_names) > 0)
self.handler.on_update(hosts)
self.handler.on_update(mapped_hosts)
def as_allowed_hostname(self, hostname):
for domain in self.dns_domains_globs:
if DNSLabel(hostname).matchGlob(domain):
return hostname
return "%s.%s" % (hostname, self.default_domain)
......@@ -2,8 +2,6 @@ import docker
from threading import Thread
from dnslib import DNSLabel
class DockerHost:
def __init__(self, host_names, ip, interface=None):
......@@ -14,25 +12,17 @@ class DockerHost:
class DockerWatcher(Thread):
"""
Thread based module for wathing for docker container changes.
Thread based module for watching for docker container changes.
"""
def __init__(self, handler, domain_globs=None, default_domain=None, cli=None):
def __init__(self, handler, cli=None):
super().__init__()
if cli is None:
cli = docker.from_env()
if domain_globs is None:
domain_globs = ["*.docker"]
if default_domain is None:
default_domain = ".docker"
self.daemon = True
self.handler = handler
self.domain_globs = domain_globs
self.default_domain = default_domain
self.cli = cli
def run(self) -> None:
......@@ -46,51 +36,45 @@ class DockerWatcher(Thread):
return
def collect_from_containers(self):
hostnames = []
domain_records = {}
for c in self.cli.containers.list():
# the records
common_hostnames = []
# container_id (.docker), only the first 12 characters need to be used
container_id = c.attrs['Id']
common_hostnames.append(container_id[0:12])
# hostname (.docker), hostname.domainname (.docker)
hostname = c.attrs['Config']['Hostname']
domain = c.attrs['Config'].get('Domainname')
# if no explicit --hostname is provided, than it will be the first 12 characters of the container_id.
# In that case, the hostname can be ignored
if hostname not in container_id:
if len(domain) > 0:
hostname = '%s.%s' % (hostname, domain)
if '.' not in hostname:
hostname += self.default_domain
common_hostnames.append('%s.%s' % (hostname, domain))
else:
common_hostnames.append(hostname)
# get container name
name = c.attrs['Name'][1:]
# now read network settings
settings = c.attrs['NetworkSettings']
for netname, network in settings.get('Networks', {}).items():
ip = network.get('IPAddress', False)
if not ip or ip == "":
continue
record = domain_records.get(ip, [])
# record the container name DOT network
# 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))
# check if the hostname is allowed
for domain in self.domain_globs:
if DNSLabel(hostname).matchGlob(domain):
record.append(hostname)
record.append('%s.%s' % (name, netname))
# do not append record if it's empty
if len(record) > 0:
domain_records[ip] = record
for ip, hosts in domain_records.items():
hostnames.append(DockerHost(hosts, ip))
hostnames = [DockerHost(hosts, ip) for ip, hosts in domain_records.items()]
self.handler.handle_hosts(hostnames)
......
......@@ -17,7 +17,7 @@
## 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
# 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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment