Files
china-operator-ip/justfile
T

304 lines
10 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"
aria2c -s 4 -x 4 -q -o "${outfile}" --allow-overwrite=true "{{url}}"
echo "INFO> ${outfile} updated ($(wc -l < "${outfile}") records)" >&2
# 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 }
# 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
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