Télécharger Sync-Blocklist.ps1
#requires -RunAsAdministrator
[CmdletBinding()]
param(
[string]$BaseUrl = "https://blocklist.example.net",
[string]$Slug = "all",
[string]$StateDir = "$env:ProgramData\BlocklistSync",
[int]$ChunkSize = 500
)
$ErrorActionPreference = "Stop"
$BaseUrl = $BaseUrl.TrimEnd('/')
if (-not $BaseUrl.StartsWith('https://')) { throw 'BaseUrl must use HTTPS' }
if ($Slug -notmatch '^[a-z0-9]+(?:-[a-z0-9]+)*$') { throw 'Invalid slug' }
if ($ChunkSize -lt 1 -or $ChunkSize -gt 2000) { throw 'ChunkSize must be between 1 and 2000' }
New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
$StatePath = Join-Path $StateDir "$Slug-state.json"
$Cache4 = Join-Path $StateDir "$Slug-ipv4.txt"
$Cache6 = Join-Path $StateDir "$Slug-ipv6.txt"
function Read-State {
if (Test-Path $StatePath) {
try { return Get-Content -Raw -LiteralPath $StatePath | ConvertFrom-Json }
catch { return [pscustomobject]@{} }
}
return [pscustomobject]@{}
}
function Test-NetworkLine([string]$Line, [int]$Family) {
if ([string]::IsNullOrWhiteSpace($Line) -or $Line -ne $Line.Trim() -or $Line.StartsWith('#')) { return $false }
$parts = $Line.Split('/', 2)
$ip = $null
if (-not [System.Net.IPAddress]::TryParse($parts[0], [ref]$ip)) { return $false }
if ($Family -eq 4 -and $ip.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { return $false }
if ($Family -eq 6 -and $ip.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetworkV6) { return $false }
if ($parts.Count -eq 2) {
$prefix = 0
if (-not [int]::TryParse($parts[1], [ref]$prefix)) { return $false }
if ($Family -eq 4 -and ($prefix -lt 0 -or $prefix -gt 32)) { return $false }
if ($Family -eq 6 -and ($prefix -lt 0 -or $prefix -gt 128)) { return $false }
}
return $true
}
function Validate-List([string]$Text, [int]$Family) {
if ($Text.Length -gt 8MB) { throw "IPv$Family list exceeds local safety limit" }
$lines = @()
foreach ($line in ($Text -split "`r?`n")) {
if ($line -eq '') { continue } # permits the final LF and a completely empty publication
if (-not (Test-NetworkLine $line $Family)) { throw "Invalid IPv$Family line: $line" }
$lines += $line
if ($lines.Count -gt 100000) { throw "IPv$Family list contains too many entries" }
}
return @($lines | Select-Object -Unique)
}
function Fetch-List([int]$Family, [string]$CachePath, $State) {
$suffix = if ($Family -eq 4) { 'ipv4.txt' } else { 'ipv6.txt' }
$uri = "$BaseUrl/blocklists/$Slug/$suffix"
$client = [System.Net.Http.HttpClient]::new()
try {
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri)
$request.Headers.UserAgent.ParseAdd('BlocklistSync-Windows/1.0')
$etagProperty = "etag$Family"
$etag = $State.$etagProperty
if ($etag) { [void]$request.Headers.TryAddWithoutValidation('If-None-Match', [string]$etag) }
$response = $client.SendAsync($request).GetAwaiter().GetResult()
$code = [int]$response.StatusCode
if ($code -eq 304) {
if (-not (Test-Path $CachePath)) { throw "HTTP 304 for IPv$Family but local cache is missing" }
$text = Get-Content -Raw -LiteralPath $CachePath
return [pscustomobject]@{ Lines = (Validate-List $text $Family); ETag = $etag; Changed = $false }
}
if ($code -eq 404 -or $code -eq 503) { throw "Upstream HTTP $code for IPv$Family; existing firewall rules are kept" }
if ($code -ne 200) { throw "Unexpected upstream HTTP $code for IPv$Family" }
$media = $response.Content.Headers.ContentType.MediaType
if ($media -ne 'text/plain') { throw "Unexpected Content-Type $media for IPv$Family" }
$text = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$lines = Validate-List $text $Family
$tmp = "$CachePath.new"
[System.IO.File]::WriteAllText($tmp, $text, [System.Text.UTF8Encoding]::new($false))
Move-Item -Force -LiteralPath $tmp -Destination $CachePath
$newEtag = if ($response.Headers.ETag) { $response.Headers.ETag.ToString() } else { $null }
return [pscustomobject]@{ Lines = $lines; ETag = $newEtag; Changed = $true }
}
finally { $client.Dispose() }
}
function Get-Sha12([string[]]$V4, [string[]]$V6) {
$joined = "v4`n" + ($V4 -join "`n") + "`nv6`n" + ($V6 -join "`n")
$bytes = [System.Text.Encoding]::UTF8.GetBytes($joined)
$sha = [System.Security.Cryptography.SHA256]::Create()
try { return (($sha.ComputeHash($bytes) | ForEach-Object ToString x2) -join '').Substring(0,12) }
finally { $sha.Dispose() }
}
function Add-ChunkRules([string[]]$Addresses, [string]$FamilyLabel, [string]$Version, [System.Collections.Generic.List[string]]$Created) {
for ($i = 0; $i -lt $Addresses.Count; $i += $ChunkSize) {
$last = [Math]::Min($i + $ChunkSize - 1, $Addresses.Count - 1)
$chunk = @($Addresses[$i..$last])
$index = [int]($i / $ChunkSize)
$name = "BlocklistSync-$Slug-$Version-$FamilyLabel-$index"
New-NetFirewallRule -Name $name -DisplayName $name -Description "IP Blocklist Manager Phase 19 slug=$Slug version=$Version" -Direction Inbound -Action Block -Profile Any -RemoteAddress $chunk | Out-Null
$Created.Add($name)
}
}
$state = Read-State
$v4 = Fetch-List 4 $Cache4 $state
$v6 = Fetch-List 6 $Cache6 $state
if (-not $v4.Changed -and -not $v6.Changed) {
Write-Host "Publication unchanged; firewall not modified."
exit 0
}
$version = Get-Sha12 $v4.Lines $v6.Lines
$newPrefix = "BlocklistSync-$Slug-$version-"
$created = [System.Collections.Generic.List[string]]::new()
try {
if ($v4.Lines.Count -gt 0) { Add-ChunkRules $v4.Lines 'v4' $version $created }
if ($v6.Lines.Count -gt 0) { Add-ChunkRules $v6.Lines 'v6' $version $created }
# Only after the full new ruleset exists do we remove the previous version.
Get-NetFirewallRule | Where-Object {
$_.Name -like "BlocklistSync-$Slug-*" -and $_.Name -notlike "$newPrefix*"
} | Remove-NetFirewallRule
$newState = [ordered]@{ etag4 = $v4.ETag; etag6 = $v6.ETag; version = $version; updatedUtc = [DateTime]::UtcNow.ToString('o') }
$newState | ConvertTo-Json | Set-Content -Encoding UTF8 -LiteralPath $StatePath
Write-Host "Applied slug=$Slug version=$version IPv4=$($v4.Lines.Count) IPv6=$($v6.Lines.Count)"
}
catch {
foreach ($name in $created) {
Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue
}
throw
}