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) }