summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkali kaneko (leap communications) <kali@leap.se>2020-02-17 17:13:25 +0100
committerkali kaneko (leap communications) <kali@leap.se>2020-04-30 22:10:15 +0200
commit75b26ec7f4f001a13db2c8dd1fa02d7481fd2b72 (patch)
tree555134c02f5cfcf22f3060fac59bf51fe8a64f1a
parentaf04003d9e37ef4a08c29e967962df40d2541660 (diff)
[feat] initial implementation of windows service
-rw-r--r--go.mod2
-rw-r--r--go.sum3
-rw-r--r--pkg/helper/darwin.go8
-rw-r--r--pkg/helper/helper.go9
-rw-r--r--pkg/helper/linux.go8
-rw-r--r--pkg/helper/windows.go99
-rw-r--r--pkg/helper/windows_install.go91
-rw-r--r--pkg/helper/windows_manage.go62
-rw-r--r--pkg/helper/windows_service.go72
9 files changed, 351 insertions, 3 deletions
diff --git a/go.mod b/go.mod
index 9aef301..c6525d0 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,7 @@ require (
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/crypto v0.0.0-20191105034135-c7e5f84aec59 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
- golang.org/x/sys v0.0.0-20191105142833-ac3223d80179 // indirect
+ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20200427153019-a90b7300be7c // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
diff --git a/go.sum b/go.sum
index 2b5c0b7..7b5a675 100644
--- a/go.sum
+++ b/go.sum
@@ -131,7 +131,10 @@ golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191104094858-e8c54fb511f6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191105142833-ac3223d80179 h1:IqVhUQp5B9ARnZUcfqXy6zP+A+YuPpP7IFo8gFeCOzU=
golang.org/x/sys v0.0.0-20191105142833-ac3223d80179/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
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 <command>\n"+
+ " where <command> 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))
+}