diff --git a/go.mod b/go.mod
index 9aef301c4247b579e1a05dbfe41c455cdc4771e4..c6525d052b9739df5022b96310306a7cc577b30d 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 2b5c0b7b3ae4197f72dd77aca59c778c2d54db7c..7b5a675152ff3679a66270f929980538465f89a4 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 0cee714adf6da119607cbe0e10a90491074829e5..f65012da53ca2ac7418f04294f34a33496182fd4 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 2e7ffd1847f597d02694c3b72da550e7e5147cca..ab1894e1f07ad3fcfc421dacc41afa16fc744ada 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 8ee30372d8fb0e695a3eca516ccc24beefd8c532..3aaa0fe2fb130fc85c526ee635165e8d0c404a50 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 7e47884f1d411f2e0a52aee81075d6c9d133324d..fc808537a5442c5c630d446eb6d1f3e370ff5bd6 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 0000000000000000000000000000000000000000..6743e2ad2ea12a15e67825ccdbf45de4f9e4ced7
--- /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 0000000000000000000000000000000000000000..96da9a7add43a1262684004aa841f86b90ba79d3
--- /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 0000000000000000000000000000000000000000..b35ba19f69532682c219a248030bb3b4526a4552
--- /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))
+}