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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +27 -1
M internal/dnsfwd/dnsfwd.go +12 -1
| @@ -25,9 +25,11 @@ import ( | |||
| 25 | 25 | "strconv" | |
| 26 | 26 | "sync" | |
| 27 | 27 | "sync/atomic" | |
| 28 | + | "syscall" | |
| 28 | 29 | "time" | |
| 29 | 30 | ||
| 30 | 31 | "github.com/bnema/zerowrap" | |
| 32 | + | "golang.org/x/sys/unix" | |
| 31 | 33 | ) | |
| 32 | 34 | ||
| 33 | 35 | const ( | |
| @@ -93,7 +95,16 @@ func (f *Forwarder) Listen(ctx context.Context, addr netip.Addr) error { | |||
| 93 | 95 | return fmt.Errorf("dnsfwd: already listening on %s", addr) | |
| 94 | 96 | } | |
| 95 | 97 | 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 | + | }} | |
| 97 | 108 | pc, err := lc.ListenPacket(ctx, "udp", hp) | |
| 98 | 109 | if err != nil { | |
| 99 | 110 | 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.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 | + | }} | |||
| 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) { | |||
| 440 | 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 | + | } | |
| @@ -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 | + | } | |||