arblant
firefox-vpn-client / core / internal / tunapi / pipe_windows.go
pipe_windows.go180 lines · 5.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
package tunapi

import (
	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"strings"

	"github.com/Microsoft/go-winio"
	"golang.org/x/sys/windows"
)

// PipeName is the service's named pipe.
const PipeName = `\\.\pipe\firefox-vpn-tun`

// pipeName is PipeName; tests use their own.
var pipeName = PipeName

// ServiceName is the Windows service; ExeName its executable, which the app
// also carries in its core folder.
const (
	ServiceName = "FirefoxVPNTun"
	ExeName     = "firefox-vpn-tun.exe"
)

// ErrNoService means no service answers: it is not installed, or it is
// not running.
var ErrNoService = errors.New("the TUN service is not running")

// ErrNotAllowed means the service runs but this user did not install it:
// installing it again, as this user, lets them in.
var ErrNotAllowed = errors.New("the TUN service was installed for another user")

// clientAccess is what a client asks of the pipe: read, and write data.
// Not GENERIC_WRITE, whose FILE_APPEND_DATA is, on a pipe, the right to
// create another server end of it; the pipe's DACL gives users only this.
const clientAccess = windows.GENERIC_READ | windows.FILE_WRITE_DATA

// userAccess is clientAccess as the pipe's DACL grants it to users:
// FILE_GENERIC_READ and FILE_WRITE_DATA.
const userAccess = windows.READ_CONTROL | windows.SYNCHRONIZE | windows.FILE_READ_DATA |
	windows.FILE_READ_ATTRIBUTES | windows.FILE_READ_EA | windows.FILE_WRITE_DATA

// Dial connects to the service. It refuses a pipe that is not owned by
// the system or the administrators, which another program could have
// created under the name while the service was not running.
func Dial(ctx context.Context) (net.Conn, error) {
	c, err := winio.DialPipeAccess(ctx, pipeName, clientAccess)
	if err != nil {
		switch {
		case errors.Is(err, windows.ERROR_FILE_NOT_FOUND) || errors.Is(err, os.ErrNotExist):
			return nil, ErrNoService
		case errors.Is(err, windows.ERROR_ACCESS_DENIED):
			return nil, ErrNotAllowed
		}
		return nil, err
	}
	if err := checkOwner(c); err != nil {
		c.Close()
		return nil, err
	}
	return c, nil
}

func checkOwner(c net.Conn) error {
	h, err := handleOf(c)
	if err != nil {
		return err
	}
	sd, err := windows.GetSecurityInfo(h, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION)
	if err != nil {
		return fmt.Errorf("reading the TUN service pipe's owner: %w", err)
	}
	owner, _, err := sd.Owner()
	if err != nil {
		return fmt.Errorf("reading the TUN service pipe's owner: %w", err)
	}
	if !owner.IsWellKnown(windows.WinLocalSystemSid) && !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
		return fmt.Errorf("the TUN service pipe belongs to %s, not to the system", owner)
	}
	return nil
}

func handleOf(c net.Conn) (windows.Handle, error) {
	f, ok := c.(interface{ Fd() uintptr })
	if !ok {
		return 0, errors.New("not a pipe connection")
	}
	return windows.Handle(f.Fd()), nil
}

// Listen creates the service's pipe, which the system, the administrators,
// and the users with the given SIDs can open. The pipe is owned by the
// system, and creating it fails when another program holds the name.
func Listen(users []string) (net.Listener, error) {
	sddl, err := securityDescriptor("SY", nil, users)
	if err != nil {
		return nil, err
	}
	return listenSDDL(sddl)
}

// securityDescriptor is the pipe's, in SDDL: owned by owner; full access
// for the system, the administrators, and servers, which must be allowed
// to create the pipe's server ends; userAccess for users. Only the system
// may make itself the owner, so tests serve the pipe as their own user.
func securityDescriptor(owner string, servers, users []string) (string, error) {
	var sddl strings.Builder
	fmt.Fprintf(&sddl, "O:%sG:%sD:P(A;;GA;;;SY)(A;;GA;;;BA)", owner, owner)
	for _, sid := range servers {
		fmt.Fprintf(&sddl, "(A;;GA;;;%s)", sid)
	}
	for _, sid := range users {
		if _, err := windows.StringToSid(sid); err != nil {
			return "", fmt.Errorf("bad user SID %q: %w", sid, err)
		}
		fmt.Fprintf(&sddl, "(A;;0x%x;;;%s)", userAccess, sid)
	}
	return sddl.String(), nil
}

func listenSDDL(sddl string) (net.Listener, error) {
	return winio.ListenPipe(pipeName, &winio.PipeConfig{
		SecurityDescriptor: sddl,
		InputBufferSize:    64 << 10,
		OutputBufferSize:   64 << 10,
	})
}

// Installed reports whether the service is installed; it asks the service
// manager only what any user may ask.
func Installed() (bool, error) {
	m, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT)
	if err != nil {
		return false, err
	}
	defer windows.CloseServiceHandle(m)
	name, _ := windows.UTF16PtrFromString(ServiceName)
	s, err := windows.OpenService(m, name, windows.SERVICE_QUERY_STATUS)
	if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	windows.CloseServiceHandle(s)
	return true, nil
}

// ClientProcess is the process at the other end of a connection the
// service accepted.
func ClientProcess(c net.Conn) (pid uint32, err error) {
	h, err := handleOf(c)
	if err != nil {
		return 0, err
	}
	err = windows.GetNamedPipeClientProcessId(h, &pid)
	return pid, err
}

// ProcessPath is the executable of process pid.
func ProcessPath(pid uint32) (string, error) {
	h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
	if err != nil {
		return "", err
	}
	defer windows.CloseHandle(h)
	buf := make([]uint16, windows.MAX_LONG_PATH)
	size := uint32(len(buf))
	if err := windows.QueryFullProcessImageName(h, 0, &buf[0], &size); err != nil {
		return "", err
	}
	return windows.UTF16ToString(buf[:size]), nil
}

// Allowed reports whether the client of a connection the service accepted
// may use it: the pipe's access list let in only those who may.
func Allowed(net.Conn) bool { return true }