mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-26 06:19:08 +01:00
[client] pqkem: check the glare resend carries the in-flight offer
The handshaker test only saw that an offer went out, so a fresh or empty KEM offer would have passed. The manager test pins the part the resend relies on: the same bytes while in flight, and a duplicate answered from the responder's cache.
This commit is contained in:
@@ -19,23 +19,26 @@ type bootstrapPQ struct {
|
|||||||
fakePQ
|
fakePQ
|
||||||
bootstrap bool
|
bootstrap bool
|
||||||
inFlight bool
|
inFlight bool
|
||||||
|
offer []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b bootstrapPQ) OfferPayload(string) ([]byte, uint16) { return b.offer, 0 }
|
||||||
|
|
||||||
func (b bootstrapPQ) ShouldSendBootstrapOffer(string) bool { return b.bootstrap }
|
func (b bootstrapPQ) ShouldSendBootstrapOffer(string) bool { return b.bootstrap }
|
||||||
func (b bootstrapPQ) SignalOfferInFlight(string) bool { return b.inFlight }
|
func (b bootstrapPQ) SignalOfferInFlight(string) bool { return b.inFlight }
|
||||||
|
|
||||||
// newControllerHandshaker builds a handshaker for the controller side of a peer pair and
|
// newControllerHandshaker builds a handshaker for the controller side of a peer pair and
|
||||||
// records the type of every signalling message it sends.
|
// records the body of every signalling message it sends.
|
||||||
func newControllerHandshaker(t *testing.T, pq PQHandshaker) (*Handshaker, func() []sProto.Body_Type) {
|
func newControllerHandshaker(t *testing.T, pq PQHandshaker) (*Handshaker, func() []*sProto.Body) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var sent []sProto.Body_Type
|
var sent []*sProto.Body
|
||||||
client := &signal.MockClient{
|
client := &signal.MockClient{
|
||||||
ReadyFunc: func() bool { return true },
|
ReadyFunc: func() bool { return true },
|
||||||
SendFunc: func(msg *sProto.Message) error {
|
SendFunc: func(msg *sProto.Message) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
sent = append(sent, msg.GetBody().GetType())
|
sent = append(sent, msg.GetBody())
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -44,13 +47,21 @@ func newControllerHandshaker(t *testing.T, pq PQHandshaker) (*Handshaker, func()
|
|||||||
config := ConnConfig{Key: "aaaa", LocalKey: "bbbb", PQ: pq} // LocalKey > Key: controller
|
config := ConnConfig{Key: "aaaa", LocalKey: "bbbb", PQ: pq} // LocalKey > Key: controller
|
||||||
relay := &WorkerRelay{relayManager: &relayClient.Manager{}}
|
relay := &WorkerRelay{relayManager: &relayClient.Manager{}}
|
||||||
h := NewHandshaker(log.WithField("test", t.Name()), config, NewSignaler(client, key), nil, relay, nil)
|
h := NewHandshaker(log.WithField("test", t.Name()), config, NewSignaler(client, key), nil, relay, nil)
|
||||||
return h, func() []sProto.Body_Type {
|
return h, func() []*sProto.Body {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
return append([]sProto.Body_Type(nil), sent...)
|
return append([]*sProto.Body(nil), sent...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bodyTypes(bodies []*sProto.Body) []sProto.Body_Type {
|
||||||
|
types := make([]sProto.Body_Type, 0, len(bodies))
|
||||||
|
for _, b := range bodies {
|
||||||
|
types = append(types, b.GetType())
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
// TestHandshaker_ControllerReoffersOnResponderOffer drives the responder-originated
|
// TestHandshaker_ControllerReoffersOnResponderOffer drives the responder-originated
|
||||||
// reconnect through the handshaker: when the KEM manager asks for a fresh bootstrap, the
|
// reconnect through the handshaker: when the KEM manager asks for a fresh bootstrap, the
|
||||||
// controller replies to the responder's KEM-less offer with its own offer and sends no
|
// controller replies to the responder's KEM-less offer with its own offer and sends no
|
||||||
@@ -60,7 +71,7 @@ func TestHandshaker_ControllerReoffersOnResponderOffer(t *testing.T) {
|
|||||||
|
|
||||||
h.handleRemoteOffer(OfferAnswer{})
|
h.handleRemoteOffer(OfferAnswer{})
|
||||||
|
|
||||||
assert.Equal(t, []sProto.Body_Type{sProto.Body_OFFER}, sent(),
|
assert.Equal(t, []sProto.Body_Type{sProto.Body_OFFER}, bodyTypes(sent()),
|
||||||
"the controller must reply with its own offer, not an answer")
|
"the controller must reply with its own offer, not an answer")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,14 +80,17 @@ func TestHandshaker_ControllerReoffersOnResponderOffer(t *testing.T) {
|
|||||||
// KEM-less answer could reach the responder before the KEM offer and let it bring WireGuard
|
// KEM-less answer could reach the responder before the KEM offer and let it bring WireGuard
|
||||||
// up with no PSK. It sends its in-flight offer again instead, in case the first send failed.
|
// up with no PSK. It sends its in-flight offer again instead, in case the first send failed.
|
||||||
func TestHandshaker_ControllerResendsOfferInGlare(t *testing.T) {
|
func TestHandshaker_ControllerResendsOfferInGlare(t *testing.T) {
|
||||||
h, sent := newControllerHandshaker(t, bootstrapPQ{inFlight: true})
|
inFlightOffer := []byte("in-flight KEM offer")
|
||||||
|
h, sent := newControllerHandshaker(t, bootstrapPQ{inFlight: true, offer: inFlightOffer})
|
||||||
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.Equal(t, []sProto.Body_Type{sProto.Body_OFFER}, sent(),
|
bodies := sent()
|
||||||
"in a glare the controller must resend its offer, not answer")
|
require.Len(t, bodies, 1, "in a glare the controller must send exactly one message")
|
||||||
|
assert.Equal(t, sProto.Body_OFFER, bodies[0].GetType(), "in a glare the controller must resend its offer, not answer")
|
||||||
|
assert.Equal(t, inFlightOffer, bodies[0].GetMlkemPayload(), "the resend must carry the in-flight KEM offer")
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +101,6 @@ func TestHandshaker_ControllerAnswersNonCapablePeer(t *testing.T) {
|
|||||||
|
|
||||||
h.handleRemoteOffer(OfferAnswer{})
|
h.handleRemoteOffer(OfferAnswer{})
|
||||||
|
|
||||||
assert.Equal(t, []sProto.Body_Type{sProto.Body_ANSWER}, sent(),
|
assert.Equal(t, []sProto.Body_Type{sProto.Body_ANSWER}, bodyTypes(sent()),
|
||||||
"without a KEM to run the controller must answer normally")
|
"without a KEM to run the controller must answer normally")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,3 +93,30 @@ func TestManager_SignalOfferInFlight(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, dB.SignalOfferInFlight("aaaa"), "a data-path rotation is not a signalling offer")
|
assert.False(t, dB.SignalOfferInFlight("aaaa"), "a data-path rotation is not a signalling offer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestManager_ResentSignalOfferIsTheSameExchange backs the glare resend: while our
|
||||||
|
// bootstrap offer awaits its answer, building the offer again must return the same bytes,
|
||||||
|
// not start a second exchange, and the responder must answer the duplicate from its cache
|
||||||
|
// so both sides still derive one PSK.
|
||||||
|
func TestManager_ResentSignalOfferIsTheSameExchange(t *testing.T) {
|
||||||
|
dA, dB, wgA, wgB, _ := pair(t) // dB ("bbbb") initiator
|
||||||
|
defer dA.Stop()
|
||||||
|
defer dB.Stop()
|
||||||
|
|
||||||
|
first, err := dB.SignalOffer("aaaa")
|
||||||
|
require.NoError(t, err)
|
||||||
|
resent, err := dB.SignalOffer("aaaa")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, first, resent, "a resend while in flight must carry the same offer")
|
||||||
|
|
||||||
|
answer, err := dA.SignalOnOffer("bbbb", first)
|
||||||
|
require.NoError(t, err)
|
||||||
|
duplicateAnswer, err := dA.SignalOnOffer("bbbb", resent)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, answer, duplicateAnswer, "the responder must answer a duplicate offer from its cache")
|
||||||
|
|
||||||
|
require.NoError(t, dB.SignalOnAnswer("aaaa", answer))
|
||||||
|
require.NoError(t, dB.SignalOnAnswer("aaaa", duplicateAnswer))
|
||||||
|
assert.NotEqual(t, PSK{}, wgB.psk("aaaa"))
|
||||||
|
assert.Equal(t, wgB.psk("aaaa"), wgA.psk("bbbb"), "a resent offer must still converge on one PSK")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user