mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 13:59:09 +02:00
[client] pqkem: resend the in-flight KEM offer during an offer glare
The offer exchange is installed before the Signal send, so a failed send leaves it in flight with nothing delivered. Ignoring the responder's offer then stalled the connection until the exchange timed out. Resending the same offer delivers it; the responder answers a duplicate from its cache.
This commit is contained in:
@@ -207,17 +207,21 @@ func (h *Handshaker) handleRemoteAnswer(remoteOfferAnswer OfferAnswer) {
|
|||||||
// The re-offer reuses our stable ICE session id, so the peer dedups repeats.
|
// The re-offer reuses our stable ICE session id, so the peer dedups repeats.
|
||||||
//
|
//
|
||||||
// Returns true when the caller must not answer the peer's offer: either a re-offer went
|
// Returns true when the caller must not answer the peer's offer: either a re-offer went
|
||||||
// out, or our own KEM offer is already in flight (an offer glare). In the glare the
|
// out, or our own KEM offer is already in flight (an offer glare). In the glare we send
|
||||||
// peer's credentials reach us in its answer to our offer, and a KEM-less answer from us
|
// that same offer again instead of answering: a KEM-less answer could let the peer bring
|
||||||
// could let the peer bring WireGuard up before it reads our offer, on no PSK at all.
|
// WireGuard up before it reads our offer, on no PSK at all. Resending covers an earlier
|
||||||
// Otherwise, as for a non-capable peer, it returns false and the caller answers normally:
|
// send that failed after the exchange was installed, and the peer answers a duplicate
|
||||||
// dropping the offer then would lose the only copy of the peer's ICE and relay details.
|
// from its cache. Otherwise, as for a non-capable peer, it returns false and the caller
|
||||||
|
// answers normally: the peer's offer is then the only copy of its ICE and relay details.
|
||||||
func (h *Handshaker) pqControllerReoffer() bool {
|
func (h *Handshaker) pqControllerReoffer() bool {
|
||||||
if h.config.PQ == nil || !isController(h.config) {
|
if h.config.PQ == nil || !isController(h.config) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if h.config.PQ.SignalOfferInFlight(h.config.Key) {
|
if h.config.PQ.SignalOfferInFlight(h.config.Key) {
|
||||||
h.log.Debugf("pqkem: controller received a responder offer while its own KEM offer is in flight, leaving it unanswered")
|
h.log.Debugf("pqkem: controller received a responder offer while its own KEM offer is in flight, resending that offer instead of answering")
|
||||||
|
if err := h.sendOffer(); err != nil {
|
||||||
|
h.log.Errorf("failed to resend KEM offer during an offer glare: %s", err)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if !h.config.PQ.ShouldSendBootstrapOffer(h.config.Key) {
|
if !h.config.PQ.ShouldSendBootstrapOffer(h.config.Key) {
|
||||||
|
|||||||
@@ -64,18 +64,19 @@ func TestHandshaker_ControllerReoffersOnResponderOffer(t *testing.T) {
|
|||||||
"the controller must reply with its own offer, not an answer")
|
"the controller must reply with its own offer, not an answer")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHandshaker_ControllerIgnoresResponderOfferInGlare covers an offer glare: the
|
// TestHandshaker_ControllerResendsOfferInGlare covers an offer glare: the controller's KEM
|
||||||
// controller's KEM offer and the responder's KEM-less offer cross. The controller must leave
|
// offer and the responder's KEM-less offer cross. The controller must not answer: a
|
||||||
// the responder's offer unanswered. A KEM-less answer could reach the responder before the
|
// KEM-less answer could reach the responder before the KEM offer and let it bring WireGuard
|
||||||
// KEM offer and let it bring WireGuard up with no PSK.
|
// up with no PSK. It sends its in-flight offer again instead, in case the first send failed.
|
||||||
func TestHandshaker_ControllerIgnoresResponderOfferInGlare(t *testing.T) {
|
func TestHandshaker_ControllerResendsOfferInGlare(t *testing.T) {
|
||||||
h, sent := newControllerHandshaker(t, bootstrapPQ{inFlight: true})
|
h, sent := newControllerHandshaker(t, bootstrapPQ{inFlight: true})
|
||||||
notified := false
|
notified := false
|
||||||
h.AddICEListener(func(*OfferAnswer) { notified = true }) // called synchronously
|
h.AddICEListener(func(*OfferAnswer) { notified = true }) // called synchronously
|
||||||
|
|
||||||
h.handleRemoteOffer(OfferAnswer{IceCredentials: IceCredentials{UFrag: "ufrag", Pwd: "pwd"}})
|
h.handleRemoteOffer(OfferAnswer{IceCredentials: IceCredentials{UFrag: "ufrag", Pwd: "pwd"}})
|
||||||
|
|
||||||
assert.Empty(t, sent(), "in a glare the controller must send neither an answer nor a second offer")
|
assert.Equal(t, []sProto.Body_Type{sProto.Body_OFFER}, sent(),
|
||||||
|
"in a glare the controller must resend its offer, not answer")
|
||||||
assert.False(t, notified, "in a glare the controller must not bring the connection up from the responder's offer")
|
assert.False(t, notified, "in a glare the controller must not bring the connection up from the responder's offer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user