|
|
@@ -0,0 +1,118 @@
|
|
|
+//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")
|
|
|
+}
|