diff --git a/device/send.go b/device/send.go index ff8f7da50..4bf5d6f1c 100644 --- a/device/send.go +++ b/device/send.go @@ -115,6 +115,10 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { peer.handshake.lastSentHandshake = time.Now() peer.handshake.mutex.Unlock() + return peer.sendHandshakeInitiation() +} + +func (peer *Peer) sendHandshakeInitiation() error { peer.device.log.Verbosef("%v - Sending handshake initiation", peer) msg, err := peer.device.CreateMessageInitiation(peer) @@ -139,6 +143,25 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error { return err } +// needsHandshake reports whether the peer lacks a usable session, i.e. a new +// handshake is required before traffic can flow. +func (peer *Peer) needsHandshake() bool { + keypair := peer.keypairs.Current() + return keypair == nil || + keypair.sendNonce.Load() >= RejectAfterMessages || + time.Since(keypair.created) >= RejectAfterTime +} + +func (peer *Peer) SendHandshakeInitiationOnEndpointChange() error { + peer.timers.handshakeAttempts.Store(0) + + peer.handshake.mutex.Lock() + peer.handshake.lastSentHandshake = time.Now() + peer.handshake.mutex.Unlock() + + return peer.sendHandshakeInitiation() +} + func (peer *Peer) SendHandshakeResponse() error { peer.handshake.mutex.Lock() peer.handshake.lastSentHandshake = time.Now() diff --git a/device/uapi.go b/device/uapi.go index cc69488b4..e874fe13b 100644 --- a/device/uapi.go +++ b/device/uapi.go @@ -249,10 +249,11 @@ func (device *Device) handleDeviceLine(key, value string) error { // An ipcSetPeer is the current state of an IPC set operation on a peer. type ipcSetPeer struct { - *Peer // Peer is the current peer being operated on - dummy bool // dummy reports whether this peer is a temporary, placeholder peer - created bool // new reports whether this is a newly created peer - pkaOn bool // pkaOn reports whether the peer had the persistent keepalive turn on + *Peer // Peer is the current peer being operated on + dummy bool // dummy reports whether this peer is a temporary, placeholder peer + created bool // new reports whether this is a newly created peer + pkaOn bool // pkaOn reports whether the peer had the persistent keepalive turn on + endpointChanged bool // the endpoint address changed } func (peer *ipcSetPeer) handlePostConfig() { @@ -267,6 +268,9 @@ func (peer *ipcSetPeer) handlePostConfig() { if peer.pkaOn { peer.SendKeepalive() } + if peer.endpointChanged && peer.needsHandshake() { + peer.SendHandshakeInitiationOnEndpointChange() + } peer.SendStagedPackets() } } @@ -345,6 +349,10 @@ func (device *Device) handlePeerLine(peer *ipcSetPeer, key, value string) error } peer.endpoint.Lock() defer peer.endpoint.Unlock() + // Only an actual change of an existing endpoint triggers a handshake. + if peer.endpoint.val != nil && peer.endpoint.val.DstToString() != endpoint.DstToString() { + peer.endpointChanged = true + } peer.endpoint.val = endpoint case "persistent_keepalive_interval": diff --git a/device/uapi_test.go b/device/uapi_test.go new file mode 100644 index 000000000..9df51a59a --- /dev/null +++ b/device/uapi_test.go @@ -0,0 +1,88 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved. + */ + +package device + +import ( + "encoding/hex" + "math/rand" + "testing" + + "golang.zx2c4.com/wireguard/conn" + "golang.zx2c4.com/wireguard/tun/tuntest" +) + +// TestEndpointChangeDetection checks that only replacing an already-set +// endpoint with a different address counts as a change: a first-time +// assignment or a no-op reassignment must not, so a live session is never +// rekeyed unnecessarily. +func TestEndpointChangeDetection(t *testing.T) { + dev, peer := newTestPeer(t) + + if changed := setEndpoint(t, dev, peer, "127.0.0.1:51820"); changed { + t.Error("first-time endpoint assignment reported as a change") + } + if changed := setEndpoint(t, dev, peer, "127.0.0.1:51820"); changed { + t.Error("reassigning the same endpoint reported as a change") + } + if changed := setEndpoint(t, dev, peer, "127.0.0.1:51821"); !changed { + t.Error("changing the endpoint address was not reported as a change") + } +} + +func TestNeedsHandshake(t *testing.T) { + _, peer := newTestPeer(t) + + if !peer.needsHandshake() { + t.Error("peer without a keypair should need a handshake") + } +} + +func newTestPeer(t *testing.T) (*Device, *Peer) { + t.Helper() + + var key NoisePrivateKey + if _, err := rand.Read(key[:]); err != nil { + t.Fatalf("generate private key: %v", err) + } + var peerKey NoisePrivateKey + if _, err := rand.Read(peerKey[:]); err != nil { + t.Fatalf("generate peer key: %v", err) + } + peerPub := peerKey.publicKey() + + tun := tuntest.NewChannelTUN() + dev := NewDevice(tun.TUN(), conn.NewDefaultBind(), NewLogger(LogLevelError, "test: ")) + t.Cleanup(dev.Close) + + cfg := uapiCfg( + "private_key", hex.EncodeToString(key[:]), + "listen_port", "0", + "replace_peers", "true", + "public_key", hex.EncodeToString(peerPub[:]), + "protocol_version", "1", + "replace_allowed_ips", "true", + "allowed_ip", "1.0.0.2/32", + ) + if err := dev.IpcSet(cfg); err != nil { + t.Fatalf("configure device: %v", err) + } + + peer := dev.LookupPeer(peerPub) + if peer == nil { + t.Fatal("configured peer not found") + } + return dev, peer +} + +func setEndpoint(t *testing.T, dev *Device, peer *Peer, value string) bool { + t.Helper() + + setPeer := &ipcSetPeer{Peer: peer} + if err := dev.handlePeerLine(setPeer, "endpoint", value); err != nil { + t.Fatalf("set endpoint %q: %v", value, err) + } + return setPeer.endpointChanged +}