Addresses CI fixes

This commit is contained in:
riccardom
2026-09-11 14:48:54 +02:00
parent 8a04fa4d3f
commit ecddbf2e55
9 changed files with 131 additions and 59 deletions
+9 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"math/rand" "math/rand"
"net" "net"
"net/netip" "net/netip"
@@ -3038,6 +3039,13 @@ func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) {
relayIP := decodeRelayIP(msg.GetBody().GetRelayServerIP()) relayIP := decodeRelayIP(msg.GetBody().GetRelayServerIP())
// Ports are uint16 internally; the proto widens them to uint32, so validate the
// range before narrowing (a value that does not fit is a malformed message).
mlkemPort := msg.GetBody().GetMlkemPort()
if mlkemPort > math.MaxUint16 {
return nil, fmt.Errorf("invalid ML-KEM port %d in signalling message", mlkemPort)
}
offerAnswer := peer.OfferAnswer{ offerAnswer := peer.OfferAnswer{
IceCredentials: peer.IceCredentials{ IceCredentials: peer.IceCredentials{
UFrag: remoteCred.UFrag, UFrag: remoteCred.UFrag,
@@ -3048,7 +3056,7 @@ func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) {
RosenpassPubKey: rosenpassPubKey, RosenpassPubKey: rosenpassPubKey,
RosenpassAddr: rosenpassAddr, RosenpassAddr: rosenpassAddr,
MlkemPayload: msg.GetBody().GetMlkemPayload(), MlkemPayload: msg.GetBody().GetMlkemPayload(),
MlkemPort: int(msg.GetBody().GetMlkemPort()), MlkemPort: uint16(mlkemPort),
RelaySrvAddress: msg.GetBody().GetRelayServerAddress(), RelaySrvAddress: msg.GetBody().GetRelayServerAddress(),
RelaySrvIP: relayIP, RelaySrvIP: relayIP,
SessionID: sessionID, SessionID: sessionID,
+55 -31
View File
@@ -84,14 +84,14 @@ type RosenpassConfig struct {
type PQHandshaker interface { type PQHandshaker interface {
// OfferPayload returns the KEM offer to embed in an outgoing offer (nil if this // OfferPayload returns the KEM offer to embed in an outgoing offer (nil if this
// peer is not the KEM initiator) and the local PQ data-path port to announce. // peer is not the KEM initiator) and the local PQ data-path port to announce.
OfferPayload(remoteKey string) (payload []byte, port int) OfferPayload(remoteKey string) (payload []byte, port uint16)
// ShouldSendBootstrapOffer reports whether, as the controller, we should reply to a // ShouldSendBootstrapOffer reports whether, as the controller, we should reply to a
// received responder offer with our own KEM offer (true only when no exchange is // received responder offer with our own KEM offer (true only when no exchange is
// already in flight — so we kick the KEM once and ignore further offers). // already in flight — so we kick the KEM once and ignore further offers).
ShouldSendBootstrapOffer(remoteKey string) bool ShouldSendBootstrapOffer(remoteKey string) bool
// AnswerPayload processes a received KEM offer (nil if absent) and returns the KEM // AnswerPayload processes a received KEM offer (nil if absent) and returns the KEM
// answer to embed in the outgoing answer (nil if none) and the local PQ port. // answer to embed in the outgoing answer (nil if none) and the local PQ port.
AnswerPayload(remoteKey string, recvOffer []byte) (payload []byte, port int) AnswerPayload(remoteKey string, recvOffer []byte) (payload []byte, port uint16)
// OnAnswer feeds a received KEM answer (nil if absent). // OnAnswer feeds a received KEM answer (nil if absent).
OnAnswer(remoteKey string, recvAnswer []byte) OnAnswer(remoteKey string, recvAnswer []byte)
// PSK returns the peer's latest derived post-quantum PSK to program at WG // PSK returns the peer's latest derived post-quantum PSK to program at WG
@@ -468,6 +468,9 @@ func (conn *Conn) ConnID() id.ConnID {
// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected // configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConnInfo ICEConnInfo) { func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConnInfo ICEConnInfo) {
// Read the PQ PSK before conn.mu to keep the lock order conn.mu -> manager.
pqPSK, pqOK := conn.pqPSK()
conn.mu.Lock() conn.mu.Lock()
defer conn.mu.Unlock() defer conn.mu.Unlock()
@@ -485,7 +488,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
if conn.currentConnPriority > priority { if conn.currentConnPriority > priority {
conn.Log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority) conn.Log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority)
conn.statusICE.SetConnected() conn.statusICE.SetConnected()
conn.updateIceState(iceConnInfo, time.Now()) conn.updateIceState(iceConnInfo, pqOK, time.Now())
return return
} }
@@ -528,7 +531,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
updateTime := time.Now() updateTime := time.Now()
conn.enableWgWatcherIfNeeded(updateTime) conn.enableWgWatcherIfNeeded(updateTime)
presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey) presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey, pqPSK)
if err = conn.endpointUpdater.ConfigureWGEndpoint(ep, presharedKey); err != nil { if err = conn.endpointUpdater.ConfigureWGEndpoint(ep, presharedKey); err != nil {
conn.handleConfigurationFailure(err, wgProxy) conn.handleConfigurationFailure(err, wgProxy)
return return
@@ -544,11 +547,14 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn
conn.currentConnPriority = priority conn.currentConnPriority = priority
conn.statusICE.SetConnected() conn.statusICE.SetConnected()
conn.updateIceState(iceConnInfo, updateTime) conn.updateIceState(iceConnInfo, pqOK, updateTime)
conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr, updateTime) conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr, updateTime)
} }
func (conn *Conn) onICEStateDisconnected(sessionChanged bool) { func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
// Read the PQ PSK before conn.mu to keep the lock order conn.mu -> manager.
pqPSK, _ := conn.pqPSK()
conn.mu.Lock() conn.mu.Lock()
defer conn.mu.Unlock() defer conn.mu.Unlock()
@@ -575,7 +581,7 @@ func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
// todo consider to move after the ConfigureWGEndpoint // todo consider to move after the ConfigureWGEndpoint
conn.wgProxyRelay.Work() conn.wgProxyRelay.Work()
presharedKey := conn.presharedKey(conn.rosenpassRemoteKey) presharedKey := conn.presharedKey(conn.rosenpassRemoteKey, pqPSK)
if err := conn.endpointUpdater.SwitchWGEndpoint(conn.wgProxyRelay.EndpointAddr(), presharedKey); err != nil { if err := conn.endpointUpdater.SwitchWGEndpoint(conn.wgProxyRelay.EndpointAddr(), presharedKey); err != nil {
conn.Log.Errorf("failed to switch to relay conn: %v", err) conn.Log.Errorf("failed to switch to relay conn: %v", err)
} }
@@ -613,6 +619,9 @@ func (conn *Conn) onICEStateDisconnected(sessionChanged bool) {
} }
func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
// Read the PQ PSK before conn.mu to keep the lock order conn.mu -> manager.
pqPSK, pqOK := conn.pqPSK()
conn.mu.Lock() conn.mu.Lock()
defer conn.mu.Unlock() defer conn.mu.Unlock()
@@ -641,7 +650,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String()) conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String())
conn.setRelayedProxy(wgProxy) conn.setRelayedProxy(wgProxy)
conn.statusRelay.SetConnected() conn.statusRelay.SetConnected()
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, time.Now()) conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, pqOK, time.Now())
return return
} }
@@ -652,7 +661,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
} }
updateTime := time.Now() updateTime := time.Now()
conn.enableWgWatcherIfNeeded(updateTime) conn.enableWgWatcherIfNeeded(updateTime)
if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), conn.presharedKey(rci.rosenpassPubKey)); err != nil { if err := conn.endpointUpdater.ConfigureWGEndpoint(wgProxy.EndpointAddr(), conn.presharedKey(rci.rosenpassPubKey, pqPSK)); err != nil {
if err := wgProxy.CloseConn(); err != nil { if err := wgProxy.CloseConn(); err != nil {
conn.Log.Warnf("Failed to close relay connection: %v", err) conn.Log.Warnf("Failed to close relay connection: %v", err)
} }
@@ -671,7 +680,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
conn.currentConnPriority = conntype.Relay conn.currentConnPriority = conntype.Relay
conn.statusRelay.SetConnected() conn.statusRelay.SetConnected()
conn.setRelayedProxy(wgProxy) conn.setRelayedProxy(wgProxy)
conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, updateTime) conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey, pqOK, updateTime)
conn.Log.Infof("start to communicate with peer via relay") conn.Log.Infof("start to communicate with peer via relay")
conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr, updateTime) conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr, updateTime)
} }
@@ -751,19 +760,15 @@ func (conn *Conn) RequestReoffer() {
func (conn *Conn) onWGDisconnected(watcherCtx context.Context) { func (conn *Conn) onWGDisconnected(watcherCtx context.Context) {
conn.mu.Lock() conn.mu.Lock()
defer conn.mu.Unlock()
// watcherCtx guards against a stale watcher tearing down a connection that already superseded it. // watcherCtx guards against a stale watcher tearing down a connection that already superseded it.
if conn.ctx.Err() != nil || watcherCtx.Err() != nil { if conn.ctx.Err() != nil || watcherCtx.Err() != nil {
conn.mu.Unlock()
return return
} }
conn.Log.Warnf("WireGuard handshake timeout detected, closing current connection") conn.Log.Warnf("WireGuard handshake timeout detected, closing current connection")
if conn.config.PQ != nil {
conn.config.PQ.OnDataPathDown(conn.config.Key)
}
// Close the active connection based on current priority // Close the active connection based on current priority
switch conn.currentConnPriority { switch conn.currentConnPriority {
case conntype.Relay: case conntype.Relay:
@@ -776,6 +781,15 @@ func (conn *Conn) onWGDisconnected(watcherCtx context.Context) {
} }
conn.escalateWGTimeoutLocked() conn.escalateWGTimeoutLocked()
pq := conn.config.PQ
key := conn.config.Key
conn.mu.Unlock()
// Signal the PQ manager outside conn.mu: it may re-enter Conn (reoffer) under
// conn.mu, so calling it while holding the lock would invert the lock order.
if pq != nil {
pq.OnDataPathDown(key)
}
} }
// escalateWGTimeoutLocked resets the peer's rosenpass state after repeated // escalateWGTimeoutLocked resets the peer's rosenpass state after repeated
@@ -799,14 +813,14 @@ func (conn *Conn) escalateWGTimeoutLocked() {
conn.onDisconnected(conn.config.WgConfig.RemoteKey) conn.onDisconnected(conn.config.WgConfig.RemoteKey)
} }
func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []byte, updateTime time.Time) { func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []byte, pqEstablished bool, updateTime time.Time) {
peerState := State{ peerState := State{
PubKey: conn.config.Key, PubKey: conn.config.Key,
ConnStatusUpdate: updateTime, ConnStatusUpdate: updateTime,
ConnStatus: conn.evalStatus(), ConnStatus: conn.evalStatus(),
Relayed: conn.isRelayed(), Relayed: conn.isRelayed(),
RelayServerAddress: relayServerAddr, RelayServerAddress: relayServerAddr,
RosenpassEnabled: conn.quantumResistant(rosenpassPubKey), RosenpassEnabled: conn.quantumResistant(rosenpassPubKey, pqEstablished),
} }
err := conn.statusRecorder.UpdatePeerRelayedState(peerState) err := conn.statusRecorder.UpdatePeerRelayedState(peerState)
@@ -815,7 +829,7 @@ func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []by
} }
} }
func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, updateTime time.Time) { func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, pqEstablished bool, updateTime time.Time) {
peerState := State{ peerState := State{
PubKey: conn.config.Key, PubKey: conn.config.Key,
ConnStatusUpdate: updateTime, ConnStatusUpdate: updateTime,
@@ -825,7 +839,7 @@ func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, updateTime time.Time)
RemoteIceCandidateType: iceConnInfo.RemoteIceCandidateType, RemoteIceCandidateType: iceConnInfo.RemoteIceCandidateType,
LocalIceCandidateEndpoint: iceConnInfo.LocalIceCandidateEndpoint, LocalIceCandidateEndpoint: iceConnInfo.LocalIceCandidateEndpoint,
RemoteIceCandidateEndpoint: iceConnInfo.RemoteIceCandidateEndpoint, RemoteIceCandidateEndpoint: iceConnInfo.RemoteIceCandidateEndpoint,
RosenpassEnabled: conn.quantumResistant(iceConnInfo.RosenpassPubKey), RosenpassEnabled: conn.quantumResistant(iceConnInfo.RosenpassPubKey, pqEstablished),
} }
err := conn.statusRecorder.UpdatePeerICEState(peerState) err := conn.statusRecorder.UpdatePeerICEState(peerState)
@@ -1094,13 +1108,31 @@ func (conn *Conn) AgentVersionString() string {
return conn.config.AgentVersion return conn.config.AgentVersion
} }
func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key { // pqPSK returns the post-quantum PSK derived for this peer, if the ML-KEM exchange has
// produced one. It reads the manager WITHOUT conn.mu, so callers fetch it before taking
// conn.mu: the lock order is always conn.mu -> manager, never the reverse (the manager's
// reoffer callback re-enters Conn under conn.mu).
func (conn *Conn) pqPSK() (*wgtypes.Key, bool) {
if conn.config.PQ == nil {
return nil, false
}
psk, ok := conn.config.PQ.PSK(conn.config.Key)
if !ok {
return nil, false
}
return &psk, true
}
// presharedKey resolves the WireGuard preshared key for the peer. pqPSK is the
// post-quantum PSK looked up out of band via pqPSK (nil when none is derived yet), passed
// in so the manager lock is never taken under conn.mu.
func (conn *Conn) presharedKey(remoteRosenpassKey []byte, pqPSK *wgtypes.Key) *wgtypes.Key {
// Post-quantum: once the ML-KEM exchange has derived a PSK for this peer, program // Post-quantum: once the ML-KEM exchange has derived a PSK for this peer, program
// it here so the peer's next WireGuard handshake adopts it. Applied at peer-config // it here so the peer's next WireGuard handshake adopts it. Applied at peer-config
// time (bootstrap / reconnect); steady-state rotation is pushed separately. // time (bootstrap / reconnect); steady-state rotation is pushed separately.
if conn.config.PQ != nil { if conn.config.PQ != nil {
if psk, ok := conn.config.PQ.PSK(conn.config.Key); ok { if pqPSK != nil {
return &psk return pqPSK
} }
if conn.config.PQStrict && conn.pqStrictSentinelKey != nil { if conn.config.PQStrict && conn.pqStrictSentinelKey != nil {
// Fail closed: program a non-matching sentinel so no session forms on a // Fail closed: program a non-matching sentinel so no session forms on a
@@ -1154,16 +1186,8 @@ func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
// quantumResistant reports whether the peer's tunnel is post-quantum protected, for // quantumResistant reports whether the peer's tunnel is post-quantum protected, for
// the status "Quantum resistance" field: either Rosenpass (the remote advertised a // the status "Quantum resistance" field: either Rosenpass (the remote advertised a
// Rosenpass key) or the ML-KEM exchange (a PQ PSK has been derived for this peer). // Rosenpass key) or the ML-KEM exchange (a PQ PSK has been derived for this peer).
func (conn *Conn) quantumResistant(remoteRosenpassPubKey []byte) bool { func (conn *Conn) quantumResistant(remoteRosenpassPubKey []byte, pqEstablished bool) bool {
if isRosenpassEnabled(remoteRosenpassPubKey) { return isRosenpassEnabled(remoteRosenpassPubKey) || pqEstablished
return true
}
if conn.config.PQ != nil {
if _, ok := conn.config.PQ.PSK(conn.config.Key); ok {
return true
}
}
return false
} }
func evalConnStatus(in connStatusInputs) guard.ConnStatus { func evalConnStatus(in connStatusInputs) guard.ConnStatus {
+8 -4
View File
@@ -53,10 +53,12 @@ func TestConn_presharedKey_PQ(t *testing.T) {
c.config.PQ = fakePQ{psk: derivedPSK, ok: true} c.config.PQ = fakePQ{psk: derivedPSK, ok: true}
c.config.PQStrict = strict c.config.PQStrict = strict
if strict { if strict {
sentinel, _ := wgtypes.GenerateKey() sentinel, err := wgtypes.GenerateKey()
require.NoError(t, err)
c.pqStrictSentinelKey = &sentinel c.pqStrictSentinelKey = &sentinel
} }
got := c.presharedKey(nil) pqPSK, _ := c.pqPSK()
got := c.presharedKey(nil, pqPSK)
require.NotNil(t, got) require.NotNil(t, got)
require.Equal(t, derivedPSK, *got, "the derived PQ PSK must win (strict=%v)", strict) require.Equal(t, derivedPSK, *got, "the derived PQ PSK must win (strict=%v)", strict)
} }
@@ -66,7 +68,8 @@ func TestConn_presharedKey_PQ(t *testing.T) {
c := newConn() c := newConn()
c.config.PQ = fakePQ{ok: false} c.config.PQ = fakePQ{ok: false}
c.config.PQStrict = false c.config.PQStrict = false
got := c.presharedKey(nil) pqPSK, _ := c.pqPSK()
got := c.presharedKey(nil, pqPSK)
require.NotNil(t, got, "non-strict must not block") require.NotNil(t, got, "non-strict must not block")
require.Equal(t, nbPSK, *got, "non-strict falls through to the NetBird PSK, not a sentinel") require.Equal(t, nbPSK, *got, "non-strict falls through to the NetBird PSK, not a sentinel")
}) })
@@ -78,7 +81,8 @@ func TestConn_presharedKey_PQ(t *testing.T) {
c.config.PQ = fakePQ{ok: false} c.config.PQ = fakePQ{ok: false}
c.config.PQStrict = true c.config.PQStrict = true
c.pqStrictSentinelKey = &sentinel c.pqStrictSentinelKey = &sentinel
got := c.presharedKey(nil) pqPSK, _ := c.pqPSK()
got := c.presharedKey(nil, pqPSK)
require.NotNil(t, got) require.NotNil(t, got)
require.Equal(t, sentinel, *got, "strict must return the blocking sentinel") require.Equal(t, sentinel, *got, "strict must return the blocking sentinel")
require.NotEqual(t, nbPSK, *got, "the sentinel must not be the ordinary key") require.NotEqual(t, nbPSK, *got, "the sentinel must not be the ordinary key")
+4 -4
View File
@@ -257,8 +257,8 @@ func TestConn_presharedKey(t *testing.T) {
} }
conn2.config.RosenpassConfig.PermissiveMode = test.conn2Permissive conn2.config.RosenpassConfig.PermissiveMode = test.conn2Permissive
conn1PresharedKey := conn1.presharedKey(conn2.config.RosenpassConfig.PubKey) conn1PresharedKey := conn1.presharedKey(conn2.config.RosenpassConfig.PubKey, nil)
conn2PresharedKey := conn2.presharedKey(conn1.config.RosenpassConfig.PubKey) conn2PresharedKey := conn2.presharedKey(conn1.config.RosenpassConfig.PubKey, nil)
if test.conn1ExpectedInitialKey { if test.conn1ExpectedInitialKey {
if conn1PresharedKey == nil { if conn1PresharedKey == nil {
@@ -296,14 +296,14 @@ func TestConn_presharedKey_RosenpassManaged(t *testing.T) {
// When Rosenpass has already initialized the PSK for this peer, // When Rosenpass has already initialized the PSK for this peer,
// presharedKey must return nil to avoid UpdatePeer overwriting it. // presharedKey must return nil to avoid UpdatePeer overwriting it.
conn.rosenpassInitializedPresharedKeyValidator = func(peerKey string) bool { return true } conn.rosenpassInitializedPresharedKeyValidator = func(peerKey string) bool { return true }
if k := conn.presharedKey([]byte("remote")); k != nil { if k := conn.presharedKey([]byte("remote"), nil); k != nil {
t.Fatalf("expected nil presharedKey when Rosenpass manages PSK, got %v", k) t.Fatalf("expected nil presharedKey when Rosenpass manages PSK, got %v", k)
} }
// When Rosenpass hasn't taken over yet, presharedKey should provide // When Rosenpass hasn't taken over yet, presharedKey should provide
// a non-nil initial key (deterministic or from NetBird PSK). // a non-nil initial key (deterministic or from NetBird PSK).
conn.rosenpassInitializedPresharedKeyValidator = func(peerKey string) bool { return false } conn.rosenpassInitializedPresharedKeyValidator = func(peerKey string) bool { return false }
if k := conn.presharedKey([]byte("remote")); k == nil { if k := conn.presharedKey([]byte("remote"), nil); k == nil {
t.Fatalf("expected non-nil presharedKey before Rosenpass manages PSK") t.Fatalf("expected non-nil presharedKey before Rosenpass manages PSK")
} }
} }
+29 -5
View File
@@ -47,7 +47,7 @@ type OfferAnswer struct {
// MlkemPort is the peer's ML-KEM PQ service UDP port (bound on its WG overlay // MlkemPort is the peer's ML-KEM PQ service UDP port (bound on its WG overlay
// IP) where data-path rekey messages are sent. Zero when not running the exchange. // IP) where data-path rekey messages are sent. Zero when not running the exchange.
MlkemPort int MlkemPort uint16
// relay server address // relay server address
RelaySrvAddress string RelaySrvAddress string
@@ -221,14 +221,38 @@ func (h *Handshaker) pqControllerReoffer() bool {
// pqRegisterEndpoint feeds the post-quantum handshaker the peer's data-path endpoint // pqRegisterEndpoint feeds the post-quantum handshaker the peer's data-path endpoint
// (its WG overlay IP plus the advertised pq UDP port) learned from a remote offer/answer. // (its WG overlay IP plus the advertised pq UDP port) learned from a remote offer/answer.
func (h *Handshaker) pqRegisterEndpoint(remotePort int) { func (h *Handshaker) pqRegisterEndpoint(remotePort uint16) {
if h.config.PQ == nil || remotePort < 0 || remotePort > 65535 || len(h.config.WgConfig.AllowedIps) == 0 { if h.config.PQ == nil {
return
}
overlay, ok := h.pqPeerOverlayAddr()
if !ok {
return return
} }
// remotePort may be 0 (the peer omitted it, meaning the default port); the adapter // remotePort may be 0 (the peer omitted it, meaning the default port); the adapter
// resolves 0 to DefaultPort. // resolves 0 to DefaultPort.
addr := netip.AddrPortFrom(h.config.WgConfig.AllowedIps[0].Addr(), uint16(remotePort)) h.config.PQ.SetRemoteAddr(h.config.Key, netip.AddrPortFrom(overlay, remotePort))
h.config.PQ.SetRemoteAddr(h.config.Key, addr) }
// pqPeerOverlayAddr picks the peer's overlay address for the pq data path. The pq
// transport binds on the local WG overlay IPv4, so an IPv4 AllowedIP is preferred; a v6
// prefix is used only when this interface actually has a v6 overlay, and never in place
// of a usable v4. Returns false when no suitable address exists.
func (h *Handshaker) pqPeerOverlayAddr() (netip.Addr, bool) {
var v6 netip.Addr
for _, p := range h.config.WgConfig.AllowedIps {
a := p.Addr().Unmap()
if a.Is4() {
return a, true
}
if a.Is6() && !v6.IsValid() {
v6 = a
}
}
if v6.IsValid() && h.config.WgConfig.WgInterface.Address().HasIPv6() {
return v6, true
}
return netip.Addr{}, false
} }
func (h *Handshaker) SendOffer() error { func (h *Handshaker) SendOffer() error {
+1 -1
View File
@@ -64,7 +64,7 @@ func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string,
RosenpassPubKey: offerAnswer.RosenpassPubKey, RosenpassPubKey: offerAnswer.RosenpassPubKey,
RosenpassAddr: offerAnswer.RosenpassAddr, RosenpassAddr: offerAnswer.RosenpassAddr,
MlkemPayload: offerAnswer.MlkemPayload, MlkemPayload: offerAnswer.MlkemPayload,
MlkemPort: offerAnswer.MlkemPort, MlkemPort: int(offerAnswer.MlkemPort),
RelaySrvAddress: offerAnswer.RelaySrvAddress, RelaySrvAddress: offerAnswer.RelaySrvAddress,
RelaySrvIP: offerAnswer.RelaySrvIP, RelaySrvIP: offerAnswer.RelaySrvIP,
SessionID: sessionIDBytes, SessionID: sessionIDBytes,
+7 -6
View File
@@ -3,6 +3,7 @@ package pqkem
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -19,8 +20,8 @@ func TestManager_NonCapablePeerNotOffered(t *testing.T) {
offer, err := d.SignalOffer("aaaa") offer, err := d.SignalOffer("aaaa")
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, offer, "a non-capable peer must not be offered a KEM exchange") assert.Nil(t, offer, "a non-capable peer must not be offered a KEM exchange")
require.Empty(t, wg.failed, "a non-capable peer must not raise a rekey failure") assert.Empty(t, wg.failed, "a non-capable peer must not raise a rekey failure")
} }
// TestManager_MarkNonCapableCancelsInFlight: if we start an exchange with a peer whose // TestManager_MarkNonCapableCancelsInFlight: if we start an exchange with a peer whose
@@ -42,8 +43,8 @@ func TestManager_MarkNonCapableCancelsInFlight(t *testing.T) {
next, err := d.SignalOffer("aaaa") next, err := d.SignalOffer("aaaa")
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, next, "after learning non-capability the peer is no longer offered") assert.Nil(t, next, "after learning non-capability the peer is no longer offered")
require.Empty(t, wg.failed, "cancelling an in-flight exchange must not raise a failure") assert.Empty(t, wg.failed, "cancelling an in-flight exchange must not raise a failure")
} }
// TestManager_EstablishedPeerNotDowngraded: a stray zero-port observation must not tear // TestManager_EstablishedPeerNotDowngraded: a stray zero-port observation must not tear
@@ -60,6 +61,6 @@ func TestManager_EstablishedPeerNotDowngraded(t *testing.T) {
// The peer keeps its derived PSK (MarkNonCapable is a no-op once established). // The peer keeps its derived PSK (MarkNonCapable is a no-op once established).
psk, ok := dB.PSK("aaaa") psk, ok := dB.PSK("aaaa")
require.True(t, ok, "an established peer must keep its PSK despite a stray zero") assert.True(t, ok, "an established peer must keep its PSK despite a stray zero")
require.NotEqual(t, PSK{}, psk) assert.NotEqual(t, PSK{}, psk)
} }
+4 -3
View File
@@ -5,6 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -36,7 +37,7 @@ func TestManager_InitialTimeoutFailsImmediately(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, offer) require.NotNil(t, offer)
require.Eventually(t, func() bool { return failedCount(wg) == 1 }, time.Second, 5*time.Millisecond) assert.Eventually(t, func() bool { return failedCount(wg) == 1 }, time.Second, 5*time.Millisecond)
} }
func TestManager_RekeyToleratesKFailures(t *testing.T) { func TestManager_RekeyToleratesKFailures(t *testing.T) {
@@ -65,10 +66,10 @@ func TestManager_RekeyToleratesKFailures(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
} }
require.Equal(t, 0, failedCount(wgB), "no failure before K attempts") assert.Equal(t, 0, failedCount(wgB), "no failure before K attempts")
// The K-th failure raises it once. // The K-th failure raises it once.
_, err := dB.startExchangeTest("aaaa", false, ExchangeID{}) _, err := dB.startExchangeTest("aaaa", false, ExchangeID{})
require.NoError(t, err) require.NoError(t, err)
require.Eventually(t, func() bool { return failedCount(wgB) == 1 }, time.Second, 5*time.Millisecond) assert.Eventually(t, func() bool { return failedCount(wgB) == 1 }, time.Second, 5*time.Millisecond)
} }
+14 -4
View File
@@ -57,14 +57,17 @@ type pqHandshaker struct {
// announcedPort is the PQ data-path port to advertise to peers. It is omitted (0) when // announcedPort is the PQ data-path port to advertise to peers. It is omitted (0) when
// the manager is on DefaultPort, since peers assume the default when no port is sent; // the manager is on DefaultPort, since peers assume the default when no port is sent;
// only a non-default (collision-forced) port is announced explicitly. // only a non-default (collision-forced) port is announced explicitly.
func (p pqHandshaker) announcedPort() int { func (p pqHandshaker) announcedPort() uint16 {
if port := p.mgr.LocalPort(); port != DefaultPort { if port := p.mgr.LocalPort(); port != DefaultPort {
return port return uint16(port)
} }
return 0 return 0
} }
func (p pqHandshaker) OfferPayload(remoteKey string) ([]byte, int) { // OfferPayload builds the KEM offer to attach to an outgoing signalling offer for the
// peer, plus the data-path port to announce (0 when on DefaultPort). Payload is nil when
// this side has no offer to send.
func (p pqHandshaker) OfferPayload(remoteKey string) ([]byte, uint16) {
payload, err := p.mgr.SignalOffer(pqkem.RemoteID(remoteKey)) payload, err := p.mgr.SignalOffer(pqkem.RemoteID(remoteKey))
if err != nil { if err != nil {
log.Warnf("pqkem: build offer for %s: %v", remoteKey, err) log.Warnf("pqkem: build offer for %s: %v", remoteKey, err)
@@ -72,11 +75,16 @@ func (p pqHandshaker) OfferPayload(remoteKey string) ([]byte, int) {
return payload, p.announcedPort() return payload, p.announcedPort()
} }
// ShouldSendBootstrapOffer reports whether the controller should reply to the peer's
// KEM-less offer with its own bootstrap offer instead of an answer.
func (p pqHandshaker) ShouldSendBootstrapOffer(remoteKey string) bool { func (p pqHandshaker) ShouldSendBootstrapOffer(remoteKey string) bool {
return p.mgr.ShouldSendBootstrapOffer(pqkem.RemoteID(remoteKey)) return p.mgr.ShouldSendBootstrapOffer(pqkem.RemoteID(remoteKey))
} }
func (p pqHandshaker) AnswerPayload(remoteKey string, recvOffer []byte) ([]byte, int) { // AnswerPayload processes a received KEM offer (nil when absent) and returns the KEM
// answer to attach to the outgoing signalling answer, plus the data-path port to announce
// (0 when on DefaultPort). An empty offer is treated as a capability signal.
func (p pqHandshaker) AnswerPayload(remoteKey string, recvOffer []byte) ([]byte, uint16) {
if len(recvOffer) == 0 { if len(recvOffer) == 0 {
// Capability signal (responder side): the KEM offer flows initiator->responder, // Capability signal (responder side): the KEM offer flows initiator->responder,
// so if we are the responder for this peer (it is the KEM initiator by role) an // so if we are the responder for this peer (it is the KEM initiator by role) an
@@ -95,6 +103,8 @@ func (p pqHandshaker) AnswerPayload(remoteKey string, recvOffer []byte) ([]byte,
return payload, p.announcedPort() return payload, p.announcedPort()
} }
// OnAnswer feeds a received KEM answer (nil when absent) into the exchange. An empty
// answer to our offer is treated as a capability signal on the initiator side.
func (p pqHandshaker) OnAnswer(remoteKey string, recvAnswer []byte) { func (p pqHandshaker) OnAnswer(remoteKey string, recvAnswer []byte) {
if len(recvAnswer) == 0 { if len(recvAnswer) == 0 {
// Capability signal (initiator side): the KEM answer flows responder->initiator, // Capability signal (initiator side): the KEM answer flows responder->initiator,