From 01f8988cc3659253daaf8307837a2740b91a72f0 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 6 Jun 2026 18:05:05 +0200 Subject: [PATCH] feat(membershipd): --bind flag governs HTTP + embedded NATS interface Add a --bind flag (default 127.0.0.1) to membershipd that controls which network interface both the control-plane HTTP API and the embedded NATS data plane listen on. Use 0.0.0.0 to expose the stack to the LAN so remote peers (phones, other PCs) can connect; keep the default for a loopback-only dev stack. embeddednats gains StartHost(storeDir, host, port) for explicit interface control; Start stays a backward-compatible wrapper (host "" = nats default 0.0.0.0) so the playground and tests are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmd/membershipd/main.go | 8 ++++++-- pkg/embeddednats/embeddednats.go | 20 +++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/cmd/membershipd/main.go b/cmd/membershipd/main.go index 78581e3e..4fab090d 100644 --- a/cmd/membershipd/main.go +++ b/cmd/membershipd/main.go @@ -23,6 +23,7 @@ import ( func main() { var ( + bind = flag.String("bind", "127.0.0.1", "network interface to bind the HTTP API and the embedded NATS to; use 0.0.0.0 to accept LAN/remote peers") natsURL = flag.String("nats-url", "", "external NATS url; empty starts an embedded server") httpPort = flag.String("http-port", "8470", "HTTP port for the control-plane API") dbPath = flag.String("db", "./local_files/unibus.db", "SQLite database path") @@ -40,7 +41,10 @@ func main() { natsClientURL := *natsURL if natsClientURL == "" { var err error - ns, err = embeddednats.Start(*natsStore, *natsPort) + // Bind the embedded NATS to the same interface as the HTTP API so a single + // --bind flag governs reachability: 127.0.0.1 keeps the whole stack + // loopback-only; 0.0.0.0 exposes both planes to the LAN. + ns, err = embeddednats.StartHost(*natsStore, *bind, *natsPort) if err != nil { log.Fatalf("start embedded nats: %v", err) } @@ -65,7 +69,7 @@ func main() { log.Printf("blob store: %s", *storeDir) srv := membership.NewServer(store, blobs) - addr := "127.0.0.1:" + *httpPort + addr := *bind + ":" + *httpPort httpSrv := &http.Server{Addr: addr, Handler: srv} go func() { diff --git a/pkg/embeddednats/embeddednats.go b/pkg/embeddednats/embeddednats.go index 8707ba35..1116b97e 100644 --- a/pkg/embeddednats/embeddednats.go +++ b/pkg/embeddednats/embeddednats.go @@ -13,13 +13,27 @@ import ( ) // Start launches an embedded nats-server with JetStream enabled, listening on -// the given port and persisting JetStream state under storeDir. It blocks until -// the server is ready to accept connections (up to 5s) and returns the running -// server. The caller is responsible for calling Shutdown on it. +// the given port and persisting JetStream state under storeDir. The listen host +// is left at the nats-server default ("0.0.0.0", all interfaces). It blocks +// until the server is ready to accept connections (up to 5s) and returns the +// running server. The caller is responsible for calling Shutdown on it. +// +// Start is a thin backward-compatible wrapper over StartHost; callers that need +// to control the bind interface (loopback vs LAN) should use StartHost directly. func Start(storeDir string, port int) (*server.Server, error) { + return StartHost(storeDir, "", port) +} + +// StartHost is Start with explicit control over the bind interface. host selects +// which network interface the data plane listens on: pass "127.0.0.1" to keep +// NATS loopback-only (the safe default for a single-host dev stack) or "0.0.0.0" +// to expose it to the LAN so remote peers (phones, other PCs) can connect. An +// empty host falls back to the nats-server default ("0.0.0.0", all interfaces). +func StartHost(storeDir, host string, port int) (*server.Server, error) { opts := &server.Options{ JetStream: true, StoreDir: storeDir, + Host: host, Port: port, DontListen: false, // Keep the embedded server quiet by default; the host app logs the URLs.