# VPN Fail2Ban for RRAS/SSTP on GND-SERVER - auto-blocks brute-force source IPs.
# Deployed by ClaudeTools 2026-06-23 to C:\Scripts\vpn-fail2ban.ps1; runs via scheduled task
# "VPN Fail2Ban" every 10 min as SYSTEM. Scans RRAS failed-auth events (System/RemoteAccess id 20271,
# which carry the real public source IP via the UGW3 DNAT) and adds repeat offenders (>=5 fails/3h)
# to the Windows Firewall inbound block rule 'Fail2Ban-VPN-Block'. Blocklist persisted at
# C:\Scripts\vpn-blocklist.txt; actions logged to C:\Scripts\vpn-fail2ban.log.
$ErrorActionPreference='SilentlyContinue'
$LookbackMin=180; $Threshold=5
$RuleName='Fail2Ban-VPN-Block'
$Dir='C:\Scripts'; $BlockFile=Join-Path $Dir 'vpn-blocklist.txt'; $LogFile=Join-Path $Dir 'vpn-fail2ban.log'
if(-not (Test-Path $Dir)){ New-Item -ItemType Directory -Path $Dir | Out-Null }
function IsPublic($ip){ -not ($ip -match '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.|169\.254\.|0\.)') }
# 1) tally failed VPN auth source IPs from RRAS event 20271
$since=(Get-Date).AddMinutes(-$LookbackMin); $counts=@{}
Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='RemoteAccess';Id=20271;StartTime=$since} -ErrorAction SilentlyContinue | ForEach-Object {
  if($_.Message -match 'from\s+(\d{1,3}(?:\.\d{1,3}){3})'){ $ip=$Matches[1]; $counts[$ip]=[int]$counts[$ip]+1 }
}
$offenders=@($counts.GetEnumerator() | Where-Object { $_.Value -ge $Threshold -and (IsPublic $_.Key) } | ForEach-Object { $_.Key })
# 2) merge with persisted blocklist
$blocked=@(); if(Test-Path $BlockFile){ $blocked=@(Get-Content $BlockFile | Where-Object { $_ -and (IsPublic $_) }) }
$new=@($offenders | Where-Object { $_ -notin $blocked })
if($new.Count -gt 0){ $blocked=@(($blocked+$new) | Select-Object -Unique); $blocked | Set-Content $BlockFile; "$(Get-Date -Format s)  blocked: $($new -join ', ')" | Add-Content $LogFile }
# 3) (re)apply single inbound block rule
Remove-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue
if($blocked.Count -gt 0){ New-NetFirewallRule -DisplayName $RuleName -Direction Inbound -Action Block -RemoteAddress $blocked -Profile Any -Description 'Auto-blocked VPN brute-force sources (ClaudeTools fail2ban)' | Out-Null }
