From db8b19620ec3c47a73683f5e8a17993c189dbf78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hellmann?= Date: Tue, 30 Apr 2024 15:40:34 +0200 Subject: [PATCH] fix: (windows) wireguard command permission elevation --- .gitignore | 1 + src/archiveClient/handler_findGitFiles.go | 10 +-- src/cmd/servicePush.go | 2 +- src/cmd/vpnUp.go | 19 ++--- src/cmdRunner/execCmd.go | 46 ++++++++++++ src/cmdRunner/run.go | 17 +++-- src/wg/darwin.go | 9 +-- src/wg/linux.go | 9 +-- src/wg/windows.go | 88 +++++++++++++++++++++-- 9 files changed, 164 insertions(+), 37 deletions(-) create mode 100644 src/cmdRunner/execCmd.go diff --git a/.gitignore b/.gitignore index 495c7004..329c2449 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +.sandbox include/ .sandbox/ .tool-versions diff --git a/src/archiveClient/handler_findGitFiles.go b/src/archiveClient/handler_findGitFiles.go index c208449b..f43cdb71 100644 --- a/src/archiveClient/handler_findGitFiles.go +++ b/src/archiveClient/handler_findGitFiles.go @@ -3,9 +3,9 @@ package archiveClient import ( "bufio" "bytes" + "context" "io" "os" - "os/exec" "path/filepath" "strings" @@ -13,7 +13,7 @@ import ( "github.com/zeropsio/zcli/src/cmdRunner" ) -func (h *Handler) FindGitFiles(workingDir string) (res []File, _ error) { +func (h *Handler) FindGitFiles(ctx context.Context, workingDir string) (res []File, _ error) { workingDir, err := filepath.Abs(workingDir) if err != nil { return nil, err @@ -28,8 +28,8 @@ func (h *Handler) FindGitFiles(workingDir string) (res []File, _ error) { } } - createCmd := func(name string, arg ...string) *exec.Cmd { - cmd := exec.Command(name, arg...) + createCmd := func(name string, arg ...string) *cmdRunner.ExecCmd { + cmd := cmdRunner.CommandContext(ctx, name, arg...) cmd.Dir = workingDir return cmd } @@ -102,7 +102,7 @@ func (h *Handler) FindGitFiles(workingDir string) (res []File, _ error) { return res, nil } -func (h *Handler) listFiles(cmd *exec.Cmd, fn func(path string) error) error { +func (h *Handler) listFiles(cmd *cmdRunner.ExecCmd, fn func(path string) error) error { output, err := cmdRunner.Run(cmd) if err != nil { return err diff --git a/src/cmd/servicePush.go b/src/cmd/servicePush.go index 43548928..a2fa157b 100644 --- a/src/cmd/servicePush.go +++ b/src/cmd/servicePush.go @@ -98,7 +98,7 @@ func servicePushCmd() *cmdBuilder.Cmd { return err } defer os.Remove(tempFile) - files, err := arch.FindGitFiles(cmdData.Params.GetString("workingDir")) + files, err := arch.FindGitFiles(ctx, cmdData.Params.GetString("workingDir")) if err != nil { return err } diff --git a/src/cmd/vpnUp.go b/src/cmd/vpnUp.go index 31d710b6..1bc2fce8 100644 --- a/src/cmd/vpnUp.go +++ b/src/cmd/vpnUp.go @@ -92,19 +92,14 @@ func vpnUpCmd() *cmdBuilder.Cmd { return err } - if err := func() error { - f, err := file.Open(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) - if err != nil { - return err - } - defer f.Close() + f, err := file.Open(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) + if err != nil { + return err + } + defer f.Close() - err = wg.GenerateConfig(f, privateKey, vpnSettings) - if err != nil { - return err - } - return nil - }(); err != nil { + err = wg.GenerateConfig(f, privateKey, vpnSettings) + if err != nil { return err } diff --git a/src/cmdRunner/execCmd.go b/src/cmdRunner/execCmd.go new file mode 100644 index 00000000..0db04086 --- /dev/null +++ b/src/cmdRunner/execCmd.go @@ -0,0 +1,46 @@ +package cmdRunner + +import ( + "context" + "os/exec" +) + +type Func func(ctx context.Context) error + +func CommandContext(ctx context.Context, cmd string, args ...string) *ExecCmd { + return &ExecCmd{ + Cmd: exec.CommandContext(ctx, cmd, args...), + ctx: ctx, + } +} + +type ExecCmd struct { + *exec.Cmd + ctx context.Context + before Func + after Func +} + +func (e *ExecCmd) SetBefore(f Func) *ExecCmd { + e.before = f + return e +} + +func (e *ExecCmd) execBefore() error { + if e.before == nil { + return nil + } + return e.before(e.ctx) +} + +func (e *ExecCmd) SetAfter(f Func) *ExecCmd { + e.after = f + return e +} + +func (e *ExecCmd) execAfter() error { + if e.after == nil { + return nil + } + return e.after(e.ctx) +} diff --git a/src/cmdRunner/run.go b/src/cmdRunner/run.go index e88dcd8f..69290dcc 100644 --- a/src/cmdRunner/run.go +++ b/src/cmdRunner/run.go @@ -12,13 +12,8 @@ var ErrIpAlreadySet = errors.New("RTNETLINK answers: File exists") var ErrCannotFindDevice = errors.New(`Cannot find device "wg0"`) var ErrOperationNotPermitted = errors.New(`Operation not permitted`) -type ExecErrInterface interface { - error - ExitCode() int -} - type execError struct { - cmd *exec.Cmd + cmd *ExecCmd prev error exitCode int } @@ -39,13 +34,17 @@ func (e execError) Is(target error) bool { return errors.Is(e.prev, target) } -func Run(cmd *exec.Cmd) ([]byte, ExecErrInterface) { +func Run(cmd *ExecCmd) ([]byte, error) { output := &bytes.Buffer{} errOutput := &bytes.Buffer{} cmd.Stdout = output cmd.Stderr = errOutput cmd.Env = append(os.Environ(), cmd.Env...) + if err := cmd.execBefore(); err != nil { + return nil, err + } + if err := cmd.Run(); err != nil { exitCode := 0 var exitError *exec.ExitError @@ -81,5 +80,9 @@ func Run(cmd *exec.Cmd) ([]byte, ExecErrInterface) { return nil, execError } + if err := cmd.execAfter(); err != nil { + return nil, err + } + return output.Bytes(), nil } diff --git a/src/wg/darwin.go b/src/wg/darwin.go index 68c72573..2ee9cf09 100644 --- a/src/wg/darwin.go +++ b/src/wg/darwin.go @@ -10,6 +10,7 @@ import ( "text/template" "github.com/pkg/errors" + "github.com/zeropsio/zcli/src/cmdRunner" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/zeropsio/zcli/src/i18n" @@ -34,12 +35,12 @@ func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.Proj return template.Must(template.New("wg template").Parse(vpnTmpl)).Execute(f, data) } -func UpCmd(ctx context.Context, filePath string) (err *exec.Cmd) { - return exec.CommandContext(ctx, "wg-quick", "up", filePath) +func UpCmd(ctx context.Context, filePath string) (err *cmdRunner.ExecCmd) { + return cmdRunner.CommandContext(ctx, "wg-quick", "up", filePath) } -func DownCmd(ctx context.Context, filePath, _ string) (err *exec.Cmd) { - return exec.CommandContext(ctx, "wg-quick", "down", filePath) +func DownCmd(ctx context.Context, filePath, _ string) (err *cmdRunner.ExecCmd) { + return cmdRunner.CommandContext(ctx, "wg-quick", "down", filePath) } var vpnTmpl = ` diff --git a/src/wg/linux.go b/src/wg/linux.go index ffe9d157..f2c61aa9 100644 --- a/src/wg/linux.go +++ b/src/wg/linux.go @@ -10,6 +10,7 @@ import ( "text/template" "github.com/pkg/errors" + "github.com/zeropsio/zcli/src/cmdRunner" "github.com/zeropsio/zcli/src/i18n" "github.com/zeropsio/zerops-go/dto/output" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -33,12 +34,12 @@ func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.Proj return template.Must(template.New("wg template").Parse(vpnTmpl)).Execute(f, data) } -func UpCmd(ctx context.Context, filePath string) (err *exec.Cmd) { - return exec.CommandContext(ctx, "wg-quick", "up", filePath) +func UpCmd(ctx context.Context, filePath string) (err *cmdRunner.ExecCmd) { + return cmdRunner.CommandContext(ctx, "wg-quick", "up", filePath) } -func DownCmd(ctx context.Context, filePath, _ string) (err *exec.Cmd) { - return exec.CommandContext(ctx, "wg-quick", "down", filePath) +func DownCmd(ctx context.Context, filePath, _ string) (err *cmdRunner.ExecCmd) { + return cmdRunner.CommandContext(ctx, "wg-quick", "down", filePath) } var vpnTmpl = ` diff --git a/src/wg/windows.go b/src/wg/windows.go index efc6b72c..325bf105 100644 --- a/src/wg/windows.go +++ b/src/wg/windows.go @@ -6,15 +6,24 @@ package wg import ( "context" "io" + "os" "os/exec" + "path/filepath" + "strings" "text/template" "github.com/pkg/errors" + "github.com/zeropsio/zcli/src/cmdRunner" + "github.com/zeropsio/zcli/src/constants" "github.com/zeropsio/zcli/src/i18n" "github.com/zeropsio/zerops-go/dto/output" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +// To install wireguard tunnel in windows wireguard.exe has to be run with elevated permissions. +// Only (simple) way I found to achieve this is to run Start-Process cmdlet with param '-Verb RunAS' +// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/start-process?view=powershell-7.4 + func CheckWgInstallation() error { _, err := exec.LookPath("wireguard") if err != nil { @@ -33,12 +42,72 @@ func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.Proj return template.Must(template.New("wg template").Parse(vpnTmpl)).Execute(f, data) } -func UpCmd(ctx context.Context, filePath string) (err *exec.Cmd) { - return exec.CommandContext(ctx, "wireguard", "/installtunnelservice", filePath) +func UpCmd(ctx context.Context, filePath string) (err *cmdRunner.ExecCmd) { + return cmdRunner.CommandContext(ctx, + "powershell", + "-Command", + "Start-Process", "wireguard", + "-Verb", "RunAs", + "-ArgumentList "+formatArgumentList("/installtunnelservice", filePath), + ). + SetBefore(beforeUp(filePath)) } -func DownCmd(ctx context.Context, _, interfaceName string) (err *exec.Cmd) { - return exec.CommandContext(ctx, "wireguard", "/uninstalltunnelservice", interfaceName) +// beforeUp this function tries to remove previous zerops.conf from usual wireguard configuration +// dir (at %ProgramFiles%\WireGuard\Data\Configurations) and copy a newly generated one. +// It fails with error = nil because it's only for windows wireguard GUI. +func beforeUp(zeropsConfPath string) cmdRunner.Func { + return func(_ context.Context) error { + programFiles, set := os.LookupEnv("ProgramFiles") + if !set { + return nil + } + + wgConfigDir := filepath.Join(programFiles, "WireGuard", "Data", "Configurations") + stat, err := os.Stat(wgConfigDir) + if err != nil { + return nil + } + if !stat.IsDir() { + return nil + } + + wgConfFile := filepath.Join(wgConfigDir, constants.WgConfigFile) + // remove previous zerops.conf encrypted by wireguard.exe thus ending with .dpapi + // https://git.zx2c4.com/wireguard-windows/about/docs/enterprise.md + _ = os.Remove(wgConfFile + ".dpapi") + + wgConf, err := os.OpenFile(filepath.Join(wgConfigDir, constants.WgConfigFile), os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return nil + } + defer wgConf.Close() + + zeropsConf, err := os.OpenFile(zeropsConfPath, os.O_RDONLY, 0666) + if err != nil { + _ = os.Remove(wgConfFile) + return nil + } + defer zeropsConf.Close() + + _, err = io.Copy(wgConf, zeropsConf) + if err != nil { + _ = os.Remove(wgConfFile) + return nil + } + + return nil + } +} + +func DownCmd(ctx context.Context, _, interfaceName string) (err *cmdRunner.ExecCmd) { + return cmdRunner.CommandContext(ctx, + "powershell", + "-Command", + "Start-Process", "wireguard", + "-Verb", "RunAs", + "-ArgumentList "+formatArgumentList("/uninstalltunnelservice", interfaceName), + ) } var vpnTmpl = ` @@ -60,3 +129,14 @@ Endpoint = {{.ProjectIpv4SharedEndpoint}} PersistentKeepalive = 5 ` + +func formatArgumentList(args ...string) string { + for i, a := range args { + args[i] = quote(a) + } + return strings.Join(args, ", ") +} + +func quote(in string) string { + return `"` + in + `"` +}