From f6e5e17c5fb394f7ef115e15a31fd0d8c9eff9ea Mon Sep 17 00:00:00 2001 From: riccardom Date: Fri, 7 Aug 2026 17:03:32 +0200 Subject: [PATCH] Addresses CI fixes --- client/internal/engine.go | 10 ++- client/internal/peer/conn.go | 86 +++++++++++++++-------- client/internal/peer/conn_pq_test.go | 12 ++-- client/internal/peer/conn_test.go | 8 +-- client/internal/peer/handshaker.go | 34 +++++++-- client/internal/peer/signaler.go | 2 +- client/internal/pqkem/capability_test.go | 13 ++-- client/internal/pqkem/convergence_test.go | 7 +- client/internal/pqkem_adapter.go | 18 +++-- 9 files changed, 131 insertions(+), 59 deletions(-) diff --git a/client/internal/engine.go b/client/internal/engine.go index fa631f9d1..29559c9f9 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "math/rand" "net" "net/netip" @@ -2893,6 +2894,13 @@ func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) { 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{ IceCredentials: peer.IceCredentials{ UFrag: remoteCred.UFrag, @@ -2903,7 +2911,7 @@ func convertToOfferAnswer(msg *sProto.Message) (*peer.OfferAnswer, error) { RosenpassPubKey: rosenpassPubKey, RosenpassAddr: rosenpassAddr, MlkemPayload: msg.GetBody().GetMlkemPayload(), - MlkemPort: int(msg.GetBody().GetMlkemPort()), + MlkemPort: uint16(mlkemPort), RelaySrvAddress: msg.GetBody().GetRelayServerAddress(), RelaySrvIP: relayIP, SessionID: sessionID, diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 52447fb17..fffe72915 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -83,14 +83,14 @@ type RosenpassConfig struct { type PQHandshaker interface { // 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. - 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 // 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). ShouldSendBootstrapOffer(remoteKey string) bool // 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. - 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(remoteKey string, recvAnswer []byte) // PSK returns the peer's latest derived post-quantum PSK to program at WG @@ -463,6 +463,9 @@ func (conn *Conn) ConnID() id.ConnID { // configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected 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() defer conn.mu.Unlock() @@ -480,7 +483,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn 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.statusICE.SetConnected() - conn.updateIceState(iceConnInfo, time.Now()) + conn.updateIceState(iceConnInfo, pqOK, time.Now()) return } @@ -523,7 +526,7 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn updateTime := time.Now() conn.enableWgWatcherIfNeeded(updateTime) - presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey) + presharedKey := conn.presharedKey(iceConnInfo.RosenpassPubKey, pqPSK) if err = conn.endpointUpdater.ConfigureWGEndpoint(ep, presharedKey); err != nil { conn.handleConfigurationFailure(err, wgProxy) return @@ -539,11 +542,14 @@ func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConn conn.currentConnPriority = priority conn.statusICE.SetConnected() - conn.updateIceState(iceConnInfo, updateTime) + conn.updateIceState(iceConnInfo, pqOK, updateTime) conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr, updateTime) } 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() defer conn.mu.Unlock() @@ -570,7 +576,7 @@ func (conn *Conn) onICEStateDisconnected(sessionChanged bool) { // todo consider to move after the ConfigureWGEndpoint 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 { conn.Log.Errorf("failed to switch to relay conn: %v", err) } @@ -608,6 +614,9 @@ func (conn *Conn) onICEStateDisconnected(sessionChanged bool) { } 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() defer conn.mu.Unlock() @@ -636,7 +645,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String()) conn.setRelayedProxy(wgProxy) 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 } @@ -647,7 +656,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { } updateTime := time.Now() 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 { conn.Log.Warnf("Failed to close relay connection: %v", err) } @@ -666,7 +675,7 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) { conn.currentConnPriority = conntype.Relay conn.statusRelay.SetConnected() 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.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr, updateTime) } @@ -746,19 +755,15 @@ func (conn *Conn) RequestReoffer() { func (conn *Conn) onWGDisconnected(watcherCtx context.Context) { conn.mu.Lock() - defer conn.mu.Unlock() // watcherCtx guards against a stale watcher tearing down a connection that already superseded it. if conn.ctx.Err() != nil || watcherCtx.Err() != nil { + conn.mu.Unlock() return } 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 switch conn.currentConnPriority { case conntype.Relay: @@ -771,6 +776,15 @@ func (conn *Conn) onWGDisconnected(watcherCtx context.Context) { } 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 @@ -794,14 +808,14 @@ func (conn *Conn) escalateWGTimeoutLocked() { 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{ PubKey: conn.config.Key, ConnStatusUpdate: updateTime, ConnStatus: conn.evalStatus(), Relayed: conn.isRelayed(), RelayServerAddress: relayServerAddr, - RosenpassEnabled: conn.quantumResistant(rosenpassPubKey), + RosenpassEnabled: conn.quantumResistant(rosenpassPubKey, pqEstablished), } err := conn.statusRecorder.UpdatePeerRelayedState(peerState) @@ -810,7 +824,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{ PubKey: conn.config.Key, ConnStatusUpdate: updateTime, @@ -820,7 +834,7 @@ func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, updateTime time.Time) RemoteIceCandidateType: iceConnInfo.RemoteIceCandidateType, LocalIceCandidateEndpoint: iceConnInfo.LocalIceCandidateEndpoint, RemoteIceCandidateEndpoint: iceConnInfo.RemoteIceCandidateEndpoint, - RosenpassEnabled: conn.quantumResistant(iceConnInfo.RosenpassPubKey), + RosenpassEnabled: conn.quantumResistant(iceConnInfo.RosenpassPubKey, pqEstablished), } err := conn.statusRecorder.UpdatePeerICEState(peerState) @@ -1093,13 +1107,31 @@ func (conn *Conn) AgentVersionString() string { 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 // it here so the peer's next WireGuard handshake adopts it. Applied at peer-config // time (bootstrap / reconnect); steady-state rotation is pushed separately. if conn.config.PQ != nil { - if psk, ok := conn.config.PQ.PSK(conn.config.Key); ok { - return &psk + if pqPSK != nil { + return pqPSK } if conn.config.PQStrict && conn.pqStrictSentinelKey != nil { // Fail closed: program a non-matching sentinel so no session forms on a @@ -1153,16 +1185,8 @@ func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool { // quantumResistant reports whether the peer's tunnel is post-quantum protected, for // 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). -func (conn *Conn) quantumResistant(remoteRosenpassPubKey []byte) bool { - if isRosenpassEnabled(remoteRosenpassPubKey) { - return true - } - if conn.config.PQ != nil { - if _, ok := conn.config.PQ.PSK(conn.config.Key); ok { - return true - } - } - return false +func (conn *Conn) quantumResistant(remoteRosenpassPubKey []byte, pqEstablished bool) bool { + return isRosenpassEnabled(remoteRosenpassPubKey) || pqEstablished } func evalConnStatus(in connStatusInputs) guard.ConnStatus { diff --git a/client/internal/peer/conn_pq_test.go b/client/internal/peer/conn_pq_test.go index ffc6e8c32..c4edb3424 100644 --- a/client/internal/peer/conn_pq_test.go +++ b/client/internal/peer/conn_pq_test.go @@ -53,10 +53,12 @@ func TestConn_presharedKey_PQ(t *testing.T) { c.config.PQ = fakePQ{psk: derivedPSK, ok: true} c.config.PQStrict = strict if strict { - sentinel, _ := wgtypes.GenerateKey() + sentinel, err := wgtypes.GenerateKey() + require.NoError(t, err) c.pqStrictSentinelKey = &sentinel } - got := c.presharedKey(nil) + pqPSK, _ := c.pqPSK() + got := c.presharedKey(nil, pqPSK) require.NotNil(t, got) 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.config.PQ = fakePQ{ok: 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.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.PQStrict = true c.pqStrictSentinelKey = &sentinel - got := c.presharedKey(nil) + pqPSK, _ := c.pqPSK() + got := c.presharedKey(nil, pqPSK) require.NotNil(t, got) require.Equal(t, sentinel, *got, "strict must return the blocking sentinel") require.NotEqual(t, nbPSK, *got, "the sentinel must not be the ordinary key") diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index 49979ea83..aef7c7af6 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -255,8 +255,8 @@ func TestConn_presharedKey(t *testing.T) { } conn2.config.RosenpassConfig.PermissiveMode = test.conn2Permissive - conn1PresharedKey := conn1.presharedKey(conn2.config.RosenpassConfig.PubKey) - conn2PresharedKey := conn2.presharedKey(conn1.config.RosenpassConfig.PubKey) + conn1PresharedKey := conn1.presharedKey(conn2.config.RosenpassConfig.PubKey, nil) + conn2PresharedKey := conn2.presharedKey(conn1.config.RosenpassConfig.PubKey, nil) if test.conn1ExpectedInitialKey { if conn1PresharedKey == nil { @@ -294,14 +294,14 @@ func TestConn_presharedKey_RosenpassManaged(t *testing.T) { // When Rosenpass has already initialized the PSK for this peer, // presharedKey must return nil to avoid UpdatePeer overwriting it. 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) } // When Rosenpass hasn't taken over yet, presharedKey should provide // a non-nil initial key (deterministic or from NetBird PSK). 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") } } diff --git a/client/internal/peer/handshaker.go b/client/internal/peer/handshaker.go index 6046612cb..a18e59a69 100644 --- a/client/internal/peer/handshaker.go +++ b/client/internal/peer/handshaker.go @@ -47,7 +47,7 @@ type OfferAnswer struct { // 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. - MlkemPort int + MlkemPort uint16 // relay server address RelaySrvAddress string @@ -222,14 +222,38 @@ func (h *Handshaker) pqControllerReoffer() bool { // 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. -func (h *Handshaker) pqRegisterEndpoint(remotePort int) { - if h.config.PQ == nil || remotePort < 0 || remotePort > 65535 || len(h.config.WgConfig.AllowedIps) == 0 { +func (h *Handshaker) pqRegisterEndpoint(remotePort uint16) { + if h.config.PQ == nil { + return + } + overlay, ok := h.pqPeerOverlayAddr() + if !ok { return } // remotePort may be 0 (the peer omitted it, meaning the default port); the adapter // resolves 0 to DefaultPort. - addr := netip.AddrPortFrom(h.config.WgConfig.AllowedIps[0].Addr(), uint16(remotePort)) - h.config.PQ.SetRemoteAddr(h.config.Key, addr) + h.config.PQ.SetRemoteAddr(h.config.Key, netip.AddrPortFrom(overlay, remotePort)) +} + +// 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 { diff --git a/client/internal/peer/signaler.go b/client/internal/peer/signaler.go index e286d7f86..f42a6e028 100644 --- a/client/internal/peer/signaler.go +++ b/client/internal/peer/signaler.go @@ -64,7 +64,7 @@ func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string, RosenpassPubKey: offerAnswer.RosenpassPubKey, RosenpassAddr: offerAnswer.RosenpassAddr, MlkemPayload: offerAnswer.MlkemPayload, - MlkemPort: offerAnswer.MlkemPort, + MlkemPort: int(offerAnswer.MlkemPort), RelaySrvAddress: offerAnswer.RelaySrvAddress, RelaySrvIP: offerAnswer.RelaySrvIP, SessionID: sessionIDBytes, diff --git a/client/internal/pqkem/capability_test.go b/client/internal/pqkem/capability_test.go index b6a1fe74c..b1c29d582 100644 --- a/client/internal/pqkem/capability_test.go +++ b/client/internal/pqkem/capability_test.go @@ -3,6 +3,7 @@ package pqkem import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -19,8 +20,8 @@ func TestManager_NonCapablePeerNotOffered(t *testing.T) { offer, err := d.SignalOffer("aaaa") require.NoError(t, err) - require.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.Nil(t, offer, "a non-capable peer must not be offered a KEM exchange") + 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 @@ -42,8 +43,8 @@ func TestManager_MarkNonCapableCancelsInFlight(t *testing.T) { next, err := d.SignalOffer("aaaa") require.NoError(t, err) - require.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.Nil(t, next, "after learning non-capability the peer is no longer offered") + 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 @@ -60,6 +61,6 @@ func TestManager_EstablishedPeerNotDowngraded(t *testing.T) { // The peer keeps its derived PSK (MarkNonCapable is a no-op once established). psk, ok := dB.PSK("aaaa") - require.True(t, ok, "an established peer must keep its PSK despite a stray zero") - require.NotEqual(t, PSK{}, psk) + assert.True(t, ok, "an established peer must keep its PSK despite a stray zero") + assert.NotEqual(t, PSK{}, psk) } diff --git a/client/internal/pqkem/convergence_test.go b/client/internal/pqkem/convergence_test.go index fed15d0ea..c8f4f1599 100644 --- a/client/internal/pqkem/convergence_test.go +++ b/client/internal/pqkem/convergence_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -36,7 +37,7 @@ func TestManager_InitialTimeoutFailsImmediately(t *testing.T) { require.NoError(t, err) 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) { @@ -65,10 +66,10 @@ func TestManager_RekeyToleratesKFailures(t *testing.T) { require.NoError(t, err) 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. _, err := dB.startExchangeTest("aaaa", false, ExchangeID{}) 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) } diff --git a/client/internal/pqkem_adapter.go b/client/internal/pqkem_adapter.go index be5a99c1b..8814dc229 100644 --- a/client/internal/pqkem_adapter.go +++ b/client/internal/pqkem_adapter.go @@ -57,14 +57,17 @@ type pqHandshaker struct { // 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; // 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 { - return port + return uint16(port) } 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)) if err != nil { 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() } +// 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 { 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 { // 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 @@ -95,6 +103,8 @@ func (p pqHandshaker) AnswerPayload(remoteKey string, recvOffer []byte) ([]byte, 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) { if len(recvAnswer) == 0 { // Capability signal (initiator side): the KEM answer flows responder->initiator,