|
|
@@ -0,0 +1,136 @@
|
|
|
+//go:build e2e_testing
|
|
|
+// +build e2e_testing
|
|
|
+
|
|
|
+package e2e
|
|
|
+
|
|
|
+import (
|
|
|
+ "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/udp"
|
|
|
+)
|
|
|
+
|
|
|
+// TestRecoveryTiming measures how long a tunnel takes to come back after the peer stops accepting our traffic,
|
|
|
+// which is what a laptop waking on a new network looks like from the peer's side: its NAT has no state for where
|
|
|
+// we are now, so everything we send disappears.
|
|
|
+//
|
|
|
+// It is a measurement, not a pass/fail assertion. Recovery is timed to the moment the peer punches back at us,
|
|
|
+// since that is when its NAT opens and the tunnel is usable again.
|
|
|
+//
|
|
|
+// go test -tags e2e_testing -v -run TestRecoveryTiming ./e2e/
|
|
|
+func TestRecoveryTiming(t *testing.T) {
|
|
|
+ for _, tc := range []struct {
|
|
|
+ name string
|
|
|
+ rebind bool
|
|
|
+ }{
|
|
|
+ {"no trigger", false},
|
|
|
+ {"rebind counter", true},
|
|
|
+ } {
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
+ d, lost := measureRecovery(t, tc.rebind)
|
|
|
+ t.Logf("RESULT %-16s recovered in %-9v (%d packets lost)", tc.name, d.Round(time.Millisecond), lost)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// measureRecovery returns how long until the peer punched back, and how many of our packets died meanwhile. When
|
|
|
+// rebind is true we call RebindUDPServer once the tunnel goes dark, which is what the darwin network change
|
|
|
+// monitor does and what iOS has always done. When false, nothing tells nebula anything is wrong.
|
|
|
+func measureRecovery(t *testing.T, rebind bool) (time.Duration, int) {
|
|
|
+ t.Helper()
|
|
|
+ ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
|
|
|
+
|
|
|
+ lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{
|
|
|
+ "lighthouse": m{"am_lighthouse": true},
|
|
|
+ })
|
|
|
+
|
|
|
+ peerCfg := m{
|
|
|
+ "lighthouse": m{
|
|
|
+ "hosts": []any{lhVpnIpNet[0].Addr().String()},
|
|
|
+ "interval": 600,
|
|
|
+ "local_allow_list": m{
|
|
|
+ "10.0.0.0/24": true,
|
|
|
+ "::/0": false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ "static_host_map": m{
|
|
|
+ lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", peerCfg)
|
|
|
+ theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.3/24", peerCfg)
|
|
|
+
|
|
|
+ r := router.NewR(t, lhControl, myControl, theirControl)
|
|
|
+ defer r.RenderFlow()
|
|
|
+ defer func() {
|
|
|
+ lhControl.Stop()
|
|
|
+ myControl.Stop()
|
|
|
+ theirControl.Stop()
|
|
|
+ }()
|
|
|
+
|
|
|
+ lhControl.Start()
|
|
|
+ myControl.Start()
|
|
|
+ theirControl.Start()
|
|
|
+ r.RouteFor(time.Millisecond * 500)
|
|
|
+
|
|
|
+ myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
|
|
|
+ theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
|
|
|
+
|
|
|
+ myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("establish")))
|
|
|
+ r.RouteFor(time.Second)
|
|
|
+ if myControl.GetHostInfoByVpnAddr(theirVpnIpNet[0].Addr(), false) == nil {
|
|
|
+ t.Fatal("failed to establish the tunnel we are measuring")
|
|
|
+ }
|
|
|
+ r.RouteFor(time.Millisecond * 500)
|
|
|
+
|
|
|
+ // From here the peer's NAT has no state for us, everything we send it disappears
|
|
|
+ start := time.Now()
|
|
|
+ blackholed := 0
|
|
|
+ var recovered time.Duration
|
|
|
+
|
|
|
+ if rebind {
|
|
|
+ myControl.RebindUDPServer()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Keep the tun busy the way someone retrying a stalled connection would
|
|
|
+ stop := make(chan struct{})
|
|
|
+ defer close(stop)
|
|
|
+ go func() {
|
|
|
+ tick := time.NewTicker(time.Millisecond * 200)
|
|
|
+ defer tick.Stop()
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ case <-stop:
|
|
|
+ return
|
|
|
+ case <-tick.C:
|
|
|
+ myControl.InjectTunPacket(BuildTunUDPPacket(
|
|
|
+ theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("retry")))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ r.RouteForAllExitFuncOrTimeout(time.Second*30, func(p *udp.Packet, c *nebula.Control) router.ExitType {
|
|
|
+ if c == theirControl && p.From == myControl.GetUDPAddr() {
|
|
|
+ blackholed++
|
|
|
+ return router.Drop
|
|
|
+ }
|
|
|
+
|
|
|
+ // The peer reaching us directly is the moment its NAT opened, whether that is a punch or a handshake
|
|
|
+ if c == myControl && p.From == theirUdpAddr {
|
|
|
+ recovered = time.Since(start)
|
|
|
+ return router.RouteAndExit
|
|
|
+ }
|
|
|
+
|
|
|
+ return router.KeepRouting
|
|
|
+ })
|
|
|
+
|
|
|
+ if recovered == 0 {
|
|
|
+ t.Fatalf("no recovery within 30s (%d packets blackholed)", blackholed)
|
|
|
+ }
|
|
|
+ return recovered, blackholed
|
|
|
+}
|