501 lines
14 KiB
Go
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
|
|
}
|