1
0
mirror of https://github.com/fumiama/terasu-cloudflared.git synced 2026-06-05 00:50:24 +08:00
Files
terasu-cloudflared/cmd/cloudflared/windows_service.go
Nuno Diegues 70e675f42c TUN-5551: Reintroduce FIPS compliance for linux amd64 now as separate binaries
This is a cherry-pick of 157f5d1412
followed by build/CI changes so that amd64/linux FIPS compliance is
provided by new/separate binaries/artifacts/packages.

The reasoning being that FIPS compliance places excessive requirements
in the encryption algorithms used for regular users that do not care
about that. This can cause cloudflared to reject HTTPS origins that
would otherwise be accepted without FIPS checks.

This way, by having separate binaries, existing ones remain as they
were, and only FIPS-needy users will opt-in to the new FIPS binaries.
2021-12-20 21:50:42 +00:00

298 lines
9.8 KiB
Go

//go:build windows
// +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 = "Cloudflare Tunnel agent"
windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-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 Cloudflare Tunnel Windows service",
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install Cloudflare Tunnel as a Windows service",
Action: cliutil.ConfiguredAction(installWindowsService),
},
{
Name: "uninstall",
Usage: "Uninstall the Cloudflare Tunnel 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 Cloudflare Tunnel 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("Service %s already exists", windowsServiceName)
}
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
s, err = m.CreateService(windowsServiceName, exepath, config)
if err != nil {
return errors.Wrap(err, "Cannot install service")
}
defer s.Close()
log.Info().Msg("Cloudflare Tunnel 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)
}
return nil
}
func uninstallWindowsService(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog).
With().
Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
log.Info().Msg("Uninstalling Cloudflare Tunnel Windows 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("Service %s is not installed", windowsServiceName)
}
defer s.Close()
err = s.Delete()
if err != nil {
return errors.Wrap(err, "Cannot delete service")
}
log.Info().Msg("Cloudflare Tunnel agent service is uninstalled")
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)))
}