diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 57761f2..dd1af17 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -364,6 +364,9 @@ func initCLI() { return } initLogging() + if err := checkDeactivationPin(s); errors.Is(err, errInvalidDeactivationPin) { + os.Exit(deactivationPinInvalidExitCode) + } if doTasks([]task{{s.Stop, true}}) { p.router.Cleanup() p.resetDNS() @@ -372,6 +375,8 @@ func initCLI() { }, } stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`) + stopCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for stopping ctrld`) + _ = stopCmd.Flags().MarkHidden("pin") restartCmd := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { @@ -518,10 +523,15 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, if iface == "" { iface = "auto" } + if err := checkDeactivationPin(s); errors.Is(err, errInvalidDeactivationPin) { + os.Exit(deactivationPinInvalidExitCode) + } uninstall(p, s) }, } uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, use "auto" for the default gateway interface`) + uninstallCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for uninstalling ctrld`) + _ = uninstallCmd.Flags().MarkHidden("pin") listIfacesCmd := &cobra.Command{ Use: "list", @@ -1171,6 +1181,18 @@ func processNoConfigFlags(noConfigStart bool) { v.Set("upstream", upstream) } +// defaultDeactivationPin is the default value for cdDeactivationPin. +// If cdDeactivationPin equals to this default, it means the pin code is not set from Control D API. +const defaultDeactivationPin = -1 + +// cdDeactivationPin is used in cd mode to decide whether stop and uninstall commands can be run. +var cdDeactivationPin int64 = defaultDeactivationPin + +// deactivationPinNotSet reports whether cdDeactivationPin was not set by processCDFlags. +func deactivationPinNotSet() bool { + return cdDeactivationPin == defaultDeactivationPin +} + func processCDFlags(cfg *ctrld.Config) error { logger := mainLog.Load().With().Str("mode", "cd").Logger() logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) @@ -1195,6 +1217,11 @@ func processCDFlags(cfg *ctrld.Config) error { return err } + if resolverConfig.DeactivationPin != nil { + logger.Debug().Msg("saving deactivation pin") + cdDeactivationPin = *resolverConfig.DeactivationPin + } + logger.Info().Msg("generating ctrld config from Control-D configuration") *cfg = ctrld.Config{} @@ -2049,3 +2076,29 @@ func noticeWritingControlDConfig() error { } return nil } + +// deactivationPinInvalidExitCode indicates exit code due to invalid pin code. +const deactivationPinInvalidExitCode = 126 + +// errInvalidDeactivationPin indicates that the deactivation pin is invalid. +var errInvalidDeactivationPin = errors.New("deactivation pin is invalid") + +// checkDeactivationPin validates if the deactivation pin matches one in ControlD config. +func checkDeactivationPin(s service.Service) error { + dir, err := socketDir() + if err != nil { + mainLog.Load().Err(err).Msg("could not check deactivation pin") + return err + } + cc := newSocketControlClient(s, dir) + if cc == nil { + return nil // ctrld is not running. + } + data, _ := json.Marshal(&deactivationRequest{Pin: deactivationPin}) + resp, _ := cc.post(deactivationPath, bytes.NewReader(data)) + if resp != nil && resp.StatusCode == http.StatusOK { + return nil // valid pin + } + mainLog.Load().Error().Msg("deactivation pin is invalid") + return errInvalidDeactivationPin +} diff --git a/cmd/cli/control_client.go b/cmd/cli/control_client.go index c626602..73002e8 100644 --- a/cmd/cli/control_client.go +++ b/cmd/cli/control_client.go @@ -27,3 +27,8 @@ func newControlClient(addr string) *controlClient { func (c *controlClient) post(path string, data io.Reader) (*http.Response, error) { return c.c.Post("http://unix"+path, contentTypeJson, data) } + +// deactivationRequest represents request for validating deactivation pin. +type deactivationRequest struct { + Pin int64 `json:"pin"` +} diff --git a/cmd/cli/control_server.go b/cmd/cli/control_server.go index 36749c1..117174d 100644 --- a/cmd/cli/control_server.go +++ b/cmd/cli/control_server.go @@ -16,10 +16,11 @@ import ( ) const ( - contentTypeJson = "application/json" - listClientsPath = "/clients" - startedPath = "/started" - reloadPath = "/reload" + contentTypeJson = "application/json" + listClientsPath = "/clients" + startedPath = "/started" + reloadPath = "/reload" + deactivationPath = "/deactivation" ) type controlServer struct { @@ -146,6 +147,25 @@ func (p *prog) registerControlServerHandler() { // Otherwise, reload is done. w.WriteHeader(http.StatusOK) })) + p.cs.register(deactivationPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + // Non-cd mode or pin code not set, always allowing deactivation. + if cdUID == "" || deactivationPinNotSet() { + w.WriteHeader(http.StatusOK) + return + } + + var req deactivationRequest + if err := json.NewDecoder(request.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusPreconditionFailed) + mainLog.Load().Err(err).Msg("invalid deactivation request") + return + } + code := http.StatusBadRequest + if req.Pin == cdDeactivationPin { + code = http.StatusOK + } + w.WriteHeader(code) + })) } func jsonResponse(next http.Handler) http.Handler { diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 3f1ef8b..9c64aa0 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -34,6 +34,7 @@ var ( ifaceStartStop string nextdns string cdUpstreamProto string + deactivationPin int64 mainLog atomic.Pointer[zerolog.Logger] consoleWriter zerolog.ConsoleWriter diff --git a/internal/controld/config.go b/internal/controld/config.go index 4cc6770..c095c0c 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -35,8 +35,9 @@ type ResolverConfig struct { Ctrld struct { CustomConfig string `json:"custom_config"` } `json:"ctrld"` - Exclude []string `json:"exclude"` - UID string `json:"uid"` + Exclude []string `json:"exclude"` + UID string `json:"uid"` + DeactivationPin *int64 `json:"deactivation_pin,omitempty"` } type utilityResponse struct {