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

Stephen CollinsStephen Collins
5 min read

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:

  1. It exposes a basic GUI for user input.

  2. 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:

  1. The exact button clicked

  2. The owning PID

  3. 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:

  1. Add screenshots of the UI element clicked using PrintWindow or GDI APIs.

  2. Stream logs remotely via WebSocket or HTTP server.

  3. Monitor specific ports or domains (e.g., blocklist detection).

  4. Filter by foreground window only to reduce noise.

  5. Export logs to CSV or SQLite for time-series analysis.

Or: port it to macOS. (Spoiler: I already did — read it here.)

0
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