Track UI Events and Network Activity in Windows Using Rust + C#

Have you ever clicked a button in a Windows app and wondered exactly what it triggered? What if you could log not only which button was clicked, but also which network requests that click initiated—down to the TCP connection and PID (process ID)?
In this tutorial, we'll build a system that does exactly that. It's a two-part project:
A C# WinForms app with clickable buttons that send HTTP requests.
A Rust-based watcher that hooks into Windows APIs to:
Capture mouse clicks
Inspect the UI element clicked
Track all active TCP connections
Correlate them with the application responsible
The full code is available on GitHub, and this post walks through how it works—and how you can build and run it yourself.
Want the same thing on macOS? Here's the macOS version of this tutorial.
Project Overview
We'll build a minimal, reproducible testbed for observing real-time interactions between a Windows app's UI and its network behavior. The repository is split into two folders:
.
├── ExampleWindowsApp/ # C# .NET WinForms application
├── windows-watcher/ # Rust application using system APIs
The WinForms app has three buttons. Each triggers a GET request to a placeholder API. The Rust watcher runs in the background, logging every mouse click and network connection made by any app—and then filters those logs to focus on the target application.
This is a useful starting point for learning system programming, building diagnostic tools, or even prototyping lightweight telemetry systems.
Step 1: The Example C# Application
Our first component is a WinForms application in .NET 8.0.
Here's the high-level structure:
public class MainForm : Form
{
private Button buttonA, buttonB, buttonC;
private TextBox responseTextBox;
public MainForm()
{
// ... layout code omitted ...
buttonA.Click += async (_, __) => await FetchTodoAsync(1);
buttonB.Click += async (_, __) => await FetchTodoAsync(2);
buttonC.Click += async (_, __) => await FetchTodoAsync(3);
}
private async Task FetchTodoAsync(int id)
{
using var client = new HttpClient();
var response = await client.GetStringAsync(
$"https://jsonplaceholder.typicode.com/todos/{id}");
responseTextBox.Text = response;
}
}
This app does two things well:
It exposes a basic GUI for user input.
It generates real HTTP traffic based on button clicks.
This makes it ideal for testing UI-to-network flow.
Step 2: UI and Input Monitoring in Rust
Our Rust application uses Win32 APIs to hook into system-wide mouse and keyboard events. When the left mouse button is pressed, it uses UI Automation (UIA) to inspect the UI element under the cursor.
Here's a simplified breakdown:
let element = automation.ElementFromPoint(POINT { x, y })?;
let name = element.CurrentName()?;
let automation_id = element.CurrentAutomationId()?;
let pid = element.CurrentProcessId()?;
This gives us:
The visible name of the button
The automation ID (e.g.
"ButtonA"
)The PID of the owning application
From there, we can resolve the process name, and log all of that information to a file:
[2025-04-10 15:13:07.775] Element: App='ExampleWindowsApp.exe', Name='Button A', AutomationID='ButtonA'
We also listen for the ESC
key or Ctrl+C
combo to gracefully shut down the tool.
Step 3: Monitoring TCP Connections
In parallel with UI monitoring, the Rust watcher spawns a second thread that polls GetExtendedTcpTable
, a Windows API that returns all active TCP connections along with the owning process ID (PID).
For each connection, we log:
[2025-04-10 15:13:07.882] TCP: 10.0.0.5:56832 → 104.21.64.1:47872, PID=10792, STATE=ESTABLISHED
We filter for only active or connecting sockets (SYN_SENT
, ESTABLISHED
, etc.) and store a deduplicated list of observed connections to avoid re-logging old entries.
Internally, we map each row from the TCP table to a TcpConnection
struct:
struct TcpConnection {
local: (IpAddr, u16),
remote: (IpAddr, u16),
pid: u32,
state: u32,
}
Then we write human-readable summaries to the log.
Step 4: Correlating UI Events with Network Activity
Here's where the two systems come together.
Every time the user clicks a button in the WinForms app, the Rust tool logs:
The exact button clicked
The owning PID
Any new TCP connections opened by that PID
This lets us correlate frontend actions with backend effects—without modifying the application's source code.
A typical flow might look like this:
[15:13:07.775] UI Click: App='ExampleWindowsApp.exe', Name='Button A', AutomationID='ButtonA'
[15:13:07.882] TCP: 10.0.0.5:56832 → 104.21.64.1:47872, PID=10792, STATE=ESTABLISHED
From this, we know that "Button A" initiated a network request, and we can trace its destination.
Step 5: Running the System
To run the entire setup:
1. Build and run the C# app
cd ExampleWindowsApp
start ExampleWindowsApp.sln
Build it in Visual Studio, then run it.
2. Build and run the Rust watcher
cd windows-watcher
cargo run --release
The watcher will start logging immediately. Try clicking buttons in the app and observe the log updates.
3. Log file location
Output logs are stored at:
%LOCALAPPDATA%\WindowsWatcher\windows_watcher.log
You can also view a sample session in example_output.txt
.
What You've Learned
This project walks through a powerful debugging pattern:
Hook into user input at the system level
Use UIA to extract app-specific context
Use network introspection to log outgoing connections
Combine them to create a real-time window into app behavior
No packet sniffing. No admin access required. Just clean, observable data.
Exercises for the Reader (Next Steps)
Want to go deeper? Here are a few exercises to take this further:
Add screenshots of the UI element clicked using
PrintWindow
or GDI APIs.Stream logs remotely via WebSocket or HTTP server.
Monitor specific ports or domains (e.g., blocklist detection).
Filter by foreground window only to reduce noise.
Export logs to CSV or SQLite for time-series analysis.
Or: port it to macOS. (Spoiler: I already did — read it here.)
Subscribe to my newsletter
Read articles from Stephen Collins directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Stephen Collins
Stephen Collins
Senior Software engineer currently working with a climate-tech startup