Skip to content

feat(server): opt-in IP_FREEBIND for DNS listeners - #2078

Merged
0xERR0R merged 1 commit into
mainfrom
freebind
Jun 2, 2026
Merged

0xERR0R merged 1 commit into
mainfrom
freebind

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Resolves #2026.

What & why

Blocky exits at startup with EADDRNOTAVAIL when configured to listen on an IP that isn't assigned to an interface yet (Tailscale/WireGuard tunnel IPs, Keepalived/VRRP floating IPs, overlay networks brought up after boot):

listen udp 100.64.0.4:53: bind: cannot assign requested address

This adds an opt-in ports.freeBind flag that sets the Linux IP_FREEBIND (and IPV6_FREEBIND) socket option on the DNS and DoT listeners, so the bind succeeds immediately and traffic flows once the address appears — no restart, and no system-wide ip_nonlocal_bind sysctl.

ports:
  dns: 100.64.0.4:53
  tls: 100.64.0.4:853
  freeBind: true   # Linux only; default false

Design

  • Opt-in, default false → existing deployments are unchanged. No effect on wildcard binds; requires no extra capability (unlike IP_TRANSPARENT).
  • Conditional wiring (no risk to the default path): when freeBind is off (or non-Linux), DNS servers start via ListenAndServe exactly as before. When on + Linux, the UDP/TCP/DoT sockets are pre-created with a net.ListenConfig{Control} hook and served via ActivateAndServe. Parity with miekg/dns (UDPSize, UDP sockopts, DoT TLS wrap, Shutdown) was verified against v1.1.72.
  • Cross-platform: new build-tagged server/freebind package — freebind_linux.go sets the sockopt; freebind_others.go is a no-op with Supported = false. Non-Linux logs a warning and binds normally.
  • Scope: DNS-only (plain DNS + DoT). HTTP/HTTPS/HTTP3 are out of scope (usually localhost/wildcard).

Prior art checked: dnsmasq solves this via a netlink-watching bind-dynamic strategy (heavier); AdGuard Home has no in-app option (users resort to the system-wide sysctl). A targeted opt-in setsockopt is lighter and more precise.

Testing

  • server/freebind unit test (Linux): binds to 192.0.2.1 (TEST-NET-1, never assigned) — fails without the hook, succeeds with it, for TCP and UDP. Hermetic, no privilege, no container. Self-skips if the environment already permits non-local binds.
  • server wiring test (Linux): freeBind:true pre-binds DNS to a not-yet-available address; freeBind:false does not pre-create listeners.
  • Config: default (false) + LogConfig output.
  • All Ginkgo/Gomega, per project convention.

Verified: Config + Server (71) + Freebind (3) suites pass; cross-compiles on darwin & windows; golangci-lint 0 issues; gofmt/go vet clean; config.schema.json regenerated (idempotent).

Add `ports.freeBind` (default false) which, on Linux, sets the IP_FREEBIND /
IPV6_FREEBIND socket option on the DNS and DoT listeners. This lets blocky bind
to addresses that are not yet assigned to an interface (e.g. Tailscale,
WireGuard, Keepalived/VRRP floating IPs) instead of failing at startup with
EADDRNOTAVAIL.

- new build-tagged `server/freebind` package: Control hook on Linux, no-op
  elsewhere (Supported=false)
- when enabled and supported, DNS/TCP/DoT listeners are pre-created with the
  freebind ListenConfig.Control hook and served via ActivateAndServe; the
  default path is unchanged (ListenAndServe)
- non-Linux builds log a warning and bind normally
- has no effect on wildcard binds; requires no extra capability

Closes #2026
Copilot AI review requested due to automatic review settings June 2, 2026 08:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in ports.freeBind configuration that enables the Linux IP_FREEBIND/IPV6_FREEBIND socket option on the DNS (UDP/TCP) and DoT listeners, allowing Blocky to bind to IPs that aren't yet assigned to an interface (e.g. Tailscale/WireGuard/VRRP). When the flag is off (or on non-Linux), the existing ListenAndServe path is unchanged; when on, sockets are pre-created with a net.ListenConfig.Control hook and started via ActivateAndServe.

Changes:

  • New build-tagged server/freebind package providing the Control hook and Supported flag (Linux real, others no-op).
  • server/server.go: pre-creates UDP/TCP/DoT listeners with the freebind hook when enabled and switches the start path to ActivateAndServe; warns on non-Linux.
  • Config additions (Ports.FreeBind, schema, docs, log output) and tests for both the freebind package and server wiring.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
server/server.go Threads context+freeBind into server creation, attaches pre-bound listeners, switches between ListenAndServe/ActivateAndServe.
server/freebind/freebind_linux.go Implements the Linux Control hook setting IP_FREEBIND/IPV6_FREEBIND.
server/freebind/freebind_others.go No-op stub for non-Linux platforms.
server/freebind/freebind_linux_test.go Verifies bind to TEST-NET-1 works with the hook.
server/freebind/freebind_suite_test.go Ginkgo suite registration for the freebind package.
server/server_freebind_linux_test.go Wiring test that pre-binding occurs only when FreeBind=true.
config/config.go Adds Ports.FreeBind field and updated LogConfig output.
config/ports_test.go, config/config_test.go Tests for default value and LogConfig output.
docs/configuration.md, docs/config.yml, docs/config.schema.json Documents and schemas the new option.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.75000% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.16%. Comparing base (c44017a) to head (5da5b19).

Files with missing lines Patch % Lines
server/server.go 66.66% 12 Missing and 4 partials ⚠️
server/freebind/freebind_linux.go 60.00% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2078      +/-   ##
==========================================
- Coverage   86.30%   86.16%   -0.15%     
==========================================
  Files         110      111       +1     
  Lines        8214     8260      +46     
==========================================
+ Hits         7089     7117      +28     
- Misses        916      929      +13     
- Partials      209      214       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@0xERR0R 0xERR0R added the 🔨 enhancement New feature or request label Jun 2, 2026
@0xERR0R 0xERR0R added this to the v0.31.0 milestone Jun 2, 2026
@0xERR0R
0xERR0R merged commit 5d806b6 into main Jun 2, 2026
23 of 24 checks passed
@0xERR0R
0xERR0R deleted the freebind branch June 2, 2026 10:41
dadezzz pushed a commit to dadezzz/infra_docker-compose that referenced this pull request Jun 7, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/0xerr0r/blocky](https://github.com/0xERR0R/blocky) | minor | `v0.30.0` → `v0.31.0` |

---

### Release Notes

<details>
<summary>0xERR0R/blocky (ghcr.io/0xerr0r/blocky)</summary>

### [`v0.31.0`](https://github.com/0xERR0R/blocky/releases/tag/v0.31.0)

[Compare Source](0xERR0R/blocky@v0.30.0...v0.31.0)

#### Changelog

##### Features

- [`132c42f`](0xERR0R/blocky@132c42f): feat(config): validate and log ports.dohPath ([#&#8203;2072](0xERR0R/blocky#2072)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`e43b5e5`](0xERR0R/blocky@e43b5e5): feat(querylog): add SQLite query log target ([#&#8203;2080](0xERR0R/blocky#2080)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`5d806b6`](0xERR0R/blocky@5d806b6): feat(server): opt-in IP\_FREEBIND for DNS listeners ([#&#8203;2078](0xERR0R/blocky#2078)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`f18e52b`](0xERR0R/blocky@f18e52b): feat: capability-aware image that runs under drop:ALL ([#&#8203;2071](0xERR0R/blocky#2071)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`bf19d6d`](0xERR0R/blocky@bf19d6d): feat: config JSON schema generation and schema-driven validation ([#&#8203;2066](0xERR0R/blocky#2066)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`99ae703`](0xERR0R/blocky@99ae703): feat: filter ipv6hint in HTTPS/SVCB RR when AAAA is filtered ([#&#8203;2070](0xERR0R/blocky#2070)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`c44017a`](0xERR0R/blocky@c44017a): feat: load sensitive config values from files ([#&#8203;2077](0xERR0R/blocky#2077)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`e6b41db`](0xERR0R/blocky@e6b41db): feat: per-client DNS rate limiting ([#&#8203;2063](0xERR0R/blocky#2063)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`f457ec9`](0xERR0R/blocky@f457ec9): feat: read bootstrap nameservers from a resolv.conf file (resolvFile) ([#&#8203;2068](0xERR0R/blocky#2068)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`fdcf351`](0xERR0R/blocky@fdcf351): feat: resolve client names from hosts file and custom DNS ([#&#8203;1964](0xERR0R/blocky#1964)) ([#&#8203;2069](0xERR0R/blocky#2069)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Bug fixes

- [`93cc238`](0xERR0R/blocky@93cc238): fix(caching): sync prefetched entries to Redis ([#&#8203;2079](0xERR0R/blocky#2079)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`69b0663`](0xERR0R/blocky@69b0663): fix(config): handle DNS stamp port on hostname field and bootstrap IPs ([#&#8203;2065](0xERR0R/blocky#2065)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`31f8df3`](0xERR0R/blocky@31f8df3): fix(querylog): build SQLite target only on modernc-supported platforms ([#&#8203;2082](0xERR0R/blocky#2082)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Build and dependencies

- [`a5cdeb2`](0xERR0R/blocky@a5cdeb2): build(deps): bump github.com/go-chi/chi/v5 from 5.2.5 to 5.3.0 ([#&#8203;2074](0xERR0R/blocky#2074)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`5a25f67`](0xERR0R/blocky@5a25f67): build(deps): bump github.com/mattn/go-colorable from 0.1.14 to 0.1.15 ([#&#8203;2075](0xERR0R/blocky#2075)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`1da222a`](0xERR0R/blocky@1da222a): build(deps): bump github.com/oapi-codegen/runtime from 1.4.0 to 1.4.1 ([#&#8203;2064](0xERR0R/blocky#2064)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`6ff1d13`](0xERR0R/blocky@6ff1d13): build(deps): bump golang.org/x/net from 0.54.0 to 0.55.0 ([#&#8203;2067](0xERR0R/blocky#2067)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`63f8d9a`](0xERR0R/blocky@63f8d9a): build: upgrade Go to 1.26.2 and golangci-lint to v2.12.2 ([#&#8203;2062](0xERR0R/blocky#2062)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Misc

- [`e2de6ae`](0xERR0R/blocky@e2de6ae): docs(redis): document unix socket support and add e2e test ([#&#8203;2076](0xERR0R/blocky#2076)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

***

#### ❤️ Support Blocky

Blocky is free and open source, developed in my spare time — no telemetry, no ads, no hidden
filtering. If it's useful to you, please consider supporting its continued development:

[GitHub Sponsors](https://github.com/sponsors/0xERR0R) ·
[thanks.dev](https://thanks.dev/u/gh/0xERR0R) ·
[Liberapay](https://liberapay.com/spx01) ·
[Ko-fi](https://ko-fi.com/0xerr0r) ·
[PayPal](https://paypal.me/spx01)

Thank you to everyone supporting Blocky! 🙏

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMDUuMyIsInVwZGF0ZWRJblZlciI6IjQzLjIwNS4zIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Set IP_FREEBIND on listener sockets for binding to not-yet-available addresses

2 participants