[PATCH wireguard-go] conn: allow StdNetBind to bind to specific host addresses
Mukul Sabharwal <[email protected]> Sun, 10 May 2026 19:58:10 +0000
| Newsgroups | com.zx2c4.lists.wireguard |
|---|---|
| Message-ID | <[email protected]> |
Currently StdNetBind opens its IPv4 and IPv6 sockets on the unspecified address (0.0.0.0 and [::]). Embedders that want the WireGuard listener restricted to a specific local address — for example an Android application that wants its inbound peers to reach it only over Wi-Fi and never over the cellular interface, or a host with multiple interfaces where listening on a single one is preferable for routing or security reasons — currently have to reimplement Bind from scratch just to substitute a different bind address, losing the recvmmsg / sendmmsg / GSO optimizations along the way. Add a NewStdNetBindWithBindHost(host4, host6 string) constructor that records optional per-family bind hosts on the StdNetBind. Open() passes them through to listenNet, which now uses net.JoinHostPort instead of the hardcoded ":port" wildcard. Empty strings preserve the existing any-address behavior, so NewStdNetBind() is unchanged for existing callers. Tests verify that an explicit loopback host pins both v4 and v6 listeners to loopback, and that the default constructor still binds to the unspecified address. Signed-off-by: Mukul Sabharwal <[email protected]> --- conn/bind_std.go | 35 ++++++++++++++++++++++++++++---- conn/bind_std_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/conn/bind_std.go b/conn/bind_std.go index f5c8816..9291b4b 100644 --- a/conn/bind_std.go +++ b/conn/bind_std.go @@ -46,10 +46,37 @@ type StdNetBind struct { blackhole4 bool blackhole6 bool + + // Optional bind hosts for the IPv4 and IPv6 listeners. An empty + // string preserves the historical behavior of binding to the + // unspecified address (0.0.0.0 / [::]). These are read in Open() + // under mu and not mutated thereafter. + bindHost4 string + bindHost6 string } func NewStdNetBind() Bind { + return newStdNetBind("", "") +} + +// NewStdNetBindWithBindHost returns a StdNetBind whose IPv4 / IPv6 listeners +// bind to the supplied hosts instead of the unspecified address. An empty +// string for either argument preserves the default any-address behavior for +// that family. This is useful on hosts with multiple addresses where the +// caller wants the WireGuard listener pinned to one of them — e.g. an +// Android app that wants its inbound peers to reach it only over Wi-Fi +// and never over the cellular interface. +// +// The hosts must parse as literal IP addresses (no DNS lookups are +// performed). They are not validated until Open() is called. +func NewStdNetBindWithBindHost(host4, host6 string) Bind { + return newStdNetBind(host4, host6) +} + +func newStdNetBind(host4, host6 string) Bind { return &StdNetBind{ + bindHost4: host4, + bindHost6: host6, udpAddrPool: sync.Pool{ New: func() any { return &net.UDPAddr{ @@ -119,8 +146,8 @@ func (e *StdNetEndpoint) DstToString() string { return e.AddrPort.String() } -func listenNet(network string, port int) (*net.UDPConn, int, error) { - conn, err := listenConfig().ListenPacket(context.Background(), network, ":"+strconv.Itoa(port)) +func listenNet(network, host string, port int) (*net.UDPConn, int, error) { + conn, err := listenConfig().ListenPacket(context.Background(), network, net.JoinHostPort(host, strconv.Itoa(port))) if err != nil { return nil, 0, err } @@ -156,13 +183,13 @@ again: var v4pc *ipv4.PacketConn var v6pc *ipv6.PacketConn - v4conn, port, err = listenNet("udp4", port) + v4conn, port, err = listenNet("udp4", s.bindHost4, port) if err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) { return nil, 0, err } // Listen on the same port as we're using for ipv4. - v6conn, port, err = listenNet("udp6", port) + v6conn, port, err = listenNet("udp6", s.bindHost6, port) if uport == 0 && errors.Is(err, syscall.EADDRINUSE) && tries < 100 { v4conn.Close() tries++ diff --git a/conn/bind_std_test.go b/conn/bind_std_test.go index 34a3c9a..e5d6650 100644 --- a/conn/bind_std_test.go +++ b/conn/bind_std_test.go @@ -27,6 +27,52 @@ func TestStdNetBindReceiveFuncAfterClose(t *testing.T) { } } +func TestStdNetBindWithBindHost(t *testing.T) { + bind := NewStdNetBindWithBindHost("127.0.0.1", "::1").(*StdNetBind) + if _, _, err := bind.Open(0); err != nil { + t.Fatal(err) + } + defer bind.Close() + if bind.ipv4 == nil { + t.Fatal("ipv4 listener not opened") + } + la4, ok := bind.ipv4.LocalAddr().(*net.UDPAddr) + if !ok { + t.Fatalf("ipv4 LocalAddr is not *net.UDPAddr: %T", bind.ipv4.LocalAddr()) + } + if !la4.IP.IsLoopback() { + t.Errorf("ipv4 listener bound to %v, want loopback", la4.IP) + } + if bind.ipv6 != nil { + la6, ok := bind.ipv6.LocalAddr().(*net.UDPAddr) + if !ok { + t.Fatalf("ipv6 LocalAddr is not *net.UDPAddr: %T", bind.ipv6.LocalAddr()) + } + if !la6.IP.IsLoopback() { + t.Errorf("ipv6 listener bound to %v, want loopback", la6.IP) + } + } +} + +func TestStdNetBindDefaultBindHost(t *testing.T) { + // Empty host strings must preserve the historical wildcard behavior. + bind := NewStdNetBind().(*StdNetBind) + if _, _, err := bind.Open(0); err != nil { + t.Fatal(err) + } + defer bind.Close() + if bind.ipv4 == nil { + t.Fatal("ipv4 listener not opened") + } + la4, ok := bind.ipv4.LocalAddr().(*net.UDPAddr) + if !ok { + t.Fatalf("ipv4 LocalAddr is not *net.UDPAddr: %T", bind.ipv4.LocalAddr()) + } + if !la4.IP.IsUnspecified() { + t.Errorf("ipv4 listener bound to %v, want unspecified", la4.IP) + } +} + func mockSetGSOSize(control *[]byte, gsoSize uint16) { *control = (*control)[:cap(*control)] binary.LittleEndian.PutUint16(*control, gsoSize) -- 2.53.0