hzz docs netty-evaluation.md
9.5 kB · 142 lines
Markdown
at main

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 #

  1. Explicitly launch dev.hzz.streamer.debug/tv.streamer.spike.webserver.WebServerHarnessActivity. Launching, observing, rotation and navigation never start a listener.
  2. While stopped, choose Start CIO diagnostic — loopback (default) or Start Netty evaluation — loopback. No preference is saved; live switching is disabled.
  3. 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.
  4. 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.
  5. 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.jar SHA-256 a5002698666cc7b5ea9e5543e680cd7552e3afe14652b600fcead6e07cb26a3f
  • https://repo.maven.apache.org/maven2/io/netty/netty-codec-http/4.1.111.Final/netty-codec-http-4.1.111.Final-sources.jar SHA-256 ac9f37dc9ec3808551d5fca59f5fab401787c6f170482523d03ea010ebb0330d

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.