diff --git a/Windows/README.md b/Windows/README.md index 4155b30..3544224 100644 --- a/Windows/README.md +++ b/Windows/README.md @@ -39,10 +39,14 @@ Universal proxy client for Windows applications - route any application through 2. Run the installer with Administrator privileges 3. The installer will: - Install ProxyBridge to `C:\Program Files\ProxyBridge` + - Install and start the `ProxyBridgeSvc` Windows service as `LocalSystem` + - Authorize the installing user to control the engine through the service - Add the install directory to your system PATH (GUI, CLI, and DLL all accessible) - Create Start Menu shortcuts for the GUI application - Include all required dependencies (WinDivert driver) +The installer needs Administrator privileges to register the service and WinDivert driver. After installation, the GUI can be launched normally without elevation; it controls the engine over the service's authenticated named pipes. + @@ -50,7 +54,7 @@ Universal proxy client for Windows applications - route any application through ### GUI Application -Launch `ProxyBridge.exe` with Administrator privileges for an intuitive graphical interface. +Launch `ProxyBridge.exe` normally for the graphical interface. The GUI uses `ProxyBridgeSvc` for packet capture, so it does not need Administrator privileges after installation. The standalone CLI and portable fallback still need elevation to start WinDivert directly. #### Proxy Settings @@ -401,7 +405,7 @@ Case 2: Packet match with proxy rule ### Requirements - Windows 7 or later (64-bit) -- Administrator privileges (required for WinDivert driver) +- Administrator privileges for the one-time service/WinDivert installation; direct engine or CLI runs also need elevation - Visual Studio 2019 or later (MSVC - recommended) **or** MinGW-w64 (GCC) - WinDivert 2.2.2-A or later @@ -425,9 +429,11 @@ The script automatically: # From a Visual Studio Developer Command Prompt cl /O2 /GL /W4 /D_WIN32_WINNT=0x0601 /DNDEBUG /GS /guard:cf ^ /I"path\to\windivert\include" ^ - ProxyBridge.c /LD ^ + src\ProxyBridge.c src\pb_util.c src\pb_process.c src\pb_rules.c src\pb_proxy.c ^ + src\pb_dns.c src\pb_dns_etw.c src\pb_socks5.c src\pb_http.c src\pb_conntrack.c ^ + src\pb_relay.c src\pb_sockmap.c src\pb_sni.c /LD ^ /link /LTCG /OPT:REF /RELEASE /DYNAMICBASE /NXCOMPAT ^ - WinDivert.lib ws2_32.lib iphlpapi.lib ^ + WinDivert.lib ws2_32.lib iphlpapi.lib tdh.lib shell32.lib advapi32.lib ^ /OUT:ProxyBridgeCore.dll ``` @@ -436,8 +442,10 @@ cl /O2 /GL /W4 /D_WIN32_WINNT=0x0601 /DNDEBUG /GS /guard:cf ^ ```powershell gcc -O2 -Wall -D_WIN32_WINNT=0x0601 -shared ^ -I"C:\WinDivert-2.2.2-A\include" ^ - ProxyBridge.c ^ - -L"C:\WinDivert-2.2.2-A\x64" -lWinDivert -lws2_32 -liphlpapi ^ + src\ProxyBridge.c src\pb_util.c src\pb_process.c src\pb_rules.c src\pb_proxy.c ^ + src\pb_dns.c src\pb_dns_etw.c src\pb_socks5.c src\pb_http.c src\pb_conntrack.c ^ + src\pb_relay.c src\pb_sockmap.c src\pb_sni.c ^ + -L"C:\WinDivert-2.2.2-A\x64" -lWinDivert -lws2_32 -liphlpapi -ltdh -lshell32 -ladvapi32 ^ -o ProxyBridgeCore.dll ``` diff --git a/Windows/compile.ps1 b/Windows/compile.ps1 index f7b6197..0d7d9ca 100644 --- a/Windows/compile.ps1 +++ b/Windows/compile.ps1 @@ -10,7 +10,7 @@ param( $WinDivertPath = "C:\WinDivert-2.2.2-A" $SourcePath = "src" # Core split across modular translation units (see src\pb_internal.h). -$SourceFile = "ProxyBridge.c pb_util.c pb_process.c pb_rules.c pb_proxy.c pb_dns.c pb_socks5.c pb_http.c pb_conntrack.c pb_relay.c pb_sockmap.c" +$SourceFile = "ProxyBridge.c pb_util.c pb_process.c pb_rules.c pb_proxy.c pb_dns.c pb_dns_etw.c pb_socks5.c pb_http.c pb_conntrack.c pb_relay.c pb_sockmap.c pb_sni.c" $SourceFiles = ($SourceFile.Split(' ') | ForEach-Object { "$SourcePath\$_" }) -join ' ' $OutputDLL = "ProxyBridgeCore.dll" $OutputDir = "output" @@ -69,7 +69,7 @@ function Compile-MSVC { "/LD " + "/link /LTCG /OPT:REF /OPT:ICF /RELEASE /DYNAMICBASE /NXCOMPAT " + "/LIBPATH:`"$WinDivertPath\$Arch`" " + - "WinDivert.lib ws2_32.lib iphlpapi.lib " + + "WinDivert.lib ws2_32.lib iphlpapi.lib tdh.lib shell32.lib advapi32.lib " + "/OUT:$OutputDLL" $cmd = "`"$vcvarsPath`" $Arch >nul && cl.exe $clArgs" @@ -99,7 +99,7 @@ function Compile-GCC { "-I`"$WinDivertPath\include`" " + "$SourcePath\$SourceFile " + "-L`"$WinDivertPath\$Arch`" " + - "-lWinDivert -lws2_32 -liphlpapi " + + "-lWinDivert -lws2_32 -liphlpapi -ltdh -lshell32 -ladvapi32 " + "-o $OutputDLL" Write-Host "Command: $cmd" -ForegroundColor Gray diff --git a/Windows/gui/api/pb_api.h b/Windows/gui/api/pb_api.h index 828c0d6..bbfefc0 100644 --- a/Windows/gui/api/pb_api.h +++ b/Windows/gui/api/pb_api.h @@ -18,6 +18,9 @@ typedef void (*PBLogCallback)(const char* message); typedef void (*PBConnectionCallback)(const char* process_name, DWORD pid, const char* dest_ip, unsigned short dest_port, const char* proxy_info); +typedef void (*PBConnectionCallbackEx)(const char* process_name, DWORD pid, + const char* dest_ip, const char* hostname, + unsigned short dest_port, const char* proxy_info); // exported function pointer typedefs typedef UINT32 (*PFN_AddProxyConfig)(PBProxyType, const char*, unsigned short, const char*, const char*, BOOL); @@ -36,6 +39,7 @@ typedef UINT32 (*PFN_GetRulePosition)(UINT32); typedef void (*PFN_SetLocalhostViaProxy)(BOOL); typedef void (*PFN_SetLogCallback)(PBLogCallback); typedef void (*PFN_SetConnectionCallback)(PBConnectionCallback); +typedef void (*PFN_SetConnectionCallbackEx)(PBConnectionCallbackEx); typedef void (*PFN_SetTrafficLoggingEnabled)(BOOL); typedef void (*PFN_ClearConnectionLogs)(void); typedef BOOL (*PFN_Start)(void); @@ -59,6 +63,7 @@ typedef struct { PFN_SetLocalhostViaProxy SetLocalhostViaProxy; PFN_SetLogCallback SetLogCallback; PFN_SetConnectionCallback SetConnectionCallback; + PFN_SetConnectionCallbackEx SetConnectionCallbackEx; PFN_SetTrafficLoggingEnabled SetTrafficLoggingEnabled; PFN_ClearConnectionLogs ClearConnectionLogs; PFN_Start Start; @@ -105,6 +110,7 @@ static BOOL PB_LoadDirect(PBApi* api) PB_BIND(SetLocalhostViaProxy, "SetLocalhostViaProxy"); PB_BIND(SetLogCallback, "SetLogCallback"); PB_BIND(SetConnectionCallback, "SetConnectionCallback"); + api->SetConnectionCallbackEx = (PFN_SetConnectionCallbackEx)(void*)GetProcAddress(api->dll, "ProxyBridge_SetConnectionCallbackEx"); PB_BIND(SetTrafficLoggingEnabled, "SetTrafficLoggingEnabled"); PB_BIND(ClearConnectionLogs, "ClearConnectionLogs"); PB_BIND(Start, "Start"); diff --git a/Windows/gui/api/pb_ipc.h b/Windows/gui/api/pb_ipc.h index 837bf9c..899a31f 100644 --- a/Windows/gui/api/pb_ipc.h +++ b/Windows/gui/api/pb_ipc.h @@ -25,6 +25,8 @@ static HANDLE g_ipcTestDone = NULL; static volatile LONG g_ipcTestResult = -1; static PBLogCallback g_ipcLogCb = NULL; static PBConnectionCallback g_ipcConnCb = NULL; +static PBConnectionCallbackEx g_ipcConnCbEx = NULL; +static DWORD g_ipcProtoVersion = 1; static PBTestLogCallback g_ipcTestCb = NULL; static void* g_ipcTestUser = NULL; @@ -39,7 +41,7 @@ static DWORD WINAPI PbIpcReader(LPVOID arg) if (!ReadFile(g_ipcEvt, buf, sizeof(buf), &n, NULL)) break; PbR r; PbR_Init(&r, buf, n); UINT32 type = PbR_U32(&r); - char a[8192], b[8192], c[8192]; + char a[8192], b[8192], c[8192], d[8192] = {0}; if (type == PBEVT_LOG) { PbR_Str(&r, a, sizeof(a), NULL); @@ -50,7 +52,12 @@ static DWORD WINAPI PbIpcReader(LPVOID arg) PbR_Str(&r, a, sizeof(a), NULL); UINT32 pid = PbR_U32(&r); PbR_Str(&r, b, sizeof(b), NULL); UINT32 port = PbR_U32(&r); PbR_Str(&r, c, sizeof(c), NULL); - if (r.ok && g_ipcConnCb) g_ipcConnCb(a, pid, b, (unsigned short)port, c); + if (r.ok && g_ipcProtoVersion >= 2 && r.pos < r.len) + PbR_Str(&r, d, sizeof(d), NULL); + if (r.ok && g_ipcConnCbEx) + g_ipcConnCbEx(a, pid, b, d, (unsigned short)port, c); + else if (r.ok && g_ipcConnCb) + g_ipcConnCb(a, pid, b, (unsigned short)port, c); } else if (type == PBEVT_TEST_LINE) { @@ -131,6 +138,7 @@ static void IpcSetTrafficLog(BOOL v) { PBIPC_BEGIN(PBOP_SET_TRAFFIC_LOG); PbW_U static void IpcClearConnLogs(void) { PBIPC_BEGIN(PBOP_CLEAR_CONN_LOGS); PbIpcCall(_b, _w.len); } static void IpcSetLogCallback(PBLogCallback cb) { g_ipcLogCb = cb; } static void IpcSetConnectionCallback(PBConnectionCallback cb) { g_ipcConnCb = cb; } +static void IpcSetConnectionCallbackEx(PBConnectionCallbackEx cb) { g_ipcConnCbEx = cb; } static BOOL IpcStart(void) { PBIPC_BEGIN(PBOP_START); PBIPC_END(); } static BOOL IpcStop(void) { PBIPC_BEGIN(PBOP_STOP); PBIPC_END(); } @@ -199,13 +207,15 @@ static int PB_LoadService(PBApi* api) g_ipcCtl = ctl; g_ipcEvt = evt; { - PBIPC_BEGIN(PBOP_HELLO); - if (PbIpcCall(_b, _w.len) != PB_PROTO_VERSION) + PBIPC_BEGIN(PBOP_HELLO); PbW_U32(&_w, PB_PROTO_VERSION); + UINT32 selected = PbIpcCall(_b, _w.len); + if (selected == 0 || selected > PB_PROTO_VERSION) { CloseHandle(g_ipcCtl); g_ipcCtl = INVALID_HANDLE_VALUE; CloseHandle(g_ipcEvt); g_ipcEvt = INVALID_HANDLE_VALUE; return PB_IPC_BAD_VERSION; } + g_ipcProtoVersion = selected; } g_ipcReader = CreateThread(NULL, 0, PbIpcReader, NULL, 0, NULL); @@ -218,6 +228,7 @@ static int PB_LoadService(PBApi* api) api->MoveRuleToPosition = IpcMoveRule; api->GetRulePosition = IpcGetRulePos; api->SetLocalhostViaProxy = IpcSetLocalhost; api->SetLogCallback = IpcSetLogCallback; api->SetConnectionCallback = IpcSetConnectionCallback; + api->SetConnectionCallbackEx = IpcSetConnectionCallbackEx; api->SetTrafficLoggingEnabled = IpcSetTrafficLog; api->ClearConnectionLogs = IpcClearConnLogs; api->Start = IpcStart; api->Stop = IpcStop; return PB_IPC_OK; diff --git a/Windows/gui/main.c b/Windows/gui/main.c index d38d29c..3976d6c 100644 --- a/Windows/gui/main.c +++ b/Windows/gui/main.c @@ -194,32 +194,49 @@ static void PBLogCb(const char* message) free(body); } -static void PBConnCb(const char* proc, DWORD pid, const char* ip, unsigned short port, const char* info) +static void PBConnCbEx(const char* proc, DWORD pid, const char* ip, const char* hostname, + unsigned short port, const char* info) { if (!g_hMain || !g_trafficLog) return; - wchar_t *wp = U2Wdup(proc), *wi = U2Wdup(ip), *wf = U2Wdup(info); + const char *display = hostname && hostname[0] ? hostname : ip; + wchar_t *wp = U2Wdup(proc), *wi = U2Wdup(display), *wf = U2Wdup(info); // Connection-log filters: derive protocol/action from the proxy_info and drop non-matches. if (g_fltCount > 0) { wchar_t wport[16]; _snwprintf_s(wport, 16, _TRUNCATE, L"%u", port); + wchar_t destination[640]; + wchar_t *wip = U2Wdup(ip); + _snwprintf_s(destination, 640, _TRUNCATE, L"%s %s", + wi ? wi : L"", wip ? wip : L""); const wchar_t* proto = (info && strstr(info, "(UDP)")) ? L"UDP" : L"TCP"; const wchar_t* action = (info && _strnicmp(info, "Direct", 6) == 0) ? L"Direct" : (info && _strnicmp(info, "Block", 5) == 0) ? L"Blocked" : L"Proxy"; - if (!PassesLogFilters(wp ? wp : L"", wi ? wi : L"", wport, proto, action)) - { free(wp); free(wi); free(wf); return; } + BOOL pass = PassesLogFilters(wp ? wp : L"", destination, wport, proto, action); + free(wip); + if (!pass) { free(wp); free(wi); free(wf); return; } } wchar_t ts[16]; GetTimePrefix(ts, 16); size_t n = 64 + (wp ? wcslen(wp) : 0) + (wi ? wcslen(wi) : 0) + (wf ? wcslen(wf) : 0); wchar_t* line = (wchar_t*)malloc(n * sizeof(wchar_t)); if (line) { - _snwprintf_s(line, n, _TRUNCATE, L"%s%s (PID:%lu) -> %s:%u via %s\r\n", - ts, wp ? wp : L"?", pid, wi ? wi : L"?", port, wf ? wf : L"?"); + if (display && strchr(display, ':')) + _snwprintf_s(line, n, _TRUNCATE, L"%s%s (PID:%lu) -> [%s]:%u via %s\r\n", + ts, wp ? wp : L"?", pid, wi ? wi : L"?", port, wf ? wf : L"?"); + else + _snwprintf_s(line, n, _TRUNCATE, L"%s%s (PID:%lu) -> %s:%u via %s\r\n", + ts, wp ? wp : L"?", pid, wi ? wi : L"?", port, wf ? wf : L"?"); LogStoreQueue(&g_connStore, line); } free(wp); free(wi); free(wf); } +static void PBConnCb(const char* proc, DWORD pid, const char* ip, + unsigned short port, const char* info) +{ + PBConnCbEx(proc, pid, ip, NULL, port, info); +} + // profile apply / persist static void SaveActive(void) { PB_ProfileSave(g_activeProfile, &g_profile); } @@ -1171,7 +1188,10 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmd, int show) g_api.SetLocalhostViaProxy(g_localhost); g_api.SetTrafficLoggingEnabled(g_trafficLog); g_api.SetLogCallback(PBLogCb); - g_api.SetConnectionCallback(PBConnCb); + if (g_api.SetConnectionCallbackEx) + g_api.SetConnectionCallbackEx(PBConnCbEx); + else + g_api.SetConnectionCallback(PBConnCb); ApplyConfigs(); g_started = g_api.Start(); if (g_started) { ApplyRules(); LogStoreAdd(&g_actStore, T(S_STARTED)); } diff --git a/Windows/gui/res/app.rc b/Windows/gui/res/app.rc index 7ea904c..c408e90 100644 --- a/Windows/gui/res/app.rc +++ b/Windows/gui/res/app.rc @@ -4,7 +4,7 @@ // Application icon (reuse the shared logo) IDI_APPICON ICON "logo.ico" -// Embedded manifest (admin + Common Controls v6 + DPI) +// Embedded manifest (asInvoker + Common Controls v6 + DPI) 1 24 "app.manifest" // Version info (exe Properties → Details); numbers come from resource.h diff --git a/Windows/src/ProxyBridge.c b/Windows/src/ProxyBridge.c index 48e8bc9..775a226 100644 --- a/Windows/src/ProxyBridge.c +++ b/Windows/src/ProxyBridge.c @@ -31,9 +31,6 @@ HANDLE udp_relay_thread = NULL; HANDLE cleanup_thread = NULL; PID_CACHE_ENTRY *pid_cache[PID_CACHE_SIZE] = {NULL}; volatile BOOL g_has_active_rules = FALSE; -// Set when at least one enabled rule carries a domain filter. Gates the DNS-cache -// lookup in match_rule so setups without domain rules pay zero extra cost. -volatile BOOL g_has_domain_rules = FALSE; SOCKET udp_relay_socket = INVALID_SOCKET; SOCKET udp_relay_socket6 = INVALID_SOCKET; volatile BOOL running = FALSE; @@ -41,10 +38,6 @@ DWORD g_current_process_id = 0; BOOL g_traffic_logging_enabled = TRUE; -DNS_CACHE_ENTRY *g_dns_cache[DNS_CACHE_BUCKETS]; -DNS_CACHE_ENTRY_V6 *g_dns_cache_v6[DNS_CACHE_BUCKETS]; -SRWLOCK g_dns_cache_lock; - // per src port decision cache. // // check_process_rule() resolves (src_port) to DIRECT, PROXY, or BLOCK, @@ -74,6 +67,29 @@ UINT16 g_local_relay_port = LOCAL_PROXY_PORT; BOOL g_localhost_via_proxy = FALSE; // default disabled for security - most proxy server block localhost for ssrf and also many app might not work if localhost trafic goes to remote server if proxy server is on diffrent machine LogCallback g_log_callback = NULL; ConnectionCallback g_connection_callback = NULL; +ConnectionCallbackEx g_connection_callback_ex = NULL; + +static BOOL connection_callback_active(void) +{ + return g_connection_callback != NULL || g_connection_callback_ex != NULL; +} + +static void report_connection(const char *process_name, DWORD pid, const char *ip, + const char *hostname, UINT16 port, const char *info) +{ + if (g_connection_callback_ex) + g_connection_callback_ex(process_name, pid, ip, hostname, port, info); + else if (g_connection_callback) + g_connection_callback(process_name, pid, ip, port, info); +} + +// Same line, callable from the relay threads. The relay is the only place that knows the +// destination name for a connection the DNS cache could not name at SYN time. +void pb_report_connection(const char *process_name, DWORD pid, const char *ip, + const char *hostname, UINT16 port, const char *info) +{ + report_connection(process_name, pid, ip, hostname, port, info); +} char *g_pidtbl_buf = NULL; DWORD g_pidtbl_cap = 0; @@ -157,6 +173,37 @@ static BOOL pb_divert_send(HANDLE h, PVOID pkt, UINT len, const WINDIVERT_ADDRES } #define WinDivertSend(h, p, l, s, a) pb_divert_send((h), (p), (l), (a)) +static void observe_tcp_dns_packet(BYTE *packet, UINT packet_len, + PWINDIVERT_IPHDR ip_header, PWINDIVERT_IPV6HDR ipv6_header, + PWINDIVERT_TCPHDR tcp_header, ULONGLONG packet_qpc) +{ + if (!tcp_header || + (ntohs(tcp_header->SrcPort) != 53 && ntohs(tcp_header->DstPort) != 53)) + return; + + size_t tcp_header_len = (size_t)tcp_header->HdrLength * 4; + size_t payload_offset = (size_t)((BYTE *)tcp_header - packet) + tcp_header_len; + if (tcp_header_len < sizeof(WINDIVERT_TCPHDR) || payload_offset > packet_len) + return; + + UINT8 tcp_flags = (tcp_header->Fin ? 0x01 : 0) | + (tcp_header->Syn ? 0x02 : 0) | + (tcp_header->Ack ? 0x10 : 0) | + (tcp_header->Rst ? 0x04 : 0); + const UINT8 *payload = packet + payload_offset; + int payload_len = (int)(packet_len - payload_offset); + if (ip_header) + dns_network_observe_tcp(FALSE, + (const UINT8 *)&ip_header->SrcAddr, (const UINT8 *)&ip_header->DstAddr, + ntohs(tcp_header->SrcPort), ntohs(tcp_header->DstPort), + ntohl(tcp_header->SeqNum), tcp_flags, payload, payload_len, packet_qpc); + else if (ipv6_header) + dns_network_observe_tcp(TRUE, + (const UINT8 *)ipv6_header->SrcAddr, (const UINT8 *)ipv6_header->DstAddr, + ntohs(tcp_header->SrcPort), ntohs(tcp_header->DstPort), + ntohl(tcp_header->SeqNum), tcp_flags, payload, payload_len, packet_qpc); +} + DWORD WINAPI packet_processor(LPVOID arg) { unsigned char packet[MAXBUF]; @@ -195,6 +242,29 @@ DWORD WINAPI packet_processor(LPVOID arg) WinDivertHelperParsePacket(packet, packet_len, &ip_header, &ipv6_header, NULL, NULL, NULL, &tcp_header, &udp_header, NULL, NULL, NULL, NULL); + if (udp_header && ntohs(udp_header->Length) >= sizeof(WINDIVERT_UDPHDR)) + { + size_t payload_offset = (size_t)((BYTE *)udp_header - packet) + sizeof(WINDIVERT_UDPHDR); + size_t udp_payload_len = ntohs(udp_header->Length) - sizeof(WINDIVERT_UDPHDR); + if (payload_offset <= packet_len && udp_payload_len <= packet_len - payload_offset) + { + const UINT8 *payload = packet + payload_offset; + if (ip_header) + dns_network_observe_udp(addr.Outbound, FALSE, + (const UINT8 *)&ip_header->SrcAddr, (const UINT8 *)&ip_header->DstAddr, + ntohs(udp_header->SrcPort), ntohs(udp_header->DstPort), payload, + (int)udp_payload_len, (ULONGLONG)addr.Timestamp); + else if (ipv6_header) + dns_network_observe_udp(addr.Outbound, TRUE, + (const UINT8 *)ipv6_header->SrcAddr, (const UINT8 *)ipv6_header->DstAddr, + ntohs(udp_header->SrcPort), ntohs(udp_header->DstPort), payload, + (int)udp_payload_len, (ULONGLONG)addr.Timestamp); + } + } + + observe_tcp_dns_packet(packet, packet_len, ip_header, ipv6_header, tcp_header, + (ULONGLONG)addr.Timestamp); + if (ip_header == NULL) { if (ipv6_header == NULL) { continue; } @@ -212,10 +282,21 @@ DWORD WINAPI packet_processor(LPVOID arg) { UINT16 client_sp = ntohs(udp_header->DstPort); UINT8 orig_dst6[16]; UINT16 orig_dp = 0; UINT32 dummy = 0; - if (get_connection_full_v6(client_sp, TRUE, orig_dst6, &orig_dp, &dummy)) + if (get_connection_full_v6(client_sp, TRUE, orig_dst6, &orig_dp, &dummy, + NULL, 0, NULL, NULL)) { memcpy(ipv6_header->SrcAddr, orig_dst6, 16); udp_header->SrcPort = htons(orig_dp); + if (ntohs(udp_header->Length) >= sizeof(WINDIVERT_UDPHDR)) + { + size_t payload_offset = (size_t)((BYTE *)udp_header - packet) + sizeof(WINDIVERT_UDPHDR); + size_t udp_payload_len = ntohs(udp_header->Length) - sizeof(WINDIVERT_UDPHDR); + if (payload_offset <= packet_len && udp_payload_len <= packet_len - payload_offset) + dns_network_observe_udp(FALSE, TRUE, + (const UINT8 *)ipv6_header->SrcAddr, (const UINT8 *)ipv6_header->DstAddr, + orig_dp, client_sp, packet + payload_offset, + (int)udp_payload_len, (ULONGLONG)addr.Timestamp); + } } // ::1 loopback: keep OUTBOUND so the loopback adapter echo // delivers reliably (same reasoning as IPv4 path below). @@ -248,7 +329,7 @@ DWORD WINAPI packet_processor(LPVOID arg) continue; } - if (!g_has_active_rules && g_connection_callback == NULL) + if (!g_has_active_rules && !connection_callback_active()) { WinDivertSend(windivert_handle, packet, packet_len, NULL, &addr); continue; @@ -263,9 +344,8 @@ DWORD WINAPI packet_processor(LPVOID arg) RuleAction action6u; DWORD pid6u = 0; UINT32 pcid6u = 0; - action6u = check_process_rule_v6((const UINT8*)ipv6_header->SrcAddr, sp, (const UINT8*)ipv6_header->DstAddr, dp, TRUE, &pid6u, &pcid6u); - if (action6u == RULE_ACTION_DIRECT && pid6u > 0) - udp_direct_remember(TRUE, ipv6_header->DstAddr, sp, dp); + char hostname6u[256] = {0}; + action6u = check_process_rule_v6((const UINT8*)ipv6_header->SrcAddr, sp, (const UINT8*)ipv6_header->DstAddr, dp, TRUE, (ULONGLONG)addr.Timestamp, hostname6u, &pid6u, &pcid6u); if (action6u == RULE_ACTION_PROXY && !g_localhost_via_proxy) { @@ -280,7 +360,14 @@ DWORD WINAPI packet_processor(LPVOID arg) if (action6u == RULE_ACTION_PROXY && (dp == 546 || dp == 547)) action6u = RULE_ACTION_DIRECT; - if (g_connection_callback != NULL && pid6u > 0) + if (action6u != RULE_ACTION_PROXY) + { + hostname6u[0] = '\0'; + if (action6u == RULE_ACTION_DIRECT && pid6u > 0) + udp_direct_remember(TRUE, ipv6_header->DstAddr, sp, dp); + } + + if (connection_callback_active() && pid6u > 0) { // Fold the 128-bit destination into a 32-bit key so the log-dedup // table distinguishes different IPv6 hosts. Passing 0 (as before) @@ -300,7 +387,7 @@ DWORD WINAPI packet_processor(LPVOID arg) if (action6u==RULE_ACTION_PROXY){PROXY_CONFIG*pc=find_proxy_config(pcid6u);if(pc)snprintf(pinfo,sizeof(pinfo),"Proxy %s://%s:%d (UDP)",pc->type==PROXY_TYPE_HTTP?"HTTP":"SOCKS5",pc->host,pc->port);else snprintf(pinfo,sizeof(pinfo),"Proxy (UDP)");} else if(action6u==RULE_ACTION_DIRECT) snprintf(pinfo,sizeof(pinfo),"Direct (UDP)"); else snprintf(pinfo,sizeof(pinfo),"Blocked (UDP)"); - g_connection_callback(extract_filename(pname),pid6u,dstr,dp,pinfo); + report_connection(extract_filename(pname), pid6u, dstr, hostname6u, dp, pinfo); if(g_traffic_logging_enabled) add_logged_connection(pid6u,v6key,dp,action6u); } } @@ -316,7 +403,7 @@ DWORD WINAPI packet_processor(LPVOID arg) // HTTP proxy can't relay UDP - drop continue; } - add_connection_v6(sp, TRUE, (const UINT8*)ipv6_header->SrcAddr, (const UINT8*)ipv6_header->DstAddr, dp, pcid6u); + add_connection_v6(sp, TRUE, (const UINT8*)ipv6_header->SrcAddr, (const UINT8*)ipv6_header->DstAddr, dp, pcid6u, hostname6u, pid6u, FALSE); udp_header->DstPort = htons(LOCAL_UDP_RELAY_PORT); static const UINT8 _lb6up[16]={0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; @@ -344,18 +431,10 @@ DWORD WINAPI packet_processor(LPVOID arg) continue; } } - else + else { if (udp_header->DstPort != htons(LOCAL_UDP_RELAY_PORT)) { - // Snoop IPv6 DNS responses (AAAA records) for the domain cache. - if (ntohs(udp_header->SrcPort) == 53) - { - const UINT8 *udp_payload6 = (const UINT8 *)udp_header + sizeof(WINDIVERT_UDPHDR); - int udp_payload6_len = (int)(ntohs(udp_header->Length) - sizeof(WINDIVERT_UDPHDR)); - if (udp_payload6_len > 0) - snoop_dns_response(udp_payload6, udp_payload6_len); - } WinDivertSend(windivert_handle, packet, packet_len, NULL, &addr); continue; } @@ -400,7 +479,8 @@ DWORD WINAPI packet_processor(LPVOID arg) UINT8 orig_dst6[16]; UINT16 orig_dst_port = 0; UINT32 dummy_cfg = 0; - get_connection_full_v6(client_sp, FALSE, orig_dst6, &orig_dst_port, &dummy_cfg); + get_connection_full_v6(client_sp, FALSE, orig_dst6, &orig_dst_port, &dummy_cfg, + NULL, 0, NULL, NULL); if (orig_dst_port) tcp_header->SrcPort = htons(orig_dst_port); static const UINT8 _lb6[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; @@ -414,6 +494,8 @@ DWORD WINAPI packet_processor(LPVOID arg) memcpy(ipv6_header->SrcAddr, tmp, 16); addr.Outbound = FALSE; } + observe_tcp_dns_packet(packet, packet_len, NULL, ipv6_header, tcp_header, + (ULONGLONG)addr.Timestamp); if (tcp_header->Rst) remove_connection(client_sp, FALSE, TRUE); else if (tcp_header->Fin) connection_fin(client_sp, TRUE, 2); goto ipv6_send; @@ -453,7 +535,7 @@ DWORD WINAPI packet_processor(LPVOID arg) continue; } - if (!g_has_active_rules && g_connection_callback == NULL) + if (!g_has_active_rules && !connection_callback_active()) { WinDivertSend(windivert_handle, packet, packet_len, NULL, &addr); continue; @@ -462,7 +544,8 @@ DWORD WINAPI packet_processor(LPVOID arg) RuleAction action6; DWORD pid6 = 0; UINT32 proxy_config_id6 = 0; - action6 = check_process_rule_v6((const UINT8*)ipv6_header->SrcAddr, sp, (const UINT8*)ipv6_header->DstAddr, dp, FALSE, &pid6, &proxy_config_id6); + char hostname6[256] = {0}; + action6 = check_process_rule_v6((const UINT8*)ipv6_header->SrcAddr, sp, (const UINT8*)ipv6_header->DstAddr, dp, FALSE, (ULONGLONG)addr.Timestamp, hostname6, &pid6, &proxy_config_id6); // ::1 IPv6 loopback - use same "Localhost via Proxy" toggle as IPv4 127. if (action6 == RULE_ACTION_PROXY && !g_localhost_via_proxy) @@ -477,7 +560,21 @@ DWORD WINAPI packet_processor(LPVOID arg) action6 = RULE_ACTION_DIRECT; } - if (g_connection_callback != NULL && tcp_header->Syn && !tcp_header->Ack && pid6 > 0) + if (action6 != RULE_ACTION_PROXY) + { + hostname6[0] = '\0'; + } + + // See the IPv4 path: a proxied connection whose proxy resolves DNS itself is + // logged by the relay, which is where the destination name becomes known. + BOOL defer_connection_log6 = FALSE; + if (action6 == RULE_ACTION_PROXY && hostname6[0] == '\0') + { + PROXY_CONFIG *pcfg6 = find_proxy_config(proxy_config_id6); + defer_connection_log6 = (pcfg6 != NULL && pcfg6->send_domain_to_proxy); + } + + if (connection_callback_active() && tcp_header->Syn && !tcp_header->Ack && pid6 > 0) { // Fold the 128-bit destination into a 32-bit dedup key so different // IPv6 hosts aren't collapsed into one entry (see IPv6 UDP path). @@ -510,9 +607,10 @@ DWORD WINAPI packet_processor(LPVOID arg) snprintf(proxy_info, sizeof(proxy_info), "Blocked"); const char *display_name = extract_filename(process_name); - g_connection_callback(display_name, pid6, dest_ip_str, dp, proxy_info); + if (!defer_connection_log6) + report_connection(display_name, pid6, dest_ip_str, hostname6, dp, proxy_info); - if (g_traffic_logging_enabled) + if (g_traffic_logging_enabled && !defer_connection_log6) add_logged_connection(pid6, v6key, dp, action6); } } @@ -531,7 +629,7 @@ DWORD WINAPI packet_processor(LPVOID arg) } else if (action6 == RULE_ACTION_PROXY) { - add_connection_v6(sp, FALSE, (const UINT8*)ipv6_header->SrcAddr, (const UINT8*)ipv6_header->DstAddr, dp, proxy_config_id6); + add_connection_v6(sp, FALSE, (const UINT8*)ipv6_header->SrcAddr, (const UINT8*)ipv6_header->DstAddr, dp, proxy_config_id6, hostname6, pid6, defer_connection_log6); port_set_decided(sp); tcp_header->DstPort = htons(g_local_relay_port); @@ -580,6 +678,16 @@ DWORD WINAPI packet_processor(LPVOID arg) // Restore both source IP and port to original destination ip_header->SrcAddr = orig_dest_ip; udp_header->SrcPort = htons(orig_dest_port); + if (ntohs(udp_header->Length) >= sizeof(WINDIVERT_UDPHDR)) + { + size_t payload_offset = (size_t)((BYTE *)udp_header - packet) + sizeof(WINDIVERT_UDPHDR); + size_t udp_payload_len = ntohs(udp_header->Length) - sizeof(WINDIVERT_UDPHDR); + if (payload_offset <= packet_len && udp_payload_len <= packet_len - payload_offset) + dns_network_observe_udp(FALSE, FALSE, + (const UINT8 *)&ip_header->SrcAddr, (const UINT8 *)&ip_header->DstAddr, + orig_dest_port, dst_port, packet + payload_offset, + (int)udp_payload_len, (ULONGLONG)addr.Timestamp); + } // loopback need outbound injection inbound dont work on windows loopback // bcz fast path skip the recive layer we inject into @@ -634,7 +742,7 @@ DWORD WINAPI packet_processor(LPVOID arg) UINT16 dest_port = ntohs(udp_header->DstPort); // if no rule configuree all connection direct with no checks avoid unwanted memory and pocessing whcich could delay - if (!g_has_active_rules && g_connection_callback == NULL) + if (!g_has_active_rules && !connection_callback_active()) { // No rules and no logging - pass through immediately (no checksum needed for unmodified packets) WinDivertSend(windivert_handle, packet, packet_len, NULL, &addr); @@ -652,9 +760,8 @@ DWORD WINAPI packet_processor(LPVOID arg) DWORD pid = 0; UINT32 proxy_config_id = 0; - action = check_process_rule(src_ip, src_port, dest_ip, dest_port, TRUE, &pid, &proxy_config_id); - if (action == RULE_ACTION_DIRECT && pid > 0) - udp_direct_remember(FALSE, &dest_ip, src_port, dest_port); + char hostname[256] = {0}; + action = check_process_rule(src_ip, src_port, dest_ip, dest_port, TRUE, (ULONGLONG)addr.Timestamp, hostname, &pid, &proxy_config_id); // override PROXY to DIRECT if localhost proxy is disabled and destination is localhost BYTE dest_first_octet = (dest_ip >> 0) & 0xFF; @@ -669,10 +776,17 @@ DWORD WINAPI packet_processor(LPVOID arg) if (action == RULE_ACTION_PROXY && (dest_port == 67 || dest_port == 68)) action = RULE_ACTION_DIRECT; + if (action != RULE_ACTION_PROXY) + { + hostname[0] = '\0'; + if (action == RULE_ACTION_DIRECT && pid > 0) + udp_direct_remember(FALSE, &dest_ip, src_port, dest_port); + } + // only log if callback is set // reuse pid from check_process_rule // CLI use no log flag - if (g_connection_callback != NULL && pid > 0) + if (connection_callback_active() && pid > 0) { char process_name[MAX_PROCESS_NAME]; @@ -704,7 +818,7 @@ DWORD WINAPI packet_processor(LPVOID arg) } const char* display_name = extract_filename(process_name); - g_connection_callback(display_name, pid, dest_ip_str, dest_port, proxy_info); + report_connection(display_name, pid, dest_ip_str, hostname, dest_port, proxy_info); if (g_traffic_logging_enabled) { @@ -721,7 +835,7 @@ DWORD WINAPI packet_processor(LPVOID arg) if (action == RULE_ACTION_PROXY) { - add_connection(src_port, TRUE, src_ip, dest_ip, dest_port, proxy_config_id); + add_connection(src_port, TRUE, src_ip, dest_ip, dest_port, proxy_config_id, hostname, pid, FALSE); // redirect to UDP relay server at 127.0.0.1:34011 udp_header->DstPort = htons(LOCAL_UDP_RELAY_PORT); @@ -758,16 +872,6 @@ DWORD WINAPI packet_processor(LPVOID arg) { if (udp_header->DstPort != htons(LOCAL_UDP_RELAY_PORT)) { - // Snoop DNS responses to build the IP→hostname cache used by - // socks5_connect_domain() so that SOCKS5 proxies receive the - // original hostname instead of a bare IP (issue #138). - if (ntohs(udp_header->SrcPort) == 53) - { - const UINT8 *udp_payload = (const UINT8 *)udp_header + sizeof(WINDIVERT_UDPHDR); - int udp_payload_len = (int)(ntohs(udp_header->Length) - sizeof(WINDIVERT_UDPHDR)); - if (udp_payload_len > 0) - snoop_dns_response(udp_payload, udp_payload_len); - } // Unmodified packet no checksum needed WinDivertSend(windivert_handle, packet, packet_len, NULL, &addr); continue; @@ -852,6 +956,8 @@ DWORD WINAPI packet_processor(LPVOID arg) ip_header->SrcAddr = temp_addr; addr.Outbound = FALSE; } + observe_tcp_dns_packet(packet, packet_len, ip_header, NULL, tcp_header, + (ULONGLONG)addr.Timestamp); if (tcp_header->Rst) remove_connection(dst_port, FALSE, FALSE); @@ -902,7 +1008,7 @@ DWORD WINAPI packet_processor(LPVOID arg) } // avoid rule pocess and packet process if no rules - if (!g_has_active_rules && g_connection_callback == NULL) + if (!g_has_active_rules && !connection_callback_active()) { WinDivertSend(windivert_handle, packet, packet_len, NULL, &addr); continue; @@ -912,7 +1018,8 @@ DWORD WINAPI packet_processor(LPVOID arg) DWORD pid = 0; UINT32 proxy_config_id = 0; - action = check_process_rule(src_ip, src_port, orig_dest_ip, orig_dest_port, FALSE, &pid, &proxy_config_id); + char hostname[256] = {0}; + action = check_process_rule(src_ip, src_port, orig_dest_ip, orig_dest_port, FALSE, (ULONGLONG)addr.Timestamp, hostname, &pid, &proxy_config_id); BYTE orig_dest_first_octet = (orig_dest_ip >> 0) & 0xFF; if (action == RULE_ACTION_PROXY && !g_localhost_via_proxy && orig_dest_first_octet == 127) @@ -922,8 +1029,25 @@ DWORD WINAPI packet_processor(LPVOID arg) if (action == RULE_ACTION_PROXY && is_broadcast_or_multicast(orig_dest_ip)) action = RULE_ACTION_DIRECT; + if (action != RULE_ACTION_PROXY) + { + hostname[0] = '\0'; + } + + // A proxied connection whose proxy resolves DNS itself (send hostname) is + // logged by the relay instead of here: no name is known at SYN time - the app + // may be reusing an address from its own cache without resolving anything - + // and the relay learns it from the DNS cache, or from the TLS ClientHello the + // app sends right after the handshake. See log_pending in add_connection(). + BOOL defer_connection_log = FALSE; + if (action == RULE_ACTION_PROXY && hostname[0] == '\0') + { + PROXY_CONFIG *pcfg = find_proxy_config(proxy_config_id); + defer_connection_log = (pcfg != NULL && pcfg->send_domain_to_proxy); + } + // only new TCP/SYN inital fist packet - if (g_connection_callback != NULL && tcp_header->Syn && !tcp_header->Ack && pid > 0) + if (connection_callback_active() && tcp_header->Syn && !tcp_header->Ack && pid > 0) { char process_name[MAX_PROCESS_NAME]; if (pid > 0 && get_process_name_from_pid(pid, process_name, sizeof(process_name))) @@ -956,9 +1080,10 @@ DWORD WINAPI packet_processor(LPVOID arg) } const char* display_name = extract_filename(process_name); - g_connection_callback(display_name, pid, dest_ip_str, orig_dest_port, proxy_info); + if (!defer_connection_log) + report_connection(display_name, pid, dest_ip_str, hostname, orig_dest_port, proxy_info); - if (g_traffic_logging_enabled) + if (g_traffic_logging_enabled && !defer_connection_log) { add_logged_connection(pid, orig_dest_ip, orig_dest_port, action); } @@ -983,7 +1108,7 @@ DWORD WINAPI packet_processor(LPVOID arg) } else if (action == RULE_ACTION_PROXY) { - add_connection(src_port, FALSE, src_ip, orig_dest_ip, orig_dest_port, proxy_config_id); + add_connection(src_port, FALSE, src_ip, orig_dest_ip, orig_dest_port, proxy_config_id, hostname, pid, defer_connection_log); // Mark this port as decided (not direct) so subsequent packets from // the same source port skip the rule check. The is_connection_tracked // branch above handles the actual per-packet redirect. @@ -1052,6 +1177,11 @@ PROXYBRIDGE_API void ProxyBridge_SetConnectionCallback(ConnectionCallback callba g_connection_callback = callback; } +PROXYBRIDGE_API void ProxyBridge_SetConnectionCallbackEx(ConnectionCallbackEx callback) +{ + g_connection_callback_ex = callback; +} + PROXYBRIDGE_API void ProxyBridge_SetTrafficLoggingEnabled(BOOL enable) { g_traffic_logging_enabled = enable; @@ -1074,12 +1204,16 @@ DWORD WINAPI cleanup_worker(LPVOID arg) { // 30 seconds, but in short slices: ProxyBridge_Stop() only waits briefly for this thread, and a // thread still sleeping inside the DLL when it is unloaded would crash the host process. - for (int i = 0; i < 300 && running; i++) Sleep(100); + for (int i = 0; i < 300 && running; i++) + { + Sleep(100); + if (running) dns_process_cache_snapshot_tick(FALSE); + } if (running) { cleanup_stale_connections(); cleanup_stale_pid_cache(); - cleanup_stale_dns_cache(); + dns_process_cache_cleanup(); } } return 0; @@ -1150,6 +1284,13 @@ static DWORD WINAPI fw_ensure_thread(LPVOID arg) return 0; } +static void dns_process_cache_abort_start(void) +{ + dns_process_cache_end_session(); + dns_process_cache_clear(); + dns_network_query_reset(); +} + PROXYBRIDGE_API BOOL ProxyBridge_Start(void) { char filter[FILTER_BUFFER_SIZE]; @@ -1159,14 +1300,10 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) return FALSE; InitializeSRWLock(&lock); - dns_cache_init(); + dns_process_cache_init(); + dns_network_query_reset(); udp_direct_reset(); - // If domain rules were configured before start, flush the OS DNS cache so the very - // first connections re-resolve on the wire and populate our IP->hostname snoop cache. - if (g_has_domain_rules) - flush_dns_resolver_cache(); - running = TRUE; relay_pool_start(); // optional: connections fall back to blocking relay threads without it @@ -1177,6 +1314,7 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) if (proxy_thread == NULL) { running = FALSE; + dns_process_cache_abort_start(); return FALSE; } @@ -1188,6 +1326,7 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) WaitForSingleObject(proxy_thread, INFINITE); CloseHandle(proxy_thread); proxy_thread = NULL; + dns_process_cache_abort_start(); return FALSE; } @@ -1203,6 +1342,7 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) WaitForSingleObject(proxy_thread, INFINITE); CloseHandle(proxy_thread); proxy_thread = NULL; + dns_process_cache_abort_start(); return FALSE; } } @@ -1223,6 +1363,9 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) "(udp and (outbound or loopback or (udp.DstPort == %d or udp.SrcPort == %d)) and " "udp.SrcPort != 67 and udp.DstPort != 67 and udp.SrcPort != 68 and udp.DstPort != 68) or " "(udp and not outbound and udp.SrcPort == 53) or " + "(tcp and not outbound and tcp.SrcPort == 53) or " + "(ipv6 and udp and not outbound and udp.SrcPort == 53) or " + "(ipv6 and tcp and not outbound and tcp.SrcPort == 53) or " "(ipv6 and udp and not outbound and udp.SrcPort == 53) or " "(ipv6 and tcp and (outbound or loopback or (tcp.DstPort == %d or tcp.SrcPort == %d))) or " "(ipv6 and udp and (outbound or loopback or (udp.DstPort == %d or udp.SrcPort == %d)) and " @@ -1258,14 +1401,28 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) break; } running = FALSE; + if (cleanup_thread != NULL) + { + WaitForSingleObject(cleanup_thread, 8000); + CloseHandle(cleanup_thread); + cleanup_thread = NULL; + } + if (udp_relay_thread != NULL) + { + WaitForSingleObject(udp_relay_thread, 8000); + CloseHandle(udp_relay_thread); + udp_relay_thread = NULL; + } WaitForSingleObject(proxy_thread, INFINITE); CloseHandle(proxy_thread); proxy_thread = NULL; + dns_process_cache_abort_start(); return FALSE; } // Socket-layer events give the owner PID of each new connection without a table scan. // Optional: if it cannot be opened the lookups fall back to the table. + dns_process_cache_begin_session(); sockmap_start(); // WINDIVERT_PARAM_QUEUE_LENGTH: max packets in queue (range 32–16384). @@ -1282,6 +1439,14 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) // Raise to the maximum so a burst of large packets never hits a byte cap. WinDivertSetParam(windivert_handle, WINDIVERT_PARAM_QUEUE_SIZE, 33553920); + // Do not flush the OS resolver cache to manufacture observable queries. If DNS + // evidence is unavailable, hostname recovery records a gap and uses the IP target. + update_has_active_rules(); + + // Start DNS observation before the packet reader so resolutions at engine startup + // are less likely to race the first captured connection. + dns_etw_start(); + for (int i = 0; i < NUM_PACKET_THREADS; i++) { packet_thread[i] = CreateThread(NULL, 0, packet_processor, NULL, 0, NULL); @@ -1299,6 +1464,14 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void) } WinDivertClose(windivert_handle); windivert_handle = INVALID_HANDLE_VALUE; + dns_etw_stop(); + if (cleanup_thread != NULL) + { + WaitForSingleObject(cleanup_thread, 8000); + CloseHandle(cleanup_thread); + cleanup_thread = NULL; + } + dns_process_cache_abort_start(); WaitForSingleObject(proxy_thread, INFINITE); CloseHandle(proxy_thread); proxy_thread = NULL; @@ -1364,6 +1537,10 @@ PROXYBRIDGE_API BOOL ProxyBridge_Stop(void) } } + // Drain and stop DNS observation after the packet loop is gone, before releasing + // process handles and their lifecycle waits. + dns_etw_stop(); + if (proxy_thread != NULL) { WaitForSingleObject(proxy_thread, 8000); // generous: a thread outliving Stop would run code from an unloaded DLL @@ -1407,6 +1584,7 @@ PROXYBRIDGE_API BOOL ProxyBridge_Stop(void) { CONNECTION_INFO *to_free = connection_hash_table[i]; connection_hash_table[i] = connection_hash_table[i]->next; + rev_unlink(to_free); free(to_free); } } @@ -1419,6 +1597,9 @@ PROXYBRIDGE_API BOOL ProxyBridge_Stop(void) clear_logged_connections(); clear_pid_cache(); + dns_process_cache_end_session(); + dns_process_cache_clear(); + dns_network_query_reset(); // Release the reusable owner-PID table scratch buffer (packet thread has stopped). free(g_pidtbl_buf); diff --git a/Windows/src/ProxyBridge.h b/Windows/src/ProxyBridge.h index 2d40db6..cbd52a8 100644 --- a/Windows/src/ProxyBridge.h +++ b/Windows/src/ProxyBridge.h @@ -17,6 +17,7 @@ extern "C" { typedef void (*LogCallback)(const char* message); typedef void (*ConnectionCallback)(const char* process_name, DWORD pid, const char* dest_ip, UINT16 dest_port, const char* proxy_info); +typedef void (*ConnectionCallbackEx)(const char* process_name, DWORD pid, const char* dest_ip, const char* hostname, UINT16 dest_port, const char* proxy_info); typedef enum { PROXY_TYPE_HTTP = 0, @@ -51,7 +52,8 @@ PROXYBRIDGE_API int ProxyBridge_TestProxyConfigEx(UINT32 config_id, const cha // Rule management - proxy_config_id selects which proxy config the rule uses (0 = first available) // target_domains: semicolon/comma separated domain patterns ("*", "google.com", "*.google.com"); NULL/"" = no domain restriction. -// Domain matching relies on DNS snooping of the app's own resolutions; unencrypted DNS only (DoH/DoT bypasses it). +// Domain matching uses process-scoped DNS Client ETW results plus observed UDP DNS transactions; +// application-managed encrypted DNS and DNS over TCP may leave the hostname unavailable. PROXYBRIDGE_API UINT32 ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, const char* target_domains, RuleProtocol protocol, RuleAction action, UINT32 proxy_config_id); PROXYBRIDGE_API BOOL ProxyBridge_EnableRule(UINT32 rule_id); PROXYBRIDGE_API BOOL ProxyBridge_DisableRule(UINT32 rule_id); @@ -62,6 +64,7 @@ PROXYBRIDGE_API UINT32 ProxyBridge_GetRulePosition(UINT32 rule_id); // Get curr PROXYBRIDGE_API void ProxyBridge_SetLocalhostViaProxy(BOOL enable); PROXYBRIDGE_API void ProxyBridge_SetLogCallback(LogCallback callback); PROXYBRIDGE_API void ProxyBridge_SetConnectionCallback(ConnectionCallback callback); +PROXYBRIDGE_API void ProxyBridge_SetConnectionCallbackEx(ConnectionCallbackEx callback); PROXYBRIDGE_API void ProxyBridge_SetTrafficLoggingEnabled(BOOL enable); PROXYBRIDGE_API void ProxyBridge_ClearConnectionLogs(void); // Clear connection history from memory PROXYBRIDGE_API BOOL ProxyBridge_Start(void); diff --git a/Windows/src/pb_conntrack.c b/Windows/src/pb_conntrack.c index 667c61a..97e1e93 100644 --- a/Windows/src/pb_conntrack.c +++ b/Windows/src/pb_conntrack.c @@ -2,6 +2,20 @@ // Connection tracking: forward table, reverse index, logged connections. +static CONNECTION_INFO *g_domain_rev_table[CONNECTION_HASH_SIZE]; + +static UINT32 rev_hash_domain(const char *hostname, UINT16 port) +{ + UINT32 hash = 5381u ^ port; + for (const unsigned char *p = (const unsigned char *)hostname; *p; p++) + { + unsigned char c = *p; + if (c >= 'A' && c <= 'Z') c += 'a' - 'A'; + hash = ((hash << 5) + hash) ^ c; + } + return hash % CONNECTION_HASH_SIZE; +} + // Reverse-index helpers. Keyed by the ORIGINAL destination (what the app connected to) - // an inbound relay reply carries that as its source. All callers hold `lock` exclusively. UINT32 rev_hash_v4(UINT32 dest_ip, UINT16 dest_port) @@ -25,20 +39,87 @@ void rev_insert(CONNECTION_INFO *c) c->rev_next = connection_rev_table[b]; connection_rev_table[b] = c; c->in_rev = TRUE; + if (c->is_udp && c->hostname[0]) + { + c->domain_rev_bucket = rev_hash_domain(c->hostname, c->orig_dest_port); + c->domain_rev_next = g_domain_rev_table[c->domain_rev_bucket]; + g_domain_rev_table[c->domain_rev_bucket] = c; + c->in_domain_rev = TRUE; + } } void rev_unlink(CONNECTION_INFO *c) { - if (!c->in_rev) return; - CONNECTION_INFO **pp = &connection_rev_table[c->rev_bucket]; - while (*pp) { if (*pp == c) { *pp = c->rev_next; break; } pp = &(*pp)->rev_next; } - c->in_rev = FALSE; - c->rev_next = NULL; + if (c->in_rev) + { + CONNECTION_INFO **pp = &connection_rev_table[c->rev_bucket]; + while (*pp) { if (*pp == c) { *pp = c->rev_next; break; } pp = &(*pp)->rev_next; } + c->in_rev = FALSE; + c->rev_next = NULL; + } + if (c->in_domain_rev) + { + CONNECTION_INFO **pp = &g_domain_rev_table[c->domain_rev_bucket]; + while (*pp) { if (*pp == c) { *pp = c->domain_rev_next; break; } pp = &(*pp)->domain_rev_next; } + c->in_domain_rev = FALSE; + c->domain_rev_next = NULL; + } +} + +static BOOL connection_capture_process(DWORD pid, FILETIME *created, HANDLE *process) +{ + return get_process_identity(pid, created, process); } -void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, UINT16 dest_port, UINT32 proxy_config_id) +BOOL find_unique_udp_sender_by_domain(const char *hostname, UINT16 dest_port, + UINT32 proxy_config_id, + BOOL *is_ipv6, UINT32 *client_ip, UINT8 client_ip6[16], + UINT16 *client_port) { + if (!hostname || !hostname[0] || !is_ipv6 || !client_ip || !client_ip6 || !client_port) + return FALSE; + + CONNECTION_INFO *match = NULL; + AcquireSRWLockShared(&lock); + for (CONNECTION_INFO *c = g_domain_rev_table[rev_hash_domain(hostname, dest_port)]; + c; c = c->domain_rev_next) + { + PROXY_CONFIG *cfg = find_proxy_config(c->proxy_config_id); + if (c->is_udp && cfg && cfg->config_id == proxy_config_id && + c->orig_dest_port == dest_port && + _stricmp(c->hostname, hostname) == 0) + { + if (match) { match = NULL; break; } // Ambiguous: never route a reply to an arbitrary client. + match = c; + } + } + if (match) + { + *is_ipv6 = match->is_ipv6; + *client_ip = match->src_ip; + if (match->is_ipv6) memcpy(client_ip6, match->src_ip6, 16); + *client_port = match->src_port; + InterlockedExchange64((LONGLONG volatile*)&match->last_activity, (LONGLONG)GetTickCount64()); + } + ReleaseSRWLockShared(&lock); + return match != NULL; +} + +void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, + UINT16 dest_port, UINT32 proxy_config_id, const char *hostname, + DWORD process_id, BOOL log_pending) +{ + FILETIME process_created = {0}; + HANDLE process = NULL; + BOOL has_process_identity = connection_capture_process(process_id, &process_created, &process); + const char *conn_hostname = hostname; AcquireSRWLockExclusive(&lock); + if (has_process_identity && WaitForSingleObject(process, 0) != WAIT_TIMEOUT) + { + ReleaseSRWLockExclusive(&lock); + CloseHandle(process); + return; + } int hash = src_port % CONNECTION_HASH_SIZE; CONNECTION_INFO *existing = connection_hash_table[hash]; @@ -53,11 +134,18 @@ void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, existing->orig_dest_ip = dest_ip; existing->orig_dest_port = dest_port; existing->proxy_config_id = proxy_config_id; + strncpy_s(existing->hostname, sizeof(existing->hostname), + conn_hostname ? conn_hostname : "", _TRUNCATE); + existing->log_pending = log_pending; + existing->process_id = process_id; + existing->process_created = process_created; + existing->has_process_identity = has_process_identity; existing->is_tracked = TRUE; existing->fin_flags = 0; existing->last_activity = GetTickCount64(); rev_insert(existing); ReleaseSRWLockExclusive(&lock); + if (process) CloseHandle(process); return; } existing = existing->next; @@ -66,6 +154,7 @@ void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, CONNECTION_INFO *conn = (CONNECTION_INFO *)calloc(1, sizeof(CONNECTION_INFO)); if (conn == NULL) { ReleaseSRWLockExclusive(&lock); + if (process) CloseHandle(process); return; } @@ -75,6 +164,11 @@ void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, conn->orig_dest_ip = dest_ip; conn->orig_dest_port = dest_port; conn->proxy_config_id = proxy_config_id; + strncpy_s(conn->hostname, sizeof(conn->hostname), conn_hostname ? conn_hostname : "", _TRUNCATE); + conn->log_pending = log_pending; + conn->process_id = process_id; + conn->process_created = process_created; + conn->has_process_identity = has_process_identity; conn->is_tracked = TRUE; conn->is_ipv6 = FALSE; conn->last_activity = GetTickCount64(); @@ -83,11 +177,25 @@ void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, connection_hash_table[hash] = conn; rev_insert(conn); ReleaseSRWLockExclusive(&lock); + if (process) CloseHandle(process); } -void add_connection_v6(UINT16 src_port, BOOL is_udp, const UINT8 src_ip6[16], const UINT8 dest_ip6[16], UINT16 dest_port, UINT32 proxy_config_id) +void add_connection_v6(UINT16 src_port, BOOL is_udp, const UINT8 src_ip6[16], + const UINT8 dest_ip6[16], UINT16 dest_port, + UINT32 proxy_config_id, const char *hostname, DWORD process_id, + BOOL log_pending) { + FILETIME process_created = {0}; + HANDLE process = NULL; + BOOL has_process_identity = connection_capture_process(process_id, &process_created, &process); + const char *conn_hostname = hostname; AcquireSRWLockExclusive(&lock); + if (has_process_identity && WaitForSingleObject(process, 0) != WAIT_TIMEOUT) + { + ReleaseSRWLockExclusive(&lock); + CloseHandle(process); + return; + } int hash = src_port % CONNECTION_HASH_SIZE; CONNECTION_INFO *existing = connection_hash_table[hash]; @@ -100,11 +208,18 @@ void add_connection_v6(UINT16 src_port, BOOL is_udp, const UINT8 src_ip6[16], co memcpy(existing->orig_dest_ip6, dest_ip6, 16); existing->orig_dest_port = dest_port; existing->proxy_config_id = proxy_config_id; + strncpy_s(existing->hostname, sizeof(existing->hostname), + conn_hostname ? conn_hostname : "", _TRUNCATE); + existing->log_pending = log_pending; + existing->process_id = process_id; + existing->process_created = process_created; + existing->has_process_identity = has_process_identity; existing->is_tracked = TRUE; existing->fin_flags = 0; existing->last_activity = GetTickCount64(); rev_insert(existing); ReleaseSRWLockExclusive(&lock); + if (process) CloseHandle(process); return; } existing = existing->next; @@ -113,6 +228,7 @@ void add_connection_v6(UINT16 src_port, BOOL is_udp, const UINT8 src_ip6[16], co CONNECTION_INFO *conn = (CONNECTION_INFO *)calloc(1, sizeof(CONNECTION_INFO)); if (conn == NULL) { ReleaseSRWLockExclusive(&lock); + if (process) CloseHandle(process); return; } @@ -125,15 +241,24 @@ void add_connection_v6(UINT16 src_port, BOOL is_udp, const UINT8 src_ip6[16], co memcpy(conn->orig_dest_ip6, dest_ip6, 16); conn->orig_dest_port = dest_port; conn->proxy_config_id = proxy_config_id; + strncpy_s(conn->hostname, sizeof(conn->hostname), conn_hostname ? conn_hostname : "", _TRUNCATE); + conn->log_pending = log_pending; + conn->process_id = process_id; + conn->process_created = process_created; + conn->has_process_identity = has_process_identity; conn->is_tracked = TRUE; conn->last_activity = GetTickCount64(); conn->next = connection_hash_table[hash]; connection_hash_table[hash] = conn; rev_insert(conn); ReleaseSRWLockExclusive(&lock); + if (process) CloseHandle(process); } -BOOL get_connection_full_v6(UINT16 src_port, BOOL is_udp, UINT8 dest_ip6[16], UINT16 *dest_port, UINT32 *proxy_config_id) +BOOL get_connection_full_v6(UINT16 src_port, BOOL is_udp, UINT8 dest_ip6[16], + UINT16 *dest_port, UINT32 *proxy_config_id, + char *hostname, size_t hostname_size, + DWORD *process_id, BOOL *log_pending) { BOOL found = FALSE; AcquireSRWLockShared(&lock); @@ -144,6 +269,10 @@ BOOL get_connection_full_v6(UINT16 src_port, BOOL is_udp, UINT8 dest_ip6[16], UI memcpy(dest_ip6, conn->orig_dest_ip6, 16); *dest_port = conn->orig_dest_port; if (proxy_config_id != NULL) *proxy_config_id = conn->proxy_config_id; + if (hostname && hostname_size) + strncpy_s(hostname, hostname_size, conn->hostname, _TRUNCATE); + if (process_id != NULL) *process_id = conn->process_id; + if (log_pending != NULL) *log_pending = conn->log_pending; InterlockedExchange64((LONGLONG volatile*)&conn->last_activity, (LONGLONG)GetTickCount64()); found = TRUE; break; @@ -201,7 +330,11 @@ BOOL udp_flow_validate(UINT16 src_port, BOOL is_ipv6, UINT32 dest_ip, const UINT while (*pp != NULL && !((*pp)->src_port == src_port && (*pp)->is_udp && (*pp)->is_ipv6 == is_ipv6)) pp = &(*pp)->next; c = *pp; if (c == NULL) { ReleaseSRWLockExclusive(&lock); return FALSE; } - if (c->last_activity < now && now - c->last_activity > UDP_REEVAL_IDLE_MS) + BOOL still_same = (c->orig_dest_port == dest_port) && + (is_ipv6 ? memcmp(c->orig_dest_ip6, dest_ip6, 16) == 0 + : c->orig_dest_ip == dest_ip); + if ((c->last_activity < now && now - c->last_activity > UDP_REEVAL_IDLE_MS) || + !still_same) { *pp = c->next; rev_unlink(c); @@ -210,11 +343,7 @@ BOOL udp_flow_validate(UINT16 src_port, BOOL is_ipv6, UINT32 dest_ip, const UINT } else { - rev_unlink(c); // dest changes: re-key the reverse index - c->orig_dest_port = dest_port; - if (is_ipv6) memcpy(c->orig_dest_ip6, dest_ip6, 16); else c->orig_dest_ip = dest_ip; c->last_activity = now; - rev_insert(c); valid = TRUE; } ReleaseSRWLockExclusive(&lock); @@ -301,7 +430,10 @@ BOOL get_connection(UINT16 src_port, BOOL is_udp, UINT32 *dest_ip, UINT16 *dest_ return found; } -BOOL get_connection_full(UINT16 src_port, BOOL is_udp, UINT32 *dest_ip, UINT16 *dest_port, UINT32 *proxy_config_id) +BOOL get_connection_full(UINT16 src_port, BOOL is_udp, UINT32 *dest_ip, + UINT16 *dest_port, UINT32 *proxy_config_id, + char *hostname, size_t hostname_size, + DWORD *process_id, BOOL *log_pending) { BOOL found = FALSE; @@ -317,6 +449,10 @@ BOOL get_connection_full(UINT16 src_port, BOOL is_udp, UINT32 *dest_ip, UINT16 * *dest_ip = conn->orig_dest_ip; *dest_port = conn->orig_dest_port; if (proxy_config_id != NULL) *proxy_config_id = conn->proxy_config_id; + if (hostname && hostname_size) + strncpy_s(hostname, hostname_size, conn->hostname, _TRUNCATE); + if (process_id != NULL) *process_id = conn->process_id; + if (log_pending != NULL) *log_pending = conn->log_pending; InterlockedExchange64((LONGLONG volatile*)&conn->last_activity, (LONGLONG)GetTickCount64()); found = TRUE; break; @@ -373,6 +509,29 @@ void remove_connection(UINT16 src_port, BOOL is_udp, BOOL is_ipv6) ReleaseSRWLockExclusive(&lock); } +void remove_connections_for_process(DWORD process_id, const FILETIME *process_created) +{ + if (!process_id || !process_created) return; + AcquireSRWLockExclusive(&lock); + for (int i = 0; i < CONNECTION_HASH_SIZE; i++) + { + CONNECTION_INFO **link = &connection_hash_table[i]; + while (*link) + { + CONNECTION_INFO *c = *link; + if (c->has_process_identity && c->process_id == process_id && + CompareFileTime(&c->process_created, process_created) == 0) + { + *link = c->next; + rev_unlink(c); + free(c); + } + else link = &c->next; + } + } + ReleaseSRWLockExclusive(&lock); +} + void connection_fin(UINT16 src_port, BOOL is_ipv6, UINT8 bit) { AcquireSRWLockExclusive(&lock); diff --git a/Windows/src/pb_dns.c b/Windows/src/pb_dns.c index 02d3f68..cd42417 100644 --- a/Windows/src/pb_dns.c +++ b/Windows/src/pb_dns.c @@ -1,135 +1,930 @@ #include "pb_internal.h" +#include +#include +#include +#include -// DNS snoop cache: maps intercepted A/AAAA answers back to hostnames. +#pragma comment(lib, "shell32.lib") -// dns cache -void dns_cache_init(void) +typedef struct DNS_PROCESS_ENTRY { + DWORD pid; + FILETIME created; + HANDLE process; + HANDLE wait; + volatile LONG exited; + UINT32 count; + struct DNS_PROCESS_ENTRY *next; +} DNS_PROCESS_ENTRY; + +#define DNS_PROCESS_MAX_ENTRIES 4096 +#define DNS_PROCESS_MAX_TOTAL 65536 +#define DNS_PROCESS_BUCKETS 4096 +#define DNS_SNAPSHOT_VERSION 4 +#define DNS_SNAPSHOT_MAX_BYTES (21u * 1024u * 1024u) +#define DNS_NETWORK_MAX_QUERIES 512 +#define DNS_NETWORK_QUERY_TTL_MS 30000 +#define DNS_TCP_MAX_FLOWS 16 +#define DNS_TCP_WINDOW_SIZE 65536 +#define DNS_TCP_BITMAP_SIZE (DNS_TCP_WINDOW_SIZE / 8) +#define DNS_TCP_MESSAGE_MAX 65535 +#define DNS_TCP_FLOW_TTL_MS 30000 +#define DNS_SOURCE_NETWORK_DNS 1 +// shortcut: cap DNS TCP reassembly at 16 concurrent streams and a 64 KiB window; +// raise the cap only if capture-gap measurements show real resolver traffic is evicted. + +typedef struct DNS_NETWORK_QUERY { + DWORD pid; + FILETIME process_created; + BOOL is_ipv6; + BOOL is_tcp; + UINT16 id; + UINT16 src_port; + UINT16 dst_port; + UINT16 qtype; + UINT16 qclass; + BOOL ambiguous; + UINT8 src_ip[16]; + UINT8 dst_ip[16]; + ULONGLONG tick; + char qname[256]; +} DNS_NETWORK_QUERY; + +typedef struct DNS_TCP_DIRECTION { + BOOL initialized; + UINT32 next_seq; + UINT32 ring_head; + UINT32 pending_bytes; + UINT8 *window; + UINT8 *present; + UINT8 *message; + UINT16 message_size; + UINT16 message_length; + UINT8 prefix[2]; + UINT8 prefix_length; +} DNS_TCP_DIRECTION; + +typedef struct DNS_TCP_FLOW { + BOOL used; + BOOL is_ipv6; + UINT8 client_ip[16]; + UINT8 server_ip[16]; + UINT16 client_port; + UINT16 server_port; + UINT8 fin_mask; + ULONGLONG tick; + DNS_TCP_DIRECTION direction[2]; +} DNS_TCP_FLOW; + +// Written only by the single WinDivert packet thread. Expiration bounds outstanding +// transactions; hostname associations themselves remain tied to process lifetime. +static DNS_NETWORK_QUERY g_dns_network_queries[DNS_NETWORK_MAX_QUERIES]; +static DNS_TCP_FLOW g_dns_tcp_flows[DNS_TCP_MAX_FLOWS]; + +#pragma pack(push, 1) +typedef struct DNS_SNAPSHOT_HEADER { + char magic[8]; + DWORD version; + DWORD count; + ULONGLONG boot_estimate; + ULONGLONG gaps; + DWORD flags; +} DNS_SNAPSHOT_HEADER; + +typedef struct DNS_SNAPSHOT_RECORD { + DWORD pid; + ULONGLONG process_created; + UINT16 source_event_id; + DWORD source_ttl_seconds; + UINT8 has_source_ttl; + ULONGLONG event_qpc; + ULONGLONG consumed_qpc; + ULONGLONG last_used_tick; + UINT8 family; + UINT32 ip; + UINT8 ip6[16]; + char domain[256]; +} DNS_SNAPSHOT_RECORD; +#pragma pack(pop) + +#define DNS_SNAPSHOT_INCOMPLETE 1 + +static SRWLOCK g_process_dns_lock; +static DNS_PROCESS_ENTRY *g_dns_processes; +static DNS_HOST_ENTRY *g_process_dns_entries; +static DNS_HOST_ENTRY *g_process_dns_buckets[DNS_PROCESS_BUCKETS]; +static UINT32 g_process_dns_count; +static volatile LONG64 g_process_dns_gaps; +static volatile LONG g_process_dns_initialized; +static WCHAR g_dns_snapshot_path[MAX_PATH]; +static WCHAR g_dns_running_path[MAX_PATH]; +static BOOL g_dns_snapshot_ready; +static volatile LONG64 g_dns_generation; +static volatile LONG64 g_dns_saved_generation; +static volatile LONG64 g_dns_dirty_tick; +static volatile LONG64 g_dns_last_snapshot_tick; +static volatile LONG64 g_dns_snapshot_failed_tick; +static BOOL g_dns_snapshot_incomplete; +static BOOL g_dns_previous_unclean; +static BOOL g_dns_tcp_reassembly_ready; + +static void dns_process_discard_locked(DNS_PROCESS_ENTRY *process, + DNS_PROCESS_ENTRY **retired); +static BOOL dns_process_cache_store_locked(DNS_PROCESS_ENTRY *process, BOOL is_ipv6, + UINT32 ip, const UINT8 ip6[16], + const char *normalized, UINT16 source_event_id, + ULONGLONG event_qpc, ULONGLONG consumed_qpc, + BOOL has_ttl, DWORD ttl_seconds); +static void dns_process_cache_restore_last_used(const DNS_SNAPSHOT_RECORD *record); +static BOOL dns_snapshot_prepare_paths(void); +static BOOL dns_snapshot_load(void); +static BOOL dns_snapshot_write(void); +static ULONGLONG dns_boot_estimate(void); +static void dns_snapshot_begin_session(void); +static BOOL dns_tcp_reassembly_selftest(void); + +static void dns_process_mark_dirty(void) { - InitializeSRWLock(&g_dns_cache_lock); - memset(g_dns_cache, 0, sizeof(g_dns_cache)); - memset(g_dns_cache_v6, 0, sizeof(g_dns_cache_v6)); + InterlockedIncrement64(&g_dns_generation); + InterlockedExchange64(&g_dns_dirty_tick, (LONG64)GetTickCount64()); } -UINT32 dns_bucket(UINT32 ip) +static void dns_process_note_gap(void) { - return (ip * 2654435761u) >> (32 - 10); // Knuth multiplicative hash 1024 buckets + InterlockedIncrement64(&g_process_dns_gaps); + dns_process_mark_dirty(); } -void dns_cache_store(UINT32 ip, const char *domain) +void dns_process_cache_note_gap(ULONGLONG count) { - if (!domain || domain[0] == '\0') return; - UINT32 bucket = dns_bucket(ip); - ULONGLONG now = GetTickCount64(); + if (count) + { + InterlockedExchangeAdd64(&g_process_dns_gaps, (LONG64)count); + dns_process_mark_dirty(); + } +} + +static BOOL dns_write_all(HANDLE file, const void *data, DWORD size) +{ + DWORD written = 0; + return WriteFile(file, data, size, &written, NULL) && written == size; +} + +static BOOL dns_read_all(HANDLE file, void *data, DWORD size) +{ + DWORD read = 0; + return ReadFile(file, data, size, &read, NULL) && read == size; +} + +static ULONGLONG dns_boot_estimate(void) +{ + FILETIME now; + ULARGE_INTEGER value; + GetSystemTimeAsFileTime(&now); + value.LowPart = now.dwLowDateTime; + value.HighPart = now.dwHighDateTime; + return value.QuadPart - GetTickCount64() * 10000ULL; +} + +static BOOL dns_snapshot_prepare_paths(void) +{ + if (g_dns_snapshot_ready) return TRUE; + if (!g_dns_snapshot_path[0]) + { + WCHAR program_data[MAX_PATH]; + if (SHGetFolderPathW(NULL, CSIDL_COMMON_APPDATA | CSIDL_FLAG_CREATE, + NULL, SHGFP_TYPE_CURRENT, program_data) != S_OK) + return FALSE; + + _snwprintf_s(g_dns_snapshot_path, _countof(g_dns_snapshot_path), _TRUNCATE, + L"%ls\\ProxyBridge\\hostname-cache-%ls.dat", program_data, + sizeof(void *) == 8 ? L"x64" : L"x86"); + _snwprintf_s(g_dns_running_path, _countof(g_dns_running_path), _TRUNCATE, + L"%ls\\ProxyBridge\\hostname-cache-%ls.running", program_data, + sizeof(void *) == 8 ? L"x64" : L"x86"); + } + WCHAR directory[MAX_PATH]; + wcscpy_s(directory, _countof(directory), g_dns_snapshot_path); + WCHAR *slash = wcsrchr(directory, L'\\'); + if (!slash) return FALSE; + *slash = L'\0'; + + PSECURITY_DESCRIPTOR sd = NULL; + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( + L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)", SDDL_REVISION_1, &sd, NULL)) + return FALSE; + SECURITY_ATTRIBUTES sa = { sizeof(sa), sd, FALSE }; + BOOL ok = CreateDirectoryW(directory, &sa) || GetLastError() == ERROR_ALREADY_EXISTS; + if (ok) + ok = SetFileSecurityW(directory, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, sd); + LocalFree(sd); + g_dns_snapshot_ready = ok; + return ok; +} - AcquireSRWLockExclusive(&g_dns_cache_lock); - DNS_CACHE_ENTRY *e = g_dns_cache[bucket]; - while (e) +static BOOL dns_snapshot_process_matches(const DNS_SNAPSHOT_RECORD *record) +{ + BOOL known = FALSE, matches = FALSE; + AcquireSRWLockShared(&g_process_dns_lock); + for (DNS_PROCESS_ENTRY *p = g_dns_processes; p; p = p->next) { - if (e->ip == ip) + if (p->pid == record->pid && !p->exited && WaitForSingleObject(p->process, 0) == WAIT_TIMEOUT) { - strncpy_s(e->domain, sizeof(e->domain), domain, _TRUNCATE); - e->expire_tick = now + DNS_CACHE_TTL_MS; - ReleaseSRWLockExclusive(&g_dns_cache_lock); - return; + ULARGE_INTEGER value; + value.LowPart = p->created.dwLowDateTime; + value.HighPart = p->created.dwHighDateTime; + known = TRUE; + matches = value.QuadPart == record->process_created; + break; } - e = e->next; } - DNS_CACHE_ENTRY *ne = (DNS_CACHE_ENTRY *)malloc(sizeof(DNS_CACHE_ENTRY)); - if (ne) + ReleaseSRWLockShared(&g_process_dns_lock); + if (known) return matches; + + FILETIME created; + HANDLE process = NULL; + if (!get_process_identity(record->pid, &created, &process)) return FALSE; + CloseHandle(process); + ULARGE_INTEGER value; + value.LowPart = created.dwLowDateTime; + value.HighPart = created.dwHighDateTime; + return value.QuadPart == record->process_created; +} + +static UINT32 dns_process_bucket(DWORD pid, BOOL is_ipv6, UINT32 ip, const UINT8 ip6[16]) +{ + UINT32 hash = 2166136261u ^ pid ^ (is_ipv6 ? 0x9e3779b9u : 0); + const UINT8 *bytes = is_ipv6 ? ip6 : (const UINT8 *)&ip; + size_t count = is_ipv6 ? 16 : sizeof(ip); + for (size_t i = 0; i < count; i++) hash = (hash ^ bytes[i]) * 16777619u; + return hash & (DNS_PROCESS_BUCKETS - 1); +} + +static void dns_process_unlink_hash_locked(DNS_HOST_ENTRY *entry) +{ + UINT32 bucket = dns_process_bucket(entry->process->pid, entry->is_ipv6, + entry->ip, entry->ip6); + DNS_HOST_ENTRY **link = &g_process_dns_buckets[bucket]; + while (*link && *link != entry) link = &(*link)->hash_next; + if (*link) *link = entry->hash_next; +} + +static void dns_process_remove_entries_locked(DNS_PROCESS_ENTRY *process) +{ + BOOL removed = FALSE; + DNS_HOST_ENTRY **link = &g_process_dns_entries; + while (*link) { - ne->ip = ip; - ne->expire_tick = now + DNS_CACHE_TTL_MS; - strncpy_s(ne->domain, sizeof(ne->domain), domain, _TRUNCATE); - ne->next = g_dns_cache[bucket]; - g_dns_cache[bucket] = ne; + DNS_HOST_ENTRY *entry = *link; + if (entry->process == process) + { + assert(process->count > 0 && g_process_dns_count > 0); + dns_process_unlink_hash_locked(entry); + *link = entry->next; + free(entry); + process->count--; + g_process_dns_count--; + removed = TRUE; + } + else link = &entry->next; } - ReleaseSRWLockExclusive(&g_dns_cache_lock); + if (removed) dns_process_mark_dirty(); } -BOOL dns_cache_lookup(UINT32 ip, char *out_domain, size_t out_size) +static VOID CALLBACK dns_process_exit_callback(PVOID context, BOOLEAN timed_out) { - UINT32 bucket = dns_bucket(ip); - ULONGLONG now = GetTickCount64(); - BOOL found = FALSE; + (void)timed_out; + DNS_PROCESS_ENTRY *process = (DNS_PROCESS_ENTRY *)context; + DWORD pid = process->pid; + FILETIME created = process->created; + InterlockedExchange(&process->exited, 1); + AcquireSRWLockExclusive(&g_process_dns_lock); + dns_process_remove_entries_locked(process); + ReleaseSRWLockExclusive(&g_process_dns_lock); + remove_connections_for_process(pid, &created); +} - AcquireSRWLockShared(&g_dns_cache_lock); - DNS_CACHE_ENTRY *e = g_dns_cache[bucket]; - while (e) +void dns_process_cache_init(void) +{ + if (InterlockedCompareExchange(&g_process_dns_initialized, 1, 0) == 0) + { + InitializeSRWLock(&g_process_dns_lock); + } + else dns_process_cache_clear(); + InterlockedExchange64(&g_process_dns_gaps, 0); + InterlockedExchange64(&g_dns_generation, 0); + InterlockedExchange64(&g_dns_saved_generation, 0); + InterlockedExchange64(&g_dns_dirty_tick, 0); + InterlockedExchange64(&g_dns_last_snapshot_tick, 0); + InterlockedExchange64(&g_dns_snapshot_failed_tick, 0); + g_dns_snapshot_incomplete = FALSE; + dns_snapshot_load(); + g_dns_tcp_reassembly_ready = dns_tcp_reassembly_selftest(); + if (!g_dns_tcp_reassembly_ready) { - if (e->ip == ip && e->expire_tick > now) + dns_process_cache_note_gap(1); + log_message("[DNS-TCP] stream reassembly self-check failed; TCP DNS observation is disabled"); + } +} + +void dns_process_cache_begin_session(void) +{ + dns_snapshot_begin_session(); +} + +static void dns_process_dispose(DNS_PROCESS_ENTRY *process) +{ + if (process->wait) + { + UnregisterWaitEx(process->wait, INVALID_HANDLE_VALUE); + process->wait = NULL; + } + if (process->process) CloseHandle(process->process); + free(process); +} + +void dns_process_cache_cleanup(void) +{ + DNS_PROCESS_ENTRY *retired = NULL; + AcquireSRWLockExclusive(&g_process_dns_lock); + for (DNS_PROCESS_ENTRY *p = g_dns_processes, *next; p; p = next) + { + next = p->next; + if (p->exited || WaitForSingleObject(p->process, 0) != WAIT_TIMEOUT) { - strncpy_s(out_domain, out_size, e->domain, _TRUNCATE); - found = TRUE; + InterlockedExchange(&p->exited, 1); + dns_process_discard_locked(p, &retired); + } + } + ReleaseSRWLockExclusive(&g_process_dns_lock); + while (retired) + { + DNS_PROCESS_ENTRY *next = retired->next; + remove_connections_for_process(retired->pid, &retired->created); + dns_process_dispose(retired); + retired = next; + } +} + +void dns_process_cache_clear(void) +{ + AcquireSRWLockExclusive(&g_process_dns_lock); + DNS_HOST_ENTRY *entry = g_process_dns_entries; + while (entry) + { + DNS_HOST_ENTRY *next = entry->next; + free(entry); + entry = next; + } + g_process_dns_entries = NULL; + memset(g_process_dns_buckets, 0, sizeof(g_process_dns_buckets)); + g_process_dns_count = 0; + DNS_PROCESS_ENTRY *processes = g_dns_processes; + g_dns_processes = NULL; + ReleaseSRWLockExclusive(&g_process_dns_lock); + + while (processes) + { + DNS_PROCESS_ENTRY *next = processes->next; + dns_process_dispose(processes); + processes = next; + } +} + +static BOOL dns_domain_copy(char out[256], const char *domain) +{ + size_t len; + if (!domain || (len = strlen(domain)) == 0 || len >= 256) return FALSE; + if (domain[len - 1] == '.') len--; + if (len == 0 || len >= 256) return FALSE; + for (size_t i = 0; i < len; i++) + { + unsigned char c = (unsigned char)domain[i]; + if (c <= 0x20 || c == 0x7f || c == ';' || c == '\\') return FALSE; + out[i] = (char)(c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c); + } + out[len] = '\0'; + return TRUE; +} + +static void dns_process_discard_locked(DNS_PROCESS_ENTRY *process, + DNS_PROCESS_ENTRY **retired) +{ + DNS_PROCESS_ENTRY **link = &g_dns_processes; + while (*link && *link != process) link = &(*link)->next; + if (*link) + { + *link = process->next; + dns_process_remove_entries_locked(process); + process->next = *retired; + *retired = process; + } +} + +static void dns_process_trim_locked(DNS_PROCESS_ENTRY *process) +{ + while (process->count >= DNS_PROCESS_MAX_ENTRIES || + g_process_dns_count >= DNS_PROCESS_MAX_TOTAL) + { + DNS_HOST_ENTRY **oldest_link = NULL; + LONG64 oldest_tick = LLONG_MAX; + for (DNS_HOST_ENTRY **link = &g_process_dns_entries; *link; link = &(*link)->next) + { + DNS_HOST_ENTRY *candidate = *link; + if ((g_process_dns_count >= DNS_PROCESS_MAX_TOTAL || candidate->process == process) && + candidate->last_used < oldest_tick) + { + oldest_tick = candidate->last_used; + oldest_link = link; + } + } + if (!oldest_link) break; + DNS_HOST_ENTRY *evicted = *oldest_link; + *oldest_link = evicted->next; + dns_process_unlink_hash_locked(evicted); + evicted->process->count--; + g_process_dns_count--; + free(evicted); + dns_process_note_gap(); + } + assert(process->count < DNS_PROCESS_MAX_ENTRIES); + assert(g_process_dns_count < DNS_PROCESS_MAX_TOTAL); +} + +BOOL dns_process_cache_store(DWORD pid, const FILETIME *expected_created, + BOOL is_ipv6, UINT32 ip, const UINT8 ip6[16], + const char *domain, UINT16 source_event_id, + ULONGLONG event_qpc, ULONGLONG consumed_qpc, + BOOL has_ttl, DWORD ttl_seconds) +{ + char normalized[256]; + if (pid == 0 || (is_ipv6 && !ip6) || (!is_ipv6 && ip == 0) || + !dns_domain_copy(normalized, domain)) + return FALSE; + + DNS_PROCESS_ENTRY *retired = NULL; + AcquireSRWLockExclusive(&g_process_dns_lock); + DNS_PROCESS_ENTRY *process = NULL; + for (DNS_PROCESS_ENTRY *p = g_dns_processes; p; p = p->next) + { + if (p->pid == pid && !p->exited && WaitForSingleObject(p->process, 0) == WAIT_TIMEOUT) + { + if (expected_created && CompareFileTime(&p->created, expected_created) != 0) + { + ReleaseSRWLockExclusive(&g_process_dns_lock); + dns_process_note_gap(); + return FALSE; + } + process = p; break; } - e = e->next; } - ReleaseSRWLockShared(&g_dns_cache_lock); - return found; + if (process) + { + for (DNS_PROCESS_ENTRY *p = g_dns_processes, *next; p; p = next) + { + next = p->next; + if (p->pid == pid && p != process) + dns_process_discard_locked(p, &retired); + } + BOOL stored = dns_process_cache_store_locked(process, is_ipv6, ip, ip6, + normalized, source_event_id, + event_qpc, consumed_qpc, + has_ttl, ttl_seconds); + if (!stored && process->count == 0) + dns_process_discard_locked(process, &retired); + ReleaseSRWLockExclusive(&g_process_dns_lock); + while (retired) { DNS_PROCESS_ENTRY *next = retired->next; dns_process_dispose(retired); retired = next; } + return stored; + } + ReleaseSRWLockExclusive(&g_process_dns_lock); + + FILETIME created; + HANDLE process_handle = NULL; + if (!get_process_identity(pid, &created, &process_handle)) + { + dns_process_note_gap(); + return FALSE; + } + if (expected_created && CompareFileTime(&created, expected_created) != 0) + { + CloseHandle(process_handle); + dns_process_note_gap(); + return FALSE; + } + DNS_PROCESS_ENTRY *candidate = (DNS_PROCESS_ENTRY *)calloc(1, sizeof(*candidate)); + if (!candidate) + { + CloseHandle(process_handle); + dns_process_note_gap(); + return FALSE; + } + candidate->pid = pid; + candidate->created = created; + candidate->process = process_handle; + if (!RegisterWaitForSingleObject(&candidate->wait, candidate->process, + dns_process_exit_callback, candidate, + INFINITE, WT_EXECUTEONLYONCE)) + { + dns_process_dispose(candidate); + dns_process_note_gap(); + return FALSE; + } + + AcquireSRWLockExclusive(&g_process_dns_lock); + process = NULL; + for (DNS_PROCESS_ENTRY *p = g_dns_processes; p; p = p->next) + { + if (p->pid == pid && CompareFileTime(&p->created, &created) == 0 && + !p->exited && WaitForSingleObject(p->process, 0) == WAIT_TIMEOUT) + { + process = p; + break; + } + } + if (!process && (candidate->exited || WaitForSingleObject(candidate->process, 0) != WAIT_TIMEOUT)) + { + ReleaseSRWLockExclusive(&g_process_dns_lock); + dns_process_dispose(candidate); + dns_process_note_gap(); + return FALSE; + } + for (DNS_PROCESS_ENTRY *p = g_dns_processes, *next; p; p = next) + { + next = p->next; + if (p->pid == pid && p != process) + dns_process_discard_locked(p, &retired); + } + if (!process) + { + process = candidate; + process->next = g_dns_processes; + g_dns_processes = process; + candidate = NULL; + } + BOOL stored = dns_process_cache_store_locked(process, is_ipv6, ip, ip6, + normalized, source_event_id, + event_qpc, consumed_qpc, + has_ttl, ttl_seconds); + if (!stored && process->count == 0) + dns_process_discard_locked(process, &retired); + ReleaseSRWLockExclusive(&g_process_dns_lock); + + if (candidate) dns_process_dispose(candidate); + while (retired) { DNS_PROCESS_ENTRY *next = retired->next; dns_process_dispose(retired); retired = next; } + return stored; } -UINT32 dns_bucket_v6(const UINT8 ip6[16]) +static BOOL dns_process_cache_store_locked(DNS_PROCESS_ENTRY *process, BOOL is_ipv6, + UINT32 ip, const UINT8 ip6[16], + const char *normalized, UINT16 source_event_id, + ULONGLONG event_qpc, ULONGLONG consumed_qpc, + BOOL has_ttl, DWORD ttl_seconds) { - // FNV-1a over 16 bytes, folded to DNS_CACHE_BUCKETS - UINT32 h = 2166136261u; - for (int i = 0; i < 16; i++) - h = (h ^ ip6[i]) * 16777619u; - return h & (DNS_CACHE_BUCKETS - 1); + UINT32 bucket = dns_process_bucket(process->pid, is_ipv6, ip, ip6); + for (DNS_HOST_ENTRY *entry = g_process_dns_buckets[bucket]; entry; entry = entry->hash_next) + { + BOOL same_ip = entry->is_ipv6 == is_ipv6 && + (is_ipv6 ? memcmp(entry->ip6, ip6, 16) == 0 : entry->ip == ip); + if (entry->process == process && same_ip && strcmp(entry->domain, normalized) == 0) + { + if (event_qpc > entry->event_qpc) + { + entry->event_qpc = event_qpc; + entry->consumed_qpc = consumed_qpc; + entry->source_event_id = source_event_id; + entry->has_source_ttl = has_ttl; + entry->source_ttl_seconds = ttl_seconds; + dns_process_mark_dirty(); + } + InterlockedExchange64(&entry->last_used, (LONG64)GetTickCount64()); + return TRUE; + } + } + + dns_process_trim_locked(process); + DNS_HOST_ENTRY *entry = (DNS_HOST_ENTRY *)calloc(1, sizeof(*entry)); + if (entry) + { + entry->process = process; + entry->is_ipv6 = is_ipv6; + entry->ip = ip; + if (is_ipv6) memcpy(entry->ip6, ip6, 16); + strncpy_s(entry->domain, sizeof(entry->domain), normalized, _TRUNCATE); + entry->source_event_id = source_event_id; + entry->has_source_ttl = has_ttl; + entry->source_ttl_seconds = ttl_seconds; + entry->event_qpc = event_qpc; + entry->consumed_qpc = consumed_qpc; + entry->last_used = (LONG64)GetTickCount64(); + entry->next = g_process_dns_entries; + g_process_dns_entries = entry; + entry->hash_next = g_process_dns_buckets[bucket]; + g_process_dns_buckets[bucket] = entry; + process->count++; + g_process_dns_count++; + assert(process->count <= DNS_PROCESS_MAX_ENTRIES); + assert(g_process_dns_count <= DNS_PROCESS_MAX_TOTAL); + dns_process_mark_dirty(); + } + else dns_process_note_gap(); + return entry != NULL; } -void dns_cache_store_v6(const UINT8 ip6[16], const char *domain) +static BOOL dns_snapshot_load(void) { - if (!domain || domain[0] == '\0') return; - UINT32 bucket = dns_bucket_v6(ip6); - ULONGLONG now = GetTickCount64(); + if (!dns_snapshot_prepare_paths()) + { + log_message("[DNS-CACHE] persistent cache unavailable; keeping records in memory only"); + return FALSE; + } + g_dns_previous_unclean = GetFileAttributesW(g_dns_running_path) != INVALID_FILE_ATTRIBUTES; + DWORD attrs = GetFileAttributesW(g_dns_snapshot_path); + if (attrs == INVALID_FILE_ATTRIBUTES) + { + DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) + goto invalid_snapshot; + if (g_dns_previous_unclean) + { + g_dns_snapshot_incomplete = TRUE; + dns_process_note_gap(); + } + return TRUE; + } + if (attrs & FILE_ATTRIBUTE_REPARSE_POINT) goto invalid_snapshot; - AcquireSRWLockExclusive(&g_dns_cache_lock); - DNS_CACHE_ENTRY_V6 *e = g_dns_cache_v6[bucket]; - while (e) + HANDLE file = CreateFileW(g_dns_snapshot_path, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (file == INVALID_HANDLE_VALUE) goto invalid_snapshot; + + LARGE_INTEGER file_size; + DNS_SNAPSHOT_HEADER header = {0}; + BOOL valid = GetFileSizeEx(file, &file_size) && file_size.QuadPart >= (LONGLONG)sizeof(header) && + file_size.QuadPart <= DNS_SNAPSHOT_MAX_BYTES && + dns_read_all(file, &header, sizeof(header)); + if (valid) + { + ULONGLONG expected = sizeof(header) + (ULONGLONG)header.count * sizeof(DNS_SNAPSHOT_RECORD); + valid = memcmp(header.magic, "PBDNS001", 8) == 0 && + header.version == DNS_SNAPSHOT_VERSION && + header.count <= DNS_PROCESS_MAX_TOTAL && expected == (ULONGLONG)file_size.QuadPart; + ULONGLONG current_boot = dns_boot_estimate(); + ULONGLONG boot_delta = current_boot > header.boot_estimate + ? current_boot - header.boot_estimate + : header.boot_estimate - current_boot; + if (valid && boot_delta > 600000000ULL) valid = FALSE; // 60 seconds of UTC/boot estimate drift. + } + if (!valid) + { + CloseHandle(file); + goto invalid_snapshot; + } + + InterlockedExchange64(&g_process_dns_gaps, (LONG64)header.gaps); + g_dns_snapshot_incomplete = (header.flags & DNS_SNAPSHOT_INCOMPLETE) != 0; + for (DWORD i = 0; i < header.count; i++) { - if (memcmp(e->ip6, ip6, 16) == 0) + DNS_SNAPSHOT_RECORD record; + if (!dns_read_all(file, &record, sizeof(record)) || + !memchr(record.domain, '\0', sizeof(record.domain)) || + (record.family != 4 && record.family != 6)) { - strncpy_s(e->domain, sizeof(e->domain), domain, _TRUNCATE); - e->expire_tick = now + DNS_CACHE_TTL_MS; - ReleaseSRWLockExclusive(&g_dns_cache_lock); - return; + CloseHandle(file); + dns_process_cache_clear(); + goto invalid_snapshot; + } + if (!dns_snapshot_process_matches(&record)) continue; + FILETIME expected_created; + ULARGE_INTEGER created; + created.QuadPart = record.process_created; + expected_created.dwLowDateTime = created.LowPart; + expected_created.dwHighDateTime = created.HighPart; + if (record.family == 4) + { + // The snapshot was accepted only when its boot estimate matches this boot, + // so its QPC timestamps remain comparable to current packet timestamps. + if (dns_process_cache_store(record.pid, &expected_created, FALSE, record.ip, NULL, + record.domain, record.source_event_id, + record.event_qpc, record.consumed_qpc, + record.has_source_ttl, record.source_ttl_seconds)) + dns_process_cache_restore_last_used(&record); + } + else + { + if (dns_process_cache_store(record.pid, &expected_created, TRUE, 0, record.ip6, + record.domain, record.source_event_id, + record.event_qpc, record.consumed_qpc, + record.has_source_ttl, record.source_ttl_seconds)) + dns_process_cache_restore_last_used(&record); } - e = e->next; } - DNS_CACHE_ENTRY_V6 *ne = (DNS_CACHE_ENTRY_V6 *)malloc(sizeof(DNS_CACHE_ENTRY_V6)); - if (ne) + CloseHandle(file); + if (g_dns_previous_unclean) { - memcpy(ne->ip6, ip6, 16); - ne->expire_tick = now + DNS_CACHE_TTL_MS; - strncpy_s(ne->domain, sizeof(ne->domain), domain, _TRUNCATE); - ne->next = g_dns_cache_v6[bucket]; - g_dns_cache_v6[bucket] = ne; + g_dns_snapshot_incomplete = TRUE; + dns_process_note_gap(); } - ReleaseSRWLockExclusive(&g_dns_cache_lock); + InterlockedExchange64(&g_dns_saved_generation, + InterlockedCompareExchange64(&g_dns_generation, 0, 0)); + InterlockedExchange64(&g_dns_dirty_tick, 0); + InterlockedExchange64(&g_dns_last_snapshot_tick, (LONG64)GetTickCount64()); + InterlockedExchange64(&g_dns_snapshot_failed_tick, 0); + return TRUE; + +invalid_snapshot: + g_dns_snapshot_incomplete = TRUE; + dns_process_note_gap(); + log_message("[DNS-CACHE] snapshot rejected (corrupt, stale boot session, or inaccessible)"); + return FALSE; } -BOOL dns_cache_lookup_v6(const UINT8 ip6[16], char *out_domain, size_t out_size) +static void dns_process_cache_restore_last_used(const DNS_SNAPSHOT_RECORD *record) { - UINT32 bucket = dns_bucket_v6(ip6); + BOOL is_ipv6 = record->family == 6; + UINT32 bucket = dns_process_bucket(record->pid, is_ipv6, record->ip, record->ip6); ULONGLONG now = GetTickCount64(); - BOOL found = FALSE; - - AcquireSRWLockShared(&g_dns_cache_lock); - DNS_CACHE_ENTRY_V6 *e = g_dns_cache_v6[bucket]; - while (e) + LONG64 last_used = record->last_used_tick && record->last_used_tick <= now + ? (LONG64)record->last_used_tick : (LONG64)now; + AcquireSRWLockExclusive(&g_process_dns_lock); + for (DNS_HOST_ENTRY *entry = g_process_dns_buckets[bucket]; entry; entry = entry->hash_next) { - if (memcmp(e->ip6, ip6, 16) == 0 && e->expire_tick > now) + if (entry->process->pid == record->pid && entry->is_ipv6 == is_ipv6 && + (is_ipv6 ? memcmp(entry->ip6, record->ip6, 16) == 0 : entry->ip == record->ip) && + strcmp(entry->domain, record->domain) == 0) { - strncpy_s(out_domain, out_size, e->domain, _TRUNCATE); - found = TRUE; + InterlockedExchange64(&entry->last_used, last_used); break; } - e = e->next; } - ReleaseSRWLockShared(&g_dns_cache_lock); + ReleaseSRWLockExclusive(&g_process_dns_lock); +} + +static void dns_snapshot_begin_session(void) +{ + if (!dns_snapshot_prepare_paths()) return; + struct { char magic[8]; DWORD pid; ULONGLONG boot_estimate; } marker = { 0 }; + memcpy(marker.magic, "PBRUN001", 8); + marker.pid = GetCurrentProcessId(); + marker.boot_estimate = dns_boot_estimate(); + HANDLE file = CreateFileW(g_dns_running_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_HIDDEN | FILE_FLAG_WRITE_THROUGH, NULL); + if (file == INVALID_HANDLE_VALUE || !dns_write_all(file, &marker, sizeof(marker)) || !FlushFileBuffers(file)) + { + if (file != INVALID_HANDLE_VALUE) CloseHandle(file); + log_message("[DNS-CACHE] could not write the running marker; crash recovery will be incomplete"); + g_dns_snapshot_incomplete = TRUE; + return; + } + CloseHandle(file); + if (g_dns_previous_unclean) + log_message("[DNS-CACHE] recovered records came from an unclean stop and may have gaps"); +} + +static BOOL dns_snapshot_write(void) +{ + if (!dns_snapshot_prepare_paths()) return FALSE; + UINT32 max_count; + ULONGLONG generation; + AcquireSRWLockShared(&g_process_dns_lock); + max_count = g_process_dns_count; + generation = (ULONGLONG)InterlockedCompareExchange64(&g_dns_generation, 0, 0); + DNS_SNAPSHOT_RECORD *records = max_count + ? (DNS_SNAPSHOT_RECORD *)malloc((size_t)max_count * sizeof(*records)) : NULL; + if (max_count && !records) + { + ReleaseSRWLockShared(&g_process_dns_lock); + return FALSE; + } + UINT32 count = 0; + for (DNS_HOST_ENTRY *entry = g_process_dns_entries; entry && count < max_count; entry = entry->next) + { + DNS_PROCESS_ENTRY *process = entry->process; + if (process->exited || WaitForSingleObject(process->process, 0) != WAIT_TIMEOUT) continue; + DNS_SNAPSHOT_RECORD *record = &records[count++]; + ULARGE_INTEGER created; + created.LowPart = process->created.dwLowDateTime; + created.HighPart = process->created.dwHighDateTime; + ZeroMemory(record, sizeof(*record)); + record->pid = process->pid; + record->process_created = created.QuadPart; + record->source_event_id = entry->source_event_id; + record->has_source_ttl = entry->has_source_ttl; + record->source_ttl_seconds = entry->source_ttl_seconds; + record->event_qpc = entry->event_qpc; + record->consumed_qpc = entry->consumed_qpc; + record->last_used_tick = (ULONGLONG)entry->last_used; + record->family = entry->is_ipv6 ? 6 : 4; + record->ip = entry->ip; + if (entry->is_ipv6) memcpy(record->ip6, entry->ip6, 16); + memcpy(record->domain, entry->domain, sizeof(record->domain)); + } + ReleaseSRWLockShared(&g_process_dns_lock); + + DNS_SNAPSHOT_HEADER header; + memcpy(header.magic, "PBDNS001", 8); + header.version = DNS_SNAPSHOT_VERSION; + header.count = count; + header.boot_estimate = dns_boot_estimate(); + header.gaps = dns_process_cache_gap_count(); + header.flags = g_dns_snapshot_incomplete ? DNS_SNAPSHOT_INCOMPLETE : 0; + + WCHAR temp_path[MAX_PATH]; + if (_snwprintf_s(temp_path, _countof(temp_path), _TRUNCATE, L"%ls.tmp", g_dns_snapshot_path) < 0) + { + free(records); + return FALSE; + } + HANDLE file = CreateFileW(temp_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_WRITE_THROUGH, NULL); + BOOL ok = file != INVALID_HANDLE_VALUE && dns_write_all(file, &header, sizeof(header)); + for (UINT32 i = 0; ok && i < count; i++) + ok = dns_write_all(file, &records[i], sizeof(records[i])); + if (file != INVALID_HANDLE_VALUE) + { + if (ok) ok = FlushFileBuffers(file); + CloseHandle(file); + } + free(records); + if (ok) ok = MoveFileExW(temp_path, g_dns_snapshot_path, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); + if (!ok) + { + DeleteFileW(temp_path); + return FALSE; + } + + InterlockedExchange64(&g_dns_last_snapshot_tick, (LONG64)GetTickCount64()); + InterlockedExchange64(&g_dns_saved_generation, (LONG64)generation); + InterlockedExchange64(&g_dns_snapshot_failed_tick, 0); + return TRUE; +} + +BOOL dns_process_cache_snapshot_tick(BOOL final_snapshot) +{ + if (!g_dns_snapshot_ready || !g_dns_snapshot_path[0]) return FALSE; + ULONGLONG now = GetTickCount64(); + ULONGLONG generation = (ULONGLONG)InterlockedCompareExchange64(&g_dns_generation, 0, 0); + ULONGLONG saved = (ULONGLONG)InterlockedCompareExchange64(&g_dns_saved_generation, 0, 0); + ULONGLONG last = (ULONGLONG)InterlockedCompareExchange64(&g_dns_last_snapshot_tick, 0, 0); + ULONGLONG dirty = (ULONGLONG)InterlockedCompareExchange64(&g_dns_dirty_tick, 0, 0); + ULONGLONG failed = (ULONGLONG)InterlockedCompareExchange64(&g_dns_snapshot_failed_tick, 0, 0); + if (!final_snapshot && (generation == saved || (failed && now - failed < 5000) || + ((now - dirty < 1000) && (!last || now - last < 30000)))) return TRUE; + + if (dns_snapshot_write()) return TRUE; + else + { + InterlockedExchange64(&g_dns_snapshot_failed_tick, (LONG64)now); + log_message("[DNS-CACHE] snapshot write failed; current associations remain in memory"); + return FALSE; + } +} + +void dns_process_cache_end_session(void) +{ + if (!g_dns_running_path[0]) return; + if (!dns_process_cache_snapshot_tick(TRUE)) + { + log_message("[DNS-CACHE] final snapshot failed; retaining running marker for next-start diagnostics"); + return; + } + if (!DeleteFileW(g_dns_running_path) && GetLastError() != ERROR_FILE_NOT_FOUND) + log_message("[DNS-CACHE] could not remove running marker; next start will report an incomplete recovery"); +} + +BOOL dns_process_cache_lookup(DWORD pid, BOOL is_ipv6, UINT32 ip, const UINT8 ip6[16], + ULONGLONG before_qpc, char *out_domain, size_t out_size) +{ + if (pid == 0 || (is_ipv6 && !ip6) || !out_domain || out_size == 0) return FALSE; + DNS_HOST_ENTRY *best = NULL; + UINT32 bucket = dns_process_bucket(pid, is_ipv6, ip, ip6); + AcquireSRWLockShared(&g_process_dns_lock); + for (DNS_HOST_ENTRY *entry = g_process_dns_buckets[bucket]; entry; entry = entry->hash_next) + { + if (entry->process->pid != pid || entry->process->exited || + WaitForSingleObject(entry->process->process, 0) != WAIT_TIMEOUT || + entry->is_ipv6 != is_ipv6 || + (is_ipv6 ? memcmp(entry->ip6, ip6, 16) != 0 : entry->ip != ip) || + (before_qpc && entry->event_qpc > before_qpc)) + continue; + if (!best || entry->event_qpc > best->event_qpc) best = entry; + } + if (best) + { + strncpy_s(out_domain, out_size, best->domain, _TRUNCATE); + InterlockedExchange64(&best->last_used, (LONG64)GetTickCount64()); + } + BOOL found = best != NULL; + ReleaseSRWLockShared(&g_process_dns_lock); return found; } +ULONGLONG dns_process_cache_gap_count(void) +{ + return (ULONGLONG)InterlockedCompareExchange64(&g_process_dns_gaps, 0, 0); +} + // Parse a DNS name (with pointer compression) at msg[*offset] into dst. // Advances *offset past the name on success. BOOL dns_parse_name(const UINT8 *msg, int msg_len, int *offset, char *dst, int dst_len) @@ -171,117 +966,592 @@ BOOL dns_parse_name(const UINT8 *msg, int msg_len, int *offset, char *dst, int d return FALSE; } -// Snoop an inbound DNS response (UDP payload starting at the DNS header). -// For every A-record answer, store ip → qname in the DNS cache. -void snoop_dns_response(const UINT8 *payload, int payload_len) +static BOOL dns_read_question(const UINT8 *payload, int payload_len, UINT16 qdcount, + char qname[256], UINT16 *qtype, UINT16 *qclass, + int *answers_offset) { - if (payload_len < 12) return; + int offset = 12; + if (!qdcount) return FALSE; + for (UINT16 i = 0; i < qdcount; i++) + { + char name[256]; + if (!dns_parse_name(payload, payload_len, &offset, name, sizeof(name)) || + offset > payload_len - 4) + return FALSE; + if (i == 0) + { + strncpy_s(qname, 256, name, _TRUNCATE); + *qtype = ((UINT16)payload[offset] << 8) | payload[offset + 1]; + *qclass = ((UINT16)payload[offset + 2] << 8) | payload[offset + 3]; + } + offset += 4; + } + *answers_offset = offset; + return TRUE; +} - UINT16 flags = ((UINT16)payload[2] << 8) | payload[3]; - if (!(flags & 0x8000)) return; // not a response - if (flags & 0x000F) return; // RCODE != NOERROR +static BOOL dns_read_answer(const UINT8 *payload, int payload_len, int *offset, + char owner[256], UINT16 *type, UINT16 *rclass, + UINT16 *rdlen, DWORD *ttl_seconds, int *rdata_offset) +{ + if (!dns_parse_name(payload, payload_len, offset, owner, 256) || + *offset > payload_len - 10) + return FALSE; + *type = ((UINT16)payload[*offset] << 8) | payload[*offset + 1]; + *rclass = ((UINT16)payload[*offset + 2] << 8) | payload[*offset + 3]; + *ttl_seconds = ((DWORD)payload[*offset + 4] << 24) | + ((DWORD)payload[*offset + 5] << 16) | + ((DWORD)payload[*offset + 6] << 8) | payload[*offset + 7]; + *rdlen = ((UINT16)payload[*offset + 8] << 8) | payload[*offset + 9]; + *rdata_offset = *offset + 10; + if (*rdata_offset > payload_len - *rdlen) return FALSE; + *offset = *rdata_offset + *rdlen; + return TRUE; +} + +static BOOL dns_query_same(const DNS_NETWORK_QUERY *query, BOOL is_tcp, BOOL is_ipv6, + const UINT8 *src_ip, const UINT8 *dst_ip, + UINT16 src_port, UINT16 dst_port, UINT16 id, + UINT16 qtype, UINT16 qclass, const char *qname) +{ + size_t addr_len = is_ipv6 ? 16 : 4; + return query->pid && query->is_tcp == is_tcp && query->is_ipv6 == is_ipv6 && query->id == id && + query->src_port == src_port && query->dst_port == dst_port && + query->qtype == qtype && query->qclass == qclass && + memcmp(query->src_ip, src_ip, addr_len) == 0 && + memcmp(query->dst_ip, dst_ip, addr_len) == 0 && + _stricmp(query->qname, qname) == 0; +} +static void dns_network_record_query(BOOL is_tcp, BOOL is_ipv6, const UINT8 *src_ip, + const UINT8 *dst_ip, UINT16 src_port, + UINT16 dst_port, const UINT8 *payload, + int payload_len) +{ + if (payload_len < 12) return; + UINT16 flags = ((UINT16)payload[2] << 8) | payload[3]; + if ((flags & 0x8000) || (flags & 0x7800) || (flags & 0x000f)) return; + + char qname[256] = {0}; + int answers_offset = 0; UINT16 qdcount = ((UINT16)payload[4] << 8) | payload[5]; - UINT16 ancount = ((UINT16)payload[6] << 8) | payload[7]; - if (ancount == 0) return; + UINT16 qtype = 0, qclass = 0; + if (!dns_read_question(payload, payload_len, qdcount, qname, &qtype, + &qclass, &answers_offset)) return; - int offset = 12; + DWORD pid; + if (is_ipv6) + pid = is_tcp ? get_process_id_from_connection_v6(src_ip, src_port) + : get_process_id_from_udp_connection_v6(src_ip, src_port); + else + { + UINT32 src_v4; + memcpy(&src_v4, src_ip, sizeof(src_v4)); + pid = is_tcp ? get_process_id_from_connection(src_v4, src_port) + : get_process_id_from_udp_connection(src_v4, src_port); + } + if (!pid) return; - // Extract the first question's name as the canonical hostname for this answer. - char qname[256]; - if (!dns_parse_name(payload, payload_len, &offset, qname, sizeof(qname))) return; - offset += 4; // QTYPE + QCLASS + FILETIME created; + HANDLE process = NULL; + if (!get_process_identity(pid, &created, &process)) return; + BOOL alive = WaitForSingleObject(process, 0) == WAIT_TIMEOUT; + CloseHandle(process); + if (!alive) return; - // Skip any remaining questions - for (int q = 1; q < qdcount && offset < payload_len; q++) + UINT16 id = ((UINT16)payload[0] << 8) | payload[1]; + ULONGLONG now = GetTickCount64(); + int slot = -1, empty = -1, oldest = 0; + for (int i = 0; i < DNS_NETWORK_MAX_QUERIES; i++) { - char tmp[256]; - if (!dns_parse_name(payload, payload_len, &offset, tmp, sizeof(tmp))) return; - offset += 4; + DNS_NETWORK_QUERY *query = &g_dns_network_queries[i]; + if (query->pid && now - query->tick > DNS_NETWORK_QUERY_TTL_MS) + ZeroMemory(query, sizeof(*query)); + if (query->pid && dns_query_same(query, is_tcp, is_ipv6, src_ip, dst_ip, + src_port, dst_port, id, qtype, qclass, qname)) + { + if (query->pid == pid && + CompareFileTime(&query->process_created, &created) == 0) + query->tick = now; // retransmission of the same transaction + else + { + query->ambiguous = TRUE; // same wire key was reused by another process instance + query->tick = now; + } + return; + } + if (!query->pid && empty < 0) empty = i; + if (g_dns_network_queries[i].tick < g_dns_network_queries[oldest].tick) + oldest = i; } + if (slot < 0) + { + if (empty >= 0) slot = empty; + else + { + slot = oldest; + dns_process_cache_note_gap(1); + } + } + DNS_NETWORK_QUERY *query = &g_dns_network_queries[slot]; + ZeroMemory(query, sizeof(*query)); + query->pid = pid; + query->process_created = created; + query->is_ipv6 = is_ipv6; + query->is_tcp = is_tcp; + query->id = id; + query->src_port = src_port; + query->dst_port = dst_port; + query->qtype = qtype; + query->qclass = qclass; + memcpy(query->src_ip, src_ip, is_ipv6 ? 16 : 4); + memcpy(query->dst_ip, dst_ip, is_ipv6 ? 16 : 4); + query->tick = now; + strncpy_s(query->qname, sizeof(query->qname), qname, _TRUNCATE); +} - // Parse answer RRs - for (int i = 0; i < ancount && offset < payload_len; i++) +static void dns_network_record_response(BOOL is_tcp, BOOL is_ipv6, const UINT8 *src_ip, + const UINT8 *dst_ip, UINT16 src_port, + UINT16 dst_port, const UINT8 *payload, + int payload_len, ULONGLONG packet_qpc) +{ + if (payload_len < 12) return; + UINT16 flags = ((UINT16)payload[2] << 8) | payload[3]; + if (!(flags & 0x8000) || (flags & 0x7800) || (flags & 0x000f)) return; + UINT16 qdcount = ((UINT16)payload[4] << 8) | payload[5]; + UINT16 ancount = ((UINT16)payload[6] << 8) | payload[7]; + if (!ancount || ancount > 512) return; + + char qname[256] = {0}; + int answers_offset = 0; + UINT16 qtype = 0, qclass = 0; + if (!dns_read_question(payload, payload_len, qdcount, qname, &qtype, + &qclass, &answers_offset)) return; + UINT16 id = ((UINT16)payload[0] << 8) | payload[1]; + ULONGLONG now = GetTickCount64(); + DNS_NETWORK_QUERY matched = {0}; + for (int i = 0; i < DNS_NETWORK_MAX_QUERIES; i++) { - char rname[256]; - if (!dns_parse_name(payload, payload_len, &offset, rname, sizeof(rname))) return; - if (offset + 10 > payload_len) return; + DNS_NETWORK_QUERY *query = &g_dns_network_queries[i]; + if (query->pid && now - query->tick > DNS_NETWORK_QUERY_TTL_MS) + ZeroMemory(query, sizeof(*query)); + size_t addr_len = is_ipv6 ? 16 : 4; + if (query->pid && query->is_tcp == is_tcp && query->is_ipv6 == is_ipv6 && query->id == id && + query->src_port == dst_port && query->dst_port == src_port && + query->qtype == qtype && query->qclass == qclass && + memcmp(query->src_ip, dst_ip, addr_len) == 0 && + memcmp(query->dst_ip, src_ip, addr_len) == 0 && + _stricmp(query->qname, qname) == 0) + { + matched = *query; + ZeroMemory(query, sizeof(*query)); + break; + } + } + if (!matched.pid || matched.ambiguous) return; - UINT16 rtype = ((UINT16)payload[offset + 0] << 8) | payload[offset + 1]; - UINT16 rclass = ((UINT16)payload[offset + 2] << 8) | payload[offset + 3]; - UINT16 rdlen = ((UINT16)payload[offset + 8] << 8) | payload[offset + 9]; - offset += 10; - if (offset + rdlen > payload_len) return; + char aliases[17][256] = {{0}}; + strncpy_s(aliases[0], sizeof(aliases[0]), qname, _TRUNCATE); + int alias_count = 1; + for (int depth = 0; depth < 16; depth++) + { + int offset = answers_offset; + BOOL added = FALSE; + for (UINT16 i = 0; i < ancount; i++) + { + char owner[256]; UINT16 type, rclass, rdlen; DWORD ttl; int rdata; + if (!dns_read_answer(payload, payload_len, &offset, owner, &type, + &rclass, &rdlen, &ttl, &rdata)) return; + if (type == 5 && rclass == 1 && _stricmp(owner, aliases[alias_count - 1]) == 0) + { + char target[256] = {0}; + int cname_offset = rdata; + if (!dns_parse_name(payload, payload_len, &cname_offset, target, sizeof(target)) || + cname_offset > rdata + rdlen || !target[0]) + continue; + BOOL seen = FALSE; + for (int j = 0; j < alias_count; j++) + if (_stricmp(aliases[j], target) == 0) { seen = TRUE; break; } + if (!seen) + { + strncpy_s(aliases[alias_count++], sizeof(aliases[0]), target, _TRUNCATE); + added = TRUE; + } + break; + } + } + if (!added) break; + } - if (rtype == 1 /* A */ && rclass == 1 /* IN */ && rdlen == 4) + int offset = answers_offset; + LARGE_INTEGER consumed; + QueryPerformanceCounter(&consumed); + ULONGLONG event_qpc = packet_qpc ? packet_qpc : (ULONGLONG)consumed.QuadPart; + for (UINT16 i = 0; i < ancount; i++) + { + char owner[256]; UINT16 type, rclass, rdlen; DWORD ttl; int rdata; + if (!dns_read_answer(payload, payload_len, &offset, owner, &type, + &rclass, &rdlen, &ttl, &rdata)) return; + BOOL in_chain = FALSE; + for (int j = 0; j < alias_count; j++) + if (_stricmp(owner, aliases[j]) == 0) { in_chain = TRUE; break; } + if (!in_chain || rclass != 1) continue; + if (type == 1 && rdlen == 4) { UINT32 ip; - memcpy(&ip, &payload[offset], 4); // network-byte-order, matches ip_header->DstAddr - dns_cache_store(ip, qname); + memcpy(&ip, &payload[rdata], sizeof(ip)); + dns_process_cache_store(matched.pid, &matched.process_created, FALSE, ip, NULL, + qname, DNS_SOURCE_NETWORK_DNS, event_qpc, + (ULONGLONG)consumed.QuadPart, TRUE, ttl); } - else if (rtype == 28 /* AAAA */ && rclass == 1 /* IN */ && rdlen == 16) + else if (type == 28 && rdlen == 16) { - dns_cache_store_v6(&payload[offset], qname); + dns_process_cache_store(matched.pid, &matched.process_created, TRUE, 0, + &payload[rdata], qname, DNS_SOURCE_NETWORK_DNS, + event_qpc, (ULONGLONG)consumed.QuadPart, TRUE, ttl); } - offset += rdlen; } } -// Prune expired DNS-snoop entries (IPv4 + IPv6) so the caches don't grow without bound. -void cleanup_stale_dns_cache(void) +typedef void (*DNS_TCP_BYTE_CALLBACK)(void *context, UINT8 value); + +static void dns_tcp_direction_free(DNS_TCP_DIRECTION *direction) { - ULONGLONG now = GetTickCount64(); + free(direction->window); + free(direction->present); + free(direction->message); + ZeroMemory(direction, sizeof(*direction)); +} + +static void dns_tcp_direction_reset(DNS_TCP_DIRECTION *direction) +{ + if (direction->window) ZeroMemory(direction->window, DNS_TCP_WINDOW_SIZE); + if (direction->present) ZeroMemory(direction->present, DNS_TCP_BITMAP_SIZE); + direction->initialized = FALSE; + direction->next_seq = 0; + direction->ring_head = 0; + direction->pending_bytes = 0; + direction->message_size = 0; + direction->message_length = 0; + direction->prefix_length = 0; +} + +static BOOL dns_tcp_direction_prepare(DNS_TCP_DIRECTION *direction) +{ + if (!direction->window) direction->window = (UINT8 *)malloc(DNS_TCP_WINDOW_SIZE); + if (!direction->present) direction->present = (UINT8 *)calloc(DNS_TCP_BITMAP_SIZE, 1); + if (!direction->message) direction->message = (UINT8 *)malloc(DNS_TCP_MESSAGE_MAX); + if (direction->window && direction->present && direction->message) return TRUE; + dns_tcp_direction_free(direction); + return FALSE; +} + +static BOOL dns_tcp_bit_is_set(const UINT8 *bitmap, UINT32 bit) +{ + return (bitmap[bit >> 3] & (UINT8)(1u << (bit & 7))) != 0; +} - AcquireSRWLockExclusive(&g_dns_cache_lock); - for (int i = 0; i < DNS_CACHE_BUCKETS; i++) +static void dns_tcp_bit_set(UINT8 *bitmap, UINT32 bit) +{ + bitmap[bit >> 3] |= (UINT8)(1u << (bit & 7)); +} + +static void dns_tcp_bit_clear(UINT8 *bitmap, UINT32 bit) +{ + bitmap[bit >> 3] &= (UINT8)~(1u << (bit & 7)); +} + +static BOOL dns_tcp_reassemble(DNS_TCP_DIRECTION *direction, UINT32 seq, BOOL syn, + const UINT8 *payload, int payload_len, + DNS_TCP_BYTE_CALLBACK consume, void *context) +{ + if (syn) { - DNS_CACHE_ENTRY **pp = &g_dns_cache[i]; - while (*pp != NULL) + if (!direction->initialized) { - if ((*pp)->expire_tick <= now) - { - DNS_CACHE_ENTRY *to_free = *pp; - *pp = (*pp)->next; - free(to_free); - } - else - { - pp = &(*pp)->next; - } + direction->next_seq = seq + 1; + direction->initialized = TRUE; } + seq++; + } + else if (!direction->initialized) + { + direction->next_seq = seq; + direction->initialized = TRUE; + } + if (!payload || payload_len <= 0) return TRUE; + if (!dns_tcp_direction_prepare(direction)) return FALSE; - DNS_CACHE_ENTRY_V6 **pp6 = &g_dns_cache_v6[i]; - while (*pp6 != NULL) + INT32 delta = (INT32)(seq - direction->next_seq); + if (delta < 0) + { + UINT32 overlap = direction->next_seq - seq; + if (overlap >= (UINT32)payload_len) return TRUE; + seq += overlap; + payload += overlap; + payload_len -= (int)overlap; + delta = 0; + } + if ((UINT32)delta >= DNS_TCP_WINDOW_SIZE || + (UINT32)payload_len > DNS_TCP_WINDOW_SIZE - (UINT32)delta) + { + dns_process_cache_note_gap(1); + dns_tcp_direction_reset(direction); + direction->initialized = TRUE; + direction->next_seq = seq; + if ((UINT32)payload_len > DNS_TCP_WINDOW_SIZE) return FALSE; + delta = 0; + } + + for (int i = 0; i < payload_len; i++) + { + UINT32 slot = (direction->ring_head + (UINT32)delta + (UINT32)i) & + (DNS_TCP_WINDOW_SIZE - 1); + if (!dns_tcp_bit_is_set(direction->present, slot)) { - if ((*pp6)->expire_tick <= now) - { - DNS_CACHE_ENTRY_V6 *to_free = *pp6; - *pp6 = (*pp6)->next; - free(to_free); - } - else + direction->window[slot] = payload[i]; + dns_tcp_bit_set(direction->present, slot); + direction->pending_bytes++; + } + } + + while (dns_tcp_bit_is_set(direction->present, direction->ring_head)) + { + UINT8 value = direction->window[direction->ring_head]; + dns_tcp_bit_clear(direction->present, direction->ring_head); + direction->ring_head = (direction->ring_head + 1) & (DNS_TCP_WINDOW_SIZE - 1); + direction->pending_bytes--; + direction->next_seq++; + consume(context, value); + } + return TRUE; +} + +typedef struct DNS_TCP_PARSE_CONTEXT { + DNS_TCP_FLOW *flow; + int direction; + ULONGLONG packet_qpc; +} DNS_TCP_PARSE_CONTEXT; + +static void dns_tcp_consume_dns_byte(void *opaque, UINT8 value) +{ + DNS_TCP_PARSE_CONTEXT *context = (DNS_TCP_PARSE_CONTEXT *)opaque; + DNS_TCP_DIRECTION *direction = &context->flow->direction[context->direction]; + if (direction->prefix_length < 2) + { + direction->prefix[direction->prefix_length++] = value; + if (direction->prefix_length == 2) + { + direction->message_size = ((UINT16)direction->prefix[0] << 8) | + direction->prefix[1]; + if (direction->message_size < 12) { - pp6 = &(*pp6)->next; + direction->prefix_length = 0; + direction->message_size = 0; + dns_process_cache_note_gap(1); } } + return; } - ReleaseSRWLockExclusive(&g_dns_cache_lock); + + direction->message[direction->message_length++] = value; + if (direction->message_length != direction->message_size) return; + + if (context->direction == 0) + dns_network_record_query(TRUE, context->flow->is_ipv6, + context->flow->client_ip, context->flow->server_ip, + context->flow->client_port, context->flow->server_port, + direction->message, direction->message_length); + else + dns_network_record_response(TRUE, context->flow->is_ipv6, + context->flow->server_ip, context->flow->client_ip, + context->flow->server_port, context->flow->client_port, + direction->message, direction->message_length, + context->packet_qpc); + + direction->prefix_length = 0; + direction->message_size = 0; + direction->message_length = 0; } -// Flush the Windows DNS resolver cache so applications re-resolve hostnames on the -// wire, where our port-53 snoop can capture the IP->hostname mapping. Without this a -// domain that was resolved before a domain rule existed would have no cached mapping -// and the rule could not match. DnsFlushResolverCache is loaded dynamically so we -// avoid a hard dnsapi.lib dependency (keeps the MinGW/GCC build path unchanged). -void flush_dns_resolver_cache(void) +static BOOL dns_tcp_flow_incomplete(const DNS_TCP_FLOW *flow) { - HMODULE dnsapi = LoadLibraryA("dnsapi.dll"); - if (dnsapi == NULL) - return; - typedef BOOL (WINAPI *DnsFlushFn)(void); - DnsFlushFn fn = (DnsFlushFn)GetProcAddress(dnsapi, "DnsFlushResolverCache"); - if (fn != NULL) - fn(); - FreeLibrary(dnsapi); + for (int i = 0; i < 2; i++) + { + const DNS_TCP_DIRECTION *direction = &flow->direction[i]; + if (direction->pending_bytes || direction->prefix_length || direction->message_length) + return TRUE; + } + return FALSE; +} + +static void dns_tcp_flow_free(DNS_TCP_FLOW *flow) +{ + dns_tcp_direction_free(&flow->direction[0]); + dns_tcp_direction_free(&flow->direction[1]); + ZeroMemory(flow, sizeof(*flow)); +} + +static BOOL dns_tcp_flow_matches(const DNS_TCP_FLOW *flow, BOOL is_ipv6, + const UINT8 *client_ip, const UINT8 *server_ip, + UINT16 client_port, UINT16 server_port) +{ + size_t ip_len = is_ipv6 ? 16 : 4; + return flow->used && flow->is_ipv6 == is_ipv6 && + flow->client_port == client_port && flow->server_port == server_port && + memcmp(flow->client_ip, client_ip, ip_len) == 0 && + memcmp(flow->server_ip, server_ip, ip_len) == 0; +} + +static DNS_TCP_FLOW *dns_tcp_flow_get(BOOL is_ipv6, const UINT8 *src_ip, + const UINT8 *dst_ip, UINT16 src_port, + UINT16 dst_port, BOOL create) +{ + if ((src_port == 53) == (dst_port == 53)) return NULL; + BOOL server_to_client = src_port == 53; + const UINT8 *client_ip = server_to_client ? dst_ip : src_ip; + const UINT8 *server_ip = server_to_client ? src_ip : dst_ip; + UINT16 client_port = server_to_client ? dst_port : src_port; + UINT16 server_port = server_to_client ? src_port : dst_port; + ULONGLONG now = GetTickCount64(); + int empty = -1, oldest = 0; + + for (int i = 0; i < DNS_TCP_MAX_FLOWS; i++) + { + DNS_TCP_FLOW *flow = &g_dns_tcp_flows[i]; + if (flow->used && now - flow->tick > DNS_TCP_FLOW_TTL_MS) + { + if (dns_tcp_flow_incomplete(flow)) dns_process_cache_note_gap(1); + dns_tcp_flow_free(flow); + } + if (dns_tcp_flow_matches(flow, is_ipv6, client_ip, server_ip, + client_port, server_port)) + { + flow->tick = now; + return flow; + } + if (!flow->used && empty < 0) empty = i; + if (g_dns_tcp_flows[i].tick < g_dns_tcp_flows[oldest].tick) oldest = i; + } + if (!create) return NULL; + + int slot = empty; + if (slot < 0) + { + slot = oldest; + if (dns_tcp_flow_incomplete(&g_dns_tcp_flows[slot])) + dns_process_cache_note_gap(1); + dns_tcp_flow_free(&g_dns_tcp_flows[slot]); + dns_process_cache_note_gap(1); + } + DNS_TCP_FLOW *flow = &g_dns_tcp_flows[slot]; + ZeroMemory(flow, sizeof(*flow)); + flow->used = TRUE; + flow->is_ipv6 = is_ipv6; + memcpy(flow->client_ip, client_ip, is_ipv6 ? 16 : 4); + memcpy(flow->server_ip, server_ip, is_ipv6 ? 16 : 4); + flow->client_port = client_port; + flow->server_port = server_port; + flow->tick = now; + return flow; +} + +void dns_network_observe_tcp(BOOL is_ipv6, const UINT8 *src_ip, + const UINT8 *dst_ip, UINT16 src_port, UINT16 dst_port, + UINT32 seq, UINT8 flags, const UINT8 *payload, + int payload_len, ULONGLONG packet_qpc) +{ + if (!g_dns_tcp_reassembly_ready || !src_ip || !dst_ip || + ((src_port == 53) == (dst_port == 53))) return; + + DNS_TCP_FLOW *flow = dns_tcp_flow_get(is_ipv6, src_ip, dst_ip, + src_port, dst_port, TRUE); + if (!flow) return; + BOOL server_to_client = src_port == 53; + int direction_index = server_to_client ? 1 : 0; + BOOL syn = (flags & 0x02) != 0; + BOOL ack = (flags & 0x10) != 0; + BOOL reset = (flags & 0x04) != 0; + BOOL fin = (flags & 0x01) != 0; + + if (syn && !ack) + { + if (dns_tcp_flow_incomplete(flow)) dns_process_cache_note_gap(1); + flow->fin_mask = 0; + dns_tcp_direction_reset(&flow->direction[1 - direction_index]); + dns_tcp_direction_reset(&flow->direction[direction_index]); + flow->direction[direction_index].initialized = TRUE; + flow->direction[direction_index].next_seq = seq + 1; + } + DNS_TCP_PARSE_CONTEXT context = { flow, direction_index, packet_qpc }; + if (!dns_tcp_reassemble(&flow->direction[direction_index], seq, syn, + payload, payload_len, dns_tcp_consume_dns_byte, &context)) + { + dns_process_cache_note_gap(1); + dns_tcp_direction_reset(&flow->direction[direction_index]); + } + if (fin) flow->fin_mask |= (UINT8)(1u << direction_index); + if (reset) + { + if (dns_tcp_flow_incomplete(flow)) dns_process_cache_note_gap(1); + dns_tcp_flow_free(flow); + } + else if (flow->fin_mask == 3) + { + if (dns_tcp_flow_incomplete(flow)) dns_process_cache_note_gap(1); + dns_tcp_flow_free(flow); + } +} + +typedef struct DNS_TCP_SELFTEST_OUTPUT { + UINT8 bytes[6]; + UINT32 count; +} DNS_TCP_SELFTEST_OUTPUT; + +static void dns_tcp_selftest_collect(void *opaque, UINT8 value) +{ + DNS_TCP_SELFTEST_OUTPUT *output = (DNS_TCP_SELFTEST_OUTPUT *)opaque; + if (output->count < _countof(output->bytes)) output->bytes[output->count++] = value; +} + +static BOOL dns_tcp_reassembly_selftest(void) +{ + static const UINT8 first[] = { 0, 12 }; + static const UINT8 last[] = { 3, 4 }; + static const UINT8 middle[] = { 1, 2 }; + static const UINT8 expected[] = { 0, 12, 1, 2, 3, 4 }; + DNS_TCP_DIRECTION direction = {0}; + DNS_TCP_SELFTEST_OUTPUT output = {0}; + BOOL ok = dns_tcp_reassemble(&direction, 100, FALSE, first, sizeof(first), + dns_tcp_selftest_collect, &output) && + dns_tcp_reassemble(&direction, 104, FALSE, last, sizeof(last), + dns_tcp_selftest_collect, &output) && + dns_tcp_reassemble(&direction, 102, FALSE, middle, sizeof(middle), + dns_tcp_selftest_collect, &output) && + output.count == _countof(expected) && + memcmp(output.bytes, expected, sizeof(expected)) == 0; + dns_tcp_direction_free(&direction); + return ok; +} + +void dns_network_observe_udp(BOOL outbound, BOOL is_ipv6, const UINT8 *src_ip, + const UINT8 *dst_ip, UINT16 src_port, UINT16 dst_port, + const UINT8 *payload, int payload_len, ULONGLONG packet_qpc) +{ + if (!src_ip || !dst_ip || !payload || payload_len < 12 || + ((src_port == 53) == (dst_port == 53))) return; + if (outbound) + dns_network_record_query(FALSE, is_ipv6, src_ip, dst_ip, src_port, dst_port, + payload, payload_len); + else + dns_network_record_response(FALSE, is_ipv6, src_ip, dst_ip, src_port, dst_port, + payload, payload_len, packet_qpc); +} + +void dns_network_query_reset(void) +{ + ZeroMemory(g_dns_network_queries, sizeof(g_dns_network_queries)); + for (int i = 0; i < DNS_TCP_MAX_FLOWS; i++) + dns_tcp_flow_free(&g_dns_tcp_flows[i]); } diff --git a/Windows/src/pb_dns_etw.c b/Windows/src/pb_dns_etw.c new file mode 100644 index 0000000..a64a7bf --- /dev/null +++ b/Windows/src/pb_dns_etw.c @@ -0,0 +1,1042 @@ +#include "pb_internal.h" +#include +#include +#include + +#pragma comment(lib, "tdh.lib") + +#define DNS_ETW_DECISION_WAIT_MS 5 +#define DNS_ETW_PENDING_MAX 256 +#define DNS_ETW_PENDING_TTL_MS 5000 +#define DNS_ETW_PENDING_FLUSH_INTERVAL_MS 20 +#define DNS_ETW_FALLBACK_FLUSH_INTERVAL_MS 100 + +static const GUID g_dns_client_provider = + { 0x1c95126e, 0x7eea, 0x49a9, { 0xa3, 0xfe, 0xa3, 0x78, 0xb0, 0x3d, 0xdb, 0x4d } }; + +typedef struct DNS_ETW_STATE { + TRACEHANDLE session; + TRACEHANDLE consumer; + HANDLE thread; + HANDLE flush_thread; + HANDLE selftest_thread; + DWORD selftest_pid; + HANDLE event_ready; + HANDLE flush_request; + HANDLE flush_done; + EVENT_TRACE_PROPERTIES *properties; + WCHAR name[96]; + volatile LONG stopping; + volatile LONG64 starts; + volatile LONG64 starts_pid4; + volatile LONG64 starts_pid0; + volatile LONG64 events; + volatile LONG64 mapped; + volatile LONG64 pid4; + volatile LONG64 pid0; + volatile LONG64 doh; + volatile LONG64 dot; + volatile LONG64 doh_pid4; + volatile LONG64 dot_pid4; + volatile LONG64 flush_requests; + volatile LONG64 flush_successes; + volatile LONG64 flush_failures; + volatile LONG64 lookup_waits; + volatile LONG64 lookup_hits; + volatile LONG64 lookup_timeouts; + volatile LONG64 last_flush_request_ms; + volatile LONG64 no_results_field; + volatile LONG64 unparsed_results; + volatile LONG sample_logged; + volatile LONG dumps_logged; + volatile LONG first_dumps_logged; + volatile LONG mapped_dumped; + volatile LONG selftest_dumped; + volatile LONG empty_lines_logged; + volatile LONG mapped_lines_logged; + volatile LONG64 status_ok; + volatile LONG64 status_failed; + volatile LONG64 last_failed_status; + volatile LONG64 latency[8]; + volatile LONG64 flush_latency[8]; + volatile LONG64 wait_latency[8]; + volatile LONG flush_status; + LARGE_INTEGER qpc_frequency; +} DNS_ETW_STATE; + +static DNS_ETW_STATE g_dns_etw; + +typedef struct DNS_ETW_PENDING_QUERY { + BOOL used; + DWORD pid; + FILETIME process_created; + ULONGLONG event_qpc; + ULONGLONG token; +} DNS_ETW_PENDING_QUERY; + +static SRWLOCK g_dns_etw_pending_lock = SRWLOCK_INIT; +static DNS_ETW_PENDING_QUERY g_dns_etw_pending[DNS_ETW_PENDING_MAX]; +static volatile LONG64 g_dns_etw_pending_token; + +static BOOL dns_etw_pending_expired(ULONGLONG event_qpc, ULONGLONG now_qpc) +{ + if (!event_qpc || now_qpc < event_qpc || g_dns_etw.qpc_frequency.QuadPart <= 0) + return FALSE; + ULONGLONG ttl = (ULONGLONG)g_dns_etw.qpc_frequency.QuadPart * DNS_ETW_PENDING_TTL_MS / 1000; + return now_qpc - event_qpc > ttl; +} + +static ULONGLONG dns_etw_pending_prune_locked(ULONGLONG now_qpc) +{ + ULONGLONG expired = 0; + for (size_t i = 0; i < _countof(g_dns_etw_pending); i++) + { + DNS_ETW_PENDING_QUERY *entry = &g_dns_etw_pending[i]; + if (entry->used && dns_etw_pending_expired(entry->event_qpc, now_qpc)) + { + entry->used = FALSE; + expired++; + } + } + return expired; +} + +static void dns_etw_pending_start(DWORD pid, ULONGLONG event_qpc) +{ + FILETIME created; + HANDLE process = NULL; + if (!get_process_identity(pid, &created, &process)) + { + dns_process_cache_note_gap(1); + return; + } + CloseHandle(process); + + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + DNS_ETW_PENDING_QUERY *slot = NULL; + DNS_ETW_PENDING_QUERY *oldest = NULL; + AcquireSRWLockExclusive(&g_dns_etw_pending_lock); + ULONGLONG expired = dns_etw_pending_prune_locked((ULONGLONG)now.QuadPart); + for (size_t i = 0; i < _countof(g_dns_etw_pending); i++) + { + DNS_ETW_PENDING_QUERY *entry = &g_dns_etw_pending[i]; + if (!entry->used) { slot = entry; break; } + if (!oldest || entry->event_qpc < oldest->event_qpc) oldest = entry; + } + BOOL evicted = slot == NULL; + if (!slot) slot = oldest; + if (slot) + { + slot->used = TRUE; + slot->pid = pid; + slot->process_created = created; + slot->event_qpc = event_qpc; + slot->token = (ULONGLONG)InterlockedIncrement64(&g_dns_etw_pending_token); + } + ReleaseSRWLockExclusive(&g_dns_etw_pending_lock); + if (expired) dns_process_cache_note_gap(expired); + if (evicted) dns_process_cache_note_gap(1); +} + +static BOOL dns_etw_pending_for_pid(DWORD pid, ULONGLONG before_qpc) +{ + BOOL found = FALSE; + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + AcquireSRWLockExclusive(&g_dns_etw_pending_lock); + ULONGLONG expired = dns_etw_pending_prune_locked((ULONGLONG)now.QuadPart); + for (size_t i = 0; i < _countof(g_dns_etw_pending); i++) + { + if (g_dns_etw_pending[i].used && g_dns_etw_pending[i].pid == pid && + (!before_qpc || g_dns_etw_pending[i].event_qpc <= before_qpc)) + { + found = TRUE; + break; + } + } + ReleaseSRWLockExclusive(&g_dns_etw_pending_lock); + if (expired) dns_process_cache_note_gap(expired); + return found; +} + +static BOOL dns_etw_pending_match(DWORD pid, ULONGLONG event_qpc, + FILETIME *process_created, ULONGLONG *token) +{ + DNS_ETW_PENDING_QUERY *match = NULL; + AcquireSRWLockShared(&g_dns_etw_pending_lock); + for (size_t i = 0; i < _countof(g_dns_etw_pending); i++) + { + DNS_ETW_PENDING_QUERY *entry = &g_dns_etw_pending[i]; + if (entry->used && entry->pid == pid && entry->event_qpc <= event_qpc && + !dns_etw_pending_expired(entry->event_qpc, event_qpc) && + (!match || entry->event_qpc > match->event_qpc)) + match = entry; + } + if (match) + { + *process_created = match->process_created; + *token = match->token; + } + ReleaseSRWLockShared(&g_dns_etw_pending_lock); + return match != NULL; +} + +static void dns_etw_pending_complete(ULONGLONG token) +{ + AcquireSRWLockExclusive(&g_dns_etw_pending_lock); + for (size_t i = 0; i < _countof(g_dns_etw_pending); i++) + { + DNS_ETW_PENDING_QUERY *entry = &g_dns_etw_pending[i]; + if (entry->used && entry->token == token) + { + entry->used = FALSE; + break; + } + } + ReleaseSRWLockExclusive(&g_dns_etw_pending_lock); +} + +static void dns_etw_record_latency(volatile LONG64 buckets[8], LONGLONG ticks) +{ + if (ticks < 0) ticks = 0; + if (g_dns_etw.qpc_frequency.QuadPart <= 0) return; + ULONGLONG us = (ULONGLONG)ticks * 1000000 / + (ULONGLONG)g_dns_etw.qpc_frequency.QuadPart; + static const ULONGLONG upper_us[] = { 100, 500, 1000, 5000, 10000, 50000, 100000 }; + LONG bucket = 7; + for (LONG i = 0; i < 7; i++) if (us <= upper_us[i]) { bucket = i; break; } + InterlockedIncrement64(&buckets[bucket]); +} + +static DWORD dns_etw_elapsed_ms(LARGE_INTEGER start) +{ + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + if (now.QuadPart <= start.QuadPart || g_dns_etw.qpc_frequency.QuadPart <= 0) + return 0; + ULONGLONG ticks = (ULONGLONG)(now.QuadPart - start.QuadPart); + return (DWORD)((ticks * 1000 + (ULONGLONG)g_dns_etw.qpc_frequency.QuadPart - 1) / + (ULONGLONG)g_dns_etw.qpc_frequency.QuadPart); +} + +static BOOL dns_etw_schedule_flush(BOOL has_pending_query) +{ + ULONGLONG now = GetTickCount64(); + ULONGLONG interval = has_pending_query + ? DNS_ETW_PENDING_FLUSH_INTERVAL_MS : DNS_ETW_FALLBACK_FLUSH_INTERVAL_MS; + + // A single packet thread calls lookups. Reuse an in-flight flush, but do not make + // another connection wait for it; fallback flushes are only for future lookups. + if (WaitForSingleObject(g_dns_etw.flush_done, 0) != WAIT_OBJECT_0) + return FALSE; + + LONG64 previous = InterlockedCompareExchange64(&g_dns_etw.last_flush_request_ms, 0, 0); + if (previous != 0 && now - (ULONGLONG)previous < interval) + return FALSE; + if (InterlockedCompareExchange64(&g_dns_etw.last_flush_request_ms, + (LONG64)now, previous) != previous) + return FALSE; + + ResetEvent(g_dns_etw.flush_done); + if (!SetEvent(g_dns_etw.flush_request)) + { + InterlockedExchange(&g_dns_etw.flush_status, (LONG)ERROR_INVALID_HANDLE); + SetEvent(g_dns_etw.flush_done); + return FALSE; + } + InterlockedIncrement64(&g_dns_etw.flush_requests); + return TRUE; +} + +static void dns_etw_close_events(void) +{ + if (g_dns_etw.event_ready) CloseHandle(g_dns_etw.event_ready); + if (g_dns_etw.flush_request) CloseHandle(g_dns_etw.flush_request); + if (g_dns_etw.flush_done) CloseHandle(g_dns_etw.flush_done); + g_dns_etw.event_ready = NULL; + g_dns_etw.flush_request = NULL; + g_dns_etw.flush_done = NULL; +} + +static WCHAR *etw_read_string(EVENT_RECORD *record, const WCHAR *name) +{ + PROPERTY_DATA_DESCRIPTOR desc; + ULONG size = 0; + desc.PropertyName = (ULONGLONG)(ULONG_PTR)name; + desc.ArrayIndex = ULONG_MAX; + desc.Reserved = 0; + if (TdhGetPropertySize(record, 0, NULL, 1, &desc, &size) != ERROR_SUCCESS || + size > 1024 * 1024) + return NULL; + + // A zero-length value is a real answer ("the query produced nothing"), not a missing + // field: return an empty string so callers can tell the two apart. + WCHAR *value = (WCHAR *)calloc(1, size + sizeof(WCHAR)); + if (!value) return NULL; + if (size && TdhGetProperty(record, 0, NULL, 1, &desc, size, (PBYTE)value) != ERROR_SUCCESS) + { + free(value); + return NULL; + } + value[size / sizeof(WCHAR)] = L'\0'; + return value; +} + +static BOOL etw_read_u32(EVENT_RECORD *record, const WCHAR *name, DWORD *value) +{ + PROPERTY_DATA_DESCRIPTOR desc; + ULONG size = 0; + desc.PropertyName = (ULONGLONG)(ULONG_PTR)name; + desc.ArrayIndex = ULONG_MAX; + desc.Reserved = 0; + if (TdhGetPropertySize(record, 0, NULL, 1, &desc, &size) != ERROR_SUCCESS || size != sizeof(*value)) + return FALSE; + return TdhGetProperty(record, 0, NULL, 1, &desc, size, (PBYTE)value) == ERROR_SUCCESS; +} + +static BOOL etw_hostname_utf8(const WCHAR *wide, char out[256]) +{ + if (!wide || !wide[0]) return FALSE; + int count = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide, -1, out, 256, NULL, NULL); + if (count <= 1 || count > 256) return FALSE; + size_t len = (size_t)count - 1; + if (len && out[len - 1] == '.') out[--len] = '\0'; + for (size_t i = 0; i < len; i++) + { + unsigned char c = (unsigned char)out[i]; + if (c <= 0x20 || c == 0x7f || c == ';' || c == '\\') return FALSE; + if (c >= 'A' && c <= 'Z') out[i] = (char)(c + ('a' - 'A')); + } + return len > 0; +} + +static BOOL etw_ip_token(const WCHAR *start, size_t length, char token[64]) +{ + while (length && (*start == L' ' || *start == L'\t')) { start++; length--; } + while (length && (start[length - 1] == L' ' || start[length - 1] == L'\t')) length--; + // QueryResults entries may carry a record-type prefix ("A 1.2.3.4") or be a bare + // address ("1.2.3.4"). Addresses never contain spaces, so keep only the tail + // after the last space; InetPtonA then accepts either form. + for (size_t i = length; i > 0; i--) + { + WCHAR c = start[i - 1]; + if (c == L' ' || c == L'\t') + { + start += i; + length -= i; + break; + } + } + if (!length || length >= 64) return FALSE; + for (size_t i = 0; i < length; i++) + { + if (start[i] > 0x7f) return FALSE; + token[i] = (char)start[i]; + } + token[length] = '\0'; + return TRUE; +} + +// An IPv6 answer may be an IPv4-mapped/compatible address ("::ffff:1.2.3.4"). +// The connection that follows is almost always IPv4, so record the v4 form too. +static BOOL etw_ipv6_to_ipv4(const UINT8 ip6[16], UINT32 *ip4) +{ + static const UINT8 zeros[10] = { 0 }; + if (memcmp(ip6, zeros, 10) != 0) return FALSE; + if (ip6[10] != 0x00 && ip6[10] != 0xff) return FALSE; + if (ip6[11] != ip6[10]) return FALSE; // ::a.b.c.d or ::ffff:a.b.c.d only + memcpy(ip4, ip6 + 12, 4); + return TRUE; +} + +// Ask TDH for the schema of a live event and print every top-level property with its +// value. QueryResults is undocumented and its layout varies by Windows build; guessing +// at the format is what made the first implementation silently drop every answer, so +// this dump is the authoritative answer. The engine runs as a service we cannot run a +// separate admin probe from, so the product has to report its own evidence. +static void etw_sanitize(char *text) +{ + for (size_t i = 0; text[i]; i++) + if (text[i] == '\n' || text[i] == '\r' || text[i] == '\t') text[i] = ' '; +} + +static LONG etw_claim_dump_slot(void) +{ + for (;;) + { + LONG used = InterlockedCompareExchange(&g_dns_etw.dumps_logged, 0, 0); + if (used >= 8) return -1; + if (InterlockedCompareExchange(&g_dns_etw.dumps_logged, used + 1, used) == used) + return used; + } +} + +static void etw_dump_fields(EVENT_RECORD *record, const char *reason) +{ + if (etw_claim_dump_slot() < 0) return; + + ULONG info_size = 0; + ULONG status = TdhGetEventInformation(record, 0, NULL, NULL, &info_size); + if (status != ERROR_INSUFFICIENT_BUFFER && status != ERROR_SUCCESS) + { + log_message("[DNS-ETW] field dump (%s): TdhGetEventInformation sizing failed (%lu)", + reason, status); + return; + } + TRACE_EVENT_INFO *info = (TRACE_EVENT_INFO *)calloc(1, info_size ? info_size : 1); + if (!info) return; + if (TdhGetEventInformation(record, 0, NULL, info, &info_size) != ERROR_SUCCESS) + { + log_message("[DNS-ETW] field dump (%s): TdhGetEventInformation failed (%lu)", + reason, GetLastError()); + free(info); + return; + } + + log_message("[DNS-ETW] field dump (%s): id=%u pid=%lu fields=%lu", reason, + record->EventHeader.EventDescriptor.Id, + (unsigned long)record->EventHeader.ProcessId, + (unsigned long)info->TopLevelPropertyCount); + + // Top-level properties occupy the first TopLevelPropertyCount entries; the rest are + // struct members referenced by index. + for (ULONG i = 0; i < info->TopLevelPropertyCount && i < 32; i++) + { + EVENT_PROPERTY_INFO *prop = &info->EventPropertyInfoArray[i]; + const WCHAR *prop_name = (const WCHAR *)((const BYTE *)info + prop->NameOffset); + char name[80] = { 0 }; + WideCharToMultiByte(CP_UTF8, 0, prop_name, -1, name, sizeof(name), NULL, NULL); + + PROPERTY_DATA_DESCRIPTOR desc; + desc.PropertyName = (ULONGLONG)(ULONG_PTR)prop_name; + desc.ArrayIndex = ULONG_MAX; + desc.Reserved = 0; + ULONG size = 0; + if (TdhGetPropertySize(record, 0, NULL, 1, &desc, &size) != ERROR_SUCCESS) + { + log_message("[DNS-ETW] %s = ", name); + continue; + } + if (size == 0) + { + log_message("[DNS-ETW] %s = ", name); + continue; + } + if (size > 4096) + { + log_message("[DNS-ETW] %s = <%lu bytes>", name, (unsigned long)size); + continue; + } + BYTE *raw = (BYTE *)calloc(1, (size_t)size + sizeof(WCHAR)); + if (!raw) continue; + if (TdhGetProperty(record, 0, NULL, 1, &desc, size, raw) != ERROR_SUCCESS) + { + log_message("[DNS-ETW] %s = ", name); + free(raw); + continue; + } + if (prop->nonStructType.InType == TDH_INTYPE_UNICODESTRING) + { + WCHAR *wide = (WCHAR *)raw; + wide[size / sizeof(WCHAR)] = L'\0'; + char value[320] = { 0 }; + WideCharToMultiByte(CP_UTF8, 0, wide, -1, value, sizeof(value), NULL, NULL); + etw_sanitize(value); + log_message("[DNS-ETW] %s = \"%s\"", name, value); + } + else if (size == sizeof(ULONG)) + log_message("[DNS-ETW] %s = %lu (u32)", name, *(ULONG *)raw); + else if (size == sizeof(ULONGLONG)) + log_message("[DNS-ETW] %s = %llu (u64)", name, *(ULONGLONG *)raw); + else + { + char hex[64] = { 0 }; + for (ULONG b = 0; b < size && b < 16; b++) + _snprintf_s(hex + b * 3, sizeof(hex) - b * 3, _TRUNCATE, "%02x ", raw[b]); + log_message("[DNS-ETW] %s = %s(%lu bytes)", name, hex, (unsigned long)size); + } + free(raw); + } + free(info); +} + +// One compact line per interesting event, naming the process that made the query. A +// per-process view is the only way to answer "is this app's DNS reaching us at all" +// versus "we see the query but Windows gave us no addresses for it". +static void etw_log_event_line(EVENT_RECORD *record, const char *query, + const char *kind, DWORD status) +{ + char proc[MAX_PROCESS_NAME] = { 0 }; + if (!get_process_name_from_pid(record->EventHeader.ProcessId, proc, sizeof(proc))) + _snprintf_s(proc, sizeof(proc), _TRUNCATE, "pid-%lu", + (unsigned long)record->EventHeader.ProcessId); + log_message("[DNS-ETW] %s: proc=%s pid=%lu status=%lu query=\"%s\"", + kind, proc, (unsigned long)record->EventHeader.ProcessId, + (unsigned long)status, query[0] ? query : "(none)"); +} + +static void etw_store_results(EVENT_RECORD *record, const WCHAR *query_name, + const WCHAR *query_results, const FILETIME *expected_created, + ULONGLONG consumed_qpc) +{ + char hostname[256]; + if (!etw_hostname_utf8(query_name, hostname)) return; + + // Dump the first few events unconditionally: a single sample cannot distinguish + // "this one query failed" from "results are never populated on this build". + LONG ordinal = InterlockedIncrement(&g_dns_etw.first_dumps_logged); + if (ordinal <= 3) + { + char reason[32]; + _snprintf_s(reason, sizeof(reason), _TRUNCATE, "completion event #%ld", ordinal); + etw_dump_fields(record, reason); + } + + DWORD query_status = 0; + if (etw_read_u32(record, L"QueryStatus", &query_status)) + { + if (query_status == 0) InterlockedIncrement64(&g_dns_etw.status_ok); + else + { + InterlockedIncrement64(&g_dns_etw.status_failed); + InterlockedExchange64(&g_dns_etw.last_failed_status, (LONGLONG)query_status); + } + } + + if (!query_results || !query_results[0]) + { + // Either the field is absent on this build or the query genuinely returned + // nothing. Both look identical to the parser, so dump the whole event. + InterlockedIncrement64(&g_dns_etw.no_results_field); + if (InterlockedIncrement(&g_dns_etw.empty_lines_logged) <= 12) + etw_log_event_line(record, hostname, "no addresses", query_status); + etw_dump_fields(record, "empty QueryResults"); + return; + } + + ULONGLONG event_qpc = (ULONGLONG)record->EventHeader.TimeStamp.QuadPart; + DWORD pid = record->EventHeader.ProcessId; + UINT32 stored = 0; + const WCHAR *cursor = query_results; + while (*cursor) + { + while (*cursor == L';' || *cursor == L' ' || *cursor == L'\t') cursor++; + if (!*cursor) break; + const WCHAR *end = wcschr(cursor, L';'); + if (!end) end = cursor + wcslen(cursor); + char token[64]; + if (etw_ip_token(cursor, (size_t)(end - cursor), token)) + { + struct in_addr ip4; + struct in6_addr ip6; + if (InetPtonA(AF_INET, token, &ip4) == 1) + { + if (dns_process_cache_store(pid, expected_created, FALSE, ip4.s_addr, NULL, + hostname, 3008, event_qpc, consumed_qpc, FALSE, 0)) + stored++; + } + else if (InetPtonA(AF_INET6, token, &ip6) == 1) + { + if (dns_process_cache_store(pid, expected_created, TRUE, 0, (const UINT8 *)&ip6, + hostname, 3008, event_qpc, consumed_qpc, FALSE, 0)) + stored++; + UINT32 mapped; + if (etw_ipv6_to_ipv4((const UINT8 *)&ip6, &mapped) && + dns_process_cache_store(pid, expected_created, FALSE, mapped, NULL, + hostname, 3008, event_qpc, consumed_qpc, FALSE, 0)) + stored++; + } + } + cursor = *end ? end + 1 : end; + } + + if (stored) + { + InterlockedExchangeAdd64(&g_dns_etw.mapped, stored); + if (InterlockedIncrement(&g_dns_etw.mapped_lines_logged) <= 5) + etw_log_event_line(record, hostname, "mapped", query_status); + if (InterlockedCompareExchange(&g_dns_etw.mapped_dumped, 1, 0) == 0) + etw_dump_fields(record, "first event that produced an address"); + } + else + { + // No address survived parsing. This is the signal that the QueryResults + // layout differs from what this parser expects, so capture the whole event. + InterlockedIncrement64(&g_dns_etw.unparsed_results); + etw_dump_fields(record, "no address parsed"); + } +} + +static void WINAPI dns_etw_event(EVENT_RECORD *record) +{ + if (!IsEqualGUID(&record->EventHeader.ProviderId, &g_dns_client_provider)) + return; + if (record->EventHeader.EventDescriptor.Id == 3006) + { + InterlockedIncrement64(&g_dns_etw.starts); + if (record->EventHeader.ProcessId == 4) InterlockedIncrement64(&g_dns_etw.starts_pid4); + else if (record->EventHeader.ProcessId == 0) InterlockedIncrement64(&g_dns_etw.starts_pid0); + dns_etw_pending_start(record->EventHeader.ProcessId, + (ULONGLONG)record->EventHeader.TimeStamp.QuadPart); + return; + } + if (record->EventHeader.EventDescriptor.Id == 8043 || + record->EventHeader.EventDescriptor.Id == 8044) + { + DWORD client_pid = 0; + if (record->EventHeader.EventDescriptor.Id == 8043) + { + InterlockedIncrement64(&g_dns_etw.doh); + if (etw_read_u32(record, L"ClientPID", &client_pid) && client_pid == 4) + InterlockedIncrement64(&g_dns_etw.doh_pid4); + } + else + { + InterlockedIncrement64(&g_dns_etw.dot); + if (etw_read_u32(record, L"ClientPID", &client_pid) && client_pid == 4) + InterlockedIncrement64(&g_dns_etw.dot_pid4); + } + return; + } + if (record->EventHeader.EventDescriptor.Id != 3008) return; + + LARGE_INTEGER consumed; + QueryPerformanceCounter(&consumed); + InterlockedIncrement64(&g_dns_etw.events); + if (record->EventHeader.ProcessId == 4) InterlockedIncrement64(&g_dns_etw.pid4); + else if (record->EventHeader.ProcessId == 0) InterlockedIncrement64(&g_dns_etw.pid0); + // Our own self-test query is the one event whose semantics we control: dump it + // regardless of how many samples other traffic has already consumed. + if (record->EventHeader.ProcessId == g_dns_etw.selftest_pid && + InterlockedCompareExchange(&g_dns_etw.selftest_dumped, 1, 0) == 0) + etw_dump_fields(record, "self-test query"); + + WCHAR *query = etw_read_string(record, L"QueryName"); + WCHAR *results = etw_read_string(record, L"QueryResults"); + FILETIME expected_created; + ULONGLONG pending_token = 0; + if (dns_etw_pending_match(record->EventHeader.ProcessId, + (ULONGLONG)record->EventHeader.TimeStamp.QuadPart, + &expected_created, &pending_token)) + { + etw_store_results(record, query, results, &expected_created, + (ULONGLONG)consumed.QuadPart); + dns_etw_pending_complete(pending_token); + } + else + { + dns_process_cache_note_gap(1); + if (InterlockedIncrement(&g_dns_etw.empty_lines_logged) <= 12) + log_message("[DNS-ETW] completion event for pid %lu has no matching query-start identity; ignored", + (unsigned long)record->EventHeader.ProcessId); + } + free(query); + free(results); + dns_etw_record_latency(g_dns_etw.latency, + consumed.QuadPart - record->EventHeader.TimeStamp.QuadPart); + if (g_dns_etw.event_ready) SetEvent(g_dns_etw.event_ready); +} + +// Resolve a name from this process and check whether our own DNS event produced a usable +// cache entry. The engine runs as a service, so nobody can attach an elevated probe to +// the live session; this self-test is the only way to prove the whole chain +// (event arrives -> QueryResults is populated -> parser stores it -> lookup finds it) +// without depending on whatever traffic happens to be flowing. +static DWORD WINAPI dns_etw_selftest(LPVOID unused) +{ + (void)unused; + if (InterlockedCompareExchange(&g_dns_etw.stopping, 0, 0)) return 0; + Sleep(400); // let the session settle before generating traffic + if (InterlockedCompareExchange(&g_dns_etw.stopping, 0, 0)) return 0; + + ADDRINFOEXW hints; + ZeroMemory(&hints, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + PADDRINFOEXW result = NULL; + struct timeval timeout = { 2, 0 }; + int rc = GetAddrInfoExW(L"www.microsoft.com", NULL, NS_DNS, NULL, + &hints, &result, &timeout, NULL, NULL, NULL); + if (InterlockedCompareExchange(&g_dns_etw.stopping, 0, 0)) + { + if (result) FreeAddrInfoExW(result); + return 0; + } + if (rc != 0 || !result) + { + log_message("[DNS-ETW] self-test: GetAddrInfoExW(www.microsoft.com) failed (%d)", rc); + return 0; + } + + UINT32 ip4 = 0; + UINT8 ip6[16] = { 0 }; + BOOL have_v4 = FALSE, have_v6 = FALSE; + ULONG count = 0; + for (PADDRINFOEXW node = result; node; node = node->ai_next) + { + count++; + if (node->ai_family == AF_INET && node->ai_addrlen >= sizeof(struct sockaddr_in) && !have_v4) + { + struct sockaddr_in *sa = (struct sockaddr_in *)node->ai_addr; + ip4 = sa->sin_addr.s_addr; + have_v4 = TRUE; + } + else if (node->ai_family == AF_INET6 && node->ai_addrlen >= sizeof(struct sockaddr_in6) && !have_v6) + { + struct sockaddr_in6 *sa = (struct sockaddr_in6 *)node->ai_addr; + memcpy(ip6, &sa->sin6_addr, 16); + have_v6 = TRUE; + } + } + FreeAddrInfoExW(result); + + // The session's FlushTimer is 1s, so an event can sit in the buffer for a while. + char domain[256] = { 0 }; + for (int attempt = 0; attempt < 40; attempt++) + { + if (InterlockedCompareExchange(&g_dns_etw.stopping, 0, 0)) return 0; + if (have_v4 && dns_process_cache_lookup(g_dns_etw.selftest_pid, FALSE, ip4, NULL, + 0, domain, sizeof(domain))) + { + log_message("[DNS-ETW] self-test: resolved www.microsoft.com and recovered \"%s\" from our own DNS event", + domain); + return 0; + } + if (have_v6 && dns_process_cache_lookup(g_dns_etw.selftest_pid, TRUE, 0, ip6, + 0, domain, sizeof(domain))) + { + log_message("[DNS-ETW] self-test: resolved www.microsoft.com (IPv6) and recovered \"%s\" from our own DNS event", + domain); + return 0; + } + Sleep(100); + } + + char ip_text[64] = "(none)"; + if (have_v4) format_ip_address(ip4, ip_text, sizeof(ip_text)); + log_message("[DNS-ETW] self-test: getaddrinfo returned %lu addresses (%s) but no cache entry was recovered; complete=%lld mapped=%lld empty-results=%lld unparsed=%lld", + (unsigned long)count, ip_text, g_dns_etw.events, g_dns_etw.mapped, + g_dns_etw.no_results_field, g_dns_etw.unparsed_results); + return 0; +} + +static DWORD WINAPI dns_etw_consumer(LPVOID unused) +{ + (void)unused; + TRACEHANDLE consumer = g_dns_etw.consumer; + ULONG status = ProcessTrace(&consumer, 1, NULL, NULL); + if (status != ERROR_SUCCESS && status != ERROR_CANCELLED && !g_dns_etw.stopping) + log_message("[DNS-ETW] ProcessTrace stopped with error %lu", status); + return status; +} + +static DWORD WINAPI dns_etw_flush_worker(LPVOID unused) +{ + (void)unused; + while (WaitForSingleObject(g_dns_etw.flush_request, INFINITE) == WAIT_OBJECT_0) + { + if (g_dns_etw.stopping) break; + LARGE_INTEGER start, end; + QueryPerformanceCounter(&start); + ULONG status = ControlTraceW(g_dns_etw.session, g_dns_etw.name, + g_dns_etw.properties, EVENT_TRACE_CONTROL_FLUSH); + QueryPerformanceCounter(&end); + InterlockedExchange(&g_dns_etw.flush_status, (LONG)status); + if (status == ERROR_SUCCESS) InterlockedIncrement64(&g_dns_etw.flush_successes); + else InterlockedIncrement64(&g_dns_etw.flush_failures); + dns_etw_record_latency(g_dns_etw.flush_latency, end.QuadPart - start.QuadPart); + SetEvent(g_dns_etw.flush_done); + } + return 0; +} + +BOOL dns_etw_lookup(DWORD pid, BOOL is_ipv6, UINT32 ip, const UINT8 ip6[16], + ULONGLONG before_qpc, BOOL wait_for_flush, + char *out_domain, size_t out_size) +{ + if (dns_process_cache_lookup(pid, is_ipv6, ip, ip6, before_qpc, + out_domain, out_size)) + return TRUE; + if (!wait_for_flush) + { + dns_process_cache_note_gap(1); + return FALSE; + } + if (!g_dns_etw.session || !g_dns_etw.flush_thread || !g_dns_etw.event_ready || + !g_dns_etw.flush_request || !g_dns_etw.flush_done) + { + dns_process_cache_note_gap(1); + return FALSE; + } + + BOOL has_pending_query = dns_etw_pending_for_pid(pid, before_qpc); + BOOL flush_scheduled = dns_etw_schedule_flush(has_pending_query); + if (!has_pending_query || !flush_scheduled) + { + // shortcut: without a preceding 3006, use IP for this packet; revisit if stage-one proves a safe earlier signal. + dns_process_cache_note_gap(1); + return FALSE; + } + + LARGE_INTEGER start; + QueryPerformanceCounter(&start); + InterlockedIncrement64(&g_dns_etw.lookup_waits); + + HANDLE events[] = { g_dns_etw.event_ready, g_dns_etw.flush_done }; + for (;;) + { + DWORD elapsed = dns_etw_elapsed_ms(start); + if (elapsed >= DNS_ETW_DECISION_WAIT_MS) break; + DWORD remaining = DNS_ETW_DECISION_WAIT_MS - elapsed; + DWORD wait = WaitForMultipleObjects(2, events, FALSE, remaining); + if (wait == WAIT_TIMEOUT || wait == WAIT_FAILED) break; + if (dns_process_cache_lookup(pid, is_ipv6, ip, ip6, before_qpc, + out_domain, out_size)) + { + InterlockedIncrement64(&g_dns_etw.lookup_hits); + LARGE_INTEGER end; + QueryPerformanceCounter(&end); + dns_etw_record_latency(g_dns_etw.wait_latency, end.QuadPart - start.QuadPart); + return TRUE; + } + if (wait == WAIT_OBJECT_0 + 1) + { + LONG status = InterlockedCompareExchange(&g_dns_etw.flush_status, 0, 0); + ResetEvent(g_dns_etw.flush_done); + if (status != ERROR_SUCCESS) break; + } + } + + InterlockedIncrement64(&g_dns_etw.lookup_timeouts); + LARGE_INTEGER end; + QueryPerformanceCounter(&end); + dns_etw_record_latency(g_dns_etw.wait_latency, end.QuadPart - start.QuadPart); + BOOL found = dns_process_cache_lookup(pid, is_ipv6, ip, ip6, before_qpc, + out_domain, out_size); + if (found) InterlockedIncrement64(&g_dns_etw.lookup_hits); + else dns_process_cache_note_gap(1); + return found; +} + +BOOL dns_etw_start(void) +{ + if (g_dns_etw.session || g_dns_etw.thread) return FALSE; + ZeroMemory(&g_dns_etw, sizeof(g_dns_etw)); + ZeroMemory(g_dns_etw_pending, sizeof(g_dns_etw_pending)); + InterlockedExchange64(&g_dns_etw_pending_token, 0); + if (!QueryPerformanceFrequency(&g_dns_etw.qpc_frequency) || + g_dns_etw.qpc_frequency.QuadPart <= 0) + { + log_message("[DNS-ETW] QueryPerformanceFrequency failed; DNS event timing is unavailable"); + return FALSE; + } + g_dns_etw.event_ready = CreateEventW(NULL, FALSE, FALSE, NULL); + g_dns_etw.flush_request = CreateEventW(NULL, FALSE, FALSE, NULL); + g_dns_etw.flush_done = CreateEventW(NULL, TRUE, TRUE, NULL); + if (!g_dns_etw.event_ready || !g_dns_etw.flush_request || !g_dns_etw.flush_done) + { + dns_etw_close_events(); + return FALSE; + } + _snwprintf_s(g_dns_etw.name, _countof(g_dns_etw.name), _TRUNCATE, + L"ProxyBridge DNS Client %lu", GetCurrentProcessId()); + + size_t props_size = sizeof(EVENT_TRACE_PROPERTIES) + + (_countof(g_dns_etw.name) * sizeof(WCHAR)); + g_dns_etw.properties = (EVENT_TRACE_PROPERTIES *)calloc(1, props_size); + if (!g_dns_etw.properties) + { + dns_etw_close_events(); + return FALSE; + } + EVENT_TRACE_PROPERTIES *props = g_dns_etw.properties; + props->Wnode.BufferSize = (ULONG)props_size; + props->Wnode.Guid.Data1 = 0x78c9321a; + props->Wnode.Guid.Data2 = 0x1f2b; + props->Wnode.Guid.Data3 = 0x4f63; + DWORD pid = GetCurrentProcessId(); + DWORD tick = GetTickCount(); + memcpy(&props->Wnode.Guid.Data4[0], &pid, sizeof(pid)); + memcpy(&props->Wnode.Guid.Data4[4], &tick, sizeof(tick)); + props->Wnode.Flags = WNODE_FLAG_TRACED_GUID; + // ClientContext 1 = QPC clock. This MUST match the domain of WinDivert's + // WINDIVERT_ADDRESS.Timestamp (also QPC) or every event is "in the future" + // relative to every packet and the before-SYN lookup filter rejects it all. + // (2 = system time, 3 = CPU cycle counter.) + props->Wnode.ClientContext = 1; + props->BufferSize = 64; + props->MinimumBuffers = 4; + props->MaximumBuffers = 16; + props->LogFileMode = EVENT_TRACE_REAL_TIME_MODE; + props->FlushTimer = 1; + props->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES); + wcscpy_s((WCHAR *)((BYTE *)props + props->LoggerNameOffset), + _countof(g_dns_etw.name), g_dns_etw.name); + + ULONG status = StartTraceW(&g_dns_etw.session, g_dns_etw.name, props); + if (status != ERROR_SUCCESS) + { + log_message("[DNS-ETW] StartTrace failed (%lu); process hostname recovery is unavailable", status); + free(g_dns_etw.properties); + g_dns_etw.properties = NULL; + g_dns_etw.session = 0; + dns_etw_close_events(); + return FALSE; + } + // MatchAnyKeyword=0 only enables events whose keyword is 0, so a query-completion + // event tagged with any keyword bit would never reach us. Ask for every keyword and + // filter on the event ID instead; DNS-Client is low volume even when fully enabled. + status = EnableTraceEx2(g_dns_etw.session, &g_dns_client_provider, + EVENT_CONTROL_CODE_ENABLE_PROVIDER, TRACE_LEVEL_INFORMATION, + 0xFFFFFFFFFFFFFFFFULL, 0, 0, NULL); + if (status != ERROR_SUCCESS) + { + log_message("[DNS-ETW] EnableTraceEx2 failed (%lu)", status); + ControlTraceW(g_dns_etw.session, g_dns_etw.name, props, EVENT_TRACE_CONTROL_STOP); + free(g_dns_etw.properties); + g_dns_etw.properties = NULL; + g_dns_etw.session = 0; + dns_etw_close_events(); + return FALSE; + } + + EVENT_TRACE_LOGFILEW logfile; + ZeroMemory(&logfile, sizeof(logfile)); + logfile.LoggerName = g_dns_etw.name; + logfile.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | + PROCESS_TRACE_MODE_EVENT_RECORD | + PROCESS_TRACE_MODE_RAW_TIMESTAMP; + logfile.EventRecordCallback = dns_etw_event; + g_dns_etw.consumer = OpenTraceW(&logfile); + if (g_dns_etw.consumer == INVALID_PROCESSTRACE_HANDLE) + { + log_message("[DNS-ETW] OpenTrace failed (%lu)", GetLastError()); + ControlTraceW(g_dns_etw.session, g_dns_etw.name, props, EVENT_TRACE_CONTROL_STOP); + free(g_dns_etw.properties); + g_dns_etw.properties = NULL; + g_dns_etw.session = 0; + dns_etw_close_events(); + return FALSE; + } + g_dns_etw.thread = CreateThread(NULL, 0, dns_etw_consumer, NULL, 0, NULL); + if (!g_dns_etw.thread) + { + log_message("[DNS-ETW] Could not start event consumer (%lu)", GetLastError()); + CloseTrace(g_dns_etw.consumer); + ControlTraceW(g_dns_etw.session, g_dns_etw.name, props, EVENT_TRACE_CONTROL_STOP); + free(g_dns_etw.properties); + g_dns_etw.properties = NULL; + g_dns_etw.session = 0; + g_dns_etw.consumer = INVALID_PROCESSTRACE_HANDLE; + dns_etw_close_events(); + return FALSE; + } + g_dns_etw.flush_thread = CreateThread(NULL, 0, dns_etw_flush_worker, NULL, 0, NULL); + if (!g_dns_etw.flush_thread) + { + log_message("[DNS-ETW] Could not start flush worker (%lu)", GetLastError()); + InterlockedExchange(&g_dns_etw.stopping, 1); + CloseTrace(g_dns_etw.consumer); + ControlTraceW(g_dns_etw.session, g_dns_etw.name, props, EVENT_TRACE_CONTROL_STOP); + WaitForSingleObject(g_dns_etw.thread, INFINITE); + CloseHandle(g_dns_etw.thread); + g_dns_etw.thread = NULL; + g_dns_etw.consumer = INVALID_PROCESSTRACE_HANDLE; + free(g_dns_etw.properties); + g_dns_etw.properties = NULL; + g_dns_etw.session = 0; + dns_etw_close_events(); + return FALSE; + } + log_message("[DNS-ETW] Listening for DNS Client query-completion events (QPC clock)"); + g_dns_etw.selftest_pid = GetCurrentProcessId(); + g_dns_etw.selftest_thread = CreateThread(NULL, 0, dns_etw_selftest, NULL, 0, NULL); + if (!g_dns_etw.selftest_thread) + log_message("[DNS-ETW] Could not start self-test (%lu)", GetLastError()); + return TRUE; +} + +void dns_etw_stop(void) +{ + if (!g_dns_etw.session && !g_dns_etw.thread) return; + ULONG events_lost = 0, realtime_buffers_lost = 0; + BOOL have_session_stats = FALSE; + InterlockedExchange(&g_dns_etw.stopping, 1); + if (g_dns_etw.flush_request) SetEvent(g_dns_etw.flush_request); + if (g_dns_etw.selftest_thread) + { + // The resolver has a finite timeout; join before returning so no diagnostic thread + // can execute DLL code after the service unloads the engine. + WaitForSingleObject(g_dns_etw.selftest_thread, INFINITE); + CloseHandle(g_dns_etw.selftest_thread); + g_dns_etw.selftest_thread = NULL; + } + if (g_dns_etw.flush_thread) + { + WaitForSingleObject(g_dns_etw.flush_thread, INFINITE); + CloseHandle(g_dns_etw.flush_thread); + g_dns_etw.flush_thread = NULL; + } + if (g_dns_etw.session && g_dns_etw.properties) + { + ULONG status = ControlTraceW(g_dns_etw.session, g_dns_etw.name, + g_dns_etw.properties, EVENT_TRACE_CONTROL_QUERY); + if (status == ERROR_SUCCESS) + { + have_session_stats = TRUE; + events_lost = g_dns_etw.properties->EventsLost; + realtime_buffers_lost = g_dns_etw.properties->RealTimeBuffersLost; + ULONGLONG lost = events_lost > realtime_buffers_lost ? events_lost : realtime_buffers_lost; + dns_process_cache_note_gap(lost); + } + ControlTraceW(g_dns_etw.session, g_dns_etw.name, + g_dns_etw.properties, EVENT_TRACE_CONTROL_STOP); + } + if (g_dns_etw.consumer && g_dns_etw.consumer != INVALID_PROCESSTRACE_HANDLE) + CloseTrace(g_dns_etw.consumer); + if (g_dns_etw.thread) + { + WaitForSingleObject(g_dns_etw.thread, INFINITE); + CloseHandle(g_dns_etw.thread); + } + if (have_session_stats) + log_message("[DNS-ETW] start=%lld(pid4=%lld,pid0=%lld) complete=%lld(pid4=%lld,pid0=%lld) mapped=%lld doh=%lld(dnscache_pid4=%lld) dot=%lld(dnscache_pid4=%lld) lost=%lu realtime-buffer-lost=%lu", + g_dns_etw.starts, g_dns_etw.starts_pid4, g_dns_etw.starts_pid0, + g_dns_etw.events, g_dns_etw.pid4, g_dns_etw.pid0, g_dns_etw.mapped, + g_dns_etw.doh, g_dns_etw.doh_pid4, g_dns_etw.dot, g_dns_etw.dot_pid4, + events_lost, realtime_buffers_lost); + else + log_message("[DNS-ETW] session counters unavailable at stop"); + log_message("[DNS-ETW] complete events with no QueryResults field=%lld; events where no address parsed=%lld (both should be 0)", + g_dns_etw.no_results_field, g_dns_etw.unparsed_results); + log_message("[DNS-ETW] QueryStatus: success=%lld failed=%lld last-nonzero=%lld (0 means every query reported an answer)", + g_dns_etw.status_ok, g_dns_etw.status_failed, g_dns_etw.last_failed_status); + log_message("[DNS-ETW] event-to-callback latency buckets (us <=100/500/1000/5000/10000/50000/100000/>100000): %lld/%lld/%lld/%lld/%lld/%lld/%lld/%lld; cache gaps=%llu", + g_dns_etw.latency[0], g_dns_etw.latency[1], g_dns_etw.latency[2], + g_dns_etw.latency[3], g_dns_etw.latency[4], g_dns_etw.latency[5], + g_dns_etw.latency[6], g_dns_etw.latency[7], + dns_process_cache_gap_count()); + log_message("[DNS-ETW] on-demand flushes=%lld ok=%lld failed=%lld lookups=%lld hits=%lld timeouts=%lld", + g_dns_etw.flush_requests, g_dns_etw.flush_successes, + g_dns_etw.flush_failures, g_dns_etw.lookup_waits, + g_dns_etw.lookup_hits, g_dns_etw.lookup_timeouts); + log_message("[DNS-ETW] flush duration buckets (us <=100/500/1000/5000/10000/50000/100000/>100000): %lld/%lld/%lld/%lld/%lld/%lld/%lld/%lld", + g_dns_etw.flush_latency[0], g_dns_etw.flush_latency[1], + g_dns_etw.flush_latency[2], g_dns_etw.flush_latency[3], + g_dns_etw.flush_latency[4], g_dns_etw.flush_latency[5], + g_dns_etw.flush_latency[6], g_dns_etw.flush_latency[7]); + log_message("[DNS-ETW] lookup wait buckets (us <=100/500/1000/5000/10000/50000/100000/>100000): %lld/%lld/%lld/%lld/%lld/%lld/%lld/%lld", + g_dns_etw.wait_latency[0], g_dns_etw.wait_latency[1], + g_dns_etw.wait_latency[2], g_dns_etw.wait_latency[3], + g_dns_etw.wait_latency[4], g_dns_etw.wait_latency[5], + g_dns_etw.wait_latency[6], g_dns_etw.wait_latency[7]); + if (g_dns_etw.properties) free(g_dns_etw.properties); + g_dns_etw.properties = NULL; + g_dns_etw.thread = NULL; + g_dns_etw.consumer = INVALID_PROCESSTRACE_HANDLE; + g_dns_etw.session = 0; + AcquireSRWLockExclusive(&g_dns_etw_pending_lock); + ZeroMemory(g_dns_etw_pending, sizeof(g_dns_etw_pending)); + ReleaseSRWLockExclusive(&g_dns_etw_pending_lock); + dns_etw_close_events(); +} diff --git a/Windows/src/pb_http.c b/Windows/src/pb_http.c index a42be16..733abd2 100644 --- a/Windows/src/pb_http.c +++ b/Windows/src/pb_http.c @@ -2,7 +2,8 @@ // HTTP proxy: CONNECT tunnels (IPv4/IPv6). -int http_connect_v6(SOCKET s, const UINT8 dest_ip6[16], UINT16 dest_port, const PROXY_CONFIG *cfg) +int http_connect_v6(SOCKET s, const UINT8 dest_ip6[16], UINT16 dest_port, + const PROXY_CONFIG *cfg, const char *hostname) { char request[HTTP_BUFFER_SIZE]; char response[4096]; @@ -13,14 +14,12 @@ int http_connect_v6(SOCKET s, const UINT8 dest_ip6[16], UINT16 dest_port, const char addr_str[64]; inet_ntop(AF_INET6, dest_ip6, addr_str, sizeof(addr_str)); - // Use the cached hostname only if this config opts to let the proxy resolve DNS. - char cached_domain[256]; const char *host_part; char host_buf[270]; // big enough for [ipv6]:port or domain - if (cfg->send_domain_to_proxy && dns_cache_lookup_v6(dest_ip6, cached_domain, sizeof(cached_domain))) + if (cfg->send_domain_to_proxy && hostname && hostname[0]) { - host_part = cached_domain; - strncpy_s(host_buf, sizeof(host_buf), cached_domain, _TRUNCATE); + host_part = hostname; + strncpy_s(host_buf, sizeof(host_buf), hostname, _TRUNCATE); } else { @@ -54,7 +53,8 @@ int http_connect_v6(SOCKET s, const UINT8 dest_ip6[16], UINT16 dest_port, const return 0; } -int http_connect(SOCKET s, UINT32 dest_ip, UINT16 dest_port, const PROXY_CONFIG *cfg) +int http_connect(SOCKET s, UINT32 dest_ip, UINT16 dest_port, + const PROXY_CONFIG *cfg, const char *hostname) { char request[HTTP_BUFFER_SIZE]; char response[4096]; @@ -63,13 +63,11 @@ int http_connect(SOCKET s, UINT32 dest_ip, UINT16 dest_port, const PROXY_CONFIG int status_code; BOOL use_auth = (cfg != NULL && cfg->username[0] != '\0'); - // Use the cached hostname only if this config opts to let the proxy resolve DNS. - char cached_domain[256]; char ip_str[32]; const char *host_part; - if (cfg->send_domain_to_proxy && dns_cache_lookup(dest_ip, cached_domain, sizeof(cached_domain))) + if (cfg->send_domain_to_proxy && hostname && hostname[0]) { - host_part = cached_domain; + host_part = hostname; } else { diff --git a/Windows/src/pb_internal.h b/Windows/src/pb_internal.h index b660382..47be60a 100644 --- a/Windows/src/pb_internal.h +++ b/Windows/src/pb_internal.h @@ -65,6 +65,9 @@ typedef struct PROCESS_RULE { #define SOCKS5_ATYP_DOMAIN 0x03 // send hostname to proxy rfc 1928 #define SOCKS5_AUTH_NONE 0x00 +// Where a hostname association came from. Recorded per entry so diagnostics can tell a +// wire-observed answer from one read out of a TLS ClientHello. + // Idle timeout before a connection-tracking entry is reaped. Clean closes are removed // immediately on FIN/RST, so this only sweeps entries whose FIN/RST was missed. It MUST // exceed the relay's TCP keepalive interval (5 min) and real idle periods (IMAP IDLE, @@ -72,26 +75,24 @@ typedef struct PROCESS_RULE { // gets reaped mid-session and silently breaks until restart (issue: hours-long degradation). #define CONNECTION_STALE_TIMEOUT_MS 1800000 // 30 minutes -// DNS snooping cache: maps intercepted A-record answers to their hostnames so -// that SOCKS5 conect can forward ATYP_DOMAIN instead of ATYP_IPV4, letting -// proxy servers that do their own name-resolution (e.g. mihomo) see the -// original hostname rather than a bare IP. (Resolves issue #138.) -#define DNS_CACHE_BUCKETS 1024 -#define DNS_CACHE_TTL_MS 300000 // 5 minutes - -typedef struct DNS_CACHE_ENTRY { - UINT32 ip; // network-byte-order IPv4 - char domain[256]; - ULONGLONG expire_tick; - struct DNS_CACHE_ENTRY *next; -} DNS_CACHE_ENTRY; - -typedef struct DNS_CACHE_ENTRY_V6 { - UINT8 ip6[16]; // raw IPv6 address - char domain[256]; - ULONGLONG expire_tick; - struct DNS_CACHE_ENTRY_V6 *next; -} DNS_CACHE_ENTRY_V6; +// Process-scoped hostname records populated by DNS Client ETW. Timestamps use QPC so +// they can be compared with WinDivert's packet timestamp when the clocks are confirmed. +typedef struct DNS_PROCESS_ENTRY DNS_PROCESS_ENTRY; +typedef struct DNS_HOST_ENTRY { + DNS_PROCESS_ENTRY *process; + BOOL is_ipv6; + UINT32 ip; + UINT8 ip6[16]; + char domain[256]; + UINT16 source_event_id; + DWORD source_ttl_seconds; + BOOL has_source_ttl; + ULONGLONG event_qpc; + ULONGLONG consumed_qpc; + volatile LONG64 last_used; + struct DNS_HOST_ENTRY *next; + struct DNS_HOST_ENTRY *hash_next; +} DNS_HOST_ENTRY; typedef struct CONNECTION_INFO { UINT16 src_port; @@ -102,6 +103,13 @@ typedef struct CONNECTION_INFO { BOOL is_tracked; ULONGLONG last_activity; UINT32 proxy_config_id; + char hostname[256]; + // TRUE when the connection log for this flow was handed to the relay because the proxy + // resolves DNS itself and no hostname was known yet at SYN time (see ProxyBridge.c). + BOOL log_pending; + DWORD process_id; // Preserve the observed PID for logs; has_process_identity gates lifecycle cleanup. + FILETIME process_created; + BOOL has_process_identity; BOOL is_ipv6; UINT8 src_ip6[16]; // raw IPv6 src (only valid when is_ipv6) UINT8 orig_dest_ip6[16]; // raw IPv6 dest (only valid when is_ipv6) @@ -109,6 +117,9 @@ typedef struct CONNECTION_INFO { struct CONNECTION_INFO *rev_next; // chain in the reverse table (keyed by orig_dest) UINT32 rev_bucket; // reverse bucket this entry is currently linked in BOOL in_rev; // TRUE while linked in the reverse table + struct CONNECTION_INFO *domain_rev_next; + UINT32 domain_rev_bucket; + BOOL in_domain_rev; UINT8 fin_flags; // TCP: 1 = app sent FIN, 2 = relay sent FIN (entry dropped when both seen) } CONNECTION_INFO; @@ -119,6 +130,9 @@ typedef struct { UINT32 proxy_config_id; BOOL is_ipv6; UINT8 orig_dest_ip6[16]; + char hostname[256]; + DWORD process_id; // owner of the flow, so the relay can name it in the log + BOOL log_pending; // this flow's connection log is the relay's to emit } CONNECTION_CONFIG; typedef struct { @@ -195,21 +209,18 @@ extern HANDLE udp_relay_thread; extern HANDLE cleanup_thread; extern PID_CACHE_ENTRY *pid_cache[PID_CACHE_SIZE]; extern volatile BOOL g_has_active_rules; -extern volatile BOOL g_has_domain_rules; extern SOCKET udp_relay_socket; extern SOCKET udp_relay_socket6; extern volatile BOOL running; extern DWORD g_current_process_id; extern BOOL g_traffic_logging_enabled; -extern DNS_CACHE_ENTRY *g_dns_cache[DNS_CACHE_BUCKETS]; -extern DNS_CACHE_ENTRY_V6 *g_dns_cache_v6[DNS_CACHE_BUCKETS]; -extern SRWLOCK g_dns_cache_lock; extern volatile LONG port_decided_bitmap[2048]; // 8 KB extern volatile LONG port_direct_bitmap[2048]; // 8 KB extern UINT16 g_local_relay_port; extern BOOL g_localhost_via_proxy; // default disabled for security - most proxy server block localhost for ssrf and also many app might not work if localhost trafic goes to remote server if proxy server is on diffrent machine extern LogCallback g_log_callback; extern ConnectionCallback g_connection_callback; +extern ConnectionCallbackEx g_connection_callback_ex; extern char *g_pidtbl_buf; extern DWORD g_pidtbl_cap; @@ -265,6 +276,7 @@ DWORD get_process_id_from_connection(UINT32 src_ip, UINT16 src_port); DWORD get_process_id_from_udp_connection(UINT32 src_ip, UINT16 src_port); DWORD get_process_id_from_connection_v6(const UINT8 src_ip6[16], UINT16 src_port); DWORD get_process_id_from_udp_connection_v6(const UINT8 src_ip6[16], UINT16 src_port); +BOOL get_process_identity(DWORD pid, FILETIME *created, HANDLE *process_handle); BOOL get_process_name_from_pid(DWORD pid, char *name, DWORD name_size); UINT32 pid_cache_hash(UINT32 src_ip, UINT16 src_port, BOOL is_udp); DWORD get_cached_pid(UINT32 src_ip, UINT16 src_port, BOOL is_udp); @@ -275,7 +287,7 @@ void cleanup_stale_pid_cache(void); // ---- pb_rules.c ---- BOOL is_ipv6_multicast_or_linklocal(const UINT8 ip6[16]); -RuleAction check_process_rule_v6(const UINT8 src_ip6[16], UINT16 src_port, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, DWORD *out_pid, UINT32 *out_proxy_config_id); +RuleAction check_process_rule_v6(const UINT8 src_ip6[16], UINT16 src_port, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, ULONGLONG packet_qpc, char hostname[256], DWORD *out_pid, UINT32 *out_proxy_config_id); BOOL match_ip_pattern(const char *pattern, UINT32 ip); BOOL match_port_pattern(const char *pattern, UINT16 port); BOOL ip_match_wrapper(const char *token, const void *data); @@ -293,11 +305,11 @@ BOOL match_domain_list(const char *domain_list, const char *domain); BOOL rule_has_domain_filter(const PROCESS_RULE *rule); BOOL match_domain_filter(const PROCESS_RULE *rule, const char *domain); BOOL is_broadcast_or_multicast(UINT32 ip); -RuleAction match_rule_inner(const char *process_name, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id); -RuleAction match_rule(const char *process_name, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id); -RuleAction match_rule_v6_inner(const char *process_name, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id); -RuleAction match_rule_v6(const char *process_name, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id); -RuleAction check_process_rule(UINT32 src_ip, UINT16 src_port, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, DWORD *out_pid, UINT32 *out_proxy_config_id); +RuleAction match_rule_inner(const char *process_name, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, const char *hostname, BOOL *out_needs_hostname, UINT32 *out_proxy_config_id); +RuleAction match_rule(const char *process_name, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, const char *hostname, BOOL *out_needs_hostname, UINT32 *out_proxy_config_id); +RuleAction match_rule_v6_inner(const char *process_name, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, const char *hostname, BOOL *out_needs_hostname, UINT32 *out_proxy_config_id); +RuleAction match_rule_v6(const char *process_name, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, const char *hostname, BOOL *out_needs_hostname, UINT32 *out_proxy_config_id); +RuleAction check_process_rule(UINT32 src_ip, UINT16 src_port, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, ULONGLONG packet_qpc, char hostname[256], DWORD *out_pid, UINT32 *out_proxy_config_id); void update_has_active_rules(void); // ---- pb_proxy.c ---- @@ -306,17 +318,44 @@ BOOL any_socks5_config(void); BOOL is_proxy_config_referenced(UINT32 config_id); // ---- pb_dns.c ---- -void dns_cache_init(void); -UINT32 dns_bucket(UINT32 ip); -void dns_cache_store(UINT32 ip, const char *domain); -BOOL dns_cache_lookup(UINT32 ip, char *out_domain, size_t out_size); -UINT32 dns_bucket_v6(const UINT8 ip6[16]); -void dns_cache_store_v6(const UINT8 ip6[16], const char *domain); -BOOL dns_cache_lookup_v6(const UINT8 ip6[16], char *out_domain, size_t out_size); BOOL dns_parse_name(const UINT8 *msg, int msg_len, int *offset, char *dst, int dst_len); -void snoop_dns_response(const UINT8 *payload, int payload_len); -void cleanup_stale_dns_cache(void); -void flush_dns_resolver_cache(void); +void dns_network_observe_udp(BOOL outbound, BOOL is_ipv6, const UINT8 *src_ip, + const UINT8 *dst_ip, UINT16 src_port, UINT16 dst_port, + const UINT8 *payload, int payload_len, ULONGLONG packet_qpc); +void dns_network_observe_tcp(BOOL is_ipv6, const UINT8 *src_ip, const UINT8 *dst_ip, + UINT16 src_port, UINT16 dst_port, UINT32 seq, UINT8 flags, + const UINT8 *payload, int payload_len, ULONGLONG packet_qpc); +void dns_network_query_reset(void); +void dns_process_cache_init(void); +void dns_process_cache_begin_session(void); +void dns_process_cache_clear(void); +void dns_process_cache_cleanup(void); +BOOL dns_process_cache_snapshot_tick(BOOL final_snapshot); +void dns_process_cache_end_session(void); +void dns_process_cache_note_gap(ULONGLONG count); +BOOL dns_process_cache_store(DWORD pid, const FILETIME *expected_created, BOOL is_ipv6, UINT32 ip, const UINT8 ip6[16], const char *domain, UINT16 source_event_id, ULONGLONG event_qpc, ULONGLONG consumed_qpc, BOOL has_ttl, DWORD ttl_seconds); +BOOL dns_process_cache_lookup(DWORD pid, BOOL is_ipv6, UINT32 ip, const UINT8 ip6[16], ULONGLONG before_qpc, char *out_domain, size_t out_size); +ULONGLONG dns_process_cache_gap_count(void); +BOOL dns_etw_start(void); +BOOL dns_etw_lookup(DWORD pid, BOOL is_ipv6, UINT32 ip, const UINT8 ip6[16], ULONGLONG before_qpc, BOOL wait_for_flush, char *out_domain, size_t out_size); +void dns_etw_stop(void); + +// ---- pb_sni.c ---- +// Result of scanning a TLS record stream for the ClientHello SNI extension. +typedef enum { + SNI_NOT_TLS = 0, // not a TLS handshake, or a complete ClientHello carrying no SNI + SNI_NEED_MORE = 1, // truncated ClientHello - feed more bytes and retry + SNI_FOUND = 2 +} SNI_RESULT; + +SNI_RESULT sni_parse_client_hello(const UINT8 *data, size_t length, char *out, size_t out_size); +BOOL sni_peek_from_socket(SOCKET sock, char *out, size_t out_size); + +// ---- ProxyBridge.c: connection log ---- +// Emit one connection-log line through the registered callback. The packet reader uses it, +// and so does the relay when it is the one that ends up knowing the destination name. +void pb_report_connection(const char *process_name, DWORD pid, const char *ip, + const char *hostname, UINT16 port, const char *info); // ---- pb_socks5.c ---- int socks5_read_connect_reply(SOCKET s, int *reply); @@ -327,21 +366,26 @@ int socks5_udp_associate_with_config(SOCKET s, struct sockaddr_in *relay_addr, c BOOL establish_udp_associate_for_config(PROXY_CONFIG *cfg); // ---- pb_http.c ---- -int http_connect_v6(SOCKET s, const UINT8 dest_ip6[16], UINT16 dest_port, const PROXY_CONFIG *cfg); -int http_connect(SOCKET s, UINT32 dest_ip, UINT16 dest_port, const PROXY_CONFIG *cfg); +int http_connect_v6(SOCKET s, const UINT8 dest_ip6[16], UINT16 dest_port, const PROXY_CONFIG *cfg, const char *hostname); +int http_connect(SOCKET s, UINT32 dest_ip, UINT16 dest_port, const PROXY_CONFIG *cfg, const char *hostname); // ---- pb_conntrack.c ---- UINT32 rev_hash_v4(UINT32 dest_ip, UINT16 dest_port); UINT32 rev_hash_v6(const UINT8 dest_ip6[16], UINT16 dest_port); void rev_insert(CONNECTION_INFO *c); void rev_unlink(CONNECTION_INFO *c); -void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, UINT16 dest_port, UINT32 proxy_config_id); -void add_connection_v6(UINT16 src_port, BOOL is_udp, const UINT8 src_ip6[16], const UINT8 dest_ip6[16], UINT16 dest_port, UINT32 proxy_config_id); -BOOL get_connection_full_v6(UINT16 src_port, BOOL is_udp, UINT8 dest_ip6[16], UINT16 *dest_port, UINT32 *proxy_config_id); +void add_connection(UINT16 src_port, BOOL is_udp, UINT32 src_ip, UINT32 dest_ip, UINT16 dest_port, UINT32 proxy_config_id, const char *hostname, DWORD process_id, BOOL log_pending); +void add_connection_v6(UINT16 src_port, BOOL is_udp, const UINT8 src_ip6[16], const UINT8 dest_ip6[16], UINT16 dest_port, UINT32 proxy_config_id, const char *hostname, DWORD process_id, BOOL log_pending); +BOOL get_connection_full_v6(UINT16 src_port, BOOL is_udp, UINT8 dest_ip6[16], UINT16 *dest_port, UINT32 *proxy_config_id, char *hostname, size_t hostname_size, DWORD *process_id, BOOL *log_pending); BOOL find_v6_udp_sender(const UINT8 orig_dest_ip6[16], UINT16 orig_dest_port, UINT8 src_ip6[16], UINT16 *src_port); +BOOL find_unique_udp_sender_by_domain(const char *hostname, UINT16 dest_port, + UINT32 proxy_config_id, + BOOL *is_ipv6, UINT32 *client_ip, UINT8 client_ip6[16], + UINT16 *client_port); // UDP flow validation for the app->relay redirect. A UDP "connection" is keyed by source port // only, so it must be re-checked on every datagram: (a) if the destination changed (a socket -// that moves to another server, e.g. a voice call migrating) the entry is retargeted, and +// that moves to another server, e.g. a voice call migrating) the old entry is removed and rules +// and hostname association are evaluated again, and // (b) if the flow has been idle longer than UDP_REEVAL_IDLE_MS the source port may belong to a // different socket now, so the entry is dropped and the caller re-evaluates the rules. // Returns TRUE if a valid entry exists (and was refreshed), FALSE if not tracked / evicted. @@ -365,9 +409,10 @@ void connection_fin(UINT16 src_port, BOOL is_ipv6, UINT8 bit); BOOL is_connection_tracked(UINT16 src_port, BOOL is_udp, BOOL is_ipv6); BOOL get_connection(UINT16 src_port, BOOL is_udp, UINT32 *dest_ip, UINT16 *dest_port); -BOOL get_connection_full(UINT16 src_port, BOOL is_udp, UINT32 *dest_ip, UINT16 *dest_port, UINT32 *proxy_config_id); +BOOL get_connection_full(UINT16 src_port, BOOL is_udp, UINT32 *dest_ip, UINT16 *dest_port, UINT32 *proxy_config_id, char *hostname, size_t hostname_size, DWORD *process_id, BOOL *log_pending); UINT32 get_connection_proxy_id(UINT16 src_port, BOOL is_udp); void remove_connection(UINT16 src_port, BOOL is_udp, BOOL is_ipv6); +void remove_connections_for_process(DWORD process_id, const FILETIME *process_created); void cleanup_stale_connections(void); BOOL is_connection_already_logged(DWORD pid, UINT32 dest_ip, UINT16 dest_port, RuleAction action); void add_logged_connection(DWORD pid, UINT32 dest_ip, UINT16 dest_port, RuleAction action); diff --git a/Windows/src/pb_process.c b/Windows/src/pb_process.c index 46a7955..1355457 100644 --- a/Windows/src/pb_process.c +++ b/Windows/src/pb_process.c @@ -3,6 +3,22 @@ // Process resolution: src-port -> PID lookups and the PID cache. +BOOL get_process_identity(DWORD pid, FILETIME *created, HANDLE *process_handle) +{ + if (!pid || !created || !process_handle) return FALSE; + HANDLE process = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + FILETIME exited, kernel, user; + if (!process) return FALSE; + if (!GetProcessTimes(process, created, &exited, &kernel, &user) || + WaitForSingleObject(process, 0) != WAIT_TIMEOUT) + { + CloseHandle(process); + return FALSE; + } + *process_handle = process; + return TRUE; +} + // Reusable grow-only scratch buffer for the TCP/UDP owner-PID tables. These lookups run // only on the single packet-processor thread (NUM_PACKET_THREADS == 1) and each call // finishes scanning before the next one, so one shared buffer is safe and lets us skip a diff --git a/Windows/src/pb_proxy.c b/Windows/src/pb_proxy.c index cdccb72..c7d5d07 100644 --- a/Windows/src/pb_proxy.c +++ b/Windows/src/pb_proxy.c @@ -197,9 +197,11 @@ PROXYBRIDGE_API int ProxyBridge_TestProxyConfig(UINT32 config_id, const char* ta int result; if (cfg->type == PROXY_TYPE_SOCKS5) - result = socks5_connect(sock, dest_ip, target_port, cfg); + result = cfg->send_domain_to_proxy + ? socks5_connect_domain(sock, target_host, target_port, cfg) + : socks5_connect(sock, dest_ip, target_port, cfg); else - result = http_connect(sock, dest_ip, target_port, cfg); + result = http_connect(sock, dest_ip, target_port, cfg, target_host); closesocket(sock); @@ -278,8 +280,11 @@ PROXYBRIDGE_API int ProxyBridge_TestProxyConfigEx(UINT32 config_id, const char* else { ULONGLONG h0 = GetTickCount64(); - int rc = is_socks ? socks5_connect(s, dest_ip, target_port, cfg) - : http_connect(s, dest_ip, target_port, cfg); + int rc = is_socks + ? (cfg->send_domain_to_proxy + ? socks5_connect_domain(s, target_host, target_port, cfg) + : socks5_connect(s, dest_ip, target_port, cfg)) + : http_connect(s, dest_ip, target_port, cfg, target_host); ULONGLONG h1 = GetTickCount64(); if (rc != 0) { diff --git a/Windows/src/pb_relay.c b/Windows/src/pb_relay.c index bd36c2e..a2699de 100644 --- a/Windows/src/pb_relay.c +++ b/Windows/src/pb_relay.c @@ -2,6 +2,11 @@ // Relay: TCP/UDP relay servers and per-connection worker threads. +static void relay_report_connection(DWORD process_id, BOOL is_ipv6, UINT32 dest_ip, + const UINT8 dest_ip6[16], UINT16 dest_port, + const char *hostname, const PROXY_CONFIG *proxy, + const char *failure_reason); + DWORD WINAPI udp_relay_server(LPVOID arg) { WSADATA wsa_data; @@ -166,9 +171,11 @@ DWORD WINAPI udp_relay_server(LPVOID arg) UINT32 dest_ip; UINT16 dest_port; - if (get_connection(from_port, TRUE, &dest_ip, &dest_port)) + char hostname[256] = {0}; + UINT32 proxy_config_id = 0; + if (get_connection_full(from_port, TRUE, &dest_ip, &dest_port, + &proxy_config_id, hostname, sizeof(hostname), NULL, NULL)) { - UINT32 proxy_config_id = get_connection_proxy_id(from_port, TRUE); PROXY_CONFIG *cfg = find_proxy_config(proxy_config_id); if (cfg == NULL || cfg->type != PROXY_TYPE_SOCKS5) @@ -190,19 +197,34 @@ DWORD WINAPI udp_relay_server(LPVOID arg) assoc_replaced = 1; // new sockets created - read_fds is stale } - send_buf[0] = 0; - send_buf[1] = 0; - send_buf[2] = 0; - send_buf[3] = SOCKS5_ATYP_IPV4; - send_buf[4] = (dest_ip >> 0) & 0xFF; - send_buf[5] = (dest_ip >> 8) & 0xFF; - send_buf[6] = (dest_ip >> 16) & 0xFF; - send_buf[7] = (dest_ip >> 24) & 0xFF; - send_buf[8] = (dest_port >> 8) & 0xFF; - send_buf[9] = (dest_port >> 0) & 0xFF; - memcpy(&send_buf[10], recv_buf, recv_len); - - int sent = sendto(cfg->udp_send_sock, (char*)send_buf, 10 + recv_len, 0, + size_t header_len = 10; + send_buf[0] = 0; send_buf[1] = 0; send_buf[2] = 0; + if (cfg->send_domain_to_proxy && hostname[0] && + strnlen_s(hostname, sizeof(hostname)) <= 255) + { + size_t host_len = strlen(hostname); + header_len = 7 + host_len; + if (recv_len > MAXBUF - header_len) continue; + send_buf[3] = SOCKS5_ATYP_DOMAIN; + send_buf[4] = (unsigned char)host_len; + memcpy(&send_buf[5], hostname, host_len); + send_buf[5 + host_len] = (dest_port >> 8) & 0xFF; + send_buf[6 + host_len] = dest_port & 0xFF; + } + else + { + if (recv_len > MAXBUF - header_len) continue; + send_buf[3] = SOCKS5_ATYP_IPV4; + send_buf[4] = (dest_ip >> 0) & 0xFF; + send_buf[5] = (dest_ip >> 8) & 0xFF; + send_buf[6] = (dest_ip >> 16) & 0xFF; + send_buf[7] = (dest_ip >> 24) & 0xFF; + send_buf[8] = (dest_port >> 8) & 0xFF; + send_buf[9] = dest_port & 0xFF; + } + memcpy(&send_buf[header_len], recv_buf, recv_len); + + int sent = sendto(cfg->udp_send_sock, (char*)send_buf, (int)(header_len + recv_len), 0, (struct sockaddr *)&cfg->udp_relay_addr, sizeof(cfg->udp_relay_addr)); if (sent == SOCKET_ERROR) { @@ -215,7 +237,7 @@ DWORD WINAPI udp_relay_server(LPVOID arg) // lose at most one packet during a proxy reconnect event. if (establish_udp_associate_for_config(cfg)) { - sendto(cfg->udp_send_sock, (char*)send_buf, 10 + recv_len, 0, + sendto(cfg->udp_send_sock, (char*)send_buf, (int)(header_len + recv_len), 0, (struct sockaddr *)&cfg->udp_relay_addr, sizeof(cfg->udp_relay_addr)); } assoc_replaced = 1; // sockets replaced - read_fds is stale @@ -261,7 +283,7 @@ DWORD WINAPI udp_relay_server(LPVOID arg) if (recv_len > 0) { // Packet from SOCKS5 proxy - decapsulate and forward to original sender - if (recv_len < 10) continue; + if (recv_len < 4) continue; // SOCKS5 UDP: RSV(2) + FRAG(1) + ATYP(1) + DST.ADDR + DST.PORT(2) + DATA if (recv_buf[2] != 0x00) continue; // FRAG must be 0 @@ -340,6 +362,52 @@ DWORD WINAPI udp_relay_server(LPVOID arg) (struct sockaddr*)&t6, sizeof(t6)); } } + else if (recv_buf[3] == SOCKS5_ATYP_DOMAIN && recv_len >= 8) + { + size_t host_len = recv_buf[4]; + size_t payload_offset = 7 + host_len; + if (!host_len || (size_t)recv_len < payload_offset) continue; + + char hostname[256] = {0}; + memcpy(hostname, &recv_buf[5], host_len); + if (memchr(hostname, '\0', host_len)) continue; + UINT16 domain_port = ((UINT16)recv_buf[5 + host_len] << 8) | + recv_buf[6 + host_len]; + BOOL is_ipv6 = FALSE; + UINT32 client_ip = 0; + UINT8 client_ip6[16] = {0}; + UINT16 client_port = 0; + if (!find_unique_udp_sender_by_domain(hostname, domain_port, cfg->config_id, &is_ipv6, + &client_ip, client_ip6, &client_port)) + { + log_message("[UDP RELAY] No unique session for proxy domain response %s:%u - dropped", + hostname, domain_port); + continue; + } + + if (is_ipv6 && udp_relay_socket6 != INVALID_SOCKET) + { + struct sockaddr_in6 target_addr6; + memset(&target_addr6, 0, sizeof(target_addr6)); + target_addr6.sin6_family = AF_INET6; + memcpy(&target_addr6.sin6_addr, client_ip6, 16); + target_addr6.sin6_port = htons(client_port); + sendto(udp_relay_socket6, (char*)&recv_buf[payload_offset], + (int)(recv_len - payload_offset), 0, + (struct sockaddr*)&target_addr6, sizeof(target_addr6)); + } + else if (!is_ipv6 && udp_relay_socket != INVALID_SOCKET) + { + struct sockaddr_in target_addr; + memset(&target_addr, 0, sizeof(target_addr)); + target_addr.sin_family = AF_INET; + target_addr.sin_addr.s_addr = client_ip; + target_addr.sin_port = htons(client_port); + sendto(udp_relay_socket, (char*)&recv_buf[payload_offset], + (int)(recv_len - payload_offset), 0, + (struct sockaddr*)&target_addr, sizeof(target_addr)); + } + } } } @@ -357,7 +425,9 @@ DWORD WINAPI udp_relay_server(LPVOID arg) UINT16 dest_port = 0; UINT32 proxy_config_id = 0; - if (get_connection_full_v6(from_port, TRUE, dest_ip6, &dest_port, &proxy_config_id)) + char hostname[256] = {0}; + if (get_connection_full_v6(from_port, TRUE, dest_ip6, &dest_port, + &proxy_config_id, hostname, sizeof(hostname), NULL, NULL)) { PROXY_CONFIG *cfg = find_proxy_config(proxy_config_id); if (cfg != NULL && cfg->type == PROXY_TYPE_SOCKS5) @@ -365,13 +435,30 @@ DWORD WINAPI udp_relay_server(LPVOID arg) if (!cfg->udp_connected) establish_udp_associate_for_config(cfg); if (cfg->udp_connected) { + size_t header_len = 22; send_buf[0] = 0; send_buf[1] = 0; send_buf[2] = 0; - send_buf[3] = SOCKS5_ATYP_IPV6; - memcpy(&send_buf[4], dest_ip6, 16); - send_buf[20] = (dest_port>>8)&0xFF; - send_buf[21] = (dest_port>>0)&0xFF; - memcpy(&send_buf[22], recv_buf, recv_len); - sendto(cfg->udp_send_sock, (char*)send_buf, 22+recv_len, 0, + if (cfg->send_domain_to_proxy && hostname[0] && + strnlen_s(hostname, sizeof(hostname)) <= 255) + { + size_t host_len = strlen(hostname); + header_len = 7 + host_len; + if (recv_len > MAXBUF - header_len) continue; + send_buf[3] = SOCKS5_ATYP_DOMAIN; + send_buf[4] = (unsigned char)host_len; + memcpy(&send_buf[5], hostname, host_len); + send_buf[5 + host_len] = (dest_port >> 8) & 0xFF; + send_buf[6 + host_len] = dest_port & 0xFF; + } + else + { + if (recv_len > MAXBUF - header_len) continue; + send_buf[3] = SOCKS5_ATYP_IPV6; + memcpy(&send_buf[4], dest_ip6, 16); + send_buf[20] = (dest_port >> 8) & 0xFF; + send_buf[21] = dest_port & 0xFF; + } + memcpy(&send_buf[header_len], recv_buf, recv_len); + sendto(cfg->udp_send_sock, (char*)send_buf, (int)(header_len + recv_len), 0, (struct sockaddr*)&cfg->udp_relay_addr, sizeof(cfg->udp_relay_addr)); } } @@ -513,19 +600,56 @@ DWORD WINAPI local_proxy_server(LPVOID arg) { conn_config->client_socket = client_sock; conn_config->is_ipv6 = FALSE; + conn_config->process_id = 0; + conn_config->log_pending = FALSE; UINT16 client_port = ntohs(client_addr.sin_port); - if (get_connection_full(client_port, FALSE, &conn_config->orig_dest_ip, &conn_config->orig_dest_port, &conn_config->proxy_config_id) && - relay_peer_ok_v4(client_addr.sin_addr.s_addr, conn_config->orig_dest_ip)) + BOOL mapped = get_connection_full(client_port, FALSE, &conn_config->orig_dest_ip, + &conn_config->orig_dest_port, &conn_config->proxy_config_id, + conn_config->hostname, sizeof(conn_config->hostname), + &conn_config->process_id, &conn_config->log_pending); + BOOL trusted_peer = mapped && relay_peer_ok_v4(client_addr.sin_addr.s_addr, + conn_config->orig_dest_ip); + if (trusted_peer) { InterlockedIncrement(&g_handler_threads); HANDLE conn_thread = CreateThread(NULL, 1, connection_handler, (LPVOID)conn_config, 0, NULL); if (conn_thread != NULL) { CloseHandle(conn_thread); } - else { InterlockedDecrement(&g_handler_threads); closesocket(client_sock); free(conn_config); } + else + { + DWORD error = GetLastError(); + InterlockedDecrement(&g_handler_threads); + if (conn_config->log_pending) + relay_report_connection(conn_config->process_id, FALSE, + conn_config->orig_dest_ip, NULL, + conn_config->orig_dest_port, conn_config->hostname, + find_proxy_config(conn_config->proxy_config_id), + "relay handler could not start"); + log_message("[RELAY] Could not start handler for IPv4 port %u (%lu)", + client_port, error); + closesocket(client_sock); + free(conn_config); + } + } + else + { + if (!mapped) + log_message("[RELAY] No connection metadata for IPv4 port %u", client_port); + else + { + log_message("[RELAY] Rejected untrusted IPv4 peer for tracked port %u", client_port); + if (conn_config->log_pending) + relay_report_connection(conn_config->process_id, FALSE, + conn_config->orig_dest_ip, NULL, + conn_config->orig_dest_port, conn_config->hostname, + find_proxy_config(conn_config->proxy_config_id), + "untrusted relay peer"); + } + closesocket(client_sock); + free(conn_config); } - else { closesocket(client_sock); free(conn_config); } } - else { closesocket(client_sock); } + else { log_message("[RELAY] Could not allocate IPv4 connection context"); closesocket(client_sock); } } } @@ -542,19 +666,56 @@ DWORD WINAPI local_proxy_server(LPVOID arg) { conn_config->client_socket = client_sock6; conn_config->is_ipv6 = TRUE; + conn_config->process_id = 0; + conn_config->log_pending = FALSE; UINT16 client_port = ntohs(client_addr6.sin6_port); - if (get_connection_full_v6(client_port, FALSE, conn_config->orig_dest_ip6, &conn_config->orig_dest_port, &conn_config->proxy_config_id) && - relay_peer_ok_v6(&client_addr6.sin6_addr, conn_config->orig_dest_ip6)) + BOOL mapped = get_connection_full_v6(client_port, FALSE, conn_config->orig_dest_ip6, + &conn_config->orig_dest_port, &conn_config->proxy_config_id, + conn_config->hostname, sizeof(conn_config->hostname), + &conn_config->process_id, &conn_config->log_pending); + BOOL trusted_peer = mapped && relay_peer_ok_v6(&client_addr6.sin6_addr, + conn_config->orig_dest_ip6); + if (trusted_peer) { InterlockedIncrement(&g_handler_threads); HANDLE conn_thread = CreateThread(NULL, 1, connection_handler, (LPVOID)conn_config, 0, NULL); if (conn_thread != NULL) { CloseHandle(conn_thread); } - else { InterlockedDecrement(&g_handler_threads); closesocket(client_sock6); free(conn_config); } + else + { + DWORD error = GetLastError(); + InterlockedDecrement(&g_handler_threads); + if (conn_config->log_pending) + relay_report_connection(conn_config->process_id, TRUE, 0, + conn_config->orig_dest_ip6, + conn_config->orig_dest_port, conn_config->hostname, + find_proxy_config(conn_config->proxy_config_id), + "relay handler could not start"); + log_message("[RELAY] Could not start handler for IPv6 port %u (%lu)", + client_port, error); + closesocket(client_sock6); + free(conn_config); + } + } + else + { + if (!mapped) + log_message("[RELAY] No connection metadata for IPv6 port %u", client_port); + else + { + log_message("[RELAY] Rejected untrusted IPv6 peer for tracked port %u", client_port); + if (conn_config->log_pending) + relay_report_connection(conn_config->process_id, TRUE, 0, + conn_config->orig_dest_ip6, + conn_config->orig_dest_port, conn_config->hostname, + find_proxy_config(conn_config->proxy_config_id), + "untrusted relay peer"); + } + closesocket(client_sock6); + free(conn_config); } - else { closesocket(client_sock6); free(conn_config); } } - else { closesocket(client_sock6); } + else { log_message("[RELAY] Could not allocate IPv6 connection context"); closesocket(client_sock6); } } } } @@ -580,6 +741,90 @@ volatile LONG g_handler_threads = 0; static DWORD WINAPI connection_handler_impl(LPVOID arg); +// Text form of a redirected flow's original destination, matching the packet reader's +// formatting so both producers of a connection log line look identical. +static void relay_format_dest(BOOL is_ipv6, UINT32 dest_ip, const UINT8 dest_ip6[16], + char *out, size_t out_size) +{ + if (is_ipv6) + { + struct in6_addr addr6; + memcpy(&addr6, dest_ip6, 16); + if (inet_ntop(AF_INET6, &addr6, out, (DWORD)out_size) == NULL) + _snprintf_s(out, out_size, _TRUNCATE, "::"); + } + else + { + _snprintf_s(out, out_size, _TRUNCATE, "%u.%u.%u.%u", + (unsigned)((dest_ip >> 0) & 0xFF), (unsigned)((dest_ip >> 8) & 0xFF), + (unsigned)((dest_ip >> 16) & 0xFF), (unsigned)((dest_ip >> 24) & 0xFF)); + } +} + +// Emit the connection log line that ProxyBridge.c left to the relay (log_pending). The name +// is whatever was recovered by the time the flow reached us - possibly still nothing, in +// which case the line shows the address exactly like a plain socks5 proxy does. +static void relay_report_connection(DWORD process_id, BOOL is_ipv6, UINT32 dest_ip, + const UINT8 dest_ip6[16], UINT16 dest_port, + const char *hostname, const PROXY_CONFIG *proxy, + const char *failure_reason) +{ + char process_name[MAX_PROCESS_NAME] = { 0 }; + const char *display_name = "?"; + if (process_id != 0 && get_process_name_from_pid(process_id, process_name, sizeof(process_name))) + display_name = extract_filename(process_name); + + char ip_str[64]; + relay_format_dest(is_ipv6, dest_ip, dest_ip6, ip_str, sizeof(ip_str)); + + char proxy_info[128]; + if (failure_reason != NULL) + snprintf(proxy_info, sizeof(proxy_info), "Proxy failed: %s", failure_reason); + else if (proxy != NULL) + snprintf(proxy_info, sizeof(proxy_info), "Proxy %s://%s:%u", + proxy->type == PROXY_TYPE_HTTP ? "HTTP" : "SOCKS5", proxy->host, proxy->port); + else + snprintf(proxy_info, sizeof(proxy_info), "Proxy"); + + pb_report_connection(display_name, process_id, ip_str, hostname, dest_port, proxy_info); + + if (!failure_reason && g_traffic_logging_enabled && process_id != 0) + { + UINT32 dedup_ip = dest_ip; + if (is_ipv6) + { + UINT32 words[4]; + memcpy(words, dest_ip6, sizeof(words)); + dedup_ip = words[0] ^ words[1] ^ words[2] ^ words[3]; + } + add_logged_connection(process_id, dedup_ip, dest_port, RULE_ACTION_PROXY); + } +} + +// The application resolved this destination somehow, but nothing ProxyBridge can observe saw +// the answer: the address came out of the app's own cache, so there was no DNS query on the +// wire and no DNS Client event carrying results. The TLS ClientHello the app is sending right +// now still names the site, so read it (peek only - the relay must still forward those bytes). +// SNI is connection-local evidence: do not reuse it for later rule decisions. +static void relay_learn_hostname_from_sni(SOCKET client_sock, DWORD process_id, BOOL is_ipv6, + UINT32 dest_ip, const UINT8 dest_ip6[16], + UINT16 dest_port, char hostname[256]) +{ + char sni[256]; + if (!sni_peek_from_socket(client_sock, sni, sizeof(sni))) return; + + strncpy_s(hostname, 256, sni, _TRUNCATE); + + static volatile LONG learned_logged = 0; + if (InterlockedIncrement(&learned_logged) <= 20) + { + char ip_str[64]; + relay_format_dest(is_ipv6, dest_ip, dest_ip6, ip_str, sizeof(ip_str)); + log_message("[HOSTNAME] %s:%u -> %s (PID:%lu) - read from the TLS ClientHello", + ip_str, dest_port, sni, (unsigned long)process_id); + } +} + // Every handshake thread is counted from the moment it is created until it returns, so that // ProxyBridge_Stop() can wait for all of them. A thread still running inside the DLL when the DLL // is unloaded (the service reloads the engine for every GUI connection) crashes the process. @@ -597,9 +842,13 @@ static DWORD WINAPI connection_handler_impl(LPVOID arg) UINT32 dest_ip = config->orig_dest_ip; UINT16 dest_port = config->orig_dest_port; UINT32 proxy_config_id = config->proxy_config_id; + char hostname[256]; + strncpy_s(hostname, sizeof(hostname), config->hostname, _TRUNCATE); BOOL is_ipv6 = config->is_ipv6; UINT8 dest_ip6[16]; if (is_ipv6) memcpy(dest_ip6, config->orig_dest_ip6, 16); + DWORD process_id = config->process_id; + BOOL log_pending = config->log_pending; SOCKET socks_sock; struct sockaddr_in socks_addr; @@ -610,10 +859,23 @@ static DWORD WINAPI connection_handler_impl(LPVOID arg) if (proxy == NULL || proxy->host[0] == '\0' || proxy->port == 0) { log_message("[RELAY] No proxy config (id=%u) - dropping connection", proxy_config_id); + if (log_pending) + relay_report_connection(process_id, is_ipv6, dest_ip, dest_ip6, dest_port, hostname, + NULL, "proxy configuration unavailable"); closesocket(client_sock); return 1; } + // This flow's connection log belongs to the relay (see log_pending in ProxyBridge.c): + // nothing could name the destination when the first packet was classified, but the + // client's ClientHello - which does name it - is arriving on client_sock right now. + if (log_pending && hostname[0] == '\0') + relay_learn_hostname_from_sni(client_sock, process_id, is_ipv6, dest_ip, dest_ip6, + dest_port, hostname); + if (log_pending) + relay_report_connection(process_id, is_ipv6, dest_ip, dest_ip6, dest_port, hostname, + proxy, NULL); + // Connect to proxy, use cached resolved IP to avoid DNS per connection UINT32 proxy_ip = proxy->resolved_ip ? proxy->resolved_ip : resolve_hostname(proxy->host); if (proxy_ip == 0) @@ -657,20 +919,19 @@ static DWORD WINAPI connection_handler_impl(LPVOID arg) if (proxy->type == PROXY_TYPE_SOCKS5) { int rc; - char cached_domain[256]; // Per-config: only hand the hostname to the proxy (socks5h) when this config // opts in; otherwise send the locally-resolved IP (socks5). if (is_ipv6) { - if (proxy->send_domain_to_proxy && dns_cache_lookup_v6(dest_ip6, cached_domain, sizeof(cached_domain))) - rc = socks5_connect_domain(socks_sock, cached_domain, dest_port, proxy); + if (proxy->send_domain_to_proxy && hostname[0]) + rc = socks5_connect_domain(socks_sock, hostname, dest_port, proxy); else rc = socks5_connect_v6(socks_sock, dest_ip6, dest_port, proxy); } else { - if (proxy->send_domain_to_proxy && dns_cache_lookup(dest_ip, cached_domain, sizeof(cached_domain))) - rc = socks5_connect_domain(socks_sock, cached_domain, dest_port, proxy); + if (proxy->send_domain_to_proxy && hostname[0]) + rc = socks5_connect_domain(socks_sock, hostname, dest_port, proxy); else rc = socks5_connect(socks_sock, dest_ip, dest_port, proxy); } @@ -684,8 +945,8 @@ static DWORD WINAPI connection_handler_impl(LPVOID arg) else if (proxy->type == PROXY_TYPE_HTTP) { int rc = is_ipv6 - ? http_connect_v6(socks_sock, dest_ip6, dest_port, proxy) - : http_connect(socks_sock, dest_ip, dest_port, proxy); + ? http_connect_v6(socks_sock, dest_ip6, dest_port, proxy, hostname) + : http_connect(socks_sock, dest_ip, dest_port, proxy, hostname); if (rc != 0) { reset_close(client_sock); diff --git a/Windows/src/pb_rules.c b/Windows/src/pb_rules.c index e6aa78b..1e99959 100644 --- a/Windows/src/pb_rules.c +++ b/Windows/src/pb_rules.c @@ -38,10 +38,15 @@ static DWORD retry_tcp_pid(DWORD pid, BOOL ipv6, UINT32 src_ip, const UINT8 *src return pid; } -RuleAction check_process_rule_v6(const UINT8 src_ip6[16], UINT16 src_port, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, DWORD *out_pid, UINT32 *out_proxy_config_id) +RuleAction check_process_rule_v6(const UINT8 src_ip6[16], UINT16 src_port, + const UINT8 dest_ip6[16], UINT16 dest_port, + BOOL is_udp, ULONGLONG packet_qpc, + char hostname[256], DWORD *out_pid, + UINT32 *out_proxy_config_id) { DWORD pid; char process_name[MAX_PROCESS_NAME]; + if (hostname) hostname[0] = '\0'; pid = is_udp ? get_process_id_from_udp_connection_v6(src_ip6, src_port) : get_process_id_from_connection_v6(src_ip6, src_port); @@ -53,16 +58,53 @@ RuleAction check_process_rule_v6(const UINT8 src_ip6[16], UINT16 src_port, const if (!get_process_name_from_pid(pid, process_name, sizeof(process_name))) return RULE_ACTION_DIRECT; + BOOL needs_hostname = FALSE; UINT32 proxy_config_id = 0; - RuleAction action = match_rule_v6(process_name, dest_ip6, dest_port, is_udp, &proxy_config_id); + RuleAction action = match_rule_v6(process_name, dest_ip6, dest_port, is_udp, NULL, + &needs_hostname, &proxy_config_id); + BOOL hostname_checked = FALSE; + if (needs_hostname && hostname) + { + // Flush ETW on its worker and bound capture-thread waiting to the decision budget. + dns_etw_lookup(pid, TRUE, 0, dest_ip6, packet_qpc, TRUE, hostname, 256); + hostname_checked = TRUE; + if (hostname[0]) + action = match_rule_v6(process_name, dest_ip6, dest_port, is_udp, hostname, + NULL, &proxy_config_id); + } if (action == RULE_ACTION_PROXY) { PROXY_CONFIG *cfg = find_proxy_config(proxy_config_id); if (cfg == NULL || cfg->host[0] == '\0' || cfg->port == 0) - return RULE_ACTION_DIRECT; + { + if (hostname) hostname[0] = '\0'; + return RULE_ACTION_DIRECT; // No proxy configured + } if (is_udp && cfg->type == PROXY_TYPE_HTTP) + { + if (hostname) hostname[0] = '\0'; return RULE_ACTION_DIRECT; + } + } + + // The recovered name is surfaced only for connections whose proxy resolves DNS + // itself (send hostname). It is an internal heuristic tied to that feature, so + // Direct traffic and plain socks5 proxies log the IP, not the name. + BOOL expose_hostname = FALSE; + if (action == RULE_ACTION_PROXY) + { + PROXY_CONFIG *cfg = find_proxy_config(proxy_config_id); + if (cfg && cfg->send_domain_to_proxy) expose_hostname = TRUE; + } + if (expose_hostname) + { + if (!hostname_checked && hostname) + dns_etw_lookup(pid, TRUE, 0, dest_ip6, packet_qpc, TRUE, hostname, 256); + } + else if (hostname) + { + hostname[0] = '\0'; } if (out_proxy_config_id) *out_proxy_config_id = proxy_config_id; return action; @@ -534,17 +576,17 @@ BOOL is_broadcast_or_multicast(UINT32 ip) // Matches rules by process name, IP, port, and protocol // Inner matcher - caller MUST hold g_rules_lock (shared) for the whole traversal so // rules_list and the strings it points to cannot be freed/edited mid-match. -RuleAction match_rule_inner(const char *process_name, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id) +RuleAction match_rule_inner(const char *process_name, UINT32 dest_ip, UINT16 dest_port, + BOOL is_udp, const char *hostname, + BOOL *out_needs_hostname, + UINT32 *out_proxy_config_id) { PROCESS_RULE *rule = rules_list; PROCESS_RULE *wildcard_rule = NULL; // Save fully wildcard rule for last - // Resolve the destination hostname once (only when domain rules exist) from the - // DNS-snoop cache. Reused for every rule's domain filter below. Cache miss -> unknown. - char domain[256]; - const char *dst_domain = NULL; - if (g_has_domain_rules && dns_cache_lookup(dest_ip, domain, sizeof(domain))) - dst_domain = domain; + const char *dst_domain = hostname && hostname[0] ? hostname : NULL; + // Used by the first pass to avoid waiting for unrelated domain rules. + if (out_needs_hostname) *out_needs_hostname = FALSE; while (rule != NULL) { @@ -584,12 +626,16 @@ RuleAction match_rule_inner(const char *process_name, UINT32 dest_ip, UINT16 des { // Filtered wildcard - check if it matches if (match_ip_list(rule->target_hosts, dest_ip) && - match_port_list(rule->target_ports, dest_port) && - match_domain_filter(rule, dst_domain)) + match_port_list(rule->target_ports, dest_port)) { - // Matched! Return this rule's action - if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; - return rule->action; + if (has_domain_filter && !dst_domain && out_needs_hostname) + *out_needs_hostname = TRUE; + if (match_domain_filter(rule, dst_domain)) + { + // Matched! Return this rule's action + if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; + return rule->action; + } } // Didn't match, continue rule = rule->next; @@ -610,12 +656,16 @@ RuleAction match_rule_inner(const char *process_name, UINT32 dest_ip, UINT16 des { // Process matched! Check IP, port and domain filters if (match_ip_list(rule->target_hosts, dest_ip) && - match_port_list(rule->target_ports, dest_port) && - match_domain_filter(rule, dst_domain)) + match_port_list(rule->target_ports, dest_port)) { - // All filters matched! Return this rule's action - if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; - return rule->action; + if (rule_has_domain_filter(rule) && !dst_domain && out_needs_hostname) + *out_needs_hostname = TRUE; + if (match_domain_filter(rule, dst_domain)) + { + // All filters matched! Return this rule's action + if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; + return rule->action; + } } } @@ -636,10 +686,13 @@ RuleAction match_rule_inner(const char *process_name, UINT32 dest_ip, UINT16 des // Public matcher - takes the shared rules lock so a concurrent AddRule/EditRule/ // DeleteRule/MoveRule from the GUI thread cannot free a node/string mid-match. -RuleAction match_rule(const char *process_name, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id) +RuleAction match_rule(const char *process_name, UINT32 dest_ip, UINT16 dest_port, + BOOL is_udp, const char *hostname, BOOL *out_needs_hostname, + UINT32 *out_proxy_config_id) { AcquireSRWLockShared(&g_rules_lock); - RuleAction action = match_rule_inner(process_name, dest_ip, dest_port, is_udp, out_proxy_config_id); + RuleAction action = match_rule_inner(process_name, dest_ip, dest_port, is_udp, + hostname, out_needs_hostname, out_proxy_config_id); ReleaseSRWLockShared(&g_rules_lock); return action; } @@ -648,17 +701,17 @@ RuleAction match_rule(const char *process_name, UINT32 dest_ip, UINT16 dest_port // Supports exact addresses ("::1"), CIDR ("2001:db8::/32"), and wildcards ("*"). // IPv4-format patterns in target_hosts are silently skipped for IPv6 traffic. // Caller MUST hold g_rules_lock (shared) - see match_rule_v6 wrapper below. -RuleAction match_rule_v6_inner(const char *process_name, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id) +RuleAction match_rule_v6_inner(const char *process_name, const UINT8 dest_ip6[16], + UINT16 dest_port, BOOL is_udp, const char *hostname, + BOOL *out_needs_hostname, + UINT32 *out_proxy_config_id) { PROCESS_RULE *rule = rules_list; PROCESS_RULE *wildcard_rule = NULL; - // Resolve the destination hostname once (only when domain rules exist) from the - // IPv6 DNS-snoop cache. Reused for every rule's domain filter below. - char domain[256]; - const char *dst_domain = NULL; - if (g_has_domain_rules && dns_cache_lookup_v6(dest_ip6, domain, sizeof(domain))) - dst_domain = domain; + const char *dst_domain = hostname && hostname[0] ? hostname : NULL; + // Used by the first pass to avoid waiting for unrelated domain rules. + if (out_needs_hostname) *out_needs_hostname = FALSE; while (rule != NULL) { @@ -685,11 +738,15 @@ RuleAction match_rule_v6_inner(const char *process_name, const UINT8 dest_ip6[16 if (has_ip_filter || has_port_filter || has_domain_filter) { if (match_ip_list_v6(rule->target_hosts, dest_ip6) && - match_port_list(rule->target_ports, dest_port) && - match_domain_filter(rule, dst_domain)) + match_port_list(rule->target_ports, dest_port)) { - if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; - return rule->action; + if (has_domain_filter && !dst_domain && out_needs_hostname) + *out_needs_hostname = TRUE; + if (match_domain_filter(rule, dst_domain)) + { + if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; + return rule->action; + } } rule = rule->next; continue; @@ -704,11 +761,15 @@ RuleAction match_rule_v6_inner(const char *process_name, const UINT8 dest_ip6[16 if (match_process_list(rule->process_name, process_name)) { if (match_ip_list_v6(rule->target_hosts, dest_ip6) && - match_port_list(rule->target_ports, dest_port) && - match_domain_filter(rule, dst_domain)) + match_port_list(rule->target_ports, dest_port)) { - if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; - return rule->action; + if (rule_has_domain_filter(rule) && !dst_domain && out_needs_hostname) + *out_needs_hostname = TRUE; + if (match_domain_filter(rule, dst_domain)) + { + if (out_proxy_config_id != NULL) *out_proxy_config_id = rule->proxy_config_id; + return rule->action; + } } } @@ -725,18 +786,26 @@ RuleAction match_rule_v6_inner(const char *process_name, const UINT8 dest_ip6[16 return RULE_ACTION_DIRECT; } -RuleAction match_rule_v6(const char *process_name, const UINT8 dest_ip6[16], UINT16 dest_port, BOOL is_udp, UINT32 *out_proxy_config_id) +RuleAction match_rule_v6(const char *process_name, const UINT8 dest_ip6[16], + UINT16 dest_port, BOOL is_udp, const char *hostname, + BOOL *out_needs_hostname, + UINT32 *out_proxy_config_id) { AcquireSRWLockShared(&g_rules_lock); - RuleAction action = match_rule_v6_inner(process_name, dest_ip6, dest_port, is_udp, out_proxy_config_id); + RuleAction action = match_rule_v6_inner(process_name, dest_ip6, dest_port, is_udp, + hostname, out_needs_hostname, out_proxy_config_id); ReleaseSRWLockShared(&g_rules_lock); return action; } -RuleAction check_process_rule(UINT32 src_ip, UINT16 src_port, UINT32 dest_ip, UINT16 dest_port, BOOL is_udp, DWORD *out_pid, UINT32 *out_proxy_config_id) +RuleAction check_process_rule(UINT32 src_ip, UINT16 src_port, UINT32 dest_ip, + UINT16 dest_port, BOOL is_udp, ULONGLONG packet_qpc, + char hostname[256], DWORD *out_pid, + UINT32 *out_proxy_config_id) { DWORD pid; char process_name[MAX_PROCESS_NAME]; + if (hostname) hostname[0] = '\0'; pid = is_udp ? get_process_id_from_udp_connection(src_ip, src_port) : get_process_id_from_connection(src_ip, src_port); if (pid == 0 && is_udp) @@ -758,20 +827,56 @@ RuleAction check_process_rule(UINT32 src_ip, UINT16 src_port, UINT32 dest_ip, UI if (!get_process_name_from_pid(pid, process_name, sizeof(process_name))) return RULE_ACTION_DIRECT; - // Use unified rule matching function + BOOL needs_hostname = FALSE; UINT32 proxy_config_id = 0; - RuleAction action = match_rule(process_name, dest_ip, dest_port, is_udp, &proxy_config_id); + RuleAction action = match_rule(process_name, dest_ip, dest_port, is_udp, NULL, + &needs_hostname, &proxy_config_id); + BOOL hostname_checked = FALSE; + if (needs_hostname && hostname) + { + // Flush ETW on its worker and bound capture-thread waiting to the decision budget. + dns_etw_lookup(pid, FALSE, dest_ip, NULL, packet_qpc, TRUE, hostname, 256); + hostname_checked = TRUE; + if (hostname[0]) + action = match_rule(process_name, dest_ip, dest_port, is_udp, hostname, + NULL, &proxy_config_id); + } // Additional checks for proxy configuration if (action == RULE_ACTION_PROXY) { PROXY_CONFIG *cfg = find_proxy_config(proxy_config_id); if (cfg == NULL || cfg->host[0] == '\0' || cfg->port == 0) + { + if (hostname) hostname[0] = '\0'; return RULE_ACTION_DIRECT; // No proxy configured + } // UDP: HTTP proxy doesn't support UDP - use per-rule proxy config type if (is_udp && cfg->type == PROXY_TYPE_HTTP) + { + if (hostname) hostname[0] = '\0'; return RULE_ACTION_DIRECT; + } + } + + // The recovered name is surfaced only for connections whose proxy resolves DNS + // itself (send hostname). It is an internal heuristic tied to that feature, so + // Direct traffic and plain socks5 proxies log the IP, not the name. + BOOL expose_hostname = FALSE; + if (action == RULE_ACTION_PROXY) + { + PROXY_CONFIG *cfg = find_proxy_config(proxy_config_id); + if (cfg && cfg->send_domain_to_proxy) expose_hostname = TRUE; + } + if (expose_hostname) + { + if (!hostname_checked && hostname) + dns_etw_lookup(pid, FALSE, dest_ip, NULL, packet_qpc, TRUE, hostname, 256); + } + else if (hostname) + { + hostname[0] = '\0'; } if (out_proxy_config_id != NULL) @@ -1167,7 +1272,6 @@ PROXYBRIDGE_API BOOL ProxyBridge_MoveRuleToPosition(UINT32 rule_id, UINT32 new_p void update_has_active_rules(void) { BOOL has_active = FALSE; - BOOL has_domain = FALSE; AcquireSRWLockShared(&g_rules_lock); PROCESS_RULE *rule = rules_list; @@ -1176,22 +1280,12 @@ void update_has_active_rules(void) if (rule->enabled) { has_active = TRUE; - if (rule_has_domain_filter(rule)) - { - has_domain = TRUE; - break; // both flags are now known - } + break; } rule = rule->next; } ReleaseSRWLockShared(&g_rules_lock); g_has_active_rules = has_active; - - // Edge trigger: when domain rules first become active, flush the OS DNS cache so - // subsequent connections re-resolve and populate our snoop cache. - if (has_domain && !g_has_domain_rules && running) - flush_dns_resolver_cache(); - g_has_domain_rules = has_domain; } diff --git a/Windows/src/pb_sni.c b/Windows/src/pb_sni.c new file mode 100644 index 0000000..cd4beea --- /dev/null +++ b/Windows/src/pb_sni.c @@ -0,0 +1,209 @@ +// TLS ClientHello SNI extraction. +// +// The DNS sources can only name a connection when the application resolved the name while +// ProxyBridge was watching: an app that answers from its own cache (Chromium and other +// long-lived Electron apps cache well beyond the system resolver cache) never shows up on +// the wire and never produces a DNS Client event that carries results. The ClientHello of a +// TLS connection always carries the name the application actually wants, and it arrives on +// the relay's app-side socket a few hundred microseconds after the SYN - so SNI is the one +// source that covers every TLS connection regardless of how the name was resolved. +// +// The socket is only ever PEEKED, so the bytes stay in the receive queue for the relay. + +#include "pb_internal.h" + +// Bound both socket peeking and the handshake reassembly buffer. +#define SNI_MAX_HELLO 4096 +#define SNI_PEEK_WINDOW_MS 50 +#define SNI_NO_PROGRESS_MS 10 +// shortcut: stop after 10 ms without more ClientHello bytes; increase if slow handshakes need better name recovery. + +static UINT32 sni_read_u16(const UINT8 *p) +{ + return ((UINT32)p[0] << 8) | (UINT32)p[1]; +} + +static UINT32 sni_read_u24(const UINT8 *p) +{ + return ((UINT32)p[0] << 16) | ((UINT32)p[1] << 8) | (UINT32)p[2]; +} + +// A hostname taken out of a TLS ClientHello must look like one, otherwise an arbitrary byte +// sequence that merely starts with 0x16 could be published as a name. +static BOOL sni_name_plausible(const char *name, size_t length) +{ + size_t label = 0; + BOOL dot = FALSE; + if (length == 0 || length > 253) return FALSE; + for (size_t i = 0; i < length; i++) + { + unsigned char c = (unsigned char)name[i]; + if (c == '.') + { + if (label == 0 || label > 63) return FALSE; + label = 0; + dot = TRUE; + continue; + } + if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + c == '-' || c == '_')) + return FALSE; + label++; + } + return dot && label > 0 && label <= 63; +} + +static SNI_RESULT sni_parse_handshake(const UINT8 *handshake, size_t length, + char *out, size_t out_size) +{ + if (length < 4) return SNI_NEED_MORE; + if (handshake[0] != 0x01) return SNI_NOT_TLS; + size_t hello_end = 4 + (size_t)sni_read_u24(handshake + 1); + if (hello_end <= 4 || hello_end > SNI_MAX_HELLO) return SNI_NOT_TLS; + if (hello_end > length) return SNI_NEED_MORE; + + size_t pos = 4 + 2 + 32; // legacy_version + random + if (pos + 1 > hello_end) return SNI_NOT_TLS; + size_t session_len = handshake[pos]; + pos += 1 + session_len; + if (pos + 2 > hello_end) return SNI_NOT_TLS; + size_t cipher_len = sni_read_u16(handshake + pos); + pos += 2 + cipher_len; + if (pos + 1 > hello_end) return SNI_NOT_TLS; + size_t compression_len = handshake[pos]; + pos += 1 + compression_len; + if (pos + 2 > hello_end) return SNI_NOT_TLS; // no extensions (or truncated) + + size_t extensions_end = pos + 2 + (size_t)sni_read_u16(handshake + pos); + pos += 2; + if (extensions_end > hello_end) return SNI_NOT_TLS; + + while (pos + 4 <= extensions_end) + { + UINT32 type = sni_read_u16(handshake + pos); + UINT32 size = sni_read_u16(handshake + pos + 2); + pos += 4; + if (pos + size > extensions_end) return SNI_NOT_TLS; + + if (type != 0x0000) { pos += size; continue; } + + if (size < 3) return SNI_NOT_TLS; + size_t list_end = pos + size; + size_t cursor = pos + 2; // skip server_name_list length + while (cursor + 3 <= list_end) + { + UINT32 name_type = handshake[cursor]; + UINT32 name_len = sni_read_u16(handshake + cursor + 1); + cursor += 3; + if (cursor + name_len > list_end) return SNI_NOT_TLS; + if (name_type == 0 && name_len > 0) + { + if (name_len >= out_size || name_len > 253) return SNI_NOT_TLS; + for (UINT32 i = 0; i < name_len; i++) + { + unsigned char c = handshake[cursor + i]; + out[i] = (char)(c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c); + } + out[name_len] = '\0'; + return sni_name_plausible(out, name_len) ? SNI_FOUND : SNI_NOT_TLS; + } + cursor += name_len; + } + return SNI_NOT_TLS; + } + return SNI_NOT_TLS; +} + +// Reassemble the first handshake message from one or more TLS handshake records. +// NEED_MORE means the socket peek may still contain a valid partial ClientHello. +SNI_RESULT sni_parse_client_hello(const UINT8 *data, size_t length, char *out, size_t out_size) +{ + if (!data || !out || out_size < 4) return SNI_NOT_TLS; + out[0] = '\0'; + if (length == 0) return SNI_NEED_MORE; + if (data[0] != 0x16) return SNI_NOT_TLS; + + UINT8 handshake[SNI_MAX_HELLO]; + size_t handshake_length = 0; + size_t offset = 0; + for (;;) + { + if (length - offset < 5) return SNI_NEED_MORE; + if (data[offset] != 0x16 || data[offset + 1] != 0x03) + return SNI_NOT_TLS; + + size_t record_length = sni_read_u16(data + offset + 3); + if (record_length == 0 || record_length > 18432) return SNI_NOT_TLS; + offset += 5; + + size_t available = length - offset; + size_t take = available < record_length ? available : record_length; + size_t remaining = sizeof(handshake) - handshake_length; + if (take > remaining) take = remaining; + if (take) + { + memcpy(handshake + handshake_length, data + offset, take); + handshake_length += take; + } + + if (handshake_length && handshake[0] != 0x01) return SNI_NOT_TLS; + if (handshake_length >= 4) + { + size_t hello_end = 4 + (size_t)sni_read_u24(handshake + 1); + if (hello_end <= 4 || hello_end > sizeof(handshake)) return SNI_NOT_TLS; + if (handshake_length >= hello_end) + return sni_parse_handshake(handshake, hello_end, out, out_size); + } + + if (available < record_length) return SNI_NEED_MORE; + offset += record_length; + if (offset == length) return SNI_NEED_MORE; + if (handshake_length == sizeof(handshake)) return SNI_NOT_TLS; + } +} + +// Read (without consuming) the client's ClientHello and return its SNI, if any. Returns +// quickly: it stops as soon as the buffered bytes are clearly not a TLS ClientHello, so a +// plain-HTTP or server-speaks-first protocol only pays a few microseconds. +BOOL sni_peek_from_socket(SOCKET sock, char *out, size_t out_size) +{ + UINT8 buffer[SNI_MAX_HELLO]; + ULONGLONG deadline = GetTickCount64() + SNI_PEEK_WINDOW_MS; + ULONGLONG last_progress = 0; + int previous_got = -1; + + for (;;) + { + fd_set readable; + FD_ZERO(&readable); + FD_SET(sock, &readable); + + ULONGLONG now = GetTickCount64(); + if (now >= deadline) return FALSE; + DWORD remaining = (DWORD)(deadline - now); + struct timeval tv; + tv.tv_sec = (long)(remaining / 1000); + tv.tv_usec = (long)((remaining % 1000) * 1000); + if (select(0, &readable, NULL, NULL, &tv) <= 0) return FALSE; + + int got = recv(sock, (char *)buffer, (int)sizeof(buffer), MSG_PEEK); + if (got <= 0) return FALSE; + + now = GetTickCount64(); + if (got != previous_got) + { + previous_got = got; + last_progress = now; + } + else if (now - last_progress >= SNI_NO_PROGRESS_MS) + { + return FALSE; + } + + SNI_RESULT result = sni_parse_client_hello(buffer, (size_t)got, out, out_size); + if (result == SNI_FOUND) return TRUE; + if (result == SNI_NOT_TLS) return FALSE; + if ((size_t)got == sizeof(buffer)) return FALSE; + Sleep(1); // truncated ClientHello: wait for the rest of the bytes + } +} diff --git a/Windows/src/service/pb_proto.h b/Windows/src/service/pb_proto.h index 5b6a809..8bd8a99 100644 --- a/Windows/src/service/pb_proto.h +++ b/Windows/src/service/pb_proto.h @@ -19,13 +19,13 @@ #define PB_SERVICE_NAME L"ProxyBridgeSvc" #define PB_PIPE_CTL L"\\\\.\\pipe\\ProxyBridgeCtl" #define PB_PIPE_EVT L"\\\\.\\pipe\\ProxyBridgeEvt" -#define PB_PROTO_VERSION 1 +#define PB_PROTO_VERSION 2 #define PB_MSG_MAX (64u * 1024u) #define PB_STR_MAX (32u * 1024u) #define PB_NULL_STR 0xFFFFFFFFu enum { - PBOP_HELLO = 1, // () -> PB_PROTO_VERSION + PBOP_HELLO = 1, // optional client version -> negotiated protocol version PBOP_ADD_PROXY, // type, ip, port, user, pass, sendDomain -> id PBOP_EDIT_PROXY, // id, type, ip, port, user, pass, sendDomain -> bool PBOP_DELETE_PROXY, // id -> bool @@ -46,7 +46,7 @@ enum { enum { PBEVT_LOG = 1, // str message - PBEVT_CONN, // str process, u32 pid, str ip, u32 port, str info + PBEVT_CONN, // v1 fields: str process, u32 pid, str ip, u32 port, str info; v2 appends str hostname PBEVT_TEST_LINE, // str line PBEVT_TEST_DONE // u32 result (int) }; diff --git a/Windows/src/service/pb_service.c b/Windows/src/service/pb_service.c index 3f55c93..02115ca 100644 --- a/Windows/src/service/pb_service.c +++ b/Windows/src/service/pb_service.c @@ -40,6 +40,7 @@ static HANDLE g_worker = NULL; // thread running the accept/s static HANDLE g_ctl = INVALID_HANDLE_VALUE; static HANDLE g_evt = INVALID_HANDLE_VALUE; +static DWORD g_evt_proto_version = 1; static PBApi g_api; static CRITICAL_SECTION g_evt_lock; // serialises event writes from engine threads static volatile LONG g_evt_live = 0; // 1 while an event pipe client is attached @@ -82,6 +83,16 @@ static void OnConn(const char *proc, DWORD pid, const char *ip, unsigned short p if (w.ok) SendEvent(b, w.len); } +static void OnConnEx(const char *proc, DWORD pid, const char *ip, const char *hostname, + unsigned short port, const char *info) +{ + BYTE b[8192]; PbW w; PbW_Init(&w, b, sizeof(b)); + PbW_U32(&w, PBEVT_CONN); PbW_Str(&w, proc); PbW_U32(&w, pid); + PbW_Str(&w, ip); PbW_U32(&w, port); PbW_Str(&w, info); + if (g_evt_proto_version >= 2) PbW_Str(&w, hostname); + if (w.ok) SendEvent(b, w.len); +} + // proxy test runs on its own thread so the request that started it returns immediately static volatile LONG g_test_busy = 0; typedef struct { UINT32 id; char host[512]; unsigned short port; } TestJob; @@ -120,7 +131,13 @@ static BOOL Dispatch(const BYTE *req, DWORD reqLen, PbW *resp) switch (op) { - case PBOP_HELLO: ret = PB_PROTO_VERSION; break; + case PBOP_HELLO: { + if (reqLen != 4 && reqLen != 8) return FALSE; + UINT32 client_version = r.pos < reqLen ? PbR_U32(&r) : 1; + if (!r.ok || client_version == 0) return FALSE; + g_evt_proto_version = client_version < PB_PROTO_VERSION ? client_version : PB_PROTO_VERSION; + ret = g_evt_proto_version; + break; } case PBOP_ADD_PROXY: { UINT32 type = PbR_U32(&r); PbR_Str(&r, ip, sizeof(ip), NULL); UINT32 port = PbR_U32(&r); @@ -253,7 +270,10 @@ static void ServeClient(void) // fresh engine for every client if (!PB_LoadDirect(&g_api)) return; g_api.SetLogCallback(OnLog); - g_api.SetConnectionCallback(OnConn); + if (g_api.SetConnectionCallbackEx) + g_api.SetConnectionCallbackEx(OnConnEx); + else + g_api.SetConnectionCallback(OnConn); InterlockedExchange(&g_evt_live, 1); for (;;) @@ -270,7 +290,10 @@ static void ServeClient(void) InterlockedExchange(&g_evt_live, 0); if (g_api.Stop) g_api.Stop(); if (g_api.SetLogCallback) g_api.SetLogCallback(NULL); - if (g_api.SetConnectionCallback) g_api.SetConnectionCallback(NULL); + if (g_api.SetConnectionCallbackEx) + g_api.SetConnectionCallbackEx(NULL); + else if (g_api.SetConnectionCallback) + g_api.SetConnectionCallback(NULL); PB_UnloadDirect(&g_api); } diff --git a/Windows/tools/etw_manifest.c b/Windows/tools/etw_manifest.c new file mode 100644 index 0000000..da7545f --- /dev/null +++ b/Windows/tools/etw_manifest.c @@ -0,0 +1,110 @@ +// Dumps the registered manifest of Microsoft-Windows-DNS-Client: every event ID with +// its exact property names and types. Reading a manifest needs no privileges, unlike +// starting an ETW session, so this can answer "what fields does 3008 really have" +// without an elevated shell. +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + +#pragma comment(lib, "tdh.lib") + +static const GUID g_provider = + { 0x1c95126e, 0x7eea, 0x49a9, { 0xa3, 0xfe, 0xa3, 0x78, 0xb0, 0x3d, 0xdb, 0x4d } }; + +static const char *intype_name(USHORT t) +{ + switch (t) + { + case TDH_INTYPE_NULL: return "NULL"; + case TDH_INTYPE_UNICODESTRING: return "UnicodeString"; + case TDH_INTYPE_ANSISTRING: return "AnsiString"; + case TDH_INTYPE_INT8: return "Int8"; + case TDH_INTYPE_UINT8: return "UInt8"; + case TDH_INTYPE_INT16: return "Int16"; + case TDH_INTYPE_UINT16: return "UInt16"; + case TDH_INTYPE_INT32: return "Int32"; + case TDH_INTYPE_UINT32: return "UInt32"; + case TDH_INTYPE_INT64: return "Int64"; + case TDH_INTYPE_UINT64: return "UInt64"; + case TDH_INTYPE_FLOAT: return "Float"; + case TDH_INTYPE_DOUBLE: return "Double"; + case TDH_INTYPE_BOOLEAN: return "Boolean"; + case TDH_INTYPE_BINARY: return "Binary"; + case TDH_INTYPE_GUID: return "GUID"; + case TDH_INTYPE_POINTER: return "Pointer"; + case TDH_INTYPE_FILETIME: return "FileTime"; + case TDH_INTYPE_SYSTEMTIME: return "SystemTime"; + case TDH_INTYPE_SID: return "SID"; + case TDH_INTYPE_HEXINT32: return "HexInt32"; + case TDH_INTYPE_HEXINT64: return "HexInt64"; + case TDH_INTYPE_COUNTEDSTRING: return "CountedString"; + case TDH_INTYPE_COUNTEDANSISTRING: return "CountedAnsiString"; + case TDH_INTYPE_REVERSEDCOUNTEDSTRING: return "ReversedCountedString"; + case TDH_INTYPE_NONNULLTERMINATEDSTRING: return "NonNullTerminatedString"; + case TDH_INTYPE_UNICODECHAR: return "UnicodeChar"; + case TDH_INTYPE_ANSICHAR: return "AnsiChar"; + case TDH_INTYPE_SIZET: return "SizeT"; + case TDH_INTYPE_HEXDUMP: return "HexDump"; + case TDH_INTYPE_WBEMSID: return "WbemSID"; + default: return "other"; + } +} + +int wmain(void) +{ + ULONG size = 0; + ULONG status = TdhEnumerateManifestProviderEvents(&g_provider, NULL, &size); + if (status != ERROR_INSUFFICIENT_BUFFER) + { + printf("TdhEnumerateManifestProviderEvents sizing failed (%lu)\n", status); + return 1; + } + PROVIDER_EVENT_INFO *events = (PROVIDER_EVENT_INFO *)calloc(1, size); + if (!events) return 1; + status = TdhEnumerateManifestProviderEvents(&g_provider, events, &size); + if (status != ERROR_SUCCESS) + { + printf("TdhEnumerateManifestProviderEvents failed (%lu)\n", status); + return 1; + } + + printf("Microsoft-Windows-DNS-Client: %lu event definitions\n", events->NumberOfEvents); + for (ULONG i = 0; i < events->NumberOfEvents; i++) + { + EVENT_DESCRIPTOR desc = events->EventDescriptorsArray[i]; + ULONG info_size = 0; + status = TdhGetManifestEventInformation(&g_provider, &desc, NULL, &info_size); + if (status != ERROR_INSUFFICIENT_BUFFER) + { + printf("id=%u v=%u keyword=0x%llx channel=%u : no template (%lu)\n", + desc.Id, desc.Version, (unsigned long long)desc.Keyword, + desc.Channel, status); + continue; + } + TRACE_EVENT_INFO *info = (TRACE_EVENT_INFO *)calloc(1, info_size); + if (!info) continue; + status = TdhGetManifestEventInformation(&g_provider, &desc, info, &info_size); + if (status != ERROR_SUCCESS) + { + printf("id=%u v=%u : template failed (%lu)\n", desc.Id, desc.Version, status); + free(info); + continue; + } + printf("id=%u v=%u keyword=0x%llx channel=%u level=%u props=%u top=%u\n", + desc.Id, desc.Version, (unsigned long long)desc.Keyword, desc.Channel, + desc.Level, info->PropertyCount, info->TopLevelPropertyCount); + for (ULONG p = 0; p < info->PropertyCount; p++) + { + EVENT_PROPERTY_INFO *prop = &info->EventPropertyInfoArray[p]; + const WCHAR *name = (const WCHAR *)((const BYTE *)info + prop->NameOffset); + printf(" [%u] %-26ls flags=0x%x in=%s out=%u\n", p, name, prop->Flags, + intype_name(prop->nonStructType.InType), prop->nonStructType.OutType); + } + free(info); + } + free(events); + return 0; +} diff --git a/Windows/tools/etw_probe.c b/Windows/tools/etw_probe.c new file mode 100644 index 0000000..2da532b --- /dev/null +++ b/Windows/tools/etw_probe.c @@ -0,0 +1,277 @@ +// etw_probe.c - one-shot ground-truth probe for the DNS-Client ETW provider. +// +// Purpose: the hostname-recovery implementation parses event 3008 (query complete) +// using field names and a QueryResults layout that were never confirmed against a +// live system. This tool starts the same kind of real-time session the engine uses, +// triggers its own resolutions, and dumps every property of every DNS-Client event +// so the real names, types and string formats can be read off directly. +// +// Build: cl.exe /nologo /O2 /D_UNICODE /DUNICODE etw_probe.c /link tdh.lib ws2_32.lib +// Run: etw_probe.exe [seconds] (default 12) + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "tdh.lib") +#pragma comment(lib, "ws2_32.lib") + +static const GUID DNS_CLIENT_PROVIDER = + { 0x1c95126e, 0x7eea, 0x49a9, { 0xa3, 0xfe, 0xa3, 0x78, 0xb0, 0x3d, 0xdb, 0x4d } }; + +static LARGE_INTEGER g_freq; +static volatile LONG g_stop; +static TRACEHANDLE g_consumer; + +static DWORD WINAPI consumer_thread(LPVOID unused) +{ + (void)unused; + TRACEHANDLE h = g_consumer; + ULONG r = ProcessTrace(&h, 1, NULL, NULL); + return r; +} + +static double qpc_ms(ULONGLONG ticks) +{ + return (double)ticks * 1000.0 / (double)g_freq.QuadPart; +} + +// Dump one property value in a readable form, whatever its TDH type. +static void dump_property(EVENT_RECORD *rec, PTRACE_EVENT_INFO info, ULONG index) +{ + const EVENT_PROPERTY_INFO *p = &info->EventPropertyInfoArray[index]; + const WCHAR *name = (const WCHAR *)((const BYTE *)info + p->NameOffset); + PROPERTY_DATA_DESCRIPTOR desc; + desc.PropertyName = (ULONGLONG)(ULONG_PTR)name; + desc.ArrayIndex = ULONG_MAX; + desc.Reserved = 0; + + ULONG size = 0; + if (TdhGetPropertySize(rec, 0, NULL, 1, &desc, &size) != ERROR_SUCCESS || size == 0 || size > 65536) + { + wprintf(L" %-22s \n", name); + return; + } + BYTE *buf = (BYTE *)calloc(1, size + 16); + if (!buf) return; + if (TdhGetProperty(rec, 0, NULL, 1, &desc, size, buf) != ERROR_SUCCESS) + { + wprintf(L" %-22s \n", name); + free(buf); + return; + } + + USHORT inType = (p->Flags & PropertyParamLength) ? TDH_INTYPE_UNICODESTRING + : p->nonStructType.InType; + switch (inType) + { + case TDH_INTYPE_UNICODESTRING: + { + buf[size] = 0; buf[size + 1] = 0; + wprintf(L" %-22s (wstr) \"%ls\"\n", name, (const WCHAR *)buf); + // Always show the exact bytes too: the QueryResults separator/prefix + // convention is the whole point of this probe. + wprintf(L" %-22s bytes:", name); + ULONG n = size / 2 > 64 ? 64 : size / 2; + for (ULONG i = 0; i < n; i++) wprintf(L" %04X", ((const WORD *)buf)[i]); + wprintf(L"\n"); + break; + } + case TDH_INTYPE_ANSISTRING: + buf[size] = 0; + wprintf(L" %-22s (astr) \"%hs\"\n", name, (const char *)buf); + break; + case TDH_INTYPE_UINT32: + wprintf(L" %-22s (u32) %lu\n", name, *(ULONG *)buf); + break; + case TDH_INTYPE_UINT64: + wprintf(L" %-22s (u64) %llu\n", name, *(ULONGLONG *)buf); + break; + case TDH_INTYPE_UINT16: + wprintf(L" %-22s (u16) %u\n", name, *(WORD *)buf); + break; + case TDH_INTYPE_UINT8: + wprintf(L" %-22s (u8) %u\n", name, *buf); + break; + default: + wprintf(L" %-22s (intype=%u) sz=%lu:", name, inType, size); + ULONG m = size > 32 ? 32 : size; + for (ULONG i = 0; i < m; i++) wprintf(L" %02X", buf[i]); + wprintf(L"\n"); + break; + } + free(buf); +} + +static void WINAPI on_event(EVENT_RECORD *rec) +{ + if (!IsEqualGUID(&rec->EventHeader.ProviderId, &DNS_CLIENT_PROVIDER)) + return; + // 3006 = query start, 3008 = query complete, 1018/1019 = cache, 8043/8044 = DoH/DoT. + // Print everything so we can see which IDs actually fire on this machine. + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + ULONGLONG ts = (ULONGLONG)rec->EventHeader.TimeStamp.QuadPart; + double lag = now.QuadPart > (LONGLONG)ts ? qpc_ms(now.QuadPart - (LONGLONG)ts) : -1.0; + + printf("EVENT id=%u pid=%lu tid=%lu ts=%llu lag=%.3f ms\n", + rec->EventHeader.EventDescriptor.Id, + (ULONG)rec->EventHeader.ProcessId, (ULONG)rec->EventHeader.ThreadId, + ts, lag); + + ULONG info_size = 0; + if (TdhGetEventInformation(rec, 0, NULL, NULL, &info_size) != ERROR_INSUFFICIENT_BUFFER && !info_size) + { + printf(" \n"); + return; + } + TRACE_EVENT_INFO *info = (TRACE_EVENT_INFO *)malloc(info_size); + if (!info) return; + if (TdhGetEventInformation(rec, 0, NULL, info, &info_size) != ERROR_SUCCESS) + { + printf(" \n"); + free(info); + return; + } + for (ULONG i = 0; i < info->TopLevelPropertyCount; i++) + dump_property(rec, info, i); + free(info); + fflush(stdout); +} + +static DWORD WINAPI resolver_thread(LPVOID unused) +{ + (void)unused; + const char *names[] = { "www.google.com", "api.openai.com", "www.github.com", + "1.1.1.1", "example.com" }; + // Deliberate: resolve, then immediately open a TCP connection, which is exactly + // the "resolve then connect" pattern whose ordering the engine has to guess at. + for (int round = 0; round < 3 && !g_stop; round++) + { + for (int i = 0; i < 5 && !g_stop; i++) + { + struct addrinfo *res = NULL; + LARGE_INTEGER t0, t1; + QueryPerformanceCounter(&t0); + if (getaddrinfo(names[i], "443", NULL, &res) == 0) + { + QueryPerformanceCounter(&t1); + printf("RESOLVE %-18s resolve->return = %.3f ms\n", + names[i], qpc_ms(t1.QuadPart - t0.QuadPart)); + if (res) + { + SOCKET s = socket(res->ai_family, SOCK_STREAM, IPPROTO_TCP); + if (s != INVALID_SOCKET) + { + QueryPerformanceCounter(&t1); + printf("RESOLVE %-18s resolve->connect-call = %.3f ms\n", + names[i], qpc_ms(t1.QuadPart - t0.QuadPart)); + closesocket(s); + } + freeaddrinfo(res); + } + } + else + printf("RESOLVE %-18s FAILED\n", names[i]); + fflush(stdout); + Sleep(400); + } + Sleep(1000); + } + return 0; +} + +int wmain(int argc, WCHAR **argv) +{ + int seconds = (argc > 1) ? _wtoi(argv[1]) : 12; + QueryPerformanceFrequency(&g_freq); + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); + + printf("QPC frequency: %lld Hz (1 tick = %.3f us)\n", + g_freq.QuadPart, 1e6 / (double)g_freq.QuadPart); + printf("My PID: %lu\n", GetCurrentProcessId()); + printf("Collecting DNS-Client events for %d seconds...\n\n", seconds); + + WCHAR name[64]; + _snwprintf_s(name, _countof(name), _TRUNCATE, L"PB-ETW-Probe-%lu", GetCurrentProcessId()); + size_t props_size = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(name); + EVENT_TRACE_PROPERTIES *props = (EVENT_TRACE_PROPERTIES *)calloc(1, props_size); + props->Wnode.BufferSize = (ULONG)props_size; + props->Wnode.Guid.Data1 = 0x2f0a7c19; + props->Wnode.Guid.Data2 = 0x4b31; + props->Wnode.Guid.Data3 = 0x40a2; + DWORD pid = GetCurrentProcessId(), tick = GetTickCount(); + memcpy(&props->Wnode.Guid.Data4[0], &pid, 4); + memcpy(&props->Wnode.Guid.Data4[4], &tick, 4); + props->Wnode.Flags = WNODE_FLAG_TRACED_GUID; + props->Wnode.ClientContext = 1; // QPC, same domain as WinDivert timestamps + props->BufferSize = 64; + props->MinimumBuffers = 4; + props->MaximumBuffers = 16; + props->LogFileMode = EVENT_TRACE_REAL_TIME_MODE; + props->FlushTimer = 1; + props->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES); + memcpy((BYTE *)props + props->LoggerNameOffset, name, sizeof(name)); + + TRACEHANDLE session = 0; + ULONG st = StartTraceW(&session, name, props); + if (st != ERROR_SUCCESS) + { + printf("StartTrace failed: %lu", st); + if (st == ERROR_ACCESS_DENIED) + printf(" (ERROR_ACCESS_DENIED - real-time ETW sessions need admin " + "or Performance Log Users membership)"); + printf("\n"); + return 2; + } + printf("Session started (ClientContext=1 QPC).\n\n"); + + st = EnableTraceEx2(session, &DNS_CLIENT_PROVIDER, EVENT_CONTROL_CODE_ENABLE_PROVIDER, + TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL); + if (st != ERROR_SUCCESS) + { + printf("EnableTraceEx2 failed: %lu\n", st); + ControlTraceW(session, name, props, EVENT_TRACE_CONTROL_STOP); + return 3; + } + + EVENT_TRACE_LOGFILEW logfile; + ZeroMemory(&logfile, sizeof(logfile)); + logfile.LoggerName = name; + logfile.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | + PROCESS_TRACE_MODE_EVENT_RECORD | + PROCESS_TRACE_MODE_RAW_TIMESTAMP; + logfile.EventRecordCallback = on_event; + TRACEHANDLE consumer = OpenTraceW(&logfile); + if (consumer == INVALID_PROCESSTRACE_HANDLE) + { + printf("OpenTrace failed: %lu\n", GetLastError()); + ControlTraceW(session, name, props, EVENT_TRACE_CONTROL_STOP); + return 4; + } + + // ProcessTrace blocks until the trace is closed, so run it on its own thread. + g_consumer = consumer; + HANDLE thread = CreateThread(NULL, 0, consumer_thread, NULL, 0, NULL); + + HANDLE resolver = CreateThread(NULL, 0, resolver_thread, NULL, 0, NULL); + WaitForSingleObject(resolver, INFINITE); + Sleep(1200); + InterlockedExchange(&g_stop, 1); + + printf("\nStopping...\n"); + ULONG status = ControlTraceW(session, name, props, EVENT_TRACE_CONTROL_QUERY); + if (status == ERROR_SUCCESS) + printf("EventsLost=%lu RealTimeBuffersLost=%lu BuffersWritten=%lu\n", + props->EventsLost, props->RealTimeBuffersLost, props->BuffersWritten); + CloseTrace(consumer); + ControlTraceW(session, name, props, EVENT_TRACE_CONTROL_STOP); + printf("Done.\n"); + return 0; +}