Artifact 2731415a6f6d6660d3f04b5d546daa612f2ec61f11abb3ff396b217c67714c54:
- File
leaf/agent/nats_mesh.go
— part of check-in
[eda7960b81]
at
2026-04-23 19:23:42
on branch trunk
— rename module to github.com/danmestas/EdgeSync
Fix the case-sensitive module path to match the actual GitHub repo URL (danmestas/EdgeSync). Remove tracked leaf/leaf binary and ignore .claude/skills/ scratchpad dirs. (user: [email protected], size: 8532) [annotate] [blame] [check-ins using] [more...]
package agent import ( "context" "encoding/json" "errors" "fmt" "net" "net/http" "net/url" "os" "path/filepath" "time" natsserver "github.com/nats-io/nats-server/v2/server" ) // SidecarInfo provides the sidecar details that NATSMesh needs for tunnel // establishment. Passed to Start so the mesh can drive the full sequence. type SidecarInfo struct { EndpointID string // our iroh EndpointId SocketPath string // Unix socket for HTTP calls to sidecar } // NATSMesh owns the embedded NATS server and tunnel establishment lifecycle. // // The caller creates a NATSMesh and calls Start(). Everything else — port // reservation, tunnel establishment, NATS server configuration — is internal. // // The startup order matters because the NATS leaf node protocol is asymmetric: // - Hub side sends INFO, waits for CONNECT from the leaf. // - Leaf side sends CONNECT in response to INFO. // // The sidecar's outbound tunnel binds a local TCP listener and returns its // port. The embedded NATS server must be configured with that port as a leaf // remote BEFORE starting. Meanwhile, the sidecar's inbound (accepting) side // needs to know the local NATS leaf port to connect to. // // To break the chicken-and-egg, Start pre-reserves the leaf port, uses it // to tell the sidecar where NATS will listen, establishes tunnels to get // remote ports, then starts NATS with all ports configured. type NATSMesh struct { role NATSRole upstream string // optional external NATS URL irohPeers []string // remote EndpointIds clientPort int // optional fixed client port; 0 means random storeDir string // JetStream on-disk store dir; empty falls back to ./.nats-store under CWD // Set internally during Start. reservedLeafPort int tunnelPorts map[string]uint16 // peerID -> local port server *natsserver.Server clientURL string // nats://127.0.0.1:<client-port> leafAddr string // 127.0.0.1:<leaf-port> (for sidecar --nats-addr) } // tunnelResponse is the JSON body returned by POST /nats-tunnel/{endpoint-id}. type tunnelResponse struct { Port uint16 `json:"port"` } // LeafAddr returns the pre-reserved leaf node listen address for the sidecar's // --nats-addr. Only valid after ReserveLeafPort or Start. Empty for leaf role. func (m *NATSMesh) LeafAddr() string { return m.leafAddr } // ReserveLeafPort pre-allocates an ephemeral TCP port for the NATS leaf node // listener. The sidecar needs this address at launch (--nats-addr), which // happens before Start. The listener is closed immediately — NATS rebinds it. // // This is the one method that must be called before Start when iroh is enabled. // For non-iroh usage, Start handles everything. func (m *NATSMesh) ReserveLeafPort() error { if m.role == NATSRoleLeaf { return nil } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return fmt.Errorf("reserve leaf port: %w", err) } port := ln.Addr().(*net.TCPAddr).Port ln.Close() m.reservedLeafPort = port m.leafAddr = fmt.Sprintf("127.0.0.1:%d", port) return nil } // Start brings up the NATS mesh. If sidecar info is provided, it establishes // tunnels to iroh peers first, then starts the embedded NATS server with // tunnel ports wired as leaf remotes. Returns the NATS client URL. // // Pass nil for sidecar when iroh is not enabled. func (m *NATSMesh) Start(sidecar *SidecarInfo) (clientURL string, err error) { // Step 1: establish tunnels (if sidecar available). if sidecar != nil { if tunnelErr := m.establishTunnels(sidecar); tunnelErr != nil { // Non-fatal: partial connectivity is better than none. fmt.Printf("nats mesh: tunnel establishment (non-fatal): %v\n", tunnelErr) } } // Step 2: start embedded NATS with tunnel ports as leaf remotes. opts := m.buildServerOpts() m.server, err = natsserver.NewServer(opts) if err != nil { return "", fmt.Errorf("nats mesh: create server: %w", err) } m.server.Start() if !m.server.ReadyForConnections(5 * time.Second) { m.server.Shutdown() return "", fmt.Errorf("nats mesh: server not ready within 5s (reserved leaf port: %d)", m.reservedLeafPort) } m.clientURL = m.server.ClientURL() // Determine leaf node address when not pre-reserved. if m.role != NATSRoleLeaf && m.leafAddr == "" { varz, vErr := m.server.Varz(&natsserver.VarzOptions{}) if vErr == nil && varz.LeafNode.Port > 0 { m.leafAddr = fmt.Sprintf("127.0.0.1:%d", varz.LeafNode.Port) } } return m.clientURL, nil } // Stop shuts down the embedded NATS server. func (m *NATSMesh) Stop() { if m.server != nil { m.server.Shutdown() m.server.WaitForShutdown() m.server = nil } } // establishTunnels tells the sidecar to open NATS tunnels to peers. // Each tunnel returns a local TCP port that NATS will solicit to. func (m *NATSMesh) establishTunnels(sc *SidecarInfo) error { if sc.SocketPath == "" || sc.EndpointID == "" { return nil } client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", sc.SocketPath) }, }, Timeout: 10 * time.Second, } m.tunnelPorts = make(map[string]uint16) var errs []error for _, peerID := range m.irohPeers { if !shouldSolicit(m.role, sc.EndpointID, peerID) { continue } reqURL := fmt.Sprintf("http://iroh-sidecar/nats-tunnel/%s", peerID) resp, err := client.Post(reqURL, "", nil) if err != nil { errs = append(errs, fmt.Errorf("tunnel to %s: %w", peerID, err)) continue } if resp.StatusCode != http.StatusOK { resp.Body.Close() errs = append(errs, fmt.Errorf("tunnel to %s: HTTP %d", peerID, resp.StatusCode)) continue } var tr tunnelResponse err = json.NewDecoder(resp.Body).Decode(&tr) resp.Body.Close() if err != nil { errs = append(errs, fmt.Errorf("tunnel to %s: decode response: %w", peerID, err)) continue } if tr.Port == 0 { errs = append(errs, fmt.Errorf("tunnel to %s: sidecar returned port 0", peerID)) continue } m.tunnelPorts[peerID] = tr.Port } return errors.Join(errs...) } // buildServerOpts creates nats-server options based on role. // Includes tunnel ports as additional leaf remotes so NATS solicits through the // sidecar's QUIC tunnels. func (m *NATSMesh) buildServerOpts() *natsserver.Options { opts := &natsserver.Options{ Host: "127.0.0.1", Port: -1, // random client port NoLog: true, NoSigs: true, // JetStream is always on so callers (notably agent-infra's task KV) // can open streams and KV buckets without extra handshaking. JetStream: true, StoreDir: jetStreamStoreDir(m.storeDir), } if m.clientPort > 0 { opts.Port = m.clientPort } switch m.role { case NATSRolePeer, NATSRoleHub: if m.reservedLeafPort > 0 { opts.LeafNode.Host = "127.0.0.1" opts.LeafNode.Port = m.reservedLeafPort } else { opts.LeafNode.Port = -1 // random leaf node port } case NATSRoleLeaf: // No leaf port — this node solicits outward only. } // If upstream NATS URL is configured, join as a leaf. if m.upstream != "" { u, err := url.Parse(m.upstream) if err == nil { opts.LeafNode.Remotes = append(opts.LeafNode.Remotes, &natsserver.RemoteLeafOpts{URLs: []*url.URL{u}}, ) } } // Add tunnel ports as leaf remotes. Each port is a local TCP listener // managed by the sidecar, which pipes traffic through QUIC to the remote // peer's NATS hub. for _, port := range m.tunnelPorts { u, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", port)) if err == nil { opts.LeafNode.Remotes = append(opts.LeafNode.Remotes, &natsserver.RemoteLeafOpts{URLs: []*url.URL{u}}, ) } } return opts } // jetStreamStoreDir resolves the on-disk path where JetStream will persist // streams and KV buckets. When configured is non-empty it is used verbatim. // Otherwise we fall back to "<cwd>/.nats-store" — intentionally not /tmp, // since stale JetStream state from a prior run in /tmp would silently // corrupt a later workspace that reused the same bucket names. func jetStreamStoreDir(configured string) string { if configured != "" { return configured } cwd, err := os.Getwd() if err != nil { cwd = "." } return filepath.Join(cwd, ".nats-store") } // shouldSolicit determines whether we should initiate a NATS tunnel to a peer. func shouldSolicit(role NATSRole, myID, peerID string) bool { switch role { case NATSRoleLeaf: return true case NATSRoleHub: return false case NATSRolePeer: return myID < peerID default: return false } }