derp/forward.go
Rama main · réplica de lectura del repositorio original.
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package derp
import (
"sync"
"sync/atomic"
"tailscale.com/types/key"
)
// forwardBufSize is how many bytes of forward packet frames a
// forwardBuffers buffer holds before callers must wait: the TLS
// maximum plaintext record size, so a full buffer goes out as exactly
// one record.
//
// A buffer may grow past it for a single frame that is bigger than
// that, and is then kept at its larger size rather than reallocated
// for every such frame. So a client that has forwarded frames near
// [MaxPacketSize] holds two buffers of up to about 80 KiB each, with
// append's growth, instead of two of 16 KiB. Only clients that forward
// at all, in practice DERP servers' mesh connections, allocate them.
const forwardBufSize = 16 << 10
// ForwardDroppedFrames and ForwardDroppedBytes count, process-wide,
// the forward packet frames and bytes that all Clients have dropped
// because a write failed: the frames of the failed write and those
// buffered behind it, whose [Client.ForwardPacket] calls had already
// returned. The owner reconnects, but the frames are gone, and a DERP
// server has no other way to count them, having been told they were
// forwarded. Both count a deliberate close by the owner too, if a
// write was in flight. They only ever grow.
var ForwardDroppedFrames, ForwardDroppedBytes atomic.Int64
// A forwardFlusher is what [forwardBuffers] writes to: the connection
// side of [Client.ForwardPacket], passed to each call rather than
// stored, so a zero forwardBuffers is ready to use.
type forwardFlusher interface {
// flush writes buf, which holds whole frames, to the connection.
// It must not retain buf.
flush(buf []byte) error
// flushFailed is called once, after flush returns err, with how
// many frames and bytes were dropped: those of the failed write
// and those buffered behind it. No flush follows.
flushFailed(err error, lostFrames, lostBytes int)
}
// forwardBuffers double-buffers forward packet frames on their way to
// a connection. [forwardBuffers.push] appends a frame to pending,
// and a flusher goroutine, which push starts if none is running,
// swaps pending for the buffer it just wrote and writes it, with mu
// released, until nothing is pending. Callers keep filling pending
// while a write is in progress and wait only when pending is full,
// which is how a slow connection applies backpressure. The zero value
// is ready to use.
//
// Once a write fails, err is set and every push returns it, so the
// owner can reconnect.
type forwardBuffers struct {
mu sync.Mutex
cond sync.Cond // on mu; broadcast when pending is swapped out or err is set
pending []byte // frames waiting to be written; nil until first use
frames int // number of frames in pending
idle []byte // the other buffer while no flusher runs; nil while one does
err error // sticky: the first write error
}
// push appends a forward packet frame to the pending buffer, starting
// a flusher goroutine if none is running. It waits while pending is
// full. It returns the sticky write error, if any, without buffering.
func (fb *forwardBuffers) push(w forwardFlusher, srcKey, dstKey key.NodePublic, pkt []byte) error {
buf, err := fb.add(srcKey, dstKey, pkt)
if err != nil {
return err
}
// Whether a flusher is running is encoded in the idle buffer: it is
// nil exactly while one runs, since the flusher owns it then. So add
// returns a buffer to start a flusher with only when none is running;
// otherwise the running one picks the frame up at its next swap.
if buf != nil {
go fb.run(w, buf)
}
return nil
}
// add is push's work under mu: it appends the frame to pending and,
// if no flusher is running, claims the idle buffer and returns it for
// the caller to start one with. If a flusher is running, idle is nil
// and so is the result, and the running flusher will take the frame.
func (fb *forwardBuffers) add(srcKey, dstKey key.NodePublic, pkt []byte) (idle []byte, err error) {
fb.mu.Lock()
defer fb.mu.Unlock()
if fb.pending == nil {
fb.cond.L = &fb.mu
fb.pending = make([]byte, 0, forwardBufSize)
fb.idle = make([]byte, 0, forwardBufSize)
}
// Wait for room. The flusher empties pending by swapping it out,
// which it does once the write of the other buffer completes.
// An empty buffer always has room, even for a frame bigger than
// forwardBufSize, which grows it. Fullness is measured against
// forwardBufSize rather than the buffer's capacity so that a
// grown buffer, which is kept, doesn't batch more than the rest.
bodyLen := KeyLen*2 + len(pkt)
for fb.err == nil && len(fb.pending) > 0 && len(fb.pending)+FrameHeaderLen+bodyLen > forwardBufSize {
fb.cond.Wait()
}
if fb.err != nil {
return nil, fb.err
}
b := appendFrameHeader(fb.pending, FrameForwardPacket, uint32(bodyLen))
b = srcKey.AppendTo(b)
b = dstKey.AppendTo(b)
fb.pending = append(b, pkt...)
fb.frames++
// Claim the idle buffer for a new flusher, if there is one to
// claim; a nil idle means a flusher is running and owns it.
idle, fb.idle = fb.idle, nil
return idle, nil
}
// run is the flusher goroutine. It owns buf, the buffer not being
// filled, and trades it for pending each time around until nothing
// is pending, when it parks buf as idle and exits.
func (fb *forwardBuffers) run(w forwardFlusher, buf []byte) {
for {
var frames int
if buf, frames = fb.next(buf); buf == nil {
return
}
if err := w.flush(buf); err != nil {
fb.fail(w, err, buf, frames)
return
}
}
}
// next swaps buf, which the flusher has finished with, for the
// pending buffer and returns it with its frame count. If nothing is
// pending it parks buf as idle and returns nil, and the flusher must
// exit; the next push starts another.
func (fb *forwardBuffers) next(buf []byte) (pending []byte, frames int) {
fb.mu.Lock()
defer fb.mu.Unlock()
if len(fb.pending) == 0 {
fb.idle = buf[:0]
return nil, 0
}
pending, frames = fb.pending, fb.frames
fb.pending, fb.frames = buf[:0], 0
fb.cond.Broadcast() // pending has room again
return pending, frames
}
// fail ends forwarding after writing buf, holding frames frames,
// failed with err. It makes err sticky, drops what's pending, wakes
// waiting callers so they return err, and reports the loss to the
// flusher. The flusher's buffer is not parked, so no flusher starts
// again.
func (fb *forwardBuffers) fail(w forwardFlusher, err error, buf []byte, frames int) {
fb.mu.Lock()
fb.err = err
lostFrames, lostBytes := frames+fb.frames, len(buf)+len(fb.pending)
fb.pending, fb.frames = fb.pending[:0], 0
fb.cond.Broadcast()
fb.mu.Unlock()
ForwardDroppedFrames.Add(int64(lostFrames))
ForwardDroppedBytes.Add(int64(lostBytes))
w.flushFailed(err, lostFrames, lostBytes)
}