WireGuard Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Mukul Sabharwal <mjsabby@gmail.com>
To: wireguard@lists.zx2c4.com
Cc: Jason@zx2c4.com, Mukul Sabharwal <mjsabby@gmail.com>
Subject: [PATCH wireguard-go] conn: allow StdNetBind to bind to specific host addresses
Date: Sun, 10 May 2026 19:58:10 +0000	[thread overview]
Message-ID: <20260510195810.192447-1-mjsabby@gmail.com> (raw)

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 <mjsabby@gmail.com>
---
 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


                 reply	other threads:[~2026-05-10 20:10 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260510195810.192447-1-mjsabby@gmail.com \
    --to=mjsabby@gmail.com \
    --cc=Jason@zx2c4.com \
    --cc=wireguard@lists.zx2c4.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox