mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-24 14:08:27 +02:00
fix(cli): enable macOS log-send socket with a Control D custom config
This commit is contained in:
1 parent
1da3fcf269
commit
316d5eef7e
5 files changed
+137
-19
No files matched your search
+55
-15
@@ -434,6 +434,15 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
mainLog.Load().Info().Msgf("writing intercept_mode = %q to config (requested %q)", cfg.Service.InterceptMode, interceptMode)
|
||||
}
|
||||
|
||||
// The API answer is the source of truth for a managed run. Without this,
|
||||
// a custom config added (or changed) after install is applied in memory
|
||||
// but never reaches the file, which file readers such as the macOS
|
||||
// delegated log-send gate still use.
|
||||
if managedConfigFetched && !updated && configFileDiffers(&cfg) {
|
||||
updated = true
|
||||
mainLog.Load().Info().Msg("config from Control D API differs from the config file")
|
||||
}
|
||||
|
||||
if updated {
|
||||
if err := writeConfigFile(&cfg); err != nil {
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), fmt.Sprintf("failed to write config file: %v", err), p.notifyExitToLogServer)
|
||||
@@ -527,30 +536,61 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfigFile(cfg *ctrld.Config) error {
|
||||
// configFileForWrite returns the config file that writeConfigFile writes:
|
||||
// viper's file when it read one, then --config, then defaultConfigFile. A
|
||||
// Control D custom config is read from memory, so viper names no file then.
|
||||
func configFileForWrite() string {
|
||||
if cfu := v.ConfigFileUsed(); cfu != "" {
|
||||
defaultConfigFile = cfu
|
||||
} else if configPath != "" {
|
||||
defaultConfigFile = configPath
|
||||
return cfu
|
||||
}
|
||||
if configPath != "" {
|
||||
return configPath
|
||||
}
|
||||
return defaultConfigFile
|
||||
}
|
||||
|
||||
// renderConfigFile returns the exact bytes writeConfigFile writes for cfg.
|
||||
func renderConfigFile(cfg *ctrld.Config) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if cdUID != "" {
|
||||
buf.WriteString("# AUTO-GENERATED VIA CD FLAG - DO NOT MODIFY\n\n")
|
||||
}
|
||||
enc := toml.NewEncoder(&buf).SetIndentTables(true)
|
||||
if err := enc.Encode(&cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// configFileDiffers reports whether the config file on disk is not what
|
||||
// writeConfigFile would write for cfg. A missing or unreadable file differs.
|
||||
func configFileDiffers(cfg *ctrld.Config) bool {
|
||||
want, err := renderConfigFile(cfg)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
have, err := os.ReadFile(configFileForWrite())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return !bytes.Equal(have, want)
|
||||
}
|
||||
|
||||
func writeConfigFile(cfg *ctrld.Config) error {
|
||||
defaultConfigFile = configFileForWrite()
|
||||
data, err := renderConfigFile(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(defaultConfigFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(0o644))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if cdUID != "" {
|
||||
if _, err := f.WriteString("# AUTO-GENERATED VIA CD FLAG - DO NOT MODIFY\n\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
enc := toml.NewEncoder(f).SetIndentTables(true)
|
||||
if err := enc.Encode(&cfg); err != nil {
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// readConfigFile reads in config file.
|
||||
|
||||
@@ -5,8 +5,11 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
func Test_writeConfigFile(t *testing.T) {
|
||||
@@ -22,6 +25,31 @@ func Test_writeConfigFile(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// A managed start rewrites the config file only when the API answer differs
|
||||
// from it, e.g. a custom config added after install.
|
||||
func Test_configFileDiffers(t *testing.T) {
|
||||
oldV, oldFile, oldPath, oldUID := v, defaultConfigFile, configPath, cdUID
|
||||
t.Cleanup(func() { v, defaultConfigFile, configPath, cdUID = oldV, oldFile, oldPath, oldUID })
|
||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||
configPath = filepath.Join(t.TempDir(), "ctrld.toml")
|
||||
cdUID = "test-uid"
|
||||
|
||||
generated := &ctrld.Config{Upstream: map[string]*ctrld.UpstreamConfig{"0": {Type: "doh", Endpoint: "https://dns.controld.com/test-uid"}}}
|
||||
assert.True(t, configFileDiffers(generated), "missing file must differ")
|
||||
require.NoError(t, writeConfigFile(generated))
|
||||
assert.False(t, configFileDiffers(generated), "file written from the same config must not differ")
|
||||
|
||||
custom := *generated
|
||||
custom.Service.AllowUnprivilegedLogSend = true
|
||||
assert.True(t, configFileDiffers(&custom), "custom config added after install must differ")
|
||||
require.NoError(t, writeConfigFile(&custom))
|
||||
assert.False(t, configFileDiffers(&custom))
|
||||
data, err := os.ReadFile(configPath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(data), "allow_unprivileged_log_send = true")
|
||||
assert.True(t, configFileDiffers(generated), "custom config removed must differ")
|
||||
}
|
||||
|
||||
func Test_isStableVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -90,7 +90,7 @@ func (p *prog) newDelegatedLogSendServer() (*logSendServer, error) {
|
||||
return nil, errLogSendTrust
|
||||
}
|
||||
tree := trustedLogTree{root: "/", uid: 0}
|
||||
collector := delegatedLogCollector{tree: tree, config: canonicalLogSendPath(v.ConfigFileUsed())}
|
||||
collector := delegatedLogCollector{tree: tree, config: canonicalLogSendPath(configFileForWrite())}
|
||||
// Capture source paths and budgets once. A reload cannot redirect this socket
|
||||
// to new files, identities, or destinations, nor enable an initially off socket.
|
||||
var files []*rotatingFile
|
||||
|
||||
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// These tests touch only owned temporary files and sockets: no service, PF,
|
||||
@@ -141,3 +144,45 @@ func TestDelegatedLogSendDarwinListenerLifecycle(t *testing.T) {
|
||||
t.Fatal("symlink parent accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegatedLogSendConfigPathAfterCustomConfig(t *testing.T) {
|
||||
oldV, oldFile, oldPath := v, defaultConfigFile, configPath
|
||||
t.Cleanup(func() { v, defaultConfigFile, configPath = oldV, oldFile, oldPath })
|
||||
|
||||
dir := t.TempDir()
|
||||
onDisk := filepath.Join(dir, "ctrld.toml")
|
||||
if err := os.WriteFile(onDisk, []byte("[service]\nallow_unprivileged_log_send = true\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Service start: --config names the file and readConfigFile reads it.
|
||||
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||
configPath, defaultConfigFile = onDisk, onDisk
|
||||
v.SetConfigFile(onDisk)
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := configFileForWrite(); got != onDisk {
|
||||
t.Fatalf("file config: got %q, want %q", got, onDisk)
|
||||
}
|
||||
|
||||
// --cd with a custom config: the production loader replaces viper with an
|
||||
// in-memory config, so viper no longer names any file.
|
||||
custom := base64.StdEncoding.EncodeToString([]byte("[service]\nallow_unprivileged_log_send = true\n"))
|
||||
if err := readBase64Config(custom); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.ConfigFileUsed() != "" {
|
||||
t.Fatalf("precondition: viper still names %q", v.ConfigFileUsed())
|
||||
}
|
||||
for _, tc := range []struct{ name, configPath, defaultFile string }{
|
||||
{"service --config", onDisk, "ctrld.toml"},
|
||||
{"default config file", "", onDisk},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
configPath, defaultConfigFile = tc.configPath, tc.defaultFile
|
||||
if got := configFileForWrite(); got != onDisk {
|
||||
t.Fatalf("custom config: got %q, want %q", got, onDisk)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -111,7 +111,9 @@ The `[service]` section controls general behaviors.
|
||||
|
||||
### allow_unprivileged_log_send
|
||||
|
||||
macOS only. Default: `false`. An administrator can opt in using the service's
|
||||
macOS only. Default: `false`. The configuration that ctrld generates from the
|
||||
Control D API never enables it. An administrator opts in through the device's
|
||||
custom ctrld configuration in the Control D dashboard, which ctrld writes to its
|
||||
root-owned configuration file:
|
||||
|
||||
```toml
|
||||
@@ -119,14 +121,17 @@ root-owned configuration file:
|
||||
allow_unprivileged_log_send = true
|
||||
```
|
||||
|
||||
Restart the service as administrator after enabling. Standard users can then run
|
||||
Restart the service as administrator after enabling, on a new or an existing
|
||||
install. At startup ctrld rewrites the configuration file whenever the API answer
|
||||
differs from it, so a custom config added, changed or removed after installation
|
||||
reaches the file. Standard users can then run
|
||||
`ctrld log send` without `sudo`. **Every local-user process can trigger a
|
||||
machine-wide diagnostic upload to Control D**, not just the interactive user.
|
||||
This grants upload permission, not permission to read logs or administer ctrld.
|
||||
`--full`, `log view`, `log tail`, and other privileged commands remain restricted.
|
||||
Other platforms retain their existing behavior.
|
||||
|
||||
This requires Control D mode, an on-disk TOML configuration, and file-backed logs.
|
||||
This requires Control D mode (`--cd`) with a custom configuration, and file-backed logs.
|
||||
The config, log files, rotated logs, and every parent directory must be root-owned,
|
||||
not group/world writable, and free of extended ACLs. Symlinks and hard-linked log
|
||||
files are refused; only the standard macOS `/var`, `/etc`, and `/tmp` aliases are
|
||||
|
||||
Reference in new issue
Block a user