Skip to content
This repository was archived by the owner on Jan 23, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ an exact match is required. If a generated domain address doesn't match the list

1. `<container_id>.<default_domain>`

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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions src/systemd_resolved_docker/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python3

import os
import signal

Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 13 additions & 6 deletions src/systemd_resolved_docker/dockerdnsconnector.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
22 changes: 14 additions & 8 deletions src/systemd_resolved_docker/dockerwatcher.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -85,22 +88,25 @@ 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

# 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))
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))
Expand Down
10 changes: 10 additions & 0 deletions src/systemd_resolved_docker/udpserver.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 10 additions & 6 deletions src/systemd_resolved_docker/utils.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
40 changes: 20 additions & 20 deletions systemd-resolved-docker.sysconfig
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
7 changes: 7 additions & 0 deletions test/integration/functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
76 changes: 76 additions & 0 deletions test/integration/test_ipv6.sh
Original file line number Diff line number Diff line change
@@ -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
22 changes: 12 additions & 10 deletions test/integration/test_proxy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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