Skip to content

Commit dc074a2

Browse files
committed
Release_v1.0.0
1 parent 8422a5e commit dc074a2

3 files changed

Lines changed: 138 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Changelog
2+
3+
## v1.0.0 - 2026-04-27
4+
5+
Initial public release.
6+
7+
### Added
8+
9+
- Menu-driven PowerShell syslog listener for Windows.
10+
- Residential router defaults using UDP 514.
11+
- Automatic local IP suggestion from the active default gateway adapter.
12+
- Live console dashboard with message counts, last sender, last protocol, and recent activity.
13+
- Per-source logs and server/runtime logs.
14+
- Rolling log compression with size and age rotation.
15+
- Compressed archive retention cap.
16+
- Firewall rule creation with `Get-NetFirewallRule` and `netsh` fallback.
17+
- Script-root default log directory: `GW-ROUTER-LOGS`.
18+
- Change Defaults menu for common runtime assumptions.
19+
20+
### Reliability
21+
22+
- Bounded and cached reverse DNS lookups to avoid stalling log receive loops.
23+
- Hostname lookup disabled by default for safer residential behavior.
24+
- TCP client idle cleanup for long-running sessions.
25+
- Archive pruning runs at startup and after actual rotations instead of every write.
26+
- Age-based rotation uses file creation time so append writes do not reset the rotation window.

GW-Router-Logger.ps1

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@ catch {
1414
# These script-scoped values act as the main tuning points for future maintenance.
1515
# Keeping them together makes it easier to adjust behavior without searching the file.
1616
$script:AppName = 'GW Router Logger'
17+
$script:Version = '1.0.0'
1718
$script:MaxCompressedBytes = 100MB
1819
$script:ActiveLogRotateBytes = 5MB
1920
$script:ActiveLogRotateMinutes = 60
2021
$script:RecentEventsMax = 12
2122
$script:StatusRefreshMilliseconds = 2000
2223
$script:DefaultUdpPort = 514
2324
$script:DefaultTcpPort = 514
25+
$script:DefaultResolveHostNames = $false
26+
$script:DnsLookupTimeoutMilliseconds = 250
27+
$script:SourceNameCacheTtlMinutes = 30
28+
$script:TcpClientIdleTimeoutMinutes = 15
2429
$script:LastSettings = $null
30+
$script:SourceNameCache = @{}
2531

2632
function Write-Rule {
2733
param(
@@ -59,6 +65,7 @@ function Show-StartupSplash {
5965
Write-UiLine ' (_______)(_______)|/ \|\_______)|_/ \/|/ \__/(_______/' Cyan
6066
Write-Rule -Width 78 -Color DarkCyan
6167
Write-UiLine ' GW ROUTER LOGGER' White
68+
Write-UiLine (' Version {0}' -f $script:Version) DarkGray
6269
Write-UiLine ' Residential-friendly PowerShell syslog collector' DarkGray
6370
Write-Host
6471
Write-LabelValue -Label 'Mode' -Value 'Menu-driven foreground listener' -ValueColor Gray
@@ -511,7 +518,7 @@ function Get-RunSettings {
511518
$defaultBind = $null
512519
$defaultUdp = $script:DefaultUdpPort
513520
$defaultTcp = $script:DefaultTcpPort
514-
$defaultLookup = $true
521+
$defaultLookup = $script:DefaultResolveHostNames
515522
$defaultLogPath = Join-Path -Path (Get-ScriptRootPath) -ChildPath 'GW-ROUTER-LOGS'
516523

517524
if ($script:LastSettings) {
@@ -635,18 +642,18 @@ function Rotate-And-CompressLog {
635642
)
636643

637644
if (-not (Test-Path -LiteralPath $LogFilePath)) {
638-
return
645+
return $false
639646
}
640647

641648
# Rotate active logs on either size or age so quiet systems still archive regularly
642649
# and busy systems do not let a single active file grow too large.
643650
$fileInfo = Get-Item -LiteralPath $LogFilePath
644-
$ageMinutes = ((Get-Date) - $fileInfo.LastWriteTime).TotalMinutes
651+
$ageMinutes = ((Get-Date).ToUniversalTime() - $fileInfo.CreationTimeUtc).TotalMinutes
645652
$shouldRotateBySize = $fileInfo.Length -ge $script:ActiveLogRotateBytes
646653
$shouldRotateByAge = $ageMinutes -ge $script:ActiveLogRotateMinutes
647654

648655
if (-not $shouldRotateBySize -and -not $shouldRotateByAge) {
649-
return
656+
return $false
650657
}
651658

652659
Ensure-Directory -Path $ArchiveDirectory | Out-Null
@@ -669,12 +676,14 @@ function Rotate-And-CompressLog {
669676
if ($State) {
670677
Add-RecentEvent -State $State -Message ('Archived {0}' -f [IO.Path]::GetFileName($zipPath))
671678
}
679+
return $true
672680
}
673681
catch {
674682
if ($State) {
675683
$State.LastError = 'Log rotation error: ' + $_.Exception.Message
676684
Add-RecentEvent -State $State -Message $State.LastError
677685
}
686+
return $false
678687
}
679688
}
680689

@@ -734,8 +743,10 @@ function Write-LogRecord {
734743
$line = '{0} [{1}] {2}' -f $timestamp, $Category.ToUpperInvariant(), $Message
735744
try {
736745
Add-Content -LiteralPath $targetPath -Value $line -Encoding UTF8
737-
Rotate-And-CompressLog -LogFilePath $targetPath -ArchiveDirectory $archiveRoot -State $State
738-
Enforce-CompressedArchiveCap -RootPath $Settings.LogRoot -State $State
746+
$rotated = Rotate-And-CompressLog -LogFilePath $targetPath -ArchiveDirectory $archiveRoot -State $State
747+
if ($rotated) {
748+
Enforce-CompressedArchiveCap -RootPath $Settings.LogRoot -State $State
749+
}
739750
}
740751
catch {
741752
if ($State) {
@@ -761,20 +772,52 @@ function Resolve-SourceIdentity {
761772
[bool] $ResolveHostNames
762773
)
763774

775+
$nowUtc = (Get-Date).ToUniversalTime()
776+
$cacheKey = '{0}|{1}' -f $Address, $ResolveHostNames
777+
if ($script:SourceNameCache.ContainsKey($cacheKey)) {
778+
$cached = $script:SourceNameCache[$cacheKey]
779+
if ($cached.ExpiresUtc -gt $nowUtc) {
780+
return $cached.Name
781+
}
782+
}
783+
764784
if (-not $ResolveHostNames) {
785+
$script:SourceNameCache[$cacheKey] = @{
786+
Name = $Address
787+
ExpiresUtc = $nowUtc.AddMinutes($script:SourceNameCacheTtlMinutes)
788+
}
765789
return $Address
766790
}
767791

792+
$resolvedName = $Address
793+
$asyncResult = $null
768794
try {
769-
$entry = [System.Net.Dns]::GetHostEntry($Address)
770-
if ($entry.HostName) {
771-
return '{0}_{1}' -f $entry.HostName, $Address
795+
$asyncResult = [System.Net.Dns]::BeginGetHostEntry($Address, $null, $null)
796+
$completed = $asyncResult.AsyncWaitHandle.WaitOne($script:DnsLookupTimeoutMilliseconds, $false)
797+
if ($completed) {
798+
$entry = [System.Net.Dns]::EndGetHostEntry($asyncResult)
799+
if ($entry.HostName) {
800+
$resolvedName = '{0}_{1}' -f $entry.HostName, $Address
801+
}
772802
}
773803
}
774804
catch {
775805
}
806+
finally {
807+
if ($asyncResult) {
808+
try {
809+
$asyncResult.AsyncWaitHandle.Close()
810+
}
811+
catch {
812+
}
813+
}
814+
}
776815

777-
return $Address
816+
$script:SourceNameCache[$cacheKey] = @{
817+
Name = $resolvedName
818+
ExpiresUtc = $nowUtc.AddMinutes($script:SourceNameCacheTtlMinutes)
819+
}
820+
return $resolvedName
778821
}
779822

780823
function Test-IsReservedFileStem {
@@ -1009,6 +1052,26 @@ function Show-NoLogGuidance {
10091052
Pause-ForUser -Message 'Press Enter to return to the listener'
10101053
}
10111054

1055+
function Test-TcpClientClosed {
1056+
param([System.Net.Sockets.TcpClient] $TcpClient)
1057+
1058+
try {
1059+
if (-not $TcpClient.Connected) {
1060+
return $true
1061+
}
1062+
1063+
$socket = $TcpClient.Client
1064+
if ($socket.Poll(0, [System.Net.Sockets.SelectMode]::SelectRead) -and $socket.Available -eq 0) {
1065+
return $true
1066+
}
1067+
1068+
return $false
1069+
}
1070+
catch {
1071+
return $true
1072+
}
1073+
}
1074+
10121075
function Get-CompletedTcpMessages {
10131076
param([string] $Buffer)
10141077

@@ -1155,6 +1218,7 @@ function Start-LogServer {
11551218
Resolve-PreferredLogRoot -Settings $Settings
11561219
$state = New-RuntimeState -Settings $Settings
11571220

1221+
Enforce-CompressedArchiveCap -RootPath $Settings.LogRoot -State $state
11581222
Write-ServerEvent -Settings $Settings -State $state -Message 'Server startup requested.'
11591223

11601224
if ($Settings.UdpPort -gt 0 -and -not (Test-PortAvailable -Address $Settings.BindAddress -Port $Settings.UdpPort -Protocol 'UDP')) {
@@ -1235,6 +1299,7 @@ function Start-LogServer {
12351299
Stream = $accepted.GetStream()
12361300
Buffer = ''
12371301
Address = ([string] $accepted.Client.RemoteEndPoint).Split(':')[0]
1302+
LastActivity = Get-Date
12381303
}
12391304
[void] $tcpClients.Add($clientState)
12401305
$connectMessage = 'TCP client connected: {0}' -f $clientState.Address
@@ -1251,7 +1316,19 @@ function Start-LogServer {
12511316
for ($index = $tcpClients.Count - 1; $index -ge 0; $index--) {
12521317
$clientState = $tcpClients[$index]
12531318
try {
1254-
if (-not $clientState.Client.Connected) {
1319+
$idleMinutes = ((Get-Date) - $clientState.LastActivity).TotalMinutes
1320+
if ($idleMinutes -ge $script:TcpClientIdleTimeoutMinutes) {
1321+
if ($clientState.Buffer) {
1322+
Register-ReceivedMessage -Settings $Settings -State $state -Protocol 'TCP' -Address $clientState.Address -RawMessage $clientState.Buffer
1323+
}
1324+
Add-RecentEvent -State $state -Message ('TCP client idle timeout: {0}' -f $clientState.Address)
1325+
$clientState.Stream.Dispose()
1326+
$clientState.Client.Dispose()
1327+
$tcpClients.RemoveAt($index)
1328+
continue
1329+
}
1330+
1331+
if (Test-TcpClientClosed -TcpClient $clientState.Client) {
12551332
if ($clientState.Buffer) {
12561333
Register-ReceivedMessage -Settings $Settings -State $state -Protocol 'TCP' -Address $clientState.Address -RawMessage $clientState.Buffer
12571334
}
@@ -1274,6 +1351,7 @@ function Start-LogServer {
12741351
continue
12751352
}
12761353

1354+
$clientState.LastActivity = Get-Date
12771355
$clientState.Buffer += [Text.Encoding]::UTF8.GetString($readBuffer, 0, $bytesRead)
12781356
$parsed = Get-CompletedTcpMessages -Buffer $clientState.Buffer
12791357
foreach ($message in $parsed.Messages) {
@@ -1364,6 +1442,11 @@ function Show-EnvironmentSummary {
13641442

13651443
function Show-DefaultSettingsMenu {
13661444
while ($true) {
1445+
$hostnameLookupDisplay = 'Disabled'
1446+
if ($script:DefaultResolveHostNames) {
1447+
$hostnameLookupDisplay = 'Enabled'
1448+
}
1449+
13671450
Clear-Host
13681451
Write-TitleBlock -Title 'GW ROUTER LOGGER' -Subtitle 'Change defaults'
13691452
Write-Host
@@ -1373,7 +1456,10 @@ function Show-DefaultSettingsMenu {
13731456
Write-Host ('4. Active log rotate size: {0} MB' -f ([int]($script:ActiveLogRotateBytes / 1MB)))
13741457
Write-Host ('5. Active log rotate age: {0} minutes' -f $script:ActiveLogRotateMinutes)
13751458
Write-Host ('6. Recent event lines shown: {0}' -f $script:RecentEventsMax)
1376-
Write-Host '7. Return to main menu'
1459+
Write-Host ('7. Hostname lookup default: {0}' -f $hostnameLookupDisplay)
1460+
Write-Host ('8. DNS lookup timeout: {0} ms' -f $script:DnsLookupTimeoutMilliseconds)
1461+
Write-Host ('9. TCP idle timeout: {0} minutes' -f $script:TcpClientIdleTimeoutMinutes)
1462+
Write-Host '10. Return to main menu'
13771463
Write-Host
13781464

13791465
$selection = Read-Host 'Choose a setting to change'
@@ -1399,6 +1485,15 @@ function Show-DefaultSettingsMenu {
13991485
$script:RecentEventsMax = Read-ValidatedPositiveInt -Prompt 'Recent event lines shown' -Default $script:RecentEventsMax -Minimum 3
14001486
}
14011487
'7' {
1488+
$script:DefaultResolveHostNames = Read-BooleanChoice -Prompt 'Enable hostname lookup by default' -Default $script:DefaultResolveHostNames
1489+
}
1490+
'8' {
1491+
$script:DnsLookupTimeoutMilliseconds = Read-ValidatedPositiveInt -Prompt 'DNS lookup timeout in milliseconds' -Default $script:DnsLookupTimeoutMilliseconds -Minimum 50
1492+
}
1493+
'9' {
1494+
$script:TcpClientIdleTimeoutMinutes = Read-ValidatedPositiveInt -Prompt 'TCP idle timeout in minutes' -Default $script:TcpClientIdleTimeoutMinutes -Minimum 1
1495+
}
1496+
'10' {
14021497
return
14031498
}
14041499
default {

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
`GW Router Logger` is a menu-driven PowerShell syslog collector for Windows, designed for home labs, residential routers, and small-network troubleshooting.
44

5+
Current release: `v1.0.0`
6+
57
It focuses on the things that usually break first on normal Windows machines: elevation, pathing, firewall access, bind-address selection, log rollover, and clear on-screen status.
68

79
## Why this exists
@@ -25,6 +27,8 @@ This script does the opposite. It guides setup, validates inputs, uses practical
2527
- Live status dashboard with message counts, sender tracking, and recent events
2628
- Per-source log files plus a server/runtime log
2729
- Rolling archive management with compression and a compressed retention cap
30+
- Cached, bounded hostname lookup so slow reverse DNS does not stall the listener
31+
- Stale TCP client cleanup for long-running sessions
2832
- Path validation and fallback handling for less predictable Windows environments
2933
- Built to stay readable and maintainable for later troubleshooting or feature additions
3034

@@ -55,6 +59,7 @@ By default, the script:
5559

5660
- suggests the local address on the adapter currently using the default gateway
5761
- assumes the most common router syslog setup: `UDP 514`
62+
- leaves hostname lookup disabled by default for reliability
5863
- stores logs beside the script in:
5964

6065
```text

0 commit comments

Comments
 (0)