From 75b26ec7f4f001a13db2c8dd1fa02d7481fd2b72 Mon Sep 17 00:00:00 2001 From: "kali kaneko (leap communications)" Date: Mon, 17 Feb 2020 17:13:25 +0100 Subject: [feat] initial implementation of windows service --- pkg/helper/darwin.go | 8 ++++ pkg/helper/helper.go | 9 +++- pkg/helper/linux.go | 8 ++++ pkg/helper/windows.go | 99 +++++++++++++++++++++++++++++++++++++++++++ pkg/helper/windows_install.go | 91 +++++++++++++++++++++++++++++++++++++++ pkg/helper/windows_manage.go | 62 +++++++++++++++++++++++++++ pkg/helper/windows_service.go | 72 +++++++++++++++++++++++++++++++ 7 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 pkg/helper/windows_install.go create mode 100644 pkg/helper/windows_manage.go create mode 100644 pkg/helper/windows_service.go (limited to 'pkg') diff --git a/pkg/helper/darwin.go b/pkg/helper/darwin.go index 0cee714..f65012d 100644 --- a/pkg/helper/darwin.go +++ b/pkg/helper/darwin.go @@ -60,6 +60,10 @@ var ( } ) +func parseCliArgs() { + // OSX helper does not respond to arguments +} + func daemonize() { cntxt := &daemon.Context{ PidFileName: "pid", @@ -82,6 +86,10 @@ func daemonize() { log.Print("bitmask-helper daemon started") } +func doHandleCommands(bindAddr string) { + runCommandServer(bindAddr) +} + func getOpenvpnPath() string { return openvpnPath } diff --git a/pkg/helper/helper.go b/pkg/helper/helper.go index 2e7ffd1..ab1894e 100644 --- a/pkg/helper/helper.go +++ b/pkg/helper/helper.go @@ -26,8 +26,7 @@ type openvpnT struct { cmd *exec.Cmd } -func ServeHTTP(bindAddr string) { - daemonize() +func runCommandServer(bindAddr string) { openvpn := openvpnT{nil} http.HandleFunc("/openvpn/start", openvpn.start) http.HandleFunc("/openvpn/stop", openvpn.stop) @@ -38,6 +37,12 @@ func ServeHTTP(bindAddr string) { log.Fatal(http.ListenAndServe(bindAddr, nil)) } +func ServeHTTP(bindAddr string) { + parseCliArgs() + daemonize() + doHandleCommands(bindAddr) +} + func (openvpn *openvpnT) start(w http.ResponseWriter, r *http.Request) { args, err := getArgs(r) if err != nil { diff --git a/pkg/helper/linux.go b/pkg/helper/linux.go index 8ee3037..3aaa0fe 100644 --- a/pkg/helper/linux.go +++ b/pkg/helper/linux.go @@ -40,8 +40,16 @@ var ( } ) +func parseCliArgs() { + // linux helper does not reply to args +} + func daemonize() {} +func doHandleCommands(bindAddr string) { + runCommandServer(bindAddr) +} + func getOpenvpnPath() string { if os.Getenv("SNAP") != "" { return snapOpenvpnPath diff --git a/pkg/helper/windows.go b/pkg/helper/windows.go index 7e47884..fc80853 100644 --- a/pkg/helper/windows.go +++ b/pkg/helper/windows.go @@ -17,14 +17,19 @@ package helper import ( + "fmt" "log" "os" "os/exec" + "strings" "0xacab.org/leap/bitmask-vpn/pkg/config" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" ) const ( + svcName = config.BinaryName + `-helper` appPath = `C:\Program Files\` + config.ApplicationName + `\` LogFolder = appPath openvpnPath = appPath + `openvpn.exe` @@ -36,10 +41,66 @@ var ( "--script-security", "1", "--block-outside-dns", } + httpBindAddr string ) +func parseCliArgs() { + isIntSess, err := svc.IsAnInteractiveSession() + if err != nil { + log.Fatalf("Failed to determine if we are running in an interactive session: %v", err) + } + if !isIntSess { + runService(svcName, false) + return + } + admin := isAdmin() + fmt.Printf("Running as admin: %v\n", admin) + if !admin { + log.Fatal("Needs to be run as administrator") + } + if len(os.Args) < 2 { + usage("ERROR: no command specified") + } + cmd := strings.ToLower(os.Args[1]) + switch cmd { + case "debug": + runService(svcName, true) + return + case "install": + // TODO get binary name + err = installService(svcName, "bitmask-helper service") + case "remove": + err = removeService(svcName) + case "start": + err = startService(svcName) + case "stop": + err = controlService(svcName, svc.Stop, svc.Stopped) + default: + usage(fmt.Sprintf("ERROR: Invalid command %s", cmd)) + } + if err != nil { + log.Fatalf("Failed to %s %s: %v", cmd, svcName, err) + } + return +} + +func usage(errmsg string) { + fmt.Fprintf(os.Stderr, + "%s\n\n"+ + "usage: %s \n"+ + " where is one of\n"+ + " install, remove, debug, start, stop\n", + errmsg, os.Args[0]) + os.Exit(2) +} + func daemonize() {} +// http server is called from within Execute in windows +func doHandleCommands(bindAddr string) { + httpBindAddr = bindAddr +} + func getOpenvpnPath() string { if _, err := os.Stat(openvpnPath); !os.IsNotExist(err) { return openvpnPath @@ -67,3 +128,41 @@ func firewallIsUp() bool { log.Println("IsUp firewall: do nothing, not implemented") return false } + +func isAdmin() bool { + var sid *windows.SID + + // Although this looks scary, it is directly copied from the + // official windows documentation. The Go API for this is a + // direct wrap around the official C++ API. + // See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership + err := windows.AllocateAndInitializeSid( + &windows.SECURITY_NT_AUTHORITY, + 2, + windows.SECURITY_BUILTIN_DOMAIN_RID, + windows.DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + &sid) + if err != nil { + log.Fatalf("SID Error: %s", err) + return false + } + + // This appears to cast a null pointer so I'm not sure why this + // works, but this guy says it does and it Works for Me™: + // https://github.com/golang/go/issues/28804#issuecomment-438838144 + token := windows.Token(0) + + member, err := token.IsMember(sid) + //fmt.Println("Admin?", member) + if err != nil { + log.Fatalf("Token Membership Error: %s", err) + return false + } + return member + + // Also note that an admin is _not_ necessarily considered + // elevated. + // For elevation see https://github.com/mozey/run-as-admin + //fmt.Println("Elevated?", token.IsElevated()) +} diff --git a/pkg/helper/windows_install.go b/pkg/helper/windows_install.go new file mode 100644 index 0000000..6743e2a --- /dev/null +++ b/pkg/helper/windows_install.go @@ -0,0 +1,91 @@ +// +build windows +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package helper + +import ( + "fmt" + "os" + "path/filepath" + + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +func exePath() (string, error) { + prog := os.Args[0] + p, err := filepath.Abs(prog) + if err != nil { + return "", err + } + fi, err := os.Stat(p) + if err == nil { + if !fi.Mode().IsDir() { + return p, nil + } + err = fmt.Errorf("%s is directory", p) + } + if filepath.Ext(p) == "" { + p += ".exe" + fi, err := os.Stat(p) + if err == nil { + if !fi.Mode().IsDir() { + return p, nil + } + err = fmt.Errorf("%s is directory", p) + } + } + return "", err +} + +func installService(name, desc string) error { + exepath, err := exePath() + if err != nil { + return err + } + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err == nil { + s.Close() + return fmt.Errorf("service %s already exists", name) + } + s, err = m.CreateService(name, exepath, mgr.Config{DisplayName: desc}, "is", "auto-started") + if err != nil { + return err + } + defer s.Close() + err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info) + if err != nil { + s.Delete() + return fmt.Errorf("SetupEventLogSource() failed: %s", err) + } + return nil +} + +func removeService(name string) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err != nil { + return fmt.Errorf("service %s is not installed", name) + } + defer s.Close() + err = s.Delete() + if err != nil { + return err + } + err = eventlog.Remove(name) + if err != nil { + return fmt.Errorf("RemoveEventLogSource() failed: %s", err) + } + return nil +} diff --git a/pkg/helper/windows_manage.go b/pkg/helper/windows_manage.go new file mode 100644 index 0000000..96da9a7 --- /dev/null +++ b/pkg/helper/windows_manage.go @@ -0,0 +1,62 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package helper + +import ( + "fmt" + "time" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) + +func startService(name string) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err != nil { + return fmt.Errorf("could not access service: %v", err) + } + defer s.Close() + err = s.Start("is", "manual-started") + if err != nil { + return fmt.Errorf("could not start service: %v", err) + } + return nil +} + +func controlService(name string, c svc.Cmd, to svc.State) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(name) + if err != nil { + return fmt.Errorf("could not access service: %v", err) + } + defer s.Close() + status, err := s.Control(c) + if err != nil { + return fmt.Errorf("could not send control=%d: %v", c, err) + } + timeout := time.Now().Add(10 * time.Second) + for status.State != to { + if timeout.Before(time.Now()) { + return fmt.Errorf("timeout waiting for service to go to state=%d", to) + } + time.Sleep(300 * time.Millisecond) + status, err = s.Query() + if err != nil { + return fmt.Errorf("could not retrieve service status: %v", err) + } + } + return nil +} diff --git a/pkg/helper/windows_service.go b/pkg/helper/windows_service.go new file mode 100644 index 0000000..b35ba19 --- /dev/null +++ b/pkg/helper/windows_service.go @@ -0,0 +1,72 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package helper + +import ( + "fmt" + //"strings" + //"time" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" + "golang.org/x/sys/windows/svc/eventlog" +) + +var elog debug.Log + +type myservice struct{} + +func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + changes <- svc.Status{State: svc.StartPending} + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + // TODO use httpBindAddr + go runCommandServer("localhost:7171") +loop: + for { + select { + case c := <-r: + switch c.Cmd { + // TODO start?? + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + elog.Info(1, "shutting down service") + break loop + default: + elog.Error(1, fmt.Sprintf("unexpected control request #%d", c)) + } + } + } + changes <- svc.Status{State: svc.StopPending} + return +} + +func runService(name string, isDebug bool) { + var err error + if isDebug { + elog = debug.New(name) + } else { + elog, err = eventlog.Open(name) + if err != nil { + return + } + } + defer elog.Close() + + elog.Info(1, fmt.Sprintf("starting %s service", name)) + run := svc.Run + if isDebug { + run = debug.Run + } + err = run(name, &myservice{}) + if err != nil { + elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err)) + return + } + elog.Info(1, fmt.Sprintf("%s service stopped", name)) +} -- cgit v1.2.3