Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [noble, jammy, focal, trixie, bookworm, bullseye]
os: [noble, jammy, focal, trixie, bookworm, bullseye,
noble-desktop, jammy-desktop, focal-desktop,
trixie-desktop, bookworm-desktop, bullseye-desktop]
steps:
- uses: actions/checkout@v4

Expand Down
16 changes: 15 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,18 @@ test-systemd: build
./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local
./test/systemd/test-systemd.sh $(OS)

.PHONY: build test run test-systemd
test-systemd-all: build
@for os in noble jammy focal trixie bookworm bullseye noble-desktop jammy-desktop focal-desktop trixie-desktop bookworm-desktop bullseye-desktop; do \
echo "=== Testing $$os ==="; \
./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local && \
./test/systemd/test-systemd.sh $$os || echo "FAILED: $$os"; \
done

test-systemd-desktop-all: build
@for os in noble-desktop jammy-desktop focal-desktop trixie-desktop bookworm-desktop bullseye-desktop; do \
echo "=== Testing $$os ==="; \
./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local && \
./test/systemd/test-systemd.sh $$os || echo "FAILED: $$os"; \
done

.PHONY: build test run test-systemd test-systemd-all test-systemd-desktop-all
74 changes: 43 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ between containers and the host.

## Features

- **Automatic DNS Resolution**: Resolve Docker container names with a custom TLD to their respective IP addresses.
- **Caching Mechanism**: Configurable TTL for DNS cache entries to improve performance.
- **Simple Configuration**: Minimal setup with command-line options or a configuration file.
- **Debian Package**: Easy-to-install `.deb` package for seamless integration.
- **Fallback DNS**: Forwards non-Docker queries to configurable DNS servers.
- **Lightweight and Fast**: Built with Go for high performance.
- **Automatic DNS Resolution**: Resolve Docker container names with a custom TLD (default `.docker`) to their IP addresses. Supports multiple TLDs and containers on any Docker network.
- **Fallback DNS**: Forwards non-Docker queries in parallel to configurable upstream resolvers (default: `8.8.8.8`, `1.1.1.1`, `8.8.4.4`), returning the first successful response.
- **Caching**: TTL-based DNS cache with background eviction, size limits, and hit/miss telemetry.
- **Rate Limiting**: Per-IP token-bucket rate limiter with automatic idle cleanup.
- **Health & Metrics**: HTTP server on `:8080` exposes `/health` and `/metrics` (cache stats, query counts, error rates).
- **UDP + TCP**: Full DNS protocol support with EDNS0 handling and proper truncation.
- **Debian Package**: `.deb` package with automatic systemd integration and clean uninstall.
- **Tested on 12 Configurations**: Full install → resolve → uninstall lifecycle CI on Ubuntu 20.04/22.04/24.04 and Debian 11/12/13, both server and desktop variants.
- **Lightweight**: Single Go binary, minimal resource footprint.

## Architecture

Expand Down Expand Up @@ -146,23 +149,18 @@ integrates with DNS resolution for each ditro/version..

---

## Important Notes
## DNS Integration

### Default Resolver Integration
The `.deb` package auto-detects your system's DNS resolver and integrates accordingly:

- While Docker DNS runs by default on a custom IP (not `127.0.0.1`), it is possible to use tools like `dig` to test
queries:
```bash
dig mycontainer.docker @127.0.0.153 +short
```
- However, making Docker DNS the system-wide default resolver requires additional configuration or hacks.
- **Ubuntu** (systemd-resolved): routing domain drop-in at `/etc/systemd/resolved.conf.d/docker-dns.conf` — only
`.docker` queries go to docker-dns, everything else is untouched
- **Debian Desktop** (NetworkManager): dispatcher script at `/etc/NetworkManager/dispatcher.d/docker-dns` re-prepends
the nameserver after every NM event
- **Debian Server** (plain resolv.conf): prepends `nameserver 127.0.0.153` to `/etc/resolv.conf`

### Systemd-Resolved Compatibility: Direct integration with
`systemd-resolved` is non-trivial and not recommended without advanced setup.

### Protocol

* `docker-dns` is configured to run on `UDP` mode only, so `TCP` requests won't be answered
See [docs/Systemd.md](./docs/Systemd.md) for the full details, browser DNS-over-HTTPS caveats, and manual integration
examples for custom resolvers (dnsmasq, unbound, Pi-hole).

---

Expand Down Expand Up @@ -192,17 +190,31 @@ integrates with DNS resolution for each ditro/version..
- Once you have the binary generated, you can run at as follows (linux):
```bash
sudo ./docker-dns -h
Usage of docker-dns:
-ip string
IP address the DNS server listens on (default "127.0.0.153")
-tld string
Comma-separated managed top-level domains for container resolution (default "docker")
-ttl int
TTL in seconds for cache entries and DNS responses (default 300)
-resolvers string
Comma-separated fallback DNS resolver IPs (default "8.8.8.8,1.1.1.1,8.8.4.4")
-forward-timeout duration
Per-resolver timeout for forwarded DNS queries (default 2s)
Usage of docker-dns:
-ip string
IP address the DNS server listens on (default "127.0.0.153")
-tld string
Comma-separated managed top-level domains for container resolution (default "docker")
-ttl int
TTL in seconds for cache entries and DNS responses (default 300)
-resolvers string
Comma-separated fallback DNS resolver IPs (default "8.8.8.8,1.1.1.1,8.8.4.4")
-forward-timeout duration
Per-resolver timeout for forwarded DNS queries (default 2s)
-rate-limit float
Max queries/sec per client IP; 0 disables rate limiting (default 100)
-rate-burst int
Burst allowance for per-IP rate limiting (default 50)
-max-cache-size int
Max DNS cache entries; 0 = unlimited (default 10000)
-http-addr string
Address for the health/metrics HTTP server; empty to disable (default ":8080")
-docker-timeout duration
Timeout for Docker API calls (default 5s)
-docker-host string
Docker host override (empty = use DOCKER_HOST env / socket default)
-log-level string
Log level: debug | info | warn | error (default "info")
```
- P.S: `sudo` (or `root`) is required as the server will be listening on port `53`, which is
a [previewed port](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html)
Expand Down
52 changes: 44 additions & 8 deletions build/dpkg/DEBIAN/postinst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ configure)
done
ROUTING_DOMAINS=$(echo "$ROUTING_DOMAINS" | xargs) # trim leading space

# Prepend `nameserver $CUSTOM_IP` to /etc/resolv.conf (idempotent).
prepend_resolv_conf() {
grep -q "$CUSTOM_IP" /etc/resolv.conf 2>/dev/null && \
echo "detected an entry of $CUSTOM_IP in /etc/resolv.conf, removing.." && \
grep -v "$CUSTOM_IP" /etc/resolv.conf > /tmp/resolv.tmp && \
cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
{ printf '# Generated by docker-dns\nnameserver %s\n' "$CUSTOM_IP"; cat /etc/resolv.conf; } > /tmp/resolv.tmp \
&& cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
}

if systemctl is-active systemd-resolved.service >/dev/null 2>&1; then
echo "Detected systemd-resolved running. Extending its configuration.."

Expand All @@ -72,15 +82,41 @@ Domains=$ROUTING_DOMAINS
EOF
systemctl restart systemd-resolved.service
echo "systemd-resolved extended with DNS=$CUSTOM_IP and Domains=$ROUTING_DOMAINS."
elif systemctl is-active NetworkManager.service >/dev/null 2>&1; then
# NetworkManager active without systemd-resolved (Debian Desktop).
# NM rewrites /etc/resolv.conf on every connection event and would clobber
# a one-shot prepend. Install a dispatcher script so NM re-prepends for us
# after every up/down/dhcp event, and prepend once immediately so DNS
# works between now and the next NM event.
echo "Detected NetworkManager without systemd-resolved. Installing dispatcher.."

mkdir -p /etc/NetworkManager/dispatcher.d
cat <<EOF >/etc/NetworkManager/dispatcher.d/docker-dns
#!/bin/bash
# Re-prepend docker-dns nameserver after NetworkManager rewrites /etc/resolv.conf.
# NM invokes dispatchers with "\$1 = interface" and "\$2 = action".
# up/dhcp*-change: NM has just written resolv.conf with connection-provided DNS.
# down: NM rewrites resolv.conf to drop the departing interface's DNS servers.
case "\$2" in
up|down|dhcp4-change|dhcp6-change)
if ! grep -q '$CUSTOM_IP' /etc/resolv.conf 2>/dev/null; then
{ printf '# Generated by docker-dns\nnameserver %s\n' '$CUSTOM_IP'
cat /etc/resolv.conf
} > /tmp/resolv.docker-dns.tmp \\
&& cat /tmp/resolv.docker-dns.tmp > /etc/resolv.conf \\
&& rm -f /tmp/resolv.docker-dns.tmp
fi
;;
esac
EOF
chmod 0755 /etc/NetworkManager/dispatcher.d/docker-dns
chown root:root /etc/NetworkManager/dispatcher.d/docker-dns

prepend_resolv_conf
echo "NetworkManager dispatcher installed; /etc/resolv.conf prepended with nameserver $CUSTOM_IP."
else
echo "systemd-resolved is not active. Updating /etc/resolv.conf.."
grep -q "$CUSTOM_IP" /etc/resolv.conf 2>/dev/null && \
echo "detected an entry of $CUSTOM_IP in /etc/resolv.conf, removing.." && \
grep -v "$CUSTOM_IP" /etc/resolv.conf > /tmp/resolv.tmp && \
cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
# Prepend so docker-dns is queried before other nameservers
{ printf '# Generated by docker-dns\nnameserver %s\n' "$CUSTOM_IP"; cat /etc/resolv.conf; } > /tmp/resolv.tmp \
&& cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
echo "Neither systemd-resolved nor NetworkManager is active. Updating /etc/resolv.conf.."
prepend_resolv_conf
echo "/etc/resolv.conf updated with nameserver $CUSTOM_IP."
fi
;;
Expand Down
4 changes: 4 additions & 0 deletions build/dpkg/DEBIAN/postrm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ remove|purge)
rm -f /etc/unbound/unbound.conf.d/docker-dns.conf
systemctl restart unbound 2>/dev/null || true

# NetworkManager dispatcher — remove BEFORE restarting NM so a restart event
# cannot fire the dispatcher and re-add the nameserver to resolv.conf.
rm -f /etc/NetworkManager/dispatcher.d/docker-dns

# NetworkManager dnsmasq
rm -f /etc/NetworkManager/dnsmasq.d/docker-dns.conf
systemctl restart NetworkManager 2>/dev/null || true
Expand Down
96 changes: 74 additions & 22 deletions docs/Systemd.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,31 +86,69 @@ The `~` prefix on `docker` makes it a routing domain, not a search domain. This

This is the same mechanism used by VPNs, LXD, and corporate split-DNS setups.

### On systems without systemd-resolved (Debian default)
### On systems with NetworkManager (Debian Desktop)

The postinst appends docker-dns as a nameserver to `/etc/resolv.conf`:
On Debian Desktop, NetworkManager is active but systemd-resolved is not installed. NM runs with `dns=default`, meaning
it writes `/etc/resolv.conf` directly on every connection event, DHCP renewal, or service restart. A one-shot prepend
to `/etc/resolv.conf` would be overwritten the next time NM touches the file.

The postinst detects this (NM active, resolved not active) and installs a dispatcher script:

```bash
# /etc/NetworkManager/dispatcher.d/docker-dns
```

The dispatcher re-prepends `nameserver 127.0.0.153` to `/etc/resolv.conf` after every NM connection event (`up`,
`down`, `dhcp4-change`, `dhcp6-change`). The postinst also does an immediate one-shot prepend so DNS works between
install and the next NM event.

The postrm removes the dispatcher script and cleans up `/etc/resolv.conf` on uninstall.

### On systems without systemd-resolved or NetworkManager (Debian Server)

On minimal or server installs where neither systemd-resolved nor NetworkManager is active, the postinst prepends
docker-dns as a nameserver to `/etc/resolv.conf`:

```
# Generated by docker-dns
nameserver 127.0.0.153
```

Since docker-dns handles `.docker` queries itself and forwards everything else to its configured upstream resolvers (
default: 8.8.8.8, 1.1.1.1, 8.8.4.4), this works for both container resolution and normal DNS.
Since docker-dns handles `.docker` queries itself and forwards everything else to its configured upstream resolvers
(default: 8.8.8.8, 1.1.1.1, 8.8.4.4), this works for both container resolution and normal DNS.

The postrm removes this entry on uninstall using the `# Generated by docker-dns` comment as a marker.

### On systems with other resolvers
### Custom DNS Resolvers (Manual Configuration)

The postinst detects the active resolver and writes the appropriate configuration:
docker-dns's automatic integration covers the default DNS stack on all supported Ubuntu and Debian versions (server and
desktop). If you run a custom local resolver (dnsmasq, unbound, Pi-hole, or NM with `dns=dnsmasq`), you already know
your DNS stack — add the forwarding rule manually.

| Resolver | Config file | Directive |
|-------------------------------|-------------------------------------------------|-------------------------------------|
| dnsmasq | `/etc/dnsmasq.d/docker-dns.conf` | `server=/docker/127.0.0.153` |
| unbound | `/etc/unbound/unbound.conf.d/docker-dns.conf` | `stub-zone` pointing to 127.0.0.153 |
| NetworkManager (dnsmasq mode) | `/etc/NetworkManager/dnsmasq.d/docker-dns.conf` | `server=/docker/127.0.0.153` |
**dnsmasq** (standalone or Pi-hole):
```
# /etc/dnsmasq.d/docker-dns.conf
server=/docker/127.0.0.153
```
Then: `sudo systemctl restart dnsmasq`

**unbound**:
```yaml
# /etc/unbound/unbound.conf.d/docker-dns.conf
stub-zone:
name: "docker"
stub-addr: 127.0.0.153
```
Then: `sudo systemctl restart unbound`

All of these are cleaned up by the postrm on uninstall.
**NetworkManager with dns=dnsmasq**:
```
# /etc/NetworkManager/dnsmasq.d/docker-dns.conf
server=/docker/127.0.0.153
```
Then: `sudo systemctl restart NetworkManager`

Note: the postrm does not clean up manually-created configs. If you uninstall docker-dns, remove these files yourself.

---

Expand Down Expand Up @@ -187,12 +225,18 @@ systemctl restart dnsmasq 2>/dev/null || true
rm -f /etc/unbound/unbound.conf.d/docker-dns.conf
systemctl restart unbound 2>/dev/null || true

# NetworkManager dispatcher — removed before NM restart so a restart event
# cannot fire the dispatcher and re-add the nameserver to resolv.conf
rm -f /etc/NetworkManager/dispatcher.d/docker-dns

# NetworkManager dnsmasq
rm -f /etc/NetworkManager/dnsmasq.d/docker-dns.conf
systemctl restart NetworkManager 2>/dev/null || true

# Plain resolv.conf: remove the appended entry
sed -i '/# Generated by docker-dns/,+1d' /etc/resolv.conf
# Plain resolv.conf: remove the prepended entry
awk '/# Generated by docker-dns/{skip=1; next} skip{skip=0; next} 1' \
/etc/resolv.conf > /tmp/resolv.tmp \
&& cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
```

Each `systemctl restart` is guarded with `2>/dev/null || true` so it silently skips services that aren't installed.
Expand All @@ -205,14 +249,22 @@ The systemd integration tests (`make test-systemd OS=<codename>`) verify the ful
lifecycle on each target. Both the direct path (`dig @127.0.0.153`) and the system-level path (`dig` without `@`, going
through whatever the OS uses for DNS) are tested.

| OS | Version | DNS method | Assertions |
|----------|--------------|-----------------------------------|------------|
| Noble | Ubuntu 24.04 | systemd-resolved + routing domain | 13 |
| Jammy | Ubuntu 22.04 | systemd-resolved + routing domain | 13 |
| Focal | Ubuntu 20.04 | systemd-resolved + routing domain | 13 |
| Trixie | Debian 13 | plain resolv.conf | 12 |
| Bookworm | Debian 12 | plain resolv.conf | 12 |
| Bullseye | Debian 11 | plain resolv.conf | 12 |
| OS | Version | Variant | DNS Method | Assertions |
|----------|--------------|---------|-------------------------------------------------|------------|
| Noble | Ubuntu 24.04 | Server | systemd-resolved + routing domain | 13 |
| Noble | Ubuntu 24.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 |
| Jammy | Ubuntu 22.04 | Server | systemd-resolved + routing domain | 13 |
| Jammy | Ubuntu 22.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 |
| Focal | Ubuntu 20.04 | Server | systemd-resolved + routing domain | 13 |
| Focal | Ubuntu 20.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 |
| Trixie | Debian 13 | Server | plain resolv.conf | 12 |
| Trixie | Debian 13 | Desktop | NM dispatcher + resolv.conf | 15 |
| Bookworm | Debian 12 | Server | plain resolv.conf | 12 |
| Bookworm | Debian 12 | Desktop | NM dispatcher + resolv.conf | 15 |
| Bullseye | Debian 11 | Server | plain resolv.conf | 12 |
| Bullseye | Debian 11 | Desktop | NM dispatcher + resolv.conf | 15 |

Desktop variants additionally test that docker-dns resolution survives a full `systemctl restart NetworkManager` cycle.

Each test runs inside a privileged Docker container with systemd as PID 1, Docker-in-Docker for container resolution,
and full DNS chain verification including post-uninstall cleanup.
Expand Down
Loading
Loading