← trentontompkins.com

Your PC's Flight Recorder: turn Wireshark into an always-on network log

Almost everyone uses Wireshark the same way: something breaks, you go "ugh, fine," you launch it, you hit record, and you stare at a firehose hoping the problem happens again while you're watching. It's a reactive tool. You only ever see the scene of the crime if you happened to have the camera rolling.

But Wireshark has a headless engine called tshark, and there's nothing stopping you from running it 24/7 in the background, quietly appending one plain-English line for every packet your machine sends or receives. The result is a rolling, human-readable log of everything your computer has talked to — a flight recorder for your network. When something looks weird, you don't start recording. You just open the log and scroll back to the moment it happened.

It's about 40 lines of PowerShell, it costs $0, and once you've got it you'll wonder why it isn't built into the OS.

What it actually looks like

Each packet becomes a single readable line — timestamp, who → who, protocol, and a short description. No hex, no clicking, just text you can grep, tail, or open in Notepad:

2026-06-03 21:56:07.565  192.168.0.145 -> 72.60.31.144   TCP    62053 -> 22 [ACK] Seq=1 Ack=81
2026-06-03 21:56:07.592  67.195.176.151 -> 192.168.0.145  TLSv1.2  Application Data
2026-06-03 21:56:07.595  192.168.0.145 -> 67.195.176.151  TLSv1.2  Application Data

That's it. A timestamped, append-only ledger of every conversation your machine has. Leave it running for a week and you've got a complete history you can search.

The one trick everybody misses

Here's the part that trips people up, and it's the whole reason this needs a wrapper instead of a one-liner. An always-on capture writes forever, so you obviously need to cap the file size and rotate it. The naive plan is to have some other script — a scheduled cleanup, a log rotator — watch the file and truncate it when it gets big.

That doesn't work. While a capture is running, the capturing process holds an open handle on the file. On Windows, another process trying to truncate or rename it hits a sharing violation; on Linux you'll truncate it out from under the writer and end up with a sparse, half-corrupt file. You can't tidy up a log that something else is actively writing to.

The insight: the process that owns the file handle has to be the one that does the rotation. So instead of piping tshark straight to a file and walking away, you let PowerShell own the file handle — it reads tshark's output line by line, writes each line itself, and checks the size as it goes. When the file crosses the cap, the same process closes the handle, rotates the old files, and reopens a fresh one. No second process, no sharing violation, no corruption.

How the capture loop works

The whole thing is one PowerShell script. Walking through the pieces:

1. One -i per live adapter. tshark captures one interface at a time, so we ask Windows for every physical adapter that's currently Up and hand tshark a -i <name> for each one. Ethernet, Wi-Fi, whatever's plugged in — all of it gets logged into the same file.

$ifs = Get-NetAdapter -Physical | Where-Object { $_.Status -eq 'Up' } |
       Select-Object -ExpandProperty Name
$iargs = @(); foreach ($n in $ifs) { $iargs += @('-i', $n) }

2. Human-readable lines. The flags do the heavy lifting: -l flushes after every packet (so the log is live, not buffered), -n skips DNS name resolution (faster, and you get raw IPs you can look up yourself), and -t ad prints an absolute date-and-time stamp on every line. tshark's default one-line-per-packet summary is the human-readable format — you don't have to build it.

& $TShark @iargs -l -n -t ad 2>$null | ForEach-Object {
    $sw.WriteLine($_)        # PowerShell writes each line itself — it owns the handle
    ...
}

3. Size-cap rotation (the trick from above). PowerShell appends each line through a StreamWriter it controls. After each write it checks the stream length; when it passes the cap (500 MB here), it closes the handle, shuffles traffic.log → .1 → .2 → .3 (dropping the oldest), and reopens a fresh empty file. You keep a few hundred MB of recent history and the disk never fills.

function Rotate {
    if (Test-Path "$Log.$Keep") { Remove-Item "$Log.$Keep" -Force }
    for ($i = $Keep - 1; $i -ge 1; $i--) {
        if (Test-Path "$Log.$i") { Move-Item "$Log.$i" "$Log.$($i+1)" -Force }
    }
    if (Test-Path $Log) { Move-Item $Log "$Log.1" -Force }
}
# ...inside the per-line loop:
elseif ($sw.BaseStream.Length -gt $MaxBytes) {
    $sw.Close()
    Rotate
    $sw = New-Object System.IO.StreamWriter($Log, $false)   # fresh file
    $sw.AutoFlush = $true
}

4. A daily-reset flag. If you'd rather start clean each day (handy if another tool indexes the log overnight), drop an empty flag file — say traffic_reset.flag — anywhere the script can see it. Because the capturing process is the only one allowed to touch the open file, it watches for the flag and truncates its own log when it sees one, then deletes the flag. Same principle: whoever owns the handle does the housekeeping.

if (Test-Path $ResetFlag) {
    $sw.Close()
    Remove-Item $ResetFlag -Force
    for ($i = 1; $i -le $Keep; $i++) {
        if (Test-Path "$Log.$i") { Remove-Item "$Log.$i" -Force }
    }
    $sw = New-Object System.IO.StreamWriter($Log, $false)   # truncate to empty
    $sw.AutoFlush = $true
}

5. Single-instance self-kill. Two captures writing the same file would fight over it, so at startup the script kills any other copy of itself plus any orphaned tshark processes before it opens the log. Exactly one capture, exactly one writer.

$me = $PID
Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" |
    Where-Object { $_.ProcessId -ne $me -and $_.CommandLine -like '*traffic_capture.ps1*' } |
    ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
Get-Process tshark | Stop-Process -Force

6. An outer restart loop. Adapters come and go — you unplug Ethernet, you join a new Wi-Fi network, you wake from sleep. The whole capture is wrapped in a while ($true) that re-scans the adapters and restarts tshark whenever it exits. The log just keeps growing across all of it.

Making it survive reboots

A flight recorder you have to remember to turn on isn't a flight recorder. The companion script registers the capture as a SYSTEM scheduled task that starts at boot and at logon, restarts itself up to 999 times if it ever dies, and has no execution time limit so Windows never reaps it for running "too long." It even self-elevates — run it from any PowerShell window and it pops its own UAC prompt.

$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File C:\monitor\traffic_capture.ps1"
$triggers = @(
    (New-ScheduledTaskTrigger -AtStartup),
    (New-ScheduledTaskTrigger -AtLogOn)
)
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries `
    -DontStopIfGoingOnBatteries -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
    -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

Register-ScheduledTask -TaskName "Monitor_TrafficCapture" -Action $action `
    -Trigger $triggers -Settings $settings -Principal $principal -Force

Running as SYSTEM matters: it means the capture is alive even before anyone logs in, and it doesn't die when you log out. From the moment the machine powers on, it's recording.

The one-time setup

You need Wireshark installed — that gets you tshark.exe (the scripts expect it at C:\Program Files\Wireshark\tshark.exe) and, just as important, the Npcap driver that does the actual packet capture. The Wireshark installer bundles Npcap; just click through and let it install. On Windows:

winget install WiresharkFoundation.Wireshark

Then drop the two scripts in a folder (I keep mine in C:\monitor\, and the log lands at C:\monitor\traffic.log — change those paths to taste), and run the register script once:

powershell -ExecutionPolicy Bypass -File register_traffic_task.ps1

Approve the UAC prompt and that's the whole install. The task starts immediately and comes back on every reboot. Watch traffic.log for a few seconds — if it's growing, you're recording.

What it's good for

  • "What is my PC actually talking to?" Idle the machine for ten minutes, then read the log. Every background service, updater, and telemetry beacon shows up as an IP. Paste the surprising ones into a whois lookup and you'll learn a lot about what your software does when you're not looking.
  • Catching phone-home apps. Installed some free utility and feel uneasy? The flight recorder shows exactly where it connects and how often — no trusting the privacy policy.
  • Privacy auditing. A rolling, timestamped record of outbound connections is a real audit trail, not a vibe.
  • Debugging. "It worked five minutes ago" is a solvable problem when you have five minutes of packet history to scroll back through. No more "try to reproduce it while I record."
  • The world's nerdiest alibi. Timestamped proof of what your machine was doing at 2:14 a.m. Hopefully you never need it — but it's there.

Is this legal and ethical?

On your own machine, watching your own traffic: yes, completely. This is the digital equivalent of keeping a log of the calls your own phone makes. The one rule — and it's a real one — is don't capture networks or machines you don't own. Putting an adapter in promiscuous mode on your employer's network, a coffee-shop Wi-Fi, or anyone else's box to read their packets is a different thing entirely, often an illegal one. Keep it to your own hardware and your own traffic and you're firmly in the clear.

The whole thing

Two small PowerShell files, MIT-licensed, yours to use and butcher. Below is the always-on capture loop in full; the companion register_traffic_task.ps1 is in the download.

⬇ Download the scripts  (both scripts + README + license)

# traffic_capture.ps1 — always-on readable packet logger -> C:\monitor\traffic.log
# Captures every packet (in + out) on all Up physical adapters via tshark, writes one
# human-readable line per packet. PowerShell owns the file handle so it can ROTATE
# cleanly at a size cap (keeps traffic.log + .1/.2/.3) — never fills the disk. The
# outer loop restarts tshark if an adapter changes / it exits. Run via the
# Monitor_TrafficCapture scheduled task (SYSTEM, at-startup, auto-restart).
$ErrorActionPreference = 'Continue'
$TShark   = "C:\Program Files\Wireshark\tshark.exe"
$Log      = "C:\monitor\traffic.log"
$MaxBytes = 500MB        # rotate when traffic.log passes this
$Keep     = 3            # keep traffic.log.1 .. .3
# The fast-search indexer drops this flag on its (daily) run; we — the process that
# OWNS the file handle — truncate our own log when we see it, so traffic.log gets a
# clean daily reset and never grows unbounded. (Truncating from another process would
# sharing-violate or leave a sparse file.)
$ResetFlag = "C:\monitor\traffic_reset.flag"

# Single-instance: kill any OTHER traffic_capture loop + orphan tshark so two
# captures never fight over traffic.log (e.g. detached launch vs the SYSTEM task).
$me = $PID
Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
    Where-Object { $_.ProcessId -ne $me -and $_.CommandLine -like '*traffic_capture.ps1*' } |
    ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
Get-Process tshark -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep 1

function Rotate {
    if (Test-Path "$Log.$Keep") { Remove-Item "$Log.$Keep" -Force -ErrorAction SilentlyContinue }
    for ($i = $Keep - 1; $i -ge 1; $i--) {
        if (Test-Path "$Log.$i") { Move-Item "$Log.$i" "$Log.$($i+1)" -Force -ErrorAction SilentlyContinue }
    }
    if (Test-Path $Log) { Move-Item $Log "$Log.1" -Force -ErrorAction SilentlyContinue }
}

while ($true) {
    if (-not (Test-Path $TShark)) { Start-Sleep 30; continue }   # wait for Wireshark to be installed
    $ifs = Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' } | Select-Object -ExpandProperty Name
    if (-not $ifs) { Start-Sleep 10; continue }
    $iargs = @(); foreach ($n in $ifs) { $iargs += @('-i', $n) }
    $sw = $null
    try {
        $sw = New-Object System.IO.StreamWriter($Log, $true)   # append
        $sw.AutoFlush = $true
        # -l flush per packet, -n no name resolution (faster), -t ad absolute timestamp
        & $TShark @iargs -l -n -t ad 2>$null | ForEach-Object {
            $sw.WriteLine($_)
            if (Test-Path $ResetFlag) {
                # Indexer asked for a reset: truncate our own log + drop rotations,
                # reopen fresh (append=$false truncates). We own the handle, so clean.
                $sw.Close()
                Remove-Item $ResetFlag -Force -ErrorAction SilentlyContinue
                for ($i = 1; $i -le $Keep; $i++) {
                    if (Test-Path "$Log.$i") { Remove-Item "$Log.$i" -Force -ErrorAction SilentlyContinue }
                }
                $sw = New-Object System.IO.StreamWriter($Log, $false)   # truncate to empty
                $sw.AutoFlush = $true
            }
            elseif ($sw.BaseStream.Length -gt $MaxBytes) {
                $sw.Close()
                Rotate
                $sw = New-Object System.IO.StreamWriter($Log, $false)   # fresh file
                $sw.AutoFlush = $true
            }
        }
    } catch {
    } finally {
        if ($sw) { $sw.Close() }
    }
    Start-Sleep 3   # tshark exited (adapter change/error) -> restart capture
}

Why this matters

The lesson isn't really about Wireshark. It's that a "reactive" tool is often just a proactive tool you haven't left running yet. The engine to record your whole network was sitting on your disk the entire time; all it needed was forty lines of glue to turn "launch it when something breaks" into "it's already been recording." A surprising number of "I wish my computer told me what it was doing" wishes are one small script away from "actually, it does."

The traffic-capture scripts are released under the MIT License — Copyright © 2026 Trent Tompkins.