//go:build e2e_testing package e2e import ( "net/netip" "testing" "time" "github.com/slackhq/nebula" "github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/e2e/router" "github.com/slackhq/nebula/header" "github.com/slackhq/nebula/udp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // relayedHybridTunnel brings up a hybrid tunnel whose msg2 completes by the // relay: every direct msg2 fragment is lost, and drop decides what else is. It // returns the controls with the tunnel up and the first packet delivered. func relayedHybridTunnel(t *testing.T, drop func(p *udp.Packet, receiver *nebula.Control, them netip.AddrPort, me *nebula.Control) bool) (myControl, theirControl *nebula.Control, myVpn, theirVpn []netip.Prefix, myUdp, theirUdp netip.AddrPort, r *router.R) { t.Helper() ca := newHybridCA(t) useRelays := pqRequire() useRelays["relay"] = m{"use_relays": true} useRelays["counters"] = m{"try_promote": 5} amRelay := pqRequire() amRelay["relay"] = m{"am_relay": true} var relayControl *nebula.Control var relayVpn []netip.Prefix var relayUdp netip.AddrPort myControl, myVpn, myUdp, _ = newSimpleServer(cert.Version3, ca.ca, ca.key, "me ", "10.128.0.1/24", useRelays) relayControl, relayVpn, relayUdp, _ = newSimpleServer(cert.Version3, ca.ca, ca.key, "relay ", "10.128.0.128/24", amRelay) theirControl, theirVpn, theirUdp, _ = newSimpleServer(cert.Version3, ca.ca, ca.key, "them ", "10.128.0.2/24", useRelays) myControl.InjectLightHouseAddr(theirVpn[0].Addr(), theirUdp) myControl.InjectLightHouseAddr(relayVpn[0].Addr(), relayUdp) myControl.InjectRelays(theirVpn[0].Addr(), []netip.Addr{relayVpn[0].Addr()}) relayControl.InjectLightHouseAddr(theirVpn[0].Addr(), theirUdp) r = router.NewR(t, myControl, relayControl, theirControl) myControl.Start() relayControl.Start() theirControl.Start() t.Cleanup(func() { myControl.Stop() relayControl.Stop() theirControl.Stop() }) myControl.InjectTunPacket(BuildTunUDPPacket(theirVpn[0].Addr(), 80, myVpn[0].Addr(), 80, []byte("first"))) require.True(t, routeUntilData(t, r, theirControl, 10*time.Second, func(p *udp.Packet, receiver *nebula.Control) router.ExitType { outer := &header.H{} relayed := outer.Parse(p.Data) == nil && outer.Type == header.Message && outer.Subtype == header.MessageRelay if h, fh := handshakeOf(p); h != nil && fh != nil && h.MessageCounter == 2 && !relayed && p.From == theirUdp && receiver == myControl { return router.Drop } if drop(p, receiver, theirUdp, myControl) { return router.Drop } return router.KeepRouting }), "tunnel never came up") theirControl.GetFromTun(true) return } // exchange sends n packets each way, routing with drop, and reports how many // arrived in each direction. func exchange(t *testing.T, r *router.R, myControl, theirControl *nebula.Control, myVpn, theirVpn []netip.Prefix, theirUdp netip.AddrPort, n int, drop func(p *udp.Packet, receiver *nebula.Control, them netip.AddrPort, me *nebula.Control) bool) (toThem, toMe int) { t.Helper() route := func(to *nebula.Control) bool { return routeUntilData(t, r, to, 300*time.Millisecond, func(p *udp.Packet, receiver *nebula.Control) router.ExitType { if drop(p, receiver, theirUdp, myControl) { return router.Drop } return router.KeepRouting }) } for range n { myControl.InjectTunPacket(BuildTunUDPPacket(theirVpn[0].Addr(), 80, myVpn[0].Addr(), 80, []byte("to them"))) if route(theirControl) { theirControl.GetFromTun(true) toThem++ } theirControl.InjectTunPacket(BuildTunUDPPacket(myVpn[0].Addr(), 80, theirVpn[0].Addr(), 80, []byte("to me"))) if route(myControl) { myControl.GetFromTun(true) toMe++ } } return } func noDrop(*udp.Packet, *nebula.Control, netip.AddrPort, *nebula.Control) bool { return false } // TestHybridRelayPromotion has msg2 complete by the relay although the direct // path works both ways, as it often does on real networks where the tunnel to // the relay is already up and the direct path still needs a punch. Both ends // start relayed, and the usual promotion moves them to direct. // // With the direct path working only toward the responder, promotion's direct // probe makes the responder roam onto it and lose its replies, hybrid or // classical alike: roaming on receipt assumes a path works both ways. Roam path // validation (claude/roam-path-validation) would hold the responder on the relay. func TestHybridRelayPromotion(t *testing.T) { t.Parallel() myControl, theirControl, myVpn, theirVpn, myUdp, theirUdp, r := relayedHybridTunnel(t, noDrop) defer r.RenderFlow() theirs := theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false) require.NotNil(t, theirs) assert.False(t, theirs.CurrentRemote.IsValid(), "msg3 came by the relay, so the responder starts relayed") toThem, toMe := exchange(t, r, myControl, theirControl, myVpn, theirVpn, theirUdp, 20, noDrop) assert.Equal(t, 20, toThem) assert.Equal(t, 20, toMe) assert.Equal(t, theirUdp, myControl.GetHostInfoByVpnAddr(theirVpn[0].Addr(), false).CurrentRemote, "the initiator promoted to direct") assert.Equal(t, myUdp, theirControl.GetHostInfoByVpnAddr(myVpn[0].Addr(), false).CurrentRemote, "the responder promoted to direct") }