//go:build windows package main // Copypasta from the example files: // https://github.com/golang/sys/blob/master/windows/svc/example import ( "fmt" "os" "syscall" "time" "unsafe" "github.com/pkg/errors" "github.com/urfave/cli/v2" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/logger" ) const ( windowsServiceName = "Cloudflared" windowsServiceDescription = "Cloudflared agent" windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/as-a-service/windows/" recoverActionDelay = time.Second * 20 failureCountResetPeriod = time.Hour * 24 // not defined in golang.org/x/sys/windows package // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx serviceConfigFailureActionsFlag = 4 // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT // https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299- serviceControllerConnectionFailure = 1063 LogFieldWindowsServiceName = "windowsServiceName" ) func runApp(app *cli.App, graceShutdownC chan struct{}) { app.Commands = append(app.Commands, &cli.Command{ Name: "service", Usage: "Manages the cloudflared Windows service", Subcommands: []*cli.Command{ { Name: "install", Usage: "Install cloudflared as a Windows service", Action: cliutil.ConfiguredAction(installWindowsService), }, { Name: "uninstall", Usage: "Uninstall the cloudflared service", Action: cliutil.ConfiguredAction(uninstallWindowsService), }, }, }) // `IsAnInteractiveSession()` isn't exactly equivalent to "should the // process run as a normal EXE?" There are legitimate non-service cases, // like running cloudflared in a GCP startup script, for which // `IsAnInteractiveSession()` returns false. For more context, see: // https://github.com/judwhite/go-svc/issues/6 // It seems that the "correct way" to check "is this a normal EXE?" is: // 1. attempt to connect to the Service Control Manager // 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT // This involves actually trying to start the service. log := logger.Create(nil) isIntSess, err := svc.IsAnInteractiveSession() if err != nil { log.Fatal().Err(err).Msg("failed to determine if we are running in an interactive session") } if isIntSess { app.Run(os.Args) return } // Run executes service name by calling windowsService which is a Handler // interface that implements Execute method. // It will set service status to stop after Execute returns err = svc.Run(windowsServiceName, &windowsService{app: app, graceShutdownC: graceShutdownC}) if err != nil { if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure { // Hack: assume this is a false negative from the IsAnInteractiveSession() check above. // Run the app in "interactive" mode anyway. app.Run(os.Args) return } log.Fatal().Err(err).Msgf("%s service failed", windowsServiceName) } } type windowsService struct { app *cli.App graceShutdownC chan struct{} } // Execute is called by the service manager when service starts, the state // of the service will be set to Stopped when this function returns. func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) { log := logger.Create(nil) elog, err := eventlog.Open(windowsServiceName) if err != nil { log.Err(err).Msgf("Cannot open event log for %s", windowsServiceName) return } defer elog.Close() elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName)) defer func() { elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName)) }() // the arguments passed here are only meaningful if they were manually // specified by the user, e.g. using the Services console or `sc start`. // https://docs.microsoft.com/en-us/windows/desktop/services/service-entry-point // https://stackoverflow.com/a/6235139 var args []string if len(serviceArgs) > 1 { args = serviceArgs } else { // fall back to the arguments from ImagePath (or, as sc calls it, binPath) args = os.Args } elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args)) statusChan <- svc.Status{State: svc.StartPending} errC := make(chan error) go func() { errC <- s.app.Run(args) }() statusChan <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: statusChan <- c.CurrentStatus case svc.Stop, svc.Shutdown: if s.graceShutdownC != nil { // start graceful shutdown elog.Info(1, "cloudflared starting graceful shutdown") close(s.graceShutdownC) s.graceShutdownC = nil statusChan <- svc.Status{State: svc.StopPending} continue } // repeated attempts at graceful shutdown forces immediate stop elog.Info(1, "cloudflared terminating immediately") statusChan <- svc.Status{State: svc.StopPending} return false, 0 default: elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) } case err := <-errC: if err != nil { elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err)) ssec = true errno = 1 } else { elog.Info(1, "cloudflared terminated without error") errno = 0 } return } } } func installWindowsService(c *cli.Context) error { zeroLogger := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) zeroLogger.Info().Msg("Installing cloudflared Windows service") exepath, err := os.Executable() if err != nil { return errors.Wrap(err, "Cannot find path name that start the process") } m, err := mgr.Connect() if err != nil { return errors.Wrap(err, "Cannot establish a connection to the service control manager") } defer m.Disconnect() s, err := m.OpenService(windowsServiceName) log := zeroLogger.With().Str(LogFieldWindowsServiceName, windowsServiceName).Logger() if err == nil { s.Close() return fmt.Errorf(serviceAlreadyExistsWarn(windowsServiceName)) } extraArgs, err := getServiceExtraArgsFromCliArgs(c, &log) if err != nil { errMsg := "Unable to determine extra arguments for windows service" log.Err(err).Msg(errMsg) return errors.Wrap(err, errMsg) } config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription} s, err = m.CreateService(windowsServiceName, exepath, config, extraArgs...) if err != nil { return errors.Wrap(err, "Cannot install service") } defer s.Close() log.Info().Msg("cloudflared agent service is installed") err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info) if err != nil { s.Delete() return errors.Wrap(err, "Cannot install event logger") } err = configRecoveryOption(s.Handle) if err != nil { log.Err(err).Msg("Cannot set service recovery actions") log.Info().Msgf("See %s to manually configure service recovery actions", windowsServiceUrl) } err = s.Start() if err == nil { log.Info().Msg("Agent service for cloudflared installed successfully") } return err } func uninstallWindowsService(c *cli.Context) error { log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog). With(). Str(LogFieldWindowsServiceName, windowsServiceName).Logger() log.Info().Msg("Uninstalling cloudflared agent service") m, err := mgr.Connect() if err != nil { return errors.Wrap(err, "Cannot establish a connection to the service control manager") } defer m.Disconnect() s, err := m.OpenService(windowsServiceName) if err != nil { return fmt.Errorf("Agent service %s is not installed, so it could not be uninstalled", windowsServiceName) } defer s.Close() if status, err := s.Query(); err == nil && status.State == svc.Running { log.Info().Msg("Stopping cloudflared agent service") if _, err := s.Control(svc.Stop); err != nil { log.Info().Err(err).Msg("Failed to stop cloudflared agent service, you may need to stop it manually to complete uninstall.") } } err = s.Delete() if err != nil { return errors.Wrap(err, "Cannot delete agent service") } log.Info().Msg("Agent service for cloudflared was uninstalled successfully") err = eventlog.Remove(windowsServiceName) if err != nil { return errors.Wrap(err, "Cannot remove event logger") } return nil } // defined in https://msdn.microsoft.com/en-us/library/windows/desktop/ms685126(v=vs.85).aspx type scAction int // https://msdn.microsoft.com/en-us/library/windows/desktop/ms685126(v=vs.85).aspx const ( scActionNone scAction = iota scActionRestart scActionReboot scActionRunCommand ) // defined in https://msdn.microsoft.com/en-us/library/windows/desktop/ms685939(v=vs.85).aspx type serviceFailureActions struct { // time to wait to reset the failure count to zero if there are no failures in seconds resetPeriod uint32 rebootMsg *uint16 command *uint16 // If failure count is greater than actionCount, the service controller repeats // the last action in actions actionCount uint32 actions uintptr } // https://msdn.microsoft.com/en-us/library/windows/desktop/ms685937(v=vs.85).aspx // Not supported in Windows Server 2003 and Windows XP type serviceFailureActionsFlag struct { // enableActionsForStopsWithErr is of type BOOL, which is declared as // typedef int BOOL in C enableActionsForStopsWithErr int } type recoveryAction struct { recoveryType uint32 // The time to wait before performing the specified action, in milliseconds delay uint32 } // until https://github.com/golang/go/issues/23239 is release, we will need to // configure through ChangeServiceConfig2 func configRecoveryOption(handle windows.Handle) error { actions := []recoveryAction{ {recoveryType: uint32(scActionRestart), delay: uint32(recoverActionDelay / time.Millisecond)}, } serviceRecoveryActions := serviceFailureActions{ resetPeriod: uint32(failureCountResetPeriod / time.Second), actionCount: uint32(len(actions)), actions: uintptr(unsafe.Pointer(&actions[0])), } if err := windows.ChangeServiceConfig2(handle, windows.SERVICE_CONFIG_FAILURE_ACTIONS, (*byte)(unsafe.Pointer(&serviceRecoveryActions))); err != nil { return err } serviceFailureActionsFlag := serviceFailureActionsFlag{enableActionsForStopsWithErr: 1} return windows.ChangeServiceConfig2(handle, serviceConfigFailureActionsFlag, (*byte)(unsafe.Pointer(&serviceFailureActionsFlag))) }