325 lines
10 KiB
Go
325 lines
10 KiB
Go
//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")
|
|
}
|
|
const supportedFlags = FlagSniff | FlagSendOnly
|
|
if flags&^supportedFlags != 0 {
|
|
return E.New("windivert: unknown flag bits")
|
|
}
|
|
if flags&FlagSniff != 0 && flags&FlagSendOnly != 0 {
|
|
return E.New("windivert: FlagSniff and FlagSendOnly are mutually exclusive")
|
|
}
|
|
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
|
|
}
|