Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CRONET_GO_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
98d539ce67568fb911654e66a14cf4247ed833ec
617d38f41f935b46a68f550d9add2e38abb3f168
60 changes: 59 additions & 1 deletion common/windivert/driver_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,63 @@ const (
// so Open can try CreateFile first and only install on FILE_NOT_FOUND.
var driverDevName, _ = windows.UTF16PtrFromString(driverDeviceName)

// acquireDevice opens the kernel device, installing the driver when it is
// absent. The driver is marked for deletion at install time (see
// installDriver), so it unloads once its last handle closes; the next Open
// must reinstall it. When that Open races the still-in-progress unload,
// CreateFile does not report a clean ERROR_FILE_NOT_FOUND — the
// \\.\WinDivert symlink still resolves while the device object behind it is
// torn down, so the open fails with ERROR_NO_SUCH_DEVICE. Treat every
// "device not currently openable" code as a reinstall trigger and retry a
// bounded number of times so the teardown of a prior instance settles.
func acquireDevice() (windows.Handle, error) {
const maxRetries = 20
for retry := 0; ; retry++ {
device, err := openDevice()
if err == nil {
return device, nil
}
fatal := driverOpenFatal(err)
if fatal != nil {
return 0, fatal
}
err = installDriver()
if err != nil {
return 0, err
}
device, err = openDevice()
if err == nil {
return device, nil
}
fatal = driverOpenFatal(err)
if fatal != nil {
return 0, fatal
}
// Still absent right after a successful install: a prior instance's
// lingering device object shadows the freshly loaded one. Back off
// and retry the whole install/open.
if retry >= maxRetries {
return 0, E.Cause(err, "windivert: open device")
}
time.Sleep(50 * time.Millisecond)
}
}

// driverOpenFatal maps an openDevice failure to the error the caller should
// surface, or nil when the failure means the driver is absent and a
// (re)install should be attempted.
func driverOpenFatal(err error) error {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return E.Cause(err, "windivert: open device (administrator required)")
}
if errors.Is(err, windows.ERROR_FILE_NOT_FOUND) ||
errors.Is(err, windows.ERROR_PATH_NOT_FOUND) ||
errors.Is(err, windows.ERROR_NO_SUCH_DEVICE) {
return nil
}
return E.Cause(err, "windivert: open device")
}

// Requires SeLoadDriverPrivilege (Administrator). Running the 386 build
// under WOW64 on a 64-bit kernel is rejected — use the amd64 build.
func installDriver() error {
Expand Down Expand Up @@ -81,7 +138,8 @@ func installDriver() error {
return nil
}
retryable := errors.Is(err, windows.ERROR_SERVICE_MARKED_FOR_DELETE) ||
errors.Is(err, windows.ERROR_SERVICE_DISABLED)
errors.Is(err, windows.ERROR_SERVICE_DISABLED) ||
errors.Is(err, windows.ERROR_OBJECT_ALREADY_EXISTS)
if !retryable || attempt >= 20 {
return err
}
Expand Down
24 changes: 2 additions & 22 deletions common/windivert/handle_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,9 @@ func Open(filter *Filter, layer Layer, priority int16, flags Flag) (*Handle, err
if err != nil {
return nil, err
}
device, err := openDevice()
device, err := acquireDevice()
if err != nil {
if !errors.Is(err, windows.ERROR_FILE_NOT_FOUND) &&
!errors.Is(err, windows.ERROR_PATH_NOT_FOUND) {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return nil, E.Cause(err, "windivert: open device (administrator required)")
}
return nil, E.Cause(err, "windivert: open device")
}
// Device node missing: kernel driver not loaded. Install + retry.
// Matches WinDivertOpen's lazy-install path; avoids racing StartService
// against a still-loaded driver whose SCM record is marked for deletion.
err = installDriver()
if err != nil {
return nil, err
}
device, err = openDevice()
if err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return nil, E.Cause(err, "windivert: open device (administrator required)")
}
return nil, E.Cause(err, "windivert: open device")
}
return nil, err
}
event, err := windows.CreateEvent(nil, 1, 0, nil) // manual reset, unsignaled
if err != nil {
Expand Down
45 changes: 20 additions & 25 deletions common/windivert/integration_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/netip"
"os"
"path/filepath"
"strconv"
"testing"
"time"

Expand Down Expand Up @@ -110,36 +109,33 @@ func stopDriver(t *testing.T) {
}

// The image lock on the cached .sys can outlive SERVICE_STOPPED by tens of
// seconds (observed on GitHub-hosted runners), but it only blocks writes
// and deletes — rename is permitted. Move the locked file aside instead of
// waiting for the release.
func plantTamperedDriver(t *testing.T, target string, planted []byte) {
// seconds (observed on GitHub-hosted runners), and on current runner images
// it blocks renames as well as writes and deletes. Tests that need to tamper
// with the cache therefore redirect it to a directory the kernel has never
// loaded a driver from. Not t.TempDir: once StartService maps a .sys from
// the directory, the image lock makes the cleanup RemoveAll fail the test.
func setTempDriverCache(t *testing.T) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
err := os.WriteFile(target, planted, 0o644)
if err == nil {
return
}
moved := target + ".locked-" + strconv.FormatInt(time.Now().UnixNano(), 10)
require.NoError(t, os.Rename(target, moved))
t.Cleanup(func() { os.Remove(moved) })
require.NoError(t, os.WriteFile(target, planted, 0o644))
dir, err := os.MkdirTemp("", "sing-box-windivert-test-")
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(dir) })
t.Setenv("LocalAppData", dir)
}

// A foreign .sys planted in the user-writable cache must never reach
// StartService: the install path has to detect the mismatch against the
// embedded asset and repair the file before handing it to SCM.
func TestIntegrationTamperedCacheRepaired(t *testing.T) {
// Open/close once so a working install is the baseline, then stop the
// driver so the cached file is writable for tampering.
h := openHandle(t, nil, FlagSendOnly)
require.NoError(t, h.Close())
setTempDriverCache(t)
// The driver left running by earlier tests would satisfy Open without
// touching the cache; stop it so the install path runs.
stopDriver(t)

target := cachedDriverPath(t)
plantTamperedDriver(t, target, []byte("planted payload, not the WinDivert driver"))
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
require.NoError(t, os.WriteFile(target, []byte("planted payload, not the WinDivert driver"), 0o644))

h = openHandle(t, nil, FlagSendOnly)
h := openHandle(t, nil, FlagSendOnly)
require.NoError(t, h.Close())

content, err := os.ReadFile(target)
Expand All @@ -151,11 +147,10 @@ func TestIntegrationTamperedCacheRepaired(t *testing.T) {
// install completes; without this, the file could be swapped between
// verification and the kernel mapping it.
func TestIntegrationDriverFileLockedWhileHeld(t *testing.T) {
target := cachedDriverPath(t)
// Stop the driver so the kernel image lock is gone and the failures
// asserted below can only come from the handle extractVerified holds.
stopDriver(t)
plantTamperedDriver(t, target, sysBytes)
// A fresh cache directory guarantees the failures asserted below can
// only come from the handle extractVerified holds, not a kernel image
// lock left by earlier tests.
setTempDriverCache(t)

sysPath, sysFile, err := extractVerified()
require.NoError(t, err)
Expand Down
99 changes: 62 additions & 37 deletions daemon/started_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,26 +440,35 @@ func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.Serve
return err
}
defer s.urlTestObserver.UnSubscribe(subscription)
statusSubscription, statusDone, err := s.serviceStatusObserver.Subscribe()
if err != nil {
return err
}
defer s.serviceStatusObserver.UnSubscribe(statusSubscription)
for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
var groups *Groups
if s.serviceStatus.Status == ServiceStatus_STARTED {
groups = s.readGroups()
} else {
groups = &Groups{}
}
groups := s.readGroups()
s.serviceAccess.RUnlock()
err = server.Send(groups)
if err != nil {
return err
}
select {
case <-subscription:
case <-statusSubscription:
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
case <-statusDone:
return nil
}
}
}
Expand Down Expand Up @@ -537,31 +546,40 @@ func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.Se
return err
}
defer s.clashModeObserver.UnSubscribe(subscription)
statusSubscription, statusDone, err := s.serviceStatusObserver.Subscribe()
if err != nil {
return err
}
defer s.serviceStatusObserver.UnSubscribe(statusSubscription)
for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
clashServer := s.instance.clashServer
if clashServer == nil {
s.serviceAccess.RUnlock()
return status.Error(codes.NotFound, "clash mode not available")
var message *ClashMode
if s.serviceStatus.Status == ServiceStatus_STARTED {
clashServer := s.instance.clashServer
if clashServer == nil {
s.serviceAccess.RUnlock()
return status.Error(codes.NotFound, "clash mode not available")
}
message = &ClashMode{Mode: clashServer.Mode()}
} else {
message = &ClashMode{}
}
message := &ClashMode{Mode: clashServer.Mode()}
s.serviceAccess.RUnlock()
err = server.Send(message)
if err != nil {
return err
}
select {
case <-subscription:
case <-statusSubscription:
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
case <-statusDone:
return nil
}
}
}
Expand Down Expand Up @@ -1050,50 +1068,57 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server
return err
}
defer s.urlTestObserver.UnSubscribe(subscription)
statusSubscription, statusDone, err := s.serviceStatusObserver.Subscribe()
if err != nil {
return err
}
defer s.serviceStatusObserver.UnSubscribe(statusSubscription)
for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
boxService := s.instance
started := s.serviceStatus.Status == ServiceStatus_STARTED
s.serviceAccess.RUnlock()
historyStorage := boxService.urlTestHistoryStorage
var list OutboundList
for _, ob := range boxService.outboundManager.Outbounds() {
item := &GroupItem{
Tag: ob.Tag(),
Type: ob.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
for _, ep := range boxService.endpointManager.Endpoints() {
item := &GroupItem{
Tag: ep.Tag(),
Type: ep.Type(),
if started {
historyStorage := boxService.urlTestHistoryStorage
for _, ob := range boxService.outboundManager.Outbounds() {
item := &GroupItem{
Tag: ob.Tag(),
Type: ob.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
for _, ep := range boxService.endpointManager.Endpoints() {
item := &GroupItem{
Tag: ep.Tag(),
Type: ep.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
list.Outbounds = append(list.Outbounds, item)
}
err = server.Send(&list)
if err != nil {
return err
}
select {
case <-subscription:
case <-statusSubscription:
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
case <-statusDone:
return nil
}
}
}
Expand Down
Loading