| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- //go:build windows && !e2e_testing
- package overlay
- import (
- "bytes"
- "fmt"
- "net/netip"
- "strings"
- "testing"
- "github.com/slackhq/nebula/config"
- "github.com/slackhq/nebula/test"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- "golang.org/x/sys/windows"
- "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
- )
- type routeTest struct {
- c *config.C
- tun *winTun
- luid winipcfg.LUID
- log *bytes.Buffer
- }
- // newRouteTest makes a wintun device of its own, before installing tun.unsafe_routes from cfg
- func newRouteTest(t *testing.T, cfg string) *routeTest {
- t.Helper()
- if !windows.GetCurrentProcessToken().IsElevated() {
- t.Skip("creating a wintun device needs an elevated process")
- }
- if err := checkWinTunExists(); err != nil {
- t.Skipf("wintun.dll is not where nebula looks for it: %v", err)
- }
- rt := &routeTest{log: &bytes.Buffer{}}
- l := test.NewLoggerWithOutput(rt.log)
- rt.c = config.NewC(l)
- require.NoError(t, rt.c.LoadString(cfg))
- var err error
- rt.tun, err = newTun(rt.c, l, []netip.Prefix{netip.MustParsePrefix("10.250.0.1/24")}, false)
- require.NoError(t, err)
- t.Cleanup(func() { _ = rt.tun.Close() })
- rt.luid = winipcfg.LUID(rt.tun.tun.LUID())
- return rt
- }
- func (rt *routeTest) metric(t *testing.T, cidr string) uint32 {
- t.Helper()
- p := netip.MustParsePrefix(cidr)
- row, err := rt.luid.Route(p, unspecifiedNextHop(p))
- require.NoError(t, err)
- return row.Metric
- }
- // reload applies cfg and expects nothing in it to fail
- func (rt *routeTest) reload(t *testing.T, cfg string) {
- t.Helper()
- res := rt.c.ReloadConfigString(cfg)
- require.NoError(t, res.Err)
- assert.False(t, res.Failed, "the reload reported a failure")
- }
- func unsafeRoutes(routes ...string) string {
- var b strings.Builder
- b.WriteString("tun:\n dev: nebroutetest\n unsafe_routes:\n")
- // cidr[@metric][!], the ! for install: false
- for _, r := range routes {
- r, noInstall := strings.CutSuffix(r, "!")
- cidr, metric, _ := strings.Cut(r, "@")
- fmt.Fprintf(&b, " - route: %s\n via: 10.250.0.2\n", cidr)
- if metric != "" {
- fmt.Fprintf(&b, " metric: %s\n", metric)
- }
- if noInstall {
- b.WriteString(" install: false\n")
- }
- }
- return b.String()
- }
- // A reload that adds a route re-adds the ones already installed, Windows refusing those as already there isn't a failure
- func TestWinTun_ReloadKeepsInstalledRoutes(t *testing.T) {
- forFamilies(t, func(t *testing.T, a, b string) {
- rt := newRouteTest(t, unsafeRoutes(a))
- require.NoError(t, rt.tun.Activate())
- rt.reload(t, unsafeRoutes(a, b))
- assert.NotContains(t, rt.log.String(), "Failed to add route")
- rt.metric(t, a)
- rt.metric(t, b)
- })
- }
- // Windows calls a route the same by destination and next hop, so a changed metric is set on the installed route in
- // place rather than deleted and added, which would leave a moment with no route
- func TestWinTun_ReloadMetricChangeApplies(t *testing.T) {
- forFamilies(t, func(t *testing.T, a, b string) {
- rt := newRouteTest(t, unsafeRoutes(a+"@100"))
- require.NoError(t, rt.tun.Activate())
- require.Equal(t, uint32(100), rt.metric(t, a))
- rt.reload(t, unsafeRoutes(a+"@200"))
- assert.NotContains(t, rt.log.String(), "Failed to add route")
- assert.NotContains(t, rt.log.String(), "Removed route")
- assert.Equal(t, uint32(200), rt.metric(t, a))
- })
- }
- // A route someone else put on the device with another metric is brought to ours, at startup too
- func TestWinTun_ForeignRouteGetsOurMetric(t *testing.T) {
- forFamilies(t, func(t *testing.T, a, b string) {
- rt := newRouteTest(t, unsafeRoutes(a+"@100"))
- p := netip.MustParsePrefix(a)
- require.NoError(t, rt.luid.AddRoute(p, unspecifiedNextHop(p), 50))
- require.NoError(t, rt.tun.Activate())
- assert.Equal(t, uint32(100), rt.metric(t, a))
- assert.Contains(t, rt.log.String(), "level=INFO msg=\"Updated route metric\"")
- })
- }
- // A CIDR listed twice is installed once, by its last entry, and reloads leave it there instead of flipping between the
- // two metrics
- func TestWinTun_DuplicateCidrInstallsTheLastEntry(t *testing.T) {
- forFamilies(t, func(t *testing.T, a, b string) {
- rt := newRouteTest(t, unsafeRoutes(a+"@100", a+"@200"))
- require.NoError(t, rt.tun.Activate())
- assert.Equal(t, uint32(200), rt.metric(t, a))
- rt.reload(t, unsafeRoutes(a+"@100", a+"@200", b))
- assert.Equal(t, uint32(200), rt.metric(t, a))
- assert.NotContains(t, rt.log.String(), "Updated route metric")
- assert.NotContains(t, rt.log.String(), "Failed to add route")
- })
- }
- // Dropping the duplicate that wasn't installed leaves the installed route alone. Windows deletes by destination and
- // next hop, deleting the dropped one would take the live route with it
- func TestWinTun_DroppingAnUninstalledDuplicateKeepsTheRoute(t *testing.T) {
- forFamilies(t, func(t *testing.T, a, b string) {
- rt := newRouteTest(t, unsafeRoutes(a+"@100", a+"@200"))
- require.NoError(t, rt.tun.Activate())
- rt.reload(t, unsafeRoutes(a+"@200"))
- assert.NotContains(t, rt.log.String(), "Removed route")
- assert.Equal(t, uint32(200), rt.metric(t, a))
- })
- }
- // A reload ends where a fresh start with the same config would, a CIDR whose last entry says not to install it is
- // removed
- func TestWinTun_ReloadMatchesAFreshStart(t *testing.T) {
- forFamilies(t, func(t *testing.T, a, b string) {
- cidr := netip.MustParsePrefix(a)
- routes := unsafeRoutes(a+"@100", a+"@100!")
- fresh := newRouteTest(t, routes)
- require.NoError(t, fresh.tun.Activate())
- _, err := fresh.luid.Route(cidr, unspecifiedNextHop(cidr))
- require.Error(t, err, "a fresh start installed it")
- require.NoError(t, fresh.tun.Close())
- rt := newRouteTest(t, unsafeRoutes(a+"@100"))
- require.NoError(t, rt.tun.Activate())
- rt.reload(t, routes)
- _, err = rt.luid.Route(cidr, unspecifiedNextHop(cidr))
- assert.Error(t, err, "the reload left it installed")
- })
- }
- // Removing a route that is already gone is what we wanted, not a failure
- func TestWinTun_ReloadRouteAlreadyGone(t *testing.T) {
- forFamilies(t, func(t *testing.T, a, b string) {
- rt := newRouteTest(t, unsafeRoutes(a, b))
- require.NoError(t, rt.tun.Activate())
- p := netip.MustParsePrefix(b)
- require.NoError(t, rt.luid.DeleteRoute(p, unspecifiedNextHop(p)))
- rt.reload(t, unsafeRoutes(a))
- })
- }
- // A network category we can't parse would fail the restart, so it fails the reload instead of asking for one
- func TestWinTun_ReloadInvalidNetworkCategoryFails(t *testing.T) {
- rt := newRouteTest(t, unsafeRoutes())
- for range 2 {
- res := rt.c.ReloadConfigString(unsafeRoutes() + " network_category: nope\n")
- require.NoError(t, res.Err)
- assert.True(t, res.Failed, "a bad category that didn't change since the last reload is still bad")
- assert.False(t, res.RestartRequired)
- }
- // Fixing it back to the running category needs nothing
- res := rt.c.ReloadConfigString(unsafeRoutes() + " network_category: private\n")
- require.NoError(t, res.Err)
- assert.False(t, res.Failed)
- assert.False(t, res.RestartRequired)
- res = rt.c.ReloadConfigString(unsafeRoutes() + " network_category: public\n")
- require.NoError(t, res.Err)
- assert.Equal(t, []string{"tun.network_category"}, res.RestartKeys)
- }
- // forFamilies runs f with two unsafe_routes CIDRs of each address family, the tun's own network stays IPv4
- func forFamilies(t *testing.T, f func(t *testing.T, a, b string)) {
- t.Run("ipv4", func(t *testing.T) { f(t, "10.251.1.0/24", "10.251.2.0/24") })
- t.Run("ipv6", func(t *testing.T) { f(t, "fd00:251:1::/64", "fd00:251:2::/64") })
- }
|