- 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
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:
- Extract all exported types (structs, interfaces, type aliases)
- Extract all exported methods on those types (receiver functions)
- Extract all exported standalone functions
- Note key constants, enums, and protocol states
- Note important unexported helpers that implement core logic (functions >20 lines)
- 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:
- Search for a matching type, method, or function in .NET
- If found, compare the behavior: does it handle the same edge cases? Same error paths?
- If partially implemented, note what's missing
- If not found, note it as MISSING
Step 3: Cross-Reference Tests
Compare Go test functions against .NET test methods:
- For each Go
Test*function, check if a corresponding .NET[Fact]or[Theory]exists - Note which test scenarios are covered and which are missing
- 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.gogolang/nats-server/server/leafnode_proxy_test.gogolang/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:
- CONNECT flow is only partially wired: .NET now has
LeafConnectInfo+SendLeafConnectAsync, but solicited connection flow still primarily handshakes withLEAF <id>and does not yet fully mirror Go connect-process sequencing. - 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. - No INFO protocol handling: Dynamic URL list updates, compression negotiation, and permission updates over async INFO are unimplemented.
- No compression: S2 compression negotiation between hub and leaf is entirely absent.
- No HTTP proxy tunnel:
establishHTTPProxyTunnelfor WebSocket-via-proxy leaf connections is not ported. - 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.
- No route/gateway propagation: When a leaf sub arrives, Go also updates routes and gateways; .NET does not.
- 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.
- No JetStream RAFT integration:
clearObserverState,checkJetStreamMigrate(actual RAFT step-down), andcheckInternalSyncConsumersare all absent. - WebSocket path incomplete: The
WebSocketStreamAdapteradapts the byte stream, but the HTTP upgrade handshake (GET /leafnodewithSec-WebSocket-Key) is not implemented for solicited WS leaf connections.
Keeping This File Updated
After porting work is completed:
- Update status: Change
MISSING → PORTEDorPARTIAL → PORTEDfor each item completed - Add .NET path: Fill in the ".NET Equivalent" column with the actual file:line
- 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 - Add a changelog entry below with date and summary of what was ported
- 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 |