fix: sync temp auth key expiry boundaries

This commit is contained in:
A 2026-07-13 23:04:15 +08:00
parent 305e8a0008
commit 20a310f6ca
50 changed files with 3626 additions and 335 deletions

View file

@ -567,6 +567,18 @@ func (c *Conn) failTransport() {
c.closeTransport()
}
// fenceUnavailableAuthKey turns protocol expiry into a connection-level terminal
// boundary. Outbound producers may discover expiry before the read loop sees the
// client's next frame; in that case the socket is closed so the client reconnects
// and receives the ordinary -404 admission response for the stale raw key.
func (c *Conn) fenceUnavailableAuthKey() {
if c == nil || !c.authKeyProtocolUnavailableNow() {
return
}
c.beginTerminalShutdown()
c.closeTransport()
}
// fenceUndeliveredRPCResult is the no-reentry terminal path used from a task's
// release callback. That callback may itself run while rpcClose.Do is draining
// queued tasks, so calling beginCloseInboundRPCScheduler again would deadlock on
@ -1022,6 +1034,13 @@ func (c *Conn) outboundQueue(op outboundOp) chan outboundOp {
}
func (c *Conn) beginOutboundEnqueue() bool {
// A temporary key is unusable for both inbound RPCs and server-originated
// updates at the same absolute boundary. Reject before encoding admission;
// the actor and write path repeat this check to close the two race windows.
if c.authKeyProtocolUnavailableNow() {
c.fenceUnavailableAuthKey()
return false
}
c.outboundEnqueueMu.Lock()
defer c.outboundEnqueueMu.Unlock()
if c.outboundClosing || c.isRetired() {
@ -1146,6 +1165,14 @@ func (c *Conn) drainOutbound() {
}
func (c *Conn) handleOutboundOp(state *outboundState, op outboundOp) {
// An operation can sit in a bounded queue across the protocol expiry instant.
// It must be failed and released without touching the wire.
if c.authKeyProtocolUnavailableNow() {
op.releaseReservation(state.budget)
op.finish(outboundResult{err: ErrConnClosed})
c.fenceUnavailableAuthKey()
return
}
if op.kind != outboundSend {
defer op.releaseReservation(state.budget)
}
@ -1570,7 +1597,20 @@ type deadlineOutboundScratchWriter interface {
SendDeadlineWithScratch(deadline time.Time, b *bin.Buffer, scratch *[]byte) error
}
type deadlineOutboundGuardedScratchWriter interface {
SendDeadlineWithScratchGuarded(deadline time.Time, b *bin.Buffer, scratch *[]byte, guard func() error) error
}
var (
errAuthKeyUnavailableAtPhysicalWrite = errors.New("auth key unavailable at physical write admission")
errConnRetiredAtPhysicalWrite = errors.New("connection retired at physical write admission")
)
func (c *Conn) writeFrame(ctx context.Context, frame *outboundFrame) error {
if c.authKeyProtocolUnavailableNow() {
c.fenceUnavailableAuthKey()
return ErrConnClosed
}
if ctx == nil {
ctx = context.Background()
}
@ -1595,12 +1635,28 @@ func (c *Conn) writeFrame(ctx context.Context, frame *outboundFrame) error {
if err := prewriteDeadlineError(ctx, deadline); err != nil {
return fmt.Errorf("outbound deadline before write: %w", err)
}
// Scratch admission and encryption may straddle expires_at. Recheck at the
// final pre-write barrier so neither fresh sends nor resends use a stale key.
if c.authKeyProtocolUnavailableNow() {
c.fenceUnavailableAuthKey()
return ErrConnClosed
}
writer := c.writer
if writer == nil {
writer = c.transport
}
if sw, ok := writer.(deadlineOutboundScratchWriter); ok {
if guarded, ok := writer.(deadlineOutboundGuardedScratchWriter); ok {
err = guarded.SendDeadlineWithScratchGuarded(deadline, out, &scratch.codec, func() error {
if c.isRetired() {
return errConnRetiredAtPhysicalWrite
}
if c.authKeyProtocolUnavailableNow() {
return errAuthKeyUnavailableAtPhysicalWrite
}
return nil
})
} else if sw, ok := writer.(deadlineOutboundScratchWriter); ok {
err = sw.SendDeadlineWithScratch(deadline, out, &scratch.codec)
} else if dw, ok := writer.(deadlineOutboundWriter); ok {
err = dw.SendDeadline(deadline, out)
@ -1614,6 +1670,18 @@ func (c *Conn) writeFrame(ctx context.Context, frame *outboundFrame) error {
err = writer.Send(sendCtx, out)
cancel()
}
if errors.Is(err, errAuthKeyUnavailableAtPhysicalWrite) {
// The guarded lease has already released physical write ownership and no
// raw bytes were emitted. Fence outside writeMu so Close cannot deadlock.
c.fenceUnavailableAuthKey()
return ErrConnClosed
}
if errors.Is(err, errConnRetiredAtPhysicalWrite) {
// Terminal shutdown already owns lifecycle/transport close. Do not call
// failTransport here: sendTerminalProtoError is waiting for this actor to
// drain and must retain the lease long enough to write the final bare -404.
return ErrConnClosed
}
if err != nil {
// 任一 partial write / timeout 都可能破坏 MTProto 帧边界;该 socket
// 不可继续复用。这里只发 terminal 信号,不在 actor 内等待自身退出。