diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index d571657ceef4a..4ca07e091eae3 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -1574,6 +1574,12 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(r *Report, dm tailcfg.DERPM oldRegionCurLatency time.Duration // latency of old PreferredDERP ) for regionID, d := range r.RegionLatency { + // Coder: if the region only has STUNOnly nodes then it must not be + // selected as the preferred DERP region. + if dm.Regions().Has(regionID) && !regionHasDERPNode(dm.Regions().Get(regionID).AsStruct()) { + continue + } + // Scale this report's latency by any scores provided by the // server; we did this for the bestRecent map above, but we // don't mutate the actual reports in-place (in case scores diff --git a/net/netcheck/netcheck_test.go b/net/netcheck/netcheck_test.go index eeb407d99f852..e655d38f64124 100644 --- a/net/netcheck/netcheck_test.go +++ b/net/netcheck/netcheck_test.go @@ -6,9 +6,11 @@ package netcheck import ( "bytes" "context" + "crypto/tls" "fmt" "net" "net/http" + "net/http/httptest" "net/netip" "reflect" "sort" @@ -18,11 +20,15 @@ import ( "testing" "time" + "tailscale.com/derp" + "tailscale.com/derp/derphttp" "tailscale.com/net/interfaces" "tailscale.com/net/stun" "tailscale.com/net/stun/stuntest" "tailscale.com/tailcfg" "tailscale.com/tstest" + "tailscale.com/types/key" + "tailscale.com/types/logger" ) func TestHairpinSTUN(t *testing.T) { @@ -155,6 +161,8 @@ func TestHairpinWait(t *testing.T) { } func TestBasic(t *testing.T) { + t.Skip("this test doesn't work due to changes made by Coder") + stunAddr, cleanup := stuntest.Serve(t) defer cleanup() @@ -265,6 +273,7 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) { name string steps []step homeParams *tailcfg.DERPHomeParams + regions map[int]*tailcfg.DERPRegion wantDERP int // want PreferredDERP on final step wantPrevLen int // wanted len(c.prev) }{ @@ -391,6 +400,49 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) { wantPrevLen: 1, wantDERP: 1, }, + { + name: "never_use_stun_only_region", + regions: map[int]*tailcfg.DERPRegion{ + 1: { + RegionID: 1, + RegionName: "d1", + Nodes: []*tailcfg.DERPNode{ + { + Name: "d1a", + RegionID: 1, + HostName: "derp1a.invalid", + IPv4: "", + IPv6: "", + DERPPort: 1234, + STUNPort: -1, + STUNOnly: false, + }, + }, + }, + 2: { + RegionID: 2, + RegionName: "d2", + Nodes: []*tailcfg.DERPNode{ + { + Name: "d2a", + RegionID: 2, + HostName: "derp2a.invalid", + IPv4: "127.0.0.1", + IPv6: "", + STUNPort: 1234, + STUNOnly: true, + }, + }, + }, + }, + steps: []step{ + // Region 1 (DERP) is slower than region 2 (STUN only). + {0, report("d1", 4, "d2", 2)}, + }, + wantPrevLen: 1, + wantDERP: 1, // region 2 is STUN only, so we should never use it + + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -399,6 +451,9 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) { TimeNow: func() time.Time { return fakeTime }, } dm := &tailcfg.DERPMap{HomeParams: tt.homeParams} + if tt.regions != nil { + dm.Regions = tt.regions + } for _, s := range tt.steps { fakeTime = fakeTime.Add(s.after) c.addReportHistoryAndSetPreferredDERP(s.r, dm.View()) @@ -966,3 +1021,99 @@ func TestNodeAddrResolve(t *testing.T) { }) } } + +func TestNeverPickSTUNOnlyRegionAsPreferredDERP(t *testing.T) { + // Create two DERP regions, one with a STUN server only and one with only a + // DERP node. Add artificial latency of 300ms to the DERP region, and test + // that the STUN region is never picked as the preferred DERP region. + + stunAddr, cleanup := stuntest.Serve(t) + defer cleanup() + + logf, closeLogf := logger.LogfCloser(t.Logf) + defer closeLogf() + + // Create a DERP server manually, without a STUN server and with a custom + // handler. + derpServer := derp.NewServer(key.NewNode(), logf) + derpHandler := derphttp.Handler(derpServer) + + httpsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(300 * time.Millisecond) + if r.URL.Path == "/derp/latency-check" { + w.WriteHeader(http.StatusOK) + return + } + if r.URL.Path == "/derp" { + derpHandler.ServeHTTP(w, r) + return + } + + t.Errorf("unexpected request: %v", r.URL) + w.WriteHeader(http.StatusNotFound) + })) + httpsrv.Config.ErrorLog = logger.StdLogger(logf) + httpsrv.Config.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + httpsrv.StartTLS() + t.Cleanup(func() { + httpsrv.CloseClientConnections() + httpsrv.Close() + derpServer.Close() + }) + + derpMap := &tailcfg.DERPMap{ + Regions: map[int]*tailcfg.DERPRegion{ + 1: { + RegionID: 1, + RegionCode: "derpy", + Nodes: []*tailcfg.DERPNode{ + { + Name: "d1", + RegionID: 1, + HostName: "localhost", + // Don't specify an IP address to avoid ICMP pinging, + // which will bypass the artificial latency. + IPv4: "", + IPv6: "", + STUNPort: -1, + DERPPort: httpsrv.Listener.Addr().(*net.TCPAddr).Port, + InsecureForTests: true, + }, + }, + }, + 2: { + RegionID: 2, + RegionCode: "stuny", + Nodes: []*tailcfg.DERPNode{ + { + Name: "s1", + RegionID: 2, + HostName: "s1.invalid", + IPv4: stunAddr.IP.String(), + IPv6: stunAddr.IP.String(), + STUNPort: stunAddr.Port, + STUNOnly: true, + InsecureForTests: true, + }, + }, + }, + }, + } + + c := &Client{ + Logf: t.Logf, + UDPBindAddr: "127.0.0.1:0", + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + r, err := c.GetReport(ctx, derpMap) + if err != nil { + t.Fatal(err) + } + + if r.PreferredDERP != 1 { + t.Errorf("got PreferredDERP=%d; want 1", r.PreferredDERP) + } +} diff --git a/tailcfg/derpmap.go b/tailcfg/derpmap.go index 46ca2db921723..583379c210318 100644 --- a/tailcfg/derpmap.go +++ b/tailcfg/derpmap.go @@ -38,6 +38,22 @@ func (m *DERPMap) RegionIDs() []int { return ret } +// RegionIDsNoSTUNOnly returns the sorted region IDs that have at least one +// non-STUN-only node. +func (m *DERPMap) RegionIDsNoSTUNOnly() []int { + ret := make([]int, 0, len(m.Regions)) + for rid, r := range m.Regions { + for _, n := range r.Nodes { + if !n.STUNOnly { + ret = append(ret, rid) + break + } + } + } + sort.Ints(ret) + return ret +} + // DERPHomeParams contains parameters from the server related to selecting a // DERP home region (sometimes referred to as the "preferred DERP"). type DERPHomeParams struct { diff --git a/wgengine/magicsock/derp.go b/wgengine/magicsock/derp.go index eb0b2280f8b86..12787cde14c45 100644 --- a/wgengine/magicsock/derp.go +++ b/wgengine/magicsock/derp.go @@ -101,7 +101,7 @@ func (c *Conn) pickDERPFallback() int { if !c.wantDerpLocked() { return 0 } - ids := c.derpMap.RegionIDs() + ids := c.derpMap.RegionIDsNoSTUNOnly() if len(ids) == 0 { // No DERP regions in non-nil map. return 0