Skip to content

Commit 1f65363

Browse files
authored
feat: add derphttp option to always use websocket fallback (#34)
1 parent dd62954 commit 1f65363

File tree

4 files changed

+79
-0
lines changed

4 files changed

+79
-0
lines changed

derp/derphttp/derphttp_client.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ type Client struct {
5858
MeshKey string // optional; for trusted clients
5959
IsProber bool // optional; for probers to optional declare themselves as such
6060

61+
// Allow forcing WebSocket fallback for situations where proxies do not
62+
// play well with `Upgrade: derp`. Turning this on will cause the client to
63+
// always try WebSocket on it's first attempt. The callback will not be
64+
// called if this is true.
65+
//
66+
// Example proxies include:
67+
// - Azure Application Proxy (which redirects to login)
68+
ForceWebsockets bool
6169
forcedWebsocket atomic.Bool // optional; set if the server has failed to upgrade the connection on the DERP server
6270
forcedWebsocketCallback atomic.Pointer[func(int, string)]
6371

@@ -301,6 +309,9 @@ func (c *Client) preferIPv6() bool {
301309
var dialWebsocketFunc func(ctx context.Context, urlStr string, tlsConfig *tls.Config, httpHeader http.Header) (net.Conn, error)
302310

303311
func (c *Client) useWebsockets() bool {
312+
if c.ForceWebsockets {
313+
return true
314+
}
304315
if runtime.GOOS == "js" {
305316
return true
306317
}

derp/derphttp/derphttp_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,63 @@ func TestHTTP2OnlyServer(t *testing.T) {
265265

266266
c.Close()
267267
}
268+
269+
func TestForceWebsockets(t *testing.T) {
270+
serverPrivateKey := key.NewNode()
271+
s := derp.NewServer(serverPrivateKey, t.Logf)
272+
defer s.Close()
273+
274+
httpsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
275+
up := r.Header.Get("Upgrade")
276+
if up == "" {
277+
Handler(s).ServeHTTP(w, r)
278+
return
279+
}
280+
if up != "websocket" {
281+
// Should only attempt to upgrade to websocket.
282+
t.Errorf("unexpected Upgrade header: %q", up)
283+
return
284+
}
285+
286+
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{})
287+
if err != nil {
288+
t.Errorf("websocket.Accept: %v", err)
289+
return
290+
}
291+
defer c.Close(websocket.StatusInternalError, "closing")
292+
wc := wsconn.NetConn(context.Background(), c, websocket.MessageBinary)
293+
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
294+
s.Accept(context.Background(), wc, brw, r.RemoteAddr)
295+
}))
296+
defer httpsrv.Close()
297+
httpsrv.TLS = &tls.Config{
298+
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
299+
// Add this to ensure fast start works!
300+
cert := httpsrv.TLS.Certificates[0]
301+
cert.Certificate = append(cert.Certificate, s.MetaCert())
302+
return &cert, nil
303+
},
304+
}
305+
httpsrv.StartTLS()
306+
307+
serverURL := httpsrv.URL
308+
t.Logf("server URL: %s", serverURL)
309+
310+
c, err := NewClient(key.NewNode(), serverURL, t.Logf)
311+
if err != nil {
312+
t.Fatalf("NewClient: %v", err)
313+
}
314+
c.ForceWebsockets = true
315+
c.TLSConfig = &tls.Config{
316+
ServerName: "example.com",
317+
RootCAs: httpsrv.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs,
318+
}
319+
defer c.Close()
320+
321+
err = c.Connect(context.Background())
322+
if err != nil {
323+
t.Fatalf("client errored initial connect: %v", err)
324+
}
325+
326+
c.Close()
327+
}

wgengine/magicsock/derp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha
351351
if header != nil {
352352
dc.Header = header.Clone()
353353
}
354+
dc.ForceWebsockets = c.derpForceWebsockets.Load()
354355
dialer := c.derpRegionDialer.Load()
355356
if dialer != nil {
356357
dc.SetRegionDialer(*dialer)

wgengine/magicsock/magicsock.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ type Conn struct {
170170
// headers that are passed to the DERP HTTP client
171171
derpHeader atomic.Pointer[http.Header]
172172

173+
// whether websocket is always used by the DERP HTTP client
174+
derpForceWebsockets atomic.Bool
175+
173176
// derpRegionDialer is passed to the DERP client
174177
derpRegionDialer atomic.Pointer[func(ctx context.Context, region *tailcfg.DERPRegion) net.Conn]
175178

@@ -1660,6 +1663,10 @@ func (c *Conn) SetDERPHeader(header http.Header) {
16601663
c.derpHeader.Store(&header)
16611664
}
16621665

1666+
func (c *Conn) SetDERPForceWebsockets(v bool) {
1667+
c.derpForceWebsockets.Store(v)
1668+
}
1669+
16631670
func (c *Conn) SetDERPRegionDialer(dialer func(ctx context.Context, region *tailcfg.DERPRegion) net.Conn) {
16641671
c.derpRegionDialer.Store(&dialer)
16651672
c.mu.Lock()

0 commit comments

Comments
 (0)