tun_windows_route_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. //go:build windows && !e2e_testing
  2. package overlay
  3. import (
  4. "bytes"
  5. "fmt"
  6. "net/netip"
  7. "strings"
  8. "testing"
  9. "github.com/slackhq/nebula/config"
  10. "github.com/slackhq/nebula/test"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. "golang.org/x/sys/windows"
  14. "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
  15. )
  16. type routeTest struct {
  17. c *config.C
  18. tun *winTun
  19. luid winipcfg.LUID
  20. log *bytes.Buffer
  21. }
  22. // newRouteTest makes a wintun device of its own, before installing tun.unsafe_routes from cfg
  23. func newRouteTest(t *testing.T, cfg string) *routeTest {
  24. t.Helper()
  25. if !windows.GetCurrentProcessToken().IsElevated() {
  26. t.Skip("creating a wintun device needs an elevated process")
  27. }
  28. if err := checkWinTunExists(); err != nil {
  29. t.Skipf("wintun.dll is not where nebula looks for it: %v", err)
  30. }
  31. rt := &routeTest{log: &bytes.Buffer{}}
  32. l := test.NewLoggerWithOutput(rt.log)
  33. rt.c = config.NewC(l)
  34. require.NoError(t, rt.c.LoadString(cfg))
  35. var err error
  36. rt.tun, err = newTun(rt.c, l, []netip.Prefix{netip.MustParsePrefix("10.250.0.1/24")}, false)
  37. require.NoError(t, err)
  38. t.Cleanup(func() { _ = rt.tun.Close() })
  39. rt.luid = winipcfg.LUID(rt.tun.tun.LUID())
  40. return rt
  41. }
  42. func (rt *routeTest) metric(t *testing.T, cidr string) uint32 {
  43. t.Helper()
  44. p := netip.MustParsePrefix(cidr)
  45. row, err := rt.luid.Route(p, unspecifiedNextHop(p))
  46. require.NoError(t, err)
  47. return row.Metric
  48. }
  49. func unsafeRoutes(routes ...string) string {
  50. var b strings.Builder
  51. b.WriteString("tun:\n dev: nebroutetest\n unsafe_routes:\n")
  52. // cidr[@metric][!], the ! for install: false
  53. for _, r := range routes {
  54. r, noInstall := strings.CutSuffix(r, "!")
  55. cidr, metric, _ := strings.Cut(r, "@")
  56. fmt.Fprintf(&b, " - route: %s\n via: 10.250.0.2\n", cidr)
  57. if metric != "" {
  58. fmt.Fprintf(&b, " metric: %s\n", metric)
  59. }
  60. if noInstall {
  61. b.WriteString(" install: false\n")
  62. }
  63. }
  64. return b.String()
  65. }
  66. // A reload that adds a route re-adds the ones already installed, Windows refusing those as already there isn't a failure
  67. func TestWinTun_ReloadKeepsInstalledRoutes(t *testing.T) {
  68. forFamilies(t, func(t *testing.T, a, b string) {
  69. rt := newRouteTest(t, unsafeRoutes(a))
  70. require.NoError(t, rt.tun.Activate())
  71. require.NoError(t, rt.c.ReloadConfigString(unsafeRoutes(a, b)))
  72. assert.NotContains(t, rt.log.String(), "Failed to add route")
  73. rt.metric(t, a)
  74. rt.metric(t, b)
  75. })
  76. }
  77. // Windows calls a route the same by destination and next hop, so a changed metric is set on the installed route in
  78. // place rather than deleted and added, which would leave a moment with no route
  79. func TestWinTun_ReloadMetricChangeApplies(t *testing.T) {
  80. forFamilies(t, func(t *testing.T, a, b string) {
  81. rt := newRouteTest(t, unsafeRoutes(a+"@100"))
  82. require.NoError(t, rt.tun.Activate())
  83. require.Equal(t, uint32(100), rt.metric(t, a))
  84. require.NoError(t, rt.c.ReloadConfigString(unsafeRoutes(a+"@200")))
  85. assert.NotContains(t, rt.log.String(), "Failed to add route")
  86. assert.NotContains(t, rt.log.String(), "Removed route")
  87. assert.Equal(t, uint32(200), rt.metric(t, a))
  88. })
  89. }
  90. // A route someone else put on the device with another metric is brought to ours, at startup too
  91. func TestWinTun_ForeignRouteGetsOurMetric(t *testing.T) {
  92. forFamilies(t, func(t *testing.T, a, b string) {
  93. rt := newRouteTest(t, unsafeRoutes(a+"@100"))
  94. p := netip.MustParsePrefix(a)
  95. require.NoError(t, rt.luid.AddRoute(p, unspecifiedNextHop(p), 50))
  96. require.NoError(t, rt.tun.Activate())
  97. assert.Equal(t, uint32(100), rt.metric(t, a))
  98. assert.Contains(t, rt.log.String(), "level=INFO msg=\"Updated route metric\"")
  99. })
  100. }
  101. // A CIDR listed twice is installed once, by its last entry, and reloads leave it there instead of flipping between the
  102. // two metrics
  103. func TestWinTun_DuplicateCidrInstallsTheLastEntry(t *testing.T) {
  104. forFamilies(t, func(t *testing.T, a, b string) {
  105. rt := newRouteTest(t, unsafeRoutes(a+"@100", a+"@200"))
  106. require.NoError(t, rt.tun.Activate())
  107. assert.Equal(t, uint32(200), rt.metric(t, a))
  108. require.NoError(t, rt.c.ReloadConfigString(unsafeRoutes(a+"@100", a+"@200", b)))
  109. assert.Equal(t, uint32(200), rt.metric(t, a))
  110. assert.NotContains(t, rt.log.String(), "Updated route metric")
  111. assert.NotContains(t, rt.log.String(), "Failed to add route")
  112. })
  113. }
  114. // Dropping the duplicate that wasn't installed leaves the installed route alone. Windows deletes by destination and
  115. // next hop, deleting the dropped one would take the live route with it
  116. func TestWinTun_DroppingAnUninstalledDuplicateKeepsTheRoute(t *testing.T) {
  117. forFamilies(t, func(t *testing.T, a, b string) {
  118. rt := newRouteTest(t, unsafeRoutes(a+"@100", a+"@200"))
  119. require.NoError(t, rt.tun.Activate())
  120. require.NoError(t, rt.c.ReloadConfigString(unsafeRoutes(a+"@200")))
  121. assert.NotContains(t, rt.log.String(), "Removed route")
  122. assert.Equal(t, uint32(200), rt.metric(t, a))
  123. })
  124. }
  125. // A reload ends where a fresh start with the same config would, a CIDR whose last entry says not to install it is
  126. // removed
  127. func TestWinTun_ReloadMatchesAFreshStart(t *testing.T) {
  128. forFamilies(t, func(t *testing.T, a, b string) {
  129. cidr := netip.MustParsePrefix(a)
  130. routes := unsafeRoutes(a+"@100", a+"@100!")
  131. fresh := newRouteTest(t, routes)
  132. require.NoError(t, fresh.tun.Activate())
  133. _, err := fresh.luid.Route(cidr, unspecifiedNextHop(cidr))
  134. require.Error(t, err, "a fresh start installed it")
  135. require.NoError(t, fresh.tun.Close())
  136. rt := newRouteTest(t, unsafeRoutes(a+"@100"))
  137. require.NoError(t, rt.tun.Activate())
  138. require.NoError(t, rt.c.ReloadConfigString(routes))
  139. _, err = rt.luid.Route(cidr, unspecifiedNextHop(cidr))
  140. assert.Error(t, err, "the reload left it installed")
  141. })
  142. }
  143. // forFamilies runs f with two unsafe_routes CIDRs of each address family, the tun's own network stays IPv4
  144. func forFamilies(t *testing.T, f func(t *testing.T, a, b string)) {
  145. t.Run("ipv4", func(t *testing.T) { f(t, "10.251.1.0/24", "10.251.2.0/24") })
  146. t.Run("ipv6", func(t *testing.T) { f(t, "fd00:251:1::/64", "fd00:251:2::/64") })
  147. }