From 05798327889f68196e8b431e4fb04fa8d1df4dcd Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 21 Aug 2023 15:30:45 +0000 Subject: [PATCH 1/2] feat: add derphttp option to always use websocket fallback --- derp/derphttp/derphttp_client.go | 11 ++++++ derp/derphttp/derphttp_test.go | 60 ++++++++++++++++++++++++++++++++ wgengine/magicsock/derp.go | 1 + wgengine/magicsock/magicsock.go | 7 ++++ 4 files changed, 79 insertions(+) diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index e70966af6543f..baf8e2caac9ec 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -58,6 +58,14 @@ type Client struct { MeshKey string // optional; for trusted clients IsProber bool // optional; for probers to optional declare themselves as such + // Allow forcing WebSocket fallback for situations where proxies do not + // play well with `Upgrade: derp`. Turning this on will cause the client to + // always try WebSocket on it's first attempt. The callback will not be + // called if this is true. + // + // Example proxies include: + // - Azure Application Proxy (which redirects to login) + AlwaysUseWebsockets bool forcedWebsocket atomic.Bool // optional; set if the server has failed to upgrade the connection on the DERP server forcedWebsocketCallback atomic.Pointer[func(int, string)] @@ -301,6 +309,9 @@ func (c *Client) preferIPv6() bool { var dialWebsocketFunc func(ctx context.Context, urlStr string, tlsConfig *tls.Config, httpHeader http.Header) (net.Conn, error) func (c *Client) useWebsockets() bool { + if c.AlwaysUseWebsockets { + return true + } if runtime.GOOS == "js" { return true } diff --git a/derp/derphttp/derphttp_test.go b/derp/derphttp/derphttp_test.go index 20d43966b8a09..a6cddb5e751b4 100644 --- a/derp/derphttp/derphttp_test.go +++ b/derp/derphttp/derphttp_test.go @@ -265,3 +265,63 @@ func TestHTTP2OnlyServer(t *testing.T) { c.Close() } + +func TestAlwaysUseWebsocket(t *testing.T) { + serverPrivateKey := key.NewNode() + s := derp.NewServer(serverPrivateKey, t.Logf) + defer s.Close() + + httpsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + up := r.Header.Get("Upgrade") + if up == "" { + Handler(s).ServeHTTP(w, r) + return + } + if up != "websocket" { + // Should only attempt to upgrade to websocket. + t.Errorf("unexpected Upgrade header: %q", up) + return + } + + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{}) + if err != nil { + t.Errorf("websocket.Accept: %v", err) + return + } + defer c.Close(websocket.StatusInternalError, "closing") + wc := wsconn.NetConn(context.Background(), c, websocket.MessageBinary) + brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc)) + s.Accept(context.Background(), wc, brw, r.RemoteAddr) + })) + defer httpsrv.Close() + httpsrv.TLS = &tls.Config{ + GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { + // Add this to ensure fast start works! + cert := httpsrv.TLS.Certificates[0] + cert.Certificate = append(cert.Certificate, s.MetaCert()) + return &cert, nil + }, + } + httpsrv.StartTLS() + + serverURL := httpsrv.URL + t.Logf("server URL: %s", serverURL) + + c, err := NewClient(key.NewNode(), serverURL, t.Logf) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + c.AlwaysUseWebsockets = true + c.TLSConfig = &tls.Config{ + ServerName: "example.com", + RootCAs: httpsrv.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs, + } + defer c.Close() + + err = c.Connect(context.Background()) + if err != nil { + t.Fatalf("client errored initial connect: %v", err) + } + + c.Close() +} diff --git a/wgengine/magicsock/derp.go b/wgengine/magicsock/derp.go index d177555baa542..4b4bb459122b4 100644 --- a/wgengine/magicsock/derp.go +++ b/wgengine/magicsock/derp.go @@ -351,6 +351,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha if header != nil { dc.Header = header.Clone() } + dc.AlwaysUseWebsockets = c.derpAlwaysUseWebsockets.Load() dialer := c.derpRegionDialer.Load() if dialer != nil { dc.SetRegionDialer(*dialer) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 0c9297b6eb699..c69529217e3e2 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -170,6 +170,9 @@ type Conn struct { // headers that are passed to the DERP HTTP client derpHeader atomic.Pointer[http.Header] + // whether websocket is always used by the DERP HTTP client + derpAlwaysUseWebsockets atomic.Bool + // derpRegionDialer is passed to the DERP client derpRegionDialer atomic.Pointer[func(ctx context.Context, region *tailcfg.DERPRegion) net.Conn] @@ -1660,6 +1663,10 @@ func (c *Conn) SetDERPHeader(header http.Header) { c.derpHeader.Store(&header) } +func (c *Conn) SetDERPAlwaysUseWebsockets(v bool) { + c.derpAlwaysUseWebsockets.Store(v) +} + func (c *Conn) SetDERPRegionDialer(dialer func(ctx context.Context, region *tailcfg.DERPRegion) net.Conn) { c.derpRegionDialer.Store(&dialer) c.mu.Lock() From ca072a240cdcd2f748622c8fe32dcb4560fce46d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 21 Aug 2023 15:37:37 +0000 Subject: [PATCH 2/2] fixup! feat: add derphttp option to always use websocket fallback --- derp/derphttp/derphttp_client.go | 4 ++-- derp/derphttp/derphttp_test.go | 4 ++-- wgengine/magicsock/derp.go | 2 +- wgengine/magicsock/magicsock.go | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index baf8e2caac9ec..cc97c6e71a09d 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -65,7 +65,7 @@ type Client struct { // // Example proxies include: // - Azure Application Proxy (which redirects to login) - AlwaysUseWebsockets bool + ForceWebsockets bool forcedWebsocket atomic.Bool // optional; set if the server has failed to upgrade the connection on the DERP server forcedWebsocketCallback atomic.Pointer[func(int, string)] @@ -309,7 +309,7 @@ func (c *Client) preferIPv6() bool { var dialWebsocketFunc func(ctx context.Context, urlStr string, tlsConfig *tls.Config, httpHeader http.Header) (net.Conn, error) func (c *Client) useWebsockets() bool { - if c.AlwaysUseWebsockets { + if c.ForceWebsockets { return true } if runtime.GOOS == "js" { diff --git a/derp/derphttp/derphttp_test.go b/derp/derphttp/derphttp_test.go index a6cddb5e751b4..01ee561db0e22 100644 --- a/derp/derphttp/derphttp_test.go +++ b/derp/derphttp/derphttp_test.go @@ -266,7 +266,7 @@ func TestHTTP2OnlyServer(t *testing.T) { c.Close() } -func TestAlwaysUseWebsocket(t *testing.T) { +func TestForceWebsockets(t *testing.T) { serverPrivateKey := key.NewNode() s := derp.NewServer(serverPrivateKey, t.Logf) defer s.Close() @@ -311,7 +311,7 @@ func TestAlwaysUseWebsocket(t *testing.T) { if err != nil { t.Fatalf("NewClient: %v", err) } - c.AlwaysUseWebsockets = true + c.ForceWebsockets = true c.TLSConfig = &tls.Config{ ServerName: "example.com", RootCAs: httpsrv.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs, diff --git a/wgengine/magicsock/derp.go b/wgengine/magicsock/derp.go index 4b4bb459122b4..eb0b2280f8b86 100644 --- a/wgengine/magicsock/derp.go +++ b/wgengine/magicsock/derp.go @@ -351,7 +351,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha if header != nil { dc.Header = header.Clone() } - dc.AlwaysUseWebsockets = c.derpAlwaysUseWebsockets.Load() + dc.ForceWebsockets = c.derpForceWebsockets.Load() dialer := c.derpRegionDialer.Load() if dialer != nil { dc.SetRegionDialer(*dialer) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index c69529217e3e2..10e2d4a67336e 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -171,7 +171,7 @@ type Conn struct { derpHeader atomic.Pointer[http.Header] // whether websocket is always used by the DERP HTTP client - derpAlwaysUseWebsockets atomic.Bool + derpForceWebsockets atomic.Bool // derpRegionDialer is passed to the DERP client derpRegionDialer atomic.Pointer[func(ctx context.Context, region *tailcfg.DERPRegion) net.Conn] @@ -1663,8 +1663,8 @@ func (c *Conn) SetDERPHeader(header http.Header) { c.derpHeader.Store(&header) } -func (c *Conn) SetDERPAlwaysUseWebsockets(v bool) { - c.derpAlwaysUseWebsockets.Store(v) +func (c *Conn) SetDERPForceWebsockets(v bool) { + c.derpForceWebsockets.Store(v) } func (c *Conn) SetDERPRegionDialer(dialer func(ctx context.Context, region *tailcfg.DERPRegion) net.Conn) {