package quic

import (
	
	
	
	
	
	
	
	

	
	
	
	
)

// This file is the X1 Stage 5f send-side multipath orchestration
// (draft-ietf-quic-multipath): opening a second PathID, validating it with a
// PATH_CHALLENGE/PATH_RESPONSE exchange, and scheduling 1-RTT sends over it.
//
// It is deliberately separate from path_manager_outgoing.go, which is the RFC
// 9000 single-path connection-MIGRATION manager (the int64 pathID concept that
// switches the active 4-tuple and discards the old one). Multipath keeps BOTH
// paths alive: PathIDZero never stops carrying data, and a non-zero path is an
// additional, independent number space + congestion controller (5a) addressed
// by its own connection IDs (5c). Nothing here touches pathManagerOutgoing.
//
// Threading: every field of multipathOutgoing is owned by the connection's run
// goroutine. The sentPacketHandler / receivedPacketHandler / connIDGenerator
// have no locks and are read by the packer on every 1-RTT packet, so opening a
// path from an application goroutine would race them (confirmed with -race).
// Conn.OpenPath therefore only enqueues an openPathRequest; the run loop
// performs the actual provisioning in processOpenPathRequests.

// ErrPathLimit is returned by OpenPath when the peer has not yet advertised a
// large enough MAX_PATH_ID, or when the requested path would exceed the local
// limit. The condition can be transient immediately after handshake completion,
// while the peer's MAX_PATH_ID frame is still in flight.
var ErrPathLimit = errors.New("quic: path limit prevents opening path")

// pathOpenState tracks the local lifecycle of one non-zero multipath PathID.
// It mirrors the recovery-irrelevant subset of reference/paths.rs that the
// initiator drives: sending PATH_CHALLENGEs (on_path_challenges_unconfirmed,
// paths.rs:185) until a PATH_RESPONSE validates the path (validated,
// paths.rs:200), then reporting it (open_status, paths.rs:273).
type pathOpenState struct {
	id protocol.PathID

	// challenges holds the PATH_CHALLENGE tokens we have sent on this path and
	// not yet seen validated. A PATH_RESPONSE carrying any of them validates the
	// path (paths.rs:497-527: a response to any sent challenge validates).
	challenges [][8]byte

	// challengeSent is set once we have emitted at least one PATH_CHALLENGE, so
	// driveMultipath does not re-send on every run-loop iteration.
	challengeSent bool

	// validated is set when a matching PATH_RESPONSE has been received. Until
	// then the path carries only its PATH_CHALLENGE; application data waits for
	// validation (RFC 9000 ยง8: do not send non-probing data on an unvalidated
	// path). It corresponds to PathData::validated (paths.rs:200).
	validated bool

	// validatedChan is closed when validated flips to true, so Conn.OpenPath
	// (running on the application goroutine) can block until the run loop
	// reports the path usable.
	validatedChan chan struct{}

	// pendingResponses holds PATH_RESPONSE tokens we owe the peer for
	// PATH_CHALLENGEs it sent on this path. driveMultipath flushes them.
	pendingResponses [][8]byte

	// sendData is the per-path application send queue: DATAGRAM payloads the
	// application asked to send specifically over this path
	// (MultipathPath.SendDatagram). Keeping it per-path (rather than reusing the
	// connection datagram queue) is what makes "this datagram rode path N"
	// deterministic, and leaves the path-0 send loop byte-identical. It is
	// drained only in the run goroutine.
	sendData [][]byte

	// qntRoute is the validated remote address for a QNT-opened path. Packets
	// for this path are sent to this address instead of the connection's
	// original remote address.
	qntRoute netip.AddrPort
	// qntUDPAddr is the allocation-bearing net representation of qntRoute. The
	// route is immutable after path creation, so convert it once rather than on
	// every pass through the send loop.
	qntUDPAddr *net.UDPAddr

	// cidBlockedSent is set after we ask the peer for a path connection ID.
	cidBlockedSent bool
}

// openPathRequest is the command Conn.OpenPath hands to the run goroutine. The
// run loop fills in result/err and closes done.
type openPathRequest struct {
	pid  protocol.PathID
	done chan struct{}
	err  error
	path *MultipathPath
}

// multipathOutgoing is the run-goroutine-owned send-side multipath state.
type multipathOutgoing struct {
	paths map[protocol.PathID]*pathOpenState
	// nextPathID is the PathID to assign to the next opened path. PathIDZero is
	// the always-present initial path, so non-zero paths start at 1.
	nextPathID protocol.PathID
	// migratedRemote is the direct route the ordinary (path-0) send conn has been
	// migrated onto after a QNT route was validated+selected. Zero until the first
	// migration; guards processQNTValidatedPathOpen against re-migrating every
	// round. Ordinary stream frames egress here, not the relay-mapped remote.
	migratedRemote netip.AddrPort
	// premigrationRemote is the ordinary send remote captured before the first
	// QNT migration. For the relay-first path this is the relay-mapped address;
	// for direct-first dials Endpoint may seed a relay fallback explicitly.
	premigrationRemote net.Addr
	// revertedRoute / revertedRouteUntil impose a cooldown on re-migrating to
	// a route that was recently reverted. A fresh QNT validation is evidence a
	// route came back (the challenge round-tripped on the exact 4-tuple), but
	// validated candidates from the same round can arrive seconds apart, and
	// re-migrating into a link that is still flapping would churn the
	// connection through repeated congestion resets. After the cooldown a
	// freshly validated route is trusted again, so a transient flap does not
	// forfeit the direct path for the connection's lifetime.
	revertedRoute      netip.AddrPort
	revertedRouteUntil monotime.Time
}

// qntRemigrationCooldown is how long after a revert a reverted route is
// refused re-migration. QNT re-validates routes on the holepunch upgrade
// cadence, so one cooldown window typically spans a full validation round.
const qntRemigrationCooldown = 30 * time.Second

func newMultipathOutgoing() *multipathOutgoing {
	return &multipathOutgoing{
		paths:      make(map[protocol.PathID]*pathOpenState),
		nextPathID: 1,
	}
}

// queuePathResponse records a PATH_RESPONSE owed on path pid. The path may not
// be provisioned yet if this is racing the lazy join; the response is dropped
// in that case, and the peer will re-send its PATH_CHALLENGE.
func ( *multipathOutgoing) ( protocol.PathID,  [8]byte) {
	,  := .paths[]
	if ! {
		return
	}
	.pendingResponses = append(.pendingResponses, )
}

// MultipathPath is the application handle to a non-zero multipath PathID. It is
// returned by Conn.OpenPath. Validated blocks until the path completes its
// PATH_CHALLENGE/PATH_RESPONSE validation, at which point 1-RTT packets
// (including application data) are scheduled over it by the run loop.
type MultipathPath struct {
	conn      *Conn
	id        protocol.PathID
	validated chan struct{}
}

// PathID returns the draft-multipath PathID of this path.
func ( *MultipathPath) () protocol.PathID { return .id }

// Validated blocks until the path has been validated (a PATH_RESPONSE to our
// PATH_CHALLENGE arrived) or ctx is done / the connection closed.
func ( *MultipathPath) ( context.Context) error {
	select {
	case <-.validated:
		return nil
	case <-.conn.ctx.Done():
		return context.Cause(.conn.ctx)
	case <-.Done():
		return context.Cause()
	}
}

// OpenPath opens a second QUIC multipath path (draft-ietf-quic-multipath) over
// the connection's existing socket, distinguished from PathIDZero by its own
// connection IDs rather than its 4-tuple. It is the draft-multipath path-open,
// NOT the RFC 9000 single-path migration AddPath: both paths stay alive.
//
// OpenPath requires multipath to have been negotiated and the handshake to be
// confirmed (PATH_NEW_CONNECTION_ID / PATH_CHALLENGE are 1-RTT-only). It hands
// the request to the run goroutine, which provisions the per-path send/recv
// state, issues a path connection ID, and begins the PATH_CHALLENGE validation.
// The returned MultipathPath.Validated blocks until the path is usable.
//
// tr is accepted for API parity with AddPath and future multi-socket paths; in
// this build the second path shares the connection's socket (the peer demuxes
// by connection ID), so tr may be nil.
func ( *Conn) ( *Transport) (*MultipathPath, error) {
	if !.multipathNegotiated() {
		return nil, errors.New("quic: multipath not negotiated")
	}
	 := &openPathRequest{done: make(chan struct{})}
	select {
	case .openPathQueue <- :
	case <-.ctx.Done():
		return nil, .ctx.Err()
	}
	.scheduleSending()
	select {
	case <-.done:
		return .path, .err
	case <-.ctx.Done():
		return nil, .ctx.Err()
	}
}

// PathAcksReceived returns the number of PATH_ACK / PATH_ACK_ECN frames this
// connection has received for its non-zero multipath paths. It is safe to call
// from any goroutine and is used to confirm a second path's packets were
// acknowledged (driving that path's bytes-in-flight down).
func ( *Conn) () uint64 { return .pathAcksReceived.Load() }

// LastPathAckID returns the PathID carried by the most recently received
// PATH_ACK / PATH_ACK_ECN frame and whether any such frame has been received.
// It lets a test confirm an acknowledgement arrived for a specific non-zero
// path. It is safe to call from any goroutine.
func ( *Conn) () (protocol.PathID, bool) {
	 := .lastPathAckID.Load()
	if  == 0 {
		return protocol.PathIDZero, false
	}
	return protocol.PathID( - 1), true
}

// pathStatsRequest is the command Conn.PathStats hands to the run goroutine,
// which fills in stats/ok and closes done.
type pathStatsRequest struct {
	pid   protocol.PathID
	stats ackhandler.PathDebugStats
	ok    bool
	done  chan struct{}
}

// PathStats returns the live application-data recovery snapshot for the
// multipath PathID pid (its own number space + congestion controller). It is a
// test-support hook: the query runs on the connection's run goroutine โ€” the
// only goroutine permitted to read the lock-free sentPacketHandler โ€” so it is
// race-free. ok is false if pid is not an open path or the connection closed.
func ( *Conn) ( protocol.PathID) (ackhandler.PathDebugStats, bool) {
	 := &pathStatsRequest{pid: , done: make(chan struct{})}
	select {
	case .pathStatsQueue <- :
	case <-.ctx.Done():
		return ackhandler.PathDebugStats{}, false
	}
	.scheduleSending()
	select {
	case <-.done:
		return .stats, .ok
	case <-.ctx.Done():
		return ackhandler.PathDebugStats{}, false
	}
}

// PathInfo is a snapshot of one qng multipath path's application-facing state.
//
// Path 0 is the initial path and is not listed here. The entries returned by
// [Conn.Paths] are real qng non-zero paths provisioned by OpenPath or by a peer
// packet that caused lazy path join. RemoteAddr is set only when qng has an
// address that actually routes the path, currently for QNT-opened paths.
type PathInfo struct {
	// ID is the QUIC multipath PathID.
	ID protocol.PathID
	// Validated reports whether the path completed PATH_CHALLENGE /
	// PATH_RESPONSE validation and can carry non-probing application data.
	Validated bool
	// RemoteAddr is the remote UDP route for this path, when known.
	RemoteAddr netip.AddrPort
	// SmoothedRTT is the path's application-data RTT estimate, when HasRTT is
	// true.
	SmoothedRTT time.Duration
	// HasRTT reports whether SmoothedRTT was observed for this path.
	HasRTT bool
	// BytesInFlight is the path's current application-data bytes in flight,
	// when HasBytesInFlight is true.
	BytesInFlight protocol.ByteCount
	// HasBytesInFlight reports whether BytesInFlight was observed for this path.
	HasBytesInFlight bool
	// BytesSent is the cumulative 1-RTT/0-RTT application-data packet
	// bytes sent on this path, when HasBytesSent is true. It excludes
	// Initial/Handshake packets and UDP framing overhead.
	BytesSent uint64
	// HasBytesSent reports whether BytesSent was observed for this path.
	HasBytesSent bool
	// BytesReceived is the cumulative 1-RTT/0-RTT application-data packet
	// bytes received on this path, when HasBytesReceived is true. It excludes
	// Initial/Handshake packets and UDP framing overhead.
	BytesReceived uint64
	// HasBytesReceived reports whether BytesReceived was observed for this path.
	HasBytesReceived bool
	// CongestionWindow is the path's current congestion window, when
	// HasCongestionWindow is true.
	CongestionWindow protocol.ByteCount
	// HasCongestionWindow reports whether CongestionWindow was observed for this
	// path.
	HasCongestionWindow bool
	// LostPackets is the number of application-data packets declared lost on
	// this path, when HasLoss is true.
	LostPackets uint64
	// LostBytes is the number of application-data bytes declared lost on this
	// path, when HasLoss is true.
	LostBytes uint64
	// HasLoss reports whether LostPackets and LostBytes were observed for this
	// path.
	HasLoss bool
}

// pathSnapshotRequest is the command Conn.Paths hands to the run goroutine,
// which fills in paths and closes done.
type pathSnapshotRequest struct {
	performance performanceSnapshotRequest
	paths       []PathInfo
	done        chan struct{}
}

// Paths returns a snapshot of this connection's non-zero qng multipath paths.
// The query runs on the connection's run goroutine because the path-open state
// is owned there. It returns nil if no non-zero path has been opened or the
// connection is closed.
func ( *Conn) () []PathInfo {
	 := &pathSnapshotRequest{done: make(chan struct{})}
	select {
	case .pathSnapshotQueue <- :
	case <-.ctx.Done():
		return nil
	}
	.scheduleSending()
	select {
	case <-.done:
		return .paths
	case <-.ctx.Done():
		return nil
	}
}

// SetMigrationFallbackRemote records a fallback remote for QNT active
// migration. It is used when the connection was established directly but the
// caller also knows a relay route for the peer.
func ( *Conn) ( net.Addr) {
	if  == nil {
		return
	}
	 := &setMigrationFallbackRequest{addr: , done: make(chan struct{})}
	select {
	case .setMigrationFallbackQueue <- :
	case <-.ctx.Done():
		return
	}
	.scheduleSending()
	select {
	case <-.done:
	case <-.ctx.Done():
	}
}

type setMigrationFallbackRequest struct {
	addr net.Addr
	done chan struct{}
}

func ( *Conn) () {
	for {
		select {
		case  := <-.setMigrationFallbackQueue:
			if .multipathOut == nil {
				.multipathOut = newMultipathOutgoing()
			}
			if .multipathOut.premigrationRemote == nil ||
				(.conn != nil && addrsEqual(.multipathOut.premigrationRemote, .conn.RemoteAddr())) {
				.multipathOut.premigrationRemote = .addr
			}
			close(.done)
		default:
			return
		}
	}
}

// processPathSnapshotRequests answers pending Paths queries. Run goroutine
// only (called from the run loop), so reading multipathOut is safe.
func ( *Conn) () {
	for {
		select {
		case  := <-.pathSnapshotQueue:
			.performance.fill(&.performance)
			if .multipathOut != nil && len(.multipathOut.paths) > 0 {
				.paths = make([]PathInfo, 0, len(.multipathOut.paths))
				for ,  := range .multipathOut.paths {
					 := PathInfo{
						ID:         ,
						Validated:  .validated,
						RemoteAddr: .qntRoute,
					}
					if ,  := .sentPacketHandler.PathDebugStats();  {
						if .HasRTT {
							.SmoothedRTT = .SmoothedRTT
							.HasRTT = true
						}
						.BytesInFlight = .BytesInFlight
						.HasBytesInFlight = true
						.BytesSent = .BytesSent
						.HasBytesSent = true
						.BytesReceived = .BytesReceived
						.HasBytesReceived = true
						.CongestionWindow = .CongestionWindow
						.HasCongestionWindow = true
						.LostPackets = .LostPackets
						.LostBytes = .LostBytes
						.HasLoss = true
					}
					.paths = append(.paths, )
				}
				sort.Slice(.paths, func(,  int) bool {
					return .paths[].ID < .paths[].ID
				})
			}
			close(.done)
		default:
			return
		}
	}
}

// processPathStatsRequests answers pending PathStats queries. Run goroutine
// only (called from the run loop), so reading the sentPacketHandler is safe.
func ( *Conn) () {
	for {
		select {
		case  := <-.pathStatsQueue:
			.stats, .ok = .sentPacketHandler.PathDebugStats(.pid)
			close(.done)
		default:
			return
		}
	}
}

// processOpenPathRequests drains pending OpenPath requests. It runs in the run
// goroutine, so it can safely provision the (unlocked) sentPacketHandler /
// receivedPacketHandler / connIDGenerator state for the new path.
func ( *Conn) () error {
	for {
		select {
		case  := <-.openPathQueue:
			.path, .err = .openPathLocked()
			close(.done)
		default:
			return nil
		}
	}
}

// processQNTValidatedPathOpen consumes at most one validated QNT candidate and
// provisions a route-bearing multipath path for it. Run goroutine only.
//
// A QNT route provisioned here carries per-path DATAGRAM sends, but ordinary
// QUIC stream frames egress through the connection's path-0 send conn, which
// still targets the relay-mapped remote set at establishment. On the client,
// once a direct route is validated+selected we also migrate the ordinary send
// conn onto it (RFC 9000 ยง9 connection migration): change the send remote to
// the direct 4-tuple and reset MTU for the new path. This makes stream payload
// follow the selected direct path instead of staying on relay. The server then
// observes app data on the direct path and completes its existing passive
// migration.
func ( *Conn) ( monotime.Time) error {
	, , ,  := .qntOpenValidatedPathLocked()
	if errors.Is(, ErrPathLimit) {
		return nil
	}
	if  != nil {
		return 
	}
	if  && .perspective == protocol.PerspectiveClient {
		.migrateOrdinarySendToQNTRoute(, )
		.scheduleSending()
	}
	return nil
}

// migrateOrdinarySendToQNTRoute points the connection's path-0 send conn at a
// validated direct route so ordinary stream frames egress there. It mirrors the
// server-side passive-migration block (handlePathChallenge) and runs at most
// once per route, in the run goroutine.
func ( *Conn) ( netip.AddrPort,  monotime.Time) {
	// Migrating the ordinary send conn touches the send-side recovery state and
	// the live send conn, which only exist once the handshake is confirmed. The
	// same gate guards driveMultipath. A QNT route only validates well after the
	// handshake in practice, so this never skips a real migration.
	if !.handshakeConfirmed || .conn == nil {
		return
	}
	if !.IsValid() || .Port() == 0 {
		return
	}
	if  := .multipathOut;  != nil {
		if .migratedRemote ==  ||
			(.revertedRoute ==  && .Before(.revertedRouteUntil)) {
			return
		}
	}
	 := protocol.ByteCount(.config.InitialPacketSize)
	 := protocol.ByteCount(protocol.MaxPacketBufferSize)
	if  := .peerParams.Load(); .MaxUDPPayloadSize > 0 && .MaxUDPPayloadSize <  {
		 = .MaxUDPPayloadSize
	}
	.sentPacketHandler.MigratedPath(, )
	.currentMTUEstimate.Store(uint32(estimateMaxPayloadSize()))
	.mtuDiscoverer.Reset(, , )
	if .multipathOut != nil && .multipathOut.premigrationRemote == nil {
		.multipathOut.premigrationRemote = .conn.RemoteAddr()
	}
	.conn.ChangeRemoteAddr(net.UDPAddrFromAddrPort(), packetInfo{})
	// Send one ordinary non-probing frame on the migrated direct remote before
	// application streams depend on it. The peer's existing RFC 9000 passive
	// migration path switches its return address only after observing non-probing
	// traffic on the new 4-tuple.
	.framer.QueueControlFrame(&wire.PingFrame{})
	if .multipathOut != nil {
		.multipathOut.migratedRemote = 
	}
}

func ( *Conn) ( monotime.Time) {
	const  = 3
	 := .multipathOut
	if  == nil || !.migratedRemote.IsValid() || .premigrationRemote == nil {
		return
	}
	if .sentPacketHandler.PTOCount() <  {
		return
	}
	.revertQNTMigration()
}

func ( *Conn) ( monotime.Time) {
	 := .qntMigrationFallbackDeadline()
	if .IsZero() || .Before() {
		return
	}
	.revertQNTMigration()
}

func ( *Conn) () monotime.Time {
	 := .multipathOut
	if  == nil || !.migratedRemote.IsValid() || .premigrationRemote == nil {
		return 0
	}
	 := max(5*time.Second, .rttStats.PTO(true)*3)
	return .lastPacketReceivedTime.Add()
}

func ( *Conn) ( monotime.Time) {
	 := .multipathOut
	if  == nil || !.migratedRemote.IsValid() || .premigrationRemote == nil {
		return
	}
	 := protocol.ByteCount(.config.InitialPacketSize)
	 := protocol.ByteCount(protocol.MaxPacketBufferSize)
	if  := .peerParams.Load(); .MaxUDPPayloadSize > 0 && .MaxUDPPayloadSize <  {
		 = .MaxUDPPayloadSize
	}
	.sentPacketHandler.MigratedPath(, )
	.currentMTUEstimate.Store(uint32(estimateMaxPayloadSize()))
	.mtuDiscoverer.Reset(, , )
	.conn.ChangeRemoteAddr(.premigrationRemote, packetInfo{})
	.revertedRoute = .migratedRemote
	.revertedRouteUntil = .Add(qntRemigrationCooldown)
	.migratedRemote = netip.AddrPort{}
	.scheduleSending()
}

// openPathLocked provisions a new non-zero path. Invariant: run goroutine only.
func ( *Conn) () (*MultipathPath, error) {
	if .multipathOut == nil {
		.multipathOut = newMultipathOutgoing()
	}
	 := .multipathOut.nextPathID
	if !.canOpenPath() {
		return nil, fmt.Errorf("%w: path %d peer max path id not raised that far", ErrPathLimit, )
	}
	.multipathOut.nextPathID++

	if  := .allocatePathLocked();  != nil {
		return nil, 
	}

	 := &pathOpenState{id: , validatedChan: make(chan struct{})}
	.multipathOut.paths[] = 
	.scheduleSending() // wake the loop to send the first PATH_CHALLENGE
	return &MultipathPath{conn: , id: , validated: .validatedChan}, nil
}

func ( *Conn) () (protocol.PathID, netip.AddrPort, bool, error) {
	if ,  := .qntPeekValidatedProbe(); ! {
		return protocol.PathIDZero, netip.AddrPort{}, false, nil
	}
	if .multipathOut == nil {
		.multipathOut = newMultipathOutgoing()
	}
	 := .multipathOut.nextPathID
	if !.canOpenPath() {
		return protocol.PathIDZero, netip.AddrPort{}, false, fmt.Errorf("%w: path %d peer max path id not raised that far", ErrPathLimit, )
	}
	if  := .allocatePathLocked();  != nil {
		return protocol.PathIDZero, netip.AddrPort{}, false, 
	}
	,  := .qntPopValidatedProbe()
	if ! {
		return protocol.PathIDZero, netip.AddrPort{}, false, nil
	}
	.multipathOut.nextPathID++
	 := &pathOpenState{
		id:            ,
		validated:     true,
		validatedChan: make(chan struct{}),
		qntRoute:      ,
		qntUDPAddr:    qntProbeUDPAddr(),
	}
	close(.validatedChan)
	.multipathOut.paths[] = 
	return , , true, nil
}

func ( *Conn) ( protocol.PathID) error {
	// Per-path send + receive recovery state (5a / 5e). Each gets its own
	// independent packet-number space.
	if  := .sentPacketHandler.AddPath();  != nil {
		return fmt.Errorf("quic: add send path %d: %w", , )
	}
	if  := .receivedPacketHandler.AddPath(, .logger);  != nil {
		.sentPacketHandler.RemovePath()
		return fmt.Errorf("quic: add recv path %d: %w", , )
	}
	// Issue one of our connection IDs for this path (5c), so the peer can address
	// packets to it. This queues a PATH_NEW_CONNECTION_ID (0x3e78) on path 0.
	if ,  := .issuePathConnID();  != nil {
		.receivedPacketHandler.RemovePath()
		.sentPacketHandler.RemovePath()
		return fmt.Errorf("quic: issue path %d connection id: %w", , )
	}
	return nil
}

// driveMultipath performs the per-path send work for every open non-zero path.
// It runs in the run goroutine after the ordinary path-0 send. For each path it
// either (a) sends a PATH_CHALLENGE if the path is still unvalidated and we have
// its DCID, or (b) packs a 1-RTT packet (PATH_ACK + any pending data) once the
// path is validated. It returns after sending at most one datagram per path so
// the run loop keeps cycling through receives.
func ( *Conn) ( monotime.Time) error {
	if .multipathOut == nil || !.handshakeConfirmed {
		return nil
	}
	for ,  := range .multipathOut.paths {
		,  := .destConnIDForPath()
		if ! {
			// The peer has not issued a PATH_NEW_CONNECTION_ID for this path yet;
			// ask once and try again after it responds.
			if !.cidBlockedSent {
				.queueControlFrame(&wire.PathCIDsBlockedFrame{PathID: , NextSeq: 0})
				.cidBlockedSent = true
			}
			continue
		}
		// Flush any PATH_RESPONSEs we owe on this path first, so the peer can
		// validate it promptly.
		if len(.pendingResponses) > 0 {
			 := make([]ackhandler.Frame, 0, len(.pendingResponses))
			for ,  := range .pendingResponses {
				 = append(, ackhandler.Frame{Frame: &wire.PathResponseFrame{Data: }})
			}
			.pendingResponses = nil
			if  := .sendPathPacket(, , , , );  != nil {
				return 
			}
		}
		if !.validated {
			if  := .sendPathChallenge(, , , );  != nil {
				return 
			}
			continue
		}
		if  := .sendOnPath(, , );  != nil {
			return 
		}
	}
	return nil
}

// pathDatagram is a DATAGRAM payload destined for a specific multipath PathID,
// carried from MultipathPath.SendDatagram into the run goroutine.
type pathDatagram struct {
	pid  protocol.PathID
	data []byte
}

// SendDatagram queues a DATAGRAM to be sent specifically over this multipath
// path. The datagram is delivered to the peer's ordinary datagram receive queue
// (DATAGRAM frames are not path-scoped on the wire), but it is guaranteed to
// ride a 1-RTT packet addressed to this path's connection ID and drawn from this
// path's packet-number space โ€” which is what makes it observable as "data over
// PathID n".
func ( *MultipathPath) ( []byte) error {
	return .conn.SendDatagramOnPath(.id, )
}

// SendDatagramOnPath queues a DATAGRAM to be sent over the multipath PathID pid.
// It is the thread-safe entry point both the OpenPath initiator
// (MultipathPath.SendDatagram) and the lazily-joined peer use to put data on a
// non-zero path: it only touches pathDatagramQueue (a channel), never the
// run-goroutine-owned multipathOut, so it is safe to call from any goroutine.
// The run loop drops the datagram if pid is not an open path.
func ( *Conn) ( protocol.PathID,  []byte) error {
	if  == protocol.PathIDZero {
		return errors.New("quic: SendDatagramOnPath requires a non-zero path id")
	}
	 := make([]byte, len())
	copy(, )
	select {
	case .pathDatagramQueue <- pathDatagram{pid: , data: }:
		.scheduleSending()
		return nil
	case <-.ctx.Done():
		return context.Cause(.ctx)
	}
}

// drainPathDatagrams moves queued per-path DATAGRAM sends into their path's send
// queue. Run goroutine only (called from the run loop alongside
// processOpenPathRequests).
func ( *Conn) () {
	if .multipathOut == nil {
		return
	}
	for {
		select {
		case  := <-.pathDatagramQueue:
			if ,  := .multipathOut.paths[.pid];  {
				.sendData = append(.sendData, .data)
			}
		default:
			return
		}
	}
}

// sendPathChallenge emits a PATH_CHALLENGE on path pid (addressed to the peer's
// pid DCID), recording its token for later validation. It mirrors
// PathData::record_path_challenge_sent (paths.rs:436-447). It sends at most one
// challenge per path until a response arrives or the path is re-armed.
func ( *Conn) ( protocol.PathID,  protocol.ConnectionID,  *pathOpenState,  monotime.Time) error {
	if .challengeSent {
		return nil
	}
	 := .sentPacketHandler.SendModeForPath(, )
	if  != ackhandler.SendAny &&  != ackhandler.SendAck {
		return nil
	}
	var  [8]byte
	if ,  := rand.Read([:]);  != nil {
		return 
	}
	.challenges = append(.challenges, )
	.challengeSent = true
	 := ackhandler.Frame{Frame: &wire.PathChallengeFrame{Data: }}
	return .sendPathPacket(, , , []ackhandler.Frame{}, )
}

// sendOnPath packs and sends one 1-RTT packet targeting the validated path pid:
// a PATH_ACK{pid} for what we received plus that path's pending DATAGRAM
// payloads (its own send queue). It is a no-op (no datagram sent) when there is
// nothing to pack. Application data riding pid carries its bytes-in-flight on
// pid's own controller, so it is driven down by the peer's PATH_ACK{pid}.
func ( *Conn) ( protocol.PathID,  *pathOpenState,  monotime.Time) error {
	if hasInvalidQNTRoute() {
		return nil
	}
	if .pathSendQueueBlocked() {
		return nil
	}
	 := .sentPacketHandler.SendModeForPath(, )
	if  != ackhandler.SendAny &&  != ackhandler.SendAck {
		return nil
	}
	// Only feed application datagrams once the congestion controller permits new
	// data; on SendAck we still emit the PATH_ACK but hold the data.
	var  [][]byte
	if  == ackhandler.SendAny {
		 = .sendData
	}
	 := getPacketBuffer()
	 := .multipathECNMode()
	, ,  := .packer.AppendPacketForPath(, , , .maxPacketSize(), , .version)
	if  != nil {
		.Release()
		if  == errNothingToPack {
			return nil
		}
		return 
	}
	// Drop exactly the datagrams that were packed into this packet; any that did
	// not fit stay queued for the next packet.
	if  > 0 {
		.sendData = .sendData[:]
		if len(.sendData) == 0 {
			.sendData = nil
		}
	}
	.logShortHeaderPacket(, , .Len())
	.registerPackedShortHeaderPacket(, , )
	.sendPathBuffer(, , )
	// If there is more queued data on this path, keep the loop cycling.
	if len(.sendData) > 0 {
		.scheduleSending()
	}
	return nil
}

// sendPathPacket packs the given frames into a single 1-RTT packet on path pid
// (its DCID + its packet number) and sends it. Used for PATH_CHALLENGE, which
// must not be coalesced with path-0 data because it has to ride the new path's
// connection ID so the peer attributes the validation to pid.
func ( *Conn) ( protocol.PathID,  protocol.ConnectionID,  *pathOpenState,  []ackhandler.Frame,  monotime.Time) error {
	if hasInvalidQNTRoute() {
		return nil
	}
	if .pathSendQueueBlocked() {
		return nil
	}
	 := getPacketBuffer()
	 := .multipathECNMode()
	,  := .packer.PackPathFramesPacket(, , , , .maxPacketSize(), .version)
	if  != nil {
		.Release()
		return 
	}
	.logShortHeaderPacket(, , .Len())
	.registerPackedShortHeaderPacket(, , )
	.sendPathBuffer(, , )
	return nil
}

func ( *Conn) ( *pathOpenState) bool {
	if  != nil && .qntRoute.IsValid() {
		return false
	}
	if .sendQueue == nil || !.sendQueue.WouldBlock() {
		return false
	}
	.scheduleSending()
	return true
}

func ( *Conn) ( *packetBuffer,  protocol.ECN,  *pathOpenState) {
	if  != nil && .qntRoute.IsValid() {
		 := .qntUDPAddr
		if  == nil {
			 = qntProbeUDPAddr(.qntRoute)
			.qntUDPAddr = 
		}
		if  == nil {
			.Release()
			return
		}
		.sendQNTProbeBuffer(, )
		return
	}
	.sendQueue.Send(, 0, )
}

func hasInvalidQNTRoute( *pathOpenState) bool {
	return  != nil && .qntRoute.IsValid() && !validQNTProbeAddr(.qntRoute)
}

func ( *Conn) () protocol.ECN {
	if !.conn.capabilities().ECN {
		return protocol.ECNUnsupported
	}
	return protocol.ECNNon
}

// handleMultipathPathResponse checks whether a received PATH_RESPONSE validates
// one of our outstanding multipath PATH_CHALLENGEs. A response to any token we
// sent on a path validates that path (paths.rs:505-527: validates the path the
// challenge was sent on, regardless of the path the response arrived on). It
// reports whether the token matched a multipath challenge. Run goroutine only.
func ( *Conn) ( *wire.PathResponseFrame) bool {
	if .multipathOut == nil {
		return false
	}
	for ,  := range .multipathOut.paths {
		for ,  := range .challenges {
			if  == .Data {
				if !.validated {
					.validated = true
					.challenges = nil
					close(.validatedChan)
					.scheduleSending() // start sending data on the now-validated path
				}
				return true
			}
		}
	}
	return false
}