Просмотр исходного кода

Let the config turn the control socket off

The control socket was on for every daemon with no way to decline it. An
operator who does not want a local control endpoint, or a host whose
policy forbids one, now sets local_api.control.enabled to false.

- local_api.control.enabled, true unless set; reloadable both ways, and turning it off removes the socket
- the daemon logs at startup that the socket is disabled
- nebula ctl reading such a config says the socket is disabled instead of dialing; -socket still wins
- example config, CHANGELOG and tests for the wiring and the client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matt Richardson 1 день назад
Родитель
Сommit
50f760e6e5
6 измененных файлов с 81 добавлено и 6 удалено
  1. 6 0
      cmd/internal/ctl/ctl.go
  2. 14 0
      cmd/internal/ctl/ctl_test.go
  3. 2 0
      examples/config.yml
  4. 12 0
      localapi/path.go
  5. 16 6
      localapi/wire.go
  6. 31 0
      localapi/wire_test.go

+ 6 - 0
cmd/internal/ctl/ctl.go

@@ -418,6 +418,12 @@ func socketFromConfigFile(configPath string, named bool) (path, readConfig, over
 		return "", "", "", fmt.Errorf("nebula: could not read %s: %s", configPath, err)
 	}
 
+	// A daemon running this config serves no socket; dialing the path would only say
+	// that nothing is there.
+	if !localapi.ControlEnabled(c) {
+		return "", "", "", fmt.Errorf("nebula: the control socket is disabled in %s by %s", configPath, localapi.CtlEnabledKey)
+	}
+
 	// The same call the daemon makes, so the two agree on the path and on what makes a
 	// path unusable.
 	p, err := localapi.SocketPathFromConfig(c)

+ 14 - 0
cmd/internal/ctl/ctl_test.go

@@ -58,6 +58,20 @@ func TestResolveSocketTrustsANamedConfig(t *testing.T) {
 	assert.Equal(t, nowhereSocket, p)
 }
 
+// TestResolveSocketSaysTheSocketIsDisabled: a config that turns the control socket off
+// is answered with that, not with a dial to a path nothing serves. -socket still wins.
+func TestResolveSocketSaysTheSocketIsDisabled(t *testing.T) {
+	cfg := filepath.Join(t.TempDir(), "config.yml")
+	require.NoError(t, os.WriteFile(cfg, []byte("local_api:\n  control:\n    enabled: false\n"), 0600))
+
+	_, _, _, err := resolveSocket("", cfg)
+	require.ErrorContains(t, err, "the control socket is disabled in "+cfg+" by local_api.control.enabled")
+
+	p, _, _, err := resolveSocket("/explicit.sock", cfg)
+	require.NoError(t, err)
+	assert.Equal(t, "/explicit.sock", p)
+}
+
 // TestResolveSocketUsesTheDefaultWithoutAnOverride: a config that does not name the
 // socket means the default path, the same one the daemon serves with that config, and
 // no config at all means the default too, since that is where a daemon started without

+ 2 - 0
examples/config.yml

@@ -244,6 +244,8 @@ punchy:
   # `nebula ctl <command>`. It defaults to /var/run/nebula/ctl.sock on Linux, macOS and
   # the BSDs; on Windows it is a named pipe under \\.\pipe\ProtectedPrefix\Administrators.
   #control:
+    # Set to false to turn the control socket off. Reloadable.
+    #enabled: true
     # The absolute path where the control socket should live, or a full pipe name on
     # Windows. The last directory is created if missing, and must be owned by root or by
     # the user running nebula. It must not be writable by anyone else.

+ 12 - 0
localapi/path.go

@@ -3,6 +3,18 @@ package localapi
 // DirName is the directory under /var/run that holds nebula's sockets by default.
 const DirName = "nebula"
 
+// CtlEnabledKey is the optional config key that turns the control socket off. It is on
+// unless the config says false, and reloadable: a reload that turns it off removes the
+// socket, and one that turns it back on brings it up again.
+const CtlEnabledKey = "local_api.control.enabled"
+
+// ControlEnabled reports whether a config leaves the control socket on. nebula ctl reads
+// it too, so a client pointed at a config that turned the socket off says so rather than
+// failing to dial.
+func ControlEnabled(c interface{ GetBool(string, bool) bool }) bool {
+	return c.GetBool(CtlEnabledKey, true)
+}
+
 // CtlSocketKey is the optional config key naming the control socket. Unset, the socket
 // is DefaultSocketPath, one fixed path that suits a daemon running as root, which is
 // what the tun device needs anyway. A daemon run as another user, or a second daemon on

+ 16 - 6
localapi/wire.go

@@ -9,15 +9,14 @@ import (
 	"github.com/slackhq/nebula/config"
 )
 
-// Wire brings up the local control socket for a running nebula and keeps it pointed at
-// the configured path across reloads.
+// Wire brings up the local control socket for a running nebula, unless the config turns
+// it off, and keeps it following the config across reloads.
 //
 // Every entry point that runs nebula as a daemon calls this, so the endpoint behaves the
 // same whether nebula was started from a shell or by a service manager. It deliberately
-// does not live in nebula.Main: the endpoint is on by default with no key to disable it,
-// so an embedder inside an app sandbox would have no way to decline a socket it cannot
-// use. Wiring it at the daemon entry points means embedders get the command registry and
-// no listener.
+// does not live in nebula.Main: the endpoint is on by default, so an embedder inside an
+// app sandbox would have to know to turn off a socket it cannot use. Wiring it at the
+// daemon entry points means embedders get the command registry and no listener.
 //
 // Every failure below is a warning, bar one. Nebula is expected to keep forwarding
 // packets whether or not anyone can reach it; but a name that another process holds is
@@ -35,6 +34,9 @@ func Wire(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Regist
 	// the path is a no-op.
 	reconcile := followControlConfig(ctx, l, c, srv)
 	reconcile(c)
+	if !ControlEnabled(c) {
+		l.Info("Local control socket is disabled", "key", CtlEnabledKey)
+	}
 	return srv
 }
 
@@ -59,6 +61,14 @@ func Wire(ctx context.Context, l *slog.Logger, c *config.C, reg *commands.Regist
 // down and skip every callback queued behind this one.
 func followControlConfig(ctx context.Context, l *slog.Logger, c *config.C, srv *Server) func(*config.C) {
 	reconcile := func(c *config.C) {
+		if !ControlEnabled(c) {
+			if srv.Path() != "" {
+				srv.Stop()
+				l.Info("Stopped the local control socket, it is disabled in the reloaded config")
+			}
+			return
+		}
+
 		want, err := SocketPathFromConfig(c)
 		if err != nil {
 			if srv.Path() == "" {

+ 31 - 0
localapi/wire_test.go

@@ -142,3 +142,34 @@ func TestWireRecoversOnPlainReloadAfterBadBind(t *testing.T) {
 	assert.Empty(t, errText)
 	assert.Equal(t, "[still]", out)
 }
+
+// TestWireControlCanBeDisabled: local_api.control.enabled false serves no socket, and
+// the key follows reloads both ways.
+func TestWireControlCanBeDisabled(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	t.Cleanup(cancel)
+
+	path := testSocket(t)
+	section := func(enabled bool) string {
+		return fmt.Sprintf("local_api:\n  control:\n    enabled: %t\n    socket: %s\n", enabled, path)
+	}
+
+	c := config.NewC(test.NewLogger())
+	require.NoError(t, c.LoadString(section(false)))
+	srv := Wire(ctx, test.NewLogger(), c, basicRegistry())
+	t.Cleanup(srv.Stop)
+	assert.Empty(t, srv.Path())
+	_, err := os.Lstat(path)
+	assert.True(t, os.IsNotExist(err), "no socket is bound")
+
+	require.NoError(t, c.ReloadConfigString(section(true)).Err)
+	assert.Equal(t, path, srv.Path())
+	out, errText := run(t, path, "echo", "on")
+	assert.Empty(t, errText)
+	assert.Equal(t, "[on]", out)
+
+	require.NoError(t, c.ReloadConfigString(section(false)).Err)
+	assert.Empty(t, srv.Path())
+	_, err = os.Lstat(path)
+	assert.True(t, os.IsNotExist(err), "the socket is removed")
+}