Skip to content

Commit 5734fc5

Browse files
committed
fix: add websocket fallback when http2 is used
HTTP/2 doesn't support the 'Upgrade' header, which breaks the DERP exchange.
1 parent fed359a commit 5734fc5

File tree

2 files changed

+105
-24
lines changed

2 files changed

+105
-24
lines changed

derp/derphttp/derphttp_client.go

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -347,26 +347,26 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
347347
case c.useWebsockets():
348348
var urlStr string
349349
var tlsConfig *tls.Config
350-
// WebSocket connections require a DERP to proxy.
351-
// DERP mappings have no explicit requirements on the ordering
352-
// of DERP vs STUNOnly nodes. So we pick the first non-STUNOnly one.
353-
var node *tailcfg.DERPNode
354-
for _, n := range reg.Nodes {
355-
if n.STUNOnly {
356-
continue
350+
if reg != nil {
351+
// WebSocket connections require a DERP to proxy.
352+
// DERP mappings have no explicit requirements on the ordering
353+
// of DERP vs STUNOnly nodes. So we pick the first non-STUNOnly one.
354+
var node *tailcfg.DERPNode
355+
for _, n := range reg.Nodes {
356+
if n.STUNOnly {
357+
continue
358+
}
359+
node = n
360+
break
361+
}
362+
if node == nil {
363+
return nil, 0, errors.New("no non-STUN-only nodes in region")
357364
}
358-
node = n
359-
break
360-
}
361-
if node == nil {
362-
return nil, 0, errors.New("no non-STUN-only nodes in region")
363-
}
364-
if c.url != nil {
365-
urlStr = c.url.String()
366-
tlsConfig = c.tlsConfig(nil)
367-
} else {
368365
urlStr = c.urlString(node)
369366
tlsConfig = c.tlsConfig(node)
367+
} else if c.url != nil {
368+
urlStr = c.url.String()
369+
tlsConfig = c.tlsConfig(nil)
370370
}
371371
c.logf("%s: connecting websocket to %v", caller, urlStr)
372372
conn, err := dialWebsocketFunc(ctx, urlStr, tlsConfig, c.Header)
@@ -492,6 +492,17 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
492492
// No need to flush the HTTP request. the derp.Client's initial
493493
// client auth frame will flush it.
494494
} else {
495+
regionID := 0
496+
if reg != nil {
497+
regionID = reg.RegionID
498+
}
499+
500+
if tlsState != nil && tlsState.NegotiatedProtocol == "h2" {
501+
reason := fmt.Sprintf("The server wanted us to use HTTP/2, but DERP requires Upgrade which needs HTTP/1.1")
502+
c.forceWebsockets(regionID, reason)
503+
return nil, 0, fmt.Errorf("DERP server did not switch protocols: %s", reason)
504+
}
505+
495506
if err := req.Write(brw); err != nil {
496507
return nil, 0, err
497508
}
@@ -512,17 +523,12 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
512523
// For safety, we'll assume that status codes >= 400 are
513524
// proxies that don't support WebSockets.
514525
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
515-
reason := fmt.Sprintf("GET failed with status code %d: %s", resp.StatusCode, b)
516-
c.logf("We'll use WebSockets on the next connection attempt. A proxy could be disallowing the use of 'Upgrade: derp': %s", reason)
517-
c.forcedWebsocket.Store(true)
518-
forcedWebsocketCallback := c.forcedWebsocketCallback.Load()
519-
if forcedWebsocketCallback != nil {
520-
go (*forcedWebsocketCallback)(reg.RegionID, reason)
521-
}
526+
c.forceWebsockets(regionID, fmt.Sprintf("GET failed with status code %d (a proxy could be disallowing the use of 'Upgrade: derp'): %s", resp.StatusCode, b))
522527
}
523528

524529
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
525530
}
531+
526532
}
527533
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf,
528534
derp.MeshKey(c.MeshKey),
@@ -571,6 +577,15 @@ func (c *Client) SetForcedWebsocketCallback(callback func(region int, reason str
571577
c.forcedWebsocketCallback.Store(&callback)
572578
}
573579

580+
func (c *Client) forceWebsockets(regionID int, reason string) {
581+
c.logf("We'll use WebSockets on the next connection attempt: %s", reason)
582+
c.forcedWebsocket.Store(true)
583+
forcedWebsocketCallback := c.forcedWebsocketCallback.Load()
584+
if forcedWebsocketCallback != nil {
585+
go (*forcedWebsocketCallback)(regionID, reason)
586+
}
587+
}
588+
574589
func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
575590
host := c.url.Hostname()
576591
if c.dialer != nil {
@@ -635,6 +650,7 @@ func (c *Client) tlsConfig(node *tailcfg.DERPNode) *tls.Config {
635650
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
636651
}
637652
}
653+
tlsConf.NextProtos = []string{"h2", "http/1.1"}
638654
return tlsConf
639655
}
640656

derp/derphttp/derphttp_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@
44
package derphttp
55

66
import (
7+
"bufio"
78
"bytes"
89
"context"
910
"crypto/tls"
1011
"net"
1112
"net/http"
13+
"net/http/httptest"
14+
"strings"
1215
"sync"
1316
"testing"
1417
"time"
1518

19+
"nhooyr.io/websocket"
1620
"tailscale.com/derp"
21+
"tailscale.com/net/wsconn"
1722
"tailscale.com/types/key"
1823
)
1924

@@ -206,3 +211,63 @@ func TestPing(t *testing.T) {
206211
t.Fatalf("Ping: %v", err)
207212
}
208213
}
214+
215+
func TestHTTP2WebSocketFallback(t *testing.T) {
216+
serverPrivateKey := key.NewNode()
217+
s := derp.NewServer(serverPrivateKey, t.Logf)
218+
defer s.Close()
219+
220+
httpsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
221+
up := r.Header.Get("Upgrade")
222+
if up != "websocket" {
223+
Handler(s).ServeHTTP(w, r)
224+
return
225+
}
226+
227+
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{})
228+
if err != nil {
229+
t.Errorf("websocket.Accept: %v", err)
230+
return
231+
}
232+
defer c.Close(websocket.StatusInternalError, "closing")
233+
wc := wsconn.NetConn(context.Background(), c, websocket.MessageBinary)
234+
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
235+
s.Accept(context.Background(), wc, brw, r.RemoteAddr)
236+
}))
237+
httpsrv.TLS = &tls.Config{
238+
NextProtos: []string{"h2", "http/1.1"},
239+
}
240+
httpsrv.StartTLS()
241+
242+
serverURL := httpsrv.URL
243+
t.Logf("server URL: %s", serverURL)
244+
245+
c, err := NewClient(key.NewNode(), serverURL, t.Logf)
246+
if err != nil {
247+
t.Fatalf("NewClient: %v", err)
248+
}
249+
c.TLSConfig = &tls.Config{
250+
ServerName: "example.com",
251+
RootCAs: httpsrv.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs,
252+
}
253+
defer c.Close()
254+
reasonCh := make(chan string, 1)
255+
c.SetForcedWebsocketCallback(func(region int, reason string) {
256+
select {
257+
case reasonCh <- reason:
258+
default:
259+
}
260+
})
261+
err = c.Connect(context.Background())
262+
if err == nil {
263+
// Expect an error!
264+
t.Fatal("client didn't error on initial connect")
265+
}
266+
reason := <-reasonCh
267+
if !strings.Contains(reason, "wanted us to use HTTP/2") {
268+
t.Fatalf("reason doesn't contain message: %s", reason)
269+
}
270+
if err := c.Connect(context.Background()); err != nil {
271+
t.Fatalf("client Connect: %v", err)
272+
}
273+
}

0 commit comments

Comments
 (0)