This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ geographic regions to make sure users have low latency to their DERP home.
Regions generally have multiple nodes per region "meshed" (routing to each
other) together for redundancy: it allows for cloud failures or upgrades without
kicking users out to a higher latency region. Instead, clients will reconnect to
the next node in the region. Each node in the region is required to to be meshed
the next node in the region. Each node in the region is required to be meshed
with every other node in the region and forward packets to the other nodes in
the region. Packets are forwarded only one hop within the region. There is no
routing between regions. The assumption is that the mesh TCP connections are
+50 -50
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package derp implements the Designated Encrypted Relay for Packets (DERP)
@@ -27,7 +27,7 @@ import (
// including its on-wire framing overhead)
const MaxPacketSize = 64 << 10
// Magic is the DERP Magic number, sent in the frameServerKey frame
// Magic is the DERP Magic number, sent in the FrameServerKey frame
// upon initial connection.
const Magic = "DERP🔑" // 8 bytes: 0x44 45 52 50 f0 9f 94 91
@@ -58,15 +58,15 @@ Protocol flow:
Login:
* client connects
* server sends frameServerKey
* client sends frameClientInfo
* server sends frameServerInfo
* server sends FrameServerKey
* client sends FrameClientInfo
* server sends FrameServerInfo
Steady state:
* server occasionally sends frameKeepAlive (or framePing)
* client responds to any framePing with a framePong
* client sends frameSendPacket
* server then sends frameRecvPacket to recipient
* server occasionally sends FrameKeepAlive (or FramePing)
* client responds to any FramePing with a FramePong
* client sends FrameSendPacket
* server then sends FrameRecvPacket to recipient
*/
const (
FrameServerKey = FrameType(0x01) // 8B magic + 32B public key + (0+ bytes future use)
@@ -78,35 +78,38 @@ const (
FrameKeepAlive = FrameType(0x06) // no payload, no-op (to be replaced with ping/pong)
FrameNotePreferred = FrameType(0x07) // 1 byte payload: 0x01 or 0x00 for whether this is client's home node
// framePeerGone is sent from server to client to signal that
// FramePeerGone is sent from server to client to signal that
// a previous sender is no longer connected. That is, if A
// sent to B, and then if A disconnects, the server sends
// framePeerGone to B so B can forget that a reverse path
// FramePeerGone to B so B can forget that a reverse path
// exists on that connection to get back to A. It is also sent
// if A tries to send a CallMeMaybe to B and the server has no
// record of B
FramePeerGone = FrameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
// framePeerPresent is like framePeerGone, but for other members of the DERP
// FramePeerPresent is like FramePeerGone, but for other members of the DERP
// region when they're meshed up together.
//
// The message is at least 32 bytes (the public key of the peer that's
// connected). If there are at least 18 bytes remaining after that, it's the
// 16 byte IP + 2 byte BE uint16 port of the client. If there's another byte
// remaining after that, it's a PeerPresentFlags byte.
// While current servers send 41 bytes, old servers will send fewer, and newer
// servers might send more.
// remaining after that, it's a PeerPresentFlags byte. If there's another
// byte remaining after that, it's the length of the client's advertised
// app name (see ClientInfo.AppName), followed by that many bytes of app
// name.
// While current servers send at least 42 bytes, old servers will send
// fewer, and newer servers might send more.
FramePeerPresent = FrameType(0x09)
// frameWatchConns is how one DERP node in a regional mesh
// FrameWatchConns is how one DERP node in a regional mesh
// subscribes to the others in the region.
// There's no payload. If the sender doesn't have permission, the connection
// is closed. Otherwise, the client is initially flooded with
// framePeerPresent for all connected nodes, and then a stream of
// framePeerPresent & framePeerGone has peers connect and disconnect.
// FramePeerPresent for all connected nodes, and then a stream of
// FramePeerPresent & FramePeerGone has peers connect and disconnect.
FrameWatchConns = FrameType(0x10)
// frameClosePeer is a privileged frame type (requires the
// FrameClosePeer is a privileged frame type (requires the
// mesh key for now) that closes the provided peer's
// connection. (To be used for cluster load balancing
// purposes, when clients end up on a non-ideal node)
@@ -115,14 +118,14 @@ const (
FramePing = FrameType(0x12) // 8 byte ping payload, to be echoed back in framePong
FramePong = FrameType(0x13) // 8 byte payload, the contents of the ping being replied to
// frameHealth is sent from server to client to tell the client
// FrameHealth is sent from server to client to tell the client
// if their connection is unhealthy somehow. Currently the only unhealthy state
// is whether the connection is detected as a duplicate.
// The entire frame body is the text of the error message. An empty message
// clears the error state.
FrameHealth = FrameType(0x14)
// frameRestarting is sent from server to client for the
// FrameRestarting is sent from server to client for the
// server to declare that it's restarting. Payload is two big
// endian uint32 durations in milliseconds: when to reconnect,
// and how long to try total. See ServerRestartingMessage docs for
@@ -140,7 +143,7 @@ const (
PeerGoneReasonMeshConnBroke = PeerGoneReasonType(0xf0) // invented by Client.RunWatchConnectionLoop on disconnect; not sent on the wire
)
// PeerPresentFlags is an optional byte of bit flags sent after a framePeerPresent message.
// PeerPresentFlags is an optional byte of bit flags sent after a FramePeerPresent message.
//
// For a modern server, the value should always be non-zero. If the value is zero,
// that means the server doesn't support this field.
@@ -168,6 +171,8 @@ const FastStartHeader = "Derp-Fast-Start"
var bin = binary.BigEndian
// writeUint32 writes v to bw one byte at a time
// as a big-endian uint32.
func writeUint32(bw *bufio.Writer, v uint32) error {
var b [4]byte
bin.PutUint32(b[:], v)
@@ -183,21 +188,6 @@ func writeUint32(bw *bufio.Writer, v uint32) error {
return nil
}
func readUint32(br *bufio.Reader) (uint32, error) {
var b [4]byte
// Reading a byte at a time is a bit silly,
// but it causes b not to escape,
// which more than pays for the silliness.
for i := range &b {
c, err := br.ReadByte()
if err != nil {
return 0, err
}
b[i] = c
}
return bin.Uint32(b[:]), nil
}
// ReadFrameTypeHeader reads a frame header from br and
// verifies that the frame type matches wantType.
//
@@ -213,18 +203,16 @@ func ReadFrameTypeHeader(br *bufio.Reader, wantType FrameType) (frameLen uint32,
return frameLen, err
}
// ReadFrameHeader reads the header of a DERP frame,
// reading 5 bytes from br.
// ReadFrameHeader reads a DERP frame header ([FrameHeaderLen] bytes) from br.
// It uses Peek+Discard to read directly from bufio's internal buffer
// without copying or allocating.
func ReadFrameHeader(br *bufio.Reader) (t FrameType, frameLen uint32, err error) {
tb, err := br.ReadByte()
hdr, err := br.Peek(FrameHeaderLen)
if err != nil {
return 0, 0, err
}
frameLen, err = readUint32(br)
if err != nil {
return 0, 0, err
}
return FrameType(tb), frameLen, nil
defer br.Discard(FrameHeaderLen)
return FrameType(hdr[0]), bin.Uint32(hdr[1:FrameHeaderLen]), nil
}
// readFrame reads a frame header and then reads its payload into
@@ -260,14 +248,26 @@ func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t FrameType, frameLe
return t, frameLen, err
}
// WriteFrameHeader writes a frame header to bw.
//
// The frame header is 5 bytes: a one byte frame type
// followed by a big-endian uint32 length of the
// remaining frame (not including the 5 byte header).
// WriteFrameHeader writes a DERP frame header to bw: a one-byte frame
// type followed by a big-endian uint32 frame length.
//
// It uses AvailableBuffer to append the header directly into bufio's
// internal buffer without allocation, falling back to WriteByte when
// the buffer has insufficient space.
// It does not flush bw.
func WriteFrameHeader(bw *bufio.Writer, t FrameType, frameLen uint32) error {
// Fast path: enough space in the buffer to append the header
// directly without allocation via AvailableBuffer.
if bw.Available() >= FrameHeaderLen {
buf := bw.AvailableBuffer()
buf = append(buf, byte(t))
buf = bin.AppendUint32(buf, frameLen)
_, err := bw.Write(buf)
return err
}
// Slow path: buffer nearly full. Write byte-at-a-time to let
// bufio flush as needed, avoiding a heap allocation from append
// growing past AvailableBuffer's capacity.
if err := bw.WriteByte(byte(t)); err != nil {
return err
}
+71 -11
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package derp
@@ -33,6 +33,7 @@ type Client struct {
meshKey key.DERPMesh
canAckPings bool
isProber bool
appName string
wmu sync.Mutex // hold while writing to bw
bw *bufio.Writer
@@ -60,6 +61,7 @@ type clientOpt struct {
ServerPub key.NodePublic
CanAckPings bool
IsProber bool
AppName string
}
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
@@ -69,7 +71,7 @@ type clientOpt struct {
func MeshKey(k key.DERPMesh) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = k }) }
// IsProber returns a ClientOpt to pass to the DERP server during connect to
// declare that this client is a a prober.
// declare that this client is a prober.
func IsProber(v bool) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.IsProber = v }) }
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
@@ -84,6 +86,31 @@ func CanAckPings(v bool) ClientOpt {
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
}
// AppName returns a ClientOpt to set an opaque app name string to
// advertise to the DERP server for stats purposes. It is sent to the
// server in the ClientInfo. The name must be valid per [ValidAppName]
// or NewClient returns an error.
func AppName(name string) ClientOpt {
return clientOptFunc(func(o *clientOpt) { o.AppName = name })
}
// MaxAppNameLen is the maximum length in bytes of a [ClientInfo.AppName].
const MaxAppNameLen = 32
// ValidAppName reports whether name is a valid app name: at most
// [MaxAppNameLen] bytes of printable ASCII. The empty string is valid.
func ValidAppName(name string) bool {
if len(name) > MaxAppNameLen {
return false
}
for i := range len(name) {
if b := name[i]; b < ' ' || b > '~' {
return false
}
}
return true
}
func NewClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
var opt clientOpt
for _, o := range opts {
@@ -92,6 +119,9 @@ func NewClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf
}
o.update(&opt)
}
if !ValidAppName(opt.AppName) {
return nil, fmt.Errorf("invalid AppName %.40q", opt.AppName)
}
return newClient(privateKey, nc, brw, logf, opt)
}
@@ -106,6 +136,7 @@ func newClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf
meshKey: opt.MeshKey,
canAckPings: opt.CanAckPings,
isProber: opt.IsProber,
appName: opt.AppName,
clock: tstime.StdClock{},
}
if opt.ServerPub.IsZero() {
@@ -167,7 +198,7 @@ type ClientInfo struct {
// trusted clients. It's required to subscribe to the
// connection list & forward packets. It's empty for regular
// users.
MeshKey key.DERPMesh `json:"meshKey,omitempty,omitzero"`
MeshKey key.DERPMesh `json:"meshKey,omitzero"`
// Version is the DERP protocol version that the client was built with.
// See the ProtocolVersion const.
@@ -179,6 +210,12 @@ type ClientInfo struct {
// IsProber is whether this client is a prober.
IsProber bool `json:",omitempty"`
// AppName is an optional opaque app name string the client
// advertises to the server for stats purposes. It must be
// valid per [ValidAppName] or the server rejects the
// connection.
AppName string `json:",omitempty"`
}
// Equal reports if two clientInfo values are equal.
@@ -186,7 +223,7 @@ func (c *ClientInfo) Equal(other *ClientInfo) bool {
if c == nil || other == nil {
return c == other
}
if c.Version != other.Version || c.CanAckPings != other.CanAckPings || c.IsProber != other.IsProber {
if c.Version != other.Version || c.CanAckPings != other.CanAckPings || c.IsProber != other.IsProber || c.AppName != other.AppName {
return false
}
return c.MeshKey.Equal(other.MeshKey)
@@ -198,6 +235,7 @@ func (c *Client) sendClientKey() error {
MeshKey: c.meshKey,
CanAckPings: c.canAckPings,
IsProber: c.isProber,
AppName: c.appName,
})
if err != nil {
return err
@@ -249,15 +287,18 @@ func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
return c.bw.Flush()
}
func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err error) {
// ForwardPacket sends a forward packet frame carrying pkt from srcKey
// to dstKey. It writes and flushes pkt before returning and does not
// retain it.
func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt LoanedBytes) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("derp.ForwardPacket: %w", err)
}
}()
if len(pkt) > MaxPacketSize {
return fmt.Errorf("packet too big: %d", len(pkt))
if pkt.Len() > MaxPacketSize {
return fmt.Errorf("packet too big: %d", pkt.Len())
}
c.wmu.Lock()
@@ -266,7 +307,7 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
timer := c.clock.AfterFunc(5*time.Second, c.writeTimeoutFired)
defer timer.Stop()
if err := WriteFrameHeader(c.bw, FrameForwardPacket, uint32(KeyLen*2+len(pkt))); err != nil {
if err := WriteFrameHeader(c.bw, FrameForwardPacket, uint32(KeyLen*2+pkt.Len())); err != nil {
return err
}
if _, err := c.bw.Write(srcKey.AppendTo(nil)); err != nil {
@@ -275,7 +316,7 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
return err
}
if _, err := c.bw.Write(pkt); err != nil {
if _, err := pkt.WriteTo(c.bw); err != nil {
return err
}
return c.bw.Flush()
@@ -394,6 +435,10 @@ type PeerPresentMessage struct {
IPPort netip.AddrPort
// Flags is a bitmask of info about the client.
Flags PeerPresentFlags
// AppName is the optional app name the client advertised in
// its ClientInfo, if any. It's empty if the client didn't
// send one or the server is too old to relay it.
AppName string
}
func (PeerPresentMessage) msg() {}
@@ -554,7 +599,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
return sm, nil
case FrameKeepAlive:
// A one-way keep-alive message that doesn't require an acknowledgement.
// This predated framePing/framePong.
// This predated FramePing/FramePong.
return KeepAliveMessage{}, nil
case FramePeerGone:
if n < KeyLen {
@@ -594,12 +639,27 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
binary.BigEndian.Uint16(chunk[ipLen:]),
)
chunk, _, ok = cutLeadingN(remain, 1)
chunk, remain, ok = cutLeadingN(remain, 1)
if !ok {
// Older server which doesn't send PeerPresentFlags.
return msg, nil
}
msg.Flags = PeerPresentFlags(chunk[0])
chunk, remain, ok = cutLeadingN(remain, 1)
if !ok {
// Older server which doesn't send the app name.
return msg, nil
}
nameLen := int(chunk[0])
chunk, _, ok = cutLeadingN(remain, nameLen)
if !ok {
c.logf("[unexpected] short peerPresent app name from DERP server")
return msg, nil
}
if name := string(chunk); ValidAppName(name) {
msg.AppName = name
}
return msg, nil
case FrameRecvPacket:
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package derpconst contains constants used by the DERP client and server.
+35 -5
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package derphttp implements DERP-over-HTTP.
@@ -29,6 +29,7 @@ import (
"time"
"go4.org/mem"
"golang.org/x/net/http/httpguts"
"tailscale.com/derp"
"tailscale.com/derp/derpconst"
"tailscale.com/envknob"
@@ -60,6 +61,7 @@ type Client struct {
DNSCache *dnscache.Resolver // optional; nil means no caching
MeshKey key.DERPMesh // optional; for trusted clients
IsProber bool // optional; for probers to optional declare themselves as such
AppName string // optional; opaque app name to advertise to the server for stats
// WatchConnectionChanges is whether the client wishes to subscribe to
// notifications about clients connecting & disconnecting.
@@ -179,7 +181,7 @@ func NewClient(privateKey key.NodePrivate, serverURL string, logf logger.Logf, n
// isStarted reports whether this client has been used yet.
//
// If if reports false, it may still have its exported fields configured.
// If it reports false, it may still have its exported fields configured.
func (c *Client) isStarted() bool {
c.mu.Lock()
defer c.mu.Unlock()
@@ -280,10 +282,16 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
return c.url.String()
}
proto := "https"
defaultPort := 443
if debugUseDERPHTTP() {
proto = "http"
defaultPort = 80
}
return fmt.Sprintf("%s://%s/derp", proto, node.HostName)
host := node.HostName
if node.DERPPort != 0 && node.DERPPort != defaultPort {
host = net.JoinHostPort(host, fmt.Sprint(node.DERPPort))
}
return fmt.Sprintf("%s://%s/derp", proto, host)
}
// AddressFamilySelector decides whether IPv6 is preferred for
@@ -407,6 +415,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
derp.MeshKey(c.MeshKey),
derp.CanAckPings(c.canAckPings),
derp.IsProber(c.IsProber),
derp.AppName(c.AppName),
)
if err != nil {
return nil, 0, err
@@ -552,6 +561,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
derp.ServerPublicKey(serverPub),
derp.CanAckPings(c.canAckPings),
derp.IsProber(c.IsProber),
derp.AppName(c.AppName),
)
if err != nil {
return nil, 0, err
@@ -836,6 +846,16 @@ func firstStr(a, b string) string {
// dialNodeUsingProxy connects to n using a CONNECT to the HTTP(s) proxy in proxyURL.
func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, proxyURL *url.URL) (_ net.Conn, err error) {
// n.HostName comes from the control-supplied DERP map and is written
// verbatim into the CONNECT request line and Host header below. Reject
// anything that isn't a valid host value so a hostname carrying CR/LF (or
// other control bytes) can't inject extra headers or a second request into
// the proxy connection. ValidHostHeader accepts the empty string, so check
// for that separately.
if n.HostName == "" || !httpguts.ValidHostHeader(n.HostName) {
return nil, fmt.Errorf("derphttp: invalid DERP node hostname %q", n.HostName)
}
pu := proxyURL
var proxyConn net.Conn
if pu.Scheme == "https" {
@@ -867,7 +887,15 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
}
}()
target := net.JoinHostPort(n.HostName, "443")
// Keep port selection in sync with dialNode.
port := "443"
if !c.useHTTPS() {
port = "3340"
}
if n.DERPPort != 0 {
port = fmt.Sprint(n.DERPPort)
}
target := net.JoinHostPort(n.HostName, port)
var authHeader string
if buildfeatures.HasUseProxy {
@@ -1002,7 +1030,9 @@ func (c *Client) LocalAddr() (netip.AddrPort, error) {
return la, nil
}
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
// ForwardPacket forwards b from the node from to the node to over the
// mesh connection. It does not retain b after it returns.
func (c *Client) ForwardPacket(from, to key.NodePublic, b derp.LoanedBytes) error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.ForwardPacket")
if err != nil {
return err
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package derphttp
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build js || ((linux || darwin) && ts_debug_websockets)
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(js || ((linux || darwin) && ts_debug_websockets))
+33
View File
@@ -0,0 +1,33 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package derp
import (
"bytes"
"io"
)
// LoanedBytes is a packet payload lent to a callee for the duration of
// a call. The lender reuses the memory once the call returns, so the
// callee must not retain it; Clone gives it a copy to keep.
//
// It deliberately has no accessor for the underlying slice.
type LoanedBytes struct {
bs []byte
}
// LoanBytes lends b for the duration of the call it's passed to.
func LoanBytes(b []byte) LoanedBytes { return LoanedBytes{bs: b} }
// Len returns the number of bytes.
func (b LoanedBytes) Len() int { return len(b.bs) }
// WriteTo writes the bytes to w.
func (b LoanedBytes) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(b.bs)
return int64(n), err
}
// Clone returns a copy of the bytes that the caller owns.
func (b LoanedBytes) Clone() []byte { return bytes.Clone(b.bs) }