diff --git a/cmd/frpc/main.go b/cmd/frpc/main.go index f7651bca169..70ef7b53d6b 100644 --- a/cmd/frpc/main.go +++ b/cmd/frpc/main.go @@ -22,5 +22,5 @@ import ( func main() { system.EnableCompatibilityMode() - sub.Execute() + system.Run("FrpClient", sub.Execute) } diff --git a/cmd/frpc/sub/install_windows.go b/cmd/frpc/sub/install_windows.go new file mode 100644 index 00000000000..1aa65d77b35 --- /dev/null +++ b/cmd/frpc/sub/install_windows.go @@ -0,0 +1,263 @@ +// Copyright 2021 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sub + +import ( + "errors" + "fmt" + "github.com/fatedier/frp/pkg/policy/security" + "github.com/fatedier/frp/pkg/util/log/events" + "github.com/fatedier/frp/pkg/util/system" + "github.com/fatedier/frp/pkg/util/version" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/mgr" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/v1/validation" +) + +var ( + verifyInstallation = false + restricted = false +) + +const fmtServiceDesc = "Frp is a fast reverse proxy that allows you to expose a local server located behind a NAT or firewall to the Internet. This service is %s." + +func init() { + installCmd.PersistentFlags().BoolVarP(&verifyInstallation, "verify", "", false, "verify config(s) before installation") + installCmd.PersistentFlags().BoolVarP(&restricted, "restricted", "", false, "run service in restricted context") + + installCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, + fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", "))) + + rootCmd.AddCommand(installCmd) + rootCmd.AddCommand(uninstallCmd) +} + +var installCmd = &cobra.Command{ + Use: "install", + Short: "Install this executable as a Windows service or update the existing service (run as privileged user)", + RunE: func(cmd *cobra.Command, args []string) error { + if showVersion { + fmt.Println(version.Full()) + return nil + } + + execPath, err := os.Executable() + if err != nil { + fmt.Println("frpc: the executable no longer exists") + os.Exit(1) + } + stat, err := os.Stat(execPath) + if err != nil || stat.IsDir() { + fmt.Println("frpc: the executable is no longer valid") + os.Exit(1) + } + + // Ignore other params if "--config-dir" specified + if cfgDir != "" { + if verifyInstallation { + var hasValidCfg = false + err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return os.ErrNotExist + } + if d.IsDir() { + return nil + } + cfgFile1 := cfgDir + "\\" + d.Name() + if verifyCfg(cfgFile1) == nil { + fmt.Printf("frpc: the configuration file %s syntax is ok\n", cfgFile1) + hasValidCfg = true + } + return nil + }) + if !hasValidCfg { + err = errors.New("no valid configuration file found") + } + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } + err := installService(execPath, "--config-dir", cfgDir) + if err != nil { + os.Exit(1) + } + return nil + } + + // Ignore other params if "-c" / "--config" specified + if cfgFile != "" { + if verifyInstallation { + err := verifyCfg(cfgFile) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Printf("frpc: the configuration file %s syntax is ok\n", cfgFile) + } + err := installService(execPath, "--config", cfgFile) + if err != nil { + os.Exit(1) + } + return nil + } + + err = installService(execPath, args...) + if err != nil { + os.Exit(1) + } + + return nil + }, +} + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Remove the Windows service installed for this executable (run as privileged user)", + RunE: func(cmd *cobra.Command, args []string) error { + if showVersion { + fmt.Println(version.Full()) + return nil + } + + err := uninstallService() + if err != nil { + os.Exit(1) + } + + return nil + }, +} + +func verifyCfg(f string) error { + cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(f, strictConfigMode) + if err != nil { + return err + } + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } + if err != nil { + return err + } + return nil +} + +func installService(exec string, args ...string) error { + scm, err := mgr.Connect() + if err != nil { + fmt.Println("frpc: Failed connect to SCM (Permission Denied)") + return err + } + defer func() { + _ = scm.Disconnect() + }() + + // Check and modify existing service + if modifyService(scm, exec, args...) == nil { + return nil + } + // Create new service + var objName string + var sidType uint32 = windows.SERVICE_SID_TYPE_UNRESTRICTED + if restricted { + objName = "NT AUTHORITY\\LocalService" + sidType = windows.SERVICE_SID_TYPE_RESTRICTED + } + _, err = scm.CreateService("frpc", exec, mgr.Config{ + ErrorControl: mgr.ErrorNormal, + TagId: 0, + Dependencies: []string{"Tcpip"}, + ServiceStartName: objName, + DisplayName: system.ServiceName, + Description: fmt.Sprintf(fmtServiceDesc, system.ServiceName), + SidType: sidType, + DelayedAutoStart: false, + }, args...) + if err != nil { + return err + } + _ = events.CreateEventSource(system.ServiceName) + fmt.Println("Service successfully installed.") + return nil +} + +func modifyService(scm *mgr.Mgr, exec string, args ...string) error { + service, err := scm.OpenService("frpc") + if err != nil { + return err + } + defer func(service *mgr.Service) { + _ = service.Close() + }(service) + serviceConfig, err := service.Config() + if err != nil { + return err + } + s := syscall.EscapeArg(exec) + for _, v := range args { + s += " " + syscall.EscapeArg(v) + } + serviceConfig.BinaryPathName = s + var objName string + var sidType uint32 = windows.SERVICE_SID_TYPE_UNRESTRICTED + if restricted { + objName = "NT AUTHORITY\\LocalService" + sidType = windows.SERVICE_SID_TYPE_RESTRICTED + } + serviceConfig.ServiceStartName = objName + serviceConfig.SidType = sidType + err = service.UpdateConfig(serviceConfig) + if err != nil { + return err + } + return nil +} + +func uninstallService() error { + scm, err := mgr.Connect() + if err != nil { + return err + } + defer func() { + _ = scm.Disconnect() + }() + + service, err := scm.OpenService("frpc") + if err != nil { + return err + } + defer func(service *mgr.Service) { + _ = service.Close() + }(service) + err = service.Delete() + if err != nil { + return err + } + _ = events.DeleteEventSource(system.ServiceName) + fmt.Println("Service successfully removed.") + return nil +} diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 1c2d8d5eaae..89c5ca2b6d4 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -17,6 +17,7 @@ package sub import ( "context" "fmt" + "github.com/fatedier/frp/pkg/util/system" "io/fs" "os" "os/signal" @@ -47,7 +48,7 @@ var ( ) func init() { - rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frpc") rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors") @@ -154,6 +155,9 @@ func startService( cfgFile string, ) error { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) + defer func() { + _ = log.DestroyEventWriter() + }() if cfgFile != "" { log.Infof("start frpc service for config file [%s]", cfgFile) @@ -170,6 +174,22 @@ func startService( return err } + // Setup system.PauseF and system.ContinueF + system.PauseF = func() error { + return svr.UpdateAllConfigurer([]v1.ProxyConfigurer{}, []v1.VisitorConfigurer{}) + } + system.ContinueF = func() error { + cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) + if err != nil { + return err + } + _, err = validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures) + if err != nil { + return err + } + return svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs) + } + shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic" // Capture the exit signal if we use kcp or quic. if shouldGracefulClose { diff --git a/cmd/frps/install_windows.go b/cmd/frps/install_windows.go new file mode 100644 index 00000000000..7d46a7ff219 --- /dev/null +++ b/cmd/frps/install_windows.go @@ -0,0 +1,227 @@ +// Copyright 2021 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "github.com/fatedier/frp/pkg/policy/security" + "github.com/fatedier/frp/pkg/util/log/events" + "github.com/fatedier/frp/pkg/util/system" + "github.com/fatedier/frp/pkg/util/version" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/mgr" + "os" + "strings" + "syscall" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/v1/validation" +) + +var ( + verifyInstallation = false + restricted = false +) + +const fmtServiceDesc = "Frp is a fast reverse proxy that allows you to expose a local server located behind a NAT or firewall to the Internet. This service is %s." + +func init() { + installCmd.PersistentFlags().BoolVarP(&verifyInstallation, "verify", "", false, "verify config(s) before installation") + installCmd.PersistentFlags().BoolVarP(&restricted, "restricted", "", false, "run service in restricted context") + installCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{}, + fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", "))) + + rootCmd.AddCommand(installCmd) + rootCmd.AddCommand(uninstallCmd) +} + +var installCmd = &cobra.Command{ + Use: "install", + Short: "Install this executable as a Windows service or update the existing service (run as privileged user)", + RunE: func(cmd *cobra.Command, args []string) error { + if showVersion { + fmt.Println(version.Full()) + return nil + } + + execPath, err := os.Executable() + if err != nil { + fmt.Println("frps: the executable no longer exists") + os.Exit(1) + } + stat, err := os.Stat(execPath) + if err != nil || stat.IsDir() { + fmt.Println("frps: the executable is no longer valid") + os.Exit(1) + } + + // Ignore other params if "-c" / "--config" specified + if cfgFile != "" { + if verifyInstallation { + err := verifyCfg(cfgFile) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile) + } + err := installService(execPath, "--config", cfgFile) + if err != nil { + os.Exit(1) + } + return nil + } + + err = installService(execPath, args...) + if err != nil { + os.Exit(1) + } + + return nil + }, +} + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Remove the Windows service installed for this executable (run as privileged user)", + RunE: func(cmd *cobra.Command, args []string) error { + if showVersion { + fmt.Println(version.Full()) + return nil + } + + err := uninstallService() + if err != nil { + os.Exit(1) + } + + return nil + }, +} + +func verifyCfg(f string) error { + svrCfg, _, err := config.LoadServerConfig(f, strictConfigMode) + if err != nil { + return err + } + unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe) + validator := validation.NewConfigValidator(unsafeFeatures) + warning, err := validator.ValidateServerConfig(svrCfg) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } + if err != nil { + return err + } + return nil +} + +func installService(exec string, args ...string) error { + scm, err := mgr.Connect() + if err != nil { + fmt.Println("frps: Failed connect to SCM (Permission Denied)") + return err + } + defer func() { + _ = scm.Disconnect() + }() + + // Check and modify existing service + if modifyService(scm, exec, args...) == nil { + return nil + } + // Create new service + var objName string + var sidType uint32 = windows.SERVICE_SID_TYPE_UNRESTRICTED + if restricted { + objName = "NT AUTHORITY\\LocalService" + sidType = windows.SERVICE_SID_TYPE_RESTRICTED + } + _, err = scm.CreateService("frps", exec, mgr.Config{ + ErrorControl: mgr.ErrorNormal, + TagId: 0, + Dependencies: []string{"Tcpip"}, + ServiceStartName: objName, + DisplayName: system.ServiceName, + Description: fmt.Sprintf(fmtServiceDesc, system.ServiceName), + SidType: sidType, + DelayedAutoStart: false, + }, args...) + if err != nil { + return err + } + _ = events.CreateEventSource(system.ServiceName) + fmt.Println("Service successfully installed.") + return nil +} + +func modifyService(scm *mgr.Mgr, exec string, args ...string) error { + service, err := scm.OpenService("frps") + if err != nil { + return err + } + defer func(service *mgr.Service) { + _ = service.Close() + }(service) + serviceConfig, err := service.Config() + if err != nil { + return err + } + s := syscall.EscapeArg(exec) + for _, v := range args { + s += " " + syscall.EscapeArg(v) + } + serviceConfig.BinaryPathName = s + var objName string + var sidType uint32 = windows.SERVICE_SID_TYPE_UNRESTRICTED + if restricted { + objName = "NT AUTHORITY\\LocalService" + sidType = windows.SERVICE_SID_TYPE_RESTRICTED + } + serviceConfig.ServiceStartName = objName + serviceConfig.SidType = sidType + err = service.UpdateConfig(serviceConfig) + if err != nil { + return err + } + return nil +} + +func uninstallService() error { + scm, err := mgr.Connect() + if err != nil { + return err + } + defer func() { + _ = scm.Disconnect() + }() + + service, err := scm.OpenService("frps") + if err != nil { + return err + } + defer func(service *mgr.Service) { + _ = service.Close() + }(service) + err = service.Delete() + if err != nil { + return err + } + _ = events.DeleteEventSource(system.ServiceName) + fmt.Println("Service successfully removed.") + return nil +} diff --git a/cmd/frps/main.go b/cmd/frps/main.go index 3cb398d8290..fe47a78b3c2 100644 --- a/cmd/frps/main.go +++ b/cmd/frps/main.go @@ -22,5 +22,5 @@ import ( func main() { system.EnableCompatibilityMode() - Execute() + system.Run("FrpServer", Execute) } diff --git a/cmd/frps/root.go b/cmd/frps/root.go index e6ab008a343..60a2d9d7140 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -17,6 +17,7 @@ package main import ( "context" "fmt" + "github.com/fatedier/frp/pkg/util/system" "os" "strings" @@ -110,6 +111,9 @@ func Execute() { func runServer(cfg *v1.ServerConfig) (err error) { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) + defer func() { + _ = log.DestroyEventWriter() + }() if cfgFile != "" { log.Infof("frps uses config file: %s", cfgFile) @@ -121,6 +125,10 @@ func runServer(cfg *v1.ServerConfig) (err error) { if err != nil { return err } + // Setup only system.ContinueF, an empty function, to make svc continue + system.ContinueF = func() error { + return nil + } log.Infof("frps started successfully") svr.Run(context.Background()) return diff --git a/go.mod b/go.mod index e23facdeab5..a4e897dedc8 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( golang.org/x/net v0.43.0 golang.org/x/oauth2 v0.28.0 golang.org/x/sync v0.16.0 + golang.org/x/sys v0.35.0 golang.org/x/time v0.5.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 gopkg.in/ini.v1 v1.67.0 @@ -68,7 +69,6 @@ require ( github.com/vishvananda/netns v0.0.4 // indirect go.uber.org/automaxprocs v1.6.0 // indirect golang.org/x/mod v0.27.0 // indirect - golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.36.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect diff --git a/pkg/config/flags.go b/pkg/config/flags.go index 6027b622411..ff447c2987d 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -159,7 +159,7 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp", fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols)) cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") - cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path") + cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console, eventlog (Windows only) or file path") cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days") cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate") diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go index 8989855485b..a5978a6605d 100644 --- a/pkg/config/v1/common.go +++ b/pkg/config/v1/common.go @@ -106,8 +106,9 @@ type NatTraversalConfig struct { type LogConfig struct { // This is destination where frp should write the logs. - // If "console" is used, logs will be printed to stdout, otherwise, - // logs will be written to the specified file. + // If "console" is used, logs will be printed to stdout, + // if "eventlog" is used, logs will be sent to Windows events, + // otherwise, logs will be written to the specified file. // By default, this value is "console". To string `json:"to,omitempty"` // Level specifies the minimum log level. Valid values are "trace", @@ -121,7 +122,7 @@ type LogConfig struct { } func (c *LogConfig) Complete() { - c.To = util.EmptyOr(c.To, "console") + c.To = util.EmptyOr(c.To, map[bool]string{true: "eventlog", false: "console"}[isWinSvc()]) c.Level = util.EmptyOr(c.Level, "info") c.MaxDays = util.EmptyOr(c.MaxDays, 3) } diff --git a/pkg/config/v1/svc.go b/pkg/config/v1/svc.go new file mode 100644 index 00000000000..2dde1e6f770 --- /dev/null +++ b/pkg/config/v1/svc.go @@ -0,0 +1,24 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +package v1 + +// isWinSvc checks if we are running as a Windows service. +// Always return false on non-windows platforms. +func isWinSvc() bool { + return false +} + diff --git a/pkg/config/v1/svc_windows.go b/pkg/config/v1/svc_windows.go new file mode 100644 index 00000000000..f2c2b6c0537 --- /dev/null +++ b/pkg/config/v1/svc_windows.go @@ -0,0 +1,24 @@ +// Copyright 2023 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "golang.org/x/sys/windows/svc" + +// isWinSvc checks if we are running as a Windows service. +// Always return false on non-windows platforms. +func isWinSvc() bool { + sv, _ := svc.IsWindowsService() + return sv +} diff --git a/pkg/util/log/eventlog.go b/pkg/util/log/eventlog.go new file mode 100644 index 00000000000..ee78d8d1a6f --- /dev/null +++ b/pkg/util/log/eventlog.go @@ -0,0 +1,50 @@ +// Copyright 2016 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !windows + +package log + +import ( + "github.com/fatedier/golib/log" + "time" +) + +type EventWriter struct { +} + +func (e EventWriter) Write(p []byte) (n int, err error) { + return log.DefaultWriter.Write(p) +} + +func (e EventWriter) WriteLog(p []byte, _ log.Level, _ time.Time) (n int, err error) { + return e.Write(p) +} + +// GetEventWriter returns current EventWriter instance. +// Returns nil if not initialized. +func GetEventWriter() *EventWriter { + return nil +} + +// InitEventWriter tries initializing an EventWriter instance. +func InitEventWriter() error { + return nil +} + +// DestroyEventWriter closes the EventWriter instance. +// This should be called in defer. +func DestroyEventWriter() error { + return nil +} \ No newline at end of file diff --git a/pkg/util/log/eventlog_windows.go b/pkg/util/log/eventlog_windows.go new file mode 100644 index 00000000000..dcba76c6a76 --- /dev/null +++ b/pkg/util/log/eventlog_windows.go @@ -0,0 +1,111 @@ +// Copyright 2016 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "errors" + "github.com/fatedier/frp/pkg/util/log/events" + "github.com/fatedier/frp/pkg/util/system" + "github.com/fatedier/golib/log" + "golang.org/x/sys/windows/svc/eventlog" + "strings" + "time" +) + +var eventWriter *EventWriter = nil + +type EventWriter struct { + logInstance *eventlog.Log +} + +func (e EventWriter) Write(p []byte) (n int, err error) { + // Write at default log level and current time (fallback) + return e.WriteLog(p, log.InfoLevel, time.Now()) +} + +func (e EventWriter) WriteLog(p []byte, level log.Level, _ time.Time) (n int, err error) { + var eid uint32 = events.Undefined + s := strings.TrimSpace(string(p)) + switch level { + case log.TraceLevel: + fallthrough + case log.DebugLevel: + eid = events.Undefined + fallthrough + case log.InfoLevel: + eid = events.InfoUndefined + err := e.logInstance.Info(eid, s) + if err != nil { + return 0, err + } + return len(s), nil + case log.WarnLevel: + eid = events.WarnUndefined + err := e.logInstance.Warning(eid, s) + if err != nil { + return 0, err + } + return len(s), nil + case log.ErrorLevel: + eid = events.ErrUndefined + err := e.logInstance.Error(eid, s) + if err != nil { + return 0, err + } + return len(s), nil + default: + // This should not happen + return 0, errors.ErrUnsupported + } +} + +// GetEventWriter returns current EventWriter instance. +// Returns nil if not successfully initialized. +func GetEventWriter() *EventWriter { + return eventWriter +} + +// InitEventWriter tries initializing an EventWriter instance. +func InitEventWriter() error { + if eventWriter != nil { + return nil + } + if !events.SourceExists(system.ServiceName) { + err := events.CreateEventSource(system.ServiceName) + if err != nil { + return err + } + } + logInstance, err := eventlog.Open(system.ServiceName) + if err != nil { + return err + } + eventWriter = &EventWriter{ + logInstance: logInstance, + } + return nil +} + +// DestroyEventWriter closes the EventWriter instance. +// This should be called in defer. +func DestroyEventWriter() error { + if eventWriter == nil { + return nil + } + defer func() { + eventWriter = nil + }() + return eventWriter.logInstance.Close() +} \ No newline at end of file diff --git a/pkg/util/log/events/api_windows.go b/pkg/util/log/events/api_windows.go new file mode 100644 index 00000000000..9ce65c9d1a7 --- /dev/null +++ b/pkg/util/log/events/api_windows.go @@ -0,0 +1,115 @@ +// Copyright 2016 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "golang.org/x/sys/windows/registry" + "os" +) + +func SourceExists(serviceName string) bool { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Frp\\" + serviceName, registry.QUERY_VALUE) + if err != nil { + return false + } + defer func(key registry.Key) { + _ = key.Close() + }(key) + value, _, err := key.GetStringValue("EventMessageFile") + if err != nil { + return false + } + stat, err := os.Stat(value) + if err != nil { + return false + } + return !stat.IsDir() +} + +func CreateEventSource(serviceName string) error { + msgFile, err := getEventMessageFile() + if err != nil { + return err + } + eventKey, err := registry.OpenKey(registry.LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\EventLog", registry.CREATE_SUB_KEY) + if err != nil { + return err + } + defer func(eventKey registry.Key) { + _ = eventKey.Close() + }(eventKey) + + frpKey, _, err := registry.CreateKey(eventKey, "Frp", registry.CREATE_SUB_KEY) + if err != nil { + return err + } + defer func(frpKey registry.Key) { + _ = frpKey.Close() + }(frpKey) + + srcKey, _, err := registry.CreateKey(frpKey, serviceName, registry.SET_VALUE) + if err != nil { + return err + } + defer func(srcKey registry.Key) { + _ = srcKey.Close() + }(srcKey) + + err = srcKey.SetExpandStringValue("EventMessageFile", msgFile) + if err != nil { + return err + } + return nil +} + +func DeleteEventSource(serviceName string) error { + err := registry.DeleteKey(registry.LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Frp\\" + serviceName) + if err != nil { + return err + } + + // Delete the whole tree if nothing but "Frp" is left. Simply abort if any error occurred + frpKey, err := registry.OpenKey(registry.LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Frp", registry.ENUMERATE_SUB_KEYS) + if err != nil { + return nil + } + defer func(frpKey registry.Key) { + _ = frpKey.Close() + }(frpKey) + + keyNames, err := frpKey.ReadSubKeyNames(0) + if err != nil { + return nil + } + if len(keyNames) == 1 && keyNames[0] == "Frp" { + err := registry.DeleteKey(registry.LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Frp") + if err != nil { + return nil + } + } + return nil +} + +func getEventMessageFile() (string, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Client", registry.QUERY_VALUE) + if err != nil { + return "", err + } + path, _, err := key.GetStringValue("InstallPath") + if err != nil { + return "", err + } + return path + "EventLogMessages.dll", nil +} \ No newline at end of file diff --git a/pkg/util/log/events/events.go b/pkg/util/log/events/events.go new file mode 100644 index 00000000000..5493cbaefbe --- /dev/null +++ b/pkg/util/log/events/events.go @@ -0,0 +1,36 @@ +// Copyright 2016 fatedier, fatedier@gmail.com +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +// Event IDs. +// See https://learn.microsoft.com/zh-cn/windows/win32/eventlog/event-identifiers + +const ( + Undefined = iota +) + +const ( + InfoUndefined = iota + 1000 +) + +const ( + WarnUndefined = iota + 2000 +) + +const ( + ErrUndefined = iota + 3000 +) + + diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go index 327d4ef654d..548fad24b46 100644 --- a/pkg/util/log/log.go +++ b/pkg/util/log/log.go @@ -41,13 +41,24 @@ func init() { func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bool) { options := []log.Option{} - if logPath == "console" { - if !disableLogColor { - options = append(options, - log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{ - Colorful: true, - }, os.Stdout)), - ) + if logPath == "console" || logPath == "eventlog" { + if logPath == "eventlog" { + if InitEventWriter() == nil && GetEventWriter() != nil { + options = append(options, + log.WithOutput(GetEventWriter()), + ) + } else { + logPath = "console" + } + } + if logPath == "console" { + if !disableLogColor { + options = append(options, + log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{ + Colorful: true, + }, os.Stdout)), + ) + } } } else { writer := log.NewRotateFileWriter(log.RotateFileConfig{ diff --git a/pkg/util/system/system.go b/pkg/util/system/system.go index ae40e85e214..5e5d9a5afa9 100644 --- a/pkg/util/system/system.go +++ b/pkg/util/system/system.go @@ -12,11 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !android +//go:build !android && !windows package system +var ServiceName = "" +var PauseF func() error = nil +var ContinueF func() error = nil + // EnableCompatibilityMode enables compatibility mode for different system. // For example, on Android, the inability to obtain the correct time zone will result in incorrect log time output. func EnableCompatibilityMode() { } + +// Run wraps Execute function for different system. +// For example, on Windows, it runs as a Windows service if necessary. +func Run(name string, f func()) { + // Run as a usual program. + ServiceName = name + f() +} diff --git a/pkg/util/system/system_android.go b/pkg/util/system/system_android.go index 6fcfdbc1361..30a90f48730 100644 --- a/pkg/util/system/system_android.go +++ b/pkg/util/system/system_android.go @@ -22,6 +22,10 @@ import ( "time" ) +var ServiceName = "" +var PauseF func() error = nil +var ContinueF func() error = nil + func EnableCompatibilityMode() { fixTimezone() fixDNSResolver() @@ -68,3 +72,11 @@ func fixDNSResolver() { }, } } + +// Run wraps Execute function for different system. +// For example, on Windows, it runs as a Windows service if necessary. +func Run(name string, f func()) { + // Run as a usual program. + ServiceName = name + f() +} diff --git a/pkg/util/system/system_windows.go b/pkg/util/system/system_windows.go new file mode 100644 index 00000000000..5eaa3545bb9 --- /dev/null +++ b/pkg/util/system/system_windows.go @@ -0,0 +1,119 @@ +// Copyright 2024 The frp Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package system + +import ( + "fmt" + "golang.org/x/sys/windows/svc" + "os" +) + +var ServiceName = "" +var PauseF func() error = nil +var ContinueF func() error = nil + +// EnableCompatibilityMode enables compatibility mode for different system. +// For example, on Android, the inability to obtain the correct time zone will result in incorrect log time output. +func EnableCompatibilityMode() { +} + +// Run wraps Execute function for different system. +// For example, on Windows, it runs as a Windows service if necessary. +func Run(name string, f func()) { + ServiceName = name + // Check if we are running as a Windows service. + inService, err := svc.IsWindowsService() + if err != nil { + os.Exit(1) + } else if inService { + // Start as a service. + err := svc.Run(name, &frpService{ + f: f, + }) + if err != nil { + os.Exit(1) + } + } else { + // Run as a usual program. + f() + } +} + +type frpService struct { + f func() +} + +func (f frpService) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { + s <- svc.Status{State: svc.StartPending} + + defer func() { + s <- svc.Status{State: svc.StopPending} + fmt.Println("Stopping service...") + }() + + // Main function. + if len(args) > 1 { + // Replace all parameters if specified in Services MMC + os.Args = append(os.Args[:1], args[1:]...) + } + go func() { + f.f() + os.Exit(0) + }() + + // Wait until ContinueF is set. + for ContinueF == nil { + } + if PauseF != nil && ContinueF != nil { + s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue} + } else { + s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} + } + fmt.Println("Service started.") + + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Stop, svc.Shutdown: + return + case svc.Interrogate: + s <- c.CurrentStatus + case svc.Pause: + if PauseF != nil && ContinueF != nil { + s <- svc.Status{State: svc.PausePending, Accepts: svc.AcceptStop | svc.AcceptShutdown} + err := PauseF() + if err != nil { + return false, 1 + } + s <- svc.Status{State: svc.Paused, Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue} + fmt.Println("Service paused.") + } + case svc.Continue: + if PauseF != nil && ContinueF != nil { + fmt.Println("Service resumed, reloading configurations...") + s <- svc.Status{State: svc.ContinuePending, Accepts: svc.AcceptStop | svc.AcceptShutdown} + err := ContinueF() + if err != nil { + return false, 1 + } + s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue} + } + default: + fmt.Printf("ERROR: Unexpected services control request #%d\n", c) + } + } + } +}