mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-24 14:08:27 +02:00
When no intercept mode is selected by flag or config, a new installation on macOS or a Windows desktop edition uses "dns". Windows Server, iOS and other platforms keep interface DNS mode unless intercept is selected explicitly, and so does a config that cannot run in intercept mode. Existing Standard installations are not switched on upgrade: ctrld-client start marks the services it installs fresh, a service with neither a mode nor the marker keeps interface DNS mode, and the macOS package pins such a service to "off" explicitly. An explicit "off" stays final and is persisted. Master adaptation: the run flag and start install hunk move from commands.go to commands_run.go and commands_service_start.go and use the start command's logger and svcConfig; setDNS logs through p.Info(); the flag test builds the run command with InitRunCmd; user-facing commands, docs and the pkg test use the ctrld-client binary name.
216 lines
8.8 KiB
Go
216 lines
8.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/kardianos/service"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
// interceptByDefault reports whether this host intercepts DNS when no mode is
|
|
// selected: the supported desktop platforms, macOS and Windows workstation
|
|
// editions. Windows Server is excluded: a server, a domain controller running
|
|
// its own DNS server in particular, keeps interface DNS mode unless intercept
|
|
// is selected explicitly, which still works there. Elsewhere intercept is not
|
|
// supported at all. It is a variable so tests pin both behaviors on any host.
|
|
var interceptByDefault = interceptByDefaultOn(runtime.GOOS, ctrld.IsDesktopPlatform())
|
|
|
|
// interceptByDefaultOn is interceptByDefault for a given GOOS. iOS builds the
|
|
// darwin files, so ctrld.IsDesktopPlatform reports it as a desktop, but it has
|
|
// no pf to intercept with.
|
|
func interceptByDefaultOn(goos string, desktop bool) bool {
|
|
return desktop && goos != "ios"
|
|
}
|
|
|
|
// interceptBlockedByOSUpstream reports whether intercept mode cannot serve an
|
|
// OS-resolver upstream on this platform. On macOS, pf redirects ctrld's own
|
|
// port-53 queries to such an upstream back to ctrld. It is a variable so tests
|
|
// pin both behaviors on any host.
|
|
var interceptBlockedByOSUpstream = runtime.GOOS == "darwin" || runtime.GOOS == "ios"
|
|
|
|
// interceptConfigBlocker reports why cfg cannot run in intercept mode on this
|
|
// platform, or nil. An explicit selection of intercept mode fails on it in
|
|
// validateDNSIntercept; the default does not select intercept mode for it.
|
|
func interceptConfigBlocker(cfg *ctrld.Config) error {
|
|
if !interceptBlockedByOSUpstream || cfg == nil {
|
|
return nil
|
|
}
|
|
for name, uc := range cfg.Upstream {
|
|
if uc != nil && (uc.Type == ctrld.ResolverTypeOS || uc.Type == "") {
|
|
return fmt.Errorf("dns intercept: upstream %q uses OS resolver (port 53) which would create "+
|
|
"a redirect loop with pf. Use DoH upstreams (--proto doh) with dns-intercept mode", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Where the effective intercept mode came from, for logs and status output.
|
|
const (
|
|
interceptModeSourceFlag = "flag"
|
|
interceptModeSourceConfig = "config"
|
|
interceptModeSourceDefault = "default"
|
|
)
|
|
|
|
// interceptModeSource records where setDNS took the effective mode from. setDNS
|
|
// replaces interceptMode with the resolved value, so the flag alone no longer
|
|
// tells an explicit selection from the platform default afterwards.
|
|
var interceptModeSource string
|
|
|
|
// runningAsService reports whether this process runs as the installed service,
|
|
// started by the OS service manager, rather than interactively. It is a
|
|
// variable so tests pin both behaviors.
|
|
var runningAsService = func() bool { return !service.Interactive() }
|
|
|
|
// interceptModeDefaultFlag marks a service that "ctrld start" installed with
|
|
// no intercept mode selected: the service applies the platform default
|
|
// (defaultInterceptMode). It is recorded as one --intercept-mode-default=true
|
|
// argument. Every ctrld run since 2023 ignores unknown flags, so the marker is
|
|
// safe for a downgraded binary, and the inline value keeps an older parser
|
|
// from taking the next argument as its value.
|
|
const interceptModeDefaultFlag = "--intercept-mode-default"
|
|
|
|
// interceptModeDefault is set by the hidden --intercept-mode-default flag of
|
|
// "ctrld run".
|
|
var interceptModeDefault bool
|
|
|
|
// errInterceptDefaultLegacyService is why an installed service that records
|
|
// no intercept mode keeps interface DNS mode.
|
|
var errInterceptDefaultLegacyService = errors.New("the service records no intercept mode, so it was installed " +
|
|
"before dns intercept mode became the default; keeping interface DNS mode")
|
|
|
|
// platformDefaultInterceptMode is the mode a new installation, or a foreground
|
|
// run, uses for cfg when neither --intercept-mode nor service.intercept_mode
|
|
// selects one: "dns" on the supported desktop platforms, and none (interface
|
|
// DNS mode) elsewhere, so Windows Server keeps its DNS setup and an
|
|
// unsupported platform never enters an intercept start that is known to fail.
|
|
//
|
|
// The default also steps back to interface DNS mode for a config that cannot
|
|
// run in intercept mode, and returns the reason: a config that worked in
|
|
// interface DNS mode must not start failing because the default changed.
|
|
func platformDefaultInterceptMode(cfg *ctrld.Config) (mode string, blocked error) {
|
|
if !interceptByDefault {
|
|
return "", nil
|
|
}
|
|
if err := interceptConfigBlocker(cfg); err != nil {
|
|
return "", err
|
|
}
|
|
return "dns", nil
|
|
}
|
|
|
|
// defaultInterceptMode is the mode this process uses for cfg when neither
|
|
// --intercept-mode nor service.intercept_mode selects one. It ranks below both,
|
|
// so a mode in a Control D custom config or a --base64_config still wins.
|
|
//
|
|
// An installed service gets the platform default only if "ctrld start"
|
|
// marked it as a fresh install with --intercept-mode-default
|
|
// (installInterceptMode). A service with neither a mode nor the marker was
|
|
// installed before dns intercept mode became the default, in what was then
|
|
// Standard mode. It keeps interface DNS mode: an upgrade, whichever path
|
|
// performs it, must not silently switch an existing Standard install to
|
|
// intercept mode.
|
|
//
|
|
// The default is decided here, against the config the service runs with,
|
|
// which in Control D mode is only known once the service fetched it.
|
|
func defaultInterceptMode(cfg *ctrld.Config) (mode string, blocked error) {
|
|
if interceptByDefault && runningAsService() && !interceptModeDefault {
|
|
return "", errInterceptDefaultLegacyService
|
|
}
|
|
return platformDefaultInterceptMode(cfg)
|
|
}
|
|
|
|
// interceptModeFromArgs returns the --intercept-mode value in a service's
|
|
// arguments, in either the "--intercept-mode value" or the
|
|
// "--intercept-mode=value" form, and whether the flag is present.
|
|
func interceptModeFromArgs(args []string) (mode string, ok bool) {
|
|
for i, arg := range args {
|
|
switch {
|
|
case arg == "--intercept-mode":
|
|
if i+1 < len(args) {
|
|
return args[i+1], true
|
|
}
|
|
return "", true
|
|
case strings.HasPrefix(arg, "--intercept-mode="):
|
|
return strings.TrimPrefix(arg, "--intercept-mode="), true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// interceptDefaultFromArgs reports whether a service's arguments carry the
|
|
// --intercept-mode-default marker, as "--intercept-mode-default" or
|
|
// "--intercept-mode-default=<bool>".
|
|
func interceptDefaultFromArgs(args []string) bool {
|
|
for _, arg := range args {
|
|
switch {
|
|
case arg == interceptModeDefaultFlag:
|
|
return true
|
|
case strings.HasPrefix(arg, interceptModeDefaultFlag+"="):
|
|
on, err := strconv.ParseBool(strings.TrimPrefix(arg, interceptModeDefaultFlag+"="))
|
|
return err == nil && on
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// installInterceptMode decides what "ctrld start" records in the arguments of
|
|
// the service it installs when the command line selects no mode: an explicit
|
|
// mode, the --intercept-mode-default marker (markDefault), or nothing.
|
|
//
|
|
// Recording is what lets an installed service tell a fresh install from one
|
|
// that predates the dns default (defaultInterceptMode):
|
|
//
|
|
// - a fresh install: the marker. The service applies the platform default
|
|
// itself, below service.intercept_mode and against the config it fetches,
|
|
// so a mode or an os upstream in a Control D custom config still counts.
|
|
// - a reinstall over an existing service: keep what its arguments record,
|
|
// a mode or the marker. If they record neither, it was installed before the
|
|
// default and ran in Standard mode, so record "off". existingArgs is nil
|
|
// when they could not be read; that also records "off", so a read failure
|
|
// cannot switch a Standard install to intercept mode. A reinstall whose
|
|
// config sets service.intercept_mode records nothing: the config decides.
|
|
//
|
|
// Platforms without the dns default record nothing, as before.
|
|
func installInterceptMode(flag string, cfg *ctrld.Config, installed bool, existingArgs []string) (mode string, markDefault bool) {
|
|
if flag != "" || !interceptByDefault {
|
|
return "", false
|
|
}
|
|
if !installed {
|
|
return "", true
|
|
}
|
|
if mode, ok := interceptModeFromArgs(existingArgs); ok && validInterceptMode(mode) {
|
|
return mode, false
|
|
}
|
|
if interceptDefaultFromArgs(existingArgs) {
|
|
return "", true
|
|
}
|
|
if cfg != nil && cfg.Service.InterceptMode != "" {
|
|
return "", false
|
|
}
|
|
return "off", false
|
|
}
|
|
|
|
// resolveInterceptMode applies the intercept mode precedence: an explicit
|
|
// --intercept-mode value, then service.intercept_mode, then the platform
|
|
// default for cfg. "off" at either explicit level is a selection like "dns" or
|
|
// "hard", and stops the lookup: it selects interface DNS mode even where the
|
|
// default is "dns".
|
|
func resolveInterceptMode(flag string, cfg *ctrld.Config) (mode, source string) {
|
|
configured := ""
|
|
if cfg != nil {
|
|
configured = cfg.Service.InterceptMode
|
|
}
|
|
switch {
|
|
case flag != "":
|
|
return flag, interceptModeSourceFlag
|
|
case configured != "":
|
|
return configured, interceptModeSourceConfig
|
|
}
|
|
mode, _ = defaultInterceptMode(cfg)
|
|
return mode, interceptModeSourceDefault
|
|
}
|