Debug-only Netty evaluation #
Status and scope #
The user explicitly approved evaluating Netty instead of maintaining a CIO fork (decision D5). The evaluation passed independent security and lifecycle/build reviews with loopback quarantine retained. It is landed and installed. Native Streamer loopback checks passed after fixing a shared protocol regex rejected by Android; see device evidence.
This is not production engine selection, full M3, or the M0.5 paired-phone/standby pass.
Production :webserver and normal HOME startup remain unchanged. No LAN/tailnet listener,
proxy, forwarding or tunneling workaround is enabled. CIO's separate known-failure tests remain.
Operator procedure #
- Explicitly launch
dev.hzz.streamer.debug/tv.streamer.spike.webserver.WebServerHarnessActivity. Launching, observing, rotation and navigation never start a listener. - While stopped, choose Start CIO diagnostic — loopback (default) or Start Netty evaluation — loopback. No preference is saved; live switching is disabled.
- Confirm selected engine, RUNNING and actual port. Only the same device can reach
http://127.0.0.1:<actual-port>/. No phone URL exists yet. - The private screen shows a single-use pairing code. Do not capture codes, cookies or private endpoints. Pairing/session rules are in the lab protocol. Loopback requests remain read-only except pairing; authenticated Probe/Revoke return 403.
- Navigate away/back to observe process ownership. End every experiment with Stop. The engine identity remains visible; port becomes none and sessions are invalidated.
A single maximum 15-minute run timer remains separate from transport timers. Android suspend can defer it. Events distinguish OPERATOR_STOP from RUN_LIFETIME_EXPIRED, not inferred process loss. Private display is unsaved and FLAG_SECURE; bounded message-free startup diagnosis is available without reading the pairing code (see native investigation).
Fixed-length request policy #
Netty requests accept HTTP/1.1 with fixed Content-Length only, maximum 128 bytes. Methods other than GET/HEAD require explicit length; zero is allowed. GET/HEAD can omit length. Known-byte-length browser JSON POSTs work. Streaming uploads are not supported by this lab. Future configuration/import APIs need a separately reviewed payload policy; the lab is not those APIs.
Every initial Transfer-Encoding field is rejected before normalization and before chunk-body parsing: chunked, identity, combinations, duplicates and mixed case. This deliberately avoids pinned Netty's permissive chunk delimiter/extension handling and dropped forbidden trailers. It does not certify Netty's chunk parser. Server chunked responses/SSE remain supported.
Public upstream decoder hooks reject duplicate/comma/noncanonical/overflowing CL, CL+TE, Content-Encoding, Expect, Upgrade, duplicate Host and unsupported HTTP versions before dispatch. The complete-body barrier retains at most 128 bytes and forwards only complete valid requests. Initial lines are bounded to 1024 bytes and headers to 4096. Header whitespace/unfolding remains upstream behavior, not a locally written HTTP parser or a reject-all-obs-fold claim.
Standalone rejected framing emits one empty 400, then closes. This differs from protocol-level JSON errors. Invalid overlapping requests close without appending a response to a predecessor that may already be committed. No raw request text or exception message is reflected.
Transport phases and resource scope #
- Engine-shared admission permits eight active accepted channels; excess channels close without waiting. Admission occurs before reads but after framework child-handler allocation: this is not a hard OS backlog, transient allocation, total-memory or flood-resistance guarantee.
- One connection, worker and call thread is configured for Netty groups. Application work also uses shared Dispatchers.Default; three threads is not a whole-process thread cap.
- An absolute five-second first-header timer begins at activation. Accepted headers replace it with an absolute five-second incomplete-body timer. Trickle bytes never renew either.
- Active responses, including SSE, have no short header/idle timer. Only successful terminal LastHttpContent write completion starts the five-second next-header/idle deadline; headers, flush and write enqueue are not completion. Subsequent partial headers do not renew it.
- One in-flight request and zero pending decoded requests are allowed. Overlap closes the connection without extra response/dispatch; the preceding response may be truncated. Sequential keepalive works; general HTTP pipelining is intentionally unsupported.
- Expiry closes without HTTP output. Rejection, teardown and handler removal cancel timers. Channel closure cancels the structured application call scope, including suspended streams.
- Stop closes admission before stopping the engine. Cancellation/failed bind disposes candidates; old expiry callbacks cannot stop a replacement engine. Process commands retain bounded, ordered Stop/Start handling. No Activity context is retained.
Destination and pairing boundary #
BrowserProtocol uses call.request.local.localAddress and localPort, never Host-derived
serverHost, remote IP or forwarding headers. Inspection of pinned NettyConnectionPoint source
and bytecode confirms these come from channel.localAddress(). Real-channel tests compare
those values directly. Exact numeric destination/Host/Origin checks and session rules remain.
Actual tailnet-positive behavior is not established by loopback tests.
Hard runtime guards reject network mode through transport, fixed-port seam, owner, singleton and constructed commands, both stopped and running. No wildcard bind exists. Cookies are host/path-, not port-scoped; this remains a trusted same-device lab, not isolation from local apps.
Dependencies and release isolation #
Added ktor-server-netty using the existing Ktor 2.3.12 pin; no existing version changed.
The resolved debug graph selects Netty 4.1.111.Final, Kotlin 2.0.21 and coroutines 1.9.0.
Public source archives inspected (not copied into the repository):
https://repo.maven.apache.org/maven2/io/ktor/ktor-server-netty-jvm/2.3.12/ktor-server-netty-jvm-2.3.12-sources.jarSHA-256a5002698666cc7b5ea9e5543e680cd7552e3afe14652b600fcead6e07cb26a3fhttps://repo.maven.apache.org/maven2/io/netty/netty-codec-http/4.1.111.Final/netty-codec-http-4.1.111.Final-sources.jarSHA-256ac9f37dc9ec3808551d5fca59f5fab401787c6f170482523d03ea010ebb0330d
Ktor's public channelPipelineConfig and Netty HttpDecoderConfig/headers factories, decoder,
encoder and channel handlers implement the boundary. No parser fork, proxy or private reflection.
Ktor expects the replacement codec to retain the pipeline name codec; tests caught and fixed
that integration detail.
AGP's public debug-variant API merges exactly META-INF/io.netty.versions.properties, preserving
all 13 modules' versions/build/commit metadata. No blanket META-INF exclusion or global merge.
The APK verifier checks debug presence, metadata, and release runtime/DEX/component/resource
absence of experimental Netty and the harness. Shared production Ktor CIO remains present.
No Netty native executable library was added. Host packaging alone did not prove ART compatibility.
Verification #
Real loopback tests—not just mocked Ktor routes—cover framing/encoding/header normalization, upgrades/version prefaces, exact 128/129-byte limits, rejected live-code payloads, pipelined trailing bytes, one-400/EOF attribution and unchanged challenge/attempt/session state. Review-discovered malformed chunk/trailer cases now reject at the TE header; ordinary valid chunked requests also reject. Fixed-length pairing/cookies and response SSE continue to pass.
Socket tests cover silent/partial/trickling first and subsequent headers, incomplete bodies, idle keepalive, admission saturation/release, response write-completion timing, overlap closure, later SSE output and disconnect/Stop cancellation without another write. Authentic destination forgery tests, full BrowserProtocol replay and owner bind/restart/TTL/cancellation tests remain. CIO's noncompliance characterization is unchanged and separately labelled.
Debug Activity tests cover explicit engine selection, Start/Stop, rotation/navigation, ordered commands, sanitized failures and runtime network rejection. Native results are separately scoped in the device report; no host result is presented as a phone/standby or minSdk-23 runtime pass.
The coordinator ran the declared gate before/after landing, plus release assembly and APK checks:
ANDROID_HOME=/home/dev/android-sdk ./gradlew test :app:assembleDebug :app:assembleRelease --max-workers=2 --console=plain
ANDROID_HOME=/home/dev/android-sdk python3 spikes/webserver/verify-apk-boundary.py
git diff --check
Next gate #
Review a separate explicit network-enabling change, retaining strict destination/Origin/session rules. Then measure a real paired phone, actual tailnet destination and LAN read-only behavior, revocation, normal launcher use and standby/wake. Tailscale installation/join was requested but has not yet been confirmed. No credential forms or configuration mutations are enabled here.