Skip to main content

Part 2: Lines, Callbacks & Lifetime Safety

This part covers the contracts that cause the most crashes when they are broken: the per-connection callback lifecycle, who owns a line, and how to survive the re-entrant callbacks that can destroy a line out from under you.

If you read only one part of this guide before touching connection flow, read this one. Prerequisite: Part 1 (objects, directions, lifecycle).

The Six Flow Callbacks​

Every connection is driven by six events, each in two directions (upstream toward next, downstream toward prev):

EventMeaningDirection notes
InitA line is starting; initialize this tunnel's per-line state.Flows upstream from the creating adapter.
EstRepeatable notification that an adapter's transport is established (e.g. the remote socket connected); protocol negotiation is separate.Normally relayed downstream, back toward the originating adapter.
PayloadA chunk of data (sbuf_t) to transform and forward.Either direction.
PauseRepeatable advisory request to stop production; ordinary middle nodes relay it transparently.Either direction; adapters, exact-line endpoints, SpeedLimit and bandwidth-enabled UserController handle producer control.
ResumeRepeatable advisory request to resume production; it does not prove all pressure has cleared.Either direction; ordinary middle nodes relay it transparently.
FinishThis direction is closing.Directional and destructive. See below.

A tunnel implements only the handlers whose behavior differs from its constructor defaults. tunnelCreate() terminates on upstream Est or downstream Init unless the node explicitly overrides that callback; its other flow defaults forward to the neighbor. Packet constructors preserve both-direction lifecycle forwarding. For example, an encryptor overrides Payload in both directions but can leave Pause/Resume at default.

Dispatch Through Helpers​

All inter-tunnel flow events must use tunnelNextUpStream*, tunnelPrevDownStream*, tunnelUpStream*, or tunnelDownStream*. This applies to all six events in both directions, including delivery to a selected branch. The helpers enforce the line's active-tunnel mask at delivery time.

Treat the fnInit*, fnEst*, fnPayload*, fnFin*, fnPause*, and fnResume* fields as handler-registration slots. Tunnel constructors may assign their handlers. Outside handler assignments, production tunnel code must not access these fields, including invocation, caching, copying or passing callbacks elsewhere; only the framework's dispatch implementation may invoke them directly. Keep tunnel_t visible in the header so the dispatch helpers can remain inline. Struct visibility does not authorize direct dispatch.

Pause/Resume defaults forward every notification, including duplicates. Dispatch does not store a paused bit, count holds, gate Payload, or infer a node's role from its chain position. Registered handlers still run, including handlers that map different lines or implement the explicit limiter exception. Ordinary middle nodes use the forwarding defaults. This transparency is separate from permanent per-line bypass: a node that still transforms payload or owns state must remain active.

For a known target, use tunnelUpStream* or tunnelDownStream*; these include that target in bypass resolution. Pass the helper itself to reference guards and line-task schedulers, so the mask is checked when delivery actually occurs:

lineCallWithRef(line, tunnelUpStreamInit, target);
lineCallWithRefWithBuf(line, tunnelUpStreamPayload, target, payload);

Do not pass target->fnInitU or target->fnPayloadU instead. The existing reference, buffer-transfer and scheduling contracts still apply. This rule covers flow delivery, not the separate framework lifecycle hooks such as onChain, onIndex and onStop.

tests/tunnel_callback_dispatch_policy_test.py checks tunnel C sources and headers for callback-field reads, including direct calls and captures, while allowing handler assignments and ignoring comments and literals. Run it through the production source-policy selection; it does not require a compiled binary.

Permanent Per-Line Tunnel Bypass​

Every new or recycled line starts with active_tunnels_mask == UINT64_MAX. Bit t->chain_index enables that tunnel's flow callbacks. On the line's owner worker, tunnelBypassLine(t, line) permanently clears that bit; there is no re-enable operation. tunnelIsActiveForLine(t, line) queries it.

Bypass covers all six events in both directions, including Init and Finish. The direct tunnelUpStream* / tunnelDownStream* helpers include their supplied target in the search. The tunnelNextUpStream* / tunnelPrevDownStream* helpers start at the neighbour. Both follow actual next/prev links until an active tunnel is found; chain-index order does not determine the path. Forwarding defaults, context delivery, pipe delivery and packet-anchor publication use these helpers. Raw callback-pointer calls do not perform bypass resolution.

A tunnel may bypass itself only when it is fully transparent along those links for that exact line. It must call its LinestateDestroy routine before tunnelBypassLine(). Destruction must settle retained buffers, timers, queued work, references and other cleanup obligations, then zero the entire aligned t->lstate_size reservation, including padding. Merely zeroing pointers does not replace releasing the resources they own. tunnelBypassLine() asserts every byte of that region is zero in Debug before clearing the bit; it performs no cleanup itself. The tunnel will not receive a later Finish. A node with zero per-line state and no cleanup obligations needs no destroy routine; it can bypass directly once its one-time work is complete. Line owners, endpoints, branch selectors and any tunnel still needed for routing, transformation or flow control must remain active. Every valid dispatch path must reach an active endpoint. Skipping a tunnel does not authorize callbacks into an uninitialized target or change buffer ownership.

Publish bypass before calling outward if a reentrant callback should skip the tunnel. Bypass does not cancel an already-running callback, and ordinary line reference/aliveness rules still apply. A newly created companion line starts with every bit enabled independently of its source line. For persistent worker packet lines, clearing a bit affects every packet using that worker's line. An immediate certified packet enqueue sink must remain active for inline delivery to be eligible; bypass must not redirect that privileged delivery to an uncertified neighbour.

Router and SniffRouter retirement​

Both routers borrow the source owner's normal line. Router can choose a route during Init when all rules depend only on existing metadata; payload-dependent rules and SniffRouter defer branch Init until classification. Publish the selected route and the startup replay guard before calling branch Init. Reentrant upstream input must wait until that Init returns and follow the original classified bytes. Keep the guard through every replay handoff so newer input cannot overtake the retained suffix. Finish clears local state; an outer replay must stop even when the borrowed line remains logically alive.

Nested startup stream input coalesces in the existing ordinary pending buffer, with an inclusive 2 MiB logical limit. This limit applies to newly retained reentrant input, not to the original admitted classification batch. Both routers latch the incoming UDP transport during Init, before branch context rewrites. Each keeps datagrams in a separate FIFO and only copies bytes into the sniffing view. The FIFO, including startup reentry, admits at most 2 MiB of payload, 4 MiB of buffer capacity charge, and 1,024 entries; empty datagrams retain their boundaries and consume entry and capacity allowance. Refusal destroys local state and finishes only initialized directions, with the still-live previous side last. Complete initial input retains the classifier's existing size policy. Once startup replay settles, opaque forwarding needs no additional queue.

Router destroys its state and bypasses after selecting the actual next path, including a rule whose callable target equals next. SniffRouter bypasses only its default-next route. Configured SniffRouter targets retain their distinct upstream-Est suppression, even when a target equals next. Alternate branches remain active in both nodes because generic bypass cannot preserve their routing. Pause/Resume already relay every repeat to the selected branch and do not gate replay; before branch selection there is no initialized upstream recipient.

How a Line Starts​

A normal connection begins with Init, originated by an adapter:

  1. An adapter (e.g. TcpListener) accepts a socket and creates a line_t.
  2. It calls tunnelNextUpStreamInit().
  3. That invokes the next tunnel's fnInitU, which initializes its own line state and forwards Init further upstream.
  4. Each middle tunnel initializes its per-line state in turn.
  5. The final adapter (e.g. TcpConnector) starts the real network operation.
Init is not isolated

A tunnel's Init can synchronously trigger more callbacks before it returns — Payload, Est, Pause, Resume, or even Finish — because the downstream tunnel it called into may immediately push data back or close. This is the same re-entrancy hazard described below; it simply starts at line birth.

Rules for Init:

  • Initialize this tunnel's line state in Init. Later callbacks are allowed to assume that state already exists.
  • Do not add an initialized boolean to guard later callbacks. In the normal WaterWall lifecycle Init always runs first for a tunnel; an initialized flag is almost always a band-aid over unsafe control flow. Add one only when an explicitly documented line-role/callback contract truly requires state before ordinary Init, and cover that exception directly.
  • If your Init calls a re-entrant forwarding helper and then still needs to touch the line afterward, protect it (see Line References). If your Init just forwards and returns, you do not need to hold a reference.

Resolve Context Before Propagating Init​

For a normal line, a node that selects, changes, or validates source or destination context must complete its own context decision, including any admission or branch decision that determines that handoff, before propagating Init to the next stage. Publish the resulting address/domain, port, protocol, source identity, selected branch, and required association state before calling out. The next stage may immediately consume or snapshot that context in Init; changing it afterward cannot repair an earlier routing or connection decision.

Initialize this node's local per-line state as soon as Init arrives. Holding propagation does not defer local initialization. If the necessary information is already available, complete the decision and propagate during Init. If it requires incoming bytes or asynchronous work, process that work locally and withhold outward Init until the decision is complete. Retained input must stay ordered and bounded. Do not forward Payload into a stage that has not received Init; preserve the startup FIFO and re-entrancy rules when releasing input. If the decision rejects the line, close only the stages already initialized, following the ordinary Finish and line-ownership rules.

This requirement concerns this node's own decision for the exact line and branch being initialized. A later node may legitimately perform its own context rewrite. A separately created child or carrier line has its own context handoff and ownership; initializing that transport does not require completing protocol negotiation that needs the transport to exist. Est remains transport readiness and follows its separate forwarding rules.

Examples include DomainResolver waiting for destination DNS, HeaderServer waiting for its port or PROXY header, and protocol servers publishing their selected target and association before backend Init.

Line Lifetime: refcount and alive​

A line_t carries an exactly 32-bit atomic refc and a boolean alive (see ww/net/line.h). These are two different ideas, and conflating them causes bugs:

  • lineRef(line) increments refc. It guarantees the allocation memory will not be freed while you hold the reference. A line reference is not a mutex or lock: it provides no mutual exclusion and does not transfer worker affinity.
  • lineUnref(line) releases one reference. A final release physically reclaims only a logically dead line; releasing a live line to zero is a fatal contract violation.
  • lineDestroy(line) sets alive = false and drops the creator's reference.
  • lineIsAlive(line) reports whether the line is still logically open.

The crucial distinction:

Holding a line reference keeps the allocation memory valid. It does not lock line contents, grant cross-thread access, or mean the line is still logically alive. After a re-entrant callback, a referenced line may be alive == false. Always re-check lineIsAlive() before continuing.

Logical death is immediate; physical reclamation is not. lineDestroy() clears alive and drops one reference. If anyone else still holds a reference via lineRef() the allocation remains present until they unref — which is exactly what makes the owner postcondition below both checkable and cheap:

lineRef(l);                  /* keep the allocation present for the assertion */
owner->fnFinD(owner, l);
assert(! lineIsAlive(l)); /* logical death is immediate */
lineUnref(l); /* physical free may occur right here */

A logically dead line must never receive another flow callback, and by the time it is reclaimed every tunnel's line state must already be zeroed.

The thread which drops the final reference is not necessarily the line's event worker. A device thread or unregistered scheduler can hold a legitimate reference while the owner destroys the line and closes message admission. The final lineUnref() therefore returns the allocation through a thread-safe shared-pool path when no registered event worker is current; only a registered event worker may use the local-pool fast path. Chain teardown treats checked-out line-pool items as a hard ordering violation, so the shared master pool remains alive until every outstanding reference is gone.

The reference count is also the publication edge for final reclamation. Every acquire-release decrement publishes the releasing thread's preceding teardown writes; the RMW which observes 1 -> 0 acquires that release sequence before reading alive, zeroed tunnel state, routing/domain storage, or credentials. Code that adds another direct reference-count decrement must preserve this release/acquire protocol. Do not rely on worker-message locks or an unrelated readiness flag to publish line state to a foreign final releaser.

Line reference counters use atomic_u32_t from ww/libc/watomic.h. Access published counters through its atomic*U32* helpers, including in tests. The generic atomic API uses pointer-sized storage on the Windows fallback and cannot operate on these 32-bit fields. Incrementing a zero or maximum reference count is a fatal invariant violation.

Authenticated identity publication​

AuthenticationClient separates cached admission from control-session readiness. Every credential family and new connection admission checks started && !stopping, a valid installed table/clock basis, no terminal continuity error, and no sticky explicit Authenticate rejection under control_mutex -> users_lock -> collection/user locks. A public availability precheck is only a snapshot; the actual lookup/reservation must recheck it under those locks. After initial synchronization ordinary outages/re-authentication and malformed refreshes keep the last usable RAM cache without an age timeout. Only a correlated authentication-failed for the current Authenticate sets rejection; only a validated successful Session clears it. After a valid producer credential match, session capacity/token/epoch refusal returns session-unavailable, preserving the old session/checkpoint and usable client cache/pending batch while the existing generic RemoteError path reconnects with bounded backoff. accounting-continuity-lost remains distinct. Session expiry/timeout does not fabricate credential rejection. Existing connections retain accounting, cached enforcement and reservation release during rejection. Reused HTTP requests perform new admission; in-flight exchanges and CONNECT streams continue.

A correlated stats permission refusal keeps this cached admission policy. Nonterminal push failures preserve the exact batch and first-use/debt state, reconnect and reauthenticate after a monotonic not-before deadline of at least 30 seconds (or a larger configured reconnect interval). Required retry scheduling remains active with optional intervals zero; explicit work, first use and Resume cannot bypass it. Only a matching batch ACK clears refusal recovery. No pull may replace the baseline while that batch is unresolved, so cached policy may remain stale. Terminal epoch continuity, sequence and replay inconsistency still block new admission. This recovery needs the same live server checkpoint; startup permission changes or process restart do not provide checkpoint recovery.

A required/in-flight pull defers first-use reporting while preserving its request latch and per-user flags. After a non-accounting refresh failure, an installed usable cache may report against its unchanged baseline between traversal attempts. Pull and reporting retries retain separate deadlines; never send a new batch during an active traversal. Pull/ACK completion continues remembered reporting. Paused control writes do not rearm the short progress timer; Resume restarts eligible work explicitly. Worker quiescence cancels both progress and reconnect timer roots.

Credential eligibility and handle liveness check cached enabled state, expiry and cumulative traffic quota. Persisted IP/connection/device/speed gauges must not reject credentials. Live capacity is the separate atomic local reservation operation. Local allowances can overshoot global quotas across disconnected clients; administrative changes arrive with a valid refresh, and already-produced usage remains acceptable after policy changes.

Consumers that must try several UUID-derived credentials use AuthenticationClient's typed UUID source metadata and generation-checked copy pages. A page visits at most 256 source rows, including rows without UUID credentials, and returns only owned UUID/durable-ID values. Callers must bound their retained copies and crypto work, wipe copied credentials on release, and never retain private user pointers or perform crypto under database locks. Every page in one candidate snapshot uses the same installed users_generation; a changed generation invalidates unfinished construction. Include currently ineligible UUID accounts in the snapshot and recheck eligibility after protocol proof. authenticationclientResolveUUIDCandidateWithProfile() checks the current UUID owner, durable ID and continuous membership generation floor with availability, eligibility and profile copying under the normal lock order. Ordinary refresh preserves continuous identities; removal/recreation or UUID reassignment invalidates older candidates. The returned handle uses the current generation. Snapshot limits report configured sync bounds, not a process-wide RAM cap.

Shared credential copying in lineAddUser(), lineAddAuthenticatedCredentials() (including lineSetAuthenticatedCredentials()), lineCopyUsers() and AuthenticationClient profile lookups intentionally fails fast under the project heap-allocation policy. The memory manager terminates on OOM before a failed copy can return. These APIs must never return or continue with partially copied identity. Existing signatures and ordinary cleanup remain unchanged; this contract does not require secure wiping of these copies.

Absent fields retain their existing semantics. A null profile output still requests only the handle and requires no profile-string copies. The global stringDuplicate() function returns NULL for a null source; copying a non-null source follows the fatal allocation policy, including node-local copies.

Recoverable shared-copy results, transactional staging and caller rollback are an explicit policy redesign requiring a maintainer request. Do not introduce them as incidental hardening of these shared APIs.

Paged client table publication​

Producer synchronization uses shared request 9 (GetUsersPage) and Users response 3. A traversal owns a private incremental table and page clock ranges. It replays an unresolved usage batch before starting and keeps pull_required through every page, inter-page gap and terminal validation. A pending correlation represents one request; removing it must not release the traversal barrier. First-use/push work remains latched and local traffic continues accumulating.

Under control_mutex, pending capacity gates request admission and short progress continuations. Full capacity retains desired pulls/pushes/catch-up work without batch reconstruction, warnings or a one-millisecond retry loop. Include ordinary pull_requested in runnable work. A recognized response completes semantic validation and enrollment/ACK/error processing before waking queued work; raw pending removal never sends while locked. Local send rollback defers continuation after actually releasing capacity; stale correlations release nothing. Optional jobs and request timeout may all be zero: waiting indefinitely for an unanswered request must not busy retry. Multiple Pings retain their own absolute deadlines and the timer tracks the earliest one.

Periodic ticks join an active traversal. An explicit authenticationclientRequestPull() during traversal coalesces into one later traversal, and installation preserves that latch. Producer pages retain an immutable server snapshot, so configuration edits do not invalidate its continuation or final validation. A failed refresh releases staging and backs off that pull without preventing new reports from an already installed baseline. Due pull retries still get a turn under continuing traffic. Initial synchronization and unresolved-batch replay remain barriers.

Every request binds the current epoch and last acknowledged sequence/digest. Only matching ACK/enrollment updates those local facts. Usage ACKs require strict bounded JSON validation before settling counters or releasing retry storage; reject trailing input, duplicate decoded keys, decoded NUL and malformed UTF-8 without changing accounting state. Check exact decimal-string metadata, instance/config revision, total, contiguous positions, bounded count, canonical advancing cursor, completion flags and checkpoint before admitting rows. Count exact received user object spans before construction. Positive host-representable sync-max-users/sync-max-bytes default to 100000 and 268435456. They bound one traversal, including an early declared-total refusal, without whole-total reservation. Resource or protocol failure releases staging only and preserves active cache, debt, clocks and generation.

Each producer session retains a reference to an immutable encoded snapshot. Sessions may share a snapshot acquired at the same configuration and statistics revision; a new acquisition cannot reuse pre-ACK counters. Snapshot offsets, rows and construction scratch share the server's finite snapshot-max-bytes allowance (256 MiB by default). Capture and page reads hold server state ownership and release it before transport callbacks. Terminal validation, replacement, session expiry/destruction and a renewable idle lease release references; the default lease is 60 seconds. Resource refusal never publishes a partial snapshot. Administrative list-users retains its separate live revision cursor contract. Each page carries current wall/accounting timestamps, while snapshot revisions and records remain fixed. Envelope revisions may be newer; installed revisions come from the validated snapshot, with latest observed remote revisions stored separately. The client requires terminal validation before publishing.

Before runtime movement, finish structural/ID/index validation, baseline copying, page-clock coverage and current debt completeness. Under the table lock, merge current local deltas/observations, then migrate reservations, shaping and carry. Migration/publication is a nonfailing commit. Reapply each user's OWN page wall/accounting/receipt samples after migration, because runtime migration replaces clock state. Publish active and baseline together and increment generation once. Keep relative catch-up debt even if saturation hides it or an account is absent; never prune debt to finish a traversal. An absent account's owed traffic and first-use evidence move into retained orphan obligations before publication. They do not block installing other accounts and are not sent while the ID is absent. The same durable ID's later return reattaches its obligation exactly once. The retained ledger and replacement overlaps have a finite allowance derived from sync-max-bytes; capacity refusal preserves the old table and every obligation. Status includes orphan_debt_count, and detailed accounting includes these obligations. Authoritatively absent IDs cannot admit new connections; live-handle accounting/close and shaping-ticket settlement retain their existing ownership rules. Retain the original local first-use origin, report timestamp and strongest elapsed-age sample alongside orphan debt, including observations already acknowledged before later bytes became owed. Restore that evidence after runtime migration and before installing the new clock projection; absence must never restart duration expiry. Each newly appearing account receives a handle-generation floor. Continuous membership migrates that floor, while removal and reappearance establish a new one: an older handle must neither become active again nor release a new connection's reservation. Independently owned shaping tickets still settle by their own tokens.

Only an explicit usage-accounts-missing response may resolve a rejected batch without an ACK. Validate its current epoch, exact attempted sequence and payload digest, unchanged prior checkpoint sequence/digest, and unique missing IDs belonging to that batch before releasing immutable attempt storage. Preserve debt, baseline and checkpoint; the uncommitted sequence remains reusable for the surviving accounts. A lost reply or unproved/malformed refusal never permits rewriting an uncertain batch. Exact accepted-batch replay remains acknowledgeable after account removal.

Account configuration and accounting ownership​

usersPatchConfigByIdentifier() pins an existing user with the collection write lock through validation and commit. Its typed user_config_patch_t has a presence bit per editable leaf and carries no credentials, counters, runtime state or creation/first-use timestamps. It reuses the existing string/CIDR preparation and index transaction. Finish every expected duplicate/overlap/capacity precondition before publishing fields, then apply only selected leaves under user->lock. Never merge a previously copied whole timeinfo or limits structure into a patch: that can overwrite concurrent first-use accounting or omitted policy fields. Collection-to-user lock order remains unchanged; first-use updates share that user lock. Configuration edits preserve live reservations and client refreshes migrate them by durable ID while rebuilding the expiry projection.

Collection lookups return borrowed user/string pointers whose lifetime needs external collection ownership when removal or replacement can run concurrently. Successful non-self usersCopy(dest, src) replaces the destination and invalidates all of its old user/string pointers; failed copy preserves them, and self-copy is a no-op. Standalone indexed-field setters such as userSetId and userSetWireGuardAllowedIps must not mutate accounts already owned by a collection; use collection transactions so every lookup index changes together. Runtime self-move is also a no-op.

Each owned name, password, email, notes and normalized Allowed-IP string carries its derived JSON-escaped content charge and encoding/representability result. Prepare replacements before shared locks and transfer their metadata with their text; imports, name-map hints, setters, snapshots, copies and moves preserve this ownership invariant. Generic object callers may hold unexportable text, recorded by that result. Never repair missing metadata by rescanning stored strings under API transaction locks. Direct fixture replacements must update the same metadata.

A successful prepared configuration patch transfers its owned values, clears its aliases/presence masks and consumes the preparation; only destruction is valid afterward. Busy, timeout and other precommit refusals leave it reusable. Prepared account creation remains reusable, including a later duplicate-ID attempt after successful creation.

Fresh JSON imports derive each credential once and use structural/index validation. Trusted baseline copies preserve those derived values without recomputation. Public usersValidate and userPasswordDataValid still perform the full credential consistency diagnostic in every build, and arbitrary caller-supplied checked-add objects retain that check. An incremental required-ID import rolls back only its own appended rows on failure; successful pages never copy or rederive the accumulated table. The collection keeps full diagnostic, standalone and legacy update APIs available.

AuthenticationServer validates at most 64 request frames before executing an envelope. The session at entry governs all siblings, and successful Authenticate does not authorize adjacent requests. Without a valid token, frame payload is at most 16 KiB. Authenticate data is at most 16,375 bytes and uses strict quiet JSON parsing with depth two and eight lexical values; configured credentials must fit the exact escaped handshake including a resume epoch. Failed Authenticate closes further input admission before response handoff while preserving bounded paused response delivery. Only authenticated administrative refusals and commits enter the audit queue; anonymous refusals consume no slots.

Producer module descriptors are the single source for operation names, permissions, store mutation, state-lock class and handlers. Parse each envelope once into at most 64 borrowed frame views before any operation; run mutation-first and read-second over those views under one state lock. Authenticate mutates session state without becoming a store mutation. Page requests take exclusive ownership for snapshot bookkeeping; point reads take shared ownership. An unauthenticated envelope without Authenticate returns bounded refusals under shared ownership. Session resolution must be repeated after reacquiring exclusive ownership. Unknown wire operations remain ordinary bounded refusals. Digest lookup uses canonical base64 with exact decoded lengths; password payloads stay length-bounded and are copied before C-string use.

Request 9 is paged synchronization (GetUsersPage, with the retained GetAllUsers C alias). Each request independently checks token, pull permission and the producer's epoch/acknowledged sequence/digest. Instance/config/position cursors bind the session's retained snapshot, not authority. At most 128 users and a 256 KiB response target form ordinary pages; one exceptional first row may use the hard response cap after exact metadata overhead. Admit encoded row size before copying its strings. A terminal cursor requires one final validation under the same state lock before client publication. Configuration and traffic edits leave an acquired snapshot unchanged. Paging never acknowledges usage. See the producer page schema.

AuthenticationServer UpdateUser selects a nonzero exact durable ID and accepts only explicit configuration leaves. It rejects protected/unknown/duplicate keys, empty containers, null, invalid UTF-8/NUL, excessive nesting and trailing input. The checked lexical parser consumes the full authentication payload with depth limit four and at most 64 lexical values before JSON-tree allocation, without applying host-config file-size limits. Successful nonempty patches bump config_revision once, even for a same-value assignment, inside the server state write lock; stats_revision and producer checkpoint are unchanged. Competing patches serialize: omitted fields survive and the latest accepted edit to the same field wins. There is no expected-revision precondition or remote reset. For example, a name-only patch after a 100-byte account receives 10 more bytes leaves 110 bytes and its first-use timestamp intact.

Password replacement is a separate prepared collection transaction, exposed by AuthenticationServer's explicitly permitted change-user-password API. It checks the server-wide edit token and changes the password plus all four credential indexes together. Configuration-only patch types and producer UpdateUser still cannot change credentials. Preserve the stable user address/ID, configuration, traffic, first-use evidence, creation/expiry, statistics revision, producer checkpoints and live reservations; a credentials-only commit takes no statistics lock. Exact password bytes matter. A UUID-form password adds or replaces its UUID identity; changing to plain text removes it. Case-equivalent UUID spellings keep that identity for the same account, while changed derived keys stop resolving.

Clients receive the change on their ordinary refresh schedules. Before a valid refresh, an offline or connected cache may still accept its old credential. Refresh migrates reservations, accounting clocks, first-use/ACK evidence and expiry projection by durable ID, including pending exact-batch replay and traffic produced during a pull. Existing connections retain accounting and reservation release; rotation does not forcibly terminate them. Public reads omit credentials, so an unknown password-change outcome requires a fresh read and an explicit decision to resubmit the desired password with its current token, without automatic retries.

AuthenticationClient's traffic-multiplier converts measured upload/download bytes at its two public traffic entry points, before counters and quota enforcement, using fixed-point integer arithmetic with scale 1000. The startup-fixed JSON number defaults to 1, accepts 0.001 through 1000 and at most three effective decimal places. Callers must supply raw measured bytes without pre-scaling. Store a separate fractional charge remainder per user and direction; rounding each payload independently changes the charge with fragmentation. Update remainders, charged counters and first-use evidence in the same user/stats transaction. Migrate the remainders by durable ID on refresh, including same-ID credential changes; reconnect/ACK must not clear them. Generic copies, database JSON and sync batches exclude these process-local remainders. Removal/destruction discards them, with no crash recovery. Apply checked saturation to charged counters without unsigned wraparound.

Existing and pulled counters, batch deltas, baselines, immutable retries and catch-up debt already use charged quota bytes; never scale them again. AuthenticationServer and other producer/import paths accept charged bytes as supplied. Traffic limits and API usage/accounting totals use the same units. Bandwidth, speed, reservations and timestamps keep their existing meanings. Start first-use expiry from a positive raw measurement even when its charged increment is zero.

Remote usage reporting uses only deduplicated PushUserStats batches with root epoch, sequence, users and entry id, up, down, first-usage-at-ms, all required and no other fields. The same strict lexical rules apply. Recognize an exact replay using its original payload bytes before consulting current account existence; a subsequent account edit/removal must not prevent that ACK. New batches validate all entries before any mutation. Server aggregate bytes and the earliest accepted first-use observation are accounting state; creation is import metadata, while limits/enabled/expiry are configuration. Connection/IP reservations remain local runtime state. Stored or imported IP/device/connection/speed gauges are not live global occupancy or an admission source. See the statistics ownership table for the current boundaries and requirements for future telemetry. No live gauge reporting, reset, or crash-recovery behavior is implied. Producer write JSON is bounded before tree allocation: PushUserStats allows depth three and at most 7 + 9 * 4096 lexical values (including keys), UpdateUser allows depth four and 64 values, and full AddNewUser allows depth four and 256 values. Keep these independent of the 16 MiB wire cap and preserve credential-string cleanup on post-parse refusal. Envelope response assembly reserves space for a correlated error for every remaining frame, so an oversized read response cannot discard the acknowledgement of an earlier committed write.

First nonzero measured client traffic, including a discounted increment below one charged byte, captures a valid local monotonic origin (zero is allowed) and immutable positive accounting-clock estimate inside the user write lock, then updates counters/expiry under stats write lock. Generic server/import helpers without a client clock never invent observations. Both client traffic APIs use this transaction; zero-byte calls, login, reservation and refresh rebasing do not start use. Scheduling latches are separate from evidence.

GetAllUsers requires exact canonical server-time-ms and positive accounting-time-ms, with no local-wall fallback. The server initializes one wall/monotonic accounting anchor per instance, then advances it only with fresh monotonic elapsed time and saturation. Clients sample receipt before parsing and retain maximum elapsed first-use age across local observations, prior age and canonical estimates. Checked differences never rewind an anchor. Future imported origins remain future until reached or superseded by real earlier use. Duration and absolute-date policy project separately; unchanged pulls keep the earlier absolute deadline and never extend relative lifetime. Explicit edits recompute only their component. Already expired at monotonic zero must remain expired, not become unlimited.

Generic user/user-collection helpers accepting expiry now_ms use local monotonic milliseconds when a client expiry projection is valid. Without that projection they use the wall-time domain of persisted absolute/first-use expiry. Do not pass pacing time to an unclocked account's expiry check. Client traffic observation/accounting helpers with an installed accounting clock sample fresh monotonic time while locked instead of using the caller's fallback now_ms.

userSetClientViewExpiry() changes only the local expiry projection: valid zero means no projected expiry, and clearing validity restores persisted wall-time expiry. It clears the projection's expired marker, while retaining accounting anchors, first-use/ACK evidence and age. A later clock installation or observed traffic can rebuild the projection from that retained state.

Runtime clock/evidence/ACK state and fractional charge remainders migrate by durable ID with reservations, including same-ID credential changes, under ordered collection locks, both user locks, then both stats locks. Keep new authoritative policy/timeinfo and rebuild projections before table publication. Generic user/users copies and JSON exclude these process-local fields. Explicit accounting snapshots capture counters and observations together and retain only owned values across replacement.

The runtime-migration destination must be an unpublished staging table without live reservations or waiters of its own: moving state retires the destination's previous runtime storage. Migration keeps its newly imported policy/timeinfo, transfers matching source accounting clocks and reservations, and requires fresh per-user clock projection before publication. It is not a merge of two active runtime tables.

Every batch entry includes the producer's same immutable observation, even after its ACK. Zero-delta metadata-only entries report unacknowledged observations at saturation; matching ACK settles only included totals/observation markers and keeps origins/age. New batches validate all entries before mutation, sample accounting time once, clamp future estimates and merge the smallest nonzero canonical observation, then publish checkpoint and one stats revision. Configuration patches cannot write these fields; unrelated missing-only first-use APIs retain their contract. Cross-host estimates include network/serialization delay and rate drift; local monotonic suspend semantics remain unchanged. This introduces no crash recovery, remote reset, live telemetry or global reservation coordinator.

WIO Descriptor Ownership​

A wio_t stores its owned primary descriptor directly as int fd, defined in ww/event/wevent.h. wioGetFD() returns that descriptor, or -1 after it has been closed or released. Descriptor zero is valid. The descriptor has no separate allocation, reference count, or pool; only the WIO allocation has deferred lifetime protection.

WIO close removes watcher interest and settles its queued writes, invokes the close callback while the descriptor is still available, then invalidates and closes the descriptor exactly once. Deferred graceful close keeps it open until draining ends. Reentrant freeing in the close callback preserves the WIO allocation until that close frame returns. On Windows, the native close kind is captured at descriptor adoption, so sockets use closesocket() and CRT file descriptors use _close() even if the logical I/O type changes later.

The separate io_slot field preserves descriptor-table bookkeeping after fd becomes -1. It is never used for I/O. Pending and native IOCP operation roots protect the WIO allocation after close; they do not keep its socket open.

NIO splice reads move socket bytes into a wrapper's private pipe before invoking the callback. The wrapper contains neither a source pointer nor a source reference; its length reflects the actual transfer. After delivery the read path does not inspect the source or buffer, so callbacks may close/free the source immediately. No socket reservation or post-callback source guard is needed for this read path.

Each splice allocation exclusively owns its private pipe. Buffers can be retained or partially consumed and deferred immediately, and source WIO reclamation cannot affect them. Queued writes preserve their private pairs until completion or discard. Graceful close drains them; cancellation drains or closes each private pair before recycling. See the private-body rules and AAA/BBB example. This source-independent lifetime does not make pipe bytes ordinary pointer data: parsers and retaining transforms follow the BufferStream representation contract in Part 3.

Freeing a pending NIO detaches its descriptor-table slot immediately but defers recycling the WIO allocation until the pending dispatcher releases it. Shutdown also settles these detached pending allocations before destroying the WIO pool.

pending also prevents reuse of the allocation for a new descriptor. If the OS reuses an fd whose closed NIO is still pending, wioGet() retires that old object on its original loop and creates a fresh WIO for the new connection. The old pending entry never follows an accepted socket to another worker. A closed NIO whose pending dispatch has finished can still be reused in place.

wioDetach() runs on the original owner thread and wioAttach() on the destination owner thread. Both require stopped read/write interest and no pending dispatch; a pending WIO triggers LOGF() and abortProgramNow(1). Stopping interest alone does not release pending-list ownership. Defer a handoff until the original loop has finished dispatch. SocketManager hands off newly accepted, non-pending WIOs, including when an fd number was reused. The dispatcher retains its cur->loop == loop ownership check as well.

wioReleaseNoClose() preserves descriptors owned by another subsystem, such as c-ares. It removes watcher interest, detaches the WIO's array slot, and sets fd to -1 without closing the external descriptor. The external owner remains responsible for closing it. Pending/native operations and callback frames still settle before the WIO allocation can return to its existing pool.

SocketManager endpoints and selection​

SocketManager owns one separately allocated endpoint per physical listening socket. Its persistent pointer collection is the sole listener/UDP side-data inventory. An endpoint owns its interface-scope string and authoritative WIO slot; its lifetime must not depend on the logical filter that first created it. WIO userdata points to the endpoint, including on newly accepted TCP sockets. Static UDP callbacks resolve the endpoint's existing udpsock_t; posted datagrams retain that public side-data pointer. Dynamic UdpListener endpoints use their separate provider-owned lifecycle.

Physical sharing requires equal protocol, bind family/address/wildcard class, bound port and effective scope. Dispatch uses the receiving endpoint's scope as eligibility: a scoped arrival admits that interface's filters and unrestricted filters; unknown scope admits only unrestricted filters. On platforms without device binding, the resolved interface bind address supplies address eligibility. The shared TCP/UDP selector must apply the same protocol, logical port, scope, address-tier and ACL predicate to fresh candidates and cached targets.

Address tiers remain outermost: exact, same-family wildcard, then dual-stack IPv6 wildcard for IPv4. Priorities scan high to low with registration ties kept. An eligible unbalanced filter returns immediately, including after balanced candidates accumulate. Balanced selection retains the first group's candidate set and cached-hit short circuit. Sticky keys include normalized client/local IPs, logical destination port, address tier, effective scope and protocol; client source port is not part of this key. Redirected arrivals retain their logical original port separately from the physical endpoint's bound port.

Selection owns no WIO, buffer or line and performs no transport/tunnel callback. TCP applies the selected filter's socket options before committing a new sticky entry and detaching/posting the accepted WIO. Failed option setup closes once without trying another filter. UDP commits before posting its buffer/side-data. Cached hits refresh during selection; failed cache insertion still allows delivery. Balance-table pointers live privately in filters; socket_filter_option_t contains listener configuration only. TcpListener still creates/owns accepted normal lines; UdpListener still creates/owns the peer lines in each physical socket's worker-local idle tables.

Scheduled Line Tasks​

lineScheduleTask(), lineScheduleTaskWithBuf(), and the two delayed variants transfer one task to the immutable event worker recorded by the line. Immediate work and a delayed call with delay_ms == 0 are always queued for a later loop iteration; neither executes inline. Timer creation and task execution remain owner-worker-affine even when submission originates on another worker, the device thread or an unregistered thread.

Submission precondition​

The caller must hold an independently synchronized live handle until the scheduler takes its own line reference. That means both:

  • a physical reference keeps the allocation present; and
  • a gate, registry lock, or equivalent publication protocol prevents lineDestroy() from racing the call.

An unprotected raw pointer is invalid. A physical reference alone is also insufficient because it does not keep alive true. The scheduler retains the line after the call, but it does not retain the supplied tunnel_t; the runtime or component must keep that tunnel valid until task-or-cancellation settlement.

What the result proves​

Every scheduling result is line_task_submit_result_e and must be inspected:

ResultSynchronous proof
kLineTaskSubmitRejectedSettledThe task will not run. Optional cancellation notification and all scheduler-owned record, line-reference, and buffer settlement have completed before return.
kLineTaskSubmitAcceptedAsyncOwnership entered an asynchronous queue or setup path. It does not prove that a positive-delay timer is armed, that the task remains pending when observed, or that the task will run.
kLineTaskSubmitTimerArmedA non-zero-delay call made on the line's owner worker installed its timer before return. Later line death or quiescence can still cancel it.

The immediate APIs never return TimerArmed. Delayed zero returns AcceptedAsync. A positive delay submitted on the owner returns TimerArmed after successful installation. The same positive delay submitted from any other thread returns AcceptedAsync once owner-side setup is queued.

Scheduling records and timers use normal fail-fast allocation. Positive-delay worker messages install their timer with wtimerAdd(). On the loop's owner worker, timeout records use genericpoolGetItem() from a worker-local pool backed by GSTATE.masterpool_timers. Local width is RAM_PROFILE; master width is 2 * RAM_PROFILE. Pool constructors double those widths for cache capacity, and local refills request RAM_PROFILE records. Allocation either supplies storage or terminates the process; heap exhaustion is not a scheduling cancellation reason and does not invoke cancellation cleanup.

Every pooled checkout resets the record. Expiry, explicit deletion, quiesced/deferred deletion, loop destruction, and pending-event reclamation return records only at final release; an executing callback's pending timer cannot be reused by a nested submission. Worker pools outlive loop cleanup and the shared master outlives all workers. Foreign startup callers and standalone loops without a pool use normal event allocation without borrowing an owner's local pool. Period timers also retain normal event allocation.

Shutdown admission can still refuse timer installation. On the owner worker, that refusal reports AdmissionClosed and returns RejectedSettled. A foreign submission returns AcceptedAsync once setup is queued, and may subsequently be canceled before arming. Cancellation may be delivered before, concurrently with, or after the submitter observes acceptance. The task never runs inline: doing so would violate its minimum delay and can make a self-rescheduling task recurse without bound. Independent delayed tasks have no equal-deadline FIFO guarantee. Ordered work belongs in one explicit queue drained by one scheduled task.

Exactly one terminal outcome​

A non-null LineTaskCancelFn makes the public outcome exactly one of task or cancellation. Cancellation may run synchronously and re-entrantly before the scheduling call returns, later on the owner worker, on the submitting thread, or during detached-queue teardown. The scheduler holds its physical line reference during notification, but the line may already be logically dead and its tunnel line state may already be destroyed. The callback is notification only: it owns neither a transferred buffer nor scheduler cleanup.

Set a pending latch before submission so synchronous cancellation can settle it. A cancellation handler may touch only state whose lifetime and concurrency are independently proved, such as an atomic tunnel-state latch kept alive by the component lifecycle. It must not restart normal work during quiescence or teardown. Passing NULL deliberately declines notification; internal ownership still settles exactly once.

Cancellation reasons are precise:

ReasonMeaning
kLineTaskCancelTargetUnavailableThe target WID has no ordinary event-worker queue.
kLineTaskCancelAdmissionClosedThe target no longer admits normal work or timer setup.
kLineTaskCancelEnqueueFailureQueue-size geometry or wake publication refused submission.
kLineTaskCancelQuiescedWorker quiescence settled accepted work before execution.
kLineTaskCancelTeardownDetached queue teardown settled the task.
kLineTaskCancelLineDeadDispatch found the retained allocation logically dead.

After foreign admission, submitter and owner progress independently:

submitter lane -> setup admission commits result -> return/observe AcceptedAsync
owner lane -> accepted setup may be canceled -> cancellation(Quiesced)
cancellation may precede, race with, or follow submitter observation

Memory and resource safety are unconditional; task progress is not. Required work therefore needs a synchronous RejectedSettled fallback and an authoritative owner/Stop path for cancellation after acceptance. Deliberately lossy payload and observational-statistics work may pass NULL and explicitly discard the typed result:

const line_task_submit_result_e result =
lineScheduleDelayedTask(parent, muxclientLogStatsTask, 5000, t, NULL);
discard result; /* statistics are intentionally best effort */

A required pending-state design can use an independently safe atomic latch in tunnel state. Both terminal callbacks clear the same latch; neither assumes that a timer was armed or reaches into possibly destroyed line state:

static void retryCancelled(tunnel_t *t, line_t *line, line_task_cancel_reason_e reason)
{
retry_tstate_t *ts = tunnelGetState(t);
atomicStoreExplicit(&ts->pending[lineGetWID(line)], false, memory_order_release);
discard reason; /* owner drain is authoritative after cancellation */
}

atomicStoreExplicit(&ts->pending[wid], true, memory_order_release);
const line_task_submit_result_e result =
lineScheduleDelayedTask(line, retryTask, delay_ms, t, retryCancelled);
if (result == kLineTaskSubmitRejectedSettled)
{
/* retryCancelled already cleared pending; perform only safe inline policy. */
}

Do not let both a cancellation callback and the RejectedSettled branch perform the same fallback. Notification has already run when that result is returned.

The shared pipe_tunnel follows the same rule for ordered cross-worker streams. One explicit pair owner holds the borrowed/owned roles and references; any lost Init, data, control, or Finish atomically makes the pair terminal. Each side detaches on its owner worker, only the created companion line is destroyed, and the owner-worker drain is the no-message fallback. The pair retains both physical line references until both sides and every queued message have detached; releasing a target reference at the first-side detach would let a concurrent sender race storage reuse.

Who May Destroy a Line​

Only the tunnel that created a line may call lineDestroy() on it — lineCreate() or lineCreateForWorker() for that exact line_t instance.

Ownership belongs to the line, not to the node type. Do not infer it from a name such as "Client", "Server", "Listener", or "Connector":

  • TcpListener calls lineCreate() for every accepted connection and is that line's owner.
  • TcpConnector owns a socket, not a line. It calls neither lineCreate() nor lineDestroy(); it consumes a line created toward the prev side. Owning an OS resource is not owning a line_t.
  • A tunnel routinely mixes roles: MuxClient owns its parent/carrier line while borrowing every child line; HttpClient owns the upload/download transport lines it splits a borrowed main line into; Socks5Server, TrojanServer, and VlessServer own UDP remote lines while borrowing the client line. Decide per line, in the same handler, before cleaning anything up.

Trace each production creation site during review and keep its ownership and Finish behavior covered by the owning component's runtime tests.

Servers creating owned UDP backend lines must copy the observed client's source address with addresscontextCopy() before backend Init so admission can enforce source-IP limits while preserving the requested UDP destination.

Ownership and topology​

For ordinary connection streams, the usual layout is:

creator/owner -> middle tunnels -> borrowing socket adapter
left upstream toward next -> right

Normal lines are almost always left-owned in this layout. The creator sends upstream Init; middle tunnels and the right-side adapter borrow that same line. The owner may itself be a middle node creating a separate parent, child, or transport line. "Left-owned" describes the path back to that exact line's creator, not the first node in the configuration file or its chain-array index. TcpListener, UdpListener, and the head-only UdpStatelessSocket follow this pattern for accepted connections or incoming peers. Closing their OS resource at a borrowing node does not transfer ownership of the line.

An ordinary middle stream tunnel may use the upstream-then-downstream local Finish sequence below under this topology contract. The downstream Finish travels back to the owner and may destroy the line; it must be the last use of that line in the close path. This is distinct from handling a received Finish, which is forwarded only away from its sender.

Keep these special cases separate:

Line/pathOwnership and lifetime
Worker packet lineShared worker-local packet state, allocated and released by the chain. It stays logically alive throughout runtime; a node must never destroy it on Finish. Worker affinity is not per-connection ownership.
Reverse nodes and BridgeBridge forwards the same line while reversing callback direction; it does not become the owner. Follow the exact reverse-node ownership contract at the direction-changing boundary.
Internal companion transport lineA node that creates one owns that normal line, even inside a packet chain. For example, WireGuardDevice creates normal companion transport lines; when transport faces prev, their Init travels downstream. These are not persistent worker packet lines or ordinary left-owned stream ingress.

The reverse topology has two mandatory direct connections:

Bridge -> ReverseClient
ReverseServer -> Bridge

No node may sit between either pair, including a transparent transform, logger, or limiter. Put extra processing outside these gaps. In particular, ReverseClient's downstream Init must reach Bridge directly; Bridge turns it into upstream Init on the paired branch. An intervening ordinary stream tunnel cannot rely on the usual left-owner model at that boundary. These rules define supported configurations even though startup does not currently enforce the adjacencies automatically; see Part 5.

Do not infer that every normal line in every topology is left-owned, or that an explicit downstream Init handler changes ownership. Trace special/internal creation paths before applying the ordinary stream close sequence. A physical reference keeps allocation memory available; it does not keep a destroyed line logically alive.

Reverse activation and pairing​

ReverseServer borrows both normal halves. The local/user half enters its owner worker's waiting list in downstream Init, before establishment can reenter; pairing must not require a first application Payload. Reverse handshake validation still precedes admission of the reverse half. Either arrival order initiates local or cross-worker matching; notifications resolve live peer lists on their owner worker and never carry a foreign line pointer. PipeTunnel owns any normal companion it creates for migration.

After Est returns, hold an unpaired user source with Pause. After validating the complete reverse handshake and owning its accompanying application tail, hold that reverse source while matching. Never pause an incomplete handshake. Publish the combined source-notification latch before callbacks; waiting/startup FIFO holds and peer-consumer pressure are distinct reasons. Activation does not clear consumer pressure. Drain eligible older application buffers before releasing a source with Resume, and preserve both the waiting hold and consumer pressure through PipeTunnel migration. A source may pair, close or admit bounded input reentrantly from any notification.

A pair emits exactly one length-bearing activation prefix before user bytes. ReverseClient owns both of its normal lines and opens its destination only after consuming the complete prefix, independently of application input. The unused pair's existing idle deadline covers partial activation. Publish pair state and retained FIFO ownership before Init/Payload/Est/Pause/Resume callbacks. Hold and recheck both exact halves when continuing initial replay; a close releases all pending buffers before propagating Finish away from its sender. The server hands off activation exactly once even when its consumer is paused, separately from application backlog. This bounded startup exception does not bypass receiver hard limits or Finish, and a paused transport may defer the physical write. Retained application output waits for its consumer's Resume. After activation and initial replay, application forwarding remains direct. See the wire and storage contract in Part 3.

Neither reverse node can permanently bypass itself: every paired delivery maps between two different normal lines, and Finish still needs that association. ReverseServer retains its combined waiting/FIFO/consumer holds as an exact-line endpoint. ReverseClient forwards every Pause/Resume once destination Init has started. Before that Init, it remembers only an outstanding transport Pause; Resume cancels it. Publish destination initialization before calling out, then replay any remaining Pause after Init returns, before handing off the initial application body. Clear the pending flag before notification so nested signals supersede it. Keep the existing activation FIFO guard and bounds through this callback; Pause does not gate the admitted body or nested replay.

ReverseClient accepts activation after transport Init even if Est has not yet arrived. Completing activation moves the pair from its current connecting or unused count to active exactly once. A late or repeated Est marks transport readiness without returning an active pair to the spare pool or replenishing it again. Est during a partial activation may still move connecting to unused; it does not reset the prefix parser or clear pending Pause.

Shutdown-complete owners and adapters​

Immediate Finish ownership is only half of the normal-line contract. Every autonomous creator must also be able to enumerate all of its live lines during worker shutdown. Publish each new line into exactly one owner-worker inventory before any re-entrant Init or other callback can expose it, and remove the entry exactly once on every close path. Clear the slot or idle-table entry before invoking Finish: the callback can re-enter, and a repeated stop must see an empty slot and do nothing.

A connection-admitting/source owner stops admission, enumerates its remaining lines, destroys its state, sends Finish away from any sender as applicable, and returns with every owned line logically dead. A connection-holding tail adapter has a different obligation: enumerate every line whose Init reached it, detach and close its OS resource, destroy its adapter state, and send exactly one downstream Finish so the real creator closes the line. A tail owns no responsibility before its Init; the autonomous or internal creator must cover that interval itself.

Source-owner draining runs on the owning event worker while the event loop, chain, tunnel states, tail adapters, and line pools are still alive. NodeManager visits tunnels in chain storage order, which is not guaranteed to be source-to-tail. A middle or tail worker hook therefore cannot require borrowed line sets to be empty based on neighbour ordering, and it must not build a duplicate inventory merely to compensate. Middle tunnels do inventory and drain normal lines they create internally. Final onDestroy, after all worker drains, may diagnose a residual borrowed registry. Persistent packet lines and documented terminal absorbers with no per-line resource are explicit exceptions rather than missing inventories.

Every other tunnel must propagate Finish and let the owner destroy the line. Calling lineDestroy() on a line you did not create is a serious bug.

There is also a debug invariant to respect: when a line is finally freed, lineUnRefInternal() asserts that every tunnel's line state is already zeroed (debugAssertZeroBuf). That is why each tunnel must destroy its own line state before the real line-closing Finish propagates — unless it is intentionally holding a positive reference across the close.

Protecting a Line Across Re-entrant Callbacks​

These inter-tunnel calls can indirectly close the line before they return. Treat every one of them as dangerous:

tunnelNextUpStreamInit       tunnelNextUpStreamPayload
tunnelPrevDownStreamInit tunnelPrevDownStreamPayload
tunnelNextUpStreamEst tunnelNextUpStreamPause tunnelNextUpStreamResume
tunnelPrevDownStreamEst tunnelPrevDownStreamPause tunnelPrevDownStreamResume

After any of these returns:

  • the line may already be dead,
  • this tunnel's line state may already be destroyed and zeroed,
  • any code that touches line or ls is unsafe unless you protected the line.

Never reason: "the call returned, so my line state is still valid."

Method A — lineCallWithRef() (preferred)​

lineCallWithRef() and lineCallWithRefWithBuf() hold a temporary reference across the callback and tell you whether the line survived. Their actual implementation (see ww/net/line.h) is:

static inline bool lineCallWithRef(line_t *const line, LineTaskFnNoBuf task, tunnel_t *t)
{
lineRef(line);
task(t, line);
if (! lineIsAlive(line))
{
lineUnref(line);
return false; // line died during the callback
}
lineUnref(line);
return true; // line still alive; safe to continue
}

Use it whenever you must keep working with the line after a re-entrant call:

if (! lineCallWithRef(line, tunnelNextUpStreamInit, t))
{
return; // line died; do not touch line or ls
}

// Still alive here. Re-read state if you need it.
my_lstate_t *ls = lineGetState(line, t);

For payload forwarding, use the buffered variant:

if (! lineCallWithRefWithBuf(line, tunnelNextUpStreamPayload, t, buf))
{
return;
}

When a helper returns false:

  • do not touch line,
  • do not touch this tunnel's line state,
  • do not call your LinestateDestroy() "just to clean up" — the close path that killed the line already did that,
  • if you still own a buffer you did not hand off, recycle it without touching the dead line state.

Re-entrancy can also cross exact-line boundaries. A callback on line A may enter a shared neighbouring tunnel that closes a different tracked line B. If a caller keeps B in a pointer array or registry snapshot and will examine it after the callback on A, it must retain B with lineRef() before the first callback and release that reference on every path. Holding a reference only on A, or holding a registry mutex while copying raw pointers and then releasing it, does not preserve B's storage.

For example, a SOCKS UDP client can create a backend remote line whose Init closes the control association and provider-owned client line. The caller must capture the client's buffer pool and retain the client before invoking the remote callback; retaining only the remote makes neither the dead client's lstate nor lineReuseBuffer(client, ...) safe after return. If the client died, recycle still-local bytes through the captured pool, unref the retained client, and return without reading either line's destroyed state.

When you do not need the wrapper

If you call a forwarding helper and then immediately return with no further work on the line, wrapping it in lineCallWithRef() buys you nothing — the boolean result would not change your behavior. This is common for plain Init and Finish pass-throughs.

Method B — Manual lineRef() / lineUnref()​

Use manual reference management when you need explicit control across several calls:

lineRef(line);

// one or more potentially re-entrant callbacks ...

if (! lineIsAlive(line))
{
lineUnref(line);
return;
}

// still alive: safe to continue
lineUnref(line);

Rules: always unref what you ref; never read tunnel state after destroying it; if a helper both destroys your line state and propagates Finish, the caller must return immediately afterward.

Finish Semantics​

Finish is directional and destructive, and it has one property that makes it special among all the callbacks:

Finish forbids reflection on the same line

If Tunnel A sends Finish to Tunnel B for a line, Tunnel B must not call any callback (including Finish) back in that same direction for that line_t.

This guarantee is specific to that line and direction. Handling Finish on line A may synchronously send a payload on, or close, another still-live line B. This can happen entirely on one event worker; worker-local ownership does not prevent it.

For example, A and B may share a UDP socket. While its close handler notifies A, A's owner may cause a send on B before B receives Finish. B being alive, or holding a line reference, does not make the shared socket usable. Shared-resource teardown must close operation admission for all affected users before its first outward callback, and cached send/queue paths must honor that gate. Review callbacks on siblings as well as callbacks on the line being finished; see UDP send retirement in Part 7.

Concretely:

  • A tunnel that receives upstream Finish must not send tunnelPrevDownStream* (Init/Payload/Est/Pause/Resume/Finish) back toward the sender for that line.
  • A tunnel that receives downstream Finish must not send tunnelNextUpStream* back toward the sender for that line.

This is called reflection, and it is a frequent crash source precisely because tunnels destroy their line state before propagating Finish — reflecting a callback calls into a tunnel whose state for that line is already gone.

Turn the guarantee around and it becomes a simple, load-bearing rule: because Finish is non-re-entrant, you are promised you will not be called back on the finished side — so you must extend the same promise and send nothing back. The instant you receive Finish from a direction, that direction is closed for that line. No Payload, no Est, no Pause/Resume, no second Finish, for any reason, on any later event for that line_t.

Also remember the destruction direction: a downstream Finish that reaches the adapter which created the line will lead to lineDestroy(). So after you propagate a downstream Finish, assume the line can be gone.

The Simplest Clean Finish​

A middle tunnel with nothing to flush just destroys its own line state and forwards. This is EncryptionClient's upstream finish, verbatim:

void encryptionclientTunnelUpStreamFinish(tunnel_t *t, line_t *l)
{
encryptionclient_lstate_t *ls = lineGetState(l, t);
encryptionclientLinestateDestroy(ls); // destroy local state first
tunnelNextUpStreamFinish(t, l); // then propagate
}

Order matters: destroy local state before propagating the real close. Do not read any field of ls after LinestateDestroy(ls) — assume it is zeroed.

Closing Both Directions From the Middle​

When a middle tunnel itself decides to tear down an ordinary borrowed, left-owned connection (a protocol error, a limit hit), it is responsible for closing both sides. Under the ownership/topology contract above, the ordering is:

  1. Destroy this tunnel's own line state.
  2. Send the upstream Finish first. The right side borrows this line and must neither destroy it nor reflect callbacks toward the sender.
  3. Send the downstream Finish second. This reaches the left-side owner and can destroy the line.
  4. Return immediately.
my_lstate_t *ls = lineGetState(l, t);
myLinestateDestroy(ls); // 1. local state gone

tunnelNextUpStreamFinish(t, l); // 2. close forward (line still alive)
tunnelPrevDownStreamFinish(t, l); // 3. close backward (line may die now)
return; // 4. do not touch l or ls again

The line surviving the first Finish follows from its owner being on the other side, together with the no-reflection rule. Non-re-entrancy alone does not prove that an arbitrary recipient cannot own and destroy a line. Apply the specific ownership contract at reverse/Bridge boundaries and for internal companion lines; do not apply this normal-line teardown recipe to a worker packet line.

Adapters are simpler: sitting at one end of the chain, they only ever send Finish once, toward the opposite direction.

Lines you own are different again, and they carry a stronger, checkable obligation — the next section.

The Owner Finish Postcondition​

When a tunnel receives Finish for a normal line it created, that handler must not return while the line is still logically alive:

/* on return from the owner's Finish handler for a normal line */
assert(! lineIsAlive(line));

The postcondition is about logical death only. Outstanding lineRef() references may keep the allocation present until suspended or re-entrant frames unwind; the memory does not have to be freed before the handler returns.

Before returning, an owner must:

  1. Detach every producer that can still target the line — io callbacks, timers, idle-table items, lookup maps, queues, scheduled tasks.
  2. Complete whatever cleanup is legal for the direction that sent Finish.
  3. Destroy its own line state exactly once.
  4. Propagate Finish only where the topology requires, and only away from the sender — the no-reflection rule is unchanged by ownership.
  5. Call lineDestroy(line) unless a nested path already did.
  6. Return immediately, touching no destroyed state.

For keyed or timed ownership, detach the map/timer entry and clear the consumer's handle before invoking a re-entrant Finish or destroying the line. An indexed idle entry must be removed immediately on explicit close; retaining a long-deadline tombstone makes memory proportional to historical churn rather than live ownership.

A creator such as MuxClient does exactly that. The excerpt below is muxclientTunnelDownStreamFinish() — the fnFinD slot, so next is the sender. The parent's children hang off the prev side, and prev is therefore the only direction anything may travel:

/* fnFinD on the parent line MuxClient created; next finished us. */
while (child_ls)
{
/* ... */
muxclientLinestateDestroy(child_ls);
tunnelPrevDownStreamFinish(t, child_l); // away from the sender
/* ... */
}

muxclientLinestateDestroy(parent_ls);
lineDestroy(parent_l); // only the owner may do this

The general shape, with the copy-to-stack step that keeps the code safe after LinestateDestroy(). Read the direction off the callback slot the handler is installed in — this one is fnFinU, so prev sent it and next is the only side that may be told:

void ownerUpStreamFinish(tunnel_t *t, line_t *l)
{
owner_lstate_t *ls = lineGetState(l, t);

ownerDetachResources(t, l, ls); /* 1 */

bool propagate_away = ls->next_init_sent; /* copy before destroying */
ownerLinestateDestroy(ls); /* 3 */

if (propagate_away)
{
tunnelNextUpStreamFinish(t, l); /* 4 */
}
if (lineIsAlive(l))
{
lineDestroy(l); /* 5 */
}
return; /* 6 */
}

The fnFinD mirror is the same function with tunnelPrevDownStreamFinish() in step 4. An endpoint owner — a chain head such as TcpListener or UdpStatelessSocket — has no prev at all, so its fnFinD propagates nothing and step 4 disappears. The postcondition is unchanged either way.

Why it matters. Without it, this sequence corrupts memory: a middle tunnel forwards a re-entrant callback; a nested Finish destroys that middle tunnel's per-line state; the line owner receives the propagated Finish but leaves the line alive; the outer callback then reads lineIsAlive(line) == true and resumes against state that no longer exists. The postcondition makes lineIsAlive() an honest post-callback signal.

Borrowed lines are the mirror image. Destroy only your own per-line state, propagate the same directional Finish away from the sender when the topology requires it, and never call lineDestroy() — the real owner's destroy must stay valid. Never reflect Finish, payload, Est, Pause, or Resume back toward the side that finished you.

A middle tunnel that also owns the line still follows the both-directions ordering above for its local close, and then the owner postcondition applies on top: the line must be dead before that close frame returns.

requestProgramShutdown() does not satisfy the postcondition. It schedules global teardown on worker 0; the current worker callback returns and unwinds through every suspended frame first. An owner whose Finish reaches a terminal failure must still detach, destroy its state, and mark the line dead — and then request the shutdown.

Deferred OS cleanup is not an excuse to stay alive. If a resource needs asynchronous teardown, detach it from the line first and give the deferred cleanup object its own copy of whatever it needs. A live line is not a cleanup token.

Packet lines are excluded. They are never subject to the owned-normal-line postcondition and must never be destroyed at runtime. A Finish on one is not automatically a contract violation: a packet-lifecycle anchor may reject it, a transparent middle transform forwards it in the same direction, and an intentional terminal absorber may absorb it when there is nothing onward to notify or release. The exact handler role and direction decide which category applies — see Part 4.

Most Tunnels Need No Directional-Finish State​

A direct consequence of non-re-entrancy: do not give a tunnel prev_finished / next_finished (or can_upstream / can_downstream) flags by default. A tunnel that simply forwards Finish is guaranteed never to be called back on the finished side, so it has nothing to remember. A speculative "did this side finish?" boolean is almost always a control-flow problem being patched instead of fixed — the same anti-pattern as an initialized flag.

There is exactly one situation that justifies directional-close state: the tunnel must send final bytes before it closes, including a bounded delayed fallback batch (the next subsections). That send re-enters the adapter or branch, which can emit Pause/Resume, and the flag is what lets handlers drop anything that would reflect toward the already-finished side. The flag is bookkeeping for that hazard and nothing else.

Real tunnels carry exactly this state and no more. The names differ; the role is identical:

TunnelDirectional-close stateWhy it needs it
TlsServerupstream_finished, downstream_finishing, fallback_close_drainingSends TLS alert / close and settles delayed fallback before closing
TrojanServer, VlessServerfallback_close_drainingSettles a delayed fallback batch without reflecting toward the finished previous side
TcpOverUdpClientcan_downstreamMust finish its reliable-stream teardown before closing
TcpOverUdpServercan_upstreamThe same, in the opposite direction
HttpClient, HttpServer, HttpProxyClient, Socks5Serverprev_finished, next_finishedSend final protocol bytes before transport close

If your tunnel does not send final bytes during Finish, it should not carry any of these flags. Destroy local state and forward — that is the whole job.

Final Protocol Bytes Before Closing​

Some protocols must emit closing bytes before the transport closes: TLS close_notify, an HTTP final chunk, HTTP/2 END_STREAM, a WebSocket close frame, a KCP close frame. Sending those bytes during Finish opens a dangerous re-entrant window.

The hazard, step by step:

1. Tunnel receives Finish from side A.
2. It sends final protocol bytes toward side B.
3. Side B's adapter blocks while writing and emits Pause (or later Resume).
4. That Pause/Resume travels back through the tunnel...
5. ...and if the tunnel reflects it toward side A — whose state was already
destroyed by the original Finish — it calls into freed/zeroed state. Crash.

The required safe shape:

  1. lineRef(line) — you are about to send bytes re-entrantly.
  2. Mark the sender side finished first, using the tunnel's existing directional state (prev_finished, next_finished, can_downstream = false, can_upstream = false, etc.). Your Pause/Resume handlers must check this and refuse to reflect toward a finished side.
  3. Send the final protocol bytes toward the other side.
  4. Destroy this tunnel's local line state.
  5. Propagate the real WaterWall Finish.
  6. lineUnref(line).
  7. Return immediately. Do not touch local line state afterward.

Direction examples:

  • Handling upstream Finish, sending final bytes upstream to next: any downstream Pause/Resume caused by that send must not be forwarded back to prev.
  • Handling downstream Finish, sending final bytes downstream to prev: any upstream Pause/Resume caused by that send must not be forwarded back to next.

Do not invent a new initialized flag for this. Reuse the tunnel's existing directional finish/close state, or — if the tunnel genuinely lacks one and this is the final-bytes case — add an explicit directional close flag. As stressed above, this is the only place such a flag belongs.

TlsServer SNI fallback admission​

TlsServer borrows the normal line created by its upstream owner. With both sni and fallback configured, its OpenSSL early ClientHello callback compares the visible SNI before TLS output. A mismatch or absent SNI suspends OpenSSL; the payload driver must replay the retained original wire bytes to fallback before later payloads. Matching SNI continues to the protected branch. Without fallback, the SNI gate still rejects mismatches. Malformed ClientHello/SNI input rejected before routing stays on the TLS error path.

The original transcript is bounded to 1 MiB, including record headers and coalesced trailing bytes, and the existing handshake deadline covers incomplete input. Retained snapshots use compact byte copies and coalescing; never retain a full receive allocation for each tiny fragment merely because the logical byte count is below the limit. Copies carry the line pool's left padding for later replay. ServerHello, including HelloRetryRequest, commits the protected TLS branch: never switch to fallback afterward. Starting fallback releases TLS state and disarms its deadline before branch Init. Queue the entire initial input before invoking Init, including the triggering plaintext buffer when applicable. While Init is on the stack, later re-entrant input may enqueue behind it, but neither inline payload nor scheduled drains may run, including through Resume. After Init survives, release this admission barrier and drain ready input in FIFO order, including during advisory Pause. A close during Init discards queued input instead of sending payload into an unfinished Init callback. Preserve the borrowed line's reference/owner rules.

Before branch selection, retain only a pending client-side Pause for the branch that has not received Init. Resume cancels that pending notification. Once the selected branch has received Init, relay every Pause/Resume, including duplicates; clear any pending notification before an outward callback so reentry cannot replay stale pressure. Keep the line referenced when work continues after the notification and stop if it closes the line. This startup mapping does not gate TLS record output or delayed fallback payloads on Resume.

Delayed Fallback Batches Before Closing​

A scheduled line task holds a physical line reference only. It does not keep the line logically alive, and its runner intentionally skips the callback after the owner destroys the line. A borrowed middle tunnel therefore must never park an upstream Finish behind a line timer merely because its fallback payload queue is delayed.

For an initialized fallback branch, use this close order:

  1. Mark the previous side finished and publish a fallback-drain gate before any final branch Payload callback. An empty queue can destroy local state and synchronously finish the initialized branch immediately.
  2. For a non-empty bounded FIFO, capture the branch and line pool, detach the queue from line state, and establish that its total fits the destination buffer before coalescing it in FIFO order. An admission limit that already proves this bound needs only an assertion at the drain, provided no path can bypass the limit. One callback bounds the final handoff and its reentrant teardown window.
  3. Never flush while branch Init is unfinished. TrojanServer, VlessServer and TlsServer may deliver this final batch through advisory Pause; receiver limits still apply. Recycle any batch disallowed by the node's close gate before finishing the branch.
  4. While the final callback runs, absorb supported Init, Est, Pause, and Resume callbacks, and recycle any returned Payload. A callback explicitly forbidden by the receiving node remains fatal; for example, TlsServer rejects downstream Init even during this drain. A re-entrant downstream Finish records that the branch already closed and is absorbed, so no reflected or duplicate branch Finish follows.
  5. Re-check lineIsAlive() after the final payload while holding the temporary reference. If it died, unref and return without touching line state. Otherwise destroy local line state before the final branch Finish; after that finish, only unref and return.

The old timer has no cancellation handle. Emptying/detaching its queue logically cancels the work; once the real owner destroys the line, the scheduled runner becomes a harmless reference release. Preserving the exact remaining delay after Finish instead requires a detached session object that owns copied buffers, timer state, and branch resources without any line_t or line-state reference. That is a different design, not an excuse to keep a borrowed line alive.

With delay zero, the safe referenced inline path requires completed branch Init and no older FIFO batch or scheduled drain. TrojanServer, VlessServer and TlsServer do not gate it on Pause. Retained bytes keep their ordered drain path, so later payload cannot overtake them.

Adapters Flush On Their Own​

Adapters such as TcpConnector and TcpListener may flush queued bytes before they actually close the socket after receiving Finish. So a middle tunnel should: send its final protocol bytes, destroy local state, forward Finish — and then trust the adapter to flush. Do not keep protocol state alive just to "wait for the socket to drain."

The optional TCP peer write-shutdown diagnostic is implemented on Linux excluding Android and Cygwin. It can identify a fallback peer only within the same network namespace; unavailable diagnostics return false. Android builds use the false-returning stub, and TcpConnector does not enable the diagnostic there. Android therefore retains ordinary EOF closure without diagnostic-triggered fallback socket lingering.

TCP adapter idle deadlines​

TcpListener starts each owned normal line with a 60000 ms idle deadline. Listener-side payload activity extends it using the 600000 ms active idle timeout; the idle-table refresh only extends an existing deadline. Explicit initial-idle-timeout-ms and active-idle-timeout-ms settings override these defaults. TcpConnector uses a 600000 ms idle limit for its borrowed normal lines, refreshed on adapter activity. Keep both active defaults aligned so neither adapter introduces a shorter cleanup limit into a fallback path. Target closure still propagates Finish without waiting for these idle deadlines; socket output may drain under the existing adapter close contract.

TcpConnector explicitly sets a separate 60000 ms WIO connect timeout before starting each outbound TCP attempt, after address resolution. Both NIO and IOCP cancel that timer on successful connection. It bounds connection establishment, not first-payload silence, and immediate socket errors can still finish the line sooner. The node's deadline does not change the generic WIO connect default for other callers.

The initial listener wait approximates NGINX's default, while ten minutes is a transport cleanup policy, not an NGINX maximum. Neither adapter tracks HTTP request stages. TlsServer owns its separate absolute handshake deadline and disarms it when entering fallback. Protocol-specific header/body/write deadlines and lingering-close behavior cannot be obtained by changing adapter idle limits alone. Fallback targets allowing longer inactivity require every applicable idle limit in the chain to accommodate it.

MuxClient fixed-parent selection​

Fixed mode assigns a worker-local key to each new child and a stable identity to each owned parent. Selection first prefers non-suspect eligible parents, then uses rendezvous hashing, with the parent identity breaking equal scores. max-children remains a hard eligibility cap; existing associations are never remapped. The child is initialized as an unattached borrowed line before parent selection invokes any neighbour, and a physical reference protects that Init. Reentrant source Finish destroys the unattached state without reflection; a finished child must not be attached or established afterward.

MuxClient's keepalive defaults true in all modes, with ping-interval 15000 ms and tolerance-ms 45000 ms (integers in 1..INT_MAX). Each parent schedules its first probe one ping-interval after downstream transport Est, sending on the first eligible local-idle expiry at or after that deadline. Waiting for this probe does not make the parent suspect or block child attachment; reply timing starts only when a probe is sent. A parent-local transport notification latch sets this schedule even if a downstream junction already marked the shared line established; repeated Est does not reset it. MuxServer's boolean keepalive defaults true and controls replies only. Disabled endpoints consume controls without responding or acknowledging. Only a matching complete Pong on the same parent clears an outstanding probe; user traffic and partial frames do not. The first matching Pong confirms peer support. Unanswered discovery probes are retried without suspicion or retirement, so disabled responders are safe but parents that never confirm support have no health-based recovery.

One quarter of elapsed tolerance (at least 1 ms) makes a confirmed parent lower priority in fixed selection. Full tolerance permits soft retirement in every mode on reply-deadline expiry or new-child selection, including during Pause. Late matching replies restore unretired parents. Retirement preserves attached children and the authoritative owner inventory, but never returns a parent to selection. Each worker retains at most one keepalive-retired parent per fixed slot, or one in timer/counter modes. Pending final output still counts; Unregister settles the count exactly once. At the bound, occupied unresponsive parents remain selectable and the deadline item retries retirement after 1000 ms without changing the original probe time. Empty unresponsive parents close through normal parent-loss cleanup without waiting for Resume. A watchdog-retired parent closes when its last child leaves, discarding blocked final output. Ordinary timer/counter rotation still preserves output until drain. No active child is forcibly terminated and no expiry callback creates replacement parents.

Probe times use wloopNowMonotonicMS() on the owner worker. Parent transport Pause and child FlowPause do not suspend scheduled probes or outstanding reply deadlines. New probes wait for unpaused parent output, an idle output pump and an empty FIFO; only handoff starts the reply clock. Blocked probes retry after 1000 ms while retaining their original due timestamp; missed wakeups coalesce into one due probe. The local table's approximately one-second wakeup precision applies even to shorter configured intervals and tolerances. A separate item tracks the reply deadline, so matching Pong cancels that obligation without rescheduling the next probe. Discovery expiry clears the unanswered obligation and permits another due probe without penalizing a peer whose keepalive support remains unconfirmed. Pong replies share the ordinary parent FIFO, charge limit and Pause gate. Downstream buffering and transfer after handoff consume tolerance. Unconfirmed support still prevents health-based retirement. Quiet connections and one-way uploads need only the Mux peer's reply. Timer-mode parent age uses the same monotonic clock; wall-clock changes do not rotate transport connections. MuxServer's rejected-Open burst refill also uses its owner's monotonic clock. See Part 7 for table and parent ownership.

Est, Pause, and Resume​

  • Est is a repeatable notification of adapter transport establishment. Continuing middle tunnels must relay it promptly, including repeats; their own protocol readiness must not delay or replace that signal.
  • Pause / Resume are repeatable advisory backpressure signals. Ordinary middle nodes forward them transparently, including duplicates. They are not counted holds or proof that no more payload will arrive. Emit them in the intended direction and never toward a finished side. See Flow-Control Roles and Admission.

Est Is Transport Readiness​

Adapters originate and consume transport establishment for their connection. Est may arrive more than once for the same live, initialized line. It reports transport readiness; it does not initialize or restart the connection. Every supported recipient must tolerate repeated notifications. Est does not clear Pause or release a producer controlled by an adapter, exact-line endpoint or SpeedLimit.

On a continuing same-line path, an intermediate node must forward every received Est, including repeats, in its supported callback direction without waiting for its request header, first application payload, authentication, protocol reply, handshake completion, timer or consumer Resume. Do not keep a notification latch solely to suppress forwarding. In the ordinary upstream-created chain, next-side Est continues toward prev with tunnelPrevDownStreamEst().

Transport establishment and protocol readiness are different facts. A middle node may record transport state and perform required protocol work, but it must keep the protocol's readiness and payload barriers separate. Est does not prove authentication, destination acceptance by a remote proxy, or completion of TLS or another negotiation. Use explicit protocol state and, where two cooperating nodes need notification, a dedicated protocol-completion hook. Do not send a second transport Est when that protocol work completes.

Middle nodes may still use Est for their own logic. Guard work that must happen once separately from forwarding: repeated notifications must not resend a greeting, recreate resources, restart a deadline or repeat an accounting transition. Publish that local state before forwarding, because the previous node can synchronously submit Payload, Est, Pause, Resume or Finish. Keep accepted payloads ordered and bounded until their protocol can carry them. Preserve the required wire order, including control messages before application bodies, even when application data arrives from the Est callback. Pause is independent of the establishment signal. Adapters, exact-line endpoints, SpeedLimit and bandwidth-enabled UserController retain producer control; ordinary middle-node scheduled output need not wait for Resume. Continuing after any callback still requires the exact line references and logical-life checks described above. Never forward into a finished direction or on a logically dead line.

On their owned normal lines, TesterClient and SpeedTestClient consume the first Est to start sending. Record that first notification before scheduling work; repeated Est, including reentry during a Payload callback, must not queue another send or retry timer or bypass a configured inter-burst delay.

Classify each exact line at topology boundaries. A node that owns a carrier, multiplexes children, pairs streams or bridges a packet line can terminate one line lifecycle and map readiness to another; it must use that role's explicit mapping and notify a live application association once. Do not blindly forward an owned helper line into a neighbour that was never initialized for it, broadcast one child's Est to unrelated children, or manufacture establishment before the relevant transport or local lifecycle endpoint exists. Boundary ownership is not permission to delay a continuing line's Est behind protocol negotiation.

Payload admission does not require a prior Est. After the adjacent Init has admitted the exact line, a live node may submit valid protocol bytes while the transport is connecting; the transport owner handles connection-time writes. This does not permit Payload before Init, unauthenticated forwarding, application bytes before required wire headers, or callbacks into a finished direction.

lineIsEstablished() / lineMarkEstablished() describe line establishment, not protocol readiness. lineMarkEstablished() is idempotent: marking an already established live line leaves it established and does not dispatch a callback. A downstream endpoint may mark the shared line before forwarding Est, so the shared marker does not prove that this transform has performed its own one-time work and must not suppress forwarding. Retain a local guard when that work needs one. This rule does not enable otherwise unsupported upstream Est callbacks on ordinary tunnels, change disabled adapter edges, or replace the packet lifecycle rules.

Flow-Control Roles and Admission​

Pause/Resume are advisory and repeatable. Ordinary middle nodes must relay both signals transparently, including repeated Pause while already paused and repeated Resume while already resumed. Do not keep consumer-pause state solely to suppress duplicates or gate ordinary middle-node output. A timer becoming due or a protocol backlog becoming ready may produce output without waiting for Resume. FIFO order, protocol readiness, ownership, hard limits and line lifetime still apply; Pause transparency does not remove those obligations.

Classify the role for the exact line, rather than the node's position in the configured chain. A node may be a middle transform on one line and the head or tail of a carrier, child or paired line on another. Those boundaries must keep their explicit signal mapping; blindly following next/prev with the wrong line is never transparent forwarding.

RoleResponsibility
Transport adapter or exact-line head/tailHandle repeated notifications idempotently, control new reads/emissions and independent drains, serialize writes and enforce local queue limits.
Ordinary middle transform or parserRelay every Pause/Resume. Transform admitted input and retain only necessary framing/protocol/ordering state under explicit bounds.
Ordinary middle node with scheduled outputRelay every Pause/Resume. Due timers and ready backlogs may send without Resume, in FIFO and protocol order.
Association or multiplexing boundaryMap exact lines and combine pressure reasons for the producers it controls. Being in the middle of a configured chain does not remove its endpoint responsibilities.
SpeedLimit and bandwidth-enabled UserControllerExplicit middle-node exception: combine local throttling with external pressure; handle or absorb notifications so neither reason is released by the other. UserController datagram backlog alone does not hold its producer.

An ordinary middle node may originate advisory Pause/Resume toward either live, initialized side. After originating Pause, it must arrange its own eventual Resume or close even if no further input arrives. Do not wait for another Payload to release the source that was just asked to stop. Repeated Pause is not a counted hold requiring an equal number of Resume calls.

These advisory requests do not carry the identity of every outstanding issuer. An advisory Resume may override another node's Pause; it does not promise that all receivers are writable or all queues empty. Endpoints and the explicit limiters retain the combination of pressure reasons that they explicitly own. Ordinary middle nodes do not recreate that machinery merely to relay notifications.

The accepted overload policy is bounded retention and closure on refusal, rather than a guarantee that all middle producers stop during congestion. A receiver can receive both in-flight data and later scheduled output after requesting Pause. Each buffering node must enforce its own limits; an adapter cannot bound data still held inside another node. Limits are node-specific, not a universal 2 MiB allowance, and several upstream queues may converge on one receiver.

Time deadlines are separate from storage limits. They continue according to the node's timeout policy. If a protocol starts its reply deadline at Payload handoff, that deadline can include time spent queued behind congestion; timeout closure before actual wire transmission is an accepted consequence. Handoff is not proof of transmission. No generic timer or payload gate belongs in ww dispatch.

Node-specific descriptions below also document scheduling gates implemented by individual nodes. Those gates do not establish a general requirement that an ordinary middle node wait for Resume. Endpoint and explicit-limiter flow control remains required independently of those local scheduling choices.

The reverse activation prefix has a narrow startup exception: ReverseServer hands off that single bounded control buffer through Pause to open the destination. It never attaches retained application bytes to the exception; their independent drain still waits for Resume.

Completing an admitted input batch​

An input batch consists of one accepted Payload delivery and the bounded parser prefix/partial frame needed to interpret it. The node must establish its ownership and FIFO position before invoking another node. Completing this batch can emit multiple Payload callbacks; these are expected to reach a receiver even after Pause propagated synchronously through the chain.

Trojan and VLESS UDP decoders complete all complete datagrams in that accepted input after Pause. They do not leave ready datagrams queued solely for Resume or stop header parsing solely because Pause arrived during an earlier delivery. They retain only an incomplete suffix or input held for a genuine protocol or reentrancy barrier. Reentrant input has its own FIFO position and bound; serialize already-admitted nested batches after older input within the outer synchronous dispatch. They must not be relabeled as one ever-growing batch. Work deferred beyond that dispatch remains a bounded, ordered backlog. An ordinary middle node may release ready work without Resume; endpoint and explicit-limiter drains retain their flow control.

Finish, logical death, protocol rejection and hard-limit refusal are terminal: stop immediately and release the untransferred remainder. Batch completion never permits callbacks after Finish or requires a receiver to exceed its hard limit. A partial frame still needs later input; its existence must not itself suppress the Resume that allows its producer to complete it.

Receiver tolerance and FIFO ownership​

A receiver requesting Pause must retain finite admission headroom and tolerate further delivery up to its hard limits. Pause does not guarantee that scheduled middle-node output stops. Specify the supported batch envelope, transformation expansion, queue high/low watermarks, hard limits and overflow policy. Count retained bytes and entries or allocation charge; tiny and empty datagrams must consume a finite budget. One coalesced stream input can decode into many datagrams, so a payload-byte limit alone is not a complete resource bound.

Tolerance is not unlimited acceptance or a guarantee that every protocol-valid burst fits during congestion. If a burst exhausts the hard limit, close the smallest supported association or apply the node's documented datagram-drop policy. Never drop arbitrary TCP bytes, silently lose an accepted suffix, or allocate without a bound to finish a batch. The sender rechecks exact-line life after each handoff and stops on closure. Stronger admission reservations may be used where an entire maximum batch must be guaranteed, but that guarantee must be explicit.

Publish ownership, queue accounting and notification state before emitting Pause, Resume or any callback that can reenter. A callback must not allow newer data to overtake an older buffer that is still only in a local variable. Ordinary pop releases budget usage before handing a buffer onward. If the same budget still owns active retention, transfer to a reservation without uncharging; publish consumption before callbacks and release when that responsibility ends. Use one authoritative FIFO per independent ordering obligation; a guarded admission/drain can serialize reentry without adding permanent queues to every middle node.

At an endpoint or an explicit limiter, Resume releases a producer according to the reasons that endpoint or limiter owns; it does not prove that every queue in the chain is empty. Ordinary middle nodes relay notifications without consumer-permission queues or a Resume prerequisite for due output. Locally originated advisory pressure still needs its own release path.

Bgp4Client and Bgp4Server retain a receive stream for incomplete frames and a bounded output FIFO for complete encoded batches and reentrant input. Their per-direction pump guards preserve FIFO while default Pause/Resume callbacks forward every notification transparently; admitted complete output does not wait for Resume. Such protocol and ordering obligations require explicit bounds and exact-line role handling. They do not require every ordinary encoder/decoder to own a transport queue.

MuxClient and MuxServer are examples of explicit local buffering: parent transport Pause forbids parent-bound Payload, including Open, Close, flow controls and Ping/Pong. Admitted encoded output uses one bounded per-parent FIFO and charge limit. New probes remain due until direct handoff is possible, without joining that backlog. Mux nodes pause child producers at its aggregate queue-capacity threshold, in addition to individual writer holds. Complete queued frames retain valid private bodies under local queue limits. Incoming assembly and attached child queues share the existing parent receive limit, with beneficial pressure compaction followed by largest-child shedding or parent close. There is no worker-wide pipe quota. Pause still gates Payload while allowing bounded retention; see the Part 3 contract. A blocked child writer acquires its own local producer hold immediately after admission, including empty Data and complete splice batches. A direct write that leaves the transport paused holds its writer before returning; a transient Pause then Resume uses the final permission. Controls alone are not attributed writers. Idle siblings below aggregate high water remain runnable. At high water the separate pause-all latch holds every attached open child.

The local hold FIFO borrows attached child state and is independent of that latch. Release requires a live parent state, no Finish/quiescence, writable transport and charge at/below low water, even when pause-all never activated. Recheck after every callback and before each release/pop. New output appends behind existing FIFO entries; renewed transport pressure or charge above low water stops release. A writer held again joins behind remaining waiters. Snapshot references protect every member before any callback, followed by logical-life, attachment and state checks. No persistent references or second cursor are used.

Init/Est defers permission callbacks without confusing a recorded reason with an emitted Pause. Completion reconciles the current reasons once safe. Peer pressure and terminal-close state remain independent; local unlink and terminal paths remove held-list membership without emitting Resume. A writable empty parent must release ready waiters rather than depend on a future transport event. TcpListener also preserves a Pause received during accepted-line Init: it enables initial reads only if Init returns alive and the source is still unpaused.

Mux's fixed header cache owns consumed header bytes until the declared body has been consumed. Data body bytes reach a writable child incrementally, with no Payload boundary guarantee and no callback for empty Data. Paused children keep available chunks in their bounded FIFO; Mux remains an endpoint for its separate parent and child lines, so this queue still waits for that child's Resume. The parser publishes the remaining body length before callbacks and serializes reentrant parent input behind existing bytes. A child closing during a partial Data frame makes its remaining body discard-only; only after consuming that suffix may the next header be parsed. Parent loss cannot retract an already delivered prefix, and accepted queued chunks keep their detached-drain policy. No callback observes detached stream ownership. Large splice encoding publishes the complete frame batch and Open state before any callback. A Pause during its first delivery leaves all later frames in the parent FIFO; nested Data or child Close follows that remainder. Parent death discards it, while child death alone does not release its cost. Ordinary large encoding keeps one aggregate.

A local child Pause submits FlowPause immediately, without a queued-byte threshold. MuxClient defers this control until Open has been admitted. After submitting the complete first payload, including a large splice batch, it must re-check both referenced lines and the child's attachment and pause state before submitting any deferred FlowPause. A resume or close during submission must not produce a stale control, and an already submitted FlowPause must not be repeated. FlowResume still requires a writable child and logical queued payload below its configured resume threshold.

Submitting a Mux control frame can close its child during queue-pressure callbacks while leaving the parent alive. The receive path distinguishes those outcomes: it retains the parent across callbacks, stops using the closed child, discards any remaining body for it, and continues parsing sibling traffic on the surviving parent. Remaining incoming child queues still undergo the normal parent memory-limit check.

UdpConnector rechecks both line life and read-pause state after its fallback receive-time Est callback. If Est pauses the line, the already-read datagram is recycled under the same drop policy as other paused receives; it is not delivered through Pause. Later datagrams may be delivered after Resume.

Transport queue admission must be transactional before threshold callbacks. Once a queue owns the incoming buffer, callback-driven close releases it through that queue; the caller must not recycle it again. If admission fails before ownership transfers, the caller recycles it through the captured line pool and settles the affected flow. Exact-line references protect callbacks, not logical life. Preserve this ordering in connecting, paused-write and write-completion paths.

TCP adapters attach their FIFO to a shared byte/charge budget and transfer popped entries to an active WIO reservation. The original admitted capacity charge remains until completion or terminal callback detachment. Partial write callbacks, live write returns and new admissions refresh logical bytes from WIO without reading the transferred buffer. Cleanup settles reservations before destroying line state; deferred WIO cleanup retains no budget pointer. UdpConnector initialization and all per-destination DNS queues attach to one line budget, including recreated DNS queues; socket datagram writes do not create an active reservation or retry queue. Empty retained buffers consume charge and entries.

TLS/Reality pending plaintext queues share their existing byte/entry limits with active backlog reservations. Consumed plaintext releases bytes before wire-output callbacks, while capacity and the entry remain charged until buffer release. Direct synchronous inputs retain their existing admitted-input behavior. TrojanServer attaches its populated FIFO only when selecting CONNECT or fallback output; initial and UDP parser fragments retain their separate byte and framing policies.

Numeric thresholds and hysteresis are implementation details. Read the target adapter's payload.c, helpers.c, and structure.h; do not copy one adapter's current tolerance into another node or into this guide as a universal value.

ConnectionFisher line mapping and retirement​

ConnectionFisherClient terminates its borrowed main line and owns the candidate child lines. It must keep the winning-child mapping, combined backlog/transport pressure and Finish ownership for the association's lifetime. Its independent backlog drain obeys child Pause; its main-side reply Pause is held until a winner exists so it cannot block the probe exchange. Candidate Est notifications map to one main-line readiness notification. These are endpoint responsibilities, so the client cannot permanently bypass itself.

ConnectionFisherServer borrows one normal line along its actual next/prev path. It validates FISH?, sends FISH!, and delays next Init until the first application body. The current body and nested input retain FIFO position and their existing 2 MiB/1,024-buffer bound across reply, Init and pending-Pause callbacks. Keep only the latest upstream Pause before next Init; replay it after Init unless a newer Pause/Resume handled that state during reentry. Once Init starts, relay every Pause/Resume in both directions and every downstream Est, including repeats.

After Init and all admitted replay settle, destroy and zero the server's entire aligned line state, then permanently bypass it. Neither Est nor Resume is a prerequisite. No callbacks remain for the server after retirement, including Finish; the client retains its distinct-line mapping.

TcpOverUdp local KCP pressure​

TcpOverUdpClient and TcpOverUdpServer borrow normal lines and continuously transform stream payloads into KCP datagrams and back, optionally through FEC. Their timers and protocol state prevent permanent bypass. Incoming Pause/Resume already relay transparently, including repeats, with directional Finish guards; they do not stop KCP updates or due output.

KCP/FEC output callbacks enqueue owned datagrams in contexts. Neighboring tunnel callbacks run only after the KCP/FEC call returns, because they may destroy the protocol state. Preserve this output staging and the subsequent line/handle checks.

Their separate source-pressure latch represents locally queued and unacknowledged KCP segments. Reconcile it after KCP update and packet delivery, outside KCP's output callback, with the exact line referenced. Publish the latch before sending Pause above the configured threshold or Resume below it; equality retains the existing state. ACK-only input must be able to release the local hold. Admit the complete source payload before any pressure callback so reentrant input cannot overtake it. After notification, recheck both line life and the KCP handle: Finish may have destroyed this borrowed node's state without destroying the line. The automatically derived threshold saturates at INT_MAX, matching KCP's signed queue-count interface. The send-buffer setting remains an advisory threshold, not a hard admission cap.

Raw QUIC transforms​

QuicClient and QuicServer borrow the existing normal line and create neither sockets nor companion/application lines. Client upstream plaintext becomes next-side carrier data; server upstream carrier data becomes next-side plaintext. Their outputs preserve these directions even though the transforms have opposite roles. One QUIC v1 connection carries one client-initiated bidirectional application stream per line. The client sends encrypted opening byte 0x00; the server validates and consumes it before releasing application data, including queued server-first output. It is never application data.

Both nodes initialize and publish local state before forwarding next Init once. Client handshake startup is independent of Est. Every supported downstream Est is relayed, including repeats; authenticated protocol readiness produces no extra Est. Application bytes wait under hard budgets until handshake/stream readiness, while input and library output are serialized against reentry. Neighbor callbacks run after the QUICHE driver call returns. After each callback, check both line life and local attachment: Finish may retire this transform on a still-live borrowed line.

Incoming Pause/Resume relay, including duplicates. Due loss/ACK alarms and ready FIFO output continue through advisory Pause. A separate local send-pressure latch asks only the application source to pause when any retained byte count, entry count or allocation charge exceeds three quarters of its hard limit. Resume requires all three below one half; equality preserves its state. The allocation limit is twice the configured byte budget. Accepted unacknowledged chunks remain charged, so ACK-only input must be able to release pressure. An external Pause reaching a UDP adapter can stop ACK ingress; finite pauses recover when deadlines permit, while prolonged pauses can close the association.

A received Finish closes its sender's direction before cleanup and never emits back toward that side. Cancel alarms and detach protocol state before forwarding away. A local failure settles local state and finishes next before prev under the ordinary borrowed-line topology. Neither QUIC node calls lineDestroy(). Application-side Finish may make a bounded synchronous ready-wire drain only toward the still-open carrier, honoring congestion/pacing and guarding every reentrant signal against the finished side. No asynchronous reliable-drain promise survives received Finish: the line owner may destroy it immediately after the handler returns. Final unacknowledged data may be lost; the peer has finite deadlines. Application stream FIN/reset closes the association after preceding decoded bytes, without application half-close.

SpeedLimit independent drains​

SpeedLimit combines its directional queue hold with external consumer pressure. It publishes each reason before notification and sends Resume only when both have cleared, without a temporary Resume followed by Pause. Consumer Pause stops that direction's timer/backlog output; a due timer leaves the bounded queue intact and does not poll while blocked. Consumer Resume rearms queued work, including an empty-queue local-release tick, before any outward notification. Timer admission refusal, including a refused reset of an existing timer, settles local buffers/timers and closes through the normal line owner. Keep the reset result; an existing timer pointer does not guarantee rearm admission. Each notification path returns without touching line state after the callback, which may synchronously close or reenter the connection. Token availability never overrides consumer pressure, and empty queued input cannot stall a drain.

UserController shared user shaping​

Bandwidth-enabled UserController is an explicit producer-control limiter on its borrowed normal lines. For streams, its local queue hold and external consumer Pause are independent reasons: neither releases the other. Datagram backlog does not create a producer hold or send Pause/Resume solely for local shaping: UDP producers may otherwise discard arrivals before the controller's bounded queue fills. Datagram queue overflow drops the newly refused input; explicit external consumer Pause/Resume still applies and is propagated. The direction carrying queued upstream bytes controls the producer toward prev; downstream bytes control the producer toward next. Mapping those bytes to upload/download depends separately on the saved Init origin. A consumer Pause suspends that direction's drain and removes its eligible fair turn. Resume establishes progress before notifying the producer, because notification may reenter Payload or Finish. Anonymous and unlimited immediate forwarding remain transparent; a retained FIFO still has priority after a finite-to-unlimited transition. UserController still borrows the line. For an internal close, save the Init origin before clearing local state, finish the noncreator side first, then the creator side; reverse-origin lines therefore finish prev before next. Receiving Finish still propagates only away from its sender.

A user's independent directional bucket and fair-turn queue belong to one AuthenticationClient runtime and are shared across controllers and workers. Waiter identity is a non-reused runtime ticket, not a raw user pointer or numeric ID alone. No user, collection or budget lock covers a neighboring callback. Ready queues receive bounded FIFO turns; paused, canceled, empty or removed tickets do not block eligible work. Refill uses fresh monotonic samples across workers; burst capacity is max(1, ceil(R / 10)) bytes at actual logical rate R bytes/s, with zero unlimited and no unsigned-64 rate clamp. The one-byte minimum permits integer-byte progress at very low rates. Whole datagrams can hold a turn while earning credit, so FIFO fairness does not promise equal latency. Bandwidth preview and delivery commit use separate samples for pacing and expiry. Buckets, grants and retries use fresh monotonic milliseconds. Installed client expiry projections retain their local monotonic interpretation, including an explicit projection without a client clock. A standalone user without that projection compares absolute and first-use-duration expiry against fresh wall time under the same locks; wall-clock corrections never refill buckets. Preview does not charge. Commit rechecks shared enabled/expiry/traffic eligibility and charges only delivered bytes through the same directional fractional-remainder primitive as ordinary scaled traffic accounting. Live reservation gauges remain separate from stored gauge predicates.

An oversized head datagram must earn complete credit before release. Credit earned before its activation is limited to ordinary capacity; abandoning its turn cannot donate an oversized burst to another ticket.

Managed positive arrival observes first use before retention, independently of charged traffic. Delivery first previews eligibility without consuming credit, prepares exact output and reserves all retained costs, then revalidates and commits bandwidth plus multiplied quota under shared locks. Successful commit is immediately followed by ownership handoff, without intervening fallible preparation or scheduling. Streams may split; datagrams stay whole. A quota-exhausting final chunk is handed off once before closure. Queued, canceled or discarded data consumes no quota or multiplier remainder. An already exhausted user cannot commit more. Public traffic-accounting APIs retain their ordinary accounting behavior.

The creator binds a pure owner-worker payload-mode resolver before preparation, or a manual instance configures payload-mode as stream, datagram or dest_context->protocol; conflicting or missing sources reject startup. The destination option reads protocol metadata once during Init: exactly TCP selects stream and exactly UDP selects datagram; missing, ambiguous or unsupported protocol rejects the line, even when unmanaged. It is opt-in, not an implicit fallback. Use it only when destination metadata is ready before Init and describes payload framing at this point in the chain; transport conversion may leave routing metadata that no longer describes local payloads. Either Init direction evaluates and saves the mode before admission or forwarding. Late promotion uses that saved mode, independently of later routing metadata changes. SOCKS/Trojan/VLESS classify their initialized exact-line role, HTTP uses stream, and WireGuard's normal transport uses datagram in either orientation. This metadata API adds no WireGuard authentication integration.

StreamFragmenter upstream scheduling​

TlsClient's optional fragment object creates an owned private StreamFragmenter after the TLS core and before its configured next node. Both borrow the same source-owned normal line. The complete initial TLS flight reaches the helper as one Payload; subsequent TLS output uses the same ordinary callback path. The helper's default Est wait gates its own output without holding the transport Est notification. Record shaping precedes stream fragmentation; their queues and budgets remain independent. Raw ClientHello-generation calls do not enter this per-line helper.

StreamFragmenter borrows ordinary upstream-created Layer 4 lines. In default stream mode, its immutable settings select the first count Payload arrivals or arrivals strictly before a duration-ms deadline established at the first downstream Est by default. Every arrival consumes counter scope, including empty, bypassed, and too-short input. Eligibility and selected absolute cut offsets are fixed before admission or any reentrant handoff.

With tls-hello-fragment: true, the first non-empty arrival latches eligibility and bypass for one initial ClientHello; later callbacks can complete it after scope expiry. Earlier empty callbacks still count. Selected cuts are sampled once when the four-byte handshake header reveals the complete message length. Offsets count handshake bytes, including that header and excluding TLS record headers. The node keeps the exact original wire prefix in a single budgeted candidate FIFO job. It accepts supported handshake records up to a 64 KiB message ending at an original record boundary, preserves record versions and boundaries, and adds headers only for selected cuts inside an original record. Failure to recognize that initial candidate, bypass, no selected cuts, or assembly deadline expiry makes the original prefix ready unchanged and ends detection for the line. No later ClientHello is probed. A suffix from the completing input is admitted behind the candidate before any callback can deliver newer input.

Optional wait-for-est defaults to true. When enabled, all pre-Est upstream input joins the same bounded FIFO, including bypassed, empty and exhausted-scope input. No upstream handoff or fragment-delay timer starts before the first downstream Est has finished forwarding. Counter scope still counts arrivals; timed scope admits pre-Est arrivals and starts its window on the first Est. Publish receipt and the timed deadline before forwarding Est promptly, retaining the startup gate across notification and checking exact-line life afterward. Nested Payload, Resume or Est cannot bypass that gate. Duplicate Est continues forwarding without restarting eligibility or scheduling. Pressure notifications remain transparent before Est and do not gate fragment scheduling. Disabled shaping does not disable the startup wait. Explicit false permits pre-Est scheduling and starts timed eligibility in Init. Finish settles the same FIFO without waiting for Est. This is a node-specific scheduling policy, not a general prerequisite for middle-node payload admission or a promise of transport packet boundaries.

One per-line FIFO owns original payload jobs and the active remainder. Each job's first delay starts at the active head; subsequent delays start after the previous fragment is forwarded. Bypassed and no-longer-eligible arrivals retain their FIFO position. New arrivals during an active drain append behind its remainder, including from Pause, Resume, or Payload callbacks.

Once shaping eligibility is exhausted, or TLS handling reaches its terminal opaque phase, retire the node only after the startup wait is open, its FIFO and candidate are empty, no timer or drain is active, and its own source pressure is released. Destroy and zero the full line-state region before permanent bypass. Subsequent callbacks skip the node, including Finish. A collecting TLS candidate remains active after scope expiry. Generic timed expiry is observed on input or drain; there is no extra timer solely to retire an idle line. Disabled shaping may retire during Init when wait-for-est is false, or after the first Est and queued work settle when it is true.

The incomplete TLS candidate also occupies that FIFO, but its parser can advance through bounded admitted input while the startup Est wait closes the output gate. The separate assembly deadline starts with the first selected non-empty input, includes time waiting for Est, and may arm the line's one timer before Est. It is canceled on successful commitment. After completion, the first fragment delay starts only when the ready job reaches the head and the startup gate is open. Expiry makes original bytes ready without bypassing the startup Est wait or older FIFO work; Finish discards them. An empty callback during collection needs no extra parser job.

Pause/Resume use transparent default forwarding in both directions, including duplicates. Due fragments, completed TLS candidates, and ready FIFO work do not wait for Resume. Monotonic deadline checks prevent the event loop's coarse timer rounding from sending early. Downstream bytes and Est retain ordinary immediate forwarding.

Retained jobs, including the active head, share fixed inclusive limits of 8 MiB logical bytes, 8 MiB canonical buffer-capacity charge, and 1,024 jobs. Local source pressure starts at 6 MiB charge or 768 jobs and clears only at/below both 3 MiB and 384 jobs. Release this local hold automatically with Resume when retention reaches those low watermarks. Forwarded Pause/Resume are independent advisory signals: they neither settle this hold nor prevent its release. Publish ownership and updated accounting before callbacks. Ordinary allocation charge remains held until its remainder transfers; splice charge can shrink with body consumption. One transient output fragment is additional extraction workspace, not another retained FIFO. Neither charge nor the entry limit promises physical kernel pipe memory usage.

The splice-capable node extracts generic selected ranges through sbufMoveRangeTo, with ordinary fallback, and transfers the final remainder directly. TLS inspection uses range helpers to materialize only the candidate prefix; later opaque splice buffers can still pass intact. Rewriting temporarily reserves both original and replacement buffers in the same budget before replacing the job. Received Finish cancels its timer and discards all queued bytes before forwarding away from the sender. Local admission/setup refusal closes the borrowed line upstream then downstream; it never destroys that line itself. Generic event-loop quiescence detaches timer callbacks before the creator's owner-worker line drain reclaims the retained timer handle and jobs. See the node reference for configuration and concrete cut examples.

Client destination preparation before DNS​

TcpConnector selects its destination once before DNS and retains that destination's socket options and outbound IP range in the resolver's prepare-hook storage. Its own Init initializes the ordinary connector line state and consumes the prepared settings directly during socket creation, before connect can trigger callbacks. Keep destination selection and its settings together across DNS.

TcpConnector and UdpConnector check constant domains against the address context's 1–255-byte length before narrowing, for both single and weighted destinations. TcpConnector's constant CIDRs use the strict shared parser at construction: IPv4 accepts prefixes 0–32 and IPv6 accepts 64–128. Randomization replaces only host bits, including the complete IPv4 /0 and IPv6 /64 ranges.

TrojanClient, VlessClient and Socks5Client apply their configured address, port and TCP/UDP selection to the line's destination context before local DNS resolution. Their internal DomainResolver invokes a client-owned prepare hook; the resolver itself does not choose the client's protocol. These hooks need no custom resolver line-state storage.

DNS resolution replaces the destination address while preserving the selected protocol flags. When Init reaches the client, read those flags from the prepared destination context. Without local DNS, the client applies the target directly in Init and then reads the same flags. Target preparation leaves exactly one of TCP or UDP selected; this is a debug invariant at the client entry, not another runtime configuration check.

Do not apply the target settings a second time after DNS: a configured domain could overwrite the resolved destination. Protocol-selection warnings and the existing TCP fallback for an invalid incoming dest_context->protocol belong to target preparation and occur there once.

HttpProxyClient also prepares its configured/dynamic origin in a client-owned DomainResolver hook when local resolution is enabled for CONNECT. It is TCP-only: validate and reject explicit UDP/ICMP/packet inputs before replacing context; an unspecified protocol becomes TCP. Copy dynamic address and port before any setter resets their storage. Do not apply the other clients' protocol fallback rules here. After DNS, format the numeric origin without reapplying settings, before the following connector replaces context with the proxy endpoint.

ConnectionToPackets also prepares its destination in its internal DomainResolver hook before DNS. Its optional target-address and port independently replace the corresponding incoming fields; omission or their exact dest_context->... selectors preserve those fields. Preserve the destination protocol during address replacement: an exact destination TCP/UDP protocol wins, otherwise infer it from the source context. The resolver stores the selected protocol for the bridge's Init and uses only-ipv4. A configured literal IPv6 address fails configuration; an unusable dynamic destination fails only its line. Consume the resolved address in Init without applying the configured target again. source-ipv4 selects the virtual packet source, independently of this destination preparation.

UDP line role names​

TrojanClient, VlessClient and Socks5Client name the borrowed UDP-facing line UdpApplication and refer to it through application_line. Application is relative to this protocol layer: the preceding chain may contain another protocol node. TrojanClient and VlessClient name their internally owned TCP line UdpCarrier and link to it through carrier_line; UDP describes the traffic carried, not the carrier's transport protocol. Socks5Client instead distinguishes its internally owned UdpControl TCP line and UdpRelay UDP line.

These names describe protocol roles. Server-side client, control and UDP remote roles retain their own names; prev and next describe callback direction and neighboring tunnels. Determine ownership from the exact line's creation site.

SOCKS transport readiness and protocol ordering​

Socks5Client forwards every direct TCP transport Est before or after negotiation, including repeats, while starting its greeting only once. A UDP control carrier maps transport establishment to its application once. Authentication, command success and relay readiness retain their distinct application-data gates. Its pending application FIFO uses the default 2 MiB / 1,024-buffer backlog budget described in Part 3. Direct TCP releases this FIFO when protocol-ready, without waiting for Resume, and keeps nested input behind older bytes. It forwards every Pause/Resume in both directions, including during negotiation. The first retained application input originates an advisory source Pause; draining that local backlog releases its hold without waiting for transport Resume. UDP retains combined control/relay output pressure and its protocol backlog hold at the association boundary. Its control handshake remains separate from application receive pressure. Proxy reply parsing has one synchronous owner. Nested downstream Payload cannot advance the parser recursively or overtake the accepted reply tail. A retained TCP response backlog drains in FIFO order without a consumer-Pause gate. Ready direct TCP forwarding keeps its synchronous path. UDP application receive pressure maps to the owned relay, including replay when that relay is created; control tails are discarded after association acceptance.

Direct TCP permanently bypasses Socks5Client after the successful command reply has been consumed, all parser/reentrant input and application FIFOs have drained, and its local source hold has been released. Destroy and zero its complete line state before clearing the active bit. A source-pressure or Est callback may finish setup reentrantly; suspended continuations check the active bit before using that state again. UDP application, control and relay lines remain active for protocol framing, signal mapping and owned-line cleanup.

Socks5Server forwards every CONNECT transport Est, including repeats, and generates its success reply only once. That reply still precedes backend response bodies, including under Init/Est reentry and Pause. CONNECT success and ready ordering queues do not wait for Resume. Repeated TCP Pause/Resume forwards transparently; before outgoing Init, retain the latest receiver Pause for replay to the initialized branch, with an early Resume canceling that pending request. UDP control, client and remote line roles retain their explicit mapping and endpoint behavior. Its control-input scope serializes parser progression: nested Payload joins the upstream FIFO behind the active input. Method, authentication and UDP-association transitions and identity are committed before reply callbacks. Failure replies close input admission before calling prev; reentrant Finish still completes local cleanup before the borrowed-line owner destroys its line.

CONNECT TCP permanently bypasses Socks5Server after its success-reply handoff, outgoing Init and startup pressure replay, and both directional ordering queues have settled. Drain work added by a response callback in the opposite direction before retiring. Input and control-dispatch guards keep state alive for older parser/Init callbacks; a suspended Est continuation recognizes nested retirement. Destroy and zero local state first; credentials already published on the line remain available. UDP control/client/remote lines stay active for their endpoint and association responsibilities.

DomainResolver accepts upstream Init and delays next Init until destination DNS completes. Its prepare hook runs before resolution and has no direction argument. Downstream Init and upstream Est retain the normal constructor's rejection policy; this is a node-specific contract, not a rule for reverse/Bridge or companion lines. Payload, Pause/Resume and Finish still operate in both ordinary directions.

The resolver keeps one authoritative upstream backlog through next Init. The Open phase permits downstream Est and replies immediately; an Init guard serializes new upstream input behind older DNS input. DNS completion releases that FIFO independently and observes next-side Pause. Admission checks logical bytes and retained allocation charge separately against 2 MiB per line, including empty buffers. The local DNS/backlog hold combines with next-side pressure before notifying prev. Upstream receive pressure is remembered during DNS and forwarded to next once Init starts. Failures before next Init finish only prev; internal failures after it starts finish both initialized sides. Received Finish destroys local state first and propagates only away from its sender.

On a normal line, the resolver permanently bypasses itself once next Init has returned, its pending FIFO is empty, and its local hold and neighbour pressure notifications have settled. Neither direction needs to be unpaused when the permissions already match. Destroy the entire resolver state, including embedded prepare-hook storage, before clearing the active bit. Prepare-state consumers must finish using that storage during next Init and copy anything needed later; the destroy hook runs at bypass or earlier Finish, never again after bypass. Source-pressure callbacks can complete this transition reentrantly, so suspended continuations check the active bit before touching state. Actual worker packet lines remain active; normal lines in a packet-containing chain remain eligible.

TrojanClient establishment and association ownership​

TrojanClient borrows direct TCP and UDP application normal lines. UDP creates one owned normal TCP carrier on the application's worker; only that carrier receives next Init. The application keeps its UDP routing context. Signal forwarding maps the application and carrier explicitly, retaining both exact lines across callbacks and checking logical life and association afterwards.

A valid first application payload can submit the request after next Init, without waiting for transport Est. The request and complete first payload share one ordinary buffer; later TCP forwarding is opaque. Direct TCP transport Est is forwarded promptly, including repeats; UDP carrier Est maps once to its application. The first transport Est starts an idle first-payload deadline only if the request has not already been sent; repeated Est does not restart that deadline. The header-only timer sends when due on direct TCP, including during advisory Pause. TCP Pause/Resume notifications are forwarded transparently, including duplicates. The owned UDP carrier retains its output-pressure gate: its due header waits for carrier Resume. An admitted application Payload can still complete its combined handoff under the batch contract.

Ordinary TCP forwarding and one-datagram upstream wrapping do not gain separate Pause queues. Downstream UDP retains framing input and completes the current accepted input batch. Keep only the guard/storage needed for parser reentry, initialization ordering or a genuine protocol wait. A node-wide pump is not a contract; use explicit directional handlers and focused helpers for actual work.

For direct TCP, assembling the complete first request ends this node's setup work. Destroy and zero its line state, including cancellation of the first-payload timer, then permanently bypass the node before handing off that buffer. Reentrant callbacks use the transparent path; suspended Est callbacks must check the active bit before accessing their retired state. No transport Est or protocol reply is required for this transition. UDP application and owned carrier lines remain active for framing, association mapping and cleanup.

Close cancels the deadline, detaches both association links and destroys locally owned buffers/state before outward Finish. Only the carrier is destroyed here. Source-owner shutdown drains the dependent carrier while loops, chains and pools remain alive. No duplicate borrowed-line inventory is needed.

VlessClient establishment and association ownership​

VlessClient borrows direct TCP and UDP application lines and owns only each UDP application's dependent TCP carrier on the same worker. Both links are attached before next Init. The line role determines the VLESS command: direct lines use TCP, while UDP application lines route requests through their UDP carrier role. Keep the configured protocol selection in tunnel state; per-line state need not store a second copy of the role's protocol. Its first-payload batching, idle deadline, signal mapping and opaque TCP paths follow the same ownership/admission rules as TrojanClient. Every direct TCP Est is forwarded, including repeats; UDP carrier Est maps once to its application. Repeated Est does not restart the first-payload deadline.

VLESS protocol readiness remains separate. The request must precede upstream body, but neither transport Est nor a response is required before submitting a valid request/body batch after Init. Downstream body waits for complete validated response metadata. Consuming that metadata cannot lose or reorder a coalesced TCP body or the first UDP frame. UDP input retains incomplete framing bytes, completes all ready datagrams in its active accepted batch and preserves an incomplete suffix.

Record request admission before the first outward callback so nested data cannot send a second request or overtake it. Protocol/reentry storage is bounded; an otherwise-ready TCP buffer is not retained merely for Pause or Est. Direct TCP Pause/Resume is transparent, including duplicates, and its first-payload timer sends when due without waiting for Resume. The owned UDP carrier retains its output-pressure gate and exact-line signal mapping as described for TrojanClient. Close clears timer/parser/ordering state and both links before outward Finish, never reflects toward its sender, and leaves an owned carrier logically dead.

Direct TCP permanently bypasses VlessClient only after request admission and complete response metadata consumption, once the outer receive batch has drained all retained and reentrant body input. Either protocol direction may complete first; neither alone permits bypass. Destroy and zero local state before clearing the active bit, including canceling the first-payload timer. Suspended Est callbacks must recognize state retired by nested callbacks. UDP application and carrier lines remain active for framing, signal mapping and owned-line cleanup.

TrojanServer associations and fallback replay​

TrojanServer borrows the client normal line and owns each UDP backend on that client's worker. Target, credentials, map membership and callback-ready state must be published before backend Init. AuthenticationClient is a lookup dependency; UserController runs only on the protected branch. Authentication, initial parsing and exact fallback replay remain required independently of transport Est.

TrojanServer and VlessServer acquire their locally owned credential snapshots before protected or fallback Init. Authentication rejection/unavailability retain their existing fallback policy. Heap allocation for local snapshots and configured local users follows the same fatal policy as shared line-identity and AuthenticationClient profile copies; see the fail-fast publication contract.

After selecting and initializing the TCP/fallback branch, ordinary synchronous body/reply forwarding needs no queue solely for Pause or Est. Preserve older body or replay bytes across reentrant branch Init; this is a protocol-ordering barrier, not permission to maintain permanent TCP transport queues. A local fallback may send Payload and Finish within Init without ever sending Est.

Authenticated TCP on the normal next branch permanently bypasses TrojanServer at the pump's quiescent exit: onward Init has returned and retained body and reentrant input have drained. TCP Pause/Resume is forwarded transparently, including duplicates. A receiver Pause before branch selection is remembered and replayed onto the initialized branch; it does not gate TCP output. Destroy and zero the server's state before clearing its active bit. Credentials already published on the line remain available to later nodes. This transition does not wait for Est or require either direction to be unpaused. UDP stays active for framing and owned-backend cleanup. Fallback also stays active, because its selected branch and possible delayed replay cannot be replaced by following next.

Trojan UDP backend identity is the original address kind, exact address/domain bytes and length, and port. Hashes select buckets; full equality selects a backend. The remote owns an immutable key borrowed by the client map, and detaches it before freeing it. Routing/DNS rewrites cannot change lookup/removal identity; wildcard IPv4 and IPv6 remain distinct, and domain spelling and case are preserved.

UDP is an association junction. Upstream callbacks operate on the borrowed client; backend upstream events use each owned backend, and replies/notifications return through downstream callbacks. Aggregate backend pressure by exact membership: one backend's Resume cannot release a carrier still paused by another backend. Client reply pressure applies to every initialized backend, including new backends. Publish each contribution before callbacks, clear it on removal, and restart traversal after callbacks rather than retaining raw sibling pointers or iterators.

Pause stops future carrier admission, not completion of the carrier input already being decoded. Each complete datagram in that active input is submitted to its selected backend even if an earlier datagram caused Pause; the backend's receiver owns bounded in-flight tolerance. Preserve framing/destination checks, initial input ordering and the incomplete suffix. Stop immediately if the affected line or association closes. Do not drain unrelated pending input as part of this batch.

Replies received on initialized live backends do not wait for backend Est. They may forward synchronously with the required datagram framing. If independent backend producers or reentry require an association FIFO, keep one bounded encoded FIFO, account entries before callbacks, and stop its independent drain at consumer Pause. Already-admitted immutable replies belong to the client FIFO and do not need a borrowed backend pointer kept alive merely as an Est waiter.

Intentional fallback delay owns a bounded FIFO and scheduled drain. Due replay does not wait for Resume; both directions relay advisory Pause/Resume, including duplicates, along the selected branch. Source Finish permits at most one synchronous final batch outside branch Init, including during advisory Pause, with the finished side closed before any callback. Returned callbacks cannot revive parsing or reopen the finished direction. Init, delay and reentry still preserve the same FIFO order.

Retain the client across association callbacks and each exact backend across its own Init/Payload/permission calls. Close detaches membership and selected-input links before callbacks, releases only locally owned bytes and returns with owned backends logically dead. Source shutdown settles dependent lines and delayed work before reclaiming their reachable resources.

VlessServer association and replay ordering​

VlessServer borrows its client normal line and owns one dependent UDP backend for the initial request's fixed destination. Publish destination, credentials and association links before backend Init; serialize nested input behind older parser bytes. Hold the client across backend callbacks and each exact backend across its own callbacks. Backend death can leave the client alive and permit recreation; client Est is still sent only once. Settle any extracted, undelivered body if Init fails. Replay client receiver pressure onto new/recreated backends before delivery. Backend source pressure is separate from client receiver pressure. Publish the current backend's contribution and the emitted source hold before callbacks; duplicate signals do not add holds or releases. Detach clears the contribution before callbacks. After the old backend is logically dead, release a previously sent hold on a surviving client, retaining that client independently through notification. Resume may close the client or install a paused replacement; never write stale association state afterward. Client teardown and shutdown suppress source restart.

TCP and fallback forward every Pause/Resume transparently. Before branch selection, remember the latest receiver signal for replay onto the initialized branch; update that startup state during Init reentry so a newer Resume cancels stale Pause. UDP retains receiver pressure for backend recreation and response draining.

Every same-line TCP and fallback Est is forwarded promptly, including repeats; an owned UDP backend still maps readiness to its application once. Est does not emit a standalone VLESS response header. The header accompanies the first nonempty TCP reply or first framed UDP reply in one ordinary buffer, including replies arriving before Est. Empty UDP datagrams are valid; an empty first UDP reply emits the response header and zero-length prefix together as 00 00 00 00. Empty TCP input, Resume and Finish do not trigger a header-only send, and there is no header-only timer. TCP reply draining proceeds through advisory Pause; UDP retains its response-pressure gate. Publish header ownership before callbacks so Est and reply reentry cannot duplicate the header or reorder replies.

TCP Init and unauthenticated fallback selection secure all older body/replay ownership before outward callbacks. Fallback restores inspected metadata before its untouched tail and gates every drain during branch Init. Delayed replay does not wait for Resume; the narrow source-Finish batch runs only outside Init, with the previous side already closed. Source-owner shutdown settles parser storage, delayed work and the owned backend while pools remain alive; VlessServer adds no inventory of borrowed lines. See Part 3 for representation and retention limits.

Authenticated TCP on the normal next branch permanently bypasses VlessServer after the first response header/body handoff, once branch initialization, retained input and response queues have settled. Destroy and zero local state before clearing the active bit. Est alone or an empty reply does not complete this transition. Transparent TCP Resume forwarding does not start a reply drain or require a later permission-reconciliation step. Credentials already published on the line remain available to later nodes. UDP and fallback remain active for framing, owned-backend cleanup, branch selection and delayed replay.

VmessClient bootstrap and association ownership​

VmessClient borrows direct TCP and UDP application normal lines. For each UDP application, VmessClient creates one dependent TCP carrier on the same worker and is that carrier's exact-line head and owner. Initialize both states and publish both association links before next Init. Snapshot the final target before transport nodes replace destination context with the VMess server endpoint. Optional DomainResolver is an owned helper before the client and applies that target before local DNS. The outbound UUID is not an authenticated local-peer identity and must not create duplicate managed accounting on the carrier.

Init emits no request. The first eligible payload may precede transport Est, reenter Est or arrive after advisory Pause; it sends immediately without awaiting a response. Publish request ownership, expected response marker and both codec contexts before the first outward Payload. Direct TCP forwards every Est promptly, including repeats; UDP maps the carrier's first Est once. The first Est sets one idle deadline. Repeated Est never resets it. Direct TCP due output proceeds through advisory Pause; an owned UDP carrier defers only its idle header while that exact carrier is paused. Empty TCP and empty native/oversized local UDP do not trigger a request. Packetaddr preserves empty application datagrams through a nonempty address envelope. There is no separate response deadline because a backend can legitimately remain silent.

Independent TX/RX guards preserve each admitted FIFO across nested payloads and synchronous responses. Drain complete records incrementally through advisory Pause, stopping on closure. Keep the client active for the whole connection, including NONE and zero; no successful-TCP permanent bypass applies. UDP records preserve datagram boundaries, and authenticated empty records terminate only after tag verification.

Finish publishes the sender's closed side before any outward callback, detaches associations, cancels timers, settles buffers and wipes session material. Protocol termination may go only toward an open wire side after request submission when send-termination is true (the default). False omits that record and uses the same full-close path, including strict counter exhaustion. Incoming valid EOF remains accepted independently. Finish never starts a request. Never reflect Finish or pressure toward its closed sender. An owned carrier receiving Finish must be logically dead before returning, including when Finish reenters final-record handoff. Temporary references preserve allocation; recheck logical life and association membership after callbacks. Borrowed application lines remain their source owner's responsibility during normal close and shutdown.

VmessServer authentication and association ownership​

VmessServer borrows its carrier and owns its dependent UDP backend normal lines. Publish verified destination and identity before protected Init; managed mode inserts UserController on decoded traffic, while fallback bypasses that helper. TLS and WebSocket remain separate preceding nodes and require no VMess chain-introspection setting.

Native UDP keeps one fixed-destination backend. Authenticated packetaddr UDP uses a bounded per-carrier associative table of immutable IP/family/port destinations: packetaddr-max-destinations defaults to 64 and accepts 1..1024. Each live entry owns one same-worker normal backend, with copied source/listener context and authenticated identity published before protected Init. Managed UserController sees each decoded application datagram once; envelope/carrier bytes are not separate application traffic. At capacity, drop only a datagram requiring a new destination. An exact backend Finish removes its entry, permitting later recreation while admission remains open. Replies carry that backend's immutable target and queued reply bytes remain independently owned.

Publish table removal, inventory removal and association detachment before callbacks. Recheck logical life and exact membership after reentry; references preserve allocation only. Carrier closure closes backend admission and drains every owned entry; worker stop also drains the separate owner inventory before pools and chains are reclaimed. Pressure relays retain the existing exact-line head/tail roles and finite response FIFO; packetaddr does not grant ordinary transforms a new producer gate. All these lines are normal lines, and only their creators destroy them.

The server authenticates/parses the request before applying allowed-security, UDP and packetaddr permissions, and must enforce them before protected Init or backend creation. Denied authenticated requests close without fallback. The body allow-list classifies chunked NONE as none and unframed NONE as zero; it introduces no new wire mode or header-authentication mechanism. Mux.Cool/XUDP remain unsupported; WaterWall's custom Mux is not an interoperable substitute.

A node shares one immutable, reference-counted vector of copied UUID identities and derived keys across workers. Build it from generation-checked AuthenticationClient pages outside broad credential locks. Pending searches pin the vector, use one worker-owned trial cipher context, and yield through a coalesced worker pump after 256 trials or one millisecond. Ordinary ready continuations have no deliberate timer delay or throughput rate limit. A stale-vector miss must reconsider an installed replacement before selecting fallback; successful header verification must revalidate UUID ownership, durable ID, identity incarnation and current eligibility before publishing the handle. Account refresh must not reset replay protection.

AuthID and session replay tables are shared by all workers of one VmessServer instance, with full-key equality and atomic admission. Keep AuthID marks through the entire inclusive 120-second past/future window, including the future edge. A nondecreasing wall-clock cutoff prevents a backward correction from reviving purged timestamps. Session marks retain UUID/key/IV identity for three monotonic minutes. Capacity refusal must never evict live protection. Separate node instances have independent replay domains.

Authentication admission counts pending carriers, retained input and all live credential-vector allocations, including construction and pinned old generations. The absolute handshake deadline starts at Init and input never extends it. Quiescence closes admission and detaches worker timers; queued work settles under normal worker-message ownership. Worker stop closes pending carriers through their owners and releases construction state. Ordinary source-owner drain settles established carriers and dependent UDP lines before pools, helpers or shared state are reclaimed.

HttpProxyServer fallback association​

HttpProxyServer borrows the client normal line and owns a separate child. Every child callback uses that child's selected branch entry, including pressure and Finish; helper insertion must not be bypassed. Detach the child, branch entry and worker inventory before closing it, and destroy that exact owned line before returning from its Finish handler. Retain the session and exact lines across reentrant calls; physical references do not keep associations logically alive.

Publish original replay, working input and deferred tails in FIFO order before fallback Init. An active Payload suffix must be admitted before Init or outward pressure can reenter. No drain or pressure notification enters unfinished Init. Record receiver Pause/Resume before forwarding, even before branch selection; reconcile the latest state after Init, without waiting for Est. Nested Resume cancels earlier Pause, and independent backlog pressure still applies. Child Est maps promptly and once to the borrowed client, independently of replay or Pause. Only the first Est for each child clears its connect deadline and records initial progress. Repeated notifications are not data progress and must not extend the session's idle deadline. A child may send raw Payload and Finish inside Init without Est: retain its reply, discard upstream work, destroy the child, and drain accepted client output before Finish. Never reflect callbacks into that finished child.

In both directions, publish the active Payload remainder before a streaming slice can call out. Nested input appends after that remainder and shares the existing delivery allowance; it must not enter a FIFO that drains before the older suffix. Close and early final-response handling settle active input as well as queued input. Raw fallback and CONNECT relay drain each direction's retained buffers directly, without needing spare working capacity from the opposite direction. Child Finish preserves accepted response bytes, including an active remainder, until the client can receive them; client Finish discards them immediately.

Worked Example: Tracing a Payload​

Consider the chain:

TcpListener -> ObfuscatorClient -> TlsClient -> TcpConnector
(head) (middle) (middle) (tail)

Upstream (client to remote):

client bytes arrive at TcpListener's socket
TcpListener: tunnelNextUpStreamPayload(t, line, buf)
-> ObfuscatorClient.fnPayloadU: scramble bytes, forward upstream
-> TlsClient.fnPayloadU: encrypt via BoringSSL, forward upstream
-> TcpConnector.fnPayloadU: write ciphertext to the remote socket

Downstream (remote to client):

remote bytes arrive at TcpConnector's socket
TcpConnector: tunnelPrevDownStreamPayload(t, line, buf)
-> TlsClient.fnPayloadD: decrypt, forward downstream
-> ObfuscatorClient.fnPayloadD: unscramble, forward downstream
-> TcpListener.fnPayloadD: write plaintext to the accepted client

Notice that the same TlsClient instance handles encrypt on fnPayloadU and decrypt on fnPayloadD. The direction, not the tunnel, decides the transformation. If TlsClient's upstream payload write blocks at TcpConnector, backpressure flows back as a Pause in the downstream direction — and must not be reflected past a side that has already finished.

Checklist for This Part​

Before you call a flow change done:

  • Does Init initialize this tunnel's line state before any callback can use it?
  • Does a normal-line context selector or rewriter complete and publish its own context/admission decision before propagating Init to the selected stage?
  • Are upstream callbacks forwarded only with tunnelNextUpStream*, and downstream only with tunnelPrevDownStream*?
  • Does every re-entrant call either return immediately or protect the line with lineCallWithRef() / manual lineRef() + lineIsAlive()?
  • On a false from lineCallWithRef(), do you avoid touching line, ls, and LinestateDestroy()?
  • Does Finish destroy local state before propagating the real close?
  • When closing from the middle, do you finish the noncreator side before the creator side, then return?
  • If you send final protocol bytes during Finish, do you mark the sender side finished before the send and block reflected Pause/Resume?
  • Are Pause/Resume repeatable and transparent in ordinary middle nodes, with due output independent of Resume? Do adapters, exact-line endpoints, SpeedLimit and bandwidth-enabled UserController retain producer control? Are local advisory holds released without relying on more input, and are FIFO, receiver limits and overflow/close behavior explicit?
  • Does retained data have a parser, protocol, ordering or producer obligation, rather than an unnecessary output queue copied into a transparent transform?
  • Does only the line owner ever call lineDestroy()?
  • Did you classify each line as owned normal, borrowed normal, or packet before editing its Finish path?
  • Does every owner Finish path for a normal line return with ! lineIsAlive(line) — including the error, timeout, and terminal-verdict branches?
  • Are io callbacks, timers, idle items, and map entries detached before the owner's state and line are destroyed?
  • Does a borrowed-line path avoid lineDestroy() entirely?
  • Is requestProgramShutdown() used in addition to closing an owned line, never instead of it?
  • Did you avoid adding an initialized flag the source does not require?

HttpProxyClient single exchange​

HttpProxyClient borrows one normal line from its source and creates no child line. An optional owned DomainResolver tunnel precedes the client core on that same line. Origin DNS completes before core Init, client timers, or next Init begin; early input stays in the resolver FIFO. DNS uses the service timeout/retry policy, not the client idle deadline. Failed DNS finishes only the initialized source. Core Init starts idle accounting, including subsequent proxy DNS/connect waiting. CONNECT response timing starts at header handoff; incomplete header timing starts at its first byte. Paused time counts after these deadlines start. Init snapshots the origin before next Init can replace destination context, then hands off its complete request header after next Init returns. An Init/header guard keeps reentrant application data behind that header. Est forwards promptly, including repeats, independently of HTTP readiness. CONNECT releases application input only after a 2xx reply; normal HTTP uploads need not wait for the response.

Pause/Resume relay transparently, including duplicates. Ready headers, upload and response backlogs, and due timer work do not wait for Resume; protocol and reentrancy barriers still preserve order. The client's own startup/FIFO Pause has an independent release path. CONNECT remains active after acceptance because the configured idle deadline still observes traffic and closes an idle stream; permanent bypass would discard that obligation. HTTP mode also retains framing and response-completion work.

Fixed/bodyless request completion leaves the response live. A complete framed response closes both directions and cancels upload, including a suspended chunk suffix; trailing bytes never start another exchange. Downstream Finish completes only close-delimited responses or an established CONNECT stream; other incomplete responses are truncated. The borrowed line is always destroyed by its source. Chunked source Finish can emit one terminal chunk only with no older backlog or active frame, a sent request header and normal loop admission, before response completion. Its final-byte window suppresses reflection and duplicate next Finish. It then destroys local state and propagates Finish: it cannot wait for a response after source Finish. There is no separate upload-completion callback, and an empty Payload does not finish the body. A terminal HTTP chunk would suffice to end a request without TCP half-close, but Finish cannot represent that send-only completion. Finite uploads that need a later response must use a known Content-Length; chunked exchanges require peers that respond while the upload remains open. See the HttpProxyClient completion limitation. Worker quiescence unlinks the timer registry; it is not another borrowed-line inventory.


Continue to Part 3: Buffers, Padding & Shift Buffers.

Packet/connection bridge ownership​

PTC owns the normal lines it creates on the delivered packet worker. CTP borrows the preceding source's normal line and owns only its PCB/local state. Both use chain-owned persistent packet lines, which neither bridge nor engine destroys. The runtime owns each worker engine beyond individual line Finish.

lwIP callbacks execute on that engine's event worker. Normal-line callbacks and arbitrary packet neighbours remain queued: a neighbour must not recursively inject while stack input scratch is active. PTC alone may deliver directly on its owner packet line to a reciprocal immediate L3 sink advertising kNodeFlagPacketPayloadEnqueueOnly. The sink may validate, enqueue without waiting for capacity, or drop; it must not enter an engine, pump a loop, invoke another tunnel callback, write the native device, or destroy the packet line. TunDevice advertises this contract on all supported platforms independently of GSO/checksum mode. Its payload callback appends to the submitting event worker's bounded output FIFO and schedules one deferred flush on that worker. Native writes and buffer recycling happen in that later callback, outside the lwIP entry scope. Topology and capability stay fixed, so queued and inline output cannot alternate and overtake on a route. Node worker drain detaches CTP's PCB, registration and deadline while preserving borrowed state, pending buffers and references until its true source sends Finish, regardless of node storage order.

Trusted TUN/PTC transport checksums do not change line ownership, repair-request consumption, callback direction, or receive credit. Device assurance stays in private FIFO work until publication on the chain-owned packet line; generated normal lines remain owned and drained by PTC.