Introduces a forced WG handshake on initial MLKEM bootstrap.

To ensure two peers agree on a key, we need asymmetry. one peer is
the controller ("initiator") the other is the "responder".

Otherwise imagine two offers in parallel driving two answers at the same time

   A                   B
   | <----B-OFFER----- |
   | -----A-OFFER----> |
   |                   |
   |                   |
   ---------------------------------
  |****** ICE + WG Handshake ****** |
   ---------------------------------
   |                   |
   | <----B-ANSWER---- |
   | -----A-ANSWER---> |

PSK is derived on receive of offer, so A and B derive different PSKs.
When WG handshake takes place it picks misaligned PSKs.

So we impair the two nodes and only the offer of one of the two (the controller/initiator)
carries the KEM material.

This means that if the responder OFFER/ANSWER comes first, when the controller/initiator's one
completes (and the genuine PSK is shared between A and B, we need to force a new WG handshake with
the proper keys.
This commit is contained in:
riccardom
2026-08-06 14:42:07 +02:00
parent 7699697231
commit b5a72eca65
9 changed files with 103 additions and 15 deletions

View File

@@ -670,6 +670,14 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
conn.RequestReoffer()
}
},
// On a freshly bootstrapped PSK, force the peer's WireGuard session to
// re-handshake so it adopts the PSK even if the tunnel already came up on
// a pre-PQ key (KEM-vs-endpoint-config race).
rehandshake: func(remoteKey string) {
if conn, ok := e.peerStore.PeerConn(remoteKey); ok {
conn.ForcePQRehandshake()
}
},
}
e.pqkemManager = pqkem.NewManager(pqkem.LocalID(publicKey.String()), cbHandler, pqkem.NewLogger())
e.pqkemManager.Start(tr)