Skip to content

Commit ff10b20

Browse files
authored
Merge pull request #2 from MedUnes/feat/support-desktop
feat(support desktop)
2 parents 70e2d19 + 0cbf0d7 commit ff10b20

17 files changed

Lines changed: 711 additions & 118 deletions

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ jobs:
4646
strategy:
4747
fail-fast: false
4848
matrix:
49-
os: [noble, jammy, focal, trixie, bookworm, bullseye]
49+
os: [noble, jammy, focal, trixie, bookworm, bullseye,
50+
noble-desktop, jammy-desktop, focal-desktop,
51+
trixie-desktop, bookworm-desktop, bullseye-desktop]
5052
steps:
5153
- uses: actions/checkout@v4
5254

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,18 @@ test-systemd: build
1212
./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local
1313
./test/systemd/test-systemd.sh $(OS)
1414

15-
.PHONY: build test run test-systemd
15+
test-systemd-all: build
16+
@for os in noble jammy focal trixie bookworm bullseye noble-desktop jammy-desktop focal-desktop trixie-desktop bookworm-desktop bullseye-desktop; do \
17+
echo "=== Testing $$os ==="; \
18+
./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local && \
19+
./test/systemd/test-systemd.sh $$os || echo "FAILED: $$os"; \
20+
done
21+
22+
test-systemd-desktop-all: build
23+
@for os in noble-desktop jammy-desktop focal-desktop trixie-desktop bookworm-desktop bullseye-desktop; do \
24+
echo "=== Testing $$os ==="; \
25+
./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local && \
26+
./test/systemd/test-systemd.sh $$os || echo "FAILED: $$os"; \
27+
done
28+
29+
.PHONY: build test run test-systemd test-systemd-all test-systemd-desktop-all

README.md

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ between containers and the host.
1111

1212
## Features
1313

14-
- **Automatic DNS Resolution**: Resolve Docker container names with a custom TLD to their respective IP addresses.
15-
- **Caching Mechanism**: Configurable TTL for DNS cache entries to improve performance.
16-
- **Simple Configuration**: Minimal setup with command-line options or a configuration file.
17-
- **Debian Package**: Easy-to-install `.deb` package for seamless integration.
18-
- **Fallback DNS**: Forwards non-Docker queries to configurable DNS servers.
19-
- **Lightweight and Fast**: Built with Go for high performance.
14+
- **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.
15+
- **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.
16+
- **Caching**: TTL-based DNS cache with background eviction, size limits, and hit/miss telemetry.
17+
- **Rate Limiting**: Per-IP token-bucket rate limiter with automatic idle cleanup.
18+
- **Health & Metrics**: HTTP server on `:8080` exposes `/health` and `/metrics` (cache stats, query counts, error rates).
19+
- **UDP + TCP**: Full DNS protocol support with EDNS0 handling and proper truncation.
20+
- **Debian Package**: `.deb` package with automatic systemd integration and clean uninstall.
21+
- **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.
22+
- **Lightweight**: Single Go binary, minimal resource footprint.
2023

2124
## Architecture
2225

@@ -146,23 +149,18 @@ integrates with DNS resolution for each ditro/version..
146149
147150
---
148151
149-
## Important Notes
152+
## DNS Integration
150153
151-
### Default Resolver Integration
154+
The `.deb` package auto-detects your system's DNS resolver and integrates accordingly:
152155

153-
- 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
154-
queries:
155-
```bash
156-
dig mycontainer.docker @127.0.0.153 +short
157-
```
158-
- However, making Docker DNS the system-wide default resolver requires additional configuration or hacks.
156+
- **Ubuntu** (systemd-resolved): routing domain drop-in at `/etc/systemd/resolved.conf.d/docker-dns.conf` — only
157+
`.docker` queries go to docker-dns, everything else is untouched
158+
- **Debian Desktop** (NetworkManager): dispatcher script at `/etc/NetworkManager/dispatcher.d/docker-dns` re-prepends
159+
the nameserver after every NM event
160+
- **Debian Server** (plain resolv.conf): prepends `nameserver 127.0.0.153` to `/etc/resolv.conf`
159161

160-
### Systemd-Resolved Compatibility: Direct integration with
161-
`systemd-resolved` is non-trivial and not recommended without advanced setup.
162-
163-
### Protocol
164-
165-
* `docker-dns` is configured to run on `UDP` mode only, so `TCP` requests won't be answered
162+
See [docs/Systemd.md](./docs/Systemd.md) for the full details, browser DNS-over-HTTPS caveats, and manual integration
163+
examples for custom resolvers (dnsmasq, unbound, Pi-hole).
166164

167165
---
168166

@@ -192,17 +190,31 @@ integrates with DNS resolution for each ditro/version..
192190
- Once you have the binary generated, you can run at as follows (linux):
193191
```bash
194192
sudo ./docker-dns -h
195-
Usage of docker-dns:
196-
-ip string
197-
IP address the DNS server listens on (default "127.0.0.153")
198-
-tld string
199-
Comma-separated managed top-level domains for container resolution (default "docker")
200-
-ttl int
201-
TTL in seconds for cache entries and DNS responses (default 300)
202-
-resolvers string
203-
Comma-separated fallback DNS resolver IPs (default "8.8.8.8,1.1.1.1,8.8.4.4")
204-
-forward-timeout duration
205-
Per-resolver timeout for forwarded DNS queries (default 2s)
193+
Usage of docker-dns:
194+
-ip string
195+
IP address the DNS server listens on (default "127.0.0.153")
196+
-tld string
197+
Comma-separated managed top-level domains for container resolution (default "docker")
198+
-ttl int
199+
TTL in seconds for cache entries and DNS responses (default 300)
200+
-resolvers string
201+
Comma-separated fallback DNS resolver IPs (default "8.8.8.8,1.1.1.1,8.8.4.4")
202+
-forward-timeout duration
203+
Per-resolver timeout for forwarded DNS queries (default 2s)
204+
-rate-limit float
205+
Max queries/sec per client IP; 0 disables rate limiting (default 100)
206+
-rate-burst int
207+
Burst allowance for per-IP rate limiting (default 50)
208+
-max-cache-size int
209+
Max DNS cache entries; 0 = unlimited (default 10000)
210+
-http-addr string
211+
Address for the health/metrics HTTP server; empty to disable (default ":8080")
212+
-docker-timeout duration
213+
Timeout for Docker API calls (default 5s)
214+
-docker-host string
215+
Docker host override (empty = use DOCKER_HOST env / socket default)
216+
-log-level string
217+
Log level: debug | info | warn | error (default "info")
206218
```
207219
- P.S: `sudo` (or `root`) is required as the server will be listening on port `53`, which is
208220
a [previewed port](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html)

build/dpkg/DEBIAN/postinst

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ configure)
5757
done
5858
ROUTING_DOMAINS=$(echo "$ROUTING_DOMAINS" | xargs) # trim leading space
5959

60+
# Prepend `nameserver $CUSTOM_IP` to /etc/resolv.conf (idempotent).
61+
prepend_resolv_conf() {
62+
grep -q "$CUSTOM_IP" /etc/resolv.conf 2>/dev/null && \
63+
echo "detected an entry of $CUSTOM_IP in /etc/resolv.conf, removing.." && \
64+
grep -v "$CUSTOM_IP" /etc/resolv.conf > /tmp/resolv.tmp && \
65+
cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
66+
{ printf '# Generated by docker-dns\nnameserver %s\n' "$CUSTOM_IP"; cat /etc/resolv.conf; } > /tmp/resolv.tmp \
67+
&& cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
68+
}
69+
6070
if systemctl is-active systemd-resolved.service >/dev/null 2>&1; then
6171
echo "Detected systemd-resolved running. Extending its configuration.."
6272

@@ -72,15 +82,41 @@ Domains=$ROUTING_DOMAINS
7282
EOF
7383
systemctl restart systemd-resolved.service
7484
echo "systemd-resolved extended with DNS=$CUSTOM_IP and Domains=$ROUTING_DOMAINS."
85+
elif systemctl is-active NetworkManager.service >/dev/null 2>&1; then
86+
# NetworkManager active without systemd-resolved (Debian Desktop).
87+
# NM rewrites /etc/resolv.conf on every connection event and would clobber
88+
# a one-shot prepend. Install a dispatcher script so NM re-prepends for us
89+
# after every up/down/dhcp event, and prepend once immediately so DNS
90+
# works between now and the next NM event.
91+
echo "Detected NetworkManager without systemd-resolved. Installing dispatcher.."
92+
93+
mkdir -p /etc/NetworkManager/dispatcher.d
94+
cat <<EOF >/etc/NetworkManager/dispatcher.d/docker-dns
95+
#!/bin/bash
96+
# Re-prepend docker-dns nameserver after NetworkManager rewrites /etc/resolv.conf.
97+
# NM invokes dispatchers with "\$1 = interface" and "\$2 = action".
98+
# up/dhcp*-change: NM has just written resolv.conf with connection-provided DNS.
99+
# down: NM rewrites resolv.conf to drop the departing interface's DNS servers.
100+
case "\$2" in
101+
up|down|dhcp4-change|dhcp6-change)
102+
if ! grep -q '$CUSTOM_IP' /etc/resolv.conf 2>/dev/null; then
103+
{ printf '# Generated by docker-dns\nnameserver %s\n' '$CUSTOM_IP'
104+
cat /etc/resolv.conf
105+
} > /tmp/resolv.docker-dns.tmp \\
106+
&& cat /tmp/resolv.docker-dns.tmp > /etc/resolv.conf \\
107+
&& rm -f /tmp/resolv.docker-dns.tmp
108+
fi
109+
;;
110+
esac
111+
EOF
112+
chmod 0755 /etc/NetworkManager/dispatcher.d/docker-dns
113+
chown root:root /etc/NetworkManager/dispatcher.d/docker-dns
114+
115+
prepend_resolv_conf
116+
echo "NetworkManager dispatcher installed; /etc/resolv.conf prepended with nameserver $CUSTOM_IP."
75117
else
76-
echo "systemd-resolved is not active. Updating /etc/resolv.conf.."
77-
grep -q "$CUSTOM_IP" /etc/resolv.conf 2>/dev/null && \
78-
echo "detected an entry of $CUSTOM_IP in /etc/resolv.conf, removing.." && \
79-
grep -v "$CUSTOM_IP" /etc/resolv.conf > /tmp/resolv.tmp && \
80-
cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
81-
# Prepend so docker-dns is queried before other nameservers
82-
{ printf '# Generated by docker-dns\nnameserver %s\n' "$CUSTOM_IP"; cat /etc/resolv.conf; } > /tmp/resolv.tmp \
83-
&& cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp
118+
echo "Neither systemd-resolved nor NetworkManager is active. Updating /etc/resolv.conf.."
119+
prepend_resolv_conf
84120
echo "/etc/resolv.conf updated with nameserver $CUSTOM_IP."
85121
fi
86122
;;

build/dpkg/DEBIAN/postrm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ remove|purge)
2222
rm -f /etc/unbound/unbound.conf.d/docker-dns.conf
2323
systemctl restart unbound 2>/dev/null || true
2424

25+
# NetworkManager dispatcher — remove BEFORE restarting NM so a restart event
26+
# cannot fire the dispatcher and re-add the nameserver to resolv.conf.
27+
rm -f /etc/NetworkManager/dispatcher.d/docker-dns
28+
2529
# NetworkManager dnsmasq
2630
rm -f /etc/NetworkManager/dnsmasq.d/docker-dns.conf
2731
systemctl restart NetworkManager 2>/dev/null || true

docs/Systemd.md

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -86,31 +86,69 @@ The `~` prefix on `docker` makes it a routing domain, not a search domain. This
8686

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

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

91-
The postinst appends docker-dns as a nameserver to `/etc/resolv.conf`:
91+
On Debian Desktop, NetworkManager is active but systemd-resolved is not installed. NM runs with `dns=default`, meaning
92+
it writes `/etc/resolv.conf` directly on every connection event, DHCP renewal, or service restart. A one-shot prepend
93+
to `/etc/resolv.conf` would be overwritten the next time NM touches the file.
94+
95+
The postinst detects this (NM active, resolved not active) and installs a dispatcher script:
96+
97+
```bash
98+
# /etc/NetworkManager/dispatcher.d/docker-dns
99+
```
100+
101+
The dispatcher re-prepends `nameserver 127.0.0.153` to `/etc/resolv.conf` after every NM connection event (`up`,
102+
`down`, `dhcp4-change`, `dhcp6-change`). The postinst also does an immediate one-shot prepend so DNS works between
103+
install and the next NM event.
104+
105+
The postrm removes the dispatcher script and cleans up `/etc/resolv.conf` on uninstall.
106+
107+
### On systems without systemd-resolved or NetworkManager (Debian Server)
108+
109+
On minimal or server installs where neither systemd-resolved nor NetworkManager is active, the postinst prepends
110+
docker-dns as a nameserver to `/etc/resolv.conf`:
92111

93112
```
94113
# Generated by docker-dns
95114
nameserver 127.0.0.153
96115
```
97116

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

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

103-
### On systems with other resolvers
122+
### Custom DNS Resolvers (Manual Configuration)
104123

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

107-
| Resolver | Config file | Directive |
108-
|-------------------------------|-------------------------------------------------|-------------------------------------|
109-
| dnsmasq | `/etc/dnsmasq.d/docker-dns.conf` | `server=/docker/127.0.0.153` |
110-
| unbound | `/etc/unbound/unbound.conf.d/docker-dns.conf` | `stub-zone` pointing to 127.0.0.153 |
111-
| NetworkManager (dnsmasq mode) | `/etc/NetworkManager/dnsmasq.d/docker-dns.conf` | `server=/docker/127.0.0.153` |
128+
**dnsmasq** (standalone or Pi-hole):
129+
```
130+
# /etc/dnsmasq.d/docker-dns.conf
131+
server=/docker/127.0.0.153
132+
```
133+
Then: `sudo systemctl restart dnsmasq`
134+
135+
**unbound**:
136+
```yaml
137+
# /etc/unbound/unbound.conf.d/docker-dns.conf
138+
stub-zone:
139+
name: "docker"
140+
stub-addr: 127.0.0.153
141+
```
142+
Then: `sudo systemctl restart unbound`
112143

113-
All of these are cleaned up by the postrm on uninstall.
144+
**NetworkManager with dns=dnsmasq**:
145+
```
146+
# /etc/NetworkManager/dnsmasq.d/docker-dns.conf
147+
server=/docker/127.0.0.153
148+
```
149+
Then: `sudo systemctl restart NetworkManager`
150+
151+
Note: the postrm does not clean up manually-created configs. If you uninstall docker-dns, remove these files yourself.
114152
115153
---
116154
@@ -187,12 +225,18 @@ systemctl restart dnsmasq 2>/dev/null || true
187225
rm -f /etc/unbound/unbound.conf.d/docker-dns.conf
188226
systemctl restart unbound 2>/dev/null || true
189227

228+
# NetworkManager dispatcher — removed before NM restart so a restart event
229+
# cannot fire the dispatcher and re-add the nameserver to resolv.conf
230+
rm -f /etc/NetworkManager/dispatcher.d/docker-dns
231+
190232
# NetworkManager dnsmasq
191233
rm -f /etc/NetworkManager/dnsmasq.d/docker-dns.conf
192234
systemctl restart NetworkManager 2>/dev/null || true
193235

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

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

208-
| OS | Version | DNS method | Assertions |
209-
|----------|--------------|-----------------------------------|------------|
210-
| Noble | Ubuntu 24.04 | systemd-resolved + routing domain | 13 |
211-
| Jammy | Ubuntu 22.04 | systemd-resolved + routing domain | 13 |
212-
| Focal | Ubuntu 20.04 | systemd-resolved + routing domain | 13 |
213-
| Trixie | Debian 13 | plain resolv.conf | 12 |
214-
| Bookworm | Debian 12 | plain resolv.conf | 12 |
215-
| Bullseye | Debian 11 | plain resolv.conf | 12 |
252+
| OS | Version | Variant | DNS Method | Assertions |
253+
|----------|--------------|---------|-------------------------------------------------|------------|
254+
| Noble | Ubuntu 24.04 | Server | systemd-resolved + routing domain | 13 |
255+
| Noble | Ubuntu 24.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 |
256+
| Jammy | Ubuntu 22.04 | Server | systemd-resolved + routing domain | 13 |
257+
| Jammy | Ubuntu 22.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 |
258+
| Focal | Ubuntu 20.04 | Server | systemd-resolved + routing domain | 13 |
259+
| Focal | Ubuntu 20.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 |
260+
| Trixie | Debian 13 | Server | plain resolv.conf | 12 |
261+
| Trixie | Debian 13 | Desktop | NM dispatcher + resolv.conf | 15 |
262+
| Bookworm | Debian 12 | Server | plain resolv.conf | 12 |
263+
| Bookworm | Debian 12 | Desktop | NM dispatcher + resolv.conf | 15 |
264+
| Bullseye | Debian 11 | Server | plain resolv.conf | 12 |
265+
| Bullseye | Debian 11 | Desktop | NM dispatcher + resolv.conf | 15 |
266+
267+
Desktop variants additionally test that docker-dns resolution survives a full `systemctl restart NetworkManager` cycle.
216268

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

0 commit comments

Comments
 (0)