20260828 收口Feature151 TA constraints优化与验证
This commit is contained in:
@@ -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/"
|
||||
|
||||
@@ -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。
|
||||
|
||||
178
scripts/ta_constraints/generate_afrinic_current_holdings.py
Executable file
178
scripts/ta_constraints/generate_afrinic_current_holdings.py
Executable 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)
|
||||
Reference in New Issue
Block a user