owpengram-server/internal/mtprotoedge/frame_budget.go

251 lines
8 KiB
Go

package mtprotoedge
import (
"encoding/binary"
"errors"
"fmt"
"io"
"sync/atomic"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/proto/codec"
"github.com/iamxvbaba/td/transport"
)
const defaultInboundFrameGlobalMaxBytes int64 = 512 << 20
var (
// ErrInboundFrameBudgetExceeded means the process-wide wire+plaintext reservation for a
// newly announced transport frame could not be acquired. The length prefix has been read,
// but the payload buffer has not been allocated and the connection must be closed.
ErrInboundFrameBudgetExceeded = errors.New("inbound frame global byte budget exceeded")
errInboundFrameCodecUnsupported = errors.New("transport codec cannot preflight inbound frame length")
errInboundFrameNotReserved = errors.New("transport codec returned a frame without reserving inbound bytes")
)
// InboundFrameBudgetedCodec is the fail-safe extension point for a custom Options.Codec.
// Implementations must parse and validate the frame length, call reserve exactly once before
// allocating or growing the payload buffer, and keep the reservation valid until Read returns.
// Built-in abridged/intermediate/padded-intermediate/full codecs are recognized directly.
type InboundFrameBudgetedCodec interface {
transport.Codec
ReadWithInboundFrameBudget(r io.Reader, b *bin.Buffer, reserve func(wireBytes, plaintextBytes int64) error) error
}
// inboundFrameBudget accounts the two per-frame buffers that can coexist while an encrypted
// request is handled: transport/wire bytes and decrypted plaintext. It deliberately charges the
// maximum plaintext size announced by framing even for an unencrypted handshake frame; that
// conservative rule makes admission independent of auth state and prevents allocation before
// auth_key_id can be inspected.
type inboundFrameBudget struct {
max int64
used atomic.Int64
}
func newInboundFrameBudget(max int64) *inboundFrameBudget {
if max <= 0 {
max = defaultInboundFrameGlobalMaxBytes
}
return &inboundFrameBudget{max: max}
}
func (b *inboundFrameBudget) reserve(wireBytes, plaintextBytes int64) (int64, error) {
return b.growReservation(0, wireBytes, plaintextBytes)
}
// growReservation atomically raises one connection's existing retained/frame reservation to
// cover a newly announced frame. Keeping the old charge until this transition is what makes a
// reused transport/plaintext backing remain accounted between frames; a small next frame cannot
// release a previously large allocation while still retaining its capacity.
func (b *inboundFrameBudget) growReservation(current, wireBytes, plaintextBytes int64) (int64, error) {
if current < 0 || wireBytes <= 0 || plaintextBytes < 0 || wireBytes > b.max || plaintextBytes > b.max-wireBytes {
return 0, fmt.Errorf("%w: wire=%d plaintext=%d limit=%d", ErrInboundFrameBudgetExceeded, wireBytes, plaintextBytes, b.max)
}
target := wireBytes + plaintextBytes
if target <= current {
return current, nil
}
n := target - current
for {
used := b.used.Load()
if n > b.max-used {
return 0, fmt.Errorf("%w: requested=%d used=%d limit=%d", ErrInboundFrameBudgetExceeded, n, used, b.max)
}
if b.used.CompareAndSwap(used, used+n) {
return target, nil
}
}
}
func (b *inboundFrameBudget) release(n int64) {
if n == 0 {
return
}
used := b.used.Add(-n)
if used < 0 {
// This is an internal ownership invariant, not recoverable input. A negative value would
// silently disable admission for subsequent frames, so fail loudly during development.
panic("mtprotoedge: inbound frame budget released more than reserved")
}
}
func (b *inboundFrameBudget) usedBytes() int64 {
return b.used.Load()
}
type inboundFrameCodecKind uint8
const (
inboundFrameCodecUnknown inboundFrameCodecKind = iota
inboundFrameCodecQuickAckAbridged
inboundFrameCodecAbridged
inboundFrameCodecIntermediate
inboundFrameCodecPaddedIntermediate
inboundFrameCodecFull
inboundFrameCodecCustom
)
func classifyInboundFrameCodec(c transport.Codec) inboundFrameCodecKind {
switch v := c.(type) {
case *quickAckAbridgedCodec:
return inboundFrameCodecQuickAckAbridged
case codec.Abridged, *codec.Abridged:
return inboundFrameCodecAbridged
case *quickAckIntermediateCodec, codec.Intermediate, *codec.Intermediate:
return inboundFrameCodecIntermediate
case *quickAckPaddedIntermediateCodec, codec.PaddedIntermediate, *codec.PaddedIntermediate:
return inboundFrameCodecPaddedIntermediate
case *codec.Full:
return inboundFrameCodecFull
case codec.NoHeader:
return classifyInboundFrameCodec(v.Codec)
case *codec.NoHeader:
if v == nil {
return inboundFrameCodecUnknown
}
return classifyInboundFrameCodec(v.Codec)
case InboundFrameBudgetedCodec:
return inboundFrameCodecCustom
default:
return inboundFrameCodecUnknown
}
}
func unwrapInboundFrameBudgetedCodec(c transport.Codec) InboundFrameBudgetedCodec {
switch v := c.(type) {
case InboundFrameBudgetedCodec:
return v
case codec.NoHeader:
return unwrapInboundFrameBudgetedCodec(v.Codec)
case *codec.NoHeader:
if v != nil {
return unwrapInboundFrameBudgetedCodec(v.Codec)
}
}
return nil
}
// inboundFramePreflightReader consumes only the framing length prefix, reserves the announced
// wire+plaintext bytes, and only then exposes the final prefix bytes to the codec. Consequently a
// budget error is observed by codec.Read before it can ResetN/Expand the payload buffer.
type inboundFramePreflightReader struct {
r io.Reader
kind inboundFrameCodecKind
reserve func(wireBytes, plaintextBytes int64) error
abridgedFirstDelivered bool
done bool
}
func (r *inboundFramePreflightReader) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
if r.done {
return r.r.Read(p)
}
switch r.kind {
case inboundFrameCodecQuickAckAbridged:
return r.readAbridgedPrefix(p, true)
case inboundFrameCodecAbridged:
return r.readAbridgedPrefix(p, false)
case inboundFrameCodecIntermediate, inboundFrameCodecPaddedIntermediate:
return r.readWordPrefix(p, false)
case inboundFrameCodecFull:
return r.readWordPrefix(p, true)
default:
return 0, errInboundFrameCodecUnsupported
}
}
func (r *inboundFramePreflightReader) readAbridgedPrefix(p []byte, quickAck bool) (int, error) {
if !r.abridgedFirstDelivered {
var first [1]byte
if _, err := io.ReadFull(r.r, first[:]); err != nil {
return 0, err
}
lengthByte := first[0]
extended := lengthByte >= 0x7f
if quickAck {
lengthByte &= 0x7f
extended = lengthByte == 0x7f
}
if !extended {
n := int64(lengthByte) * bin.Word
if err := reserveCompatFrame(r.reserve, n, n); err != nil {
return 0, err
}
r.done = true
}
r.abridgedFirstDelivered = true
p[0] = first[0]
return 1, nil
}
var tail [3]byte
if _, err := io.ReadFull(r.r, tail[:]); err != nil {
return 0, err
}
words := uint32(tail[0]) | uint32(tail[1])<<8 | uint32(tail[2])<<16
n := int64(words) * bin.Word
if err := reserveCompatFrame(r.reserve, n, n); err != nil {
return 0, err
}
r.done = true
return copy(p, tail[:]), nil
}
func (r *inboundFramePreflightReader) readWordPrefix(p []byte, full bool) (int, error) {
var header [bin.Word]byte
if _, err := io.ReadFull(r.r, header[:]); err != nil {
return 0, err
}
raw := int64(binary.LittleEndian.Uint32(header[:]))
var wireBytes, plaintextBytes int64
if full {
// Full transport length includes length + sequence + payload + CRC.
if raw < 3*bin.Word || raw > maxTransportMessageSize {
return 0, fmt.Errorf("invalid full transport message length %d", raw)
}
wireBytes = raw
plaintextBytes = raw - 3*bin.Word
} else {
wireBytes = raw &^ int64(quickAckResponseFlag)
plaintextBytes = wireBytes
}
if err := reserveCompatFrame(r.reserve, wireBytes, plaintextBytes); err != nil {
return 0, err
}
r.done = true
return copy(p, header[:]), nil
}
func reserveCompatFrame(reserve func(wireBytes, plaintextBytes int64) error, wireBytes, plaintextBytes int64) error {
if wireBytes <= 0 || wireBytes > maxTransportMessageSize {
return fmt.Errorf("invalid transport message length %d", wireBytes)
}
return reserve(wireBytes, plaintextBytes)
}