diff --git a/simplewall.vcxproj b/simplewall.vcxproj
index bb8abb6d..544e514b 100644
--- a/simplewall.vcxproj
+++ b/simplewall.vcxproj
@@ -393,6 +393,7 @@
+
@@ -422,6 +423,7 @@
+
diff --git a/simplewall.vcxproj.filters b/simplewall.vcxproj.filters
index 332a5192..38893209 100644
--- a/simplewall.vcxproj.filters
+++ b/simplewall.vcxproj.filters
@@ -75,6 +75,9 @@
Source Files
+
+ Source Files
+
@@ -158,6 +161,9 @@
Header Files
+
+ Header Files
+
diff --git a/src/controls.c b/src/controls.c
index cbcc3486..7f683b19 100644
--- a/src/controls.c
+++ b/src/controls.c
@@ -15,6 +15,7 @@ VOID _app_getapptooltipstring (
PR_STRING path = NULL;
PITEM_APP ptr_app;
PR_STRING string, value;
+ LONG traffic_error;
ptr_app = _app_getappitem (app_hash);
@@ -53,6 +54,19 @@ VOID _app_getapptooltipstring (
_r_obj_appendstringbuilder (buffer, SZ_CRLF);
}
+ // UDP accounting status
+ if (ptr_network && ptr_network->protocol == IPPROTO_UDP && (ptr_network->is_stats_initialized || _InterlockedCompareExchange (&ptr_network->traffic_error, 0, 0)))
+ {
+ traffic_error = _InterlockedCompareExchange (&ptr_network->traffic_error, 0, 0);
+
+ _r_obj_appendstringbuilder (buffer, _r_locale_getstring (traffic_error == ERROR_NOT_READY ? IDS_UDP_PENDING : (traffic_error ? IDS_UDP_UNAVAILABLE : IDS_UDP_ACCOUNTING)));
+
+ if (traffic_error && traffic_error != ERROR_NOT_READY)
+ _r_obj_appendstringbuilderformat (buffer, L" (%lu)", (ULONG)traffic_error);
+
+ _r_obj_appendstringbuilder (buffer, SZ_CRLF);
+ }
+
// file information
_r_obj_initializestringbuilder (&sb, 0);
@@ -552,7 +566,7 @@ LONG _app_getstateicon (
default:
{
- return IDI_INACTIVE;
+ return IDI_INACTIVE; // never match!
}
}
}
@@ -658,25 +672,14 @@ VOID _app_settrayicon (
_In_ ENUM_INSTALL_TYPE install_type
)
{
- HICON current_handle, new_handle;
- LONG icon_id, icon_size;
+ HICON hicon;
+ LONG icon_size;
icon_size = _r_dc_getsystemmetrics (SM_CXSMICON, _r_dc_gettaskbardpi ());
- icon_id = _app_getstateicon (install_type);
- current_handle = (HICON)_InterlockedCompareExchangePointer ((volatile PVOID_PTR)&config.htray_icon, NULL, config.htray_icon);
+ hicon = _r_sys_loadsharedicon (_r_sys_getimagebase (), MAKEINTRESOURCE (_app_getstateicon (install_type)), icon_size);
- if (current_handle)
- DestroyIcon (current_handle);
-
- _r_sys_loadicon (&new_handle, _r_sys_getimagebase (), MAKEINTRESOURCE (icon_id), icon_size);
-
- _r_tray_setinfo (hwnd, &GUID_TrayIcon, new_handle, _r_app_getname ());
-
- current_handle = (HICON)_InterlockedCompareExchangePointer ((volatile PVOID_PTR)&config.htray_icon, new_handle, NULL);
-
- if (current_handle)
- DestroyIcon (current_handle);
+ _r_tray_setinfo (hwnd, &GUID_TrayIcon, hicon, _r_app_getname ());
}
VOID _app_imagelist_init (
diff --git a/src/global.h b/src/global.h
index fe776b2e..0a0ba3b3 100644
--- a/src/global.h
+++ b/src/global.h
@@ -22,6 +22,8 @@
#include
#include
#include
+#include
+#include
#include "app.h"
#include "rapp.h"
@@ -114,5 +116,6 @@ DECLSPEC_SELECTANY const LONG64 timer_array[] =
#include "search.h"
#include "security.h"
#include "timer.h"
+#include "udpstats.h"
#include "uwp.h"
#include "wfp.h"
diff --git a/src/main.c b/src/main.c
index ca9938f4..3181166e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -266,6 +266,24 @@ VOID _app_config_apply (
break;
}
+ case IDM_CONNECTIONS_ENABLE:
+ {
+ new_val = !_r_config_getboolean (L"IsNetworkMonitorEnabled", TRUE, NULL);
+ break;
+ }
+
+ case IDM_CONNECTIONS_SHOWAITCONNECTIONS:
+ {
+ new_val = !_r_config_getboolean (L"IsNetworkShowWaitConnections", TRUE, NULL);
+ break;
+ }
+
+ case IDM_CONNECTIONS_MEASUREUDPTRAFFIC:
+ {
+ new_val = !_r_config_getboolean (L"IsUdpTrafficEnabled", FALSE, NULL);
+ break;
+ }
+
default:
{
return;
@@ -411,6 +429,33 @@ VOID _app_config_apply (
break;
}
+ case IDM_CONNECTIONS_ENABLE:
+ {
+ _r_config_setboolean (L"IsNetworkMonitorEnabled", new_val, NULL);
+
+ _r_menu_checkitem (hmenu, IDM_CONNECTIONS_ENABLE, 0, MF_BYCOMMAND, new_val);
+
+ break;
+ }
+
+ case IDM_CONNECTIONS_SHOWAITCONNECTIONS:
+ {
+ _r_config_setboolean (L"IsNetworkShowWaitConnections", new_val, NULL);
+
+ _r_menu_checkitem (hmenu, IDM_CONNECTIONS_SHOWAITCONNECTIONS, 0, MF_BYCOMMAND, new_val);
+
+ break;
+ }
+
+ case IDM_CONNECTIONS_MEASUREUDPTRAFFIC:
+ {
+ _r_config_setboolean (L"IsUdpTrafficEnabled", new_val, NULL);
+
+ _r_menu_checkitem (hmenu, IDM_CONNECTIONS_MEASUREUDPTRAFFIC, 0, MF_BYCOMMAND, new_val);
+
+ break;
+ }
+
case IDC_USESTEALTHMODE_CHK:
{
_r_config_setboolean (L"UseStealthMode", new_val, NULL);
@@ -527,6 +572,7 @@ VOID _app_config_apply (
case IDM_PROFILETYPE_PLAIN:
case IDM_PROFILETYPE_COMPRESSED:
case IDM_PROFILETYPE_ENCRYPTED:
+ case IDM_CONNECTIONS_MEASUREUDPTRAFFIC:
case IDC_USENETWORKRESOLUTION_CHK:
case IDM_USENETWORKRESOLUTION_CHK:
case IDC_USECERTIFICATES_CHK:
@@ -2109,6 +2155,7 @@ INT_PTR CALLBACK DlgProc (
case WM_DESTROY:
{
+ _app_network_stop ();
_app_loginit (FALSE);
if (_r_queuedlock_islocked (&lock_apply))
@@ -3066,6 +3113,9 @@ INT_PTR CALLBACK DlgProc (
case IDM_PROFILETYPE_PLAIN:
case IDM_PROFILETYPE_COMPRESSED:
case IDM_PROFILETYPE_ENCRYPTED:
+ case IDM_CONNECTIONS_ENABLE:
+ case IDM_CONNECTIONS_SHOWAITCONNECTIONS:
+ case IDM_CONNECTIONS_MEASUREUDPTRAFFIC:
case IDM_USENETWORKRESOLUTION_CHK:
case IDM_USECERTIFICATES_CHK:
case IDM_KEEPUNUSED_CHK:
diff --git a/src/main.h b/src/main.h
index 85b0e3ce..c2d5a93d 100644
--- a/src/main.h
+++ b/src/main.h
@@ -209,7 +209,6 @@ typedef struct _STATIC_DATA
PSID builtin_admins_sid;
volatile HWND hnotification;
- volatile HICON htray_icon;
volatile HANDLE hnetevent;
volatile HANDLE hlogfile;
volatile LONG log_id;
@@ -269,7 +268,6 @@ typedef struct _ITEM_LOG
volatile PR_STRING local_host_str;
LONG64 timestamp;
-
UINT64 filter_id;
ULONG app_hash;
@@ -429,12 +427,11 @@ typedef struct _ITEM_NETWORK
UINT16 remote_port;
UINT16 local_port;
-
UINT8 protocol;
- BOOLEAN is_connection;
- BOOLEAN is_stats_enabled;
BOOLEAN is_stats_initialized;
+ BOOLEAN is_stats_enabled;
+ BOOLEAN is_connection;
volatile LONG64 download_speed;
volatile LONG64 upload_speed;
@@ -444,6 +441,9 @@ typedef struct _ITEM_NETWORK
ULONG64 last_bytes_in;
ULONG64 last_bytes_out;
ULONG64 last_stats_tick;
+ ULONG64 udp_created;
+
+ volatile LONG traffic_error;
} ITEM_NETWORK, *PITEM_NETWORK;
typedef struct _ITEM_STATUS
@@ -550,3 +550,70 @@ typedef struct _ITEM_TAB_CONTEXT
INT listview_id;
INT locale_id;
} ITEM_TAB_CONTEXT, *PITEM_TAB_CONTEXT;
+
+typedef struct _UDP_ENDPOINT
+{
+ BYTE address[0x10];
+ ULONG64 created; // UDP owner table bind timestamp, in FILETIME units
+ ULONG scope_id;
+ ULONG pid;
+ ADDRESS_FAMILY af;
+ USHORT port; // host byte order
+} UDP_ENDPOINT, *PUDP_ENDPOINT;
+
+typedef struct UDP_ENTRY
+{
+ struct UDP_ENTRY *next;
+ UDP_ENDPOINT endpoint;
+ ULONG64 since;
+ ULONG64 received;
+ ULONG64 sent;
+ ULONG error_code;
+ ULONG epoch;
+ BOOLEAN is_observed;
+} UDP_ENTRY, *PUDP_ENTRY;
+
+typedef struct UDP_AFD_SOCKET
+{
+ struct UDP_AFD_SOCKET *next;
+ UDP_ENDPOINT endpoint;
+ ULONG64 handle;
+ BOOLEAN is_bound;
+ BOOLEAN is_unsupported;
+} UDP_AFD_SOCKET, *PUDP_AFD_SOCKET;
+
+typedef struct _UDP_TRACE_PROPERTIES
+{
+ EVENT_TRACE_PROPERTIES properties;
+ WCHAR name[0x40];
+} UDP_TRACE_PROPERTIES, *PUDP_TRACE_PROPERTIES;
+
+#define UDP_SESSION_SEMAPHORE L"Global\\simplewall-UDP-owner"
+#define UDP_SESSION_NAME L"simplewall-UDP"
+#define UDP_MAX_ENDPOINTS 0x2000
+#define UDP_BUCKETS 0x100
+
+typedef struct _SW_UDP_STATS
+{
+ RTL_SRWLOCK control_lock;
+ RTL_SRWLOCK lock;
+ UDP_ENTRY *buckets[UDP_BUCKETS];
+ UDP_AFD_SOCKET *sockets[UDP_BUCKETS]; // owned by the ETW consumer thread
+ HANDLE hthread;
+ HANDLE hguard;
+ TRACEHANDLE hsession;
+ TRACEHANDLE hconsumer;
+ ULONG socket_count;
+ ULONG count;
+ ULONG epoch;
+ volatile LONG error_code;
+ volatile LONG stopping;
+ BOOLEAN is_owns_guard;
+} SW_UDP_STATS, *PSW_UDP_STATS;
+
+typedef struct _UDP_SNAPSHOT
+{
+ ULONG64 received;
+ ULONG64 sent;
+ ULONG error_code; // nonzero means totals must not be presented as complete
+} UDP_SNAPSHOT, *PUDP_SNAPSHOT;
diff --git a/src/messages.c b/src/messages.c
index 74236044..6f030a32 100644
--- a/src/messages.c
+++ b/src/messages.c
@@ -78,6 +78,10 @@ VOID _app_message_initialize (
_r_menu_checkitem (hmenu, IDM_PROFILETYPE_PLAIN, IDM_PROFILETYPE_ENCRYPTED, MF_BYCOMMAND, IDM_PROFILETYPE_PLAIN + _r_calc_clamp (_r_config_getlong (L"ProfileType", 0, NULL), 0, 2));
+ _r_menu_checkitem (hmenu, IDM_CONNECTIONS_ENABLE, 0, MF_BYCOMMAND, _r_config_getboolean (L"IsNetworkMonitorEnabled", TRUE, NULL));
+ _r_menu_checkitem (hmenu, IDM_CONNECTIONS_SHOWAITCONNECTIONS, 0, MF_BYCOMMAND, _r_config_getboolean (L"IsNetworkShowWaitConnections", TRUE, NULL));
+ _r_menu_checkitem (hmenu, IDM_CONNECTIONS_MEASUREUDPTRAFFIC, 0, MF_BYCOMMAND, _r_config_getboolean (L"IsUdpTrafficEnabled", FALSE, NULL));
+
is_enabled = _r_config_getboolean (L"IsHashesEnabled", FALSE, NULL);
_r_menu_checkitem (hmenu, IDM_USECERTIFICATES_CHK, 0, MF_BYCOMMAND, _r_config_getboolean (L"IsCertificatesEnabled", TRUE, NULL));
@@ -187,6 +191,10 @@ VOID _app_message_localize (
_r_menu_setitemtext (hmenu, IDM_PROFILETYPE_COMPRESSED, FALSE, _r_locale_getstring (IDS_PROFILE_TYPE_COMPRESSED));
_r_menu_setitemtext (hmenu, IDM_PROFILETYPE_ENCRYPTED, FALSE, _r_locale_getstring (IDS_PROFILE_TYPE_ENCRYPTED));
+ _r_menu_setitemtext (hmenu, IDM_CONNECTIONS_ENABLE, FALSE, _r_locale_getstring (IDS_CONNECTIONS_ENABLE));
+ _r_menu_setitemtext (hmenu, IDM_CONNECTIONS_SHOWAITCONNECTIONS, FALSE, _r_locale_getstring (IDS_CONNECTIONS_SHOWAITCONNECTIONS));
+ _r_menu_setitemtext (hmenu, IDM_CONNECTIONS_MEASUREUDPTRAFFIC, FALSE, _r_locale_getstring (IDS_CONNECTIONS_MEASUREUDPTRAFFIC));
+
_r_menu_setitemtext (hmenu, IDM_USENETWORKRESOLUTION_CHK, FALSE, _r_locale_getstring (IDS_USENETWORKRESOLUTION_CHK));
_r_menu_setitemtext (hmenu, IDM_USECERTIFICATES_CHK, FALSE, _r_locale_getstring (IDS_USECERTIFICATES_CHK));
_r_menu_setitemtext (hmenu, IDM_KEEPUNUSED_CHK, FALSE, _r_locale_getstring (IDS_KEEPUNUSED_CHK));
@@ -199,6 +207,7 @@ VOID _app_message_localize (
{
_r_menu_setitemtext (hsubmenu, 5, TRUE, _r_locale_getstring (IDS_TRAY_RULES));
_r_menu_setitemtext (hsubmenu, 6, TRUE, _r_locale_getstring (IDS_PROFILE_TYPE));
+ _r_menu_setitemtext (hsubmenu, 7, TRUE, _r_locale_getstring (IDS_TAB_NETWORK));
}
recommended_string = _r_locale_getstring (IDS_RECOMMENDED);
@@ -1405,6 +1414,12 @@ VOID _app_displayinfonetwork_callback (
case 10:
case 11:
{
+ if (ptr_network->protocol == IPPROTO_UDP && _InterlockedCompareExchange (&ptr_network->traffic_error, 0, 0))
+ {
+ _r_str_copy (lpnmlv->item.pszText, lpnmlv->item.cchTextMax, L"\x2014");
+ break;
+ }
+
if (!ptr_network->is_stats_initialized)
break;
diff --git a/src/network.c b/src/network.c
index 78cb783c..d21b005e 100644
--- a/src/network.c
+++ b/src/network.c
@@ -113,7 +113,54 @@ static VOID _app_network_update_tcp6_stats (
_app_network_update_stats_values (ptr_network, data_rod.DataBytesIn, data_rod.DataBytesOut);
}
-_Ret_maybenull_
+static VOID _app_network_update_udp_stats (
+ _Inout_ PITEM_NETWORK ptr_network,
+ _In_ PITEM_NETWORK_CONTEXT context,
+ _In_ ULONG pid,
+ _In_ ULONG64 created,
+ _In_ ULONG scope_id
+)
+{
+ UDP_ENDPOINT endpoint = {0};
+ UDP_SNAPSHOT snapshot;
+
+ if (!context->udp_stats)
+ return;
+
+ endpoint.pid = pid;
+ endpoint.af = ptr_network->af;
+ endpoint.port = ptr_network->local_port;
+ endpoint.created = created;
+ endpoint.scope_id = scope_id;
+
+ RtlCopyMemory (endpoint.address, &ptr_network->local_addr, endpoint.af == AF_INET ? 4 : 16); // set correct sizes
+
+ udp_stats_read (context->udp_stats, &endpoint, &snapshot);
+
+ _InterlockedExchange (&ptr_network->traffic_error, snapshot.error_code);
+
+ if (snapshot.error_code)
+ {
+ ptr_network->is_stats_initialized = FALSE;
+
+ return;
+ }
+
+ if (ptr_network->udp_created != created)
+ {
+ ptr_network->udp_created = created;
+ ptr_network->is_stats_initialized = FALSE;
+
+ _InterlockedExchange64 (&ptr_network->download_speed, 0);
+ _InterlockedExchange64 (&ptr_network->upload_speed, 0);
+ }
+
+ _app_network_update_stats_values (ptr_network, snapshot.received, snapshot.sent);
+
+ _InterlockedExchange64 (&ptr_network->download_total, snapshot.received);
+ _InterlockedExchange64 (&ptr_network->upload_total, snapshot.sent);
+}
+
PITEM_NETWORK_CONTEXT _app_network_getcontext ()
{
static R_INITONCE init_once = PR_INITONCE_INIT;
@@ -132,23 +179,41 @@ PITEM_NETWORK_CONTEXT _app_network_getcontext ()
return network_context;
}
+VOID _app_network_stop ()
+{
+ // the original network worker lives until process exit. keep the stopped context valid for that worker; stop/join only our independent ETW consumer.
+ udp_stats_stop (_app_network_getcontext ()->udp_stats);
+}
+
VOID _app_network_initialize (
_In_ HWND hwnd
)
{
PITEM_NETWORK_CONTEXT network_context;
R_ENVIRONMENT environment;
+ ULONG status;
if (!_r_config_getboolean (L"IsNetworkMonitorEnabled", TRUE, NULL))
return;
network_context = _app_network_getcontext ();
- if (!network_context)
+ if (!network_context) // USELESS!
return;
network_context->hwnd = hwnd;
+ // opt-in: sockets created before capture cannot be mapped by AFD events
+ if (_r_config_getboolean (L"IsUdpTrafficEnabled", FALSE, NULL))
+ {
+ network_context->udp_stats = udp_stats_create ();
+
+ status = udp_stats_start (network_context->udp_stats);
+
+ if (status != STATUS_SUCCESS && status != STATUS_ALREADY_INITIALIZED)
+ _r_log (LOG_LEVEL_WARNING, NULL, L"udp_stats_start", NULL, status);
+ }
+
_r_queuedlock_acquireexclusive (&network_context->lock_network);
_r_obj_clearhashtable (network_context->network_ptr);
_r_queuedlock_releaseexclusive (&network_context->lock_network);
@@ -176,6 +241,16 @@ VOID _app_network_uninitialize (
_r_queuedlock_releaseexclusive (&context->lock_checker);
}
+FORCEINLINE BOOLEAN _app_network_iswaitingconnection (
+ _In_ ULONG pid
+)
+{
+ if (pid == PROC_WAITING_PID && !_r_config_getboolean (L"IsNetworkShowWaitConnections", TRUE, NULL))
+ return TRUE;
+
+ return FALSE;
+}
+
VOID _app_network_generatetable (
_Inout_ PITEM_NETWORK_CONTEXT network_context
)
@@ -184,12 +259,16 @@ VOID _app_network_generatetable (
PMIB_UDP6TABLE_OWNER_MODULE udp6_table;
PMIB_TCPTABLE_OWNER_MODULE tcp4_table;
PMIB_UDPTABLE_OWNER_MODULE udp4_table;
- PITEM_NETWORK ptr_network;
IN_ADDR local_addr, remote_addr;
+ PITEM_NETWORK ptr_network;
PVOID buffer;
ULONG allocated_size, network_hash, required_size = 0;
+ BOOL udp4_complete = FALSE, udp6_complete = FALSE;
ULONG status;
+ udp_stats_poll (network_context->udp_stats);
+ udp_stats_begin_refresh (network_context->udp_stats);
+
_r_queuedlock_acquireexclusive (&network_context->lock_checker);
_r_obj_clearhashtable (network_context->checker_ptr);
_r_queuedlock_releaseexclusive (&network_context->lock_checker);
@@ -209,6 +288,9 @@ VOID _app_network_generatetable (
{
for (ULONG i = 0; i < tcp4_table->dwNumEntries; i++)
{
+ if (_app_network_iswaitingconnection (tcp4_table->table[i].dwOwningPid))
+ continue;
+
RtlZeroMemory (&remote_addr, sizeof (IN_ADDR));
RtlZeroMemory (&local_addr, sizeof (IN_ADDR));
@@ -246,6 +328,7 @@ VOID _app_network_generatetable (
if (!_app_network_getpath (ptr_network, tcp4_table->table[i].dwOwningPid, tcp4_table->table[i].OwningModuleInfo))
{
_r_obj_dereference (ptr_network);
+
continue;
}
@@ -299,6 +382,9 @@ VOID _app_network_generatetable (
{
for (ULONG i = 0; i < tcp6_table->dwNumEntries; i++)
{
+ if (_app_network_iswaitingconnection (tcp6_table->table[i].dwOwningPid))
+ continue;
+
network_hash = _app_network_gethash (
AF_INET6,
tcp6_table->table[i].dwOwningPid,
@@ -330,6 +416,7 @@ VOID _app_network_generatetable (
if (!_app_network_getpath (ptr_network, tcp6_table->table[i].dwOwningPid, tcp6_table->table[i].OwningModuleInfo))
{
_r_obj_dereference (ptr_network);
+
continue;
}
@@ -381,6 +468,8 @@ VOID _app_network_generatetable (
if (status == NO_ERROR)
{
+ udp4_complete = TRUE;
+
for (ULONG i = 0; i < udp4_table->dwNumEntries; i++)
{
RtlZeroMemory (&local_addr, sizeof (IN_ADDR));
@@ -389,20 +478,30 @@ VOID _app_network_generatetable (
network_hash = _app_network_gethash (AF_INET, udp4_table->table[i].dwOwningPid, NULL, 0, &local_addr, udp4_table->table[i].dwLocalPort, IPPROTO_UDP, 0);
- if (_app_network_isitemfound (network_hash))
+ ptr_network = _app_network_getitem (network_hash);
+
+ if (ptr_network)
{
+ _app_network_update_udp_stats (ptr_network, network_context, udp4_table->table[i].dwOwningPid, udp4_table->table[i].liCreateTimestamp.QuadPart, 0);
+
_r_queuedlock_acquireexclusive (&network_context->lock_checker);
_r_obj_addhashtablepointer (network_context->checker_ptr, network_hash, NULL);
_r_queuedlock_releaseexclusive (&network_context->lock_checker);
+ _r_obj_dereference (ptr_network);
+
continue;
}
+ if (_app_network_iswaitingconnection (udp4_table->table[i].dwOwningPid))
+ continue;
+
ptr_network = (PITEM_NETWORK)_r_obj_allocate (sizeof (ITEM_NETWORK), &_app_dereferencenetwork);
if (!_app_network_getpath (ptr_network, udp4_table->table[i].dwOwningPid, udp4_table->table[i].OwningModuleInfo))
{
_r_obj_dereference (ptr_network);
+
continue;
}
@@ -413,6 +512,8 @@ VOID _app_network_generatetable (
ptr_network->local_addr.S_un.S_addr = udp4_table->table[i].dwLocalAddr;
ptr_network->local_port = _r_byteswap_ushort ((USHORT)udp4_table->table[i].dwLocalPort);
+ _app_network_update_udp_stats (ptr_network, network_context, udp4_table->table[i].dwOwningPid, udp4_table->table[i].liCreateTimestamp.QuadPart, 0);
+
if (_app_network_isvalidconnection (ptr_network->af, &ptr_network->local_addr))
ptr_network->is_connection = TRUE;
@@ -444,24 +545,36 @@ VOID _app_network_generatetable (
if (status == NO_ERROR)
{
+ udp6_complete = TRUE;
+
for (ULONG i = 0; i < udp6_table->dwNumEntries; i++)
{
network_hash = _app_network_gethash (AF_INET6, udp6_table->table[i].dwOwningPid, NULL, 0, udp6_table->table[i].ucLocalAddr, udp6_table->table[i].dwLocalPort, IPPROTO_UDP, 0);
- if (_app_network_isitemfound (network_hash))
+ ptr_network = _app_network_getitem (network_hash);
+
+ if (ptr_network)
{
+ _app_network_update_udp_stats (ptr_network, network_context, udp6_table->table[i].dwOwningPid, udp6_table->table[i].liCreateTimestamp.QuadPart, udp6_table->table[i].dwLocalScopeId);
+
_r_queuedlock_acquireexclusive (&network_context->lock_checker);
_r_obj_addhashtablepointer (network_context->checker_ptr, network_hash, NULL);
_r_queuedlock_releaseexclusive (&network_context->lock_checker);
+ _r_obj_dereference (ptr_network);
+
continue;
}
+ if (_app_network_iswaitingconnection (udp6_table->table[i].dwOwningPid))
+ continue;
+
ptr_network = (PITEM_NETWORK)_r_obj_allocate (sizeof (ITEM_NETWORK), &_app_dereferencenetwork);
if (!_app_network_getpath (ptr_network, udp6_table->table[i].dwOwningPid, udp6_table->table[i].OwningModuleInfo))
{
_r_obj_dereference (ptr_network);
+
continue;
}
@@ -472,6 +585,8 @@ VOID _app_network_generatetable (
RtlCopyMemory (ptr_network->local_addr6.u.Byte, udp6_table->table[i].ucLocalAddr, FWP_V6_ADDR_SIZE);
ptr_network->local_port = _r_byteswap_ushort ((USHORT)udp6_table->table[i].dwLocalPort);
+ _app_network_update_udp_stats (ptr_network, network_context, udp6_table->table[i].dwOwningPid, udp6_table->table[i].liCreateTimestamp.QuadPart, udp6_table->table[i].dwLocalScopeId);
+
if (_app_network_isvalidconnection (ptr_network->af, &ptr_network->local_addr6))
ptr_network->is_connection = TRUE;
@@ -486,6 +601,8 @@ VOID _app_network_generatetable (
}
}
+ udp_stats_end_refresh (network_context->udp_stats, udp4_complete, udp6_complete);
+
if (buffer)
_r_mem_free (buffer);
}
@@ -772,7 +889,7 @@ VOID _app_network_printlistviewtable (
_app_listview_addnetworkitem (network_context->hwnd, network_hash);
- if (ptr_network->path && ptr_network->app_hash)
+ if (ptr_network->app_hash && ptr_network->path)
_app_getfileinformation (ptr_network->path, ptr_network->app_hash, ptr_network->type, IDC_NETWORK);
// resolve network address
diff --git a/src/network.h b/src/network.h
index 84f09a72..0fbf807c 100644
--- a/src/network.h
+++ b/src/network.h
@@ -3,17 +3,25 @@
#pragma once
+#include "udpstats.h"
+
typedef struct _ITEM_NETWORK_CONTEXT
{
- HWND hwnd;
-
R_QUEUED_LOCK lock_network;
R_QUEUED_LOCK lock_checker;
PR_HASHTABLE network_ptr;
PR_HASHTABLE checker_ptr;
+
+ SW_UDP_STATS *udp_stats;
+
+ HWND hwnd;
} ITEM_NETWORK_CONTEXT, *PITEM_NETWORK_CONTEXT;
+PITEM_NETWORK_CONTEXT _app_network_getcontext ();
+
+VOID _app_network_stop ();
+
VOID _app_network_initialize (
_In_ HWND hwnd
);
diff --git a/src/profile.c b/src/profile.c
index a4e01566..6cf10f52 100644
--- a/src/profile.c
+++ b/src/profile.c
@@ -797,20 +797,22 @@ VOID _app_freeapplication (
{
PITEM_RULE ptr_rule;
- _r_queuedlock_acquireshared (&lock_rules);
-
- for (ULONG_PTR i = 0; i < _r_obj_getlistsize (rules_list); i++)
+ if (hwnd)
{
- ptr_rule = (PITEM_RULE)_r_obj_getlistitem (rules_list, i);
+ _r_queuedlock_acquireshared (&lock_rules);
- if (!ptr_rule || ptr_rule->type != DATA_RULE_USER)
- continue;
+ for (ULONG_PTR i = 0; i < _r_obj_getlistsize (rules_list); i++)
+ {
+ ptr_rule = (PITEM_RULE)_r_obj_getlistitem (rules_list, i);
+
+ if (!ptr_rule || ptr_rule->type != DATA_RULE_USER)
+ continue;
- if (hwnd)
_app_ruleremoveapp (hwnd, i, ptr_rule, app_hash);
- }
+ }
- _r_queuedlock_releaseshared (&lock_rules);
+ _r_queuedlock_releaseshared (&lock_rules);
+ }
_r_obj_removehashtableitem (apps_table, app_hash);
}
@@ -982,7 +984,7 @@ VOID _app_ruleremoveapp (
if (!ptr_rule->apps || !_r_obj_removehashtableitem (ptr_rule->apps, app_hash))
return;
- if (ptr_rule->is_enabled && _r_obj_isempty (ptr_rule->apps))
+ if (ptr_rule->is_enabled && _r_obj_isempty2 (ptr_rule->apps))
{
ptr_rule->is_enabled = FALSE;
ptr_rule->is_haveerrors = FALSE;
diff --git a/src/udpstats.c b/src/udpstats.c
new file mode 100644
index 00000000..8d095779
--- /dev/null
+++ b/src/udpstats.c
@@ -0,0 +1,794 @@
+// simplewall
+// Copyright (c) 2026 Henry++
+// Copyright (c) 2026 Tarkiin
+// UDP events contain metadata only; no packet payload is captured or stored
+#include "global.h"
+
+FORCEINLINE static ULONG udp_bucket (
+ _In_ ULONG pid,
+ _In_ USHORT port
+)
+{
+ return (pid ^ port) % UDP_BUCKETS;
+}
+
+static VOID udp_initialize_properties (
+ _Out_ PUDP_TRACE_PROPERTIES buffer
+)
+{
+ RtlZeroMemory (buffer, sizeof (UDP_TRACE_PROPERTIES));
+
+ buffer->properties.Wnode.BufferSize = sizeof (UDP_TRACE_PROPERTIES);
+ buffer->properties.Wnode.Guid = udp_session;
+ buffer->properties.Wnode.ClientContext = 1; // QPC; ProcessTrace converts to FILETIME
+ buffer->properties.Wnode.Flags = WNODE_FLAG_TRACED_GUID;
+ buffer->properties.BufferSize = 64;
+ //buffer->properties.MinimumBuffers = 0; // already 0
+ buffer->properties.MaximumBuffers = 128;
+ buffer->properties.FlushTimer = 1;
+ buffer->properties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
+ buffer->properties.LoggerNameOffset = FIELD_OFFSET (UDP_TRACE_PROPERTIES, name);
+}
+
+// select a bound socket, not a remote flow. UDP listeners may use wildcard addresses and IPv6 sockets may receive IPv4. never credit two rows for one event.
+static INT udp_match (
+ _In_ const PUDP_ENDPOINT endpoint,
+ _In_ ADDRESS_FAMILY af,
+ _In_ const PBYTE address
+)
+{
+ ULONG_PTR length;
+ const BYTE mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF};
+ const BYTE zero[16] = {0};
+
+ if (endpoint->af == af)
+ {
+ length = (af == AF_INET) ? 4 : 16; // set correct sizes
+
+ if (RtlEqualMemory (endpoint->address, address, length))
+ return 3;
+
+ if (RtlEqualMemory (endpoint->address, zero, length))
+ return 2;
+ }
+ else if (af == AF_INET && endpoint->af == AF_INET6)
+ {
+ if (RtlEqualMemory (endpoint->address, mapped, 12) && !RtlEqualMemory (endpoint->address + 12, address, 4)) // set correct sizes
+ return 2;
+
+ if (RtlEqualMemory (endpoint->address, zero, 16)) // set correct sizes
+ return 1;
+ }
+
+ return 0;
+}
+
+static VOID udp_account (
+ _In_ PSW_UDP_STATS stats,
+ _In_ ULONG pid,
+ _In_ ULONG size,
+ _In_ USHORT port,
+ _In_ ADDRESS_FAMILY af,
+ _In_ const PBYTE address,
+ _In_ ULONG scope_id,
+ _In_ BOOLEAN is_receive,
+ _In_ ULONG64 timestamp,
+ _In_ ULONG error_code
+)
+{
+ UDP_ENTRY *best = NULL, *entry;
+ INT candidate, score = 0;
+ BOOLEAN is_ambiguous = FALSE;
+
+ AcquireSRWLockExclusive (&stats->lock);
+
+ for (entry = stats->buckets[udp_bucket (pid, port)]; entry; entry = entry->next)
+ {
+ if (entry->endpoint.pid != pid || entry->endpoint.port != port)
+ continue;
+
+ if (af == AF_INET6 && scope_id != MAXDWORD && entry->endpoint.scope_id != scope_id)
+ continue;
+
+ candidate = udp_match (&entry->endpoint, af, address);
+
+ if (candidate > score)
+ {
+ best = entry;
+ score = candidate;
+ is_ambiguous = FALSE;
+ }
+ else if (candidate && candidate == score)
+ {
+ is_ambiguous = TRUE;
+ }
+ }
+
+ if (best && !is_ambiguous && timestamp >= best->since)
+ {
+ if (error_code != ERROR_SUCCESS)
+ best->error_code = error_code;
+
+ best->is_observed = TRUE;
+
+ if (is_receive)
+ {
+ best->received += size;
+ }
+ else
+ {
+ best->sent += size;
+ }
+ }
+ else if (is_ambiguous)
+ {
+ // never silently attribute an event to one of several equally good rows
+ for (entry = stats->buckets[udp_bucket (pid, port)]; entry; entry = entry->next)
+ {
+ if (entry->endpoint.pid == pid && entry->endpoint.port == port && udp_match (&entry->endpoint, af, address) == score)
+ entry->error_code = ERROR_INVALID_DATA;
+ }
+ }
+
+ ReleaseSRWLockExclusive (&stats->lock);
+}
+
+static ULONG udp_u32 (
+ _In_ const PBYTE data
+)
+{
+ ULONG value;
+
+ RtlCopyMemory (&value, data, sizeof (ULONG));
+
+ return value;
+}
+
+static VOID udp_afd_event (
+ _In_ PSW_UDP_STATS stats,
+ _In_ PEVENT_RECORD event
+)
+{
+ UDP_AFD_SOCKET **link, *socket;
+ ULONG64 handle = 0;
+ ULONG pointer_size = (event->EventHeader.Flags & EVENT_HEADER_FLAG_32_BIT_HEADER) ? sizeof (ULONG) : sizeof (ULONG64), prefix = 8 + 2 * pointer_size, length, size_offset;
+ USHORT af, port, id = event->EventHeader.EventDescriptor.Id;
+ const PBYTE data = (const PBYTE)(event->UserData);
+ BOOLEAN is_receive = id == 1004 || id == 1006 || id == 1009 || id == 1012 || id == 1015;
+ BOOLEAN is_transfer = is_receive || id == 1003 || id == 1005 || id == 1007 || id == 1011 || id == 1013;
+
+ // RIO uses a different completion path. mark just that socket unavailable.
+ if (id == 4006)
+ {
+ if (event->EventHeader.EventDescriptor.Version != 0 || !data || event->UserDataLength < pointer_size)
+ goto unsupported;
+
+ RtlCopyMemory (&handle, data, pointer_size);
+
+ for (socket = stats->sockets[(handle >> 4) % UDP_BUCKETS]; socket; socket = socket->next)
+ {
+ if (socket->handle == handle)
+ {
+ socket->is_unsupported = TRUE;
+
+ if (socket->is_bound)
+ {
+ udp_account (
+ stats,
+ socket->endpoint.pid,
+ 0,
+ socket->endpoint.port,
+ socket->endpoint.af,
+ socket->endpoint.address,
+ socket->endpoint.scope_id,
+ FALSE,
+ event->EventHeader.TimeStamp.QuadPart,
+ ERROR_NOT_SUPPORTED
+ );
+ }
+
+ break;
+ }
+ }
+
+ return;
+ }
+
+ if (id != 1000 && id != 1001 && id != 1002 && id != 1030 && !is_transfer)
+ return;
+
+ if (event->EventHeader.EventDescriptor.Version != 0 || !data || event->UserDataLength < prefix)
+ goto unsupported;
+
+ RtlCopyMemory (&handle, data + 8 + pointer_size, pointer_size);
+
+ link = &stats->sockets[(handle >> 4) % UDP_BUCKETS];
+
+ while (*link && (*link)->handle != handle)
+ {
+ link = &(*link)->next;
+ }
+
+ socket = *link;
+
+ if ((id == 1001 || id == 1002) && udp_u32 (data) == 0)
+ {
+ if (socket)
+ {
+ *link = socket->next;
+
+ stats->socket_count -= 1;
+
+ _r_mem_free (socket);
+ }
+
+ return;
+ }
+
+ if (id == 1000 && udp_u32 (data) == 0)
+ {
+ if (event->UserDataLength < prefix + 12 + pointer_size + 4)
+ goto unsupported;
+
+ // kernel endpoint addresses are reused, including by TCP sockets
+ if (socket)
+ {
+ *link = socket->next;
+
+ stats->socket_count -= 1;
+
+ _r_mem_free (socket);
+
+ socket = NULL;
+ }
+
+ if (udp_u32 (data + prefix + 4) != SOCK_DGRAM || udp_u32 (data + prefix + 8) != IPPROTO_UDP)
+ return;
+
+ if (!socket)
+ {
+ if (stats->socket_count >= UDP_MAX_ENDPOINTS)
+ {
+ _InterlockedExchange (&stats->error_code, ERROR_NOT_ENOUGH_MEMORY);
+
+ return;
+ }
+
+ socket = (UDP_AFD_SOCKET *)_r_mem_allocatesafe (sizeof (UDP_AFD_SOCKET));
+
+ if (!socket)
+ {
+ _InterlockedExchange (&stats->error_code, ERROR_NOT_ENOUGH_MEMORY);
+
+ return;
+ }
+
+ socket->next = *link;
+
+ *link = socket;
+
+ stats->socket_count += 1;
+ }
+
+ socket->handle = handle;
+ socket->is_bound = FALSE;
+
+ RtlZeroMemory (&socket->endpoint, sizeof (UDP_ENDPOINT));
+
+ socket->endpoint.pid = udp_u32 (data + prefix + 12);
+
+ return;
+ }
+
+ if (!socket || udp_u32 (data) != 1)
+ return;
+
+ if (id == 1030)
+ {
+ if (event->UserDataLength < prefix + 8)
+ goto unsupported;
+
+ if (udp_u32 (data + prefix) != ERROR_SUCCESS)
+ return;
+
+ length = udp_u32 (data + prefix + 4);
+
+ if (length > event->UserDataLength - prefix - 8 || length < 4)
+ goto unsupported;
+
+ RtlCopyMemory (&af, data + prefix + 8, 2);
+ RtlCopyMemory (&port, data + prefix + 10, 2);
+
+ if ((af != AF_INET && af != AF_INET6) || length < (af == AF_INET ? 16U : 28U))
+ goto unsupported;
+
+ socket->endpoint.af = af;
+ socket->endpoint.port = (USHORT)((port >> 8) | (port << 8));
+
+ RtlCopyMemory (socket->endpoint.address, data + prefix + (af == AF_INET ? 12 : 16), af == AF_INET ? 4 : 16);
+
+ socket->endpoint.scope_id = (af == AF_INET6) ? udp_u32 (data + prefix + 32) : 0;
+ socket->is_bound = socket->endpoint.port != 0;
+
+ if (socket->is_bound && socket->is_unsupported)
+ {
+ udp_account (
+ stats,
+ socket->endpoint.pid,
+ 0,
+ socket->endpoint.port,
+ af,
+ socket->endpoint.address,
+ socket->endpoint.scope_id,
+ FALSE,
+ event->EventHeader.TimeStamp.QuadPart,
+ ERROR_NOT_SUPPORTED
+ );
+ }
+
+ return;
+ }
+
+ if (is_transfer && socket->is_bound)
+ {
+ size_offset = prefix + 4 + pointer_size;
+
+ if (event->UserDataLength < size_offset + 8)
+ goto unsupported;
+
+ if (udp_u32 (data + size_offset + 4) != ERROR_SUCCESS)
+ return;
+
+ udp_account (
+ stats,
+ socket->endpoint.pid,
+ udp_u32 (data + size_offset),
+ socket->endpoint.port,
+ socket->endpoint.af,
+ socket->endpoint.address,
+ socket->endpoint.scope_id,
+ is_receive,
+ event->EventHeader.TimeStamp.QuadPart,
+ socket->is_unsupported ? ERROR_NOT_SUPPORTED : ERROR_SUCCESS
+ );
+ }
+
+ return;
+
+unsupported:
+
+ _InterlockedExchange (&stats->error_code, ERROR_NOT_SUPPORTED);
+}
+
+static VOID WINAPI udp_event (
+ _In_ PEVENT_RECORD event
+)
+{
+ PSW_UDP_STATS stats = event->UserContext;
+#if defined(UDP_STATS_DIAGNOSTIC)
+ UDP_STATS_DIAGNOSTIC (event);
+#endif // UDP_STATS_DIAGNOSTIC
+
+ if (_InterlockedCompareExchange (&stats->stopping, 0, 0))
+ return;
+
+ if (IsEqualGUID (&event->EventHeader.ProviderId, &afd_provider))
+ udp_afd_event (stats, event);
+}
+
+static ULONG WINAPI udp_buffer (
+ _In_ PEVENT_TRACE_LOGFILEW buffer
+)
+{
+ PSW_UDP_STATS stats = (PSW_UDP_STATS)buffer->Context;
+
+ if (buffer->EventsLost)
+ _InterlockedExchange (&stats->error_code, ERROR_DATA_NOT_ACCEPTED);
+
+ return !_InterlockedCompareExchange (&stats->stopping, 0, 0);
+}
+
+static VOID NTAPI udp_thread (
+ _In_ PVOID context
+)
+{
+ PSW_UDP_STATS stats = (PSW_UDP_STATS)context;
+ ULONG result;
+
+ result = ProcessTrace (&stats->hconsumer, 1, NULL, NULL);
+
+ if (!_InterlockedCompareExchange (&stats->stopping, 0, 0))
+ _InterlockedExchange (&stats->error_code, result ? result : ERROR_OPERATION_ABORTED);
+}
+
+_Ret_maybenull_
+PSW_UDP_STATS udp_stats_create ()
+{
+ PSW_UDP_STATS stats;
+
+ stats = (PSW_UDP_STATS)_r_mem_allocatesafe (sizeof (SW_UDP_STATS));
+
+ if (stats)
+ {
+ stats->hconsumer = INVALID_PROCESSTRACE_HANDLE;
+ stats->error_code = ERROR_NOT_READY;
+ }
+
+ return stats;
+}
+
+NTSTATUS udp_stats_start (
+ _Inout_opt_ PSW_UDP_STATS stats
+)
+{
+ EVENT_TRACE_LOGFILEW logfile = {0};
+ UDP_TRACE_PROPERTIES properties;
+ OBJECT_ATTRIBUTES oa = {0};
+ R_ENVIRONMENT environment;
+ UNICODE_STRING us;
+ WCHAR buffer[0x80];
+ TRACEHANDLE hsession = 0;
+ NTSTATUS status;
+
+ if (!stats)
+ return STATUS_MEMORY_NOT_ALLOCATED;
+
+ if (stats->hguard || _InterlockedCompareExchange (&stats->stopping, 0, 0))
+ return STATUS_ALREADY_INITIALIZED;
+
+ // a global semaphore distinguishes an orphan left by a crash from an active simplewall instance. never stop another tracing application's session.
+ _r_str_printf (buffer, RTL_NUMBER_OF (buffer), L"\\Sessions\\%u\\BaseNamedObjects\\%s", NtCurrentPeb ()->SessionId, UDP_SESSION_SEMAPHORE);
+
+ _r_obj_initializeunicodestring (&us, buffer);
+
+ InitializeObjectAttributes (&oa, &us, OBJ_CASE_INSENSITIVE | OBJ_OPENIF, NULL, NULL);
+
+ status = NtCreateSemaphore (&stats->hguard, SEMAPHORE_ALL_ACCESS, &oa, 1, 1);
+
+ if (NT_SUCCESS (status) || status == STATUS_OBJECT_NAME_COLLISION)
+ {
+ status = _r_sys_waitforsingleobject (stats->hguard, 0);
+
+ stats->is_owns_guard = (status == STATUS_WAIT_0);
+
+ status = stats->is_owns_guard ? STATUS_SUCCESS : STATUS_ALREADY_INITIALIZED;
+ }
+ else
+ {
+ stats->hguard = NULL;
+
+ goto fail;
+ }
+
+ udp_initialize_properties (&properties);
+
+ status = StartTraceW (&hsession, UDP_SESSION_NAME, &properties.properties);
+
+ if (status == ERROR_ALREADY_EXISTS)
+ {
+ udp_initialize_properties (&properties);
+
+ status = ControlTraceW (0, UDP_SESSION_NAME, &properties.properties, EVENT_TRACE_CONTROL_QUERY);
+
+ if (status == ERROR_SUCCESS && IsEqualGUID (&properties.properties.Wnode.Guid, &udp_session))
+ {
+ status = ControlTraceW (0, UDP_SESSION_NAME, &properties.properties, EVENT_TRACE_CONTROL_STOP);
+
+ if (status == ERROR_SUCCESS)
+ {
+ udp_initialize_properties (&properties);
+
+ status = StartTraceW (&hsession, UDP_SESSION_NAME, &properties.properties);
+ }
+ }
+ else if (status == ERROR_SUCCESS)
+ {
+ status = STATUS_ALREADY_INITIALIZED;
+ }
+ }
+
+ if (status != STATUS_SUCCESS)
+ goto fail;
+
+ stats->hsession = hsession;
+
+ // DATAGRAM plus RIO lifecycle metadata; no verbose packet-buffer events
+ status = EnableTraceEx2 (hsession, &afd_provider, EVENT_CONTROL_CODE_ENABLE_PROVIDER, TRACE_LEVEL_INFORMATION, 0x41, 0, 0, NULL);
+
+ if (status != STATUS_SUCCESS)
+ {
+ status = _r_sys_doserrortontstatus (status);
+
+ goto fail;
+ }
+
+ logfile.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;
+ logfile.LoggerName = UDP_SESSION_NAME;
+ logfile.EventRecordCallback = &udp_event;
+ logfile.BufferCallback = &udp_buffer;
+ logfile.Context = stats;
+
+ stats->hconsumer = OpenTraceW (&logfile);
+
+ if (stats->hconsumer == INVALID_PROCESSTRACE_HANDLE)
+ {
+ status = _r_sys_doserrortontstatus (NtLastError ());
+
+ goto fail;
+ }
+
+ _InterlockedExchange (&stats->error_code, STATUS_SUCCESS);
+
+ _r_sys_setenvironment (&environment, THREAD_PRIORITY_ABOVE_NORMAL, IoPriorityHigh, MEMORY_PRIORITY_NORMAL);
+
+ status = _r_sys_createthread (&stats->hthread, NtCurrentProcess (), &udp_thread, stats, &environment, L"UdpMonitoring");
+
+ if (!NT_SUCCESS (status))
+ goto fail;
+
+ NtResumeThread (stats->hthread, NULL);
+
+ return STATUS_SUCCESS;
+
+fail:
+
+ udp_stats_stop (stats);
+
+ _InterlockedExchange (&stats->error_code, status);
+
+ return status;
+}
+
+VOID udp_stats_stop (
+ _In_opt_ PSW_UDP_STATS stats
+)
+{
+ UDP_TRACE_PROPERTIES properties;
+
+ if (!stats)
+ return;
+
+ AcquireSRWLockExclusive (&stats->control_lock);
+
+ _InterlockedExchange (&stats->error_code, ERROR_OPERATION_ABORTED);
+ _InterlockedExchange (&stats->stopping, TRUE);
+
+ if (stats->hsession)
+ {
+ udp_initialize_properties (&properties);
+
+ ControlTraceW (stats->hsession, NULL, &properties.properties, EVENT_TRACE_CONTROL_STOP);
+
+ stats->hsession = 0;
+ }
+
+ if (stats->hconsumer != INVALID_PROCESSTRACE_HANDLE)
+ CloseTrace (stats->hconsumer);
+
+ if (stats->hthread)
+ {
+ _r_sys_waitforsingleobject (stats->hthread, INFINITE);
+
+ NtClose (stats->hthread);
+
+ stats->hthread = NULL;
+ }
+
+ stats->hconsumer = INVALID_PROCESSTRACE_HANDLE;
+
+ if (stats->hguard)
+ {
+ if (stats->is_owns_guard)
+ NtReleaseSemaphore (stats->hguard, 1, NULL);
+
+ NtClose (stats->hguard);
+ stats->hguard = NULL;
+
+ stats->is_owns_guard = FALSE;
+ }
+
+ ReleaseSRWLockExclusive (&stats->control_lock);
+}
+
+VOID udp_stats_destroy (
+ _In_opt_ PSW_UDP_STATS stats
+)
+{
+ UDP_AFD_SOCKET *next, *socket;
+ UDP_ENTRY *entry, *next_entry;
+
+ if (!stats)
+ return;
+
+ udp_stats_stop (stats);
+
+ for (ULONG i = 0; i < UDP_BUCKETS; i++)
+ {
+ socket = stats->sockets[i];
+
+ while (socket)
+ {
+ next = socket->next;
+
+ _r_mem_free (socket);
+
+ socket = next;
+ }
+
+ entry = stats->buckets[i];
+
+ while (entry)
+ {
+ next_entry = entry->next;
+
+ _r_mem_free (entry);
+
+ entry = next_entry;
+ }
+ }
+
+ _r_mem_free (stats);
+}
+
+ULONG udp_stats_poll (
+ _In_opt_ PSW_UDP_STATS stats
+)
+{
+ UDP_TRACE_PROPERTIES properties;
+ ULONG status;
+
+ if (!stats)
+ return ERROR_NOT_READY;
+
+ AcquireSRWLockExclusive (&stats->control_lock);
+
+ status = _InterlockedCompareExchange (&stats->error_code, 0, 0);
+
+ if (status == ERROR_SUCCESS && stats->hsession)
+ {
+ udp_initialize_properties (&properties);
+
+ status = ControlTraceW (stats->hsession, NULL, &properties.properties, EVENT_TRACE_CONTROL_QUERY);
+
+ if (status == ERROR_SUCCESS && (properties.properties.EventsLost || properties.properties.RealTimeBuffersLost || properties.properties.LogBuffersLost))
+ status = ERROR_DATA_NOT_ACCEPTED;
+
+ if (status != ERROR_SUCCESS)
+ _InterlockedExchange (&stats->error_code, status);
+ }
+
+ ReleaseSRWLockExclusive (&stats->control_lock);
+
+ return status;
+}
+
+VOID udp_stats_begin_refresh (
+ _Inout_opt_ PSW_UDP_STATS stats
+)
+{
+ if (!stats)
+ return;
+
+ AcquireSRWLockExclusive (&stats->lock);
+ stats->epoch += 1;
+ ReleaseSRWLockExclusive (&stats->lock);
+}
+
+VOID udp_stats_read (
+ _Inout_ PSW_UDP_STATS stats,
+ _In_ PUDP_ENDPOINT endpoint,
+ _Out_ PUDP_SNAPSHOT snapshot
+)
+{
+ LARGE_INTEGER li;
+ UDP_ENTRY *entry;
+ ULONG bucket;
+
+ RtlZeroMemory (snapshot, sizeof (UDP_SNAPSHOT));
+
+ snapshot->error_code = stats ? _InterlockedCompareExchange (&stats->error_code, 0, 0) : ERROR_NOT_READY;
+
+ if (snapshot->error_code != ERROR_SUCCESS)
+ return;
+
+ bucket = udp_bucket (endpoint->pid, endpoint->port);
+
+ AcquireSRWLockExclusive (&stats->lock);
+
+ for (entry = stats->buckets[bucket]; entry; entry = entry->next)
+ {
+ if (entry->endpoint.pid == endpoint->pid && entry->endpoint.port == endpoint->port && entry->endpoint.af == endpoint->af &&
+ entry->endpoint.scope_id == endpoint->scope_id && RtlEqualMemory (entry->endpoint.address, endpoint->address, endpoint->af == AF_INET ? 4 : 16))
+ {
+ break;
+ }
+ }
+
+ if (!entry)
+ {
+ if (stats->count < UDP_MAX_ENDPOINTS)
+ entry = (PUDP_ENTRY)_r_mem_allocatesafe (sizeof (UDP_ENTRY));
+
+ if (!entry)
+ {
+ snapshot->error_code = ERROR_NOT_ENOUGH_MEMORY;
+
+ ReleaseSRWLockExclusive (&stats->lock);
+
+ return;
+ }
+
+ entry->next = stats->buckets[bucket];
+ stats->buckets[bucket] = entry;
+ stats->count += 1;
+ }
+
+ if (!entry->since || entry->endpoint.created != endpoint->created)
+ {
+ entry->endpoint = *endpoint;
+ entry->since = _r_sys_gettimestamp (&li)->QuadPart;
+
+ if (endpoint->created > entry->since)
+ entry->since = endpoint->created;
+
+ entry->received = entry->sent = 0;
+ entry->error_code = ERROR_SUCCESS;
+ entry->is_observed = FALSE;
+ }
+
+ snapshot->error_code = entry->error_code ? entry->error_code : (entry->is_observed ? ERROR_SUCCESS : ERROR_NOT_READY);
+ snapshot->received = entry->received;
+ snapshot->sent = entry->sent;
+ entry->epoch = stats->epoch;
+
+ ReleaseSRWLockExclusive (&stats->lock);
+}
+
+VOID udp_stats_end_refresh (
+ _In_opt_ SW_UDP_STATS *stats,
+ _In_ BOOLEAN ipv4_complete,
+ _In_ BOOLEAN ipv6_complete
+)
+{
+ UDP_ENTRY *entry, *next, *previous;
+
+ if (!stats)
+ return;
+
+ AcquireSRWLockExclusive (&stats->lock);
+
+ for (ULONG i = 0; i < UDP_BUCKETS; i++)
+ {
+ entry = stats->buckets[i];
+ previous = NULL;
+
+ while (entry)
+ {
+ next = entry->next;
+
+ if (entry->epoch != stats->epoch && (entry->endpoint.af == AF_INET ? ipv4_complete : ipv6_complete))
+ {
+ if (previous)
+ {
+ previous->next = next;
+ }
+ else
+ {
+ stats->buckets[i] = next;
+ }
+
+ _r_mem_free (entry);
+
+ stats->count -= 1;
+ }
+ else
+ {
+ previous = entry;
+ }
+
+ entry = next;
+ }
+ }
+
+ ReleaseSRWLockExclusive (&stats->lock);
+}
diff --git a/src/udpstats.h b/src/udpstats.h
new file mode 100644
index 00000000..f4424728
--- /dev/null
+++ b/src/udpstats.h
@@ -0,0 +1,45 @@
+// simplewall
+// Copyright (c) 2026 Henry++
+// Copyright (c) 2026 Tarkiin
+// UDP events contain metadata only; no packet payload is captured or stored
+#pragma once
+
+#include "global.h"
+
+static const GUID afd_provider = {0xE53C6823, 0x7BB8, 0x44BB, {0x90, 0xDC, 0x3F, 0x86, 0x09, 0x0D, 0x48, 0xA6}};
+static const GUID udp_session = {0x4BFA58D6, 0x5D5D, 0x46F2, {0x93, 0x93, 0xC9, 0xE0, 0x5E, 0x27, 0xB4, 0x71}};
+
+_Ret_maybenull_
+PSW_UDP_STATS udp_stats_create ();
+
+NTSTATUS udp_stats_start (
+ _Inout_opt_ PSW_UDP_STATS stats
+);
+
+VOID udp_stats_stop (
+ _In_opt_ PSW_UDP_STATS stats
+);
+
+VOID udp_stats_destroy (
+ _In_opt_ PSW_UDP_STATS stats
+);
+
+ULONG udp_stats_poll (
+ _In_opt_ PSW_UDP_STATS stats
+);
+
+VOID udp_stats_begin_refresh (
+ _Inout_opt_ PSW_UDP_STATS stats
+);
+
+VOID udp_stats_read (
+ _Inout_ PSW_UDP_STATS stats,
+ _In_ PUDP_ENDPOINT endpoint,
+ _Out_ PUDP_SNAPSHOT snapshot
+);
+
+VOID udp_stats_end_refresh (
+ _In_opt_ SW_UDP_STATS *stats,
+ _In_ BOOLEAN ipv4_complete,
+ _In_ BOOLEAN ipv6_complete
+);