e2e: Back to back hybrid handshakes, and a classical attempt crossing a hybrid one

This commit is contained in:
JackDoan committed 2026-10-14 12:56:12 -05:00
1 parent bbade4dc9a
commit 5edcaefa33
1 file changed
+104
+104
View File
@@ -397,3 +397,107 @@ func TestHybridRelays(t *testing.T) {
relayControl.Stop() relayControl.Stop()
theirControl.Stop() theirControl.Stop()
} }
// TestHybridBackToBack rebuilds the tunnel more times than the responder's
// per-source quota allows messages in progress, faster than its table expires
// entries. The repair fragments trailing each completed msg1 must not count
// against that quota, or later handshakes fall back to classical.
func TestHybridBackToBack(t *testing.T) {
t.Parallel()
myControl, theirControl, myVpn, theirVpn, _, theirUdp, r := newHybridPair(t, pqConfig(true, true, false), pqConfig(false, true, false))
defer r.RenderFlow()
for i := range 6 {
// Closing the last tunnel forgets the peer's address.
myControl.InjectLightHouseAddr(theirVpn[0].Addr(), theirUdp)
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpn[0].Addr(), 80, myVpn[0].Addr(), 80, []byte("Hi from me")))
require.True(t, routeUntilData(t, r, theirControl, 5*time.Second, nil), "tunnel %d never came up", i)
theirControl.GetFromTun(true)
// Let the trailing repair fragments land before tearing down.
r.RouteForAllExitFuncOrTimeout(50*time.Millisecond, func(*udp.Packet, *nebula.Control) router.ExitType { return router.KeepRouting })
mine := myControl.GetHostInfoByVpnAddr(theirVpn[0].Addr(), false)
theirs := theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false)
require.NotNil(t, mine)
require.NotNil(t, theirs)
assert.Equal(t, cert.Version3, mine.Cert.Version(), "tunnel %d is hybrid", i)
assert.Equal(t, cert.Version3, theirs.Cert.Version(), "tunnel %d is hybrid", i)
myControl.CloseTunnel(theirVpn[0].Addr(), true)
theirControl.CloseTunnel(myVpn[0].Addr(), true)
}
myControl.Stop()
theirControl.Stop()
}
// TestHybridRaceWithClassical has the initiator's classical attempt reach the
// responder, which completes it at once, while the hybrid handshake is still
// waiting for msg3. The initiator gets the hybrid msg2 first and commits to it,
// so the responder must take the hybrid tunnel when msg3 arrives even though the
// classical one is newer.
func TestHybridRaceWithClassical(t *testing.T) {
t.Parallel()
// Both trust the v2 form of the CA too, or the classical attempt could not complete.
ca := newHybridCA(t)
ca2, _ := ca.v2()
myControl, theirControl, myVpn, theirVpn, _, _, r := newHybridPairWithCA(t, ca,
withCA(pqConfig(true, true, false), ca.ca, ca2),
withCA(pqConfig(false, true, false), ca.ca, ca2),
)
defer r.RenderFlow()
var heldMsg2 []*udp.Packet
var sawClassical, recvErrors int
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpn[0].Addr(), 80, myVpn[0].Addr(), 80, []byte("Hi from me")))
require.True(t, r.RouteForAllExitFuncOrTimeout(3*time.Second, func(p *udp.Packet, receiver *nebula.Control) router.ExitType {
h, fh := handshakeOf(p)
switch {
case h != nil && fh != nil && h.MessageCounter == 2:
// Hold the hybrid msg2 back until the classical attempt has gone through.
heldMsg2 = append(heldMsg2, p.Copy())
return router.Drop
case h != nil && fh == nil && h.MessageCounter == 1:
sawClassical++
return router.RouteAndExit
}
return router.KeepRouting
}), "no classical attempt")
require.Positive(t, sawClassical)
// Lose the classical msg2, so the initiator only ever completes the hybrid handshake.
r.RouteForAllExitFuncOrTimeout(50*time.Millisecond, func(p *udp.Packet, receiver *nebula.Control) router.ExitType {
if h, fh := handshakeOf(p); h != nil && fh == nil && h.MessageCounter == 2 {
return router.Drop
}
if h, fh := handshakeOf(p); h != nil && fh != nil && h.MessageCounter == 2 {
heldMsg2 = append(heldMsg2, p.Copy())
return router.Drop
}
return router.KeepRouting
})
require.NotNil(t, theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false), "the responder completed the classical handshake")
for _, p := range heldMsg2 {
r.InjectUDPPacket(theirControl, myControl, p)
}
require.True(t, routeUntilData(t, r, theirControl, 3*time.Second, func(p *udp.Packet, _ *nebula.Control) router.ExitType {
if isType(p, header.RecvError) {
recvErrors++
}
return router.KeepRouting
}), "data never arrived")
assertUdpPacket(t, []byte("Hi from me"), theirControl.GetFromTun(true), myVpn[0].Addr(), theirVpn[0].Addr(), 80, 80)
assert.Zero(t, recvErrors)
mine := myControl.GetHostInfoByVpnAddr(theirVpn[0].Addr(), false)
theirs := theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false)
require.NotNil(t, mine)
require.NotNil(t, theirs)
assert.Equal(t, cert.Version3, mine.Cert.Version())
assert.Equal(t, cert.Version3, theirs.Cert.Version(), "the responder's primary tunnel is the hybrid one")
assert.Equal(t, mine.LocalIndex, theirs.RemoteIndex)
myControl.Stop()
theirControl.Stop()
}