mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
feat(cli): port provisioning hardening to v1.0
This commit is contained in:
1 parent
e109c9be33
commit
3cd20a3eef
26 files changed
+3706
-672
No files matched your search
+259
-65
@@ -24,6 +24,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/cuonglm/osinfo"
|
||||
@@ -140,6 +141,7 @@ func initCLI() {
|
||||
initClientsCmd()
|
||||
initUpgradeCmd()
|
||||
initLogCmd()
|
||||
initDiagCmd()
|
||||
}
|
||||
|
||||
// isMobile reports whether the current OS is a mobile platform.
|
||||
@@ -183,8 +185,12 @@ func isStableVersion(vs string) bool {
|
||||
// RunCobraCommand runs ctrld cli.
|
||||
func RunCobraCommand(cmd *cobra.Command) {
|
||||
noConfigStart = isNoConfigStart(cmd)
|
||||
checkStrFlagEmpty(cmd, cdUidFlagName)
|
||||
checkStrFlagEmpty(cmd, cdOrgFlagName)
|
||||
if !checkStrFlagEmpty(cmd, cdUidFlagName) {
|
||||
return
|
||||
}
|
||||
if !checkStrFlagEmpty(cmd, cdOrgFlagName) {
|
||||
return
|
||||
}
|
||||
run(nil, make(chan struct{}))
|
||||
}
|
||||
|
||||
@@ -223,7 +229,8 @@ func CheckDeactivationPin(pin int64, stopCh chan struct{}) int {
|
||||
// run runs ctrld cli with given app callback and stop channel.
|
||||
func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
if stopCh == nil {
|
||||
mainLog.Load().Fatal().Msg("stopCh is nil")
|
||||
failRunUnclassified(mainLog.Load().Error(), "run() called with a nil stop channel", nil)
|
||||
return
|
||||
}
|
||||
waitCh := make(chan struct{})
|
||||
p := &prog{
|
||||
@@ -261,14 +268,10 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
} else {
|
||||
mainLog.Load().Warn().Err(err).Msgf("unable to resolve socket address: %s", sockPath)
|
||||
}
|
||||
notifyExitToLogServer := func() {
|
||||
if p.logConn != nil {
|
||||
_, _ = p.logConn.Write([]byte(msgExit))
|
||||
}
|
||||
}
|
||||
|
||||
if daemon && runtime.GOOS == "windows" {
|
||||
mainLog.Load().Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.")
|
||||
failRunUnclassified(mainLog.Load().Error(), "cannot run in daemon mode; please install a Windows service", p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
|
||||
if !daemon {
|
||||
@@ -277,7 +280,8 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
go func() {
|
||||
s, err := newService(p, svcConfig)
|
||||
if err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed create new service")
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), fmt.Sprintf("failed to create new service: %v", err), p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
if err := s.Run(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to start service")
|
||||
@@ -288,18 +292,20 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
tryReadingConfig(writeDefaultConfig)
|
||||
|
||||
if err := readBase64Config(configBase64); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to read base64 config")
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), fmt.Sprintf("failed to read base64 config: %v", err), p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
processNoConfigFlags(noConfigStart)
|
||||
|
||||
// After s.Run() was called, if ctrld is going to be terminated for any reason,
|
||||
// write msgExit to p.logConn so others (like "ctrld start") won't have to wait for timeout.
|
||||
p.mu.Lock()
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
notifyExitToLogServer()
|
||||
mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err)
|
||||
}
|
||||
unmarshalErr := v.Unmarshal(&cfg)
|
||||
p.mu.Unlock()
|
||||
if unmarshalErr != nil {
|
||||
failRunUnclassified(mainLog.Load().Error(), fmt.Sprintf("failed to unmarshal config: %v", unmarshalErr), p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
|
||||
processLogAndCacheFlags(v, &cfg)
|
||||
|
||||
@@ -312,8 +318,8 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
|
||||
// Wait for network up.
|
||||
if !ctrldnet.Up() {
|
||||
notifyExitToLogServer()
|
||||
mainLog.Load().Fatal().Msg("network is not up yet")
|
||||
failRunUnclassified(mainLog.Load().Error(), "network is not up yet", p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
|
||||
p.router = router.New(&cfg, cdUID != "")
|
||||
@@ -327,8 +333,8 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
// time for validating server certificate. Some routers need NTP synchronization
|
||||
// to set the current time, so this check must happen before processCDFlags.
|
||||
if err := p.router.PreRun(); err != nil {
|
||||
notifyExitToLogServer()
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to perform router pre-run check")
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), fmt.Sprintf("failed to perform router pre-run check: %v", err), p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
|
||||
oldLogPath := cfg.Service.LogPath
|
||||
@@ -336,7 +342,9 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
cdUID = uid
|
||||
}
|
||||
if cdUID != "" {
|
||||
validateCdUpstreamProtocol()
|
||||
if !validateCdUpstreamProtocol(p.notifyExitToLogServer) {
|
||||
return
|
||||
}
|
||||
// Bound API preflight by the service lifetime. Without this, a stop request
|
||||
// arriving while the API is unreachable leaves this retry/backoff loop running
|
||||
// after "service stopped" was logged, so the process keeps working on behalf of
|
||||
@@ -353,7 +361,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
// not see a failed start and apply its restart policy to a service the
|
||||
// operator just asked to stop.
|
||||
mainLog.Load().Notice().Msg("stop requested while fetching resolver config, shutting down")
|
||||
notifyExitToLogServer()
|
||||
p.notifyExitToLogServer()
|
||||
return
|
||||
case pf.err != nil:
|
||||
if isMobile() {
|
||||
@@ -361,7 +369,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
return
|
||||
}
|
||||
|
||||
handleAPIPreflightFailure(p, pf.err, notifyExitToLogServer)
|
||||
handleAPIPreflightFailure(p, pf.err, p.notifyExitToLogServer)
|
||||
return
|
||||
default:
|
||||
p.mu.Lock()
|
||||
@@ -370,7 +378,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
}
|
||||
}
|
||||
|
||||
updated := updateListenerConfig(&cfg, notifyExitToLogServer)
|
||||
updated := updateListenerConfig(&cfg, p.notifyExitToLogServer)
|
||||
|
||||
// Bootstrap and listener binding both succeeded, so an earlier run's
|
||||
// recorded failure no longer describes this install.
|
||||
@@ -390,11 +398,10 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
|
||||
if updated {
|
||||
if err := writeConfigFile(&cfg); err != nil {
|
||||
notifyExitToLogServer()
|
||||
mainLog.Load().Fatal().Err(err).Msg("failed to write config file")
|
||||
} else {
|
||||
mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile)
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), fmt.Sprintf("failed to write config file: %v", err), p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile)
|
||||
}
|
||||
|
||||
if newLogPath := cfg.Service.LogPath; newLogPath != "" && oldLogPath != newLogPath {
|
||||
@@ -412,31 +419,28 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||
}
|
||||
|
||||
if err := validateConfig(&cfg); err != nil {
|
||||
notifyExitToLogServer()
|
||||
os.Exit(1)
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), fmt.Sprintf("invalid config: %v", err), p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
initCache()
|
||||
|
||||
if daemon {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to find the binary")
|
||||
notifyExitToLogServer()
|
||||
os.Exit(1)
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), "failed to find the binary", p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
curDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to get current working directory")
|
||||
notifyExitToLogServer()
|
||||
os.Exit(1)
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), "failed to get current working directory", p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
// If running as daemon, re-run the command in background, with daemon off.
|
||||
cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...)
|
||||
cmd.Dir = curDir
|
||||
if err := cmd.Start(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to start process as daemon")
|
||||
notifyExitToLogServer()
|
||||
os.Exit(1)
|
||||
failRunUnclassified(mainLog.Load().Error().Err(err), "failed to start process as daemon", p.notifyExitToLogServer)
|
||||
return
|
||||
}
|
||||
mainLog.Load().Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started")
|
||||
os.Exit(0)
|
||||
@@ -741,9 +745,10 @@ func permanentAPIRejection(err error) (*controld.ErrorResponse, bool) {
|
||||
}
|
||||
|
||||
// apiFailureCode maps a bootstrap preflight error to its provisioning code.
|
||||
// A deleted device gets its own code because it triggers self-uninstall;
|
||||
// other permanent rejections are generic; anything else counts as
|
||||
// reachability trouble worth retrying.
|
||||
// A deleted device gets its own code because it triggers self-uninstall; a
|
||||
// permanent rejection with a known token reason gets its own code; other
|
||||
// permanent rejections are generic; anything else counts as reachability
|
||||
// trouble worth retrying.
|
||||
func apiFailureCode(err error) (provisionFailureCode, bool) {
|
||||
if err == nil {
|
||||
return "", false
|
||||
@@ -752,12 +757,62 @@ func apiFailureCode(err error) (provisionFailureCode, bool) {
|
||||
if errors.As(err, &uer) && uer.ErrorField.Code == controld.InvalidConfigCode {
|
||||
return provisionCodeAPIDeviceInvalid, true
|
||||
}
|
||||
if _, ok := permanentAPIRejection(err); ok {
|
||||
if rejection, ok := permanentAPIRejection(err); ok {
|
||||
if code, ok := tokenFailureCodeForReason(rejection.ErrorField.Metadata.Reason); ok {
|
||||
return code, true
|
||||
}
|
||||
return provisionCodeAPIRejected, true
|
||||
}
|
||||
return provisionCodeAPIUnreachable, true
|
||||
}
|
||||
|
||||
// tokenFailureCodeForReason maps error.metadata.reason on a provisioning-token
|
||||
// rejection to its failure code. An empty or unrecognized reason is not
|
||||
// classified here; the caller falls back to the generic rejection code.
|
||||
func tokenFailureCodeForReason(reason string) (provisionFailureCode, bool) {
|
||||
switch reason {
|
||||
case controld.ReasonTokenInvalid:
|
||||
return provisionCodeTokenInvalid, true
|
||||
case controld.ReasonTokenExpired:
|
||||
return provisionCodeTokenExpired, true
|
||||
case controld.ReasonTokenLimitReached:
|
||||
return provisionCodeTokenLimitReached, true
|
||||
case controld.ReasonTokenDisabled:
|
||||
return provisionCodeTokenDisabled, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// tokenRejectionMessage returns the human message for a TOKEN_* code: what is
|
||||
// wrong with the provisioning code and one concrete next action. Empty for
|
||||
// any other code, so callers know to fall back to a generic message. Never
|
||||
// includes the token itself.
|
||||
func tokenRejectionMessage(code provisionFailureCode) string {
|
||||
switch code {
|
||||
case provisionCodeTokenInvalid:
|
||||
return "the provisioning code is not valid; check the code and re-enter it exactly as given"
|
||||
case provisionCodeTokenExpired:
|
||||
return "the provisioning code has expired; get a new provisioning code from your administrator"
|
||||
case provisionCodeTokenLimitReached:
|
||||
return "the provisioning code reached its device limit; free up a device slot or use a different code"
|
||||
case provisionCodeTokenDisabled:
|
||||
return "the provisioning code was invalidated; download a profile from an active provisioning code"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// provisionTokenFailureMessage returns the result-file message for a failed
|
||||
// provision-token exchange: a token-specific message when the API gave a
|
||||
// known reason, the generic exchange-failure summary otherwise.
|
||||
func provisionTokenFailureMessage(code provisionFailureCode, err error) string {
|
||||
if msg := tokenRejectionMessage(code); msg != "" {
|
||||
return msg
|
||||
}
|
||||
return fmt.Sprintf("provision token exchange failed: %v", err)
|
||||
}
|
||||
|
||||
// apiRejectionSummary reports the HTTP status only. The API's raw error body
|
||||
// can echo back the value the caller sent, so it stays out of the artifact.
|
||||
func apiRejectionSummary(statusCode int) string {
|
||||
@@ -1215,7 +1270,15 @@ func selfCheckResolveDomain(ctx context.Context, addr, scope string, domain stri
|
||||
return errSelfCheckNoAnswer
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
// unixServiceHomeDir is where a ctrld service with root rights keeps its
|
||||
// files on macOS, Linux, and the BSDs.
|
||||
const unixServiceHomeDir = "/etc/controld"
|
||||
|
||||
// serviceHomeDir returns the directory where a ctrld service with root or
|
||||
// administrator rights keeps its files. It has no fallback to the home
|
||||
// directory of the current user. A caller without root that only reads can
|
||||
// thus look where the service wrote.
|
||||
func serviceHomeDir() (string, error) {
|
||||
dir, err := router.HomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1237,7 +1300,17 @@ func userHomeDir() (string, error) {
|
||||
if isMobile() {
|
||||
return homedir, nil
|
||||
}
|
||||
dir = "/etc/controld"
|
||||
return unixServiceHomeDir, nil
|
||||
}
|
||||
|
||||
// userHomeDir is serviceHomeDir with a fallback: when the current user
|
||||
// cannot write to the unix service home, it returns the home directory of
|
||||
// that user instead.
|
||||
func userHomeDir() (string, error) {
|
||||
dir, err := serviceHomeDir()
|
||||
if err != nil || dir != unixServiceHomeDir {
|
||||
return dir, err
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return os.UserHomeDir() // fallback to user home directory
|
||||
}
|
||||
@@ -1898,6 +1971,84 @@ func osVersion() string {
|
||||
return oi.String()
|
||||
}
|
||||
|
||||
// provisionTokenMinLen, provisionTokenMaxLen, and provisionTokenExpectedPrefix
|
||||
// describe the shape of a --cd-org value the input stage checks before any
|
||||
// network call.
|
||||
const (
|
||||
provisionTokenMinLen = 6
|
||||
provisionTokenMaxLen = 64
|
||||
provisionTokenExpectedPrefix = "org-v1-"
|
||||
)
|
||||
|
||||
// provisionTokenShapeValid reports whether token looks like something the API
|
||||
// could parse: a length between provisionTokenMinLen and provisionTokenMaxLen,
|
||||
// with no whitespace or control characters. It does not require the
|
||||
// "org-v1-" prefix - legacy provisioning codes predate that convention and
|
||||
// are handled with a warning, not a failure.
|
||||
func provisionTokenShapeValid(token string) bool {
|
||||
if len(token) < provisionTokenMinLen || len(token) > provisionTokenMaxLen {
|
||||
return false
|
||||
}
|
||||
for _, r := range token {
|
||||
if unicode.IsSpace(r) || unicode.IsControl(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// validateProvisionTokenShape classifies an obviously malformed --cd-org
|
||||
// value (PROVISION_TOKEN_MALFORMED) before any network call, so a typo or a
|
||||
// pasted URL fails fast instead of going through a doomed API round trip. A
|
||||
// token missing the "org-v1-" prefix only gets a logged warning: legacy
|
||||
// provisioning codes may lack it and still work.
|
||||
func validateProvisionTokenShape() bool {
|
||||
if !provisionTokenShapeValid(cdOrg) {
|
||||
msg := fmt.Sprintf("--cd-org provisioning code is malformed: must be %d-%d characters with no whitespace or control characters", provisionTokenMinLen, provisionTokenMaxLen)
|
||||
failProvision(newProvisionResult(provisionCodeProvisionTokenMalformed, msg, nil, provisionSecrets()...), nil)
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(cdOrg, provisionTokenExpectedPrefix) {
|
||||
mainLog.Load().Warn().Msg("--cd-org provisioning code does not have the expected org-v1- prefix; continuing, since legacy codes may lack it")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// validateCustomHostnameFlag classifies an invalid --custom-hostname value
|
||||
// (CUSTOM_HOSTNAME_INVALID) before any network call. ctrld's accept/reject
|
||||
// rule (validHostname) is unchanged - this only names the field, the
|
||||
// offending character(s), and the allowed format instead of a bare fatal
|
||||
// exit. ControlD's device-name formatting folds or strips some characters
|
||||
// ctrld still accepts (dot, space, plus), so a hostname using one of those
|
||||
// gets a one-line notice instead: the registered device name may differ from
|
||||
// what was requested.
|
||||
func validateCustomHostnameFlag() bool {
|
||||
if customHostname == "" {
|
||||
return true
|
||||
}
|
||||
if !validHostname(customHostname) {
|
||||
failProvision(newProvisionResult(provisionCodeCustomHostnameInvalid, customHostnameFailureMessage(customHostname), nil, provisionSecrets()...), nil)
|
||||
return false
|
||||
}
|
||||
if hostnameMayBeFoldedByServer(customHostname) {
|
||||
mainLog.Load().Notice().Msgf("device name %q contains characters ControlD may fold or strip when it registers the device; the registered name may differ", customHostname)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// validateInterceptModeFlag classifies an invalid --intercept-mode value
|
||||
// (INTERCEPT_MODE_INVALID) on the provisioning boundary, so a typo fails with
|
||||
// a stable code instead of a bare fatal exit an installer wrapper cannot tell
|
||||
// apart from any other crash. An empty mode means the flag was not set.
|
||||
func validateInterceptModeFlag(mode string) bool {
|
||||
if mode == "" || validInterceptMode(mode) {
|
||||
return true
|
||||
}
|
||||
msg := fmt.Sprintf("--intercept-mode %q is not valid: must be 'off', 'dns', or 'hard'", mode)
|
||||
failProvision(newProvisionResult(provisionCodeInterceptModeInvalid, msg, nil, provisionSecrets()...), nil)
|
||||
return false
|
||||
}
|
||||
|
||||
// cdUIDFromProvToken fetch UID from ControlD API using provision token.
|
||||
func cdUIDFromProvToken() string {
|
||||
// --cd flag supersedes --cd-org, ignore it if both are supplied.
|
||||
@@ -1908,9 +2059,11 @@ func cdUIDFromProvToken() string {
|
||||
if cdOrg == "" {
|
||||
return ""
|
||||
}
|
||||
// Validate custom hostname if provided.
|
||||
if customHostname != "" && !validHostname(customHostname) {
|
||||
mainLog.Load().Fatal().Msgf("invalid custom hostname: %q", customHostname)
|
||||
if !validateProvisionTokenShape() {
|
||||
return ""
|
||||
}
|
||||
if !validateCustomHostnameFlag() {
|
||||
return ""
|
||||
}
|
||||
|
||||
req := &controld.UtilityOrgRequest{
|
||||
@@ -1926,7 +2079,7 @@ func cdUIDFromProvToken() string {
|
||||
code, _ := apiFailureCode(err)
|
||||
mainLog.Load().Error().Msgf("failed to fetch resolver uid with provision token: %s: %s",
|
||||
redactToken(cdOrg), redactSecrets(err.Error(), provisionSecrets()...))
|
||||
failProvision(newProvisionResult(code, fmt.Sprintf("provision token exchange failed: %v", err), nil, provisionSecrets()...), nil)
|
||||
failProvision(newProvisionResult(code, provisionTokenFailureMessage(code, err), nil, provisionSecrets()...), nil)
|
||||
return ""
|
||||
}
|
||||
return resolverConfig.UID
|
||||
@@ -2032,32 +2185,55 @@ func newSocketControlClientMobile(dir string, stopCh chan struct{}) *controlClie
|
||||
}
|
||||
|
||||
// checkStrFlagEmpty validates if a string flag was set to an empty string.
|
||||
// If yes, emitting a fatal error message.
|
||||
func checkStrFlagEmpty(cmd *cobra.Command, flagName string) {
|
||||
// An explicit empty --cd-org is a malformed provisioning token, classified on
|
||||
// the provisioning boundary; every other flag keeps the bare fatal, since
|
||||
// this helper is shared with flags outside that boundary. Returns false when
|
||||
// the caller must stop: provisionExit is stubbed out under test, so nothing
|
||||
// else stops execution from falling through.
|
||||
func checkStrFlagEmpty(cmd *cobra.Command, flagName string) bool {
|
||||
fl := cmd.Flags().Lookup(flagName)
|
||||
if !fl.Changed || fl.Value.Type() != "string" {
|
||||
return
|
||||
if !fl.Changed || fl.Value.Type() != "string" || fl.Value.String() != "" {
|
||||
return true
|
||||
}
|
||||
if fl.Value.String() == "" {
|
||||
mainLog.Load().Fatal().Msgf(`flag "--%s" value must be non-empty`, fl.Name)
|
||||
if flagName == cdOrgFlagName {
|
||||
msg := fmt.Sprintf("--%s provisioning code is malformed: value must be non-empty", cdOrgFlagName)
|
||||
failProvision(newProvisionResult(provisionCodeProvisionTokenMalformed, msg, nil, provisionSecrets()...), nil)
|
||||
return false
|
||||
}
|
||||
mainLog.Load().Fatal().Msgf(`flag "--%s" value must be non-empty`, fl.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
func validateCdUpstreamProtocol() {
|
||||
// validateCdUpstreamProtocol validates the Control D upstream protocol,
|
||||
// classified on the provisioning boundary since it only runs once --cd is
|
||||
// set. notify unblocks a waiting "ctrld start" on the daemon side; the parent
|
||||
// process, which has no one waiting, passes nil. Returns false when the
|
||||
// caller must stop.
|
||||
func validateCdUpstreamProtocol(notify func()) bool {
|
||||
if cdUID == "" {
|
||||
return
|
||||
return true
|
||||
}
|
||||
switch cdUpstreamProto {
|
||||
case ctrld.ResolverTypeDOH, ctrld.ResolverTypeDOH3:
|
||||
return true
|
||||
default:
|
||||
mainLog.Load().Fatal().Msg(`flag "--protocol" must be "doh" or "doh3"`)
|
||||
msg := fmt.Sprintf("--proto %q is not valid: must be 'doh' or 'doh3'", cdUpstreamProto)
|
||||
failProvision(newProvisionResult(provisionCodeInvalidFlagCombination, msg, nil, provisionSecrets()...), notify)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateCdAndNextDNSFlags() {
|
||||
// validateCdAndNextDNSFlags validates that Control D and NextDNS flags are
|
||||
// not used together, classified on the provisioning boundary since it only
|
||||
// fires once --cd or --cd-org is set. Returns false when the caller must
|
||||
// stop.
|
||||
func validateCdAndNextDNSFlags() bool {
|
||||
if (cdUID != "" || cdOrg != "") && nextdns != "" {
|
||||
mainLog.Load().Fatal().Msgf("--%s/--%s could not be used with --%s", cdUidFlagName, cdOrgFlagName, nextdnsFlagName)
|
||||
msg := fmt.Sprintf("--%s/--%s cannot be used together with --%s", cdUidFlagName, cdOrgFlagName, nextdnsFlagName)
|
||||
failProvision(newProvisionResult(provisionCodeInvalidFlagCombination, msg, nil, provisionSecrets()...), nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// removeNextDNSFromArgs removes the --nextdns from command line arguments.
|
||||
@@ -2274,7 +2450,25 @@ func runningIface(s service.Service) *ifaceResponse {
|
||||
return nil
|
||||
}
|
||||
|
||||
// apiRejectionMessage returns the result-file message for a permanent API
|
||||
// rejection: the HTTP status for a device-invalid or generic rejection, or
|
||||
// the raw error for anything else (network trouble, an unreachable API).
|
||||
func apiRejectionMessage(err error) string {
|
||||
var uer *controld.ErrorResponse
|
||||
if errors.As(err, &uer) && uer.ErrorField.Code == controld.InvalidConfigCode {
|
||||
return apiRejectionSummary(uer.StatusCode)
|
||||
}
|
||||
if rejection, ok := permanentAPIRejection(err); ok {
|
||||
return apiRejectionSummary(rejection.StatusCode)
|
||||
}
|
||||
return fmt.Sprintf("failed to fetch resolver config: %v", err)
|
||||
}
|
||||
|
||||
// doValidateCdRemoteConfig fetches and validates custom config for cdUID.
|
||||
// fatal distinguishes the two callers: a direct install-time caller passes
|
||||
// true and classifies a fetch failure on the provisioning boundary; the
|
||||
// restart path passes false and gets the error back to decide for itself,
|
||||
// with no process exit.
|
||||
func doValidateCdRemoteConfig(cdUID string, fatal bool) error {
|
||||
// Username is only sent during initial provisioning (cdUIDFromProvToken).
|
||||
// All subsequent calls use lightweight metadata to avoid EDR triggers.
|
||||
@@ -2283,16 +2477,16 @@ func doValidateCdRemoteConfig(cdUID string, fatal bool) error {
|
||||
Version: rootCmd.Version,
|
||||
Metadata: ctrld.SystemMetadataRuntime(context.Background()),
|
||||
}
|
||||
rc, err := controld.FetchResolverConfig(context.Background(), req, cdDev)
|
||||
rc, err := fetchResolverConfig(context.Background(), req, cdDev)
|
||||
if err != nil {
|
||||
logger := mainLog.Load().Fatal()
|
||||
if !fatal {
|
||||
logger = mainLog.Load().Warn()
|
||||
}
|
||||
logger.Err(err).Err(err).Msgf("failed to fetch resolver uid: %s", cdUID)
|
||||
if !fatal {
|
||||
mainLog.Load().Warn().Err(err).Msgf("failed to fetch resolver config for %s", redactToken(cdUID))
|
||||
return err
|
||||
}
|
||||
code, _ := apiFailureCode(err)
|
||||
mainLog.Load().Error().Err(err).Msgf("failed to fetch resolver config for %s", redactToken(cdUID))
|
||||
failProvision(newProvisionResult(code, apiRejectionMessage(err), nil, provisionSecrets()...), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// return earlier if there's no custom config.
|
||||
|
||||
Reference in new issue
Block a user