| English | 中文版 |
RFC Number: NPS-RFC-0001
Title: Add NCP connection preamble for native-mode traffic identification
Status: Accepted (Phase 1 — spec + .NET reference implementation landed)
Author(s): Ori Lynn iamzerolin@gmail.com (LabAcacia)
Shepherd: Ori Lynn (pre-1.0 fast-track per spec/cr/README.md)
Created: 2026-04-21
Last-Updated: 2026-04-25
Accepted: 2026-04-25 (pre-1.0 fast-track; see spec/cr/README.md)
Activated: (set when first reference SDK ships, target v1.0-alpha.3)
Supersedes: none
Superseded-By: none
Affected Specs: NPS-1 NCP, spec/error-codes.md, spec/status-codes.md
Affected SDKs: .NET, Python, TypeScript, Java, Rust, Go
—
Define an 8-byte constant preamble b"NPS/1.0\n" that every NCP native
mode client MUST send once, immediately after the transport (TCP / QUIC)
handshake and before its first HelloFrame. Servers that see any other
byte sequence in those 8 bytes close the connection without emitting an
ErrorFrame. HTTP mode is not affected. Adds one error code
(NCP-PREAMBLE-INVALID) and one status code (NPS-PROTO-PREAMBLE-INVALID).
NCP native mode currently relies on HelloFrame (0x06) as the
first-byte-after-handshake signal. This works for well-behaved peers but
has two operational weaknesses:
Misrouted traffic is hard to reject cheaply. If a non-NPS client
(HTTP/1.x probe, Redis client, port scanner, misconfigured reverse
proxy) hits the native-mode port, the server parses the first byte as
a frame type, reads a bogus length field, then eventually fails on a
downstream parse. A constant preamble lets the server reject in the
first read(8) with zero frame-parser exposure.
Major-version bumps have no wire-level gate. An NPS 2.0 client
connecting to a 1.x server gets a CapsFrame negotiation failure
mid-handshake instead of a clean “wrong protocol version” rejection
before any frame parsing happens. Embedding the major version in the
preamble gives us a cheap, explicit compatibility check at the byte
level.
This also answers a review comment from 2026-04-20 arguing NCP “needs a magic code to handle TCP packet-boundary ambiguity”. The commenter conflated two concerns (length-prefix framing vs. traffic identification) — NCP’s length-prefixed header already handles framing (see §5.2), but connection-level traffic identification is a legitimate separate concern that this RFC addresses.
This RFC does NOT:
Content-Type: application/nwp-frame.HelloFrame or CapsFrame negotiation semantics.ALPN string selection. ALPN is an orthogonal discovery
mechanism; a future RFC may mandate an ALPN token like nps/1 for
native mode over QUIC, but that is not this RFC.Preamble byte sequence (sent once per connection, before any frame):
Offset 0 1 2 3 4 5 6 7
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ N │ P │ S │ / │ 1 │ . │ 0 │\n │
└───┴───┴───┴───┴───┴───┴───┴───┘
0x4E 50 53 2F 31 2E 30 0A
8 bytes, ASCII, ends with 0x0A (LF). Hex: 4E 50 53 2F 31 2E 30 0A.
Why ASCII + LF:
tcpdump/Wireshark show it as NPS/1.0\n inline — zero ambiguity in
packet captures.N (0x4E) does not collide with any assigned NCP frame
type (AnchorFrame 0x01..ErrorFrame 0xFE — the 0x40–0x4F range
is currently used for NWP frames 0x41–0x43, so 0x4E is
Reserved; §4.3 formalizes the reservation).\n gives line-oriented protocol inspectors a clean stop.HTTP/1.1 starts with HEAD/GET /POST etc. — NPS/ cannot be
parsed as a valid HTTP request line by any compliant HTTP parser.HTTP/2 preamble is PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n (24 bytes,
starts with PRI ) — distinct.Handshake flow (native mode):
Client Server
│ │
│──── TCP/QUIC handshake ───────────────────── │
│ │
│──── 8 bytes "NPS/1.0\n" (preamble) ──────→ │
│ │ validate
│ │ (close on mismatch,
│ │ no ErrorFrame)
│──── HelloFrame (0x06) ────────────────────→ │
│ │
│ ←─── CapsFrame (0x04) ────────────────────── │
│ │
│──── application frames ←→ ───────────────── │
The preamble is not a frame — it has no header, no Flags, no length field. It is raw constant bytes.
Server validation rules:
b"NPS/1.0\n", proceed to frame parsing.ErrorFrame (the peer is not known to
speak NCP; an ErrorFrame write would leak framing details to
scanners).Client behavior:
HelloFrame back-to-back with the preamble
(no round-trip wait), since server validates byte-by-byte against a
constant.Future major-version semantics:
b"NPS/2.0\n" etc. are reserved for future major versions.NPS/2.0\n MUST close the connection
(unknown major), MAY write a one-time 32-byte diagnostic line
NPS-PREAMBLE-UNSUPPORTED-VERSION\n before closing. This is
allowed because the peer has self-identified as an NPS speaker.0 in 1.0) is reserved; v1 clients MUST send
0 and v1 servers MUST accept 0 only. A future RFC may define
minor-version negotiation.None. The preamble is transport-level, below any NWM surface.
New error code in spec/error-codes.md:
| Error Code | NPS Status | Description |
|---|---|---|
NCP-PREAMBLE-INVALID |
NPS-PROTO-PREAMBLE-INVALID |
Client sent invalid or malformed preamble; connection closed without frame-level response |
New status code in spec/status-codes.md:
| NPS Status | HTTP Mapping | Description |
|---|---|---|
NPS-PROTO-PREAMBLE-INVALID |
400 Bad Request (not emitted; native-mode only) |
Native-mode preamble mismatch |
Note: this status is never transmitted on the wire because the server closes silently. It exists so that SDK-internal telemetry (logs, metrics) can classify the close reason consistently.
Frame-type namespace reservation in spec/frame-registry.yaml:
Add a note that the byte value 0x4E (ASCII N), when seen as the
first byte of a native-mode connection, is interpreted as the start of
the preamble, not as a frame type. Frame type 0x4E MUST NOT be
assigned to any NCP frame.
Native-mode connection state (server side):
[LISTEN] ──accept──→ [PREAMBLE-WAIT] ──8 bytes match──→ [FRAMING]
│ │
│──mismatch──→ [CLOSING] │
│──timeout(10s)──→ [CLOSING] │
│
[FRAMING] ──────→ [HANDSHAKE]
HelloFrame
│
[HANDSHAKE] ────→ [ESTABLISHED]
CapsFrame sent
Timeouts:
PREAMBLE-WAIT → CLOSING: 10 seconds (covers slow links + TLS
handshake tail; aggressive enough to defend against slowloris).CLOSING: close within 500 ms after decision.NCP-FRAME-UNKNOWN-TYPE from old servers.NCP-PREAMBLE-INVALID in its own logs.min_agent_version need to be raised? Yes. Bump to the
first version that implements this RFC — 21-day window applies.Breaking-change rationale: native mode is still Phase 2+ per
spec/NPS-Roadmap.md; no GA shipments depend on native mode yet.
Taking the break now is cheaper than retrofitting after 1.0 GA.
Prefix every frame with a 2-byte magic (e.g. 0x4E 0x50).
Keep the current design: first frame is HelloFrame (0x06); server
parses it; non-NPS traffic fails on frame parse.
E.g., 0x89 4E 50 53 (PNG-style high-bit + “NPS”).
tcpdump; loses the “HTTP parser
rejects immediately” property.Rely on TLS ALPN (nps/1) for protocol identification; no in-band
preamble.
nps/1 ALPN for TLS-over-QUIC/TCP native mode as a
complement to the preamble. Defense in depth.memcmp-equivalent,
no dynamic allocation, no length-field parsing, no unicode handling.
Strictly easier to audit than the existing frame header parser.PREAMBLE-WAIT timeout.| Phase | Scope | Exit criterion |
|---|---|---|
| 1 | Spec merged + .NET reference implementation behind NpsNativeOptions.RequirePreamble flag (default false) |
Unit tests green; cross-version interop test (preamble-aware client ↔ preamble-aware server) |
| 2 | All 6 SDKs implement with flag default false | Cross-SDK interop matrix green; at least one SDK’s native-mode sample runs end-to-end with preamble on both sides |
| 3 | Flag default flips to true across SDKs; release notes call out the break | No open regressions for 1 release cycle; min_agent_version bumped with 21-day deprecation window |
| 4 | Remove the flag — preamble mandatory | Flag removal PR lands in each SDK; docs updated |
| SDK | Owner | Status | Notes |
|---|---|---|---|
| .NET | Ori Lynn | pending | Primary reference; lands first |
| Python | TBD | pending | |
| TypeScript | TBD | pending | Browser: native mode not applicable; Node.js only |
| Java | TBD | pending | |
| Rust | TBD | pending | |
| Go | TBD | pending |
New tests that land with this RFC:
NPS/1.0\n + HelloFrame;
server accepts, returns CapsFrame."GET / HTT", all-zero, NPS/2.0\n); server closes within 500 ms
with no frame response.NPS/2.0\n; server MAY
write NPS-PREAMBLE-UNSUPPORTED-VERSION\n diagnostic before close
(test asserts close happens, diagnostic is optional).Existing test changes:
Cross-SDK interop:
No new benchmark needed. Wire-size impact is 8 bytes per connection, amortized to zero for any connection that exchanges >1 frame. Latency impact is zero because client can pipeline preamble + HelloFrame in a single write.
If a future measurement shows the 10-second PREAMBLE-WAIT timeout
blocks >0.1% of legitimate connections, reopen this RFC’s open
question #2.
None yet. An experimental branch will be attached before moving to
Accepted. Target measurements:
| Metric | Baseline | Proposed | Delta | Method |
|---|---|---|---|---|
| Per-connection wire overhead | 0 bytes | 8 bytes | +8 B | trivial |
| Handshake RTT (localhost loopback) | TBD | TBD | target: no regression | .NET BenchmarkDotNet on native-mode loopback |
| Server-side cost to reject non-NPS scan | full frame-parse attempt | 8-byte memcmp |
target: ≥10× cheaper | scan-simulation harness |
HelloFrame? Shepherd decision needed.
Default position: no — HelloFrame already carries full capability
declaration; preamble stays minimal. Target: resolved before
Accepted.PREAMBLE-WAIT timeout of 10 s — should this be
configurable? Owner: Ori Lynn. Target: default stays 10 s, SDK MAY
expose a knob; resolved by adding a note to this RFC before
Accepted.nps/1 for native mode
over TLS-enabled transports. Complements the in-band preamble (§5.4).0).| Date | Author | Change |
|---|---|---|
| 2026-04-21 | Ori Lynn | Initial draft |
| 2026-04-25 | Ori Lynn | Accepted via pre-1.0 fast-track. Spec changes landed: §2.6.1 in NPS-1-NCP, error code NCP-PREAMBLE-INVALID, status code NPS-PROTO-PREAMBLE-INVALID, 0x4E reservation in frame-registry.yaml. Phase 1 .NET reference helpers (NPS.Core.Ncp.NcpPreamble) landed alongside; Phase 2 (other 5 SDKs) and Phase 3 (default-on flip) deferred per RFC §8.1. |