mirror of
https://github.com/gaoyifan/china-operator-ip.git
synced 2026-10-16 20:51:00 +08:00
b2b8edc0dd
Shared upstream attribution could assign CERNET prefixes to China Telecom or Unicom even when CERNET originated the route. Stop at the closest configured operator in each prefix/origin's common AS-path suffix, while preserving downstream coverage and multi-origin observations. Correct overlapping CNGI/legacy ASN name matches, include operator boundaries in the cache key, and wire the policy into list generation. Accumulate common suffixes instead of retaining every observed path. Add synthetic MRT and dual-stack regressions, fixed ASN matching checks, and document the resulting list semantics. Validation: 26 Rust tests and Ruby operator checks pass. Frozen-input full generation preserves china and the seven-operator address union. All 41 sample classifications agree with the RIB paths; a fresh-cache rerun after the internal simplification produces 28 byte-identical artifacts. Refs #64
338 lines
12 KiB
Makefile
338 lines
12 KiB
Makefile
set unstable
|
|
|
|
default: prepare all stat
|
|
|
|
# Install or update bgp tooling dependencies
|
|
dependency:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
cargo build --release --locked
|
|
|
|
if ! bgpkit-broker --version >/dev/null 2>&1; then
|
|
cargo binstall --secure --no-confirm bgpkit-broker@0.7.0
|
|
fi
|
|
|
|
target/release/china-operator-ip --version
|
|
bgpkit-broker --version
|
|
|
|
# Download and normalize latest autnums list
|
|
prepare_autnums:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
aria2c -s 4 -x 4 -q -o autnums.html --allow-overwrite=true https://bgp.potaroo.net/cidr/autnums.html
|
|
awk -F'[<>]' '{print $3,$5}' autnums.html | grep '^AS' > asnames.txt
|
|
rm -f autnums.html
|
|
echo "INFO> asnames.txt updated ($(wc -l < asnames.txt) entries)" >&2
|
|
|
|
# Download an RIR extended delegated statistics file
|
|
prepare_registry registry url:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
outfile="delegated-{{registry}}-extended-latest"
|
|
download="${outfile}.download"
|
|
rm -f "${download}" "${download}.aria2"
|
|
|
|
if aria2c -s 4 -x 4 -q -o "${download}" --allow-overwrite=true "{{url}}" && [[ -s "${download}" ]]; then
|
|
mv "${download}" "${outfile}"
|
|
echo "INFO> ${outfile} updated ($(wc -l < "${outfile}") records)" >&2
|
|
elif [[ -s "${outfile}" ]]; then
|
|
rm -f "${download}" "${download}.aria2"
|
|
echo "WARNING> failed to update ${outfile}; using cached copy ($(wc -l < "${outfile}") records)" >&2
|
|
else
|
|
rm -f "${download}" "${download}.aria2"
|
|
echo "ERROR> failed to update ${outfile} and no cached copy is available" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Download all RIR delegated statistics
|
|
[parallel]
|
|
prepare_registries: \
|
|
(prepare_registry "afrinic" "https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest") \
|
|
(prepare_registry "apnic" "https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest") \
|
|
(prepare_registry "arin" "https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest") \
|
|
(prepare_registry "lacnic" "https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest") \
|
|
(prepare_registry "ripencc" "https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest")
|
|
|
|
# Download the latest RIB snapshot for a collector
|
|
prepare_rib collector:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
url="$(bgpkit-broker latest -c "{{collector}}" --json \
|
|
| jq -r '.[] | select(.data_type | contains("rib")) | .url' \
|
|
| head -n 1)"
|
|
|
|
if [[ -z "${url}" ]]; then
|
|
echo "Unable to determine {{collector}} RIB download url" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${url}" =~ (\.gz|\.bz2)$ ]]; then
|
|
suffix="${BASH_REMATCH[1]}"
|
|
else
|
|
echo "Unsupported archive format for {{collector}}: ${url}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
outfile="rib-{{collector}}${suffix}"
|
|
|
|
rm -f "${outfile}"
|
|
aria2c -s 4 -x 4 -q -o "${outfile}" "${url}"
|
|
stat "${outfile}"
|
|
echo "INFO> ${outfile} ready for BGP classification" >&2
|
|
|
|
# Download the latest RIB snapshots (rrc21, rrc12, route-views6)
|
|
[parallel]
|
|
prepare_ribs: (prepare_rib "rrc00") (prepare_rib "rrc21") (prepare_rib "rrc12") (prepare_rib "route-views6")
|
|
|
|
# Prepare data for generation
|
|
[parallel]
|
|
prepare: prepare_autnums prepare_registries prepare_ribs
|
|
|
|
# Print allocated and assigned prefixes registered to COUNTRY by any RIR
|
|
registry_prefixes country:
|
|
#!/usr/bin/env ruby
|
|
require "ipaddr"
|
|
|
|
country = "{{country}}"
|
|
registries = %w[afrinic apnic arin lacnic ripencc]
|
|
files = registries.map { |registry| "delegated-#{registry}-extended-latest" }
|
|
missing = files.reject { |file| File.file?(file) && File.size?(file) }
|
|
abort("Missing delegated RIR files: #{missing.join(", ")}. Run 'just prepare_registries' first.") unless missing.empty?
|
|
prefixes = []
|
|
|
|
files.each do |file|
|
|
File.foreach(file) do |line|
|
|
_registry, record_country, type, start, value, _date, status = line.chomp.split("|", 8)
|
|
next unless record_country == country
|
|
next unless %w[allocated assigned].include?(status)
|
|
|
|
case type
|
|
when "ipv4"
|
|
current = IPAddr.new(start).to_i
|
|
remaining = Integer(value)
|
|
while remaining.positive?
|
|
alignment = current.zero? ? (1 << 32) : current & -current
|
|
block = [alignment, 1 << (remaining.bit_length - 1)].min
|
|
prefix_len = 32 - (block.bit_length - 1)
|
|
prefixes << [4, current, prefix_len, "#{IPAddr.new(current, Socket::AF_INET)}/#{prefix_len}"]
|
|
current += block
|
|
remaining -= block
|
|
end
|
|
when "ipv6"
|
|
prefix_len = Integer(value)
|
|
network = IPAddr.new("#{start}/#{prefix_len}")
|
|
prefixes << [6, network.to_i, prefix_len, "#{network}/#{prefix_len}"]
|
|
end
|
|
end
|
|
end
|
|
|
|
abort("No registered prefixes found for #{country}") if prefixes.empty?
|
|
prefixes.uniq.sort_by { |family, address, prefix_len, _| [family, address, prefix_len] }.each do |*_, prefix|
|
|
puts prefix
|
|
end
|
|
|
|
# Print raw ASN candidates for OPERATOR based on operators.yaml
|
|
get_asn_candidates_raw operator:
|
|
#!/usr/bin/env ruby
|
|
require "yaml"
|
|
|
|
cfg, asnames = "operators.yaml", "asnames.txt"
|
|
abort("Missing config: #{cfg}") unless File.file?(cfg)
|
|
abort("Missing asnames.txt. Run 'just prepare_autnums' first.") unless File.file?(asnames) && File.size?(asnames)
|
|
|
|
op = YAML.load_file(cfg).fetch("operators").fetch("{{operator}}")
|
|
country = op.fetch("country")
|
|
pattern_re = Regexp.new(op["pattern"].to_s, Regexp::IGNORECASE)
|
|
exclude_re = Regexp.new(op.fetch("exclude", "^$"), Regexp::IGNORECASE)
|
|
|
|
File.foreach(asnames) do |line|
|
|
line.chomp!
|
|
match = line.match(/^AS(\d+)\b.*,\s*([A-Z]{2})$/)
|
|
asn, line_country = match&.captures
|
|
next unless line_country == country
|
|
next unless pattern_re.match?(line)
|
|
next if exclude_re.match?(line)
|
|
puts asn
|
|
end
|
|
|
|
# Print static ASN candidates for OPERATOR based on operators.yaml
|
|
get_asn_candidates operator:
|
|
#!/usr/bin/env ruby
|
|
require "set"
|
|
require "yaml"
|
|
|
|
operator = "{{operator}}"
|
|
cfg = YAML.load_file("operators.yaml").fetch("operators").fetch(operator)
|
|
exclude_asn = cfg.fetch("exclude_asn", []).map(&:to_s).to_set
|
|
|
|
candidate_asns = IO.popen(["just", "get_asn_candidates_raw", operator], &:read).split
|
|
abort("Failed to get raw ASN candidates for #{operator}") unless $?.success?
|
|
|
|
candidate_asns.each do |asn|
|
|
puts asn unless exclude_asn.include?(asn)
|
|
end
|
|
|
|
# Print the trusted transit ASN set configured for OPERATOR
|
|
trusted_transit_asn operator:
|
|
#!/usr/bin/env ruby
|
|
require "set"
|
|
require "yaml"
|
|
|
|
operator = "{{operator}}"
|
|
operators = YAML.load_file("operators.yaml").fetch("operators")
|
|
cfg = operators.fetch(operator)
|
|
trusted_operators = cfg.fetch("trusted_transit_operators", [])
|
|
trusted_asns = Set.new
|
|
|
|
trusted_operators.each do |trusted_operator|
|
|
output = IO.popen(["just", "get_asn_candidates", trusted_operator], &:read)
|
|
abort("Failed to get trusted transit ASNs for #{trusted_operator}") unless $?.success?
|
|
trusted_asns.merge(output.split)
|
|
end
|
|
|
|
trusted_asns.sort_by(&:to_i).each { |asn| puts asn }
|
|
|
|
# Print all known operator ASNs used as shared-upstream boundaries
|
|
operator_asns:
|
|
#!/usr/bin/env ruby
|
|
require "set"
|
|
require "yaml"
|
|
|
|
asns = Set.new
|
|
YAML.load_file("operators.yaml").fetch("operators").each do |operator, cfg|
|
|
next if cfg.fetch("origin_only", false)
|
|
output = IO.popen(["just", "get_asn_candidates", operator], &:read)
|
|
abort("Failed to get operator ASNs for #{operator}") unless $?.success?
|
|
asns.merge(output.split)
|
|
end
|
|
asns.sort_by(&:to_i).each { |asn| puts asn }
|
|
|
|
# Generate IP lists for a single operator
|
|
gen operator:
|
|
#!/usr/bin/env ruby
|
|
require "fileutils"
|
|
require "yaml"
|
|
|
|
operator = "{{operator}}"
|
|
FileUtils.mkdir_p("result")
|
|
out, v4, v6 = %W[result/#{operator}46.txt result/#{operator}.txt result/#{operator}6.txt]
|
|
cfg = YAML.load_file("operators.yaml").fetch("operators").fetch(operator)
|
|
origin_only = cfg.fetch("origin_only", false)
|
|
|
|
ribs = Dir["rib-*.{gz,bz2}"].sort
|
|
abort("No rib-*.gz or rib-*.bz2 files found. Run 'just prepare_ribs' first.") if ribs.empty?
|
|
classifier = ["target/release/china-operator-ip", "--ignore-private-asn", "--cache"]
|
|
classifier << "--origin-only" if origin_only
|
|
unless origin_only
|
|
operator_asns = IO.popen(["just", "operator_asns"], &:read)
|
|
abort("Failed to get operator ASNs") unless $?.success?
|
|
operator_asn_path = "result/.operator-asns.txt"
|
|
File.write(operator_asn_path, operator_asns)
|
|
classifier += ["--operator-asn-file", operator_asn_path]
|
|
end
|
|
if country = cfg["registry_fallback_country"]
|
|
registered = IO.popen(["just", "registry_prefixes", country], &:read)
|
|
abort("Failed to get registered prefixes for #{country}") unless $?.success?
|
|
registered_path = "result/.#{operator}.registered.txt"
|
|
File.write(registered_path, registered)
|
|
classifier += ["--fallback-prefix-file", registered_path]
|
|
end
|
|
if cfg.fetch("trusted_transit_operators", []).any?
|
|
trusted_transit = IO.popen(["just", "trusted_transit_asn", operator], &:read)
|
|
abort("Failed to get trusted transit ASNs for #{operator}") unless $?.success?
|
|
trusted_transit_path = "result/.#{operator}.trusted-transit.txt"
|
|
File.write(trusted_transit_path, trusted_transit)
|
|
classifier += [
|
|
"--trusted-cn-transit-file",
|
|
trusted_transit_path,
|
|
"--asn-country-file",
|
|
"asnames.txt",
|
|
]
|
|
end
|
|
classifier += ribs.flat_map { |r| ["--mrt-file", r] }
|
|
|
|
warn "INFO> #{operator} start"
|
|
asns = IO.popen(["just", "get_asn_candidates", operator], &:read)
|
|
abort("Failed to get ASN list for #{operator}") unless $?.success?
|
|
abort("Failed to run BGP classifier for #{operator}") unless system(*classifier, *asns.split, out: out)
|
|
|
|
v6_lines, v4_lines = File.readlines(out).partition { |line| line.include?(":") }
|
|
File.write(v4, v4_lines.join)
|
|
File.write(v6, v6_lines.join)
|
|
warn "INFO> #{operator} done (v4=#{v4_lines.length} v6=#{v6_lines.length})"
|
|
|
|
# Generate IP lists for all operators sequentially
|
|
all:
|
|
#!/usr/bin/env ruby
|
|
require "yaml"
|
|
|
|
ops = YAML.load_file("operators.yaml").fetch("operators").keys.sort
|
|
ops.each do |op|
|
|
status = system("just", "gen", op)
|
|
exit($?.exitstatus || 1) unless status
|
|
end
|
|
|
|
guard:
|
|
#!/usr/bin/env ruby
|
|
{"china.txt" => 3000, "china6.txt" => 1000}.each do |f, min|
|
|
next if File.foreach("result/#{f}").count >= min
|
|
warn "#{f} too small"
|
|
exit 1
|
|
end
|
|
warn "INFO> guard checks passed"
|
|
|
|
# Summarize total IPv4/IPv6 address space per operator
|
|
stat:
|
|
#!/usr/bin/env ruby
|
|
require "yaml"
|
|
|
|
dir = "result"
|
|
files = Dir.exist?(dir) ? Dir.glob("#{dir}/*.txt").sort : []
|
|
files.reject! { |p| p.end_with?("46.txt") }
|
|
abort("result/*.txt files missing") if files.empty?
|
|
|
|
report = files.map do |p|
|
|
base = p.end_with?("6.txt") ? 48 : 32
|
|
total = File.foreach(p).sum do |line|
|
|
match = %r{/(\d+)}.match(line)
|
|
next 0 unless match
|
|
prefix_len = match[1].to_i
|
|
prefix_len <= base ? (1 << (base - prefix_len)) : 0
|
|
end
|
|
"#{File.basename(p, ".txt")}\n#{total}"
|
|
end.join("\n\n") + "\n"
|
|
|
|
print report
|
|
File.write("#{dir}/stat", report)
|
|
|
|
# Publish generated results into the ip-lists branch
|
|
upload: guard
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
rm -f ip-lists/{.,}*.txt
|
|
mv result/{*,.*.txt} ip-lists
|
|
cd ip-lists
|
|
tree -H . -P "*.txt|stat" -T "China Operator IP - prebuild results" > index.html
|
|
git config user.name "GitHub Actions"
|
|
git config user.email noreply@github.com
|
|
git add .
|
|
git commit -m "update $(date +%Y-%m-%d)"
|
|
git push -q
|
|
|
|
# Refresh CDN cache for all files in ip-lists directory
|
|
refresh_jsdelivr repository:
|
|
#!/usr/bin/env ruby
|
|
require "net/http"
|
|
|
|
dir = "ip-lists"
|
|
abort("#{dir} directory not found") unless Dir.exist?(dir)
|
|
|
|
Dir.children(dir).sort.each do |file|
|
|
warn "INFO> purging CDN cache for #{file}"
|
|
puts Net::HTTP.get_response(URI("https://purge.jsdelivr.net/gh/{{repository}}@#{dir}/#{file}")).inspect
|
|
end
|