// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package dnsfallback import ( "context" "encoding/json" "flag" "os" "path/filepath" "reflect" "testing" "tailscale.com/net/netmon" "tailscale.com/tailcfg" "tailscale.com/types/logger" ) func TestGetDERPMap(t *testing.T) { dm := getDERPMap() if dm == nil { t.Fatal("nil") } if len(dm.Regions) == 0 { t.Fatal("no regions") } } func TestCache(t *testing.T) { cacheFile := filepath.Join(t.TempDir(), "cache.json") // Write initial cache value initialCache := &tailcfg.DERPMap{ Regions: map[int]*tailcfg.DERPRegion{ 99: { RegionID: 99, RegionCode: "test", RegionName: "Testville", Nodes: []*tailcfg.DERPNode{{ Name: "99a", RegionID: 99, HostName: "derp99a.tailscale.com", IPv4: "1.2.3.4", }}, }, // Intentionally attempt to "overwrite" something 1: { RegionID: 1, RegionCode: "r1", RegionName: "r1", Nodes: []*tailcfg.DERPNode{{ Name: "1c", RegionID: 1, HostName: "derp1c.tailscale.com", IPv4: "127.0.0.1", IPv6: "::1", }}, }, }, } d, err := json.Marshal(initialCache) if err != nil { t.Fatal(err) } if err := os.WriteFile(cacheFile, d, 0666); err != nil { t.Fatal(err) } // Clear any existing cached DERP map(s) cachedDERPMap.Store(nil) // Load the cache SetCachePath(cacheFile, t.Logf) if cm := cachedDERPMap.Load(); !reflect.DeepEqual(initialCache, cm) { t.Fatalf("cached map was %+v; want %+v", cm, initialCache) } // Verify that our DERP map is merged with the cache. dm := getDERPMap() region, ok := dm.Regions[99] if !ok { t.Fatal("expected region 99") } if !reflect.DeepEqual(region, initialCache.Regions[99]) { t.Fatalf("region 99: got %+v; want %+v", region, initialCache.Regions[99]) } // Verify that our cache can't override a statically-baked-in DERP server. n0 := dm.Regions[1].Nodes[0] if n0.IPv4 == "127.0.0.1" || n0.IPv6 == "::1" { t.Errorf("got %+v; expected no overwrite for node", n0) } // Also, make sure that the static DERP map still has the same first // node as when this test was last written/updated; this ensures that // we don't accidentally start allowing overwrites due to some of the // test's assumptions changing out from underneath us as we update the // JSON file of fallback servers. if getStaticDERPMap().Regions[1].Nodes[0].HostName != "derp1c.tailscale.com" { t.Errorf("DERP server has a different name; please update this test") } } func TestCacheUnchanged(t *testing.T) { cacheFile := filepath.Join(t.TempDir(), "cache.json") // Write initial cache value initialCache := &tailcfg.DERPMap{ Regions: map[int]*tailcfg.DERPRegion{ 99: { RegionID: 99, RegionCode: "test", RegionName: "Testville", Nodes: []*tailcfg.DERPNode{{ Name: "99a", RegionID: 99, HostName: "derp99a.tailscale.com", IPv4: "1.2.3.4", }}, }, }, } d, err := json.Marshal(initialCache) if err != nil { t.Fatal(err) } if err := os.WriteFile(cacheFile, d, 0666); err != nil { t.Fatal(err) } // Clear any existing cached DERP map(s) cachedDERPMap.Store(nil) // Load the cache SetCachePath(cacheFile, t.Logf) if cm := cachedDERPMap.Load(); !reflect.DeepEqual(initialCache, cm) { t.Fatalf("cached map was %+v; want %+v", cm, initialCache) } // Remove the cache file on-disk, then re-set to the current value. If // our equality comparison is working, we won't rewrite the file // on-disk since the cached value won't have changed. if err := os.Remove(cacheFile); err != nil { t.Fatal(err) } UpdateCache(initialCache, t.Logf) if _, err := os.Stat(cacheFile); !os.IsNotExist(err) { t.Fatalf("got err=%v; expected to not find cache file", err) } // Now, update the cache with something slightly different and verify // that we did re-write the file on-disk. updatedCache := &tailcfg.DERPMap{ Regions: map[int]*tailcfg.DERPRegion{ 99: { RegionID: 99, RegionCode: "test", RegionName: "Testville", Nodes: []*tailcfg.DERPNode{ /* set below */ }, }, }, } clonedNode := *initialCache.Regions[99].Nodes[0] clonedNode.IPv4 = "1.2.3.5" updatedCache.Regions[99].Nodes = append(updatedCache.Regions[99].Nodes, &clonedNode) UpdateCache(updatedCache, t.Logf) if st, err := os.Stat(cacheFile); err != nil { t.Fatalf("could not stat cache file; err=%v", err) } else if !st.Mode().IsRegular() || st.Size() == 0 { t.Fatalf("didn't find non-empty regular file; mode=%v size=%d", st.Mode(), st.Size()) } } var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests") func TestLookup(t *testing.T) { if !*extNetwork { t.Skip("skipping test without --use-external-network") } logf, closeLogf := logger.LogfCloser(t.Logf) defer closeLogf() netMon, err := netmon.New(logf) if err != nil { t.Fatal(err) } resolver := &fallbackResolver{ logf: logf, netMon: netMon, waitForCompare: true, } addrs, err := resolver.Lookup(context.Background(), "controlplane.tailscale.com") if err != nil { t.Fatal(err) } t.Logf("addrs: %+v", addrs) }