detached 83e4171

fix(dnsfwd): bind the gateway address with IP_FREEBIND

Docker configures the bridge address when the network is created, podman
(netavark) only when the first container starts, which is after the policy
and the DNS listener. Listening on a not-yet-configured address failed with
'cannot assign requested address'. Free-bind lets the listener come first;
the address appears on the bridge when the container starts.
commit
83e4171f425d0ba2d3a157f438569c02eacebee1
author
brice · · 2026-10-12 21:34 UTC
parent
35b40b2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +27 -1
M internal/dnsfwd/dnsfwd.go +12 -1
@@ -25,9 +25,11 @@ import (
2525 "strconv"
2626 "sync"
2727 "sync/atomic"
28+ "syscall"
2829 "time"
2930
3031 "github.com/bnema/zerowrap"
32+ "golang.org/x/sys/unix"
3133 )
3234
3335 const (
@@ -93,7 +95,16 @@ func (f *Forwarder) Listen(ctx context.Context, addr netip.Addr) error {
9395 return fmt.Errorf("dnsfwd: already listening on %s", addr)
9496 }
9597 hp := net.JoinHostPort(addr.String(), strconv.Itoa(f.port))
96- var lc net.ListenConfig
98+ // IP_FREEBIND: the gateway address may not exist yet. Docker creates the
99+ // bridge (and its address) with the network, but podman's netavark only when
100+ // the first container starts, which is after the policy and this listener.
101+ lc := net.ListenConfig{Control: func(_, _ string, c syscall.RawConn) error {
102+ var serr error
103+ if err := c.Control(func(fd uintptr) { serr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_FREEBIND, 1) }); err != nil {
104+ return err
105+ }
106+ return serr
107+ }}
97108 pc, err := lc.ListenPacket(ctx, "udp", hp)
98109 if err != nil {
99110 return fmt.Errorf("dnsfwd: listen udp %s: %w", hp, err)
@@ -25,9 +25,11 @@ import (
25 "strconv"25 "strconv"
26 "sync"26 "sync"
27 "sync/atomic"27 "sync/atomic"
28+ "syscall"
28 "time"29 "time"
29 30
30 "github.com/bnema/zerowrap"31 "github.com/bnema/zerowrap"
32+ "golang.org/x/sys/unix"
31 )33 )
32 34
33 const (35 const (
@@ -93,7 +95,16 @@ func (f *Forwarder) Listen(ctx context.Context, addr netip.Addr) error {
93 return fmt.Errorf("dnsfwd: already listening on %s", addr)95 return fmt.Errorf("dnsfwd: already listening on %s", addr)
94 }96 }
95 hp := net.JoinHostPort(addr.String(), strconv.Itoa(f.port))97 hp := net.JoinHostPort(addr.String(), strconv.Itoa(f.port))
96- var lc net.ListenConfig98+ // IP_FREEBIND: the gateway address may not exist yet. Docker creates the
99+ // bridge (and its address) with the network, but podman's netavark only when
100+ // the first container starts, which is after the policy and this listener.
101+ lc := net.ListenConfig{Control: func(_, _ string, c syscall.RawConn) error {
102+ var serr error
103+ if err := c.Control(func(fd uintptr) { serr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_FREEBIND, 1) }); err != nil {
104+ return err
105+ }
106+ return serr
107+ }}
97 pc, err := lc.ListenPacket(ctx, "udp", hp)108 pc, err := lc.ListenPacket(ctx, "udp", hp)
98 if err != nil {109 if err != nil {
99 return fmt.Errorf("dnsfwd: listen udp %s: %w", hp, err)110 return fmt.Errorf("dnsfwd: listen udp %s: %w", hp, err)
M internal/dnsfwd/dnsfwd_test.go +15 -0
@@ -440,3 +440,18 @@ func TestUDPSaturationDropsAndIsolatesListeners(t *testing.T) {
440440 t.Fatalf("another listener must not be starved by the flood: n=%d err=%v", n, err)
441441 }
442442 }
443+
444+// The gateway address can be configured after the listener starts (podman's
445+// netavark creates the bridge address only when the first container starts):
446+// Listen must bind an address the host does not own yet (IP_FREEBIND).
447+func TestListenOnAnAddressTheHostDoesNotOwnYet(t *testing.T) {
448+ f := New(func() []string { return nil }, 0)
449+ t.Cleanup(f.CloseAll)
450+ ip := netip.MustParseAddr("203.0.113.77") // TEST-NET-3: never configured here
451+ if err := f.Listen(context.Background(), ip); err != nil {
452+ t.Fatalf("listen on a not-yet-configured address: %v", err)
453+ }
454+ if _, ok := f.Addr(ip); !ok {
455+ t.Fatal("no listener")
456+ }
457+}
@@ -440,3 +440,18 @@ func TestUDPSaturationDropsAndIsolatesListeners(t *testing.T) {
440 t.Fatalf("another listener must not be starved by the flood: n=%d err=%v", n, err)440 t.Fatalf("another listener must not be starved by the flood: n=%d err=%v", n, err)
441 }441 }
442 }442 }
443+
444+// The gateway address can be configured after the listener starts (podman's
445+// netavark creates the bridge address only when the first container starts):
446+// Listen must bind an address the host does not own yet (IP_FREEBIND).
447+func TestListenOnAnAddressTheHostDoesNotOwnYet(t *testing.T) {
448+ f := New(func() []string { return nil }, 0)
449+ t.Cleanup(f.CloseAll)
450+ ip := netip.MustParseAddr("203.0.113.77") // TEST-NET-3: never configured here
451+ if err := f.Listen(context.Background(), ip); err != nil {
452+ t.Fatalf("listen on a not-yet-configured address: %v", err)
453+ }
454+ if _, ok := f.Addr(ip); !ok {
455+ t.Fatal("no listener")
456+ }
457+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close