refactor(blocklist): build carve-outs on request, ship none
A carve-out names a real network, and which ones a shard should exempt depends
on where its players actually are. Publishing one in ModernUO makes that policy
call for everybody and puts a specific provider's address space in the repo, so
the script now builds them on request instead:
.\Export-IpBlocklist.ps1 -AddCarveout starlink -Asn 14593
That drops the embedded prefix blob and the table listing it. What is left is the
mechanism: fetch an ASN's current announcements, collapse them, write
ip-allowlist-<name>.txt.
Carve-outs are discovered rather than configured. Every ip-allowlist*.txt beside
the output is subtracted, by the generator and by the shard, so a file an admin
adds needs no config edit and no code change -- blocklist.json's allowlistFiles
now accepts wildcards and defaults to one pattern. FileAllowlist expands per poll
rather than at startup, so a carve-out added later is picked up without a
restart, and skips anything that is not really .txt so the generator's .tmp
sibling can never be read mid-swap.
Each carve-out file carries an asn= marker in its header, which is how
-RefreshCarveouts finds what to rebuild without this script keeping a list of
anyone's networks. A hand-written allowlist has no marker and is never rewritten.
-AddCarveout validates -Asn before any download rather than after 60MB of feeds.
Only ip-allowlist.txt is still created unprompted; deleting a carve-out now drops
it for good instead of having it written back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b6c0a0bde8
commit
a5d49894cd
5 changed files with 204 additions and 173 deletions
|
|
@ -73,12 +73,12 @@ public record BlocklistSettings
|
|||
/// entry also suppresses ban contributions, which the generator alone cannot do. See
|
||||
/// <see cref="FileAllowlist"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The filename may contain wildcards, which is how the default picks up a carve-out an admin adds
|
||||
/// without anyone editing this file.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("allowlistFiles")]
|
||||
public string[] AllowlistFiles { get; set; } =
|
||||
[
|
||||
"Configuration/ip-allowlist.txt",
|
||||
"Configuration/ip-allowlist-starlink.txt"
|
||||
];
|
||||
public string[] AllowlistFiles { get; set; } = ["Configuration/ip-allowlist*.txt"];
|
||||
|
||||
/// <summary>How often the file is checked for changes. Reloads only happen when it actually changed.</summary>
|
||||
[JsonPropertyName("reloadInterval")]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
|
|
@ -42,7 +43,7 @@ public static class FileAllowlist
|
|||
// reference swap is the whole synchronization story — readers see the old or the new snapshot, whole.
|
||||
private static volatile BlocklistSnapshot _snapshot = BlocklistSnapshot.Empty;
|
||||
|
||||
private static string[] _paths = [];
|
||||
private static string[] _patterns = [];
|
||||
private static TimeSpan _interval = TimeSpan.FromSeconds(60);
|
||||
private static long _lastStamp;
|
||||
private static CancellationTokenSource _cts;
|
||||
|
|
@ -61,10 +62,10 @@ public static class FileAllowlist
|
|||
return;
|
||||
}
|
||||
|
||||
_paths = ResolvePaths(settings.AllowlistFiles);
|
||||
_patterns = ResolvePaths(settings.AllowlistFiles);
|
||||
_interval = settings.ReloadInterval <= TimeSpan.Zero ? TimeSpan.FromSeconds(60) : settings.ReloadInterval;
|
||||
|
||||
if (_paths.Length == 0)
|
||||
if (_patterns.Length == 0)
|
||||
{
|
||||
logger.Information("File allowlist disabled (\"allowlistFiles\" empty in blocklist.json)");
|
||||
return;
|
||||
|
|
@ -110,6 +111,59 @@ public static class FileAllowlist
|
|||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands the configured patterns to actual files. Done per poll rather than once, so a carve-out an
|
||||
/// admin adds is picked up without a restart.
|
||||
/// </summary>
|
||||
private static string[] ExpandPaths()
|
||||
{
|
||||
var files = new List<string>();
|
||||
|
||||
for (var i = 0; i < _patterns.Length; i++)
|
||||
{
|
||||
var pattern = _patterns[i];
|
||||
var name = Path.GetFileName(pattern);
|
||||
|
||||
if (name.IndexOf('*') < 0 && name.IndexOf('?') < 0)
|
||||
{
|
||||
if (File.Exists(pattern))
|
||||
{
|
||||
files.Add(pattern);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(pattern);
|
||||
if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var matches = Directory.GetFiles(dir, name);
|
||||
Array.Sort(matches, StringComparer.Ordinal);
|
||||
|
||||
for (var j = 0; j < matches.Length; j++)
|
||||
{
|
||||
// Windows wildcard matching still honours legacy short names, so ".txt" can pull in the
|
||||
// generator's ".txt.tmp" mid-swap. Check the real extension.
|
||||
if (matches[j].EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
files.Add(matches[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Unreadable directory; the next poll retries.
|
||||
}
|
||||
}
|
||||
|
||||
return files.ToArray();
|
||||
}
|
||||
|
||||
private static async ValueTask PollLoop(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
|
|
@ -155,12 +209,13 @@ public static class FileAllowlist
|
|||
private static long Stamp()
|
||||
{
|
||||
var stamp = 0L;
|
||||
var paths = ExpandPaths();
|
||||
|
||||
for (var i = 0; i < _paths.Length; i++)
|
||||
for (var i = 0; i < paths.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(_paths[i]);
|
||||
var info = new FileInfo(paths[i]);
|
||||
if (info.Exists)
|
||||
{
|
||||
stamp = stamp * 31 + info.LastWriteTimeUtc.Ticks + info.Length;
|
||||
|
|
@ -205,19 +260,20 @@ public static class FileAllowlist
|
|||
{
|
||||
files = 0;
|
||||
|
||||
var chunks = new byte[_paths.Length][];
|
||||
var paths = ExpandPaths();
|
||||
var chunks = new byte[paths.Length][];
|
||||
var total = 0;
|
||||
|
||||
for (var i = 0; i < _paths.Length; i++)
|
||||
for (var i = 0; i < paths.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_paths[i]))
|
||||
if (!File.Exists(paths[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(_paths[i]);
|
||||
var bytes = File.ReadAllBytes(paths[i]);
|
||||
chunks[i] = bytes;
|
||||
total += bytes.Length + 1; // + newline separator
|
||||
files++;
|
||||
|
|
@ -225,7 +281,7 @@ public static class FileAllowlist
|
|||
catch (Exception e)
|
||||
{
|
||||
// Fail open per file: losing one entry beats refusing to load the rest.
|
||||
logger.Warning(e, "Could not read allowlist \"{Path}\"", _paths[i]);
|
||||
logger.Warning(e, "Could not read allowlist \"{Path}\"", paths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue