NetTrace.psm1
#Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS NetTrace PowerShell Module for Windows Network Tracing .DESCRIPTION A PowerShell module that provides functionality to perform network traces using Windows native Netsh utility. Creates multiple trace files with automatic circular rotation based on file size limits. Features non-blocking operation, comprehensive logging, and background monitoring for optimal performance. .NOTES File Name : NetTrace.psm1 Version : 1.1.0 Author : Naveed Khan Company : Hogwarts Copyright : (c) 2025 Naveed Khan. All rights reserved. License : MIT License Prerequisite : Windows 10/11 with Netsh utility Requires : Administrator privileges Compatibility : PowerShell 5.1 and PowerShell 7+ .LINK https://github.com/khannaveed2020/NetTrace .LINK https://github.com/khannaveed2020/NetTrace/blob/main/README.md #> # Module variables $script:IsTracing = $false $script:TraceJob = $null $script:TracePath = $null $script:MaxFiles = 0 $script:MaxSizeMB = 0 $script:FilesCreated = 0 $script:FilesRolled = 0 $script:CurrentLogFile = $null $script:MonitorFlag = $null $script:CounterFile = $null <# .SYNOPSIS Starts a network trace using netsh trace with automatic file rotation .DESCRIPTION Starts a network trace with circular file management. Creates files up to the specified limit, then replaces the oldest file when a new one is needed. Continues until manually stopped. .PARAMETER File Maximum number of trace files to maintain simultaneously. When this limit is reached, the oldest file is deleted when creating a new one (circular buffer behavior). .PARAMETER FileSize Maximum size of each trace file in MB. Must be at least 10 MB (netsh trace limitation). When a file reaches this size, it rotates to a new file. .PARAMETER Path Directory path where trace files will be stored. .PARAMETER Stop Stops the currently running trace. .PARAMETER LogNetshOutput Logs all netsh trace output to a file named 'netsh_trace.log' in the trace directory. This suppresses console output while preserving it for troubleshooting. .PARAMETER Log When specified, enables detailed logging of all trace operations to a log file. Without this parameter, the module operates without creating log files, reducing disk I/O. Recommended for troubleshooting or monitoring trace progress. .PARAMETER Verbose Shows detailed information about files created, rolled, and deleted during circular management. .EXAMPLE NetTrace -File 2 -FileSize 10 -Path "C:\Traces" Creates up to 2 files of 10MB each in C:\Traces directory. When the 3rd file is needed, automatically deletes the oldest file (circular buffer management). Runs in background with non-blocking console operation. .EXAMPLE NetTrace -File 5 -FileSize 50 -Path "C:\Traces" -Verbose Maintains 5 files of 50MB each with detailed verbose output showing file creation, rotation, and deletion activities. Perfect for monitoring file management behavior. .EXAMPLE NetTrace -File 3 -FileSize 25 -Path "D:\NetworkTraces" -LogNetshOutput Creates 3 trace files of 25MB each in D:\NetworkTraces directory. All netsh trace output is logged to D:\NetworkTraces\netsh_trace.log for troubleshooting purposes. .EXAMPLE NetTrace -File 4 -FileSize 15 -Path "C:\Traces" -Log Creates up to 4 files of 15MB each with detailed logging enabled. Progress and file operations are logged to C:\Traces\NetTrace_*.log for monitoring and troubleshooting. .EXAMPLE NetTrace -Stop Stops the currently running network trace session and performs cleanup of background processes. Returns summary information about files created and rotated. .EXAMPLE # Start tracing with monitoring NetTrace -File 4 -FileSize 20 -Path "C:\Traces" Get-Content "C:\Traces\NetTrace_*.log" -Wait Starts network tracing and simultaneously monitors the log file for real-time activity. Use Ctrl+C to stop monitoring, then use 'NetTrace -Stop' to stop the trace. #> function NetTrace { [CmdletBinding()] param( [Parameter(Mandatory=$false)] [int]$File, [Parameter(Mandatory=$false)] [int]$FileSize, [Parameter(Mandatory=$false)] [string]$Path, [Parameter(Mandatory=$false)] [switch]$Stop, [Parameter(Mandatory=$false)] [switch]$LogNetshOutput, [Parameter(Mandatory=$false)] [switch]$Log ) try { # Check for administrator privileges $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]$currentUser $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { throw "This module requires Administrator privileges. Please run PowerShell as Administrator." } # Handle stop request if ($Stop) { Stop-NetTraceCapture return } # Validate parameters if ($File -le 0) { throw "File parameter must be a positive integer" } if ($FileSize -le 0) { throw "FileSize parameter must be a positive integer" } if ($FileSize -lt 10) { throw "FileSize must be at least 10 MB. Netsh trace has a minimum file size of 10MB - smaller values default to 512MB." } if ([string]::IsNullOrWhiteSpace($Path)) { throw "Path parameter is required" } # Ensure directory exists if (!(Test-Path $Path)) { New-Item -Path $Path -ItemType Directory -Force | Out-Null if ($VerbosePreference -eq 'Continue') { Write-Verbose "Created directory: $Path" } } # Check if already tracing if ($script:IsTracing) { Write-Warning "A trace is already running. Use NetTrace -Stop first." return } # Start trace capture Start-NetTraceCapture -Path $Path -MaxFiles $File -MaxSizeMB $FileSize -LogOutput:$LogNetshOutput -EnableLogging:$Log } catch { Write-Error "Error in NetTrace: $($_.Exception.Message)" throw } } function Start-NetTraceCapture { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Collections.Hashtable])] param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [int]$MaxFiles, [Parameter(Mandatory)] [int]$MaxSizeMB, [Parameter()] [bool]$LogOutput = $false, [Parameter()] [bool]$EnableLogging = $false ) try { $script:TracePath = $Path $script:MaxFiles = $MaxFiles $script:MaxSizeMB = $MaxSizeMB $script:FilesCreated = 0 $script:FilesRolled = 0 if ($VerbosePreference -eq 'Continue') { Write-Information "Starting network trace..." -InformationAction Continue Write-Information "Path: $Path" -InformationAction Continue Write-Information "Max Files: $MaxFiles" -InformationAction Continue Write-Information "Max Size: $MaxSizeMB MB" -InformationAction Continue } # Create log file for all output if logging is enabled if ($EnableLogging) { $script:CurrentLogFile = Join-Path $Path "NetTrace_$(Get-Date -Format 'yyyy-MM-dd_HHmmss').log" "NetTrace session started at $(Get-Date)" | Out-File -FilePath $script:CurrentLogFile -Encoding UTF8 "Command: NetTrace -File $MaxFiles -FileSize $MaxSizeMB -Path '$Path'" | Out-File -FilePath $script:CurrentLogFile -Append -Encoding UTF8 "=" * 60 | Out-File -FilePath $script:CurrentLogFile -Append -Encoding UTF8 "" | Out-File -FilePath $script:CurrentLogFile -Append -Encoding UTF8 } else { $script:CurrentLogFile = $null } # Create counter file for tracking counts $script:CounterFile = Join-Path $Path ".nettrace_counters" "0,0" | Out-File -FilePath $script:CounterFile -Encoding UTF8 # Force stop any existing netsh trace to ensure clean start (non-blocking) Start-Job -ScriptBlock { & netsh trace stop 2>&1 | Out-Null } | Out-Null # Get computer name and calculate max size in bytes $computerName = $env:COMPUTERNAME $maxSizeBytes = $MaxSizeMB * 1MB # Create monitoring flag for stop functionality $script:MonitorFlag = [System.Threading.ManualResetEvent]::new($false) $script:IsTracing = $true # Show initial message if ($VerbosePreference -eq 'Continue') { Write-Information "Network trace started successfully with circular file management." -InformationAction Continue Write-Information "Will maintain $MaxFiles files of $MaxSizeMB MB each, replacing oldest when full." -InformationAction Continue Write-Information "Use 'NetTrace -Stop' to stop." -InformationAction Continue } # Start background job for file monitoring $script:TraceJob = Start-Job -ScriptBlock { # Use Using: scope for variables passed from parent scope $TracePath = $using:Path $MaxFiles = $using:MaxFiles $MaxSizeMB = $using:MaxSizeMB $LogOutput = $using:LogOutput $LogFile = $using:script:CurrentLogFile $CounterFile = $using:script:CounterFile $fileNumber = 1 $filesCreated = 0 $filesRolled = 0 $fileHistory = @() # Track created files for circular management $computerName = $env:COMPUTERNAME $maxSizeBytes = $MaxSizeMB * 1MB # Function to save counter file function Save-CounterFile { param($FilesCreated, $FilesRolled, $CounterFile) try { "$FilesCreated,$FilesRolled" | Out-File -FilePath $CounterFile -Encoding UTF8 } catch { Write-Error "Failed to update counter file: $($_.Exception.Message)" } } # Function to write to log file if logging is enabled function Write-ToLog { param($Message, $LogFile) if ($LogFile) { try { $Message | Out-File -FilePath $LogFile -Append -Encoding UTF8 } catch { Write-Error "Failed to write to log file: $($_.Exception.Message)" } } } # Create flag file to check if we should continue $flagFile = Join-Path $TracePath ".nettrace_running" "running" | Out-File -FilePath $flagFile -Force # Small delay to ensure any previous netsh stop command has completed Start-Sleep -Seconds 1 # Create files continuously with circular management while (Test-Path $flagFile) { # Generate filename with computer name and timestamp $dateStamp = Get-Date -Format "dd-MM-yy" $timeStamp = Get-Date -Format "HHmmss" $traceFile = Join-Path $TracePath "$computerName`_$dateStamp-$timeStamp.etl" $fileName = [System.IO.Path]::GetFileName($traceFile) # Log file creation Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - Creating File #$fileNumber : $fileName" -LogFile $LogFile # Start netsh trace with report disabled and no additional data capture $arguments = @("trace", "start", "capture=yes", "report=disabled", "overwrite=yes", "maxSize=$MaxSizeMB", "tracefile=`"$traceFile`"") # Execute netsh and capture output to suppress console spam $netshOutput = & netsh $arguments 2>&1 $process = [PSCustomObject]@{ ExitCode = $LASTEXITCODE } # Log netsh output to file if requested if ($LogOutput) { $netshLogFile = Join-Path $TracePath "netsh_trace.log" "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - START TRACE:" | Out-File -FilePath $netshLogFile -Append -Encoding UTF8 $netshOutput | Out-File -FilePath $netshLogFile -Append -Encoding UTF8 "" | Out-File -FilePath $netshLogFile -Append -Encoding UTF8 } # Always log to main log file Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - Netsh trace started for: $fileName" -LogFile $LogFile if ($process.ExitCode -ne 0) { Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - ERROR: Failed to start trace. Exit code: $($process.ExitCode)" -LogFile $LogFile break } $filesCreated++ Save-CounterFile -FilesCreated $filesCreated -FilesRolled $filesRolled -CounterFile $CounterFile # Add current file to history $fileHistory += @{ Number = $fileNumber Path = $traceFile Name = $fileName } Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - Monitoring file size (limit: $MaxSizeMB MB)..." -LogFile $LogFile # Monitor this specific file until it reaches size limit $fileRotated = $false while (-not $fileRotated -and (Test-Path $flagFile)) { # Use shorter sleep for better responsiveness and accuracy Start-Sleep -Milliseconds 500 if (Test-Path $traceFile) { $fileSize = (Get-Item $traceFile).Length $sizeMB = [math]::Round($fileSize/1MB, 2) Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - File: $fileName - Size: $sizeMB MB / $MaxSizeMB MB" -LogFile $LogFile if ($fileSize -ge $maxSizeBytes) { # Size limit reached, rotate to next file Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - Size limit reached! Rolling to new file..." -LogFile $LogFile # Stop current trace $stopOutput = & netsh trace stop 2>&1 $stopProcess = [PSCustomObject]@{ ExitCode = $LASTEXITCODE } # Log stop output if requested if ($LogOutput) { $netshLogFile = Join-Path $TracePath "netsh_trace.log" "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - STOP TRACE:" | Out-File -FilePath $netshLogFile -Append -Encoding UTF8 $stopOutput | Out-File -FilePath $netshLogFile -Append -Encoding UTF8 "" | Out-File -FilePath $netshLogFile -Append -Encoding UTF8 } # Always log to main log file Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - Trace stopped for file: $fileName" -LogFile $LogFile if ($stopProcess.ExitCode -eq 0) { $filesRolled++ Save-CounterFile -FilesCreated $filesCreated -FilesRolled $filesRolled -CounterFile $CounterFile $fileRotated = $true $fileNumber++ # Circular file management: remove oldest file if we exceed MaxFiles if ($fileHistory.Count -gt $MaxFiles) { $oldestFile = $fileHistory[0] if (Test-Path $oldestFile.Path) { Remove-Item $oldestFile.Path -Force -ErrorAction SilentlyContinue Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - Removed oldest file: $($oldestFile.Name)" -LogFile $LogFile } # Remove from history $fileHistory = $fileHistory[1..($fileHistory.Count-1)] } } else { Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - ERROR: Failed to stop trace. Exit code: $($stopProcess.ExitCode)" -LogFile $LogFile break } } } else { Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - ERROR: Trace file not found: $traceFile" -LogFile $LogFile break } } } # Stop trace when manually stopped & netsh trace stop 2>&1 | Out-Null # Output summary to log Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - Trace session ended" -LogFile $LogFile Write-ToLog -Message "$(Get-Date -Format 'HH:mm:ss') - SUMMARY: Files created: $filesCreated, Files rolled: $filesRolled" -LogFile $LogFile Write-ToLog -Message ("=" * 60) -LogFile $LogFile } Write-Information "Trace monitoring started in background." -InformationAction Continue if ($EnableLogging) { Write-Information "All output is being logged to: $($script:CurrentLogFile)" -InformationAction Continue Write-Information "You can monitor progress with: Get-Content '$($script:CurrentLogFile)' -Wait" -InformationAction Continue } else { Write-Information "Logging is disabled. Use -Log parameter to enable detailed logging." -InformationAction Continue } Write-Information "Use 'NetTrace -Stop' to stop the trace." -InformationAction Continue # Wait for the background job to create the first file and update counters Start-Sleep -Seconds 3 # Get current counts from counter file $currentCounts = Get-CurrentCount # Return summary information return @{ FilesCreated = $currentCounts.FilesCreated FilesRolled = $currentCounts.FilesRolled Success = $true } } catch { Write-Error "Error starting network trace: $($_.Exception.Message)" throw } } function Get-CurrentCount { try { if ($script:CounterFile -and (Test-Path $script:CounterFile)) { $content = Get-Content $script:CounterFile -ErrorAction SilentlyContinue if ($content) { $parts = $content.Split(',') if ($parts.Count -eq 2) { return @{ FilesCreated = [int]$parts[0] FilesRolled = [int]$parts[1] } } } } } catch { Write-Error "Failed to read counter file: $($_.Exception.Message)" } return @{ FilesCreated = 0 FilesRolled = 0 } } function Stop-NetTraceCapture { [CmdletBinding(SupportsShouldProcess)] [OutputType([System.Collections.Hashtable])] param() try { if ($VerbosePreference -eq 'Continue') { Write-Information "Stopping network trace..." -InformationAction Continue } # Set the flag to stop monitoring $script:IsTracing = $false # Remove flag file to stop background job FIRST if ($script:TracePath) { $flagFile = Join-Path $script:TracePath ".nettrace_running" Remove-Item $flagFile -Force -ErrorAction SilentlyContinue } # Give background job time to see the flag removal and stop gracefully Start-Sleep -Milliseconds 1000 # Check if trace is still running first $statusOutput = & netsh trace show status 2>&1 $isTraceRunning = $statusOutput -notmatch "no trace session currently in progress" $stopSuccess = $true # Assume success by default if ($isTraceRunning) { # Stop netsh trace (try multiple times if needed due to potential race conditions) $stopAttempts = 0 $maxAttempts = 3 $stopSuccess = $false while ($stopAttempts -lt $maxAttempts -and -not $stopSuccess) { $stopAttempts++ & netsh trace stop 2>&1 | Out-Null $process = [PSCustomObject]@{ ExitCode = $LASTEXITCODE } if ($process.ExitCode -eq 0) { $stopSuccess = $true } else { # If first attempt fails, wait a bit and try again if ($stopAttempts -lt $maxAttempts) { Start-Sleep -Milliseconds 500 } } } } else { # Trace is already stopped, so we're successful $process = [PSCustomObject]@{ ExitCode = 0 } } if ($stopSuccess) { Write-Information "Trace stopped." -InformationAction Continue # Log the stop action if ($script:CurrentLogFile -and (Test-Path $script:CurrentLogFile)) { "$(Get-Date -Format 'HH:mm:ss') - Manual stop command received" | Out-File -FilePath $script:CurrentLogFile -Append -Encoding UTF8 "$(Get-Date -Format 'HH:mm:ss') - NetTrace session ended by user" | Out-File -FilePath $script:CurrentLogFile -Append -Encoding UTF8 Write-Information "Final logs saved to: $($script:CurrentLogFile)" -InformationAction Continue } # Get final counts from counter file $finalCounts = Get-CurrentCount return @{ Success = $true FilesCreated = $finalCounts.FilesCreated FilesRolled = $finalCounts.FilesRolled } } else { if ($VerbosePreference -eq 'Continue') { Write-Warning "Failed to stop trace after $stopAttempts attempts. Last exit code: $($process.ExitCode)" } else { Write-Warning "Failed to stop trace after multiple attempts." } return @{ Success = $false Error = "netsh trace stop failed after $stopAttempts attempts. Last exit code: $($process.ExitCode)" } } } catch { Write-Error "Error stopping network trace: $($_.Exception.Message)" return @{ Success = $false Error = $_.Exception.Message } } finally { # Clean up script variables $script:IsTracing = $false $script:CurrentLogFile = $null # Clean up counter file if ($script:CounterFile -and (Test-Path $script:CounterFile)) { Remove-Item $script:CounterFile -Force -ErrorAction SilentlyContinue } $script:CounterFile = $null # Clean up background job if ($script:TraceJob) { # Give the job a moment to see the flag file removal and stop gracefully Start-Sleep -Milliseconds 500 if ($script:TraceJob.State -eq 'Running') { Stop-Job -Job $script:TraceJob -ErrorAction SilentlyContinue } Remove-Job -Job $script:TraceJob -Force -ErrorAction SilentlyContinue $script:TraceJob = $null } if ($script:MonitorFlag) { $script:MonitorFlag.Set() $script:MonitorFlag.Dispose() $script:MonitorFlag = $null } } } # Export the main function Export-ModuleMember -Function NetTrace |