control/controlhttp: allow client and server to communicate over WebSockets
We can't do Noise-over-HTTP in Wasm/JS (because we don't have bidirectional communication), but we should be able to do it over WebSockets. Reuses derp WebSocket support that allows us to turn a WebSocket connection into a net.Conn. Updates #3157 Signed-off-by: Mihai Parparita <mihai@tailscale.com>pull/4802/head
parent
80157f3f37
commit
a9f32656f5
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
"tailscale.com/derp"
|
"tailscale.com/derp"
|
||||||
"tailscale.com/derp/wsconn"
|
"tailscale.com/net/wsconn"
|
||||||
)
|
)
|
||||||
|
|
||||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||||
|
|
|
@ -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/control/controlknobs from tailscale.com/net/portmapper
|
||||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
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/disco from tailscale.com/derp
|
||||||
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/hostinfo from tailscale.com/net/interfaces+
|
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/tlsdial from tailscale.com/derp/derphttp
|
||||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
💣 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/paths from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||||
|
|
|
@ -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 from tailscale.com/net/interfaces+
|
||||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
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/fse from github.com/klauspost/compress/huff0
|
||||||
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
|
||||||
github.com/klauspost/compress/internal/cpuinfo 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/netaddr from tailscale.com/control/controlclient+
|
||||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||||
W 💣 inet.af/wf from tailscale.com/wf
|
W 💣 inet.af/wf from tailscale.com/wf
|
||||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||||
tailscale.com from tailscale.com/version
|
tailscale.com from tailscale.com/version
|
||||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||||
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
|
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/control/controlknobs from tailscale.com/control/controlclient+
|
||||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
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/disco from tailscale.com/derp+
|
||||||
tailscale.com/envknob from tailscale.com/control/controlclient+
|
tailscale.com/envknob from tailscale.com/control/controlclient+
|
||||||
tailscale.com/health 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/tsdial from tailscale.com/control/controlclient+
|
||||||
💣 tailscale.com/net/tshttpproxy 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/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/paths from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||||
|
|
|
@ -1097,12 +1097,6 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||||
var out tailcfg.OverTLSPublicKeyResponse
|
var out tailcfg.OverTLSPublicKeyResponse
|
||||||
jsonErr := json.Unmarshal(b, &out)
|
jsonErr := json.Unmarshal(b, &out)
|
||||||
if jsonErr == nil {
|
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
|
return &out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !js
|
||||||
|
// +build !js
|
||||||
|
|
||||||
// Package controlhttp implements the Tailscale 2021 control protocol
|
// Package controlhttp implements the Tailscale 2021 control protocol
|
||||||
// base transport over HTTP.
|
// base transport over HTTP.
|
||||||
//
|
//
|
||||||
|
@ -40,21 +43,6 @@ import (
|
||||||
"tailscale.com/types/key"
|
"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
|
// Dial connects to the HTTP server at addr, requests to switch to the
|
||||||
// Tailscale control protocol, and returns an established control
|
// Tailscale control protocol, and returns an established control
|
||||||
// protocol connection.
|
// protocol connection.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
)
|
|
@ -11,8 +11,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"nhooyr.io/websocket"
|
||||||
"tailscale.com/control/controlbase"
|
"tailscale.com/control/controlbase"
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
|
"tailscale.com/net/wsconn"
|
||||||
"tailscale.com/types/key"
|
"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)
|
http.Error(w, "missing next protocol", http.StatusBadRequest)
|
||||||
return nil, errors.New("no next protocol in HTTP request")
|
return nil, errors.New("no next protocol in HTTP request")
|
||||||
}
|
}
|
||||||
|
if next == "websocket" {
|
||||||
|
return acceptWebsocket(ctx, w, r, private)
|
||||||
|
}
|
||||||
if next != upgradeHeaderValue {
|
if next != upgradeHeaderValue {
|
||||||
http.Error(w, "unknown next protocol", http.StatusBadRequest)
|
http.Error(w, "unknown next protocol", http.StatusBadRequest)
|
||||||
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"nhooyr.io/websocket"
|
"nhooyr.io/websocket"
|
||||||
"tailscale.com/derp/wsconn"
|
"tailscale.com/net/wsconn"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -23,7 +23,7 @@ func New(c *websocket.Conn) net.Conn {
|
||||||
return &websocketConn{c: c}
|
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
|
// treating a websocket.Conn as a byte stream, ignoring the WebSocket
|
||||||
// frame/message boundaries.
|
// frame/message boundaries.
|
||||||
type websocketConn struct {
|
type websocketConn struct {
|
Loading…
Reference in New Issue