Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: always avoid picking stun-only regions as preferred region #38

Merged
merged 2 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions net/netcheck/netcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
151 changes: 151 additions & 0 deletions net/netcheck/netcheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ package netcheck
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"reflect"
"sort"
Expand All @@ -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) {
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
}{
Expand Down Expand Up @@ -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) {
Expand All @@ -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())
Expand Down Expand Up @@ -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)
}
}
16 changes: 16 additions & 0 deletions tailcfg/derpmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion wgengine/magicsock/derp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading