From ab3263502a3eb2ba008e7aab61709cb11c8aac1e Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Mon, 25 Feb 2019 18:47:03 +0100 Subject: service: introduce base of services --- service/errors.go | 19 +++ service/install.go | 196 +++++++++++++++++++++++++++++ service/mksyscall.go | 8 ++ service/service_manager.go | 294 ++++++++++++++++++++++++++++++++++++++++++++ service/service_tunnel.go | 174 ++++++++++++++++++++++++++ service/zsyscall_windows.go | 116 +++++++++++++++++ 6 files changed, 807 insertions(+) create mode 100644 service/errors.go create mode 100644 service/install.go create mode 100644 service/mksyscall.go create mode 100644 service/service_manager.go create mode 100644 service/service_tunnel.go create mode 100644 service/zsyscall_windows.go (limited to 'service') diff --git a/service/errors.go b/service/errors.go new file mode 100644 index 00000000..b6566d00 --- /dev/null +++ b/service/errors.go @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2019 WireGuard LLC. All Rights Reserved. + */ + +package service + +const ( + ERROR_LOG_CONTAINER_OPEN_FAILED uint32 = 0x000019F1 + ERROR_INVALID_PARAMETER uint32 = 0x00000057 + ERROR_OPEN_FAILED uint32 = 0x0000006E + ERROR_ADAP_HDW_ERR uint32 = 0x00000039 + ERROR_PIPE_LISTENING uint32 = 0x00000218 + ERROR_BAD_LOGON_SESSION_STATE uint32 = 0x00000555 + ERROR_BAD_PATHNAME uint32 = 0x000000A1 + ERROR_FILE_NOT_FOUND uint32 = 0x00000002 + ERROR_SERVER_SID_MISMATCH uint32 = 0x00000274 + ERROR_NETWORK_BUSY uint32 = 0x00000036 +) diff --git a/service/install.go b/service/install.go new file mode 100644 index 00000000..32131f94 --- /dev/null +++ b/service/install.go @@ -0,0 +1,196 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package service + +import ( + "errors" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + svcdbg "golang.org/x/sys/windows/svc/debug" + "golang.org/x/sys/windows/svc/mgr" + "golang.zx2c4.com/wireguard/windows/conf" + "os" + "time" +) + +var cachedServiceManager *mgr.Mgr + +func serviceManager() (*mgr.Mgr, error) { + if cachedServiceManager != nil { + return cachedServiceManager, nil + } + m, err := mgr.Connect() + if err != nil { + return nil, err + } + cachedServiceManager = m + return cachedServiceManager, nil +} + +func InstallManager() error { + m, err := serviceManager() + if err != nil { + return err + } + path, err := os.Executable() + if err != nil { + return nil + } + + //TODO: Do we want to bail if executable isn't being run from the right location? + + serviceName := "WireGuard Manager" + service, err := m.OpenService(serviceName) + if err == nil { + status, err := service.Query() + if err != nil { + service.Close() + return err + } + if status.State != svc.Stopped { + service.Close() + return errors.New("Manager already installed and running") + } + err = service.Delete() + service.Close() + if err != nil { + return err + } + for { + service, err = m.OpenService(serviceName) + if err != nil { + break + } + service.Close() + time.Sleep(time.Second) + } + } + + config := mgr.Config{ + ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, + StartType: mgr.StartAutomatic, + ErrorControl: mgr.ErrorNormal, + DisplayName: serviceName, + } + + service, err = m.CreateService(serviceName, path, config, "/managerservice") + if err != nil { + return err + } + service.Start() + return service.Close() +} + +func UninstallManager() error { + m, err := serviceManager() + if err != nil { + return err + } + serviceName := "WireGuard Manager" + service, err := m.OpenService(serviceName) + if err != nil { + return err + } + service.Control(svc.Stop) + err = service.Delete() + err2 := service.Close() + if err != nil { + return err + } + return err2 +} + +func RunManager() error { + return svc.Run("WireGuard Manager", &managerService{}) +} + +func InstallTunnel(configPath string) error { + m, err := serviceManager() + if err != nil { + return err + } + path, err := os.Executable() + if err != nil { + return nil + } + + name, err := conf.NameFromPath(configPath) + if err != nil { + return err + } + + serviceName := "WireGuard Tunnel: " + name + service, err := m.OpenService(serviceName) + if err == nil { + status, err := service.Query() + if err != nil { + service.Close() + return err + } + if status.State != svc.Stopped { + service.Close() + return errors.New("Tunnel already installed and running") + } + err = service.Delete() + service.Close() + if err != nil { + return err + } + for { + service, err = m.OpenService(serviceName) + if err != nil { + break + } + service.Close() + time.Sleep(time.Second) + } + } + + config := mgr.Config{ + ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, + StartType: mgr.StartAutomatic, + ErrorControl: mgr.ErrorNormal, + DisplayName: serviceName, + } + + service, err = m.CreateService(serviceName, path, config, "/tunnelservice", configPath) + if err != nil { + return err + } + service.Start() + return service.Close() +} + +func UninstallTunnel(name string) error { + m, err := serviceManager() + if err != nil { + return err + } + serviceName := "WireGuard Tunnel: " + name + service, err := m.OpenService(serviceName) + if err != nil { + return err + } + service.Control(svc.Stop) + err = service.Delete() + err2 := service.Close() + if err != nil { + return err + } + return err2 +} + +func RunTunnel(confPath string, debug bool) error { + name, err := conf.NameFromPath(confPath) + if err != nil { + return err + } + if debug { + return svcdbg.Run("WireGuard Tunnel: "+name, &tunnelService{confPath, true}) + } else { + return svc.Run("WireGuard Tunnel: "+name, &tunnelService{confPath, false}) + } +} diff --git a/service/mksyscall.go b/service/mksyscall.go new file mode 100644 index 00000000..f80b9d1a --- /dev/null +++ b/service/mksyscall.go @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package service + +//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go service_manager.go ipc_server.go ipc_event.go diff --git a/service/service_manager.go b/service/service_manager.go new file mode 100644 index 00000000..9f529bd1 --- /dev/null +++ b/service/service_manager.go @@ -0,0 +1,294 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package service + +import ( + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/eventlog" + "log" + "os" + "strconv" + "sync" + "syscall" + "unsafe" +) + +const ( + wtsSessionLogon uint32 = 5 + wtsSessionLogoff uint32 = 6 +) + +type wtsState int + +const ( + wtsActive wtsState = iota + wtsConnected + wtsConnectQuery + wtsShadow + wtsDisconnected + wtsIdle + wtsListen + wtsReset + wtsDown + wtsInit +) + +type wtsSessionNotification struct { + size uint32 + sessionID uint32 +} + +type wtsSessionInfo struct { + sessionID uint32 + windowStationName *uint16 + state wtsState +} + +type wellKnownSidType uint32 + +const ( + winBuiltinAdministratorsSid wellKnownSidType = 26 +) + +//sys wtfQueryUserToken(session uint32, token *windows.Token) (err error) = wtsapi32.WTSQueryUserToken +//sys wtsEnumerateSessions(handle windows.Handle, reserved uint32, version uint32, sessions **wtsSessionInfo, count *uint32) (err error) = wtsapi32.WTSEnumerateSessionsW +//sys wtsFreeMemory(ptr uintptr) = wtsapi32.WTSFreeMemory +//sys createWellKnownSid(sidType wellKnownSidType, domainSid *windows.SID, sid *windows.SID, sizeSid *uint32) (err error) = advapi32.CreateWellKnownSid + +//TODO: Upstream this to x/sys/windows +func localWellKnownSid(sidType wellKnownSidType) (*windows.SID, error) { + n := uint32(50) + for { + b := make([]byte, n) + sid := (*windows.SID)(unsafe.Pointer(&b[0])) + err := createWellKnownSid(sidType, nil, sid, &n) + if err == nil { + return sid, nil + } + if err != windows.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + if n <= uint32(len(b)) { + return nil, err + } + } +} + +type managerService struct{} + +type elogger struct { + *eventlog.Log +} + +func (elog elogger) Write(p []byte) (n int, err error) { + msg := string(p) + n = len(msg) + err = elog.Warning(1, msg) + return +} + +func (service *managerService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { + changes <- svc.Status{State: svc.StartPending} + + //TODO: remember to clean this up in the msi uninstaller + eventlog.InstallAsEventCreate("WireGuard", eventlog.Info|eventlog.Warning|eventlog.Error) + elog, err := eventlog.Open("WireGuard") + if err != nil { + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_LOG_CONTAINER_OPEN_FAILED + return + } + log.SetOutput(elogger{elog}) + + path, err := os.Executable() + if err != nil { + elog.Error(1, "Unable to determine own executable path: "+err.Error()) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_BAD_PATHNAME + return + } + + adminSid, err := localWellKnownSid(winBuiltinAdministratorsSid) + if err != nil { + elog.Error(1, "Unable to find Administrators SID: "+err.Error()) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_SERVER_SID_MISMATCH + return + } + + devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0) + if err != nil { + elog.Error(1, "Unable to open NUL file: "+err.Error()) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_FILE_NOT_FOUND + return + } + + procs := make(map[uint32]*os.Process) + procsLock := sync.Mutex{} + var startProcess func(session uint32) + + startProcess = func(session uint32) { + for { + var userToken windows.Token + err := wtfQueryUserToken(session, &userToken) + if err != nil { + return + } + + //TODO: SECURITY CRITICIAL! + //TODO: Isn't it better to use an impersonation token and userToken.IsMember instead? + gs, err := userToken.GetTokenGroups() + if err != nil { + elog.Error(1, "Unable to lookup user groups from token: "+err.Error()) + return + } + p := unsafe.Pointer(&gs.Groups[0]) + //TODO: x/sys/windows/svc/security.go uses 2 << 20, but shouldn't this be 1 << 20? Send upstream + groups := (*[1 << 20]windows.SIDAndAttributes)(p)[:gs.GroupCount] + isAdmin := false + for _, g := range groups { + if windows.EqualSid(g.Sid, adminSid) { + isAdmin = true + break + } + } + if !isAdmin { + return + } + + user, err := userToken.GetTokenUser() + if err != nil { + elog.Error(1, "Unable to lookup user from token: "+err.Error()) + return + } + username, domain, accType, err := user.User.Sid.LookupAccount("") + if err != nil { + elog.Error(1, "Unable to lookup username from sid: "+err.Error()) + return + } + if accType != windows.SidTypeUser { + return + } + + ourReader, theirReader, theirReaderStr, ourWriter, theirWriter, theirWriterStr, err := inheritableSocketpairEmulation() + if err != nil { + elog.Error(1, "Unable to create two inheritable pipes: "+err.Error()) + return + } + err = IPCServerListen(ourReader, ourWriter) + if err != nil { + elog.Error(1, "Unable to listen on IPC pipes: "+err.Error()) + return + } + + elog.Info(1, "Starting UI process for user: "+username+", domain: "+domain) + attr := &os.ProcAttr{ + Sys: &syscall.SysProcAttr{ + Token: syscall.Token(userToken), + }, + Files: []*os.File{devNull, devNull, devNull}, + } + proc, err := os.StartProcess(path, []string{path, "/ui", theirReaderStr, theirWriterStr}, attr) + theirReader.Close() + theirWriter.Close() + if err != nil { + elog.Error(1, "Unable to start manager UI process: "+err.Error()) + return + } + + procsLock.Lock() + procs[session] = proc + procsLock.Unlock() + proc.Wait() + procsLock.Lock() + delete(procs, session) + procsLock.Unlock() + ourReader.Close() + ourWriter.Close() + } + } + + var sessionsPointer *wtsSessionInfo + var count uint32 + err = wtsEnumerateSessions(0, 0, 1, &sessionsPointer, &count) + if err != nil { + elog.Error(1, "Unable to enumerate current sessions: "+err.Error()) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_BAD_LOGON_SESSION_STATE + return + } + sessions := *(*[]wtsSessionInfo)(unsafe.Pointer(&struct { + addr *wtsSessionInfo + len int + cap int + }{sessionsPointer, int(count), int(count)})) + for _, session := range sessions { + if session.state == wtsActive { + go startProcess(session.sessionID) + } + } + wtsFreeMemory(uintptr(unsafe.Pointer(sessionsPointer))) + + changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptSessionChange} + + uninstall := false +loop: + for { + select { + case <-quitManagersChan: + uninstall = true + break loop + case c := <-r: + switch c.Cmd { + case svc.Stop: + break loop + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.SessionChange: + //TODO: All the logic here depends on https://go-review.googlesource.com/c/sys/+/158698 being merged + if c.EventType != wtsSessionLogon && c.EventType != wtsSessionLogoff { + continue + } + sessionNotification := (*wtsSessionNotification)(unsafe.Pointer(c.EventData)) + if uintptr(sessionNotification.size) != unsafe.Sizeof(*sessionNotification) { + elog.Error(1, "Unexpected size of WTSSESSION_NOTIFICATION: "+strconv.Itoa(int(sessionNotification.size))) + continue + } + if c.EventType == wtsSessionLogoff { + procsLock.Lock() + if proc, ok := procs[sessionNotification.sessionID]; ok { + proc.Kill() + } + procsLock.Unlock() + } else if c.EventType == wtsSessionLogon { + procsLock.Lock() + if _, ok := procs[sessionNotification.sessionID]; !ok { + go startProcess(sessionNotification.sessionID) + } + procsLock.Unlock() + } + default: + elog.Info(1, "Unexpected service control request "+strconv.Itoa(int(c.Cmd))) + } + } + } + + changes <- svc.Status{State: svc.StopPending} + procsLock.Lock() + for _, proc := range procs { + proc.Kill() + } + procsLock.Unlock() + if uninstall { + err = UninstallManager() + if err != nil { + elog.Error(1, "Unable to uninstaller manager when quitting: "+err.Error()) + } + } + return +} diff --git a/service/service_tunnel.go b/service/service_tunnel.go new file mode 100644 index 00000000..b1f1df60 --- /dev/null +++ b/service/service_tunnel.go @@ -0,0 +1,174 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2017-2019 WireGuard LLC. All Rights Reserved. + */ + +package service + +import ( + "bufio" + "log" + "net" + "strings" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" + "golang.org/x/sys/windows/svc/eventlog" + + "golang.zx2c4.com/winipcfg" + "golang.zx2c4.com/wireguard/windows/conf" + "golang.zx2c4.com/wireguard/windows/service/tun" +) + +type confElogger struct { + elog debug.Log + conf *conf.Config + level int +} + +func (elog confElogger) Write(p []byte) (n int, err error) { + msg := elog.conf.Name + ": " + string(p) + n = len(msg) + switch elog.level { + case 1: + err = elog.elog.Info(1, msg) + case 2: + err = elog.elog.Warning(1, msg) + case 3: + err = elog.elog.Error(1, msg) + } + return +} + +type tunnelService struct { + path string + debug bool +} + +func (service *tunnelService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { + changes <- svc.Status{State: svc.StartPending} + + var elog debug.Log + var err error + if service.debug { + elog = debug.New("WireGuard") + } else { + //TODO: remember to clean this up in the msi uninstaller + eventlog.InstallAsEventCreate("WireGuard", eventlog.Info|eventlog.Warning|eventlog.Error) + elog, err = eventlog.Open("WireGuard") + if err != nil { + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_LOG_CONTAINER_OPEN_FAILED + return + } + } + + conf, err := conf.LoadFromPath(service.path) + if err != nil { + elog.Error(1, "Unable to load configuration file from path "+service.path+": "+err.Error()) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_OPEN_FAILED + return + } + + logger := &Logger{ + Debug: log.New(&confElogger{elog: elog, conf: conf, level: 1}, "", 0), + Info: log.New(&confElogger{elog: elog, conf: conf, level: 2}, "", 0), + Error: log.New(&confElogger{elog: elog, conf: conf, level: 3}, "", 0), + } + + logger.Info.Println("Starting wireguard-go version", WireGuardGoVersion) + logger.Debug.Println("Debug log enabled") + + tun, err := tun.CreateTUN(conf.Name) + if err == nil { + realInterfaceName, err2 := tun.Name() + if err2 == nil { + conf.Name = realInterfaceName + } + } else { + logger.Error.Println("Failed to create TUN device:", err) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_ADAP_HDW_ERR + return + } + + device := NewDevice(tun, logger) + device.Up() + logger.Info.Println("Device started") + + uapi, err := UAPIListen(conf.Name) + if err != nil { + logger.Error.Println("Failed to listen on uapi socket:", err) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_PIPE_LISTENING + device.Close() + return + } + errs := make(chan error) + + go func() { + for { + conn, err := uapi.Accept() + if err != nil { + errs <- err + return + } + go ipcHandle(device, conn) + } + }() + logger.Info.Println("UAPI listener started") + uapiConf, err := conf.ToUAPI() + if err != nil { + logger.Error.Println("Failed to convert to UAPI serialization:", err) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_INVALID_PARAMETER + device.Close() + return + } + ipcSetOperation(device, bufio.NewReader(strings.NewReader(uapiConf))) + + //TODO: configure addresses, routes, and DNS with winipcfg + iface, err := winipcfg.InterfaceFromFriendlyName(conf.Name) + if err == nil { + a := make([]*net.IPNet, len(conf.Interface.Addresses)) + for i, addr := range conf.Interface.Addresses { + a[i] = &net.IPNet{addr.IP, net.CIDRMask(int(addr.Cidr), len(addr.IP))} + } + err = iface.SetAddresses(a) + } + if err != nil { + logger.Error.Println("Unable to setup interface addresses:", err) + changes <- svc.Status{State: svc.StopPending} + exitCode = ERROR_NETWORK_BUSY + device.Close() + return + } + + changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop} + +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Stop: + break loop + case svc.Interrogate: + changes <- c.CurrentStatus + default: + logger.Error.Printf("Unexpected service control request #%d", c) + } + case <-errs: + break loop + case <-device.Wait(): + break loop + } + } + + changes <- svc.Status{State: svc.StopPending} + logger.Info.Println("Shutting down") + uapi.Close() + device.Close() + return +} diff --git a/service/zsyscall_windows.go b/service/zsyscall_windows.go new file mode 100644 index 00000000..79d4ccf5 --- /dev/null +++ b/service/zsyscall_windows.go @@ -0,0 +1,116 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package service + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return nil + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modwtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll") + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + moduser32 = windows.NewLazySystemDLL("user32.dll") + + procWTSQueryUserToken = modwtsapi32.NewProc("WTSQueryUserToken") + procWTSEnumerateSessionsW = modwtsapi32.NewProc("WTSEnumerateSessionsW") + procWTSFreeMemory = modwtsapi32.NewProc("WTSFreeMemory") + procCreateWellKnownSid = modadvapi32.NewProc("CreateWellKnownSid") + procPostMessageW = moduser32.NewProc("PostMessageW") + procRegisterWindowMessageW = moduser32.NewProc("RegisterWindowMessageW") +) + +func wtfQueryUserToken(session uint32, token *windows.Token) (err error) { + r1, _, e1 := syscall.Syscall(procWTSQueryUserToken.Addr(), 2, uintptr(session), uintptr(unsafe.Pointer(token)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func wtsEnumerateSessions(handle windows.Handle, reserved uint32, version uint32, sessions **wtsSessionInfo, count *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procWTSEnumerateSessionsW.Addr(), 5, uintptr(handle), uintptr(reserved), uintptr(version), uintptr(unsafe.Pointer(sessions)), uintptr(unsafe.Pointer(count)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func wtsFreeMemory(ptr uintptr) { + syscall.Syscall(procWTSFreeMemory.Addr(), 1, uintptr(ptr), 0, 0) + return +} + +func createWellKnownSid(sidType wellKnownSidType, domainSid *windows.SID, sid *windows.SID, sizeSid *uint32) (err error) { + r1, _, e1 := syscall.Syscall6(procCreateWellKnownSid.Addr(), 4, uintptr(sidType), uintptr(unsafe.Pointer(domainSid)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(sizeSid)), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func postMessage(hwnd windows.Handle, msg uint, wparam uintptr, lparam uintptr) (err error) { + r1, _, e1 := syscall.Syscall6(procPostMessageW.Addr(), 4, uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func registerWindowMessage(name *uint16) (message uint, err error) { + r0, _, e1 := syscall.Syscall(procRegisterWindowMessageW.Addr(), 1, uintptr(unsafe.Pointer(name)), 0, 0) + message = uint(r0) + if message == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} -- cgit v1.2.3-59-g8ed1b