Package WinDivert drivers instead of embedding it

This commit is contained in:
世界
2026-08-30 17:41:45 +08:00
parent 9ea88e556a
commit baa99c6f8f
18 changed files with 291 additions and 255 deletions
+54
View File
@@ -1,6 +1,9 @@
package main package main
import ( import (
"bytes"
"crypto/sha256"
"encoding/hex"
"flag" "flag"
"os" "os"
"os/exec" "os/exec"
@@ -9,6 +12,7 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/common/windivert"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
@@ -93,6 +97,53 @@ func build() error {
if err != nil { if err != nil {
return E.Cause(err, "build sing-box daemon") return E.Cause(err, "build sing-box daemon")
} }
if operatingSystem == "windows" {
err = stageWinDivertDriver(architecture, filepath.Dir(absoluteOutputPath))
if err != nil {
return err
}
}
return nil
}
func stageWinDivertDriver(architecture string, outputDirectory string) error {
var assetName, assetDigest string
switch architecture {
case "amd64":
assetName, assetDigest = windivert.Asset64Name, windivert.Asset64SHA256
case "386":
assetName, assetDigest = windivert.Asset32Name, windivert.Asset32SHA256
}
for _, name := range []string{windivert.Asset64Name, windivert.Asset32Name} {
if name == assetName {
continue
}
err := os.Remove(filepath.Join(outputDirectory, name))
if err != nil && !os.IsNotExist(err) {
return E.Cause(err, "remove stale ", name)
}
}
if assetName == "" {
return nil
}
assetDirectory := filepath.Join("common", "windivert", "assets")
content, err := os.ReadFile(filepath.Join(assetDirectory, assetName))
if err != nil {
return E.Cause(err, "read ", assetName)
}
checksum := sha256.Sum256(content)
if hex.EncodeToString(checksum[:]) != assetDigest {
return E.New(assetName, " does not match the digest declared in common/windivert")
}
targetPath := filepath.Join(outputDirectory, assetName)
staged, err := os.ReadFile(targetPath)
if err == nil && bytes.Equal(staged, content) {
return nil
}
err = os.WriteFile(targetPath, content, 0o644)
if err != nil {
return E.Cause(err, "write ", assetName)
}
return nil return nil
} }
@@ -112,6 +163,9 @@ func buildTags(operatingSystem string, architecture string, cgoEnabled bool) ([]
return nil, E.Cause(err, "read build tags") return nil, E.Cause(err, "read build tags")
} }
tags := strings.Split(strings.TrimSpace(string(content)), ",") tags := strings.Split(strings.TrimSpace(string(content)), ",")
if operatingSystem == "windows" {
tags = append(tags, "with_external_windivert")
}
if debugEnabled { if debugEnabled {
tags = append(tags, "debug") tags = append(tags, "debug")
} }
-1
View File
@@ -38,7 +38,6 @@ func TestAddressSetTCPChecksum(t *testing.T) {
require.Equal(t, uint32(0), addr.bits) require.Equal(t, uint32(0), addr.bits)
} }
// Setters must not disturb sibling bits.
func TestAddressFlagBitsIndependent(t *testing.T) { func TestAddressFlagBitsIndependent(t *testing.T) {
t.Parallel() t.Parallel()
var addr Address var addr Address
+4 -6
View File
@@ -2,9 +2,7 @@
package windivert package windivert
import _ "embed" const (
driverAssetName = Asset32Name
//go:embed assets/WinDivert32.sys driverAssetDigest = Asset32SHA256
var sysBytes []byte )
func driverSysName() string { return "WinDivert32.sys" }
+4 -6
View File
@@ -2,9 +2,7 @@
package windivert package windivert
import _ "embed" const (
driverAssetName = Asset64Name
//go:embed assets/WinDivert64.sys driverAssetDigest = Asset64SHA256
var sysBytes []byte )
func driverSysName() string { return "WinDivert64.sys" }
+4 -3
View File
@@ -2,6 +2,7 @@
package windivert package windivert
var sysBytes []byte const (
driverAssetName = ""
func driverSysName() string { return "" } driverAssetDigest = ""
)
@@ -0,0 +1,7 @@
//go:build windows && !with_external_windivert
package windivert
func driverAsset() ([]byte, error) {
return sysBytes, nil
}
@@ -0,0 +1,29 @@
//go:build windows && with_external_windivert
package windivert
import (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
E "github.com/sagernet/sing/common/exceptions"
)
func driverAsset() ([]byte, error) {
executablePath, err := os.Executable()
if err != nil {
return nil, E.Cause(err, "windivert: locate executable")
}
assetPath := filepath.Join(filepath.Dir(executablePath), driverAssetName)
content, err := os.ReadFile(assetPath)
if err != nil {
return nil, E.Cause(err, "windivert: read ", assetPath)
}
digest := sha256.Sum256(content)
if hex.EncodeToString(digest[:]) != driverAssetDigest {
return nil, E.New("windivert: ", assetPath, " does not match the WinDivert ", AssetVersion, " digest")
}
return content, nil
}
+31 -40
View File
@@ -11,33 +11,44 @@ import (
"strconv" "strconv"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
) )
func extractVerified() (string, *os.File, error) { func driverFilePath() (string, error) {
if len(sysBytes) == 0 { if driverAssetName == "" {
return "", nil, E.New("windivert: unsupported architecture ", runtime.GOARCH) return "", E.New("windivert: unsupported architecture ", runtime.GOARCH)
} }
base, err := os.UserCacheDir() base, err := os.UserCacheDir()
if err != nil { if err != nil {
return "", nil, E.Cause(err, "windivert: locate user cache dir") return "", E.Cause(err, "windivert: locate user cache dir")
} }
dir := filepath.Join(base, "sing-box", "windivert", "v"+AssetVersion) return filepath.Join(base, "sing-box", "windivert", "v"+AssetVersion, driverAssetName), nil
err = os.MkdirAll(dir, 0o755) }
if err != nil {
return "", nil, E.Cause(err, "windivert: mkdir ", dir)
}
target := filepath.Join(dir, driverSysName())
func openVerifiedDriver() (string, *os.File, error) {
target, err := driverFilePath()
if err != nil {
return "", nil, err
}
assetContent, err := driverAsset()
if err != nil {
return "", nil, err
}
err = os.MkdirAll(filepath.Dir(target), 0o755)
if err != nil {
return "", nil, E.Cause(err, "windivert: mkdir ", filepath.Dir(target))
}
var (
sysFile *os.File
content []byte
)
for attempt := 0; ; attempt++ { for attempt := 0; ; attempt++ {
sysFile, err := openDriverFile(target) sysFile, err = openDriverFile(target)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return "", nil, E.Cause(err, "windivert: open ", target) return "", nil, E.Cause(err, "windivert: open ", target)
} }
err = writeDriverFile(target) err = writeDriverFile(target, assetContent)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -46,48 +57,28 @@ func extractVerified() (string, *os.File, error) {
return "", nil, E.Cause(err, "windivert: open ", target) return "", nil, E.Cause(err, "windivert: open ", target)
} }
} }
content, err := io.ReadAll(sysFile) content, err = io.ReadAll(sysFile)
if err != nil { if err != nil {
sysFile.Close() sysFile.Close()
return "", nil, E.Cause(err, "windivert: read ", target) return "", nil, E.Cause(err, "windivert: read ", target)
} }
if bytes.Equal(content, sysBytes) { if bytes.Equal(content, assetContent) {
return target, sysFile, nil return target, sysFile, nil
} }
sysFile.Close() sysFile.Close()
if attempt > 0 { if attempt > 0 {
return "", nil, E.New("windivert: driver file ", target, " is being concurrently modified") return "", nil, E.New("windivert: driver file ", target, " is being concurrently modified")
} }
err = writeDriverFile(target) err = writeDriverFile(target, assetContent)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
} }
} }
func openDriverFile(path string) (*os.File, error) { func writeDriverFile(target string, content []byte) error {
pathW, err := windows.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
handle, err := windows.CreateFile(
pathW,
windows.GENERIC_READ,
windows.FILE_SHARE_READ,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(handle), path), nil
}
func writeDriverFile(target string) error {
temporaryPath := target + ".tmp-" + strconv.Itoa(os.Getpid()) temporaryPath := target + ".tmp-" + strconv.Itoa(os.Getpid())
err := os.WriteFile(temporaryPath, sysBytes, 0o644) err := os.WriteFile(temporaryPath, content, 0o644)
if err != nil { if err != nil {
return E.Cause(err, "windivert: write ", filepath.Base(target)) return E.Cause(err, "windivert: write ", filepath.Base(target))
} }
@@ -0,0 +1,61 @@
//go:build windows && !with_external_windivert
package windivert
import (
"bytes"
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
// The image lock on a loaded .sys can outlive SERVICE_STOPPED by tens of
// seconds (observed on GitHub-hosted runners), and on current runner images
// it blocks renames as well as writes and deletes.
func setTempDriverCache(t *testing.T) {
t.Helper()
dir, err := os.MkdirTemp("", "sing-box-windivert-test-")
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(dir) })
t.Setenv("LocalAppData", dir)
}
func TestIntegrationTamperedCacheRepaired(t *testing.T) {
setTempDriverCache(t)
stopDriver(t)
target, err := driverFilePath()
require.NoError(t, err)
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)
require.NoError(t, h.Close())
content, err := os.ReadFile(target)
require.NoError(t, err)
require.True(t, bytes.Equal(content, sysBytes), "cached driver was not repaired to the embedded asset")
}
func TestIntegrationDriverFileLockedWhileHeld(t *testing.T) {
setTempDriverCache(t)
sysPath, sysFile, err := openVerifiedDriver()
require.NoError(t, err)
defer sysFile.Close()
writeErr := os.WriteFile(sysPath, []byte("overwrite attempt"), 0o644)
require.Error(t, writeErr)
require.True(t, errors.Is(writeErr, windows.ERROR_SHARING_VIOLATION),
"expected sharing violation, got %v", writeErr)
evil := sysPath + ".evil"
require.NoError(t, os.WriteFile(evil, []byte("replacement attempt"), 0o644))
defer os.Remove(evil)
renameErr := os.Rename(evil, sysPath)
require.Error(t, renameErr)
}
+38 -9
View File
@@ -4,6 +4,7 @@ package windivert
import ( import (
"errors" "errors"
"os"
"runtime" "runtime"
"time" "time"
@@ -73,7 +74,7 @@ func installAndOpenDevice() (windows.Handle, error) {
return 0, fatalErr return 0, fatalErr
} }
sysPath, sysFile, err := extractVerified() sysPath, sysFile, err := openVerifiedDriver()
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -130,11 +131,8 @@ func tryInstallService(manager windows.Handle, serviceNameW, sysPathW *uint16) e
err = windows.StartService(service, 0, nil) err = windows.StartService(service, 0, nil)
if err == nil { if err == nil {
// Mark for deletion so the driver unregisters when the last handle // Upstream WinDivert.dll marks the service for deletion only in the
// closes or on next reboot. Matches the upstream DLL's behavior: // process whose StartService succeeded.
// only the process that actually started the service takes on the
// cleanup responsibility. If another process already started it,
// we leave DeleteService to them.
_ = windows.DeleteService(service) _ = windows.DeleteService(service)
return nil return nil
} }
@@ -142,9 +140,8 @@ func tryInstallService(manager windows.Handle, serviceNameW, sysPathW *uint16) e
return nil return nil
} }
if errors.Is(err, windows.ERROR_SERVICE_DISABLED) { if errors.Is(err, windows.ERROR_SERVICE_DISABLED) {
// The disabled check precedes the running check: a running service // StartService on a running service that is marked for deletion
// marked for deletion reports ERROR_SERVICE_DISABLED instead of // reports ERROR_SERVICE_DISABLED, not ERROR_SERVICE_ALREADY_RUNNING.
// ERROR_SERVICE_ALREADY_RUNNING. The device is nonetheless up.
var status windows.SERVICE_STATUS var status windows.SERVICE_STATUS
queryErr := windows.QueryServiceStatus(service, &status) queryErr := windows.QueryServiceStatus(service, &status)
if queryErr == nil && status.CurrentState == windows.SERVICE_RUNNING { if queryErr == nil && status.CurrentState == windows.SERVICE_RUNNING {
@@ -157,6 +154,18 @@ func tryInstallService(manager windows.Handle, serviceNameW, sysPathW *uint16) e
func openOrCreateService(manager windows.Handle, serviceNameW, sysPathW *uint16) (windows.Handle, error) { func openOrCreateService(manager windows.Handle, serviceNameW, sysPathW *uint16) (windows.Handle, error) {
service, err := windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS) service, err := windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS)
if err == nil { if err == nil {
err = windows.ChangeServiceConfig(
service,
windows.SERVICE_NO_CHANGE,
windows.SERVICE_NO_CHANGE,
windows.SERVICE_NO_CHANGE,
sysPathW,
nil, nil, nil, nil, nil, nil,
)
if err != nil {
windows.CloseServiceHandle(service)
return 0, E.Cause(err, "windivert: point service at ", driverAssetName)
}
return service, nil return service, nil
} }
service, err = windows.CreateService( service, err = windows.CreateService(
@@ -188,3 +197,23 @@ func wrapDriverInstallError(err error) error {
} }
return E.Cause(err, "windivert: create service") return E.Cause(err, "windivert: create service")
} }
func openDriverFile(path string) (*os.File, error) {
pathW, err := windows.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
handle, err := windows.CreateFile(
pathW,
windows.GENERIC_READ,
windows.FILE_SHARE_READ,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(handle), path), nil
}
+8
View File
@@ -0,0 +1,8 @@
//go:build windows && 386 && !with_external_windivert
package windivert
import _ "embed"
//go:embed assets/WinDivert32.sys
var sysBytes []byte
+8
View File
@@ -0,0 +1,8 @@
//go:build windows && amd64 && !with_external_windivert
package windivert
import _ "embed"
//go:embed assets/WinDivert64.sys
var sysBytes []byte
+5
View File
@@ -0,0 +1,5 @@
//go:build windows && !amd64 && !386 && !with_external_windivert
package windivert
var sysBytes []byte
+10 -30
View File
@@ -60,32 +60,24 @@ const (
) )
type filterInst struct { type filterInst struct {
field uint16 // 11 bits used field uint16
test uint8 // 5 bits used test uint8
success uint16 success uint16
failure uint16 failure uint16
neg bool neg bool
arg [4]uint32 arg [4]uint32
} }
// Filter is a typed specification of packets to capture. It replaces
// WinDivert's filter string language.
//
// Zero value = "reject all" (match nothing), suitable for send-only handles.
type Filter struct { type Filter struct {
insts []filterInst insts []filterInst
anyInsts []filterInst // trailing OR block: any match accepts anyInsts []filterInst
flags uint64 // filter flags for STARTUP ioctl flags uint64
} }
// reject returns a filter that matches no packet. The empty insts slice
// is encoded as a single rejecting instruction by encode().
func reject() *Filter { func reject() *Filter {
return &Filter{} return &Filter{}
} }
// OutboundTCP returns a filter matching outbound TCP packets on the given
// 5-tuple. Both addresses must share an address family (IPv4 or IPv6).
func OutboundTCP(src, dst netip.AddrPort) (*Filter, error) { func OutboundTCP(src, dst netip.AddrPort) (*Filter, error) {
if !src.IsValid() || !dst.IsValid() { if !src.IsValid() || !dst.IsValid() {
return nil, E.New("windivert: filter: invalid address port") return nil, E.New("windivert: filter: invalid address port")
@@ -96,8 +88,6 @@ func OutboundTCP(src, dst netip.AddrPort) (*Filter, error) {
f := &Filter{ f := &Filter{
flags: filterFlagOutbound, flags: filterFlagOutbound,
} }
// Insts chain as AND: each test's failure = REJECT, success = next inst.
// The final inst's success = ACCEPT.
f.add(fieldOutbound, testEQ, argUint32(1)) f.add(fieldOutbound, testEQ, argUint32(1))
if src.Addr().Is4() { if src.Addr().Is4() {
f.flags |= filterFlagIP f.flags |= filterFlagIP
@@ -214,21 +204,17 @@ func (f *Filter) addAny(field uint16, test uint8, arg [4]uint32) {
func argUint32(v uint32) [4]uint32 { return [4]uint32{v, 0, 0, 0} } func argUint32(v uint32) [4]uint32 { return [4]uint32{v, 0, 0, 0} }
// argIPv4 encodes an IPv4 address for IP_SRCADDR/IP_DSTADDR. The driver // The driver compares IP_SRCADDR/IP_DSTADDR against an IPv4-mapped-IPv6
// compares against an IPv4-mapped-IPv6 form: {host_order_u32, 0x0000FFFF, // form: {host_order_u32, 0x0000FFFF, 0, 0} (sys/windivert.c
// 0, 0} (see sys/windivert.c windivert_get_ipv4_addr and the IPv4_SRCADDR // windivert_get_ipv4_addr).
// val-word construction). Omitting the 0x0000FFFF marker causes the EQ
// test to fail for every packet.
func argIPv4(addr netip.Addr) [4]uint32 { func argIPv4(addr netip.Addr) [4]uint32 {
b := addr.As4() b := addr.As4()
return [4]uint32{binary.BigEndian.Uint32(b[:]), 0x0000FFFF, 0, 0} return [4]uint32{binary.BigEndian.Uint32(b[:]), 0x0000FFFF, 0, 0}
} }
// argIPv6 encodes an IPv6 address for IPV6_SRCADDR/IPV6_DSTADDR. The // The driver stores IPV6_SRCADDR/IPV6_DSTADDR as four host-order uint32s in
// driver stores the address as four host-order uint32s in REVERSED word // reversed word order: val[0]=low (bytes 12..15), val[3]=high (bytes 0..3)
// order: val[0]=low (bytes 12..15), val[3]=high (bytes 0..3). See // (sys/windivert.c windivert_outbound_network_v6_classify).
// sys/windivert.c windivert_outbound_network_v6_classify val-word
// construction.
func argIPv6(addr netip.Addr) [4]uint32 { func argIPv6(addr netip.Addr) [4]uint32 {
b := addr.As16() b := addr.As16()
return [4]uint32{ return [4]uint32{
@@ -239,15 +225,9 @@ func argIPv6(addr netip.Addr) [4]uint32 {
} }
} }
// encode serializes the Filter to the on-wire WINDIVERT_FILTER[] format
// plus the filter_flags for STARTUP ioctl. insts chain as AND (failure
// rejects); anyInsts follow as an OR block (success accepts, failure falls
// through to the next alternative).
func (f *Filter) encode() ([]byte, uint64, error) { func (f *Filter) encode() ([]byte, uint64, error) {
total := len(f.insts) + len(f.anyInsts) total := len(f.insts) + len(f.anyInsts)
if total == 0 { if total == 0 {
// "Reject all" — one instruction, ZERO == 0 is always true, but we
// invert by setting both success and failure to REJECT.
return encodeInst(filterInst{ return encodeInst(filterInst{
field: fieldZero, field: fieldZero,
test: testEQ, test: testEQ,
+11 -44
View File
@@ -14,17 +14,6 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
// Handle owns a WinDivert kernel device handle plus a private event for
// overlapped I/O. Methods on *Handle are not safe for concurrent use
// across goroutines (there is a single shared event per Handle).
//
// addr is a per-Handle Address buffer the IOCTL struct embeds a pointer
// to. It lives on the heap (as a field of a heap-allocated Handle) so
// the pointer value stored as bytes in the ioctl buffer remains valid
// across stack growth between buildIoctl* and the DeviceIoControl
// syscall — stack-local Address values are not safe for this pattern
// because Go's escape analysis does not see the pointer through the
// unsafe.Pointer → uintptr → bytes conversion.
type Handle struct { type Handle struct {
device windows.Handle device windows.Handle
event windows.Handle event windows.Handle
@@ -36,9 +25,6 @@ type Handle struct {
sendAddrs []Address sendAddrs []Address
} }
// Filter may be nil for "reject all", suitable for send-only handles.
// Requires Administrator on first call per process (installs the kernel
// driver via SCM); subsequent calls reuse the running driver.
func Open(filter *Filter, layer Layer, priority int16, flags Flag) (*Handle, error) { func Open(filter *Filter, layer Layer, priority int16, flags Flag) (*Handle, error) {
err := validateOpenArgs(layer, priority, flags) err := validateOpenArgs(layer, priority, flags)
if err != nil { if err != nil {
@@ -105,8 +91,6 @@ func validateOpenArgs(layer Layer, priority int16, flags Flag) error {
func (h *Handle) initialize(layer Layer, priority int16, flags Flag) error { func (h *Handle) initialize(layer Layer, priority int16, flags Flag) error {
in := buildIoctlInitialize(layer, priority, flags) in := buildIoctlInitialize(layer, priority, flags)
// WINDIVERT_VERSION is a 64-byte packed struct; only the first 20
// bytes (magic, major, minor, bits) carry data, the rest is reserved.
var outBuf [versionStructSize]byte var outBuf [versionStructSize]byte
binary.LittleEndian.PutUint64(outBuf[0:8], magicDLL) binary.LittleEndian.PutUint64(outBuf[0:8], magicDLL)
binary.LittleEndian.PutUint32(outBuf[8:12], versionMajor) binary.LittleEndian.PutUint32(outBuf[8:12], versionMajor)
@@ -137,7 +121,6 @@ func (h *Handle) startup(filterBin []byte, filterFlags uint64) error {
return nil return nil
} }
// If the handle is closed mid-Recv the error wraps ERROR_OPERATION_ABORTED.
func (h *Handle) Recv(buf []byte) (int, Address, error) { func (h *Handle) Recv(buf []byte) (int, Address, error) {
if len(buf) == 0 { if len(buf) == 0 {
return 0, Address{}, E.New("windivert: recv: zero-length buffer") return 0, Address{}, E.New("windivert: recv: zero-length buffer")
@@ -158,12 +141,9 @@ const BatchMax = 255
const addressSize = uint32(unsafe.Sizeof(Address{})) const addressSize = uint32(unsafe.Sizeof(Address{}))
// RecvBatch receives up to BatchMax packets in one ioctl. The driver packs // The driver packs packets back-to-back into buf with no padding and copies
// packets back-to-back into buf with no padding and copies exactly each // exactly each packet's IP total length, and returns as soon as at least one
// packet's IP total length, so boundaries are recovered by walking the IP // packet is available.
// length fields. It returns as soon as at least one packet is available;
// it never waits to fill the batch. The returned Address slice is owned by
// the Handle and is overwritten by the next RecvBatch.
func (h *Handle) RecvBatch(buf []byte) (int, []Address, error) { func (h *Handle) RecvBatch(buf []byte) (int, []Address, error) {
if len(buf) < MTUMax { if len(buf) < MTUMax {
return 0, nil, E.New("windivert: recv batch: buffer smaller than MTUMax") return 0, nil, E.New("windivert: recv batch: buffer smaller than MTUMax")
@@ -181,9 +161,8 @@ func (h *Handle) RecvBatch(buf []byte) (int, []Address, error) {
return int(n), h.recvAddrs[:h.recvAddrsLen/addressSize], nil return int(n), h.recvAddrs[:h.recvAddrsLen/addressSize], nil
} }
// SendBatch injects the packets packed back-to-back in buf, one Address per // The driver recovers packet boundaries from the IP total-length fields and
// packet. The driver recovers packet boundaries from the IP total-length // rejects the whole batch if they do not add up to len(buf).
// fields and rejects the whole batch if they do not add up to len(buf).
func (h *Handle) SendBatch(buf []byte, addrs []Address) (int, error) { func (h *Handle) SendBatch(buf []byte, addrs []Address) (int, error) {
if len(addrs) == 0 || len(addrs) > BatchMax { if len(addrs) == 0 || len(addrs) > BatchMax {
return 0, E.New("windivert: send batch: invalid packet count ", len(addrs)) return 0, E.New("windivert: send batch: invalid packet count ", len(addrs))
@@ -223,7 +202,6 @@ func (h *Handle) Send(packet []byte, addr *Address) (int, error) {
return int(n), nil return int(n), nil
} }
// Idempotent. Aborts any in-flight I/O on the handle.
func (h *Handle) Close() error { func (h *Handle) Close() error {
h.closing.Do(func() { h.closing.Do(func() {
var errs []error var errs []error
@@ -288,15 +266,8 @@ const ioctlSize = 16
// carry data; the rest is reserved zero padding. // carry data; the rest is reserved zero padding.
const versionStructSize = 64 const versionStructSize = 64
// doIoctl performs a single synchronous (blocking) overlapped // NtDeviceIoControlFile clears the event to nonsignaled before queuing each
// DeviceIoControl. The handle is opened with FILE_FLAG_OVERLAPPED so // request, so one event can be reused across calls without ResetEvent.
// DeviceIoControl may return ERROR_IO_PENDING; we then wait for
// completion via GetOverlappedResult. Event is passed in so callers can
// reuse it across calls on the same handle (avoids per-call CreateEvent).
// No explicit ResetEvent is needed: NtDeviceIoControlFile clears the
// event to nonsignaled before queuing each request, and on synchronous
// completion (DeviceIoControl returns success) lpBytesReturned is
// already filled, so GetOverlappedResult is skipped entirely.
func doIoctl(handle windows.Handle, code uint32, in []byte, out []byte, event windows.Handle) (uint32, error) { func doIoctl(handle windows.Handle, code uint32, in []byte, out []byte, event windows.Handle) (uint32, error) {
var overlapped windows.Overlapped var overlapped windows.Overlapped
overlapped.HEvent = event overlapped.HEvent = event
@@ -344,10 +315,8 @@ func buildIoctlStartup(filterFlags uint64) [ioctlSize]byte {
return buf return buf
} }
// buildIoctlRecv packs a user-space pointer to a WINDIVERT_ADDRESS into // The driver dereferences the packed pointer to write the received packet's
// the ioctl struct. The driver dereferences it to write the address for // WINDIVERT_ADDRESS.
// the received packet. Caller must keep the Address alive via
// runtime.KeepAlive.
func buildIoctlRecv(addr *Address) [ioctlSize]byte { func buildIoctlRecv(addr *Address) [ioctlSize]byte {
var buf [ioctlSize]byte var buf [ioctlSize]byte
binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addr)))) binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addr))))
@@ -355,10 +324,8 @@ func buildIoctlRecv(addr *Address) [ioctlSize]byte {
return buf return buf
} }
// buildIoctlRecvBatch additionally passes addr_len_ptr, a pointer to the // addr_len_ptr carries the Address array capacity in bytes; the driver
// Address array capacity in bytes; the driver overwrites it with the bytes // overwrites it with the bytes actually written (packet count × 80).
// actually written (packet count × 80). Caller must keep both pointees
// alive via runtime.KeepAlive.
func buildIoctlRecvBatch(addrs *Address, addrsLen *uint32) [ioctlSize]byte { func buildIoctlRecvBatch(addrs *Address, addrsLen *uint32) [ioctlSize]byte {
var buf [ioctlSize]byte var buf [ioctlSize]byte
binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addrs)))) binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addrs))))
-2
View File
@@ -101,9 +101,7 @@ func TestValidateOpenArgsFlags(t *testing.T) {
require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0)) require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0))
require.NoError(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly)) require.NoError(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly))
require.NoError(t, validateOpenArgs(LayerNetwork, 0, FlagSniff)) require.NoError(t, validateOpenArgs(LayerNetwork, 0, FlagSniff))
// Sniff and send-only describe contradictory handle roles.
require.Error(t, validateOpenArgs(LayerNetwork, 0, FlagSniff|FlagSendOnly)) require.Error(t, validateOpenArgs(LayerNetwork, 0, FlagSniff|FlagSendOnly))
// Unknown flag bits must be rejected to surface caller mistakes early.
require.Error(t, validateOpenArgs(LayerNetwork, 0, Flag(0x10))) require.Error(t, validateOpenArgs(LayerNetwork, 0, Flag(0x10)))
require.Error(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly|Flag(0x10))) require.Error(t, validateOpenArgs(LayerNetwork, 0, FlagSendOnly|Flag(0x10)))
} }
+3 -86
View File
@@ -1,14 +1,12 @@
//go:build windows //go:build windows && !with_external_windivert
package windivert package windivert
import ( import (
"bytes"
"errors" "errors"
"log" "log"
"net/netip" "net/netip"
"os" "os"
"path/filepath"
"testing" "testing"
"time" "time"
@@ -37,26 +35,18 @@ func openHandle(t *testing.T, filter *Filter, flags Flag) *Handle {
return h return h
} }
// A send-only handle installs+opens the driver but does not attach a
// receive filter, so it exercises the full driver-install path without
// diverting any live traffic on the host.
func TestIntegrationOpenSendOnly(t *testing.T) { func TestIntegrationOpenSendOnly(t *testing.T) {
h := openHandle(t, nil, FlagSendOnly) h := openHandle(t, nil, FlagSendOnly)
require.NoError(t, h.Close()) require.NoError(t, h.Close())
} }
// Close is idempotent per the doc contract.
func TestIntegrationCloseTwice(t *testing.T) { func TestIntegrationCloseTwice(t *testing.T) {
h := openHandle(t, nil, FlagSendOnly) h := openHandle(t, nil, FlagSendOnly)
require.NoError(t, h.Close()) require.NoError(t, h.Close())
require.NoError(t, h.Close()) require.NoError(t, h.Close())
} }
// Recv must unblock when the handle is closed concurrently. Without this,
// the spoofer's run goroutine could deadlock on shutdown.
func TestIntegrationRecvAbortsOnClose(t *testing.T) { func TestIntegrationRecvAbortsOnClose(t *testing.T) {
// A filter no live traffic will match, so Recv blocks indefinitely
// until Close aborts the overlapped I/O.
filter, err := OutboundTCP( filter, err := OutboundTCP(
netip.MustParseAddrPort("10.255.255.254:1"), netip.MustParseAddrPort("10.255.255.254:1"),
netip.MustParseAddrPort("10.255.255.253:2"), netip.MustParseAddrPort("10.255.255.253:2"),
@@ -71,7 +61,6 @@ func TestIntegrationRecvAbortsOnClose(t *testing.T) {
errCh <- recvErr errCh <- recvErr
}() }()
// Let Recv reach the blocking DeviceIoControl before Close races in.
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
require.NoError(t, h.Close()) require.NoError(t, h.Close())
@@ -85,18 +74,8 @@ func TestIntegrationRecvAbortsOnClose(t *testing.T) {
} }
} }
func cachedDriverPath(t *testing.T) string {
t.Helper()
base, err := os.UserCacheDir()
require.NoError(t, err)
return filepath.Join(base, "sing-box", "windivert", "v"+AssetVersion, driverSysName())
}
// The driver does not unload when the last handle closes: it stays running // The driver does not unload when the last handle closes: it stays running
// (and the memory manager keeps its backing image write-locked) until // until explicitly stopped, like `sc stop WinDivert`.
// explicitly stopped, like `sc stop WinDivert`. The install-time
// DeleteService mark then removes the record once the last SCM handle
// closes.
func stopDriver(t *testing.T) { func stopDriver(t *testing.T) {
t.Helper() t.Helper()
manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT) manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT)
@@ -122,7 +101,7 @@ func stopDriver(t *testing.T) {
require.True(t, errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST), "open driver service: %v", err) require.True(t, errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST), "open driver service: %v", err)
} }
// SCM can report SERVICE_STOPPED before the driver finishes deleting its // SCM can report SERVICE_STOPPED before the driver finishes deleting its
// device object. Wait for the absence acquireDevice uses to trigger install. // device object.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
device, openErr := openDevice() device, openErr := openDevice()
if openErr == nil { if openErr == nil {
@@ -135,68 +114,6 @@ func stopDriver(t *testing.T) {
}, 60*time.Second, 200*time.Millisecond, "driver device remained openable after stop") }, 60*time.Second, 200*time.Millisecond, "driver device remained openable after stop")
} }
// The image lock on the cached .sys can outlive SERVICE_STOPPED by tens of
// 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()
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) {
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)
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)
require.NoError(t, h.Close())
content, err := os.ReadFile(target)
require.NoError(t, err)
require.True(t, bytes.Equal(content, sysBytes), "cached driver was not repaired to the embedded asset")
}
// The verified handle must lock the file against writers and renames until
// install completes; without this, the file could be swapped between
// verification and the kernel mapping it.
func TestIntegrationDriverFileLockedWhileHeld(t *testing.T) {
// 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)
defer sysFile.Close()
writeErr := os.WriteFile(sysPath, []byte("overwrite attempt"), 0o644)
require.Error(t, writeErr)
require.True(t, errors.Is(writeErr, windows.ERROR_SHARING_VIOLATION),
"expected sharing violation, got %v", writeErr)
evil := sysPath + ".evil"
require.NoError(t, os.WriteFile(evil, []byte("replacement attempt"), 0o644))
defer os.Remove(evil)
renameErr := os.Rename(evil, sysPath)
require.Error(t, renameErr)
}
// Two concurrent Open calls must both succeed: the first wins the driver
// install race, the second reuses the already-running service.
func TestIntegrationConcurrentOpen(t *testing.T) { func TestIntegrationConcurrentOpen(t *testing.T) {
stopDriver(t) stopDriver(t)
start := make(chan struct{}) start := make(chan struct{})
+14 -28
View File
@@ -1,21 +1,19 @@
// Package windivert provides a pure-Go binding to the WinDivert kernel // Upstream: https://github.com/basil00/WinDivert v2.2.2, redistributed
// driver on Windows (amd64 and 386). User-mode WinDivert calls are // under its LGPL v3 option; see assets/LICENSE.txt.
// reimplemented in Go; only the signed kernel driver is embedded as an
// asset, since SCM-installed drivers must live on disk. The on-disk copy
// is verified byte-for-byte against the embedded asset on every install
// and held open deny-write while the kernel loads it.
//
// Administrator is required for the first Open in a process so SCM can
// load the driver. Upstream: https://github.com/basil00/WinDivert v2.2.2,
// redistributed under its LGPL v3 option; see assets/LICENSE.txt.
package windivert package windivert
import "unsafe" import "unsafe"
const AssetVersion = "2.2.2" const AssetVersion = "2.2.2"
// MTUMax is WINDIVERT_MTU_MAX from windivert.h (40 + 0xFFFF). Suitable as const (
// a single-packet receive buffer size. Asset64Name = "WinDivert64.sys"
Asset32Name = "WinDivert32.sys"
Asset64SHA256 = "8da085332782708d8767bcace5327a6ec7283c17cfb85e40b03cd2323a90ddc2"
Asset32SHA256 = "2f43f4251be4d72dd56c91bf6cce475d379eb9ba6c4dda2be3022ea633d5e807"
)
// WINDIVERT_MTU_MAX from windivert.h.
const MTUMax = 40 + 0xFFFF const MTUMax = 40 + 0xFFFF
type Layer uint32 type Layer uint32
@@ -25,11 +23,7 @@ const LayerNetwork Layer = 0
type Flag uint64 type Flag uint64
const ( const (
// FlagSniff opens a passive observer: the driver copies matching packets FlagSniff Flag = 0x0001
// to userspace without removing them from the network stack. Send is not
// required (and not allowed) on a sniffing handle.
FlagSniff Flag = 0x0001
// FlagSendOnly opens a write-only injection handle; Recv is not allowed.
FlagSendOnly Flag = 0x0008 FlagSendOnly Flag = 0x0008
) )
@@ -38,13 +32,9 @@ const (
PriorityLowest int16 = -30000 PriorityLowest int16 = -30000
) )
// Address mirrors WINDIVERT_ADDRESS from windivert.h (80 bytes, // WINDIVERT_ADDRESS from windivert.h: bits packs Layer:8 | Event:8 | flags |
// little-endian on both amd64 and 386): // Reserved1:8, and the trailing 64 bytes are a union of
// // WINDIVERT_DATA_NETWORK / FLOW / SOCKET / REFLECT.
// 0: INT64 Timestamp
// 8: UINT32 bitfield: Layer:8 | Event:8 | flags | Reserved1:8
// 12: UINT32 Reserved2
// 16: 64 bytes union (WINDIVERT_DATA_NETWORK / FLOW / SOCKET / REFLECT)
type Address struct { type Address struct {
Timestamp int64 Timestamp int64
bits uint32 bits uint32
@@ -54,7 +44,6 @@ type Address struct {
var _ [80]byte = [unsafe.Sizeof(Address{})]byte{} var _ [80]byte = [unsafe.Sizeof(Address{})]byte{}
// Bit positions inside the Address's packed flags word.
const ( const (
addrBitOutbound = 17 addrBitOutbound = 17
addrBitIPv6 = 20 addrBitIPv6 = 20
@@ -73,9 +62,6 @@ func setFlagBit(bits uint32, pos uint, v bool) uint32 {
func (a *Address) IPv6() bool { return getFlagBit(a.bits, addrBitIPv6) } func (a *Address) IPv6() bool { return getFlagBit(a.bits, addrBitIPv6) }
// SetIPv6 declares the address family of a packet built for injection. The
// driver reads it to select the IPv6 network layer; a received address
// already carries it, but a from-scratch injection address must set it.
func (a *Address) SetIPv6(v bool) { func (a *Address) SetIPv6(v bool) {
a.bits = setFlagBit(a.bits, addrBitIPv6, v) a.bits = setFlagBit(a.bits, addrBitIPv6, v)
} }