mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-28 07:19:08 +01:00
Addresses CI fixes
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user