[PR #144] conn: fix sticky source address on multi-IP hosts #164

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

📋 Pull Request Information

Original PR: https://github.com/amnezia-vpn/amneziawg-go/pull/144
Author: @akaWolf
Created: 6/8/2026
Status: 🔄 Open

Base: master ← Head: fix-sticky-pktinfo


📝 Commits (3)

  • 9d2f826 conn: enable IP_PKTINFO ancillary data on PacketConn
  • 73b3791 conn: reset udpAddrPool slice length before copy
  • 3588e16 conn: propagate ancillary data to coalesced split frames

📊 Changes

1 file changed (+9 additions, -2 deletions)

View changed files

📝 conn/bind_std.go (+9 -2)

📄 Description

Summary

Three independent bugs in conn/bind_std.go break the sticky source address feature on multi-IP hosts. On a server with multiple global IPv6 addresses (e.g. a static ::100 and a DHCPv6-assigned ::943), the server responds to handshake initiations with the kernel's route-preferred source rather than the destination the peer connected to, so the peer rejects the response and the session never establishes.

Patches

  1. conn: enable IP_PKTINFO ancillary data on PacketConn — call SetControlMessage(FlagDst|FlagInterface, true) after NewPacketConn. Without it, ipv6.PacketConn.ReadBatch/RecvMsgs discards the PKTINFO cmsg even though IPV6_RECVPKTINFO=1 is set at the socket level — Message.NN always returns 0, ep.src never populates, sticky source becomes a silent no-op.
  2. conn: reset udpAddrPool slice length before copy — Send() truncates ua.IP after copy(...), so after a v4 send the pooled *net.UDPAddr has len(IP)==4, and the next v6 reuse only writes the upper 4 bytes of the IPv6 destination, leaving 12 stale bytes from the previous send. Resize the slice before the copy.
  3. conn: propagate ancillary data to coalesced split frames — splitCoalescedMessages copies Buffers[0], N, and Addr to each child frame but never propagates OOB/NN. With UDP_GRO active (Linux 5.12+), every frame past the first loses its PKTINFO and sticky source breaks even after fix #1 is applied.

Each commit fixes one independent issue and is self-contained; they can be applied or reverted individually.

Reproduction

Network namespaces with a server holding ::100 and ::943 on the same /64, where ip route get <client> returns src ::943:

sudo ip netns add srvns && sudo ip netns add cltns
sudo ip link add veth-srv type veth peer name veth-srvNS
sudo ip link add veth-clt type veth peer name veth-cltNS
sudo ip link set veth-srvNS netns srvns
sudo ip link set veth-cltNS netns cltns
sudo ip link add br-test type bridge
sudo ip link set veth-srv master br-test
sudo ip link set veth-clt master br-test
sudo ip link set veth-srv up; sudo ip link set veth-clt up; sudo ip link set br-test up
sudo ip netns exec srvns ip link set veth-srvNS up
sudo ip netns exec srvns ip -6 addr add fd00:1::100/64 dev veth-srvNS
sudo ip netns exec srvns ip -6 addr add fd00:1::943/64 dev veth-srvNS
sudo ip netns exec cltns ip link set veth-cltNS up
sudo ip netns exec cltns ip -6 addr add fd00:1::1/64 dev veth-cltNS
sleep 3  # DAD

Then run an amneziawg-go server in srvns listening on port 53400, and a client in cltns with Endpoint = [fd00:1::100]:53400. Without the patches, tcpdump -i br-test shows the handshake response coming from fd00:1::943. With the patches, the response comes from fd00:1::100 and the client does not roam endpoints.

Testing

End-to-end tested in netns on Linux 6.x (both arm64 and x86_64). Verified against a real-world production setup where this bug was causing handshake failures for an IPv6 client behind a sticky-source-sensitive peer; with the patches applied, the workaround route (ip -6 route ... src ::100) was removed and the tunnel handshakes cleanly.

This same patch series is being submitted to wireguard-go upstream (the bugs are identical there since amneziawg-go inherits conn/bind_std.go).

Test plan

  • All three commits build with go 1.23+
  • Reproduction in netns shows the handshake response source IP changes from ::943 to ::100 after the patches
  • Existing handshake/keepalive flows continue to work
  • No new dependencies introduced

🔄 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/144 **Author:** [@akaWolf](https://github.com/akaWolf) **Created:** 6/8/2026 **Status:** 🔄 Open **Base:** `master` ← **Head:** `fix-sticky-pktinfo` --- ### 📝 Commits (3) - [`9d2f826`](https://github.com/amnezia-vpn/amneziawg-go/commit/9d2f8265b0d8e3af62296f4f58e079301f56e246) conn: enable IP_PKTINFO ancillary data on PacketConn - [`73b3791`](https://github.com/amnezia-vpn/amneziawg-go/commit/73b3791d41b6906b3cb2f94cda54b01873a6ae22) conn: reset udpAddrPool slice length before copy - [`3588e16`](https://github.com/amnezia-vpn/amneziawg-go/commit/3588e1640813c2d39431fdece3d37018d6f14866) conn: propagate ancillary data to coalesced split frames ### 📊 Changes **1 file changed** (+9 additions, -2 deletions) <details> <summary>View changed files</summary> 📝 `conn/bind_std.go` (+9 -2) </details> ### 📄 Description ## Summary Three independent bugs in `conn/bind_std.go` break the sticky source address feature on multi-IP hosts. On a server with multiple global IPv6 addresses (e.g. a static `::100` and a DHCPv6-assigned `::943`), the server responds to handshake initiations with the kernel's route-preferred source rather than the destination the peer connected to, so the peer rejects the response and the session never establishes. ## Patches 1. **`conn: enable IP_PKTINFO ancillary data on PacketConn`** — call `SetControlMessage(FlagDst|FlagInterface, true)` after `NewPacketConn`. Without it, `ipv6.PacketConn.ReadBatch`/`RecvMsgs` discards the PKTINFO cmsg even though `IPV6_RECVPKTINFO=1` is set at the socket level — `Message.NN` always returns 0, `ep.src` never populates, sticky source becomes a silent no-op. 2. **`conn: reset udpAddrPool slice length before copy`** — `Send()` truncates `ua.IP` *after* `copy(...)`, so after a v4 send the pooled `*net.UDPAddr` has `len(IP)==4`, and the next v6 reuse only writes the upper 4 bytes of the IPv6 destination, leaving 12 stale bytes from the previous send. Resize the slice before the copy. 3. **`conn: propagate ancillary data to coalesced split frames`** — `splitCoalescedMessages` copies `Buffers[0]`, `N`, and `Addr` to each child frame but never propagates `OOB`/`NN`. With UDP_GRO active (Linux 5.12+), every frame past the first loses its PKTINFO and sticky source breaks even after fix #1 is applied. Each commit fixes one independent issue and is self-contained; they can be applied or reverted individually. ## Reproduction Network namespaces with a server holding `::100` and `::943` on the same `/64`, where `ip route get <client>` returns `src ::943`: ``` sudo ip netns add srvns && sudo ip netns add cltns sudo ip link add veth-srv type veth peer name veth-srvNS sudo ip link add veth-clt type veth peer name veth-cltNS sudo ip link set veth-srvNS netns srvns sudo ip link set veth-cltNS netns cltns sudo ip link add br-test type bridge sudo ip link set veth-srv master br-test sudo ip link set veth-clt master br-test sudo ip link set veth-srv up; sudo ip link set veth-clt up; sudo ip link set br-test up sudo ip netns exec srvns ip link set veth-srvNS up sudo ip netns exec srvns ip -6 addr add fd00:1::100/64 dev veth-srvNS sudo ip netns exec srvns ip -6 addr add fd00:1::943/64 dev veth-srvNS sudo ip netns exec cltns ip link set veth-cltNS up sudo ip netns exec cltns ip -6 addr add fd00:1::1/64 dev veth-cltNS sleep 3 # DAD ``` Then run an amneziawg-go server in `srvns` listening on port 53400, and a client in `cltns` with `Endpoint = [fd00:1::100]:53400`. Without the patches, `tcpdump -i br-test` shows the handshake response coming from `fd00:1::943`. With the patches, the response comes from `fd00:1::100` and the client does not roam endpoints. ## Testing End-to-end tested in netns on Linux 6.x (both arm64 and x86_64). Verified against a real-world production setup where this bug was causing handshake failures for an IPv6 client behind a sticky-source-sensitive peer; with the patches applied, the workaround route (`ip -6 route ... src ::100`) was removed and the tunnel handshakes cleanly. This same patch series is being submitted to wireguard-go upstream (the bugs are identical there since amneziawg-go inherits `conn/bind_std.go`). ## Test plan - [x] All three commits build with `go 1.23+` - [x] Reproduction in netns shows the handshake response source IP changes from `::943` to `::100` after the patches - [x] Existing handshake/keepalive flows continue to work - [x] No new dependencies introduced --- <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#164
No description provided.