Run hybrid handshakes: v3 holders initiate them, responders wait for msg3, initiators resend it until confirmed

This commit is contained in:
JackDoan committed 2026-10-14 11:03:04 -05:00
1 parent 5c5254dc18
commit b26c1e9c3e
8 files changed
+913 -36

No files matched your search

+8
View File
@@ -92,6 +92,14 @@ func (cs *ConnectionState) NextMessageCounter() (uint64, bool) {
return c, true
}
// heardFromPeer reports whether anything from the peer has decrypted under these
// keys, beyond the handshake messages the replay window was seeded with.
func (cs *ConnectionState) heardFromPeer(handshakeMessages uint64) bool {
cs.decryptLock.Lock()
defer cs.decryptLock.Unlock()
return cs.window.current > handshakeMessages
}
func (cs *ConnectionState) Curve() cert.Curve {
return cs.myCert.Curve()
}
+399
View File
@@ -0,0 +1,399 @@
//go:build e2e_testing
package e2e
import (
"net/netip"
"sync"
"testing"
"time"
"github.com/slackhq/nebula"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/e2e/router"
"github.com/slackhq/nebula/handshake/fragment"
"github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/udp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// hybridTally counts handshake packets by subtype and message.
type hybridTally struct {
sync.Mutex
hybrid, phase1, classical int
msg3 int
maxLen int
}
func (ht *hybridTally) observe(p *udp.Packet) {
h, _ := handshakeOf(p)
if h == nil {
return
}
ht.Lock()
defer ht.Unlock()
switch h.Subtype {
case header.HandshakeIXHybridMLKEM1024:
ht.hybrid++
ht.maxLen = max(ht.maxLen, len(p.Data))
if h.MessageCounter == 3 {
ht.msg3++
}
case header.HandshakeIXPSK2MLKEM1024:
ht.phase1++
default:
ht.classical++
}
}
func isType(p *udp.Packet, t header.MessageType) bool {
h := &header.H{}
return h.Parse(p.Data) == nil && h.Type == t
}
type hybridCA struct {
ca cert.Certificate
key []byte
}
func newHybridCA(t *testing.T) hybridCA {
t.Helper()
ca, _, key, _ := cert_test.NewTestCaCert(cert.Version3, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
return hybridCA{ca: ca, key: key}
}
// v2 returns the v2 form of the CA and its classical key, what a node that only has v2 certificates gets.
func (h hybridCA) v2() (cert.Certificate, []byte) {
return cert.V2Form(h.ca), h.key[:len(h.key)-cert.MLDSA87PrivateKeySize]
}
// trust returns a pki.ca override trusting the given CAs.
func trust(cas ...cert.Certificate) string {
var s string
for _, c := range cas {
b, err := c.MarshalPEM()
if err != nil {
panic(err)
}
s += string(b)
}
return s
}
func withCA(cfg m, cas ...cert.Certificate) m {
if cfg == nil {
cfg = m{}
}
cfg["pki"] = m{"ca": trust(cas...)}
return cfg
}
func newHybridPair(t *testing.T, mine, theirs m) (myControl, theirControl *nebula.Control, myVpn, theirVpn []netip.Prefix, myUdp, theirUdp netip.AddrPort, r *router.R) {
t.Helper()
return newHybridPairWithCA(t, newHybridCA(t), mine, theirs)
}
func newHybridPairWithCA(t *testing.T, ca hybridCA, mine, theirs m) (myControl, theirControl *nebula.Control, myVpn, theirVpn []netip.Prefix, myUdp, theirUdp netip.AddrPort, r *router.R) {
t.Helper()
myControl, myVpn, myUdp, _ = newSimpleServer(cert.Version3, ca.ca, ca.key, "me", "10.128.0.1/24", mine)
theirControl, theirVpn, theirUdp, _ = newSimpleServer(cert.Version3, ca.ca, ca.key, "them", "10.128.0.2/24", theirs)
myControl.InjectLightHouseAddr(theirVpn[0].Addr(), theirUdp)
r = router.NewR(t, myControl, theirControl)
myControl.Start()
theirControl.Start()
return
}
func routeUntilData(t *testing.T, r *router.R, to *nebula.Control, timeout time.Duration, fn func(p *udp.Packet, receiver *nebula.Control) router.ExitType) bool {
t.Helper()
return r.RouteForAllExitFuncOrTimeout(timeout, func(p *udp.Packet, receiver *nebula.Control) router.ExitType {
if fn != nil {
if e := fn(p, receiver); e != router.KeepRouting {
return e
}
}
if isTunnelData(p, receiver, to) {
return router.RouteAndExit
}
return router.KeepRouting
})
}
func TestHybridHandshake(t *testing.T) {
t.Parallel()
myControl, theirControl, myVpn, theirVpn, myUdp, theirUdp, r := newHybridPair(t, pqConfig(true, true, false), pqConfig(false, true, false))
defer r.RenderFlow()
var tally hybridTally
var recvErrors int
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, func(p *udp.Packet, _ *nebula.Control) router.ExitType {
tally.observe(p)
if isType(p, header.RecvError) {
recvErrors++
}
return router.KeepRouting
}), "tunnel never came up")
assertUdpPacket(t, []byte("Hi from me"), theirControl.GetFromTun(true), myVpn[0].Addr(), theirVpn[0].Addr(), 80, 80)
assert.Zero(t, tally.classical+tally.phase1, "a node with a v3 certificate speaks the hybrid handshake")
assert.GreaterOrEqual(t, tally.hybrid, 7+10+2, "all three messages travel as several fragments")
assert.Positive(t, tally.msg3)
assert.LessOrEqual(t, tally.maxLen, fragment.MaxPacketLen)
assert.Zero(t, recvErrors)
assertHostInfoPair(t, myUdp, theirUdp, myVpn, theirVpn, myControl, theirControl)
assertTunnel(t, myVpn[0].Addr(), theirVpn[0].Addr(), myControl, theirControl, r)
for _, c := range []*nebula.Control{myControl, theirControl} {
assert.Empty(t, c.ListHostmapIndexes(true), "no pending index may outlive the handshake")
}
assert.Equal(t, cert.Version3, myControl.GetHostInfoByVpnAddr(theirVpn[0].Addr(), false).Cert.Version())
assert.Equal(t, cert.Version3, theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false).Cert.Version())
myControl.Stop()
theirControl.Stop()
}
// TestHybridConfirmationLost loses the responder's confirmation. The initiator
// keeps resending msg3, and the responder, already done, confirms again rather
// than leave the initiator to tear the tunnel down.
func TestHybridConfirmationLost(t *testing.T) {
t.Parallel()
// Three retries make the handshake timeout 400ms, so the wait below outlasts it.
mine := pqConfig(true, true, false)
mine["handshakes"].(m)["retries"] = 3
myControl, theirControl, myVpn, theirVpn, _, _, r := newHybridPair(t, mine, pqConfig(false, true, false))
defer r.RenderFlow()
var tally hybridTally
var droppedConfirms, confirms int
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpn[0].Addr(), 80, myVpn[0].Addr(), 80, []byte("Hi from me")))
r.RouteForAllExitFuncOrTimeout(2*time.Second, func(p *udp.Packet, receiver *nebula.Control) router.ExitType {
tally.observe(p)
if receiver == myControl && isType(p, header.Test) {
if droppedConfirms == 0 {
droppedConfirms++
return router.Drop
}
confirms++
return router.RouteAndExit
}
return router.KeepRouting
})
assert.Equal(t, 1, droppedConfirms)
assert.Positive(t, confirms, "the responder confirms again")
assert.Greater(t, tally.msg3, 4, "msg3 was resent with fresh symbols")
assertUdpPacket(t, []byte("Hi from me"), theirControl.GetFromTun(true), myVpn[0].Addr(), theirVpn[0].Addr(), 80, 80)
// Long past the handshake timeout, the tunnel must still be there.
time.Sleep(700 * time.Millisecond)
r.FlushAll()
assertTunnel(t, myVpn[0].Addr(), theirVpn[0].Addr(), myControl, theirControl, r)
myControl.Stop()
theirControl.Stop()
}
// TestHybridNeverConfirmed loses every msg3. The responder never completes, must
// not answer the initiator's data with RecvErrors meanwhile, and the initiator
// closes the tunnel at the handshake timeout.
func TestHybridNeverConfirmed(t *testing.T) {
t.Parallel()
mine := pqConfig(true, true, false)
mine["handshakes"].(m)["retries"] = 3
theirs := pqConfig(false, true, false)
theirs["handshakes"].(m)["retries"] = 3
myControl, theirControl, myVpn, theirVpn, _, _, r := newHybridPair(t, mine, theirs)
defer r.RenderFlow()
var recvErrors, dropped int
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpn[0].Addr(), 80, myVpn[0].Addr(), 80, []byte("Hi from me")))
r.RouteForAllExitFuncOrTimeout(1500*time.Millisecond, func(p *udp.Packet, receiver *nebula.Control) router.ExitType {
if h, fh := handshakeOf(p); fh != nil && h.MessageCounter == 3 {
dropped++
return router.Drop
}
if isType(p, header.RecvError) {
recvErrors++
}
return router.KeepRouting
})
assert.Greater(t, dropped, 4, "msg3 is resent")
assert.Zero(t, recvErrors, "data for a pending responder is dropped quietly")
assert.Nil(t, theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false), "the responder never completes")
assert.Nil(t, myControl.GetHostInfoByVpnAddr(theirVpn[0].Addr(), false), "the initiator gave up on the tunnel")
assert.Empty(t, theirControl.ListHostmapIndexes(true), "the pending responder expired")
myControl.Stop()
theirControl.Stop()
}
// TestHybridToV2Peer has a v3 initiator meet a peer that speaks phase 1 post-quantum
// handshakes but holds only a v2 certificate. Holding v3, the initiator never
// offers phase 1, so the tunnel comes up classical on the v2 form.
func TestHybridToV2Peer(t *testing.T) {
t.Parallel()
ca := newHybridCA(t)
ca2, key2 := ca.v2()
myControl, myVpn, myUdp, _ := newSimpleServer(cert.Version3, ca.ca, ca.key, "me", "10.128.0.1/24", withCA(pqConfig(true, true, false), ca.ca, ca2))
theirControl, theirVpn, theirUdp, _ := newSimpleServer(cert.Version2, ca2, key2, "them", "10.128.0.2/24", pqConfig(true, true, false))
myControl.InjectLightHouseAddr(theirVpn[0].Addr(), theirUdp)
r := router.NewR(t, myControl, theirControl)
defer r.RenderFlow()
myControl.Start()
theirControl.Start()
var tally hybridTally
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, func(p *udp.Packet, _ *nebula.Control) router.ExitType {
tally.observe(p)
return router.KeepRouting
}), "tunnel never came up")
assertUdpPacket(t, []byte("Hi from me"), theirControl.GetFromTun(true), myVpn[0].Addr(), theirVpn[0].Addr(), 80, 80)
assert.Positive(t, tally.hybrid, "the hybrid attempt comes first")
assert.Zero(t, tally.phase1, "a v3 holder never initiates phase 1")
assert.Positive(t, tally.classical)
assertHostInfoPair(t, myUdp, theirUdp, myVpn, theirVpn, myControl, theirControl)
assert.Equal(t, cert.Version2, theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false).Cert.Version())
assert.Empty(t, myControl.ListHostmapIndexes(true))
assert.Empty(t, theirControl.ListHostmapIndexes(true))
myControl.Stop()
theirControl.Stop()
}
// TestHybridDistrustedV3 has an initiator that trusts only the v2 form of the CA.
// Its hybrid attempt fails verifying the responder's v3 certificate, which must
// leave the classical attempt going rather than end the handshake.
func TestHybridDistrustedV3(t *testing.T) {
t.Parallel()
ca := newHybridCA(t)
ca2, _ := ca.v2()
myControl, theirControl, myVpn, theirVpn, _, _, r := newHybridPairWithCA(t, ca,
withCA(pqConfig(true, true, false), ca2),
withCA(pqConfig(false, true, false), ca.ca, ca2),
)
defer r.RenderFlow()
var tally hybridTally
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, func(p *udp.Packet, _ *nebula.Control) router.ExitType {
tally.observe(p)
return router.KeepRouting
}), "tunnel never came up")
assertUdpPacket(t, []byte("Hi from me"), theirControl.GetFromTun(true), myVpn[0].Addr(), theirVpn[0].Addr(), 80, 80)
assert.Positive(t, tally.hybrid)
assert.Positive(t, tally.classical)
assert.Equal(t, cert.Version2, myControl.GetHostInfoByVpnAddr(theirVpn[0].Addr(), false).Cert.Version())
assert.Empty(t, myControl.ListHostmapIndexes(true))
myControl.Stop()
theirControl.Stop()
}
// TestHybridRequire checks require with a v3 certificate means hybrid only.
func TestHybridRequire(t *testing.T) {
t.Parallel()
t.Run("both", func(t *testing.T) {
myControl, theirControl, myVpn, theirVpn, _, _, r := newHybridPair(t, pqRequire(), pqRequire())
defer r.RenderFlow()
var tally hybridTally
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, func(p *udp.Packet, _ *nebula.Control) router.ExitType {
tally.observe(p)
return router.KeepRouting
}), "tunnel never came up")
assertUdpPacket(t, []byte("Hi from me"), theirControl.GetFromTun(true), myVpn[0].Addr(), theirVpn[0].Addr(), 80, 80)
assert.Zero(t, tally.classical+tally.phase1)
assertTunnel(t, myVpn[0].Addr(), theirVpn[0].Addr(), myControl, theirControl, r)
myControl.Stop()
theirControl.Stop()
})
t.Run("responder refuses phase 1", func(t *testing.T) {
ca := newHybridCA(t)
ca2, key2 := ca.v2()
myControl, myVpn, _, _ := newSimpleServer(cert.Version2, ca2, key2, "me", "10.128.0.1/24", pqRequire())
theirControl, theirVpn, theirUdp, _ := newSimpleServer(cert.Version3, ca.ca, ca.key, "them", "10.128.0.2/24", withCA(pqRequire(), ca.ca, ca2))
myControl.InjectLightHouseAddr(theirVpn[0].Addr(), theirUdp)
r := router.NewR(t, myControl, theirControl)
defer r.RenderFlow()
myControl.Start()
theirControl.Start()
var tally hybridTally
var answers int
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpn[0].Addr(), 80, myVpn[0].Addr(), 80, []byte("Hi from me")))
r.RouteForAllExitFuncOrTimeout(time.Second, func(p *udp.Packet, receiver *nebula.Control) router.ExitType {
tally.observe(p)
if h, _ := handshakeOf(p); h != nil && h.MessageCounter == 2 {
answers++
}
return router.KeepRouting
})
assert.Positive(t, tally.phase1)
assert.Zero(t, answers, "a phase 1 msg1 gets no answer from a v3 node requiring post-quantum")
assert.Nil(t, theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false))
myControl.Stop()
theirControl.Stop()
})
}
func TestHybridRelays(t *testing.T) {
t.Parallel()
ca := newHybridCA(t)
useRelays := pqConfig(true, true, false)
useRelays["relay"] = m{"use_relays": true}
amRelay := pqConfig(true, true, false)
amRelay["relay"] = m{"am_relay": true}
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version3, ca.ca, ca.key, "me ", "10.128.0.1/24", useRelays)
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version3, ca.ca, ca.key, "relay ", "10.128.0.128/24", amRelay)
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version3, ca.ca, ca.key, "them ", "10.128.0.2/24", useRelays)
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
r := router.NewR(t, myControl, relayControl, theirControl)
defer r.RenderFlow()
myControl.Start()
relayControl.Start()
theirControl.Start()
var relayedMsg3, relayedClassical int
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
require.True(t, routeUntilData(t, r, theirControl, 10*time.Second, func(p *udp.Packet, _ *nebula.Control) router.ExitType {
outer := &header.H{}
if outer.Parse(p.Data) == nil && outer.Type == header.Message && outer.Subtype == header.MessageRelay {
if h, fh := handshakeOf(p); h != nil && fh != nil {
assert.LessOrEqual(t, len(p.Data), fragment.MaxPacketLen+32, "a relayed fragment fits the IPv6 minimum MTU")
if h.MessageCounter == 3 {
relayedMsg3++
}
} else if h != nil {
relayedClassical++
}
}
return router.KeepRouting
}), "tunnel never came up")
assertUdpPacket(t, []byte("Hi from me"), theirControl.GetFromTun(true), myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
assert.Positive(t, relayedMsg3, "msg3 goes through the relay too")
assert.Zero(t, relayedClassical)
r.RenderHostmaps("Final hostmaps", myControl, relayControl, theirControl)
myControl.Stop()
relayControl.Stop()
theirControl.Stop()
}
+6
View File
@@ -134,6 +134,12 @@ func (m *Machine) Subtype() header.MessageSubType {
return m.subtype
}
// PeerCert returns the peer's verified certificate once a message carrying it
// has been read, which for a three message pattern is before completion.
func (m *Machine) PeerCert() *cert.CachedCertificate {
return m.result.RemoteCert
}
// MessageIndex returns the noise handshake message index, which equals the
// wire counter of the most recently sent or received message.
func (m *Machine) MessageIndex() int {
+63 -11
View File
@@ -90,6 +90,8 @@ type HandshakeManager struct {
// pqCookies and pqCookieLimiter serve cookies while pqTable is under load.
pqCookies *pqCookies
pqCookieLimiter *ratelimit.Limiter
// hybrid holds hybrid handshakes waiting on their third message.
hybrid hybridState
}
type HandshakeHostInfo struct {
@@ -107,6 +109,9 @@ type HandshakeHostInfo struct {
machine *handshake.Machine // The handshake state machine, set during stage 0 (initiator) or beginHandshake (responder multi-message)
pq *pqAttempt // The initiator's post-quantum attempt, pending alongside the classical one
lastPQ bool // Whether the latest attempt was post-quantum
noPQ bool // The post-quantum attempt failed for good, only classical is left
responder *hybridResponder // Set for a responder's hybrid handshake waiting for msg3
}
func (hh *HandshakeHostInfo) cachePacket(l *slog.Logger, t header.MessageType, st header.MessageSubType, packet []byte, f packetCallback, m *cachedPacketMetrics) {
@@ -150,6 +155,7 @@ func NewHandshakeManager(l *slog.Logger, mainHostMap *HostMap, lightHouse *Light
l: l,
pqTable: fragment.NewTable[pqFragKey, pqFragSource](pqTableConfig),
pqAnswered: newPQAnswered(hsTimeout(config.retries, config.tryInterval)),
hybrid: newHybridState(),
}
pq := config.pq
hm.pq.Store(&pq)
@@ -174,6 +180,8 @@ func (hm *HandshakeManager) Run(ctx context.Context) {
hm.NextOutboundHandshakeTimerTick(now)
hm.pqTable.Expire(now)
hm.pqAnswered.expire(now)
hm.expireHybridResponders(now)
hm.tickHybridConfirms(now)
}
}
}
@@ -183,7 +191,7 @@ func (hm *HandshakeManager) HandleIncoming(via ViaSender, packet []byte, h *head
// don't yet support) are dropped here rather than silently routed through
// the IX path. Add a case when introducing a new pattern.
switch h.Subtype {
case header.HandshakeIXPSK0, pqSubtype:
case header.HandshakeIXPSK0, header.HandshakeIXPSK2MLKEM1024, hybridSubtype:
// supported
default:
hm.l.Debug("dropping handshake with unsupported subtype",
@@ -775,6 +783,28 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
cs := f.pki.getCertState()
v := cs.DefaultVersion()
allocIndex := func() (uint32, error) { return generateIndex(f.l) }
// A hybrid responder answers with its v3 certificate and has to wait for msg3,
// so its index is registered right away for msg3 to find.
var pending *HandshakeHostInfo
if h.Subtype == hybridSubtype {
v = cert.Version3
pending = &HandshakeHostInfo{
hostinfo: &HostInfo{
HandshakePacket: make(map[uint8][]byte, 0),
relayState: RelayState{
relays: nil,
relayForByAddr: map[netip.Addr]*Relay{},
relayForByIdx: map[uint32]*Relay{},
},
},
startTime: time.Now(),
responder: &hybridResponder{},
}
allocIndex = func() (uint32, error) { return hm.allocateIndex(pending) }
}
if cs.GetCredential(v) == nil {
f.l.Error("Unable to handshake with host because no certificate is available",
"from", via, "certVersion", v)
@@ -783,7 +813,7 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
machine, err := handshake.NewMachine(
v, cs.GetCredential,
hm.certVerifier(), func() (uint32, error) { return generateIndex(f.l) },
hm.certVerifier(), allocIndex,
false, h.Subtype,
)
if err != nil {
@@ -793,10 +823,18 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
response, result, err := machine.ProcessPacket(nil, packet)
if err != nil {
if pending != nil {
hm.DeleteHostInfo(pending.hostinfo)
}
f.l.Error("Failed to process handshake packet", "from", via, "error", err)
return
}
if result == nil && pending != nil {
hm.awaitMsg3(via, pending, machine, packet, response, received)
return
}
if result == nil {
// Multi-message pattern: the responder Machine would need to be
// registered in hm.indexes so a future inbound packet finds it via
@@ -938,22 +976,28 @@ func (hm *HandshakeManager) processResponse(via ViaSender, hh *HandshakeHostInfo
response, result, err := machine.ProcessPacket(nil, packet)
if err != nil {
// Recoverable errors are routine noise, log at Debug. Fatal errors get a Warn.
if machine.Failed() {
if !machine.Failed() {
f.l.Debug("Failed to process handshake packet",
"vpnAddrs", hostinfo.vpnAddrs, "from", via, "error", err)
} else if hh.pq != nil && machine == hh.pq.machine && !hm.pqConfig().require {
// A post-quantum attempt can fail where the classical one would not, a v3
// certificate from a CA only trusted in its v2 form for one. Keep going
// with classical alone.
f.l.Warn("Failed to process post-quantum handshake packet, continuing with classical",
"vpnAddrs", hostinfo.vpnAddrs, "from", via, "error", err)
hm.abandonPQ(hh)
} else {
f.l.Warn("Failed to process handshake packet, abandoning",
"vpnAddrs", hostinfo.vpnAddrs, "from", via, "error", err)
hm.DeleteHostInfo(hostinfo)
} else {
f.l.Debug("Failed to process handshake packet",
"vpnAddrs", hostinfo.vpnAddrs, "from", via, "error", err)
}
return
}
if response != nil {
hm.sendHandshakeResponse(via, response, hostinfo, false, 0)
}
if result == nil {
if response != nil {
hm.sendHandshakeResponse(via, response, hostinfo, false, 0)
}
return
}
@@ -1068,6 +1112,12 @@ func (hm *HandshakeManager) processResponse(via ViaSender, hh *HandshakeHostInfo
hm.Complete(hostinfo, f)
hm.notePQPeer(machine.Subtype(), vpnAddrs)
// A three message pattern finishes with our msg3, which goes out before any
// cached packets do and keeps going until the responder is heard from.
if response != nil {
hm.startHybridConfirm(via, hostinfo, response, result.MessageIndex)
}
if len(hh.packetStore) > 0 {
if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("Sending stored packets", "count", len(hh.packetStore))
@@ -1166,7 +1216,9 @@ func (hm *HandshakeManager) sendHandshakeResponse(via ViaSender, msg []byte, hos
"initiatorIndex", hostinfo.remoteIndexId,
"responderIndex", hostinfo.localIndexId,
}
if peerCert := hostinfo.ConnectionState.peerCert; peerCert != nil {
if hostinfo.ConnectionState == nil {
// A hybrid responder answers msg1 before it has keys.
} else if peerCert := hostinfo.ConnectionState.peerCert; peerCert != nil {
logFields = append(logFields,
"certName", peerCert.Certificate.Name(),
"certVersion", peerCert.Certificate.Version(),
+380
View File
@@ -0,0 +1,380 @@
package nebula
import (
"context"
"errors"
"log/slog"
"sync"
"time"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/handshake"
"github.com/slackhq/nebula/handshake/fragment"
"github.com/slackhq/nebula/header"
)
// Hybrid handshakes (ix_hybrid_mlkem1024) take three messages, so unlike IX the
// responder has to wait for one more message and the initiator finishes by
// sending rather than receiving:
//
// - The responder answers msg1 with msg2 and keeps the handshake pending under
// its local index until msg3 arrives. Data for that index is dropped quietly
// meanwhile, it must not draw a RecvError. Once msg3 verifies the tunnel
// goes live and an empty TestReply tells the initiator so.
// - The initiator has the keys as soon as it writes msg3, so the tunnel goes
// live then, readable only by the real responder. msg3 is resent with fresh
// symbols until something from the responder decrypts, and if nothing does
// within the handshake timeout the tunnel is closed.
const (
hybridSubtype = header.HandshakeIXHybridMLKEM1024
// hybridMsg3MaxLen bounds the msg3 a pending responder reassembles. msg3 is
// a ciphertext and two tags, about 1.6KB.
hybridMsg3MaxLen = 4096
)
// hybridResponder is the responder half of a pending hybrid handshake, waiting
// for msg3.
type hybridResponder struct {
msg3 *fragment.Assembly
expires time.Time
}
// hybridConfirm is an initiator's completed hybrid handshake that has not yet
// heard from the responder.
type hybridConfirm struct {
hostinfo *HostInfo
sender *fragment.Sender
send func([]byte) error
handshakeLen uint64 // messages the handshake took, which the replay window was seeded with
deadline time.Time
next time.Time
attempts int
}
// hybridState holds the hybrid handshakes waiting on their last step.
type hybridState struct {
// responders maps local indexes to pending responders. Guarded by the
// HandshakeManager lock; the entries are also in its indexes.
responders map[uint32]*HandshakeHostInfo
confirmsMu sync.Mutex
confirms map[*HostInfo]*hybridConfirm
}
func newHybridState() hybridState {
return hybridState{
responders: map[uint32]*HandshakeHostInfo{},
confirms: map[*HostInfo]*hybridConfirm{},
}
}
// awaitMsg3 keeps a responder's hybrid handshake pending after msg1, and sends
// msg2. hh is already in the indexes under the local index the Machine
// allocated.
func (hm *HandshakeManager) awaitMsg3(via ViaSender, hh *HandshakeHostInfo, machine *handshake.Machine, packet, response []byte, received int) {
f := hm.f
hostinfo := hh.hostinfo
remoteCert := machine.PeerCert()
if remoteCert == nil {
f.l.Error("Handshake did not produce a peer certificate", "from", via)
hm.DeleteHostInfo(hostinfo)
return
}
vpnAddrs, anyVpnAddrsInCommon, ok := hm.validatePeerCert(via, remoteCert)
if !ok {
hm.DeleteHostInfo(hostinfo)
return
}
var rh header.H
if err := rh.Parse(response); err != nil {
hm.DeleteHostInfo(hostinfo)
return
}
now := time.Now()
hostinfo.vpnAddrs = vpnAddrs
hostinfo.remoteIndexId = rh.RemoteIndex
// packet aliases the listener's incoming buffer, so this copy must stay.
hostinfo.HandshakePacket[handshakePacketStage0] = append([]byte(nil), packet[header.Len:]...)
hostinfo.HandshakePacket[handshakePacketStage2] = response
hostinfo.remotes = f.lightHouse.QueryCache(vpnAddrs)
if !via.IsRelayed {
hostinfo.SetRemote(via.UdpAddr)
}
hh.machine = machine
hh.responder.expires = now.Add(hsTimeout(hm.config.retries, hm.config.tryInterval))
hm.Lock()
hm.hybrid.responders[hostinfo.localIndexId] = hh
hm.Unlock()
msg := "Handshake message received"
if !anyVpnAddrsInCommon {
msg = "Handshake message received, but no vpnNetworks in common."
}
f.l.Info(msg,
"vpnAddrs", vpnAddrs,
"from", via,
"certName", remoteCert.Certificate.Name(),
"certVersion", remoteCert.Certificate.Version(),
"fingerprint", remoteCert.Fingerprint,
"issuer", remoteCert.Certificate.Issuer(),
"initiatorIndex", hostinfo.remoteIndexId,
"responderIndex", hostinfo.localIndexId,
"handshake", m{"stage": uint64(machine.MessageIndex()), "style": header.SubTypeName(header.Handshake, machine.Subtype())},
)
hm.sendHandshakeResponse(via, response, hostinfo, false, received)
hm.pqAnswered.add(packet[header.Len:], hostinfo, now)
}
// continueHybridResponder collects msg3 fragments for a pending responder and
// completes the tunnel once they decode and verify.
func (hm *HandshakeManager) continueHybridResponder(via ViaSender, h *header.H, fh fragment.Header, sym []byte) {
hh := hm.queryIndex(h.RemoteIndex)
if hh == nil || hh.responder == nil {
hm.reconfirmHybrid(h.RemoteIndex)
return
}
if fh.MsgLen > hybridMsg3MaxLen {
return
}
hh.Lock()
defer hh.Unlock()
if !hm.stillPending(hh, h.RemoteIndex) || fh.FragID != hh.hostinfo.remoteIndexId {
return
}
r := hh.responder
if r.msg3 == nil {
a, err := fragment.NewAssembly(fh)
if err != nil {
return
}
r.msg3 = a
}
ready, err := r.msg3.Add(fh, sym)
if errors.Is(err, fragment.ErrMismatch) {
r.msg3 = nil
return
}
if err != nil || !ready {
return
}
msg, err := r.msg3.Decode()
r.msg3 = nil
if err != nil {
hm.l.Debug("failed to decode handshake fragments", "from", via, "error", err)
return
}
_, result, err := hh.machine.ProcessPacket(nil, reassembled(h, msg))
if err != nil {
if hh.machine.Failed() {
hm.l.Warn("Failed to process handshake packet, abandoning",
"vpnAddrs", hh.hostinfo.vpnAddrs, "from", via, "error", err)
hm.forgetHybridResponder(hh)
} else {
hm.l.Debug("Failed to process handshake packet",
"vpnAddrs", hh.hostinfo.vpnAddrs, "from", via, "error", err)
}
return
}
if result == nil {
return
}
hm.completeHybridResponder(via, hh, result)
}
// completeHybridResponder brings up the tunnel of a responder whose msg3 verified.
// The caller must hold hh.Lock.
func (hm *HandshakeManager) completeHybridResponder(via ViaSender, hh *HandshakeHostInfo, result *handshake.Result) {
f := hm.f
hostinfo := hh.hostinfo
machine := hh.machine
defer hm.forgetHybridResponder(hh)
cs, err := newConnectionStateFromResult(result)
if err != nil {
f.l.Error("Discarding handshake with an invalid message index", "error", err, "vpnAddrs", hostinfo.vpnAddrs)
return
}
hostinfo.ConnectionState = cs
hostinfo.remoteIndexId = result.RemoteIndex
hostinfo.lastHandshakeTime = result.HandshakeTime
if !via.IsRelayed {
hostinfo.SetRemote(via.UdpAddr)
} else {
hostinfo.relayState.InsertRelayTo(via.relayHI.vpnAddrs[0])
}
remoteCert := result.RemoteCert
hostinfo.buildNetworks(f.myVpnNetworksTable, remoteCert.Certificate)
existing, err := hm.CheckAndComplete(hostinfo, handshakePacketStage0, f)
if err != nil {
hm.handleCheckAndCompleteError(err, existing, hostinfo, via, machine.Subtype(), 0)
return
}
f.l.Info("Handshake completed",
"vpnAddrs", hostinfo.vpnAddrs,
"from", via,
"certName", remoteCert.Certificate.Name(),
"certVersion", remoteCert.Certificate.Version(),
"fingerprint", remoteCert.Fingerprint,
"issuer", remoteCert.Certificate.Issuer(),
"initiatorIndex", result.RemoteIndex,
"responderIndex", result.LocalIndex,
"handshake", m{"stage": uint64(machine.MessageIndex()), "style": header.SubTypeName(header.Handshake, machine.Subtype())},
"durationNs", time.Since(hh.startTime).Nanoseconds(),
)
// Let the initiator know, even if there is no traffic to do it.
hostinfo.handshakeResendAt.Store(time.Now().UnixNano())
hm.sendHybridConfirmation(hostinfo)
hm.notePQPeer(machine.Subtype(), hostinfo.vpnAddrs)
hostinfo.remotes.RefreshFromHandshake(hostinfo.vpnAddrs)
if f.lightHouse.IsAnyLighthouseAddr(hostinfo.vpnAddrs) {
f.lightHouse.TriggerUpdate()
}
}
func (hm *HandshakeManager) sendHybridConfirmation(hostinfo *HostInfo) {
hm.f.send(header.Test, header.TestReply, hostinfo.ConnectionState, hostinfo, []byte{}, make([]byte, 12, 12), make([]byte, mtu))
}
// reconfirmHybrid answers a msg3 for a hybrid tunnel this responder already
// completed: the initiator is still resending it, so the confirmation was lost.
// Replies are rate limited to one per retry interval and go to the tunnel's
// remote, never to wherever the fragment claims to come from.
func (hm *HandshakeManager) reconfirmHybrid(index uint32) {
hostinfo := hm.mainHostMap.QueryIndex(index)
if hostinfo == nil || hostinfo.ConnectionState == nil || hostinfo.ConnectionState.initiator {
return
}
if pc := hostinfo.ConnectionState.peerCert; pc == nil || pc.Certificate.Version() != cert.Version3 {
return
}
now := time.Now().UnixNano()
last := hostinfo.handshakeResendAt.Load()
if now-last < int64(hm.config.tryInterval) || !hostinfo.handshakeResendAt.CompareAndSwap(last, now) {
return
}
hm.sendHybridConfirmation(hostinfo)
}
// forgetHybridResponder releases a pending responder's index.
func (hm *HandshakeManager) forgetHybridResponder(hh *HandshakeHostInfo) {
hm.Lock()
defer hm.Unlock()
idx := hh.hostinfo.localIndexId
if hm.indexes[idx] == hh {
delete(hm.indexes, idx)
}
if hm.hybrid.responders[idx] == hh {
delete(hm.hybrid.responders, idx)
}
}
// isHybridResponder reports whether index belongs to a responder still waiting
// for msg3, whose initiator may already be sending data.
func (hm *HandshakeManager) isHybridResponder(index uint32) bool {
hm.RLock()
defer hm.RUnlock()
_, ok := hm.hybrid.responders[index]
return ok
}
// expireHybridResponders drops responders that never got their msg3.
func (hm *HandshakeManager) expireHybridResponders(now time.Time) {
hm.Lock()
defer hm.Unlock()
for idx, hh := range hm.hybrid.responders {
if now.After(hh.responder.expires) {
if hm.indexes[idx] == hh {
delete(hm.indexes, idx)
}
delete(hm.hybrid.responders, idx)
}
}
}
// startHybridConfirm sends an initiator's msg3 and keeps resending it until the
// responder is heard from on the new tunnel.
func (hm *HandshakeManager) startHybridConfirm(via ViaSender, hostinfo *HostInfo, msg3 []byte, handshakeLen uint64) {
var h header.H
if err := h.Parse(msg3); err != nil {
return
}
s, err := fragment.NewSender(msg3[header.Len:], h.Subtype, h.RemoteIndex, h.MessageCounter, hostinfo.localIndexId)
if err != nil {
hostinfo.logger(hm.l).Error("Failed to fragment handshake", "error", err)
return
}
now := time.Now()
c := &hybridConfirm{
hostinfo: hostinfo,
sender: s,
send: hm.sendTo(via),
handshakeLen: handshakeLen,
deadline: now.Add(hsTimeout(hm.config.retries, hm.config.tryInterval)),
next: now.Add(hm.config.tryInterval),
attempts: 1,
}
hm.hybrid.confirmsMu.Lock()
hm.hybrid.confirms[hostinfo] = c
hm.hybrid.confirmsMu.Unlock()
hm.sendHybridMsg3(c)
}
func (hm *HandshakeManager) sendHybridMsg3(c *hybridConfirm) {
pkts, later, err := c.sender.Attempt(fragment.DefaultRepair(c.sender.Layout().K))
if err != nil {
return
}
hm.messageMetrics.Tx(header.Handshake, hybridSubtype, int64(len(pkts)+len(later)))
if err := hm.sendFragmented(pkts, later, c.send); err != nil {
c.hostinfo.logger(hm.l).Debug("Failed to send handshake message", "error", err)
}
}
// tickHybridConfirms resends msg3 to responders not heard from yet, and closes
// tunnels whose responder never was.
func (hm *HandshakeManager) tickHybridConfirms(now time.Time) {
var resend, closing []*hybridConfirm
hm.hybrid.confirmsMu.Lock()
for hostinfo, c := range hm.hybrid.confirms {
switch {
case hm.mainHostMap.QueryIndex(hostinfo.localIndexId) != hostinfo:
delete(hm.hybrid.confirms, hostinfo)
case hostinfo.ConnectionState.heardFromPeer(c.handshakeLen):
delete(hm.hybrid.confirms, hostinfo)
case now.After(c.deadline):
delete(hm.hybrid.confirms, hostinfo)
closing = append(closing, c)
case !now.Before(c.next):
c.attempts++
c.next = now.Add(hm.config.tryInterval * time.Duration(c.attempts))
resend = append(resend, c)
}
}
hm.hybrid.confirmsMu.Unlock()
for _, c := range resend {
if hm.l.Enabled(context.Background(), slog.LevelDebug) {
c.hostinfo.logger(hm.l).Debug("Resending handshake message, responder not heard from yet", "attempts", c.attempts)
}
hm.sendHybridMsg3(c)
}
for _, c := range closing {
c.hostinfo.logger(hm.l).Info("Responder never confirmed the handshake, closing tunnel",
"localIndex", c.hostinfo.localIndexId, "remoteIndex", c.hostinfo.remoteIndexId)
hm.f.closeTunnel(c.hostinfo)
}
}
+45 -15
View File
@@ -12,6 +12,7 @@ import (
"sync"
"time"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/handshake"
"github.com/slackhq/nebula/handshake/fragment"
@@ -19,9 +20,6 @@ import (
)
const (
// pqSubtype is the post-quantum handshake this node speaks.
pqSubtype = header.HandshakeIXPSK2MLKEM1024
// DefaultPQRepairDelay is how long repair fragments trail the data fragments.
DefaultPQRepairDelay = 10 * time.Millisecond
@@ -91,6 +89,7 @@ func (hm *HandshakeManager) reloadPQ(c *config.C) {
// local index, so whichever answer arrives first wins.
type pqAttempt struct {
machine *handshake.Machine
subtype header.MessageSubType
index uint32
sender *fragment.Sender
msg2 *fragment.Assembly
@@ -145,7 +144,7 @@ func reassembled(h *header.H, msg []byte) []byte {
func (hm *HandshakeManager) usePQ(hh *HandshakeHostInfo, vpnIp netip.Addr, triggered bool) bool {
cfg := hm.pqConfig()
switch {
case !cfg.initiate:
case !cfg.initiate, hh.noPQ:
return false
case cfg.require:
return true
@@ -199,21 +198,27 @@ func (hm *HandshakeManager) relayEstablished(target netip.Addr, relayHI *HostInf
})
}
// buildPQAttempt creates the initiator's PQ Machine and msg1 sender.
// buildPQAttempt creates the initiator's PQ Machine and msg1 sender. A node with
// a v3 certificate speaks the hybrid handshake, anyone else the ephemeral KEM one.
func (hm *HandshakeManager) buildPQAttempt(hh *HandshakeHostInfo) bool {
cs := hm.f.pki.getCertState()
v := hm.initiatingVersion(hh, cs)
subtype := header.HandshakeIXPSK2MLKEM1024
if cs.GetCredential(cert.Version3) != nil {
v = cert.Version3
subtype = hybridSubtype
}
if cs.GetCredential(v) == nil {
hm.f.l.Error("Unable to handshake with host because no certificate is available",
"vpnAddrs", hh.hostinfo.vpnAddrs, "certVersion", v)
return false
}
pq := &pqAttempt{}
pq := &pqAttempt{subtype: subtype}
machine, err := handshake.NewMachine(
v, cs.GetCredential,
hm.certVerifier(), func() (uint32, error) { return hm.allocatePQIndex(hh, pq) },
true, pqSubtype,
true, subtype,
)
if err != nil {
hm.f.l.Error("Failed to create handshake machine",
@@ -229,7 +234,7 @@ func (hm *HandshakeManager) buildPQAttempt(hh *HandshakeHostInfo) bool {
return false
}
sender, err := fragment.NewSender(msg[header.Len:], pqSubtype, 0, 1, pq.index)
sender, err := fragment.NewSender(msg[header.Len:], subtype, 0, 1, pq.index)
if err != nil {
hm.f.l.Error("Failed to fragment handshake",
"vpnAddrs", hh.hostinfo.vpnAddrs, "error", err)
@@ -271,6 +276,14 @@ func (hm *HandshakeManager) allocatePQIndex(hh *HandshakeHostInfo, pq *pqAttempt
return 0, errors.New("failed to generate unique localIndexId")
}
// abandonPQ drops a failed PQ attempt, leaving classical attempts only. The
// caller must hold hh.Lock.
func (hm *HandshakeManager) abandonPQ(hh *HandshakeHostInfo) {
hm.releasePQIndex(hh, hh.pq)
hh.pq = nil
hh.noPQ = true
}
func (hm *HandshakeManager) releasePQIndex(hh *HandshakeHostInfo, pq *pqAttempt) {
hm.Lock()
defer hm.Unlock()
@@ -314,10 +327,22 @@ func (hm *HandshakeManager) handleFragment(via ViaSender, packet []byte, h *head
switch h.MessageCounter {
case 1:
if !hm.pqConfig().accept {
cfg := hm.pqConfig()
if !cfg.accept {
hm.l.Debug("dropping post-quantum handshake, not accepting them", "from", via)
return
}
if h.Subtype == hybridSubtype || cfg.require {
hasV3 := hm.f.pki.getCertState().GetCredential(cert.Version3) != nil
if h.Subtype == hybridSubtype && !hasV3 {
hm.l.Debug("dropping hybrid handshake, no v3 certificate to answer it with", "from", via)
return
}
if h.Subtype != hybridSubtype && hasV3 {
hm.l.Debug("dropping post-quantum handshake, hybrid handshakes are required", "from", via)
return
}
}
if h.RemoteIndex != 0 {
hm.l.Debug("dropping stage-1 handshake with non-zero RemoteIndex",
"from", via, "remoteIndex", h.RemoteIndex)
@@ -326,7 +351,7 @@ func (hm *HandshakeManager) handleFragment(via ViaSender, packet []byte, h *head
now := time.Now()
key, src := pqFragKeyFor(via, fh.FragID)
if hm.pqTable.Load() >= pqCookieLoad && !(fh.HasCookie() && hm.pqCookies.valid(key, fh.Cookie, now)) {
hm.sendCookie(via, fh, key, len(packet), now)
hm.sendCookie(via, h.Subtype, fh, key, len(packet), now)
return
}
res, err := hm.pqTable.Add(key, src, now, fh, sym)
@@ -350,6 +375,11 @@ func (hm *HandshakeManager) handleFragment(via ViaSender, packet []byte, h *head
if hh := hm.queryIndex(h.RemoteIndex); hh != nil {
hm.continuePQ(via, hh, h, fh, sym)
}
case 3:
if h.Subtype == hybridSubtype {
hm.continueHybridResponder(via, h, fh, sym)
}
}
}
@@ -477,7 +507,7 @@ func (hm *HandshakeManager) sendFragmentedResponse(via ViaSender, msg []byte, ho
// notePQPeer remembers peers that completed a PQ handshake, for sticky mode.
func (hm *HandshakeManager) notePQPeer(subtype header.MessageSubType, vpnAddrs []netip.Addr) {
if !hm.pqConfig().sticky || subtype != pqSubtype {
if !hm.pqConfig().sticky || !header.IsFragmented(subtype) {
return
}
for _, a := range vpnAddrs {
@@ -625,7 +655,7 @@ func (c *pqCookies) valid(key pqFragKey, cookie fragment.Cookie, now time.Time)
// sendCookie answers a msg1 fragment that arrived without a valid cookie while
// under load. The reply is never larger than what triggered it, and replies are
// rate limited as a whole.
func (hm *HandshakeManager) sendCookie(via ViaSender, fh fragment.Header, key pqFragKey, received int, now time.Time) {
func (hm *HandshakeManager) sendCookie(via ViaSender, subtype header.MessageSubType, fh fragment.Header, key pqFragKey, received int, now time.Time) {
if received < pqCookieReplyLen {
return
}
@@ -634,9 +664,9 @@ func (hm *HandshakeManager) sendCookie(via ViaSender, fh fragment.Header, key pq
}
c := hm.pqCookies.make(key, now)
reply := make([]byte, header.Len, pqCookieReplyLen)
header.Encode(reply, header.Version, header.Handshake, pqSubtype, fh.FragID, pqCookieCounter)
header.Encode(reply, header.Version, header.Handshake, subtype, fh.FragID, pqCookieCounter)
reply = append(reply, c[:]...)
hm.f.messageMetrics.Tx(header.Handshake, pqSubtype, 1)
hm.f.messageMetrics.Tx(header.Handshake, subtype, 1)
if err := hm.sendTo(via)(reply); err != nil {
hm.l.Debug("Failed to send handshake cookie", "to", via, "error", err)
}
@@ -674,7 +704,7 @@ func (hm *HandshakeManager) handleCookie(via ViaSender, packet []byte, h *header
if err != nil {
return
}
hm.messageMetrics.Tx(header.Handshake, pqSubtype, int64(len(pkts)+len(later)))
hm.messageMetrics.Tx(header.Handshake, pq.subtype, int64(len(pkts)+len(later)))
if err := hm.sendFragmented(pkts, later, hm.sendTo(via)); err != nil {
hm.l.Debug("Failed to resend handshake with cookie", "to", via, "error", err)
}
+9 -9
View File
@@ -62,7 +62,7 @@ func fragmentIndexes(t *testing.T, pkts [][]byte, fragID, remoteIndex uint32) []
require.LessOrEqual(t, len(p), fragment.MaxPacketLen)
var h header.H
require.NoError(t, h.Parse(p))
assert.Equal(t, pqSubtype, h.Subtype)
assert.Equal(t, header.HandshakeIXPSK2MLKEM1024, h.Subtype)
assert.Equal(t, remoteIndex, h.RemoteIndex)
assert.Equal(t, uint64(2), h.MessageCounter)
fh, _, err := fragment.Parse(p[header.Len:])
@@ -80,7 +80,7 @@ func TestSendFragmentedResponse(t *testing.T) {
// A 2000 byte message is 2 symbols of 1000, so packets of 1024 bytes.
msg := make([]byte, header.Len, header.Len+2000)
header.Encode(msg, header.Version, header.Handshake, pqSubtype, 5, 2)
header.Encode(msg, header.Version, header.Handshake, header.HandshakeIXPSK2MLKEM1024, 5, 2)
r := rand.New(rand.NewPCG(1, 1))
for range 2000 {
msg = append(msg, byte(r.Uint32()))
@@ -183,7 +183,7 @@ func TestPQCookies(t *testing.T) {
// pqMsg1Fragment builds the first fragment of a msg1, optionally with a cookie.
func pqMsg1Fragment(t *testing.T, fragID uint32, cookie *fragment.Cookie) ([]byte, *header.H) {
t.Helper()
s, err := fragment.NewSender(make([]byte, 2000), pqSubtype, 0, 1, fragID)
s, err := fragment.NewSender(make([]byte, 2000), header.HandshakeIXPSK2MLKEM1024, 0, 1, fragID)
require.NoError(t, err)
if cookie != nil {
s.SetCookie(*cookie)
@@ -238,7 +238,7 @@ func TestPQCookieUnderLoad(t *testing.T) {
var rh header.H
require.NoError(t, rh.Parse(reply))
assert.Equal(t, header.Handshake, rh.Type)
assert.Equal(t, pqSubtype, rh.Subtype)
assert.Equal(t, header.HandshakeIXPSK2MLKEM1024, rh.Subtype)
assert.Equal(t, uint32(7), rh.RemoteIndex, "the reply names the initiator's FragID")
assert.Equal(t, uint64(pqCookieCounter), rh.MessageCounter)
copy(cookie[:], reply[header.Len:])
@@ -270,7 +270,7 @@ func TestPQCookieUnderLoad(t *testing.T) {
t.Run("a reply never outgrows what prompted it", func(t *testing.T) {
p, h := pqMsg1Fragment(t, 8, nil)
hm.sendCookie(via, fragment.Header{FragID: 8}, pqFragKey{}, pqCookieReplyLen-1, time.Now())
hm.sendCookie(via, header.HandshakeIXPSK2MLKEM1024, fragment.Header{FragID: 8}, pqFragKey{}, pqCookieReplyLen-1, time.Now())
assert.Empty(t, conn.take())
hm.handleFragment(via, p, h)
assert.Len(t, conn.take(), 1)
@@ -281,7 +281,7 @@ func TestPQHandleCookie(t *testing.T) {
hm, conn := newPQTestManager(t)
via := ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.1:4242")}
s, err := fragment.NewSender(make([]byte, 2000), pqSubtype, 0, 1, 99)
s, err := fragment.NewSender(make([]byte, 2000), header.HandshakeIXPSK2MLKEM1024, 0, 1, 99)
require.NoError(t, err)
_, _, err = s.Attempt(2)
require.NoError(t, err)
@@ -289,7 +289,7 @@ func TestPQHandleCookie(t *testing.T) {
hm.indexes[99] = hh
reply := make([]byte, header.Len, pqCookieReplyLen)
header.Encode(reply, header.Version, header.Handshake, pqSubtype, 99, pqCookieCounter)
header.Encode(reply, header.Version, header.Handshake, header.HandshakeIXPSK2MLKEM1024, 99, pqCookieCounter)
reply = append(reply, bytes16(0xab)...)
var h header.H
require.NoError(t, h.Parse(reply))
@@ -311,7 +311,7 @@ func TestPQHandleCookie(t *testing.T) {
t.Run("replies for other indexes are ignored", func(t *testing.T) {
other := append([]byte(nil), reply...)
header.Encode(other, header.Version, header.Handshake, pqSubtype, 98, pqCookieCounter)
header.Encode(other, header.Version, header.Handshake, header.HandshakeIXPSK2MLKEM1024, 98, pqCookieCounter)
var oh header.H
require.NoError(t, oh.Parse(other))
hh.pq.cookieResendAt = time.Time{}
@@ -393,6 +393,6 @@ func TestUsePQ(t *testing.T) {
set(pqHandshakeConfig{initiate: true, sticky: true})
assert.Equal(t, []bool{true, false}, attempts(false, false), "sticky alternates until the peer has done PQ")
hm.notePQPeer(pqSubtype, []netip.Addr{peer})
hm.notePQPeer(header.HandshakeIXPSK2MLKEM1024, []netip.Addr{peer})
assert.Equal(t, []bool{true, true, true}, attempts(false, false, false), "then only PQ")
}
+3 -1
View File
@@ -101,7 +101,9 @@ func (f *Interface) readOutsidePackets(via ViaSender, packet []byte, rxc *rxCont
// At this point we should have a valid existing tunnel, verify and send
// recvError if necessary
if hostinfo == nil || hostinfo.ConnectionState == nil {
if !via.IsRelayed {
// A hybrid responder waiting for msg3 already gets data from an initiator
// that has its keys. A RecvError would tear that new tunnel down.
if !via.IsRelayed && !f.handshakeManager.isHybridResponder(h.RemoteIndex) {
f.maybeSendRecvError(via.UdpAddr, h.RemoteIndex)
}
return