From 8eb4e8acfc2d6744b8d104c62f1d534663ff963d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 3 Mar 2026 20:36:23 +0800 Subject: [PATCH 01/38] Add MAC address include/exclude filtering for nftables auto-redirect Support filtering traffic by source MAC address in the prerouting chain, using ether addr payload matching with set lookups for multiple addresses. --- redirect_nftables_rules.go | 144 +++++++++++++++++++++++++++++++++++++ tun.go | 2 + 2 files changed, 146 insertions(+) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 9f950a38..27765f5b 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -3,6 +3,7 @@ package tun import ( + "net" "net/netip" _ "unsafe" @@ -375,6 +376,149 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft }) } } + if len(r.tunOptions.IncludeMACAddress) > 0 { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFTYPE, Register: 1}, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint16(unix.ARPHRD_ETHER), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + if len(r.tunOptions.IncludeMACAddress) > 1 { + includeMACSet := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeEtherAddr, + } + err := nft.AddSet(includeMACSet, common.Map(r.tunOptions.IncludeMACAddress, func(it net.HardwareAddr) nftables.SetElement { + return nftables.SetElement{ + Key: []byte(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: includeMACSet.ID, + SetName: includeMACSet.Name, + Invert: true, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: []byte(r.tunOptions.IncludeMACAddress[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } + if len(r.tunOptions.ExcludeMACAddress) > 0 { + if len(r.tunOptions.ExcludeMACAddress) > 1 { + excludeMACSet := &nftables.Set{ + Table: table, + Anonymous: true, + Constant: true, + KeyType: nftables.TypeEtherAddr, + } + err := nft.AddSet(excludeMACSet, common.Map(r.tunOptions.ExcludeMACAddress, func(it net.HardwareAddr) nftables.SetElement { + return nftables.SetElement{ + Key: []byte(it), + } + })) + if err != nil { + return err + } + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Lookup{ + SourceRegister: 1, + SetID: excludeMACSet.ID, + SetName: excludeMACSet.Name, + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } else { + nft.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Payload{ + OperationType: expr.PayloadLoad, + DestRegister: 1, + Base: expr.PayloadBaseLLHeader, + Offset: 6, + Len: 6, + }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: []byte(r.tunOptions.ExcludeMACAddress[0]), + }, + &expr.Counter{}, + &expr.Verdict{ + Kind: expr.VerdictReturn, + }, + }, + }) + } + } } else { if len(r.tunOptions.IncludeUID) > 0 { if len(r.tunOptions.IncludeUID) > 1 || r.tunOptions.IncludeUID[0].Start != r.tunOptions.IncludeUID[0].End { diff --git a/tun.go b/tun.go index 35cd0956..5f417bef 100644 --- a/tun.go +++ b/tun.go @@ -102,6 +102,8 @@ type Options struct { IncludeAndroidUser []int IncludePackage []string ExcludePackage []string + IncludeMACAddress []net.HardwareAddr + ExcludeMACAddress []net.HardwareAddr InterfaceFinder control.InterfaceFinder InterfaceMonitor DefaultInterfaceMonitor FileDescriptor int From c406dd189ecbbe2a98518aa8ee846356cc344b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 15:06:55 +0800 Subject: [PATCH 02/38] draft: windows auto redirect --- internal/winredirect/amd64/winredirect.sys | 1 + internal/winredirect/arm/winredirect.sys | 1 + internal/winredirect/arm64/winredirect.sys | 1 + internal/winredirect/driver/winredirect.c | 574 ++++++++++++++++++++ internal/winredirect/driver/winredirect.h | 145 +++++ internal/winredirect/driver/winredirect.inf | 59 ++ internal/winredirect/embed_windows_386.go | 6 + internal/winredirect/embed_windows_amd64.go | 6 + internal/winredirect/embed_windows_arm.go | 6 + internal/winredirect/embed_windows_arm64.go | 6 + internal/winredirect/ioctl_windows.go | 51 ++ internal/winredirect/manager_windows.go | 154 ++++++ internal/winredirect/types_windows.go | 48 ++ internal/winredirect/x86/winredirect.sys | 1 + internal/winsys/constants.go | 21 + redirect_metadata.go | 24 + redirect_server_windows.go | 172 ++++++ redirect_stub.go | 2 +- redirect_windows.go | 401 ++++++++++++++ 19 files changed, 1678 insertions(+), 1 deletion(-) create mode 100644 internal/winredirect/amd64/winredirect.sys create mode 100644 internal/winredirect/arm/winredirect.sys create mode 100644 internal/winredirect/arm64/winredirect.sys create mode 100644 internal/winredirect/driver/winredirect.c create mode 100644 internal/winredirect/driver/winredirect.h create mode 100644 internal/winredirect/driver/winredirect.inf create mode 100644 internal/winredirect/embed_windows_386.go create mode 100644 internal/winredirect/embed_windows_amd64.go create mode 100644 internal/winredirect/embed_windows_arm.go create mode 100644 internal/winredirect/embed_windows_arm64.go create mode 100644 internal/winredirect/ioctl_windows.go create mode 100644 internal/winredirect/manager_windows.go create mode 100644 internal/winredirect/types_windows.go create mode 100644 internal/winredirect/x86/winredirect.sys create mode 100644 redirect_metadata.go create mode 100644 redirect_server_windows.go create mode 100644 redirect_windows.go diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys new file mode 100644 index 00000000..311c8dd0 --- /dev/null +++ b/internal/winredirect/amd64/winredirect.sys @@ -0,0 +1 @@ +PLACEHOLDER \ No newline at end of file diff --git a/internal/winredirect/arm/winredirect.sys b/internal/winredirect/arm/winredirect.sys new file mode 100644 index 00000000..311c8dd0 --- /dev/null +++ b/internal/winredirect/arm/winredirect.sys @@ -0,0 +1 @@ +PLACEHOLDER \ No newline at end of file diff --git a/internal/winredirect/arm64/winredirect.sys b/internal/winredirect/arm64/winredirect.sys new file mode 100644 index 00000000..311c8dd0 --- /dev/null +++ b/internal/winredirect/arm64/winredirect.sys @@ -0,0 +1 @@ +PLACEHOLDER \ No newline at end of file diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c new file mode 100644 index 00000000..be0360a9 --- /dev/null +++ b/internal/winredirect/driver/winredirect.c @@ -0,0 +1,574 @@ +#include "winredirect.h" + +// {7B3A8D2E-4F5C-11EE-BE56-0242AC120002} +DEFINE_GUID(WINREDIRECT_PROVIDER_KEY, + 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x02); + +// {7B3A8D2E-4F5C-11EE-BE56-0242AC120003} +DEFINE_GUID(WINREDIRECT_SUBLAYER_KEY, + 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x03); + +// {7B3A8D2E-4F5C-11EE-BE56-0242AC120004} +DEFINE_GUID(WINREDIRECT_CALLOUT_V4_KEY, + 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x04); + +// {7B3A8D2E-4F5C-11EE-BE56-0242AC120005} +DEFINE_GUID(WINREDIRECT_CALLOUT_V6_KEY, + 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x05); + +static PDRIVER_CONTEXT g_Ctx = NULL; + +NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) +{ + NTSTATUS status; + WDF_DRIVER_CONFIG driverConfig; + WDF_OBJECT_ATTRIBUTES driverAttrs; + WDFDRIVER driver; + WDFDEVICE device; + PWDFDEVICE_INIT deviceInit; + WDF_OBJECT_ATTRIBUTES deviceAttrs; + UNICODE_STRING deviceName = RTL_CONSTANT_STRING(DEVICE_NAME); + UNICODE_STRING symlinkName = RTL_CONSTANT_STRING(SYMLINK_NAME); + PDRIVER_CONTEXT ctx; + + WDF_DRIVER_CONFIG_INIT(&driverConfig, WDF_NO_EVENT_CALLBACK); + driverConfig.DriverInitFlags = WdfDriverInitNonPnpDriver; + driverConfig.EvtDriverUnload = EvtDriverUnload; + + WDF_OBJECT_ATTRIBUTES_INIT(&driverAttrs); + status = WdfDriverCreate(DriverObject, RegistryPath, &driverAttrs, &driverConfig, &driver); + if (!NT_SUCCESS(status)) return status; + + deviceInit = WdfControlDeviceInitAllocate(driver, &SDDL_DEVOBJ_SYS_ALL_ADM_ALL); + if (!deviceInit) return STATUS_INSUFFICIENT_RESOURCES; + + WdfDeviceInitSetDeviceType(deviceInit, FILE_DEVICE_NETWORK); + WdfDeviceInitSetCharacteristics(deviceInit, FILE_DEVICE_SECURE_OPEN, FALSE); + + status = WdfDeviceInitAssignName(deviceInit, &deviceName); + if (!NT_SUCCESS(status)) { WdfDeviceInitFree(deviceInit); return status; } + + WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttrs, DRIVER_CONTEXT); + status = WdfDeviceCreate(&deviceInit, &deviceAttrs, &device); + if (!NT_SUCCESS(status)) return status; + + status = WdfDeviceCreateSymbolicLink(device, &symlinkName); + if (!NT_SUCCESS(status)) return status; + + ctx = GetDriverContext(device); + RtlZeroMemory(ctx, sizeof(DRIVER_CONTEXT)); + ctx->Device = device; + InitializeListHead(&ctx->PendingList); + KeInitializeSpinLock(&ctx->PendingLock); + g_Ctx = ctx; + + // Create manual-dispatch queue for pending IOCTLs + WDF_IO_QUEUE_CONFIG queueConfig; + WDF_IO_QUEUE_CONFIG_INIT(&queueConfig, WdfIoQueueDispatchManual); + queueConfig.EvtIoCanceledOnQueue = EvtIoCanceledOnQueue; + status = WdfIoQueueCreate(device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, &ctx->PendingIoctlQueue); + if (!NT_SUCCESS(status)) return status; + + // Create default queue for all other IOCTLs + WDF_IO_QUEUE_CONFIG defaultQueueConfig; + WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&defaultQueueConfig, WdfIoQueueDispatchParallel); + defaultQueueConfig.EvtIoDeviceControl = EvtIoDeviceControl; + status = WdfIoQueueCreate(device, &defaultQueueConfig, WDF_NO_OBJECT_ATTRIBUTES, NULL); + if (!NT_SUCCESS(status)) return status; + + // Create timeout timer (sweeps stale pending entries every 5 seconds) + WDF_TIMER_CONFIG timerConfig; + WDF_TIMER_CONFIG_INIT_PERIODIC(&timerConfig, EvtTimeoutTimer, 5000); + WDF_OBJECT_ATTRIBUTES timerAttrs; + WDF_OBJECT_ATTRIBUTES_INIT(&timerAttrs); + timerAttrs.ParentObject = device; + status = WdfTimerCreate(&timerConfig, &timerAttrs, &ctx->TimeoutTimer); + if (!NT_SUCCESS(status)) return status; + + WdfControlFinishInitializing(device); + return STATUS_SUCCESS; +} + +void EvtDriverUnload(_In_ WDFDRIVER Driver) +{ + UNREFERENCED_PARAMETER(Driver); + if (g_Ctx) { + WfpCleanup(g_Ctx); + PendingFlushAll(g_Ctx); + } +} + +void EvtIoDeviceControl( + _In_ WDFQUEUE Queue, + _In_ WDFREQUEST Request, + _In_ size_t OutputBufferLength, + _In_ size_t InputBufferLength, + _In_ ULONG IoControlCode) +{ + UNREFERENCED_PARAMETER(Queue); + PDRIVER_CONTEXT ctx = g_Ctx; + NTSTATUS status = STATUS_SUCCESS; + PVOID inBuf = NULL, outBuf = NULL; + size_t inLen = 0, outLen = 0; + + switch (IoControlCode) { + case IOCTL_WINREDIRECT_SET_CONFIG: + status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_CONFIG), &inBuf, &inLen); + if (NT_SUCCESS(status)) { + RtlCopyMemory(&ctx->Config, inBuf, sizeof(WINREDIRECT_CONFIG)); + } + WdfRequestComplete(Request, status); + break; + + case IOCTL_WINREDIRECT_START: + if (ctx->Running) { + WdfRequestComplete(Request, STATUS_ALREADY_REGISTERED); + break; + } + status = WfpSetup(ctx); + if (NT_SUCCESS(status)) { + ctx->Running = TRUE; + WdfTimerStart(ctx->TimeoutTimer, WDF_REL_TIMEOUT_IN_SEC(5)); + } + WdfRequestComplete(Request, status); + break; + + case IOCTL_WINREDIRECT_STOP: + if (ctx->Running) { + WdfTimerStop(ctx->TimeoutTimer, FALSE); + WfpCleanup(ctx); + PendingFlushAll(ctx); + ctx->Running = FALSE; + } + WdfRequestComplete(Request, STATUS_SUCCESS); + break; + + case IOCTL_WINREDIRECT_GET_PENDING: + // Forward to manual queue — will be completed when a connection arrives + status = WdfRequestForwardToIoQueue(Request, ctx->PendingIoctlQueue); + if (!NT_SUCCESS(status)) { + WdfRequestComplete(Request, status); + } + break; + + case IOCTL_WINREDIRECT_SET_VERDICT: { + status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_VERDICT), &inBuf, &inLen); + if (!NT_SUCCESS(status)) { + WdfRequestComplete(Request, status); + break; + } + WINREDIRECT_VERDICT* v = (WINREDIRECT_VERDICT*)inBuf; + PPENDING_ENTRY entry = PendingFindByID(ctx, v->ConnID); + if (entry) { + ExecuteVerdict(ctx, entry, v->Verdict); + ExFreePoolWithTag(entry, 'rniW'); + } + WdfRequestComplete(Request, entry ? STATUS_SUCCESS : STATUS_NOT_FOUND); + break; + } + + default: + WdfRequestComplete(Request, STATUS_INVALID_DEVICE_REQUEST); + break; + } +} + +void EvtIoCanceledOnQueue(_In_ WDFQUEUE Queue, _In_ WDFREQUEST Request) +{ + UNREFERENCED_PARAMETER(Queue); + WdfRequestComplete(Request, STATUS_CANCELLED); +} + +void EvtTimeoutTimer(_In_ WDFTIMER Timer) +{ + UNREFERENCED_PARAMETER(Timer); + if (!g_Ctx) return; + + LARGE_INTEGER now; + KeQuerySystemTime(&now); + KIRQL irql; + KeAcquireSpinLock(&g_Ctx->PendingLock, &irql); + + PLIST_ENTRY entry = g_Ctx->PendingList.Flink; + while (entry != &g_Ctx->PendingList) { + PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + entry = entry->Flink; + + // Auto-bypass entries older than 5 seconds + LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; // to seconds + if (elapsed >= 5) { + RemoveEntryList(&pending->ListEntry); + KeReleaseSpinLock(&g_Ctx->PendingLock, irql); + ExecuteVerdict(g_Ctx, pending, VERDICT_BYPASS); + ExFreePoolWithTag(pending, 'rniW'); + KeAcquireSpinLock(&g_Ctx->PendingLock, &irql); + entry = g_Ctx->PendingList.Flink; + } + } + + KeReleaseSpinLock(&g_Ctx->PendingLock, irql); +} + +// --- WFP Setup --- + +NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx) +{ + NTSTATUS status; + FWPM_SESSION0 session = { .flags = FWPM_SESSION_FLAG_DYNAMIC }; + + status = FwpmEngineOpen0(NULL, RPC_C_AUTHN_DEFAULT, NULL, &session, &Ctx->EngineHandle); + if (!NT_SUCCESS(status)) return status; + + FWPM_SUBLAYER0 subLayer = { + .subLayerKey = WINREDIRECT_SUBLAYER_KEY, + .displayData = { .name = L"WinRedirect SubLayer" }, + .weight = MAXUINT16, + }; + status = FwpmSubLayerAdd0(Ctx->EngineHandle, &subLayer, NULL); + if (!NT_SUCCESS(status)) goto cleanup; + + // Create redirect handle + status = FwpsRedirectHandleCreate0(&WINREDIRECT_PROVIDER_KEY, 0, &Ctx->RedirectHandle); + if (!NT_SUCCESS(status)) goto cleanup; + + // Register callouts + FWPS_CALLOUT3 sCalloutV4 = { + .calloutKey = WINREDIRECT_CALLOUT_V4_KEY, + .classifyFn = ClassifyFnV4, + .notifyFn = NotifyFn, + }; + status = FwpsCalloutRegister3(WdfDeviceWdmGetDeviceObject(Ctx->Device), &sCalloutV4, &Ctx->CalloutIdV4); + if (!NT_SUCCESS(status)) goto cleanup; + + FWPS_CALLOUT3 sCalloutV6 = { + .calloutKey = WINREDIRECT_CALLOUT_V6_KEY, + .classifyFn = ClassifyFnV6, + .notifyFn = NotifyFn, + }; + status = FwpsCalloutRegister3(WdfDeviceWdmGetDeviceObject(Ctx->Device), &sCalloutV6, &Ctx->CalloutIdV6); + if (!NT_SUCCESS(status)) goto cleanup; + + // Add callouts to BFE + FWPM_CALLOUT0 mCalloutV4 = { + .calloutKey = WINREDIRECT_CALLOUT_V4_KEY, + .displayData = { .name = L"WinRedirect V4 Callout" }, + .applicableLayer = FWPM_LAYER_ALE_CONNECT_REDIRECT_V4, + }; + status = FwpmCalloutAdd0(Ctx->EngineHandle, &mCalloutV4, NULL, NULL); + if (!NT_SUCCESS(status)) goto cleanup; + + FWPM_CALLOUT0 mCalloutV6 = { + .calloutKey = WINREDIRECT_CALLOUT_V6_KEY, + .displayData = { .name = L"WinRedirect V6 Callout" }, + .applicableLayer = FWPM_LAYER_ALE_CONNECT_REDIRECT_V6, + }; + status = FwpmCalloutAdd0(Ctx->EngineHandle, &mCalloutV6, NULL, NULL); + if (!NT_SUCCESS(status)) goto cleanup; + + // Add filters — condition: TCP only + FWPM_FILTER_CONDITION0 tcpCondition = { + .fieldKey = FWPM_CONDITION_IP_PROTOCOL, + .matchType = FWP_MATCH_EQUAL, + .conditionValue = { .type = FWP_UINT8, .uint8 = IPPROTO_TCP }, + }; + + FWPM_FILTER0 filterV4 = { + .displayData = { .name = L"WinRedirect V4 Filter" }, + .layerKey = FWPM_LAYER_ALE_CONNECT_REDIRECT_V4, + .subLayerKey = WINREDIRECT_SUBLAYER_KEY, + .action = { .type = FWP_ACTION_CALLOUT_TERMINATING, .calloutKey = WINREDIRECT_CALLOUT_V4_KEY }, + .weight = { .type = FWP_UINT8, .uint8 = 15 }, + .numFilterConditions = 1, + .filterCondition = &tcpCondition, + }; + status = FwpmFilterAdd0(Ctx->EngineHandle, &filterV4, NULL, &Ctx->FilterIdV4); + if (!NT_SUCCESS(status)) goto cleanup; + + FWPM_FILTER0 filterV6 = { + .displayData = { .name = L"WinRedirect V6 Filter" }, + .layerKey = FWPM_LAYER_ALE_CONNECT_REDIRECT_V6, + .subLayerKey = WINREDIRECT_SUBLAYER_KEY, + .action = { .type = FWP_ACTION_CALLOUT_TERMINATING, .calloutKey = WINREDIRECT_CALLOUT_V6_KEY }, + .weight = { .type = FWP_UINT8, .uint8 = 15 }, + .numFilterConditions = 1, + .filterCondition = &tcpCondition, + }; + status = FwpmFilterAdd0(Ctx->EngineHandle, &filterV6, NULL, &Ctx->FilterIdV6); + if (!NT_SUCCESS(status)) goto cleanup; + + return STATUS_SUCCESS; + +cleanup: + WfpCleanup(Ctx); + return status; +} + +void WfpCleanup(_In_ PDRIVER_CONTEXT Ctx) +{ + if (Ctx->RedirectHandle) { + FwpsRedirectHandleDestroy0(Ctx->RedirectHandle); + Ctx->RedirectHandle = NULL; + } + if (Ctx->CalloutIdV4) { + FwpsCalloutUnregisterById0(Ctx->CalloutIdV4); + Ctx->CalloutIdV4 = 0; + } + if (Ctx->CalloutIdV6) { + FwpsCalloutUnregisterById0(Ctx->CalloutIdV6); + Ctx->CalloutIdV6 = 0; + } + if (Ctx->EngineHandle) { + FwpmEngineClose0(Ctx->EngineHandle); + Ctx->EngineHandle = NULL; + } +} + +NTSTATUS NTAPI NotifyFn( + _In_ FWPS_CALLOUT_NOTIFY_TYPE notifyType, + _In_ const GUID* filterKey, + _Inout_ FWPS_FILTER3* filter) +{ + UNREFERENCED_PARAMETER(notifyType); + UNREFERENCED_PARAMETER(filterKey); + UNREFERENCED_PARAMETER(filter); + return STATUS_SUCCESS; +} + +// --- Classify callbacks --- + +static void ClassifyFnCommon( + _In_ UINT8 addressFamily, + _In_ const FWPS_INCOMING_VALUES0* inFixedValues, + _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, + _Inout_opt_ void* layerData, + _In_opt_ const void* classifyContext, + _In_ const FWPS_FILTER3* filter, + _In_ UINT64 flowContext, + _Inout_ FWPS_CLASSIFY_OUT0* classifyOut, + _In_ UINT32 localAddrIdx, + _In_ UINT32 localPortIdx, + _In_ UINT32 remoteAddrIdx, + _In_ UINT32 remotePortIdx) +{ + PDRIVER_CONTEXT ctx = g_Ctx; + if (!ctx || !ctx->Running) { + classifyOut->actionType = FWP_ACTION_PERMIT; + return; + } + + // Allocate pending entry + PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(PENDING_ENTRY), 'rniW'); + if (!entry) { + classifyOut->actionType = FWP_ACTION_PERMIT; + return; + } + + RtlZeroMemory(entry, sizeof(PENDING_ENTRY)); + entry->ConnID = InterlockedIncrement64(&ctx->NextConnID); + entry->AddressFamily = addressFamily; + entry->FilterId = filter->filterId; + + // Extract addresses + if (addressFamily == AF_INET) { + UINT32 srcIp = inFixedValues->incomingValue[localAddrIdx].value.uint32; + UINT32 dstIp = inFixedValues->incomingValue[remoteAddrIdx].value.uint32; + // WFP stores IPv4 in host byte order + *(UINT32*)entry->SrcAddr = RtlUlongByteSwap(srcIp); + *(UINT32*)entry->DstAddr = RtlUlongByteSwap(dstIp); + } else { + RtlCopyMemory(entry->SrcAddr, + inFixedValues->incomingValue[localAddrIdx].value.byteArray16->byteArray16, 16); + RtlCopyMemory(entry->DstAddr, + inFixedValues->incomingValue[remoteAddrIdx].value.byteArray16->byteArray16, 16); + } + entry->SrcPort = inFixedValues->incomingValue[localPortIdx].value.uint16; + entry->DstPort = inFixedValues->incomingValue[remotePortIdx].value.uint16; + + // Extract PID from metadata + if (FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_PROCESS_ID)) { + entry->ProcessID = (UINT32)inMetaValues->processId; + } + + // Pend the classify + UINT64 classifyHandle; + NTSTATUS status = FwpsAcquireClassifyHandle0(classifyOut, 0, &classifyHandle); + if (!NT_SUCCESS(status)) { + ExFreePoolWithTag(entry, 'rniW'); + classifyOut->actionType = FWP_ACTION_PERMIT; + return; + } + + entry->ClassifyHandle = classifyHandle; + + status = FwpsPendClassify0( + inFixedValues, inMetaValues, layerData, + classifyContext, filter, flowContext, classifyOut); + if (!NT_SUCCESS(status)) { + FwpsReleaseClassifyHandle0(classifyHandle); + ExFreePoolWithTag(entry, 'rniW'); + classifyOut->actionType = FWP_ACTION_PERMIT; + return; + } + + KeQuerySystemTime(&entry->Timestamp); + PendingInsert(ctx, entry); + + // Try to complete a waiting IOCTL_GET_PENDING + WDFREQUEST request; + status = WdfIoQueueRetrieveNextRequest(ctx->PendingIoctlQueue, &request); + if (NT_SUCCESS(status)) { + PVOID outBuf; + status = WdfRequestRetrieveOutputBuffer(request, sizeof(WINREDIRECT_PENDING_CONN), &outBuf, NULL); + if (NT_SUCCESS(status)) { + WINREDIRECT_PENDING_CONN* out = (WINREDIRECT_PENDING_CONN*)outBuf; + RtlZeroMemory(out, sizeof(*out)); + out->ConnID = entry->ConnID; + out->AddressFamily = entry->AddressFamily; + RtlCopyMemory(out->SrcAddr, entry->SrcAddr, 16); + out->SrcPort = entry->SrcPort; + RtlCopyMemory(out->DstAddr, entry->DstAddr, 16); + out->DstPort = entry->DstPort; + out->ProcessID = entry->ProcessID; + WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, sizeof(WINREDIRECT_PENDING_CONN)); + } else { + WdfRequestComplete(request, status); + } + } +} + +void NTAPI ClassifyFnV4( + _In_ const FWPS_INCOMING_VALUES0* inFixedValues, + _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, + _Inout_opt_ void* layerData, + _In_opt_ const void* classifyContext, + _In_ const FWPS_FILTER3* filter, + _In_ UINT64 flowContext, + _Inout_ FWPS_CLASSIFY_OUT0* classifyOut) +{ + ClassifyFnCommon(AF_INET, inFixedValues, inMetaValues, layerData, + classifyContext, filter, flowContext, classifyOut, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V4_IP_LOCAL_ADDRESS, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V4_IP_LOCAL_PORT, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V4_IP_REMOTE_ADDRESS, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V4_IP_REMOTE_PORT); +} + +void NTAPI ClassifyFnV6( + _In_ const FWPS_INCOMING_VALUES0* inFixedValues, + _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, + _Inout_opt_ void* layerData, + _In_opt_ const void* classifyContext, + _In_ const FWPS_FILTER3* filter, + _In_ UINT64 flowContext, + _Inout_ FWPS_CLASSIFY_OUT0* classifyOut) +{ + ClassifyFnCommon(AF_INET6, inFixedValues, inMetaValues, layerData, + classifyContext, filter, flowContext, classifyOut, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V6_IP_LOCAL_ADDRESS, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V6_IP_LOCAL_PORT, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V6_IP_REMOTE_ADDRESS, + FWPS_FIELD_ALE_CONNECT_REDIRECT_V6_IP_REMOTE_PORT); +} + +// --- Pending connection management --- + +PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx) +{ + UNREFERENCED_PARAMETER(Ctx); + return (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(PENDING_ENTRY), 'rniW'); +} + +void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) +{ + KIRQL irql; + KeAcquireSpinLock(&Ctx->PendingLock, &irql); + InsertTailList(&Ctx->PendingList, &Entry->ListEntry); + KeReleaseSpinLock(&Ctx->PendingLock, irql); +} + +PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID) +{ + KIRQL irql; + PPENDING_ENTRY found = NULL; + KeAcquireSpinLock(&Ctx->PendingLock, &irql); + PLIST_ENTRY entry = Ctx->PendingList.Flink; + while (entry != &Ctx->PendingList) { + PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + if (pending->ConnID == ConnID) { + RemoveEntryList(entry); + found = pending; + break; + } + entry = entry->Flink; + } + KeReleaseSpinLock(&Ctx->PendingLock, irql); + return found; +} + +void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) +{ + KIRQL irql; + KeAcquireSpinLock(&Ctx->PendingLock, &irql); + RemoveEntryList(&Entry->ListEntry); + KeReleaseSpinLock(&Ctx->PendingLock, irql); +} + +void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx) +{ + KIRQL irql; + KeAcquireSpinLock(&Ctx->PendingLock, &irql); + while (!IsListEmpty(&Ctx->PendingList)) { + PLIST_ENTRY entry = RemoveHeadList(&Ctx->PendingList); + PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + KeReleaseSpinLock(&Ctx->PendingLock, irql); + ExecuteVerdict(Ctx, pending, VERDICT_BYPASS); + ExFreePoolWithTag(pending, 'rniW'); + KeAcquireSpinLock(&Ctx->PendingLock, &irql); + } + KeReleaseSpinLock(&Ctx->PendingLock, irql); +} + +// --- Verdict execution --- + +void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UINT32 Verdict) +{ + FWPS_CLASSIFY_OUT0 classifyOut = { 0 }; + + if (Verdict == VERDICT_REDIRECT) { + FWPS_CONNECT_REQUEST0* connReq = NULL; + NTSTATUS status = FwpsAcquireWritableLayerDataPointer0( + Entry->ClassifyHandle, Entry->FilterId, 0, + (PVOID*)&connReq, &classifyOut); + + if (NT_SUCCESS(status) && connReq) { + if (Entry->AddressFamily == AF_INET) { + SOCKADDR_IN* addr = (SOCKADDR_IN*)&connReq->remoteAddressAndPort; + addr->sin_family = AF_INET; + addr->sin_addr.s_addr = RtlUlongByteSwap(0x7F000001); // 127.0.0.1 + addr->sin_port = RtlUshortByteSwap(Ctx->Config.RedirectPort); + } else { + SOCKADDR_IN6* addr = (SOCKADDR_IN6*)&connReq->remoteAddressAndPort; + RtlZeroMemory(addr, sizeof(SOCKADDR_IN6)); + addr->sin6_family = AF_INET6; + addr->sin6_addr.u.Byte[15] = 1; // ::1 + addr->sin6_port = RtlUshortByteSwap(Ctx->Config.RedirectPort); + } + connReq->localRedirectTargetPID = Ctx->Config.ProxyPID; + connReq->localRedirectHandle = Ctx->RedirectHandle; + + FwpsApplyModifiedLayerData0(Entry->ClassifyHandle, connReq, 0); + } + + classifyOut.actionType = FWP_ACTION_PERMIT; + classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; + } else if (Verdict == VERDICT_BYPASS) { + classifyOut.actionType = FWP_ACTION_PERMIT; + classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; + } else { // VERDICT_DROP + classifyOut.actionType = FWP_ACTION_BLOCK; + classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; + } + + FwpsCompleteClassify0(Entry->ClassifyHandle, 0, &classifyOut); + FwpsReleaseClassifyHandle0(Entry->ClassifyHandle); +} diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h new file mode 100644 index 00000000..64acddaf --- /dev/null +++ b/internal/winredirect/driver/winredirect.h @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Device names +#define DEVICE_NAME L"\\Device\\WinRedirect" +#define SYMLINK_NAME L"\\DosDevices\\WinRedirect" + +// IOCTL codes — must match Go types_windows.go +#define IOCTL_WINREDIRECT_SET_CONFIG CTL_CODE(FILE_DEVICE_NETWORK, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_WINREDIRECT_START CTL_CODE(FILE_DEVICE_NETWORK, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_WINREDIRECT_STOP CTL_CODE(FILE_DEVICE_NETWORK, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_WINREDIRECT_GET_PENDING CTL_CODE(FILE_DEVICE_NETWORK, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_WINREDIRECT_SET_VERDICT CTL_CODE(FILE_DEVICE_NETWORK, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS) + +// Verdict values +#define VERDICT_REDIRECT 0 +#define VERDICT_BYPASS 1 +#define VERDICT_DROP 2 + +// Shared structures — must match Go types_windows.go layout + +#pragma pack(push, 1) + +typedef struct _WINREDIRECT_CONFIG { + UINT16 RedirectPort; + UINT8 _pad0[2]; + UINT32 ProxyPID; +} WINREDIRECT_CONFIG; + +typedef struct _WINREDIRECT_PENDING_CONN { + UINT64 ConnID; + UINT8 AddressFamily; + UINT8 _pad0[3]; + UINT8 SrcAddr[16]; + UINT16 SrcPort; + UINT8 _pad1[2]; + UINT8 DstAddr[16]; + UINT16 DstPort; + UINT8 _pad2[2]; + UINT32 ProcessID; +} WINREDIRECT_PENDING_CONN; + +typedef struct _WINREDIRECT_VERDICT { + UINT64 ConnID; + UINT32 Verdict; + UINT8 _pad0[4]; +} WINREDIRECT_VERDICT; + +#pragma pack(pop) + +// Internal pending connection entry +typedef struct _PENDING_ENTRY { + LIST_ENTRY ListEntry; + UINT64 ConnID; + UINT64 ClassifyHandle; + UINT64 FilterId; + UINT8 AddressFamily; + UINT8 SrcAddr[16]; + UINT16 SrcPort; + UINT8 DstAddr[16]; + UINT16 DstPort; + UINT32 ProcessID; + LARGE_INTEGER Timestamp; +} PENDING_ENTRY, *PPENDING_ENTRY; + +// Global driver context +typedef struct _DRIVER_CONTEXT { + WDFDEVICE Device; + WDFQUEUE PendingIoctlQueue; + + // WFP handles + HANDLE EngineHandle; + UINT32 CalloutIdV4; + UINT32 CalloutIdV6; + UINT64 FilterIdV4; + UINT64 FilterIdV6; + HANDLE RedirectHandle; + + // Configuration + WINREDIRECT_CONFIG Config; + BOOLEAN Running; + + // Pending connections + LIST_ENTRY PendingList; + KSPIN_LOCK PendingLock; + volatile LONG64 NextConnID; + + // Timeout timer + WDFTIMER TimeoutTimer; +} DRIVER_CONTEXT, *PDRIVER_CONTEXT; + +WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DRIVER_CONTEXT, GetDriverContext) + +// Function declarations +DRIVER_INITIALIZE DriverEntry; +EVT_WDF_DRIVER_UNLOAD EvtDriverUnload; +EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL EvtIoDeviceControl; +EVT_WDF_IO_QUEUE_IO_CANCELED_ON_QUEUE EvtIoCanceledOnQueue; +EVT_WDF_TIMER EvtTimeoutTimer; + +// WFP functions +NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx); +void WfpCleanup(_In_ PDRIVER_CONTEXT Ctx); + +// Classify callbacks +void NTAPI ClassifyFnV4( + _In_ const FWPS_INCOMING_VALUES0* inFixedValues, + _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, + _Inout_opt_ void* layerData, + _In_opt_ const void* classifyContext, + _In_ const FWPS_FILTER3* filter, + _In_ UINT64 flowContext, + _Inout_ FWPS_CLASSIFY_OUT0* classifyOut +); + +void NTAPI ClassifyFnV6( + _In_ const FWPS_INCOMING_VALUES0* inFixedValues, + _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, + _Inout_opt_ void* layerData, + _In_opt_ const void* classifyContext, + _In_ const FWPS_FILTER3* filter, + _In_ UINT64 flowContext, + _Inout_ FWPS_CLASSIFY_OUT0* classifyOut +); + +NTSTATUS NTAPI NotifyFn( + _In_ FWPS_CALLOUT_NOTIFY_TYPE notifyType, + _In_ const GUID* filterKey, + _Inout_ FWPS_FILTER3* filter +); + +// Pending management +PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx); +void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry); +PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID); +void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry); +void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx); + +// Verdict execution +void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UINT32 Verdict); diff --git a/internal/winredirect/driver/winredirect.inf b/internal/winredirect/driver/winredirect.inf new file mode 100644 index 00000000..4b671a0b --- /dev/null +++ b/internal/winredirect/driver/winredirect.inf @@ -0,0 +1,59 @@ +[Version] +Signature = "$WINDOWS NT$" +Class = WFPCALLOUTDRIVER +ClassGuid = {57465043-616C-6C6F-7574-5F636C617373} +Provider = %ProviderString% +CatalogFile = winredirect.cat +DriverVer = +PnpLockdown = 1 + +[DestinationDirs] +DefaultDestDir = 13 +WinRedirect.Files = 13 + +[DefaultInstall.NTamd64] +CopyFiles = WinRedirect.Files + +[DefaultInstall.NTarm64] +CopyFiles = WinRedirect.Files + +[DefaultInstall.NTarm] +CopyFiles = WinRedirect.Files + +[DefaultInstall.NTx86] +CopyFiles = WinRedirect.Files + +[DefaultInstall.NTamd64.Services] +AddService = WinRedirect,,WinRedirect.Service + +[DefaultInstall.NTarm64.Services] +AddService = WinRedirect,,WinRedirect.Service + +[DefaultInstall.NTarm.Services] +AddService = WinRedirect,,WinRedirect.Service + +[DefaultInstall.NTx86.Services] +AddService = WinRedirect,,WinRedirect.Service + +[WinRedirect.Service] +DisplayName = %ServiceName% +Description = %ServiceDesc% +ServiceBinary = %13%\winredirect.sys +ServiceType = 1 ; SERVICE_KERNEL_DRIVER +StartType = 3 ; SERVICE_DEMAND_START +ErrorControl = 1 ; SERVICE_ERROR_NORMAL + +[WinRedirect.Files] +winredirect.sys + +[SourceDisksNames] +1 = %DiskName% + +[SourceDisksFiles] +winredirect.sys = 1 + +[Strings] +ProviderString = "sing-tun" +ServiceName = "WinRedirect" +ServiceDesc = "WFP TCP Connection Redirect Driver" +DiskName = "WinRedirect Installation Disk" diff --git a/internal/winredirect/embed_windows_386.go b/internal/winredirect/embed_windows_386.go new file mode 100644 index 00000000..e941d3b5 --- /dev/null +++ b/internal/winredirect/embed_windows_386.go @@ -0,0 +1,6 @@ +package winredirect + +import _ "embed" + +//go:embed x86/winredirect.sys +var driverContent []byte diff --git a/internal/winredirect/embed_windows_amd64.go b/internal/winredirect/embed_windows_amd64.go new file mode 100644 index 00000000..2013e198 --- /dev/null +++ b/internal/winredirect/embed_windows_amd64.go @@ -0,0 +1,6 @@ +package winredirect + +import _ "embed" + +//go:embed amd64/winredirect.sys +var driverContent []byte diff --git a/internal/winredirect/embed_windows_arm.go b/internal/winredirect/embed_windows_arm.go new file mode 100644 index 00000000..46a3ba6f --- /dev/null +++ b/internal/winredirect/embed_windows_arm.go @@ -0,0 +1,6 @@ +package winredirect + +import _ "embed" + +//go:embed arm/winredirect.sys +var driverContent []byte diff --git a/internal/winredirect/embed_windows_arm64.go b/internal/winredirect/embed_windows_arm64.go new file mode 100644 index 00000000..4a108639 --- /dev/null +++ b/internal/winredirect/embed_windows_arm64.go @@ -0,0 +1,6 @@ +package winredirect + +import _ "embed" + +//go:embed arm64/winredirect.sys +var driverContent []byte diff --git a/internal/winredirect/ioctl_windows.go b/internal/winredirect/ioctl_windows.go new file mode 100644 index 00000000..f3803050 --- /dev/null +++ b/internal/winredirect/ioctl_windows.go @@ -0,0 +1,51 @@ +package winredirect + +import ( + "unsafe" +) + +func (m *Manager) SetConfig(cfg *Config) error { + _, err := m.ioctl( + ioctlSetConfig, + unsafe.Pointer(cfg), + uint32(unsafe.Sizeof(*cfg)), + nil, 0, + ) + return err +} + +func (m *Manager) StartRedirect() error { + _, err := m.ioctl(ioctlStart, nil, 0, nil, 0) + return err +} + +func (m *Manager) StopRedirect() error { + _, err := m.ioctl(ioctlStop, nil, 0, nil, 0) + return err +} + +// GetPendingConn blocks until a connection needs a verdict. +// Multiple goroutines may call this concurrently (inverted IOCTL pattern). +func (m *Manager) GetPendingConn() (*PendingConn, error) { + var conn PendingConn + _, err := m.ioctl( + ioctlGetPending, + nil, 0, + unsafe.Pointer(&conn), + uint32(unsafe.Sizeof(conn)), + ) + if err != nil { + return nil, err + } + return &conn, nil +} + +func (m *Manager) SetVerdict(v *Verdict) error { + _, err := m.ioctl( + ioctlSetVerdict, + unsafe.Pointer(v), + uint32(unsafe.Sizeof(*v)), + nil, 0, + ) + return err +} diff --git a/internal/winredirect/manager_windows.go b/internal/winredirect/manager_windows.go new file mode 100644 index 00000000..83a9dfc6 --- /dev/null +++ b/internal/winredirect/manager_windows.go @@ -0,0 +1,154 @@ +package winredirect + +import ( + "fmt" + "os" + "path/filepath" + "unsafe" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/mgr" +) + +const ( + serviceName = "WinRedirect" + devicePath = `\\.\WinRedirect` +) + +type Manager struct { + driverPath string + device windows.Handle +} + +func NewManager() (*Manager, error) { + tmpDir, err := os.MkdirTemp("", "winredirect-*") + if err != nil { + return nil, fmt.Errorf("create temp dir: %w", err) + } + driverPath := filepath.Join(tmpDir, "winredirect.sys") + if err = os.WriteFile(driverPath, driverContent, 0o644); err != nil { + os.RemoveAll(tmpDir) + return nil, fmt.Errorf("write driver: %w", err) + } + return &Manager{ + driverPath: driverPath, + device: windows.InvalidHandle, + }, nil +} + +func (m *Manager) Install() error { + scm, err := mgr.Connect() + if err != nil { + return fmt.Errorf("connect SCM: %w", err) + } + defer scm.Disconnect() + + // Remove stale service if it exists + if existing, err := scm.OpenService(serviceName); err == nil { + existing.Control(windows.SERVICE_CONTROL_STOP) + existing.Delete() + existing.Close() + } + + svc, err := scm.CreateService(serviceName, m.driverPath, mgr.Config{ + ServiceType: windows.SERVICE_KERNEL_DRIVER, + StartType: mgr.StartManual, + }) + if err != nil { + return fmt.Errorf("create service: %w", err) + } + svc.Close() + return nil +} + +func (m *Manager) Start() error { + scm, err := mgr.Connect() + if err != nil { + return fmt.Errorf("connect SCM: %w", err) + } + defer scm.Disconnect() + + svc, err := scm.OpenService(serviceName) + if err != nil { + return fmt.Errorf("open service: %w", err) + } + defer svc.Close() + + if err = svc.Start(); err != nil { + return fmt.Errorf("start service: %w", err) + } + return nil +} + +func (m *Manager) OpenDevice() error { + path, err := windows.UTF16PtrFromString(devicePath) + if err != nil { + return err + } + handle, err := windows.CreateFile( + path, + windows.GENERIC_READ|windows.GENERIC_WRITE, + 0, + nil, + windows.OPEN_EXISTING, + windows.FILE_FLAG_OVERLAPPED, + 0, + ) + if err != nil { + return fmt.Errorf("open device: %w", err) + } + m.device = handle + return nil +} + +func (m *Manager) ioctl(code uint32, inBuf unsafe.Pointer, inSize uint32, outBuf unsafe.Pointer, outSize uint32) (uint32, error) { + var bytesReturned uint32 + overlapped := &windows.Overlapped{} + overlapped.HEvent, _ = windows.CreateEvent(nil, 1, 0, nil) + defer windows.CloseHandle(overlapped.HEvent) + + err := windows.DeviceIoControl( + m.device, + code, + (*byte)(inBuf), + inSize, + (*byte)(outBuf), + outSize, + &bytesReturned, + overlapped, + ) + if err == windows.ERROR_IO_PENDING { + _, err = windows.WaitForSingleObject(overlapped.HEvent, windows.INFINITE) + if err != nil { + return 0, err + } + err = windows.GetOverlappedResult(m.device, overlapped, &bytesReturned, false) + } + if err != nil { + return 0, err + } + return bytesReturned, nil +} + +func (m *Manager) Close() error { + if m.device != windows.InvalidHandle { + m.StopRedirect() + windows.CloseHandle(m.device) + m.device = windows.InvalidHandle + } + + scm, err := mgr.Connect() + if err == nil { + if svc, err := scm.OpenService(serviceName); err == nil { + svc.Control(windows.SERVICE_CONTROL_STOP) + svc.Delete() + svc.Close() + } + scm.Disconnect() + } + + if m.driverPath != "" { + os.RemoveAll(filepath.Dir(m.driverPath)) + } + return nil +} diff --git a/internal/winredirect/types_windows.go b/internal/winredirect/types_windows.go new file mode 100644 index 00000000..9066ca0a --- /dev/null +++ b/internal/winredirect/types_windows.go @@ -0,0 +1,48 @@ +package winredirect + +// IOCTL codes matching the kernel driver definitions. +// CTL_CODE(FILE_DEVICE_NETWORK=0x12, function, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0) +const ( + ioctlSetConfig = (0x00120000 | (0x800 << 2)) // IOCTL_WINREDIRECT_SET_CONFIG + ioctlStart = (0x00120000 | (0x801 << 2)) // IOCTL_WINREDIRECT_START + ioctlStop = (0x00120000 | (0x802 << 2)) // IOCTL_WINREDIRECT_STOP + ioctlGetPending = (0x00120000 | (0x803 << 2)) // IOCTL_WINREDIRECT_GET_PENDING + ioctlSetVerdict = (0x00120000 | (0x804 << 2)) // IOCTL_WINREDIRECT_SET_VERDICT +) + +const ( + VerdictRedirect = 0 + VerdictBypass = 1 + VerdictDrop = 2 +) + +// Config is sent to the driver via IOCTL_SET_CONFIG. +// Must match WINREDIRECT_CONFIG in the driver. +type Config struct { + RedirectPort uint16 + _ [2]byte // padding + ProxyPID uint32 +} + +// PendingConn is received from the driver via IOCTL_GET_PENDING. +// Must match WINREDIRECT_PENDING_CONN in the driver. +type PendingConn struct { + ConnID uint64 + AddressFamily uint8 + _ [3]byte // padding + SrcAddr [16]byte + SrcPort uint16 + _ [2]byte // padding + DstAddr [16]byte + DstPort uint16 + _ [2]byte // padding + ProcessID uint32 +} + +// Verdict is sent to the driver via IOCTL_SET_VERDICT. +// Must match WINREDIRECT_VERDICT in the driver. +type Verdict struct { + ConnID uint64 + Verdict uint32 + _ [4]byte // padding for alignment +} diff --git a/internal/winredirect/x86/winredirect.sys b/internal/winredirect/x86/winredirect.sys new file mode 100644 index 00000000..311c8dd0 --- /dev/null +++ b/internal/winredirect/x86/winredirect.sys @@ -0,0 +1 @@ +PLACEHOLDER \ No newline at end of file diff --git a/internal/winsys/constants.go b/internal/winsys/constants.go index 1173ea9a..1bdc4c00 100644 --- a/internal/winsys/constants.go +++ b/internal/winsys/constants.go @@ -132,9 +132,30 @@ var FWPM_CONDITION_ALE_APP_ID = windows.GUID{ } const ( + IPPROTO_TCP uint32 = 6 IPPROTO_UDP uint32 = 17 ) +// https://learn.microsoft.com/en-us/windows-hardware/drivers/network/ale-connect-redirect-layers +var FWPM_LAYER_ALE_CONNECT_REDIRECT_V4 = windows.GUID{ + Data1: 0xc4f7e4c3, + Data2: 0x3455, + Data3: 0x4c3a, + Data4: [8]byte{0xa2, 0x17, 0x31, 0x7c, 0x8f, 0xc8, 0xf0, 0xd1}, +} + +var FWPM_LAYER_ALE_CONNECT_REDIRECT_V6 = windows.GUID{ + Data1: 0x587e54a7, + Data2: 0x8440, + Data3: 0x4b2a, + Data4: [8]byte{0xa3, 0x53, 0x2e, 0x45, 0xd8, 0x80, 0x25, 0x4f}, +} + +const ( + FWP_ACTION_FLAG_CALLOUT uint32 = 0x00004000 + FWP_ACTION_CALLOUT_TERMINATING uint32 = (0x00000003 | FWP_ACTION_FLAG_CALLOUT | FWP_ACTION_FLAG_TERMINATING) +) + const ( FWP_ACTION_FLAG_TERMINATING uint32 = 0x00001000 FWP_ACTION_BLOCK uint32 = (0x00000001 | FWP_ACTION_FLAG_TERMINATING) diff --git a/redirect_metadata.go b/redirect_metadata.go new file mode 100644 index 00000000..3a40e321 --- /dev/null +++ b/redirect_metadata.go @@ -0,0 +1,24 @@ +package tun + +import "context" + +// AutoRedirectMetadata carries process info obtained cheaply at redirect time. +// On Windows, WFP classify provides PID for free; Go resolves path from PID. +// This replaces the expensive process finder (netlink diag / sysctl / GetExtendedTcpTable) +// used in sing-box when process-based routing rules are configured. +type AutoRedirectMetadata struct { + ProcessID uint32 + ProcessPath string + UserId int32 // -1 if unknown +} + +type autoRedirectMetadataKey struct{} + +func ContextWithAutoRedirectMetadata(ctx context.Context, metadata *AutoRedirectMetadata) context.Context { + return context.WithValue(ctx, autoRedirectMetadataKey{}, metadata) +} + +func AutoRedirectMetadataFromContext(ctx context.Context) *AutoRedirectMetadata { + metadata, _ := ctx.Value(autoRedirectMetadataKey{}).(*AutoRedirectMetadata) + return metadata +} diff --git a/redirect_server_windows.go b/redirect_server_windows.go new file mode 100644 index 00000000..d3394847 --- /dev/null +++ b/redirect_server_windows.go @@ -0,0 +1,172 @@ +package tun + +import ( + "context" + "errors" + "net" + "net/netip" + "sync" + "sync/atomic" + "time" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type redirectServer struct { + ctx context.Context + handler N.TCPConnectionHandlerEx + logger logger.Logger + listenAddr netip.Addr + listener *net.TCPListener + connTable *connMetadataTable + inShutdown atomic.Bool +} + +func newRedirectServerWindows(ctx context.Context, handler N.TCPConnectionHandlerEx, logger logger.Logger, listenAddr netip.Addr) *redirectServer { + return &redirectServer{ + ctx: ctx, + handler: handler, + logger: logger, + listenAddr: listenAddr, + connTable: newConnMetadataTable(), + } +} + +func (s *redirectServer) Start() error { + var listenConfig net.ListenConfig + listenConfig.KeepAlive = 10 * time.Minute + listener, err := listenConfig.Listen(s.ctx, M.NetworkFromNetAddr("tcp", s.listenAddr), M.SocksaddrFrom(s.listenAddr, 0).String()) + if err != nil { + return err + } + s.listener = listener.(*net.TCPListener) + go s.loopIn() + return nil +} + +func (s *redirectServer) Close() error { + s.inShutdown.Store(true) + if s.listener != nil { + return s.listener.Close() + } + return nil +} + +func (s *redirectServer) loopIn() { + for { + conn, err := s.listener.AcceptTCP() + if err != nil { + var netError net.Error + //nolint:staticcheck + if errors.As(err, &netError) && netError.Temporary() { + s.logger.Error(err) + continue + } + if s.inShutdown.Load() && E.IsClosed(err) { + return + } + s.listener.Close() + s.logger.Error("serve error: ", err) + return + } + source := M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() + entry, ok := s.connTable.Lookup(source) + if !ok { + _ = conn.SetLinger(0) + _ = conn.Close() + s.logger.Error("process redirect connection from ", source, ": no metadata") + continue + } + destination := entry.Destination + if entry.IsDNS { + destination = entry.DNSServer + } + ctx := s.ctx + if entry.Metadata != nil { + ctx = ContextWithAutoRedirectMetadata(ctx, entry.Metadata) + } + go s.handler.NewConnectionEx(ctx, conn, source, destination, nil) + } +} + +// connMetadataTable maps source address → connection metadata. +// Entries are populated by pre-match workers before sending redirect verdicts, +// and consumed by the redirect server upon accepting connections. +type connMetadataTable struct { + mu sync.Mutex + entries map[connKey]*connEntry +} + +type connKey struct { + Addr netip.Addr + Port uint16 +} + +type connEntry struct { + Destination M.Socksaddr + Metadata *AutoRedirectMetadata + IsDNS bool + DNSServer M.Socksaddr + CreatedAt time.Time +} + +func newConnMetadataTable() *connMetadataTable { + t := &connMetadataTable{ + entries: make(map[connKey]*connEntry), + } + go t.cleanupLoop() + return t +} + +func (t *connMetadataTable) Store(src M.Socksaddr, dst M.Socksaddr, metadata *AutoRedirectMetadata) { + key := connKey{Addr: src.Addr, Port: src.Port} + t.mu.Lock() + defer t.mu.Unlock() + t.entries[key] = &connEntry{ + Destination: dst, + Metadata: metadata, + CreatedAt: time.Now(), + } +} + +func (t *connMetadataTable) StoreDNS(src M.Socksaddr, originalDst M.Socksaddr, dnsServer M.Socksaddr, metadata *AutoRedirectMetadata) { + key := connKey{Addr: src.Addr, Port: src.Port} + t.mu.Lock() + defer t.mu.Unlock() + t.entries[key] = &connEntry{ + Destination: originalDst, + Metadata: metadata, + IsDNS: true, + DNSServer: dnsServer, + CreatedAt: time.Now(), + } +} + +func (t *connMetadataTable) Lookup(src M.Socksaddr) (*connEntry, bool) { + key := connKey{Addr: src.Addr, Port: src.Port} + t.mu.Lock() + defer t.mu.Unlock() + entry, ok := t.entries[key] + if ok { + delete(t.entries, key) + } + return entry, ok +} + +func (t *connMetadataTable) cleanupLoop() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + for range ticker.C { + t.mu.Lock() + now := time.Now() + for key, entry := range t.entries { + if now.Sub(entry.CreatedAt) > 30*time.Second { + delete(t.entries, key) + } + } + t.mu.Unlock() + } +} diff --git a/redirect_stub.go b/redirect_stub.go index 040ef124..8bd94ca2 100644 --- a/redirect_stub.go +++ b/redirect_stub.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !(linux || windows) package tun diff --git a/redirect_windows.go b/redirect_windows.go new file mode 100644 index 00000000..88d99f5f --- /dev/null +++ b/redirect_windows.go @@ -0,0 +1,401 @@ +package tun + +import ( + "context" + "errors" + "net/netip" + "os" + "slices" + "strings" + "sync" + + "github.com/sagernet/sing-tun/internal/winredirect" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/x/list" + + "go4.org/netipx" + "golang.org/x/sys/windows" +) + +type autoRedirect struct { + tunOptions *Options + ctx context.Context + handler Handler + logger logger.Logger + networkMonitor NetworkUpdateMonitor + networkListener *list.Element[NetworkUpdateCallback] + interfaceFinder control.InterfaceFinder + driverManager *winredirect.Manager + redirectServer *redirectServer + routeAddressSet *[]*netipx.IPSet + routeExcludeAddressSet *[]*netipx.IPSet + + enableIPv4 bool + enableIPv6 bool + + localAddressMu sync.RWMutex + localAddresses []netip.Prefix + + workerCount int +} + +func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { + r := &autoRedirect{ + tunOptions: options.TunOptions, + ctx: options.Context, + handler: options.Handler, + logger: options.Logger, + networkMonitor: options.NetworkMonitor, + interfaceFinder: options.InterfaceFinder, + routeAddressSet: options.RouteAddressSet, + routeExcludeAddressSet: options.RouteExcludeAddressSet, + workerCount: 4, + } + return r, nil +} + +func (r *autoRedirect) Start() error { + r.enableIPv4 = len(r.tunOptions.Inet4Address) > 0 + r.enableIPv6 = len(r.tunOptions.Inet6Address) > 0 + if !r.enableIPv4 && !r.enableIPv6 { + return E.New("no address configured") + } + + manager, err := winredirect.NewManager() + if err != nil { + return E.Cause(err, "create driver manager") + } + r.driverManager = manager + + err = manager.Install() + if err != nil { + manager.Close() + return E.Cause(err, "install driver") + } + + err = manager.Start() + if err != nil { + manager.Close() + return E.Cause(err, "start driver") + } + + err = manager.OpenDevice() + if err != nil { + manager.Close() + return E.Cause(err, "open driver device") + } + + var listenAddr netip.Addr + if r.enableIPv6 { + listenAddr = netip.IPv6Unspecified() + } else { + listenAddr = netip.IPv4Unspecified() + } + server := newRedirectServerWindows(r.ctx, r.handler, r.logger, listenAddr) + err = server.Start() + if err != nil { + manager.Close() + return E.Cause(err, "start redirect server") + } + r.redirectServer = server + + redirectPort := M.AddrPortFromNet(server.listener.Addr()).Port() + err = manager.SetConfig(&winredirect.Config{ + RedirectPort: redirectPort, + ProxyPID: uint32(os.Getpid()), + }) + if err != nil { + server.Close() + manager.Close() + return E.Cause(err, "set driver config") + } + + err = manager.StartRedirect() + if err != nil { + server.Close() + manager.Close() + return E.Cause(err, "start redirect") + } + + r.updateLocalAddresses() + if r.networkMonitor != nil { + r.networkListener = r.networkMonitor.RegisterCallback(func() { + r.updateLocalAddresses() + }) + } + + for i := 0; i < r.workerCount; i++ { + go r.preMatchWorker() + } + + return nil +} + +func (r *autoRedirect) Close() error { + if r.networkMonitor != nil && r.networkListener != nil { + r.networkMonitor.UnregisterCallback(r.networkListener) + } + return common.Close( + common.PtrOrNil(r.redirectServer), + common.PtrOrNil(r.driverManager), + ) +} + +func (r *autoRedirect) UpdateRouteAddressSet() { + // Dynamic route address sets are updated via pointer indirection. + // The IPSet pointers are swapped atomically by the caller. + // No driver communication needed — all filtering is in Go. +} + +func (r *autoRedirect) preMatchWorker() { + for { + conn, err := r.driverManager.GetPendingConn() + if err != nil { + return + } + verdict := r.evaluateConnection(conn) + r.driverManager.SetVerdict(&winredirect.Verdict{ + ConnID: conn.ConnID, + Verdict: verdict, + }) + } +} + +func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 { + dst := pendingConnDst(conn) + src := pendingConnSrc(conn) + + // 1. Loopback destinations + if dst.Addr.IsLoopback() { + return winredirect.VerdictBypass + } + + // 2. Static route address whitelist (Inet4/6RouteAddress) + if r.hasStaticRouteAddress() && !r.matchStaticRouteAddress(dst.Addr) { + return winredirect.VerdictBypass + } + + // 3. Static route address blacklist (Inet4/6RouteExcludeAddress) + if r.matchStaticRouteExcludeAddress(dst.Addr) { + return winredirect.VerdictBypass + } + + // 4. DNS hijack: port 53 from local network → redirect to DNS server + if !r.tunOptions.EXP_DisableDNSHijack && dst.Port == 53 { + if r.isLocalAddress(src.Addr) { + dnsServer := r.dnsServerForFamily(dst.Addr) + if dnsServer.IsValid() { + metadata := r.resolveMetadata(conn) + r.redirectServer.connTable.StoreDNS(src, dst, M.SocksaddrFrom(dnsServer, 53), metadata) + return winredirect.VerdictRedirect + } + } + } + + // 5. Local address exclusion + if r.isLocalAddress(dst.Addr) { + return winredirect.VerdictBypass + } + + // 6. Dynamic route address set whitelist + if r.hasDynamicRouteAddressSet() && !r.matchDynamicRouteAddressSet(dst.Addr) { + return winredirect.VerdictBypass + } + + // 7. Dynamic route address set blacklist + if r.matchDynamicRouteExcludeAddressSet(dst.Addr) { + return winredirect.VerdictBypass + } + + // 8. Strict route: reject disabled address family + if r.tunOptions.StrictRoute && r.isDisabledFamily(dst.Addr) { + return winredirect.VerdictDrop + } + + // 9. Resolve PID → process path + metadata := r.resolveMetadata(conn) + + // 10. PrepareConnection (NFQUEUE equivalent) + _, err := r.handler.PrepareConnection("tcp", src, dst, nil, 0) + if errors.Is(err, ErrBypass) { + return winredirect.VerdictBypass + } + if errors.Is(err, ErrDrop) || errors.Is(err, ErrReset) || err != nil { + return winredirect.VerdictDrop + } + + // 11. Store metadata for redirect server + r.redirectServer.connTable.Store(src, dst, metadata) + + return winredirect.VerdictRedirect +} + +func (r *autoRedirect) resolveMetadata(conn *winredirect.PendingConn) *AutoRedirectMetadata { + processPath, _ := queryFullProcessImageName(conn.ProcessID) + return &AutoRedirectMetadata{ + ProcessID: conn.ProcessID, + ProcessPath: processPath, + UserId: -1, + } +} + +func (r *autoRedirect) updateLocalAddresses() { + if r.interfaceFinder == nil { + return + } + r.interfaceFinder.Update() + newLocalAddresses := common.FlatMap(r.interfaceFinder.Interfaces(), func(it control.Interface) []netip.Prefix { + return common.Filter(it.Addresses, func(prefix netip.Prefix) bool { + return it.Name == "Loopback Pseudo-Interface 1" || prefix.Addr().IsGlobalUnicast() + }) + }) + r.localAddressMu.Lock() + defer r.localAddressMu.Unlock() + if slices.Equal(newLocalAddresses, r.localAddresses) { + return + } + r.localAddresses = newLocalAddresses + if r.logger != nil { + r.logger.Debug("updating local address set to [", strings.Join(common.Map(newLocalAddresses, func(it netip.Prefix) string { + return it.String() + }), ", ")+"]") + } +} + +func (r *autoRedirect) isLocalAddress(addr netip.Addr) bool { + r.localAddressMu.RLock() + defer r.localAddressMu.RUnlock() + for _, prefix := range r.localAddresses { + if prefix.Contains(addr) { + return true + } + } + return false +} + +func (r *autoRedirect) hasStaticRouteAddress() bool { + return len(r.tunOptions.Inet4RouteAddress) > 0 || len(r.tunOptions.Inet6RouteAddress) > 0 +} + +func (r *autoRedirect) matchStaticRouteAddress(addr netip.Addr) bool { + var prefixes []netip.Prefix + if addr.Is4() { + prefixes = r.tunOptions.Inet4RouteAddress + } else { + prefixes = r.tunOptions.Inet6RouteAddress + } + for _, prefix := range prefixes { + if prefix.Contains(addr) { + return true + } + } + return false +} + +func (r *autoRedirect) matchStaticRouteExcludeAddress(addr netip.Addr) bool { + var prefixes []netip.Prefix + if addr.Is4() { + prefixes = r.tunOptions.Inet4RouteExcludeAddress + } else { + prefixes = r.tunOptions.Inet6RouteExcludeAddress + } + for _, prefix := range prefixes { + if prefix.Contains(addr) { + return true + } + } + return false +} + +func (r *autoRedirect) hasDynamicRouteAddressSet() bool { + if r.routeAddressSet == nil { + return false + } + sets := *r.routeAddressSet + return len(sets) > 0 +} + +func (r *autoRedirect) matchDynamicRouteAddressSet(addr netip.Addr) bool { + if r.routeAddressSet == nil { + return true + } + for _, set := range *r.routeAddressSet { + if set.Contains(addr) { + return true + } + } + return false +} + +func (r *autoRedirect) matchDynamicRouteExcludeAddressSet(addr netip.Addr) bool { + if r.routeExcludeAddressSet == nil { + return false + } + for _, set := range *r.routeExcludeAddressSet { + if set.Contains(addr) { + return true + } + } + return false +} + +func (r *autoRedirect) isDisabledFamily(addr netip.Addr) bool { + if addr.Is4() { + return !r.enableIPv4 + } + return !r.enableIPv6 +} + +func (r *autoRedirect) dnsServerForFamily(addr netip.Addr) netip.Addr { + isV4 := addr.Is4() + dnsServer := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { + return it.Is4() == isV4 + }) + if dnsServer.IsValid() { + return dnsServer + } + if isV4 { + if len(r.tunOptions.Inet4Address) > 0 && HasNextAddress(r.tunOptions.Inet4Address[0], 1) { + return r.tunOptions.Inet4Address[0].Addr().Next() + } + } else { + if len(r.tunOptions.Inet6Address) > 0 && HasNextAddress(r.tunOptions.Inet6Address[0], 1) { + return r.tunOptions.Inet6Address[0].Addr().Next() + } + } + return netip.Addr{} +} + +func pendingConnSrc(conn *winredirect.PendingConn) M.Socksaddr { + return M.SocksaddrFrom(pendingAddr(conn.AddressFamily, conn.SrcAddr), conn.SrcPort) +} + +func pendingConnDst(conn *winredirect.PendingConn) M.Socksaddr { + return M.SocksaddrFrom(pendingAddr(conn.AddressFamily, conn.DstAddr), conn.DstPort) +} + +func pendingAddr(af uint8, raw [16]byte) netip.Addr { + if af == 2 { // AF_INET + return netip.AddrFrom4([4]byte(raw[:4])) + } + return netip.AddrFrom16(raw) +} + +func queryFullProcessImageName(pid uint32) (string, error) { + handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + if err != nil { + return "", err + } + defer windows.CloseHandle(handle) + var buf [windows.MAX_PATH]uint16 + n := uint32(len(buf)) + err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &n) + if err != nil { + return "", err + } + return windows.UTF16ToString(buf[:n]), nil +} From 29f3a52a66bdf0c53177fcb14fe3ecf51a4a0e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Mar 2026 23:55:57 +0800 Subject: [PATCH 03/38] Fix windows redirect driver: unique GUIDs, config locking, paged pool - Replace linearly-incremented GUIDs with independently generated ones - Add FAST_MUTEX ConfigLock to protect Config reads/writes - Use InterlockedCompareExchange for Running flag - Move timer timeout processing to WDFWORKITEM (PASSIVE_LEVEL) - Switch KSPIN_LOCK to FAST_MUTEX for PendingLock - Switch NonPagedPool to PagedPool for PENDING_ENTRY allocations --- internal/winredirect/driver/winredirect.c | 99 ++++++++++++++--------- internal/winredirect/driver/winredirect.h | 13 +-- 2 files changed, 67 insertions(+), 45 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index be0360a9..3524a570 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -1,20 +1,20 @@ #include "winredirect.h" -// {7B3A8D2E-4F5C-11EE-BE56-0242AC120002} +// {E513903C-D2F3-4D8C-9458-0483E7D7A01F} DEFINE_GUID(WINREDIRECT_PROVIDER_KEY, - 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x02); + 0xe513903c, 0xd2f3, 0x4d8c, 0x94, 0x58, 0x04, 0x83, 0xe7, 0xd7, 0xa0, 0x1f); -// {7B3A8D2E-4F5C-11EE-BE56-0242AC120003} +// {8987A44E-ECB2-4A47-9FB6-B749C804FA3B} DEFINE_GUID(WINREDIRECT_SUBLAYER_KEY, - 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x03); + 0x8987a44e, 0xecb2, 0x4a47, 0x9f, 0xb6, 0xb7, 0x49, 0xc8, 0x04, 0xfa, 0x3b); -// {7B3A8D2E-4F5C-11EE-BE56-0242AC120004} +// {7EA20C4E-1A93-427E-80DC-E18A60AAB73B} DEFINE_GUID(WINREDIRECT_CALLOUT_V4_KEY, - 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x04); + 0x7ea20c4e, 0x1a93, 0x427e, 0x80, 0xdc, 0xe1, 0x8a, 0x60, 0xaa, 0xb7, 0x3b); -// {7B3A8D2E-4F5C-11EE-BE56-0242AC120005} +// {AABE8538-0A09-4D47-8E61-1127CE5BB1AB} DEFINE_GUID(WINREDIRECT_CALLOUT_V6_KEY, - 0x7b3a8d2e, 0x4f5c, 0x11ee, 0xbe, 0x56, 0x02, 0x42, 0xac, 0x12, 0x00, 0x05); + 0xaabe8538, 0x0a09, 0x4d47, 0x8e, 0x61, 0x11, 0x27, 0xce, 0x5b, 0xb1, 0xab); static PDRIVER_CONTEXT g_Ctx = NULL; @@ -59,7 +59,8 @@ NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING Regi RtlZeroMemory(ctx, sizeof(DRIVER_CONTEXT)); ctx->Device = device; InitializeListHead(&ctx->PendingList); - KeInitializeSpinLock(&ctx->PendingLock); + ExInitializeFastMutex(&ctx->PendingLock); + ExInitializeFastMutex(&ctx->ConfigLock); g_Ctx = ctx; // Create manual-dispatch queue for pending IOCTLs @@ -85,6 +86,15 @@ NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING Regi status = WdfTimerCreate(&timerConfig, &timerAttrs, &ctx->TimeoutTimer); if (!NT_SUCCESS(status)) return status; + // Create work item for timeout processing at PASSIVE_LEVEL + WDF_WORKITEM_CONFIG workItemConfig; + WDF_WORKITEM_CONFIG_INIT(&workItemConfig, EvtTimeoutWorkItem); + WDF_OBJECT_ATTRIBUTES workItemAttrs; + WDF_OBJECT_ATTRIBUTES_INIT(&workItemAttrs); + workItemAttrs.ParentObject = device; + status = WdfWorkItemCreate(&workItemConfig, &workItemAttrs, &ctx->TimeoutWorkItem); + if (!NT_SUCCESS(status)) return status; + WdfControlFinishInitializing(device); return STATUS_SUCCESS; } @@ -108,37 +118,40 @@ void EvtIoDeviceControl( UNREFERENCED_PARAMETER(Queue); PDRIVER_CONTEXT ctx = g_Ctx; NTSTATUS status = STATUS_SUCCESS; - PVOID inBuf = NULL, outBuf = NULL; - size_t inLen = 0, outLen = 0; + PVOID inBuf = NULL; + size_t inLen = 0; switch (IoControlCode) { case IOCTL_WINREDIRECT_SET_CONFIG: status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_CONFIG), &inBuf, &inLen); if (NT_SUCCESS(status)) { + ExAcquireFastMutex(&ctx->ConfigLock); RtlCopyMemory(&ctx->Config, inBuf, sizeof(WINREDIRECT_CONFIG)); + ExReleaseFastMutex(&ctx->ConfigLock); } WdfRequestComplete(Request, status); break; case IOCTL_WINREDIRECT_START: - if (ctx->Running) { + if (InterlockedCompareExchange(&ctx->Running, TRUE, FALSE) != FALSE) { WdfRequestComplete(Request, STATUS_ALREADY_REGISTERED); break; } status = WfpSetup(ctx); if (NT_SUCCESS(status)) { - ctx->Running = TRUE; WdfTimerStart(ctx->TimeoutTimer, WDF_REL_TIMEOUT_IN_SEC(5)); + } else { + InterlockedExchange(&ctx->Running, FALSE); } WdfRequestComplete(Request, status); break; case IOCTL_WINREDIRECT_STOP: - if (ctx->Running) { - WdfTimerStop(ctx->TimeoutTimer, FALSE); + if (InterlockedCompareExchange(&ctx->Running, FALSE, TRUE) == TRUE) { + WdfTimerStop(ctx->TimeoutTimer, TRUE); + WdfWorkItemFlush(ctx->TimeoutWorkItem); WfpCleanup(ctx); PendingFlushAll(ctx); - ctx->Running = FALSE; } WdfRequestComplete(Request, STATUS_SUCCESS); break; @@ -183,11 +196,17 @@ void EvtTimeoutTimer(_In_ WDFTIMER Timer) { UNREFERENCED_PARAMETER(Timer); if (!g_Ctx) return; + WdfWorkItemEnqueue(g_Ctx->TimeoutWorkItem); +} + +void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) +{ + UNREFERENCED_PARAMETER(WorkItem); + if (!g_Ctx) return; LARGE_INTEGER now; KeQuerySystemTime(&now); - KIRQL irql; - KeAcquireSpinLock(&g_Ctx->PendingLock, &irql); + ExAcquireFastMutex(&g_Ctx->PendingLock); PLIST_ENTRY entry = g_Ctx->PendingList.Flink; while (entry != &g_Ctx->PendingList) { @@ -198,15 +217,15 @@ void EvtTimeoutTimer(_In_ WDFTIMER Timer) LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; // to seconds if (elapsed >= 5) { RemoveEntryList(&pending->ListEntry); - KeReleaseSpinLock(&g_Ctx->PendingLock, irql); + ExReleaseFastMutex(&g_Ctx->PendingLock); ExecuteVerdict(g_Ctx, pending, VERDICT_BYPASS); ExFreePoolWithTag(pending, 'rniW'); - KeAcquireSpinLock(&g_Ctx->PendingLock, &irql); + ExAcquireFastMutex(&g_Ctx->PendingLock); entry = g_Ctx->PendingList.Flink; } } - KeReleaseSpinLock(&g_Ctx->PendingLock, irql); + ExReleaseFastMutex(&g_Ctx->PendingLock); } // --- WFP Setup --- @@ -357,7 +376,7 @@ static void ClassifyFnCommon( } // Allocate pending entry - PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(PENDING_ENTRY), 'rniW'); + PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_PAGED, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { classifyOut->actionType = FWP_ACTION_PERMIT; return; @@ -475,22 +494,20 @@ void NTAPI ClassifyFnV6( PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx) { UNREFERENCED_PARAMETER(Ctx); - return (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(PENDING_ENTRY), 'rniW'); + return (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_PAGED, sizeof(PENDING_ENTRY), 'rniW'); } void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) { - KIRQL irql; - KeAcquireSpinLock(&Ctx->PendingLock, &irql); + ExAcquireFastMutex(&Ctx->PendingLock); InsertTailList(&Ctx->PendingList, &Entry->ListEntry); - KeReleaseSpinLock(&Ctx->PendingLock, irql); + ExReleaseFastMutex(&Ctx->PendingLock); } PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID) { - KIRQL irql; PPENDING_ENTRY found = NULL; - KeAcquireSpinLock(&Ctx->PendingLock, &irql); + ExAcquireFastMutex(&Ctx->PendingLock); PLIST_ENTRY entry = Ctx->PendingList.Flink; while (entry != &Ctx->PendingList) { PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); @@ -501,31 +518,29 @@ PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID) } entry = entry->Flink; } - KeReleaseSpinLock(&Ctx->PendingLock, irql); + ExReleaseFastMutex(&Ctx->PendingLock); return found; } void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) { - KIRQL irql; - KeAcquireSpinLock(&Ctx->PendingLock, &irql); + ExAcquireFastMutex(&Ctx->PendingLock); RemoveEntryList(&Entry->ListEntry); - KeReleaseSpinLock(&Ctx->PendingLock, irql); + ExReleaseFastMutex(&Ctx->PendingLock); } void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx) { - KIRQL irql; - KeAcquireSpinLock(&Ctx->PendingLock, &irql); + ExAcquireFastMutex(&Ctx->PendingLock); while (!IsListEmpty(&Ctx->PendingList)) { PLIST_ENTRY entry = RemoveHeadList(&Ctx->PendingList); PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); - KeReleaseSpinLock(&Ctx->PendingLock, irql); + ExReleaseFastMutex(&Ctx->PendingLock); ExecuteVerdict(Ctx, pending, VERDICT_BYPASS); ExFreePoolWithTag(pending, 'rniW'); - KeAcquireSpinLock(&Ctx->PendingLock, &irql); + ExAcquireFastMutex(&Ctx->PendingLock); } - KeReleaseSpinLock(&Ctx->PendingLock, irql); + ExReleaseFastMutex(&Ctx->PendingLock); } // --- Verdict execution --- @@ -541,19 +556,23 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI (PVOID*)&connReq, &classifyOut); if (NT_SUCCESS(status) && connReq) { + ExAcquireFastMutex(&Ctx->ConfigLock); + WINREDIRECT_CONFIG config = Ctx->Config; + ExReleaseFastMutex(&Ctx->ConfigLock); + if (Entry->AddressFamily == AF_INET) { SOCKADDR_IN* addr = (SOCKADDR_IN*)&connReq->remoteAddressAndPort; addr->sin_family = AF_INET; addr->sin_addr.s_addr = RtlUlongByteSwap(0x7F000001); // 127.0.0.1 - addr->sin_port = RtlUshortByteSwap(Ctx->Config.RedirectPort); + addr->sin_port = RtlUshortByteSwap(config.RedirectPort); } else { SOCKADDR_IN6* addr = (SOCKADDR_IN6*)&connReq->remoteAddressAndPort; RtlZeroMemory(addr, sizeof(SOCKADDR_IN6)); addr->sin6_family = AF_INET6; addr->sin6_addr.u.Byte[15] = 1; // ::1 - addr->sin6_port = RtlUshortByteSwap(Ctx->Config.RedirectPort); + addr->sin6_port = RtlUshortByteSwap(config.RedirectPort); } - connReq->localRedirectTargetPID = Ctx->Config.ProxyPID; + connReq->localRedirectTargetPID = config.ProxyPID; connReq->localRedirectHandle = Ctx->RedirectHandle; FwpsApplyModifiedLayerData0(Entry->ClassifyHandle, connReq, 0); diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 64acddaf..986eb1c1 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -81,17 +81,19 @@ typedef struct _DRIVER_CONTEXT { UINT64 FilterIdV6; HANDLE RedirectHandle; - // Configuration + // Configuration (protected by ConfigLock) + FAST_MUTEX ConfigLock; WINREDIRECT_CONFIG Config; - BOOLEAN Running; + volatile LONG Running; - // Pending connections + // Pending connections (protected by PendingLock) LIST_ENTRY PendingList; - KSPIN_LOCK PendingLock; + FAST_MUTEX PendingLock; volatile LONG64 NextConnID; - // Timeout timer + // Timeout timer + work item WDFTIMER TimeoutTimer; + WDFWORKITEM TimeoutWorkItem; } DRIVER_CONTEXT, *PDRIVER_CONTEXT; WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DRIVER_CONTEXT, GetDriverContext) @@ -102,6 +104,7 @@ EVT_WDF_DRIVER_UNLOAD EvtDriverUnload; EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL EvtIoDeviceControl; EVT_WDF_IO_QUEUE_IO_CANCELED_ON_QUEUE EvtIoCanceledOnQueue; EVT_WDF_TIMER EvtTimeoutTimer; +EVT_WDF_WORKITEM EvtTimeoutWorkItem; // WFP functions NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx); From 9a64b3db776227f48d40e14d370213990b711b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 01:23:57 +0800 Subject: [PATCH 04/38] Add VS project files for winredirect driver --- internal/winredirect/driver/winredirect.sln | 40 ++++++++ .../winredirect/driver/winredirect.vcxproj | 97 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 internal/winredirect/driver/winredirect.sln create mode 100644 internal/winredirect/driver/winredirect.vcxproj diff --git a/internal/winredirect/driver/winredirect.sln b/internal/winredirect/driver/winredirect.sln new file mode 100644 index 00000000..cb39766a --- /dev/null +++ b/internal/winredirect/driver/winredirect.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "winredirect", "winredirect.vcxproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345678}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|Win32 = Debug|Win32 + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|ARM.ActiveCfg = Debug|ARM + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|ARM.Build.0 = Debug|ARM + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|ARM64.Build.0 = Debug|ARM64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Win32.ActiveCfg = Debug|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Debug|Win32.Build.0 = Debug|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|ARM.ActiveCfg = Release|ARM + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|ARM.Build.0 = Release|ARM + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|ARM64.ActiveCfg = Release|ARM64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|ARM64.Build.0 = Release|ARM64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|Win32.ActiveCfg = Release|Win32 + {A1B2C3D4-1234-5678-9ABC-DEF012345678}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj new file mode 100644 index 00000000..1cdca5aa --- /dev/null +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -0,0 +1,97 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM + + + Release + ARM + + + Debug + ARM64 + + + Release + ARM64 + + + + {A1B2C3D4-1234-5678-9ABC-DEF012345678} + {1bc93793-694f-48fe-9372-81e2b05556fd} + winredirect + 12.0 + winredirect + + + + + true + + + false + + + WindowsKernelModeDriver10.0 + Driver + KMDF + Desktop + Windows10 + <_NT_TARGET_VERSION>0xA000002 + + + + + + + + + + $(SolutionDir)build\$(Platform)\$(Configuration)\ + $(SolutionDir)intermediate\$(Platform)\$(Configuration)\ + + + + + %(AdditionalIncludeDirectories) + NDIS_WDM;NDIS630;NDIS_SUPPORT_NDIS6;%(PreprocessorDefinitions) + false + + + Fwpkclnt.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) + + + true + + + + + + + + + + + + + + + From 0747b28a01b888d42b7c1f2b4d383c470cf80c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 01:24:41 +0800 Subject: [PATCH 05/38] Disable Spectre mitigation for driver build --- internal/winredirect/driver/winredirect.vcxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj index 1cdca5aa..b9cb08e6 100644 --- a/internal/winredirect/driver/winredirect.vcxproj +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -74,6 +74,7 @@ %(AdditionalIncludeDirectories) NDIS_WDM;NDIS630;NDIS_SUPPORT_NDIS6;%(PreprocessorDefinitions) false + false Fwpkclnt.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) From fae30d8036d0b79e853cd33eab714ab3ee25ec43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 01:25:12 +0800 Subject: [PATCH 06/38] Fix: move SpectreMitigation to PropertyGroup level --- internal/winredirect/driver/winredirect.vcxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj index b9cb08e6..5b0dd61d 100644 --- a/internal/winredirect/driver/winredirect.vcxproj +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -56,6 +56,7 @@ Desktop Windows10 <_NT_TARGET_VERSION>0xA000002 + false From 55ca6729e701cc0ce56cf2eccc128c3835e21e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 01:26:57 +0800 Subject: [PATCH 07/38] Fix driver: downgrade to WFP v1 APIs for older WDK compatibility --- internal/winredirect/driver/winredirect.c | 20 ++++++++++---------- internal/winredirect/driver/winredirect.h | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 3524a570..b150c795 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -251,20 +251,20 @@ NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx) if (!NT_SUCCESS(status)) goto cleanup; // Register callouts - FWPS_CALLOUT3 sCalloutV4 = { + FWPS_CALLOUT1 sCalloutV4 = { .calloutKey = WINREDIRECT_CALLOUT_V4_KEY, .classifyFn = ClassifyFnV4, .notifyFn = NotifyFn, }; - status = FwpsCalloutRegister3(WdfDeviceWdmGetDeviceObject(Ctx->Device), &sCalloutV4, &Ctx->CalloutIdV4); + status = FwpsCalloutRegister1(WdfDeviceWdmGetDeviceObject(Ctx->Device), &sCalloutV4, &Ctx->CalloutIdV4); if (!NT_SUCCESS(status)) goto cleanup; - FWPS_CALLOUT3 sCalloutV6 = { + FWPS_CALLOUT1 sCalloutV6 = { .calloutKey = WINREDIRECT_CALLOUT_V6_KEY, .classifyFn = ClassifyFnV6, .notifyFn = NotifyFn, }; - status = FwpsCalloutRegister3(WdfDeviceWdmGetDeviceObject(Ctx->Device), &sCalloutV6, &Ctx->CalloutIdV6); + status = FwpsCalloutRegister1(WdfDeviceWdmGetDeviceObject(Ctx->Device), &sCalloutV6, &Ctx->CalloutIdV6); if (!NT_SUCCESS(status)) goto cleanup; // Add callouts to BFE @@ -345,7 +345,7 @@ void WfpCleanup(_In_ PDRIVER_CONTEXT Ctx) NTSTATUS NTAPI NotifyFn( _In_ FWPS_CALLOUT_NOTIFY_TYPE notifyType, _In_ const GUID* filterKey, - _Inout_ FWPS_FILTER3* filter) + _Inout_ FWPS_FILTER1* filter) { UNREFERENCED_PARAMETER(notifyType); UNREFERENCED_PARAMETER(filterKey); @@ -361,7 +361,7 @@ static void ClassifyFnCommon( _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, - _In_ const FWPS_FILTER3* filter, + _In_ const FWPS_FILTER1* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut, _In_ UINT32 localAddrIdx, @@ -376,7 +376,7 @@ static void ClassifyFnCommon( } // Allocate pending entry - PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_PAGED, sizeof(PENDING_ENTRY), 'rniW'); + PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { classifyOut->actionType = FWP_ACTION_PERMIT; return; @@ -460,7 +460,7 @@ void NTAPI ClassifyFnV4( _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, - _In_ const FWPS_FILTER3* filter, + _In_ const FWPS_FILTER1* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut) { @@ -477,7 +477,7 @@ void NTAPI ClassifyFnV6( _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, - _In_ const FWPS_FILTER3* filter, + _In_ const FWPS_FILTER1* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut) { @@ -494,7 +494,7 @@ void NTAPI ClassifyFnV6( PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx) { UNREFERENCED_PARAMETER(Ctx); - return (PPENDING_ENTRY)ExAllocatePool2(POOL_FLAG_PAGED, sizeof(PENDING_ENTRY), 'rniW'); + return (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); } void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 986eb1c1..4d5549e9 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -116,7 +116,7 @@ void NTAPI ClassifyFnV4( _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, - _In_ const FWPS_FILTER3* filter, + _In_ const FWPS_FILTER1* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut ); @@ -126,7 +126,7 @@ void NTAPI ClassifyFnV6( _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ void* layerData, _In_opt_ const void* classifyContext, - _In_ const FWPS_FILTER3* filter, + _In_ const FWPS_FILTER1* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut ); @@ -134,7 +134,7 @@ void NTAPI ClassifyFnV6( NTSTATUS NTAPI NotifyFn( _In_ FWPS_CALLOUT_NOTIFY_TYPE notifyType, _In_ const GUID* filterKey, - _Inout_ FWPS_FILTER3* filter + _Inout_ FWPS_FILTER1* filter ); // Pending management From d66a9a825fd9ae83769317bfd23d61a165b63eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 01:27:55 +0800 Subject: [PATCH 08/38] Fix FwpsPendClassify0 signature, suppress deprecation warnings --- internal/winredirect/driver/winredirect.c | 6 +++--- internal/winredirect/driver/winredirect.vcxproj | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index b150c795..352d9a86 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -1,3 +1,5 @@ +#pragma warning(disable: 4996) // ExAllocatePoolWithTag deprecation + #include "winredirect.h" // {E513903C-D2F3-4D8C-9458-0483E7D7A01F} @@ -419,9 +421,7 @@ static void ClassifyFnCommon( entry->ClassifyHandle = classifyHandle; - status = FwpsPendClassify0( - inFixedValues, inMetaValues, layerData, - classifyContext, filter, flowContext, classifyOut); + status = FwpsPendClassify0(classifyHandle, filter->filterId, 0, classifyOut); if (!NT_SUCCESS(status)) { FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj index 5b0dd61d..b2f3d271 100644 --- a/internal/winredirect/driver/winredirect.vcxproj +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -83,6 +83,9 @@ true + + false + From b3d27a4f5d958d62b4e325f4f4c7c25d88b5ca44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 01:29:12 +0800 Subject: [PATCH 09/38] Fix linker: add initguid.h, wdmsec.lib, uuid.lib; disable InfVerif --- internal/winredirect/driver/winredirect.h | 2 ++ internal/winredirect/driver/winredirect.vcxproj | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 4d5549e9..43527486 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include #include diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj index b2f3d271..267f5b0c 100644 --- a/internal/winredirect/driver/winredirect.vcxproj +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -78,7 +78,7 @@ false - Fwpkclnt.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) + Fwpkclnt.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)uuid.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) true @@ -86,6 +86,9 @@ false + + sha256 + @@ -95,7 +98,9 @@ - + + true + From 4592a8cda7ac1100e5736b9dafb4fd11d4400cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 01:29:57 +0800 Subject: [PATCH 10/38] Remove uuid.lib, override InfVerif/Inf2Cat targets --- internal/winredirect/driver/winredirect.vcxproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj index 267f5b0c..151b56cf 100644 --- a/internal/winredirect/driver/winredirect.vcxproj +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -78,7 +78,7 @@ false - Fwpkclnt.lib;$(DDK_LIB_PATH)wdmsec.lib;$(DDK_LIB_PATH)uuid.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) + Fwpkclnt.lib;$(DDK_LIB_PATH)wdmsec.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) true @@ -103,5 +103,8 @@ + + + From 035a441c179ed077ca9e51b27c2469bac937b6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 10:21:18 +0800 Subject: [PATCH 11/38] Fix BSOD: add FWPS_RIGHT_ACTION_WRITE check, NULL pointer guards for IPv6, safe ExecuteVerdict fallback --- internal/winredirect/driver/winredirect.c | 34 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 352d9a86..818f06de 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -377,6 +377,11 @@ static void ClassifyFnCommon( return; } + // Must have write rights to modify the classify decision + if (!(classifyOut->rights & FWPS_RIGHT_ACTION_WRITE)) { + return; + } + // Allocate pending entry PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { @@ -389,7 +394,7 @@ static void ClassifyFnCommon( entry->AddressFamily = addressFamily; entry->FilterId = filter->filterId; - // Extract addresses + // Extract addresses with NULL checks for IPv6 pointers if (addressFamily == AF_INET) { UINT32 srcIp = inFixedValues->incomingValue[localAddrIdx].value.uint32; UINT32 dstIp = inFixedValues->incomingValue[remoteAddrIdx].value.uint32; @@ -397,10 +402,19 @@ static void ClassifyFnCommon( *(UINT32*)entry->SrcAddr = RtlUlongByteSwap(srcIp); *(UINT32*)entry->DstAddr = RtlUlongByteSwap(dstIp); } else { - RtlCopyMemory(entry->SrcAddr, - inFixedValues->incomingValue[localAddrIdx].value.byteArray16->byteArray16, 16); - RtlCopyMemory(entry->DstAddr, - inFixedValues->incomingValue[remoteAddrIdx].value.byteArray16->byteArray16, 16); + FWP_BYTE_ARRAY16* srcArr = inFixedValues->incomingValue[localAddrIdx].value.byteArray16; + FWP_BYTE_ARRAY16* dstArr = inFixedValues->incomingValue[remoteAddrIdx].value.byteArray16; + if (srcArr) { + RtlCopyMemory(entry->SrcAddr, srcArr->byteArray16, 16); + } + if (dstArr) { + RtlCopyMemory(entry->DstAddr, dstArr->byteArray16, 16); + } else { + // No destination address available — cannot redirect, bail out + ExFreePoolWithTag(entry, 'rniW'); + classifyOut->actionType = FWP_ACTION_PERMIT; + return; + } } entry->SrcPort = inFixedValues->incomingValue[localPortIdx].value.uint16; entry->DstPort = inFixedValues->incomingValue[remotePortIdx].value.uint16; @@ -548,6 +562,7 @@ void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx) void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UINT32 Verdict) { FWPS_CLASSIFY_OUT0 classifyOut = { 0 }; + classifyOut.rights = FWPS_RIGHT_ACTION_WRITE; if (Verdict == VERDICT_REDIRECT) { FWPS_CONNECT_REQUEST0* connReq = NULL; @@ -555,7 +570,13 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI Entry->ClassifyHandle, Entry->FilterId, 0, (PVOID*)&connReq, &classifyOut); - if (NT_SUCCESS(status) && connReq) { + if (!NT_SUCCESS(status) || !connReq) { + // Failed to acquire writable data — fall back to bypass + Verdict = VERDICT_BYPASS; + goto complete; + } + + if (TRUE) { ExAcquireFastMutex(&Ctx->ConfigLock); WINREDIRECT_CONFIG config = Ctx->Config; ExReleaseFastMutex(&Ctx->ConfigLock); @@ -581,6 +602,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI classifyOut.actionType = FWP_ACTION_PERMIT; classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; } else if (Verdict == VERDICT_BYPASS) { +complete: classifyOut.actionType = FWP_ACTION_PERMIT; classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; } else { // VERDICT_DROP From db8080690434d27c6605eec66c0704b6d97249a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 12:04:28 +0800 Subject: [PATCH 12/38] Fix Windows auto-redirect async redirect flow --- cmd/test_auto_redirect/main.go | 170 +++++++++ cmd/test_redirect/main.go | 337 +++++++++++++++++ internal/winredirect/amd64/winredirect.sys | Bin 11 -> 19264 bytes internal/winredirect/driver/winredirect.c | 421 ++++++++++++++++----- internal/winredirect/driver/winredirect.h | 9 +- redirect_server.go | 12 +- redirect_server_windows.go | 30 +- redirect_windows.go | 5 + 8 files changed, 881 insertions(+), 103 deletions(-) create mode 100644 cmd/test_auto_redirect/main.go create mode 100644 cmd/test_redirect/main.go diff --git a/cmd/test_auto_redirect/main.go b/cmd/test_auto_redirect/main.go new file mode 100644 index 00000000..66da9204 --- /dev/null +++ b/cmd/test_auto_redirect/main.go @@ -0,0 +1,170 @@ +//go:build windows + +package main + +import ( + "context" + "fmt" + "net" + "net/netip" + "os" + "os/exec" + "strings" + "time" + + tun "github.com/sagernet/sing-tun" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +const ( + autoClientModeArg = "client" + autoTestTarget = "198.18.0.1:65000" +) + +type testHandler struct{} + +func (h *testHandler) PrepareConnection( + network string, + source M.Socksaddr, + destination M.Socksaddr, + routeContext tun.DirectRouteContext, + timeout time.Duration, +) (tun.DirectRouteDestination, error) { + return nil, nil +} + +func (h *testHandler) NewConnectionEx( + ctx context.Context, + conn net.Conn, + source M.Socksaddr, + destination M.Socksaddr, + onClose N.CloseHandlerFunc, +) { + defer conn.Close() + if onClose != nil { + defer onClose(nil) + } + if metadata := tun.AutoRedirectMetadataFromContext(ctx); metadata != nil { + fmt.Printf("[redirect] source=%s destination=%s pid=%d path=%s\n", + source, destination, metadata.ProcessID, metadata.ProcessPath) + } else { + fmt.Printf("[redirect] source=%s destination=%s metadata=\n", source, destination) + } + _, _ = conn.Write([]byte("AUTO REDIRECT OK\n")) +} + +func (h *testHandler) NewPacketConnectionEx( + ctx context.Context, + conn N.PacketConn, + source M.Socksaddr, + destination M.Socksaddr, + onClose N.CloseHandlerFunc, +) { + if onClose != nil { + onClose(nil) + } + _ = conn.Close() +} + +func main() { + var err error + if len(os.Args) > 1 && os.Args[1] == autoClientModeArg { + err = runClient(autoTestTarget) + } else { + err = runAutoRedirect() + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func runAutoRedirect() error { + options := tun.Options{ + Inet4Address: []netip.Prefix{netip.MustParsePrefix("198.18.0.2/30")}, + } + + redirect, err := tun.NewAutoRedirect(tun.AutoRedirectOptions{ + TunOptions: &options, + Context: context.Background(), + Handler: &testHandler{}, + }) + if err != nil { + return fmt.Errorf("new auto redirect: %w", err) + } + defer redirect.Close() + + if err = redirect.Start(); err != nil { + return fmt.Errorf("start auto redirect: %w", err) + } + + time.Sleep(500 * time.Millisecond) + fmt.Printf("[1] AutoRedirect started\n") + + fmt.Printf("[2] Self-dialing %s (should bypass) ...\n", autoTestTarget) + if err = expectBypass(autoTestTarget); err != nil { + return fmt.Errorf("self bypass check failed: %w", err) + } + fmt.Println("[2] Bypass confirmed") + + fmt.Printf("[3] Child client dialing %s (should redirect) ...\n", autoTestTarget) + output, err := runExternalClient() + if err != nil { + return fmt.Errorf("child client failed: %w\n%s", err, output) + } + fmt.Print(output) + if !strings.Contains(output, "AUTO REDIRECT OK") { + return fmt.Errorf("child client did not receive redirected response") + } + fmt.Println("[3] Redirect confirmed") + + return nil +} + +func runClient(target string) error { + fmt.Printf("[client] Dialing %s ...\n", target) + conn, err := net.DialTimeout("tcp", target, 5*time.Second) + if err != nil { + return fmt.Errorf("client dial: %w", err) + } + defer conn.Close() + + bufData := make([]byte, 256) + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := conn.Read(bufData) + if err != nil { + return fmt.Errorf("client read: %w", err) + } + fmt.Printf("[client] Response: %q\n", string(bufData[:n])) + return nil +} + +func runExternalClient() (string, error) { + executable, err := os.Executable() + if err != nil { + return "", err + } + cmd := exec.Command(executable, autoClientModeArg) + output, err := cmd.CombinedOutput() + return string(output), err +} + +func expectBypass(target string) error { + conn, err := net.DialTimeout("tcp", target, 1200*time.Millisecond) + if err == nil { + defer conn.Close() + bufData := make([]byte, 64) + _ = conn.SetReadDeadline(time.Now().Add(300 * time.Millisecond)) + n, readErr := conn.Read(bufData) + if n > 0 && string(bufData[:n]) == "AUTO REDIRECT OK\n" { + return fmt.Errorf("proxy process was redirected unexpectedly") + } + if readErr == nil { + return fmt.Errorf("unexpected data from bypass connection: %q", string(bufData[:n])) + } + } + return nil +} + +var _ tun.Handler = (*testHandler)(nil) diff --git a/cmd/test_redirect/main.go b/cmd/test_redirect/main.go new file mode 100644 index 00000000..9d7b57b1 --- /dev/null +++ b/cmd/test_redirect/main.go @@ -0,0 +1,337 @@ +//go:build windows + +package main + +import ( + "fmt" + "net" + "net/netip" + "os" + "os/exec" + "strings" + "sync" + "sync/atomic" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + ioctlSetConfig = 0x00120000 | (0x800 << 2) + ioctlStart = 0x00120000 | (0x801 << 2) + ioctlStop = 0x00120000 | (0x802 << 2) + ioctlGetPending = 0x00120000 | (0x803 << 2) + ioctlSetVerdict = 0x00120000 | (0x804 << 2) +) + +type Config struct { + RedirectPort uint16 + _ [2]byte + ProxyPID uint32 +} + +type PendingConn struct { + ConnID uint64 + AddressFamily uint8 + _pad0 [3]byte + SrcAddr [16]byte + SrcPort uint16 + _pad1 [2]byte + DstAddr [16]byte + DstPort uint16 + _pad2 [2]byte + ProcessID uint32 +} + +type VerdictMsg struct { + ConnID uint64 + Verdict uint32 + _pad [4]byte +} + +var ( + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procCancelIoEx = modkernel32.NewProc("CancelIoEx") +) + +const ( + clientModeArg = "client" + testTarget = "198.18.0.1:65000" +) + +var targetVerdict = strings.ToUpper(strings.TrimSpace(os.Getenv("WINREDIRECT_TARGET_VERDICT"))) + +func main() { + var err error + if len(os.Args) > 1 && os.Args[1] == clientModeArg { + err = runClient(testTarget) + } else { + err = runProxy() + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func runProxy() error { + // 1. Start redirect TCP listener + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return fmt.Errorf("listen: %w", err) + } + defer listener.Close() + port := listener.Addr().(*net.TCPAddr).Port + fmt.Printf("[1] Redirect server on 127.0.0.1:%d\n", port) + + var wg sync.WaitGroup + var acceptedCount atomic.Int32 + wg.Add(1) + go func() { + defer wg.Done() + for { + conn, err := listener.Accept() + if err != nil { + return + } + acceptedCount.Add(1) + fmt.Printf("[redirect] Accepted from %s\n", conn.RemoteAddr()) + conn.Write([]byte("REDIRECTED OK\n")) + conn.Close() + } + }() + + // 2. Open driver + device := openDevice() + defer windows.CloseHandle(device) + fmt.Println("[2] Device opened") + + // 3. Configure + cfg := Config{RedirectPort: uint16(port), ProxyPID: uint32(os.Getpid())} + err = devctl(device, ioctlSetConfig, unsafe.Pointer(&cfg), unsafe.Sizeof(cfg), nil, 0) + if err != nil { + return fmt.Errorf("SET_CONFIG: %w", err) + } + fmt.Printf("[3] Config: port=%d pid=%d\n", port, os.Getpid()) + + // 4. Start + err = devctl(device, ioctlStart, nil, 0, nil, 0) + if err != nil { + return fmt.Errorf("START: %w", err) + } + fmt.Println("[4] WFP started — all TCP connections will be intercepted") + + // 5. Pre-match worker + stopCh := make(chan struct{}) + defer close(stopCh) + wg.Add(1) + go func() { + defer wg.Done() + pendingWorker(device, stopCh, port) + }() + defer wg.Wait() + defer devctl(device, ioctlStop, nil, 0, nil, 0) + + // 6. Proxy process should bypass its own outbound connect. + time.Sleep(300 * time.Millisecond) + fmt.Printf("\n[5] Proxy self-dialing %s (should bypass) ...\n", testTarget) + err = expectBypass(testTarget, &acceptedCount) + if err != nil { + return fmt.Errorf("self-bypass check failed: %w", err) + } + fmt.Println("[5] Bypass confirmed") + + // 7. External client process should be redirected. + fmt.Printf("\n[6] Child client dialing %s (should redirect) ...\n", testTarget) + if targetVerdict == "BYPASS" { + fmt.Println("[6] Target verdict override: BYPASS") + } + output, err := runExternalClient() + if err != nil { + return fmt.Errorf("child client failed: %w\n%s", err, output) + } + fmt.Print(output) + if !strings.Contains(output, "REDIRECTED OK") { + return fmt.Errorf("child client did not receive redirected response") + } + if acceptedCount.Load() != 1 { + return fmt.Errorf("redirect server accepted %d connections, want 1", acceptedCount.Load()) + } + fmt.Println("[6] Redirect confirmed") + + // 8. Cleanup + time.Sleep(300 * time.Millisecond) + fmt.Println("\n[7] Stopped") + fmt.Println("[8] Done!") + return nil +} + +func runClient(target string) error { + fmt.Printf("[client] Dialing %s ...\n", target) + conn, err := net.DialTimeout("tcp", target, 5*time.Second) + if err != nil { + return fmt.Errorf("client dial: %w", err) + } + defer conn.Close() + + buf := make([]byte, 256) + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := conn.Read(buf) + if err != nil { + return fmt.Errorf("client read: %w", err) + } + fmt.Printf("[client] Response: %q\n", string(buf[:n])) + return nil +} + +func runExternalClient() (string, error) { + executable, err := os.Executable() + if err != nil { + return "", err + } + cmd := exec.Command(executable, clientModeArg) + output, err := cmd.CombinedOutput() + return string(output), err +} + +func expectBypass(target string, acceptedCount *atomic.Int32) error { + initialAccepted := acceptedCount.Load() + conn, err := net.DialTimeout("tcp", target, 1200*time.Millisecond) + if err == nil { + defer conn.Close() + buf := make([]byte, 64) + _ = conn.SetReadDeadline(time.Now().Add(300 * time.Millisecond)) + n, readErr := conn.Read(buf) + if n > 0 && string(buf[:n]) == "REDIRECTED OK\n" { + return fmt.Errorf("proxy process was redirected unexpectedly") + } + if readErr == nil { + return fmt.Errorf("unexpected data from bypass connection: %q", string(buf[:n])) + } + } + time.Sleep(300 * time.Millisecond) + if acceptedCount.Load() != initialAccepted { + return fmt.Errorf("redirect server accepted proxy self-connection") + } + return nil +} + +func pendingWorker(device windows.Handle, stop <-chan struct{}, redirectPort int) { + for { + select { + case <-stop: + return + default: + } + + var conn PendingConn + err := devctlOverlapped(device, ioctlGetPending, nil, 0, + unsafe.Pointer(&conn), unsafe.Sizeof(conn), 2*time.Second) + if err != nil { + continue + } + + src := fmtAddr(conn.AddressFamily, conn.SrcAddr, conn.SrcPort) + dst := fmtAddr(conn.AddressFamily, conn.DstAddr, conn.DstPort) + proc := procName(conn.ProcessID) + + fmt.Printf("[>] connID=%d %s -> %s pid=%d (%s)\n", + conn.ConnID, src, dst, conn.ProcessID, proc) + + verdict := uint32(1) // BYPASS + verdictText := "BYPASS" + if dst == testTarget && targetVerdict != "BYPASS" { + verdict = 0 // REDIRECT + verdictText = fmt.Sprintf("REDIRECT -> 127.0.0.1:%d", redirectPort) + } + + v := VerdictMsg{ConnID: conn.ConnID, Verdict: verdict} + err = devctl(device, ioctlSetVerdict, unsafe.Pointer(&v), unsafe.Sizeof(v), nil, 0) + if err != nil { + fmt.Printf("[!] verdict failed: %v\n", err) + } else { + fmt.Printf("[<] connID=%d %s\n", conn.ConnID, verdictText) + } + } +} + +func openDevice() windows.Handle { + p, _ := windows.UTF16PtrFromString(`\\.\WinRedirect`) + h, err := windows.CreateFile(p, + windows.GENERIC_READ|windows.GENERIC_WRITE, + 0, nil, windows.OPEN_EXISTING, + windows.FILE_FLAG_OVERLAPPED, 0) + fatal("CreateFile", err) + return h +} + +func devctl(h windows.Handle, code uint32, in unsafe.Pointer, inSz uintptr, out unsafe.Pointer, outSz uintptr) error { + var ret uint32 + return windows.DeviceIoControl(h, code, + (*byte)(in), uint32(inSz), + (*byte)(out), uint32(outSz), + &ret, nil) +} + +func devctlOverlapped(h windows.Handle, code uint32, in unsafe.Pointer, inSz uintptr, out unsafe.Pointer, outSz uintptr, timeout time.Duration) error { + ol := &windows.Overlapped{} + var err error + ol.HEvent, err = windows.CreateEvent(nil, 1, 0, nil) + if err != nil { + return err + } + defer windows.CloseHandle(ol.HEvent) + + var ret uint32 + err = windows.DeviceIoControl(h, code, + (*byte)(in), uint32(inSz), + (*byte)(out), uint32(outSz), + &ret, ol) + if err == windows.ERROR_IO_PENDING { + r, _ := windows.WaitForSingleObject(ol.HEvent, uint32(timeout.Milliseconds())) + if r != windows.WAIT_OBJECT_0 { + procCancelIoEx.Call(uintptr(h), uintptr(unsafe.Pointer(ol))) + return fmt.Errorf("timeout") + } + err = windows.GetOverlappedResult(h, ol, &ret, false) + } + return err +} + +func fmtAddr(af uint8, raw [16]byte, port uint16) string { + var addr netip.Addr + if af == 2 { + addr = netip.AddrFrom4([4]byte(raw[:4])) + } else { + addr = netip.AddrFrom16(raw) + } + return netip.AddrPortFrom(addr, port).String() +} + +func procName(pid uint32) string { + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + if err != nil { + return "?" + } + defer windows.CloseHandle(h) + var buf [260]uint16 + n := uint32(260) + if windows.QueryFullProcessImageName(h, 0, &buf[0], &n) != nil { + return "?" + } + s := windows.UTF16ToString(buf[:n]) + for i := len(s) - 1; i >= 0; i-- { + if s[i] == '\\' { + return s[i+1:] + } + } + return s +} + +func fatal(ctx string, err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", ctx, err) + os.Exit(1) + } +} diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys index 311c8dd0658759f372c9be9a2065c327fdb1a203..a0315527f8ee73133468af342071f2d90e9947c9 100644 GIT binary patch literal 19264 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�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 literal 11 ScmWIWaddX|@b__X4FUiX$^#?- diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 818f06de..b202f676 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -20,6 +20,141 @@ DEFINE_GUID(WINREDIRECT_CALLOUT_V6_KEY, static PDRIVER_CONTEXT g_Ctx = NULL; +static void PermitClassify(_Inout_ FWPS_CLASSIFY_OUT0* classifyOut) +{ + classifyOut->actionType = FWP_ACTION_PERMIT; + classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE; +} + +static BOOLEAN IsLoopbackAddress(_In_ UINT8 AddressFamily, _In_reads_(16) const UINT8* Address) +{ + if (AddressFamily == AF_INET) { + return Address[0] == 127; + } + + if (AddressFamily == AF_INET6) { + for (UINT32 i = 0; i < 15; i++) { + if (Address[i] != 0) { + return FALSE; + } + } + return Address[15] == 1; + } + + return FALSE; +} + +static BOOLEAN IsAnyAddress(_In_ UINT8 AddressFamily, _In_reads_(16) const UINT8* Address) +{ + if (AddressFamily == AF_INET) { + return Address[0] == 0 && Address[1] == 0 && Address[2] == 0 && Address[3] == 0; + } + + if (AddressFamily == AF_INET6) { + for (UINT32 i = 0; i < 16; i++) { + if (Address[i] != 0) { + return FALSE; + } + } + return TRUE; + } + + return FALSE; +} + +typedef struct _LOCAL_REDIRECT_CONTEXT { + SOCKADDR_STORAGE OriginalRemoteAddressAndPort; + UINT32 ProcessId; +} LOCAL_REDIRECT_CONTEXT, *PLOCAL_REDIRECT_CONTEXT; + +static WINREDIRECT_CONFIG ReadConfigSnapshot(_In_ PDRIVER_CONTEXT Ctx) +{ + WINREDIRECT_CONFIG config; + KIRQL oldIrql; + + RtlZeroMemory(&config, sizeof(config)); + KeAcquireSpinLock(&Ctx->ConfigLock, &oldIrql); + config = Ctx->Config; + KeReleaseSpinLock(&Ctx->ConfigLock, oldIrql); + + return config; +} + +static void CancelPendingIoctlRequests(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status) +{ + WDFREQUEST request; + + while (NT_SUCCESS(WdfIoQueueRetrieveNextRequest(Ctx->PendingIoctlQueue, &request))) { + WdfRequestComplete(request, Status); + } +} + +static PPENDING_ENTRY PendingReserveNextUndelivered(_In_ PDRIVER_CONTEXT Ctx) +{ + PPENDING_ENTRY found = NULL; + KIRQL oldIrql; + + KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); + PLIST_ENTRY entry = Ctx->PendingList.Flink; + while (entry != &Ctx->PendingList) { + PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + if (!pending->Delivered) { + pending->Delivered = TRUE; + found = pending; + break; + } + entry = entry->Flink; + } + KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); + + return found; +} + +static void PendingSetDelivered(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ BOOLEAN Delivered) +{ + KIRQL oldIrql; + + KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); + Entry->Delivered = Delivered; + KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); +} + +static void TryCompletePendingRequests(_In_ PDRIVER_CONTEXT Ctx) +{ + for (;;) { + PPENDING_ENTRY pending = PendingReserveNextUndelivered(Ctx); + if (!pending) { + break; + } + + WDFREQUEST request; + NTSTATUS status = WdfIoQueueRetrieveNextRequest(Ctx->PendingIoctlQueue, &request); + if (!NT_SUCCESS(status)) { + PendingSetDelivered(Ctx, pending, FALSE); + break; + } + + PVOID outBuf; + status = WdfRequestRetrieveOutputBuffer(request, sizeof(WINREDIRECT_PENDING_CONN), &outBuf, NULL); + if (!NT_SUCCESS(status)) { + PendingSetDelivered(Ctx, pending, FALSE); + WdfRequestComplete(request, status); + continue; + } + + WINREDIRECT_PENDING_CONN* out = (WINREDIRECT_PENDING_CONN*)outBuf; + RtlZeroMemory(out, sizeof(*out)); + out->ConnID = pending->ConnID; + out->AddressFamily = pending->AddressFamily; + RtlCopyMemory(out->SrcAddr, pending->SrcAddr, 16); + out->SrcPort = pending->SrcPort; + RtlCopyMemory(out->DstAddr, pending->DstAddr, 16); + out->DstPort = pending->DstPort; + out->ProcessID = pending->ProcessID; + WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, sizeof(WINREDIRECT_PENDING_CONN)); + } +} + NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { NTSTATUS status; @@ -61,8 +196,8 @@ NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING Regi RtlZeroMemory(ctx, sizeof(DRIVER_CONTEXT)); ctx->Device = device; InitializeListHead(&ctx->PendingList); - ExInitializeFastMutex(&ctx->PendingLock); - ExInitializeFastMutex(&ctx->ConfigLock); + KeInitializeSpinLock(&ctx->PendingLock); + KeInitializeSpinLock(&ctx->ConfigLock); g_Ctx = ctx; // Create manual-dispatch queue for pending IOCTLs @@ -97,6 +232,14 @@ NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING Regi status = WdfWorkItemCreate(&workItemConfig, &workItemAttrs, &ctx->TimeoutWorkItem); if (!NT_SUCCESS(status)) return status; + WDF_WORKITEM_CONFIG pendingDeliveryConfig; + WDF_WORKITEM_CONFIG_INIT(&pendingDeliveryConfig, EvtPendingDeliveryWorkItem); + WDF_OBJECT_ATTRIBUTES pendingDeliveryAttrs; + WDF_OBJECT_ATTRIBUTES_INIT(&pendingDeliveryAttrs); + pendingDeliveryAttrs.ParentObject = device; + status = WdfWorkItemCreate(&pendingDeliveryConfig, &pendingDeliveryAttrs, &ctx->PendingDeliveryWorkItem); + if (!NT_SUCCESS(status)) return status; + WdfControlFinishInitializing(device); return STATUS_SUCCESS; } @@ -106,7 +249,9 @@ void EvtDriverUnload(_In_ WDFDRIVER Driver) UNREFERENCED_PARAMETER(Driver); if (g_Ctx) { WfpCleanup(g_Ctx); + WdfWorkItemFlush(g_Ctx->PendingDeliveryWorkItem); PendingFlushAll(g_Ctx); + CancelPendingIoctlRequests(g_Ctx, STATUS_CANCELLED); } } @@ -127,9 +272,10 @@ void EvtIoDeviceControl( case IOCTL_WINREDIRECT_SET_CONFIG: status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_CONFIG), &inBuf, &inLen); if (NT_SUCCESS(status)) { - ExAcquireFastMutex(&ctx->ConfigLock); + KIRQL oldIrql; + KeAcquireSpinLock(&ctx->ConfigLock, &oldIrql); RtlCopyMemory(&ctx->Config, inBuf, sizeof(WINREDIRECT_CONFIG)); - ExReleaseFastMutex(&ctx->ConfigLock); + KeReleaseSpinLock(&ctx->ConfigLock, oldIrql); } WdfRequestComplete(Request, status); break; @@ -153,7 +299,9 @@ void EvtIoDeviceControl( WdfTimerStop(ctx->TimeoutTimer, TRUE); WdfWorkItemFlush(ctx->TimeoutWorkItem); WfpCleanup(ctx); + WdfWorkItemFlush(ctx->PendingDeliveryWorkItem); PendingFlushAll(ctx); + CancelPendingIoctlRequests(ctx, STATUS_CANCELLED); } WdfRequestComplete(Request, STATUS_SUCCESS); break; @@ -163,6 +311,8 @@ void EvtIoDeviceControl( status = WdfRequestForwardToIoQueue(Request, ctx->PendingIoctlQueue); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); + } else { + WdfWorkItemEnqueue(ctx->PendingDeliveryWorkItem); } break; @@ -208,26 +358,42 @@ void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) LARGE_INTEGER now; KeQuerySystemTime(&now); - ExAcquireFastMutex(&g_Ctx->PendingLock); + for (;;) { + KIRQL oldIrql; + PPENDING_ENTRY expired = NULL; + + KeAcquireSpinLock(&g_Ctx->PendingLock, &oldIrql); + + PLIST_ENTRY entry = g_Ctx->PendingList.Flink; + while (entry != &g_Ctx->PendingList) { + PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + entry = entry->Flink; + + // Auto-bypass entries older than 5 seconds + LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; // to seconds + if (elapsed >= 5) { + RemoveEntryList(&pending->ListEntry); + expired = pending; + break; + } + } - PLIST_ENTRY entry = g_Ctx->PendingList.Flink; - while (entry != &g_Ctx->PendingList) { - PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); - entry = entry->Flink; + KeReleaseSpinLock(&g_Ctx->PendingLock, oldIrql); - // Auto-bypass entries older than 5 seconds - LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; // to seconds - if (elapsed >= 5) { - RemoveEntryList(&pending->ListEntry); - ExReleaseFastMutex(&g_Ctx->PendingLock); - ExecuteVerdict(g_Ctx, pending, VERDICT_BYPASS); - ExFreePoolWithTag(pending, 'rniW'); - ExAcquireFastMutex(&g_Ctx->PendingLock); - entry = g_Ctx->PendingList.Flink; + if (!expired) { + break; } + + ExecuteVerdict(g_Ctx, expired, VERDICT_BYPASS); + ExFreePoolWithTag(expired, 'rniW'); } +} - ExReleaseFastMutex(&g_Ctx->PendingLock); +void EvtPendingDeliveryWorkItem(_In_ WDFWORKITEM WorkItem) +{ + UNREFERENCED_PARAMETER(WorkItem); + if (!g_Ctx || !g_Ctx->Running) return; + TryCompletePendingRequests(g_Ctx); } // --- WFP Setup --- @@ -373,7 +539,7 @@ static void ClassifyFnCommon( { PDRIVER_CONTEXT ctx = g_Ctx; if (!ctx || !ctx->Running) { - classifyOut->actionType = FWP_ACTION_PERMIT; + PermitClassify(classifyOut); return; } @@ -382,10 +548,37 @@ static void ClassifyFnCommon( return; } + WINREDIRECT_CONFIG config = ReadConfigSnapshot(ctx); + +#if (NTDDI_VERSION >= NTDDI_WIN8) + if (ctx->RedirectHandle && + FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_REDIRECT_RECORD_HANDLE)) { + FWPS_CONNECTION_REDIRECT_STATE redirectState = + FwpsQueryConnectionRedirectState0(inMetaValues->redirectRecords, ctx->RedirectHandle, NULL); + switch (redirectState) { + case FWPS_CONNECTION_REDIRECTED_BY_SELF: + case FWPS_CONNECTION_PREVIOUSLY_REDIRECTED_BY_SELF: + PermitClassify(classifyOut); + return; + case FWPS_CONNECTION_NOT_REDIRECTED: + case FWPS_CONNECTION_REDIRECTED_BY_OTHER: + default: + break; + } + } +#endif + + if (config.ProxyPID != 0 && + FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_PROCESS_ID) && + (UINT32)inMetaValues->processId == config.ProxyPID) { + PermitClassify(classifyOut); + return; + } + // Allocate pending entry PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { - classifyOut->actionType = FWP_ACTION_PERMIT; + PermitClassify(classifyOut); return; } @@ -393,6 +586,7 @@ static void ClassifyFnCommon( entry->ConnID = InterlockedIncrement64(&ctx->NextConnID); entry->AddressFamily = addressFamily; entry->FilterId = filter->filterId; + entry->ClassifyOut = *classifyOut; // Extract addresses with NULL checks for IPv6 pointers if (addressFamily == AF_INET) { @@ -412,61 +606,70 @@ static void ClassifyFnCommon( } else { // No destination address available — cannot redirect, bail out ExFreePoolWithTag(entry, 'rniW'); - classifyOut->actionType = FWP_ACTION_PERMIT; + PermitClassify(classifyOut); return; } } entry->SrcPort = inFixedValues->incomingValue[localPortIdx].value.uint16; entry->DstPort = inFixedValues->incomingValue[remotePortIdx].value.uint16; + if (IsLoopbackAddress(addressFamily, entry->DstAddr)) { + ExFreePoolWithTag(entry, 'rniW'); + PermitClassify(classifyOut); + return; + } + // Extract PID from metadata if (FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_PROCESS_ID)) { entry->ProcessID = (UINT32)inMetaValues->processId; } + if (!classifyContext) { + ExFreePoolWithTag(entry, 'rniW'); + PermitClassify(classifyOut); + return; + } + // Pend the classify UINT64 classifyHandle; - NTSTATUS status = FwpsAcquireClassifyHandle0(classifyOut, 0, &classifyHandle); + NTSTATUS status = FwpsAcquireClassifyHandle0((void*)classifyContext, 0, &classifyHandle); if (!NT_SUCCESS(status)) { ExFreePoolWithTag(entry, 'rniW'); - classifyOut->actionType = FWP_ACTION_PERMIT; + PermitClassify(classifyOut); return; } entry->ClassifyHandle = classifyHandle; + entry->ClassifyOut = *classifyOut; + + status = FwpsAcquireWritableLayerDataPointer0( + classifyHandle, filter->filterId, 0, + &entry->WritableLayerData, classifyOut); + if (!NT_SUCCESS(status) || !entry->WritableLayerData) { + FwpsReleaseClassifyHandle0(classifyHandle); + ExFreePoolWithTag(entry, 'rniW'); + PermitClassify(classifyOut); + return; + } status = FwpsPendClassify0(classifyHandle, filter->filterId, 0, classifyOut); if (!NT_SUCCESS(status)) { + FwpsApplyModifiedLayerData0( + classifyHandle, + entry->WritableLayerData, + FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS); FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); - classifyOut->actionType = FWP_ACTION_PERMIT; + PermitClassify(classifyOut); return; } + classifyOut->actionType = FWP_ACTION_BLOCK; + classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE; + KeQuerySystemTime(&entry->Timestamp); PendingInsert(ctx, entry); - - // Try to complete a waiting IOCTL_GET_PENDING - WDFREQUEST request; - status = WdfIoQueueRetrieveNextRequest(ctx->PendingIoctlQueue, &request); - if (NT_SUCCESS(status)) { - PVOID outBuf; - status = WdfRequestRetrieveOutputBuffer(request, sizeof(WINREDIRECT_PENDING_CONN), &outBuf, NULL); - if (NT_SUCCESS(status)) { - WINREDIRECT_PENDING_CONN* out = (WINREDIRECT_PENDING_CONN*)outBuf; - RtlZeroMemory(out, sizeof(*out)); - out->ConnID = entry->ConnID; - out->AddressFamily = entry->AddressFamily; - RtlCopyMemory(out->SrcAddr, entry->SrcAddr, 16); - out->SrcPort = entry->SrcPort; - RtlCopyMemory(out->DstAddr, entry->DstAddr, 16); - out->DstPort = entry->DstPort; - out->ProcessID = entry->ProcessID; - WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, sizeof(WINREDIRECT_PENDING_CONN)); - } else { - WdfRequestComplete(request, status); - } - } + WdfWorkItemEnqueue(ctx->PendingDeliveryWorkItem); } void NTAPI ClassifyFnV4( @@ -513,15 +716,17 @@ PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx) void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) { - ExAcquireFastMutex(&Ctx->PendingLock); + KIRQL oldIrql; + KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); InsertTailList(&Ctx->PendingList, &Entry->ListEntry); - ExReleaseFastMutex(&Ctx->PendingLock); + KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); } PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID) { PPENDING_ENTRY found = NULL; - ExAcquireFastMutex(&Ctx->PendingLock); + KIRQL oldIrql; + KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); PLIST_ENTRY entry = Ctx->PendingList.Flink; while (entry != &Ctx->PendingList) { PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); @@ -532,77 +737,103 @@ PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID) } entry = entry->Flink; } - ExReleaseFastMutex(&Ctx->PendingLock); + KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); return found; } void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) { - ExAcquireFastMutex(&Ctx->PendingLock); + KIRQL oldIrql; + KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); RemoveEntryList(&Entry->ListEntry); - ExReleaseFastMutex(&Ctx->PendingLock); + KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); } void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx) { - ExAcquireFastMutex(&Ctx->PendingLock); - while (!IsListEmpty(&Ctx->PendingList)) { - PLIST_ENTRY entry = RemoveHeadList(&Ctx->PendingList); - PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); - ExReleaseFastMutex(&Ctx->PendingLock); + for (;;) { + KIRQL oldIrql; + PPENDING_ENTRY pending = NULL; + + KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); + if (!IsListEmpty(&Ctx->PendingList)) { + PLIST_ENTRY entry = RemoveHeadList(&Ctx->PendingList); + pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + } + KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); + + if (!pending) { + break; + } + ExecuteVerdict(Ctx, pending, VERDICT_BYPASS); ExFreePoolWithTag(pending, 'rniW'); - ExAcquireFastMutex(&Ctx->PendingLock); } - ExReleaseFastMutex(&Ctx->PendingLock); } // --- Verdict execution --- void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UINT32 Verdict) { - FWPS_CLASSIFY_OUT0 classifyOut = { 0 }; - classifyOut.rights = FWPS_RIGHT_ACTION_WRITE; + FWPS_CLASSIFY_OUT0 classifyOut = Entry->ClassifyOut; + FWPS_CONNECT_REQUEST0* connReq = (FWPS_CONNECT_REQUEST0*)Entry->WritableLayerData; if (Verdict == VERDICT_REDIRECT) { - FWPS_CONNECT_REQUEST0* connReq = NULL; - NTSTATUS status = FwpsAcquireWritableLayerDataPointer0( - Entry->ClassifyHandle, Entry->FilterId, 0, - (PVOID*)&connReq, &classifyOut); - - if (!NT_SUCCESS(status) || !connReq) { - // Failed to acquire writable data — fall back to bypass + WINREDIRECT_CONFIG config = ReadConfigSnapshot(Ctx); + if (!connReq || + config.RedirectPort == 0 || config.ProxyPID == 0 || Ctx->RedirectHandle == NULL) { Verdict = VERDICT_BYPASS; - goto complete; - } - - if (TRUE) { - ExAcquireFastMutex(&Ctx->ConfigLock); - WINREDIRECT_CONFIG config = Ctx->Config; - ExReleaseFastMutex(&Ctx->ConfigLock); - - if (Entry->AddressFamily == AF_INET) { - SOCKADDR_IN* addr = (SOCKADDR_IN*)&connReq->remoteAddressAndPort; - addr->sin_family = AF_INET; - addr->sin_addr.s_addr = RtlUlongByteSwap(0x7F000001); // 127.0.0.1 - addr->sin_port = RtlUshortByteSwap(config.RedirectPort); + } else { + SOCKADDR_STORAGE* redirectContext = + (SOCKADDR_STORAGE*)ExAllocatePoolWithTag(NonPagedPool, sizeof(SOCKADDR_STORAGE) * 2, 'rniW'); + if (!redirectContext) { + Verdict = VERDICT_BYPASS; } else { - SOCKADDR_IN6* addr = (SOCKADDR_IN6*)&connReq->remoteAddressAndPort; - RtlZeroMemory(addr, sizeof(SOCKADDR_IN6)); - addr->sin6_family = AF_INET6; - addr->sin6_addr.u.Byte[15] = 1; // ::1 - addr->sin6_port = RtlUshortByteSwap(config.RedirectPort); + RtlZeroMemory(redirectContext, sizeof(SOCKADDR_STORAGE) * 2); + RtlCopyMemory(&redirectContext[0], &connReq->remoteAddressAndPort, sizeof(SOCKADDR_STORAGE)); + RtlCopyMemory(&redirectContext[1], &connReq->localAddressAndPort, sizeof(SOCKADDR_STORAGE)); + + connReq->localRedirectHandle = Ctx->RedirectHandle; + connReq->localRedirectTargetPID = config.ProxyPID; + connReq->localRedirectContext = redirectContext; + connReq->localRedirectContextSize = sizeof(SOCKADDR_STORAGE) * 2; + + if (Entry->AddressFamily == AF_INET) { + SOCKADDR_IN* localAddr = (SOCKADDR_IN*)&connReq->localAddressAndPort; + SOCKADDR_IN* addr = (SOCKADDR_IN*)&connReq->remoteAddressAndPort; + addr->sin_family = AF_INET; + if (localAddr->sin_addr.s_addr == 0) { + addr->sin_addr.s_addr = RtlUlongByteSwap(0x7F000001); // 127.0.0.1 + } else { + addr->sin_addr = localAddr->sin_addr; + } + addr->sin_port = RtlUshortByteSwap(config.RedirectPort); + } else { + SOCKADDR_IN6* localAddr = (SOCKADDR_IN6*)&connReq->localAddressAndPort; + SOCKADDR_IN6* addr = (SOCKADDR_IN6*)&connReq->remoteAddressAndPort; + if (IsAnyAddress(AF_INET6, localAddr->sin6_addr.u.Byte)) { + RtlZeroMemory(addr, sizeof(SOCKADDR_IN6)); + addr->sin6_family = AF_INET6; + addr->sin6_addr.u.Byte[15] = 1; // ::1 + } else { + *addr = *localAddr; + addr->sin6_family = AF_INET6; + } + addr->sin6_port = RtlUshortByteSwap(config.RedirectPort); + } } - connReq->localRedirectTargetPID = config.ProxyPID; - connReq->localRedirectHandle = Ctx->RedirectHandle; - - FwpsApplyModifiedLayerData0(Entry->ClassifyHandle, connReq, 0); } + } - classifyOut.actionType = FWP_ACTION_PERMIT; - classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; - } else if (Verdict == VERDICT_BYPASS) { -complete: + if (Entry->WritableLayerData) { + FwpsApplyModifiedLayerData0( + Entry->ClassifyHandle, + Entry->WritableLayerData, + FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS); + Entry->WritableLayerData = NULL; + } + + if (Verdict == VERDICT_REDIRECT || Verdict == VERDICT_BYPASS) { classifyOut.actionType = FWP_ACTION_PERMIT; classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; } else { // VERDICT_DROP diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 43527486..9ad3e47b 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -61,6 +61,9 @@ typedef struct _PENDING_ENTRY { UINT64 ConnID; UINT64 ClassifyHandle; UINT64 FilterId; + FWPS_CLASSIFY_OUT0 ClassifyOut; + PVOID WritableLayerData; + BOOLEAN Delivered; UINT8 AddressFamily; UINT8 SrcAddr[16]; UINT16 SrcPort; @@ -84,18 +87,19 @@ typedef struct _DRIVER_CONTEXT { HANDLE RedirectHandle; // Configuration (protected by ConfigLock) - FAST_MUTEX ConfigLock; + KSPIN_LOCK ConfigLock; WINREDIRECT_CONFIG Config; volatile LONG Running; // Pending connections (protected by PendingLock) LIST_ENTRY PendingList; - FAST_MUTEX PendingLock; + KSPIN_LOCK PendingLock; volatile LONG64 NextConnID; // Timeout timer + work item WDFTIMER TimeoutTimer; WDFWORKITEM TimeoutWorkItem; + WDFWORKITEM PendingDeliveryWorkItem; } DRIVER_CONTEXT, *PDRIVER_CONTEXT; WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DRIVER_CONTEXT, GetDriverContext) @@ -107,6 +111,7 @@ EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL EvtIoDeviceControl; EVT_WDF_IO_QUEUE_IO_CANCELED_ON_QUEUE EvtIoCanceledOnQueue; EVT_WDF_TIMER EvtTimeoutTimer; EVT_WDF_WORKITEM EvtTimeoutWorkItem; +EVT_WDF_WORKITEM EvtPendingDeliveryWorkItem; // WFP functions NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx); diff --git a/redirect_server.go b/redirect_server.go index 7590b35d..822322fb 100644 --- a/redirect_server.go +++ b/redirect_server.go @@ -26,6 +26,12 @@ type redirectServer struct { inShutdown atomic.Bool } +func (s *redirectServer) logError(args ...any) { + if s.logger != nil { + s.logger.Error(args...) + } +} + func newRedirectServer(ctx context.Context, handler N.TCPConnectionHandlerEx, logger logger.Logger, listenAddr netip.Addr) *redirectServer { return &redirectServer{ ctx: ctx, @@ -60,14 +66,14 @@ func (s *redirectServer) loopIn() { var netError net.Error //nolint:staticcheck if errors.As(err, &netError) && netError.Temporary() { - s.logger.Error(err) + s.logError(err) continue } if s.inShutdown.Load() && E.IsClosed(err) { return } s.listener.Close() - s.logger.Error("serve error: ", err) + s.logError("serve error: ", err) continue } source := M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() @@ -75,7 +81,7 @@ func (s *redirectServer) loopIn() { if err != nil { _ = conn.SetLinger(0) _ = conn.Close() - s.logger.Error("process redirect connection from ", source, ": invalid connection: ", err) + s.logError("process redirect connection from ", source, ": invalid connection: ", err) continue } go s.handler.NewConnectionEx(s.ctx, conn, source, M.SocksaddrFromNetIP(destination).Unwrap(), nil) diff --git a/redirect_server_windows.go b/redirect_server_windows.go index d3394847..538a67ea 100644 --- a/redirect_server_windows.go +++ b/redirect_server_windows.go @@ -25,6 +25,12 @@ type redirectServer struct { inShutdown atomic.Bool } +func (s *redirectServer) logError(args ...any) { + if s.logger != nil { + s.logger.Error(args...) + } +} + func newRedirectServerWindows(ctx context.Context, handler N.TCPConnectionHandlerEx, logger logger.Logger, listenAddr netip.Addr) *redirectServer { return &redirectServer{ ctx: ctx, @@ -62,14 +68,14 @@ func (s *redirectServer) loopIn() { var netError net.Error //nolint:staticcheck if errors.As(err, &netError) && netError.Temporary() { - s.logger.Error(err) + s.logError(err) continue } if s.inShutdown.Load() && E.IsClosed(err) { return } s.listener.Close() - s.logger.Error("serve error: ", err) + s.logError("serve error: ", err) return } source := M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() @@ -77,7 +83,7 @@ func (s *redirectServer) loopIn() { if !ok { _ = conn.SetLinger(0) _ = conn.Close() - s.logger.Error("process redirect connection from ", source, ": no metadata") + s.logError("process redirect connection from ", source, ": no metadata") continue } destination := entry.Destination @@ -152,6 +158,24 @@ func (t *connMetadataTable) Lookup(src M.Socksaddr) (*connEntry, bool) { entry, ok := t.entries[key] if ok { delete(t.entries, key) + return entry, true + } + + // ALE_CONNECT_REDIRECT may report an unspecified local source address + // (0.0.0.0/::) before connect completes, while the accepted redirected + // connection later appears as loopback with the same source port. + if src.Addr.IsLoopback() { + var fallbackAddr netip.Addr + if src.Addr.Is4() { + fallbackAddr = netip.IPv4Unspecified() + } else { + fallbackAddr = netip.IPv6Unspecified() + } + fallbackKey := connKey{Addr: fallbackAddr, Port: src.Port} + entry, ok = t.entries[fallbackKey] + if ok { + delete(t.entries, fallbackKey) + } } return entry, ok } diff --git a/redirect_windows.go b/redirect_windows.go index 88d99f5f..91752b80 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -169,6 +169,11 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 dst := pendingConnDst(conn) src := pendingConnSrc(conn) + // Proxy process outbound connections must never be redirected back into itself. + if conn.ProcessID == uint32(os.Getpid()) { + return winredirect.VerdictBypass + } + // 1. Loopback destinations if dst.Addr.IsLoopback() { return winredirect.VerdictBypass From fde0beafc4d6d686127d9caaae57c9036f4649e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 12:07:00 +0800 Subject: [PATCH 13/38] Add Windows auto-redirect soak harness --- cmd/test_auto_redirect/main.go | 129 ++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 17 deletions(-) diff --git a/cmd/test_auto_redirect/main.go b/cmd/test_auto_redirect/main.go index 66da9204..7c331d0a 100644 --- a/cmd/test_auto_redirect/main.go +++ b/cmd/test_auto_redirect/main.go @@ -9,7 +9,9 @@ import ( "net/netip" "os" "os/exec" + "strconv" "strings" + "sync" "time" tun "github.com/sagernet/sing-tun" @@ -22,7 +24,22 @@ const ( autoTestTarget = "198.18.0.1:65000" ) -type testHandler struct{} +type redirectEvent struct { + source string + destination string + processID uint32 + processPath string +} + +type testHandler struct { + redirectEvents chan redirectEvent +} + +func newTestHandler(bufferSize int) *testHandler { + return &testHandler{ + redirectEvents: make(chan redirectEvent, bufferSize), + } +} func (h *testHandler) PrepareConnection( network string, @@ -48,8 +65,18 @@ func (h *testHandler) NewConnectionEx( if metadata := tun.AutoRedirectMetadataFromContext(ctx); metadata != nil { fmt.Printf("[redirect] source=%s destination=%s pid=%d path=%s\n", source, destination, metadata.ProcessID, metadata.ProcessPath) + h.redirectEvents <- redirectEvent{ + source: source.String(), + destination: destination.String(), + processID: metadata.ProcessID, + processPath: metadata.ProcessPath, + } } else { fmt.Printf("[redirect] source=%s destination=%s metadata=\n", source, destination) + h.redirectEvents <- redirectEvent{ + source: source.String(), + destination: destination.String(), + } } _, _ = conn.Write([]byte("AUTO REDIRECT OK\n")) } @@ -81,14 +108,18 @@ func main() { } func runAutoRedirect() error { + iterations := envInt("WINREDIRECT_ITERATIONS", 3) + concurrency := envInt("WINREDIRECT_CONCURRENCY", 3) + options := tun.Options{ Inet4Address: []netip.Prefix{netip.MustParsePrefix("198.18.0.2/30")}, } + handler := newTestHandler(iterations*concurrency + 8) redirect, err := tun.NewAutoRedirect(tun.AutoRedirectOptions{ TunOptions: &options, Context: context.Background(), - Handler: &testHandler{}, + Handler: handler, }) if err != nil { return fmt.Errorf("new auto redirect: %w", err) @@ -100,24 +131,32 @@ func runAutoRedirect() error { } time.Sleep(500 * time.Millisecond) - fmt.Printf("[1] AutoRedirect started\n") + fmt.Printf("[1] AutoRedirect started iterations=%d concurrency=%d\n", iterations, concurrency) - fmt.Printf("[2] Self-dialing %s (should bypass) ...\n", autoTestTarget) - if err = expectBypass(autoTestTarget); err != nil { - return fmt.Errorf("self bypass check failed: %w", err) - } - fmt.Println("[2] Bypass confirmed") + for iteration := 1; iteration <= iterations; iteration++ { + fmt.Printf("[2.%d] Self-dialing %s (should bypass) ...\n", iteration, autoTestTarget) + if err = expectBypass(autoTestTarget); err != nil { + return fmt.Errorf("iteration %d self bypass check failed: %w", iteration, err) + } + fmt.Printf("[2.%d] Bypass confirmed\n", iteration) - fmt.Printf("[3] Child client dialing %s (should redirect) ...\n", autoTestTarget) - output, err := runExternalClient() - if err != nil { - return fmt.Errorf("child client failed: %w\n%s", err, output) - } - fmt.Print(output) - if !strings.Contains(output, "AUTO REDIRECT OK") { - return fmt.Errorf("child client did not receive redirected response") + fmt.Printf("[3.%d] Launching %d child clients ...\n", iteration, concurrency) + outputs, err := runExternalClients(concurrency) + if err != nil { + return fmt.Errorf("iteration %d child clients failed: %w\n%s", iteration, err, strings.Join(outputs, "\n")) + } + for _, output := range outputs { + fmt.Print(output) + if !strings.Contains(output, "AUTO REDIRECT OK") { + return fmt.Errorf("iteration %d child client did not receive redirected response", iteration) + } + } + + if err = handler.expectRedirectEvents(concurrency, 5*time.Second); err != nil { + return fmt.Errorf("iteration %d redirect validation failed: %w", iteration, err) + } + fmt.Printf("[3.%d] Redirect confirmed\n", iteration) } - fmt.Println("[3] Redirect confirmed") return nil } @@ -150,6 +189,29 @@ func runExternalClient() (string, error) { return string(output), err } +func runExternalClients(concurrency int) ([]string, error) { + outputs := make([]string, concurrency) + errs := make([]error, concurrency) + + var wg sync.WaitGroup + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + outputs[index], errs[index] = runExternalClient() + }(i) + } + wg.Wait() + + for _, err := range errs { + if err != nil { + return outputs, err + } + } + + return outputs, nil +} + func expectBypass(target string) error { conn, err := net.DialTimeout("tcp", target, 1200*time.Millisecond) if err == nil { @@ -167,4 +229,37 @@ func expectBypass(target string) error { return nil } +func (h *testHandler) expectRedirectEvents(expected int, timeout time.Duration) error { + deadline := time.After(timeout) + for i := 0; i < expected; i++ { + select { + case event := <-h.redirectEvents: + if event.destination != autoTestTarget { + return fmt.Errorf("unexpected destination %s", event.destination) + } + if event.processID == 0 { + return fmt.Errorf("missing process id for redirected connection") + } + if !strings.Contains(strings.ToLower(event.processPath), "test_auto_redirect.exe") { + return fmt.Errorf("unexpected process path %q", event.processPath) + } + case <-deadline: + return fmt.Errorf("timed out waiting for redirect events") + } + } + return nil +} + +func envInt(key string, defaultValue int) int { + value := strings.TrimSpace(os.Getenv(key)) + if value == "" { + return defaultValue + } + parsed, err := strconv.Atoi(value) + if err != nil || parsed <= 0 { + return defaultValue + } + return parsed +} + var _ tun.Handler = (*testHandler)(nil) From 50f2f372e0d63c904fd130a36b872b4d8c1f6d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 15:17:17 +0800 Subject: [PATCH 14/38] Gate Windows auto-redirect by best route --- internal/winipcfg/winipcfg.go | 13 ++ internal/winipcfg/zwinipcfg_windows.go | 79 ++++---- internal/winredirect/driver/winredirect.c | 58 ++++++ internal/winredirect/driver/winredirect.h | 2 + .../winredirect/driver/winredirect.vcxproj | 2 +- internal/winredirect/types_windows.go | 1 + redirect_windows.go | 169 ++++++++---------- 7 files changed, 190 insertions(+), 134 deletions(-) diff --git a/internal/winipcfg/winipcfg.go b/internal/winipcfg/winipcfg.go index e24157b9..8ae0fe1a 100644 --- a/internal/winipcfg/winipcfg.go +++ b/internal/winipcfg/winipcfg.go @@ -134,6 +134,7 @@ func GetAnycastIPAddressTable(family AddressFamily) ([]MibAnycastIPAddressRow, e // //sys getIPForwardTable2(family AddressFamily, table **mibIPforwardTable2) (ret error) = iphlpapi.GetIpForwardTable2 +//sys getBestRoute2(interfaceLUID *LUID, interfaceIndex uint32, sourceAddress *RawSockaddrInet, destinationAddress *RawSockaddrInet, sortOptions uint32, bestRoute *MibIPforwardRow2, bestSourceAddress *RawSockaddrInet) (ret error) = iphlpapi.GetBestRoute2 //sys initializeIPForwardEntry(route *MibIPforwardRow2) = iphlpapi.InitializeIpForwardEntry //sys getIPForwardEntry2(route *MibIPforwardRow2) (ret error) = iphlpapi.GetIpForwardEntry2 //sys setIPForwardEntry2(route *MibIPforwardRow2) (ret error) = iphlpapi.SetIpForwardEntry2 @@ -153,6 +154,18 @@ func GetIPForwardTable2(family AddressFamily) ([]MibIPforwardRow2, error) { return t, nil } +// GetBestRoute2 function retrieves the best route for the specified destination IP address on the local computer. +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getbestroute2 +func GetBestRoute2(interfaceLUID *LUID, interfaceIndex uint32, sourceAddress *RawSockaddrInet, destinationAddress *RawSockaddrInet, sortOptions uint32) (*MibIPforwardRow2, *RawSockaddrInet, error) { + bestRoute := &MibIPforwardRow2{} + bestSourceAddress := &RawSockaddrInet{} + err := getBestRoute2(interfaceLUID, interfaceIndex, sourceAddress, destinationAddress, sortOptions, bestRoute, bestSourceAddress) + if err != nil { + return nil, nil, err + } + return bestRoute, bestSourceAddress, nil +} + // // Notifications-related functions // diff --git a/internal/winipcfg/zwinipcfg_windows.go b/internal/winipcfg/zwinipcfg_windows.go index 3a0d8680..691afed8 100644 --- a/internal/winipcfg/zwinipcfg_windows.go +++ b/internal/winipcfg/zwinipcfg_windows.go @@ -53,6 +53,7 @@ var ( procFreeMibTable = modiphlpapi.NewProc("FreeMibTable") procGetAnycastIpAddressEntry = modiphlpapi.NewProc("GetAnycastIpAddressEntry") procGetAnycastIpAddressTable = modiphlpapi.NewProc("GetAnycastIpAddressTable") + procGetBestRoute2 = modiphlpapi.NewProc("GetBestRoute2") procGetIfEntry2 = modiphlpapi.NewProc("GetIfEntry2") procGetIfTable2Ex = modiphlpapi.NewProc("GetIfTable2Ex") procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2") @@ -74,7 +75,7 @@ var ( ) func cancelMibChangeNotify2(notificationHandle windows.Handle) (ret error) { - r0, _, _ := syscall.Syscall(procCancelMibChangeNotify2.Addr(), 1, uintptr(notificationHandle), 0, 0) + r0, _, _ := syscall.SyscallN(procCancelMibChangeNotify2.Addr(), uintptr(notificationHandle)) if r0 != 0 { ret = syscall.Errno(r0) } @@ -82,7 +83,7 @@ func cancelMibChangeNotify2(notificationHandle windows.Handle) (ret error) { } func convertInterfaceGUIDToLUID(interfaceGUID *windows.GUID, interfaceLUID *LUID) (ret error) { - r0, _, _ := syscall.Syscall(procConvertInterfaceGuidToLuid.Addr(), 2, uintptr(unsafe.Pointer(interfaceGUID)), uintptr(unsafe.Pointer(interfaceLUID)), 0) + r0, _, _ := syscall.SyscallN(procConvertInterfaceGuidToLuid.Addr(), uintptr(unsafe.Pointer(interfaceGUID)), uintptr(unsafe.Pointer(interfaceLUID))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -90,7 +91,7 @@ func convertInterfaceGUIDToLUID(interfaceGUID *windows.GUID, interfaceLUID *LUID } func convertInterfaceIndexToLUID(interfaceIndex uint32, interfaceLUID *LUID) (ret error) { - r0, _, _ := syscall.Syscall(procConvertInterfaceIndexToLuid.Addr(), 2, uintptr(interfaceIndex), uintptr(unsafe.Pointer(interfaceLUID)), 0) + r0, _, _ := syscall.SyscallN(procConvertInterfaceIndexToLuid.Addr(), uintptr(interfaceIndex), uintptr(unsafe.Pointer(interfaceLUID))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -98,7 +99,7 @@ func convertInterfaceIndexToLUID(interfaceIndex uint32, interfaceLUID *LUID) (re } func convertInterfaceLUIDToGUID(interfaceLUID *LUID, interfaceGUID *windows.GUID) (ret error) { - r0, _, _ := syscall.Syscall(procConvertInterfaceLuidToGuid.Addr(), 2, uintptr(unsafe.Pointer(interfaceLUID)), uintptr(unsafe.Pointer(interfaceGUID)), 0) + r0, _, _ := syscall.SyscallN(procConvertInterfaceLuidToGuid.Addr(), uintptr(unsafe.Pointer(interfaceLUID)), uintptr(unsafe.Pointer(interfaceGUID))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -106,7 +107,7 @@ func convertInterfaceLUIDToGUID(interfaceLUID *LUID, interfaceGUID *windows.GUID } func createAnycastIPAddressEntry(row *MibAnycastIPAddressRow) (ret error) { - r0, _, _ := syscall.Syscall(procCreateAnycastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procCreateAnycastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -114,7 +115,7 @@ func createAnycastIPAddressEntry(row *MibAnycastIPAddressRow) (ret error) { } func createIPForwardEntry2(route *MibIPforwardRow2) (ret error) { - r0, _, _ := syscall.Syscall(procCreateIpForwardEntry2.Addr(), 1, uintptr(unsafe.Pointer(route)), 0, 0) + r0, _, _ := syscall.SyscallN(procCreateIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -122,7 +123,7 @@ func createIPForwardEntry2(route *MibIPforwardRow2) (ret error) { } func createUnicastIPAddressEntry(row *MibUnicastIPAddressRow) (ret error) { - r0, _, _ := syscall.Syscall(procCreateUnicastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procCreateUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -130,7 +131,7 @@ func createUnicastIPAddressEntry(row *MibUnicastIPAddressRow) (ret error) { } func deleteAnycastIPAddressEntry(row *MibAnycastIPAddressRow) (ret error) { - r0, _, _ := syscall.Syscall(procDeleteAnycastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procDeleteAnycastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -138,7 +139,7 @@ func deleteAnycastIPAddressEntry(row *MibAnycastIPAddressRow) (ret error) { } func deleteIPForwardEntry2(route *MibIPforwardRow2) (ret error) { - r0, _, _ := syscall.Syscall(procDeleteIpForwardEntry2.Addr(), 1, uintptr(unsafe.Pointer(route)), 0, 0) + r0, _, _ := syscall.SyscallN(procDeleteIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -146,7 +147,7 @@ func deleteIPForwardEntry2(route *MibIPforwardRow2) (ret error) { } func deleteUnicastIPAddressEntry(row *MibUnicastIPAddressRow) (ret error) { - r0, _, _ := syscall.Syscall(procDeleteUnicastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procDeleteUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -154,12 +155,12 @@ func deleteUnicastIPAddressEntry(row *MibUnicastIPAddressRow) (ret error) { } func freeMibTable(memory unsafe.Pointer) { - syscall.Syscall(procFreeMibTable.Addr(), 1, uintptr(memory), 0, 0) + syscall.SyscallN(procFreeMibTable.Addr(), uintptr(memory)) return } func getAnycastIPAddressEntry(row *MibAnycastIPAddressRow) (ret error) { - r0, _, _ := syscall.Syscall(procGetAnycastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procGetAnycastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -167,7 +168,15 @@ func getAnycastIPAddressEntry(row *MibAnycastIPAddressRow) (ret error) { } func getAnycastIPAddressTable(family AddressFamily, table **mibAnycastIPAddressTable) (ret error) { - r0, _, _ := syscall.Syscall(procGetAnycastIpAddressTable.Addr(), 2, uintptr(family), uintptr(unsafe.Pointer(table)), 0) + r0, _, _ := syscall.SyscallN(procGetAnycastIpAddressTable.Addr(), uintptr(family), uintptr(unsafe.Pointer(table))) + if r0 != 0 { + ret = syscall.Errno(r0) + } + return +} + +func getBestRoute2(interfaceLUID *LUID, interfaceIndex uint32, sourceAddress *RawSockaddrInet, destinationAddress *RawSockaddrInet, sortOptions uint32, bestRoute *MibIPforwardRow2, bestSourceAddress *RawSockaddrInet) (ret error) { + r0, _, _ := syscall.SyscallN(procGetBestRoute2.Addr(), uintptr(unsafe.Pointer(interfaceLUID)), uintptr(interfaceIndex), uintptr(unsafe.Pointer(sourceAddress)), uintptr(unsafe.Pointer(destinationAddress)), uintptr(sortOptions), uintptr(unsafe.Pointer(bestRoute)), uintptr(unsafe.Pointer(bestSourceAddress))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -175,7 +184,7 @@ func getAnycastIPAddressTable(family AddressFamily, table **mibAnycastIPAddressT } func getIfEntry2(row *MibIfRow2) (ret error) { - r0, _, _ := syscall.Syscall(procGetIfEntry2.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procGetIfEntry2.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -183,7 +192,7 @@ func getIfEntry2(row *MibIfRow2) (ret error) { } func getIfTable2Ex(level MibIfEntryLevel, table **mibIfTable2) (ret error) { - r0, _, _ := syscall.Syscall(procGetIfTable2Ex.Addr(), 2, uintptr(level), uintptr(unsafe.Pointer(table)), 0) + r0, _, _ := syscall.SyscallN(procGetIfTable2Ex.Addr(), uintptr(level), uintptr(unsafe.Pointer(table))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -191,7 +200,7 @@ func getIfTable2Ex(level MibIfEntryLevel, table **mibIfTable2) (ret error) { } func getIPForwardEntry2(route *MibIPforwardRow2) (ret error) { - r0, _, _ := syscall.Syscall(procGetIpForwardEntry2.Addr(), 1, uintptr(unsafe.Pointer(route)), 0, 0) + r0, _, _ := syscall.SyscallN(procGetIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -199,7 +208,7 @@ func getIPForwardEntry2(route *MibIPforwardRow2) (ret error) { } func getIPForwardTable2(family AddressFamily, table **mibIPforwardTable2) (ret error) { - r0, _, _ := syscall.Syscall(procGetIpForwardTable2.Addr(), 2, uintptr(family), uintptr(unsafe.Pointer(table)), 0) + r0, _, _ := syscall.SyscallN(procGetIpForwardTable2.Addr(), uintptr(family), uintptr(unsafe.Pointer(table))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -207,7 +216,7 @@ func getIPForwardTable2(family AddressFamily, table **mibIPforwardTable2) (ret e } func getIPInterfaceEntry(row *MibIPInterfaceRow) (ret error) { - r0, _, _ := syscall.Syscall(procGetIpInterfaceEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procGetIpInterfaceEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -215,7 +224,7 @@ func getIPInterfaceEntry(row *MibIPInterfaceRow) (ret error) { } func getIPInterfaceTable(family AddressFamily, table **mibIPInterfaceTable) (ret error) { - r0, _, _ := syscall.Syscall(procGetIpInterfaceTable.Addr(), 2, uintptr(family), uintptr(unsafe.Pointer(table)), 0) + r0, _, _ := syscall.SyscallN(procGetIpInterfaceTable.Addr(), uintptr(family), uintptr(unsafe.Pointer(table))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -223,7 +232,7 @@ func getIPInterfaceTable(family AddressFamily, table **mibIPInterfaceTable) (ret } func getUnicastIPAddressEntry(row *MibUnicastIPAddressRow) (ret error) { - r0, _, _ := syscall.Syscall(procGetUnicastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procGetUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -231,7 +240,7 @@ func getUnicastIPAddressEntry(row *MibUnicastIPAddressRow) (ret error) { } func getUnicastIPAddressTable(family AddressFamily, table **mibUnicastIPAddressTable) (ret error) { - r0, _, _ := syscall.Syscall(procGetUnicastIpAddressTable.Addr(), 2, uintptr(family), uintptr(unsafe.Pointer(table)), 0) + r0, _, _ := syscall.SyscallN(procGetUnicastIpAddressTable.Addr(), uintptr(family), uintptr(unsafe.Pointer(table))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -239,17 +248,17 @@ func getUnicastIPAddressTable(family AddressFamily, table **mibUnicastIPAddressT } func initializeIPForwardEntry(route *MibIPforwardRow2) { - syscall.Syscall(procInitializeIpForwardEntry.Addr(), 1, uintptr(unsafe.Pointer(route)), 0, 0) + syscall.SyscallN(procInitializeIpForwardEntry.Addr(), uintptr(unsafe.Pointer(route))) return } func initializeIPInterfaceEntry(row *MibIPInterfaceRow) { - syscall.Syscall(procInitializeIpInterfaceEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + syscall.SyscallN(procInitializeIpInterfaceEntry.Addr(), uintptr(unsafe.Pointer(row))) return } func initializeUnicastIPAddressEntry(row *MibUnicastIPAddressRow) { - syscall.Syscall(procInitializeUnicastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + syscall.SyscallN(procInitializeUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) return } @@ -258,7 +267,7 @@ func notifyIPInterfaceChange(family AddressFamily, callback uintptr, callerConte if initialNotification { _p0 = 1 } - r0, _, _ := syscall.Syscall6(procNotifyIpInterfaceChange.Addr(), 5, uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)), 0) + r0, _, _ := syscall.SyscallN(procNotifyIpInterfaceChange.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -270,7 +279,7 @@ func notifyRouteChange2(family AddressFamily, callback uintptr, callerContext ui if initialNotification { _p0 = 1 } - r0, _, _ := syscall.Syscall6(procNotifyRouteChange2.Addr(), 5, uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)), 0) + r0, _, _ := syscall.SyscallN(procNotifyRouteChange2.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -282,19 +291,19 @@ func notifyUnicastIPAddressChange(family AddressFamily, callback uintptr, caller if initialNotification { _p0 = 1 } - r0, _, _ := syscall.Syscall6(procNotifyUnicastIpAddressChange.Addr(), 5, uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)), 0) + r0, _, _ := syscall.SyscallN(procNotifyUnicastIpAddressChange.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle))) if r0 != 0 { ret = syscall.Errno(r0) } return } -func setInterfaceDnsSettingsByDwords(guid1 uintptr, guid2 uintptr, guid3 uintptr, guid4 uintptr, settings *DnsInterfaceSettings) (ret error) { +func setInterfaceDnsSettingsByPtr(guid *windows.GUID, settings *DnsInterfaceSettings) (ret error) { ret = procSetInterfaceDnsSettings.Find() if ret != nil { return } - r0, _, _ := syscall.Syscall6(procSetInterfaceDnsSettings.Addr(), 5, uintptr(guid1), uintptr(guid2), uintptr(guid3), uintptr(guid4), uintptr(unsafe.Pointer(settings)), 0) + r0, _, _ := syscall.SyscallN(procSetInterfaceDnsSettings.Addr(), uintptr(unsafe.Pointer(guid)), uintptr(unsafe.Pointer(settings))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -306,19 +315,19 @@ func setInterfaceDnsSettingsByQwords(guid1 uintptr, guid2 uintptr, settings *Dns if ret != nil { return } - r0, _, _ := syscall.Syscall(procSetInterfaceDnsSettings.Addr(), 3, uintptr(guid1), uintptr(guid2), uintptr(unsafe.Pointer(settings))) + r0, _, _ := syscall.SyscallN(procSetInterfaceDnsSettings.Addr(), uintptr(guid1), uintptr(guid2), uintptr(unsafe.Pointer(settings))) if r0 != 0 { ret = syscall.Errno(r0) } return } -func setInterfaceDnsSettingsByPtr(guid *windows.GUID, settings *DnsInterfaceSettings) (ret error) { +func setInterfaceDnsSettingsByDwords(guid1 uintptr, guid2 uintptr, guid3 uintptr, guid4 uintptr, settings *DnsInterfaceSettings) (ret error) { ret = procSetInterfaceDnsSettings.Find() if ret != nil { return } - r0, _, _ := syscall.Syscall(procSetInterfaceDnsSettings.Addr(), 2, uintptr(unsafe.Pointer(guid)), uintptr(unsafe.Pointer(settings)), 0) + r0, _, _ := syscall.SyscallN(procSetInterfaceDnsSettings.Addr(), uintptr(guid1), uintptr(guid2), uintptr(guid3), uintptr(guid4), uintptr(unsafe.Pointer(settings))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -326,7 +335,7 @@ func setInterfaceDnsSettingsByPtr(guid *windows.GUID, settings *DnsInterfaceSett } func setIPForwardEntry2(route *MibIPforwardRow2) (ret error) { - r0, _, _ := syscall.Syscall(procSetIpForwardEntry2.Addr(), 1, uintptr(unsafe.Pointer(route)), 0, 0) + r0, _, _ := syscall.SyscallN(procSetIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -334,7 +343,7 @@ func setIPForwardEntry2(route *MibIPforwardRow2) (ret error) { } func setIPInterfaceEntry(row *MibIPInterfaceRow) (ret error) { - r0, _, _ := syscall.Syscall(procSetIpInterfaceEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procSetIpInterfaceEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } @@ -342,7 +351,7 @@ func setIPInterfaceEntry(row *MibIPInterfaceRow) (ret error) { } func setUnicastIPAddressEntry(row *MibUnicastIPAddressRow) (ret error) { - r0, _, _ := syscall.Syscall(procSetUnicastIpAddressEntry.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + r0, _, _ := syscall.SyscallN(procSetUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { ret = syscall.Errno(r0) } diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index b202f676..4bbd844c 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -62,6 +62,58 @@ static BOOLEAN IsAnyAddress(_In_ UINT8 AddressFamily, _In_reads_(16) const UINT8 return FALSE; } +typedef enum _BEST_ROUTE_RESULT { + BestRouteUnknown = 0, + BestRouteTun = 1, + BestRouteOther = 2, +} BEST_ROUTE_RESULT; + +static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const WINREDIRECT_CONFIG* Config, _In_ const PENDING_ENTRY* Entry) +{ + SOCKADDR_INET sourceAddress; + SOCKADDR_INET destinationAddress; + SOCKADDR_INET* sourceAddressPtr = NULL; + MIB_IPFORWARD_ROW2 bestRoute; + NETIO_STATUS status; + + if (Config->TunIndex == 0 || KeGetCurrentIrql() >= DISPATCH_LEVEL) { + return BestRouteUnknown; + } + + RtlZeroMemory(&sourceAddress, sizeof(sourceAddress)); + RtlZeroMemory(&destinationAddress, sizeof(destinationAddress)); + RtlZeroMemory(&bestRoute, sizeof(bestRoute)); + + if (Entry->AddressFamily == AF_INET) { + destinationAddress.Ipv4.sin_family = AF_INET; + RtlCopyMemory(&destinationAddress.Ipv4.sin_addr, Entry->DstAddr, sizeof(destinationAddress.Ipv4.sin_addr)); + if (!IsAnyAddress(AF_INET, Entry->SrcAddr)) { + sourceAddress.Ipv4.sin_family = AF_INET; + RtlCopyMemory(&sourceAddress.Ipv4.sin_addr, Entry->SrcAddr, sizeof(sourceAddress.Ipv4.sin_addr)); + sourceAddressPtr = &sourceAddress; + } + } else if (Entry->AddressFamily == AF_INET6) { + destinationAddress.Ipv6.sin6_family = AF_INET6; + RtlCopyMemory(destinationAddress.Ipv6.sin6_addr.u.Byte, Entry->DstAddr, sizeof(destinationAddress.Ipv6.sin6_addr.u.Byte)); + if (!IsAnyAddress(AF_INET6, Entry->SrcAddr)) { + sourceAddress.Ipv6.sin6_family = AF_INET6; + RtlCopyMemory(sourceAddress.Ipv6.sin6_addr.u.Byte, Entry->SrcAddr, sizeof(sourceAddress.Ipv6.sin6_addr.u.Byte)); + sourceAddressPtr = &sourceAddress; + } + } else { + return BestRouteUnknown; + } + + status = GetBestRoute2(NULL, 0, sourceAddressPtr, &destinationAddress, 0, &bestRoute, NULL); + if (status != NO_ERROR) { + return BestRouteUnknown; + } + if (bestRoute.InterfaceIndex == Config->TunIndex) { + return BestRouteTun; + } + return BestRouteOther; +} + typedef struct _LOCAL_REDIRECT_CONTEXT { SOCKADDR_STORAGE OriginalRemoteAddressAndPort; UINT32 ProcessId; @@ -619,6 +671,12 @@ static void ClassifyFnCommon( return; } + if (BestRouteForEntry(&config, entry) == BestRouteOther) { + ExFreePoolWithTag(entry, 'rniW'); + PermitClassify(classifyOut); + return; + } + // Extract PID from metadata if (FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_PROCESS_ID)) { entry->ProcessID = (UINT32)inMetaValues->processId; diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 9ad3e47b..b2e632a9 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -7,6 +7,7 @@ #include #include #include +#include // Device names #define DEVICE_NAME L"\\Device\\WinRedirect" @@ -32,6 +33,7 @@ typedef struct _WINREDIRECT_CONFIG { UINT16 RedirectPort; UINT8 _pad0[2]; UINT32 ProxyPID; + UINT32 TunIndex; } WINREDIRECT_CONFIG; typedef struct _WINREDIRECT_PENDING_CONN { diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj index 151b56cf..d1b9741d 100644 --- a/internal/winredirect/driver/winredirect.vcxproj +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -78,7 +78,7 @@ false - Fwpkclnt.lib;$(DDK_LIB_PATH)wdmsec.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) + Fwpkclnt.lib;$(DDK_LIB_PATH)netio.lib;$(DDK_LIB_PATH)wdmsec.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) true diff --git a/internal/winredirect/types_windows.go b/internal/winredirect/types_windows.go index 9066ca0a..705efdfb 100644 --- a/internal/winredirect/types_windows.go +++ b/internal/winredirect/types_windows.go @@ -22,6 +22,7 @@ type Config struct { RedirectPort uint16 _ [2]byte // padding ProxyPID uint32 + TunIndex uint32 } // PendingConn is received from the driver via IOCTL_GET_PENDING. diff --git a/redirect_windows.go b/redirect_windows.go index 91752b80..20ad3f9e 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -3,12 +3,14 @@ package tun import ( "context" "errors" + "net" "net/netip" "os" "slices" "strings" "sync" + "github.com/sagernet/sing-tun/internal/winipcfg" "github.com/sagernet/sing-tun/internal/winredirect" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" @@ -37,8 +39,9 @@ type autoRedirect struct { enableIPv4 bool enableIPv6 bool - localAddressMu sync.RWMutex - localAddresses []netip.Prefix + localAddressMu sync.RWMutex + localAddresses []netip.Prefix + tunInterfaceIndex uint32 workerCount int } @@ -103,10 +106,19 @@ func (r *autoRedirect) Start() error { } r.redirectServer = server + tunInterfaceIndex, err := r.resolveTunInterfaceIndex() + if err != nil { + server.Close() + manager.Close() + return E.Cause(err, "resolve tun interface") + } + r.tunInterfaceIndex = tunInterfaceIndex + redirectPort := M.AddrPortFromNet(server.listener.Addr()).Port() err = manager.SetConfig(&winredirect.Config{ RedirectPort: redirectPort, ProxyPID: uint32(os.Getpid()), + TunIndex: tunInterfaceIndex, }) if err != nil { server.Close() @@ -179,17 +191,12 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 return winredirect.VerdictBypass } - // 2. Static route address whitelist (Inet4/6RouteAddress) - if r.hasStaticRouteAddress() && !r.matchStaticRouteAddress(dst.Addr) { + // Only intercept connections whose Windows best route resolves to the TUN interface. + if !r.routeWouldHitTun(src.Addr, dst.Addr) { return winredirect.VerdictBypass } - // 3. Static route address blacklist (Inet4/6RouteExcludeAddress) - if r.matchStaticRouteExcludeAddress(dst.Addr) { - return winredirect.VerdictBypass - } - - // 4. DNS hijack: port 53 from local network → redirect to DNS server + // DNS hijack: port 53 from local network → redirect to DNS server if !r.tunOptions.EXP_DisableDNSHijack && dst.Port == 53 { if r.isLocalAddress(src.Addr) { dnsServer := r.dnsServerForFamily(dst.Addr) @@ -201,39 +208,27 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 } } - // 5. Local address exclusion - if r.isLocalAddress(dst.Addr) { - return winredirect.VerdictBypass - } - - // 6. Dynamic route address set whitelist - if r.hasDynamicRouteAddressSet() && !r.matchDynamicRouteAddressSet(dst.Addr) { - return winredirect.VerdictBypass - } - - // 7. Dynamic route address set blacklist - if r.matchDynamicRouteExcludeAddressSet(dst.Addr) { - return winredirect.VerdictBypass - } - - // 8. Strict route: reject disabled address family + // Strict route: reject disabled address family if r.tunOptions.StrictRoute && r.isDisabledFamily(dst.Addr) { return winredirect.VerdictDrop } - // 9. Resolve PID → process path + // Resolve PID → process path metadata := r.resolveMetadata(conn) - // 10. PrepareConnection (NFQUEUE equivalent) + // PrepareConnection (NFQUEUE equivalent) _, err := r.handler.PrepareConnection("tcp", src, dst, nil, 0) - if errors.Is(err, ErrBypass) { + if errors.Is(err, ErrDrop) { + return winredirect.VerdictDrop + } + if errors.Is(err, ErrReset) { return winredirect.VerdictBypass } - if errors.Is(err, ErrDrop) || errors.Is(err, ErrReset) || err != nil { - return winredirect.VerdictDrop + if err != nil && !errors.Is(err, ErrBypass) && r.logger != nil { + r.logger.Warn("prepare connection fallback to redirect: ", err) } - // 11. Store metadata for redirect server + // Store metadata for redirect server r.redirectServer.connTable.Store(src, dst, metadata) return winredirect.VerdictRedirect @@ -282,72 +277,6 @@ func (r *autoRedirect) isLocalAddress(addr netip.Addr) bool { return false } -func (r *autoRedirect) hasStaticRouteAddress() bool { - return len(r.tunOptions.Inet4RouteAddress) > 0 || len(r.tunOptions.Inet6RouteAddress) > 0 -} - -func (r *autoRedirect) matchStaticRouteAddress(addr netip.Addr) bool { - var prefixes []netip.Prefix - if addr.Is4() { - prefixes = r.tunOptions.Inet4RouteAddress - } else { - prefixes = r.tunOptions.Inet6RouteAddress - } - for _, prefix := range prefixes { - if prefix.Contains(addr) { - return true - } - } - return false -} - -func (r *autoRedirect) matchStaticRouteExcludeAddress(addr netip.Addr) bool { - var prefixes []netip.Prefix - if addr.Is4() { - prefixes = r.tunOptions.Inet4RouteExcludeAddress - } else { - prefixes = r.tunOptions.Inet6RouteExcludeAddress - } - for _, prefix := range prefixes { - if prefix.Contains(addr) { - return true - } - } - return false -} - -func (r *autoRedirect) hasDynamicRouteAddressSet() bool { - if r.routeAddressSet == nil { - return false - } - sets := *r.routeAddressSet - return len(sets) > 0 -} - -func (r *autoRedirect) matchDynamicRouteAddressSet(addr netip.Addr) bool { - if r.routeAddressSet == nil { - return true - } - for _, set := range *r.routeAddressSet { - if set.Contains(addr) { - return true - } - } - return false -} - -func (r *autoRedirect) matchDynamicRouteExcludeAddressSet(addr netip.Addr) bool { - if r.routeExcludeAddressSet == nil { - return false - } - for _, set := range *r.routeExcludeAddressSet { - if set.Contains(addr) { - return true - } - } - return false -} - func (r *autoRedirect) isDisabledFamily(addr netip.Addr) bool { if addr.Is4() { return !r.enableIPv4 @@ -375,6 +304,50 @@ func (r *autoRedirect) dnsServerForFamily(addr netip.Addr) netip.Addr { return netip.Addr{} } +func (r *autoRedirect) resolveTunInterfaceIndex() (uint32, error) { + if r.interfaceFinder != nil { + if err := r.interfaceFinder.Update(); err == nil { + iface, err := r.interfaceFinder.ByName(r.tunOptions.Name) + if err == nil { + return uint32(iface.Index), nil + } + } + } + iface, err := net.InterfaceByName(r.tunOptions.Name) + if err != nil { + return 0, err + } + return uint32(iface.Index), nil +} + +func (r *autoRedirect) routeWouldHitTun(src netip.Addr, dst netip.Addr) bool { + if r.tunInterfaceIndex == 0 || !dst.IsValid() { + return true + } + + var destinationAddress winipcfg.RawSockaddrInet + if err := destinationAddress.SetAddr(dst); err != nil { + return true + } + + var sourceAddress *winipcfg.RawSockaddrInet + if src.IsValid() && !src.IsUnspecified() { + source := new(winipcfg.RawSockaddrInet) + if err := source.SetAddr(src); err == nil { + sourceAddress = source + } + } + + bestRoute, _, err := winipcfg.GetBestRoute2(nil, 0, sourceAddress, &destinationAddress, 0) + if err != nil { + if r.logger != nil { + r.logger.Warn("get best route fallback to redirect: ", err) + } + return true + } + return bestRoute.InterfaceIndex == r.tunInterfaceIndex +} + func pendingConnSrc(conn *winredirect.PendingConn) M.Socksaddr { return M.SocksaddrFrom(pendingAddr(conn.AddressFamily, conn.SrcAddr), conn.SrcPort) } From 804fef4355373a0cdfe6936f63d79b3c56277783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 15:29:20 +0800 Subject: [PATCH 15/38] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3f3c51ce..03b9ffb5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .DS_Store !/README.md /*.md +/.claude/ +/CLAUDE.md \ No newline at end of file From 33d32a4e5714ee1657393ebbe542cda3ac668c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 15:36:55 +0800 Subject: [PATCH 16/38] Use GUID route gating for Windows auto-redirect --- cmd/test_redirect/main.go | 337 ---------------------- internal/winredirect/driver/winredirect.c | 82 ++++-- internal/winredirect/driver/winredirect.h | 10 +- internal/winredirect/types_windows.go | 2 +- redirect_windows.go | 66 ++--- 5 files changed, 90 insertions(+), 407 deletions(-) delete mode 100644 cmd/test_redirect/main.go diff --git a/cmd/test_redirect/main.go b/cmd/test_redirect/main.go deleted file mode 100644 index 9d7b57b1..00000000 --- a/cmd/test_redirect/main.go +++ /dev/null @@ -1,337 +0,0 @@ -//go:build windows - -package main - -import ( - "fmt" - "net" - "net/netip" - "os" - "os/exec" - "strings" - "sync" - "sync/atomic" - "time" - "unsafe" - - "golang.org/x/sys/windows" -) - -const ( - ioctlSetConfig = 0x00120000 | (0x800 << 2) - ioctlStart = 0x00120000 | (0x801 << 2) - ioctlStop = 0x00120000 | (0x802 << 2) - ioctlGetPending = 0x00120000 | (0x803 << 2) - ioctlSetVerdict = 0x00120000 | (0x804 << 2) -) - -type Config struct { - RedirectPort uint16 - _ [2]byte - ProxyPID uint32 -} - -type PendingConn struct { - ConnID uint64 - AddressFamily uint8 - _pad0 [3]byte - SrcAddr [16]byte - SrcPort uint16 - _pad1 [2]byte - DstAddr [16]byte - DstPort uint16 - _pad2 [2]byte - ProcessID uint32 -} - -type VerdictMsg struct { - ConnID uint64 - Verdict uint32 - _pad [4]byte -} - -var ( - modkernel32 = windows.NewLazySystemDLL("kernel32.dll") - procCancelIoEx = modkernel32.NewProc("CancelIoEx") -) - -const ( - clientModeArg = "client" - testTarget = "198.18.0.1:65000" -) - -var targetVerdict = strings.ToUpper(strings.TrimSpace(os.Getenv("WINREDIRECT_TARGET_VERDICT"))) - -func main() { - var err error - if len(os.Args) > 1 && os.Args[1] == clientModeArg { - err = runClient(testTarget) - } else { - err = runProxy() - } - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func runProxy() error { - // 1. Start redirect TCP listener - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return fmt.Errorf("listen: %w", err) - } - defer listener.Close() - port := listener.Addr().(*net.TCPAddr).Port - fmt.Printf("[1] Redirect server on 127.0.0.1:%d\n", port) - - var wg sync.WaitGroup - var acceptedCount atomic.Int32 - wg.Add(1) - go func() { - defer wg.Done() - for { - conn, err := listener.Accept() - if err != nil { - return - } - acceptedCount.Add(1) - fmt.Printf("[redirect] Accepted from %s\n", conn.RemoteAddr()) - conn.Write([]byte("REDIRECTED OK\n")) - conn.Close() - } - }() - - // 2. Open driver - device := openDevice() - defer windows.CloseHandle(device) - fmt.Println("[2] Device opened") - - // 3. Configure - cfg := Config{RedirectPort: uint16(port), ProxyPID: uint32(os.Getpid())} - err = devctl(device, ioctlSetConfig, unsafe.Pointer(&cfg), unsafe.Sizeof(cfg), nil, 0) - if err != nil { - return fmt.Errorf("SET_CONFIG: %w", err) - } - fmt.Printf("[3] Config: port=%d pid=%d\n", port, os.Getpid()) - - // 4. Start - err = devctl(device, ioctlStart, nil, 0, nil, 0) - if err != nil { - return fmt.Errorf("START: %w", err) - } - fmt.Println("[4] WFP started — all TCP connections will be intercepted") - - // 5. Pre-match worker - stopCh := make(chan struct{}) - defer close(stopCh) - wg.Add(1) - go func() { - defer wg.Done() - pendingWorker(device, stopCh, port) - }() - defer wg.Wait() - defer devctl(device, ioctlStop, nil, 0, nil, 0) - - // 6. Proxy process should bypass its own outbound connect. - time.Sleep(300 * time.Millisecond) - fmt.Printf("\n[5] Proxy self-dialing %s (should bypass) ...\n", testTarget) - err = expectBypass(testTarget, &acceptedCount) - if err != nil { - return fmt.Errorf("self-bypass check failed: %w", err) - } - fmt.Println("[5] Bypass confirmed") - - // 7. External client process should be redirected. - fmt.Printf("\n[6] Child client dialing %s (should redirect) ...\n", testTarget) - if targetVerdict == "BYPASS" { - fmt.Println("[6] Target verdict override: BYPASS") - } - output, err := runExternalClient() - if err != nil { - return fmt.Errorf("child client failed: %w\n%s", err, output) - } - fmt.Print(output) - if !strings.Contains(output, "REDIRECTED OK") { - return fmt.Errorf("child client did not receive redirected response") - } - if acceptedCount.Load() != 1 { - return fmt.Errorf("redirect server accepted %d connections, want 1", acceptedCount.Load()) - } - fmt.Println("[6] Redirect confirmed") - - // 8. Cleanup - time.Sleep(300 * time.Millisecond) - fmt.Println("\n[7] Stopped") - fmt.Println("[8] Done!") - return nil -} - -func runClient(target string) error { - fmt.Printf("[client] Dialing %s ...\n", target) - conn, err := net.DialTimeout("tcp", target, 5*time.Second) - if err != nil { - return fmt.Errorf("client dial: %w", err) - } - defer conn.Close() - - buf := make([]byte, 256) - _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - n, err := conn.Read(buf) - if err != nil { - return fmt.Errorf("client read: %w", err) - } - fmt.Printf("[client] Response: %q\n", string(buf[:n])) - return nil -} - -func runExternalClient() (string, error) { - executable, err := os.Executable() - if err != nil { - return "", err - } - cmd := exec.Command(executable, clientModeArg) - output, err := cmd.CombinedOutput() - return string(output), err -} - -func expectBypass(target string, acceptedCount *atomic.Int32) error { - initialAccepted := acceptedCount.Load() - conn, err := net.DialTimeout("tcp", target, 1200*time.Millisecond) - if err == nil { - defer conn.Close() - buf := make([]byte, 64) - _ = conn.SetReadDeadline(time.Now().Add(300 * time.Millisecond)) - n, readErr := conn.Read(buf) - if n > 0 && string(buf[:n]) == "REDIRECTED OK\n" { - return fmt.Errorf("proxy process was redirected unexpectedly") - } - if readErr == nil { - return fmt.Errorf("unexpected data from bypass connection: %q", string(buf[:n])) - } - } - time.Sleep(300 * time.Millisecond) - if acceptedCount.Load() != initialAccepted { - return fmt.Errorf("redirect server accepted proxy self-connection") - } - return nil -} - -func pendingWorker(device windows.Handle, stop <-chan struct{}, redirectPort int) { - for { - select { - case <-stop: - return - default: - } - - var conn PendingConn - err := devctlOverlapped(device, ioctlGetPending, nil, 0, - unsafe.Pointer(&conn), unsafe.Sizeof(conn), 2*time.Second) - if err != nil { - continue - } - - src := fmtAddr(conn.AddressFamily, conn.SrcAddr, conn.SrcPort) - dst := fmtAddr(conn.AddressFamily, conn.DstAddr, conn.DstPort) - proc := procName(conn.ProcessID) - - fmt.Printf("[>] connID=%d %s -> %s pid=%d (%s)\n", - conn.ConnID, src, dst, conn.ProcessID, proc) - - verdict := uint32(1) // BYPASS - verdictText := "BYPASS" - if dst == testTarget && targetVerdict != "BYPASS" { - verdict = 0 // REDIRECT - verdictText = fmt.Sprintf("REDIRECT -> 127.0.0.1:%d", redirectPort) - } - - v := VerdictMsg{ConnID: conn.ConnID, Verdict: verdict} - err = devctl(device, ioctlSetVerdict, unsafe.Pointer(&v), unsafe.Sizeof(v), nil, 0) - if err != nil { - fmt.Printf("[!] verdict failed: %v\n", err) - } else { - fmt.Printf("[<] connID=%d %s\n", conn.ConnID, verdictText) - } - } -} - -func openDevice() windows.Handle { - p, _ := windows.UTF16PtrFromString(`\\.\WinRedirect`) - h, err := windows.CreateFile(p, - windows.GENERIC_READ|windows.GENERIC_WRITE, - 0, nil, windows.OPEN_EXISTING, - windows.FILE_FLAG_OVERLAPPED, 0) - fatal("CreateFile", err) - return h -} - -func devctl(h windows.Handle, code uint32, in unsafe.Pointer, inSz uintptr, out unsafe.Pointer, outSz uintptr) error { - var ret uint32 - return windows.DeviceIoControl(h, code, - (*byte)(in), uint32(inSz), - (*byte)(out), uint32(outSz), - &ret, nil) -} - -func devctlOverlapped(h windows.Handle, code uint32, in unsafe.Pointer, inSz uintptr, out unsafe.Pointer, outSz uintptr, timeout time.Duration) error { - ol := &windows.Overlapped{} - var err error - ol.HEvent, err = windows.CreateEvent(nil, 1, 0, nil) - if err != nil { - return err - } - defer windows.CloseHandle(ol.HEvent) - - var ret uint32 - err = windows.DeviceIoControl(h, code, - (*byte)(in), uint32(inSz), - (*byte)(out), uint32(outSz), - &ret, ol) - if err == windows.ERROR_IO_PENDING { - r, _ := windows.WaitForSingleObject(ol.HEvent, uint32(timeout.Milliseconds())) - if r != windows.WAIT_OBJECT_0 { - procCancelIoEx.Call(uintptr(h), uintptr(unsafe.Pointer(ol))) - return fmt.Errorf("timeout") - } - err = windows.GetOverlappedResult(h, ol, &ret, false) - } - return err -} - -func fmtAddr(af uint8, raw [16]byte, port uint16) string { - var addr netip.Addr - if af == 2 { - addr = netip.AddrFrom4([4]byte(raw[:4])) - } else { - addr = netip.AddrFrom16(raw) - } - return netip.AddrPortFrom(addr, port).String() -} - -func procName(pid uint32) string { - h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid) - if err != nil { - return "?" - } - defer windows.CloseHandle(h) - var buf [260]uint16 - n := uint32(260) - if windows.QueryFullProcessImageName(h, 0, &buf[0], &n) != nil { - return "?" - } - s := windows.UTF16ToString(buf[:n]) - for i := len(s) - 1; i >= 0; i-- { - if s[i] == '\\' { - return s[i+1:] - } - } - return s -} - -func fatal(ctx string, err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "%s: %v\n", ctx, err) - os.Exit(1) - } -} diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 4bbd844c..c8beaa2a 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -68,7 +68,32 @@ typedef enum _BEST_ROUTE_RESULT { BestRouteOther = 2, } BEST_ROUTE_RESULT; -static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const WINREDIRECT_CONFIG* Config, _In_ const PENDING_ENTRY* Entry) +static BOOLEAN IsZeroGuid(_In_reads_(16) const UINT8* GuidBytes) +{ + for (UINT32 i = 0; i < sizeof(GUID); i++) { + if (GuidBytes[i] != 0) { + return FALSE; + } + } + return TRUE; +} + +static CONFIG_SNAPSHOT ReadConfigSnapshot(_In_ PDRIVER_CONTEXT Ctx) +{ + CONFIG_SNAPSHOT snapshot; + KIRQL oldIrql; + + RtlZeroMemory(&snapshot, sizeof(snapshot)); + KeAcquireSpinLock(&Ctx->ConfigLock, &oldIrql); + snapshot.Config = Ctx->Config; + snapshot.TunLuid = Ctx->TunLuid; + snapshot.HasTunLuid = Ctx->HasTunLuid; + KeReleaseSpinLock(&Ctx->ConfigLock, oldIrql); + + return snapshot; +} + +static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const CONFIG_SNAPSHOT* Snapshot, _In_ const PENDING_ENTRY* Entry) { SOCKADDR_INET sourceAddress; SOCKADDR_INET destinationAddress; @@ -76,7 +101,7 @@ static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const WINREDIRECT_CONFIG* Config MIB_IPFORWARD_ROW2 bestRoute; NETIO_STATUS status; - if (Config->TunIndex == 0 || KeGetCurrentIrql() >= DISPATCH_LEVEL) { + if (!Snapshot->HasTunLuid || KeGetCurrentIrql() >= DISPATCH_LEVEL) { return BestRouteUnknown; } @@ -108,7 +133,7 @@ static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const WINREDIRECT_CONFIG* Config if (status != NO_ERROR) { return BestRouteUnknown; } - if (bestRoute.InterfaceIndex == Config->TunIndex) { + if (RtlEqualMemory(&bestRoute.InterfaceLuid, &Snapshot->TunLuid, sizeof(NET_LUID))) { return BestRouteTun; } return BestRouteOther; @@ -119,19 +144,6 @@ typedef struct _LOCAL_REDIRECT_CONTEXT { UINT32 ProcessId; } LOCAL_REDIRECT_CONTEXT, *PLOCAL_REDIRECT_CONTEXT; -static WINREDIRECT_CONFIG ReadConfigSnapshot(_In_ PDRIVER_CONTEXT Ctx) -{ - WINREDIRECT_CONFIG config; - KIRQL oldIrql; - - RtlZeroMemory(&config, sizeof(config)); - KeAcquireSpinLock(&Ctx->ConfigLock, &oldIrql); - config = Ctx->Config; - KeReleaseSpinLock(&Ctx->ConfigLock, oldIrql); - - return config; -} - static void CancelPendingIoctlRequests(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status) { WDFREQUEST request; @@ -324,10 +336,24 @@ void EvtIoDeviceControl( case IOCTL_WINREDIRECT_SET_CONFIG: status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_CONFIG), &inBuf, &inLen); if (NT_SUCCESS(status)) { + WINREDIRECT_CONFIG* config = (WINREDIRECT_CONFIG*)inBuf; + GUID tunGuid; + NET_LUID tunLuid; KIRQL oldIrql; - KeAcquireSpinLock(&ctx->ConfigLock, &oldIrql); - RtlCopyMemory(&ctx->Config, inBuf, sizeof(WINREDIRECT_CONFIG)); - KeReleaseSpinLock(&ctx->ConfigLock, oldIrql); + if (IsZeroGuid(config->TunGuid)) { + status = STATUS_INVALID_PARAMETER; + } else { + RtlZeroMemory(&tunGuid, sizeof(tunGuid)); + RtlCopyMemory(&tunGuid, config->TunGuid, sizeof(tunGuid)); + status = ConvertInterfaceGuidToLuid(&tunGuid, &tunLuid); + } + if (NT_SUCCESS(status)) { + KeAcquireSpinLock(&ctx->ConfigLock, &oldIrql); + RtlCopyMemory(&ctx->Config, config, sizeof(WINREDIRECT_CONFIG)); + ctx->TunLuid = tunLuid; + ctx->HasTunLuid = TRUE; + KeReleaseSpinLock(&ctx->ConfigLock, oldIrql); + } } WdfRequestComplete(Request, status); break; @@ -600,7 +626,7 @@ static void ClassifyFnCommon( return; } - WINREDIRECT_CONFIG config = ReadConfigSnapshot(ctx); + CONFIG_SNAPSHOT snapshot = ReadConfigSnapshot(ctx); #if (NTDDI_VERSION >= NTDDI_WIN8) if (ctx->RedirectHandle && @@ -620,9 +646,9 @@ static void ClassifyFnCommon( } #endif - if (config.ProxyPID != 0 && + if (snapshot.Config.ProxyPID != 0 && FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_PROCESS_ID) && - (UINT32)inMetaValues->processId == config.ProxyPID) { + (UINT32)inMetaValues->processId == snapshot.Config.ProxyPID) { PermitClassify(classifyOut); return; } @@ -671,7 +697,7 @@ static void ClassifyFnCommon( return; } - if (BestRouteForEntry(&config, entry) == BestRouteOther) { + if (BestRouteForEntry(&snapshot, entry) == BestRouteOther) { ExFreePoolWithTag(entry, 'rniW'); PermitClassify(classifyOut); return; @@ -837,9 +863,9 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI FWPS_CONNECT_REQUEST0* connReq = (FWPS_CONNECT_REQUEST0*)Entry->WritableLayerData; if (Verdict == VERDICT_REDIRECT) { - WINREDIRECT_CONFIG config = ReadConfigSnapshot(Ctx); + CONFIG_SNAPSHOT snapshot = ReadConfigSnapshot(Ctx); if (!connReq || - config.RedirectPort == 0 || config.ProxyPID == 0 || Ctx->RedirectHandle == NULL) { + snapshot.Config.RedirectPort == 0 || snapshot.Config.ProxyPID == 0 || Ctx->RedirectHandle == NULL) { Verdict = VERDICT_BYPASS; } else { SOCKADDR_STORAGE* redirectContext = @@ -852,7 +878,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI RtlCopyMemory(&redirectContext[1], &connReq->localAddressAndPort, sizeof(SOCKADDR_STORAGE)); connReq->localRedirectHandle = Ctx->RedirectHandle; - connReq->localRedirectTargetPID = config.ProxyPID; + connReq->localRedirectTargetPID = snapshot.Config.ProxyPID; connReq->localRedirectContext = redirectContext; connReq->localRedirectContextSize = sizeof(SOCKADDR_STORAGE) * 2; @@ -865,7 +891,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI } else { addr->sin_addr = localAddr->sin_addr; } - addr->sin_port = RtlUshortByteSwap(config.RedirectPort); + addr->sin_port = RtlUshortByteSwap(snapshot.Config.RedirectPort); } else { SOCKADDR_IN6* localAddr = (SOCKADDR_IN6*)&connReq->localAddressAndPort; SOCKADDR_IN6* addr = (SOCKADDR_IN6*)&connReq->remoteAddressAndPort; @@ -877,7 +903,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI *addr = *localAddr; addr->sin6_family = AF_INET6; } - addr->sin6_port = RtlUshortByteSwap(config.RedirectPort); + addr->sin6_port = RtlUshortByteSwap(snapshot.Config.RedirectPort); } } } diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index b2e632a9..e5264a47 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -33,9 +33,15 @@ typedef struct _WINREDIRECT_CONFIG { UINT16 RedirectPort; UINT8 _pad0[2]; UINT32 ProxyPID; - UINT32 TunIndex; + UINT8 TunGuid[16]; } WINREDIRECT_CONFIG; +typedef struct _CONFIG_SNAPSHOT { + WINREDIRECT_CONFIG Config; + NET_LUID TunLuid; + BOOLEAN HasTunLuid; +} CONFIG_SNAPSHOT, *PCONFIG_SNAPSHOT; + typedef struct _WINREDIRECT_PENDING_CONN { UINT64 ConnID; UINT8 AddressFamily; @@ -91,6 +97,8 @@ typedef struct _DRIVER_CONTEXT { // Configuration (protected by ConfigLock) KSPIN_LOCK ConfigLock; WINREDIRECT_CONFIG Config; + NET_LUID TunLuid; + BOOLEAN HasTunLuid; volatile LONG Running; // Pending connections (protected by PendingLock) diff --git a/internal/winredirect/types_windows.go b/internal/winredirect/types_windows.go index 705efdfb..a591dc40 100644 --- a/internal/winredirect/types_windows.go +++ b/internal/winredirect/types_windows.go @@ -22,7 +22,7 @@ type Config struct { RedirectPort uint16 _ [2]byte // padding ProxyPID uint32 - TunIndex uint32 + TunGUID [16]byte } // PendingConn is received from the driver via IOCTL_GET_PENDING. diff --git a/redirect_windows.go b/redirect_windows.go index 20ad3f9e..0a6d9846 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -9,6 +9,7 @@ import ( "slices" "strings" "sync" + "unsafe" "github.com/sagernet/sing-tun/internal/winipcfg" "github.com/sagernet/sing-tun/internal/winredirect" @@ -39,9 +40,8 @@ type autoRedirect struct { enableIPv4 bool enableIPv6 bool - localAddressMu sync.RWMutex - localAddresses []netip.Prefix - tunInterfaceIndex uint32 + localAddressMu sync.RWMutex + localAddresses []netip.Prefix workerCount int } @@ -106,19 +106,18 @@ func (r *autoRedirect) Start() error { } r.redirectServer = server - tunInterfaceIndex, err := r.resolveTunInterfaceIndex() + tunGUID, err := r.resolveTunInterfaceGUID() if err != nil { server.Close() manager.Close() return E.Cause(err, "resolve tun interface") } - r.tunInterfaceIndex = tunInterfaceIndex redirectPort := M.AddrPortFromNet(server.listener.Addr()).Port() err = manager.SetConfig(&winredirect.Config{ RedirectPort: redirectPort, ProxyPID: uint32(os.Getpid()), - TunIndex: tunInterfaceIndex, + TunGUID: tunGUID, }) if err != nil { server.Close() @@ -191,11 +190,6 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 return winredirect.VerdictBypass } - // Only intercept connections whose Windows best route resolves to the TUN interface. - if !r.routeWouldHitTun(src.Addr, dst.Addr) { - return winredirect.VerdictBypass - } - // DNS hijack: port 53 from local network → redirect to DNS server if !r.tunOptions.EXP_DisableDNSHijack && dst.Port == 53 { if r.isLocalAddress(src.Addr) { @@ -304,48 +298,40 @@ func (r *autoRedirect) dnsServerForFamily(addr netip.Addr) netip.Addr { return netip.Addr{} } -func (r *autoRedirect) resolveTunInterfaceIndex() (uint32, error) { +func (r *autoRedirect) resolveTunInterfaceGUID() ([16]byte, error) { if r.interfaceFinder != nil { if err := r.interfaceFinder.Update(); err == nil { iface, err := r.interfaceFinder.ByName(r.tunOptions.Name) if err == nil { - return uint32(iface.Index), nil + luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) + if err != nil { + return [16]byte{}, err + } + guid, err := luid.GUID() + if err != nil { + return [16]byte{}, err + } + return guidBytes(guid), nil } } } iface, err := net.InterfaceByName(r.tunOptions.Name) if err != nil { - return 0, err - } - return uint32(iface.Index), nil -} - -func (r *autoRedirect) routeWouldHitTun(src netip.Addr, dst netip.Addr) bool { - if r.tunInterfaceIndex == 0 || !dst.IsValid() { - return true - } - - var destinationAddress winipcfg.RawSockaddrInet - if err := destinationAddress.SetAddr(dst); err != nil { - return true + return [16]byte{}, err } - - var sourceAddress *winipcfg.RawSockaddrInet - if src.IsValid() && !src.IsUnspecified() { - source := new(winipcfg.RawSockaddrInet) - if err := source.SetAddr(src); err == nil { - sourceAddress = source - } + luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) + if err != nil { + return [16]byte{}, err } - - bestRoute, _, err := winipcfg.GetBestRoute2(nil, 0, sourceAddress, &destinationAddress, 0) + guid, err := luid.GUID() if err != nil { - if r.logger != nil { - r.logger.Warn("get best route fallback to redirect: ", err) - } - return true + return [16]byte{}, err } - return bestRoute.InterfaceIndex == r.tunInterfaceIndex + return guidBytes(guid), nil +} + +func guidBytes(guid *windows.GUID) [16]byte { + return *(*[16]byte)(unsafe.Pointer(guid)) } func pendingConnSrc(conn *winredirect.PendingConn) M.Socksaddr { From 39825243e6f160da56fa4fedfa5885230cf70e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 15:44:24 +0800 Subject: [PATCH 17/38] Fix Windows driver route status check --- internal/winredirect/driver/winredirect.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index c8beaa2a..ee6dd2a4 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -130,7 +130,7 @@ static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const CONFIG_SNAPSHOT* Snapshot, } status = GetBestRoute2(NULL, 0, sourceAddressPtr, &destinationAddress, 0, &bestRoute, NULL); - if (status != NO_ERROR) { + if (status != STATUS_SUCCESS) { return BestRouteUnknown; } if (RtlEqualMemory(&bestRoute.InterfaceLuid, &Snapshot->TunLuid, sizeof(NET_LUID))) { From f3de8ca792adc4189919460dcd1d71c4e966863d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 15:49:02 +0800 Subject: [PATCH 18/38] Fix Windows driver warnings --- internal/winredirect/driver/winredirect.c | 8 ++++---- internal/winredirect/driver/winredirect.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index ee6dd2a4..71f3b599 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -338,7 +338,7 @@ void EvtIoDeviceControl( if (NT_SUCCESS(status)) { WINREDIRECT_CONFIG* config = (WINREDIRECT_CONFIG*)inBuf; GUID tunGuid; - NET_LUID tunLuid; + NET_LUID tunLuid = {0}; KIRQL oldIrql; if (IsZeroGuid(config->TunGuid)) { status = STATUS_INVALID_PARAMETER; @@ -385,7 +385,7 @@ void EvtIoDeviceControl( break; case IOCTL_WINREDIRECT_GET_PENDING: - // Forward to manual queue — will be completed when a connection arrives + // Forward to manual queue - will be completed when a connection arrives status = WdfRequestForwardToIoQueue(Request, ctx->PendingIoctlQueue); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); @@ -530,7 +530,7 @@ NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx) status = FwpmCalloutAdd0(Ctx->EngineHandle, &mCalloutV6, NULL, NULL); if (!NT_SUCCESS(status)) goto cleanup; - // Add filters — condition: TCP only + // Add filters - condition: TCP only FWPM_FILTER_CONDITION0 tcpCondition = { .fieldKey = FWPM_CONDITION_IP_PROTOCOL, .matchType = FWP_MATCH_EQUAL, @@ -682,7 +682,7 @@ static void ClassifyFnCommon( if (dstArr) { RtlCopyMemory(entry->DstAddr, dstArr->byteArray16, 16); } else { - // No destination address available — cannot redirect, bail out + // No destination address available - cannot redirect, bail out ExFreePoolWithTag(entry, 'rniW'); PermitClassify(classifyOut); return; diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index e5264a47..3a8f29fd 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -13,7 +13,7 @@ #define DEVICE_NAME L"\\Device\\WinRedirect" #define SYMLINK_NAME L"\\DosDevices\\WinRedirect" -// IOCTL codes — must match Go types_windows.go +// IOCTL codes - must match Go types_windows.go #define IOCTL_WINREDIRECT_SET_CONFIG CTL_CODE(FILE_DEVICE_NETWORK, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WINREDIRECT_START CTL_CODE(FILE_DEVICE_NETWORK, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WINREDIRECT_STOP CTL_CODE(FILE_DEVICE_NETWORK, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) @@ -25,7 +25,7 @@ #define VERDICT_BYPASS 1 #define VERDICT_DROP 2 -// Shared structures — must match Go types_windows.go layout +// Shared structures - must match Go types_windows.go layout #pragma pack(push, 1) From fa93cb0ecd4befd7e0a77d44dfa613c4cc7312dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 15:56:20 +0800 Subject: [PATCH 19/38] Fix Windows auto-redirect test harness --- cmd/test_auto_redirect/main.go | 134 ++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 27 deletions(-) diff --git a/cmd/test_auto_redirect/main.go b/cmd/test_auto_redirect/main.go index 7c331d0a..02824f76 100644 --- a/cmd/test_auto_redirect/main.go +++ b/cmd/test_auto_redirect/main.go @@ -4,17 +4,20 @@ package main import ( "context" + "flag" "fmt" "net" "net/netip" "os" "os/exec" - "strconv" "strings" "sync" "time" tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/internal/winipcfg" + "github.com/sagernet/sing/common/control" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -22,6 +25,9 @@ import ( const ( autoClientModeArg = "client" autoTestTarget = "198.18.0.1:65000" + autoTestRoute = "198.18.0.0/15" + autoTestPrefix = "198.18.0.2/30" + autoTestMTU = 1500 ) type redirectEvent struct { @@ -99,7 +105,7 @@ func main() { if len(os.Args) > 1 && os.Args[1] == autoClientModeArg { err = runClient(autoTestTarget) } else { - err = runAutoRedirect() + err = runAutoRedirect(parseConfig(os.Args[1:])) } if err != nil { fmt.Fprintln(os.Stderr, err) @@ -107,19 +113,82 @@ func main() { } } -func runAutoRedirect() error { - iterations := envInt("WINREDIRECT_ITERATIONS", 3) - concurrency := envInt("WINREDIRECT_CONCURRENCY", 3) +type testConfig struct { + iterations int + concurrency int +} + +func parseConfig(args []string) testConfig { + fs := flag.NewFlagSet("test_auto_redirect", flag.ExitOnError) + cfg := testConfig{} + fs.IntVar(&cfg.iterations, "iterations", 3, "number of test rounds") + fs.IntVar(&cfg.concurrency, "concurrency", 3, "number of child clients per round") + _ = fs.Parse(args) + if cfg.iterations <= 0 { + cfg.iterations = 3 + } + if cfg.concurrency <= 0 { + cfg.concurrency = 3 + } + return cfg +} + +func runAutoRedirect(cfg testConfig) error { + log := logger.NOP() + interfaceFinder := control.NewDefaultInterfaceFinder() + networkMonitor, err := tun.NewNetworkUpdateMonitor(log) + if err != nil { + return fmt.Errorf("create network monitor: %w", err) + } + defer networkMonitor.Close() + if err = networkMonitor.Start(); err != nil { + return fmt.Errorf("start network monitor: %w", err) + } + + interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(networkMonitor, log, tun.DefaultInterfaceMonitorOptions{ + InterfaceFinder: interfaceFinder, + }) + if err != nil { + return fmt.Errorf("create interface monitor: %w", err) + } + defer interfaceMonitor.Close() + if err = interfaceMonitor.Start(); err != nil { + return fmt.Errorf("start interface monitor: %w", err) + } options := tun.Options{ - Inet4Address: []netip.Prefix{netip.MustParsePrefix("198.18.0.2/30")}, + Name: tun.CalculateInterfaceName("singtun-test"), + MTU: autoTestMTU, + AutoRoute: true, + Inet4Address: []netip.Prefix{netip.MustParsePrefix(autoTestPrefix)}, + Inet4RouteAddress: []netip.Prefix{netip.MustParsePrefix(autoTestRoute)}, + InterfaceFinder: interfaceFinder, + InterfaceMonitor: interfaceMonitor, + Logger: log, + } + + tunDevice, err := tun.New(options) + if err != nil { + return fmt.Errorf("create tun: %w", err) + } + defer tunDevice.Close() + if err = tunDevice.Start(); err != nil { + return fmt.Errorf("start tun: %w", err) } - handler := newTestHandler(iterations*concurrency + 8) + + if err = ensureBestRoute(options.Name, netip.MustParseAddr("198.18.0.1")); err != nil { + return fmt.Errorf("verify best route: %w", err) + } + + handler := newTestHandler(cfg.iterations*cfg.concurrency + 8) redirect, err := tun.NewAutoRedirect(tun.AutoRedirectOptions{ - TunOptions: &options, - Context: context.Background(), - Handler: handler, + TunOptions: &options, + Context: context.Background(), + Handler: handler, + Logger: log, + NetworkMonitor: networkMonitor, + InterfaceFinder: interfaceFinder, }) if err != nil { return fmt.Errorf("new auto redirect: %w", err) @@ -131,17 +200,17 @@ func runAutoRedirect() error { } time.Sleep(500 * time.Millisecond) - fmt.Printf("[1] AutoRedirect started iterations=%d concurrency=%d\n", iterations, concurrency) + fmt.Printf("[1] AutoRedirect started interface=%s iterations=%d concurrency=%d\n", options.Name, cfg.iterations, cfg.concurrency) - for iteration := 1; iteration <= iterations; iteration++ { + for iteration := 1; iteration <= cfg.iterations; iteration++ { fmt.Printf("[2.%d] Self-dialing %s (should bypass) ...\n", iteration, autoTestTarget) if err = expectBypass(autoTestTarget); err != nil { return fmt.Errorf("iteration %d self bypass check failed: %w", iteration, err) } fmt.Printf("[2.%d] Bypass confirmed\n", iteration) - fmt.Printf("[3.%d] Launching %d child clients ...\n", iteration, concurrency) - outputs, err := runExternalClients(concurrency) + fmt.Printf("[3.%d] Launching %d child clients ...\n", iteration, cfg.concurrency) + outputs, err := runExternalClients(cfg.concurrency) if err != nil { return fmt.Errorf("iteration %d child clients failed: %w\n%s", iteration, err, strings.Join(outputs, "\n")) } @@ -152,7 +221,7 @@ func runAutoRedirect() error { } } - if err = handler.expectRedirectEvents(concurrency, 5*time.Second); err != nil { + if err = handler.expectRedirectEvents(cfg.concurrency, 5*time.Second); err != nil { return fmt.Errorf("iteration %d redirect validation failed: %w", iteration, err) } fmt.Printf("[3.%d] Redirect confirmed\n", iteration) @@ -161,6 +230,29 @@ func runAutoRedirect() error { return nil } +func ensureBestRoute(interfaceName string, destination netip.Addr) error { + iface, err := net.InterfaceByName(interfaceName) + if err != nil { + return err + } + luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) + if err != nil { + return err + } + var destinationAddress winipcfg.RawSockaddrInet + if err = destinationAddress.SetAddr(destination); err != nil { + return err + } + bestRoute, _, err := winipcfg.GetBestRoute2(nil, 0, nil, &destinationAddress, 0) + if err != nil { + return err + } + if bestRoute.InterfaceLUID != luid { + return fmt.Errorf("destination %s routed via %v, want %v", destination, bestRoute.InterfaceLUID, luid) + } + return nil +} + func runClient(target string) error { fmt.Printf("[client] Dialing %s ...\n", target) conn, err := net.DialTimeout("tcp", target, 5*time.Second) @@ -250,16 +342,4 @@ func (h *testHandler) expectRedirectEvents(expected int, timeout time.Duration) return nil } -func envInt(key string, defaultValue int) int { - value := strings.TrimSpace(os.Getenv(key)) - if value == "" { - return defaultValue - } - parsed, err := strconv.Atoi(value) - if err != nil || parsed <= 0 { - return defaultValue - } - return parsed -} - var _ tun.Handler = (*testHandler)(nil) From d728fea9cd7c6d6b5b0c5ad6f9819148a4eacf17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 16:00:04 +0800 Subject: [PATCH 20/38] Update embedded Windows redirect drivers --- internal/winredirect/amd64/winredirect.sys | Bin 19264 -> 20312 bytes internal/winredirect/arm64/winredirect.sys | Bin 11 -> 28064 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys index a0315527f8ee73133468af342071f2d90e9947c9..16c134d0a607183b3a4826be850b1ca4150e3494 100644 GIT binary patch delta 10196 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 delta 9087 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 diff --git a/internal/winredirect/arm64/winredirect.sys b/internal/winredirect/arm64/winredirect.sys index 311c8dd0658759f372c9be9a2065c327fdb1a203..e5ea0386d201c0e1cb427a247f17f79c1cb2da2b 100644 GIT binary patch literal 28064 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 Date: Fri, 27 Mar 2026 16:49:20 +0800 Subject: [PATCH 21/38] windows: fail fast on redirect runtime errors --- internal/winredirect/amd64/winredirect.sys | Bin 20312 -> 20824 bytes internal/winredirect/arm64/winredirect.sys | Bin 28064 -> 29088 bytes internal/winredirect/driver/winredirect.c | 293 +++++++++++++++++---- internal/winredirect/driver/winredirect.h | 13 +- redirect.go | 1 + redirect_server_windows.go | 10 +- redirect_windows.go | 68 ++++- 7 files changed, 314 insertions(+), 71 deletions(-) diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys index 16c134d0a607183b3a4826be850b1ca4150e3494..ed42ab4d1d0cb1e9b34f5f69b60a31e486630b4f 100644 GIT binary patch delta 9546 zcmZ`;34Bx4(!WWYk}lz+i_NtOO${0$*k%A?*z}rJ z5TpY{S{ik1NkY?p0Yl&8(-xEirF+F`L6BHyJEu7B7n2&W3dLb@0H?}gZy&uc z4Xj2Tsr8A*Vo$X)Ws@8zuCSf_LG(Evvlm!vlfsAPaIpRtpkVSO-=N+PUsP=_WTj4w#u>wsW?Vq4PZj!5^&6k(qO^tLWDB+JxpV zv~jjzdxAsh-~Mxx0SlK|X@A*vI@GT}>2RyXO1fNgj8rPklxEDDS#k9A>C-ZUTBqw} z#bcC}73zCBlTT*D@4Q72oVG7ZS%;f_ zlM2V%y&vyuq@|yfnx@TLxFl@4cPKHrB11vlrKU%?BHoeNTcL?5g3$H}jKg+LYN9zZ zdpnfd587xV2MT` z6@l6i^rq~iWn(E}Q@<&1ek=grX@*(M3IP73o{K1voeK%0A!Kt0Bxos=3 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 delta 8646 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 hah_qXz0j7nd1vjah#h0gbA}ny+{{!vjzgYkP diff --git a/internal/winredirect/arm64/winredirect.sys b/internal/winredirect/arm64/winredirect.sys index e5ea0386d201c0e1cb427a247f17f79c1cb2da2b..2b26ea0ee5534c79145316977ccdbdf0fe9beb66 100644 GIT binary patch delta 7495 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 delta 6463 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 diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 71f3b599..48d03aec 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -26,6 +26,49 @@ static void PermitClassify(_Inout_ FWPS_CLASSIFY_OUT0* classifyOut) classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE; } +static void BlockClassify(_Inout_ FWPS_CLASSIFY_OUT0* classifyOut) +{ + classifyOut->actionType = FWP_ACTION_BLOCK; + classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE; +} + +static NTSTATUS ReadFatalStatus(_In_ PDRIVER_CONTEXT Ctx) +{ + return (NTSTATUS)InterlockedCompareExchange(&Ctx->FatalStatus, STATUS_SUCCESS, STATUS_SUCCESS); +} + +static NTSTATUS NormalizeFatalStatus(_In_ NTSTATUS Status) +{ + if (NT_SUCCESS(Status)) { + return STATUS_DRIVER_INTERNAL_ERROR; + } + return Status; +} + +static NTSTATUS TriggerFatal(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status) +{ + NTSTATUS normalized = NormalizeFatalStatus(Status); + NTSTATUS previous = (NTSTATUS)InterlockedCompareExchange(&Ctx->FatalStatus, normalized, STATUS_SUCCESS); + + if (previous == STATUS_SUCCESS) { + WdfWorkItemEnqueue(Ctx->FatalWorkItem); + return normalized; + } + + return previous; +} + +static void FailClosedClassify( + _In_ PDRIVER_CONTEXT Ctx, + _Inout_ FWPS_CLASSIFY_OUT0* classifyOut, + _In_ NTSTATUS Status) +{ + if (Ctx) { + TriggerFatal(Ctx, Status); + } + BlockClassify(classifyOut); +} + static BOOLEAN IsLoopbackAddress(_In_ UINT8 AddressFamily, _In_reads_(16) const UINT8* Address) { if (AddressFamily == AF_INET) { @@ -63,7 +106,6 @@ static BOOLEAN IsAnyAddress(_In_ UINT8 AddressFamily, _In_reads_(16) const UINT8 } typedef enum _BEST_ROUTE_RESULT { - BestRouteUnknown = 0, BestRouteTun = 1, BestRouteOther = 2, } BEST_ROUTE_RESULT; @@ -93,7 +135,10 @@ static CONFIG_SNAPSHOT ReadConfigSnapshot(_In_ PDRIVER_CONTEXT Ctx) return snapshot; } -static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const CONFIG_SNAPSHOT* Snapshot, _In_ const PENDING_ENTRY* Entry) +static NTSTATUS BestRouteForEntry( + _In_ const CONFIG_SNAPSHOT* Snapshot, + _In_ const PENDING_ENTRY* Entry, + _Out_ BEST_ROUTE_RESULT* Result) { SOCKADDR_INET sourceAddress; SOCKADDR_INET destinationAddress; @@ -101,8 +146,11 @@ static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const CONFIG_SNAPSHOT* Snapshot, MIB_IPFORWARD_ROW2 bestRoute; NETIO_STATUS status; - if (!Snapshot->HasTunLuid || KeGetCurrentIrql() >= DISPATCH_LEVEL) { - return BestRouteUnknown; + if (!Snapshot->HasTunLuid) { + return STATUS_INVALID_DEVICE_STATE; + } + if (KeGetCurrentIrql() >= DISPATCH_LEVEL) { + return STATUS_NOT_SUPPORTED; } RtlZeroMemory(&sourceAddress, sizeof(sourceAddress)); @@ -126,17 +174,19 @@ static BEST_ROUTE_RESULT BestRouteForEntry(_In_ const CONFIG_SNAPSHOT* Snapshot, sourceAddressPtr = &sourceAddress; } } else { - return BestRouteUnknown; + return STATUS_INVALID_PARAMETER; } status = GetBestRoute2(NULL, 0, sourceAddressPtr, &destinationAddress, 0, &bestRoute, NULL); if (status != STATUS_SUCCESS) { - return BestRouteUnknown; + return status; } if (RtlEqualMemory(&bestRoute.InterfaceLuid, &Snapshot->TunLuid, sizeof(NET_LUID))) { - return BestRouteTun; + *Result = BestRouteTun; + return STATUS_SUCCESS; } - return BestRouteOther; + *Result = BestRouteOther; + return STATUS_SUCCESS; } typedef struct _LOCAL_REDIRECT_CONTEXT { @@ -153,7 +203,20 @@ static void CancelPendingIoctlRequests(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS S } } -static PPENDING_ENTRY PendingReserveNextUndelivered(_In_ PDRIVER_CONTEXT Ctx) +static void ShutdownRedirect(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT32 PendingVerdict, _In_ NTSTATUS RequestStatus) +{ + if (InterlockedCompareExchange(&Ctx->Running, FALSE, TRUE) == TRUE) { + WdfTimerStop(Ctx->TimeoutTimer, TRUE); + WdfWorkItemFlush(Ctx->TimeoutWorkItem); + WfpCleanup(Ctx); + WdfWorkItemFlush(Ctx->PendingDeliveryWorkItem); + } + + PendingFlushAll(Ctx, PendingVerdict); + CancelPendingIoctlRequests(Ctx, RequestStatus); +} + +static PPENDING_ENTRY PendingReserveNextQueued(_In_ PDRIVER_CONTEXT Ctx) { PPENDING_ENTRY found = NULL; KIRQL oldIrql; @@ -162,8 +225,8 @@ static PPENDING_ENTRY PendingReserveNextUndelivered(_In_ PDRIVER_CONTEXT Ctx) PLIST_ENTRY entry = Ctx->PendingList.Flink; while (entry != &Ctx->PendingList) { PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); - if (!pending->Delivered) { - pending->Delivered = TRUE; + if (pending->DeliveryState == PendingDeliveryQueued) { + pending->DeliveryState = PendingDeliveryCopying; found = pending; break; } @@ -174,19 +237,23 @@ static PPENDING_ENTRY PendingReserveNextUndelivered(_In_ PDRIVER_CONTEXT Ctx) return found; } -static void PendingSetDelivered(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ BOOLEAN Delivered) +static void PendingSetDeliveryState(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ LONG State) { KIRQL oldIrql; KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); - Entry->Delivered = Delivered; + Entry->DeliveryState = State; KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); } static void TryCompletePendingRequests(_In_ PDRIVER_CONTEXT Ctx) { + if (ReadFatalStatus(Ctx) != STATUS_SUCCESS) { + return; + } + for (;;) { - PPENDING_ENTRY pending = PendingReserveNextUndelivered(Ctx); + PPENDING_ENTRY pending = PendingReserveNextQueued(Ctx); if (!pending) { break; } @@ -194,14 +261,14 @@ static void TryCompletePendingRequests(_In_ PDRIVER_CONTEXT Ctx) WDFREQUEST request; NTSTATUS status = WdfIoQueueRetrieveNextRequest(Ctx->PendingIoctlQueue, &request); if (!NT_SUCCESS(status)) { - PendingSetDelivered(Ctx, pending, FALSE); + PendingSetDeliveryState(Ctx, pending, PendingDeliveryQueued); break; } PVOID outBuf; status = WdfRequestRetrieveOutputBuffer(request, sizeof(WINREDIRECT_PENDING_CONN), &outBuf, NULL); if (!NT_SUCCESS(status)) { - PendingSetDelivered(Ctx, pending, FALSE); + PendingSetDeliveryState(Ctx, pending, PendingDeliveryQueued); WdfRequestComplete(request, status); continue; } @@ -215,6 +282,7 @@ static void TryCompletePendingRequests(_In_ PDRIVER_CONTEXT Ctx) RtlCopyMemory(out->DstAddr, pending->DstAddr, 16); out->DstPort = pending->DstPort; out->ProcessID = pending->ProcessID; + PendingSetDeliveryState(Ctx, pending, PendingDeliveryDelivered); WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, sizeof(WINREDIRECT_PENDING_CONN)); } } @@ -304,18 +372,30 @@ NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING Regi status = WdfWorkItemCreate(&pendingDeliveryConfig, &pendingDeliveryAttrs, &ctx->PendingDeliveryWorkItem); if (!NT_SUCCESS(status)) return status; + WDF_WORKITEM_CONFIG fatalConfig; + WDF_WORKITEM_CONFIG_INIT(&fatalConfig, EvtFatalWorkItem); + WDF_OBJECT_ATTRIBUTES fatalAttrs; + WDF_OBJECT_ATTRIBUTES_INIT(&fatalAttrs); + fatalAttrs.ParentObject = device; + status = WdfWorkItemCreate(&fatalConfig, &fatalAttrs, &ctx->FatalWorkItem); + if (!NT_SUCCESS(status)) return status; + WdfControlFinishInitializing(device); return STATUS_SUCCESS; } void EvtDriverUnload(_In_ WDFDRIVER Driver) { + NTSTATUS fatalStatus; + UNREFERENCED_PARAMETER(Driver); if (g_Ctx) { - WfpCleanup(g_Ctx); - WdfWorkItemFlush(g_Ctx->PendingDeliveryWorkItem); - PendingFlushAll(g_Ctx); - CancelPendingIoctlRequests(g_Ctx, STATUS_CANCELLED); + WdfWorkItemFlush(g_Ctx->FatalWorkItem); + fatalStatus = ReadFatalStatus(g_Ctx); + ShutdownRedirect( + g_Ctx, + fatalStatus != STATUS_SUCCESS ? VERDICT_DROP : VERDICT_BYPASS, + fatalStatus != STATUS_SUCCESS ? fatalStatus : STATUS_CANCELLED); } } @@ -327,20 +407,38 @@ void EvtIoDeviceControl( _In_ ULONG IoControlCode) { UNREFERENCED_PARAMETER(Queue); + UNREFERENCED_PARAMETER(OutputBufferLength); + UNREFERENCED_PARAMETER(InputBufferLength); PDRIVER_CONTEXT ctx = g_Ctx; NTSTATUS status = STATUS_SUCCESS; + NTSTATUS fatalStatus = STATUS_SUCCESS; PVOID inBuf = NULL; size_t inLen = 0; + if (!ctx) { + WdfRequestComplete(Request, STATUS_INVALID_DEVICE_STATE); + return; + } + + fatalStatus = ReadFatalStatus(ctx); + switch (IoControlCode) { case IOCTL_WINREDIRECT_SET_CONFIG: + if (fatalStatus != STATUS_SUCCESS) { + WdfRequestComplete(Request, fatalStatus); + break; + } + if (ctx->Running) { + WdfRequestComplete(Request, STATUS_DEVICE_BUSY); + break; + } status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_CONFIG), &inBuf, &inLen); if (NT_SUCCESS(status)) { WINREDIRECT_CONFIG* config = (WINREDIRECT_CONFIG*)inBuf; GUID tunGuid; NET_LUID tunLuid = {0}; KIRQL oldIrql; - if (IsZeroGuid(config->TunGuid)) { + if (config->RedirectPort == 0 || config->ProxyPID == 0 || IsZeroGuid(config->TunGuid)) { status = STATUS_INVALID_PARAMETER; } else { RtlZeroMemory(&tunGuid, sizeof(tunGuid)); @@ -358,11 +456,22 @@ void EvtIoDeviceControl( WdfRequestComplete(Request, status); break; - case IOCTL_WINREDIRECT_START: + case IOCTL_WINREDIRECT_START: { + CONFIG_SNAPSHOT snapshot; + if (fatalStatus != STATUS_SUCCESS) { + WdfRequestComplete(Request, fatalStatus); + break; + } if (InterlockedCompareExchange(&ctx->Running, TRUE, FALSE) != FALSE) { WdfRequestComplete(Request, STATUS_ALREADY_REGISTERED); break; } + snapshot = ReadConfigSnapshot(ctx); + if (!snapshot.HasTunLuid || snapshot.Config.RedirectPort == 0 || snapshot.Config.ProxyPID == 0) { + InterlockedExchange(&ctx->Running, FALSE); + WdfRequestComplete(Request, STATUS_INVALID_DEVICE_STATE); + break; + } status = WfpSetup(ctx); if (NT_SUCCESS(status)) { WdfTimerStart(ctx->TimeoutTimer, WDF_REL_TIMEOUT_IN_SEC(5)); @@ -371,20 +480,26 @@ void EvtIoDeviceControl( } WdfRequestComplete(Request, status); break; + } case IOCTL_WINREDIRECT_STOP: - if (InterlockedCompareExchange(&ctx->Running, FALSE, TRUE) == TRUE) { - WdfTimerStop(ctx->TimeoutTimer, TRUE); - WdfWorkItemFlush(ctx->TimeoutWorkItem); - WfpCleanup(ctx); - WdfWorkItemFlush(ctx->PendingDeliveryWorkItem); - PendingFlushAll(ctx); - CancelPendingIoctlRequests(ctx, STATUS_CANCELLED); + if (fatalStatus != STATUS_SUCCESS) { + ShutdownRedirect(ctx, VERDICT_DROP, fatalStatus); + } else { + ShutdownRedirect(ctx, VERDICT_BYPASS, STATUS_CANCELLED); } WdfRequestComplete(Request, STATUS_SUCCESS); break; case IOCTL_WINREDIRECT_GET_PENDING: + if (fatalStatus != STATUS_SUCCESS) { + WdfRequestComplete(Request, fatalStatus); + break; + } + if (!ctx->Running) { + WdfRequestComplete(Request, STATUS_DEVICE_NOT_READY); + break; + } // Forward to manual queue - will be completed when a connection arrives status = WdfRequestForwardToIoQueue(Request, ctx->PendingIoctlQueue); if (!NT_SUCCESS(status)) { @@ -395,6 +510,14 @@ void EvtIoDeviceControl( break; case IOCTL_WINREDIRECT_SET_VERDICT: { + if (fatalStatus != STATUS_SUCCESS) { + WdfRequestComplete(Request, fatalStatus); + break; + } + if (!ctx->Running) { + WdfRequestComplete(Request, STATUS_DEVICE_NOT_READY); + break; + } status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_VERDICT), &inBuf, &inLen); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); @@ -433,6 +556,7 @@ void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) { UNREFERENCED_PARAMETER(WorkItem); if (!g_Ctx) return; + if (ReadFatalStatus(g_Ctx) != STATUS_SUCCESS) return; LARGE_INTEGER now; KeQuerySystemTime(&now); @@ -447,7 +571,11 @@ void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); entry = entry->Flink; - // Auto-bypass entries older than 5 seconds + if (pending->DeliveryState != PendingDeliveryQueued) { + continue; + } + + // Auto-bypass queued entries older than 5 seconds. LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; // to seconds if (elapsed >= 5) { RemoveEntryList(&pending->ListEntry); @@ -470,10 +598,25 @@ void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) void EvtPendingDeliveryWorkItem(_In_ WDFWORKITEM WorkItem) { UNREFERENCED_PARAMETER(WorkItem); - if (!g_Ctx || !g_Ctx->Running) return; + if (!g_Ctx || !g_Ctx->Running || ReadFatalStatus(g_Ctx) != STATUS_SUCCESS) return; TryCompletePendingRequests(g_Ctx); } +void EvtFatalWorkItem(_In_ WDFWORKITEM WorkItem) +{ + NTSTATUS fatalStatus; + + UNREFERENCED_PARAMETER(WorkItem); + if (!g_Ctx) return; + + fatalStatus = ReadFatalStatus(g_Ctx); + if (fatalStatus == STATUS_SUCCESS) { + return; + } + + ShutdownRedirect(g_Ctx, VERDICT_DROP, fatalStatus); +} + // --- WFP Setup --- NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx) @@ -616,17 +759,33 @@ static void ClassifyFnCommon( _In_ UINT32 remotePortIdx) { PDRIVER_CONTEXT ctx = g_Ctx; + NTSTATUS fatalStatus; + NTSTATUS status; + CONFIG_SNAPSHOT snapshot; + PPENDING_ENTRY entry; + BEST_ROUTE_RESULT bestRoute; + UINT64 classifyHandle; + + UNREFERENCED_PARAMETER(layerData); + UNREFERENCED_PARAMETER(flowContext); + if (!ctx || !ctx->Running) { PermitClassify(classifyOut); return; } + fatalStatus = ReadFatalStatus(ctx); + if (fatalStatus != STATUS_SUCCESS) { + BlockClassify(classifyOut); + return; + } + // Must have write rights to modify the classify decision if (!(classifyOut->rights & FWPS_RIGHT_ACTION_WRITE)) { return; } - CONFIG_SNAPSHOT snapshot = ReadConfigSnapshot(ctx); + snapshot = ReadConfigSnapshot(ctx); #if (NTDDI_VERSION >= NTDDI_WIN8) if (ctx->RedirectHandle && @@ -654,9 +813,9 @@ static void ClassifyFnCommon( } // Allocate pending entry - PPENDING_ENTRY entry = (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); + entry = (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { - PermitClassify(classifyOut); + FailClosedClassify(ctx, classifyOut, STATUS_INSUFFICIENT_RESOURCES); return; } @@ -682,9 +841,8 @@ static void ClassifyFnCommon( if (dstArr) { RtlCopyMemory(entry->DstAddr, dstArr->byteArray16, 16); } else { - // No destination address available - cannot redirect, bail out ExFreePoolWithTag(entry, 'rniW'); - PermitClassify(classifyOut); + FailClosedClassify(ctx, classifyOut, STATUS_INVALID_ADDRESS_COMPONENT); return; } } @@ -697,7 +855,13 @@ static void ClassifyFnCommon( return; } - if (BestRouteForEntry(&snapshot, entry) == BestRouteOther) { + status = BestRouteForEntry(&snapshot, entry, &bestRoute); + if (!NT_SUCCESS(status)) { + ExFreePoolWithTag(entry, 'rniW'); + FailClosedClassify(ctx, classifyOut, status); + return; + } + if (bestRoute == BestRouteOther) { ExFreePoolWithTag(entry, 'rniW'); PermitClassify(classifyOut); return; @@ -710,16 +874,15 @@ static void ClassifyFnCommon( if (!classifyContext) { ExFreePoolWithTag(entry, 'rniW'); - PermitClassify(classifyOut); + FailClosedClassify(ctx, classifyOut, STATUS_INVALID_DEVICE_STATE); return; } // Pend the classify - UINT64 classifyHandle; - NTSTATUS status = FwpsAcquireClassifyHandle0((void*)classifyContext, 0, &classifyHandle); + status = FwpsAcquireClassifyHandle0((void*)classifyContext, 0, &classifyHandle); if (!NT_SUCCESS(status)) { ExFreePoolWithTag(entry, 'rniW'); - PermitClassify(classifyOut); + FailClosedClassify(ctx, classifyOut, status); return; } @@ -732,7 +895,7 @@ static void ClassifyFnCommon( if (!NT_SUCCESS(status) || !entry->WritableLayerData) { FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); - PermitClassify(classifyOut); + FailClosedClassify(ctx, classifyOut, !NT_SUCCESS(status) ? status : STATUS_INVALID_DEVICE_STATE); return; } @@ -744,14 +907,14 @@ static void ClassifyFnCommon( FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS); FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); - PermitClassify(classifyOut); + FailClosedClassify(ctx, classifyOut, status); return; } - classifyOut->actionType = FWP_ACTION_BLOCK; - classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE; + BlockClassify(classifyOut); KeQuerySystemTime(&entry->Timestamp); + entry->DeliveryState = PendingDeliveryQueued; PendingInsert(ctx, entry); WdfWorkItemEnqueue(ctx->PendingDeliveryWorkItem); } @@ -833,7 +996,7 @@ void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); } -void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx) +void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT32 Verdict) { for (;;) { KIRQL oldIrql; @@ -850,7 +1013,7 @@ void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx) break; } - ExecuteVerdict(Ctx, pending, VERDICT_BYPASS); + ExecuteVerdict(Ctx, pending, Verdict); ExFreePoolWithTag(pending, 'rniW'); } } @@ -861,27 +1024,27 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI { FWPS_CLASSIFY_OUT0 classifyOut = Entry->ClassifyOut; FWPS_CONNECT_REQUEST0* connReq = (FWPS_CONNECT_REQUEST0*)Entry->WritableLayerData; + NTSTATUS redirectStatus = STATUS_SUCCESS; + CONFIG_SNAPSHOT snapshot; if (Verdict == VERDICT_REDIRECT) { - CONFIG_SNAPSHOT snapshot = ReadConfigSnapshot(Ctx); + snapshot = ReadConfigSnapshot(Ctx); if (!connReq || - snapshot.Config.RedirectPort == 0 || snapshot.Config.ProxyPID == 0 || Ctx->RedirectHandle == NULL) { - Verdict = VERDICT_BYPASS; + !snapshot.HasTunLuid || + snapshot.Config.RedirectPort == 0 || + snapshot.Config.ProxyPID == 0 || + Ctx->RedirectHandle == NULL) { + redirectStatus = STATUS_INVALID_DEVICE_STATE; } else { SOCKADDR_STORAGE* redirectContext = (SOCKADDR_STORAGE*)ExAllocatePoolWithTag(NonPagedPool, sizeof(SOCKADDR_STORAGE) * 2, 'rniW'); if (!redirectContext) { - Verdict = VERDICT_BYPASS; + redirectStatus = STATUS_INSUFFICIENT_RESOURCES; } else { RtlZeroMemory(redirectContext, sizeof(SOCKADDR_STORAGE) * 2); RtlCopyMemory(&redirectContext[0], &connReq->remoteAddressAndPort, sizeof(SOCKADDR_STORAGE)); RtlCopyMemory(&redirectContext[1], &connReq->localAddressAndPort, sizeof(SOCKADDR_STORAGE)); - connReq->localRedirectHandle = Ctx->RedirectHandle; - connReq->localRedirectTargetPID = snapshot.Config.ProxyPID; - connReq->localRedirectContext = redirectContext; - connReq->localRedirectContextSize = sizeof(SOCKADDR_STORAGE) * 2; - if (Entry->AddressFamily == AF_INET) { SOCKADDR_IN* localAddr = (SOCKADDR_IN*)&connReq->localAddressAndPort; SOCKADDR_IN* addr = (SOCKADDR_IN*)&connReq->remoteAddressAndPort; @@ -892,7 +1055,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI addr->sin_addr = localAddr->sin_addr; } addr->sin_port = RtlUshortByteSwap(snapshot.Config.RedirectPort); - } else { + } else if (Entry->AddressFamily == AF_INET6) { SOCKADDR_IN6* localAddr = (SOCKADDR_IN6*)&connReq->localAddressAndPort; SOCKADDR_IN6* addr = (SOCKADDR_IN6*)&connReq->remoteAddressAndPort; if (IsAnyAddress(AF_INET6, localAddr->sin6_addr.u.Byte)) { @@ -904,9 +1067,25 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI addr->sin6_family = AF_INET6; } addr->sin6_port = RtlUshortByteSwap(snapshot.Config.RedirectPort); + } else { + redirectStatus = STATUS_INVALID_PARAMETER; + } + + if (NT_SUCCESS(redirectStatus)) { + connReq->localRedirectHandle = Ctx->RedirectHandle; + connReq->localRedirectTargetPID = snapshot.Config.ProxyPID; + connReq->localRedirectContext = redirectContext; + connReq->localRedirectContextSize = sizeof(SOCKADDR_STORAGE) * 2; + } else { + ExFreePoolWithTag(redirectContext, 'rniW'); } } } + + if (!NT_SUCCESS(redirectStatus)) { + TriggerFatal(Ctx, redirectStatus); + Verdict = VERDICT_DROP; + } } if (Entry->WritableLayerData) { diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 3a8f29fd..9273098c 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -63,6 +63,12 @@ typedef struct _WINREDIRECT_VERDICT { #pragma pack(pop) +typedef enum _PENDING_DELIVERY_STATE { + PendingDeliveryQueued = 0, + PendingDeliveryCopying = 1, + PendingDeliveryDelivered = 2, +} PENDING_DELIVERY_STATE; + // Internal pending connection entry typedef struct _PENDING_ENTRY { LIST_ENTRY ListEntry; @@ -71,7 +77,7 @@ typedef struct _PENDING_ENTRY { UINT64 FilterId; FWPS_CLASSIFY_OUT0 ClassifyOut; PVOID WritableLayerData; - BOOLEAN Delivered; + volatile LONG DeliveryState; UINT8 AddressFamily; UINT8 SrcAddr[16]; UINT16 SrcPort; @@ -100,6 +106,7 @@ typedef struct _DRIVER_CONTEXT { NET_LUID TunLuid; BOOLEAN HasTunLuid; volatile LONG Running; + volatile LONG FatalStatus; // Pending connections (protected by PendingLock) LIST_ENTRY PendingList; @@ -110,6 +117,7 @@ typedef struct _DRIVER_CONTEXT { WDFTIMER TimeoutTimer; WDFWORKITEM TimeoutWorkItem; WDFWORKITEM PendingDeliveryWorkItem; + WDFWORKITEM FatalWorkItem; } DRIVER_CONTEXT, *PDRIVER_CONTEXT; WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DRIVER_CONTEXT, GetDriverContext) @@ -122,6 +130,7 @@ EVT_WDF_IO_QUEUE_IO_CANCELED_ON_QUEUE EvtIoCanceledOnQueue; EVT_WDF_TIMER EvtTimeoutTimer; EVT_WDF_WORKITEM EvtTimeoutWorkItem; EVT_WDF_WORKITEM EvtPendingDeliveryWorkItem; +EVT_WDF_WORKITEM EvtFatalWorkItem; // WFP functions NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx); @@ -159,7 +168,7 @@ PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx); void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry); PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID); void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry); -void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx); +void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT32 Verdict); // Verdict execution void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UINT32 Verdict); diff --git a/redirect.go b/redirect.go index dcf3e720..2f8d85ac 100644 --- a/redirect.go +++ b/redirect.go @@ -27,6 +27,7 @@ type AutoRedirectOptions struct { Context context.Context Handler Handler Logger logger.Logger + ErrorHandler func(error) NetworkMonitor NetworkUpdateMonitor InterfaceFinder control.InterfaceFinder TableName string diff --git a/redirect_server_windows.go b/redirect_server_windows.go index 538a67ea..1f45e84e 100644 --- a/redirect_server_windows.go +++ b/redirect_server_windows.go @@ -19,6 +19,7 @@ type redirectServer struct { ctx context.Context handler N.TCPConnectionHandlerEx logger logger.Logger + onFatal func(error) listenAddr netip.Addr listener *net.TCPListener connTable *connMetadataTable @@ -31,11 +32,12 @@ func (s *redirectServer) logError(args ...any) { } } -func newRedirectServerWindows(ctx context.Context, handler N.TCPConnectionHandlerEx, logger logger.Logger, listenAddr netip.Addr) *redirectServer { +func newRedirectServerWindows(ctx context.Context, handler N.TCPConnectionHandlerEx, logger logger.Logger, listenAddr netip.Addr, onFatal func(error)) *redirectServer { return &redirectServer{ ctx: ctx, handler: handler, logger: logger, + onFatal: onFatal, listenAddr: listenAddr, connTable: newConnMetadataTable(), } @@ -75,7 +77,11 @@ func (s *redirectServer) loopIn() { return } s.listener.Close() - s.logError("serve error: ", err) + if s.onFatal != nil { + s.onFatal(E.Cause(err, "accept redirect connection")) + } else { + s.logError("serve error: ", err) + } return } source := M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() diff --git a/redirect_windows.go b/redirect_windows.go index 0a6d9846..774a7a11 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -9,6 +9,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "unsafe" "github.com/sagernet/sing-tun/internal/winipcfg" @@ -29,6 +30,7 @@ type autoRedirect struct { ctx context.Context handler Handler logger logger.Logger + errorHandler func(error) networkMonitor NetworkUpdateMonitor networkListener *list.Element[NetworkUpdateCallback] interfaceFinder control.InterfaceFinder @@ -44,6 +46,11 @@ type autoRedirect struct { localAddresses []netip.Prefix workerCount int + + closing atomic.Bool + closeOnce sync.Once + closeErr error + fatalOnce sync.Once } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { @@ -52,6 +59,7 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { ctx: options.Context, handler: options.Handler, logger: options.Logger, + errorHandler: options.ErrorHandler, networkMonitor: options.NetworkMonitor, interfaceFinder: options.InterfaceFinder, routeAddressSet: options.RouteAddressSet, @@ -77,18 +85,21 @@ func (r *autoRedirect) Start() error { err = manager.Install() if err != nil { manager.Close() + r.driverManager = nil return E.Cause(err, "install driver") } err = manager.Start() if err != nil { manager.Close() + r.driverManager = nil return E.Cause(err, "start driver") } err = manager.OpenDevice() if err != nil { manager.Close() + r.driverManager = nil return E.Cause(err, "open driver device") } @@ -98,18 +109,22 @@ func (r *autoRedirect) Start() error { } else { listenAddr = netip.IPv4Unspecified() } - server := newRedirectServerWindows(r.ctx, r.handler, r.logger, listenAddr) + server := newRedirectServerWindows(r.ctx, r.handler, r.logger, listenAddr, r.handleFatalError) + r.redirectServer = server err = server.Start() if err != nil { + r.redirectServer = nil manager.Close() + r.driverManager = nil return E.Cause(err, "start redirect server") } - r.redirectServer = server tunGUID, err := r.resolveTunInterfaceGUID() if err != nil { server.Close() manager.Close() + r.redirectServer = nil + r.driverManager = nil return E.Cause(err, "resolve tun interface") } @@ -122,6 +137,8 @@ func (r *autoRedirect) Start() error { if err != nil { server.Close() manager.Close() + r.redirectServer = nil + r.driverManager = nil return E.Cause(err, "set driver config") } @@ -129,6 +146,8 @@ func (r *autoRedirect) Start() error { if err != nil { server.Close() manager.Close() + r.redirectServer = nil + r.driverManager = nil return E.Cause(err, "start redirect") } @@ -147,13 +166,18 @@ func (r *autoRedirect) Start() error { } func (r *autoRedirect) Close() error { - if r.networkMonitor != nil && r.networkListener != nil { - r.networkMonitor.UnregisterCallback(r.networkListener) - } - return common.Close( - common.PtrOrNil(r.redirectServer), - common.PtrOrNil(r.driverManager), - ) + r.closing.Store(true) + r.closeOnce.Do(func() { + if r.networkMonitor != nil && r.networkListener != nil { + r.networkMonitor.UnregisterCallback(r.networkListener) + r.networkListener = nil + } + r.closeErr = common.Close( + common.PtrOrNil(r.redirectServer), + common.PtrOrNil(r.driverManager), + ) + }) + return r.closeErr } func (r *autoRedirect) UpdateRouteAddressSet() { @@ -166,16 +190,40 @@ func (r *autoRedirect) preMatchWorker() { for { conn, err := r.driverManager.GetPendingConn() if err != nil { + if !r.closing.Load() { + r.handleFatalError(E.Cause(err, "get pending connection")) + } return } verdict := r.evaluateConnection(conn) - r.driverManager.SetVerdict(&winredirect.Verdict{ + err = r.driverManager.SetVerdict(&winredirect.Verdict{ ConnID: conn.ConnID, Verdict: verdict, }) + if err != nil { + if !r.closing.Load() { + r.handleFatalError(E.Cause(err, "set redirect verdict")) + } + return + } } } +func (r *autoRedirect) handleFatalError(err error) { + if err == nil || r.closing.Load() { + return + } + r.fatalOnce.Do(func() { + if r.logger != nil { + r.logger.Error("windows auto-redirect fatal error: ", err) + } + _ = r.Close() + if r.errorHandler != nil { + r.errorHandler(err) + } + }) +} + func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 { dst := pendingConnDst(conn) src := pendingConnSrc(conn) From 4337150443772de682d0f5f8ee759b14a2f69148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 17:54:08 +0800 Subject: [PATCH 22/38] windows: pass context through auto redirect --- cmd/test_auto_redirect/main.go | 345 --------------------------------- nfqueue_linux.go | 2 +- redirect.go | 1 + redirect_server_windows.go | 13 +- redirect_windows.go | 26 ++- stack_gvisor_icmp.go | 2 + stack_gvisor_tcp.go | 2 +- stack_gvisor_udp.go | 2 +- stack_system.go | 8 +- stack_system_nat.go | 4 +- tun.go | 2 + 11 files changed, 45 insertions(+), 362 deletions(-) delete mode 100644 cmd/test_auto_redirect/main.go diff --git a/cmd/test_auto_redirect/main.go b/cmd/test_auto_redirect/main.go deleted file mode 100644 index 02824f76..00000000 --- a/cmd/test_auto_redirect/main.go +++ /dev/null @@ -1,345 +0,0 @@ -//go:build windows - -package main - -import ( - "context" - "flag" - "fmt" - "net" - "net/netip" - "os" - "os/exec" - "strings" - "sync" - "time" - - tun "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/internal/winipcfg" - "github.com/sagernet/sing/common/control" - "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -const ( - autoClientModeArg = "client" - autoTestTarget = "198.18.0.1:65000" - autoTestRoute = "198.18.0.0/15" - autoTestPrefix = "198.18.0.2/30" - autoTestMTU = 1500 -) - -type redirectEvent struct { - source string - destination string - processID uint32 - processPath string -} - -type testHandler struct { - redirectEvents chan redirectEvent -} - -func newTestHandler(bufferSize int) *testHandler { - return &testHandler{ - redirectEvents: make(chan redirectEvent, bufferSize), - } -} - -func (h *testHandler) PrepareConnection( - network string, - source M.Socksaddr, - destination M.Socksaddr, - routeContext tun.DirectRouteContext, - timeout time.Duration, -) (tun.DirectRouteDestination, error) { - return nil, nil -} - -func (h *testHandler) NewConnectionEx( - ctx context.Context, - conn net.Conn, - source M.Socksaddr, - destination M.Socksaddr, - onClose N.CloseHandlerFunc, -) { - defer conn.Close() - if onClose != nil { - defer onClose(nil) - } - if metadata := tun.AutoRedirectMetadataFromContext(ctx); metadata != nil { - fmt.Printf("[redirect] source=%s destination=%s pid=%d path=%s\n", - source, destination, metadata.ProcessID, metadata.ProcessPath) - h.redirectEvents <- redirectEvent{ - source: source.String(), - destination: destination.String(), - processID: metadata.ProcessID, - processPath: metadata.ProcessPath, - } - } else { - fmt.Printf("[redirect] source=%s destination=%s metadata=\n", source, destination) - h.redirectEvents <- redirectEvent{ - source: source.String(), - destination: destination.String(), - } - } - _, _ = conn.Write([]byte("AUTO REDIRECT OK\n")) -} - -func (h *testHandler) NewPacketConnectionEx( - ctx context.Context, - conn N.PacketConn, - source M.Socksaddr, - destination M.Socksaddr, - onClose N.CloseHandlerFunc, -) { - if onClose != nil { - onClose(nil) - } - _ = conn.Close() -} - -func main() { - var err error - if len(os.Args) > 1 && os.Args[1] == autoClientModeArg { - err = runClient(autoTestTarget) - } else { - err = runAutoRedirect(parseConfig(os.Args[1:])) - } - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -type testConfig struct { - iterations int - concurrency int -} - -func parseConfig(args []string) testConfig { - fs := flag.NewFlagSet("test_auto_redirect", flag.ExitOnError) - cfg := testConfig{} - fs.IntVar(&cfg.iterations, "iterations", 3, "number of test rounds") - fs.IntVar(&cfg.concurrency, "concurrency", 3, "number of child clients per round") - _ = fs.Parse(args) - if cfg.iterations <= 0 { - cfg.iterations = 3 - } - if cfg.concurrency <= 0 { - cfg.concurrency = 3 - } - return cfg -} - -func runAutoRedirect(cfg testConfig) error { - log := logger.NOP() - interfaceFinder := control.NewDefaultInterfaceFinder() - networkMonitor, err := tun.NewNetworkUpdateMonitor(log) - if err != nil { - return fmt.Errorf("create network monitor: %w", err) - } - defer networkMonitor.Close() - if err = networkMonitor.Start(); err != nil { - return fmt.Errorf("start network monitor: %w", err) - } - - interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(networkMonitor, log, tun.DefaultInterfaceMonitorOptions{ - InterfaceFinder: interfaceFinder, - }) - if err != nil { - return fmt.Errorf("create interface monitor: %w", err) - } - defer interfaceMonitor.Close() - if err = interfaceMonitor.Start(); err != nil { - return fmt.Errorf("start interface monitor: %w", err) - } - - options := tun.Options{ - Name: tun.CalculateInterfaceName("singtun-test"), - MTU: autoTestMTU, - AutoRoute: true, - Inet4Address: []netip.Prefix{netip.MustParsePrefix(autoTestPrefix)}, - Inet4RouteAddress: []netip.Prefix{netip.MustParsePrefix(autoTestRoute)}, - InterfaceFinder: interfaceFinder, - InterfaceMonitor: interfaceMonitor, - Logger: log, - } - - tunDevice, err := tun.New(options) - if err != nil { - return fmt.Errorf("create tun: %w", err) - } - defer tunDevice.Close() - if err = tunDevice.Start(); err != nil { - return fmt.Errorf("start tun: %w", err) - } - - if err = ensureBestRoute(options.Name, netip.MustParseAddr("198.18.0.1")); err != nil { - return fmt.Errorf("verify best route: %w", err) - } - - handler := newTestHandler(cfg.iterations*cfg.concurrency + 8) - - redirect, err := tun.NewAutoRedirect(tun.AutoRedirectOptions{ - TunOptions: &options, - Context: context.Background(), - Handler: handler, - Logger: log, - NetworkMonitor: networkMonitor, - InterfaceFinder: interfaceFinder, - }) - if err != nil { - return fmt.Errorf("new auto redirect: %w", err) - } - defer redirect.Close() - - if err = redirect.Start(); err != nil { - return fmt.Errorf("start auto redirect: %w", err) - } - - time.Sleep(500 * time.Millisecond) - fmt.Printf("[1] AutoRedirect started interface=%s iterations=%d concurrency=%d\n", options.Name, cfg.iterations, cfg.concurrency) - - for iteration := 1; iteration <= cfg.iterations; iteration++ { - fmt.Printf("[2.%d] Self-dialing %s (should bypass) ...\n", iteration, autoTestTarget) - if err = expectBypass(autoTestTarget); err != nil { - return fmt.Errorf("iteration %d self bypass check failed: %w", iteration, err) - } - fmt.Printf("[2.%d] Bypass confirmed\n", iteration) - - fmt.Printf("[3.%d] Launching %d child clients ...\n", iteration, cfg.concurrency) - outputs, err := runExternalClients(cfg.concurrency) - if err != nil { - return fmt.Errorf("iteration %d child clients failed: %w\n%s", iteration, err, strings.Join(outputs, "\n")) - } - for _, output := range outputs { - fmt.Print(output) - if !strings.Contains(output, "AUTO REDIRECT OK") { - return fmt.Errorf("iteration %d child client did not receive redirected response", iteration) - } - } - - if err = handler.expectRedirectEvents(cfg.concurrency, 5*time.Second); err != nil { - return fmt.Errorf("iteration %d redirect validation failed: %w", iteration, err) - } - fmt.Printf("[3.%d] Redirect confirmed\n", iteration) - } - - return nil -} - -func ensureBestRoute(interfaceName string, destination netip.Addr) error { - iface, err := net.InterfaceByName(interfaceName) - if err != nil { - return err - } - luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) - if err != nil { - return err - } - var destinationAddress winipcfg.RawSockaddrInet - if err = destinationAddress.SetAddr(destination); err != nil { - return err - } - bestRoute, _, err := winipcfg.GetBestRoute2(nil, 0, nil, &destinationAddress, 0) - if err != nil { - return err - } - if bestRoute.InterfaceLUID != luid { - return fmt.Errorf("destination %s routed via %v, want %v", destination, bestRoute.InterfaceLUID, luid) - } - return nil -} - -func runClient(target string) error { - fmt.Printf("[client] Dialing %s ...\n", target) - conn, err := net.DialTimeout("tcp", target, 5*time.Second) - if err != nil { - return fmt.Errorf("client dial: %w", err) - } - defer conn.Close() - - bufData := make([]byte, 256) - _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - n, err := conn.Read(bufData) - if err != nil { - return fmt.Errorf("client read: %w", err) - } - fmt.Printf("[client] Response: %q\n", string(bufData[:n])) - return nil -} - -func runExternalClient() (string, error) { - executable, err := os.Executable() - if err != nil { - return "", err - } - cmd := exec.Command(executable, autoClientModeArg) - output, err := cmd.CombinedOutput() - return string(output), err -} - -func runExternalClients(concurrency int) ([]string, error) { - outputs := make([]string, concurrency) - errs := make([]error, concurrency) - - var wg sync.WaitGroup - for i := 0; i < concurrency; i++ { - wg.Add(1) - go func(index int) { - defer wg.Done() - outputs[index], errs[index] = runExternalClient() - }(i) - } - wg.Wait() - - for _, err := range errs { - if err != nil { - return outputs, err - } - } - - return outputs, nil -} - -func expectBypass(target string) error { - conn, err := net.DialTimeout("tcp", target, 1200*time.Millisecond) - if err == nil { - defer conn.Close() - bufData := make([]byte, 64) - _ = conn.SetReadDeadline(time.Now().Add(300 * time.Millisecond)) - n, readErr := conn.Read(bufData) - if n > 0 && string(bufData[:n]) == "AUTO REDIRECT OK\n" { - return fmt.Errorf("proxy process was redirected unexpectedly") - } - if readErr == nil { - return fmt.Errorf("unexpected data from bypass connection: %q", string(bufData[:n])) - } - } - return nil -} - -func (h *testHandler) expectRedirectEvents(expected int, timeout time.Duration) error { - deadline := time.After(timeout) - for i := 0; i < expected; i++ { - select { - case event := <-h.redirectEvents: - if event.destination != autoTestTarget { - return fmt.Errorf("unexpected destination %s", event.destination) - } - if event.processID == 0 { - return fmt.Errorf("missing process id for redirected connection") - } - if !strings.Contains(strings.ToLower(event.processPath), "test_auto_redirect.exe") { - return fmt.Errorf("unexpected process path %q", event.processPath) - } - case <-deadline: - return fmt.Errorf("timed out waiting for redirect events") - } - } - return nil -} - -var _ tun.Handler = (*testHandler)(nil) diff --git a/nfqueue_linux.go b/nfqueue_linux.go index baaefb54..4b8a6eba 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -212,7 +212,7 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int { return 0 } - _, pErr := h.handler.PrepareConnection(N.NetworkTCP, srcAddr, dstAddr, nil, 0) + _, pErr := h.handler.PrepareConnection(h.ctx, N.NetworkTCP, srcAddr, dstAddr, nil, 0) // Use NfRepeat for bypass/reset so the packet re-enters the chain // from the beginning, allowing mark-checking rules to save the mark diff --git a/redirect.go b/redirect.go index 2f8d85ac..5a04528a 100644 --- a/redirect.go +++ b/redirect.go @@ -25,6 +25,7 @@ type AutoRedirect interface { type AutoRedirectOptions struct { TunOptions *Options Context context.Context + ConnContext func(ctx context.Context) context.Context Handler Handler Logger logger.Logger ErrorHandler func(error) diff --git a/redirect_server_windows.go b/redirect_server_windows.go index 1f45e84e..8f7063c9 100644 --- a/redirect_server_windows.go +++ b/redirect_server_windows.go @@ -96,9 +96,9 @@ func (s *redirectServer) loopIn() { if entry.IsDNS { destination = entry.DNSServer } - ctx := s.ctx - if entry.Metadata != nil { - ctx = ContextWithAutoRedirectMetadata(ctx, entry.Metadata) + ctx := entry.Context + if ctx == nil { + ctx = s.ctx } go s.handler.NewConnectionEx(ctx, conn, source, destination, nil) } @@ -119,6 +119,7 @@ type connKey struct { type connEntry struct { Destination M.Socksaddr + Context context.Context Metadata *AutoRedirectMetadata IsDNS bool DNSServer M.Socksaddr @@ -133,23 +134,25 @@ func newConnMetadataTable() *connMetadataTable { return t } -func (t *connMetadataTable) Store(src M.Socksaddr, dst M.Socksaddr, metadata *AutoRedirectMetadata) { +func (t *connMetadataTable) Store(src M.Socksaddr, dst M.Socksaddr, ctx context.Context, metadata *AutoRedirectMetadata) { key := connKey{Addr: src.Addr, Port: src.Port} t.mu.Lock() defer t.mu.Unlock() t.entries[key] = &connEntry{ Destination: dst, + Context: ctx, Metadata: metadata, CreatedAt: time.Now(), } } -func (t *connMetadataTable) StoreDNS(src M.Socksaddr, originalDst M.Socksaddr, dnsServer M.Socksaddr, metadata *AutoRedirectMetadata) { +func (t *connMetadataTable) StoreDNS(src M.Socksaddr, originalDst M.Socksaddr, dnsServer M.Socksaddr, ctx context.Context, metadata *AutoRedirectMetadata) { key := connKey{Addr: src.Addr, Port: src.Port} t.mu.Lock() defer t.mu.Unlock() t.entries[key] = &connEntry{ Destination: originalDst, + Context: ctx, Metadata: metadata, IsDNS: true, DNSServer: dnsServer, diff --git a/redirect_windows.go b/redirect_windows.go index 774a7a11..a8e0d476 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -28,6 +28,7 @@ import ( type autoRedirect struct { tunOptions *Options ctx context.Context + connContext func(context.Context) context.Context handler Handler logger logger.Logger errorHandler func(error) @@ -57,6 +58,7 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { r := &autoRedirect{ tunOptions: options.TunOptions, ctx: options.Context, + connContext: options.ConnContext, handler: options.Handler, logger: options.Logger, errorHandler: options.ErrorHandler, @@ -64,7 +66,7 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { interfaceFinder: options.InterfaceFinder, routeAddressSet: options.RouteAddressSet, routeExcludeAddressSet: options.RouteExcludeAddressSet, - workerCount: 4, + workerCount: 1, } return r, nil } @@ -244,7 +246,8 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 dnsServer := r.dnsServerForFamily(dst.Addr) if dnsServer.IsValid() { metadata := r.resolveMetadata(conn) - r.redirectServer.connTable.StoreDNS(src, dst, M.SocksaddrFrom(dnsServer, 53), metadata) + ctx := r.newConnContext(metadata) + r.redirectServer.connTable.StoreDNS(src, dst, M.SocksaddrFrom(dnsServer, 53), ctx, metadata) return winredirect.VerdictRedirect } } @@ -257,9 +260,10 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 // Resolve PID → process path metadata := r.resolveMetadata(conn) + ctx := r.newConnContext(metadata) // PrepareConnection (NFQUEUE equivalent) - _, err := r.handler.PrepareConnection("tcp", src, dst, nil, 0) + _, err := r.handler.PrepareConnection(ctx, "tcp", src, dst, nil, 0) if errors.Is(err, ErrDrop) { return winredirect.VerdictDrop } @@ -271,11 +275,25 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 } // Store metadata for redirect server - r.redirectServer.connTable.Store(src, dst, metadata) + r.redirectServer.connTable.Store(src, dst, ctx, metadata) return winredirect.VerdictRedirect } +func (r *autoRedirect) newConnContext(metadata *AutoRedirectMetadata) context.Context { + ctx := r.ctx + if ctx == nil { + ctx = context.Background() + } + if r.connContext != nil { + ctx = r.connContext(ctx) + } + if metadata != nil { + ctx = ContextWithAutoRedirectMetadata(ctx, metadata) + } + return ctx +} + func (r *autoRedirect) resolveMetadata(conn *winredirect.PendingConn) *AutoRedirectMetadata { processPath, _ := queryFullProcessImageName(conn.ProcessID) return &AutoRedirectMetadata{ diff --git a/stack_gvisor_icmp.go b/stack_gvisor_icmp.go index da5549b6..ed33c335 100644 --- a/stack_gvisor_icmp.go +++ b/stack_gvisor_icmp.go @@ -62,6 +62,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa if destinationAddr != f.inet4Address { action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return f.handler.PrepareConnection( + f.ctx, N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), @@ -123,6 +124,7 @@ func (f *ICMPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pa if destinationAddr != f.inet6Address { action, err := f.mapping.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return f.handler.PrepareConnection( + f.ctx, N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), diff --git a/stack_gvisor_tcp.go b/stack_gvisor_tcp.go index 0c63ee11..82327509 100644 --- a/stack_gvisor_tcp.go +++ b/stack_gvisor_tcp.go @@ -79,7 +79,7 @@ func (f *TCPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func (f *TCPForwarder) Forward(r *tcp.ForwarderRequest) { source := M.SocksaddrFrom(AddrFromAddress(r.ID().RemoteAddress), r.ID().RemotePort) destination := M.SocksaddrFrom(AddrFromAddress(r.ID().LocalAddress), r.ID().LocalPort) - _, pErr := f.handler.PrepareConnection(N.NetworkTCP, source, destination, nil, 0) + _, pErr := f.handler.PrepareConnection(f.ctx, N.NetworkTCP, source, destination, nil, 0) if pErr != nil { r.Complete(!errors.Is(pErr, ErrDrop)) return diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index 2e8ff3e7..1a21c19d 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -58,7 +58,7 @@ func (f *UDPForwarder) HandlePacket(id stack.TransportEndpointID, pkt *stack.Pac func rangeIterate(r stack.Range, fn func(*buffer.View)) func (f *UDPForwarder) PreparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - _, pErr := f.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) + _, pErr := f.handler.PrepareConnection(f.ctx, N.NetworkUDP, source, destination, nil, 0) if pErr != nil { if !errors.Is(pErr, ErrDrop) { gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) diff --git a/stack_system.go b/stack_system.go index e2cdd45e..84127470 100644 --- a/stack_system.go +++ b/stack_system.go @@ -399,7 +399,7 @@ func (s *System) processIPv4TCP(ipHdr header.IPv4, tcpHdr header.TCP) (bool, err } } if !loopback { - natPort, err := s.tcpNat.Lookup(source, destination, s.handler) + natPort, err := s.tcpNat.Lookup(s.ctx, source, destination, s.handler) if err != nil { if errors.Is(err, ErrDrop) { return false, nil @@ -494,7 +494,7 @@ func (s *System) processIPv6TCP(ipHdr header.IPv6, tcpHdr header.TCP) (bool, err } } if !loopback { - natPort, err := s.tcpNat.Lookup(source, destination, s.handler) + natPort, err := s.tcpNat.Lookup(s.ctx, source, destination, s.handler) if err != nil { if errors.Is(err, ErrDrop) { return false, nil @@ -589,7 +589,7 @@ func (s *System) processIPv6UDP(ipHdr header.IPv6, udpHdr header.UDP) error { } func (s *System) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - _, pErr := s.handler.PrepareConnection(N.NetworkUDP, source, destination, nil, 0) + _, pErr := s.handler.PrepareConnection(s.ctx, N.NetworkUDP, source, destination, nil, 0) if pErr != nil { if !errors.Is(pErr, ErrDrop) { if source.IsIPv4() { @@ -640,6 +640,7 @@ func (s *System) processIPv4ICMP(ipHdr header.IPv4, icmpHdr header.ICMPv4) (bool if destinationAddr != s.inet4Address { action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return s.handler.PrepareConnection( + s.ctx, N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), @@ -715,6 +716,7 @@ func (s *System) processIPv6ICMP(ipHdr header.IPv6, icmpHdr header.ICMPv6) (bool if destinationAddr != s.inet6Address { action, err := s.directNat.Lookup(DirectRouteSession{Source: sourceAddr, Destination: destinationAddr}, func(timeout time.Duration) (DirectRouteDestination, error) { return s.handler.PrepareConnection( + s.ctx, N.NetworkICMP, M.SocksaddrFrom(sourceAddr, 0), M.SocksaddrFrom(destinationAddr, 0), diff --git a/stack_system_nat.go b/stack_system_nat.go index 636b561d..0ab62beb 100644 --- a/stack_system_nat.go +++ b/stack_system_nat.go @@ -80,14 +80,14 @@ func (n *TCPNat) LookupBack(port uint16) *TCPSession { return session } -func (n *TCPNat) Lookup(source netip.AddrPort, destination netip.AddrPort, handler Handler) (uint16, error) { +func (n *TCPNat) Lookup(ctx context.Context, source netip.AddrPort, destination netip.AddrPort, handler Handler) (uint16, error) { n.addrAccess.RLock() port, loaded := n.addrMap[source] n.addrAccess.RUnlock() if loaded { return port, nil } - _, pErr := handler.PrepareConnection(N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination), nil, 0) + _, pErr := handler.PrepareConnection(ctx, N.NetworkTCP, M.SocksaddrFromNetIP(source), M.SocksaddrFromNetIP(destination), nil, 0) if pErr != nil { return 0, pErr } diff --git a/tun.go b/tun.go index 5f417bef..608e46f1 100644 --- a/tun.go +++ b/tun.go @@ -1,6 +1,7 @@ package tun import ( + "context" "io" "net" "net/netip" @@ -20,6 +21,7 @@ import ( type Handler interface { PrepareConnection( + ctx context.Context, network string, source M.Socksaddr, destination M.Socksaddr, From 80f9f8c1a6804d3275fa8ce1db928c51778668b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 18:37:54 +0800 Subject: [PATCH 23/38] windows: fix GetBestRoute2 NULL param, add fatal error context Fix STATUS_INVALID_PARAMETER from GetBestRoute2 by passing a valid BestSourceAddress output buffer instead of NULL. Demote route lookup errors from fatal to permit/bypass. Add string context to TriggerFatal so the Go side can report which driver operation failed (GET_FATAL_INFO IOCTL). --- internal/winredirect/amd64/winredirect.sys | Bin 20824 -> 21336 bytes internal/winredirect/arm64/winredirect.sys | Bin 29088 -> 29632 bytes internal/winredirect/driver/winredirect.c | 47 +++++++++++++-------- internal/winredirect/driver/winredirect.h | 10 ++++- internal/winredirect/ioctl_windows.go | 14 ++++++ internal/winredirect/types_windows.go | 10 ++++- redirect_windows.go | 16 ++++++- 7 files changed, 76 insertions(+), 21 deletions(-) diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys index ed42ab4d1d0cb1e9b34f5f69b60a31e486630b4f..3ec832a2b779104930c2b2da37127f652bf74ccd 100644 GIT binary patch delta 7924 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 E0{oW??f?J) delta 7294 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 diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 48d03aec..59cb9522 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -45,12 +45,13 @@ static NTSTATUS NormalizeFatalStatus(_In_ NTSTATUS Status) return Status; } -static NTSTATUS TriggerFatal(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status) +static NTSTATUS TriggerFatal(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status, _In_ const char* Message) { NTSTATUS normalized = NormalizeFatalStatus(Status); NTSTATUS previous = (NTSTATUS)InterlockedCompareExchange(&Ctx->FatalStatus, normalized, STATUS_SUCCESS); if (previous == STATUS_SUCCESS) { + RtlStringCbCopyA(Ctx->FatalMessage, sizeof(Ctx->FatalMessage), Message); WdfWorkItemEnqueue(Ctx->FatalWorkItem); return normalized; } @@ -61,10 +62,11 @@ static NTSTATUS TriggerFatal(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status) static void FailClosedClassify( _In_ PDRIVER_CONTEXT Ctx, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut, - _In_ NTSTATUS Status) + _In_ NTSTATUS Status, + _In_ const char* Message) { if (Ctx) { - TriggerFatal(Ctx, Status); + TriggerFatal(Ctx, Status, Message); } BlockClassify(classifyOut); } @@ -142,6 +144,7 @@ static NTSTATUS BestRouteForEntry( { SOCKADDR_INET sourceAddress; SOCKADDR_INET destinationAddress; + SOCKADDR_INET bestSourceAddress; SOCKADDR_INET* sourceAddressPtr = NULL; MIB_IPFORWARD_ROW2 bestRoute; NETIO_STATUS status; @@ -155,6 +158,7 @@ static NTSTATUS BestRouteForEntry( RtlZeroMemory(&sourceAddress, sizeof(sourceAddress)); RtlZeroMemory(&destinationAddress, sizeof(destinationAddress)); + RtlZeroMemory(&bestSourceAddress, sizeof(bestSourceAddress)); RtlZeroMemory(&bestRoute, sizeof(bestRoute)); if (Entry->AddressFamily == AF_INET) { @@ -177,7 +181,7 @@ static NTSTATUS BestRouteForEntry( return STATUS_INVALID_PARAMETER; } - status = GetBestRoute2(NULL, 0, sourceAddressPtr, &destinationAddress, 0, &bestRoute, NULL); + status = GetBestRoute2(NULL, 0, sourceAddressPtr, &destinationAddress, 0, &bestRoute, &bestSourceAddress); if (status != STATUS_SUCCESS) { return status; } @@ -533,6 +537,20 @@ void EvtIoDeviceControl( break; } + case IOCTL_WINREDIRECT_GET_FATAL_INFO: { + PVOID outBuf; + status = WdfRequestRetrieveOutputBuffer(Request, sizeof(WINREDIRECT_FATAL_INFO), &outBuf, NULL); + if (!NT_SUCCESS(status)) { + WdfRequestComplete(Request, status); + break; + } + WINREDIRECT_FATAL_INFO* info = (WINREDIRECT_FATAL_INFO*)outBuf; + info->Status = (UINT32)ReadFatalStatus(ctx); + RtlStringCbCopyA(info->Message, sizeof(info->Message), ctx->FatalMessage); + WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, sizeof(WINREDIRECT_FATAL_INFO)); + break; + } + default: WdfRequestComplete(Request, STATUS_INVALID_DEVICE_REQUEST); break; @@ -815,7 +833,7 @@ static void ClassifyFnCommon( // Allocate pending entry entry = (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { - FailClosedClassify(ctx, classifyOut, STATUS_INSUFFICIENT_RESOURCES); + FailClosedClassify(ctx, classifyOut, STATUS_INSUFFICIENT_RESOURCES, "allocate pending entry"); return; } @@ -842,7 +860,7 @@ static void ClassifyFnCommon( RtlCopyMemory(entry->DstAddr, dstArr->byteArray16, 16); } else { ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, STATUS_INVALID_ADDRESS_COMPONENT); + FailClosedClassify(ctx, classifyOut, STATUS_INVALID_ADDRESS_COMPONENT, "ipv6 null destination"); return; } } @@ -856,12 +874,7 @@ static void ClassifyFnCommon( } status = BestRouteForEntry(&snapshot, entry, &bestRoute); - if (!NT_SUCCESS(status)) { - ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, status); - return; - } - if (bestRoute == BestRouteOther) { + if (!NT_SUCCESS(status) || bestRoute == BestRouteOther) { ExFreePoolWithTag(entry, 'rniW'); PermitClassify(classifyOut); return; @@ -874,7 +887,7 @@ static void ClassifyFnCommon( if (!classifyContext) { ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, STATUS_INVALID_DEVICE_STATE); + FailClosedClassify(ctx, classifyOut, STATUS_INVALID_DEVICE_STATE, "no classify context"); return; } @@ -882,7 +895,7 @@ static void ClassifyFnCommon( status = FwpsAcquireClassifyHandle0((void*)classifyContext, 0, &classifyHandle); if (!NT_SUCCESS(status)) { ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, status); + FailClosedClassify(ctx, classifyOut, status, "acquire classify handle"); return; } @@ -895,7 +908,7 @@ static void ClassifyFnCommon( if (!NT_SUCCESS(status) || !entry->WritableLayerData) { FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, !NT_SUCCESS(status) ? status : STATUS_INVALID_DEVICE_STATE); + FailClosedClassify(ctx, classifyOut, !NT_SUCCESS(status) ? status : STATUS_INVALID_DEVICE_STATE, "acquire writable layer data"); return; } @@ -907,7 +920,7 @@ static void ClassifyFnCommon( FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS); FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, status); + FailClosedClassify(ctx, classifyOut, status, "pend classify"); return; } @@ -1083,7 +1096,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI } if (!NT_SUCCESS(redirectStatus)) { - TriggerFatal(Ctx, redirectStatus); + TriggerFatal(Ctx, redirectStatus, "execute redirect"); Verdict = VERDICT_DROP; } } diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 9273098c..6a10b670 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -8,6 +8,7 @@ #include #include #include +#include // Device names #define DEVICE_NAME L"\\Device\\WinRedirect" @@ -18,7 +19,8 @@ #define IOCTL_WINREDIRECT_START CTL_CODE(FILE_DEVICE_NETWORK, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WINREDIRECT_STOP CTL_CODE(FILE_DEVICE_NETWORK, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WINREDIRECT_GET_PENDING CTL_CODE(FILE_DEVICE_NETWORK, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS) -#define IOCTL_WINREDIRECT_SET_VERDICT CTL_CODE(FILE_DEVICE_NETWORK, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_WINREDIRECT_SET_VERDICT CTL_CODE(FILE_DEVICE_NETWORK, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_WINREDIRECT_GET_FATAL_INFO CTL_CODE(FILE_DEVICE_NETWORK, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS) // Verdict values #define VERDICT_REDIRECT 0 @@ -61,6 +63,11 @@ typedef struct _WINREDIRECT_VERDICT { UINT8 _pad0[4]; } WINREDIRECT_VERDICT; +typedef struct _WINREDIRECT_FATAL_INFO { + UINT32 Status; + CHAR Message[128]; +} WINREDIRECT_FATAL_INFO; + #pragma pack(pop) typedef enum _PENDING_DELIVERY_STATE { @@ -107,6 +114,7 @@ typedef struct _DRIVER_CONTEXT { BOOLEAN HasTunLuid; volatile LONG Running; volatile LONG FatalStatus; + CHAR FatalMessage[128]; // Pending connections (protected by PendingLock) LIST_ENTRY PendingList; diff --git a/internal/winredirect/ioctl_windows.go b/internal/winredirect/ioctl_windows.go index f3803050..cfe1ee36 100644 --- a/internal/winredirect/ioctl_windows.go +++ b/internal/winredirect/ioctl_windows.go @@ -49,3 +49,17 @@ func (m *Manager) SetVerdict(v *Verdict) error { ) return err } + +func (m *Manager) GetFatalInfo() (*FatalInfo, error) { + var info FatalInfo + _, err := m.ioctl( + ioctlGetFatalInfo, + nil, 0, + unsafe.Pointer(&info), + uint32(unsafe.Sizeof(info)), + ) + if err != nil { + return nil, err + } + return &info, nil +} diff --git a/internal/winredirect/types_windows.go b/internal/winredirect/types_windows.go index a591dc40..efa4f17b 100644 --- a/internal/winredirect/types_windows.go +++ b/internal/winredirect/types_windows.go @@ -7,7 +7,8 @@ const ( ioctlStart = (0x00120000 | (0x801 << 2)) // IOCTL_WINREDIRECT_START ioctlStop = (0x00120000 | (0x802 << 2)) // IOCTL_WINREDIRECT_STOP ioctlGetPending = (0x00120000 | (0x803 << 2)) // IOCTL_WINREDIRECT_GET_PENDING - ioctlSetVerdict = (0x00120000 | (0x804 << 2)) // IOCTL_WINREDIRECT_SET_VERDICT + ioctlSetVerdict = (0x00120000 | (0x804 << 2)) // IOCTL_WINREDIRECT_SET_VERDICT + ioctlGetFatalInfo = (0x00120000 | (0x805 << 2)) // IOCTL_WINREDIRECT_GET_FATAL_INFO ) const ( @@ -47,3 +48,10 @@ type Verdict struct { Verdict uint32 _ [4]byte // padding for alignment } + +// FatalInfo is received from the driver via IOCTL_GET_FATAL_INFO. +// Must match WINREDIRECT_FATAL_INFO in the driver. +type FatalInfo struct { + Status uint32 + Message [128]byte +} diff --git a/redirect_windows.go b/redirect_windows.go index a8e0d476..1672f2e6 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -193,7 +193,7 @@ func (r *autoRedirect) preMatchWorker() { conn, err := r.driverManager.GetPendingConn() if err != nil { if !r.closing.Load() { - r.handleFatalError(E.Cause(err, "get pending connection")) + r.handleFatalError(E.Cause(r.enrichDriverError(err), "get pending connection")) } return } @@ -204,13 +204,25 @@ func (r *autoRedirect) preMatchWorker() { }) if err != nil { if !r.closing.Load() { - r.handleFatalError(E.Cause(err, "set redirect verdict")) + r.handleFatalError(E.Cause(r.enrichDriverError(err), "set redirect verdict")) } return } } } +func (r *autoRedirect) enrichDriverError(err error) error { + fatalInfo, infoErr := r.driverManager.GetFatalInfo() + if infoErr != nil || fatalInfo.Status == 0 { + return err + } + message := strings.TrimRight(string(fatalInfo.Message[:]), "\x00") + if message == "" { + return err + } + return E.Cause(windows.Errno(fatalInfo.Status), message) +} + func (r *autoRedirect) handleFatalError(err error) { if err == nil || r.closing.Load() { return From 4aea1b697fcf3a0605258de5380b020525737db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 19:00:30 +0800 Subject: [PATCH 24/38] windows: simplify auto-redirect startup and TUN GUID resolution --- redirect_windows.go | 83 +++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 49 deletions(-) diff --git a/redirect_windows.go b/redirect_windows.go index 1672f2e6..b40d3463 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -77,7 +77,29 @@ func (r *autoRedirect) Start() error { if !r.enableIPv4 && !r.enableIPv6 { return E.New("no address configured") } + err := r.startRedirect() + if err != nil { + common.Close( + common.PtrOrNil(r.redirectServer), + common.PtrOrNil(r.driverManager), + ) + r.redirectServer = nil + r.driverManager = nil + return err + } + r.updateLocalAddresses() + if r.networkMonitor != nil { + r.networkListener = r.networkMonitor.RegisterCallback(func() { + r.updateLocalAddresses() + }) + } + for i := 0; i < r.workerCount; i++ { + go r.preMatchWorker() + } + return nil +} +func (r *autoRedirect) startRedirect() error { manager, err := winredirect.NewManager() if err != nil { return E.Cause(err, "create driver manager") @@ -86,22 +108,16 @@ func (r *autoRedirect) Start() error { err = manager.Install() if err != nil { - manager.Close() - r.driverManager = nil return E.Cause(err, "install driver") } err = manager.Start() if err != nil { - manager.Close() - r.driverManager = nil return E.Cause(err, "start driver") } err = manager.OpenDevice() if err != nil { - manager.Close() - r.driverManager = nil return E.Cause(err, "open driver device") } @@ -115,18 +131,11 @@ func (r *autoRedirect) Start() error { r.redirectServer = server err = server.Start() if err != nil { - r.redirectServer = nil - manager.Close() - r.driverManager = nil return E.Cause(err, "start redirect server") } tunGUID, err := r.resolveTunInterfaceGUID() if err != nil { - server.Close() - manager.Close() - r.redirectServer = nil - r.driverManager = nil return E.Cause(err, "resolve tun interface") } @@ -137,33 +146,14 @@ func (r *autoRedirect) Start() error { TunGUID: tunGUID, }) if err != nil { - server.Close() - manager.Close() - r.redirectServer = nil - r.driverManager = nil return E.Cause(err, "set driver config") } err = manager.StartRedirect() if err != nil { - server.Close() - manager.Close() - r.redirectServer = nil - r.driverManager = nil return E.Cause(err, "start redirect") } - r.updateLocalAddresses() - if r.networkMonitor != nil { - r.networkListener = r.networkMonitor.RegisterCallback(func() { - r.updateLocalAddresses() - }) - } - - for i := 0; i < r.workerCount; i++ { - go r.preMatchWorker() - } - return nil } @@ -377,27 +367,22 @@ func (r *autoRedirect) dnsServerForFamily(addr netip.Addr) netip.Addr { } func (r *autoRedirect) resolveTunInterfaceGUID() ([16]byte, error) { + var index int if r.interfaceFinder != nil { - if err := r.interfaceFinder.Update(); err == nil { - iface, err := r.interfaceFinder.ByName(r.tunOptions.Name) - if err == nil { - luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) - if err != nil { - return [16]byte{}, err - } - guid, err := luid.GUID() - if err != nil { - return [16]byte{}, err - } - return guidBytes(guid), nil - } + _ = r.interfaceFinder.Update() + iface, err := r.interfaceFinder.ByName(r.tunOptions.Name) + if err == nil { + index = iface.Index } } - iface, err := net.InterfaceByName(r.tunOptions.Name) - if err != nil { - return [16]byte{}, err + if index == 0 { + iface, err := net.InterfaceByName(r.tunOptions.Name) + if err != nil { + return [16]byte{}, err + } + index = iface.Index } - luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) + luid, err := winipcfg.LUIDFromIndex(uint32(index)) if err != nil { return [16]byte{}, err } From b211d06501d328e5e3723bad82409dfffd15c710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 19:18:35 +0800 Subject: [PATCH 25/38] windows: clean up dead code, comments, and fix goroutine leak - Remove unused routeAddressSet/routeExcludeAddressSet fields and netipx import - Remove dead Metadata field from connEntry (already in Context) - Remove WHAT comments per project code-comment rules - Fix goroutine leak in connMetadataTable.cleanupLoop (add done channel) - Cache os.Getpid() as selfPID field to avoid repeated syscalls --- internal/winipcfg/winipcfg.go | 13 ----- internal/winipcfg/zwinipcfg_windows.go | 9 ---- internal/winredirect/types_windows.go | 12 ++--- internal/winsys/constants.go | 2 +- redirect_nftables_rules.go | 3 +- redirect_route_linux.go | 2 +- redirect_server_windows.go | 42 ++++++++++------ redirect_windows.go | 66 +++++++++++--------------- 8 files changed, 63 insertions(+), 86 deletions(-) diff --git a/internal/winipcfg/winipcfg.go b/internal/winipcfg/winipcfg.go index 8ae0fe1a..e24157b9 100644 --- a/internal/winipcfg/winipcfg.go +++ b/internal/winipcfg/winipcfg.go @@ -134,7 +134,6 @@ func GetAnycastIPAddressTable(family AddressFamily) ([]MibAnycastIPAddressRow, e // //sys getIPForwardTable2(family AddressFamily, table **mibIPforwardTable2) (ret error) = iphlpapi.GetIpForwardTable2 -//sys getBestRoute2(interfaceLUID *LUID, interfaceIndex uint32, sourceAddress *RawSockaddrInet, destinationAddress *RawSockaddrInet, sortOptions uint32, bestRoute *MibIPforwardRow2, bestSourceAddress *RawSockaddrInet) (ret error) = iphlpapi.GetBestRoute2 //sys initializeIPForwardEntry(route *MibIPforwardRow2) = iphlpapi.InitializeIpForwardEntry //sys getIPForwardEntry2(route *MibIPforwardRow2) (ret error) = iphlpapi.GetIpForwardEntry2 //sys setIPForwardEntry2(route *MibIPforwardRow2) (ret error) = iphlpapi.SetIpForwardEntry2 @@ -154,18 +153,6 @@ func GetIPForwardTable2(family AddressFamily) ([]MibIPforwardRow2, error) { return t, nil } -// GetBestRoute2 function retrieves the best route for the specified destination IP address on the local computer. -// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getbestroute2 -func GetBestRoute2(interfaceLUID *LUID, interfaceIndex uint32, sourceAddress *RawSockaddrInet, destinationAddress *RawSockaddrInet, sortOptions uint32) (*MibIPforwardRow2, *RawSockaddrInet, error) { - bestRoute := &MibIPforwardRow2{} - bestSourceAddress := &RawSockaddrInet{} - err := getBestRoute2(interfaceLUID, interfaceIndex, sourceAddress, destinationAddress, sortOptions, bestRoute, bestSourceAddress) - if err != nil { - return nil, nil, err - } - return bestRoute, bestSourceAddress, nil -} - // // Notifications-related functions // diff --git a/internal/winipcfg/zwinipcfg_windows.go b/internal/winipcfg/zwinipcfg_windows.go index 691afed8..052134c5 100644 --- a/internal/winipcfg/zwinipcfg_windows.go +++ b/internal/winipcfg/zwinipcfg_windows.go @@ -53,7 +53,6 @@ var ( procFreeMibTable = modiphlpapi.NewProc("FreeMibTable") procGetAnycastIpAddressEntry = modiphlpapi.NewProc("GetAnycastIpAddressEntry") procGetAnycastIpAddressTable = modiphlpapi.NewProc("GetAnycastIpAddressTable") - procGetBestRoute2 = modiphlpapi.NewProc("GetBestRoute2") procGetIfEntry2 = modiphlpapi.NewProc("GetIfEntry2") procGetIfTable2Ex = modiphlpapi.NewProc("GetIfTable2Ex") procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2") @@ -175,14 +174,6 @@ func getAnycastIPAddressTable(family AddressFamily, table **mibAnycastIPAddressT return } -func getBestRoute2(interfaceLUID *LUID, interfaceIndex uint32, sourceAddress *RawSockaddrInet, destinationAddress *RawSockaddrInet, sortOptions uint32, bestRoute *MibIPforwardRow2, bestSourceAddress *RawSockaddrInet) (ret error) { - r0, _, _ := syscall.SyscallN(procGetBestRoute2.Addr(), uintptr(unsafe.Pointer(interfaceLUID)), uintptr(interfaceIndex), uintptr(unsafe.Pointer(sourceAddress)), uintptr(unsafe.Pointer(destinationAddress)), uintptr(sortOptions), uintptr(unsafe.Pointer(bestRoute)), uintptr(unsafe.Pointer(bestSourceAddress))) - if r0 != 0 { - ret = syscall.Errno(r0) - } - return -} - func getIfEntry2(row *MibIfRow2) (ret error) { r0, _, _ := syscall.SyscallN(procGetIfEntry2.Addr(), uintptr(unsafe.Pointer(row))) if r0 != 0 { diff --git a/internal/winredirect/types_windows.go b/internal/winredirect/types_windows.go index efa4f17b..832ff5b6 100644 --- a/internal/winredirect/types_windows.go +++ b/internal/winredirect/types_windows.go @@ -3,12 +3,12 @@ package winredirect // IOCTL codes matching the kernel driver definitions. // CTL_CODE(FILE_DEVICE_NETWORK=0x12, function, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0) const ( - ioctlSetConfig = (0x00120000 | (0x800 << 2)) // IOCTL_WINREDIRECT_SET_CONFIG - ioctlStart = (0x00120000 | (0x801 << 2)) // IOCTL_WINREDIRECT_START - ioctlStop = (0x00120000 | (0x802 << 2)) // IOCTL_WINREDIRECT_STOP - ioctlGetPending = (0x00120000 | (0x803 << 2)) // IOCTL_WINREDIRECT_GET_PENDING - ioctlSetVerdict = (0x00120000 | (0x804 << 2)) // IOCTL_WINREDIRECT_SET_VERDICT - ioctlGetFatalInfo = (0x00120000 | (0x805 << 2)) // IOCTL_WINREDIRECT_GET_FATAL_INFO + ioctlSetConfig = (0x00120000 | (0x800 << 2)) // IOCTL_WINREDIRECT_SET_CONFIG + ioctlStart = (0x00120000 | (0x801 << 2)) // IOCTL_WINREDIRECT_START + ioctlStop = (0x00120000 | (0x802 << 2)) // IOCTL_WINREDIRECT_STOP + ioctlGetPending = (0x00120000 | (0x803 << 2)) // IOCTL_WINREDIRECT_GET_PENDING + ioctlSetVerdict = (0x00120000 | (0x804 << 2)) // IOCTL_WINREDIRECT_SET_VERDICT + ioctlGetFatalInfo = (0x00120000 | (0x805 << 2)) // IOCTL_WINREDIRECT_GET_FATAL_INFO ) const ( diff --git a/internal/winsys/constants.go b/internal/winsys/constants.go index 1bdc4c00..e71b4f74 100644 --- a/internal/winsys/constants.go +++ b/internal/winsys/constants.go @@ -152,7 +152,7 @@ var FWPM_LAYER_ALE_CONNECT_REDIRECT_V6 = windows.GUID{ } const ( - FWP_ACTION_FLAG_CALLOUT uint32 = 0x00004000 + FWP_ACTION_FLAG_CALLOUT uint32 = 0x00004000 FWP_ACTION_CALLOUT_TERMINATING uint32 = (0x00000003 | FWP_ACTION_FLAG_CALLOUT | FWP_ACTION_FLAG_TERMINATING) ) diff --git a/redirect_nftables_rules.go b/redirect_nftables_rules.go index 27765f5b..7e47be08 100644 --- a/redirect_nftables_rules.go +++ b/redirect_nftables_rules.go @@ -12,9 +12,8 @@ import ( "github.com/sagernet/nftables/expr" "github.com/sagernet/nftables/userdata" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/ranges" - E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/ranges" "golang.org/x/exp/slices" "golang.org/x/sys/unix" diff --git a/redirect_route_linux.go b/redirect_route_linux.go index db79cac6..7e0868c6 100644 --- a/redirect_route_linux.go +++ b/redirect_route_linux.go @@ -8,9 +8,9 @@ import ( "net/netip" "github.com/sagernet/netlink" - E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" "golang.org/x/sys/unix" ) diff --git a/redirect_server_windows.go b/redirect_server_windows.go index 8f7063c9..22cd6fba 100644 --- a/redirect_server_windows.go +++ b/redirect_server_windows.go @@ -57,6 +57,9 @@ func (s *redirectServer) Start() error { func (s *redirectServer) Close() error { s.inShutdown.Store(true) + if s.connTable != nil { + s.connTable.Close() + } if s.listener != nil { return s.listener.Close() } @@ -104,12 +107,10 @@ func (s *redirectServer) loopIn() { } } -// connMetadataTable maps source address → connection metadata. -// Entries are populated by pre-match workers before sending redirect verdicts, -// and consumed by the redirect server upon accepting connections. type connMetadataTable struct { mu sync.Mutex entries map[connKey]*connEntry + done chan struct{} } type connKey struct { @@ -120,7 +121,6 @@ type connKey struct { type connEntry struct { Destination M.Socksaddr Context context.Context - Metadata *AutoRedirectMetadata IsDNS bool DNSServer M.Socksaddr CreatedAt time.Time @@ -129,31 +129,38 @@ type connEntry struct { func newConnMetadataTable() *connMetadataTable { t := &connMetadataTable{ entries: make(map[connKey]*connEntry), + done: make(chan struct{}), } go t.cleanupLoop() return t } -func (t *connMetadataTable) Store(src M.Socksaddr, dst M.Socksaddr, ctx context.Context, metadata *AutoRedirectMetadata) { +func (t *connMetadataTable) Close() { + select { + case <-t.done: + default: + close(t.done) + } +} + +func (t *connMetadataTable) Store(src M.Socksaddr, dst M.Socksaddr, ctx context.Context) { key := connKey{Addr: src.Addr, Port: src.Port} t.mu.Lock() defer t.mu.Unlock() t.entries[key] = &connEntry{ Destination: dst, Context: ctx, - Metadata: metadata, CreatedAt: time.Now(), } } -func (t *connMetadataTable) StoreDNS(src M.Socksaddr, originalDst M.Socksaddr, dnsServer M.Socksaddr, ctx context.Context, metadata *AutoRedirectMetadata) { +func (t *connMetadataTable) StoreDNS(src M.Socksaddr, originalDst M.Socksaddr, dnsServer M.Socksaddr, ctx context.Context) { key := connKey{Addr: src.Addr, Port: src.Port} t.mu.Lock() defer t.mu.Unlock() t.entries[key] = &connEntry{ Destination: originalDst, Context: ctx, - Metadata: metadata, IsDNS: true, DNSServer: dnsServer, CreatedAt: time.Now(), @@ -192,14 +199,19 @@ func (t *connMetadataTable) Lookup(src M.Socksaddr) (*connEntry, bool) { func (t *connMetadataTable) cleanupLoop() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() - for range ticker.C { - t.mu.Lock() - now := time.Now() - for key, entry := range t.entries { - if now.Sub(entry.CreatedAt) > 30*time.Second { - delete(t.entries, key) + for { + select { + case <-t.done: + return + case <-ticker.C: + t.mu.Lock() + now := time.Now() + for key, entry := range t.entries { + if now.Sub(entry.CreatedAt) > 30*time.Second { + delete(t.entries, key) + } } + t.mu.Unlock() } - t.mu.Unlock() } } diff --git a/redirect_windows.go b/redirect_windows.go index b40d3463..d88fee0f 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -21,25 +21,23 @@ import ( M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/x/list" - "go4.org/netipx" "golang.org/x/sys/windows" ) type autoRedirect struct { - tunOptions *Options - ctx context.Context - connContext func(context.Context) context.Context - handler Handler - logger logger.Logger - errorHandler func(error) - networkMonitor NetworkUpdateMonitor - networkListener *list.Element[NetworkUpdateCallback] - interfaceFinder control.InterfaceFinder - driverManager *winredirect.Manager - redirectServer *redirectServer - routeAddressSet *[]*netipx.IPSet - routeExcludeAddressSet *[]*netipx.IPSet - + tunOptions *Options + ctx context.Context + connContext func(context.Context) context.Context + handler Handler + logger logger.Logger + errorHandler func(error) + networkMonitor NetworkUpdateMonitor + networkListener *list.Element[NetworkUpdateCallback] + interfaceFinder control.InterfaceFinder + driverManager *winredirect.Manager + redirectServer *redirectServer + + selfPID uint32 enableIPv4 bool enableIPv6 bool @@ -56,22 +54,21 @@ type autoRedirect struct { func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { r := &autoRedirect{ - tunOptions: options.TunOptions, - ctx: options.Context, - connContext: options.ConnContext, - handler: options.Handler, - logger: options.Logger, - errorHandler: options.ErrorHandler, - networkMonitor: options.NetworkMonitor, - interfaceFinder: options.InterfaceFinder, - routeAddressSet: options.RouteAddressSet, - routeExcludeAddressSet: options.RouteExcludeAddressSet, - workerCount: 1, + tunOptions: options.TunOptions, + ctx: options.Context, + connContext: options.ConnContext, + handler: options.Handler, + logger: options.Logger, + errorHandler: options.ErrorHandler, + networkMonitor: options.NetworkMonitor, + interfaceFinder: options.InterfaceFinder, + workerCount: 1, } return r, nil } func (r *autoRedirect) Start() error { + r.selfPID = uint32(os.Getpid()) r.enableIPv4 = len(r.tunOptions.Inet4Address) > 0 r.enableIPv6 = len(r.tunOptions.Inet6Address) > 0 if !r.enableIPv4 && !r.enableIPv6 { @@ -142,7 +139,7 @@ func (r *autoRedirect) startRedirect() error { redirectPort := M.AddrPortFromNet(server.listener.Addr()).Port() err = manager.SetConfig(&winredirect.Config{ RedirectPort: redirectPort, - ProxyPID: uint32(os.Getpid()), + ProxyPID: r.selfPID, TunGUID: tunGUID, }) if err != nil { @@ -173,9 +170,6 @@ func (r *autoRedirect) Close() error { } func (r *autoRedirect) UpdateRouteAddressSet() { - // Dynamic route address sets are updated via pointer indirection. - // The IPSet pointers are swapped atomically by the caller. - // No driver communication needed — all filtering is in Go. } func (r *autoRedirect) preMatchWorker() { @@ -233,38 +227,33 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 src := pendingConnSrc(conn) // Proxy process outbound connections must never be redirected back into itself. - if conn.ProcessID == uint32(os.Getpid()) { + if conn.ProcessID == r.selfPID { return winredirect.VerdictBypass } - // 1. Loopback destinations if dst.Addr.IsLoopback() { return winredirect.VerdictBypass } - // DNS hijack: port 53 from local network → redirect to DNS server if !r.tunOptions.EXP_DisableDNSHijack && dst.Port == 53 { if r.isLocalAddress(src.Addr) { dnsServer := r.dnsServerForFamily(dst.Addr) if dnsServer.IsValid() { metadata := r.resolveMetadata(conn) ctx := r.newConnContext(metadata) - r.redirectServer.connTable.StoreDNS(src, dst, M.SocksaddrFrom(dnsServer, 53), ctx, metadata) + r.redirectServer.connTable.StoreDNS(src, dst, M.SocksaddrFrom(dnsServer, 53), ctx) return winredirect.VerdictRedirect } } } - // Strict route: reject disabled address family if r.tunOptions.StrictRoute && r.isDisabledFamily(dst.Addr) { return winredirect.VerdictDrop } - // Resolve PID → process path metadata := r.resolveMetadata(conn) ctx := r.newConnContext(metadata) - // PrepareConnection (NFQUEUE equivalent) _, err := r.handler.PrepareConnection(ctx, "tcp", src, dst, nil, 0) if errors.Is(err, ErrDrop) { return winredirect.VerdictDrop @@ -276,8 +265,7 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 r.logger.Warn("prepare connection fallback to redirect: ", err) } - // Store metadata for redirect server - r.redirectServer.connTable.Store(src, dst, ctx, metadata) + r.redirectServer.connTable.Store(src, dst, ctx) return winredirect.VerdictRedirect } From 8a062adb78dbd36be4249580c401ec7286750e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 19:40:53 +0800 Subject: [PATCH 26/38] Clarify Windows route lookup fallback --- internal/winredirect/driver/winredirect.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 59cb9522..f93d4143 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -152,6 +152,9 @@ static NTSTATUS BestRouteForEntry( if (!Snapshot->HasTunLuid) { return STATUS_INVALID_DEVICE_STATE; } + // GetBestRoute2 requires IRQL < DISPATCH_LEVEL. We do not currently + // characterize every runtime context where route lookup can be unavailable, + // so report a normal lookup failure and let the caller decide the fallback. if (KeGetCurrentIrql() >= DISPATCH_LEVEL) { return STATUS_NOT_SUPPORTED; } @@ -874,6 +877,10 @@ static void ClassifyFnCommon( } status = BestRouteForEntry(&snapshot, entry, &bestRoute); + // Windows auto-redirect is best-effort: only redirect connections that are + // positively identified as already routed to the TUN. If route lookup says + // "not TUN" or fails for a context we do not currently characterize, leave + // the original connect alone instead of redirecting or blocking unknown traffic. if (!NT_SUCCESS(status) || bestRoute == BestRouteOther) { ExFreePoolWithTag(entry, 'rniW'); PermitClassify(classifyOut); From 7bcf9d193d228afa7e5e705b165df7706ed920eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 19:45:23 +0800 Subject: [PATCH 27/38] Clarify Windows ErrReset bypass semantics --- redirect_windows.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/redirect_windows.go b/redirect_windows.go index d88fee0f..a84148fe 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -259,6 +259,10 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 return winredirect.VerdictDrop } if errors.Is(err, ErrReset) { + // Pending entries reaching userspace here have already been identified as + // TUN-bound by the driver. Bypass means "do not locally redirect to the + // Windows redirect listener"; the original connect continues into the TUN, + // where reset semantics are enforced by the TUN stack. return winredirect.VerdictBypass } if err != nil && !errors.Is(err, ErrBypass) && r.logger != nil { From 1b2f80f2735e438be55d21475d08f84bba258f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 20:24:40 +0800 Subject: [PATCH 28/38] windows: fail-fast on stuck pending entries instead of auto-bypass Replace the per-entry auto-bypass timeout with a fatal error that stops the entire redirect system. Any pending entry (Queued or Delivered) alive for 15 seconds without a verdict now triggers TriggerFatal, surfacing the bug to userspace immediately. --- internal/winredirect/driver/winredirect.c | 41 ++++++++++------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index f93d4143..32a4c86c 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -581,38 +581,31 @@ void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) LARGE_INTEGER now; KeQuerySystemTime(&now); - for (;;) { - KIRQL oldIrql; - PPENDING_ENTRY expired = NULL; - - KeAcquireSpinLock(&g_Ctx->PendingLock, &oldIrql); + BOOLEAN stuck = FALSE; + KIRQL oldIrql; - PLIST_ENTRY entry = g_Ctx->PendingList.Flink; - while (entry != &g_Ctx->PendingList) { - PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); - entry = entry->Flink; + KeAcquireSpinLock(&g_Ctx->PendingLock, &oldIrql); - if (pending->DeliveryState != PendingDeliveryQueued) { - continue; - } + PLIST_ENTRY entry = g_Ctx->PendingList.Flink; + while (entry != &g_Ctx->PendingList) { + PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + entry = entry->Flink; - // Auto-bypass queued entries older than 5 seconds. - LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; // to seconds - if (elapsed >= 5) { - RemoveEntryList(&pending->ListEntry); - expired = pending; - break; - } + if (pending->DeliveryState == PendingDeliveryCopying) { + continue; } - KeReleaseSpinLock(&g_Ctx->PendingLock, oldIrql); - - if (!expired) { + LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; + if (elapsed >= 15) { + stuck = TRUE; break; } + } + + KeReleaseSpinLock(&g_Ctx->PendingLock, oldIrql); - ExecuteVerdict(g_Ctx, expired, VERDICT_BYPASS); - ExFreePoolWithTag(expired, 'rniW'); + if (stuck) { + TriggerFatal(g_Ctx, STATUS_DRIVER_INTERNAL_ERROR, "pending entry timed out without verdict"); } } From aff1ecf4b20be5410289abd547a2c4809a3b3fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 20:48:42 +0800 Subject: [PATCH 29/38] windows: remove dead code and fix struct packing - Remove unused PendingAllocate, PendingRemove, LOCAL_REDIRECT_CONTEXT - Remove redundant ClassifyOut copy before FwpsAcquireClassifyHandle0 - Move CONFIG_SNAPSHOT out of pragma pack(1) (kernel-internal, not IPC) - Use winsys.AF_INET constant instead of magic number --- internal/winredirect/driver/winredirect.c | 20 -------------------- internal/winredirect/driver/winredirect.h | 14 ++++++-------- redirect_windows.go | 3 ++- 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 32a4c86c..51d50375 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -196,11 +196,6 @@ static NTSTATUS BestRouteForEntry( return STATUS_SUCCESS; } -typedef struct _LOCAL_REDIRECT_CONTEXT { - SOCKADDR_STORAGE OriginalRemoteAddressAndPort; - UINT32 ProcessId; -} LOCAL_REDIRECT_CONTEXT, *PLOCAL_REDIRECT_CONTEXT; - static void CancelPendingIoctlRequests(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status) { WDFREQUEST request; @@ -837,7 +832,6 @@ static void ClassifyFnCommon( entry->ConnID = InterlockedIncrement64(&ctx->NextConnID); entry->AddressFamily = addressFamily; entry->FilterId = filter->filterId; - entry->ClassifyOut = *classifyOut; // Extract addresses with NULL checks for IPv6 pointers if (addressFamily == AF_INET) { @@ -968,12 +962,6 @@ void NTAPI ClassifyFnV6( // --- Pending connection management --- -PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx) -{ - UNREFERENCED_PARAMETER(Ctx); - return (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); -} - void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) { KIRQL oldIrql; @@ -1001,14 +989,6 @@ PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID) return found; } -void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry) -{ - KIRQL oldIrql; - KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); - RemoveEntryList(&Entry->ListEntry); - KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); -} - void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT32 Verdict) { for (;;) { diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 6a10b670..d0c1f11f 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -38,12 +38,6 @@ typedef struct _WINREDIRECT_CONFIG { UINT8 TunGuid[16]; } WINREDIRECT_CONFIG; -typedef struct _CONFIG_SNAPSHOT { - WINREDIRECT_CONFIG Config; - NET_LUID TunLuid; - BOOLEAN HasTunLuid; -} CONFIG_SNAPSHOT, *PCONFIG_SNAPSHOT; - typedef struct _WINREDIRECT_PENDING_CONN { UINT64 ConnID; UINT8 AddressFamily; @@ -70,6 +64,12 @@ typedef struct _WINREDIRECT_FATAL_INFO { #pragma pack(pop) +typedef struct _CONFIG_SNAPSHOT { + WINREDIRECT_CONFIG Config; + NET_LUID TunLuid; + BOOLEAN HasTunLuid; +} CONFIG_SNAPSHOT, *PCONFIG_SNAPSHOT; + typedef enum _PENDING_DELIVERY_STATE { PendingDeliveryQueued = 0, PendingDeliveryCopying = 1, @@ -172,10 +172,8 @@ NTSTATUS NTAPI NotifyFn( ); // Pending management -PPENDING_ENTRY PendingAllocate(_In_ PDRIVER_CONTEXT Ctx); void PendingInsert(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry); PPENDING_ENTRY PendingFindByID(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT64 ConnID); -void PendingRemove(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry); void PendingFlushAll(_In_ PDRIVER_CONTEXT Ctx, _In_ UINT32 Verdict); // Verdict execution diff --git a/redirect_windows.go b/redirect_windows.go index a84148fe..ce6a66a6 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-tun/internal/winipcfg" "github.com/sagernet/sing-tun/internal/winredirect" + "github.com/sagernet/sing-tun/internal/winsys" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -398,7 +399,7 @@ func pendingConnDst(conn *winredirect.PendingConn) M.Socksaddr { } func pendingAddr(af uint8, raw [16]byte) netip.Addr { - if af == 2 { // AF_INET + if af == winsys.AF_INET { return netip.AddrFrom4([4]byte(raw[:4])) } return netip.AddrFrom16(raw) From e432565a974b8fe330b3be2685837a5e0853d69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 20:55:25 +0800 Subject: [PATCH 30/38] windows: fix review issues in auto-redirect - Preserve original IOCTL error when enriching with driver fatal info - Add PrepareConnection check to DNS redirect path for reject(drop) - Replace single worker loop with goroutine-per-request dispatch - Log ErrBypass at debug instead of warn for unknown errors - Add debug logging for interfaceFinder errors in GUID resolution --- redirect_windows.go | 53 ++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/redirect_windows.go b/redirect_windows.go index ce6a66a6..17e8e661 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -20,6 +20,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" "golang.org/x/sys/windows" @@ -45,8 +46,6 @@ type autoRedirect struct { localAddressMu sync.RWMutex localAddresses []netip.Prefix - workerCount int - closing atomic.Bool closeOnce sync.Once closeErr error @@ -63,7 +62,6 @@ func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { errorHandler: options.ErrorHandler, networkMonitor: options.NetworkMonitor, interfaceFinder: options.InterfaceFinder, - workerCount: 1, } return r, nil } @@ -91,9 +89,7 @@ func (r *autoRedirect) Start() error { r.updateLocalAddresses() }) } - for i := 0; i < r.workerCount; i++ { - go r.preMatchWorker() - } + go r.dispatchLoop() return nil } @@ -173,7 +169,7 @@ func (r *autoRedirect) Close() error { func (r *autoRedirect) UpdateRouteAddressSet() { } -func (r *autoRedirect) preMatchWorker() { +func (r *autoRedirect) dispatchLoop() { for { conn, err := r.driverManager.GetPendingConn() if err != nil { @@ -182,16 +178,19 @@ func (r *autoRedirect) preMatchWorker() { } return } - verdict := r.evaluateConnection(conn) - err = r.driverManager.SetVerdict(&winredirect.Verdict{ - ConnID: conn.ConnID, - Verdict: verdict, - }) - if err != nil { - if !r.closing.Load() { - r.handleFatalError(E.Cause(r.enrichDriverError(err), "set redirect verdict")) - } - return + go r.handlePendingConn(conn) + } +} + +func (r *autoRedirect) handlePendingConn(conn *winredirect.PendingConn) { + verdict := r.evaluateConnection(conn) + err := r.driverManager.SetVerdict(&winredirect.Verdict{ + ConnID: conn.ConnID, + Verdict: verdict, + }) + if err != nil { + if !r.closing.Load() { + r.handleFatalError(E.Cause(r.enrichDriverError(err), "set redirect verdict")) } } } @@ -205,7 +204,7 @@ func (r *autoRedirect) enrichDriverError(err error) error { if message == "" { return err } - return E.Cause(windows.Errno(fatalInfo.Status), message) + return E.Cause(err, E.Cause(windows.Errno(fatalInfo.Status), message)) } func (r *autoRedirect) handleFatalError(err error) { @@ -242,6 +241,10 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 if dnsServer.IsValid() { metadata := r.resolveMetadata(conn) ctx := r.newConnContext(metadata) + _, err := r.handler.PrepareConnection(ctx, "tcp", src, dst, nil, 0) + if errors.Is(err, ErrDrop) { + return winredirect.VerdictDrop + } r.redirectServer.connTable.StoreDNS(src, dst, M.SocksaddrFrom(dnsServer, 53), ctx) return winredirect.VerdictRedirect } @@ -255,7 +258,7 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 metadata := r.resolveMetadata(conn) ctx := r.newConnContext(metadata) - _, err := r.handler.PrepareConnection(ctx, "tcp", src, dst, nil, 0) + _, err := r.handler.PrepareConnection(ctx, N.NetworkTCP, src, dst, nil, 0) if errors.Is(err, ErrDrop) { return winredirect.VerdictDrop } @@ -266,8 +269,8 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 // where reset semantics are enforced by the TUN stack. return winredirect.VerdictBypass } - if err != nil && !errors.Is(err, ErrBypass) && r.logger != nil { - r.logger.Warn("prepare connection fallback to redirect: ", err) + if errors.Is(err, ErrBypass) && r.logger != nil { + r.logger.Debug("bypass not supported on Windows, redirecting: ", src, " -> ", dst) } r.redirectServer.connTable.Store(src, dst, ctx) @@ -362,8 +365,14 @@ func (r *autoRedirect) dnsServerForFamily(addr netip.Addr) netip.Addr { func (r *autoRedirect) resolveTunInterfaceGUID() ([16]byte, error) { var index int if r.interfaceFinder != nil { - _ = r.interfaceFinder.Update() + err := r.interfaceFinder.Update() + if err != nil && r.logger != nil { + r.logger.Debug("update interface finder: ", err) + } iface, err := r.interfaceFinder.ByName(r.tunOptions.Name) + if err != nil && r.logger != nil { + r.logger.Debug("interface finder lookup: ", err) + } if err == nil { index = iface.Index } From 85dc2d52a0b870b464e5f5040d514410b0099357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Mar 2026 21:07:47 +0800 Subject: [PATCH 31/38] windows: fail open timed-out redirect verdicts --- internal/winredirect/amd64/winredirect.sys | Bin 21336 -> 21848 bytes internal/winredirect/arm64/winredirect.sys | Bin 29632 -> 29632 bytes internal/winredirect/driver/winredirect.c | 69 ++++++++++++++------- redirect_windows.go | 7 +++ 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys index 3ec832a2b779104930c2b2da37127f652bf74ccd..0aed40dbd73a23254a0dae271e0143a7f748848f 100644 GIT binary patch delta 5932 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 X%6r?E9ywL_?$9**!HXiQjpP0Y-M1yD diff --git a/internal/winredirect/arm64/winredirect.sys b/internal/winredirect/arm64/winredirect.sys index d34a6f0d0da8f1a1851bed28eaff306672b2921f..221dc14ae3406feb136297fb7b588e42518915cc 100644 GIT binary patch delta 2691 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 PO+{3a%4gQ?JCEQ$u?DM> delta 2568 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 diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index 51d50375..c82e6162 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -20,6 +20,9 @@ DEFINE_GUID(WINREDIRECT_CALLOUT_V6_KEY, static PDRIVER_CONTEXT g_Ctx = NULL; +#define PENDING_QUEUED_TIMEOUT_SECONDS 5 +#define PENDING_DELIVERED_TIMEOUT_SECONDS 15 + static void PermitClassify(_Inout_ FWPS_CLASSIFY_OUT0* classifyOut) { classifyOut->actionType = FWP_ACTION_PERMIT; @@ -239,12 +242,19 @@ static PPENDING_ENTRY PendingReserveNextQueued(_In_ PDRIVER_CONTEXT Ctx) return found; } -static void PendingSetDeliveryState(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ LONG State) +static void PendingSetDeliveryState( + _In_ PDRIVER_CONTEXT Ctx, + _In_ PPENDING_ENTRY Entry, + _In_ LONG State, + _In_opt_ const LARGE_INTEGER* Timestamp) { KIRQL oldIrql; KeAcquireSpinLock(&Ctx->PendingLock, &oldIrql); Entry->DeliveryState = State; + if (Timestamp) { + Entry->Timestamp = *Timestamp; + } KeReleaseSpinLock(&Ctx->PendingLock, oldIrql); } @@ -263,14 +273,14 @@ static void TryCompletePendingRequests(_In_ PDRIVER_CONTEXT Ctx) WDFREQUEST request; NTSTATUS status = WdfIoQueueRetrieveNextRequest(Ctx->PendingIoctlQueue, &request); if (!NT_SUCCESS(status)) { - PendingSetDeliveryState(Ctx, pending, PendingDeliveryQueued); + PendingSetDeliveryState(Ctx, pending, PendingDeliveryQueued, NULL); break; } PVOID outBuf; status = WdfRequestRetrieveOutputBuffer(request, sizeof(WINREDIRECT_PENDING_CONN), &outBuf, NULL); if (!NT_SUCCESS(status)) { - PendingSetDeliveryState(Ctx, pending, PendingDeliveryQueued); + PendingSetDeliveryState(Ctx, pending, PendingDeliveryQueued, NULL); WdfRequestComplete(request, status); continue; } @@ -284,7 +294,9 @@ static void TryCompletePendingRequests(_In_ PDRIVER_CONTEXT Ctx) RtlCopyMemory(out->DstAddr, pending->DstAddr, 16); out->DstPort = pending->DstPort; out->ProcessID = pending->ProcessID; - PendingSetDeliveryState(Ctx, pending, PendingDeliveryDelivered); + LARGE_INTEGER now; + KeQuerySystemTime(&now); + PendingSetDeliveryState(Ctx, pending, PendingDeliveryDelivered, &now); WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, sizeof(WINREDIRECT_PENDING_CONN)); } } @@ -574,33 +586,44 @@ void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) if (!g_Ctx) return; if (ReadFatalStatus(g_Ctx) != STATUS_SUCCESS) return; - LARGE_INTEGER now; - KeQuerySystemTime(&now); - BOOLEAN stuck = FALSE; - KIRQL oldIrql; + for (;;) { + LARGE_INTEGER now; + PPENDING_ENTRY expired = NULL; + KIRQL oldIrql; - KeAcquireSpinLock(&g_Ctx->PendingLock, &oldIrql); + KeQuerySystemTime(&now); + KeAcquireSpinLock(&g_Ctx->PendingLock, &oldIrql); - PLIST_ENTRY entry = g_Ctx->PendingList.Flink; - while (entry != &g_Ctx->PendingList) { - PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); - entry = entry->Flink; + PLIST_ENTRY entry = g_Ctx->PendingList.Flink; + while (entry != &g_Ctx->PendingList) { + PPENDING_ENTRY pending = CONTAINING_RECORD(entry, PENDING_ENTRY, ListEntry); + LONGLONG timeoutSeconds = 0; + entry = entry->Flink; - if (pending->DeliveryState == PendingDeliveryCopying) { - continue; + if (pending->DeliveryState == PendingDeliveryQueued) { + timeoutSeconds = PENDING_QUEUED_TIMEOUT_SECONDS; + } else if (pending->DeliveryState == PendingDeliveryDelivered) { + timeoutSeconds = PENDING_DELIVERED_TIMEOUT_SECONDS; + } else { + continue; + } + + LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; + if (elapsed >= timeoutSeconds) { + RemoveEntryList(&pending->ListEntry); + expired = pending; + break; + } } - LONGLONG elapsed = (now.QuadPart - pending->Timestamp.QuadPart) / 10000000LL; - if (elapsed >= 15) { - stuck = TRUE; + KeReleaseSpinLock(&g_Ctx->PendingLock, oldIrql); + + if (!expired) { break; } - } - - KeReleaseSpinLock(&g_Ctx->PendingLock, oldIrql); - if (stuck) { - TriggerFatal(g_Ctx, STATUS_DRIVER_INTERNAL_ERROR, "pending entry timed out without verdict"); + ExecuteVerdict(g_Ctx, expired, VERDICT_BYPASS); + ExFreePoolWithTag(expired, 'rniW'); } } diff --git a/redirect_windows.go b/redirect_windows.go index 17e8e661..157c9f8f 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -189,12 +189,19 @@ func (r *autoRedirect) handlePendingConn(conn *winredirect.PendingConn) { Verdict: verdict, }) if err != nil { + if isStaleVerdictError(err) { + return + } if !r.closing.Load() { r.handleFatalError(E.Cause(r.enrichDriverError(err), "set redirect verdict")) } } } +func isStaleVerdictError(err error) bool { + return errors.Is(err, windows.ERROR_NOT_FOUND) || errors.Is(err, windows.ERROR_FILE_NOT_FOUND) +} + func (r *autoRedirect) enrichDriverError(err error) error { fatalInfo, infoErr := r.driverManager.GetFatalInfo() if infoErr != nil || fatalInfo.Status == 0 { From 9b306c0794ca21f3e835c0da90fef25f11b8c257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Mar 2026 01:10:19 +0800 Subject: [PATCH 32/38] windows: modernize driver API usage and simplify GUID handling --- internal/winredirect/driver/winredirect.c | 27 +++++------------------ internal/winredirect/driver/winredirect.h | 2 +- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index c82e6162..3b643bee 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -1,4 +1,3 @@ -#pragma warning(disable: 4996) // ExAllocatePoolWithTag deprecation #include "winredirect.h" @@ -115,16 +114,6 @@ typedef enum _BEST_ROUTE_RESULT { BestRouteOther = 2, } BEST_ROUTE_RESULT; -static BOOLEAN IsZeroGuid(_In_reads_(16) const UINT8* GuidBytes) -{ - for (UINT32 i = 0; i < sizeof(GUID); i++) { - if (GuidBytes[i] != 0) { - return FALSE; - } - } - return TRUE; -} - static CONFIG_SNAPSHOT ReadConfigSnapshot(_In_ PDRIVER_CONTEXT Ctx) { CONFIG_SNAPSHOT snapshot; @@ -191,7 +180,7 @@ static NTSTATUS BestRouteForEntry( if (status != STATUS_SUCCESS) { return status; } - if (RtlEqualMemory(&bestRoute.InterfaceLuid, &Snapshot->TunLuid, sizeof(NET_LUID))) { + if (bestRoute.InterfaceLuid.Value == Snapshot->TunLuid.Value) { *Result = BestRouteTun; return STATUS_SUCCESS; } @@ -449,15 +438,12 @@ void EvtIoDeviceControl( status = WdfRequestRetrieveInputBuffer(Request, sizeof(WINREDIRECT_CONFIG), &inBuf, &inLen); if (NT_SUCCESS(status)) { WINREDIRECT_CONFIG* config = (WINREDIRECT_CONFIG*)inBuf; - GUID tunGuid; NET_LUID tunLuid = {0}; KIRQL oldIrql; - if (config->RedirectPort == 0 || config->ProxyPID == 0 || IsZeroGuid(config->TunGuid)) { + if (config->RedirectPort == 0 || config->ProxyPID == 0 || InlineIsEqualGUID(&GUID_NULL, &config->TunGuid)) { status = STATUS_INVALID_PARAMETER; } else { - RtlZeroMemory(&tunGuid, sizeof(tunGuid)); - RtlCopyMemory(&tunGuid, config->TunGuid, sizeof(tunGuid)); - status = ConvertInterfaceGuidToLuid(&tunGuid, &tunLuid); + status = ConvertInterfaceGuidToLuid(&config->TunGuid, &tunLuid); } if (NT_SUCCESS(status)) { KeAcquireSpinLock(&ctx->ConfigLock, &oldIrql); @@ -845,13 +831,11 @@ static void ClassifyFnCommon( } // Allocate pending entry - entry = (PPENDING_ENTRY)ExAllocatePoolWithTag(NonPagedPool, sizeof(PENDING_ENTRY), 'rniW'); + entry = (PPENDING_ENTRY)ExAllocatePoolZero(PagedPool, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { FailClosedClassify(ctx, classifyOut, STATUS_INSUFFICIENT_RESOURCES, "allocate pending entry"); return; } - - RtlZeroMemory(entry, sizeof(PENDING_ENTRY)); entry->ConnID = InterlockedIncrement64(&ctx->NextConnID); entry->AddressFamily = addressFamily; entry->FilterId = filter->filterId; @@ -1053,11 +1037,10 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI redirectStatus = STATUS_INVALID_DEVICE_STATE; } else { SOCKADDR_STORAGE* redirectContext = - (SOCKADDR_STORAGE*)ExAllocatePoolWithTag(NonPagedPool, sizeof(SOCKADDR_STORAGE) * 2, 'rniW'); + (SOCKADDR_STORAGE*)ExAllocatePoolZero(PagedPool, sizeof(SOCKADDR_STORAGE) * 2, 'rniW'); if (!redirectContext) { redirectStatus = STATUS_INSUFFICIENT_RESOURCES; } else { - RtlZeroMemory(redirectContext, sizeof(SOCKADDR_STORAGE) * 2); RtlCopyMemory(&redirectContext[0], &connReq->remoteAddressAndPort, sizeof(SOCKADDR_STORAGE)); RtlCopyMemory(&redirectContext[1], &connReq->localAddressAndPort, sizeof(SOCKADDR_STORAGE)); diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index d0c1f11f..5a758e73 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -35,7 +35,7 @@ typedef struct _WINREDIRECT_CONFIG { UINT16 RedirectPort; UINT8 _pad0[2]; UINT32 ProxyPID; - UINT8 TunGuid[16]; + GUID TunGuid; } WINREDIRECT_CONFIG; typedef struct _WINREDIRECT_PENDING_CONN { From 255b479aca9254286734a5acfb0c980b0046870d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Mar 2026 09:05:51 +0800 Subject: [PATCH 33/38] ci: sync bundled winredirect drivers --- .github/scripts/compare_signed_pe.py | 89 +++++++ .github/workflows/winredirect-driver-sync.yml | 228 ++++++++++++++++++ internal/winredirect/amd64/winredirect.sys | Bin 21848 -> 21840 bytes internal/winredirect/arm64/winredirect.sys | Bin 29632 -> 24952 bytes internal/winredirect/driver/winredirect.c | 3 +- .../winredirect/driver/winredirect.vcxproj | 1 + 6 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/compare_signed_pe.py create mode 100644 .github/workflows/winredirect-driver-sync.yml diff --git a/.github/scripts/compare_signed_pe.py b/.github/scripts/compare_signed_pe.py new file mode 100644 index 00000000..7a11ecc3 --- /dev/null +++ b/.github/scripts/compare_signed_pe.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import hashlib +import sys +from pathlib import Path + + +class PEFormatError(ValueError): + pass + + +def read_u16(data: bytes, offset: int) -> int: + return int.from_bytes(data[offset:offset + 2], "little") + + +def read_u32(data: bytes, offset: int) -> int: + return int.from_bytes(data[offset:offset + 4], "little") + + +def canonicalize_signed_pe(path: Path) -> bytes: + data = bytearray(path.read_bytes()) + if len(data) < 0x40 or data[:2] != b"MZ": + raise PEFormatError(f"{path}: missing DOS header") + + pe_offset = read_u32(data, 0x3C) + if pe_offset + 24 > len(data) or data[pe_offset:pe_offset + 4] != b"PE\x00\x00": + raise PEFormatError(f"{path}: missing PE header") + + optional_offset = pe_offset + 24 + magic = read_u16(data, optional_offset) + if magic == 0x10B: + data_directory_offset = optional_offset + 96 + elif magic == 0x20B: + data_directory_offset = optional_offset + 112 + else: + raise PEFormatError(f"{path}: unsupported optional header magic 0x{magic:04x}") + + checksum_offset = optional_offset + 64 + if checksum_offset + 4 > len(data): + raise PEFormatError(f"{path}: truncated optional header") + data[checksum_offset:checksum_offset + 4] = b"\x00" * 4 + + security_directory_offset = data_directory_offset + 8 * 4 + if security_directory_offset + 8 > len(data): + raise PEFormatError(f"{path}: truncated data directories") + + certificate_offset = read_u32(data, security_directory_offset) + certificate_size = read_u32(data, security_directory_offset + 4) + data[security_directory_offset:security_directory_offset + 8] = b"\x00" * 8 + + if certificate_offset == 0 or certificate_size == 0: + return bytes(data) + + certificate_end = certificate_offset + certificate_size + if certificate_end > len(data): + raise PEFormatError(f"{path}: certificate table exceeds file size") + + return bytes(data[:certificate_offset] + data[certificate_end:]) + + +def canonical_sha256(path: Path) -> str: + return hashlib.sha256(canonicalize_signed_pe(path)).hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Compare two PE files after stripping Authenticode-only differences.", + ) + parser.add_argument("left", type=Path) + parser.add_argument("right", type=Path) + args = parser.parse_args() + + try: + left_hash = canonical_sha256(args.left) + right_hash = canonical_sha256(args.right) + except (OSError, PEFormatError) as exc: + print(exc, file=sys.stderr) + return 2 + + print(f"{args.left}: {left_hash}") + print(f"{args.right}: {right_hash}") + return 0 if left_hash == right_hash else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/winredirect-driver-sync.yml b/.github/workflows/winredirect-driver-sync.yml new file mode 100644 index 00000000..0172d32b --- /dev/null +++ b/.github/workflows/winredirect-driver-sync.yml @@ -0,0 +1,228 @@ +name: sync winredirect driver + +on: + pull_request: + branches: + - main + - dev + paths: + - '.github/scripts/compare_signed_pe.py' + - '.github/workflows/winredirect-driver-sync.yml' + - 'internal/winredirect/driver/**' + +permissions: + contents: write + +concurrency: + group: winredirect-driver-${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }} + cancel-in-progress: true + +jobs: + sync: + name: Refresh bundled drivers + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: windows-2025 + env: + REPO_DIR: C:\Users\sekai\Projects\sing-tun + steps: + - name: Checkout PR head + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + + - name: Install Windows SDK and WDK + shell: pwsh + run: | + $sdkHeader = 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\Windows.h' + $wdkHeader = 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\km\ntddk.h' + + if (-not (Test-Path $sdkHeader)) { + winget install --source winget --exact --id Microsoft.WindowsSDK.10.0.26100 --accept-source-agreements --accept-package-agreements --disable-interactivity --log "$env:RUNNER_TEMP\sdk-install.log" + Write-Host "Windows SDK install exit code: $LASTEXITCODE" + } + + if (-not (Test-Path $wdkHeader)) { + winget install --source winget --exact --id Microsoft.WindowsWDK.10.0.26100 --accept-source-agreements --accept-package-agreements --disable-interactivity --log "$env:RUNNER_TEMP\wdk-install.log" + Write-Host "WDK install exit code: $LASTEXITCODE" + } + + if (-not (Test-Path $sdkHeader)) { + throw "Windows SDK header not found after installation: $sdkHeader" + } + if (-not (Test-Path $wdkHeader)) { + throw "WDK header not found after installation: $wdkHeader" + } + + - name: Mirror repository to stable Windows path + shell: pwsh + run: | + if (Test-Path $env:REPO_DIR) { + Remove-Item -LiteralPath $env:REPO_DIR -Recurse -Force + } + New-Item -ItemType Directory -Path (Split-Path -Parent $env:REPO_DIR) -Force | Out-Null + robocopy $env:GITHUB_WORKSPACE $env:REPO_DIR /MIR /NFL /NDL /NJH /NJS /NP + $robocopyExitCode = $LASTEXITCODE + if ($robocopyExitCode -gt 7) { + throw "robocopy failed with exit code $robocopyExitCode" + } + Write-Host "robocopy completed with exit code $robocopyExitCode" + exit 0 + + - name: Print toolchain versions + shell: pwsh + run: | + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $vsInstall = & $vswhere -latest -products * -property installationPath | Select-Object -First 1 + $msbuild = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 + if (-not $vsInstall) { + throw 'Visual Studio installation path not found' + } + if (-not $msbuild) { + throw 'MSBuild.exe not found' + } + + $vcToolsRoot = Join-Path $vsInstall 'VC\Tools\MSVC' + $vcToolsVersion = (Get-ChildItem $vcToolsRoot | Sort-Object Name | Select-Object -Last 1).Name + $kitsRoot = 'C:\Program Files (x86)\Windows Kits\10\Include' + $sdkHeader = 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\Windows.h' + $wdkHeader = 'C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\km\ntddk.h' + + function Show-FileVersion([string] $label, [string] $path) { + if (Test-Path $path) { + $item = Get-Item $path + Write-Host "$label path: $path" + Write-Host "$label version: $($item.VersionInfo.FileVersion)" + } else { + Write-Host "$label path: missing ($path)" + } + } + + Write-Host "Visual Studio install: $vsInstall" + Write-Host "MSBuild path: $msbuild" + & $msbuild -version + Write-Host "VC tools root: $vcToolsRoot" + Write-Host "VC tools version: $vcToolsVersion" + + Show-FileVersion 'cl Hostx64 x64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\x64\cl.exe") + Show-FileVersion 'cl Hostx64 ARM64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\arm64\cl.exe") + Show-FileVersion 'link Hostx64 x64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\x64\link.exe") + Show-FileVersion 'link Hostx64 ARM64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\arm64\link.exe") + + Write-Host 'Windows Kits include directories:' + Get-ChildItem $kitsRoot | Sort-Object Name | ForEach-Object { Write-Host " $($_.Name)" } + Write-Host "Windows SDK header present: $(Test-Path $sdkHeader)" + Write-Host "WDK header present: $(Test-Path $wdkHeader)" + + - name: Build, verify reproducibility, and refresh tracked drivers + id: sync + shell: pwsh + working-directory: ${{ env.REPO_DIR }} + run: | + git config --global --add safe.directory $env:REPO_DIR + + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $msbuild = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 + if (-not $msbuild) { + throw 'MSBuild.exe not found' + } + + $env:CL = '/Brepro' + $env:LINK = '/Brepro' + $artifactDir = Join-Path $env:RUNNER_TEMP 'winredirect-driver-sync' + $failureDir = Join-Path $artifactDir 'failure' + Remove-Item -LiteralPath $artifactDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null + + function Invoke-DriverBuild([string] $platform) { + Remove-Item -LiteralPath "internal/winredirect/driver/build/$platform" -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath "internal/winredirect/driver/intermediate/$platform" -Recurse -Force -ErrorAction SilentlyContinue + & $msbuild 'internal/winredirect/driver/winredirect.vcxproj' '/t:Rebuild' '/p:Configuration=Release' "/p:Platform=$platform" '/p:SpectreMitigation=false' '/v:minimal' + if ($LASTEXITCODE -ne 0) { + throw "MSBuild failed for $platform with exit code $LASTEXITCODE" + } + } + + $targets = @( + @{ platform = 'x64'; repo = 'internal/winredirect/amd64/winredirect.sys' }, + @{ platform = 'ARM64'; repo = 'internal/winredirect/arm64/winredirect.sys' } + ) + + Write-Host 'ARM and x86 are intentionally skipped here: WDK 10.0.26100 on GitHub-hosted CI does not support them.' + + $changed = $false + foreach ($target in $targets) { + $platform = $target.platform + $tracked = $target.repo + $buildOutput = "internal/winredirect/driver/build/$platform/Release/winredirect.sys" + $run1 = Join-Path $artifactDir "$platform-run1.sys" + $run2 = Join-Path $artifactDir "$platform-run2.sys" + + Invoke-DriverBuild $platform + Copy-Item -LiteralPath $buildOutput -Destination $run1 -Force + + Invoke-DriverBuild $platform + Copy-Item -LiteralPath $buildOutput -Destination $run2 -Force + + & python '.github/scripts/compare_signed_pe.py' $run1 $run2 + switch ($LASTEXITCODE) { + 0 { + Write-Host "$platform reproduced after stripping Authenticode data." + } + 1 { + New-Item -ItemType Directory -Path $failureDir -Force | Out-Null + Copy-Item -LiteralPath $run1 -Destination (Join-Path $failureDir "$platform-run1.sys") -Force + Copy-Item -LiteralPath $run2 -Destination (Join-Path $failureDir "$platform-run2.sys") -Force + throw "$platform build is not reproducible beyond signing metadata." + } + default { + throw "reproducibility comparison failed for $platform with exit code $LASTEXITCODE" + } + } + + & python '.github/scripts/compare_signed_pe.py' $run2 $tracked + switch ($LASTEXITCODE) { + 0 { + Write-Host "$platform matches the tracked driver after stripping Authenticode data." + } + 1 { + Write-Host "$platform differs beyond signing metadata; replacing tracked driver." + Copy-Item -LiteralPath $run2 -Destination $tracked -Force + git add -- $tracked + $changed = $true + } + default { + throw "comparison failed for $platform with exit code $LASTEXITCODE" + } + } + } + + if ($changed) { + "changed=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + } else { + "changed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + } + + - name: Upload failed reproducibility artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: winredirect-repro-failure-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}\winredirect-driver-sync\failure\*.sys + if-no-files-found: warn + + - name: Commit and push refreshed drivers + if: steps.sync.outputs.changed == 'true' + shell: pwsh + working-directory: ${{ env.REPO_DIR }} + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + git diff --cached --quiet + if ($LASTEXITCODE -eq 0) { + exit 0 + } + + git commit -m 'winredirect: update bundled drivers' + git push origin "HEAD:${{ github.event.pull_request.head.ref }}" diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys index 0aed40dbd73a23254a0dae271e0143a7f748848f..ac2ee63f45864e80717488b88c45569316df7eac 100644 GIT binary patch delta 8242 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 delta 8362 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 literal 29632 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 == 0 || config->ProxyPID == 0 || InlineIsEqualGUID(&GUID_NULL, &config->TunGuid)) { + if (config->RedirectPort == 0 || config->ProxyPID == 0 || InlineIsEqualGUID(&nullGuid, &config->TunGuid)) { status = STATUS_INVALID_PARAMETER; } else { status = ConvertInterfaceGuidToLuid(&config->TunGuid, &tunLuid); diff --git a/internal/winredirect/driver/winredirect.vcxproj b/internal/winredirect/driver/winredirect.vcxproj index d1b9741d..8d3afe3b 100644 --- a/internal/winredirect/driver/winredirect.vcxproj +++ b/internal/winredirect/driver/winredirect.vcxproj @@ -79,6 +79,7 @@ Fwpkclnt.lib;$(DDK_LIB_PATH)netio.lib;$(DDK_LIB_PATH)wdmsec.lib;$(KernelBufferOverflowLib);$(DDK_LIB_PATH)ntoskrnl.lib;$(DDK_LIB_PATH)hal.lib;$(DDK_LIB_PATH)wmilib.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfLdr.lib;$(KMDF_LIB_PATH)$(KMDF_VER_PATH)\WdfDriverEntry.lib;%(AdditionalDependencies) + false true From 259fc61a3412c6e5a72c6b33ea5347c80d7f33a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Mar 2026 11:09:17 +0800 Subject: [PATCH 34/38] ci: capture arm64 driver diagnostics --- .github/workflows/winredirect-driver-sync.yml | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/winredirect-driver-sync.yml b/.github/workflows/winredirect-driver-sync.yml index 0172d32b..04f90d8c 100644 --- a/.github/workflows/winredirect-driver-sync.yml +++ b/.github/workflows/winredirect-driver-sync.yml @@ -104,6 +104,10 @@ jobs: Write-Host "VC tools root: $vcToolsRoot" Write-Host "VC tools version: $vcToolsVersion" + Show-FileVersion 'cl Hostx86 x64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx86\x64\cl.exe") + Show-FileVersion 'cl Hostx86 ARM64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx86\arm64\cl.exe") + Show-FileVersion 'link Hostx86 x64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx86\x64\link.exe") + Show-FileVersion 'link Hostx86 ARM64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx86\arm64\link.exe") Show-FileVersion 'cl Hostx64 x64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\x64\cl.exe") Show-FileVersion 'cl Hostx64 ARM64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\arm64\cl.exe") Show-FileVersion 'link Hostx64 x64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\x64\link.exe") @@ -134,10 +138,18 @@ jobs: Remove-Item -LiteralPath $artifactDir -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null - function Invoke-DriverBuild([string] $platform) { + function Invoke-DriverBuild([string] $platform, [string] $buildTag) { Remove-Item -LiteralPath "internal/winredirect/driver/build/$platform" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath "internal/winredirect/driver/intermediate/$platform" -Recurse -Force -ErrorAction SilentlyContinue - & $msbuild 'internal/winredirect/driver/winredirect.vcxproj' '/t:Rebuild' '/p:Configuration=Release' "/p:Platform=$platform" '/p:SpectreMitigation=false' '/v:minimal' + if ($platform -eq 'ARM64') { + $diagDir = Join-Path $artifactDir 'arm64-diag' + New-Item -ItemType Directory -Path $diagDir -Force | Out-Null + $diagLog = Join-Path $diagDir "$buildTag.log" + $diagBinlog = Join-Path $diagDir "$buildTag.binlog" + & $msbuild 'internal/winredirect/driver/winredirect.vcxproj' '/t:Rebuild' '/p:Configuration=Release' "/p:Platform=$platform" '/p:SpectreMitigation=false' '/v:diag' "/bl:$diagBinlog" 2>&1 | Tee-Object -FilePath $diagLog + } else { + & $msbuild 'internal/winredirect/driver/winredirect.vcxproj' '/t:Rebuild' '/p:Configuration=Release' "/p:Platform=$platform" '/p:SpectreMitigation=false' '/v:minimal' + } if ($LASTEXITCODE -ne 0) { throw "MSBuild failed for $platform with exit code $LASTEXITCODE" } @@ -158,10 +170,10 @@ jobs: $run1 = Join-Path $artifactDir "$platform-run1.sys" $run2 = Join-Path $artifactDir "$platform-run2.sys" - Invoke-DriverBuild $platform + Invoke-DriverBuild $platform "$platform-run1" Copy-Item -LiteralPath $buildOutput -Destination $run1 -Force - Invoke-DriverBuild $platform + Invoke-DriverBuild $platform "$platform-run2" Copy-Item -LiteralPath $buildOutput -Destination $run2 -Force & python '.github/scripts/compare_signed_pe.py' $run1 $run2 @@ -208,7 +220,10 @@ jobs: uses: actions/upload-artifact@v4 with: name: winredirect-repro-failure-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}\winredirect-driver-sync\failure\*.sys + path: | + ${{ runner.temp }}\winredirect-driver-sync\failure\*.sys + ${{ runner.temp }}\winredirect-driver-sync\arm64-diag\*.log + ${{ runner.temp }}\winredirect-driver-sync\arm64-diag\*.binlog if-no-files-found: warn - name: Commit and push refreshed drivers From 88d347450dbeef55f9e31502fd5de46045cc97d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Mar 2026 11:42:00 +0800 Subject: [PATCH 35/38] ci: print visual studio catalog version --- .github/workflows/winredirect-driver-sync.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/winredirect-driver-sync.yml b/.github/workflows/winredirect-driver-sync.yml index 04f90d8c..f30bab11 100644 --- a/.github/workflows/winredirect-driver-sync.yml +++ b/.github/workflows/winredirect-driver-sync.yml @@ -73,6 +73,8 @@ jobs: shell: pwsh run: | $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $vsInstances = & $vswhere -all -products * -format json | ConvertFrom-Json + $vs = $vsInstances | Select-Object -First 1 $vsInstall = & $vswhere -latest -products * -property installationPath | Select-Object -First 1 $msbuild = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 if (-not $vsInstall) { @@ -98,6 +100,15 @@ jobs: } } + if ($vs) { + Write-Host "VS installation name: $($vs.installationName)" + Write-Host "VS installation version: $($vs.installationVersion)" + Write-Host "VS channel id: $($vs.channelId)" + Write-Host "VS channel manifest id: $($vs.properties.channelManifestId)" + Write-Host "VS catalog product display version: $($vs.catalog.productDisplayVersion)" + Write-Host "VS catalog product semantic version: $($vs.catalog.productSemanticVersion)" + Write-Host "VS catalog build version: $($vs.catalog.buildVersion)" + } Write-Host "Visual Studio install: $vsInstall" Write-Host "MSBuild path: $msbuild" & $msbuild -version From 3a3849d21ba6c677d564e032fa236eaf4a58d92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Mar 2026 11:56:10 +0800 Subject: [PATCH 36/38] ci: print arm64 library hashes --- .github/workflows/winredirect-driver-sync.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/winredirect-driver-sync.yml b/.github/workflows/winredirect-driver-sync.yml index f30bab11..e1070b26 100644 --- a/.github/workflows/winredirect-driver-sync.yml +++ b/.github/workflows/winredirect-driver-sync.yml @@ -100,6 +100,15 @@ jobs: } } + function Show-FileHash([string] $label, [string] $path) { + if (Test-Path $path) { + $hash = (Get-FileHash $path -Algorithm SHA256).Hash + Write-Host "$label sha256: $hash" + } else { + Write-Host "$label sha256: missing ($path)" + } + } + if ($vs) { Write-Host "VS installation name: $($vs.installationName)" Write-Host "VS installation version: $($vs.installationVersion)" @@ -124,6 +133,13 @@ jobs: Show-FileVersion 'link Hostx64 x64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\x64\link.exe") Show-FileVersion 'link Hostx64 ARM64' (Join-Path $vcToolsRoot "$vcToolsVersion\bin\Hostx64\arm64\link.exe") + Show-FileHash 'arm64rt.lib' 'C:\Program Files (x86)\Windows Kits\10\lib\10.0.26100.0\um\arm64\arm64rt.lib' + Show-FileHash 'BufferOverflowFastFailK.lib' 'C:\Program Files (x86)\Windows Kits\10\lib\10.0.26100.0\km\arm64\BufferOverflowFastFailK.lib' + Show-FileHash 'netio.lib' 'C:\Program Files (x86)\Windows Kits\10\lib\10.0.26100.0\km\arm64\netio.lib' + Show-FileHash 'ntoskrnl.lib' 'C:\Program Files (x86)\Windows Kits\10\lib\10.0.26100.0\km\arm64\ntoskrnl.lib' + Show-FileHash 'WdfLdr.lib' 'C:\Program Files (x86)\Windows Kits\10\lib\wdf\kmdf\arm64\1.15\WdfLdr.lib' + Show-FileHash 'WdfDriverEntry.lib' 'C:\Program Files (x86)\Windows Kits\10\lib\wdf\kmdf\arm64\1.15\WdfDriverEntry.lib' + Write-Host 'Windows Kits include directories:' Get-ChildItem $kitsRoot | Sort-Object Name | ForEach-Object { Write-Host " $($_.Name)" } Write-Host "Windows SDK header present: $(Test-Path $sdkHeader)" From 5d4df52809ecebe256cf7cc16231507ae7a7e3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Mar 2026 14:13:24 +0800 Subject: [PATCH 37/38] winredirect: simplify Windows verdict handling --- internal/winredirect/driver/winredirect.c | 83 +++++++++++------------ internal/winredirect/driver/winredirect.h | 3 +- internal/winredirect/types_windows.go | 5 +- redirect_windows.go | 14 ++-- 4 files changed, 52 insertions(+), 53 deletions(-) diff --git a/internal/winredirect/driver/winredirect.c b/internal/winredirect/driver/winredirect.c index b17488de..dd5f36b2 100644 --- a/internal/winredirect/driver/winredirect.c +++ b/internal/winredirect/driver/winredirect.c @@ -61,7 +61,7 @@ static NTSTATUS TriggerFatal(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status, _In return previous; } -static void FailClosedClassify( +static void TriggerFatalAndPermitClassify( _In_ PDRIVER_CONTEXT Ctx, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut, _In_ NTSTATUS Status, @@ -70,7 +70,7 @@ static void FailClosedClassify( if (Ctx) { TriggerFatal(Ctx, Status, Message); } - BlockClassify(classifyOut); + PermitClassify(classifyOut); } static BOOLEAN IsLoopbackAddress(_In_ UINT8 AddressFamily, _In_reads_(16) const UINT8* Address) @@ -129,7 +129,7 @@ static CONFIG_SNAPSHOT ReadConfigSnapshot(_In_ PDRIVER_CONTEXT Ctx) return snapshot; } -static NTSTATUS BestRouteForEntry( +static BOOLEAN TryBestRouteForEntry( _In_ const CONFIG_SNAPSHOT* Snapshot, _In_ const PENDING_ENTRY* Entry, _Out_ BEST_ROUTE_RESULT* Result) @@ -142,13 +142,13 @@ static NTSTATUS BestRouteForEntry( NETIO_STATUS status; if (!Snapshot->HasTunLuid) { - return STATUS_INVALID_DEVICE_STATE; + return FALSE; } // GetBestRoute2 requires IRQL < DISPATCH_LEVEL. We do not currently // characterize every runtime context where route lookup can be unavailable, // so report a normal lookup failure and let the caller decide the fallback. if (KeGetCurrentIrql() >= DISPATCH_LEVEL) { - return STATUS_NOT_SUPPORTED; + return FALSE; } RtlZeroMemory(&sourceAddress, sizeof(sourceAddress)); @@ -173,19 +173,19 @@ static NTSTATUS BestRouteForEntry( sourceAddressPtr = &sourceAddress; } } else { - return STATUS_INVALID_PARAMETER; + return FALSE; } status = GetBestRoute2(NULL, 0, sourceAddressPtr, &destinationAddress, 0, &bestRoute, &bestSourceAddress); - if (status != STATUS_SUCCESS) { - return status; + if (status != 0) { + return FALSE; } if (bestRoute.InterfaceLuid.Value == Snapshot->TunLuid.Value) { *Result = BestRouteTun; - return STATUS_SUCCESS; + return TRUE; } *Result = BestRouteOther; - return STATUS_SUCCESS; + return TRUE; } static void CancelPendingIoctlRequests(_In_ PDRIVER_CONTEXT Ctx, _In_ NTSTATUS Status) @@ -321,6 +321,7 @@ NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING Regi if (!NT_SUCCESS(status)) { WdfDeviceInitFree(deviceInit); return status; } WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttrs, DRIVER_CONTEXT); + deviceAttrs.ExecutionLevel = WdfExecutionLevelPassive; status = WdfDeviceCreate(&deviceInit, &deviceAttrs, &device); if (!NT_SUCCESS(status)) return status; @@ -397,7 +398,7 @@ void EvtDriverUnload(_In_ WDFDRIVER Driver) fatalStatus = ReadFatalStatus(g_Ctx); ShutdownRedirect( g_Ctx, - fatalStatus != STATUS_SUCCESS ? VERDICT_DROP : VERDICT_BYPASS, + VERDICT_PERMIT, fatalStatus != STATUS_SUCCESS ? fatalStatus : STATUS_CANCELLED); } } @@ -485,9 +486,9 @@ void EvtIoDeviceControl( case IOCTL_WINREDIRECT_STOP: if (fatalStatus != STATUS_SUCCESS) { - ShutdownRedirect(ctx, VERDICT_DROP, fatalStatus); + ShutdownRedirect(ctx, VERDICT_PERMIT, fatalStatus); } else { - ShutdownRedirect(ctx, VERDICT_BYPASS, STATUS_CANCELLED); + ShutdownRedirect(ctx, VERDICT_PERMIT, STATUS_CANCELLED); } WdfRequestComplete(Request, STATUS_SUCCESS); break; @@ -525,6 +526,10 @@ void EvtIoDeviceControl( break; } WINREDIRECT_VERDICT* v = (WINREDIRECT_VERDICT*)inBuf; + if (v->Verdict != VERDICT_REDIRECT && v->Verdict != VERDICT_PERMIT) { + WdfRequestComplete(Request, STATUS_INVALID_PARAMETER); + break; + } PPENDING_ENTRY entry = PendingFindByID(ctx, v->ConnID); if (entry) { ExecuteVerdict(ctx, entry, v->Verdict); @@ -609,7 +614,7 @@ void EvtTimeoutWorkItem(_In_ WDFWORKITEM WorkItem) break; } - ExecuteVerdict(g_Ctx, expired, VERDICT_BYPASS); + ExecuteVerdict(g_Ctx, expired, VERDICT_PERMIT); ExFreePoolWithTag(expired, 'rniW'); } } @@ -633,7 +638,7 @@ void EvtFatalWorkItem(_In_ WDFWORKITEM WorkItem) return; } - ShutdownRedirect(g_Ctx, VERDICT_DROP, fatalStatus); + ShutdownRedirect(g_Ctx, VERDICT_PERMIT, fatalStatus); } // --- WFP Setup --- @@ -732,10 +737,6 @@ NTSTATUS WfpSetup(_In_ PDRIVER_CONTEXT Ctx) void WfpCleanup(_In_ PDRIVER_CONTEXT Ctx) { - if (Ctx->RedirectHandle) { - FwpsRedirectHandleDestroy0(Ctx->RedirectHandle); - Ctx->RedirectHandle = NULL; - } if (Ctx->CalloutIdV4) { FwpsCalloutUnregisterById0(Ctx->CalloutIdV4); Ctx->CalloutIdV4 = 0; @@ -744,6 +745,10 @@ void WfpCleanup(_In_ PDRIVER_CONTEXT Ctx) FwpsCalloutUnregisterById0(Ctx->CalloutIdV6); Ctx->CalloutIdV6 = 0; } + if (Ctx->RedirectHandle) { + FwpsRedirectHandleDestroy0(Ctx->RedirectHandle); + Ctx->RedirectHandle = NULL; + } if (Ctx->EngineHandle) { FwpmEngineClose0(Ctx->EngineHandle); Ctx->EngineHandle = NULL; @@ -795,7 +800,7 @@ static void ClassifyFnCommon( fatalStatus = ReadFatalStatus(ctx); if (fatalStatus != STATUS_SUCCESS) { - BlockClassify(classifyOut); + PermitClassify(classifyOut); return; } @@ -832,9 +837,9 @@ static void ClassifyFnCommon( } // Allocate pending entry - entry = (PPENDING_ENTRY)ExAllocatePoolZero(PagedPool, sizeof(PENDING_ENTRY), 'rniW'); + entry = (PPENDING_ENTRY)ExAllocatePoolZero(NonPagedPoolNx, sizeof(PENDING_ENTRY), 'rniW'); if (!entry) { - FailClosedClassify(ctx, classifyOut, STATUS_INSUFFICIENT_RESOURCES, "allocate pending entry"); + TriggerFatalAndPermitClassify(ctx, classifyOut, STATUS_INSUFFICIENT_RESOURCES, "allocate pending entry"); return; } entry->ConnID = InterlockedIncrement64(&ctx->NextConnID); @@ -858,7 +863,7 @@ static void ClassifyFnCommon( RtlCopyMemory(entry->DstAddr, dstArr->byteArray16, 16); } else { ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, STATUS_INVALID_ADDRESS_COMPONENT, "ipv6 null destination"); + TriggerFatalAndPermitClassify(ctx, classifyOut, STATUS_INVALID_ADDRESS_COMPONENT, "ipv6 null destination"); return; } } @@ -871,16 +876,15 @@ static void ClassifyFnCommon( return; } - status = BestRouteForEntry(&snapshot, entry, &bestRoute); - // Windows auto-redirect is best-effort: only redirect connections that are - // positively identified as already routed to the TUN. If route lookup says - // "not TUN" or fails for a context we do not currently characterize, leave - // the original connect alone instead of redirecting or blocking unknown traffic. - if (!NT_SUCCESS(status) || bestRoute == BestRouteOther) { + if (!TryBestRouteForEntry(&snapshot, entry, &bestRoute) || bestRoute == BestRouteOther) { ExFreePoolWithTag(entry, 'rniW'); PermitClassify(classifyOut); return; } + // Windows auto-redirect is best-effort: only redirect connections that are + // positively identified as already routed to the TUN. If route lookup says + // "not TUN" or fails for a context we do not currently characterize, leave + // the original connect alone instead of redirecting unknown traffic. // Extract PID from metadata if (FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_PROCESS_ID)) { @@ -889,7 +893,7 @@ static void ClassifyFnCommon( if (!classifyContext) { ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, STATUS_INVALID_DEVICE_STATE, "no classify context"); + TriggerFatalAndPermitClassify(ctx, classifyOut, STATUS_INVALID_DEVICE_STATE, "no classify context"); return; } @@ -897,7 +901,7 @@ static void ClassifyFnCommon( status = FwpsAcquireClassifyHandle0((void*)classifyContext, 0, &classifyHandle); if (!NT_SUCCESS(status)) { ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, status, "acquire classify handle"); + TriggerFatalAndPermitClassify(ctx, classifyOut, status, "acquire classify handle"); return; } @@ -910,7 +914,7 @@ static void ClassifyFnCommon( if (!NT_SUCCESS(status) || !entry->WritableLayerData) { FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, !NT_SUCCESS(status) ? status : STATUS_INVALID_DEVICE_STATE, "acquire writable layer data"); + TriggerFatalAndPermitClassify(ctx, classifyOut, !NT_SUCCESS(status) ? status : STATUS_INVALID_DEVICE_STATE, "acquire writable layer data"); return; } @@ -922,7 +926,7 @@ static void ClassifyFnCommon( FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS); FwpsReleaseClassifyHandle0(classifyHandle); ExFreePoolWithTag(entry, 'rniW'); - FailClosedClassify(ctx, classifyOut, status, "pend classify"); + TriggerFatalAndPermitClassify(ctx, classifyOut, status, "pend classify"); return; } @@ -1038,7 +1042,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI redirectStatus = STATUS_INVALID_DEVICE_STATE; } else { SOCKADDR_STORAGE* redirectContext = - (SOCKADDR_STORAGE*)ExAllocatePoolZero(PagedPool, sizeof(SOCKADDR_STORAGE) * 2, 'rniW'); + (SOCKADDR_STORAGE*)ExAllocatePoolZero(NonPagedPoolNx, sizeof(SOCKADDR_STORAGE) * 2, 'rniW'); if (!redirectContext) { redirectStatus = STATUS_INSUFFICIENT_RESOURCES; } else { @@ -1084,7 +1088,7 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI if (!NT_SUCCESS(redirectStatus)) { TriggerFatal(Ctx, redirectStatus, "execute redirect"); - Verdict = VERDICT_DROP; + Verdict = VERDICT_PERMIT; } } @@ -1096,13 +1100,8 @@ void ExecuteVerdict(_In_ PDRIVER_CONTEXT Ctx, _In_ PPENDING_ENTRY Entry, _In_ UI Entry->WritableLayerData = NULL; } - if (Verdict == VERDICT_REDIRECT || Verdict == VERDICT_BYPASS) { - classifyOut.actionType = FWP_ACTION_PERMIT; - classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; - } else { // VERDICT_DROP - classifyOut.actionType = FWP_ACTION_BLOCK; - classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; - } + classifyOut.actionType = FWP_ACTION_PERMIT; + classifyOut.rights &= ~FWPS_RIGHT_ACTION_WRITE; FwpsCompleteClassify0(Entry->ClassifyHandle, 0, &classifyOut); FwpsReleaseClassifyHandle0(Entry->ClassifyHandle); diff --git a/internal/winredirect/driver/winredirect.h b/internal/winredirect/driver/winredirect.h index 5a758e73..5a231fbb 100644 --- a/internal/winredirect/driver/winredirect.h +++ b/internal/winredirect/driver/winredirect.h @@ -24,8 +24,7 @@ // Verdict values #define VERDICT_REDIRECT 0 -#define VERDICT_BYPASS 1 -#define VERDICT_DROP 2 +#define VERDICT_PERMIT 1 // Shared structures - must match Go types_windows.go layout diff --git a/internal/winredirect/types_windows.go b/internal/winredirect/types_windows.go index 832ff5b6..0dda8c0d 100644 --- a/internal/winredirect/types_windows.go +++ b/internal/winredirect/types_windows.go @@ -13,8 +13,9 @@ const ( const ( VerdictRedirect = 0 - VerdictBypass = 1 - VerdictDrop = 2 + // VerdictPermit allows the original TUN-bound connect to continue + // without local redirection. + VerdictPermit = 1 ) // Config is sent to the driver via IOCTL_SET_CONFIG. diff --git a/redirect_windows.go b/redirect_windows.go index 157c9f8f..849449a8 100644 --- a/redirect_windows.go +++ b/redirect_windows.go @@ -235,11 +235,11 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 // Proxy process outbound connections must never be redirected back into itself. if conn.ProcessID == r.selfPID { - return winredirect.VerdictBypass + return winredirect.VerdictPermit } if dst.Addr.IsLoopback() { - return winredirect.VerdictBypass + return winredirect.VerdictPermit } if !r.tunOptions.EXP_DisableDNSHijack && dst.Port == 53 { @@ -250,7 +250,7 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 ctx := r.newConnContext(metadata) _, err := r.handler.PrepareConnection(ctx, "tcp", src, dst, nil, 0) if errors.Is(err, ErrDrop) { - return winredirect.VerdictDrop + return winredirect.VerdictPermit } r.redirectServer.connTable.StoreDNS(src, dst, M.SocksaddrFrom(dnsServer, 53), ctx) return winredirect.VerdictRedirect @@ -259,7 +259,7 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 } if r.tunOptions.StrictRoute && r.isDisabledFamily(dst.Addr) { - return winredirect.VerdictDrop + return winredirect.VerdictPermit } metadata := r.resolveMetadata(conn) @@ -267,14 +267,14 @@ func (r *autoRedirect) evaluateConnection(conn *winredirect.PendingConn) uint32 _, err := r.handler.PrepareConnection(ctx, N.NetworkTCP, src, dst, nil, 0) if errors.Is(err, ErrDrop) { - return winredirect.VerdictDrop + return winredirect.VerdictPermit } if errors.Is(err, ErrReset) { // Pending entries reaching userspace here have already been identified as - // TUN-bound by the driver. Bypass means "do not locally redirect to the + // TUN-bound by the driver. Permit means "do not locally redirect to the // Windows redirect listener"; the original connect continues into the TUN, // where reset semantics are enforced by the TUN stack. - return winredirect.VerdictBypass + return winredirect.VerdictPermit } if errors.Is(err, ErrBypass) && r.logger != nil { r.logger.Debug("bypass not supported on Windows, redirecting: ", src, " -> ", dst) From 3d4d916f03742923b122871c64c3381ef6912b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Mar 2026 19:49:31 +0800 Subject: [PATCH 38/38] winredirect: Add CI build --- .github/docker/msvc-wine.dockerfile | 83 +++++ .github/workflows/winredirect-driver-sync.yml | 320 ++++++------------ internal/winredirect/amd64/winredirect.sys | Bin 21840 -> 18944 bytes internal/winredirect/arm64/winredirect.sys | Bin 24952 -> 17920 bytes internal/winredirect/x86/winredirect.sys | Bin 11 -> 15872 bytes 5 files changed, 188 insertions(+), 215 deletions(-) create mode 100644 .github/docker/msvc-wine.dockerfile diff --git a/.github/docker/msvc-wine.dockerfile b/.github/docker/msvc-wine.dockerfile new file mode 100644 index 00000000..56d8a42d --- /dev/null +++ b/.github/docker/msvc-wine.dockerfile @@ -0,0 +1,83 @@ +# Cross-compiles Windows kernel drivers using msvc-wine. +# Toolchain: MSVC 17.13 + SDK/WDK 10.0.22621 +# Architectures: x86, x64, ARM, ARM64 +FROM alpine:3.21 AS msvc-wine +RUN apk add --no-cache git +RUN git clone https://github.com/mcha-forks/msvc-wine.git /msvc-wine \ + && cd /msvc-wine && git checkout 2d5f5a3 + +FROM registry.fedoraproject.org/fedora-minimal:44 AS wine + +COPY <<-'EOF' /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:mochaa:wine.repo + [copr:copr.fedorainfracloud.org:mochaa:wine] + name=Copr repo for wine owned by mochaa + baseurl=https://download.copr.fedorainfracloud.org/results/mochaa/wine/fedora-$releasever-$basearch/ + type=rpm-md + skip_if_unavailable=True + gpgcheck=1 + gpgkey=https://download.copr.fedorainfracloud.org/results/mochaa/wine/pubkey.gpg + repo_gpgcheck=0 + enabled=1 + enabled_metadata=1 + + [copr:copr.fedorainfracloud.org:mochaa:wine:ml] + name=Copr repo for wine owned by mochaa (i386) + baseurl=https://download.copr.fedorainfracloud.org/results/mochaa/wine/fedora-$releasever-i386/ + type=rpm-md + skip_if_unavailable=True + gpgcheck=1 + gpgkey=https://download.copr.fedorainfracloud.org/results/mochaa/wine/pubkey.gpg + repo_gpgcheck=0 + cost=1100 + enabled=1 + enabled_metadata=1 +EOF + +RUN <<-EOF + set -xeu + microdnf install -y wine-core wine-core.i686 wine-mono + microdnf clean all +EOF + +RUN <<-EOF + set -xeu + wine wineboot -u + wine reg.exe add HKCU\\Software\\Wine\\Drivers /v Graphics /t REG_SZ /d null + wineserver -w +EOF + +WORKDIR /builddir + +FROM wine AS fetch-wdk +COPY --from=msvc-wine /msvc-wine/wdk-download.sh ./ +RUN WINEDEBUG=1 bash -x ./wdk-download.sh --cache wdk https://go.microsoft.com/fwlink/?linkid=2330411 + +FROM python:3.14-slim AS fetch-msvc +WORKDIR /builddir +COPY --from=msvc-wine /msvc-wine/vsdownload.py ./ +RUN PYTHONUNBUFFERED=1 ./vsdownload.py --accept-license --only-download --cache cache \ + --major=17 --msvc-version=17.13 --sdk-version=10.0.22621 --with-wdk-installer wdk/Installers \ + Microsoft.Component.MSBuild + +FROM wine AS builder +RUN <<-EOF + microdnf install -y git msitools perl + microdnf clean all +EOF +COPY --from=fetch-msvc /builddir/cache/ ./cache/ +COPY --from=fetch-wdk /builddir/wdk/Installers/ ./wdk/Installers/ +COPY --from=msvc-wine /msvc-wine/vsdownload.py ./ +COPY --from=msvc-wine /msvc-wine/patches/ ./patches/ +RUN PYTHONUNBUFFERED=1 python3 ./vsdownload.py --accept-license --cache cache --dest /opt/msvc \ + --major=17 --msvc-version=17.13 --sdk-version=10.0.22621 --with-wdk-installer wdk/Installers \ + Microsoft.Component.MSBuild +COPY --from=msvc-wine /msvc-wine/lowercase /msvc-wine/fixinclude /msvc-wine/install.sh /msvc-wine/msvctricks.cpp ./ +COPY --from=msvc-wine /msvc-wine/wrappers/ ./wrappers/ +RUN bash -x ./install.sh /opt/msvc +# WDK 22621 MSBuild targets reject Win32/ARM for km drivers (removed in Windows 11). +# The km libraries for these arches are present; only the validation blocks them. +RUN find -L /opt/msvc -iname 'windowsdriver.common.targets' \ + -exec sed -i '/not a valid architecture for Kernel mode/s/&1 | head -2 || true + echo "SDK/WDK include versions:" + docker run --rm msvc-wine:local ls /opt/msvc/kits/10/include/ + echo "WDK km lib architectures:" + docker run --rm msvc-wine:local sh -c 'ls /opt/msvc/kits/10/lib/*/km/ 2>/dev/null' || true + echo "WindowsDriver.common.targets (arch validation):" + docker run --rm msvc-wine:local sh -c 'find /opt/msvc -name "WindowsDriver.common.targets" -exec grep -n -i "valid architecture\|_SUPPORTED" {} +' 2>/dev/null || true - name: Build, verify reproducibility, and refresh tracked drivers id: sync - shell: pwsh - working-directory: ${{ env.REPO_DIR }} run: | - git config --global --add safe.directory $env:REPO_DIR - - $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' - $msbuild = & $vswhere -latest -products * -requires Microsoft.Component.MSBuild -find 'MSBuild\**\Bin\MSBuild.exe' | Select-Object -First 1 - if (-not $msbuild) { - throw 'MSBuild.exe not found' - } - - $env:CL = '/Brepro' - $env:LINK = '/Brepro' - $artifactDir = Join-Path $env:RUNNER_TEMP 'winredirect-driver-sync' - $failureDir = Join-Path $artifactDir 'failure' - Remove-Item -LiteralPath $artifactDir -Recurse -Force -ErrorAction SilentlyContinue - New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null - - function Invoke-DriverBuild([string] $platform, [string] $buildTag) { - Remove-Item -LiteralPath "internal/winredirect/driver/build/$platform" -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -LiteralPath "internal/winredirect/driver/intermediate/$platform" -Recurse -Force -ErrorAction SilentlyContinue - if ($platform -eq 'ARM64') { - $diagDir = Join-Path $artifactDir 'arm64-diag' - New-Item -ItemType Directory -Path $diagDir -Force | Out-Null - $diagLog = Join-Path $diagDir "$buildTag.log" - $diagBinlog = Join-Path $diagDir "$buildTag.binlog" - & $msbuild 'internal/winredirect/driver/winredirect.vcxproj' '/t:Rebuild' '/p:Configuration=Release' "/p:Platform=$platform" '/p:SpectreMitigation=false' '/v:diag' "/bl:$diagBinlog" 2>&1 | Tee-Object -FilePath $diagLog - } else { - & $msbuild 'internal/winredirect/driver/winredirect.vcxproj' '/t:Rebuild' '/p:Configuration=Release' "/p:Platform=$platform" '/p:SpectreMitigation=false' '/v:minimal' - } - if ($LASTEXITCODE -ne 0) { - throw "MSBuild failed for $platform with exit code $LASTEXITCODE" - } - } - - $targets = @( - @{ platform = 'x64'; repo = 'internal/winredirect/amd64/winredirect.sys' }, - @{ platform = 'ARM64'; repo = 'internal/winredirect/arm64/winredirect.sys' } + set -euo pipefail + + arches=(x64 arm64 x86) + platforms=(x64 ARM64 Win32) + tracked=( + internal/winredirect/amd64/winredirect.sys + internal/winredirect/arm64/winredirect.sys + internal/winredirect/x86/winredirect.sys ) - Write-Host 'ARM and x86 are intentionally skipped here: WDK 10.0.26100 on GitHub-hosted CI does not support them.' - - $changed = $false - foreach ($target in $targets) { - $platform = $target.platform - $tracked = $target.repo - $buildOutput = "internal/winredirect/driver/build/$platform/Release/winredirect.sys" - $run1 = Join-Path $artifactDir "$platform-run1.sys" - $run2 = Join-Path $artifactDir "$platform-run2.sys" - - Invoke-DriverBuild $platform "$platform-run1" - Copy-Item -LiteralPath $buildOutput -Destination $run1 -Force - - Invoke-DriverBuild $platform "$platform-run2" - Copy-Item -LiteralPath $buildOutput -Destination $run2 -Force - - & python '.github/scripts/compare_signed_pe.py' $run1 $run2 - switch ($LASTEXITCODE) { - 0 { - Write-Host "$platform reproduced after stripping Authenticode data." - } - 1 { - New-Item -ItemType Directory -Path $failureDir -Force | Out-Null - Copy-Item -LiteralPath $run1 -Destination (Join-Path $failureDir "$platform-run1.sys") -Force - Copy-Item -LiteralPath $run2 -Destination (Join-Path $failureDir "$platform-run2.sys") -Force - throw "$platform build is not reproducible beyond signing metadata." - } - default { - throw "reproducibility comparison failed for $platform with exit code $LASTEXITCODE" - } - } - - & python '.github/scripts/compare_signed_pe.py' $run2 $tracked - switch ($LASTEXITCODE) { - 0 { - Write-Host "$platform matches the tracked driver after stripping Authenticode data." - } - 1 { - Write-Host "$platform differs beyond signing metadata; replacing tracked driver." - Copy-Item -LiteralPath $run2 -Destination $tracked -Force - git add -- $tracked - $changed = $true - } - default { - throw "comparison failed for $platform with exit code $LASTEXITCODE" - } - } - } - - if ($changed) { - "changed=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - } else { - "changed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - } + changed=false + artifact_dir="${RUNNER_TEMP}/winredirect-driver-sync" + failure_dir="${artifact_dir}/failure" + mkdir -p "$artifact_dir" + + build_driver() { + docker run --rm \ + -v "$PWD:/src" -w /src \ + -e CL=/Brepro -e LINK=/Brepro \ + msvc-wine:local \ + /opt/msvc/bin/"$1"/msbuild \ + internal/winredirect/driver/winredirect.vcxproj \ + /t:Rebuild /p:Configuration=Release /p:SpectreMitigation=false /p:TrackFileAccess=false /v:minimal + } + + for i in "${!arches[@]}"; do + arch="${arches[$i]}" + platform="${platforms[$i]}" + tracked_file="${tracked[$i]}" + build_output="internal/winredirect/driver/build/${platform}/Release/winredirect.sys" + run1="${artifact_dir}/${arch}-run1.sys" + run2="${artifact_dir}/${arch}-run2.sys" + + echo "::group::Build ${arch} run 1" + build_driver "$arch" + cp "$build_output" "$run1" + echo "::endgroup::" + + echo "::group::Build ${arch} run 2" + build_driver "$arch" + cp "$build_output" "$run2" + echo "::endgroup::" + + echo "::group::Verify ${arch} reproducibility" + rc=0 + python3 .github/scripts/compare_signed_pe.py "$run1" "$run2" || rc=$? + case $rc in + 0) + echo "${arch} reproduced after stripping Authenticode data." + ;; + 1) + mkdir -p "$failure_dir" + cp "$run1" "${failure_dir}/${arch}-run1.sys" + cp "$run2" "${failure_dir}/${arch}-run2.sys" + echo "::error::${arch} build is not reproducible beyond signing metadata." + exit 1 + ;; + *) + echo "::error::Reproducibility comparison failed for ${arch} with exit code ${rc}" + exit "$rc" + ;; + esac + echo "::endgroup::" + + echo "::group::Compare ${arch} with tracked driver" + rc=0 + python3 .github/scripts/compare_signed_pe.py "$run2" "$tracked_file" || rc=$? + case $rc in + 0) + echo "${arch} matches the tracked driver after stripping Authenticode data." + ;; + 1|2) + echo "${arch} differs beyond signing metadata (rc=${rc}); replacing tracked driver." + cp "$run2" "$tracked_file" + git add -- "$tracked_file" + changed=true + ;; + *) + echo "::error::Comparison failed for ${arch} with exit code ${rc}" + exit "$rc" + ;; + esac + echo "::endgroup::" + done + + echo "changed=${changed}" >> "$GITHUB_OUTPUT" - name: Upload failed reproducibility artifacts if: failure() uses: actions/upload-artifact@v4 with: name: winredirect-repro-failure-${{ github.run_id }}-${{ github.run_attempt }} - path: | - ${{ runner.temp }}\winredirect-driver-sync\failure\*.sys - ${{ runner.temp }}\winredirect-driver-sync\arm64-diag\*.log - ${{ runner.temp }}\winredirect-driver-sync\arm64-diag\*.binlog + path: ${{ runner.temp }}/winredirect-driver-sync/failure/*.sys if-no-files-found: warn - name: Commit and push refreshed drivers if: steps.sync.outputs.changed == 'true' - shell: pwsh - working-directory: ${{ env.REPO_DIR }} run: | git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - - git diff --cached --quiet - if ($LASTEXITCODE -eq 0) { - exit 0 - } - + git diff --cached --quiet && exit 0 git commit -m 'winredirect: update bundled drivers' git push origin "HEAD:${{ github.event.pull_request.head.ref }}" diff --git a/internal/winredirect/amd64/winredirect.sys b/internal/winredirect/amd64/winredirect.sys index ac2ee63f45864e80717488b88c45569316df7eac..783958083e12a86ad8830437af20ec78b95ad98a 100644 GIT binary patch delta 8311 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 diff --git a/internal/winredirect/arm64/winredirect.sys b/internal/winredirect/arm64/winredirect.sys index 0476ce679a51ffa3fdba481283339bd4334ded22..f930e22b40491fcb21d20fa1c910f7fdfa4d7062 100644 GIT binary patch literal 17920 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 J=>O-L|8EJIg}nd( literal 24952 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 S)Q-R_$6wiO#s8C&uYU(Rd{~G8 diff --git a/internal/winredirect/x86/winredirect.sys b/internal/winredirect/x86/winredirect.sys index 311c8dd0658759f372c9be9a2065c327fdb1a203..a866793a2efc12e8b96b121dfedd34244b8e2d50 100644 GIT binary patch literal 15872 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 literal 11 ScmWIWaddX|@b__X4FUiX$^#?-