[PR #180] feat: implement sticky sockets on FreeBSD #188

Open
opened 2026-10-13 21:04:25 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/amnezia-vpn/amneziawg-go/pull/180
Author: @MarceloMayo74
Created: 8/14/2026
Status: 🔄 Open

Base: master ← Head: freebsd-sticky-sockets


📝 Commits (1)

  • ec5368e conn: implement sticky sockets on FreeBSD

📊 Changes

4 files changed (+456 additions, -3 deletions)

View changed files

➕ conn/controlfns_freebsd.go (+38 -0)
📝 conn/sticky_default.go (+3 -3)
➕ conn/sticky_freebsd.go (+121 -0)
➕ conn/sticky_freebsd_test.go (+294 -0)

📄 Description

What this does

Fills in getSrcFromControl() / setSrcControl() for FreeBSD, so a socket answers from the address the packet arrived on instead of whatever routing picks. This is the open TODO in conn/sticky_default.go.

The generic transport already does the work on every platform — the non-Linux path in bind_std.go uses ReadMsgUDP/WriteMsgUDP with OOB — only the implementations were empty outside Linux.

Why FreeBSD is easy

It has the whole API:

IPv4 IPv6
enable IP_RECVDSTADDR IPV6_RECVPKTINFO
receive in_addr in6_pktinfo
send IP_SENDSRCADDR IPV6_PKTINFO

And IP_SENDSRCADDR == IP_RECVDSTADDR (both 7), so the control message that came in can be handed straight back to sendmsg — the same trick the Linux code uses by keeping the raw cmsg in ep.src. IPv6 is symmetric on its own.

Why it matters

On a multi-homed host without sticky sockets, a handshake arriving on a secondary WAN gets answered out of the default gateway's interface with the wrong source address; the client discards it and the handshake never completes.

Concretely, on a pfSense firewall with two WANs, before and after:

before after
pf states two — one inbound, one outbound on the other interface one, matching the WAN rule
packets out the secondary WAN none reply leaves with the dialed address
handshake never completes completes on the first try

That is the behaviour if_wg already has in kernel, so this closes the gap for the userspace implementation.

Verification

  • go test ./conn/ on FreeBSD 16 (pfSense 2.9.0), including a round trip that exercises listenConfig(), the cmsg parse, and a sendmsg with the source pinned against the real kernel.
  • go build ./conn/ and go vet ./conn/ for freebsd, linux, windows, darwin and openbsd — the build tag change (!linux || android → (!linux && !freebsd) || android) leaves every other platform on the stub.
  • A real client completing handshakes and passing traffic over both WANs, including the one without the default route.

Two notes

Anyone extending sticky_freebsd_test.go should know that IP_SENDSRCADDR returns EINVAL on a socket bound to a specific address — it only works with a wildcard bind, which is what StdNetBind.Open() does. The code is unaffected, but a test binding 127.0.0.1 fails for that reason alone.

Unrelated to this change: GOOS=freebsd go build ./... on current master fails in golang.getoutline.org/sdk/x/smart (undefined: lookupCNAME), so the repo does not build for FreeBSD today. The conn package itself is fine, which is what this patch touches.

The change applies identically to wireguard-go, which is where these files come from.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/amnezia-vpn/amneziawg-go/pull/180 **Author:** [@MarceloMayo74](https://github.com/MarceloMayo74) **Created:** 8/14/2026 **Status:** 🔄 Open **Base:** `master` ← **Head:** `freebsd-sticky-sockets` --- ### 📝 Commits (1) - [`ec5368e`](https://github.com/amnezia-vpn/amneziawg-go/commit/ec5368e5216e192c59730c549ddc2ccd47b34f8d) conn: implement sticky sockets on FreeBSD ### 📊 Changes **4 files changed** (+456 additions, -3 deletions) <details> <summary>View changed files</summary> ➕ `conn/controlfns_freebsd.go` (+38 -0) 📝 `conn/sticky_default.go` (+3 -3) ➕ `conn/sticky_freebsd.go` (+121 -0) ➕ `conn/sticky_freebsd_test.go` (+294 -0) </details> ### 📄 Description ## What this does Fills in `getSrcFromControl()` / `setSrcControl()` for FreeBSD, so a socket answers from the address the packet arrived on instead of whatever routing picks. This is the open `TODO` in `conn/sticky_default.go`. The generic transport already does the work on every platform — the non-Linux path in `bind_std.go` uses `ReadMsgUDP`/`WriteMsgUDP` with OOB — only the implementations were empty outside Linux. ## Why FreeBSD is easy It has the whole API: | | IPv4 | IPv6 | |---|---|---| | enable | `IP_RECVDSTADDR` | `IPV6_RECVPKTINFO` | | receive | `in_addr` | `in6_pktinfo` | | send | `IP_SENDSRCADDR` | `IPV6_PKTINFO` | And `IP_SENDSRCADDR == IP_RECVDSTADDR` (both 7), so the control message that came in can be handed straight back to `sendmsg` — the same trick the Linux code uses by keeping the raw cmsg in `ep.src`. IPv6 is symmetric on its own. ## Why it matters On a multi-homed host without sticky sockets, a handshake arriving on a secondary WAN gets answered out of the default gateway's interface with the wrong source address; the client discards it and the handshake never completes. Concretely, on a pfSense firewall with two WANs, before and after: | | before | after | |---|---|---| | pf states | two — one inbound, one outbound on the other interface | one, matching the WAN rule | | packets out the secondary WAN | none | reply leaves with the dialed address | | handshake | never completes | completes on the first try | That is the behaviour `if_wg` already has in kernel, so this closes the gap for the userspace implementation. ## Verification - `go test ./conn/` on FreeBSD 16 (pfSense 2.9.0), including a round trip that exercises `listenConfig()`, the cmsg parse, and a `sendmsg` with the source pinned against the real kernel. - `go build ./conn/` and `go vet ./conn/` for `freebsd`, `linux`, `windows`, `darwin` and `openbsd` — the build tag change (`!linux || android` → `(!linux && !freebsd) || android`) leaves every other platform on the stub. - A real client completing handshakes and passing traffic over both WANs, including the one without the default route. ## Two notes Anyone extending `sticky_freebsd_test.go` should know that `IP_SENDSRCADDR` returns `EINVAL` on a socket bound to a specific address — it only works with a wildcard bind, which is what `StdNetBind.Open()` does. The code is unaffected, but a test binding `127.0.0.1` fails for that reason alone. Unrelated to this change: `GOOS=freebsd go build ./...` on current `master` fails in `golang.getoutline.org/sdk/x/smart` (`undefined: lookupCNAME`), so the repo does not build for FreeBSD today. The `conn` package itself is fine, which is what this patch touches. The change applies identically to `wireguard-go`, which is where these files come from. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amneziawg-go#188
No description provided.