internal/mtprotoedge/conn.go
Rama main · réplica de lectura del repositorio original.
package mtprotoedge
import (
"bufio"
"context"
"encoding/hex"
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/crypto"
"github.com/iamxvbaba/td/proto"
"github.com/iamxvbaba/td/tlprofile"
"github.com/iamxvbaba/td/transport"
)
// Conn is a client connection for an identified session, holding all the
// context needed to send it encrypted messages. It is managed by SessionManager
// and shared by request responses and proactive pushes.
//
// Send is concurrency-safe: all outbound messages first enter the per-Conn
// outbound actor, which serially assigns msg_id/seq_no, encrypts and writes to
// the transport, avoiding MTProto ordering errors from interleaved concurrent
// RPC responses and pushes.
type outboundWriter interface {
Send(context.Context, *bin.Buffer) error
}
type connLifecycle uint32
const (
// The zero value is deliberately provisional so test/embedded Conn values start
// outside every SessionManager index until they complete activation.
connLifecycleProvisional connLifecycle = iota
connLifecycleClaiming
connLifecycleActive
// retired is terminal and irreversible. A physical connection that lost an
// activation claim must never become visible again, even if its read goroutine
// was already between preflight and publish when a replacement arrived.
connLifecycleRetired
)
var (
// ErrLayerProfileUnsupported means the requested profile has no generated
// exact codec. Profiles are never clamped to the nearest supported layer.
ErrLayerProfileUnsupported = errors.New("unsupported exact layer profile")
// ErrLayerProfileConflict means one admitted request carried contradictory
// profile evidence. A later, well-formed invokeWithLayer is allowed to correct
// the connection profile and therefore does not use this error.
ErrLayerProfileConflict = errors.New("connection layer profile conflict")
// ErrLayerProfileEpochExhausted is a defensive terminal guard. Reaching it
// would require more than four billion effective layer corrections on one
// physical connection, so wrapping the epoch and making stale pushes current
// again is never safe.
ErrLayerProfileEpochExhausted = errors.New("connection layer profile epoch exhausted")
)
type Conn struct {
transport transport.Conn
// transportLease owns exactly one generation of the physical transport.
// It is nil only for directly constructed test/embedded Conns that retain
// the legacy raw-transport close fallback.
transportLease *physicalTransportLease
writer outboundWriter
cipher crypto.Cipher
msgID *proto.MessageIDGen
writeTimeout time.Duration
metrics Metrics
// now shares the Server protocol clock with inbound expiry admission. Tests may
// advance it without sleeping; construction-only Conns fall back to time.Now.
now func() time.Time
authKeyID [8]byte
// authKeyHex is the hex cache of authKeyID: every RPC's structured log
// carries it, so it is computed once at connection setup to avoid repeated
// hex encoding allocations on the hot path.
authKeyHex string
// authKeyExpiresAt=0 means a permanent key; a positive value is the absolute
// protocol expiry time determined at handshake for a temporary/media-temporary
// key; -1 is the legacy-unknown sentinel used only for migration (the edge
// rejects it with -404 before creating the Conn). Immutable after Conn creation.
authKeyExpiresAt int
sessionID int64
salt int64
key crypto.AuthKey
outbound chan *outboundOp
outboundControl chan *outboundOp
// Critical RPC results (session/difference convergence) and large bulk
// responses have independent bounded lanes. The actor remains the sole writer.
outboundCritical chan *outboundOp
outboundBulk chan *outboundOp
outboundStop chan struct{}
outboundDone chan struct{}
outboundClose sync.Once
// outboundEnqueueMu orders producer registration against terminal close. Close
// flips closing under this lock before waiting, so no WaitGroup Add can race Wait.
outboundEnqueueMu sync.Mutex
outboundEnqueueWG sync.WaitGroup
outboundClosing bool
// Queue backing is intentionally small and bounded per Conn; control has a separate queue
// and strict actor priority. Server-created connections share outboundTrackedBudget.
outboundQueueSize int
outboundControlQueueSize int
outboundTrackedBudget *outboundTrackedBudget
outboundBudgetOnce sync.Once
// Encoded MTProto service frames and control vectors use independent headroom: pong,
// new_session_created, bad_msg and msgs_ack must remain admissible when the body budget is
// full. Content-related control frames keep this budget while pending for resend.
outboundControlTrackedBudget *outboundTrackedBudget
outboundControlBudgetOnce sync.Once
outboundCriticalTrackedBudget *outboundTrackedBudget
outboundCriticalBudgetOnce sync.Once
outboundScratchPool *outboundScratchPool
outboundScratchOnce sync.Once
outboundOpPool *outboundOpPool
outboundReplayBodyPool *outboundReplayBodyPool
// outboundState outlives this physical Conn generation. A replacement
// physical connection for the same auth key/session reuses it.
outboundState *outboundState
// lifecycle is the sole monotonic activation/retirement state machine.
// retired never transitions back to claiming/active; one atomic state avoids
// contradictory activation and shutdown observations.
lifecycle atomic.Uint32
transportClose sync.Once
rpcScheduler *inboundRPCScheduler
// rpcDeliveryHooks belongs to the owning Server. Directly constructed test
// Conns leave it nil and use the package test fallback.
rpcDeliveryHooks *rpcDeliveryHookExecutor
rpcCancel context.CancelFunc
rpcClose sync.Once
rpcMu sync.Mutex
rpcWG sync.WaitGroup
// rpcReservationWG tracks the short window from pre-Copy budget to
// commit/abort, so that when Close returns the global/per-connection budget
// has either been returned or handed off to an explicit queued/running task.
rpcReservationWG sync.WaitGroup
rpcTimeout time.Duration
rpcQueue []inboundRPC
rpcQueueSize int
rpcReserved int
rpcRunning int
rpcReady bool
rpcClosed bool
// rpcReplayRestores is a per-physical-connection ordering barrier. An exact
// replayed/rewrapped init request has already executed its business handler,
// but its wrapper/client/readiness state becomes authoritative only after the
// replacement rpc_result is physically written. Queued naked RPCs remain
// admitted and budgeted, but are not scheduler-runnable until every such
// restore finishes or the connection is fenced.
rpcReplayRestores int
// Rewrap aliasing never delays execution. initialized stops collecting
// candidates after the first valid init wrapper on this physical generation.
rpcRewrapInitialized atomic.Bool
// layerRPCAdmissionTraceLogged bounds production INFO diagnostics to the
// first non-unknown exact-admission rejection on this physical generation.
// Repeated malformed requests remain visible at Debug without turning an
// authenticated reconnect/session into an unbounded INFO log source.
layerRPCAdmissionTraceLogged atomic.Bool
// rpcResultAcked is invoked by the sole outbound actor after it resolves an
// acknowledged server frame back to the rpc_result request msg_id.
rpcResultAcked func(*Conn, int64)
// inflightRPCBytes tracks the memory charge of reserved/queued/executing
// inbound RPCs; legacy equals the copied body, exact is the conservative
// inflated value of typed materialization. Together with
// maxInflightRPCBytes it puts a memory budget on the RPC queue (not just a count).
inflightRPCBytes atomic.Int64
// A single connection only keeps a concurrency quota; the actual worker comes
// from the Server's shared pool, avoiding a reserved goroutine per connection.
rpcRootCtx context.Context
rpcMaxInflight int
// remoteAddr is the peer address of the MTProto connection (from
// net.Conn.RemoteAddr), keeping only the host part; when binding a device
// authorization it is written to authorizations.ip so the login IP is visible
// in the admin panel.
remoteAddr string
// sentContentMessages is retained only for standalone construction tests.
// Server connections allocate seq_no from logical-session outboundState.
sentContentMessages int32
// outboundRand is accessed only by the outbound actor: a buffered pre-read of
// the cipher's random source, amortizing the getrandom syscall for each
// frame's padding down to about one per ~1KiB.
outboundRand *bufio.Reader
identityMu sync.RWMutex
businessAuthKeyID [8]byte
businessAuthKeyHex string
businessAuthKeyResolved bool
userID atomic.Int64
userIDResolved atomic.Bool
receivesUpdates atomic.Bool
// membershipsSynced indicates that this connection's channel membership push
// route (byMemberChannel) has been established successfully. Together with
// receivesUpdates it forms "session fully ready": on membership sync failure
// it stays false, letting the flag short-circuit admission and the next RPC
// retry the sync, avoiding a session that is flagged ready but lacks channel
// routes silently missing supergroup pushes.
membershipsSynced atomic.Bool
// updatesActivationToken/At are protected by SessionManager.mu. They make
// readiness activation single-flight per physical connection generation;
// an old delivery callback can only release the exact token it acquired.
updatesActivationToken uint64
updatesActivationAt time.Time
// bootstrapProbeToken/bootstrapProbed are protected by SessionManager.mu.
// A delivered updates baseline performs the durable bootstrap-job probe once
// per physical connection generation. A failed callback releases the token;
// a replacement Conn starts with a fresh zero value and probes again.
bootstrapProbeToken uint64
bootstrapProbed bool
// membershipGen is the revision number of this connection's channel
// membership index: any incremental revision (Add/Remove for join/leave/kick,
// wholesale clearing on identity switch/offline) increments it. A full sync
// samples it before reading the persistent membership list and carries it back
// for comparison when committing, detecting the lost-update race where an
// incremental revision happens inside the read window
// (SetSessionChannelMemberships switches to the merge path and retries while
// still not ready).
membershipGen atomic.Int64
// createdAt is the connection setup time, used to evict the genuinely oldest
// connection when the session count for the same auth_key hits its cap.
createdAt time.Time
// layerProfileState atomically packs profile, provenance and epoch. A profile
// inherited from auth-key metadata is only a default; a later well-formed
// invokeWithLayer may correct it. The epoch fences proactive updates prepared
// before that correction without invalidating request-bound RPC results.
layerProfileMu sync.RWMutex
// layerProfileEvidenceMsgID is protected by layerProfileMu. Zero means the
// selected profile came from inherited/legacy recovery and therefore has no
// ordered client-message cursor yet. Positive values are the newest accepted
// invokeWithLayer message for this exact MTProto session.
layerProfileEvidenceMsgID int64
// layerProfileEvidenceLayer retains the raw negotiated Layer even when this
// binary has no generated codec for it. In that case the packed profile stays
// Unknown, but msg_id ordering can still admit a newer supported correction
// and reject an older/same-id rollback.
layerProfileEvidenceLayer int
layerProfileState atomic.Uint64
// clientLayer is the package-internal mirror used only by legacy state-machine
// regression tests. Production application RPC/result/update encoding uses
// the structured profile state. Zero is unknown and must fail closed if a legacy
// application value reaches an outbound boundary.
clientLayer atomic.Int32
}
func (c *Conn) lifecycleState() connLifecycle {
if c == nil {
return connLifecycleRetired
}
return connLifecycle(c.lifecycle.Load())
}
func (c *Conn) isRetired() bool {
return c == nil || c.lifecycleState() == connLifecycleRetired
}
// retire irreversibly fences the logical connection. The caller that wins the
// transition may additionally own one-shot physical cleanup.
func (c *Conn) retire() bool {
if c == nil {
return false
}
for {
state := c.lifecycle.Load()
if connLifecycle(state) == connLifecycleRetired {
return false
}
if c.lifecycle.CompareAndSwap(state, uint32(connLifecycleRetired)) {
return true
}
}
}
func (c *Conn) beginActivationClaim() bool {
if c == nil || !c.isPhysicalTransportCurrentOpen() {
return false
}
if !c.lifecycle.CompareAndSwap(uint32(connLifecycleProvisional), uint32(connLifecycleClaiming)) {
return false
}
// Physical close can win after the pre-check but before the lifecycle CAS.
// Do not let a doomed claimant enter SessionManager and retire a healthy old
// owner for the same logical session.
if c.lifecycleState() != connLifecycleClaiming || !c.isPhysicalTransportCurrentOpen() {
c.retire()
return false
}
return true
}
func (c *Conn) publishActivation() bool {
if c == nil || !c.isPhysicalTransportCurrentOpen() {
return false
}
if !c.lifecycle.CompareAndSwap(uint32(connLifecycleClaiming), uint32(connLifecycleActive)) {
return false
}
// A concurrent transport failure can retire the Conn between the first
// physical check and the CAS. Never let that intermediate active value escape.
if c.lifecycleState() != connLifecycleActive || !c.isPhysicalTransportCurrentOpen() {
c.retire()
return false
}
return true
}
func (c *Conn) isActive() bool {
return c != nil && c.lifecycleState() == connLifecycleActive && c.isPhysicalTransportCurrentOpen()
}
// transferTransportOwnership hands this Conn's physical socket to the next
// logical generation. The caller must have fenced and drained the old writer.
func (c *Conn) transferTransportOwnership() (*physicalTransportLease, bool) {
if c == nil || c.transportLease == nil {
return nil, false
}
return c.transportLease.Transfer()
}
func (c *Conn) isPhysicalTransportCurrentOpen() bool {
return c != nil && (c.transportLease == nil || c.transportLease.IsCurrentOpen())
}
// LayerProfile returns the exact TL profile currently selected for this
// connection. ok is false until admission or an inherited auth-key default
// supplies a supported generated profile.
func (c *Conn) LayerProfile() (profile tlprofile.Profile, ok bool) {
state := c.LayerProfileState()
return state.Profile, state.Origin != LayerProfileUnknown
}
// FreezeLayerProfile records explicit protocol evidence observed during ordered
// admission. Repeating the same value is idempotent. A later well-formed
// invokeWithLayer may replace either an inherited default or older explicit
// evidence; already-admitted requests retain their own immutable profile.
func (c *Conn) FreezeLayerProfile(profile tlprofile.Profile) error {
_, err := c.setLayerProfile(profile, LayerProfileExplicit, true)
return err
}
// FreezeLayerProfileAt applies explicit protocol evidence in client msg_id
// order. A duplicate older than the last accepted evidence is inert; the same
// msg_id carrying another Layer is a protocol conflict. Advancing the evidence
// cursor at an unchanged Layer does not rotate the outbound epoch because the
// wire profile itself did not change.
func (c *Conn) FreezeLayerProfileAt(profile tlprofile.Profile, msgID int64) (bool, error) {
return c.freezeLayerProfileAt(profile, msgID)
}
// SeedLayerProfile restores explicit evidence previously proven for this exact
// logical session. It is kept as the compatible same-session restore API;
// auth-key-wide metadata must use SeedInheritedLayerProfile instead.
func (c *Conn) SeedLayerProfile(profile tlprofile.Profile) error {
_, err := c.setLayerProfile(profile, LayerProfileExplicit, true)
return err
}
// SeedInheritedLayerProfile installs an auth-key-wide default only while the
// connection is still unknown. It never overwrites explicit evidence or an
// already selected inherited default; client protocol evidence owns correction.
func (c *Conn) SeedInheritedLayerProfile(profile tlprofile.Profile) error {
_, err := c.setLayerProfile(profile, LayerProfileInherited, false)
return err
}
// legacyClientLayer returns the test-only canonical-transcoder profile. Zero
// deliberately remains unknown; it must never be converted into an implicit
// canonical application profile.
func (c *Conn) legacyClientLayer() int { return int(c.clientLayer.Load()) }
// setLegacyClientLayer records the package-internal legacy mirror.
func (c *Conn) setLegacyClientLayer(layer int) { c.clientLayer.Store(int32(layer)) }
// AuthKeyID returns the connection's auth_key_id.
func (c *Conn) AuthKeyID() [8]byte { return c.authKeyID }
// AuthKeyExpiresAt returns the raw protocol key's expiry time; 0 means a permanent key.
func (c *Conn) AuthKeyExpiresAt() int { return c.authKeyExpiresAt }
func (c *Conn) authKeyProtocolUnavailableNow() bool {
if c == nil {
return true
}
now := time.Now()
if c.now != nil {
now = c.now()
}
return authKeyProtocolUnavailable(c.authKeyExpiresAt, now)
}
// BusinessAuthKeyID returns the auth_key_id from the business view.
//
// After a temp auth_key is bound it resolves to the perm auth_key; the second
// return value indicates whether this connection has completed resolution, and
// returns true even when the result equals the original auth_key_id, to avoid
// querying the binding table on every RPC.
func (c *Conn) BusinessAuthKeyID() ([8]byte, bool) {
c.identityMu.RLock()
defer c.identityMu.RUnlock()
return c.businessAuthKeyID, c.businessAuthKeyResolved
}
// BusinessAuthKeyHex returns the hex cache of the business-view auth_key_id
// (used in every RPC log, avoiding repeated encoding).
func (c *Conn) BusinessAuthKeyHex() (string, bool) {
c.identityMu.RLock()
defer c.identityMu.RUnlock()
return c.businessAuthKeyHex, c.businessAuthKeyResolved
}
// SetBusinessAuthKeyID caches the business-view auth_key_id.
func (c *Conn) SetBusinessAuthKeyID(id [8]byte) {
c.identityMu.Lock()
changed := !c.businessAuthKeyResolved || c.businessAuthKeyID != id
if changed || c.businessAuthKeyHex == "" {
c.businessAuthKeyHex = hex.EncodeToString(id[:])
}
c.businessAuthKeyID = id
c.businessAuthKeyResolved = true
c.identityMu.Unlock()
if changed {
c.userID.Store(0)
c.userIDResolved.Store(false)
}
}
// SessionID returns the connection's session_id.
func (c *Conn) SessionID() int64 { return c.sessionID }
// UserID returns the bound user id; 0 when not logged in.
func (c *Conn) UserID() int64 { return c.userID.Load() }
// UserIDResolved reports whether the user_id authorization state has been
// resolved for the current connection.
//
// resolved=true with userID=0 means this auth_key is not currently logged in;
// this way the many RPCs before login do not repeatedly query the authorization
// table, and a later successful login is overwritten with the real user by
// BindUser.
func (c *Conn) UserIDResolved() (userID int64, resolved bool) {
return c.userID.Load(), c.userIDResolved.Load()
}
// ReceivesUpdates reports whether this connection receives proactively pushed updates.
func (c *Conn) ReceivesUpdates() bool { return c.receivesUpdates.Load() }
// SetReceivesUpdates sets whether this connection receives proactively pushed updates.
// After login the main connection is set to true once updates.getState/getDifference
// have established the sync baseline.
func (c *Conn) SetReceivesUpdates(v bool) { c.receivesUpdates.Store(v) }
// setRemoteAddrStr records the MTProto connection's peer address (remote is
// shaped like host:port). It keeps only the host part (dropping the port), used
// to write authorizations.ip when binding a device authorization.
func (c *Conn) setRemoteAddrStr(remote string) {
if remote == "" {
return
}
host, _, err := net.SplitHostPort(remote)
if err != nil {
// Already a bare host (e.g. a unix socket or an IPv6 form with no port).
c.remoteAddr = remote
return
}
c.remoteAddr = host
}
// clientIP returns the connection's peer IP (host part), empty when unset.
func (c *Conn) clientIP() string { return c.remoteAddr }