// +build windows
// Copyright (C) 2018-2020 LEAP
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
package helper
import (
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"path"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
)
var (
svcName = BinaryName + `-helper-v2`
// XXX this is set to c:\WINDOWS\system32 on initialization. Do not use it, use a function call instead.
appPath = getExecDir()
LogFolder = getExecDir()
openvpnPath = path.Join(appPath, "openvpn.exe")
chocoOpenvpnPath = `C:\Program Files\OpenVPN\bin\openvpn.exe`
httpServerConf = &httpConf{}
)
func getPlatformOpenvpnFlags() []string {
return []string{
"--script-security", "1",
"--block-outside-dns",
}
}
func getExecDir() string {
ex, err := os.Executable()
if err != nil {
log.Println("Cannot find executable path")
return ""
}
/* XXX filepath.Abs is buggy, maybe because of spaces in the path. fuck it, this is good enough for now */
return strings.Replace(ex, "\\helper.exe", "", 1)
}
type httpConf struct {
BindAddr string
}
// parseCliArgs allows the helper binary to install/uninstall itself. It requires admin privileges.
// However, be warned: if you intend to use it from the command line, you will have to compile it with the Go compiler yourself.
// the version we're shipping (ie, cross-compiled with the mingw compiler) apparently is not able to output to stdout/stderr properly.
// To compile a usable version, from the top of the repo you can do:
// "cd cmd/bitmask-helper && GOOS=windows GOARCH=i386 go build"
func parseCliArgs() {
log.Println("Parsing CLI args...")
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
}
log.Println("Checking for admin")
admin := isAdmin()
fmt.Printf("Running as admin: %v\n", admin)
if !admin {
fmt.Println("Needs to be run as administrator")
os.Exit(2)
}
if len(os.Args) < 2 {
usage("ERROR: no command specified")
}
cmd := strings.ToLower(os.Args[1])
log.Println("cmd:", cmd)
switch cmd {
case "debug":
// run the service on the foreground, for debugging
runService(svcName, true)
return
case "install":
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)
}
// initializeService only initializes the server.
// we expect serveHTTP to be called from within Execute in windows
func initializeService(preferredPort int) {
port := getFirstAvailablePortFrom(preferredPort)
writePortToFile(port)
httpServerConf.BindAddr = "localhost:" + strconv.Itoa(port)
log.Println("Command server initialized to listen on", httpServerConf.BindAddr)
}
func daemonize() {}
// runServer does nothing, serveHTTP is called from within Execute in windows
func runServer(port int) {}
func getOpenvpnPath() string {
openvpn := path.Join(getExecDir(), "openvpn.exe")
if _, err := os.Stat(openvpn); !os.IsNotExist(err) {
log.Println("DEBUG: openvpnpath found,", openvpnPath)
return openvpn
} else if _, err := os.Stat(chocoOpenvpnPath); !os.IsNotExist(err) {
log.Println("DEBUG: choco openvpn found,", chocoOpenvpnPath)
return chocoOpenvpnPath
}
log.Println("DEBUG: did not find system-wide openvpn...")
return "openvpn.exe"
}
func kill(cmd *exec.Cmd) error {
return cmd.Process.Kill()
}
func firewallStart(gateways []string) error {
log.Println("Start firewall: do nothing, not implemented")
return nil
}
func firewallStop() error {
log.Println("Stop firewall: do nothing, not implemented")
return nil
}
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())
}