mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-06 05:29:07 +02:00
[client] pqkem: address review findings on convergence, strict parsing and status
Bot review (CodeRabbit, cubic) on PR #7098 surfaced several real defects: - Strict() parsed the raw env value instead of the normalized one, so a mixed-case NB_PQ_MLKEM_STRICT such as "tRuE" silently disabled fail-closed mode. Parse the lower-cased value, matching Enabled(). - OnDataPathMessage labeled a data-path answer as "signal" in logs, mislabeling every rotation answer. Use the data-path label. - A failed sendAnswer aborted the whole connection setup, skipping the relay/ICE listeners; a transient signalling failure now still brings the local transport up (the peer retries the answer). - processOffer left the reserved exchange slot in place when Respond failed, so every retransmission of that offer was dropped forever. Clear the reservation on a pre-commit error so a retry can derive again. - Dropped a no-op time.Since that implied a convergence-latency metric that was never recorded, and the now-unused startedAt field. - The strict-mode status line asserted active blocking even for a peer that is simply offline; reword it to state that no PSK is established yet. - conn_test used t.Fatalf for conditions under test; use assert.
This commit is contained in:
@@ -296,16 +296,12 @@ 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"), nil); k != nil {
|
assert.Nil(t, conn.presharedKey([]byte("remote"), nil), "expected nil presharedKey when Rosenpass manages PSK")
|
||||||
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"), nil); k == nil {
|
assert.NotNil(t, conn.presharedKey([]byte("remote"), nil), "expected non-nil presharedKey before Rosenpass manages PSK")
|
||||||
t.Fatalf("expected non-nil presharedKey before Rosenpass manages PSK")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWGTimeoutTestConn(rosenpassEnabled bool, disconnected *[]string) *Conn {
|
func newWGTimeoutTestConn(rosenpassEnabled bool, disconnected *[]string) *Conn {
|
||||||
|
|||||||
@@ -171,10 +171,11 @@ func (h *Handshaker) handleRemoteOffer(remoteOfferAnswer OfferAnswer) {
|
|||||||
// Derive+store the KEM PSK (inside sendAnswer's AnswerPayload) BEFORE bringing up the
|
// Derive+store the KEM PSK (inside sendAnswer's AnswerPayload) BEFORE bringing up the
|
||||||
// connection: the relay/ICE workers configure the WG endpoint, which pulls the PSK
|
// connection: the relay/ICE workers configure the WG endpoint, which pulls the PSK
|
||||||
// for the first handshake. Notifying them first would race the KEM exchange and hand
|
// for the first handshake. Notifying them first would race the KEM exchange and hand
|
||||||
// the first handshake a not-yet-derived key.
|
// the first handshake a not-yet-derived key. A failure to signal the answer is
|
||||||
|
// transient (the peer retries), so still bring the local transport up over relay/ICE
|
||||||
|
// instead of aborting the whole setup.
|
||||||
if err := h.sendAnswer(&remoteOfferAnswer); err != nil {
|
if err := h.sendAnswer(&remoteOfferAnswer); err != nil {
|
||||||
h.log.Errorf("failed to send remote offer confirmation: %s", err)
|
h.log.Errorf("failed to send remote offer confirmation: %s", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
h.notifyListeners(&remoteOfferAnswer)
|
h.notifyListeners(&remoteOfferAnswer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ func (m *Manager) startExchangeLocked(remoteID RemoteID, viaSignal bool, ackID E
|
|||||||
m.exchanges[remoteID] = &exchangeCtl{
|
m.exchanges[remoteID] = &exchangeCtl{
|
||||||
id: id,
|
id: id,
|
||||||
state: stateAwaitingAnswer,
|
state: stateAwaitingAnswer,
|
||||||
startedAt: time.Now(),
|
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
lastSent: raw,
|
lastSent: raw,
|
||||||
initiator: init,
|
initiator: init,
|
||||||
@@ -118,11 +117,19 @@ func (m *Manager) processOffer(remoteID RemoteID, o *OfferMsg, via string) ([]by
|
|||||||
return last, nil
|
return last, nil
|
||||||
}
|
}
|
||||||
// Reserve the slot so a concurrent duplicate offer bails.
|
// Reserve the slot so a concurrent duplicate offer bails.
|
||||||
m.exchanges[remoteID] = &exchangeCtl{id: o.ExchangeID, state: stateReserved, startedAt: time.Now()}
|
m.exchanges[remoteID] = &exchangeCtl{id: o.ExchangeID, state: stateReserved}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
answerBytes, psk, err := Respond(o.KEMOffer, m.binding(remoteID))
|
answerBytes, psk, err := Respond(o.KEMOffer, m.binding(remoteID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Respond failed before the PSK was committed: clear the reservation so a
|
||||||
|
// retransmission of this offer (a transient or malformed first packet) can retry
|
||||||
|
// instead of hitting the stuck reserved slot forever.
|
||||||
|
m.mu.Lock()
|
||||||
|
if cur := m.exchanges[remoteID]; cur != nil && cur.id == o.ExchangeID && cur.state == stateReserved {
|
||||||
|
delete(m.exchanges, remoteID)
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
raw, err := (&AnswerMsg{ExchangeID: o.ExchangeID, KEMAnswer: answerBytes}).Encode()
|
raw, err := (&AnswerMsg{ExchangeID: o.ExchangeID, KEMAnswer: answerBytes}).Encode()
|
||||||
@@ -235,7 +242,6 @@ func (m *Manager) ackConverged(remoteID RemoteID, ackID ExchangeID) {
|
|||||||
delete(m.exchanges, remoteID)
|
delete(m.exchanges, remoteID)
|
||||||
m.established[remoteID] = true
|
m.established[remoteID] = true
|
||||||
m.failures[remoteID] = 0
|
m.failures[remoteID] = 0
|
||||||
_ = time.Since(ex.startedAt) // convergence latency (metrics hook, later step)
|
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
m.trace("pqkem: previous exchange confirmed by ack", "peer", remoteID, "exchange", idHex(ackID))
|
m.trace("pqkem: previous exchange confirmed by ack", "peer", remoteID, "exchange", idHex(ackID))
|
||||||
|
|||||||
@@ -43,15 +43,16 @@ const EnvStrict = "NB_PQ_MLKEM_STRICT"
|
|||||||
// Strict reports whether strict (fail-closed) mode is enabled via the environment.
|
// Strict reports whether strict (fail-closed) mode is enabled via the environment.
|
||||||
// An empty or unrecognized value is treated as disabled (opportunistic).
|
// An empty or unrecognized value is treated as disabled (opportunistic).
|
||||||
func Strict() bool {
|
func Strict() bool {
|
||||||
switch strings.ToLower(strings.TrimSpace(os.Getenv(EnvStrict))) {
|
raw := strings.ToLower(strings.TrimSpace(os.Getenv(EnvStrict)))
|
||||||
|
switch raw {
|
||||||
case "on":
|
case "on":
|
||||||
return true
|
return true
|
||||||
case "", "off":
|
case "", "off":
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
enabled, err := strconv.ParseBool(strings.TrimSpace(os.Getenv(EnvStrict)))
|
enabled, err := strconv.ParseBool(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to parse %s value %q: %v", EnvStrict, os.Getenv(EnvStrict), err)
|
log.Warnf("failed to parse %s value %q: %v", EnvStrict, raw, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return enabled
|
return enabled
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ const (
|
|||||||
type exchangeCtl struct {
|
type exchangeCtl struct {
|
||||||
id ExchangeID
|
id ExchangeID
|
||||||
state exchangeState
|
state exchangeState
|
||||||
startedAt time.Time
|
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
lastSent []byte
|
lastSent []byte
|
||||||
initiator *Initiator
|
initiator *Initiator
|
||||||
@@ -402,7 +401,7 @@ func (m *Manager) OnDataPathMessage(remoteID RemoteID, raw []byte) error {
|
|||||||
}
|
}
|
||||||
return m.pushDataPath(remoteID, answer)
|
return m.pushDataPath(remoteID, answer)
|
||||||
case MsgAnswer:
|
case MsgAnswer:
|
||||||
return m.processAnswer(remoteID, msg.(*AnswerMsg), viaSignalLabel)
|
return m.processAnswer(remoteID, msg.(*AnswerMsg), viaDataPathLabel)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unhandled data-path message type %d from %s", typ, remoteID)
|
return fmt.Errorf("unhandled data-path message type %d from %s", typ, remoteID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -793,7 +793,7 @@ func peerQuantumResistanceStatus(established, rosenpassEnabled, rosenpassPermiss
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case mlkemEnabled && mlkemStrict:
|
case mlkemEnabled && mlkemStrict:
|
||||||
return "false (ML-KEM strict: blocking peer traffic until the exchange converges)"
|
return "false (ML-KEM strict: no post-quantum PSK established for this peer yet)"
|
||||||
case mlkemEnabled:
|
case mlkemEnabled:
|
||||||
return "false (ML-KEM: not converged yet, or peer does not run the exchange)"
|
return "false (ML-KEM: not converged yet, or peer does not run the exchange)"
|
||||||
case rosenpassEnabled && rosenpassPermissive:
|
case rosenpassEnabled && rosenpassPermissive:
|
||||||
|
|||||||
Reference in New Issue
Block a user