mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Package WinDivert drivers instead of embedding it
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" }
|
|
||||||
|
|||||||
@@ -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" }
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build windows && 386 && !with_external_windivert
|
||||||
|
|
||||||
|
package windivert
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed assets/WinDivert32.sys
|
||||||
|
var sysBytes []byte
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build windows && amd64 && !with_external_windivert
|
||||||
|
|
||||||
|
package windivert
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed assets/WinDivert64.sys
|
||||||
|
var sysBytes []byte
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build windows && !amd64 && !386 && !with_external_windivert
|
||||||
|
|
||||||
|
package windivert
|
||||||
|
|
||||||
|
var sysBytes []byte
|
||||||
+10
-30
@@ -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,
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user