diff --git a/cmd/derper/websocket.go b/cmd/derper/websocket.go index 32cc9a009..141f38078 100644 --- a/cmd/derper/websocket.go +++ b/cmd/derper/websocket.go @@ -13,7 +13,7 @@ import ( "nhooyr.io/websocket" "tailscale.com/derp" - "tailscale.com/derp/wsconn" + "tailscale.com/net/wsconn" ) var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts") diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index b6863ac7f..56d4e0b44 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -42,7 +42,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/control/controlknobs from tailscale.com/net/portmapper tailscale.com/derp from tailscale.com/derp/derphttp tailscale.com/derp/derphttp from tailscale.com/net/netcheck - L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp tailscale.com/disco from tailscale.com/derp tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+ tailscale.com/hostinfo from tailscale.com/net/interfaces+ @@ -63,6 +62,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/net/tlsdial from tailscale.com/derp/derphttp tailscale.com/net/tsaddr from tailscale.com/net/interfaces+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ + L tailscale.com/net/wsconn from tailscale.com/derp/derphttp tailscale.com/paths from tailscale.com/cmd/tailscale/cli+ tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+ tailscale.com/syncs from tailscale.com/net/interfaces+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 3b6010c25..ba0de4bc2 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -76,7 +76,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink github.com/klauspost/compress from github.com/klauspost/compress/zstd - L github.com/klauspost/compress/flate from nhooyr.io/websocket + github.com/klauspost/compress/flate from nhooyr.io/websocket github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0 github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd @@ -170,9 +170,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de inet.af/netaddr from tailscale.com/control/controlclient+ inet.af/peercred from tailscale.com/ipn/ipnserver W 💣 inet.af/wf from tailscale.com/wf - L nhooyr.io/websocket from tailscale.com/derp/derphttp+ - L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket - L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket + nhooyr.io/websocket from tailscale.com/derp/derphttp+ + nhooyr.io/websocket/internal/errd from nhooyr.io/websocket + nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket tailscale.com from tailscale.com/version tailscale.com/atomicfile from tailscale.com/ipn+ LD tailscale.com/chirp from tailscale.com/cmd/tailscaled @@ -185,7 +185,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/control/controlknobs from tailscale.com/control/controlclient+ tailscale.com/derp from tailscale.com/derp/derphttp+ tailscale.com/derp/derphttp from tailscale.com/net/netcheck+ - L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp tailscale.com/disco from tailscale.com/derp+ tailscale.com/envknob from tailscale.com/control/controlclient+ tailscale.com/health from tailscale.com/control/controlclient+ @@ -232,6 +231,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/net/tsdial from tailscale.com/control/controlclient+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+ tailscale.com/net/tstun from tailscale.com/net/dns+ + tailscale.com/net/wsconn from tailscale.com/control/controlhttp+ tailscale.com/paths from tailscale.com/ipn/ipnlocal+ tailscale.com/portlist from tailscale.com/ipn/ipnlocal tailscale.com/safesocket from tailscale.com/client/tailscale+ diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 7985796a6..7d9671dfb 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -1097,12 +1097,6 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string var out tailcfg.OverTLSPublicKeyResponse jsonErr := json.Unmarshal(b, &out) if jsonErr == nil { - if runtime.GOOS == "js" { - // As of 2022-05-20 it's not possible for js/wasm to make a bidi - // Noise connection to the control plane. Instead, for now, pretend - // like the server can't do Noise to force use of the old protocol. - out.PublicKey = key.MachinePublic{} - } return &out, nil } diff --git a/control/controlhttp/client.go b/control/controlhttp/client.go index 7a12ba017..d7169fedb 100644 --- a/control/controlhttp/client.go +++ b/control/controlhttp/client.go @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !js +// +build !js + // Package controlhttp implements the Tailscale 2021 control protocol // base transport over HTTP. // @@ -40,21 +43,6 @@ import ( "tailscale.com/types/key" ) -const ( - // upgradeHeader is the value of the Upgrade HTTP header used to - // indicate the Tailscale control protocol. - upgradeHeaderValue = "tailscale-control-protocol" - - // handshakeHeaderName is the HTTP request header that can - // optionally contain base64-encoded initial handshake - // payload, to save an RTT. - handshakeHeaderName = "X-Tailscale-Handshake" - - // serverUpgradePath is where the server-side HTTP handler to - // to do the protocol switch is located. - serverUpgradePath = "/ts2021" -) - // Dial connects to the HTTP server at addr, requests to switch to the // Tailscale control protocol, and returns an established control // protocol connection. diff --git a/control/controlhttp/client_js.go b/control/controlhttp/client_js.go new file mode 100644 index 000000000..17b4998ca --- /dev/null +++ b/control/controlhttp/client_js.go @@ -0,0 +1,56 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package controlhttp + +import ( + "context" + "encoding/base64" + "net" + "net/url" + + "nhooyr.io/websocket" + "tailscale.com/control/controlbase" + "tailscale.com/net/dnscache" + "tailscale.com/net/wsconn" + "tailscale.com/types/key" +) + +// Variant of Dial that tunnels the request over WebScokets, since we cannot do +// bi-directional communication over an HTTP connection when in JS. +func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) { + init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion) + if err != nil { + return nil, err + } + + host, addr, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + wsURL := &url.URL{ + Scheme: "ws", + Host: net.JoinHostPort(host, addr), + Path: serverUpgradePath, + // Can't set HTTP headers on the websocket request, so we have to to send + // the handshake via an HTTP header. + RawQuery: url.Values{ + handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)}, + }.Encode(), + } + wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{ + Subprotocols: []string{upgradeHeaderValue}, + }) + if err != nil { + return nil, err + } + netConn := wsconn.New(wsConn) + cbConn, err := cont(ctx, netConn) + if err != nil { + netConn.Close() + return nil, err + } + return cbConn, nil + +} diff --git a/control/controlhttp/constants.go b/control/controlhttp/constants.go new file mode 100644 index 000000000..216adc269 --- /dev/null +++ b/control/controlhttp/constants.go @@ -0,0 +1,20 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package controlhttp + +const ( + // upgradeHeader is the value of the Upgrade HTTP header used to + // indicate the Tailscale control protocol. + upgradeHeaderValue = "tailscale-control-protocol" + + // handshakeHeaderName is the HTTP request header that can + // optionally contain base64-encoded initial handshake + // payload, to save an RTT. + handshakeHeaderName = "X-Tailscale-Handshake" + + // serverUpgradePath is where the server-side HTTP handler to + // to do the protocol switch is located. + serverUpgradePath = "/ts2021" +) diff --git a/control/controlhttp/server.go b/control/controlhttp/server.go index 0e38da860..54af7433a 100644 --- a/control/controlhttp/server.go +++ b/control/controlhttp/server.go @@ -11,8 +11,10 @@ import ( "fmt" "net/http" + "nhooyr.io/websocket" "tailscale.com/control/controlbase" "tailscale.com/net/netutil" + "tailscale.com/net/wsconn" "tailscale.com/types/key" ) @@ -27,6 +29,9 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri http.Error(w, "missing next protocol", http.StatusBadRequest) return nil, errors.New("no next protocol in HTTP request") } + if next == "websocket" { + return acceptWebsocket(ctx, w, r, private) + } if next != upgradeHeaderValue { http.Error(w, "unknown next protocol", http.StatusBadRequest) return nil, fmt.Errorf("client requested unhandled next protocol %q", next) @@ -71,3 +76,42 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri return nc, nil } + +// acceptWebsocket upgrades a WebSocket connection (from a client that cannot +// speak HTTP) to a Tailscale control protocol base transport connection. +func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{upgradeHeaderValue}, + OriginPatterns: []string{"*"}, + }) + if err != nil { + return nil, fmt.Errorf("Could not accept WebSocket connection %v", err) + } + if c.Subprotocol() != upgradeHeaderValue { + c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol") + return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol()) + } + if err := r.ParseForm(); err != nil { + c.Close(websocket.StatusPolicyViolation, "Could not parse parameters") + return nil, fmt.Errorf("parse query parameters: %v", err) + } + initB64 := r.Form.Get(handshakeHeaderName) + if initB64 == "" { + c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter") + return nil, errors.New("no tailscale handshake parameter in HTTP request") + } + init, err := base64.StdEncoding.DecodeString(initB64) + if err != nil { + c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter") + return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err) + } + + conn := wsconn.New(c) + nc, err := controlbase.Server(ctx, conn, private, init) + if err != nil { + conn.Close() + return nil, fmt.Errorf("noise handshake failed: %w", err) + } + + return nc, nil +} diff --git a/derp/derphttp/websocket.go b/derp/derphttp/websocket.go index c84bbeb9d..dcc41feb1 100644 --- a/derp/derphttp/websocket.go +++ b/derp/derphttp/websocket.go @@ -13,7 +13,7 @@ import ( "net" "nhooyr.io/websocket" - "tailscale.com/derp/wsconn" + "tailscale.com/net/wsconn" ) func init() { diff --git a/derp/wsconn/wsconn.go b/net/wsconn/wsconn.go similarity index 97% rename from derp/wsconn/wsconn.go rename to net/wsconn/wsconn.go index c53096718..dbb07ba4a 100644 --- a/derp/wsconn/wsconn.go +++ b/net/wsconn/wsconn.go @@ -23,7 +23,7 @@ func New(c *websocket.Conn) net.Conn { return &websocketConn{c: c} } -// websocketConn implements derp.Conn around a *websocket.Conn, +// websocketConn implements net.Conn around a *websocket.Conn, // treating a websocket.Conn as a byte stream, ignoring the WebSocket // frame/message boundaries. type websocketConn struct {