Skip to content

Commit 4a17d5b

Browse files
authored
fix: use DERPPort in DERP URL if using node.HostName (#35)
1 parent 1f65363 commit 4a17d5b

File tree

2 files changed

+141
-14
lines changed

2 files changed

+141
-14
lines changed

derp/derphttp/derphttp_client.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -260,23 +260,26 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
260260
if c.url != nil {
261261
return c.url.String()
262262
}
263-
if node.HostName == "" {
264-
url := &url.URL{
265-
Scheme: "https",
266-
Host: fmt.Sprintf("%s:%d", node.IPv4, node.DERPPort),
267-
Path: "/derp",
268-
}
269-
if node.ForceHTTP {
270-
url.Scheme = "http"
271-
}
272-
return url.String()
273-
}
274263

275-
proto := "https"
264+
url := &url.URL{
265+
Scheme: "https",
266+
Host: node.IPv4,
267+
Path: "/derp",
268+
}
276269
if debugUseDERPHTTP() || node.ForceHTTP {
277-
proto = "http"
270+
url.Scheme = "http"
278271
}
279-
return fmt.Sprintf("%s://%s/derp", proto, node.HostName)
272+
if url.Host == "" {
273+
url.Host = node.HostName
274+
}
275+
276+
// Add the DERPPort to the URL host, unless the host already contains a port
277+
// or the DERPPort is the default port for the protocol.
278+
if node.DERPPort != 0 && !strings.Contains(url.Host, ":") && ((url.Scheme == "https" && node.DERPPort != 443) || (url.Scheme == "http" && node.DERPPort != 80)) {
279+
url.Host = fmt.Sprintf("%s:%d", url.Host, node.DERPPort)
280+
}
281+
282+
return url.String()
280283
}
281284

282285
// AddressFamilySelector decides whether IPv6 is preferred for

wgengine/magicsock/magicsock_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package magicsock
55

66
import (
7+
"bufio"
78
"bytes"
89
"context"
910
crand "crypto/rand"
@@ -12,6 +13,7 @@ import (
1213
"errors"
1314
"fmt"
1415
"io"
16+
"log"
1517
"math/rand"
1618
"net"
1719
"net/http"
@@ -37,6 +39,7 @@ import (
3739
"golang.org/x/net/icmp"
3840
"golang.org/x/net/ipv4"
3941
"golang.org/x/net/ipv6"
42+
"nhooyr.io/websocket"
4043
"tailscale.com/cmd/testwrapper/flakytest"
4144
"tailscale.com/derp"
4245
"tailscale.com/derp/derphttp"
@@ -48,6 +51,7 @@ import (
4851
"tailscale.com/net/ping"
4952
"tailscale.com/net/stun/stuntest"
5053
"tailscale.com/net/tstun"
54+
"tailscale.com/net/wsconn"
5155
"tailscale.com/tailcfg"
5256
"tailscale.com/tstest"
5357
"tailscale.com/tstest/natlab"
@@ -161,7 +165,16 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
161165
return newMagicStackWithKey(t, logf, l, derpMap, privateKey)
162166
}
163167

168+
func newMagicStackFunc(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap, updateConnFunc func(ms *Conn)) *magicStack {
169+
privateKey := key.NewNode()
170+
return newMagicStackWithKeyFunc(t, logf, l, derpMap, privateKey, updateConnFunc)
171+
}
172+
164173
func newMagicStackWithKey(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap, privateKey key.NodePrivate) *magicStack {
174+
return newMagicStackWithKeyFunc(t, logf, l, derpMap, privateKey, nil)
175+
}
176+
177+
func newMagicStackWithKeyFunc(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap, privateKey key.NodePrivate, updateConnFunc func(ms *Conn)) *magicStack {
165178
t.Helper()
166179

167180
epCh := make(chan []tailcfg.Endpoint, 100) // arbitrary
@@ -186,6 +199,11 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, l nettype.PacketListen
186199

187200
wgLogger := wglog.NewLogger(logf)
188201
dev := wgcfg.NewDevice(tsTun, conn.Bind(), wgLogger.DeviceLogger)
202+
203+
if updateConnFunc != nil {
204+
updateConnFunc(conn)
205+
}
206+
189207
dev.Up()
190208

191209
// Wait for magicsock to connect up to DERP.
@@ -2876,3 +2894,109 @@ func TestAddrForSendLockedForWireGuardOnly(t *testing.T) {
28762894
}
28772895
}
28782896
}
2897+
2898+
// Copied from cmd/derper
2899+
func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
2900+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2901+
up := strings.ToLower(r.Header.Get("Upgrade"))
2902+
2903+
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
2904+
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
2905+
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
2906+
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
2907+
base.ServeHTTP(w, r)
2908+
return
2909+
}
2910+
2911+
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
2912+
Subprotocols: []string{"derp"},
2913+
OriginPatterns: []string{"*"},
2914+
// Disable compression because we transmit WireGuard messages that
2915+
// are not compressible.
2916+
// Additionally, Safari has a broken implementation of compression
2917+
// (see https://github.com/nhooyr/websocket/issues/218) that makes
2918+
// enabling it actively harmful.
2919+
CompressionMode: websocket.CompressionDisabled,
2920+
})
2921+
if err != nil {
2922+
log.Printf("websocket.Accept: %v", err)
2923+
return
2924+
}
2925+
defer c.Close(websocket.StatusInternalError, "closing")
2926+
if c.Subprotocol() != "derp" {
2927+
c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
2928+
return
2929+
}
2930+
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary)
2931+
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
2932+
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
2933+
})
2934+
}
2935+
2936+
func TestDERPForceWebsockets(t *testing.T) {
2937+
logf, closeLogf := logger.LogfCloser(t.Logf)
2938+
defer closeLogf()
2939+
2940+
// Create a DERP server manually, without a STUN server and with a custom
2941+
// handler.
2942+
derpServer := derp.NewServer(key.NewNode(), logf)
2943+
derpHandler := derphttp.Handler(derpServer)
2944+
derpHandler = addWebSocketSupport(derpServer, derpHandler)
2945+
2946+
var upgradeCount int64
2947+
httpsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2948+
up := r.Header.Get("Upgrade")
2949+
if up != "" {
2950+
if up != "websocket" {
2951+
t.Errorf("unexpected upgrade header: %q", up)
2952+
} else {
2953+
atomic.AddInt64(&upgradeCount, 1)
2954+
}
2955+
}
2956+
2957+
derpHandler.ServeHTTP(w, r)
2958+
}))
2959+
httpsrv.Config.ErrorLog = logger.StdLogger(logf)
2960+
httpsrv.Config.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
2961+
httpsrv.StartTLS()
2962+
t.Cleanup(func() {
2963+
httpsrv.CloseClientConnections()
2964+
httpsrv.Close()
2965+
derpServer.Close()
2966+
})
2967+
2968+
derpMap := &tailcfg.DERPMap{
2969+
Regions: map[int]*tailcfg.DERPRegion{
2970+
1: {
2971+
RegionID: 1,
2972+
RegionCode: "test",
2973+
Nodes: []*tailcfg.DERPNode{
2974+
{
2975+
Name: "t1",
2976+
RegionID: 1,
2977+
HostName: "test-node.unused",
2978+
IPv4: "127.0.0.1",
2979+
IPv6: "",
2980+
STUNPort: -1,
2981+
DERPPort: httpsrv.Listener.Addr().(*net.TCPAddr).Port,
2982+
InsecureForTests: true,
2983+
},
2984+
},
2985+
},
2986+
},
2987+
}
2988+
2989+
m := &natlab.Machine{Name: "m1"}
2990+
ms := newMagicStackFunc(t, logger.WithPrefix(logf, "conn1: "), m, derpMap, func(ms *Conn) {
2991+
ms.SetDERPForceWebsockets(true)
2992+
})
2993+
defer ms.Close()
2994+
2995+
if len(ms.conn.activeDerp) == 0 {
2996+
t.Errorf("unexpected DERP empty got: %v want: >0", len(ms.conn.activeDerp))
2997+
}
2998+
2999+
if atomic.LoadInt64(&upgradeCount) == 0 {
3000+
t.Errorf("no websocket upgrade requests seen")
3001+
}
3002+
}

0 commit comments

Comments
 (0)