# 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](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: ```bash 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 ` | | `(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+ ` 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 (`+`/`|`), queue-group args, header-size field | | `(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 ` 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`: ```bash # 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: ```bash 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 |