Skip to content

Improve meshnet robustness to cluster changes - #743

Merged
kraney merged 22 commits into
openconfig:mainfrom
kraney:meshnet-repair
Aug 21, 2026
Merged

Improve meshnet robustness to cluster changes#743
kraney merged 22 commits into
openconfig:mainfrom
kraney:meshnet-repair

Conversation

@kraney

@kraney kraney commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Meshnet's algorithm is really written only for initial config. If pods restart, get reassigned to other nodes, etc., it doesn't keep the mesh current.

This introduces a number of changes to monitor K8s state post-initialization and keep the mesh properly configured.

kraney added 21 commits August 17, 2026 23:24
In pod pairs, one expects the other to do the veth setup work. If
the lazy half restarts, it continues to expect this and things
never get fixed. This updates the code to fix even in this case.
When a link is dynamically removed from the topology, clean up its
associated meshnet resources
A pod's IP can change on restart, so make sure IP is updated
when the pod resyncs
When a pod is rescheduled elsewhere, the type of link it needs may
change, so clean up and handle that case properly when needed
  #### 1. In-Memory Topology Cache (TopologyCache)

  • Location: controller.go
  • Maintains a thread-safe in-memory cache of all Topology CRs, populated on controller startup and kept updated in real-
  time via Kubernetes watch events and local status updates (handler.go).
  • Resolves handler.go, controller.go, and controller.go from memory in O(1) time with fallback, eliminating the kube-
  apiserver List/Get request storm.

  #### 2. Inverted Peer Dependency Indexing & Targeted Reconciliation

  • Location: controller.go, controller.go
  • TopologyCache maintains an inverted dependency index (peerPod -> []dependentLocalPods).
  • When a pod X starts, restarts, or updates:
      • If X is on this node, X is queued for reconciliation.
      • All local pods Y that link to X (GetDependents) are identified in O(1) and queued for reconciliation.
      • Unrelated pods and nodes in the rest of the 1500-node cluster do zero reconciliation work.

  #### 3. Protection Against Post-Configure State Decay & Restarts

  • Peer Pod Restart / NetNS Re-creation: If peer pod B restarts (updating its net_ns, src_ip, or container_id), the watch
  event for B immediately triggers reconciliation for all dependent local pods (such as A). The veth link or gRPC wire is
  re-plumbed against B's new namespace.
  • Node Rescheduling: Node IP changes (same-node ↔ remote) trigger the transition logic to tear down stale links and
  create the new link type.
  • Safety Net Periodic Resync: A 60-second periodic resync runs in the background as a safety net against any missed
  watch events or delayed state convergence.

  #### 4. Coalescing and Debouncing Work Queue (ReconcileQueue)

  • Location: controller.go
  • Debounces incoming pod arrival bursts (50ms window) so that simultaneous pod starts coalesce into a single batched
  pass.
  ### 1. Per-RPC Timeout on Remote Handshakes

  • Locations: controller.go & controller.go
  • Changes: Wrapped remoteClient.AddGRPCWiresRemoteBatch and remoteClient.GRPCWireDownRemote with a 5*time.Second context timeout (context.
  WithTimeout(ctx, 5*time.Second)).
  • Benefit: If a remote worker node is slow, booting, or dropping packets, the call quickly times out and fails gracefully rather than
  blocking the single-threaded local reconciler loop indefinitely.
  ──────
  ### 2. Prevent Duplicate RecvFrmLocalPodThread Spawn

  • Locations: gwire_rpc_handlers.go & handler.go
  • Changes:
      1. Made gwire_rpc_handlers.go check if the wire is already in memory before creating a new TAP handle or spawning a reader thread.
      2. Updated gwire_rpc_handlers.go to return an isNew boolean flag.
      3. In AddGRPCWireRemote and AddGRPCWiresRemoteBatch, only spawn go RecvFrmLocalPodThread if isNew == true.
  • Benefit: When both peer nodes reconcile simultaneously and exchange remote wire creation RPCs, existing TAP handles will not have
  multiple competing reader goroutines spawned on /dev/net/tun.
  ──────
  ### 3. Mitigate Thundering Herd Polling in CNI cmdAdd

  • Location: meshnet.go
  • Changes:
      1. Batch NetNS Lookup: Discovers all container interfaces in a single netns switch per polling tick instead of executing a netns switch
      per link.
      2. Peer Node IP Cache: Caches the peer pod node IP to avoid querying meshnetClient.Get on every 100ms tick for each link.
      3. Exponential Backoff: Replaced the fixed 100ms ticker with an exponential backoff (starting at 100ms and backing off to 1s).
      4. Nil-check: Added a peerPod != nil guard in cmdDel to prevent panics during cascading teardowns.
1. Added streamMgr.Send(topoNs, peerIP, pkt) to route packets to the multiplexed stream corresponding to (topoNs, peerIP).
2. In grpcwire.go, when a peer pod is reassigned to a different node (wire.PeerNodeIP != newPeerIP), the old stream reference is
cleanly released with streamMgr.ReleaseStream and the new stream is registered with streamMgr.GetOrCreateStream.
3. In grpcwire.go, packets dynamically read the current wire.PeerNodeIP under lock, ensuring outgoing packets instantly route to the
new node without blackholing traffic or corrupting stream refcounts.
A. Concurrent Local vs. Remote Wire Creation Race
Root Cause: When two nodes (Node A and Node B) reconcile a link simultaneously, Node A calls CreateGRPCWireLocal while sending an AddGRPCWiresRemoteBatch RPC to Node B, and Node B does the inverse. If CreateGRPCWireLocal and CreateUpdateGRPCWireRemoteTriggered execute concurrently on Node B for the same link UID, both check GetWireByUID / UpdateWireByUID before either inserts into wires.wires, resulting in duplicate TAP creation and clobbered map entries.
Fix: Added synchronization (createWireMu sync.Mutex) in

gwire_rpc_handlers.go
 across checking if a wire already exists and creating/inserting a new wire.
B. Missing Stale NetNS Wire Cleanup in AddInMemNDataStore
Root Cause: Commit

81dbf4a
 added cleanup for old wires with stale LocalPodNetNS paths (when containers restart) to wires.AddInMem. However, gRPC RPC triggers and local reconciliation use AddInMemNDataStore, which omitted that cleanup loop—leaving old netNS TAP handles and reader goroutines running indefinitely when pods restarted in a new netNS.
Fix: Extracted a helper method

cleanupOldWireLocked
 and invoked it in both AddInMem and AddInMemNDataStore. Added unit test

TestAddInMemNDataStore_StaleWireCleanup
 to cover this scenario.
C. Stream Reference Count Leak on Late Peer Discovery
Root Cause: In

RecvFrmLocalPodThread
, defer func() { ... ReleaseStream(...) }() was registered inside if peerIP != "". When CreateGRPCWireLocal spawns the thread before the peer pod is scheduled (peerIP == ""), the defer was never scheduled. When PeerNodeIP was updated later via wire.UpdateWire, GetOrCreateStream incremented the refcount, but thread termination never released it.
Fix: Moved the defer cleanup block outside if peerIP != "" so that currIP := wire.PeerNodeIP is checked upon thread exit and any acquired stream reference is released.
D. Reader Thread Leak on Teardown of Non-Ready Wires
Root Cause: In

RemoveWireAcrosAll
 and WireDownByUID, wire.StopC was only closed if wire.IsReady. If a wire was locally initiated (IsReady = false) and the pod was deleted before the remote peer completed the handshake, StopC was never closed.
Fix: Updated wire teardown methods to unconditionally close wire.StopC (if non-nil) regardless of wire.IsReady.
… obsolete CNI code

1. Dead-Wire Recovery on WireDownByUID:
   - Updated WireDownByUID in grpcwire.go to invoke RemoveWireAcrosAll(wire, true)
     instead of leaving a dead wire in the active map with closed channels and dead
     reader goroutines. This ensures future peer pod reconnects and reconciliations
     cleanly re-create TAP handles and reader threads.
   - Added unit test TestWireDownByUID_CleansUpAndRemovesFromMemory.

2. gRPC Wire Cleanup on Topology CR Deletion:
   - Added grpcwire.DeletePodWires(ns, name) to the watch.Deleted event handler in
     controller.go to clean up gRPC wires, reader goroutines, and TAP handles when
     a local Topology CR is deleted directly.

3. Prevent gRPC Conn Leaks and Timeouts in CNI MakeGRPCChanDown:
   - Added defer remote.Close() to ensure gRPC connections are closed on exit.
   - Added a 5-second context timeout to GRPCWireDownRemote RPC calls to avoid
     hanging CNI deletions when remote peers are unreachable.

4. Clean Up Obsolete Legacy Code:
   - Removed unused CreatGRPCChan and legacy retry constants from grpcwires-plugin.go,
     as link creation is now handled by the daemon reconciler.

Cross-Node Wire Teardown Resolution (third_party/meshnet/daemon/grpcwire/gwire_rpc_handlers.go):
Updated GRPCWireDownRemoteTriggered to check wireDef.LocalPodNetNs first and fallback to wireDef.TopoNs. This ensures remote wire teardowns across nodes correctly match and destroy the remote wire end even when the caller sends its own local container netns path.
Added unit test TestGRPCWireDownRemoteTriggered_RemoteNetNSFallback in third_party/meshnet/daemon/grpcwire/gwire_map_test.go.

CNI Plugin Cleanup & String Trimming (third_party/meshnet/plugin/meshnet.go):
Removed the obsolete and redundant meshnetClient.SkipReverse calls in cmdDel to prevent unnecessary Kubernetes API conflict retries on pod deletion.
Trimmed whitespace on interNodeLinkType using strings.TrimSpace and fixed the log typo in SetInterNodeLinkType ("iner" -> "inner").
  1. Standard Kubernetes Informer Integration (third_party/meshnet/daemon/meshnet/controller.go)
      • Integrated DynamicSharedInformerFactory in RunControllerLoop.
      • Cache sync, event streaming, and watch reconnection are now managed by standard client-go Informer infrastructure. Watch event callbacks update
      topoCache and trigger debounced reconciliation in RAM with 0 unnecessary API server List calls.
  2. CLI Initialization Fail-Fast Timeout (topo/node/node.go)
      • Updated GetCLIConn to apply a 5-second WithTimeoutOps during early boot connection attempts (d.Open()).
      • When pods enter PodRunning while router OS containers (JunOS, SR Linux, cEOS) are still booting up, d.Open() fails in 5 seconds instead of
      stalling on prompt-matching timeouts for 30–60 seconds per attempt. Once ready, full operational driver settings are restored.
  3. Eliminated Arista Status() Watch Thrashing (topo/node/arista/arista.go & arista_test.go)
      • Replaced continuous short-lived Watch() stream creation in arista.go Status() with direct single-pod Get() queries via n.Pods(ctx).
      • Updated arista_test.go to test Get reactors.
These variables are initialized to a non-zero default, so the check was
never successful
Don't hang and fail if the close sequence can't be sent (already
closed.) Don't return error if the close message can't be sent.
Comment thread third_party/meshnet/daemon/grpcwire/grpcwire.go Outdated
Comment thread third_party/meshnet/daemon/meshnet/controller.go
Comment thread third_party/meshnet/daemon/meshnet/controller.go Outdated
    1. Apply mutex hat idiom in GRPCWire struct (third_party/meshnet/daemon/grpcwire/grpcwire.go):
       • Reordered GRPCWire fields to place sync.Mutex directly above the mutable fields it synchronizes (IsReady,
  WireIfaceIDOnPeerNode, PeerNodeIP, StopC).

    2. Handle DeletedFinalStateUnknown tombstones (third_party/meshnet/daemon/meshnet/controller.go):
       • Unwrapped cache.DeletedFinalStateUnknown in toUnstructured and informer DeleteFunc to prevent missed topology
  deletion events during watch reconnects.

    3. Make remote wire teardown non-blocking (third_party/meshnet/daemon/meshnet/controller.go):
       • Dispatched GRPCWireDownRemote RPCs asynchronously in cleanupRemovedPodLinks to prevent blocking the main
  reconciliation loop when peer nodes are unreachable.
@kraney
kraney merged commit 577a223 into openconfig:main Aug 21, 2026
14 checks passed
@kraney
kraney deleted the meshnet-repair branch August 21, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants