Let the consistency report exclude address ranges
Docker reuses the same subnet on many hosts, and those networks aren't part of the LAN, so they show up as conflicts and unlisted addresses. The report only knew about 172.16.0.0/12 through a hardcoded rule; Docker can just as well pick 192.168.x or 10.x. The Consistency page now has an Excluded ranges card: CIDR ranges (IPv4 or IPv6) and single addresses, added with a form and removed with one click, shown to everyone and editable by operators. There's also an "Exclude range" button on each finding that pre-fills a /24 (or /64) around its address to edit. Exclusions are applied to servers, IPAM and DNS before anything is compared, so an excluded address never appears in any kind of finding, whichever source it came from, and the card says how many addresses are currently being hidden so it's clear the filter is doing something. The old hardcoded rule becomes a visible default (172.16.0.0/12) that can be removed -- it was silently wrong for anyone using 172.16/12 as a real LAN. That default is also slightly stronger than before: an address in the range is now left out even if it is in IPAM or DNS, where the old rule only skipped it when nothing else mentioned it. Remove or narrow it if that isn't wanted. Ranges are validated and normalised on the server (both families, prefix bounds, no /0, at most 50), a bad one is rejected with a message naming it and nothing is saved, and changes are audit-logged with before/after. Matching uses Node's BlockList. Stored as a settings value; managed from the report rather than admin-only Settings, like ignoring a finding. Verified with 44 checks (range parsing and rejection, boundary addresses just inside and outside a range, IPv6, single addresses, exclusion across all sources and finding kinds, the hidden-address count, route validation/roles/audit) and in a browser against the real router: add, invalid, remove the default, exclude from a finding. Real dev database mtime untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2352689fd3
commit
3f2b5da7be
6 files changed
+223
-23
No files matched your search
@@ -34,6 +34,8 @@ export default function Consistency({ user }: { user: CurrentUser }) {
|
||||
const [busyKey, setBusyKey] = useState<string | null>(null);
|
||||
const [expanded, setExpanded] = useState<Set<ConsistencyKind>>(new Set());
|
||||
const [showIgnored, setShowIgnored] = useState(false);
|
||||
const [newRange, setNewRange] = useState("");
|
||||
const [savingRanges, setSavingRanges] = useState(false);
|
||||
|
||||
function load() {
|
||||
setLoading(true);
|
||||
@@ -85,6 +87,42 @@ export default function Consistency({ user }: { user: CurrentUser }) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Saves the whole list (the server validates and normalises it) and reloads, so the findings reflect it straight away. */
|
||||
async function saveRanges(ranges: string[]): Promise<boolean> {
|
||||
setSavingRanges(true);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
try {
|
||||
await api.consistency.setExcludedRanges(ranges);
|
||||
await load();
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(readableError(err));
|
||||
return false;
|
||||
} finally {
|
||||
setSavingRanges(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function addRange(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!report || !newRange.trim()) return;
|
||||
if (await saveRanges([...report.excludedRanges, newRange.trim()])) setNewRange("");
|
||||
}
|
||||
|
||||
async function excludeAround(f: ConsistencyFinding) {
|
||||
if (!report || !f.ip) return;
|
||||
const suggestion = f.ip.includes(":") ? `${f.ip}/64` : `${f.ip.split(".").slice(0, 3).join(".")}.0/24`;
|
||||
const range = window.prompt(
|
||||
`Leave this range out of the report entirely — every address in it, from servers, IPAM and DNS alike.
|
||||
|
||||
Range (a network like 192.168.16.0/20, or a single address):`,
|
||||
suggestion,
|
||||
);
|
||||
if (range === null || !range.trim()) return;
|
||||
await saveRanges([...report.excludedRanges, range.trim()]);
|
||||
}
|
||||
|
||||
async function restore(id: number) {
|
||||
setError(null);
|
||||
try {
|
||||
@@ -149,6 +187,55 @@ export default function Consistency({ user }: { user: CurrentUser }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report && (
|
||||
<div className="card mb-3">
|
||||
<div className="card-body">
|
||||
<div className="fw-bold">Excluded ranges</div>
|
||||
<div className="text-secondary small mb-2">
|
||||
Addresses in these ranges are left out of the report completely — servers, IPAM and DNS alike. Meant for networks that
|
||||
aren't part of your LAN, like Docker's, which repeat the same subnet on many hosts.
|
||||
{report.hiddenAddresses > 0 && (
|
||||
<>
|
||||
{" "}
|
||||
Currently hiding {report.hiddenAddresses} address{report.hiddenAddresses === 1 ? "" : "es"}.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex flex-wrap gap-1 mb-2">
|
||||
{report.excludedRanges.length === 0 && <span className="text-secondary small">Nothing excluded.</span>}
|
||||
{report.excludedRanges.map((r) => (
|
||||
<span key={r} className="badge bg-secondary-lt text-secondary d-inline-flex align-items-center">
|
||||
<code className="bg-transparent p-0">{r}</code>
|
||||
{canEdit && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close ms-1"
|
||||
style={{ fontSize: "0.5rem" }}
|
||||
aria-label={`Stop excluding ${r}`}
|
||||
disabled={savingRanges}
|
||||
onClick={() => void saveRanges(report.excludedRanges.filter((x) => x !== r))}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{canEdit && (
|
||||
<form onSubmit={addRange} className="d-flex gap-2" style={{ maxWidth: 460 }}>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
placeholder="e.g. 192.168.16.0/20 or 10.1.2.3"
|
||||
value={newRange}
|
||||
onChange={(e) => setNewRange(e.target.value)}
|
||||
/>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={savingRanges || !newRange.trim()}>
|
||||
Exclude
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{report && (
|
||||
<div className="d-flex flex-wrap gap-2 mb-3">
|
||||
<button className={`btn btn-sm ${severity === "all" ? "btn-primary" : "btn-outline-secondary"}`} onClick={() => setSeverity("all")}>
|
||||
@@ -215,6 +302,11 @@ export default function Consistency({ user }: { user: CurrentUser }) {
|
||||
Add to IPAM
|
||||
</button>
|
||||
)}
|
||||
{f.ip && (
|
||||
<button className="btn btn-sm btn-outline-secondary" onClick={() => void excludeAround(f)} disabled={savingRanges} title="Leave a whole range out of the report">
|
||||
Exclude range
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-sm btn-outline-secondary" onClick={() => void ignore(f)} disabled={busyKey === f.key}>
|
||||
Ignore
|
||||
</button>
|
||||
@@ -282,9 +374,8 @@ export default function Consistency({ user }: { user: CurrentUser }) {
|
||||
|
||||
{report && (
|
||||
<div className="text-secondary small">
|
||||
Only private addresses are compared — public DNS records aren't expected to be in IPAM. Docker bridge networks (172.16–31.x)
|
||||
reported by agents are left out unless you've put them in DNS, and shared ones aren't counted as conflicts. Servers with no
|
||||
agent report, and IPv6 records (agents report IPv4 only), can't be judged against a server's addresses. Report generated{" "}
|
||||
Only private addresses are compared — public DNS records aren't expected to be in IPAM. Servers with no agent report, and
|
||||
IPv6 records (agents report IPv4 only), can't be judged against a server's addresses. Report generated{" "}
|
||||
{formatDateTime(new Date(report.generatedAt))}.
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user