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.
This commit is contained in:
Anthony Wong
2026-08-24 15:17:25 +07:00
committed by Cuong Manh Le
parent 5eb2db549b
commit 47dd64b901
2 changed files with 46 additions and 5 deletions
+12 -5
View File
@@ -1605,6 +1605,17 @@ func isExplicitInterceptListener(ip string, port int) bool {
return !(ip == "127.0.0.1" && port == 53) 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. // tryUpdateListenerConfig tries updating listener config with a working one.
// If fatal is true, and there's listen address conflicted, the function do // If fatal is true, and there's listen address conflicted, the function do
// fatal error. // 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 // 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 // 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. // 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 // dnsIntercept bool is derived later in prog.run(), but we need to know
// the intercept mode here to select the right listener probing strategy. // the intercept mode here to select the right listener probing strategy.
im := interceptMode im := listenerInterceptMode(cfg)
if im == "" || im == "off" {
im = cfg.Service.InterceptMode
}
if (im == "dns" || im == "hard") && runtime.GOOS == "darwin" { if (im == "dns" || im == "hard") && runtime.GOOS == "darwin" {
return tryUpdateListenerConfigIntercept(cfg, notifyFunc, fatal) return tryUpdateListenerConfigIntercept(cfg, notifyFunc, fatal)
} }
+34
View File
@@ -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)
}
})
}
}