From 47dd64b901eb77cc7944029f20f9d4b3da8f38c5 Mon Sep 17 00:00:00 2001 From: Anthony Wong Date: Fri, 21 Aug 2026 09:56:45 -0400 Subject: [PATCH] fix(cli): treat explicit intercept-mode off as final in listener setup tryUpdateListenerConfig treated an explicit --intercept-mode off the same as an empty flag and fell back to the persisted config value. run() selects the listener strategy before it clears the persisted mode, so the first start after a revert to standard mode selected the intercept strategy from a stale dns/hard value while setDNS kept interception off. Extract the resolution into listenerInterceptMode and make an explicit off final, the same contract as setDNS. Add a regression test that fails without the fix. --- cmd/cli/cli.go | 17 +++++++++---- cmd/cli/listener_intercept_mode_test.go | 34 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 cmd/cli/listener_intercept_mode_test.go diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 303c1d4..b3690ae 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1605,6 +1605,17 @@ func isExplicitInterceptListener(ip string, port int) bool { return !(ip == "127.0.0.1" && port == 53) } +// listenerInterceptMode resolves the mode that selects the listener binding +// strategy. An explicit "off" is final here, the same as in setDNS. A fallback +// to the config value would select the intercept strategy from a stale +// persisted mode on the first start after a revert to standard mode. +func listenerInterceptMode(cfg *ctrld.Config) string { + if interceptMode == "" { + return cfg.Service.InterceptMode + } + return interceptMode +} + // tryUpdateListenerConfig tries updating listener config with a working one. // If fatal is true, and there's listen address conflicted, the function do // fatal error. @@ -1614,13 +1625,9 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( // 1. If config has explicit non-default IP:port, use exactly that // 2. Otherwise: try 127.0.0.1:53, then 127.0.0.1:5354, then fatal // This bypasses the full cd-mode listener probing loop entirely. - // Check interceptMode (CLI flag) first, then fall back to config value. // dnsIntercept bool is derived later in prog.run(), but we need to know // the intercept mode here to select the right listener probing strategy. - im := interceptMode - if im == "" || im == "off" { - im = cfg.Service.InterceptMode - } + im := listenerInterceptMode(cfg) if (im == "dns" || im == "hard") && runtime.GOOS == "darwin" { return tryUpdateListenerConfigIntercept(cfg, notifyFunc, fatal) } diff --git a/cmd/cli/listener_intercept_mode_test.go b/cmd/cli/listener_intercept_mode_test.go new file mode 100644 index 0000000..b762432 --- /dev/null +++ b/cmd/cli/listener_intercept_mode_test.go @@ -0,0 +1,34 @@ +package cli + +import ( + "testing" + + "github.com/Control-D-Inc/ctrld" +) + +func TestListenerInterceptModeExplicitOff(t *testing.T) { + oldIntercept := interceptMode + t.Cleanup(func() { interceptMode = oldIntercept }) + + cfg := &ctrld.Config{} + cfg.Service.InterceptMode = "dns" + + tests := []struct { + name string + flag string + want string + }{ + {name: "explicit off is final", flag: "off", want: "off"}, + {name: "empty flag falls back to config", flag: "", want: "dns"}, + {name: "explicit dns wins over config", flag: "dns", want: "dns"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + interceptMode = tc.flag + if got := listenerInterceptMode(cfg); got != tc.want { + t.Fatalf("listenerInterceptMode() = %q, want %q", got, tc.want) + } + }) + } +}