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

Initial Commit

parents
Branches
Tags
No related merge requests found
Showing
with 755 additions and 0 deletions
/dist
/*.egg-info
/rpm/*.rpm
/rpm/*.tar.gz
__pycache__
the .tito/packages directory contains metadata files
named after their packages. Each file has the latest tagged
version and the project's relative directory.
__version__ = '$version'
[buildconfig]
builder = tito.builder.Builder
tagger = tito.tagger.VersionTagger
changelog_do_not_remove_cherrypick = 0
changelog_format = %s (%ae)
[version_template]
destination_file = src/systemd_resolved_docker/__init__.py
template_file = .tito/templates/__init__.py.template
LICENSE 0 → 100644
Copyright 2020-2021 Zsombor Welker
Copyright 2018-2020 Patrice Ferlet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
README.md 0 → 100644
# systemd-resolved-docker
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.
## Install
### Fedora / COPR
For Fedora and RPM based systems [COPR](https://copr.fedorainfracloud.org/coprs/flaktack/systemd-resolved-docker/) contains pre-built packages.
1. Enabled the COPR repository
dnf copr enable flaktack/systemd-resolved-docker
1. Install the package
dnf install systemd-resolved-docker
1. Start and optionally enable the services
systemctl start systemd-resolved-docker
systemctl enable systemd-resolved-docker
1. Docker should be updated to use the DNS server provided by `systemd-docker-resolved.` This may be done
globally by editing the docker daemon's configuration (`daemon.json`) or per-container using the `--dns`
flag.
```js
"dns": [
"172.17.0.1" // docker0 interface's IP address
]
```
### 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` |
| 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.
## Build
`setup.py` may be used to create a python package.
`tito` may be used to create RPMs.
## Links
Portions are based on [docker-auto-dnsmasq](https://github.com/metal3d/docker-auto-dnsmasq).
\ No newline at end of file
%global srcname systemd-resolved-docker
%global eggname systemd_resolved_docker
Name: python-%{srcname}
Version: 0.0.0
Release: 0%{?dist}
Summary: systemd-resolved and docker DNS integration
License: BSD
URL: https://pypi.python.org/pypi/systemd_resolved_docker
#Source0: ${pypi_source}
Source0: %{srcname}-%{version}.tar.gz
Source1: %{srcname}.service
Source2: %{srcname}.sysconfig
BuildArch: noarch
%global _description %{expand:
systemd-resolved and docker DNS integration}
%description %_description
%package -n python3-%{srcname}
Summary: %{summary}
%if 0%{?el6}
BuildRequires: python34-devel
BuildRequires: python34-setuptools
%else
BuildRequires: python3-devel
BuildRequires: python3-setuptools
%endif
BuildRequires: systemd-rpm-macros
%description -n python3-%{srcname} %_description
#-- PREP, BUILD & INSTALL -----------------------------------------------------#
%prep
%autosetup -n %{srcname}-%{version}
%build
%py3_build
%install
%py3_install
# SystemdD services
install -dp %{buildroot}%{_unitdir}
install -p -m 644 %{SOURCE1} %{buildroot}%{_unitdir}
# Sysconfig
install -dp %{buildroot}%{_sysconfdir}/sysconfig
install -p -m 644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{srcname}
# %%check
# %%{python3} setup.py test
%post
%systemd_post %{srcname}.service
%preun
%systemd_preun %{srcname}.service
%postun
%systemd_postun_with_restart %{srcname}.service
#-- FILES ---------------------------------------------------------------------#
# Note that there is no %%files section for the unversioned python module
%files -n python3-%{srcname}
%doc README.md
%{python3_sitelib}/%{eggname}-*.egg-info/
%{python3_sitelib}/%{eggname}/
%{_bindir}/%{srcname}
%{_unitdir}/%{srcname}.service
%config(noreplace) %{_sysconfdir}/sysconfig/%{srcname}
#-- CHANGELOG -----------------------------------------------------------------#
%changelog
%global srcname dnslib
Name: python-%{srcname}
Version: 0.9.14
Release: 1%{?dist}
Summary: dnslib python module
License: BSD
URL: https://pypi.python.org/pypi/dnslib
Source0: %{pypi_source}
BuildArch: noarch
%global _description %{expand:
A library to encode/decode DNS wire-format packets supporting both
Python 2.7 and Python 3.2+.}
%description %_description
%package -n python3-%{srcname}
Summary: %{summary}
BuildRequires: python3-devel
%description -n python3-%{srcname} %_description
%prep
%autosetup -n %{srcname}-%{version}
%build
%py3_build
%install
%py3_install
# %check
# %{python3} setup.py test
# Note that there is no %%files section for the unversioned python module
%files -n python3-%{srcname}
%license LICENSE
%doc README
%{python3_sitelib}/%{srcname}-*.egg-info/
%{python3_sitelib}/%{srcname}/
setup.py 0 → 100644
import codecs, os.path
from setuptools import setup, find_packages
# use README.md as readme
def readme():
with open('README.md') as f:
return f.read()
# get __version__ from a file
def read(rel_path):
here = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(here, rel_path), 'r') as fp:
return fp.read()
def get_version(rel_path):
for line in read(rel_path).splitlines():
if line.startswith('__version__'):
delim = '"' if '"' in line else "'"
return line.split(delim)[1]
else:
raise RuntimeError("Unable to find version string.")
setup(
name='systemd-resolved-docker',
url='https://github.com/flaktack/systemd-resolved-docker',
license='MIT',
author='Zsombor Welker',
author_email='flaktack@flaktack.net',
install_requires=["docker", "dnslib", "systemd-python", "dbus-python", "pyroute2"],
description='systemd-resolved and docker DNS integration',
long_description=readme(),
long_description_content_type="text/markdown",
package_dir={
'': 'src',
},
packages=find_packages('src'),
entry_points={
'console_scripts': [
'systemd-resolved-docker=systemd_resolved_docker.cli:main',
],
},
excluded=['rpms/*'],
# extract version from source
version=get_version("src/systemd_resolved_docker/__init__.py"),
)
__version__ = '0.0.0'
#!/usr/bin/env python3
import os
import docker
import signal
from systemd import daemon, journal
from .dockerdnsconnector import DockerDNSConnector
from .utils import find_default_docker_bridge_gateway, find_docker_dns_servers
class Handler:
def on_start(self):
daemon.notify('READY=1')
self.log("Started daemon")
def on_update(self, hosts):
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]))
self.log(message)
def on_stop(self):
self.log("Stopped daemon")
def log(self, message):
print(message)
def main():
dns_server = os.environ.get("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:
domains = [".docker"]
else:
domains = tld.split(',') if tld and len(tld) > 0 else []
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']
handler = Handler()
connector = DockerDNSConnector(listen_addresses, listen_port, dns_server, domains, default_domain, interface,
handler, cli)
connector.start()
def sig_handler(signum, frame):
handler.log("Stopping - %s" % signal.Signals(signum))
connector.stop()
signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGINT, sig_handler)
signal.pause()
if __name__ == '__main__':
main()
import ipaddress
import os
import threading
from socket import AF_INET, AF_INET6
import dbus
from dnslib import A, CLASS, QTYPE, RR
from dnslib.proxy import ProxyResolver
from dnslib.server import DNSServer
from pyroute2 import IPRoute
from .dockerwatcher import DockerWatcher
from .interceptresolver import InterceptResolver
from .zoneresolver import ZoneResolver
class DockerDNSConnector:
def __init__(self, listen_addresses, listen_port, upstream_dns_server, dns_domains, default_domain,
docker_interface, handler, cli):
super().__init__()
self.listen_addresses = listen_addresses
self.upstream_dns_server = upstream_dns_server
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]
self.resolver = ZoneResolver([])
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)
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)
def start(self):
self.watcher.start()
for server in self.servers:
server.thread = threading.Thread(target=server.server.serve_forever)
server.thread.start()
self.handler.on_start()
def stop(self):
self.update_resolved(enabled=False)
for server in self.servers:
server.stop()
for server in self.servers:
server.thread.join()
self.handler.on_stop()
def update_resolved(self, enabled=True):
with IPRoute() as ipr:
ifi = ipr.link_lookup(ifname=self.docker_interface)
if not ifi:
raise ValueError("Unknown interface '%s'" % self.docker_interface)
ifindex = ifi[0]
system_bus = dbus.SystemBus()
proxy = system_bus.get_object('org.freedesktop.resolve1', '/org/freedesktop/resolve1')
manager = dbus.Interface(proxy, 'org.freedesktop.resolve1.Manager')
if enabled:
domains = [[domain.strip("."), True] for domain in self.dns_domains]
ips = [
[
AF_INET if isinstance(ip, ipaddress.IPv4Address) else AF_INET6,
ip.packed
]
for ip in [ipaddress.ip_address(ip) for ip in self.listen_addresses]
]
manager.SetLinkDomains(ifindex, domains)
manager.SetLinkDNS(ifindex, ips)
else:
manager.RevertLink(ifindex)
def handle_hosts(self, hosts):
zone = []
host_names = []
for host in hosts:
for host_name in host.host_names:
rr = RR(host_name, QTYPE.A, CLASS.IN, 1, A(host.ip))
zone.append(rr)
host_names.append(host_name)
self.resolver.update(zone)
self.update_resolved(enabled=len(host_names) > 0)
self.handler.on_update(hosts)
import docker
from threading import Thread
from dnslib import DNSLabel
class DockerHost:
def __init__(self, host_names, ip, interface=None):
self.host_names = host_names
self.ip = ip
self.interface = interface
class DockerWatcher(Thread):
"""
Thread based module for wathing for docker container changes.
"""
def __init__(self, handler, domain_globs=None, default_domain=None, 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:
self.collect_from_containers()
for e in self.cli.events(decode=True):
status = e.get('status', False)
if status in ('die', 'start'):
self.collect_from_containers()
return
def collect_from_containers(self):
hostnames = []
domain_records = {}
for c in self.cli.containers.list():
# the records
hostname = c.attrs['Config']['Hostname']
domain = c.attrs['Config'].get('Domainname')
if len(domain) > 0:
hostname = '%s.%s' % (hostname, domain)
if '.' not in hostname:
hostname += self.default_domain
# 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")
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)
# 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))
self.handler.handle_hosts(hostnames)
if __name__ == '__main__':
def callback(hosts):
print("Received hosts:", len(hosts))
for host in hosts:
print("%s - %s" % (host.ip, ", ".join(host.host_names)))
watcher = DockerWatcher(callback)
watcher.start()
import copy
from dnslib import RCODE
from dnslib.server import BaseResolver
class InterceptResolver(BaseResolver):
"""
Simple resolver that tries to handle queries local, and if that is unhandled the forwards the requests to
a fallback resolver.
"""
def __init__(self, local_domains, local_resolver, fallback_resolver):
self.local_domains = local_domains
self.local_resolver = local_resolver
self.fallback_resolver = fallback_resolver
def resolve(self, request, handler):
if self.is_local(request.q.qname):
return self.local_resolver.resolve(request, handler)
else:
return self.fallback_resolver.resolve(request, handler)
def is_local(self, qname):
for domain in self.local_domains:
if qname.matchGlob(domain):
return True
return False
def find_docker_dns_servers(cli):
return []
def find_default_docker_bridge_gateway(cli):
networks = cli.networks.list()
addresses = []
for network in networks:
if 'Options' not in network.attrs:
continue
if 'com.docker.network.bridge.default_bridge' not in network.attrs['Options']:
continue
if network.attrs['Options']['com.docker.network.bridge.default_bridge'] != 'true':
continue
name = network.attrs['Options']['com.docker.network.bridge.name']
if 'IPAM' not in network.attrs:
continue
if 'Config' not in network.attrs['IPAM']:
continue
for config in network.attrs['IPAM']['Config']:
if 'Gateway' in config:
gateway = config['Gateway']
print("Found gateway %s for %s" % (gateway, name))
addresses.append({'gateway': gateway, 'interface': name})
return addresses
import copy
from dnslib import RR, QTYPE, RCODE
from dnslib.server import BaseResolver
class ZoneResolver(BaseResolver):
"""
Simple fixed zone file resolver.
"""
def __init__(self, zone, glob=False):
"""
Initialise resolver from zone file.
Stores RRs as a list of (label,type,rr) tuples
If 'glob' is True use glob match against zone file
"""
self.glob = glob
self.eq = 'matchGlob' if glob else '__eq__'
self.zone = []
self.update(zone)
def update(self, zone):
self.zone = [(rr.rname, QTYPE[rr.rtype], rr) for rr in zone]
def resolve(self, request, handler):
"""
Respond to DNS request - parameters are request packet & handler.
Method is expected to return DNS response
"""
reply = request.reply()
qname = request.q.qname
qtype = QTYPE[request.q.qtype]
zone = self.zone
for name, rtype, rr in zone:
# Check if label & type match
if getattr(qname, self.eq)(name) and (qtype == rtype or
qtype == 'ANY' or
rtype == 'CNAME'):
# If we have a glob match fix reply label
if self.glob:
a = copy.copy(rr)
a.rname = qname
reply.add_answer(a)
else:
reply.add_answer(rr)
# Check for A/AAAA records associated with reply and
# add in additional section
if rtype in ['CNAME', 'NS', 'MX', 'PTR']:
for a_name, a_rtype, a_rr in zone:
if a_name == rr.rdata.label and a_rtype in ['A', 'AAAA']:
reply.add_ar(a_rr)
if not reply.rr:
reply.header.rcode = RCODE.NXDOMAIN
return reply
[Unit]
Description=systemd-resolved docker DNS
After=docker.service systemd-resolved.service
[Service]
Type=notify
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=-/etc/sysconfig/systemd-resolved-docker
ExecStart=/usr/bin/systemd-resolved-docker
Restart=on-failure
[Install]
WantedBy=multi-user.target
## DNS server to use when resolving queries from docker containers.
## default: 127.0.0.53
# DNS_SERVER=127.0.0.53
## Docker interface name
## 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
## Port to listen on for queries from systemd-resolved and docker containers.
## default: 53
# LISTEN_PORT=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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment