Files
sing-box/experimental/boxdd/cmd_worker_windows.go
2026-07-13 15:08:24 +08:00

501 lines
14 KiB
Go

package main
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"os"
"strings"
"sync"
"sync/atomic"
E "github.com/sagernet/sing/common/exceptions"
"github.com/tailscale/go-winio"
winioProcess "github.com/tailscale/go-winio/pkg/process"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/mgr"
)
type windowsWorkerParent struct {
process windows.Handle
processImage windows.Handle
executable windows.Handle
executablePath string
signer []byte
userID string
sessionID uint32
pid uint32
exited chan struct{}
close sync.Once
closeError error
}
type authenticatedWorkerListener struct {
net.Listener
parent *windowsWorkerParent
}
type windowsWorkerDaemonRelay struct {
listener net.Listener
parent *windowsWorkerParent
onFailure func(error)
connections map[net.Conn]struct{}
connectionAccess sync.Mutex
connectionWaitGroup sync.WaitGroup
closing atomic.Bool
close sync.Once
closeError error
}
type windowsAuthenticatedDaemonConnection struct {
net.Conn
process windows.Handle
processImage windows.Handle
close sync.Once
closeError error
}
func prepareWorkerParent(parentProcessID uint32) (workerParent, error) {
if os.Getppid() != int(parentProcessID) {
return nil, E.New("worker was not started by the expected application process")
}
parentProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.SYNCHRONIZE, false, parentProcessID)
if err != nil {
return nil, err
}
keepProcess := false
defer func() {
if !keepProcess {
windows.CloseHandle(parentProcess)
}
}()
identity, err := processIdentity(parentProcess, parentProcessID)
if err != nil {
return nil, err
}
parentImagePath, err := winioProcess.QueryFullProcessImageName(parentProcess, winioProcess.ImageNameFormatWin32Path)
if err != nil {
return nil, E.Cause(err, "query worker parent executable")
}
parentImage, err := openLockedExecutable(parentImagePath)
if err != nil {
return nil, err
}
keepParentImage := false
defer func() {
if !keepParentImage {
windows.CloseHandle(parentImage)
}
}()
workerExecutablePath, err := os.Executable()
if err != nil {
return nil, err
}
workerExecutable, err := openLockedExecutable(workerExecutablePath)
if err != nil {
return nil, err
}
keepWorkerExecutable := false
defer func() {
if !keepWorkerExecutable {
windows.CloseHandle(workerExecutable)
}
}()
workerFinalPath, err := finalWindowsPath(workerExecutable)
if err != nil {
return nil, err
}
_, expectedApplicationPath, err := installedApplicationPath(workerFinalPath)
if err != nil {
return nil, err
}
expectedApplication, err := openLockedExecutable(expectedApplicationPath)
if err != nil {
return nil, err
}
defer windows.CloseHandle(expectedApplication)
parentFinalPath, err := finalWindowsPath(parentImage)
if err != nil {
return nil, err
}
expectedApplicationFinalPath, err := finalWindowsPath(expectedApplication)
if err != nil {
return nil, err
}
if !strings.EqualFold(parentFinalPath, expectedApplicationFinalPath) {
return nil, E.New("worker parent is not the installed sing-box application")
}
sameApplication, err := sameWindowsFile(parentImage, expectedApplication)
if err != nil {
return nil, err
}
if !sameApplication {
return nil, E.New("worker parent executable was replaced")
}
err = validateApplicationProcessRole(parentProcess, expectedApplication)
if err != nil {
return nil, err
}
workerSigner, err := authenticodeSigner(workerFinalPath, workerExecutable)
if err != nil {
return nil, err
}
parentSigner, err := authenticodeSigner(parentFinalPath, parentImage)
if err != nil {
return nil, err
}
if !bytes.Equal(workerSigner, parentSigner) {
return nil, E.New("worker and application have different signing certificates")
}
parentCreationTime, err := processCreationTime(parentProcess)
if err != nil {
return nil, err
}
workerCreationTime, err := processCreationTime(windows.CurrentProcess())
if err != nil {
return nil, err
}
if parentCreationTime >= workerCreationTime {
return nil, E.New("worker parent was created after the worker process")
}
waitResult, err := windows.WaitForSingleObject(parentProcess, 0)
if err != nil {
return nil, err
}
if waitResult != uint32(windows.WAIT_TIMEOUT) {
return nil, E.New("worker application parent exited during authentication")
}
parent := &windowsWorkerParent{
process: parentProcess,
processImage: parentImage,
executable: workerExecutable,
executablePath: workerFinalPath,
signer: workerSigner,
userID: identity.UserID,
sessionID: identity.SessionID,
pid: parentProcessID,
exited: make(chan struct{}),
}
go func() {
_, _ = windows.WaitForSingleObject(parent.process, windows.INFINITE)
close(parent.exited)
}()
keepProcess = true
keepParentImage = true
keepWorkerExecutable = true
return parent, nil
}
func listenWorkerEndpoint(path string, parent workerParent) (net.Listener, error) {
windowsParent := parent.(*windowsWorkerParent)
securityDescriptor := fmt.Sprintf(
"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;%s)",
windowsParent.userID,
)
listener, err := winio.ListenPipe(path, &winio.PipeConfig{
SecurityDescriptor: securityDescriptor,
InputBufferSize: pipeBufferSize,
OutputBufferSize: pipeBufferSize,
})
if err != nil {
return nil, err
}
authenticatedListener := &authenticatedWorkerListener{Listener: listener, parent: windowsParent}
go func() {
<-windowsParent.exited
authenticatedListener.Close()
}()
return authenticatedListener, nil
}
func (l *authenticatedWorkerListener) Accept() (net.Conn, error) {
for {
waitResult, err := windows.WaitForSingleObject(l.parent.process, 0)
if err != nil {
return nil, err
}
if waitResult != uint32(windows.WAIT_TIMEOUT) {
return nil, E.New("worker application parent exited")
}
connection, err := l.Listener.Accept()
if err != nil {
return nil, err
}
descriptorConnection, loaded := connection.(fileDescriptorConnection)
if !loaded {
connection.Close()
continue
}
var clientProcessID uint32
err = windows.GetNamedPipeClientProcessId(windows.Handle(descriptorConnection.Fd()), &clientProcessID)
if err != nil || clientProcessID != l.parent.pid {
connection.Close()
continue
}
waitResult, err = windows.WaitForSingleObject(l.parent.process, 0)
if err != nil || waitResult != uint32(windows.WAIT_TIMEOUT) {
connection.Close()
return nil, E.New("worker application parent exited")
}
return connection, nil
}
}
func (p *windowsWorkerParent) Close() error {
p.close.Do(func() {
p.closeError = E.Errors(
windows.CloseHandle(p.executable),
windows.CloseHandle(p.processImage),
windows.CloseHandle(p.process),
)
})
return p.closeError
}
func startWorkerDaemonRelay(path string, parent workerParent, onFailure func(error)) (io.Closer, error) {
if path == "" {
return nil, E.New("missing --daemon-relay-socket")
}
windowsParent := parent.(*windowsWorkerParent)
listener, err := listenWorkerEndpoint(path, parent)
if err != nil {
return nil, err
}
relay := &windowsWorkerDaemonRelay{
listener: listener,
parent: windowsParent,
onFailure: onFailure,
connections: make(map[net.Conn]struct{}),
}
go relay.serve()
return relay, nil
}
func (r *windowsWorkerDaemonRelay) serve() {
for {
connection, err := r.listener.Accept()
if err != nil {
if !r.closing.Load() && !errors.Is(err, net.ErrClosed) {
r.onFailure(E.Cause(err, "accept daemon relay connection"))
}
return
}
r.connectionAccess.Lock()
if r.closing.Load() {
r.connectionAccess.Unlock()
connection.Close()
return
}
r.connections[connection] = struct{}{}
r.connectionWaitGroup.Add(1)
r.connectionAccess.Unlock()
go func() {
r.relay(connection)
r.connectionAccess.Lock()
delete(r.connections, connection)
r.connectionAccess.Unlock()
r.connectionWaitGroup.Done()
}()
}
}
func (r *windowsWorkerDaemonRelay) relay(applicationConnection net.Conn) {
daemonConnection, err := r.connectDaemon()
if err != nil {
applicationConnection.Close()
return
}
copyCompleted := make(chan struct{}, 2)
firstCopyCompleted := make(chan struct{})
var firstCopy sync.Once
copyConnection := func(destination io.Writer, source io.Reader) {
_, _ = io.Copy(destination, source)
firstCopy.Do(func() {
close(firstCopyCompleted)
})
copyCompleted <- struct{}{}
}
go copyConnection(daemonConnection, applicationConnection)
go copyConnection(applicationConnection, daemonConnection)
select {
case <-firstCopyCompleted:
case <-r.parent.exited:
}
applicationConnection.Close()
daemonConnection.Close()
<-copyCompleted
<-copyCompleted
}
func (r *windowsWorkerDaemonRelay) connectDaemon() (net.Conn, error) {
connection, err := winio.DialPipe(daemonPipePath, nil)
if err != nil {
return nil, err
}
keepConnection := false
defer func() {
if !keepConnection {
connection.Close()
}
}()
descriptorConnection, loaded := connection.(fileDescriptorConnection)
if !loaded {
return nil, E.New("daemon endpoint is not a Windows named pipe")
}
var processID uint32
err = windows.GetNamedPipeServerProcessId(windows.Handle(descriptorConnection.Fd()), &processID)
if err != nil {
return nil, E.Cause(err, "identify daemon named pipe server")
}
process, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.SYNCHRONIZE, false, processID)
if err != nil {
return nil, E.Cause(err, "open daemon named pipe server process")
}
keepProcess := false
defer func() {
if !keepProcess {
windows.CloseHandle(process)
}
}()
err = validateDaemonProcessIdentity(processID)
if err != nil {
return nil, err
}
processImagePath, err := winioProcess.QueryFullProcessImageName(process, winioProcess.ImageNameFormatWin32Path)
if err != nil {
return nil, E.Cause(err, "query daemon named pipe server executable")
}
processImage, err := openLockedExecutable(processImagePath)
if err != nil {
return nil, E.Cause(err, "open daemon named pipe server executable")
}
keepProcessImage := false
defer func() {
if !keepProcessImage {
windows.CloseHandle(processImage)
}
}()
processImageFinalPath, err := finalWindowsPath(processImage)
if err != nil {
return nil, err
}
if !strings.EqualFold(processImageFinalPath, r.parent.executablePath) {
return nil, E.New("named pipe server is not the installed daemon")
}
sameExecutable, err := sameWindowsFile(processImage, r.parent.executable)
if err != nil {
return nil, err
}
if !sameExecutable {
return nil, E.New("named pipe server daemon executable was replaced")
}
signer, err := authenticodeSigner(processImageFinalPath, processImage)
if err != nil {
return nil, E.Cause(err, "authenticate daemon named pipe server")
}
if !bytes.Equal(signer, r.parent.signer) {
return nil, E.New("daemon server and worker have different signing certificates")
}
waitResult, err := windows.WaitForSingleObject(process, 0)
if err != nil {
return nil, err
}
if waitResult != uint32(windows.WAIT_TIMEOUT) {
return nil, E.New("daemon named pipe server exited during authentication")
}
keepConnection = true
keepProcess = true
keepProcessImage = true
return &windowsAuthenticatedDaemonConnection{
Conn: connection,
process: process,
processImage: processImage,
}, nil
}
func (r *windowsWorkerDaemonRelay) Close() error {
r.close.Do(func() {
r.closing.Store(true)
r.closeError = r.listener.Close()
r.connectionAccess.Lock()
for connection := range r.connections {
connection.Close()
}
r.connectionAccess.Unlock()
r.connectionWaitGroup.Wait()
})
return r.closeError
}
func (c *windowsAuthenticatedDaemonConnection) Close() error {
c.close.Do(func() {
c.closeError = E.Errors(
c.Conn.Close(),
windows.CloseHandle(c.processImage),
windows.CloseHandle(c.process),
)
})
return c.closeError
}
func validateDaemonProcessIdentity(processID uint32) error {
var sessionID uint32
err := windows.ProcessIdToSessionId(processID, &sessionID)
if err != nil {
return E.Cause(err, "query daemon named pipe server session")
}
if sessionID != 0 {
return E.New("daemon named pipe server is not in session zero")
}
managerHandle, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT)
if err != nil {
return E.Cause(err, "connect to service manager")
}
defer windows.CloseServiceHandle(managerHandle)
serviceNamePointer, err := windows.UTF16PtrFromString(serviceName)
if err != nil {
return err
}
serviceHandle, err := windows.OpenService(
managerHandle,
serviceNamePointer,
windows.SERVICE_QUERY_STATUS|windows.SERVICE_QUERY_CONFIG,
)
if err != nil {
return E.Cause(err, "open daemon service")
}
service := &mgr.Service{Name: serviceName, Handle: serviceHandle}
defer service.Close()
status, err := service.Query()
if err != nil {
return E.Cause(err, "query daemon service status")
}
if status.State != svc.Running || status.ProcessId != processID {
return E.New("named pipe server is not the running daemon service")
}
configuration, err := service.Config()
if err != nil {
return E.Cause(err, "query daemon service configuration")
}
if !strings.EqualFold(configuration.ServiceStartName, "LocalSystem") {
return E.New("daemon service does not run as LocalSystem")
}
return nil
}
func processCreationTime(process windows.Handle) (int64, error) {
var creationTime windows.Filetime
var exitTime windows.Filetime
var kernelTime windows.Filetime
var userTime windows.Filetime
err := windows.GetProcessTimes(process, &creationTime, &exitTime, &kernelTime, &userTime)
if err != nil {
return 0, err
}
return creationTime.Nanoseconds(), nil
}