479 lines
16 KiB
Go
479 lines
16 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
// The Windows half of approvalpipe.go: the pipe itself, who is on the other
|
|
// end of it, and the tray's way in.
|
|
//
|
|
// OVERLAPPED I/O, ALWAYS. Every connect, read and write waits on an event with
|
|
// a timeout and is cancelled when the time is up (CancelIoEx), so a client that
|
|
// connects and says nothing, or a service that hangs, holds nothing for longer
|
|
// than pipeIOTimeout. The OVERLAPPED structure lives in a heap object
|
|
// (pipeConn) for as long as the I/O on it can be pending: the kernel writes to
|
|
// it after the call returns, and a goroutine stack can move.
|
|
//
|
|
// NO GAP IN THE NAME. A pipe name exists only while one of its instances is
|
|
// open. Between the last instance closing and the next being created, any
|
|
// process could create the name and receive the tray's next code. So the
|
|
// next instance is always created BEFORE the one it replaces is handed to its
|
|
// handler or, after a failed connect, closed (pipeAcceptLoop, approvalpipe.go,
|
|
// where the suite checks it), and any instance created while none of ours is open carries
|
|
// FILE_FLAG_FIRST_PIPE_INSTANCE, which fails if someone else holds the name.
|
|
// The tray also checks that the pipe's server process is the PrivataeBastion
|
|
// service before it sends anything (pipeServerIsService).
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// pipeConn is one end of one pipe instance with its own OVERLAPPED, event and
|
|
// read buffer, all in this heap object: the kernel fills the buffer and the
|
|
// OVERLAPPED after the call that started the read has returned.
|
|
type pipeConn struct {
|
|
h windows.Handle
|
|
ev windows.Handle
|
|
ov windows.Overlapped
|
|
buf [4096]byte
|
|
}
|
|
|
|
func newPipeConn(h windows.Handle) (*pipeConn, error) {
|
|
ev, err := windows.CreateEvent(nil, 1, 0, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := &pipeConn{h: h, ev: ev}
|
|
c.ov.HEvent = ev
|
|
return c, nil
|
|
}
|
|
|
|
func (c *pipeConn) close() {
|
|
_ = windows.CloseHandle(c.h)
|
|
_ = windows.CloseHandle(c.ev)
|
|
}
|
|
|
|
var errPipeTimeout = errors.New("timed out")
|
|
|
|
// wait completes an overlapped call that was started on c. stop, when not 0,
|
|
// is a second event that cancels the wait.
|
|
func (c *pipeConn) wait(callErr error, timeout time.Duration, stop windows.Handle) (uint32, error) {
|
|
var n uint32
|
|
switch {
|
|
case callErr == nil:
|
|
case errors.Is(callErr, windows.ERROR_IO_PENDING):
|
|
ms := uint32(windows.INFINITE)
|
|
if timeout > 0 {
|
|
ms = uint32(timeout / time.Millisecond)
|
|
}
|
|
handles := []windows.Handle{c.ev}
|
|
if stop != 0 {
|
|
handles = append(handles, stop)
|
|
}
|
|
r, err := windows.WaitForMultipleObjects(handles, false, ms)
|
|
if err != nil || r != windows.WAIT_OBJECT_0 {
|
|
_ = windows.CancelIoEx(c.h, &c.ov)
|
|
_ = windows.GetOverlappedResult(c.h, &c.ov, &n, true)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if r == uint32(windows.WAIT_TIMEOUT) {
|
|
return 0, errPipeTimeout
|
|
}
|
|
return 0, errPipeStopped
|
|
}
|
|
default:
|
|
return 0, callErr
|
|
}
|
|
if err := windows.GetOverlappedResult(c.h, &c.ov, &n, false); err != nil {
|
|
return n, err
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// read reads into c.buf and returns how much arrived.
|
|
func (c *pipeConn) read(timeout time.Duration) (int, error) {
|
|
_ = windows.ResetEvent(c.ev)
|
|
var done uint32
|
|
err := windows.ReadFile(c.h, c.buf[:], &done, &c.ov)
|
|
n, err := c.wait(err, timeout, 0)
|
|
return int(n), err
|
|
}
|
|
|
|
func (c *pipeConn) write(b []byte, timeout time.Duration) error {
|
|
for len(b) > 0 {
|
|
_ = windows.ResetEvent(c.ev)
|
|
var done uint32
|
|
err := windows.WriteFile(c.h, b, &done, &c.ov)
|
|
n, err := c.wait(err, timeout, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return errors.New("short write")
|
|
}
|
|
b = b[n:]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readLine reads up to one newline, within max bytes and timeout in all.
|
|
func (c *pipeConn) readLine(max int, timeout time.Duration) ([]byte, error) {
|
|
deadline := time.Now().Add(timeout)
|
|
var out []byte
|
|
for {
|
|
left := time.Until(deadline)
|
|
if left <= 0 {
|
|
return nil, errPipeTimeout
|
|
}
|
|
n, err := c.read(left)
|
|
out = append(out, c.buf[:n]...)
|
|
if i := bytes.IndexByte(out, '\n'); i >= 0 {
|
|
return out[:i], nil
|
|
}
|
|
if len(out) > max {
|
|
return nil, errors.New("message too large")
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, windows.ERROR_MORE_DATA) {
|
|
continue
|
|
}
|
|
if errors.Is(err, windows.ERROR_BROKEN_PIPE) && len(out) > 0 {
|
|
return out, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── the server, in the service ──────────────────────────────────────────────
|
|
|
|
// approvalPipeServer tracks how many of our instances are open, so an instance
|
|
// created while none is open asks for the name to be ours alone.
|
|
type approvalPipeServer struct {
|
|
mu sync.Mutex
|
|
open int
|
|
sa *windows.SecurityAttributes
|
|
name *uint16
|
|
label string // the pipe's name, for a message
|
|
// stopEv is set when the service stops; it ends a connect that is waiting.
|
|
stopEv windows.Handle
|
|
}
|
|
|
|
// connect waits for a client on c (pipeInstances).
|
|
func (s *approvalPipeServer) connect(c *pipeConn) error {
|
|
_ = windows.ResetEvent(c.ev)
|
|
cerr := windows.ConnectNamedPipe(c.h, &c.ov)
|
|
if errors.Is(cerr, windows.ERROR_PIPE_CONNECTED) {
|
|
// A client that connected between the create and this call is
|
|
// already connected; anything else waits.
|
|
return nil
|
|
}
|
|
_, err := c.wait(cerr, 0, s.stopEv)
|
|
return err
|
|
}
|
|
|
|
func (s *approvalPipeServer) create() (*pipeConn, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
flags := uint32(windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED)
|
|
if s.open == 0 {
|
|
flags |= windows.FILE_FLAG_FIRST_PIPE_INSTANCE
|
|
}
|
|
h, err := windows.CreateNamedPipe(s.name, flags,
|
|
windows.PIPE_TYPE_BYTE|windows.PIPE_READMODE_BYTE|windows.PIPE_WAIT|windows.PIPE_REJECT_REMOTE_CLIENTS,
|
|
windows.PIPE_UNLIMITED_INSTANCES, 64<<10, 64<<10, 0, s.sa)
|
|
if err != nil {
|
|
if s.open == 0 {
|
|
return nil, fmt.Errorf("cannot create %s; if another program holds that name, this lane stays off: %w", s.label, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
c, err := newPipeConn(h)
|
|
if err != nil {
|
|
_ = windows.CloseHandle(h)
|
|
return nil, err
|
|
}
|
|
s.open++
|
|
return c, nil
|
|
}
|
|
|
|
func (s *approvalPipeServer) release(c *pipeConn) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
c.close()
|
|
s.open--
|
|
}
|
|
|
|
// startApprovalPipe serves the pipe until stop is closed. A failure, including
|
|
// the name being held by another process when the service starts, is said in
|
|
// the service's log, leaves every other lane as it was, and is retried with
|
|
// backoff (pipeServeRetrying). Each attempt's first instance carries
|
|
// FILE_FLAG_FIRST_PIPE_INSTANCE, so the service never serves under a name some
|
|
// other process created.
|
|
func startApprovalPipe(stop <-chan struct{}) {
|
|
wait := func(d time.Duration) bool {
|
|
select {
|
|
case <-stop:
|
|
return false
|
|
case <-time.After(d):
|
|
return true
|
|
}
|
|
}
|
|
go pipeServeRetrying(func(ready func()) error { return serveApprovalPipe(stop, ready) }, wait,
|
|
func(f string, a ...any) { fmt.Printf(f, a...) })
|
|
}
|
|
|
|
func serveApprovalPipe(stop <-chan struct{}, ready func()) error {
|
|
return servePipe(stop, ready, approvalPipeName, approvalPipeSDDL, servePipeConn)
|
|
}
|
|
|
|
// servePipe serves one of the service's pipes, name with the ACL sddl, until
|
|
// stop is closed, handing each connection to serve.
|
|
func servePipe(stop <-chan struct{}, ready func(), pipeName, sddl string, serve func(*pipeConn)) error {
|
|
sd, err := windows.SecurityDescriptorFromString(sddl)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot build the pipe's permissions: %w", err)
|
|
}
|
|
name, err := windows.UTF16PtrFromString(pipeName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stopEv, err := windows.CreateEvent(nil, 1, 0, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go func() {
|
|
<-stop
|
|
_ = windows.SetEvent(stopEv)
|
|
}()
|
|
s := &approvalPipeServer{
|
|
sa: &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), SecurityDescriptor: sd},
|
|
name: name,
|
|
label: pipeName,
|
|
stopEv: stopEv,
|
|
}
|
|
// The loop and its no-gap rule are in approvalpipe.go, where the suite runs them.
|
|
return pipeAcceptLoop[*pipeConn](s, stop, pipeMaxClients, time.Second,
|
|
func(f string, a ...any) { fmt.Printf(f, a...) }, ready, serve)
|
|
}
|
|
|
|
// servePipeConn answers one request and waits, briefly, for the client to
|
|
// close: closing first would discard an answer it has not read yet.
|
|
func servePipeConn(c *pipeConn) {
|
|
client, cerr := pipeClientOf(c.h)
|
|
raw, err := c.readLine(pipeMaxRequest, pipeIOTimeout)
|
|
var resp pipeResponse
|
|
switch {
|
|
case err != nil:
|
|
return
|
|
case cerr != nil:
|
|
resp = pipeResponse{Error: "refused: " + cerr.Error()}
|
|
default:
|
|
req, derr := pipeDecode(raw)
|
|
if derr != nil {
|
|
resp = pipeResponse{Error: derr.Error()}
|
|
} else {
|
|
resp = pipeServe(req, client, time.Now())
|
|
}
|
|
}
|
|
// A refused client is said in the service's log: it is a process on this
|
|
// machine trying a lane that is not its own.
|
|
if cerr != nil || pipeAccessRefusal(client) != "" {
|
|
fmt.Printf("approvals pipe: %s (pid %d)\n", resp.Error, client.PID)
|
|
}
|
|
out, _ := json.Marshal(resp)
|
|
if err := c.write(append(out, '\n'), pipeIOTimeout); err != nil {
|
|
return
|
|
}
|
|
_, _ = c.read(pipeIOTimeout)
|
|
}
|
|
|
|
// pipeClientOf identifies the process on the other end: its pid from the pipe
|
|
// (GetNamedPipeClientProcessId), its user and session from that process's
|
|
// token, and the person signed in to that session from the session's own token
|
|
// (WTSQueryUserToken, which LocalSystem may call).
|
|
//
|
|
// The process is found by its id, after it connected. An id that was freed
|
|
// and given to a new process in between would name the wrong one, so a
|
|
// process created after the connection is refused (pipeClientNewer).
|
|
func pipeClientOf(h windows.Handle) (pipeClient, error) {
|
|
connected := time.Now()
|
|
var c pipeClient
|
|
if err := windows.GetNamedPipeClientProcessId(h, &c.PID); err != nil {
|
|
return c, fmt.Errorf("cannot tell which process connected: %w", err)
|
|
}
|
|
p, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, c.PID)
|
|
if err != nil {
|
|
return c, fmt.Errorf("cannot open the connecting process (pid %d): %w", c.PID, err)
|
|
}
|
|
defer windows.CloseHandle(p)
|
|
var created, exited, kernel, user windows.Filetime
|
|
if err := windows.GetProcessTimes(p, &created, &exited, &kernel, &user); err != nil {
|
|
return c, fmt.Errorf("cannot read when the connecting process started (pid %d): %w", c.PID, err)
|
|
}
|
|
if pipeClientNewer(time.Unix(0, created.Nanoseconds()), connected) {
|
|
return c, fmt.Errorf("process %d started after the connection was made, so it is not the process that connected", c.PID)
|
|
}
|
|
var tok windows.Token
|
|
if err := windows.OpenProcessToken(p, windows.TOKEN_QUERY, &tok); err != nil {
|
|
return c, fmt.Errorf("cannot read the connecting process's account (pid %d): %w", c.PID, err)
|
|
}
|
|
defer tok.Close()
|
|
u, err := tok.GetTokenUser()
|
|
if err != nil {
|
|
return c, fmt.Errorf("cannot read the connecting process's account (pid %d): %w", c.PID, err)
|
|
}
|
|
c.UserSID = u.User.Sid.String()
|
|
// Bounded and cached (sidnames.go): a directory that does not answer
|
|
// leaves NameTimedOut set, and pipeApprove refuses.
|
|
pipeClientName(&c, winAccounts, pipeSIDLane)
|
|
var sess, n uint32
|
|
if err := windows.GetTokenInformation(tok, windows.TokenSessionId, (*byte)(unsafe.Pointer(&sess)), uint32(unsafe.Sizeof(sess)), &n); err != nil {
|
|
return c, fmt.Errorf("cannot read the connecting process's session (pid %d): %w", c.PID, err)
|
|
}
|
|
c.SessionID = sess
|
|
var st windows.Token
|
|
if windows.WTSQueryUserToken(sess, &st) == nil {
|
|
if su, err := st.GetTokenUser(); err == nil {
|
|
c.SessionUserSID = su.User.Sid.String()
|
|
}
|
|
st.Close()
|
|
}
|
|
c.SessionActive = wtsSessionActive(sess)
|
|
return c, nil
|
|
}
|
|
|
|
// wtsSessionActive reports whether a session is the one in front of a screen
|
|
// (WTSActive), rather than disconnected or idle.
|
|
func wtsSessionActive(id uint32) bool {
|
|
var sessions *windows.WTS_SESSION_INFO
|
|
var n uint32
|
|
if windows.WTSEnumerateSessions(0, 0, 1, &sessions, &n) != nil {
|
|
return false
|
|
}
|
|
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessions)))
|
|
for _, s := range unsafe.Slice(sessions, n) {
|
|
if s.SessionID == id {
|
|
return s.State == windows.WTSActive
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ── the client, in the tray ─────────────────────────────────────────────────
|
|
|
|
// pipeCall sends one request to the service and returns its answer.
|
|
func pipeCall(req pipeRequest) (pipeResponse, error) {
|
|
var resp pipeResponse
|
|
c, err := dialApprovalPipe()
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
defer c.close()
|
|
if err := pipeServerIsService(c.h); err != nil {
|
|
return resp, err
|
|
}
|
|
raw, err := json.Marshal(req)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
if err := c.write(append(raw, '\n'), pipeIOTimeout); err != nil {
|
|
return resp, fmt.Errorf("the bastion service did not take the request: %w", err)
|
|
}
|
|
line, err := c.readLine(1<<20, 2*pipeIOTimeout)
|
|
if err != nil {
|
|
return resp, fmt.Errorf("the bastion service did not answer: %w", err)
|
|
}
|
|
if err := json.Unmarshal(line, &resp); err != nil {
|
|
return resp, errors.New("the bastion service's answer could not be read")
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// dialApprovalPipe opens the pipe with exactly the rights its ACL grants
|
|
// (read, write data, synchronize; never FILE_APPEND_DATA, which GENERIC_WRITE
|
|
// would ask for), and lets the server identify this process but not act as it.
|
|
func dialApprovalPipe() (*pipeConn, error) {
|
|
return dialServicePipe(approvalPipeName)
|
|
}
|
|
|
|
// dialServicePipe opens one of the service's pipes (dialApprovalPipe).
|
|
func dialServicePipe(pipeName string) (*pipeConn, error) {
|
|
name, err := windows.UTF16PtrFromString(pipeName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := 0; ; i++ {
|
|
h, err := windows.CreateFile(name, windows.FILE_GENERIC_READ|windows.FILE_WRITE_DATA, 0, nil,
|
|
windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED|windows.SECURITY_SQOS_PRESENT|windows.SECURITY_IDENTIFICATION, 0)
|
|
if err == nil {
|
|
c, err := newPipeConn(h)
|
|
if err != nil {
|
|
_ = windows.CloseHandle(h)
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
if errors.Is(err, windows.ERROR_PIPE_BUSY) && i < 10 {
|
|
time.Sleep(200 * time.Millisecond)
|
|
continue
|
|
}
|
|
if errors.Is(err, windows.ERROR_FILE_NOT_FOUND) {
|
|
return nil, errors.New("the bastion service is not running, or is older than this program")
|
|
}
|
|
return nil, fmt.Errorf("cannot reach the bastion service: %w", err)
|
|
}
|
|
}
|
|
|
|
// pipeServerIsService refuses a pipe whose server is not the PrivataeBastion
|
|
// service's own process, so a program that got the name first is never sent
|
|
// a code.
|
|
func pipeServerIsService(h windows.Handle) error {
|
|
var pid uint32
|
|
if err := windows.GetNamedPipeServerProcessId(h, &pid); err != nil {
|
|
return fmt.Errorf("cannot tell which process serves the pipe: %w", err)
|
|
}
|
|
svcPID, err := bastionServicePID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if svcPID == 0 || pid != svcPID {
|
|
return fmt.Errorf("the pipe is served by process %d, not by the %s service; nothing was sent", pid, winServiceName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bastionServicePID is the service's process id from the Service Control
|
|
// Manager, asked with the query right every signed-in user holds. 0 when it is
|
|
// not running.
|
|
func bastionServicePID() (uint32, error) {
|
|
scm, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cannot ask the Service Control Manager: %w", err)
|
|
}
|
|
defer windows.CloseServiceHandle(scm)
|
|
name, err := windows.UTF16PtrFromString(winServiceName)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
h, err := windows.OpenService(scm, name, windows.SERVICE_QUERY_STATUS)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cannot query the %s service: %w", winServiceName, err)
|
|
}
|
|
defer windows.CloseServiceHandle(h)
|
|
var st windows.SERVICE_STATUS_PROCESS
|
|
var need uint32
|
|
if err := windows.QueryServiceStatusEx(h, windows.SC_STATUS_PROCESS_INFO,
|
|
(*byte)(unsafe.Pointer(&st)), uint32(unsafe.Sizeof(st)), &need); err != nil {
|
|
return 0, err
|
|
}
|
|
return st.ProcessId, nil
|
|
}
|