20260828 收口Feature151 TA constraints优化与验证

This commit is contained in:
2026-08-28 17:42:04 +08:00
parent b59adc65a8
commit 74cbebbd33
18 changed files with 3854 additions and 40 deletions

View File

@@ -121,6 +121,7 @@ done
cp -a "$REPO_ROOT/tests/fixtures/tal" "$STAGE_DIR/fixtures/"
cp -a "$REPO_ROOT/tests/fixtures/ta" "$STAGE_DIR/fixtures/"
cp -a "$REPO_ROOT/tests/fixtures/ta_constraints" "$STAGE_DIR/fixtures/"
cp -a "$REPO_ROOT/scripts/periodic" "$STAGE_DIR/scripts/"
cp -a "$REPO_ROOT/scripts/cir" "$STAGE_DIR/scripts/"
cp -a "$REPO_ROOT/scripts/inter_rp" "$STAGE_DIR/scripts/"

View File

@@ -100,6 +100,15 @@ ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=0
# 传给 rpki 子进程的额外参数。多个参数用空格分隔。
# 示例:RPKI_EXTRA_ARGS="--enable-roa-validation-cache"
# 实验性 transport 预热:RPKI_EXTRA_ARGS="--enable-transport-request-prefetch --enable-roa-validation-cache"
# TA constraints 的 RIPE 单 TA soak 示例(policy 并非默认启用):
# RIRS=ripe
# RPKI_EXTRA_ARGS="--ta-constraints ripe-ncc=${PACKAGE_ROOT}/fixtures/ta_constraints/ripe-ncc-afrinic-deny.constraints"
# all5 性能 A/B 的完整 AFRINIC IPv4 deny 示例(仅四个非 AFRINIC TA):
# RIRS=afrinic,apnic,arin,lacnic,ripe
# RPKI_EXTRA_ARGS="--ta-constraints apnic-rfc7730-https=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints --ta-constraints arin=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints --ta-constraints lacnic=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints --ta-constraints ripe-ncc=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints"
# M7 current-only AFRINIC holdings(delegated status=allocated/assigned;不含 legacy/ERX):
# RIRS=afrinic,apnic,arin,lacnic,ripe
# RPKI_EXTRA_ARGS="--ta-constraints apnic-rfc7730-https=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints --ta-constraints arin=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints --ta-constraints lacnic=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints --ta-constraints ripe-ncc=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints"
RPKI_EXTRA_ARGS=""
# 所有出站 HTTP 请求(RRDP / TAL / TA / 死 repo 探针)的 User-Agent。

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""Generate a current-only AFRINIC IPv4 holdings constraints fixture.
The AFRINIC delegated extended format contains one IPv4 allocation/assignment
per line. This tool keeps only active registry holdings (``allocated`` and
``assigned``), computes their exact union, and emits an allow-all policy with
the union as IPv4 deny entries. ``reserved`` and ``available`` records are
intentionally excluded.
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import ipaddress
import pathlib
import sys
import urllib.error
import urllib.request
from collections import Counter
from typing import Iterable
DEFAULT_SOURCE_URL = (
"https://ftp.afrinic.net/pub/stats/afrinic/"
"delegated-afrinic-extended-latest"
)
ACTIVE_STATUSES = {"allocated", "assigned"}
def merge_intervals(
intervals: Iterable[tuple[int, int]],
) -> list[tuple[int, int]]:
merged: list[tuple[int, int]] = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1] + 1:
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
else:
merged.append((start, end))
return merged
def parse_source(raw: bytes) -> tuple[list[tuple[int, int]], Counter[str], Counter[str]]:
status_counts: Counter[str] = Counter()
ipv4_status_counts: Counter[str] = Counter()
active: list[tuple[int, int]] = []
for line_number, raw_line in enumerate(raw.decode("utf-8", "replace").splitlines(), 1):
if not raw_line or raw_line.startswith("#"):
continue
fields = raw_line.split("|")
if len(fields) < 7 or fields[2] != "ipv4":
continue
status = fields[6]
status_counts[status] += 1
ipv4_status_counts[status] += 1
if status not in ACTIVE_STATUSES:
continue
try:
start = int(ipaddress.IPv4Address(fields[3]))
count = int(fields[4])
except (ValueError, TypeError) as error:
raise ValueError(f"line {line_number}: invalid IPv4 record: {raw_line}") from error
if count <= 0 or start + count > 1 << 32:
raise ValueError(f"line {line_number}: invalid IPv4 range: {raw_line}")
active.append((start, start + count - 1))
return active, status_counts, ipv4_status_counts
def render(
source_url: str,
retrieved: str,
source_sha256: str,
source_ipv4_count: int,
status_counts: Counter[str],
active_source: list[tuple[int, int]],
) -> tuple[str, dict[str, int]]:
merged = merge_intervals(active_source)
networks = [
network
for start, end in merged
for network in ipaddress.summarize_address_range(
ipaddress.IPv4Address(start), ipaddress.IPv4Address(end)
)
]
networks.sort(key=lambda network: int(network.network_address))
active_addresses = sum(end - start + 1 for start, end in merged)
counts = {
"source_ipv4_records": source_ipv4_count,
"allocated": status_counts["allocated"],
"assigned": status_counts["assigned"],
"reserved": status_counts["reserved"],
"available": status_counts["available"],
"selected_source_records": len(active_source),
"selected_addresses": active_addresses,
"merged_intervals": len(merged),
"cidr_rules": len(networks),
}
lines = [
"# Feature #151 M7 current AFRINIC IPv4 holdings deny fixture.",
"#",
"# Source: AFRINIC delegated extended latest.",
f"# URL: {source_url}",
f"# Retrieved: {retrieved}",
f"# Source SHA-256: {source_sha256}",
"#",
"# Selection: IPv4 records with status allocated or assigned only.",
"# Excluded statuses: reserved and available (not current registry holdings).",
(
"# Source IPv4 records: {source_ipv4_records}; allocated={allocated}; "
"assigned={assigned}; reserved={reserved}; available={available}."
).format(**counts),
"# Selected source records: {selected_source_records}; selected addresses: {selected_addresses}.".format(
**counts
),
"# Overlap/adjacency union intervals: {merged_intervals}; exact CIDR deny rules: {cidr_rules}.".format(
**counts
),
"# IPv6 and ASN remain allow-all to isolate the current AFRINIC IPv4 policy.",
"allow 0.0.0.0/0",
"allow ::/0",
"allow 0 - 4294967295",
"",
]
lines.extend(f"deny {network}" for network in networks)
return "\n".join(lines) + "\n", counts
def fetch_source(url: str, timeout: float) -> bytes:
request = urllib.request.Request(
url,
headers={"User-Agent": "rpki-dev-feature151-afrinic-holdings/1.0"},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
source = parser.add_mutually_exclusive_group()
source.add_argument("--url", default=DEFAULT_SOURCE_URL, help="AFRINIC source URL")
source.add_argument("--input", type=pathlib.Path, help="local delegated file for replay")
parser.add_argument("--output", type=pathlib.Path, required=True)
parser.add_argument("--retrieved", help="UTC date recorded in the fixture header")
parser.add_argument("--timeout", type=float, default=60.0)
args = parser.parse_args()
source_url = args.url if args.input is None else f"file://{args.input.resolve()}"
raw = args.input.read_bytes() if args.input is not None else fetch_source(args.url, args.timeout)
retrieved = args.retrieved or dt.datetime.now(dt.timezone.utc).date().isoformat()
active, status_counts, ipv4_status_counts = parse_source(raw)
rendered, counts = render(
source_url,
retrieved,
hashlib.sha256(raw).hexdigest(),
sum(ipv4_status_counts.values()),
status_counts,
active,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered, encoding="utf-8")
print(
"generated {output}: {cidr_rules} CIDRs from {selected_source_records} active "
"records ({selected_addresses} addresses); source_sha256={sha256}".format(
output=args.output,
sha256=hashlib.sha256(raw).hexdigest(),
**counts,
)
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, urllib.error.URLError) as error:
print(f"error: {error}", file=sys.stderr)
raise SystemExit(2)