PacketTunnel/Flow)Two components. FlowTable tracks every flow’s aggregate state under a hard size cap.
TCPReassembler reconstructs an ordered byte stream for the flows we actually inspect. Both
are bounded — memory safety here is the difference between a working extension and one the OS
kills.
public actor FlowTable {
public struct Config: Sendable {
public var maxFlows: Int // p. ej. 4096; tope duro
public var idleTimeout: UInt64 // ns sin tráfico ⇒ candidato a evicción
}
public init(config: Config, clock: MonotonicClock)
/// Registra un paquete en su flujo (creándolo si es nuevo) y devuelve el estado vivo.
/// Si la tabla está llena, evicta el flujo menos usado recientemente (LRU) antes de crear.
///
/// `resolvedName` es el nombre que el DNS daba a la dirección remota en el instante del paquete,
/// y **solo se usa si el flujo es nuevo**: un flujo se nombra al crearse y no cambia de nombre
/// (`packet-parsing.md` § *Naming flows*).
///
/// `quic` es la versión que este paquete llevaba en su cabecera larga, si llevaba una. Se apunta
/// según `QUICVersionReading.replaces` y, si la versión es de las que se sabe que cifran, sube
/// el flujo de `plaintext` a `encrypted` (`packet-parsing.md` § *Above L4: the QUIC long header*).
@discardableResult
public func observe(
_ packet: ParsedPacket,
direction: Direction,
length: UInt32,
resolvedName: ResolvedFlowName?,
quic: QUICVersionReading?
) -> LiveFlow
/// Marca el estado de inspección TLS de un flujo (lo fija el pipeline TLS).
public func setTLSStatus(_ status: TLSInspectionStatus, for key: FlowKey, sni: String?)
/// Anota el host que el flujo anunció en su ClientHello (lo lee el relay del stream saliente,
/// vía `SNIObserving` → `PacketPipeline.observe(sni:for:)`). **No toca `tlsStatus`**: saber a
/// quién llama un flujo no es haberlo inspeccionado. No-op si el flujo ya no está en la tabla,
/// que es lo que pasa si el nombre llega después de cerrarse.
public func setSNI(_ sni: String, for key: FlowKey)
/// Anota lo que el servidor contestó al ClientHello —versión y suite, o una alerta— (lo lee el
/// relay del stream entrante, vía `ServerTLSObserving` → `PacketPipeline.observe(serverTLS:for:)`).
/// Tampoco toca `tlsStatus`, ni el nombre. No-op si el flujo ya no está en la tabla.
public func setServerTLS(_ answer: ServerTLSAnswer, for key: FlowKey)
/// Flujos evictados/cerrados desde la última llamada, para volcarlos al store y liberar.
public func drainClosed() -> [FlowRecord]
/// Cierra por inactividad; llamado periódicamente por un timer del provider.
public func expireIdle(now: UInt64) -> [FlowRecord]
}
/// Estado vivo de un flujo mientras está en la tabla.
public struct LiveFlow: Sendable {
public let key: FlowKey
public var record: FlowRecord
public var reassembler: TCPReassembler? // presente solo si es TCP y está bajo inspección
public var lastActivity: UInt64
}
Eviction: intrusive LRU (a doubly linked list of keys + a dictionary) so observe is
O(1). On overflow, evict the LRU flow, emit its FlowRecord via drainClosed, and free its
reassembler. TCP flows that see FIN+FIN or RST close promptly regardless of LRU.
Only instantiated for flows selected for inspection (opt-in TLS on, port 443, not yet known to be pinned) or for plaintext TCP we decode. Pure passthrough TCP is not reassembled — that would waste the memory budget for no benefit.
public struct TCPReassembler: Sendable {
public struct Config: Sendable {
public var maxBufferedBytes: Int // tope por flujo (p. ej. 256 KiB)
public var maxOutOfOrderSegments: Int
}
public enum Outcome: Sendable {
case delivered(Data) // bytes nuevos, en orden, listos para la capa superior
case buffered // segmento fuera de orden, guardado, aún no entregable
case duplicate // retransmisión ya vista, ignorada
case downgraded // se superó el tope ⇒ abandonar reensamblado, pasar a metadata
}
public init(config: Config, isn: UInt32, direction: Direction)
/// Alimenta un segmento (para un sentido). Devuelve qué pudo entregarse en orden.
public mutating func accept(sequence: UInt32, payload: Data, flags: TCPFlags) -> Outcome
}
nextExpectedSeq per direction (seeded from the SYN’s ISN + 1).sequence == nextExpectedSeq): deliver payload, advance nextExpectedSeq,
then splice any buffered segments now contiguous and deliver them too.sequence > nextExpectedSeq): buffer in a sequence-ordered structure, respecting
maxOutOfOrderSegments and maxBufferedBytes.sequence + len <= nextExpectedSeq): duplicate/retransmit ⇒ ignore;
partial overlap ⇒ trim already-delivered bytes, deliver the remainder.</>, because UInt32 sequence numbers wrap..downgraded; the flow’s
tlsStatus/handling drops to metadata-only and the reassembler is released. Never grow past
the cap.A single misbehaving or adversarial flow (huge gap, then a flood of out-of-order segments) could otherwise exhaust the ~tens-of-MB extension budget and get the whole extension killed, dropping the user’s connectivity. Every buffer here has a hard ceiling and a defined degradation (downgrade to metadata) rather than unbounded growth.
UInt32 boundary..downgraded and frees memory; a 10k-flow storm keeps FlowTable at or
under maxFlows with bounded total memory.FlowRecord and O(1) behaviour under load.plaintext; an HTTP request or
an unrecognised opening leaves the status alone; no opening lowers a status or overwrites an
inspection outcome; the closing record carries it; one for a flow the table no longer has is
ignored.plaintext until a known version is seen and encrypted from
then on, in its first packet or a later one; packets without a reading change nothing; the server’s
version wins and the client cannot take it back; an unknown version is recorded without the mark;
and the closing record carries the reading.