138 lines
4.2 KiB
Go
138 lines
4.2 KiB
Go
//go:build windows
|
|
|
|
package control
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// pipePath is a kernel-local named pipe: no TCP, no firewall prompt.
|
|
const pipePath = `\\.\pipe\gpu-turnstile`
|
|
|
|
// sddlPipe grants full access to Administrators, SYSTEM and the pipe owner,
|
|
// and read+write to authenticated users — except network logons, so the
|
|
// pipe cannot be reached from another machine over SMB.
|
|
const sddlPipe = "D:(D;;GRGW;;;NU)(A;;GA;;;BA)(A;;GA;;;SY)(A;;GA;;;OW)(A;;GRGW;;;AU)"
|
|
|
|
var (
|
|
procConvertSDDL = windows.NewLazySystemDLL("advapi32.dll").
|
|
NewProc("ConvertStringSecurityDescriptorToSecurityDescriptorW")
|
|
procWaitNamedPipe = windows.NewLazySystemDLL("kernel32.dll").
|
|
NewProc("WaitNamedPipeW")
|
|
)
|
|
|
|
func waitNamedPipe(name *uint16, timeout uint32) error {
|
|
r, _, err := procWaitNamedPipe.Call(uintptr(unsafe.Pointer(name)), uintptr(timeout))
|
|
if r == 0 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func securityAttributesFromSDDL(sddl string) (*windows.SecurityAttributes, error) {
|
|
s, err := windows.UTF16PtrFromString(sddl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var sd *uint16 // SECURITY_DESCRIPTOR*, kept for the process lifetime
|
|
r, _, callErr := procConvertSDDL.Call(
|
|
uintptr(unsafe.Pointer(s)), 1, /* SDDL_REVISION_1 */
|
|
uintptr(unsafe.Pointer(&sd)), 0)
|
|
if r == 0 {
|
|
return nil, fmt.Errorf("invalid SDDL: %w", callErr)
|
|
}
|
|
sa := &windows.SecurityAttributes{
|
|
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
|
|
SecurityDescriptor: (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(sd)),
|
|
}
|
|
return sa, nil
|
|
}
|
|
|
|
// Serve starts the pipe listener in the background and returns; only a
|
|
// setup failure is reported. Each client connection is answered in its own
|
|
// goroutine. On shutdown the process exit reaps everything.
|
|
func Serve(ctx context.Context, h Handler, log *slog.Logger) error {
|
|
sa, err := securityAttributesFromSDDL(sddlPipe)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name, err := windows.UTF16PtrFromString(pipePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go func() {
|
|
for ctx.Err() == nil {
|
|
pipe, err := windows.CreateNamedPipe(name,
|
|
windows.PIPE_ACCESS_DUPLEX,
|
|
windows.PIPE_TYPE_BYTE|windows.PIPE_READMODE_BYTE|windows.PIPE_WAIT,
|
|
16, 4096, 4096, 0, sa)
|
|
if err != nil {
|
|
log.Warn("control channel stopped", "err", err)
|
|
return
|
|
}
|
|
go func() {
|
|
// Blocks until a client connects; on process exit the
|
|
// handle goes away with everything else. A client that
|
|
// raced us and connected between CreateNamedPipe and
|
|
// ConnectNamedPipe reports ERROR_PIPE_CONNECTED — that is
|
|
// a success, not a failure.
|
|
if err := windows.ConnectNamedPipe(pipe, nil); err != nil && err != errnoPipeConnected {
|
|
windows.CloseHandle(pipe)
|
|
return
|
|
}
|
|
serveConn(&pipeConn{f: os.NewFile(uintptr(pipe), pipePath), h: pipe}, h)
|
|
}()
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// errnoPipeConnected is ConnectNamedPipe's "the client connected before we
|
|
// called" result, which means the connection is established.
|
|
var errnoPipeConnected = syscall.Errno(535) // ERROR_PIPE_CONNECTED
|
|
|
|
// pipeConn adapts a pipe handle to io.ReadWriteCloser. Close flushes first
|
|
// (FlushFileBuffers blocks until the client has read the reply) and then
|
|
// disconnects — closing the bare handle right after writing can discard
|
|
// unread reply bytes, which clients see as an empty, failed request.
|
|
type pipeConn struct {
|
|
f *os.File
|
|
h windows.Handle
|
|
}
|
|
|
|
func (c *pipeConn) Read(p []byte) (int, error) { return c.f.Read(p) }
|
|
func (c *pipeConn) Write(p []byte) (int, error) { return c.f.Write(p) }
|
|
|
|
func (c *pipeConn) Close() error {
|
|
windows.FlushFileBuffers(c.h) //nolint:errcheck // best effort
|
|
windows.DisconnectNamedPipe(c.h) //nolint:errcheck // best effort
|
|
return c.f.Close()
|
|
}
|
|
|
|
// Ask sends one command to the running service and returns its reply.
|
|
func Ask(cmd string) (string, error) {
|
|
name, err := windows.UTF16PtrFromString(pipePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := waitNamedPipe(name, 2000); err != nil {
|
|
return "", ErrUnavailable
|
|
}
|
|
handle, err := windows.CreateFile(name,
|
|
windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil,
|
|
windows.OPEN_EXISTING, 0, 0)
|
|
if err != nil {
|
|
return "", ErrUnavailable
|
|
}
|
|
f := os.NewFile(uintptr(handle), pipePath)
|
|
defer f.Close()
|
|
return readReply(f, cmd)
|
|
}
|