Add TLS spoof support

This commit is contained in:
世界
2026-04-15 17:59:18 +08:00
parent 6ef38042f3
commit 08ce083b9b
45 changed files with 4227 additions and 6 deletions
+53
View File
@@ -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.
+14
View File
@@ -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" }
+14
View File
@@ -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" }
+7
View File
@@ -0,0 +1,7 @@
//go:build windows && !amd64 && !386
package windivert
func assetFiles() []assetFile { return nil }
func driverSysName() string { return "" }
+212
View File
@@ -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
}
+182
View File
@@ -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
}
+140
View File
@@ -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)
}
}
+320
View File
@@ -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
}
+106
View File
@@ -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())
}
}
+71
View File
@@ -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)
}