mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
Apply managed DNS mode in the macOS package
This commit is contained in:
+25
-8
@@ -147,6 +147,25 @@ func isMobile() bool {
|
||||
return runtime.GOOS == "android" || runtime.GOOS == "ios"
|
||||
}
|
||||
|
||||
func updateConfigInterceptMode(cfg *ctrld.Config, mode string) bool {
|
||||
desired := ""
|
||||
switch mode {
|
||||
case "dns", "hard":
|
||||
desired = mode
|
||||
case "off":
|
||||
desired = ""
|
||||
case "":
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if cfg.Service.InterceptMode == desired {
|
||||
return false
|
||||
}
|
||||
cfg.Service.InterceptMode = desired
|
||||
return true
|
||||
}
|
||||
|
||||
// isAndroid reports whether the current OS is Android.
|
||||
func isAndroid() bool {
|
||||
return runtime.GOOS == "android"
|
||||
@@ -361,14 +380,12 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
processLogAndCacheFlags(v, &cfg)
|
||||
}
|
||||
|
||||
// Persist intercept_mode to config when provided via CLI flag on full install.
|
||||
// This ensures the config file reflects the actual running mode for RMM/MDM visibility.
|
||||
if interceptMode == "dns" || interceptMode == "hard" {
|
||||
if cfg.Service.InterceptMode != interceptMode {
|
||||
cfg.Service.InterceptMode = interceptMode
|
||||
updated = true
|
||||
mainLog.Load().Info().Msgf("writing intercept_mode = %q to config", interceptMode)
|
||||
}
|
||||
// Keep config and the explicit CLI/service mode in sync. In particular, "off"
|
||||
// must clear a previously persisted dns/hard value or the next service start
|
||||
// would silently re-enable interception from config.
|
||||
if updateConfigInterceptMode(&cfg, interceptMode) {
|
||||
updated = true
|
||||
mainLog.Load().Info().Msgf("writing intercept_mode = %q to config", cfg.Service.InterceptMode)
|
||||
}
|
||||
|
||||
if updated {
|
||||
|
||||
+13
-11
@@ -394,21 +394,23 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
svcExists := serviceConfigFileExists()
|
||||
mainLog.Load().Debug().Msgf("intercept upgrade check: args=%v interceptOnly=%v svcConfigExists=%v interceptMode=%q", osArgsEarly, interceptOnly, svcExists, interceptMode)
|
||||
if interceptOnly && svcExists {
|
||||
// Remove any existing intercept flags before applying the new value.
|
||||
_ = removeServiceFlag("--intercept-mode")
|
||||
// Replace any existing split or --intercept-mode=<value> form. Keep an
|
||||
// explicit "off" argument so it overrides a previously persisted config
|
||||
// value while the service clears that value on startup.
|
||||
if err := removeServiceFlag("--intercept-mode"); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to remove existing intercept mode from service arguments")
|
||||
}
|
||||
|
||||
if interceptMode == "off" {
|
||||
// "off" = remove intercept mode entirely (just the removal above).
|
||||
mainLog.Load().Notice().Msg("Existing service detected — removing --intercept-mode from service arguments")
|
||||
mainLog.Load().Notice().Msg("Existing service detected — disabling intercept mode")
|
||||
} else {
|
||||
// Add the new mode value.
|
||||
mainLog.Load().Notice().Msgf("Existing service detected — appending --intercept-mode %s to service arguments", interceptMode)
|
||||
if err := appendServiceFlag("--intercept-mode"); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to append intercept flag to service arguments")
|
||||
}
|
||||
if err := appendServiceFlag(interceptMode); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to append intercept mode value to service arguments")
|
||||
}
|
||||
}
|
||||
if err := appendServiceFlag("--intercept-mode"); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to append intercept flag to service arguments")
|
||||
}
|
||||
if err := appendServiceFlag(interceptMode); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to append intercept mode value to service arguments")
|
||||
}
|
||||
|
||||
// Stop the service if running (bypasses ctrld pin — this is an
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
func TestUpdateConfigInterceptMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
current string
|
||||
mode string
|
||||
want string
|
||||
wantUpdated bool
|
||||
}{
|
||||
{name: "empty flag preserves config", current: "dns", mode: "", want: "dns"},
|
||||
{name: "dns is persisted", mode: "dns", want: "dns", wantUpdated: true},
|
||||
{name: "hard is persisted", current: "dns", mode: "hard", want: "hard", wantUpdated: true},
|
||||
{name: "off clears persisted mode", current: "dns", mode: "off", want: "", wantUpdated: true},
|
||||
{name: "off is idempotent", mode: "off", want: ""},
|
||||
{name: "invalid flag preserves config", current: "hard", mode: "invalid", want: "hard"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := &ctrld.Config{}
|
||||
cfg.Service.InterceptMode = tc.current
|
||||
updated := updateConfigInterceptMode(cfg, tc.mode)
|
||||
if updated != tc.wantUpdated {
|
||||
t.Fatalf("updateConfigInterceptMode() updated = %v, want %v", updated, tc.wantUpdated)
|
||||
}
|
||||
if cfg.Service.InterceptMode != tc.want {
|
||||
t.Fatalf("service.intercept_mode = %q, want %q", cfg.Service.InterceptMode, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -5,12 +5,34 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var logOutput strings.Builder
|
||||
// logOutput is the log sink for the whole test binary. Tests share it with any
|
||||
// background goroutine the code under test starts (watchdogs, timers), so it
|
||||
// must tolerate concurrent writes.
|
||||
var logOutput syncBuffer
|
||||
|
||||
// syncBuffer is a strings.Builder guarded by a mutex.
|
||||
type syncBuffer struct {
|
||||
mu sync.Mutex
|
||||
sb strings.Builder
|
||||
}
|
||||
|
||||
func (b *syncBuffer) Write(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.sb.Write(p)
|
||||
}
|
||||
|
||||
func (b *syncBuffer) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.sb.String()
|
||||
}
|
||||
|
||||
// envFakeVersionOutput makes this test binary impersonate a ctrld executable: when
|
||||
// set, the process writes the value to stdout and exits without running any test, so
|
||||
|
||||
+3
-2
@@ -930,11 +930,12 @@ func (p *prog) setDNS() {
|
||||
|
||||
// Validate and resolve intercept mode.
|
||||
// CLI flag (--intercept-mode) takes priority over config file.
|
||||
// Valid values: "" (off), "dns" (with VPN split routing), "hard" (all DNS through ctrld).
|
||||
// Valid values: "" (use config), "off" (explicitly disable), "dns" (with VPN
|
||||
// split routing), and "hard" (all DNS through ctrld).
|
||||
if interceptMode != "" && !validInterceptMode(interceptMode) {
|
||||
mainLog.Load().Fatal().Msgf("invalid --intercept-mode value %q: must be 'off', 'dns', or 'hard'", interceptMode)
|
||||
}
|
||||
if interceptMode == "" || interceptMode == "off" {
|
||||
if interceptMode == "" {
|
||||
interceptMode = p.configuredInterceptMode()
|
||||
if interceptMode != "" && interceptMode != "off" {
|
||||
mainLog.Load().Info().Msgf("Intercept mode enabled via config (intercept_mode = %q)", interceptMode)
|
||||
|
||||
@@ -146,6 +146,22 @@ func (h *interceptFallbackHarness) run(t *testing.T) {
|
||||
p.setDNS()
|
||||
}
|
||||
|
||||
func TestSetDNSExplicitOffOverridesConfig(t *testing.T) {
|
||||
h := newInterceptFallbackHarness(t, &ctrld.ListenerConfig{IP: "127.0.0.1", Port: 53})
|
||||
interceptMode = "off"
|
||||
dnsIntercept = false
|
||||
hardIntercept = false
|
||||
|
||||
h.run(t)
|
||||
|
||||
if h.interceptCalls != 0 {
|
||||
t.Fatalf("intercept start called %d time(s), want 0: explicit off must override service.intercept_mode", h.interceptCalls)
|
||||
}
|
||||
if h.installCalls != 1 {
|
||||
t.Fatalf("interface DNS installed %d time(s), want 1", h.installCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetDNSRefusesUnreachableFallback is the behaviour test for the reported outage: it
|
||||
// drives the real setDNS() lifecycle rather than the classification helper alone.
|
||||
//
|
||||
|
||||
@@ -162,6 +162,11 @@ func (s *systemd) Start() error {
|
||||
// This is necessary for running self-upgrade flow.
|
||||
func ensureSystemdKillMode(r io.Reader) (opts []*unit.UnitOption, change bool) {
|
||||
opts, err := unit.DeserializeOptions(r)
|
||||
// staticcheck sees only the explicit non-nil sends on the lexer's error
|
||||
// channel, so it reports this comparison as always true. On success the
|
||||
// lexer sends nothing and closes the channel, so the receive yields a nil
|
||||
// error and this branch is not taken.
|
||||
//lint:ignore SA4023 upstream delivers a nil error by closing the channel
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to deserialize options")
|
||||
return
|
||||
|
||||
@@ -24,19 +24,19 @@ func serviceConfigFileExists() bool {
|
||||
// to intercept mode without losing the existing --cd flag and other arguments.
|
||||
//
|
||||
// On macOS, this modifies the launchd plist at /Library/LaunchDaemons/ctrld.plist
|
||||
// using the "defaults" command, which is the standard way to edit plists.
|
||||
// using PlistBuddy for exact array reads and writes.
|
||||
//
|
||||
// The function is idempotent: if the flag already exists, it's a no-op.
|
||||
func appendServiceFlag(flag string) error {
|
||||
// Read current ProgramArguments from plist.
|
||||
out, err := exec.Command("defaults", "read", launchdPlistPath, "ProgramArguments").CombinedOutput()
|
||||
out, err := exec.Command("/usr/libexec/PlistBuddy", "-c", "Print :ProgramArguments", launchdPlistPath).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read plist ProgramArguments: %w (output: %s)", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
// Check if the flag is already present (idempotent).
|
||||
args := string(out)
|
||||
if strings.Contains(args, flag) {
|
||||
// Check exact array entries. A substring match can confuse a mode such as "off"
|
||||
// with an unrelated path or argument and leave the flag without its value.
|
||||
if serviceArgumentPresent(out, flag) {
|
||||
mainLog.Load().Debug().Msgf("Service flag %q already present in plist, skipping", flag)
|
||||
return nil
|
||||
}
|
||||
@@ -61,9 +61,8 @@ func verifyServiceRegistration() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeServiceFlag removes a CLI flag (and its value, if the next argument is not
|
||||
// a flag) from the installed service's launch arguments. For example, removing
|
||||
// "--intercept-mode" also removes the following "dns" or "hard" value argument.
|
||||
// removeServiceFlag removes both "--flag value" and "--flag=value" forms from the
|
||||
// installed service's launch arguments.
|
||||
//
|
||||
// The function is idempotent: if the flag doesn't exist, it's a no-op.
|
||||
func removeServiceFlag(flag string) error {
|
||||
@@ -92,22 +91,14 @@ func removeServiceFlag(flag string) error {
|
||||
entries = append(entries, trimmed)
|
||||
}
|
||||
|
||||
index := -1
|
||||
for i, entry := range entries {
|
||||
if entry == flag {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
index, hasValue := serviceFlagPosition(entries, flag)
|
||||
|
||||
if index < 0 {
|
||||
mainLog.Load().Debug().Msgf("Service flag %q not present in plist, skipping removal", flag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the next entry is a value (not a flag). If so, delete it first
|
||||
// (deleting by index shifts subsequent entries down, so delete value before flag).
|
||||
hasValue := index+1 < len(entries) && !strings.HasPrefix(entries[index+1], "-")
|
||||
// Delete a separate value first. An inline --flag=value entry is one array item.
|
||||
if hasValue {
|
||||
delVal := exec.Command(
|
||||
"/usr/libexec/PlistBuddy",
|
||||
@@ -132,3 +123,24 @@ func removeServiceFlag(flag string) error {
|
||||
mainLog.Load().Info().Msgf("Removed %q from service launch arguments", flag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceArgumentPresent(out []byte, argument string) bool {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if strings.TrimSpace(line) == argument {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func serviceFlagPosition(entries []string, flag string) (index int, hasValue bool) {
|
||||
for i, entry := range entries {
|
||||
switch {
|
||||
case entry == flag:
|
||||
return i, i+1 < len(entries) && !strings.HasPrefix(entries[i+1], "-")
|
||||
case strings.HasPrefix(entry, flag+"="):
|
||||
return i, false
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//go:build darwin
|
||||
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestServiceArgumentPresent(t *testing.T) {
|
||||
out := []byte("Array {\n /usr/local/bin/ctrld\n run\n --config=/Users/officer/ctrld.toml\n --intercept-mode=dns\n}\n")
|
||||
if !serviceArgumentPresent(out, "--intercept-mode=dns") {
|
||||
t.Fatal("exact inline argument was not found")
|
||||
}
|
||||
if serviceArgumentPresent(out, "--intercept-mode") {
|
||||
t.Fatal("inline flag was mistaken for a separate flag argument")
|
||||
}
|
||||
if serviceArgumentPresent(out, "off") {
|
||||
t.Fatal("substring in an unrelated path was mistaken for the off argument")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceFlagPosition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
entries []string
|
||||
wantIndex int
|
||||
wantHasValue bool
|
||||
}{
|
||||
{
|
||||
name: "split form",
|
||||
entries: []string{"run", "--cd=uid", "--intercept-mode", "dns"},
|
||||
wantIndex: 2,
|
||||
wantHasValue: true,
|
||||
},
|
||||
{
|
||||
name: "inline form",
|
||||
entries: []string{"run", "--cd=uid", "--intercept-mode=dns"},
|
||||
wantIndex: 2,
|
||||
},
|
||||
{
|
||||
name: "flag followed by another flag",
|
||||
entries: []string{"run", "--intercept-mode", "--config=/etc/ctrld.toml"},
|
||||
wantIndex: 1,
|
||||
},
|
||||
{
|
||||
name: "absent",
|
||||
entries: []string{"run", "--cd=uid"},
|
||||
wantIndex: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
index, hasValue := serviceFlagPosition(tc.entries, "--intercept-mode")
|
||||
if index != tc.wantIndex || hasValue != tc.wantHasValue {
|
||||
t.Fatalf("serviceFlagPosition() = (%d, %v), want (%d, %v)", index, hasValue, tc.wantIndex, tc.wantHasValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// errServiceFlagsUnsupported is returned by the service-argument helpers on
|
||||
// platforms that do not store service arguments in a file ctrld can rewrite.
|
||||
var errServiceFlagsUnsupported = errors.New("modifying service flags is not supported on this platform; use intercept_mode in config instead")
|
||||
|
||||
// serviceConfigFileExists checks common service config file locations on Linux.
|
||||
func serviceConfigFileExists() bool {
|
||||
// systemd unit file
|
||||
@@ -24,7 +28,7 @@ func serviceConfigFileExists() bool {
|
||||
// Linux services (systemd) store args in unit files; intercept mode
|
||||
// should be set via the config file (intercept_mode) on these platforms.
|
||||
func appendServiceFlag(flag string) error {
|
||||
return fmt.Errorf("appending service flags is not supported on this platform; use intercept_mode in config instead")
|
||||
return errServiceFlagsUnsupported
|
||||
}
|
||||
|
||||
// verifyServiceRegistration is a no-op on this platform.
|
||||
@@ -34,5 +38,5 @@ func verifyServiceRegistration() error {
|
||||
|
||||
// removeServiceFlag is not yet implemented on this platform.
|
||||
func removeServiceFlag(flag string) error {
|
||||
return fmt.Errorf("removing service flags is not supported on this platform; use intercept_mode in config instead")
|
||||
return errServiceFlagsUnsupported
|
||||
}
|
||||
|
||||
@@ -47,8 +47,9 @@ func appendServiceFlag(flag string) error {
|
||||
return fmt.Errorf("failed to read service config: %w", err)
|
||||
}
|
||||
|
||||
// Check if flag already present (idempotent).
|
||||
if strings.Contains(config.BinaryPathName, flag) {
|
||||
// Check exact arguments so a short mode such as "off" is not confused with
|
||||
// an unrelated path or value.
|
||||
if binaryPathArgumentPresent(config.BinaryPathName, flag) {
|
||||
mainLog.Load().Debug().Msgf("Service flag %q already present in BinPath, skipping", flag)
|
||||
return nil
|
||||
}
|
||||
@@ -103,9 +104,8 @@ func verifyServiceRegistration() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeServiceFlag removes a CLI flag (and its value, if present) from the installed
|
||||
// Windows service's BinPath. For example, removing "--intercept-mode" also removes
|
||||
// the following "dns" or "hard" value. The function is idempotent.
|
||||
// removeServiceFlag removes both "--flag value" and "--flag=value" forms from the
|
||||
// installed Windows service's BinPath. The function is idempotent.
|
||||
func removeServiceFlag(flag string) error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
@@ -124,25 +124,12 @@ func removeServiceFlag(flag string) error {
|
||||
return fmt.Errorf("failed to read service config: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(config.BinaryPathName, flag) {
|
||||
updatedPath, removed := removeBinaryPathFlag(config.BinaryPathName, flag)
|
||||
if !removed {
|
||||
mainLog.Load().Debug().Msgf("Service flag %q not present in BinPath, skipping removal", flag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Split BinPath into parts, find and remove the flag + its value (if any).
|
||||
parts := strings.Fields(config.BinaryPathName)
|
||||
var newParts []string
|
||||
for i := 0; i < len(parts); i++ {
|
||||
if parts[i] == flag {
|
||||
// Skip the flag. Also skip the next part if it's a value (not a flag).
|
||||
if i+1 < len(parts) && !strings.HasPrefix(parts[i+1], "-") {
|
||||
i++ // skip value too
|
||||
}
|
||||
continue
|
||||
}
|
||||
newParts = append(newParts, parts[i])
|
||||
}
|
||||
config.BinaryPathName = strings.Join(newParts, " ")
|
||||
config.BinaryPathName = updatedPath
|
||||
|
||||
if err := s.UpdateConfig(config); err != nil {
|
||||
return fmt.Errorf("failed to update service config: %w", err)
|
||||
@@ -151,3 +138,32 @@ func removeServiceFlag(flag string) error {
|
||||
mainLog.Load().Info().Msgf("Removed %q from service BinPath", flag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func binaryPathArgumentPresent(binaryPath, argument string) bool {
|
||||
for _, part := range strings.Fields(binaryPath) {
|
||||
if part == argument {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func removeBinaryPathFlag(binaryPath, flag string) (string, bool) {
|
||||
parts := strings.Fields(binaryPath)
|
||||
newParts := make([]string, 0, len(parts))
|
||||
removed := false
|
||||
for i := 0; i < len(parts); i++ {
|
||||
switch {
|
||||
case parts[i] == flag:
|
||||
removed = true
|
||||
if i+1 < len(parts) && !strings.HasPrefix(parts[i+1], "-") {
|
||||
i++
|
||||
}
|
||||
case strings.HasPrefix(parts[i], flag+"="):
|
||||
removed = true
|
||||
default:
|
||||
newParts = append(newParts, parts[i])
|
||||
}
|
||||
}
|
||||
return strings.Join(newParts, " "), removed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//go:build windows
|
||||
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBinaryPathArgumentPresent(t *testing.T) {
|
||||
path := `C:\ControlD\ctrld.exe run --config=C:\Users\officer\ctrld.toml --intercept-mode=dns`
|
||||
if !binaryPathArgumentPresent(path, "--intercept-mode=dns") {
|
||||
t.Fatal("exact inline argument was not found")
|
||||
}
|
||||
if binaryPathArgumentPresent(path, "--intercept-mode") {
|
||||
t.Fatal("inline flag was mistaken for a separate flag argument")
|
||||
}
|
||||
if binaryPathArgumentPresent(path, "off") {
|
||||
t.Fatal("substring in an unrelated path was mistaken for the off argument")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveBinaryPathFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
binaryPath string
|
||||
wantPath string
|
||||
wantRemoved bool
|
||||
}{
|
||||
{
|
||||
name: "split form",
|
||||
binaryPath: `ctrld.exe run --cd=uid --intercept-mode dns --config=ctrld.toml`,
|
||||
wantPath: `ctrld.exe run --cd=uid --config=ctrld.toml`,
|
||||
wantRemoved: true,
|
||||
},
|
||||
{
|
||||
name: "inline form",
|
||||
binaryPath: `ctrld.exe run --cd=uid --intercept-mode=dns --config=ctrld.toml`,
|
||||
wantPath: `ctrld.exe run --cd=uid --config=ctrld.toml`,
|
||||
wantRemoved: true,
|
||||
},
|
||||
{
|
||||
name: "absent",
|
||||
binaryPath: `ctrld.exe run --cd=uid`,
|
||||
wantPath: `ctrld.exe run --cd=uid`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
path, removed := removeBinaryPathFlag(tc.binaryPath, "--intercept-mode")
|
||||
if path != tc.wantPath || removed != tc.wantRemoved {
|
||||
t.Fatalf("removeBinaryPathFlag() = (%q, %v), want (%q, %v)", path, removed, tc.wantPath, tc.wantRemoved)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
||||
postinstall="$repo_root/scripts/pkg/postinstall"
|
||||
fixture=$(mktemp -d "${TMPDIR:-/tmp}/ctrld-pkg-intercept.XXXXXX")
|
||||
trap 'rm -rf "$fixture"' EXIT HUP INT TERM
|
||||
|
||||
bin="$fixture/bin"
|
||||
mkdir -p "$bin"
|
||||
|
||||
cat >"$bin/defaults" <<'EOF'
|
||||
#!/bin/sh
|
||||
key=${3:-}
|
||||
case "$key" in
|
||||
ProvisionToken)
|
||||
[ "${FAKE_TOKEN_PRESENT:-0}" = "1" ] || exit 1
|
||||
printf '%s\n' "${FAKE_TOKEN:-test-token}"
|
||||
;;
|
||||
InterceptMode)
|
||||
[ "${FAKE_MODE_PRESENT:-0}" = "1" ] || exit 1
|
||||
printf '%s\n' "${FAKE_MODE:-}"
|
||||
;;
|
||||
CustomHostname|UseDevEnvironment)
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
cat >"$bin/launchctl" <<'EOF'
|
||||
#!/bin/sh
|
||||
printf 'launchctl %s\n' "$*" >>"$CALLS"
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
cat >"$bin/ctrld" <<'EOF'
|
||||
#!/bin/sh
|
||||
printf 'ctrld %s\n' "$*" >>"$CALLS"
|
||||
case " $* " in
|
||||
*" --cd-org="*) : >"$CTRLD_POSTINSTALL_PLIST" ;;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
chmod +x "$bin/defaults" "$bin/launchctl" "$bin/ctrld"
|
||||
|
||||
assert_contains() {
|
||||
expected=$1
|
||||
file=$2
|
||||
if ! grep -Fq -- "$expected" "$file"; then
|
||||
printf 'FAIL: expected %s in %s\n' "$expected" "$file" >&2
|
||||
sed -n '1,120p' "$file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
unexpected=$1
|
||||
file=$2
|
||||
if grep -Fq -- "$unexpected" "$file"; then
|
||||
printf 'FAIL: did not expect %s in %s\n' "$unexpected" "$file" >&2
|
||||
sed -n '1,120p' "$file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_case() {
|
||||
name=$1
|
||||
existing=$2
|
||||
mode_present=$3
|
||||
mode=$4
|
||||
case_dir="$fixture/$name"
|
||||
mkdir -p "$case_dir"
|
||||
plist="$case_dir/ctrld.plist"
|
||||
prefs="$case_dir/preferences"
|
||||
calls="$case_dir/calls"
|
||||
output="$case_dir/output"
|
||||
: >"$calls"
|
||||
if [ "$existing" = "1" ]; then
|
||||
: >"$plist"
|
||||
fi
|
||||
|
||||
PATH="$bin:$PATH" \
|
||||
CALLS="$calls" \
|
||||
FAKE_TOKEN_PRESENT=1 \
|
||||
FAKE_TOKEN=test-token \
|
||||
FAKE_MODE_PRESENT="$mode_present" \
|
||||
FAKE_MODE="$mode" \
|
||||
CTRLD_POSTINSTALL_PLIST="$plist" \
|
||||
CTRLD_POSTINSTALL_CTRLD="$bin/ctrld" \
|
||||
CTRLD_POSTINSTALL_PREFS="$prefs" \
|
||||
"$postinstall" >"$output" 2>&1
|
||||
|
||||
printf '%s\n' "$case_dir"
|
||||
}
|
||||
|
||||
case_dir=$(run_case fresh-legacy 0 0 '')
|
||||
assert_contains 'ctrld start --cd-org=test-token' "$case_dir/calls"
|
||||
assert_not_contains '--intercept-mode' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case fresh-standard 0 1 standard)
|
||||
assert_contains 'ctrld start --cd-org=test-token' "$case_dir/calls"
|
||||
assert_not_contains '--intercept-mode' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case fresh-intercept 0 1 intercept-dns)
|
||||
assert_contains 'ctrld start --cd-org=test-token --intercept-mode dns' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case upgrade-legacy 1 0 '')
|
||||
assert_contains 'launchctl load' "$case_dir/calls"
|
||||
assert_not_contains 'ctrld start' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case upgrade-standard 1 1 standard)
|
||||
assert_contains 'ctrld start --intercept-mode off' "$case_dir/calls"
|
||||
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case upgrade-intercept 1 1 intercept-dns)
|
||||
assert_contains 'ctrld start --intercept-mode dns' "$case_dir/calls"
|
||||
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||
|
||||
printf 'PASS: pkg postinstall preserves legacy mode and applies standard/intercept-dns policy\n'
|
||||
Reference in New Issue
Block a user