Files
natsdotnet/gaps/leaf-nodes.md
Joseph Doherty c30e67a69d Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox)
  so NATS client distinguishes data messages from control messages
- Fix MaxAge expiry: add background timer in StreamManager for periodic pruning
- Fix JetStream wire format: Go-compatible anonymous objects with string enums,
  proper offset-based pagination for stream/consumer list APIs
- Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream)
- Add ~1000 parity tests across all subsystems (gaps closure)
- Update gap inventory docs to reflect implementation status
2026-03-12 14:09:23 -04:00

32 KiB

Leaf Nodes — Gap Analysis

This file tracks what has and hasn't been ported from Go to .NET for the Leaf Nodes module. See stillmissing.md for the full LOC comparison across all modules.

LLM Instructions: How to Analyze This Category

Step 1: Read the Go Reference Files

Read each Go source file listed below. For every file:

  1. Extract all exported types (structs, interfaces, type aliases)
  2. Extract all exported methods on those types (receiver functions)
  3. Extract all exported standalone functions
  4. Note key constants, enums, and protocol states
  5. Note important unexported helpers that implement core logic (functions >20 lines)
  6. Pay attention to concurrency patterns (goroutines, mutexes, channels) — these map to different .NET patterns

Step 2: Read the .NET Implementation Files

Read all .cs files in the .NET directories listed below. For each Go symbol found in Step 1:

  1. Search for a matching type, method, or function in .NET
  2. If found, compare the behavior: does it handle the same edge cases? Same error paths?
  3. If partially implemented, note what's missing
  4. If not found, note it as MISSING

Step 3: Cross-Reference Tests

Compare Go test functions against .NET test methods:

  1. For each Go Test* function, check if a corresponding .NET [Fact] or [Theory] exists
  2. Note which test scenarios are covered and which are missing
  3. Check the parity DB (docs/test_parity.db) for existing mappings:
    sqlite3 docs/test_parity.db "SELECT go_test, dotnet_test, confidence FROM test_mappings tm JOIN go_tests gt ON tm.go_test_id=gt.rowid JOIN dotnet_tests dt ON tm.dotnet_test_id=dt.rowid WHERE gt.go_file LIKE '%PATTERN%'"
    

Step 4: Classify Each Item

Use these status values:

Status Meaning
PORTED Equivalent exists in .NET with matching behavior
PARTIAL .NET implementation exists but is incomplete (missing edge cases, error handling, or features)
MISSING No .NET equivalent found — needs to be ported
NOT_APPLICABLE Go-specific pattern that doesn't apply to .NET (build tags, platform-specific goroutine tricks, etc.)
DEFERRED Intentionally skipped for now (document why)

Step 5: Fill In the Gap Inventory

Add rows to the Gap Inventory table below. Group by Go source file. Include the Go file and line number so a porting LLM can jump directly to the reference implementation.

Key Porting Notes for Leaf Nodes

  • Leaf nodes only share subscription interest with the hub — no full mesh.
  • Loop detection uses the $LDS. subject prefix.
  • Leaf connections use ClientKind = LEAF.
  • Leaf nodes can connect through WebSocket and support TLS.

Go Reference Files (Source)

  • golang/nats-server/server/leafnode.go — Hub-and-spoke topology for edge deployments (~3,470 lines). Only subscribed subjects shared with hub. Loop detection via $LDS. prefix.

Go Reference Files (Tests)

  • golang/nats-server/server/leafnode_test.go
  • golang/nats-server/server/leafnode_proxy_test.go
  • golang/nats-server/test/leafnode_test.go (integration)

.NET Implementation Files (Source)

  • src/NATS.Server/LeafNodes/ (all files)

.NET Implementation Files (Tests)

  • tests/NATS.Server.Tests/LeafNodes/

Gap Inventory

golang/nats-server/server/leafnode.go

Constants and Types

Go Symbol Go File:Line Status .NET Equivalent Notes
leafnodeTLSInsecureWarning (const) golang/nats-server/server/leafnode.go:47 PORTED src/NATS.Server/LeafNodes/LeafNodeManager.cs Warning logged in DisableLeafConnect; same semantic intent, no separate constant
leafNodeReconnectDelayAfterLoopDetected (const) golang/nats-server/server/leafnode.go:50 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:19, src/NATS.Server/LeafNodes/LeafConnection.cs Delay constant is now consumed by LeafConnection.LeafProcessErr for loop ERR processing. Remaining: reconnect loop still does not schedule by this delay automatically
leafNodeReconnectAfterPermViolation (const) golang/nats-server/server/leafnode.go:54 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:20, src/NATS.Server/LeafNodes/LeafConnection.cs Delay constant is now consumed by LeafPermViolation / LeafSubPermViolation. Remaining: no enforced wait-before-redial in reconnect worker
leafNodeReconnectDelayAfterClusterNameSame (const) golang/nats-server/server/leafnode.go:57 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:21, src/NATS.Server/LeafNodes/LeafConnection.cs Delay constant is now consumed by LeafProcessErr cluster-name path. Remaining: reconnect loop integration not complete
leafNodeLoopDetectionSubjectPrefix (const "$LDS.") golang/nats-server/server/leafnode.go:60 PORTED src/NATS.Server/LeafNodes/LeafLoopDetector.cs:5 LeafLoopPrefix = "$LDS."
leafNodeWSPath (const "/leafnode") golang/nats-server/server/leafnode.go:64 PORTED src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs Path constant is implicit in the WS adapter; not a named constant in .NET
leafNodeWaitBeforeClose (const 5s) golang/nats-server/server/leafnode.go:68 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:22 Constant is defined (LeafNodeWaitBeforeClose = 5s), but close-path wait timer behavior is not yet wired
leaf (unexported struct) golang/nats-server/server/leafnode.go:71 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs LeafConnection now tracks role flags (IsSolicited, IsSpoke, Isolated) and helper predicates. Missing: smap, tsub/tsubt, compression, gwSub, remote cluster/server metadata parity
leafNodeCfg (unexported struct) golang/nats-server/server/leafnode.go:107 PARTIAL src/NATS.Server/Configuration/LeafNodeOptions.cs:7 (RemoteLeafOptions) Added runtime parity fields/helpers (CurrentUrl, TlsName, URL user-info, connect-delay storage, round-robin URL picker). Remaining gaps: perms and JS migrate timer wiring
leafConnectInfo (unexported struct) golang/nats-server/server/leafnode.go:2001 PORTED src/NATS.Server/LeafNodes/LeafConnectInfo.cs CONNECT payload DTO now represented with Go-parity JSON fields (jwt, nkey, sig, hub, cluster, headers, jetstream, compression, remote_account, proto)

Methods on client (receiver functions)

Go Symbol Go File:Line Status .NET Equivalent Notes
(c *client) isSolicitedLeafNode() golang/nats-server/server/leafnode.go:121 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs:29,169 Solicited role is tracked (IsSolicited) and exposed via IsSolicitedLeafNode()
(c *client) isSpokeLeafNode() golang/nats-server/server/leafnode.go:127 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs:35,170 Spoke role is tracked (IsSpoke) and exposed via IsSpokeLeafNode()
(c *client) isHubLeafNode() golang/nats-server/server/leafnode.go:131 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs:171 Hub-role helper implemented as the complement of spoke role (!IsSpoke)
(c *client) isIsolatedLeafNode() golang/nats-server/server/leafnode.go:135 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs:41,172 Isolation flag is tracked (Isolated) and exposed via IsIsolatedLeafNode()
(c *client) sendLeafConnect(clusterName, headers) golang/nats-server/server/leafnode.go:969 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs (SendLeafConnectAsync) Added CONNECT protocol writer that serializes LeafConnectInfo JSON payload and writes CONNECT <json>
(c *client) leafClientHandshakeIfNeeded(remote, opts) golang/nats-server/server/leafnode.go:1402 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs:80 .NET PerformOutboundHandshakeAsync performs the handshake but without TLS negotiation or TLS-first logic
(c *client) processLeafnodeInfo(info) golang/nats-server/server/leafnode.go:1426 MISSING Complex INFO protocol processing (TLS negotiation, compression selection, URL updates, permission updates). Not ported
(c *client) updateLeafNodeURLs(info) golang/nats-server/server/leafnode.go:1711 MISSING Dynamically updates remote URL list from async INFO. Not ported
(c *client) doUpdateLNURLs(cfg, scheme, URLs) golang/nats-server/server/leafnode.go:1732 MISSING Helper for updateLeafNodeURLs. Not ported
(c *client) remoteCluster() golang/nats-server/server/leafnode.go:2235 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs (RemoteCluster) Handshake parser now captures cluster=... attribute and exposes it via RemoteCluster()
(c *client) updateSmap(sub, delta, isLDS) golang/nats-server/server/leafnode.go:2522 MISSING Core subject-map delta updates. .NET has PropagateLocalSubscription but no per-connection smap with refcounting
(c *client) forceAddToSmap(subj) golang/nats-server/server/leafnode.go:2567 MISSING Force-inserts a subject into the smap. Not ported
(c *client) forceRemoveFromSmap(subj) golang/nats-server/server/leafnode.go:2584 MISSING Force-removes a subject from the smap. Not ported
(c *client) sendLeafNodeSubUpdate(key, n) golang/nats-server/server/leafnode.go:2607 PORTED src/NATS.Server/LeafNodes/LeafNodeManager.cs:294 PropagateLocalSubscription / PropagateLocalUnsubscription now mirror send-side parity: spoke subscribe-permission gate (with $LDS./gateway-reply bypass) and queue-weight LS+ emission
(c *client) writeLeafSub(w, key, n) golang/nats-server/server/leafnode.go:2687 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs:135 SendLsPlusAsync now emits LS+ <account> <subject> <queue> <n> for queue subscriptions with weight, and SendLsMinusAsync mirrors LS- framing parity
(c *client) processLeafSub(argo) golang/nats-server/server/leafnode.go:2720 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs:308 Read loop parses LS+ lines including optional queue weight. Missing: loop detection check, permission check, subscription insertion into SubList, route/gateway propagation, and Go-equivalent delta/refcount updates
(c *client) handleLeafNodeLoop(sendErr) golang/nats-server/server/leafnode.go:2860 PARTIAL src/NATS.Server/LeafNodes/LeafLoopDetector.cs:13 IsLooped detects the condition. Missing: sending the error back to remote, closing connection, setting reconnect delay
(c *client) processLeafUnsub(arg) golang/nats-server/server/leafnode.go:2875 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs:200 Read loop parses LS- lines. Missing: SubList removal, route/gateway propagation
(c *client) processLeafHeaderMsgArgs(arg) golang/nats-server/server/leafnode.go:2917 MISSING Parses LMSG header arguments (header size + total size for NATS headers protocol). Not ported
(c *client) processLeafMsgArgs(arg) golang/nats-server/server/leafnode.go:3001 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs:213 .NET read loop parses LMSG lines. Missing: reply indicator (+/`
(c *client) processInboundLeafMsg(msg) golang/nats-server/server/leafnode.go:3072 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:248 ForwardMessageAsync forwards to all connections; inbound path calls _messageSink. Missing: SubList match + fanout to local subscribers, L1 result cache, gateway forwarding
(c *client) leafSubPermViolation(subj) golang/nats-server/server/leafnode.go:3148 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs (LeafSubPermViolation) Added subscription violation handler that applies solicited reconnect delay. Remaining: close/log side effects are not yet mirrored
(c *client) leafPermViolation(pub, subj) golang/nats-server/server/leafnode.go:3155 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs (LeafPermViolation) Added shared violation handler applying permission reconnect delay for solicited links. Remaining: close/log and error emission path not fully ported
(c *client) leafProcessErr(errStr) golang/nats-server/server/leafnode.go:3177 PARTIAL src/NATS.Server/LeafNodes/LeafConnection.cs (LeafProcessErr) Added ERR classifier for permission/loop/cluster-name cases that drives reconnect-delay selection. Remaining: full remote ERR processing and close semantics
(c *client) setLeafConnectDelayIfSoliciting(delay) golang/nats-server/server/leafnode.go:3196 PORTED src/NATS.Server/LeafNodes/LeafConnection.cs (SetLeafConnectDelayIfSoliciting, GetConnectDelay) Solicited-only delay setter/getter implemented and covered by parity tests
(c *client) leafNodeGetTLSConfigForSolicit(remote) golang/nats-server/server/leafnode.go:3215 MISSING Derives TLS config for solicited connection. .NET has no real TLS handshake for leaf nodes
(c *client) leafNodeSolicitWSConnection(opts, rURL, remote) golang/nats-server/server/leafnode.go:3253 PARTIAL src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs WebSocketStreamAdapter adapts a WebSocket to a Stream. Missing: HTTP upgrade negotiation (GET /leafnode request/response), TLS handshake, compression negotiation, no-masking header

Methods on Server

Go Symbol Go File:Line Status .NET Equivalent Notes
(s *Server) solicitLeafNodeRemotes(remotes) golang/nats-server/server/leafnode.go:144 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:200 StartAsync iterates _options.Remotes and spawns ConnectSolicitedWithRetryAsync. Missing: credentials file validation, system account delay, disabled-remote filtering, per-remote NKey/JWT auth
(s *Server) remoteLeafNodeStillValid(remote) golang/nats-server/server/leafnode.go:200 PORTED src/NATS.Server/LeafNodes/LeafNodeManager.cs:102 Implemented remote validity guard (configured in remotes/remoteLeaves and not disabled); retry loop now short-circuits when invalid
(s *Server) updateRemoteLeafNodesTLSConfig(opts) golang/nats-server/server/leafnode.go:432 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:157 UpdateTlsConfig updates cert/key paths. Missing: actual TLS config propagation to existing connections
(s *Server) reConnectToRemoteLeafNode(remote) golang/nats-server/server/leafnode.go:458 PORTED src/NATS.Server/LeafNodes/LeafNodeManager.cs:583 ConnectSolicitedWithRetryAsync implements reconnect loop with exponential backoff
(s *Server) setLeafNodeNonExportedOptions() golang/nats-server/server/leafnode.go:549 NOT_APPLICABLE Sets test-only options (dialTimeout, resolver). .NET uses DI/options; no direct equivalent needed
(s *Server) connectToRemoteLeafNode(remote, firstConnect) golang/nats-server/server/leafnode.go:625 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:583 ConnectSolicitedWithRetryAsync covers basic TCP connect + retry. Missing: proxy tunnel support, system account delay for first connect, JetStream migrate timer, isLeafConnectDisabled check
(s *Server) clearObserverState(remote) golang/nats-server/server/leafnode.go:768 MISSING Clears JetStream RAFT observer state after reconnect. RAFT not ported
(s *Server) checkJetStreamMigrate(remote) golang/nats-server/server/leafnode.go:802 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:367 CheckJetStreamMigrate validates domain conflicts. Missing: actual RAFT StepDown/SetObserver calls
(s *Server) isLeafConnectDisabled() golang/nats-server/server/leafnode.go:844 PORTED src/NATS.Server/LeafNodes/LeafNodeManager.cs:90 IsLeafConnectDisabled(remoteUrl) / IsGloballyDisabled
(s *Server) startLeafNodeAcceptLoop() golang/nats-server/server/leafnode.go:875 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:200 StartAsync starts an accept loop. Missing: TLS setup, INFO JSON generation, nonce generation, async INFO propagation, setLeafNodeInfoHostPortAndIP
(s *Server) copyLeafNodeInfo() golang/nats-server/server/leafnode.go:1083 MISSING Deep-copies the leaf node INFO struct. No Info type in .NET leaf module
(s *Server) addLeafNodeURL(urlStr) golang/nats-server/server/leafnode.go:1096 MISSING Adds a leaf URL from route and regenerates INFO JSON. Not ported
(s *Server) removeLeafNodeURL(urlStr) golang/nats-server/server/leafnode.go:1108 MISSING Removes a leaf URL and regenerates INFO JSON. Not ported
(s *Server) generateLeafNodeInfoJSON() golang/nats-server/server/leafnode.go:1122 MISSING Regenerates the serialized INFO JSON bytes. Not ported
(s *Server) sendAsyncLeafNodeInfo() golang/nats-server/server/leafnode.go:1131 MISSING Sends async INFO to all connected leaf nodes (URL list updates). Not ported
(s *Server) createLeafNode(conn, rURL, remote, ws) golang/nats-server/server/leafnode.go:1140 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:566 HandleInboundAsync / ConnectSolicitedAsync create connections. Missing: client struct setup, maxPay/maxSubs, TLS negotiation, websocket detection, auth timer, temp client registration, read/write loop spawning
(s *Server) negotiateLeafCompression(c, didSolicit, infoCompression, co) golang/nats-server/server/leafnode.go:1648 MISSING Negotiates S2 compression mode between hub and leaf. Not ported
(s *Server) setLeafNodeInfoHostPortAndIP() golang/nats-server/server/leafnode.go:1763 MISSING Sets advertise host/port for leaf INFO. Not ported
(s *Server) addLeafNodeConnection(c, srvName, clusterName, checkForDup) golang/nats-server/server/leafnode.go:1811 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:693 ValidateRemoteLeafNode checks for duplicate + domain conflict. Missing: JetStream domain API deny-list merging, system account detection, RAFT observer mode toggling, JS mapping table setup, actual s.leafs map insertion
(s *Server) removeLeafNodeConnection(c) golang/nats-server/server/leafnode.go:1975 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:671 WatchConnectionAsync removes connection on close. Missing: gwSub removal from gwLeafSubs, tsubt timer stop, proxyKey removal
(s *Server) checkInternalSyncConsumers(acc) golang/nats-server/server/leafnode.go:2193 MISSING Kicks JetStream source/mirror consumers after leaf connect. Requires full JetStream integration
(s *Server) sendPermsAndAccountInfo(c) golang/nats-server/server/leafnode.go:2244 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:292 SendPermsAndAccountInfo syncs allow-lists to connection. Missing: actual INFO JSON protocol send to remote, IsSystemAccount flag, ConnectInfo flag
(s *Server) initLeafNodeSmapAndSendSubs(c) golang/nats-server/server/leafnode.go:2264 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:325 InitLeafNodeSmapAndSendSubs sends LS+ for a list of subjects. Missing: gathering subs from SubList, loop detection subject, gateway interest, siReply prefix, tsub transient map, spoke-only local sub filtering
(s *Server) updateInterestForAccountOnGateway(accName, sub, delta) golang/nats-server/server/leafnode.go:2428 MISSING Called from gateway code to update leaf node smap. Requires gateway integration
(s *Server) leafNodeResumeConnectProcess(c) golang/nats-server/server/leafnode.go:3369 MISSING Sends CONNECT protocol and starts write loop on solicited leaf. Not ported (no CONNECT handshake)
(s *Server) leafNodeFinishConnectProcess(c) golang/nats-server/server/leafnode.go:3409 MISSING Registers leaf with account, initialises smap, sends sys connect event. Not ported

Methods on Account

Go Symbol Go File:Line Status .NET Equivalent Notes
(acc *Account) updateLeafNodesEx(sub, delta, hubOnly) golang/nats-server/server/leafnode.go:2442 MISSING Propagates sub interest delta to all leaf connections for the account, with hub-only option
(acc *Account) updateLeafNodes(sub, delta) golang/nats-server/server/leafnode.go:2515 PARTIAL src/NATS.Server/LeafNodes/LeafNodeManager.cs:265 PropagateLocalSubscription / PropagateLocalUnsubscription broadcast to all connections. Missing: per-connection smap refcounting, isolated/hub-only filtering, origin-cluster filtering

Methods on leafNodeCfg

Go Symbol Go File:Line Status .NET Equivalent Notes
(cfg *leafNodeCfg) pickNextURL() golang/nats-server/server/leafnode.go:510 PORTED src/NATS.Server/Configuration/LeafNodeOptions.cs:40 (RemoteLeafOptions.PickNextUrl) Round-robin URL picker implemented with per-remote current URL tracking
(cfg *leafNodeCfg) getCurrentURL() golang/nats-server/server/leafnode.go:525 PORTED src/NATS.Server/Configuration/LeafNodeOptions.cs:55 (GetCurrentUrl) Current selected URL accessor implemented
(cfg *leafNodeCfg) getConnectDelay() golang/nats-server/server/leafnode.go:533 PORTED src/NATS.Server/Configuration/LeafNodeOptions.cs:61 (GetConnectDelay) Per-remote connect-delay getter implemented
(cfg *leafNodeCfg) setConnectDelay(delay) golang/nats-server/server/leafnode.go:541 PORTED src/NATS.Server/Configuration/LeafNodeOptions.cs:67 (SetConnectDelay) Per-remote connect-delay setter implemented
(cfg *leafNodeCfg) cancelMigrateTimer() golang/nats-server/server/leafnode.go:761 PORTED src/NATS.Server/Configuration/LeafNodeOptions.cs (StartMigrateTimer, CancelMigrateTimer) Added per-remote migrate timer handle with cancellation semantics
(cfg *leafNodeCfg) saveTLSHostname(u) golang/nats-server/server/leafnode.go:858 PORTED src/NATS.Server/Configuration/LeafNodeOptions.cs:73 (SaveTlsHostname) TLS hostname extraction from URL implemented
(cfg *leafNodeCfg) saveUserPassword(u) golang/nats-server/server/leafnode.go:866 PORTED src/NATS.Server/Configuration/LeafNodeOptions.cs:83 (SaveUserPassword) Username/password extraction from URL user-info implemented

Standalone Functions

Go Symbol Go File:Line Status .NET Equivalent Notes
validateLeafNode(o *Options) golang/nats-server/server/leafnode.go:214 MISSING Validates all leaf node config options (accounts, operator mode, TLS, proxy, compression). Not ported
checkLeafMinVersionConfig(mv) golang/nats-server/server/leafnode.go:343 MISSING Validates minimum version string. Not ported
validateLeafNodeAuthOptions(o) golang/nats-server/server/leafnode.go:357 MISSING Validates single-user vs. multi-user leaf auth config. Not ported
validateLeafNodeProxyOptions(remote) golang/nats-server/server/leafnode.go:377 MISSING Validates HTTP proxy options for WebSocket leaf remotes. Not ported
newLeafNodeCfg(remote) golang/nats-server/server/leafnode.go:470 PARTIAL src/NATS.Server/Configuration/LeafNodeOptions.cs RemoteLeafOptions covers URLs and credentials. Missing: URL randomization, per-URL TLS hostname/password extraction, WS TLS detection
establishHTTPProxyTunnel(proxyURL, targetHost, timeout, username, password) golang/nats-server/server/leafnode.go:565 MISSING Establishes an HTTP CONNECT tunnel through an HTTP proxy for WebSocket leaf connections. Not ported
keyFromSub(sub) golang/nats-server/server/leafnode.go:2638 PORTED src/NATS.Server/LeafNodes/LeafSubKey.cs:19 (KeyFromSub) Helper now builds subject or subject queue keys matching Go key shape
keyFromSubWithOrigin(sub) golang/nats-server/server/leafnode.go:2664 PORTED src/NATS.Server/LeafNodes/LeafSubKey.cs:27 (KeyFromSubWithOrigin) Routed key builder now emits R ... and L ... forms with optional queue/origin segments

Constants in smap key helpers

Go Symbol Go File:Line Status .NET Equivalent Notes
keyRoutedSub, keyRoutedSubByte (const "R") golang/nats-server/server/leafnode.go:2651 PORTED src/NATS.Server/LeafNodes/LeafSubKey.cs:11-12 Routed-sub key prefix constants are defined for parity
keyRoutedLeafSub, keyRoutedLeafSubByte (const "L") golang/nats-server/server/leafnode.go:2653 PORTED src/NATS.Server/LeafNodes/LeafSubKey.cs:13-14 Routed-leaf-sub key prefix constants are defined for parity
sharedSysAccDelay (const 250ms) golang/nats-server/server/leafnode.go:562 PORTED src/NATS.Server/LeafNodes/LeafSubKey.cs:16 Shared system-account connect delay constant added (250ms)
connectProcessTimeout (const 2s) golang/nats-server/server/leafnode.go:3365 PORTED src/NATS.Server/LeafNodes/LeafSubKey.cs:17 Connect-process timeout constant added (2s)

.NET-only additions (no Go equivalent — extensions)

.NET Symbol .NET File:Line Notes
LeafConnection.SetPermissions() src/NATS.Server/LeafNodes/LeafConnection.cs:66 Allows-list sync API — exposes what Go does internally
LeafNodeManager.DisableLeafConnect() / EnableLeafConnect() src/NATS.Server/LeafNodes/LeafNodeManager.cs:98 Per-remote disable/enable API (Go uses a simple bool flag)
LeafNodeManager.DisableAllLeafConnections() / EnableAllLeafConnections() src/NATS.Server/LeafNodes/LeafNodeManager.cs:121 Global disable API
LeafNodeManager.ComputeBackoff(attempt) src/NATS.Server/LeafNodes/LeafNodeManager.cs:541 Exponential backoff utility (Go uses fixed reconnectDelay + jitter)
LeafNodeManager.RegisterLeafNodeCluster() / UnregisterLeafNodeCluster() src/NATS.Server/LeafNodes/LeafNodeManager.cs:446 Cluster topology registry (partially mirrors Go's registerLeafNodeCluster)
LeafHubSpokeMapper.Map() src/NATS.Server/LeafNodes/LeafHubSpokeMapper.cs:77 Account mapping hub↔spoke (Go does this inline in client publish path)
LeafHubSpokeMapper.IsSubjectAllowed() src/NATS.Server/LeafNodes/LeafHubSpokeMapper.cs:92 Allow/deny list filtering (mirrors Go's permission check in sendLeafNodeSubUpdate)
LeafLoopDetector.Mark() / IsLooped() / TryUnmark() src/NATS.Server/LeafNodes/LeafLoopDetector.cs Loop detection helpers (Go does this inline in processLeafSub)
WebSocketStreamAdapter src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs WebSocket→Stream adapter; Go uses its own ws read/write path in client.go
LeafPermSyncResult, LeafTlsReloadResult, LeafValidationResult src/NATS.Server/LeafNodes/LeafNodeManager.cs:758 Result types for .NET API surface (Go returns error tuples)
JetStreamMigrationResult, JetStreamMigrationStatus src/NATS.Server/LeafNodes/LeafNodeManager.cs:804 JetStream migration result type
LeafClusterInfo src/NATS.Server/LeafNodes/LeafNodeManager.cs:829 Cluster topology entry (partial Go analog)

Summary

Status Count
PORTED 29
PARTIAL 28
MISSING 26
NOT_APPLICABLE 1
DEFERRED 0
Total 84

Key Gaps

The .NET leaf node implementation is a structural scaffold — the basic connection lifecycle (accept/connect, LS+/LS- propagation, LMSG forwarding, loop detection) is present, but significant protocol depth is missing:

  1. CONNECT flow is only partially wired: .NET now has LeafConnectInfo + SendLeafConnectAsync, but solicited connection flow still primarily handshakes with LEAF <id> and does not yet fully mirror Go connect-process sequencing.
  2. No smap (subject map): Go maintains a per-connection reference-counted map (leaf.smap) to deduplicate LS+/LS- traffic. .NET broadcasts blindly to all connections.
  3. No INFO protocol handling: Dynamic URL list updates, compression negotiation, and permission updates over async INFO are unimplemented.
  4. No compression: S2 compression negotiation between hub and leaf is entirely absent.
  5. No HTTP proxy tunnel: establishHTTPProxyTunnel for WebSocket-via-proxy leaf connections is not ported.
  6. No SubList integration: Inbound leaf subscriptions (LS+) are not inserted into the server's SubList trie, so received leaf subscriptions do not create actual server-side subscription state for message routing.
  7. No route/gateway propagation: When a leaf sub arrives, Go also updates routes and gateways; .NET does not.
  8. No reconnect delay enforcement: After loop detection, permission violations, or cluster name collision, Go enforces a 30-second reconnect delay; .NET detects but does not enforce.
  9. No JetStream RAFT integration: clearObserverState, checkJetStreamMigrate (actual RAFT step-down), and checkInternalSyncConsumers are all absent.
  10. WebSocket path incomplete: The WebSocketStreamAdapter adapts the byte stream, but the HTTP upgrade handshake (GET /leafnode with Sec-WebSocket-Key) is not implemented for solicited WS leaf connections.

Keeping This File Updated

After porting work is completed:

  1. Update status: Change MISSING → PORTED or PARTIAL → PORTED for each item completed
  2. Add .NET path: Fill in the ".NET Equivalent" column with the actual file:line
  3. Re-count LOC: Update the LOC numbers in stillmissing.md:
    # Re-count .NET source LOC for this module
    find src/NATS.Server/LeafNodes/ -name '*.cs' -type f -exec cat {} + | wc -l
    # Re-count .NET test LOC for this module
    find tests/NATS.Server.Tests/LeafNodes/ -name '*.cs' -type f -exec cat {} + | wc -l
    
  4. Add a changelog entry below with date and summary of what was ported
  5. Update the parity DB if new test mappings were created:
    sqlite3 docs/test_parity.db "INSERT INTO test_mappings (go_test_id, dotnet_test_id, confidence, notes) VALUES (?, ?, 'manual', 'ported in YYYY-MM-DD session')"
    

Change Log

Date Change By
2026-02-25 File created with LLM analysis instructions auto
2026-02-25 Full gap inventory populated: 62 symbols classified (5 PORTED, 18 PARTIAL, 38 MISSING, 1 NOT_APPLICABLE) claude-sonnet-4-6
2026-02-25 Ported leaf helper parity batch: role predicates on LeafConnection, remote-validity guard in reconnect loop, remote leaf config URL/delay/TLS/userinfo helpers, and reconnect/wait constants; added focused tests and updated gap statuses codex
2026-02-25 Ported leaf smap-key parity helper batch: added routed key constants and key builders (KeyFromSub, KeyFromSubWithOrigin) plus sharedSysAccDelay and connectProcessTimeout constants with focused tests codex
2026-02-26 Ported leaf ERR/connect-delay/connect-info parity batch: added LeafConnectInfo, SendLeafConnectAsync, RemoteCluster() parsing, solicited connect-delay handlers (SetLeafConnectDelayIfSoliciting, LeafProcessErr, permission-violation helpers), and RemoteLeafOptions migrate timer cancellation helpers with focused parity tests codex
2026-02-26 Ported leaf LS+ queue-weight parity batch: added weighted LS+ emission/parsing (SendLsPlusAsync overload + read-loop queue-weight extraction), updated leaf manager propagation API to pass weights, and added focused parity tests codex
2026-02-26 Ported leaf send-side permission gate parity for spoke links: PropagateLocalSubscription now enforces spoke subscribe allow-list semantics (with loop/gateway bypass subjects), with wire-level focused tests codex