mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-12 05:37:32 +00:00
cmd/derper, derp/derphttp: add websocket support
Updates #3157 Change-Id: I337a919a3b350bc7bd9af567b49c4d5d6616abdd Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
0b62f26349
commit
505f844a43
@@ -22,6 +22,9 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -177,6 +180,20 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
}
|
||||
|
||||
// dialWebsocketFunc is non-nil (set by websocket.go's init) when compiled in.
|
||||
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
|
||||
|
||||
func useWebsockets() bool {
|
||||
if runtime.GOOS == "js" {
|
||||
return true
|
||||
}
|
||||
if dialWebsocketFunc != nil {
|
||||
v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_DERP_WS_CLIENT"))
|
||||
return v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) connect(ctx context.Context, caller string) (client *derp.Client, connGen int, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -229,10 +246,44 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}()
|
||||
|
||||
var node *tailcfg.DERPNode // nil when using c.url to dial
|
||||
if c.url != nil {
|
||||
switch {
|
||||
case useWebsockets():
|
||||
var urlStr string
|
||||
if c.url != nil {
|
||||
urlStr = c.url.String()
|
||||
} else {
|
||||
urlStr = c.urlString(reg.Nodes[0])
|
||||
}
|
||||
c.logf("%s: connecting websocket to %v", caller, urlStr)
|
||||
conn, err := dialWebsocketFunc(ctx, urlStr)
|
||||
if err != nil {
|
||||
c.logf("%s: websocket to %v error: %v", caller, urlStr, err)
|
||||
return nil, 0, err
|
||||
}
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||
derpClient, err := derp.NewClient(c.privateKey, conn, brw, c.logf,
|
||||
derp.MeshKey(c.MeshKey),
|
||||
derp.CanAckPings(c.canAckPings),
|
||||
derp.IsProber(c.IsProber),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if c.preferred {
|
||||
if err := derpClient.NotePreferred(true); err != nil {
|
||||
go conn.Close()
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
c.connGen++
|
||||
return c.client, c.connGen, nil
|
||||
case c.url != nil:
|
||||
c.logf("%s: connecting to %v", caller, c.url)
|
||||
tcpConn, err = c.dialURL(ctx)
|
||||
} else {
|
||||
default:
|
||||
c.logf("%s: connecting to derp-%d (%v)", caller, reg.RegionID, reg.RegionCode)
|
||||
tcpConn, node, err = c.dialRegion(ctx, reg)
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
@@ -20,10 +21,13 @@ const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p := r.Header.Get("Upgrade"); p != "WebSocket" && p != "DERP" {
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
log.Printf("Weird upgrade: %q", up)
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
|
33
derp/derphttp/websocket.go
Normal file
33
derp/derphttp/websocket.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
//go:build linux || js
|
||||
// +build linux js
|
||||
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dialWebsocketFunc = dialWebsocket
|
||||
}
|
||||
|
||||
func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
|
||||
c, res, err := websocket.Dial(ctx, urlStr, &websocket.DialOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("websocket Dial: %v, %+v", err, res)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("websocket: connected to %v", urlStr)
|
||||
return wsconn.New(c), nil
|
||||
}
|
104
derp/wsconn/wsconn.go
Normal file
104
derp/wsconn/wsconn.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2021 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 wsconn contains an adapter type that turns
|
||||
// a websocket connection into a net.Conn.
|
||||
package wsconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
// New returns a net.Conn wrapper around c,
|
||||
// using c to send and receive binary messages with
|
||||
// chunks of bytes with no defined framing, effectively
|
||||
// discarding all WebSocket-level message framing.
|
||||
func New(c *websocket.Conn) net.Conn {
|
||||
return &websocketConn{c: c}
|
||||
}
|
||||
|
||||
// websocketConn implements derp.Conn around a *websocket.Conn,
|
||||
// treating a websocket.Conn as a byte stream, ignoring the WebSocket
|
||||
// frame/message boundaries.
|
||||
type websocketConn struct {
|
||||
c *websocket.Conn
|
||||
|
||||
// rextra are extra bytes owned by the reader.
|
||||
rextra []byte
|
||||
|
||||
mu sync.Mutex
|
||||
rdeadline time.Time
|
||||
cancelRead context.CancelFunc
|
||||
}
|
||||
|
||||
func (wc *websocketConn) LocalAddr() net.Addr { return addr{} }
|
||||
func (wc *websocketConn) RemoteAddr() net.Addr { return addr{} }
|
||||
|
||||
type addr struct{}
|
||||
|
||||
func (addr) Network() string { return "websocket" }
|
||||
func (addr) String() string { return "websocket" }
|
||||
|
||||
func (wc *websocketConn) Read(p []byte) (n int, err error) {
|
||||
// Drain any leftover from previously.
|
||||
n = copy(p, wc.rextra)
|
||||
if n > 0 {
|
||||
wc.rextra = wc.rextra[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
|
||||
wc.mu.Lock()
|
||||
if dl := wc.rdeadline; !dl.IsZero() {
|
||||
ctx, cancel = context.WithDeadline(context.Background(), wc.rdeadline)
|
||||
} else {
|
||||
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(30*24*time.Hour))
|
||||
wc.rdeadline = time.Time{}
|
||||
}
|
||||
wc.cancelRead = cancel
|
||||
wc.mu.Unlock()
|
||||
defer cancel()
|
||||
|
||||
_, buf, err := wc.c.Read(ctx)
|
||||
n = copy(p, buf)
|
||||
wc.rextra = buf[n:]
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (wc *websocketConn) Write(p []byte) (n int, err error) {
|
||||
err = wc.c.Write(context.Background(), websocket.MessageBinary, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) Close() error { return wc.c.Close(websocket.StatusNormalClosure, "close") }
|
||||
|
||||
func (wc *websocketConn) SetDeadline(t time.Time) error {
|
||||
wc.SetReadDeadline(t)
|
||||
wc.SetWriteDeadline(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) SetReadDeadline(t time.Time) error {
|
||||
wc.mu.Lock()
|
||||
defer wc.mu.Unlock()
|
||||
if !t.IsZero() && (wc.rdeadline.IsZero() || t.Before(wc.rdeadline)) && wc.cancelRead != nil {
|
||||
wc.cancelRead()
|
||||
}
|
||||
wc.rdeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wc *websocketConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user