diff options
author | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-11-21 15:45:08 +0100 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-11-22 22:00:32 +0100 |
commit | 224336aa077020c35d3feb8a534f92c814b232e9 (patch) | |
tree | a6b366f7061ba69c31138b1961f30994b9dfc38e | |
parent | conf: allow administrators to add and remove configs easily (diff) | |
download | wireguard-windows-224336aa077020c35d3feb8a534f92c814b232e9.tar.xz wireguard-windows-224336aa077020c35d3feb8a534f92c814b232e9.zip |
tunnel: enable {Pre,Post}{Up,Down} scripts gated behind admin knob
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
-rw-r--r-- | adminregistry.md | 9 | ||||
-rw-r--r-- | conf/config.go | 4 | ||||
-rw-r--r-- | conf/filewriter_windows.go | 2 | ||||
-rw-r--r-- | conf/parser.go | 8 | ||||
-rw-r--r-- | conf/writer.go | 13 | ||||
-rw-r--r-- | services/errors.go | 3 | ||||
-rw-r--r-- | tunnel/scriptrunner.go | 78 | ||||
-rw-r--r-- | tunnel/service.go | 35 | ||||
-rw-r--r-- | ui/confview.go | 25 | ||||
-rw-r--r-- | ui/syntax/highlighter.go | 4 |
10 files changed, 169 insertions, 12 deletions
diff --git a/adminregistry.md b/adminregistry.md index c7b8e559..1f4c8d44 100644 --- a/adminregistry.md +++ b/adminregistry.md @@ -16,3 +16,12 @@ users belonging to the Network Configuration Operators builtin group - Quitting the manager is forbidden. However, basic functionality such as starting and stopping tunnels remains intact. + +#### `HKLM\Software\WireGuard\DangerousScriptExecution` + +When this key is set to `DWORD(1)`, the tunnel service will execute the commands +specified in the `PreUp`, `PostUp`, `PreDown`, and `PostDown` options of a +tunnel configuration. Note that this execution is done as the Local System user, +which runs with the highest permissions on the operating system, and is therefore +a real target of malware. Therefore, you should enable this option only with the +utmost trepidation. diff --git a/conf/config.go b/conf/config.go index 1ce1988d..9eb6157e 100644 --- a/conf/config.go +++ b/conf/config.go @@ -49,6 +49,10 @@ type Interface struct { MTU uint16 DNS []net.IP DNSSearch []string + PreUp string + PostUp string + PreDown string + PostDown string } type Peer struct { diff --git a/conf/filewriter_windows.go b/conf/filewriter_windows.go index 9fb1f566..8860ba31 100644 --- a/conf/filewriter_windows.go +++ b/conf/filewriter_windows.go @@ -69,4 +69,4 @@ func writeEncryptedFile(destination string, contents []byte) error { return err } return nil -}
\ No newline at end of file +} diff --git a/conf/parser.go b/conf/parser.go index da21e796..17b0e21f 100644 --- a/conf/parser.go +++ b/conf/parser.go @@ -282,6 +282,14 @@ func FromWgQuick(s string, name string) (*Config, error) { conf.Interface.DNS = append(conf.Interface.DNS, a) } } + case "preup": + conf.Interface.PreUp = val + case "postup": + conf.Interface.PostUp = val + case "predown": + conf.Interface.PreDown = val + case "postdown": + conf.Interface.PostDown = val default: return nil, &ParseError{l18n.Sprintf("Invalid key for [Interface] section"), key} } diff --git a/conf/writer.go b/conf/writer.go index 365e65e9..6b16f843 100644 --- a/conf/writer.go +++ b/conf/writer.go @@ -41,6 +41,19 @@ func (conf *Config) ToWgQuick() string { output.WriteString(fmt.Sprintf("MTU = %d\n", conf.Interface.MTU)) } + if len(conf.Interface.PreUp) > 0 { + output.WriteString(fmt.Sprintf("PreUp = %s\n", conf.Interface.PreUp)) + } + if len(conf.Interface.PostUp) > 0 { + output.WriteString(fmt.Sprintf("PostUp = %s\n", conf.Interface.PostUp)) + } + if len(conf.Interface.PreDown) > 0 { + output.WriteString(fmt.Sprintf("PreDown = %s\n", conf.Interface.PreDown)) + } + if len(conf.Interface.PostDown) > 0 { + output.WriteString(fmt.Sprintf("PostDown = %s\n", conf.Interface.PostDown)) + } + for _, peer := range conf.Peers { output.WriteString("\n[Peer]\n") diff --git a/services/errors.go b/services/errors.go index 19bfebca..86820b9f 100644 --- a/services/errors.go +++ b/services/errors.go @@ -30,6 +30,7 @@ const ( ErrorTrackTunnels ErrorEnumerateSessions ErrorDropPrivileges + ErrorRunScript ErrorWin32 ) @@ -65,6 +66,8 @@ func (e Error) Error() string { return "Unable to enumerate current sessions" case ErrorDropPrivileges: return "Unable to drop privileges" + case ErrorRunScript: + return "An error occurred while running a configuration script command" case ErrorWin32: return "An internal Windows error has occurred" default: diff --git a/tunnel/scriptrunner.go b/tunnel/scriptrunner.go new file mode 100644 index 00000000..d34947db --- /dev/null +++ b/tunnel/scriptrunner.go @@ -0,0 +1,78 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2020 WireGuard LLC. All Rights Reserved. + */ + +package tunnel + +import ( + "bufio" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "syscall" + + "golang.org/x/sys/windows" + + "golang.zx2c4.com/wireguard/windows/conf" +) + +func runScriptCommand(command, interfaceName string) error { + if len(command) == 0 { + return nil + } + if !conf.AdminBool("DangerousScriptExecution") { + log.Printf("Skipping execution of script, because dangerous script execution is safely disabled: %#q", command) + return nil + } + command = strings.ReplaceAll(command, "%i", interfaceName) + log.Printf("Executing: %#q", command) + comspec, _ := os.LookupEnv("COMSPEC") + if len(comspec) == 0 { + system32, err := windows.GetSystemDirectory() + if err != nil { + return err + } + comspec = filepath.Join(system32, "cmd.exe") + } + + devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0) + if err != nil { + return err + } + defer devNull.Close() + reader, writer, err := os.Pipe() + if err != nil { + return err + } + process, err := os.StartProcess(comspec, nil /* CmdLine below */, &os.ProcAttr{ + Files: []*os.File{devNull, writer, writer}, + Sys: &syscall.SysProcAttr{ + HideWindow: true, + CmdLine: fmt.Sprintf("cmd /c %s", command), + }, + }) + writer.Close() + if err != nil { + reader.Close() + return err + } + go func() { + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + log.Printf("cmd> %s", scanner.Text()) + } + }() + state, err := process.Wait() + reader.Close() + if err != nil { + return err + } + if state.ExitCode() == 0 { + return nil + } + log.Printf("Command error exit status: %d", state.ExitCode()) + return windows.ERROR_GENERIC_COMMAND_FAILED +} diff --git a/tunnel/service.go b/tunnel/service.go index 33b5c116..e07fe139 100644 --- a/tunnel/service.go +++ b/tunnel/service.go @@ -41,6 +41,7 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, var uapi net.Listener var watcher *interfaceWatcher var nativeTun *tun.NativeTun + var config *conf.Config var err error serviceError := services.ErrorSuccess @@ -84,6 +85,9 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, } }() + if logErr == nil && dev != nil && config != nil { + logErr = runScriptCommand(config.Interface.PreDown, config.Name) + } if watcher != nil { watcher.Destroy() } @@ -93,6 +97,9 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, if dev != nil { dev.Close() } + if logErr == nil && dev != nil && config != nil { + _ = runScriptCommand(config.Interface.PostDown, config.Name) + } stopIt <- true log.Println("Shutting down") }() @@ -113,19 +120,19 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, } }() - conf, err := conf.LoadFromPath(service.Path) + config, err = conf.LoadFromPath(service.Path) if err != nil { serviceError = services.ErrorLoadConfiguration return } - conf.DeduplicateNetworkEntries() + config.DeduplicateNetworkEntries() err = CopyConfigOwnerToIPCSecurityDescriptor(service.Path) if err != nil { serviceError = services.ErrorLoadConfiguration return } - logPrefix := fmt.Sprintf("[%s] ", conf.Name) + logPrefix := fmt.Sprintf("[%s] ", config.Name) log.SetPrefix(logPrefix) log.Println("Starting", version.UserAgent()) @@ -151,14 +158,14 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, } log.Println("Resolving DNS names") - uapiConf, err := conf.ToUAPI() + uapiConf, err := config.ToUAPI() if err != nil { serviceError = services.ErrorDNSLookup return } log.Println("Creating Wintun interface") - wintun, err := tun.CreateTUNWithRequestedGUID(conf.Name, deterministicGUID(conf), 0) + wintun, err := tun.CreateTUNWithRequestedGUID(config.Name, deterministicGUID(config), 0) if err != nil { serviceError = services.ErrorCreateWintun return @@ -171,7 +178,13 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, log.Printf("Using Wintun/%d.%d", (wintunVersion>>16)&0xffff, wintunVersion&0xffff) } - err = enableFirewall(conf, nativeTun) + err = runScriptCommand(config.Interface.PreUp, config.Name) + if err != nil { + serviceError = services.ErrorRunScript + return + } + + err = enableFirewall(config, nativeTun) if err != nil { serviceError = services.ErrorFirewall return @@ -190,7 +203,7 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, dev = device.NewDevice(wintun, logger) log.Println("Setting interface configuration") - uapi, err = ipc.UAPIListen(conf.Name) + uapi, err = ipc.UAPIListen(config.Name) if err != nil { serviceError = services.ErrorUAPIListen return @@ -205,7 +218,7 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, log.Println("Bringing peers up") dev.Up() - watcher.Configure(dev, conf, nativeTun) + watcher.Configure(dev, config, nativeTun) log.Println("Listening for UAPI requests") go func() { @@ -218,6 +231,12 @@ func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, } }() + err = runScriptCommand(config.Interface.PostUp, config.Name) + if err != nil { + serviceError = services.ErrorRunScript + return + } + changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} log.Println("Startup complete") diff --git a/ui/confview.go b/ui/confview.go index 089f6572..c712c29d 100644 --- a/ui/confview.go +++ b/ui/confview.go @@ -50,6 +50,7 @@ type interfaceView struct { mtu *labelTextLine addresses *labelTextLine dns *labelTextLine + scripts *labelTextLine toggleActive *toggleActiveLine lines []widgetsLine } @@ -305,6 +306,7 @@ func newInterfaceView(parent walk.Container) (*interfaceView, error) { {l18n.Sprintf("MTU:"), &iv.mtu}, {l18n.Sprintf("Addresses:"), &iv.addresses}, {l18n.Sprintf("DNS servers:"), &iv.dns}, + {l18n.Sprintf("Scripts:"), &iv.scripts}, } if iv.lines, err = createLabelTextLines(items, parent, &disposables); err != nil { return nil, err @@ -402,6 +404,29 @@ func (iv *interfaceView) apply(c *conf.Interface) { } else { iv.dns.hide() } + + var scriptsInUse []string + if len(c.PreUp) > 0 { + scriptsInUse = append(scriptsInUse, l18n.Sprintf("pre-up")) + } + if len(c.PostUp) > 0 { + scriptsInUse = append(scriptsInUse, l18n.Sprintf("post-up")) + } + if len(c.PreDown) > 0 { + scriptsInUse = append(scriptsInUse, l18n.Sprintf("pre-down")) + } + if len(c.PostDown) > 0 { + scriptsInUse = append(scriptsInUse, l18n.Sprintf("post-down")) + } + if len(scriptsInUse) > 0 { + if conf.AdminBool("DangerousScriptExecution") { + iv.scripts.show(strings.Join(scriptsInUse, l18n.EnumerationSeparator())) + } else { + iv.scripts.show(l18n.Sprintf("disabled, per policy")) + } + } else { + iv.scripts.hide() + } } func (pv *peerView) widgetsLines() []widgetsLine { diff --git a/ui/syntax/highlighter.go b/ui/syntax/highlighter.go index fa6a6530..f6edc712 100644 --- a/ui/syntax/highlighter.go +++ b/ui/syntax/highlighter.go @@ -405,8 +405,6 @@ func (s stringSpan) field() field { return fieldEndpoint case s.isCaselessSame("PersistentKeepalive"): return fieldPersistentKeepalive - } - /* TODO: uncomment this once we support these in the client case s.isCaselessSame("PreUp"): return fieldPreUp case s.isCaselessSame("PostUp"): @@ -415,7 +413,7 @@ func (s stringSpan) field() field { return fieldPreDown case s.isCaselessSame("PostDown"): return fieldPostDown - */ + } return fieldInvalid } |