Add TLS spoof support
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package windivert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAddressSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, uintptr(80), unsafe.Sizeof(Address{}))
|
||||
}
|
||||
|
||||
func TestAddressIPv6(t *testing.T) {
|
||||
t.Parallel()
|
||||
var addr Address
|
||||
require.False(t, addr.IPv6())
|
||||
addr.bits = 1 << addrBitIPv6
|
||||
require.True(t, addr.IPv6())
|
||||
}
|
||||
|
||||
func TestAddressSetIPChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
var addr Address
|
||||
addr.SetIPChecksum(true)
|
||||
require.Equal(t, uint32(1<<addrBitIPChecksum), addr.bits)
|
||||
addr.SetIPChecksum(false)
|
||||
require.Equal(t, uint32(0), addr.bits)
|
||||
}
|
||||
|
||||
func TestAddressSetTCPChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
var addr Address
|
||||
addr.SetTCPChecksum(true)
|
||||
require.Equal(t, uint32(1<<addrBitTCPChecksum), addr.bits)
|
||||
addr.SetTCPChecksum(false)
|
||||
require.Equal(t, uint32(0), addr.bits)
|
||||
}
|
||||
|
||||
// Setters must not disturb sibling bits.
|
||||
func TestAddressFlagBitsIndependent(t *testing.T) {
|
||||
t.Parallel()
|
||||
var addr Address
|
||||
addr.SetIPChecksum(true)
|
||||
addr.SetTCPChecksum(true)
|
||||
addr.bits |= 1 << addrBitIPv6
|
||||
|
||||
addr.SetIPChecksum(false)
|
||||
require.False(t, addr.bits&(1<<addrBitIPChecksum) != 0)
|
||||
require.True(t, addr.bits&(1<<addrBitTCPChecksum) != 0)
|
||||
require.True(t, addr.bits&(1<<addrBitIPv6) != 0)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
//go:build windows && 386
|
||||
|
||||
package windivert
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed assets/WinDivert32.sys
|
||||
var sysBytes []byte
|
||||
|
||||
func assetFiles() []assetFile {
|
||||
return []assetFile{{"WinDivert32.sys", sysBytes}}
|
||||
}
|
||||
|
||||
func driverSysName() string { return "WinDivert32.sys" }
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build windows && amd64
|
||||
|
||||
package windivert
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed assets/WinDivert64.sys
|
||||
var sysBytes []byte
|
||||
|
||||
func assetFiles() []assetFile {
|
||||
return []assetFile{{"WinDivert64.sys", sysBytes}}
|
||||
}
|
||||
|
||||
func driverSysName() string { return "WinDivert64.sys" }
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build windows && !amd64 && !386
|
||||
|
||||
package windivert
|
||||
|
||||
func assetFiles() []assetFile { return nil }
|
||||
|
||||
func driverSysName() string { return "" }
|
||||
@@ -0,0 +1,212 @@
|
||||
//go:build windows
|
||||
|
||||
package windivert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
driverServiceName = "WinDivert"
|
||||
driverDeviceName = `\\.\WinDivert`
|
||||
)
|
||||
|
||||
var (
|
||||
driverOnce sync.Once
|
||||
driverErr error
|
||||
// driverDevName is ASCII-safe and must be available before ensureDriver
|
||||
// so Open can try CreateFile first and only install on FILE_NOT_FOUND.
|
||||
driverDevName, _ = windows.UTF16PtrFromString(driverDeviceName)
|
||||
)
|
||||
|
||||
// Requires SeLoadDriverPrivilege (Administrator). Running the 386 build
|
||||
// under WOW64 on a 64-bit kernel is rejected — use the amd64 build.
|
||||
func ensureDriver() error {
|
||||
driverOnce.Do(func() {
|
||||
driverErr = installDriver()
|
||||
})
|
||||
return driverErr
|
||||
}
|
||||
|
||||
func installDriver() error {
|
||||
if runtime.GOARCH == "386" {
|
||||
var isWow64 bool
|
||||
err := windows.IsWow64Process(windows.CurrentProcess(), &isWow64)
|
||||
if err == nil && isWow64 {
|
||||
return E.New("windivert: 386 build detected running under WOW64 on a 64-bit kernel; use the amd64 build")
|
||||
}
|
||||
}
|
||||
|
||||
dir, err := ensureExtracted()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sysPath := filepath.Join(dir, driverSysName())
|
||||
sysPathW, err := windows.UTF16PtrFromString(sysPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: utf16 driver path")
|
||||
}
|
||||
|
||||
// Serialize driver install across concurrent processes.
|
||||
mutexName, _ := windows.UTF16PtrFromString("WinDivertDriverInstallMutex")
|
||||
mutex, err := windows.CreateMutex(nil, false, mutexName)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: create install mutex")
|
||||
}
|
||||
defer windows.CloseHandle(mutex)
|
||||
_, err = windows.WaitForSingleObject(mutex, windows.INFINITE)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: wait install mutex")
|
||||
}
|
||||
defer windows.ReleaseMutex(mutex)
|
||||
|
||||
manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_ALL_ACCESS)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: open SCM")
|
||||
}
|
||||
defer windows.CloseServiceHandle(manager)
|
||||
|
||||
serviceNameW, _ := windows.UTF16PtrFromString(driverServiceName)
|
||||
service, err := windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS)
|
||||
if err != nil {
|
||||
service, err = windows.CreateService(
|
||||
manager,
|
||||
serviceNameW,
|
||||
serviceNameW,
|
||||
windows.SERVICE_ALL_ACCESS,
|
||||
windows.SERVICE_KERNEL_DRIVER,
|
||||
windows.SERVICE_DEMAND_START,
|
||||
windows.SERVICE_ERROR_NORMAL,
|
||||
sysPathW,
|
||||
nil, nil, nil, nil, nil,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_SERVICE_EXISTS) {
|
||||
service, err = windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS)
|
||||
}
|
||||
if err != nil {
|
||||
return wrapDriverInstallError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
defer windows.CloseServiceHandle(service)
|
||||
|
||||
err = windows.StartService(service, 0, nil)
|
||||
if err != nil && errors.Is(err, windows.ERROR_SERVICE_DISABLED) {
|
||||
// A prior process called DeleteService on a still-running kernel
|
||||
// driver: SCM marks the record for deletion and flips START_TYPE
|
||||
// to DISABLED until the last handle closes. Re-enable so we can
|
||||
// start it instead of waiting for a reboot.
|
||||
err = windows.ChangeServiceConfig(
|
||||
service,
|
||||
windows.SERVICE_NO_CHANGE,
|
||||
windows.SERVICE_DEMAND_START,
|
||||
windows.SERVICE_NO_CHANGE,
|
||||
nil, nil, nil, nil, nil, nil, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: re-enable disabled service")
|
||||
}
|
||||
err = windows.StartService(service, 0, nil)
|
||||
}
|
||||
if err == nil {
|
||||
// Mark for deletion so the driver unregisters when the last handle
|
||||
// closes or on next reboot. Matches the upstream DLL's behavior:
|
||||
// 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)
|
||||
} else if !errors.Is(err, windows.ERROR_SERVICE_ALREADY_RUNNING) {
|
||||
return E.Cause(err, "windivert: start service")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapDriverInstallError(err error) error {
|
||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
return E.Cause(err, "windivert: installing the kernel driver requires Administrator privileges")
|
||||
}
|
||||
return E.Cause(err, "windivert: create service")
|
||||
}
|
||||
|
||||
type assetFile struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
var (
|
||||
extractOnce sync.Once
|
||||
extractErr error
|
||||
extractDir string
|
||||
)
|
||||
|
||||
// The on-disk copy is protected by Windows Authenticode signature
|
||||
// enforcement, which rejects any tampered .sys at StartService time.
|
||||
func ensureExtracted() (string, error) {
|
||||
extractOnce.Do(func() {
|
||||
extractDir, extractErr = extractImpl()
|
||||
})
|
||||
return extractDir, extractErr
|
||||
}
|
||||
|
||||
func extractImpl() (string, error) {
|
||||
files := assetFiles()
|
||||
if len(files) == 0 {
|
||||
return "", E.New("windivert: unsupported architecture ", runtime.GOARCH)
|
||||
}
|
||||
|
||||
base, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return "", E.Cause(err, "windivert: locate user cache dir")
|
||||
}
|
||||
dir := filepath.Join(base, "sing-box", "windivert", "v"+AssetVersion)
|
||||
err = os.MkdirAll(dir, 0o755)
|
||||
if err != nil {
|
||||
return "", E.Cause(err, "windivert: mkdir ", dir)
|
||||
}
|
||||
|
||||
for _, asset := range files {
|
||||
err = ensureAsset(dir, asset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// Concurrent sing-box processes race on os.Rename (atomic on NTFS);
|
||||
// whichever wins creates the final file. Writers that lose the race
|
||||
// silently discard their temp copy.
|
||||
func ensureAsset(dir string, asset assetFile) error {
|
||||
target := filepath.Join(dir, asset.name)
|
||||
_, err := os.Stat(target)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return E.Cause(err, "windivert: stat ", asset.name)
|
||||
}
|
||||
tmp := target + ".tmp-" + strconv.Itoa(os.Getpid())
|
||||
err = os.WriteFile(tmp, asset.data, 0o644)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: write ", asset.name)
|
||||
}
|
||||
err = os.Rename(tmp, target)
|
||||
if err != nil {
|
||||
os.Remove(tmp)
|
||||
if _, statErr := os.Stat(target); statErr == nil {
|
||||
return nil
|
||||
}
|
||||
return E.Cause(err, "windivert: rename ", asset.name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package windivert
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
// WINDIVERT_FILTER VM instruction layout (24 bytes, #pragma pack(1)):
|
||||
//
|
||||
// word 0 (LE): field:11 | test:5 | success:16
|
||||
// word 1 (LE): failure:16 | neg:1 | reserved:15
|
||||
// words 2..5: arg[4] (native-endian uint32 each)
|
||||
//
|
||||
// The driver walks this as a decision tree: evaluate the test at inst i;
|
||||
// on success jump to success; on failure jump to failure. Continuations
|
||||
// 0x7FFE and 0x7FFF are ACCEPT and REJECT terminals.
|
||||
const (
|
||||
filterInstBytes = 24
|
||||
filterMaxInsts = 256
|
||||
|
||||
fieldZero = 0
|
||||
fieldOutbound = 2
|
||||
fieldIP = 5
|
||||
fieldIPv6 = 6
|
||||
fieldTCP = 8
|
||||
fieldIPSrcAddr = 21
|
||||
fieldIPDstAddr = 22
|
||||
fieldIPv6SrcAddr = 28
|
||||
fieldIPv6DstAddr = 29
|
||||
fieldTCPSrcPort = 38
|
||||
fieldTCPDstPort = 39
|
||||
|
||||
testEQ = 0
|
||||
|
||||
resultAccept uint16 = 0x7FFE
|
||||
resultReject uint16 = 0x7FFF
|
||||
)
|
||||
|
||||
// Filter flags passed to IOCTL_WINDIVERT_STARTUP alongside the compiled
|
||||
// filter. These tell the driver what *kinds* of packets the filter might
|
||||
// match, used as a kernel-side fast-reject.
|
||||
const (
|
||||
filterFlagOutbound uint64 = 0x0020
|
||||
filterFlagIP uint64 = 0x0040
|
||||
filterFlagIPv6 uint64 = 0x0080
|
||||
)
|
||||
|
||||
type filterInst struct {
|
||||
field uint16 // 11 bits used
|
||||
test uint8 // 5 bits used
|
||||
success uint16
|
||||
failure uint16
|
||||
neg bool
|
||||
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 {
|
||||
insts []filterInst
|
||||
flags uint64 // filter flags for STARTUP ioctl
|
||||
}
|
||||
|
||||
// reject returns a filter that matches no packet. The empty insts slice
|
||||
// is encoded as a single rejecting instruction by encode().
|
||||
func reject() *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) {
|
||||
if !src.IsValid() || !dst.IsValid() {
|
||||
return nil, E.New("windivert: filter: invalid address port")
|
||||
}
|
||||
if src.Addr().Is4() != dst.Addr().Is4() {
|
||||
return nil, E.New("windivert: filter: mixed IPv4/IPv6")
|
||||
}
|
||||
f := &Filter{
|
||||
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))
|
||||
if src.Addr().Is4() {
|
||||
f.flags |= filterFlagIP
|
||||
f.add(fieldIP, testEQ, argUint32(1))
|
||||
f.add(fieldTCP, testEQ, argUint32(1))
|
||||
f.add(fieldIPSrcAddr, testEQ, argIPv4(src.Addr()))
|
||||
f.add(fieldIPDstAddr, testEQ, argIPv4(dst.Addr()))
|
||||
} else {
|
||||
f.flags |= filterFlagIPv6
|
||||
f.add(fieldIPv6, testEQ, argUint32(1))
|
||||
f.add(fieldTCP, testEQ, argUint32(1))
|
||||
f.add(fieldIPv6SrcAddr, testEQ, argIPv6(src.Addr()))
|
||||
f.add(fieldIPv6DstAddr, testEQ, argIPv6(dst.Addr()))
|
||||
}
|
||||
f.add(fieldTCPSrcPort, testEQ, argUint32(uint32(src.Port())))
|
||||
f.add(fieldTCPDstPort, testEQ, argUint32(uint32(dst.Port())))
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *Filter) add(field uint16, test uint8, arg [4]uint32) {
|
||||
f.insts = append(f.insts, filterInst{field: field, test: test, arg: arg})
|
||||
}
|
||||
|
||||
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
|
||||
// compares against an IPv4-mapped-IPv6 form: {host_order_u32, 0x0000FFFF,
|
||||
// 0, 0} (see sys/windivert.c windivert_get_ipv4_addr and the IPv4_SRCADDR
|
||||
// val-word construction). Omitting the 0x0000FFFF marker causes the EQ
|
||||
// test to fail for every packet.
|
||||
func argIPv4(addr netip.Addr) [4]uint32 {
|
||||
b := addr.As4()
|
||||
return [4]uint32{binary.BigEndian.Uint32(b[:]), 0x0000FFFF, 0, 0}
|
||||
}
|
||||
|
||||
// argIPv6 encodes an IPv6 address for IPV6_SRCADDR/IPV6_DSTADDR. The
|
||||
// driver stores the address as four host-order uint32s in REVERSED word
|
||||
// order: val[0]=low (bytes 12..15), val[3]=high (bytes 0..3). See
|
||||
// sys/windivert.c windivert_outbound_network_v6_classify val-word
|
||||
// construction.
|
||||
func argIPv6(addr netip.Addr) [4]uint32 {
|
||||
b := addr.As16()
|
||||
return [4]uint32{
|
||||
binary.BigEndian.Uint32(b[12:16]),
|
||||
binary.BigEndian.Uint32(b[8:12]),
|
||||
binary.BigEndian.Uint32(b[4:8]),
|
||||
binary.BigEndian.Uint32(b[0:4]),
|
||||
}
|
||||
}
|
||||
|
||||
// encode serializes the Filter to the on-wire WINDIVERT_FILTER[] format
|
||||
// plus the filter_flags for STARTUP ioctl.
|
||||
func (f *Filter) encode() ([]byte, uint64, error) {
|
||||
if len(f.insts) == 0 {
|
||||
// "Reject all" — one instruction, ZERO == 0 is always true, but we
|
||||
// invert by setting both success and failure to REJECT.
|
||||
return encodeInst(filterInst{
|
||||
field: fieldZero,
|
||||
test: testEQ,
|
||||
success: resultReject,
|
||||
failure: resultReject,
|
||||
}), 0, nil
|
||||
}
|
||||
if len(f.insts) > filterMaxInsts-1 {
|
||||
return nil, 0, E.New("windivert: filter too long")
|
||||
}
|
||||
buf := make([]byte, 0, filterInstBytes*len(f.insts))
|
||||
for i, inst := range f.insts {
|
||||
if i == len(f.insts)-1 {
|
||||
inst.success = resultAccept
|
||||
} else {
|
||||
inst.success = uint16(i + 1)
|
||||
}
|
||||
inst.failure = resultReject
|
||||
buf = append(buf, encodeInst(inst)...)
|
||||
}
|
||||
return buf, f.flags, nil
|
||||
}
|
||||
|
||||
func encodeInst(inst filterInst) []byte {
|
||||
out := make([]byte, filterInstBytes)
|
||||
word0 := uint32(inst.field&0x7FF) | uint32(inst.test&0x1F)<<11 |
|
||||
uint32(inst.success)<<16
|
||||
word1 := uint32(inst.failure)
|
||||
if inst.neg {
|
||||
word1 |= 1 << 16
|
||||
}
|
||||
binary.LittleEndian.PutUint32(out[0:4], word0)
|
||||
binary.LittleEndian.PutUint32(out[4:8], word1)
|
||||
binary.LittleEndian.PutUint32(out[8:12], inst.arg[0])
|
||||
binary.LittleEndian.PutUint32(out[12:16], inst.arg[1])
|
||||
binary.LittleEndian.PutUint32(out[16:20], inst.arg[2])
|
||||
binary.LittleEndian.PutUint32(out[20:24], inst.arg[3])
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package windivert
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRejectFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
bin, flags, err := reject().encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(bin) != filterInstBytes {
|
||||
t.Fatalf("reject filter len: got %d, want %d", len(bin), filterInstBytes)
|
||||
}
|
||||
if flags != 0 {
|
||||
t.Fatalf("reject filter flags: got %x, want 0", flags)
|
||||
}
|
||||
// word0: field=ZERO=0, test=EQ=0, success=REJECT=0x7FFF
|
||||
word0 := binary.LittleEndian.Uint32(bin[0:4])
|
||||
if word0 != uint32(resultReject)<<16 {
|
||||
t.Fatalf("reject word0 = %08x", word0)
|
||||
}
|
||||
// word1: failure=REJECT
|
||||
word1 := binary.LittleEndian.Uint32(bin[4:8])
|
||||
if word1 != uint32(resultReject) {
|
||||
t.Fatalf("reject word1 = %08x", word1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundTCPFilterIPv4(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := netip.MustParseAddrPort("10.1.2.3:54321")
|
||||
dst := netip.MustParseAddrPort("1.2.3.4:443")
|
||||
f, err := OutboundTCP(src, dst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin, flags, err := f.encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := filterFlagOutbound | filterFlagIP; flags != want {
|
||||
t.Fatalf("flags: got %x, want %x", flags, want)
|
||||
}
|
||||
// 7 instructions: OUTBOUND, IP, TCP, IP_SRCADDR, IP_DSTADDR, TCP_SRCPORT, TCP_DSTPORT
|
||||
const wantInsts = 7
|
||||
if len(bin) != wantInsts*filterInstBytes {
|
||||
t.Fatalf("instruction count: got %d, want %d", len(bin)/filterInstBytes, wantInsts)
|
||||
}
|
||||
|
||||
// Inst 0: OUTBOUND == 1, success=1, failure=REJECT
|
||||
checkInst(t, bin[0*filterInstBytes:], 0, fieldOutbound, testEQ, 1, resultReject, 1)
|
||||
// Inst 1: IP == 1, success=2
|
||||
checkInst(t, bin[1*filterInstBytes:], 1, fieldIP, testEQ, 2, resultReject, 1)
|
||||
// Inst 2: TCP == 1, success=3
|
||||
checkInst(t, bin[2*filterInstBytes:], 2, fieldTCP, testEQ, 3, resultReject, 1)
|
||||
// Inst 3: IP_SRCADDR == 10.1.2.3 (host-order uint32 = 0x0A010203, arg[1]=0x0000FFFF marker)
|
||||
checkInst(t, bin[3*filterInstBytes:], 3, fieldIPSrcAddr, testEQ, 4, resultReject, 0x0A010203)
|
||||
checkArg1(t, bin[3*filterInstBytes:], 3, 0x0000FFFF)
|
||||
// Inst 4: IP_DSTADDR == 1.2.3.4
|
||||
checkInst(t, bin[4*filterInstBytes:], 4, fieldIPDstAddr, testEQ, 5, resultReject, 0x01020304)
|
||||
checkArg1(t, bin[4*filterInstBytes:], 4, 0x0000FFFF)
|
||||
// Inst 5: TCP_SRCPORT == 54321
|
||||
checkInst(t, bin[5*filterInstBytes:], 5, fieldTCPSrcPort, testEQ, 6, resultReject, 54321)
|
||||
// Last inst 6: TCP_DSTPORT == 443, success=ACCEPT
|
||||
checkInst(t, bin[6*filterInstBytes:], 6, fieldTCPDstPort, testEQ, resultAccept, resultReject, 443)
|
||||
}
|
||||
|
||||
func TestOutboundTCPFilterIPv6(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := netip.MustParseAddrPort("[2001:db8::1]:54321")
|
||||
dst := netip.MustParseAddrPort("[2001:db8::2]:443")
|
||||
f, err := OutboundTCP(src, dst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin, flags, err := f.encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := filterFlagOutbound | filterFlagIPv6; flags != want {
|
||||
t.Fatalf("flags: got %x, want %x", flags, want)
|
||||
}
|
||||
// Inst 3: IPv6_SRCADDR. The driver stores the address in reversed
|
||||
// word order: arg[0]=low (bytes 12..15)=1, arg[3]=high (bytes 0..3)=0x20010db8.
|
||||
off := 3 * filterInstBytes
|
||||
a0 := binary.LittleEndian.Uint32(bin[off+8:])
|
||||
a1 := binary.LittleEndian.Uint32(bin[off+12:])
|
||||
a2 := binary.LittleEndian.Uint32(bin[off+16:])
|
||||
a3 := binary.LittleEndian.Uint32(bin[off+20:])
|
||||
if a0 != 1 || a1 != 0 || a2 != 0 || a3 != 0x20010db8 {
|
||||
t.Fatalf("ipv6 src arg=[%08x %08x %08x %08x], want [1 0 0 0x20010db8]", a0, a1, a2, a3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundTCPFilterMixedFamily(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := netip.MustParseAddrPort("10.0.0.1:1234")
|
||||
dst := netip.MustParseAddrPort("[2001:db8::1]:443")
|
||||
if _, err := OutboundTCP(src, dst); err == nil {
|
||||
t.Fatal("expected error for mixed families")
|
||||
}
|
||||
}
|
||||
|
||||
func checkArg1(t *testing.T, raw []byte, idx int, arg1 uint32) {
|
||||
t.Helper()
|
||||
got := binary.LittleEndian.Uint32(raw[12:16])
|
||||
if got != arg1 {
|
||||
t.Errorf("inst %d arg[1]: got %08x, want %08x", idx, got, arg1)
|
||||
}
|
||||
}
|
||||
|
||||
func checkInst(t *testing.T, raw []byte, idx int, field uint16, test uint8, success, failure uint16, arg0 uint32) {
|
||||
t.Helper()
|
||||
word0 := binary.LittleEndian.Uint32(raw[0:4])
|
||||
word1 := binary.LittleEndian.Uint32(raw[4:8])
|
||||
a0 := binary.LittleEndian.Uint32(raw[8:12])
|
||||
gotField := uint16(word0 & 0x7FF)
|
||||
gotTest := uint8((word0 >> 11) & 0x1F)
|
||||
gotSuccess := uint16(word0 >> 16)
|
||||
gotFailure := uint16(word1 & 0xFFFF)
|
||||
if gotField != field {
|
||||
t.Errorf("inst %d field: got %d, want %d", idx, gotField, field)
|
||||
}
|
||||
if gotTest != test {
|
||||
t.Errorf("inst %d test: got %d, want %d", idx, gotTest, test)
|
||||
}
|
||||
if gotSuccess != success {
|
||||
t.Errorf("inst %d success: got %d, want %d", idx, gotSuccess, success)
|
||||
}
|
||||
if gotFailure != failure {
|
||||
t.Errorf("inst %d failure: got %d, want %d", idx, gotFailure, failure)
|
||||
}
|
||||
if a0 != arg0 {
|
||||
t.Errorf("inst %d arg[0]: got %08x, want %08x", idx, a0, arg0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
//go:build windows
|
||||
|
||||
package windivert
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"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 {
|
||||
device windows.Handle
|
||||
event windows.Handle
|
||||
closing sync.Once
|
||||
closeErr error
|
||||
addr 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) {
|
||||
err := validateOpenArgs(layer, priority, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter == nil {
|
||||
filter = reject()
|
||||
}
|
||||
filterBin, filterFlags, err := filter.encode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
device, err := openDevice()
|
||||
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 = ensureDriver()
|
||||
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")
|
||||
}
|
||||
}
|
||||
event, err := windows.CreateEvent(nil, 1, 0, nil) // manual reset, unsignaled
|
||||
if err != nil {
|
||||
windows.CloseHandle(device)
|
||||
return nil, E.Cause(err, "windivert: create event")
|
||||
}
|
||||
h := &Handle{device: device, event: event}
|
||||
|
||||
err = h.initialize(layer, priority, flags)
|
||||
if err != nil {
|
||||
h.Close()
|
||||
return nil, err
|
||||
}
|
||||
err = h.startup(filterBin, filterFlags)
|
||||
if err != nil {
|
||||
h.Close()
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func openDevice() (windows.Handle, error) {
|
||||
return windows.CreateFile(
|
||||
driverDevName,
|
||||
windows.GENERIC_READ|windows.GENERIC_WRITE,
|
||||
0, nil,
|
||||
windows.OPEN_EXISTING,
|
||||
windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OVERLAPPED,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
func validateOpenArgs(layer Layer, priority int16, flags Flag) error {
|
||||
if layer != LayerNetwork {
|
||||
return E.New("windivert: invalid layer ", uint32(layer))
|
||||
}
|
||||
if priority < PriorityLowest || priority > PriorityHighest {
|
||||
return E.New("windivert: priority out of range")
|
||||
}
|
||||
if flags&^FlagSendOnly != 0 {
|
||||
return E.New("windivert: unknown flag bits")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handle) initialize(layer Layer, priority int16, flags Flag) error {
|
||||
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
|
||||
binary.LittleEndian.PutUint64(outBuf[0:8], magicDLL)
|
||||
binary.LittleEndian.PutUint32(outBuf[8:12], versionMajor)
|
||||
binary.LittleEndian.PutUint32(outBuf[12:16], versionMinor)
|
||||
binary.LittleEndian.PutUint32(outBuf[16:20], uint32(unsafe.Sizeof(uintptr(0))*8))
|
||||
_, err := doIoctl(h.device, ioctlInitialize, in[:], outBuf[:], h.event)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: initialize ioctl")
|
||||
}
|
||||
gotMagic := binary.LittleEndian.Uint64(outBuf[0:8])
|
||||
if gotMagic != magicSYS {
|
||||
return E.New("windivert: driver magic mismatch (got ", gotMagic, ")")
|
||||
}
|
||||
gotMajor := binary.LittleEndian.Uint32(outBuf[8:12])
|
||||
if gotMajor < versionMajor {
|
||||
gotMinor := binary.LittleEndian.Uint32(outBuf[12:16])
|
||||
return E.New("windivert: driver version too old: ", gotMajor, ".", gotMinor)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handle) startup(filterBin []byte, filterFlags uint64) error {
|
||||
in := buildIoctlStartup(filterFlags)
|
||||
_, err := doIoctl(h.device, ioctlStartup, in[:], filterBin, h.event)
|
||||
if err != nil {
|
||||
return E.Cause(err, "windivert: startup ioctl")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the handle is closed mid-Recv the error wraps ERROR_OPERATION_ABORTED.
|
||||
func (h *Handle) Recv(buf []byte) (int, Address, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, Address{}, E.New("windivert: recv: zero-length buffer")
|
||||
}
|
||||
h.addr = Address{}
|
||||
in := buildIoctlRecv(&h.addr)
|
||||
n, err := doIoctl(h.device, ioctlRecv, in[:], buf, h.event)
|
||||
runtime.KeepAlive(h)
|
||||
if err != nil {
|
||||
return 0, Address{}, err
|
||||
}
|
||||
return int(n), h.addr, nil
|
||||
}
|
||||
|
||||
// The address's Outbound flag controls whether the packet is sent toward
|
||||
// the wire (outbound=true) or delivered up the stack (outbound=false).
|
||||
// IfIdx and SubIfIdx can stay zero — the driver uses the routing table
|
||||
// when IfIdx=0.
|
||||
func (h *Handle) Send(packet []byte, addr *Address) (int, error) {
|
||||
if len(packet) == 0 {
|
||||
return 0, E.New("windivert: send: empty packet")
|
||||
}
|
||||
if addr == nil {
|
||||
return 0, E.New("windivert: send: nil address")
|
||||
}
|
||||
h.addr = *addr
|
||||
in := buildIoctlSend(&h.addr)
|
||||
n, err := doIoctl(h.device, ioctlSend, in[:], packet, h.event)
|
||||
runtime.KeepAlive(h)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
// Idempotent. Aborts any in-flight I/O on the handle.
|
||||
func (h *Handle) Close() error {
|
||||
h.closing.Do(func() {
|
||||
var errs []error
|
||||
if h.device != 0 {
|
||||
err := windows.CloseHandle(h.device)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
h.device = 0
|
||||
}
|
||||
if h.event != 0 {
|
||||
err := windows.CloseHandle(h.event)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
h.event = 0
|
||||
}
|
||||
h.closeErr = E.Errors(errs...)
|
||||
})
|
||||
return h.closeErr
|
||||
}
|
||||
|
||||
// IOCTL codes from windivert_device.h. CTL_CODE macro layout:
|
||||
//
|
||||
// (DeviceType << 16) | (Access << 14) | (Function << 2) | Method
|
||||
const (
|
||||
fileDeviceNetwork uint32 = 0x12
|
||||
accessReadWrite uint32 = 3 // FILE_READ_DATA | FILE_WRITE_DATA
|
||||
accessRead uint32 = 1
|
||||
|
||||
methodInDirect uint32 = 1
|
||||
methodOutDirect uint32 = 2
|
||||
)
|
||||
|
||||
func ctlCode(deviceType, access, function, method uint32) uint32 {
|
||||
return (deviceType << 16) | (access << 14) | (function << 2) | method
|
||||
}
|
||||
|
||||
var (
|
||||
ioctlInitialize = ctlCode(fileDeviceNetwork, accessReadWrite, 0x921, methodOutDirect)
|
||||
ioctlStartup = ctlCode(fileDeviceNetwork, accessReadWrite, 0x922, methodInDirect)
|
||||
ioctlRecv = ctlCode(fileDeviceNetwork, accessRead, 0x923, methodOutDirect)
|
||||
ioctlSend = ctlCode(fileDeviceNetwork, accessReadWrite, 0x924, methodInDirect)
|
||||
)
|
||||
|
||||
// Magic numbers exchanged during INITIALIZE. DLL sends magicDLL in the
|
||||
// version struct; driver returns magicSYS on success.
|
||||
const (
|
||||
magicDLL uint64 = 0x4C4C447669645724 // "$WdivDLL" in LE bytes
|
||||
magicSYS uint64 = 0x5359537669645723 // "#WdivSYS" in LE bytes
|
||||
)
|
||||
|
||||
const (
|
||||
versionMajor uint32 = 2
|
||||
versionMinor uint32 = 2
|
||||
)
|
||||
|
||||
// Size of the WINDIVERT_IOCTL union on wire (packed).
|
||||
const ioctlSize = 16
|
||||
|
||||
// Size of WINDIVERT_VERSION on wire (packed). Only the first 20 bytes
|
||||
// carry data; the rest is reserved zero padding.
|
||||
const versionStructSize = 64
|
||||
|
||||
// doIoctl performs a single synchronous (blocking) overlapped
|
||||
// DeviceIoControl. The handle is opened with FILE_FLAG_OVERLAPPED so
|
||||
// DeviceIoControl returns 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).
|
||||
func doIoctl(handle windows.Handle, code uint32, in []byte, out []byte, event windows.Handle) (uint32, error) {
|
||||
var overlapped windows.Overlapped
|
||||
overlapped.HEvent = event
|
||||
_ = windows.ResetEvent(event)
|
||||
|
||||
var inPtr *byte
|
||||
var inLen uint32
|
||||
if len(in) > 0 {
|
||||
inPtr = &in[0]
|
||||
inLen = uint32(len(in))
|
||||
}
|
||||
var outPtr *byte
|
||||
var outLen uint32
|
||||
if len(out) > 0 {
|
||||
outPtr = &out[0]
|
||||
outLen = uint32(len(out))
|
||||
}
|
||||
var returned uint32
|
||||
err := windows.DeviceIoControl(handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped)
|
||||
if err != nil && !errors.Is(err, windows.ERROR_IO_PENDING) {
|
||||
return 0, err
|
||||
}
|
||||
err = windows.GetOverlappedResult(handle, &overlapped, &returned, true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return returned, nil
|
||||
}
|
||||
|
||||
func buildIoctlInitialize(layer Layer, priority int16, flags Flag) [ioctlSize]byte {
|
||||
var buf [ioctlSize]byte
|
||||
binary.LittleEndian.PutUint32(buf[0:4], uint32(layer))
|
||||
// The driver expects priority + WINDIVERT_PRIORITY_HIGHEST (30000) so
|
||||
// the low range maps to non-negative integers.
|
||||
binary.LittleEndian.PutUint32(buf[4:8], uint32(int32(priority)+int32(PriorityHighest)))
|
||||
binary.LittleEndian.PutUint64(buf[8:16], uint64(flags))
|
||||
return buf
|
||||
}
|
||||
|
||||
func buildIoctlStartup(filterFlags uint64) [ioctlSize]byte {
|
||||
var buf [ioctlSize]byte
|
||||
binary.LittleEndian.PutUint64(buf[0:8], filterFlags)
|
||||
return buf
|
||||
}
|
||||
|
||||
// buildIoctlRecv packs a user-space pointer to a WINDIVERT_ADDRESS into
|
||||
// the ioctl struct. The driver dereferences it to write the address for
|
||||
// the received packet. Caller must keep the Address alive via
|
||||
// runtime.KeepAlive.
|
||||
func buildIoctlRecv(addr *Address) [ioctlSize]byte {
|
||||
var buf [ioctlSize]byte
|
||||
binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addr))))
|
||||
binary.LittleEndian.PutUint64(buf[8:16], 0)
|
||||
return buf
|
||||
}
|
||||
|
||||
func buildIoctlSend(addr *Address) [ioctlSize]byte {
|
||||
var buf [ioctlSize]byte
|
||||
binary.LittleEndian.PutUint64(buf[0:8], uint64(uintptr(unsafe.Pointer(addr))))
|
||||
binary.LittleEndian.PutUint64(buf[8:16], uint64(unsafe.Sizeof(Address{})))
|
||||
return buf
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//go:build windows
|
||||
|
||||
package windivert
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// CTL_CODE macro from Windows DDK:
|
||||
//
|
||||
// (DeviceType<<16) | (Access<<14) | (Function<<2) | Method
|
||||
func TestCtlCodeMatchesDDK(t *testing.T) {
|
||||
t.Parallel()
|
||||
// FILE_DEVICE_NETWORK=0x12, FILE_READ_DATA|FILE_WRITE_DATA=3, METHOD_OUT_DIRECT=2
|
||||
require.Equal(t, uint32(0x12E486), ctlCode(0x12, 3, 0x921, 2))
|
||||
// FILE_READ_DATA=1, METHOD_OUT_DIRECT=2
|
||||
require.Equal(t, uint32(0x12648E), ctlCode(0x12, 1, 0x923, 2))
|
||||
}
|
||||
|
||||
// Baked-in against windivert_device.h @ v2.2.2. A mismatch here means the
|
||||
// kernel will reject every ioctl with ERROR_INVALID_FUNCTION.
|
||||
func TestIoctlCodesMatchUpstream(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, uint32(0x12E486), ioctlInitialize)
|
||||
require.Equal(t, uint32(0x12E489), ioctlStartup)
|
||||
require.Equal(t, uint32(0x12648E), ioctlRecv)
|
||||
require.Equal(t, uint32(0x12E491), ioctlSend)
|
||||
}
|
||||
|
||||
func TestBuildIoctlInitialize(t *testing.T) {
|
||||
t.Parallel()
|
||||
buf := buildIoctlInitialize(LayerNetwork, 100, FlagSendOnly)
|
||||
require.Equal(t, uint32(LayerNetwork), binary.LittleEndian.Uint32(buf[0:4]))
|
||||
// Driver expects priority+PriorityHighest(30000) so the range is non-negative.
|
||||
require.Equal(t, uint32(30100), binary.LittleEndian.Uint32(buf[4:8]))
|
||||
require.Equal(t, uint64(FlagSendOnly), binary.LittleEndian.Uint64(buf[8:16]))
|
||||
}
|
||||
|
||||
func TestBuildIoctlInitializePriorityRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
lowest := buildIoctlInitialize(LayerNetwork, PriorityLowest, 0)
|
||||
require.Equal(t, uint32(0), binary.LittleEndian.Uint32(lowest[4:8]))
|
||||
highest := buildIoctlInitialize(LayerNetwork, PriorityHighest, 0)
|
||||
require.Equal(t, uint32(60000), binary.LittleEndian.Uint32(highest[4:8]))
|
||||
zero := buildIoctlInitialize(LayerNetwork, 0, 0)
|
||||
require.Equal(t, uint32(30000), binary.LittleEndian.Uint32(zero[4:8]))
|
||||
}
|
||||
|
||||
func TestBuildIoctlStartup(t *testing.T) {
|
||||
t.Parallel()
|
||||
flags := filterFlagOutbound | filterFlagIP
|
||||
buf := buildIoctlStartup(flags)
|
||||
require.Equal(t, flags, binary.LittleEndian.Uint64(buf[0:8]))
|
||||
// The second quad-word is unused for STARTUP.
|
||||
require.Equal(t, uint64(0), binary.LittleEndian.Uint64(buf[8:16]))
|
||||
}
|
||||
|
||||
func TestBuildIoctlRecvEmbedsAddressPointer(t *testing.T) {
|
||||
t.Parallel()
|
||||
addr := &Address{Timestamp: 0xCAFEBABE}
|
||||
buf := buildIoctlRecv(addr)
|
||||
require.Equal(t, uint64(uintptr(unsafe.Pointer(addr))),
|
||||
binary.LittleEndian.Uint64(buf[0:8]))
|
||||
// RECV does not carry an address length; driver writes full Address back.
|
||||
require.Equal(t, uint64(0), binary.LittleEndian.Uint64(buf[8:16]))
|
||||
}
|
||||
|
||||
func TestBuildIoctlSendEmbedsAddressPointerAndSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
addr := &Address{}
|
||||
buf := buildIoctlSend(addr)
|
||||
require.Equal(t, uint64(uintptr(unsafe.Pointer(addr))),
|
||||
binary.LittleEndian.Uint64(buf[0:8]))
|
||||
require.Equal(t, uint64(unsafe.Sizeof(Address{})),
|
||||
binary.LittleEndian.Uint64(buf[8:16]))
|
||||
require.Equal(t, uint64(80), binary.LittleEndian.Uint64(buf[8:16]))
|
||||
}
|
||||
|
||||
func TestValidateOpenArgsLayer(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0))
|
||||
require.Error(t, validateOpenArgs(Layer(1), 0, 0))
|
||||
require.Error(t, validateOpenArgs(Layer(42), 0, 0))
|
||||
}
|
||||
|
||||
func TestValidateOpenArgsPriorityBounds(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NoError(t, validateOpenArgs(LayerNetwork, PriorityHighest, 0))
|
||||
require.NoError(t, validateOpenArgs(LayerNetwork, PriorityLowest, 0))
|
||||
require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0))
|
||||
require.Error(t, validateOpenArgs(LayerNetwork, PriorityHighest+1, 0))
|
||||
require.Error(t, validateOpenArgs(LayerNetwork, PriorityLowest-1, 0))
|
||||
}
|
||||
|
||||
func TestValidateOpenArgsFlags(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NoError(t, validateOpenArgs(LayerNetwork, 0, 0))
|
||||
require.NoError(t, validateOpenArgs(LayerNetwork, 0, 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, FlagSendOnly|Flag(0x10)))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//go:build windows
|
||||
|
||||
package windivert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func openHandle(t *testing.T, filter *Filter, flags Flag) *Handle {
|
||||
t.Helper()
|
||||
h, err := Open(filter, LayerNetwork, 0, flags)
|
||||
require.NoError(t, err)
|
||||
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) {
|
||||
h := openHandle(t, nil, FlagSendOnly)
|
||||
require.NoError(t, h.Close())
|
||||
}
|
||||
|
||||
// Close is idempotent per the doc contract.
|
||||
func TestIntegrationCloseTwice(t *testing.T) {
|
||||
h := openHandle(t, nil, FlagSendOnly)
|
||||
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) {
|
||||
// A filter no live traffic will match, so Recv blocks indefinitely
|
||||
// until Close aborts the overlapped I/O.
|
||||
filter, err := OutboundTCP(
|
||||
netip.MustParseAddrPort("10.255.255.254:1"),
|
||||
netip.MustParseAddrPort("10.255.255.253:2"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
h := openHandle(t, filter, 0)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
buf := make([]byte, MTUMax)
|
||||
_, _, recvErr := h.Recv(buf)
|
||||
errCh <- recvErr
|
||||
}()
|
||||
|
||||
// Let Recv reach the blocking DeviceIoControl before Close races in.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
require.NoError(t, h.Close())
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, windows.ERROR_OPERATION_ABORTED),
|
||||
"Recv should return ERROR_OPERATION_ABORTED, got %v", err)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("Recv did not unblock within 3s after Close")
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
errCh := make(chan error, 2)
|
||||
handles := make(chan *Handle, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
go func() {
|
||||
h, err := Open(nil, LayerNetwork, 0, FlagSendOnly)
|
||||
handles <- h
|
||||
errCh <- err
|
||||
}()
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
err := <-errCh
|
||||
h := <-handles
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, h.Close())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Package windivert provides a pure-Go binding to the WinDivert kernel
|
||||
// driver on Windows (amd64 and 386). User-mode WinDivert calls are
|
||||
// reimplemented in Go; only the signed kernel driver is embedded as an
|
||||
// asset, since SCM-installed drivers must live on disk and their
|
||||
// Authenticode signature forbids modification.
|
||||
//
|
||||
// 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
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const AssetVersion = "2.2.2"
|
||||
|
||||
// MTUMax is WINDIVERT_MTU_MAX from windivert.h (40 + 0xFFFF). Suitable as
|
||||
// a single-packet receive buffer size.
|
||||
const MTUMax = 40 + 0xFFFF
|
||||
|
||||
type Layer uint32
|
||||
|
||||
const LayerNetwork Layer = 0
|
||||
|
||||
type Flag uint64
|
||||
|
||||
const FlagSendOnly Flag = 0x0008
|
||||
|
||||
const (
|
||||
PriorityHighest int16 = 30000
|
||||
PriorityLowest int16 = -30000
|
||||
)
|
||||
|
||||
// Address mirrors WINDIVERT_ADDRESS from windivert.h (80 bytes,
|
||||
// little-endian on both amd64 and 386):
|
||||
//
|
||||
// 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 {
|
||||
Timestamp int64
|
||||
bits uint32
|
||||
Reserved2 uint32
|
||||
union [64]byte
|
||||
}
|
||||
|
||||
var _ [80]byte = [unsafe.Sizeof(Address{})]byte{}
|
||||
|
||||
// Bit positions inside the Address's packed flags word.
|
||||
const (
|
||||
addrBitIPv6 = 20
|
||||
addrBitIPChecksum = 21
|
||||
addrBitTCPChecksum = 22
|
||||
)
|
||||
|
||||
func getFlagBit(bits uint32, pos uint) bool { return bits&(1<<pos) != 0 }
|
||||
func setFlagBit(bits uint32, pos uint, v bool) uint32 {
|
||||
if v {
|
||||
return bits | (1 << pos)
|
||||
}
|
||||
return bits &^ (1 << pos)
|
||||
}
|
||||
|
||||
func (a *Address) IPv6() bool { return getFlagBit(a.bits, addrBitIPv6) }
|
||||
func (a *Address) SetIPChecksum(v bool) {
|
||||
a.bits = setFlagBit(a.bits, addrBitIPChecksum, v)
|
||||
}
|
||||
|
||||
func (a *Address) SetTCPChecksum(v bool) {
|
||||
a.bits = setFlagBit(a.bits, addrBitTCPChecksum, v)
|
||||
}
|
||||
Reference in New Issue
Block a user