From fdfcf3bd17b43bad1b0dc3ad59387456a8eda112 Mon Sep 17 00:00:00 2001 From: Matt Richardson Date: Tue, 20 Oct 2026 14:42:02 -0400 Subject: [PATCH] Compare the two sockets' configured paths as paths, not strings The check that the control socket and the info API are not configured on one path compared the strings, so a doubled or trailing separator, or a case difference on windows where pipe names are case-insensitive, got past it and the second bind failed with a message about another nebula. Co-Authored-By: Claude Fable 5.1 --- localapi/wire.go | 16 +++++++++++++++- localapi/wire_test.go | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/localapi/wire.go b/localapi/wire.go index f6b75919..066372ea 100644 --- a/localapi/wire.go +++ b/localapi/wire.go @@ -4,6 +4,9 @@ import ( "context" "errors" "log/slog" + "path/filepath" + "runtime" + "strings" "github.com/slackhq/nebula/commands" "github.com/slackhq/nebula/config" @@ -63,13 +66,24 @@ func sharedSocketPath(c *config.C) (string, bool) { } info, err := infoConfigFrom(c) - if err != nil || !info.Enabled || info.Path != ctl { + if err != nil || !info.Enabled || !samePath(info.Path, ctl) { return "", false } return ctl, true } +// samePath reports whether two configured socket paths name one node: cleaned, so a +// doubled separator or a trailing one does not hide the match, and on windows, where +// pipe names are case-insensitive, compared without case. +func samePath(a, b string) bool { + a, b = filepath.Clean(a), filepath.Clean(b) + if runtime.GOOS == "windows" { + return strings.EqualFold(a, b) + } + return a == b +} + // wireControl brings up the control socket and keeps it pointed at the configured path. func wireControl(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Registry) *Server { srv := NewServer(l, reg) diff --git a/localapi/wire_test.go b/localapi/wire_test.go index 8f2c16f0..e34ee6b8 100644 --- a/localapi/wire_test.go +++ b/localapi/wire_test.go @@ -198,6 +198,12 @@ func TestWireRefusesOnePathForBothAPIs(t *testing.T) { t.Cleanup(eps.Stop) assert.Empty(t, eps.Control.Path(), "the control socket does not take the info API's path") assert.Empty(t, eps.Info.Path(), "the info API does not take the control socket's path") + + // The same node spelled differently is the same node. + doubled := filepath.Join(filepath.Dir(shared), "") + "//" + filepath.Base(shared) + require.NoError(t, c.ReloadConfigString(section(shared, doubled)).Err) + assert.Empty(t, eps.Control.Path()) + assert.Empty(t, eps.Info.Path()) _, err := os.Stat(shared) assert.True(t, os.IsNotExist(err), "neither side bound the shared path")