mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
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>
This commit is contained in:
parent
80157f3f37
commit
a9f32656f5
@ -13,7 +13,7 @@
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/wsconn"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
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/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+
|
||||
|
@ -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+
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 @@
|
||||
"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.
|
||||
|
56
control/controlhttp/client_js.go
Normal file
56
control/controlhttp/client_js.go
Normal file
@ -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
|
||||
|
||||
}
|
20
control/controlhttp/constants.go
Normal file
20
control/controlhttp/constants.go
Normal file
@ -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 @@
|
||||
"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
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp/wsconn"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -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 {
|
Loading…
Reference in New Issue
Block a user