mirror of
https://github.com/oneclickvirt/backtrace.git
synced 2026-07-23 11:20:13 +08:00
Compare commits
No commits in common. "main" and "v0.0.11-20260720103959" have entirely different histories.
main
...
v0.0.11-20
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@ -3,18 +3,11 @@ name: 创建IPv6检测的前缀
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
concurrency:
|
||||
group: backtrace-ipv6-prefix-sync
|
||||
cancel-in-progress: false
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
fetch-ipv6-prefixes:
|
||||
runs-on: ubuntu-latest
|
||||
@ -24,20 +17,32 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 设置 Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: 获取并处理多个ASN的IPv6前缀
|
||||
run: |
|
||||
go run ./cmd/update-prefixes -output-dir bk/prefix
|
||||
mkdir -p bk/prefix/
|
||||
for asn in AS4809 AS4134 AS9929 AS4837 AS58807 AS9808 AS58453 AS23764; do
|
||||
echo "处理 $asn..."
|
||||
curl -s -A "Mozilla/5.0" \
|
||||
"https://bgp.he.net/$asn" > "${asn}.html"
|
||||
grep -oE '[0-9a-f:]+::/[0-9]+' "${asn}.html" | sort -u > tmp_prefixes.txt
|
||||
{
|
||||
while read prefix; do
|
||||
ip_part=$(echo "$prefix" | cut -d/ -f1)
|
||||
prefix_len=$(echo "$prefix" | cut -d/ -f2)
|
||||
keep_segments=$((prefix_len / 16))
|
||||
segments=$(echo "$ip_part" | tr ':' '\n' | grep -v '^$')
|
||||
kept=$(echo "$segments" | head -n "$keep_segments" | tr '\n' ':' | sed 's/:$//')
|
||||
echo "$kept"
|
||||
done < tmp_prefixes.txt
|
||||
} | sort -u > "bk/prefix/${asn,,}.txt"
|
||||
rm -f "${asn}.html" tmp_prefixes.txt
|
||||
done
|
||||
|
||||
- name: 提交更新到仓库
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add bk/prefix/*.txt bk/prefix/*.txt.manifest.json
|
||||
git add bk/prefix/*.txt
|
||||
if git diff --cached --quiet; then
|
||||
echo "无变更,跳过提交。"
|
||||
else
|
||||
|
||||
51
.github/workflows/sync-asn-metadata.yml
vendored
51
.github/workflows/sync-asn-metadata.yml
vendored
@ -1,51 +0,0 @@
|
||||
name: Sync ASN metadata
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "29 3 * * *"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/sync-asn-metadata.yml"
|
||||
- "cmd/update-asn-metadata/**"
|
||||
- "bgptools/data/**"
|
||||
- "bgptools/asn_metadata.go"
|
||||
- "bgptools/asn_metadata_test.go"
|
||||
- "model/model.go"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
|
||||
concurrency:
|
||||
group: backtrace-asn-metadata-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Update snapshot
|
||||
run: go run ./cmd/update-asn-metadata
|
||||
- name: Validate updater and runtime loader
|
||||
run: go test -race ./cmd/update-asn-metadata ./bgptools
|
||||
- name: Vet updater and runtime loader
|
||||
run: go vet ./cmd/update-asn-metadata ./bgptools
|
||||
- name: Commit semantic changes
|
||||
run: |
|
||||
if git diff --quiet -- bgptools/data/bgp-asn-map.json bgptools/data/bgp-asn-map.manifest.json; then
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add bgptools/data/bgp-asn-map.json bgptools/data/bgp-asn-map.manifest.json
|
||||
git commit -m "chore(data): update ASN metadata"
|
||||
git push
|
||||
@ -1,229 +0,0 @@
|
||||
package bgptools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ASNMetadataSchema = "backtrace.asn-metadata/v1"
|
||||
ASNMetadataManifestSchema = "backtrace.asn-metadata-manifest/v1"
|
||||
ASNMetadataMinimum = 50
|
||||
)
|
||||
|
||||
var asnMetadataSnapshotURLs = []string{
|
||||
"https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/backtrace/main/bgptools/data/bgp-asn-map.json",
|
||||
"https://raw.githubusercontent.com/oneclickvirt/backtrace/main/bgptools/data/bgp-asn-map.json",
|
||||
}
|
||||
|
||||
//go:embed data/bgp-asn-map.json
|
||||
var embeddedASNMetadata []byte
|
||||
|
||||
//go:embed data/bgp-asn-map.manifest.json
|
||||
var embeddedASNMetadataManifest []byte
|
||||
|
||||
type ASNMetadata struct {
|
||||
ASN uint32 `json:"asn"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ASNMetadataSource struct {
|
||||
Schema string `json:"schema"`
|
||||
Count int `json:"count"`
|
||||
GeneratedAt time.Time `json:"generated_at,omitempty"`
|
||||
Source string `json:"source"`
|
||||
Fallback bool `json:"fallback"`
|
||||
}
|
||||
|
||||
type asnMetadataDocument struct {
|
||||
Schema string `json:"schema"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Entries []ASNMetadata `json:"entries"`
|
||||
}
|
||||
|
||||
type asnMetadataManifest struct {
|
||||
Schema string `json:"schema"`
|
||||
File string `json:"file"`
|
||||
Count int `json:"count"`
|
||||
SHA256 string `json:"sha256"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
// LoadASNMetadata prefers the component's validated remote snapshot and
|
||||
// falls back to the compile-time snapshot. Upstream registry URLs and parsing
|
||||
// rules are intentionally not exposed to callers.
|
||||
func LoadASNMetadata(ctx context.Context, client *http.Client) ([]ASNMetadata, ASNMetadataSource, error) {
|
||||
return loadASNMetadata(ctx, client, asnMetadataSnapshotURLs, embeddedASNMetadata, embeddedASNMetadataManifest, ASNMetadataMinimum)
|
||||
}
|
||||
|
||||
// EmbeddedASNMetadata returns the validated compile-time snapshot without
|
||||
// performing network access.
|
||||
func EmbeddedASNMetadata() ([]ASNMetadata, ASNMetadataSource, error) {
|
||||
entries, generatedAt, err := parseASNMetadataDocument(embeddedASNMetadata, ASNMetadataMinimum)
|
||||
if err != nil {
|
||||
return nil, ASNMetadataSource{}, err
|
||||
}
|
||||
if err := validateASNMetadataManifest(embeddedASNMetadataManifest, embeddedASNMetadata, len(entries), generatedAt); err != nil {
|
||||
return nil, ASNMetadataSource{}, fmt.Errorf("validate embedded ASN metadata manifest: %w", err)
|
||||
}
|
||||
return entries, ASNMetadataSource{Schema: ASNMetadataSchema, Count: len(entries), GeneratedAt: generatedAt, Source: "embedded", Fallback: true}, nil
|
||||
}
|
||||
|
||||
// LookupASNMetadata resolves one ASN from the current component registry.
|
||||
func LookupASNMetadata(ctx context.Context, client *http.Client, asn string) (ASNMetadata, ASNMetadataSource, bool, error) {
|
||||
normalized, err := normalizeASN(asn)
|
||||
if err != nil {
|
||||
return ASNMetadata{}, ASNMetadataSource{}, false, err
|
||||
}
|
||||
number, _ := strconv.ParseUint(normalized, 10, 32)
|
||||
entries, source, err := LoadASNMetadata(ctx, client)
|
||||
if err != nil {
|
||||
return ASNMetadata{}, ASNMetadataSource{}, false, err
|
||||
}
|
||||
index := sort.Search(len(entries), func(index int) bool { return entries[index].ASN >= uint32(number) })
|
||||
if index >= len(entries) || entries[index].ASN != uint32(number) {
|
||||
return ASNMetadata{}, source, false, nil
|
||||
}
|
||||
return entries[index], source, true, nil
|
||||
}
|
||||
|
||||
func loadASNMetadata(ctx context.Context, client *http.Client, urls []string, embedded, embeddedManifest []byte, minimum int) ([]ASNMetadata, ASNMetadataSource, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
embeddedEntries, embeddedAt, err := parseASNMetadataDocument(embedded, minimum)
|
||||
if err != nil {
|
||||
return nil, ASNMetadataSource{}, fmt.Errorf("invalid embedded ASN metadata: %w", err)
|
||||
}
|
||||
if err := validateASNMetadataManifest(embeddedManifest, embedded, len(embeddedEntries), embeddedAt); err != nil {
|
||||
return nil, ASNMetadataSource{}, fmt.Errorf("invalid embedded ASN metadata manifest: %w", err)
|
||||
}
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 6 * time.Second}
|
||||
}
|
||||
minimumRemote := max(minimum, len(embeddedEntries)*65/100)
|
||||
for index, snapshotURL := range urls {
|
||||
manifest, fetchErr := fetchASNMetadata(ctx, client, asnMetadataManifestURL(snapshotURL))
|
||||
if fetchErr != nil {
|
||||
continue
|
||||
}
|
||||
data, fetchErr := fetchASNMetadata(ctx, client, snapshotURL)
|
||||
if fetchErr != nil {
|
||||
continue
|
||||
}
|
||||
entries, generatedAt, parseErr := parseASNMetadataDocument(data, minimumRemote)
|
||||
if parseErr == nil && validateASNMetadataManifest(manifest, data, len(entries), generatedAt) == nil {
|
||||
return entries, ASNMetadataSource{Schema: ASNMetadataSchema, Count: len(entries), GeneratedAt: generatedAt, Source: metadataRemoteSource(index), Fallback: index > 0}, nil
|
||||
}
|
||||
}
|
||||
return embeddedEntries, ASNMetadataSource{Schema: ASNMetadataSchema, Count: len(embeddedEntries), GeneratedAt: embeddedAt, Source: "embedded", Fallback: true}, nil
|
||||
}
|
||||
|
||||
func metadataRemoteSource(index int) string {
|
||||
if index == 0 {
|
||||
return "cdn"
|
||||
}
|
||||
if index == 1 {
|
||||
return "raw"
|
||||
}
|
||||
return "remote"
|
||||
}
|
||||
|
||||
func asnMetadataManifestURL(snapshotURL string) string {
|
||||
return strings.TrimSuffix(snapshotURL, ".json") + ".manifest.json"
|
||||
}
|
||||
|
||||
func validateASNMetadataManifest(data, snapshot []byte, count int, generatedAt time.Time) error {
|
||||
var manifest asnMetadataManifest
|
||||
decoder := json.NewDecoder(strings.NewReader(string(data)))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&manifest); err != nil {
|
||||
return fmt.Errorf("decode manifest: %w", err)
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
return errors.New("manifest contains trailing JSON")
|
||||
}
|
||||
if manifest.Schema != ASNMetadataManifestSchema || manifest.File != "bgp-asn-map.json" || manifest.Count != count || manifest.GeneratedAt.IsZero() || !manifest.GeneratedAt.Equal(generatedAt) {
|
||||
return errors.New("manifest schema, file, count, or generated_at is invalid")
|
||||
}
|
||||
hash := sha256.Sum256(snapshot)
|
||||
if !strings.EqualFold(manifest.SHA256, hex.EncodeToString(hash[:])) {
|
||||
return errors.New("manifest SHA-256 does not match snapshot")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchASNMetadata(ctx context.Context, client *http.Client, snapshotURL string) ([]byte, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, snapshotURL, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("create ASN metadata request failed")
|
||||
}
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("User-Agent", "oneclickvirt-backtrace-asn-metadata/1")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return nil, ctxErr
|
||||
}
|
||||
return nil, errors.New("ASN metadata request failed")
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d", response.StatusCode)
|
||||
}
|
||||
const maximumSize = 4 << 20
|
||||
data, err := io.ReadAll(io.LimitReader(response.Body, maximumSize+1))
|
||||
if err != nil {
|
||||
return nil, errors.New("ASN metadata response read failed")
|
||||
}
|
||||
if len(data) > maximumSize {
|
||||
return nil, fmt.Errorf("ASN metadata exceeds %d bytes", maximumSize)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func parseASNMetadataDocument(data []byte, minimum int) ([]ASNMetadata, time.Time, error) {
|
||||
var document asnMetadataDocument
|
||||
decoder := json.NewDecoder(strings.NewReader(string(data)))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&document); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
return nil, time.Time{}, errors.New("ASN metadata contains trailing JSON")
|
||||
}
|
||||
if document.Schema != ASNMetadataSchema || document.GeneratedAt.IsZero() {
|
||||
return nil, time.Time{}, errors.New("ASN metadata schema or generated_at is invalid")
|
||||
}
|
||||
seen := make(map[uint32]struct{}, len(document.Entries))
|
||||
entries := make([]ASNMetadata, 0, len(document.Entries))
|
||||
for _, entry := range document.Entries {
|
||||
entry.Name = strings.TrimSpace(entry.Name)
|
||||
if entry.ASN == 0 || entry.Name == "" {
|
||||
return nil, time.Time{}, errors.New("ASN metadata contains an invalid entry")
|
||||
}
|
||||
if _, exists := seen[entry.ASN]; exists {
|
||||
return nil, time.Time{}, fmt.Errorf("ASN metadata contains duplicate AS%d", entry.ASN)
|
||||
}
|
||||
seen[entry.ASN] = struct{}{}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
if len(entries) < minimum {
|
||||
return nil, time.Time{}, fmt.Errorf("ASN metadata count %d is below minimum %d", len(entries), minimum)
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].ASN < entries[j].ASN })
|
||||
return entries, document.GeneratedAt, nil
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
package bgptools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadASNMetadataPrefersValidatedRemote(t *testing.T) {
|
||||
payload := `{"schema":"backtrace.asn-metadata/v1","generated_at":"2026-07-20T00:00:00Z","entries":[{"asn":2,"name":"Two"},{"asn":1,"name":"One"}]}`
|
||||
manifest := manifestFor([]byte(payload), "bgp-asn-map.json", 2)
|
||||
if err := validateASNMetadataManifest(manifest, []byte(payload), 2, time.Date(2026, 7, 20, 0, 0, 0, 0, time.UTC)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
if strings.HasSuffix(request.URL.Path, ".manifest.json") {
|
||||
_, _ = w.Write(manifest)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(payload))
|
||||
}))
|
||||
defer server.Close()
|
||||
entries, source, err := loadASNMetadata(context.Background(), server.Client(), []string{server.URL + "/bgp-asn-map.json"}, []byte(payload), manifest, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if source.Source != "cdn" || source.Fallback || source.Count != 2 || entries[0].ASN != 1 {
|
||||
t.Fatalf("unexpected result: %#v %#v", entries, source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadASNMetadataFallsBackOnSchemaFailure(t *testing.T) {
|
||||
embedded := `{"schema":"backtrace.asn-metadata/v1","generated_at":"2026-07-20T00:00:00Z","entries":[{"asn":1,"name":"One"}]}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"schema":"wrong"}`)) }))
|
||||
defer server.Close()
|
||||
manifest := manifestFor([]byte(embedded), "bgp-asn-map.json", 1)
|
||||
entries, source, err := loadASNMetadata(context.Background(), server.Client(), []string{server.URL + "/bgp-asn-map.json"}, []byte(embedded), manifest, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if source.Source != "embedded" || !source.Fallback || len(entries) != 1 {
|
||||
t.Fatalf("unexpected fallback: %#v %#v", entries, source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadASNMetadataFallsBackFromCDNToRaw(t *testing.T) {
|
||||
payload := []byte(`{"schema":"backtrace.asn-metadata/v1","generated_at":"2026-07-20T00:00:00Z","entries":[{"asn":1,"name":"One"}]}`)
|
||||
validManifest := manifestFor(payload, "bgp-asn-map.json", 1)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path == "/cdn/bgp-asn-map.manifest.json" {
|
||||
_, _ = writer.Write([]byte(`{"schema":"backtrace.asn-metadata-manifest/v1","file":"bgp-asn-map.json","count":1,"sha256":"bad","generated_at":"2026-07-20T00:00:00Z"}`))
|
||||
return
|
||||
}
|
||||
if request.URL.Path == "/raw/bgp-asn-map.manifest.json" {
|
||||
_, _ = writer.Write(validManifest)
|
||||
return
|
||||
}
|
||||
_, _ = writer.Write(payload)
|
||||
}))
|
||||
defer server.Close()
|
||||
entries, source, err := loadASNMetadata(context.Background(), server.Client(), []string{server.URL + "/cdn/bgp-asn-map.json", server.URL + "/raw/bgp-asn-map.json"}, payload, validManifest, 1)
|
||||
if err != nil || len(entries) != 1 || source.Source != "raw" || !source.Fallback {
|
||||
t.Fatalf("unexpected raw fallback: entries=%#v source=%#v err=%v", entries, source, err)
|
||||
}
|
||||
}
|
||||
|
||||
func manifestFor(snapshot []byte, file string, count int) []byte {
|
||||
hash := sha256.Sum256(snapshot)
|
||||
return []byte(fmt.Sprintf(`{"schema":"backtrace.asn-metadata-manifest/v1","file":"%s","count":%d,"sha256":"%s","generated_at":"2026-07-20T00:00:00Z"}`, file, count, hex.EncodeToString(hash[:])))
|
||||
}
|
||||
|
||||
func TestParseASNMetadataRejectsDuplicateAndDrop(t *testing.T) {
|
||||
payload := []byte(`{"schema":"backtrace.asn-metadata/v1","generated_at":"2026-07-20T00:00:00Z","entries":[{"asn":1,"name":"One"},{"asn":1,"name":"Again"}]}`)
|
||||
if _, _, err := parseASNMetadataDocument(payload, 1); err == nil {
|
||||
t.Fatal("duplicate ASN unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
@ -1,342 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-metadata/v1",
|
||||
"generated_at": "2026-07-21T15:33:49.459211Z",
|
||||
"entries": [
|
||||
{
|
||||
"asn": 174,
|
||||
"name": "Cogent"
|
||||
},
|
||||
{
|
||||
"asn": 701,
|
||||
"name": "Verizon"
|
||||
},
|
||||
{
|
||||
"asn": 702,
|
||||
"name": "Verizon"
|
||||
},
|
||||
{
|
||||
"asn": 1239,
|
||||
"name": "Sprint"
|
||||
},
|
||||
{
|
||||
"asn": 1273,
|
||||
"name": "Vodafone"
|
||||
},
|
||||
{
|
||||
"asn": 1299,
|
||||
"name": "Arelion"
|
||||
},
|
||||
{
|
||||
"asn": 1729,
|
||||
"name": "Telia"
|
||||
},
|
||||
{
|
||||
"asn": 2497,
|
||||
"name": "IIJ"
|
||||
},
|
||||
{
|
||||
"asn": 2516,
|
||||
"name": "KDDI"
|
||||
},
|
||||
{
|
||||
"asn": 2914,
|
||||
"name": "NTT"
|
||||
},
|
||||
{
|
||||
"asn": 3209,
|
||||
"name": "Vodafone"
|
||||
},
|
||||
{
|
||||
"asn": 3257,
|
||||
"name": "GTT"
|
||||
},
|
||||
{
|
||||
"asn": 3258,
|
||||
"name": "xTom"
|
||||
},
|
||||
{
|
||||
"asn": 3301,
|
||||
"name": "Telia"
|
||||
},
|
||||
{
|
||||
"asn": 3308,
|
||||
"name": "Telia"
|
||||
},
|
||||
{
|
||||
"asn": 3320,
|
||||
"name": "DTAG"
|
||||
},
|
||||
{
|
||||
"asn": 3356,
|
||||
"name": "Lumen"
|
||||
},
|
||||
{
|
||||
"asn": 3462,
|
||||
"name": "Hinet"
|
||||
},
|
||||
{
|
||||
"asn": 3491,
|
||||
"name": "PCCW"
|
||||
},
|
||||
{
|
||||
"asn": 3786,
|
||||
"name": "LG"
|
||||
},
|
||||
{
|
||||
"asn": 4445,
|
||||
"name": "Vodafone"
|
||||
},
|
||||
{
|
||||
"asn": 4609,
|
||||
"name": "CTM"
|
||||
},
|
||||
{
|
||||
"asn": 4637,
|
||||
"name": "Telstra"
|
||||
},
|
||||
{
|
||||
"asn": 4657,
|
||||
"name": "StarHub"
|
||||
},
|
||||
{
|
||||
"asn": 4766,
|
||||
"name": "KT"
|
||||
},
|
||||
{
|
||||
"asn": 4788,
|
||||
"name": "TMNet"
|
||||
},
|
||||
{
|
||||
"asn": 5378,
|
||||
"name": "Vodafone"
|
||||
},
|
||||
{
|
||||
"asn": 5511,
|
||||
"name": "Orange"
|
||||
},
|
||||
{
|
||||
"asn": 6233,
|
||||
"name": "xTom"
|
||||
},
|
||||
{
|
||||
"asn": 6453,
|
||||
"name": "TATA"
|
||||
},
|
||||
{
|
||||
"asn": 6461,
|
||||
"name": "Zayo"
|
||||
},
|
||||
{
|
||||
"asn": 6762,
|
||||
"name": "Sparkle"
|
||||
},
|
||||
{
|
||||
"asn": 6830,
|
||||
"name": "Liberty"
|
||||
},
|
||||
{
|
||||
"asn": 6939,
|
||||
"name": "HE"
|
||||
},
|
||||
{
|
||||
"asn": 7018,
|
||||
"name": "AT\u0026T"
|
||||
},
|
||||
{
|
||||
"asn": 7473,
|
||||
"name": "SingTel"
|
||||
},
|
||||
{
|
||||
"asn": 7922,
|
||||
"name": "Comcast"
|
||||
},
|
||||
{
|
||||
"asn": 7979,
|
||||
"name": "Servers"
|
||||
},
|
||||
{
|
||||
"asn": 8075,
|
||||
"name": "MS"
|
||||
},
|
||||
{
|
||||
"asn": 8888,
|
||||
"name": "xTom"
|
||||
},
|
||||
{
|
||||
"asn": 9002,
|
||||
"name": "RETN"
|
||||
},
|
||||
{
|
||||
"asn": 9269,
|
||||
"name": "HKBN"
|
||||
},
|
||||
{
|
||||
"asn": 9304,
|
||||
"name": "HGC"
|
||||
},
|
||||
{
|
||||
"asn": 9505,
|
||||
"name": "CHT"
|
||||
},
|
||||
{
|
||||
"asn": 9644,
|
||||
"name": "SK"
|
||||
},
|
||||
{
|
||||
"asn": 9886,
|
||||
"name": "CTCSCI"
|
||||
},
|
||||
{
|
||||
"asn": 9924,
|
||||
"name": "TFN"
|
||||
},
|
||||
{
|
||||
"asn": 12389,
|
||||
"name": "RT_RU"
|
||||
},
|
||||
{
|
||||
"asn": 12876,
|
||||
"name": "Scaleway"
|
||||
},
|
||||
{
|
||||
"asn": 12956,
|
||||
"name": "Telxius"
|
||||
},
|
||||
{
|
||||
"asn": 13335,
|
||||
"name": "CF"
|
||||
},
|
||||
{
|
||||
"asn": 15169,
|
||||
"name": "Google"
|
||||
},
|
||||
{
|
||||
"asn": 15802,
|
||||
"name": "DU"
|
||||
},
|
||||
{
|
||||
"asn": 16276,
|
||||
"name": "OVH"
|
||||
},
|
||||
{
|
||||
"asn": 16509,
|
||||
"name": "Amazon"
|
||||
},
|
||||
{
|
||||
"asn": 17676,
|
||||
"name": "Softbank"
|
||||
},
|
||||
{
|
||||
"asn": 18403,
|
||||
"name": "FPT"
|
||||
},
|
||||
{
|
||||
"asn": 20473,
|
||||
"name": "Vultr"
|
||||
},
|
||||
{
|
||||
"asn": 20485,
|
||||
"name": "TTK_RU"
|
||||
},
|
||||
{
|
||||
"asn": 23961,
|
||||
"name": "Misaka"
|
||||
},
|
||||
{
|
||||
"asn": 24940,
|
||||
"name": "Hetzner"
|
||||
},
|
||||
{
|
||||
"asn": 25820,
|
||||
"name": "IT7"
|
||||
},
|
||||
{
|
||||
"asn": 30844,
|
||||
"name": "Liquid"
|
||||
},
|
||||
{
|
||||
"asn": 31898,
|
||||
"name": "Oracle"
|
||||
},
|
||||
{
|
||||
"asn": 37700,
|
||||
"name": "JINX"
|
||||
},
|
||||
{
|
||||
"asn": 38731,
|
||||
"name": "CHT"
|
||||
},
|
||||
{
|
||||
"asn": 45102,
|
||||
"name": "Alibaba"
|
||||
},
|
||||
{
|
||||
"asn": 45899,
|
||||
"name": "VNPT"
|
||||
},
|
||||
{
|
||||
"asn": 49304,
|
||||
"name": "SAKURA"
|
||||
},
|
||||
{
|
||||
"asn": 51847,
|
||||
"name": "Nearoute"
|
||||
},
|
||||
{
|
||||
"asn": 58652,
|
||||
"name": "Level3"
|
||||
},
|
||||
{
|
||||
"asn": 60068,
|
||||
"name": "Datacamp"
|
||||
},
|
||||
{
|
||||
"asn": 63150,
|
||||
"name": "Bage"
|
||||
},
|
||||
{
|
||||
"asn": 132203,
|
||||
"name": "Tencent"
|
||||
},
|
||||
{
|
||||
"asn": 133613,
|
||||
"name": "MTel"
|
||||
},
|
||||
{
|
||||
"asn": 136907,
|
||||
"name": "Huawei"
|
||||
},
|
||||
{
|
||||
"asn": 137409,
|
||||
"name": "GSL"
|
||||
},
|
||||
{
|
||||
"asn": 140096,
|
||||
"name": "JINX"
|
||||
},
|
||||
{
|
||||
"asn": 152672,
|
||||
"name": "Aiyun"
|
||||
},
|
||||
{
|
||||
"asn": 202662,
|
||||
"name": "Hytron"
|
||||
},
|
||||
{
|
||||
"asn": 209242,
|
||||
"name": "CF"
|
||||
},
|
||||
{
|
||||
"asn": 211392,
|
||||
"name": "Dream"
|
||||
},
|
||||
{
|
||||
"asn": 214996,
|
||||
"name": "netcup"
|
||||
},
|
||||
{
|
||||
"asn": 216382,
|
||||
"name": "Layer"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-metadata-manifest/v1",
|
||||
"file": "bgp-asn-map.json",
|
||||
"count": 84,
|
||||
"sha256": "f681dd78d55c27d4615d2e705de9bde24d111e8897d37a489b91e263f27f1a66",
|
||||
"generated_at": "2026-07-21T15:33:49.459211Z"
|
||||
}
|
||||
@ -53,22 +53,19 @@ func ResolveOriginASNWithConfig(parent context.Context, ip string, config Origin
|
||||
}
|
||||
requestURL, err := addQuery(config.BaseURL, "resource", parsed.String())
|
||||
if err != nil {
|
||||
return "", errors.New("invalid origin ASN source")
|
||||
return "", err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parent, config.Timeout)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return "", errors.New("create origin ASN request failed")
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "oneclickvirt-backtrace-origin-asn/1")
|
||||
response, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return "", ctxErr
|
||||
}
|
||||
return "", errors.New("origin ASN request failed")
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == http.StatusTooManyRequests {
|
||||
@ -79,7 +76,7 @@ func ResolveOriginASNWithConfig(parent context.Context, ip string, config Origin
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(response.Body, config.MaxResponseSize+1))
|
||||
if err != nil {
|
||||
return "", errors.New("origin ASN response read failed")
|
||||
return "", err
|
||||
}
|
||||
if int64(len(body)) > config.MaxResponseSize {
|
||||
return "", fmt.Errorf("origin ASN response exceeds %d bytes", config.MaxResponseSize)
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
package bgptools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type privacyRoundTripper func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn privacyRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return fn(request)
|
||||
}
|
||||
|
||||
func TestStructuredReportErrorsDoNotExposeRemoteURLs(t *testing.T) {
|
||||
var rdap *httptest.Server
|
||||
rdap = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, `{"handle":"TEST","links":[{"rel":"geofeed","href":"https://private.example/geofeed?token=secret"}]}`)
|
||||
}))
|
||||
defer rdap.Close()
|
||||
failing := &http.Client{Transport: privacyRoundTripper(func(request *http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("dial " + request.URL.String())
|
||||
})}
|
||||
report, err := QueryIPBGPReport(context.Background(), "192.0.2.1", IPBGPReportConfig{
|
||||
RDAPClient: rdap.Client(),
|
||||
RDAPBaseURL: rdap.URL,
|
||||
FetchGeofeed: true,
|
||||
GeofeedClient: failing,
|
||||
ResolveASN: func(context.Context, string) (string, error) {
|
||||
return "", errors.New("resolver https://private.example/asn?key=secret")
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, source := range report.Sources {
|
||||
for _, field := range []string{source.Source, source.Error} {
|
||||
for _, forbidden := range []string{"private.example", "token=", "key=", "secret"} {
|
||||
if strings.Contains(field, forbidden) {
|
||||
t.Fatalf("structured source leaked %q: %+v", forbidden, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(report.Geofeeds) != 1 || report.Geofeeds[0].Error != "request_failed" {
|
||||
t.Fatalf("geofeed error was not stabilized: %+v", report.Geofeeds)
|
||||
}
|
||||
encoded, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, forbidden := range []string{"token=", "key=", "secret", "userinfo@"} {
|
||||
if strings.Contains(string(encoded), forbidden) {
|
||||
t.Fatalf("structured report leaked %q: %s", forbidden, encoded)
|
||||
}
|
||||
}
|
||||
if report.RDAP == nil || len(report.RDAP.GeofeedURLs) != 1 || report.RDAP.GeofeedURLs[0] != "https://private.example/geofeed" || report.Geofeeds[0].URL != "https://private.example/geofeed" {
|
||||
t.Fatalf("geofeed URL was not sanitized consistently: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteFetchErrorsDoNotExposeSourceURL(t *testing.T) {
|
||||
client := &http.Client{Transport: privacyRoundTripper(func(request *http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("dial " + request.URL.String())
|
||||
})}
|
||||
tests := []struct {
|
||||
name string
|
||||
run func() error
|
||||
}{
|
||||
{name: "ASN metadata", run: func() error {
|
||||
_, err := fetchASNMetadata(context.Background(), client, "https://private.example/asn?token=secret")
|
||||
return err
|
||||
}},
|
||||
{name: "RDAP", run: func() error {
|
||||
_, err := QueryRDAP(context.Background(), "192.0.2.1", client, "https://private.example/rdap?token=secret")
|
||||
return err
|
||||
}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.run()
|
||||
if err == nil {
|
||||
t.Fatal("expected request failure")
|
||||
}
|
||||
for _, forbidden := range []string{"private.example", "token=", "secret"} {
|
||||
if strings.Contains(err.Error(), forbidden) {
|
||||
t.Fatalf("error leaked %q: %v", forbidden, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -60,16 +60,13 @@ func QueryRDAP(ctx context.Context, ip string, client *http.Client, baseURL stri
|
||||
requestURL := strings.TrimRight(baseURL, "/") + "/" + parsed.String()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return RDAPRecord{}, errors.New("create RDAP request failed")
|
||||
return RDAPRecord{}, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/rdap+json, application/json")
|
||||
req.Header.Set("User-Agent", "oneclickvirt-backtrace-rdap/1")
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return RDAPRecord{}, ctxErr
|
||||
}
|
||||
return RDAPRecord{}, errors.New("RDAP request failed")
|
||||
return RDAPRecord{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == http.StatusTooManyRequests {
|
||||
@ -80,7 +77,7 @@ func QueryRDAP(ctx context.Context, ip string, client *http.Client, baseURL stri
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(response.Body, (4<<20)+1))
|
||||
if err != nil {
|
||||
return RDAPRecord{}, errors.New("RDAP response read failed")
|
||||
return RDAPRecord{}, err
|
||||
}
|
||||
if len(data) > 4<<20 {
|
||||
return RDAPRecord{}, ErrRDAPResponseTooLarge
|
||||
@ -128,7 +125,7 @@ func parseRDAPRecord(ip, source string, data []byte) (RDAPRecord, error) {
|
||||
record := RDAPRecord{
|
||||
IP: ip, Handle: payload.Handle, Name: payload.Name, Type: payload.Type,
|
||||
Country: payload.Country, StartAddress: payload.StartAddress, EndAddress: payload.EndAddress,
|
||||
ParentHandle: payload.ParentHandle, Port43: payload.Port43, Status: payload.Status, Source: "rdap",
|
||||
ParentHandle: payload.ParentHandle, Port43: payload.Port43, Status: payload.Status, Source: source,
|
||||
}
|
||||
for _, event := range payload.Events {
|
||||
switch strings.ToLower(event.Action) {
|
||||
|
||||
@ -131,7 +131,7 @@ func QueryASNRelationships(ctx context.Context, asn string, cfg RelationshipConf
|
||||
report := &ASNRelationshipReport{TargetASN: normalized}
|
||||
ripeURL, err := addQuery(cfg.RIPEstatURL, "resource", normalized)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid relationship source configuration")
|
||||
return nil, err
|
||||
}
|
||||
ripeRaw, ripeStatus, ripeErr := fetchRelationshipJSON(ctx, cfg, "ripestat", ripeURL)
|
||||
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("ripestat", ripeStatus, ripeErr))
|
||||
@ -153,7 +153,7 @@ func QueryASNRelationships(ctx context.Context, asn string, cfg RelationshipConf
|
||||
|
||||
peeringURL, err := addQuery(cfg.PeeringDBURL, "asn", normalized)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid relationship source configuration")
|
||||
return nil, err
|
||||
}
|
||||
peeringRaw, peeringStatus, peeringErr := fetchRelationshipJSON(ctx, cfg, "peeringdb", peeringURL)
|
||||
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("peeringdb", peeringStatus, peeringErr))
|
||||
@ -255,16 +255,7 @@ func sourceStatus(source string, status RelationshipStatus, err error) Relations
|
||||
Timeout: status == RelationshipTimeout,
|
||||
}
|
||||
if err != nil {
|
||||
switch status {
|
||||
case RelationshipRateLimited:
|
||||
item.Error = "rate_limited"
|
||||
case RelationshipTimeout:
|
||||
item.Error = "timeout"
|
||||
case RelationshipMissingFields:
|
||||
item.Error = "missing_fields"
|
||||
default:
|
||||
item.Error = "request_failed"
|
||||
}
|
||||
item.Error = err.Error()
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
@ -217,12 +217,6 @@ func QueryIPBGPReport(parent context.Context, ip string, cfg IPBGPReportConfig)
|
||||
geofeedURLs = append(geofeedURLs, report.WHOIS.GeofeedURLs...)
|
||||
}
|
||||
geofeedURLs = uniqueSortedStrings(geofeedURLs)
|
||||
if report.RDAP != nil {
|
||||
report.RDAP.GeofeedURLs = sanitizeReportURLs(report.RDAP.GeofeedURLs)
|
||||
}
|
||||
if report.WHOIS != nil {
|
||||
report.WHOIS.GeofeedURLs = sanitizeReportURLs(report.WHOIS.GeofeedURLs)
|
||||
}
|
||||
for _, geofeedURL := range geofeedURLs {
|
||||
if !cfg.FetchGeofeed {
|
||||
report.Geofeeds = append(report.Geofeeds, GeofeedResult{URL: geofeedURL, Status: ReportUnsupported, Error: "fetch disabled"})
|
||||
@ -230,7 +224,7 @@ func QueryIPBGPReport(parent context.Context, ip string, cfg IPBGPReportConfig)
|
||||
}
|
||||
result := fetchGeofeed(ctx, geofeedURL, cfg)
|
||||
report.Geofeeds = append(report.Geofeeds, result)
|
||||
report.Sources = append(report.Sources, IPBGPSourceStatus{Source: "geofeed", Status: result.Status, Error: result.Error})
|
||||
report.Sources = append(report.Sources, IPBGPSourceStatus{Source: "geofeed:" + geofeedURL, Status: result.Status, Error: result.Error})
|
||||
}
|
||||
|
||||
asn := strings.TrimSpace(cfg.ASN)
|
||||
@ -292,7 +286,7 @@ func inferRIR(record RDAPRecord, available bool) RIRInfo {
|
||||
}
|
||||
|
||||
func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig) GeofeedResult {
|
||||
result := GeofeedResult{URL: sanitizeReportURL(rawURL)}
|
||||
result := GeofeedResult{URL: rawURL}
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
result.Status = ReportUnsupported
|
||||
@ -316,7 +310,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
result.Status = ReportError
|
||||
result.Error = stableReportError(result.Status, err)
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
req.Header.Set("Accept", "text/csv, text/plain, */*")
|
||||
@ -324,7 +318,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
result.Status = reportStatusForError(err)
|
||||
result.Error = stableReportError(result.Status, err)
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@ -347,7 +341,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
|
||||
if err != nil {
|
||||
result.Status = reportStatusForError(err)
|
||||
result.Error = stableReportError(result.Status, err)
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
if int64(len(body)) > maxBytes {
|
||||
@ -523,51 +517,11 @@ func rirNameFromText(value string) string {
|
||||
func sourceStatusForError(source string, status ReportStatus, err error) IPBGPSourceStatus {
|
||||
item := IPBGPSourceStatus{Source: source, Status: status}
|
||||
if err != nil {
|
||||
item.Error = stableReportError(status, err)
|
||||
item.Error = err.Error()
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func stableReportError(status ReportStatus, err error) string {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return "canceled"
|
||||
}
|
||||
switch status {
|
||||
case ReportRateLimited:
|
||||
return "rate_limited"
|
||||
case ReportTimeout:
|
||||
return "timeout"
|
||||
case ReportUnsupported:
|
||||
return "unsupported"
|
||||
case ReportMissingFields:
|
||||
return "missing_fields"
|
||||
default:
|
||||
return "request_failed"
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeReportURL(raw string) string {
|
||||
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
return ""
|
||||
}
|
||||
parsed.User = nil
|
||||
parsed.RawQuery = ""
|
||||
parsed.ForceQuery = false
|
||||
parsed.Fragment = ""
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func sanitizeReportURLs(values []string) []string {
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if sanitized := sanitizeReportURL(value); sanitized != "" {
|
||||
result = append(result, sanitized)
|
||||
}
|
||||
}
|
||||
return uniqueSortedStrings(result)
|
||||
}
|
||||
|
||||
func reportStatusForError(err error) ReportStatus {
|
||||
if err == nil {
|
||||
return ReportAvailable
|
||||
|
||||
@ -16,7 +16,6 @@ func safeTraceCall(fn func()) {
|
||||
}
|
||||
|
||||
func BackTrace(enableIpv6 bool) string {
|
||||
StartASNPrefixRefresh()
|
||||
if model.CachedIcmpData == "" || model.ParsedIcmpTargets == nil || time.Since(model.CachedIcmpDataFetchTime) > time.Hour {
|
||||
model.CachedIcmpData = getData(model.IcmpTargets)
|
||||
model.CachedIcmpDataFetchTime = time.Now()
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
package backtrace
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/netip"
|
||||
"strings"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed prefix/as4809.txt
|
||||
@ -45,20 +44,12 @@ var asnPrefixes = map[string][]string{
|
||||
// 判断 IPv6 地址是否匹配 ASN 中的某个前缀
|
||||
func ipv6Asn(ip string) string {
|
||||
ip = strings.ToLower(ip)
|
||||
for asn, prefixes := range currentASNPrefixes() {
|
||||
for asn, prefixes := range asnPrefixes {
|
||||
for _, prefix := range prefixes {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(prefix, "/") {
|
||||
address, addressErr := netip.ParseAddr(ip)
|
||||
network, networkErr := netip.ParsePrefix(prefix)
|
||||
if addressErr == nil && networkErr == nil && network.Contains(address) {
|
||||
return asn
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(ip, prefix) {
|
||||
return asn
|
||||
}
|
||||
|
||||
@ -1,28 +1,30 @@
|
||||
2400:9380:9002::/48
|
||||
2400:9380:9115::/48
|
||||
2400:9380:9116::/48
|
||||
2400:9380:9206::/48
|
||||
2400:9380:a003::/48
|
||||
2400:9380:a00a::/48
|
||||
2400:9380:a00c::/48
|
||||
2400:9380:a014::/48
|
||||
2400:9380:a016::/48
|
||||
2400:9380:a01f::/48
|
||||
2400:9380:a022::/48
|
||||
2400:9380:a028::/48
|
||||
2400:9380:a042::/48
|
||||
2400:9380:a110::/48
|
||||
240e:97d:8000::/48
|
||||
2804:1e48:9003::/48
|
||||
2a04:f580:9001::/48
|
||||
2a04:f580:9090::/48
|
||||
2a04:f581:110b::/48
|
||||
2a04:f581:a123::/48
|
||||
2a04:f581:a125::/48
|
||||
2c0f:f7a8:1::/48
|
||||
2c0f:f7a8:24::/48
|
||||
2c0f:f7a8:29::/48
|
||||
2c0f:f7a8:2::/48
|
||||
2c0f:f7a8:37::/48
|
||||
2c0f:f7a8:47::/48
|
||||
2c0f:f7a8:9011::/48
|
||||
2400:9380:9002
|
||||
2400:9380:9115
|
||||
2400:9380:9116
|
||||
2400:9380:9206
|
||||
2400:9380:9262
|
||||
2400:9380:a003
|
||||
2400:9380:a00a
|
||||
2400:9380:a00c
|
||||
2400:9380:a014
|
||||
2400:9380:a016
|
||||
2400:9380:a01f
|
||||
2400:9380:a022
|
||||
2400:9380:a026
|
||||
2400:9380:a028
|
||||
2400:9380:a042
|
||||
2400:9380:a110
|
||||
240e:97d:8000
|
||||
2804:1e48:9003
|
||||
2a04:f580:9001
|
||||
2a04:f580:9090
|
||||
2a04:f581:110b
|
||||
2a04:f581:a123
|
||||
2a04:f581:a125
|
||||
2c0f:f7a8:1
|
||||
2c0f:f7a8:2
|
||||
2c0f:f7a8:24
|
||||
2c0f:f7a8:29
|
||||
2c0f:f7a8:37
|
||||
2c0f:f7a8:47
|
||||
2c0f:f7a8:9011
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as23764.txt",
|
||||
"count": 28,
|
||||
"sha256": "d853ef48792875af80682ebfb9817bbbcfefd0f904cf057568664d3bdb9373bb",
|
||||
"generated_at": "2026-07-20T19:12:53Z"
|
||||
}
|
||||
@ -1,629 +1,256 @@
|
||||
2400:9380:8001::/48
|
||||
2400:9380:8003::/48
|
||||
2400:9380:8021::/48
|
||||
2400:9380:8040::/48
|
||||
2400:9380:8140::/48
|
||||
2400:9380:8201::/48
|
||||
2400:9380:8301::/48
|
||||
240e:100:8000::/34
|
||||
240e:100:c000::/34
|
||||
240e:101::/34
|
||||
240e:103:c000::/34
|
||||
240e:104:4000::/36
|
||||
240e:104:7000::/36
|
||||
240e:104:c000::/34
|
||||
240e:106:4000::/34
|
||||
240e:108:1100::/44
|
||||
240e:108:1110::/44
|
||||
240e:108:1120::/44
|
||||
240e:108:1130::/44
|
||||
240e:108:1140::/44
|
||||
240e:108:1150::/44
|
||||
240e:108:1160::/44
|
||||
240e:108:1180::/44
|
||||
240e:108:1190::/44
|
||||
240e:108:11a0::/44
|
||||
240e:108:11b0::/44
|
||||
240e:108:11c0::/44
|
||||
240e:108:11d0::/44
|
||||
240e:108:11e0::/44
|
||||
240e:108:1200::/44
|
||||
240e:108:1210::/44
|
||||
240e:108:12a0::/44
|
||||
240e:108:12c0::/44
|
||||
240e:108:12d0::/44
|
||||
240e:108:1300::/44
|
||||
240e:108:1310::/44
|
||||
240e:108:1320::/44
|
||||
240e:108:1330::/44
|
||||
240e:108:1340::/44
|
||||
240e:108:1350::/44
|
||||
240e:108:1360::/44
|
||||
240e:108:1370::/44
|
||||
240e:108:1380::/44
|
||||
240e:108:1390::/44
|
||||
240e:108:13a0::/44
|
||||
240e:108:13b0::/44
|
||||
240e:108:13c0::/44
|
||||
240e:108:13d0::/44
|
||||
240e:108:13e0::/44
|
||||
240e:11:8001::/48
|
||||
240e:12::/32
|
||||
240e:144:c000::/34
|
||||
240e:14::/31
|
||||
240e:16:1001::/48
|
||||
240e:16:1008::/48
|
||||
240e:184:c000::/34
|
||||
240e:1:8000::/33
|
||||
240e:1::/33
|
||||
240e:1c:112::/48
|
||||
240e:1c::/31
|
||||
240e:204::/30
|
||||
240e:208::/30
|
||||
240e:20c::/30
|
||||
240e:210::/30
|
||||
240e:214::/30
|
||||
240e:218::/32
|
||||
240e:219::/32
|
||||
240e:21a::/32
|
||||
240e:21b::/32
|
||||
240e:21c::/30
|
||||
240e:220::/30
|
||||
240e:224::/32
|
||||
240e:225::/32
|
||||
240e:226::/32
|
||||
240e:227::/32
|
||||
240e:22c::/30
|
||||
240e:230::/30
|
||||
240e:234::/30
|
||||
240e:238::/30
|
||||
240e:23c::/30
|
||||
240e:240::/32
|
||||
240e:241::/32
|
||||
240e:242::/32
|
||||
240e:243::/32
|
||||
240e:244::/32
|
||||
240e:245::/32
|
||||
240e:246::/32
|
||||
240e:247::/32
|
||||
240e:248::/30
|
||||
240e:24:8000::/34
|
||||
240e:24c::/30
|
||||
240e:250::/30
|
||||
240e:254::/30
|
||||
240e:258::/30
|
||||
240e:25c::/30
|
||||
240e:260::/30
|
||||
240e:264::/30
|
||||
240e:268::/30
|
||||
240e:26c::/30
|
||||
240e:270::/30
|
||||
240e:274::/30
|
||||
240e:278::/30
|
||||
240e:27c::/30
|
||||
240e:2::/33
|
||||
240e:2c:8000::/34
|
||||
240e:304::/30
|
||||
240e:308::/30
|
||||
240e:30c::/30
|
||||
240e:310::/30
|
||||
240e:314::/30
|
||||
240e:318::/32
|
||||
240e:319::/32
|
||||
240e:31a::/32
|
||||
240e:31b::/32
|
||||
240e:31c::/30
|
||||
240e:320::/30
|
||||
240e:324::/32
|
||||
240e:325::/32
|
||||
240e:326::/32
|
||||
240e:327::/32
|
||||
240e:32c::/30
|
||||
240e:330::/30
|
||||
240e:334::/30
|
||||
240e:338::/30
|
||||
240e:33c::/30
|
||||
240e:340::/32
|
||||
240e:341::/32
|
||||
240e:342::/32
|
||||
240e:343::/32
|
||||
240e:344::/30
|
||||
240e:348::/30
|
||||
240e:34c::/30
|
||||
240e:350:205::/48
|
||||
240e:350::/29
|
||||
240e:358::/29
|
||||
240e:360::/29
|
||||
240e:368::/29
|
||||
240e:370::/29
|
||||
240e:378::/29
|
||||
240e:380::/29
|
||||
240e:388::/29
|
||||
240e:390::/29
|
||||
240e:398::/29
|
||||
240e:3a0::/28
|
||||
240e:3b0::/28
|
||||
240e:404::/30
|
||||
240e:408::/30
|
||||
240e:414::/30
|
||||
240e:418::/32
|
||||
240e:419::/32
|
||||
240e:41:8000::/33
|
||||
240e:41::/33
|
||||
240e:41a::/32
|
||||
240e:41b::/32
|
||||
240e:41c::/30
|
||||
240e:420::/30
|
||||
240e:424::/32
|
||||
240e:425::/32
|
||||
240e:426::/32
|
||||
240e:427::/32
|
||||
240e:42:4000::/48
|
||||
240e:42:8000::/33
|
||||
240e:42::/33
|
||||
240e:42c::/30
|
||||
240e:430::/30
|
||||
240e:434::/30
|
||||
240e:438::/30
|
||||
240e:43:8000::/33
|
||||
240e:43:8000::/48
|
||||
240e:43::/33
|
||||
240e:43c::/30
|
||||
240e:440::/32
|
||||
240e:441::/32
|
||||
240e:442::/32
|
||||
240e:443::/32
|
||||
240e:444::/32
|
||||
240e:445::/32
|
||||
240e:446::/32
|
||||
240e:447::/32
|
||||
240e:448::/30
|
||||
240e:44:8000::/33
|
||||
240e:44::/33
|
||||
240e:44c::/30
|
||||
240e:450::/30
|
||||
240e:454::/30
|
||||
240e:458::/30
|
||||
240e:45:8000::/33
|
||||
240e:45:8000::/48
|
||||
240e:45c::/30
|
||||
240e:460::/30
|
||||
240e:464::/30
|
||||
240e:468::/30
|
||||
240e:46:5008::/48
|
||||
240e:46:8000::/33
|
||||
240e:46::/33
|
||||
240e:46c::/30
|
||||
240e:470::/30
|
||||
240e:474::/30
|
||||
240e:478::/30
|
||||
240e:47::/32
|
||||
240e:47c::/30
|
||||
240e:48:8000::/33
|
||||
240e:48::/33
|
||||
240e:49:8000::/33
|
||||
240e:49::/33
|
||||
240e:4a:4100::/48
|
||||
240e:4a:8000::/33
|
||||
240e:4a::/33
|
||||
240e:4b:1::/48
|
||||
240e:4b:8000::/33
|
||||
240e:4b::/33
|
||||
240e:4c:4006::/48
|
||||
240e:4c:5000::/48
|
||||
240e:4c:8000::/33
|
||||
240e:4c::/33
|
||||
240e:4d:50ff::/48
|
||||
240e:4d:8000::/33
|
||||
240e:4d::/33
|
||||
240e:4e:4000::/48
|
||||
240e:4e:8000::/33
|
||||
240e:4e::/33
|
||||
240e:4f:8000::/33
|
||||
240e:4f::/33
|
||||
240e:504::/30
|
||||
240e:508::/30
|
||||
240e:50:8000::/33
|
||||
240e:50::/33
|
||||
240e:514::/30
|
||||
240e:518::/32
|
||||
240e:519::/32
|
||||
240e:51::/32
|
||||
240e:51a::/32
|
||||
240e:51b::/32
|
||||
240e:51c::/30
|
||||
240e:520::/30
|
||||
240e:524::/32
|
||||
240e:525::/32
|
||||
240e:526::/32
|
||||
240e:527::/32
|
||||
240e:52:4802::/48
|
||||
240e:52:8000::/33
|
||||
240e:52::/33
|
||||
240e:52c::/30
|
||||
240e:530::/30
|
||||
240e:534::/30
|
||||
240e:538::/30
|
||||
240e:53:8000::/33
|
||||
240e:53::/33
|
||||
240e:53c::/30
|
||||
240e:540::/32
|
||||
240e:541::/32
|
||||
240e:542::/32
|
||||
240e:543::/32
|
||||
240e:544::/32
|
||||
240e:545::/32
|
||||
240e:546::/32
|
||||
240e:547::/32
|
||||
240e:548::/30
|
||||
240e:54:8000::/33
|
||||
240e:54::/33
|
||||
240e:54c::/30
|
||||
240e:550::/30
|
||||
240e:554::/30
|
||||
240e:558::/30
|
||||
240e:55:8000::/33
|
||||
240e:55::/33
|
||||
240e:55c::/30
|
||||
240e:560::/30
|
||||
240e:564::/30
|
||||
240e:568::/30
|
||||
240e:56:4807::/48
|
||||
240e:56:8000::/33
|
||||
240e:56::/33
|
||||
240e:56c::/30
|
||||
240e:570::/30
|
||||
240e:574::/30
|
||||
240e:578::/30
|
||||
240e:57:8000::/33
|
||||
240e:57::/33
|
||||
240e:57c::/30
|
||||
240e:5a:6800::/48
|
||||
240e:5a:8000::/33
|
||||
240e:5a::/33
|
||||
240e:5b:8000::/33
|
||||
240e:5b::/33
|
||||
240e:5c:8000::/33
|
||||
240e:5c::/33
|
||||
240e:5d:8000::/33
|
||||
240e:5d::/33
|
||||
240e:5e:8000::/33
|
||||
240e:5e::/33
|
||||
240e:5f:8000::/33
|
||||
240e:5f::/33
|
||||
240e:604::/30
|
||||
240e:608::/30
|
||||
240e:60::/30
|
||||
240e:60c::/30
|
||||
240e:610::/30
|
||||
240e:614::/30
|
||||
240e:618::/32
|
||||
240e:619::/32
|
||||
240e:61a::/32
|
||||
240e:61b::/32
|
||||
240e:61c::/30
|
||||
240e:61d:1401::/48
|
||||
240e:620::/30
|
||||
240e:624::/32
|
||||
240e:625::/32
|
||||
240e:626::/32
|
||||
240e:627::/32
|
||||
240e:62c::/30
|
||||
240e:630::/30
|
||||
240e:634::/30
|
||||
240e:638::/30
|
||||
240e:638::/48
|
||||
240e:63c::/30
|
||||
240e:640::/32
|
||||
240e:641::/32
|
||||
240e:642::/32
|
||||
240e:643::/32
|
||||
240e:644::/30
|
||||
240e:648::/30
|
||||
240e:64::/32
|
||||
240e:64c::/30
|
||||
240e:650::/29
|
||||
240e:658::/29
|
||||
240e:659:1160::/48
|
||||
240e:660::/29
|
||||
240e:668::/29
|
||||
240e:670::/29
|
||||
240e:678::/29
|
||||
240e:680::/29
|
||||
240e:688::/29
|
||||
240e:690::/29
|
||||
240e:698::/29
|
||||
240e:6a0::/28
|
||||
240e:6b0::/28
|
||||
240e:704::/30
|
||||
240e:708::/30
|
||||
240e:70c::/30
|
||||
240e:710::/30
|
||||
240e:714::/30
|
||||
240e:718::/32
|
||||
240e:719::/32
|
||||
240e:71a::/32
|
||||
240e:71b::/32
|
||||
240e:71c::/30
|
||||
240e:720::/30
|
||||
240e:724::/32
|
||||
240e:725::/32
|
||||
240e:726::/32
|
||||
240e:727::/32
|
||||
240e:72c::/30
|
||||
240e:730::/30
|
||||
240e:734::/30
|
||||
240e:738::/30
|
||||
240e:73c::/30
|
||||
240e:740::/32
|
||||
240e:741::/32
|
||||
240e:742::/32
|
||||
240e:743::/32
|
||||
240e:744::/30
|
||||
240e:748::/30
|
||||
240e:74c::/30
|
||||
240e:750::/29
|
||||
240e:758::/29
|
||||
240e:760::/29
|
||||
240e:768::/29
|
||||
240e:770::/29
|
||||
240e:778::/29
|
||||
240e:780::/29
|
||||
240e:788::/29
|
||||
240e:790::/29
|
||||
240e:798::/29
|
||||
240e:7::/32
|
||||
240e:7a0::/28
|
||||
240e:7b0::/28
|
||||
240e:804::/30
|
||||
240e:814::/30
|
||||
240e:81c::/30
|
||||
240e:820::/30
|
||||
240e:82c::/30
|
||||
240e:830::/30
|
||||
240e:834::/30
|
||||
240e:838::/30
|
||||
240e:83c::/30
|
||||
240e:840::/32
|
||||
240e:841::/32
|
||||
240e:848::/30
|
||||
240e:84::/31
|
||||
240e:84c::/30
|
||||
240e:850::/30
|
||||
240e:854::/30
|
||||
240e:858::/30
|
||||
240e:85c::/30
|
||||
240e:860::/30
|
||||
240e:864::/30
|
||||
240e:868::/30
|
||||
240e:86::/31
|
||||
240e:86c::/30
|
||||
240e:870::/30
|
||||
240e:874::/30
|
||||
240e:878::/30
|
||||
240e:87c::/30
|
||||
240e:88::/31
|
||||
240e:8a::/31
|
||||
240e:8c::/31
|
||||
240e:8e::/31
|
||||
240e:904::/30
|
||||
240e:908::/30
|
||||
240e:90::/31
|
||||
240e:90c::/30
|
||||
240e:910::/30
|
||||
240e:914::/30
|
||||
240e:918::/32
|
||||
240e:919::/32
|
||||
240e:91a::/32
|
||||
240e:91b::/32
|
||||
240e:91c::/30
|
||||
240e:920::/30
|
||||
240e:924::/32
|
||||
240e:925::/32
|
||||
240e:926::/32
|
||||
240e:927::/32
|
||||
240e:92::/31
|
||||
240e:92c::/30
|
||||
240e:930::/30
|
||||
240e:934::/30
|
||||
240e:938::/30
|
||||
240e:93c::/30
|
||||
240e:940::/32
|
||||
240e:941::/32
|
||||
240e:942::/32
|
||||
240e:943::/32
|
||||
240e:944::/32
|
||||
240e:945::/32
|
||||
240e:946::/32
|
||||
240e:947::/32
|
||||
240e:948::/30
|
||||
240e:94c::/30
|
||||
240e:950::/30
|
||||
240e:954::/30
|
||||
240e:958::/30
|
||||
240e:95c::/30
|
||||
240e:960::/30
|
||||
240e:964::/30
|
||||
240e:968::/30
|
||||
240e:96::/31
|
||||
240e:96c::/30
|
||||
240e:970::/30
|
||||
240e:974::/30
|
||||
240e:978::/30
|
||||
240e:97c::/30
|
||||
240e:980:9400::/40
|
||||
240e:981:f000::/36
|
||||
240e:982:1000::/36
|
||||
240e:982:2000::/36
|
||||
240e:982:3000::/36
|
||||
240e:982:4000::/36
|
||||
240e:982:5000::/36
|
||||
240e:982:6000::/36
|
||||
240e:982:7000::/36
|
||||
240e:982:8000::/36
|
||||
240e:982:9000::/36
|
||||
240e:982::/36
|
||||
240e:982:a000::/36
|
||||
240e:982:b000::/36
|
||||
240e:982:c000::/36
|
||||
240e:982:d000::/36
|
||||
240e:982:e000::/36
|
||||
240e:983:1000::/40
|
||||
240e:983:100::/40
|
||||
240e:983:1100::/40
|
||||
240e:983:1200::/40
|
||||
240e:983:1300::/40
|
||||
240e:983:1400::/40
|
||||
240e:983:1500::/40
|
||||
240e:983:1600::/40
|
||||
240e:983:1700::/40
|
||||
240e:983:1800::/40
|
||||
240e:983:1900::/40
|
||||
240e:983:1a00::/40
|
||||
240e:983:1b00::/40
|
||||
240e:983:1c00::/40
|
||||
240e:983:1d00::/40
|
||||
240e:983:1e00::/40
|
||||
240e:983::/40
|
||||
240e:983:a00::/40
|
||||
240e:983:c00::/40
|
||||
240e:983:d00::/40
|
||||
240e:983:f00::/40
|
||||
240e:98::/31
|
||||
240e:9::/32
|
||||
240e:9a::/31
|
||||
240e:9c::/30
|
||||
240e::/20
|
||||
240e::/24
|
||||
240e:a04::/30
|
||||
240e:a08::/30
|
||||
240e:a0::/31
|
||||
240e:a0c::/32
|
||||
240e:a0d::/32
|
||||
240e:a0e::/32
|
||||
240e:a0f::/32
|
||||
240e:a14::/30
|
||||
240e:a18::/32
|
||||
240e:a19::/32
|
||||
240e:a1a::/32
|
||||
240e:a1b::/32
|
||||
240e:a1c::/30
|
||||
240e:a20::/30
|
||||
240e:a24::/32
|
||||
240e:a25::/32
|
||||
240e:a26::/32
|
||||
240e:a27::/32
|
||||
240e:a2::/31
|
||||
240e:a2c::/30
|
||||
240e:a30::/30
|
||||
240e:a34::/30
|
||||
240e:a38::/30
|
||||
240e:a40::/32
|
||||
240e:a41::/32
|
||||
240e:a42::/32
|
||||
240e:a43::/32
|
||||
240e:a44::/32
|
||||
240e:a45::/32
|
||||
240e:a46::/32
|
||||
240e:a47::/32
|
||||
240e:a48::/30
|
||||
240e:a4::/31
|
||||
240e:a4c::/30
|
||||
240e:a50::/30
|
||||
240e:a54::/30
|
||||
240e:a58::/30
|
||||
240e:a5:8000::/48
|
||||
240e:a5c::/30
|
||||
240e:a60::/30
|
||||
240e:a64::/30
|
||||
240e:a68::/30
|
||||
240e:a6::/31
|
||||
240e:a6c::/30
|
||||
240e:a70::/30
|
||||
240e:a74::/30
|
||||
240e:a78::/30
|
||||
240e:a7c::/30
|
||||
240e:a8::/31
|
||||
240e:aa::/31
|
||||
240e:ac::/31
|
||||
240e:ae::/31
|
||||
240e:b04::/30
|
||||
240e:b0::/31
|
||||
240e:b0c::/30
|
||||
240e:b14::/30
|
||||
240e:b1c::/30
|
||||
240e:b20::/30
|
||||
240e:b2::/31
|
||||
240e:b2c::/30
|
||||
240e:b30::/30
|
||||
240e:b34::/30
|
||||
240e:b38::/30
|
||||
240e:b44::/30
|
||||
240e:b48::/30
|
||||
240e:b4::/31
|
||||
240e:b4c::/30
|
||||
240e:b50::/29
|
||||
240e:b60::/29
|
||||
240e:b68::/29
|
||||
240e:b6::/31
|
||||
240e:b70::/29
|
||||
240e:b78::/29
|
||||
240e:b80::/29
|
||||
240e:b88::/29
|
||||
240e:b8::/31
|
||||
240e:b90::/29
|
||||
240e:b98::/29
|
||||
240e:b:8000::/33
|
||||
240e:b::/33
|
||||
240e:ba::/31
|
||||
240e:bb0::/28
|
||||
240e:bc::/31
|
||||
240e:bd:8000::/48
|
||||
240e:be::/31
|
||||
240e:c0::/31
|
||||
240e:c2::/31
|
||||
240e:c4::/30
|
||||
240e:c60::/30
|
||||
240e:c8::/31
|
||||
240e:ca::/31
|
||||
240e:cc::/31
|
||||
240e:cd:8000::/48
|
||||
240e:ce::/31
|
||||
240e:d0::/31
|
||||
240e:d2::/31
|
||||
240e:d4::/31
|
||||
240e:d6::/31
|
||||
240e:d8::/31
|
||||
240e:da::/31
|
||||
240e:dc::/31
|
||||
240e:de::/31
|
||||
240e:e8::/31
|
||||
240e:ea::/31
|
||||
240e:ec::/31
|
||||
240e:ee::/31
|
||||
240e:f0::/31
|
||||
240e:f2::/31
|
||||
240e:f4::/31
|
||||
240e:f6::/31
|
||||
240e:f8::/31
|
||||
240e:fa::/31
|
||||
240e:fc::/31
|
||||
240e:fe::/31
|
||||
2605:9d80:8001::/48
|
||||
2605:9d80:8011::/48
|
||||
2605:9d80:8021::/48
|
||||
2605:9d80:8031::/48
|
||||
2605:9d80:8041::/48
|
||||
2605:9d80:8081::/48
|
||||
2a04:f580:8010::/48
|
||||
2a04:f580:8011::/48
|
||||
2a04:f580:8090::/48
|
||||
2a04:f580:8210::/48
|
||||
2a04:f580:8211::/48
|
||||
2a04:f580:8290::/48
|
||||
2c0f:f7a8:8011::/48
|
||||
2c0f:f7a8:8050::/48
|
||||
2c0f:f7a8:805f::/48
|
||||
2c0f:f7a8:8150::/48
|
||||
2c0f:f7a8:815f::/48
|
||||
2c0f:f7a8:8211::/48
|
||||
2400:9380:8001
|
||||
2400:9380:8003
|
||||
2400:9380:8021
|
||||
2400:9380:8040
|
||||
2400:9380:8140
|
||||
2400:9380:8201
|
||||
2400:9380:8301
|
||||
240e
|
||||
240e:1
|
||||
240e:100
|
||||
240e:101
|
||||
240e:103
|
||||
240e:104
|
||||
240e:106
|
||||
240e:108
|
||||
240e:11
|
||||
240e:11:8001
|
||||
240e:12
|
||||
240e:144
|
||||
240e:16:1001
|
||||
240e:16:1008
|
||||
240e:16:1009
|
||||
240e:184
|
||||
240e:1a
|
||||
240e:1c7
|
||||
240e:1c:112
|
||||
240e:2
|
||||
240e:218
|
||||
240e:219
|
||||
240e:21a
|
||||
240e:21b
|
||||
240e:224
|
||||
240e:225
|
||||
240e:226
|
||||
240e:227
|
||||
240e:24
|
||||
240e:240
|
||||
240e:241
|
||||
240e:242
|
||||
240e:243
|
||||
240e:244
|
||||
240e:245
|
||||
240e:246
|
||||
240e:247
|
||||
240e:2c
|
||||
240e:318
|
||||
240e:319
|
||||
240e:31a
|
||||
240e:31b
|
||||
240e:324
|
||||
240e:325
|
||||
240e:326
|
||||
240e:327
|
||||
240e:340
|
||||
240e:341
|
||||
240e:342
|
||||
240e:343
|
||||
240e:350:205
|
||||
240e:41
|
||||
240e:418
|
||||
240e:419
|
||||
240e:41a
|
||||
240e:41b
|
||||
240e:42
|
||||
240e:424
|
||||
240e:425
|
||||
240e:426
|
||||
240e:427
|
||||
240e:42:4000
|
||||
240e:43
|
||||
240e:43:8000
|
||||
240e:44
|
||||
240e:440
|
||||
240e:441
|
||||
240e:442
|
||||
240e:443
|
||||
240e:444
|
||||
240e:445
|
||||
240e:446
|
||||
240e:447
|
||||
240e:45
|
||||
240e:45:8000
|
||||
240e:46
|
||||
240e:46:5008
|
||||
240e:47
|
||||
240e:47:4
|
||||
240e:48
|
||||
240e:49
|
||||
240e:4a
|
||||
240e:4a:4100
|
||||
240e:4b
|
||||
240e:4b:1
|
||||
240e:4b:2
|
||||
240e:4c
|
||||
240e:4c:4006
|
||||
240e:4d
|
||||
240e:4d:50ff
|
||||
240e:4e
|
||||
240e:4e:4000
|
||||
240e:4f
|
||||
240e:50
|
||||
240e:51
|
||||
240e:518
|
||||
240e:519
|
||||
240e:51a
|
||||
240e:51b
|
||||
240e:52
|
||||
240e:524
|
||||
240e:525
|
||||
240e:526
|
||||
240e:527
|
||||
240e:52:4802
|
||||
240e:53
|
||||
240e:54
|
||||
240e:540
|
||||
240e:541
|
||||
240e:542
|
||||
240e:543
|
||||
240e:544
|
||||
240e:545
|
||||
240e:546
|
||||
240e:547
|
||||
240e:55
|
||||
240e:56
|
||||
240e:56:4807
|
||||
240e:57
|
||||
240e:5a
|
||||
240e:5a:6800
|
||||
240e:5b
|
||||
240e:5c
|
||||
240e:5d
|
||||
240e:5e
|
||||
240e:5f
|
||||
240e:618
|
||||
240e:619
|
||||
240e:61a
|
||||
240e:61b
|
||||
240e:61d:1401
|
||||
240e:624
|
||||
240e:625
|
||||
240e:626
|
||||
240e:627
|
||||
240e:630:b83
|
||||
240e:638
|
||||
240e:638:b00
|
||||
240e:638:b02
|
||||
240e:64
|
||||
240e:640
|
||||
240e:641
|
||||
240e:642
|
||||
240e:643
|
||||
240e:64c:340
|
||||
240e:650:2540
|
||||
240e:650:280
|
||||
240e:650:4400
|
||||
240e:658:6c20
|
||||
240e:658:6c31
|
||||
240e:658:a00
|
||||
240e:659:1160
|
||||
240e:659:11e0
|
||||
240e:659:1b0
|
||||
240e:661:6180
|
||||
240e:699
|
||||
240e:6a0
|
||||
240e:6a0:10
|
||||
240e:6a0:100
|
||||
240e:6a0:105
|
||||
240e:6a0:b0c
|
||||
240e:7
|
||||
240e:718
|
||||
240e:719
|
||||
240e:71a
|
||||
240e:71b
|
||||
240e:724
|
||||
240e:725
|
||||
240e:726
|
||||
240e:727
|
||||
240e:740
|
||||
240e:741
|
||||
240e:742
|
||||
240e:743
|
||||
240e:767
|
||||
240e:840
|
||||
240e:841
|
||||
240e:9
|
||||
240e:918
|
||||
240e:919
|
||||
240e:91a
|
||||
240e:91b
|
||||
240e:924
|
||||
240e:925
|
||||
240e:926
|
||||
240e:927
|
||||
240e:940
|
||||
240e:941
|
||||
240e:942
|
||||
240e:943
|
||||
240e:944
|
||||
240e:945
|
||||
240e:946
|
||||
240e:947
|
||||
240e:980
|
||||
240e:981
|
||||
240e:982
|
||||
240e:983
|
||||
240e:9f:8340
|
||||
240e:9f:8400
|
||||
240e:9f:8440
|
||||
240e:9f:8880
|
||||
240e:9f:88c0
|
||||
240e:a0c
|
||||
240e:a0d
|
||||
240e:a0e
|
||||
240e:a0f
|
||||
240e:a18
|
||||
240e:a19
|
||||
240e:a1a
|
||||
240e:a1b
|
||||
240e:a24
|
||||
240e:a25
|
||||
240e:a26
|
||||
240e:a27
|
||||
240e:a40
|
||||
240e:a41
|
||||
240e:a42
|
||||
240e:a43
|
||||
240e:a44
|
||||
240e:a45
|
||||
240e:a46
|
||||
240e:a47
|
||||
240e:a5:8000
|
||||
240e:a5:a100
|
||||
240e:a5:a140
|
||||
240e:a9:8010
|
||||
240e:ab:b202
|
||||
240e:b
|
||||
240e:bd:8000
|
||||
240e:cd:8000
|
||||
2605:9d80:8001
|
||||
2605:9d80:8011
|
||||
2605:9d80:8021
|
||||
2605:9d80:8031
|
||||
2605:9d80:8041
|
||||
2605:9d80:8081
|
||||
2a04:f580:8010
|
||||
2a04:f580:8011
|
||||
2a04:f580:8090
|
||||
2a04:f580:8210
|
||||
2a04:f580:8211
|
||||
2a04:f580:8290
|
||||
2c0f:f7a8:8011
|
||||
2c0f:f7a8:8050
|
||||
2c0f:f7a8:805f
|
||||
2c0f:f7a8:8150
|
||||
2c0f:f7a8:815f
|
||||
2c0f:f7a8:8211
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as4134.txt",
|
||||
"count": 629,
|
||||
"sha256": "00941f76c022566166b926670ee9358084c53465cbaaf75a3da7b73d997d1947",
|
||||
"generated_at": "2026-07-23T02:56:31Z"
|
||||
}
|
||||
@ -1,179 +1,180 @@
|
||||
2400:9380:9001::/48
|
||||
2400:9380:9009::/48
|
||||
2400:9380:9020::/48
|
||||
2400:9380:9021::/48
|
||||
2400:9380:9050::/48
|
||||
2400:9380:9051::/48
|
||||
2400:9380:9060::/48
|
||||
2400:9380:9071::/48
|
||||
2400:9380:9080::/48
|
||||
2400:9380:9081::/48
|
||||
2400:9380:90b1::/48
|
||||
2400:9380:90b2::/48
|
||||
2400:9380:90b3::/48
|
||||
2400:9380:90b4::/48
|
||||
2400:9380:90b5::/48
|
||||
2400:9380:90b6::/48
|
||||
2400:9380:90b7::/48
|
||||
2400:9380:9121::/48
|
||||
2400:9380:9220::/48
|
||||
2400:9380:9221::/48
|
||||
2400:9380:9250::/48
|
||||
2400:9380:9251::/48
|
||||
2400:9380:9260::/48
|
||||
2400:9380:9271::/48
|
||||
2400:9380:9280::/48
|
||||
2400:9380:9281::/48
|
||||
2400:9380:9282::/48
|
||||
2400:9380:92b1::/48
|
||||
2400:9380:92b2::/48
|
||||
2400:9380:92b3::/48
|
||||
2400:9380:92b4::/48
|
||||
2400:9380:92b5::/48
|
||||
2400:9380:92b6::/48
|
||||
2400:9380:92b7::/48
|
||||
240e:182:5401::/48
|
||||
240e:182:5501::/48
|
||||
240e:409:9000::/48
|
||||
240e:409:9001::/48
|
||||
240e:410:ff00::/48
|
||||
240e:411:ff00::/48
|
||||
240e:414:e710::/45
|
||||
240e:414:e718::/45
|
||||
240e:43d:fff1::/48
|
||||
240e:43d:fff3::/48
|
||||
240e:43d:fff4::/48
|
||||
240e:43d:fff7::/48
|
||||
240e:440:ac00::/48
|
||||
240e:440:ac01::/48
|
||||
240e:440:ac02::/48
|
||||
240e:441:ac00::/48
|
||||
240e:441:ac01::/48
|
||||
240e:441:ac02::/48
|
||||
240e:445:3f00::/48
|
||||
240e:446:3f00::/48
|
||||
240e:451:bfc0::/48
|
||||
240e:451:bfe0::/48
|
||||
240e:451:ffa0::/48
|
||||
240e:451:ffc0::/48
|
||||
240e:451:ffe0::/48
|
||||
240e:456:fe00::/48
|
||||
240e:457:fe00::/48
|
||||
240e:469:f400::/48
|
||||
240e:476:febf::/48
|
||||
240e:476:feff::/48
|
||||
240e:604:314::/48
|
||||
240e:604:319::/48
|
||||
240e:60e:8000::/48
|
||||
240e:60e:8001::/48
|
||||
240e:60e:e000::/35
|
||||
240e:615:7000::/40
|
||||
240e:61d:e600::/40
|
||||
240e:628:10::/45
|
||||
240e:62c:1f0::/44
|
||||
240e:62f::/48
|
||||
240e:638:f::/48
|
||||
240e:63c:3f00::/44
|
||||
240e:640:178::/48
|
||||
240e:640:179::/48
|
||||
240e:640:17a::/48
|
||||
240e:640:17b::/48
|
||||
240e:645:1000::/36
|
||||
240e:648:1e::/48
|
||||
240e:648:c00f::/48
|
||||
240e:648:c40f::/48
|
||||
240e:648:c80f::/48
|
||||
240e:648:cc0f::/48
|
||||
240e:648:d00f::/48
|
||||
240e:648:d40f::/48
|
||||
240e:648:d80f::/48
|
||||
240e:648:dc0f::/48
|
||||
240e:649:c00f::/48
|
||||
240e:649:c40f::/48
|
||||
240e:649:c80f::/48
|
||||
240e:649:cc0f::/48
|
||||
240e:649:d00f::/48
|
||||
240e:649:d40f::/48
|
||||
240e:649:d80f::/48
|
||||
240e:649:dc0f::/48
|
||||
240e:64e:d00::/44
|
||||
240e:64e:de0::/48
|
||||
240e:64e:dec::/48
|
||||
240e:64e:ded::/48
|
||||
240e:650:8000::/38
|
||||
240e:659:f100::/48
|
||||
240e:65f:e000::/36
|
||||
240e:669:b000::/40
|
||||
240e:670:d000::/36
|
||||
240e:679:1001::/48
|
||||
240e:688:ed00::/48
|
||||
240e:699:7a00::/48
|
||||
240e:699:7a01::/48
|
||||
240e:699:7b00::/48
|
||||
240e:699:7b06::/48
|
||||
240e:699:7b07::/48
|
||||
240e:699:7b08::/48
|
||||
240e:699:7b0a::/48
|
||||
240e:699:7b0c::/48
|
||||
240e:699:7b0d::/48
|
||||
240e:699:7b0f::/48
|
||||
240e:699:7b11::/48
|
||||
240e:699:7b12::/48
|
||||
240e:699:bc00::/38
|
||||
240e:6a0:d00::/48
|
||||
240e:6a0:d03::/48
|
||||
240e:6a0:d04::/48
|
||||
240e:6a0:d07::/48
|
||||
240e:6a0:d0a::/48
|
||||
240e:713:f020::/48
|
||||
240e:713:f021::/48
|
||||
240e:733:4c0::/48
|
||||
240e:733:4c1::/48
|
||||
240e:767:f000::/48
|
||||
240e:787:7000::/48
|
||||
240e:787:7001::/48
|
||||
240e:790:ff00::/40
|
||||
240e:7b6::/31
|
||||
240e:965:822::/48
|
||||
240e:eb:e800::/37
|
||||
240e:eb:f000::/37
|
||||
240e:f6:8002::/48
|
||||
240e:f6:8003::/48
|
||||
240e:ff:c020::/48
|
||||
240e:ff:c022::/48
|
||||
240e:ff:c024::/48
|
||||
240e:ff:c027::/48
|
||||
240e:ff:c029::/48
|
||||
2605:9d80:9003::/48
|
||||
2605:9d80:9013::/48
|
||||
2605:9d80:9023::/48
|
||||
2605:9d80:9033::/48
|
||||
2605:9d80:9042::/48
|
||||
2605:9d80:9071::/48
|
||||
2605:9d80:9092::/48
|
||||
2804:1e48:9001::/48
|
||||
2804:1e48:9002::/48
|
||||
2a04:f580:9010::/48
|
||||
2a04:f580:9012::/48
|
||||
2a04:f580:9013::/48
|
||||
2a04:f580:9020::/48
|
||||
2a04:f580:9030::/48
|
||||
2a04:f580:9040::/48
|
||||
2a04:f580:9050::/48
|
||||
2a04:f580:9060::/48
|
||||
2a04:f580:9070::/48
|
||||
2a04:f580:9080::/48
|
||||
2a04:f580:9210::/48
|
||||
2a04:f580:9212::/48
|
||||
2a04:f580:9213::/48
|
||||
2a04:f580:9220::/48
|
||||
2a04:f580:9230::/48
|
||||
2a04:f580:9240::/48
|
||||
2a04:f580:9250::/48
|
||||
2a04:f580:9260::/48
|
||||
2a04:f580:9270::/48
|
||||
2a04:f580:9280::/48
|
||||
2a04:f580:9290::/48
|
||||
2c0f:f7a8:9020::/48
|
||||
2c0f:f7a8:9041::/48
|
||||
2c0f:f7a8:9220::/48
|
||||
2400:9380:9001
|
||||
2400:9380:9009
|
||||
2400:9380:9020
|
||||
2400:9380:9021
|
||||
2400:9380:9050
|
||||
2400:9380:9051
|
||||
2400:9380:9060
|
||||
2400:9380:9071
|
||||
2400:9380:9080
|
||||
2400:9380:9081
|
||||
2400:9380:90b1
|
||||
2400:9380:90b2
|
||||
2400:9380:90b3
|
||||
2400:9380:90b4
|
||||
2400:9380:90b5
|
||||
2400:9380:90b6
|
||||
2400:9380:90b7
|
||||
2400:9380:9121
|
||||
2400:9380:9220
|
||||
2400:9380:9221
|
||||
2400:9380:9250
|
||||
2400:9380:9251
|
||||
2400:9380:9260
|
||||
2400:9380:9271
|
||||
2400:9380:9280
|
||||
2400:9380:9281
|
||||
2400:9380:9282
|
||||
2400:9380:92b1
|
||||
2400:9380:92b2
|
||||
2400:9380:92b3
|
||||
2400:9380:92b4
|
||||
2400:9380:92b5
|
||||
2400:9380:92b6
|
||||
2400:9380:92b7
|
||||
240e
|
||||
240e:182:5401
|
||||
240e:182:5501
|
||||
240e:409:9000
|
||||
240e:409:9001
|
||||
240e:410:ff00
|
||||
240e:411:ff00
|
||||
240e:414
|
||||
240e:43d:fff1
|
||||
240e:43d:fff3
|
||||
240e:43d:fff4
|
||||
240e:43d:fff7
|
||||
240e:440:ac00
|
||||
240e:440:ac01
|
||||
240e:440:ac02
|
||||
240e:441:ac00
|
||||
240e:441:ac01
|
||||
240e:441:ac02
|
||||
240e:445:3f00
|
||||
240e:446:3f00
|
||||
240e:451:bfc0
|
||||
240e:451:bfe0
|
||||
240e:451:ffa0
|
||||
240e:451:ffc0
|
||||
240e:451:ffe0
|
||||
240e:456:fe00
|
||||
240e:457:fe00
|
||||
240e:469:f400
|
||||
240e:476:febf
|
||||
240e:476:feff
|
||||
240e:604:314
|
||||
240e:604:319
|
||||
240e:60e
|
||||
240e:60e:8000
|
||||
240e:60e:8001
|
||||
240e:615
|
||||
240e:61d
|
||||
240e:628
|
||||
240e:62c
|
||||
240e:62f
|
||||
240e:638:f
|
||||
240e:63c
|
||||
240e:640:178
|
||||
240e:640:179
|
||||
240e:640:17a
|
||||
240e:640:17b
|
||||
240e:645
|
||||
240e:648:1e
|
||||
240e:648:c00f
|
||||
240e:648:c40f
|
||||
240e:648:c80f
|
||||
240e:648:cc0f
|
||||
240e:648:d00f
|
||||
240e:648:d40f
|
||||
240e:648:d80f
|
||||
240e:648:dc0f
|
||||
240e:649:c00f
|
||||
240e:649:c40f
|
||||
240e:649:c80f
|
||||
240e:649:cc0f
|
||||
240e:649:d00f
|
||||
240e:649:d40f
|
||||
240e:649:d80f
|
||||
240e:649:dc0f
|
||||
240e:64e
|
||||
240e:64e:de0
|
||||
240e:64e:dec
|
||||
240e:64e:ded
|
||||
240e:650
|
||||
240e:659:f100
|
||||
240e:65f
|
||||
240e:669
|
||||
240e:670
|
||||
240e:679:1001
|
||||
240e:679:800
|
||||
240e:688:ed00
|
||||
240e:699
|
||||
240e:699:7a00
|
||||
240e:699:7a01
|
||||
240e:699:7b00
|
||||
240e:699:7b06
|
||||
240e:699:7b07
|
||||
240e:699:7b08
|
||||
240e:699:7b0a
|
||||
240e:699:7b0c
|
||||
240e:699:7b0d
|
||||
240e:699:7b0f
|
||||
240e:699:7b11
|
||||
240e:699:7b12
|
||||
240e:6a0:d00
|
||||
240e:6a0:d03
|
||||
240e:6a0:d04
|
||||
240e:6a0:d07
|
||||
240e:6a0:d0a
|
||||
240e:713:f020
|
||||
240e:713:f021
|
||||
240e:733:4c0
|
||||
240e:733:4c1
|
||||
240e:767:f000
|
||||
240e:787:7000
|
||||
240e:787:7001
|
||||
240e:790
|
||||
240e:965:822
|
||||
240e:eb
|
||||
240e:f6:8002
|
||||
240e:f6:8003
|
||||
240e:ff:c020
|
||||
240e:ff:c022
|
||||
240e:ff:c024
|
||||
240e:ff:c027
|
||||
240e:ff:c029
|
||||
2605:9d80:9003
|
||||
2605:9d80:9013
|
||||
2605:9d80:9023
|
||||
2605:9d80:9033
|
||||
2605:9d80:9042
|
||||
2605:9d80:9071
|
||||
2605:9d80:9092
|
||||
2804:1e48
|
||||
2804:1e48:9001
|
||||
2804:1e48:9002
|
||||
2a04:f580:9010
|
||||
2a04:f580:9012
|
||||
2a04:f580:9013
|
||||
2a04:f580:9020
|
||||
2a04:f580:9030
|
||||
2a04:f580:9040
|
||||
2a04:f580:9050
|
||||
2a04:f580:9060
|
||||
2a04:f580:9070
|
||||
2a04:f580:9080
|
||||
2a04:f580:9210
|
||||
2a04:f580:9212
|
||||
2a04:f580:9213
|
||||
2a04:f580:9220
|
||||
2a04:f580:9230
|
||||
2a04:f580:9240
|
||||
2a04:f580:9250
|
||||
2a04:f580:9260
|
||||
2a04:f580:9270
|
||||
2a04:f580:9280
|
||||
2a04:f580:9290
|
||||
2c0f:f7a8:9020
|
||||
2c0f:f7a8:9041
|
||||
2c0f:f7a8:9211
|
||||
2c0f:f7a8:9220
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as4809.txt",
|
||||
"count": 179,
|
||||
"sha256": "1b43123532881e6dc9b08cfd56216a9eda028ff41b8ac6dfb4e8c51cb38d873c",
|
||||
"generated_at": "2026-07-20T19:12:53Z"
|
||||
}
|
||||
@ -1,470 +1,418 @@
|
||||
2001:4510:1480::/41
|
||||
2001:4511:1480::/41
|
||||
2402:18a0::/32
|
||||
2402:f140:ff13::/48
|
||||
2402:f140:ff14::/48
|
||||
2404:6500:dcb3::/48
|
||||
2405:1480:3000::/47
|
||||
2406:1e40:f012::/47
|
||||
2406:cac0:200::/40
|
||||
2406:cac0::/40
|
||||
2407:6c40:1500::/48
|
||||
2407:6c40:1600::/40
|
||||
2408:8000:10fe::/48
|
||||
2408:8000:10ff::/48
|
||||
2408:8000:2000::/36
|
||||
2408:8000:2::/48
|
||||
2408:8000:3000::/36
|
||||
2408:8000:3::/48
|
||||
2408:8000:4000::/36
|
||||
2408:8000:5000::/36
|
||||
2408:8000:6000::/36
|
||||
2408:8000:7000::/36
|
||||
2408:8000:8000::/36
|
||||
2408:8000:8010::/47
|
||||
2408:8000::/48
|
||||
2408:8000:a000::/36
|
||||
2408:8000:b000::/36
|
||||
2408:8000:c000::/36
|
||||
2408:8000:d000::/36
|
||||
2408:8000:e000::/36
|
||||
2408:8000:f000::/36
|
||||
2408:8001:1000::/36
|
||||
2408:8001:2000::/36
|
||||
2408:8001:4000::/36
|
||||
2408:8001:5000::/36
|
||||
2408:8001:6000::/36
|
||||
2408:8001:7000::/36
|
||||
2408:8001:8000::/36
|
||||
2408:8001:9000::/36
|
||||
2408:8001::/36
|
||||
2408:8001:a000::/36
|
||||
2408:8001:b000::/36
|
||||
2408:8001:c000::/36
|
||||
2408:8001:d000::/36
|
||||
2408:8001:e000::/36
|
||||
2408:8001:f000::/36
|
||||
2408:802a:8000::/33
|
||||
2408:802c::/32
|
||||
2408:803e::/32
|
||||
2408:8056::/32
|
||||
2408:80c2::/33
|
||||
2408:80c5::/33
|
||||
2408:80e2::/33
|
||||
2408:80e9:4000::/34
|
||||
2408:80f5:4000::/34
|
||||
2408:80f9:4000::/34
|
||||
2408:815f:e000::/35
|
||||
2408:8181:6000::/40
|
||||
2408:8181:8000::/40
|
||||
2408:8181::/40
|
||||
2408:8181:a000::/40
|
||||
2408:8181:a220::/44
|
||||
2408:8181:e000::/40
|
||||
2408:8182:c000::/40
|
||||
2408:8183:4000::/40
|
||||
2408:8183:8000::/40
|
||||
2408:81a2:2000::/35
|
||||
2408:81a2:4000::/35
|
||||
2408:81a3:6000::/35
|
||||
2408:8210::/32
|
||||
2408:8211::/32
|
||||
2408:8212::/32
|
||||
2408:8213::/32
|
||||
2408:8214::/32
|
||||
2408:8215::/32
|
||||
2408:821a::/32
|
||||
2408:821b::/32
|
||||
2408:8220::/32
|
||||
2408:8221::/32
|
||||
2408:8226::/32
|
||||
2408:822a::/32
|
||||
2408:822b::/32
|
||||
2408:822e::/32
|
||||
2408:822f::/32
|
||||
2408:8230::/32
|
||||
2408:8231::/32
|
||||
2408:8232::/32
|
||||
2408:8233::/32
|
||||
2408:8234::/32
|
||||
2408:8235::/32
|
||||
2408:8236::/32
|
||||
2408:8237::/32
|
||||
2408:8238::/32
|
||||
2408:8239::/32
|
||||
2408:823c::/32
|
||||
2408:823d::/32
|
||||
2408:8240::/32
|
||||
2408:8244::/30
|
||||
2408:8248::/32
|
||||
2408:8249::/32
|
||||
2408:824a::/32
|
||||
2408:824b::/32
|
||||
2408:824c::/32
|
||||
2408:824e::/32
|
||||
2408:824f::/32
|
||||
2408:8250::/32
|
||||
2408:8251::/32
|
||||
2408:8252::/32
|
||||
2408:8253::/32
|
||||
2408:8254::/32
|
||||
2408:825c::/32
|
||||
2408:825d::/32
|
||||
2408:825f::/32
|
||||
2408:8260::/32
|
||||
2408:8262::/32
|
||||
2408:8263::/32
|
||||
2408:8264::/32
|
||||
2408:8265::/32
|
||||
2408:8266::/32
|
||||
2408:826a::/32
|
||||
2408:826c::/32
|
||||
2408:826d::/32
|
||||
2408:826e::/32
|
||||
2408:826f::/32
|
||||
2408:8270::/32
|
||||
2408:8274::/32
|
||||
2408:8275::/32
|
||||
2408:8276::/32
|
||||
2408:8277::/32
|
||||
2408:8278::/32
|
||||
2408:8279::/32
|
||||
2408:827a::/32
|
||||
2408:8310::/32
|
||||
2408:8311::/32
|
||||
2408:8312::/32
|
||||
2408:8313::/32
|
||||
2408:832a::/32
|
||||
2408:832e::/32
|
||||
2408:832f::/32
|
||||
2408:8330::/32
|
||||
2408:8331::/32
|
||||
2408:8332::/32
|
||||
2408:8333::/32
|
||||
2408:8338::/32
|
||||
2408:8340::/32
|
||||
2408:8344::/30
|
||||
2408:8348::/32
|
||||
2408:8349::/32
|
||||
2408:834a::/32
|
||||
2408:834b::/32
|
||||
2408:834e::/32
|
||||
2408:834f::/32
|
||||
2408:8350::/32
|
||||
2408:8351::/32
|
||||
2408:8352::/32
|
||||
2408:8353::/32
|
||||
2408:8354::/32
|
||||
2408:8360::/32
|
||||
2408:8361::/32
|
||||
2408:8362::/32
|
||||
2408:8363::/32
|
||||
2408:8364::/32
|
||||
2408:8365::/32
|
||||
2408:836c::/32
|
||||
2408:836d::/32
|
||||
2408:836e::/32
|
||||
2408:836f::/32
|
||||
2408:8374::/32
|
||||
2408:8375::/32
|
||||
2408:8376::/32
|
||||
2408:8377::/32
|
||||
2408:8378::/32
|
||||
2408:8379::/32
|
||||
2408:837a::/32
|
||||
2408:8410::/32
|
||||
2408:8411::/32
|
||||
2408:8412::/32
|
||||
2408:8413::/32
|
||||
2408:8414::/32
|
||||
2408:8415::/32
|
||||
2408:8417::/32
|
||||
2408:8418::/32
|
||||
2408:841a::/32
|
||||
2408:841b::/32
|
||||
2408:841c::/32
|
||||
2408:841d::/32
|
||||
2408:841e::/32
|
||||
2408:8420::/32
|
||||
2408:8421::/32
|
||||
2408:8422::/32
|
||||
2408:8426::/32
|
||||
2408:8427::/32
|
||||
2408:842a::/32
|
||||
2408:842b::/32
|
||||
2408:842c::/32
|
||||
2408:842e::/32
|
||||
2408:8430::/32
|
||||
2408:8431::/32
|
||||
2408:8434::/32
|
||||
2408:8435::/32
|
||||
2408:8436::/32
|
||||
2408:8437::/32
|
||||
2408:8438::/32
|
||||
2408:8439::/32
|
||||
2408:843c::/32
|
||||
2408:843d::/32
|
||||
2408:843e::/32
|
||||
2408:843f::/32
|
||||
2408:8440::/32
|
||||
2408:8441::/32
|
||||
2408:8444::/30
|
||||
2408:8448::/32
|
||||
2408:844b::/32
|
||||
2408:844c::/32
|
||||
2408:844d::/32
|
||||
2408:844e::/32
|
||||
2408:844f::/32
|
||||
2408:8452::/32
|
||||
2408:8453::/32
|
||||
2408:8454::/32
|
||||
2408:845c::/32
|
||||
2408:845d::/32
|
||||
2408:8460::/32
|
||||
2408:8461::/32
|
||||
2408:8462::/32
|
||||
2408:8463::/32
|
||||
2408:8464::/32
|
||||
2408:8465::/32
|
||||
2408:8466::/32
|
||||
2408:8469::/32
|
||||
2408:846a::/32
|
||||
2408:846b::/32
|
||||
2408:846c::/32
|
||||
2408:846d::/32
|
||||
2408:846e::/32
|
||||
2408:846f::/32
|
||||
2408:8470::/32
|
||||
2408:8471::/32
|
||||
2408:8474::/32
|
||||
2408:8475::/32
|
||||
2408:8476::/32
|
||||
2408:8477::/32
|
||||
2408:8478::/32
|
||||
2408:8479::/32
|
||||
2408:847a::/32
|
||||
2408:84e3::/32
|
||||
2408:84e4::/32
|
||||
2408:84e5::/32
|
||||
2408:84e6::/32
|
||||
2408:84e7::/32
|
||||
2408:84e9::/32
|
||||
2408:84eb::/32
|
||||
2408:84ec::/32
|
||||
2408:84ed::/32
|
||||
2408:84ee::/32
|
||||
2408:84ef::/32
|
||||
2408:84f0::/32
|
||||
2408:84f1::/32
|
||||
2408:84f2::/32
|
||||
2408:84f4::/32
|
||||
2408:84f5::/32
|
||||
2408:84f6::/32
|
||||
2408:84f7::/32
|
||||
2408:84f8::/32
|
||||
2408:84f9::/32
|
||||
2408:84fa::/32
|
||||
2408:84fb::/32
|
||||
2408:84fc::/32
|
||||
2408:84fd::/32
|
||||
2408:84fe::/32
|
||||
2408:84ff::/32
|
||||
2408:856c::/32
|
||||
2408:856d::/32
|
||||
2408:8610::/32
|
||||
2408:8611::/32
|
||||
2408:8612::/32
|
||||
2408:8613::/32
|
||||
2408:8614::/32
|
||||
2408:8614:e20::/48
|
||||
2408:8615::/32
|
||||
2408:861a::/32
|
||||
2408:861b::/32
|
||||
2408:861c::/32
|
||||
2408:8620::/32
|
||||
2408:8621::/32
|
||||
2408:8624::/32
|
||||
2408:8625::/32
|
||||
2408:8626::/32
|
||||
2408:862a::/32
|
||||
2408:862b::/32
|
||||
2408:862d::/32
|
||||
2408:862e::/32
|
||||
2408:862f::/32
|
||||
2408:8630::/32
|
||||
2408:8631::/32
|
||||
2408:8632::/32
|
||||
2408:8633::/32
|
||||
2408:8634::/32
|
||||
2408:8635::/32
|
||||
2408:8636::/32
|
||||
2408:8637::/32
|
||||
2408:8638::/32
|
||||
2408:8639::/32
|
||||
2408:863c::/32
|
||||
2408:863d::/32
|
||||
2408:8640::/32
|
||||
2408:8642::/32
|
||||
2408:8644::/30
|
||||
2408:8648::/32
|
||||
2408:8649::/32
|
||||
2408:864c::/32
|
||||
2408:864e::/32
|
||||
2408:864f::/32
|
||||
2408:8650::/32
|
||||
2408:8651::/32
|
||||
2408:8652::/32
|
||||
2408:8653::/32
|
||||
2408:865c::/32
|
||||
2408:865d::/32
|
||||
2408:865f::/32
|
||||
2408:8660::/32
|
||||
2408:8662::/32
|
||||
2408:8663::/32
|
||||
2408:8664::/32
|
||||
2408:8665::/32
|
||||
2408:8666::/32
|
||||
2408:866a::/32
|
||||
2408:866b::/32
|
||||
2408:866c:200::/42
|
||||
2408:866c::/32
|
||||
2408:866d::/32
|
||||
2408:866e::/32
|
||||
2408:866f::/32
|
||||
2408:8670::/32
|
||||
2408:8674::/32
|
||||
2408:8675::/32
|
||||
2408:8676::/32
|
||||
2408:8677::/32
|
||||
2408:8678::/32
|
||||
2408:8679::/32
|
||||
2408:867a::/32
|
||||
2408:8710::/32
|
||||
2408:8711::/32
|
||||
2408:8712::/32
|
||||
2408:8713::/32
|
||||
2408:8719::/32
|
||||
2408:871a::/32
|
||||
2408:871b::/32
|
||||
2408:8720::/32
|
||||
2408:8721::/32
|
||||
2408:8722::/32
|
||||
2408:8723::/32
|
||||
2408:8726::/32
|
||||
2408:872b::/32
|
||||
2408:872b:e00::/47
|
||||
2408:872f::/32
|
||||
2408:8730::/32
|
||||
2408:8731::/32
|
||||
2408:8732::/32
|
||||
2408:8733::/32
|
||||
2408:8734::/32
|
||||
2408:8735::/32
|
||||
2408:8736::/32
|
||||
2408:8738::/32
|
||||
2408:873c::/32
|
||||
2408:873d::/32
|
||||
2408:8740::/32
|
||||
2408:8742::/32
|
||||
2408:8744::/30
|
||||
2408:8748::/32
|
||||
2408:8749::/32
|
||||
2408:874a::/32
|
||||
2408:874b::/32
|
||||
2408:874c::/32
|
||||
2408:874d::/32
|
||||
2408:874e::/32
|
||||
2408:874f::/32
|
||||
2408:8752::/32
|
||||
2408:875c::/32
|
||||
2408:8760::/32
|
||||
2408:8762::/32
|
||||
2408:8763::/32
|
||||
2408:8764::/32
|
||||
2408:8765::/32
|
||||
2408:8766::/32
|
||||
2408:8768::/32
|
||||
2408:876a::/32
|
||||
2408:876c:1700::/44
|
||||
2408:876c:1740::/44
|
||||
2408:876c:17c0::/44
|
||||
2408:876c::/32
|
||||
2408:876d::/32
|
||||
2408:876e::/32
|
||||
2408:876f::/32
|
||||
2408:8770::/32
|
||||
2408:8772::/32
|
||||
2408:8773::/32
|
||||
2408:8774::/32
|
||||
2408:8776::/32
|
||||
2408:8777::/32
|
||||
2408:8778::/32
|
||||
2408:8779::/32
|
||||
2408:877a::/32
|
||||
2408:8812::/32
|
||||
2408:8813::/32
|
||||
2408:8814::/32
|
||||
2408:8815::/32
|
||||
2408:8818::/32
|
||||
2408:8819::/32
|
||||
2408:882c::/32
|
||||
2408:883a::/32
|
||||
2408:8862::/32
|
||||
2408:8863::/32
|
||||
2408:8864::/32
|
||||
2408:8865::/32
|
||||
2408:8866::/32
|
||||
2408:886e::/32
|
||||
2408:886f::/32
|
||||
2408:8872::/32
|
||||
2408:8878::/32
|
||||
2408:8879::/32
|
||||
2408:887e::/32
|
||||
2408:8912::/32
|
||||
2408:8913::/32
|
||||
2408:8914::/32
|
||||
2408:8915::/32
|
||||
2408:8916::/32
|
||||
2408:8917::/32
|
||||
2408:891c::/32
|
||||
2408:8920::/32
|
||||
2408:8924::/32
|
||||
2408:892c::/32
|
||||
2408:8936::/32
|
||||
2408:893a::/32
|
||||
2408:8940::/32
|
||||
2408:8948::/32
|
||||
2408:894c::/32
|
||||
2408:894e::/32
|
||||
2408:8962::/32
|
||||
2408:8963::/32
|
||||
2408:8964::/32
|
||||
2408:8965::/32
|
||||
2408:8966::/32
|
||||
2408:896c::/32
|
||||
2408:896e::/32
|
||||
2408:896f::/32
|
||||
2408:8972::/32
|
||||
2408:8978::/32
|
||||
2408:8979::/32
|
||||
2408:897a::/32
|
||||
2408:897b::/32
|
||||
2408:897e::/32
|
||||
2408:8a21:4000::/36
|
||||
2408:8a21:5000::/36
|
||||
2408:8a22:1000::/36
|
||||
2408:8a22::/36
|
||||
2408:8a23:4000::/36
|
||||
2408:8a23:5000::/36
|
||||
2408:8a23:6000::/36
|
||||
2408:8a23:7000::/36
|
||||
2408:8a24:4000::/36
|
||||
2408:8a24:5000::/36
|
||||
2408:8a24:6000::/36
|
||||
2408:8a24:7000::/36
|
||||
2408:8a26:c000::/36
|
||||
2408:8a26:d000::/36
|
||||
2408:8a26:e000::/36
|
||||
2408:8a26:f000::/36
|
||||
2408:8a27:4000::/36
|
||||
2408:8a27:5000::/36
|
||||
2001:4510
|
||||
2001:4511
|
||||
2402:18a0
|
||||
2402:f140:ff13
|
||||
2402:f140:ff14
|
||||
2404:6500:dcb3
|
||||
2405:1480
|
||||
2406:1e40
|
||||
2406:cac0
|
||||
2407:6c40
|
||||
2407:6c40:1500
|
||||
2408
|
||||
2408:8000
|
||||
2408:8000:10fe
|
||||
2408:8000:10ff
|
||||
2408:8000:2
|
||||
2408:8000:3
|
||||
2408:8000:5005
|
||||
2408:8001
|
||||
2408:802a
|
||||
2408:802c
|
||||
2408:803e
|
||||
2408:8056
|
||||
2408:80c2
|
||||
2408:80c5
|
||||
2408:80e2
|
||||
2408:80e9
|
||||
2408:80f5
|
||||
2408:80f9
|
||||
2408:815f
|
||||
2408:8181
|
||||
2408:8182
|
||||
2408:8183
|
||||
2408:81a2
|
||||
2408:81a3
|
||||
2408:8210
|
||||
2408:8211
|
||||
2408:8212
|
||||
2408:8213
|
||||
2408:8214
|
||||
2408:8215
|
||||
2408:821a
|
||||
2408:821b
|
||||
2408:8220
|
||||
2408:8221
|
||||
2408:8226
|
||||
2408:822a
|
||||
2408:822b
|
||||
2408:822e
|
||||
2408:822f
|
||||
2408:8230
|
||||
2408:8231
|
||||
2408:8232
|
||||
2408:8233
|
||||
2408:8234
|
||||
2408:8235
|
||||
2408:8236
|
||||
2408:8237
|
||||
2408:8238
|
||||
2408:8239
|
||||
2408:823c
|
||||
2408:823d
|
||||
2408:8240
|
||||
2408:8248
|
||||
2408:8249
|
||||
2408:824a
|
||||
2408:824b
|
||||
2408:824c
|
||||
2408:824e
|
||||
2408:824f
|
||||
2408:8250
|
||||
2408:8251
|
||||
2408:8252
|
||||
2408:8253
|
||||
2408:8254
|
||||
2408:825c
|
||||
2408:825d
|
||||
2408:825f
|
||||
2408:8260
|
||||
2408:8262
|
||||
2408:8263
|
||||
2408:8264
|
||||
2408:8265
|
||||
2408:8266
|
||||
2408:826a
|
||||
2408:826c
|
||||
2408:826d
|
||||
2408:826e
|
||||
2408:826f
|
||||
2408:8270
|
||||
2408:8274
|
||||
2408:8275
|
||||
2408:8276
|
||||
2408:8277
|
||||
2408:8278
|
||||
2408:8279
|
||||
2408:827a
|
||||
2408:8310
|
||||
2408:8311
|
||||
2408:8312
|
||||
2408:8313
|
||||
2408:832a
|
||||
2408:832e
|
||||
2408:832f
|
||||
2408:8330
|
||||
2408:8331
|
||||
2408:8332
|
||||
2408:8333
|
||||
2408:8338
|
||||
2408:8340
|
||||
2408:8348
|
||||
2408:8349
|
||||
2408:834a
|
||||
2408:834b
|
||||
2408:834e
|
||||
2408:834f
|
||||
2408:8350
|
||||
2408:8351
|
||||
2408:8352
|
||||
2408:8353
|
||||
2408:8354
|
||||
2408:8360
|
||||
2408:8361
|
||||
2408:8362
|
||||
2408:8363
|
||||
2408:8364
|
||||
2408:8365
|
||||
2408:836c
|
||||
2408:836d
|
||||
2408:836e
|
||||
2408:836f
|
||||
2408:8374
|
||||
2408:8375
|
||||
2408:8376
|
||||
2408:8377
|
||||
2408:8378
|
||||
2408:8379
|
||||
2408:837a
|
||||
2408:8410
|
||||
2408:8411
|
||||
2408:8412
|
||||
2408:8413
|
||||
2408:8414
|
||||
2408:8415
|
||||
2408:8417
|
||||
2408:8418
|
||||
2408:841a
|
||||
2408:841b
|
||||
2408:841c
|
||||
2408:841d
|
||||
2408:841e
|
||||
2408:8420
|
||||
2408:8421
|
||||
2408:8422
|
||||
2408:8426
|
||||
2408:8427
|
||||
2408:842a
|
||||
2408:842b
|
||||
2408:842c
|
||||
2408:842e
|
||||
2408:8430
|
||||
2408:8431
|
||||
2408:8434
|
||||
2408:8435
|
||||
2408:8436
|
||||
2408:8437
|
||||
2408:8438
|
||||
2408:8439
|
||||
2408:843c
|
||||
2408:843d
|
||||
2408:843e
|
||||
2408:843f
|
||||
2408:8440
|
||||
2408:8441
|
||||
2408:8448
|
||||
2408:844b
|
||||
2408:844c
|
||||
2408:844d
|
||||
2408:844e
|
||||
2408:844f
|
||||
2408:8452
|
||||
2408:8453
|
||||
2408:8454
|
||||
2408:8459
|
||||
2408:845c
|
||||
2408:845d
|
||||
2408:8460
|
||||
2408:8461
|
||||
2408:8462
|
||||
2408:8463
|
||||
2408:8464
|
||||
2408:8465
|
||||
2408:8466
|
||||
2408:8469
|
||||
2408:846a
|
||||
2408:846b
|
||||
2408:846c
|
||||
2408:846d
|
||||
2408:846e
|
||||
2408:846f
|
||||
2408:8470
|
||||
2408:8471
|
||||
2408:8474
|
||||
2408:8475
|
||||
2408:8476
|
||||
2408:8477
|
||||
2408:8478
|
||||
2408:8479
|
||||
2408:847a
|
||||
2408:84e3
|
||||
2408:84e4
|
||||
2408:84e5
|
||||
2408:84e6
|
||||
2408:84e7
|
||||
2408:84e9
|
||||
2408:84eb
|
||||
2408:84ec
|
||||
2408:84ed
|
||||
2408:84ee
|
||||
2408:84ef
|
||||
2408:84f0
|
||||
2408:84f1
|
||||
2408:84f2
|
||||
2408:84f4
|
||||
2408:84f5
|
||||
2408:84f6
|
||||
2408:84f7
|
||||
2408:84f8
|
||||
2408:84f9
|
||||
2408:84fa
|
||||
2408:84fb
|
||||
2408:84fc
|
||||
2408:84fd
|
||||
2408:84fe
|
||||
2408:84ff
|
||||
2408:856c
|
||||
2408:856d
|
||||
2408:8610
|
||||
2408:8611
|
||||
2408:8612
|
||||
2408:8613
|
||||
2408:8614
|
||||
2408:8614:e20
|
||||
2408:8615
|
||||
2408:861a
|
||||
2408:861b
|
||||
2408:861c
|
||||
2408:8620
|
||||
2408:8621
|
||||
2408:8624
|
||||
2408:8625
|
||||
2408:8626
|
||||
2408:862a
|
||||
2408:862b
|
||||
2408:862d
|
||||
2408:862e
|
||||
2408:862f
|
||||
2408:8630
|
||||
2408:8631
|
||||
2408:8632
|
||||
2408:8633
|
||||
2408:8634
|
||||
2408:8635
|
||||
2408:8636
|
||||
2408:8637
|
||||
2408:8638
|
||||
2408:8639
|
||||
2408:863c
|
||||
2408:863d
|
||||
2408:8640
|
||||
2408:8642
|
||||
2408:8648
|
||||
2408:8649
|
||||
2408:8649:5a00
|
||||
2408:864c
|
||||
2408:864e
|
||||
2408:864f
|
||||
2408:8650
|
||||
2408:8651
|
||||
2408:8652
|
||||
2408:8653
|
||||
2408:865c
|
||||
2408:865d
|
||||
2408:865f
|
||||
2408:8660
|
||||
2408:8662
|
||||
2408:8663
|
||||
2408:8664
|
||||
2408:8665
|
||||
2408:8666
|
||||
2408:866a
|
||||
2408:866b
|
||||
2408:866c
|
||||
2408:866d
|
||||
2408:866e
|
||||
2408:866f
|
||||
2408:8670
|
||||
2408:8674
|
||||
2408:8675
|
||||
2408:8676
|
||||
2408:8677
|
||||
2408:8678
|
||||
2408:8679
|
||||
2408:867a
|
||||
2408:8710
|
||||
2408:8711
|
||||
2408:8712
|
||||
2408:8713
|
||||
2408:8719
|
||||
2408:871a
|
||||
2408:871b
|
||||
2408:8720
|
||||
2408:8721
|
||||
2408:8722
|
||||
2408:8723
|
||||
2408:8726
|
||||
2408:872b
|
||||
2408:872f
|
||||
2408:8730
|
||||
2408:8731
|
||||
2408:8732
|
||||
2408:8733
|
||||
2408:8734
|
||||
2408:8735
|
||||
2408:8736
|
||||
2408:8738
|
||||
2408:873c
|
||||
2408:873d
|
||||
2408:8740
|
||||
2408:8742
|
||||
2408:8748
|
||||
2408:8749
|
||||
2408:874a
|
||||
2408:874b
|
||||
2408:874c
|
||||
2408:874d
|
||||
2408:874e
|
||||
2408:874f
|
||||
2408:8752
|
||||
2408:875c
|
||||
2408:8760
|
||||
2408:8762
|
||||
2408:8763
|
||||
2408:8764
|
||||
2408:8765
|
||||
2408:8766
|
||||
2408:8768
|
||||
2408:876a
|
||||
2408:876c
|
||||
2408:876d
|
||||
2408:876e
|
||||
2408:876f
|
||||
2408:8770
|
||||
2408:8772
|
||||
2408:8773
|
||||
2408:8774
|
||||
2408:8776
|
||||
2408:8777
|
||||
2408:8778
|
||||
2408:8779
|
||||
2408:877a
|
||||
2408:8812
|
||||
2408:8813
|
||||
2408:8814
|
||||
2408:8815
|
||||
2408:8818
|
||||
2408:8819
|
||||
2408:882c
|
||||
2408:883a
|
||||
2408:8862
|
||||
2408:8863
|
||||
2408:8864
|
||||
2408:8865
|
||||
2408:8866
|
||||
2408:886e
|
||||
2408:886f
|
||||
2408:8872
|
||||
2408:8878
|
||||
2408:8879
|
||||
2408:887e
|
||||
2408:8912
|
||||
2408:8913
|
||||
2408:8914
|
||||
2408:8915
|
||||
2408:8916
|
||||
2408:8917
|
||||
2408:891c
|
||||
2408:8920
|
||||
2408:8924
|
||||
2408:892c
|
||||
2408:8936
|
||||
2408:893a
|
||||
2408:8940
|
||||
2408:8948
|
||||
2408:894c
|
||||
2408:894e
|
||||
2408:8956
|
||||
2408:8957
|
||||
2408:8962
|
||||
2408:8963
|
||||
2408:8964
|
||||
2408:8965
|
||||
2408:8966
|
||||
2408:896c
|
||||
2408:896e
|
||||
2408:896f
|
||||
2408:8972
|
||||
2408:8978
|
||||
2408:8979
|
||||
2408:897a
|
||||
2408:897b
|
||||
2408:897e
|
||||
2408:8a21
|
||||
2408:8a22
|
||||
2408:8a23
|
||||
2408:8a24
|
||||
2408:8a26
|
||||
2408:8a27
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as4837.txt",
|
||||
"count": 470,
|
||||
"sha256": "d109b070f48e97a39e94dc831aab2923b6c440de80621a58319b1ba5926fd60c",
|
||||
"generated_at": "2026-07-20T19:12:53Z"
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as58453.txt",
|
||||
"count": 17,
|
||||
"sha256": "e7e579da764e9dedf84145b5c4cdc51c4b2841517adbc6edb206896b40710119",
|
||||
"generated_at": "2026-07-20T19:12:53Z"
|
||||
}
|
||||
@ -1,2 +1,2 @@
|
||||
2402:4f00:f000::/36
|
||||
2402:4f00:f000::/48
|
||||
2402:4f00
|
||||
2402:4f00:f000
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as58807.txt",
|
||||
"count": 2,
|
||||
"sha256": "0765c7eefa122984fe69678b60ba99aa605d52601ef4ebf988c4e691e91c5361",
|
||||
"generated_at": "2026-07-20T19:12:53Z"
|
||||
}
|
||||
9143
bk/prefix/as9808.txt
9143
bk/prefix/as9808.txt
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as9808.txt",
|
||||
"count": 7170,
|
||||
"sha256": "7532e5bba93ed6b7d5cf010b22d9f5c87606392673fd9877ce28417f2baadd5d",
|
||||
"generated_at": "2026-07-23T02:56:31Z"
|
||||
}
|
||||
@ -1,144 +1,93 @@
|
||||
2408:8120:1::/48
|
||||
2408:8120:2::/48
|
||||
2408:8120:7000::/36
|
||||
2408:8256:226d::/48
|
||||
2408:8256:228b::/48
|
||||
2408:827a:8ff::/48
|
||||
2408:8411:c0c0::/48
|
||||
2408:8411:c0c1::/48
|
||||
2408:8411:c0c2::/48
|
||||
2408:8411:c0c3::/48
|
||||
2408:8411:c0c4::/48
|
||||
2408:8411:c0c5::/48
|
||||
2408:8417:f000::/44
|
||||
2408:8417:f010::/44
|
||||
2408:8417:f020::/44
|
||||
2408:8418:f000::/44
|
||||
2408:8418:f010::/44
|
||||
2408:8418:f020::/44
|
||||
2408:841d:ef20::/44
|
||||
2408:841d:ef30::/44
|
||||
2408:841e:ef10::/44
|
||||
2408:841e:ef20::/44
|
||||
2408:841e:ef30::/44
|
||||
2408:8435:b490::/47
|
||||
2408:8435:b492::/47
|
||||
2408:8435:b494::/47
|
||||
2408:8435:b496::/47
|
||||
2408:8435:b498::/47
|
||||
2408:8435:b49a::/47
|
||||
2408:843e:e010::/48
|
||||
2408:843e:e011::/48
|
||||
2408:843e:e012::/48
|
||||
2408:843e:e080::/48
|
||||
2408:843e:e081::/48
|
||||
2408:843e:e082::/48
|
||||
2408:8441:fe00::/44
|
||||
2408:8441:fe10::/44
|
||||
2408:8441:fe20::/44
|
||||
2408:8441:ff00::/44
|
||||
2408:8441:ff10::/44
|
||||
2408:8441:ff20::/44
|
||||
2408:8444:d500::/44
|
||||
2408:8444:d580::/44
|
||||
2408:8444:d700::/44
|
||||
2408:8444:d780::/44
|
||||
2408:8444:d900::/44
|
||||
2408:8444:d980::/44
|
||||
2408:844c:cb09::/48
|
||||
2408:844c:cb0a::/48
|
||||
2408:844c:cb0b::/48
|
||||
2408:844d:cb09::/48
|
||||
2408:844d:cb0a::/48
|
||||
2408:844d:cb0b::/48
|
||||
2408:844f:d408::/47
|
||||
2408:844f:d40a::/47
|
||||
2408:844f:d40c::/47
|
||||
2408:844f:d500::/47
|
||||
2408:844f:d502::/47
|
||||
2408:844f:d504::/47
|
||||
2408:8462:ff00::/44
|
||||
2408:8462:ff10::/44
|
||||
2408:8462:ff20::/44
|
||||
2408:8463:ff00::/44
|
||||
2408:8463:ff10::/44
|
||||
2408:8463:ff20::/44
|
||||
2408:8469:fc00::/48
|
||||
2408:846a:b600::/44
|
||||
2408:846a:b610::/44
|
||||
2408:846a:b620::/44
|
||||
2408:846b:b600::/44
|
||||
2408:846b:b610::/44
|
||||
2408:846b:b620::/44
|
||||
2408:8471:fa00::/48
|
||||
2408:8471:fa01::/48
|
||||
2408:8471:fa02::/48
|
||||
2408:8471:fb00::/48
|
||||
2408:8471:fb01::/48
|
||||
2408:8471:fb02::/48
|
||||
2408:8474:fb10::/44
|
||||
2408:8474:fb20::/44
|
||||
2408:8474:fb30::/44
|
||||
2408:8474:fc10::/44
|
||||
2408:8474:fc20::/44
|
||||
2408:8474:fc30::/44
|
||||
2408:8478:fb10::/44
|
||||
2408:8478:fb20::/44
|
||||
2408:8478:fb30::/44
|
||||
2408:8478:fc10::/44
|
||||
2408:8478:fc20::/44
|
||||
2408:8478:fc30::/44
|
||||
2408:847a:ff20::/48
|
||||
2408:847a:ff21::/48
|
||||
2408:847a:ff22::/48
|
||||
2408:847a:ff40::/48
|
||||
2408:847a:ff41::/48
|
||||
2408:847a:ff42::/48
|
||||
2408:8610:3bff::/48
|
||||
2408:8614:1f0::/48
|
||||
2408:8614:af0::/48
|
||||
2408:861c:1fff::/48
|
||||
2408:8625:10fb::/48
|
||||
2408:8625:fe::/48
|
||||
2408:8626:f200::/48
|
||||
2408:862b::/48
|
||||
2408:862e:2ff::/48
|
||||
2408:8634:1015::/48
|
||||
2408:8638:116::/48
|
||||
2408:8640:f100::/40
|
||||
2408:8649:1a00::/48
|
||||
2408:8649:2a00::/48
|
||||
2408:864c:10ff::/48
|
||||
2408:8651:ff00::/48
|
||||
2408:8652:ff00::/48
|
||||
2408:8656:a52::/48
|
||||
2408:865c:f0::/44
|
||||
2408:8660:100::/48
|
||||
2408:8660:a100::/48
|
||||
2408:8660:ab00::/48
|
||||
2408:8660:b500::/48
|
||||
2408:8660:bf00::/48
|
||||
2408:8666:ff00::/48
|
||||
2408:866a:a05::/48
|
||||
2408:866c:ff00::/48
|
||||
2408:8670:800::/48
|
||||
2408:8678:1400::/48
|
||||
2408:8756:3efd::/48
|
||||
2408:8a00:c000::/36
|
||||
2408:8a00:d000::/38
|
||||
2408:8a00:d400::/38
|
||||
2408:8a00:e000::/36
|
||||
2408:8a00:f000::/36
|
||||
2408:8a01::/36
|
||||
2408:8a02:b110::/44
|
||||
2408:8a02:b120::/44
|
||||
2408:8a04:8000::/36
|
||||
2408:8a04:e000::/40
|
||||
2408:8a06:100::/48
|
||||
2408:8a06:101::/48
|
||||
2408:8a06:1::/48
|
||||
2408:8a06::/48
|
||||
2408:8a07::/40
|
||||
2408:8a26:ee10::/48
|
||||
2408:8a26:f020::/48
|
||||
2408:8a26:f950::/48
|
||||
2408:8120
|
||||
2408:8120:1
|
||||
2408:8120:2
|
||||
2408:8256:226d
|
||||
2408:8256:228b
|
||||
2408:827a:8ff
|
||||
2408:8411:c0c0
|
||||
2408:8411:c0c1
|
||||
2408:8411:c0c2
|
||||
2408:8411:c0c3
|
||||
2408:8411:c0c4
|
||||
2408:8411:c0c5
|
||||
2408:8417
|
||||
2408:8418
|
||||
2408:841d
|
||||
2408:841e
|
||||
2408:8435
|
||||
2408:843e:e010
|
||||
2408:843e:e011
|
||||
2408:843e:e012
|
||||
2408:843e:e080
|
||||
2408:843e:e081
|
||||
2408:843e:e082
|
||||
2408:8441
|
||||
2408:8444
|
||||
2408:844c:cb09
|
||||
2408:844c:cb0a
|
||||
2408:844c:cb0b
|
||||
2408:844d:cb09
|
||||
2408:844d:cb0a
|
||||
2408:844d:cb0b
|
||||
2408:844f
|
||||
2408:8462
|
||||
2408:8463
|
||||
2408:8469:fc00
|
||||
2408:846a
|
||||
2408:846b
|
||||
2408:8471:fa00
|
||||
2408:8471:fa01
|
||||
2408:8471:fa02
|
||||
2408:8471:fb00
|
||||
2408:8471:fb01
|
||||
2408:8471:fb02
|
||||
2408:8474
|
||||
2408:8478
|
||||
2408:847a:ff20
|
||||
2408:847a:ff21
|
||||
2408:847a:ff22
|
||||
2408:847a:ff40
|
||||
2408:847a:ff41
|
||||
2408:847a:ff42
|
||||
2408:8610:3bff
|
||||
2408:8614:1f0
|
||||
2408:8614:af0
|
||||
2408:861c:1fff
|
||||
2408:8625:10fb
|
||||
2408:8625:fe
|
||||
2408:8626:f200
|
||||
2408:862b
|
||||
2408:862e:2ff
|
||||
2408:8634:1015
|
||||
2408:8638:116
|
||||
2408:8640
|
||||
2408:8649:1a00
|
||||
2408:8649:2a00
|
||||
2408:864c:10ff
|
||||
2408:8651:ff00
|
||||
2408:8652:ff00
|
||||
2408:8656:a52
|
||||
2408:865c
|
||||
2408:8660:100
|
||||
2408:8660:a100
|
||||
2408:8660:ab00
|
||||
2408:8660:b500
|
||||
2408:8660:bf00
|
||||
2408:8666:ff00
|
||||
2408:866a:a05
|
||||
2408:866c:ff00
|
||||
2408:8670:800
|
||||
2408:8678:1400
|
||||
2408:8756:3efd
|
||||
2408:8a00
|
||||
2408:8a01
|
||||
2408:8a02
|
||||
2408:8a04
|
||||
2408:8a06
|
||||
2408:8a06:1
|
||||
2408:8a06:100
|
||||
2408:8a06:101
|
||||
2408:8a07
|
||||
2408:8a26:ee10
|
||||
2408:8a26:f020
|
||||
2408:8a26:f950
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"schema": "backtrace.asn-prefixes/v1",
|
||||
"file": "as9929.txt",
|
||||
"count": 144,
|
||||
"sha256": "a5f65f08a447982cd2c8611005261deab3400f1232ae594fb98f6bfb3fe09084",
|
||||
"generated_at": "2026-07-20T19:12:54Z"
|
||||
}
|
||||
@ -1,294 +0,0 @@
|
||||
package backtrace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ASNPrefixRegistrySchema = "backtrace.asn-prefixes/v1"
|
||||
ASNPrefixRegistryRawBaseURL = "https://raw.githubusercontent.com/oneclickvirt/backtrace/main/bk/prefix"
|
||||
ASNPrefixRegistryCDNBaseURL = "https://cdn.spiritlhl.net/" + ASNPrefixRegistryRawBaseURL
|
||||
)
|
||||
|
||||
var knownPrefixASNs = []string{"AS23764", "AS4134", "AS4809", "AS4837", "AS58453", "AS58807", "AS9808", "AS9929"}
|
||||
var prefixFragmentPattern = regexp.MustCompile(`^[0-9a-fA-F:]+$`)
|
||||
|
||||
type ASNPrefixRegistrySource struct {
|
||||
Name string
|
||||
Base string
|
||||
ValidateManifest bool
|
||||
}
|
||||
|
||||
type ASNPrefixRegistryLoadResult struct {
|
||||
Prefixes map[string][]string
|
||||
Source string
|
||||
Fallback bool
|
||||
}
|
||||
|
||||
var activeASNPrefixes atomic.Value // stores map[string][]string; maps are immutable after publication
|
||||
var refreshASNPrefixesOnce sync.Once
|
||||
|
||||
//go:embed prefix/*.txt.manifest.json
|
||||
var embeddedPrefixManifestFS embed.FS
|
||||
|
||||
func init() {
|
||||
activeASNPrefixes.Store(cloneASNPrefixes(asnPrefixes))
|
||||
}
|
||||
|
||||
func DefaultASNPrefixRegistrySources() []ASNPrefixRegistrySource {
|
||||
return []ASNPrefixRegistrySource{
|
||||
{Name: "cdn", Base: ASNPrefixRegistryCDNBaseURL, ValidateManifest: true},
|
||||
{Name: "raw", Base: ASNPrefixRegistryRawBaseURL, ValidateManifest: true},
|
||||
}
|
||||
}
|
||||
|
||||
func LoadASNPrefixRegistry(ctx context.Context, client *http.Client, sources []ASNPrefixRegistrySource) (ASNPrefixRegistryLoadResult, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 6 * time.Second}
|
||||
}
|
||||
var lastErr error
|
||||
for index, source := range sources {
|
||||
prefixes, err := fetchASNPrefixSource(ctx, client, source)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("load %s ASN prefixes: %w", source.Name, err)
|
||||
continue
|
||||
}
|
||||
return ASNPrefixRegistryLoadResult{Prefixes: prefixes, Source: source.Name, Fallback: index > 0}, nil
|
||||
}
|
||||
embedded := cloneASNPrefixes(asnPrefixes)
|
||||
if len(embedded) > 0 {
|
||||
if err := validateEmbeddedPrefixManifests(); err != nil {
|
||||
return ASNPrefixRegistryLoadResult{}, fmt.Errorf("validate embedded ASN prefix manifests: %w", err)
|
||||
}
|
||||
return ASNPrefixRegistryLoadResult{Prefixes: embedded, Source: "embedded", Fallback: true}, nil
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("no ASN prefix registry sources configured")
|
||||
}
|
||||
return ASNPrefixRegistryLoadResult{}, lastErr
|
||||
}
|
||||
|
||||
func RefreshASNPrefixRegistry(ctx context.Context, client *http.Client, sources []ASNPrefixRegistrySource) (ASNPrefixRegistryLoadResult, error) {
|
||||
loaded, err := LoadASNPrefixRegistry(ctx, client, sources)
|
||||
if err != nil {
|
||||
return ASNPrefixRegistryLoadResult{}, err
|
||||
}
|
||||
activeASNPrefixes.Store(cloneASNPrefixes(loaded.Prefixes))
|
||||
return loaded, nil
|
||||
}
|
||||
|
||||
// StartASNPrefixRefresh performs one bounded background refresh. It never
|
||||
// delays legacy tracing and the embedded map remains active on any error.
|
||||
func StartASNPrefixRefresh() {
|
||||
refreshASNPrefixesOnce.Do(func() {
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_, _ = RefreshASNPrefixRegistry(ctx, nil, DefaultASNPrefixRegistrySources())
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
type prefixManifest struct {
|
||||
Schema string `json:"schema"`
|
||||
File string `json:"file"`
|
||||
Count int `json:"count"`
|
||||
SHA256 string `json:"sha256"`
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
}
|
||||
|
||||
func fetchASNPrefixSource(ctx context.Context, client *http.Client, source ASNPrefixRegistrySource) (map[string][]string, error) {
|
||||
base := source.Base
|
||||
parsed, err := url.Parse(strings.TrimRight(base, "/"))
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Hostname() == "" {
|
||||
return nil, errors.New("invalid ASN prefix source")
|
||||
}
|
||||
type prefixResult struct {
|
||||
asn string
|
||||
values []string
|
||||
err error
|
||||
}
|
||||
results := make(chan prefixResult, len(knownPrefixASNs))
|
||||
for _, asn := range knownPrefixASNs {
|
||||
go func(asn string) {
|
||||
endpoint := strings.TrimRight(base, "/") + "/" + strings.ToLower(asn) + ".txt"
|
||||
var manifestData []byte
|
||||
if source.ValidateManifest {
|
||||
fetchedManifest, fetchErr := fetchPrefixResource(ctx, client, endpoint+".manifest.json")
|
||||
if fetchErr != nil {
|
||||
results <- prefixResult{asn: asn, err: fmt.Errorf("manifest: %w", fetchErr)}
|
||||
return
|
||||
}
|
||||
manifestData = fetchedManifest
|
||||
}
|
||||
data, err := fetchPrefixResource(ctx, client, endpoint)
|
||||
if err != nil {
|
||||
results <- prefixResult{asn: asn, err: err}
|
||||
return
|
||||
}
|
||||
if source.ValidateManifest {
|
||||
if err := validatePrefixManifest(manifestData, data, strings.ToLower(asn)+".txt"); err != nil {
|
||||
results <- prefixResult{asn: asn, err: err}
|
||||
return
|
||||
}
|
||||
}
|
||||
values, parseErr := parseASNPrefixLines(data)
|
||||
results <- prefixResult{asn: asn, values: values, err: parseErr}
|
||||
}(asn)
|
||||
}
|
||||
result := make(map[string][]string, len(knownPrefixASNs))
|
||||
for range knownPrefixASNs {
|
||||
loaded := <-results
|
||||
if loaded.err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", loaded.asn, loaded.err)
|
||||
}
|
||||
result[loaded.asn] = loaded.values
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func fetchPrefixResource(ctx context.Context, client *http.Client, endpoint string) ([]byte, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Accept", "text/plain, application/json")
|
||||
request.Header.Set("User-Agent", "oneclickvirt-backtrace/asn-prefix-registry-v1")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d", response.StatusCode)
|
||||
}
|
||||
return io.ReadAll(io.LimitReader(response.Body, 4<<20))
|
||||
}
|
||||
|
||||
func validatePrefixManifest(data, snapshot []byte, expectedFile string) error {
|
||||
var manifest prefixManifest
|
||||
decoder := json.NewDecoder(strings.NewReader(string(data)))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&manifest); err != nil {
|
||||
return fmt.Errorf("decode manifest: %w", err)
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
return errors.New("manifest contains trailing JSON")
|
||||
}
|
||||
if manifest.Schema != ASNPrefixRegistrySchema || manifest.File != expectedFile || manifest.Count < 1 {
|
||||
return errors.New("manifest schema, file, or count is invalid")
|
||||
}
|
||||
if _, err := time.Parse(time.RFC3339, manifest.GeneratedAt); err != nil {
|
||||
return fmt.Errorf("manifest generated_at is invalid: %w", err)
|
||||
}
|
||||
hash := sha256.Sum256(snapshot)
|
||||
if !strings.EqualFold(manifest.SHA256, hex.EncodeToString(hash[:])) {
|
||||
return errors.New("manifest SHA-256 does not match snapshot")
|
||||
}
|
||||
values, err := parseASNPrefixLines(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(values) != manifest.Count {
|
||||
return fmt.Errorf("manifest count %d does not match snapshot count %d", manifest.Count, len(values))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEmbeddedPrefixManifests() error {
|
||||
for _, asn := range knownPrefixASNs {
|
||||
name := strings.ToLower(asn) + ".txt"
|
||||
manifest, err := embeddedPrefixManifestFS.ReadFile("prefix/" + name + ".manifest.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var snapshot []byte
|
||||
switch asn {
|
||||
case "AS23764":
|
||||
snapshot = []byte(as23764Data)
|
||||
case "AS4134":
|
||||
snapshot = []byte(as4134Data)
|
||||
case "AS4809":
|
||||
snapshot = []byte(as4809Data)
|
||||
case "AS4837":
|
||||
snapshot = []byte(as4837Data)
|
||||
case "AS58453":
|
||||
snapshot = []byte(as58453Data)
|
||||
case "AS58807":
|
||||
snapshot = []byte(as58807Data)
|
||||
case "AS9808":
|
||||
snapshot = []byte(as9808Data)
|
||||
case "AS9929":
|
||||
snapshot = []byte(as9929Data)
|
||||
}
|
||||
if err := validatePrefixManifest(manifest, snapshot, name); err != nil {
|
||||
return fmt.Errorf("%s: %w", asn, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseASNPrefixLines(data []byte) ([]string, error) {
|
||||
seen := make(map[string]struct{})
|
||||
values := make([]string, 0)
|
||||
for _, raw := range strings.Split(string(data), "\n") {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(value, "/") {
|
||||
prefix, err := netip.ParsePrefix(value)
|
||||
if err != nil || !prefix.Addr().Is6() {
|
||||
return nil, fmt.Errorf("invalid IPv6 prefix %q", value)
|
||||
}
|
||||
} else if !prefixFragmentPattern.MatchString(value) {
|
||||
return nil, fmt.Errorf("invalid prefix fragment %q", value)
|
||||
}
|
||||
value = strings.ToLower(value)
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
values = append(values, value)
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nil, errors.New("prefix file is empty")
|
||||
}
|
||||
sort.Strings(values)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func cloneASNPrefixes(input map[string][]string) map[string][]string {
|
||||
result := make(map[string][]string, len(input))
|
||||
for asn, values := range input {
|
||||
result[asn] = append([]string(nil), values...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func currentASNPrefixes() map[string][]string {
|
||||
if value := activeASNPrefixes.Load(); value != nil {
|
||||
return value.(map[string][]string)
|
||||
}
|
||||
return asnPrefixes
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
package backtrace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseASNPrefixLinesNormalizesAndDeduplicates(t *testing.T) {
|
||||
values, err := parseASNPrefixLines([]byte("2409:8000\n2409:8000\n2401:db8::/32\n"))
|
||||
if err != nil || len(values) != 2 || values[0] != "2401:db8::/32" {
|
||||
t.Fatalf("unexpected prefixes: %#v, %v", values, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadASNPrefixRegistryUsesRawAfterCDNFailure(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
path := strings.TrimPrefix(request.URL.Path, "/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) != 2 {
|
||||
http.NotFound(writer, request)
|
||||
return
|
||||
}
|
||||
base, file := parts[0], parts[1]
|
||||
snapshot := []byte("2409:8000\n")
|
||||
if base == "bad" && file == "as23764.txt.manifest.json" {
|
||||
_, _ = writer.Write([]byte(`{}`))
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(file, ".manifest.json") {
|
||||
hash := sha256.Sum256(snapshot)
|
||||
manifest, _ := json.Marshal(prefixManifest{Schema: ASNPrefixRegistrySchema, File: strings.TrimSuffix(file, ".manifest.json"), Count: 1, SHA256: hex.EncodeToString(hash[:]), GeneratedAt: "2026-01-01T00:00:00Z"})
|
||||
_, _ = writer.Write(manifest)
|
||||
return
|
||||
}
|
||||
_, _ = writer.Write(snapshot)
|
||||
}))
|
||||
defer server.Close()
|
||||
sources := []ASNPrefixRegistrySource{{Name: "cdn", Base: server.URL + "/bad", ValidateManifest: true}, {Name: "raw", Base: server.URL + "/good", ValidateManifest: true}}
|
||||
loaded, err := LoadASNPrefixRegistry(context.Background(), server.Client(), sources)
|
||||
if err != nil || loaded.Source != "raw" || !loaded.Fallback || len(loaded.Prefixes) != len(knownPrefixASNs) {
|
||||
t.Fatalf("unexpected load: %+v, %v", loaded, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePrefixManifestRejectsHashAndCountMismatch(t *testing.T) {
|
||||
snapshot := []byte("2409:8000\n")
|
||||
manifest := prefixManifest{Schema: ASNPrefixRegistrySchema, File: "as23764.txt", Count: 2, SHA256: strings.Repeat("0", 64), GeneratedAt: "2026-01-01T00:00:00Z"}
|
||||
data, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := validatePrefixManifest(data, snapshot, "as23764.txt"); err == nil {
|
||||
t.Fatal("invalid manifest unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEmbeddedPrefixManifests(t *testing.T) {
|
||||
if err := validateEmbeddedPrefixManifests(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedManifestFilesArePresent(t *testing.T) {
|
||||
for _, asn := range knownPrefixASNs {
|
||||
name := strings.ToLower(asn) + ".txt.manifest.json"
|
||||
if _, err := embeddedPrefixManifestFS.ReadFile(filepath.Join("prefix", name)); err != nil {
|
||||
t.Fatalf("missing embedded manifest %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadASNPrefixRegistryEmbeddedFallback(t *testing.T) {
|
||||
loaded, err := LoadASNPrefixRegistry(context.Background(), nil, nil)
|
||||
if err != nil || loaded.Source != "embedded" || !loaded.Fallback || len(loaded.Prefixes) == 0 {
|
||||
t.Fatalf("unexpected embedded load: %+v, %v", loaded, err)
|
||||
}
|
||||
}
|
||||
112
cmd/main.go
112
cmd/main.go
@ -1,11 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
@ -34,32 +32,6 @@ type ConcurrentResults struct {
|
||||
// backtraceError error
|
||||
}
|
||||
|
||||
type cliOptions struct {
|
||||
showVersion bool
|
||||
showIPInfo bool
|
||||
help bool
|
||||
ipv6 bool
|
||||
jsonOutput bool
|
||||
deep bool
|
||||
specifiedIP string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func newBacktraceFlagSet(options *cliOptions) *flag.FlagSet {
|
||||
set := flag.NewFlagSet("backtrace", flag.ContinueOnError)
|
||||
set.BoolVar(&options.help, "h", false, "Show help information")
|
||||
set.BoolVar(&options.showVersion, "v", false, "Show version")
|
||||
set.BoolVar(&options.showIPInfo, "s", true, "Disabe show ip info")
|
||||
set.BoolVar(&model.EnableLoger, "log", false, "Enable logging")
|
||||
set.BoolVar(&options.ipv6, "ipv6", false, "Enable ipv6 testing")
|
||||
set.StringVar(&options.specifiedIP, "ip", "", "Specify IP address for bgptools")
|
||||
set.BoolVar(&options.jsonOutput, "json", false, "Output structured RDAP/BGP report as JSON")
|
||||
set.BoolVar(&options.jsonOutput, "structured", false, "Alias for -json")
|
||||
set.BoolVar(&options.deep, "deep", false, "Fetch geofeed and enable WHOIS fallback")
|
||||
set.DurationVar(&options.timeout, "timeout", 15*time.Second, "Structured report timeout")
|
||||
return set
|
||||
}
|
||||
|
||||
func safeGo(wg *sync.WaitGroup, fn func()) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@ -78,52 +50,36 @@ func main() {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
var options cliOptions
|
||||
backtraceFlag := newBacktraceFlagSet(&options)
|
||||
if err := backtraceFlag.Parse(os.Args[1:]); err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
if !options.jsonOutput {
|
||||
fmt.Println(Green("Repo:"), Yellow("https://github.com/oneclickvirt/backtrace"))
|
||||
}
|
||||
if options.help {
|
||||
fmt.Println(Green("Repo:"), Yellow("https://github.com/oneclickvirt/backtrace"))
|
||||
var showVersion, showIpInfo, help, ipv6 bool
|
||||
var specifiedIP string
|
||||
backtraceFlag := flag.NewFlagSet("backtrace", flag.ContinueOnError)
|
||||
backtraceFlag.BoolVar(&help, "h", false, "Show help information")
|
||||
backtraceFlag.BoolVar(&showVersion, "v", false, "Show version")
|
||||
backtraceFlag.BoolVar(&showIpInfo, "s", true, "Disabe show ip info")
|
||||
backtraceFlag.BoolVar(&model.EnableLoger, "log", false, "Enable logging")
|
||||
backtraceFlag.BoolVar(&ipv6, "ipv6", false, "Enable ipv6 testing")
|
||||
backtraceFlag.StringVar(&specifiedIP, "ip", "", "Specify IP address for bgptools")
|
||||
backtraceFlag.Parse(os.Args[1:])
|
||||
if help {
|
||||
fmt.Printf("Usage: %s [options]\n", os.Args[0])
|
||||
backtraceFlag.PrintDefaults()
|
||||
return
|
||||
}
|
||||
if options.showVersion {
|
||||
if showVersion {
|
||||
fmt.Println(model.BackTraceVersion)
|
||||
return
|
||||
}
|
||||
if err := validateStructuredOptions(options); err != nil {
|
||||
fmt.Fprintln(os.Stderr, sanitizeErrorText(err.Error()))
|
||||
os.Exit(2)
|
||||
}
|
||||
if options.jsonOutput {
|
||||
config := bgptools.IPBGPReportConfig{
|
||||
Timeout: options.timeout,
|
||||
FetchGeofeed: options.deep,
|
||||
EnableWHOISFallback: options.deep,
|
||||
ResolveASN: func(ctx context.Context, ip string) (string, error) {
|
||||
return bgptools.ResolveOriginASNWithConfig(ctx, ip, bgptools.OriginASNConfig{Timeout: options.timeout})
|
||||
},
|
||||
}
|
||||
if err := writeStructuredReport(context.Background(), os.Stdout, options.specifiedIP, config, bgptools.QueryIPBGPReport); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "structured report failed: %s\n", sanitizeErrorText(err.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
info := IpInfo{}
|
||||
if options.showIPInfo {
|
||||
if showIpInfo {
|
||||
rsp, err := http.Get("http://ipinfo.io")
|
||||
if err != nil {
|
||||
fmt.Printf("get ip info err %s \n", sanitizeErrorText(err.Error()))
|
||||
fmt.Printf("get ip info err %v \n", err.Error())
|
||||
} else {
|
||||
defer rsp.Body.Close()
|
||||
err = json.NewDecoder(rsp.Body).Decode(&info)
|
||||
if err != nil {
|
||||
fmt.Printf("json decode err %s \n", sanitizeErrorText(err.Error()))
|
||||
fmt.Printf("json decode err %v \n", err.Error())
|
||||
} else {
|
||||
fmt.Println(Green("国家: ") + White(info.Country) + Green(" 城市: ") + White(info.City) +
|
||||
Green(" 服务商: ") + Blue(info.Org))
|
||||
@ -142,7 +98,7 @@ func main() {
|
||||
var useIPv6 bool
|
||||
switch preCheck.StackType {
|
||||
case "DualStack":
|
||||
useIPv6 = options.ipv6
|
||||
useIPv6 = ipv6
|
||||
case "IPv4":
|
||||
useIPv6 = false
|
||||
case "IPv6":
|
||||
@ -158,8 +114,8 @@ func main() {
|
||||
results := ConcurrentResults{}
|
||||
var wg sync.WaitGroup
|
||||
var targetIP string
|
||||
if options.specifiedIP != "" {
|
||||
targetIP = options.specifiedIP
|
||||
if specifiedIP != "" {
|
||||
targetIP = specifiedIP
|
||||
} else if info.Ip != "" {
|
||||
targetIP = info.Ip
|
||||
}
|
||||
@ -186,10 +142,10 @@ func main() {
|
||||
})
|
||||
wg.Wait()
|
||||
if results.bgpResult != "" {
|
||||
fmt.Print(indentLegacyOutput(results.bgpResult))
|
||||
fmt.Print(results.bgpResult)
|
||||
}
|
||||
if results.backtraceResult != "" {
|
||||
fmt.Printf("%s\n", indentLegacyOutput(results.backtraceResult))
|
||||
fmt.Printf("%s\n", results.backtraceResult)
|
||||
}
|
||||
fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考"))
|
||||
fmt.Println(Yellow("同一目标地址多个线路时,检测可能已越过汇聚层,除第一个线路外,后续信息可能无效"))
|
||||
@ -198,29 +154,3 @@ func main() {
|
||||
fmt.Scanln()
|
||||
}
|
||||
}
|
||||
|
||||
func validateStructuredOptions(options cliOptions) error {
|
||||
if options.deep && !options.jsonOutput {
|
||||
return fmt.Errorf("-deep requires -json or -structured")
|
||||
}
|
||||
if !options.jsonOutput {
|
||||
return nil
|
||||
}
|
||||
if options.specifiedIP == "" {
|
||||
return fmt.Errorf("structured report requires -ip")
|
||||
}
|
||||
if options.timeout <= 0 {
|
||||
return fmt.Errorf("structured report timeout must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeStructuredReport(ctx context.Context, output io.Writer, ip string, config bgptools.IPBGPReportConfig, query func(context.Context, string, bgptools.IPBGPReportConfig) (*bgptools.IPBGPReport, error)) error {
|
||||
report, err := query(ctx, ip, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoder := json.NewEncoder(output)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(report)
|
||||
}
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/backtrace/bgptools"
|
||||
)
|
||||
|
||||
func TestBacktraceStructuredFlagParsing(t *testing.T) {
|
||||
var options cliOptions
|
||||
set := newBacktraceFlagSet(&options)
|
||||
if err := set.Parse([]string{"-structured", "-deep", "-ip", "192.0.2.1", "-timeout", "900ms"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !options.jsonOutput || !options.deep || options.specifiedIP != "192.0.2.1" || options.timeout != 900*time.Millisecond {
|
||||
t.Fatalf("unexpected options: %+v", options)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBacktraceDefaultsPreserveLegacyMode(t *testing.T) {
|
||||
var options cliOptions
|
||||
if err := newBacktraceFlagSet(&options).Parse(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !options.showIPInfo || options.jsonOutput || options.deep || options.ipv6 || options.specifiedIP != "" || options.timeout != 15*time.Second {
|
||||
t.Fatalf("legacy defaults changed: %+v", options)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStructuredOptionsRequiresExplicitModeAndTarget(t *testing.T) {
|
||||
tests := []cliOptions{
|
||||
{deep: true, timeout: time.Second},
|
||||
{jsonOutput: true, timeout: time.Second},
|
||||
{jsonOutput: true, specifiedIP: "192.0.2.1"},
|
||||
}
|
||||
for _, options := range tests {
|
||||
if err := validateStructuredOptions(options); err == nil {
|
||||
t.Fatalf("expected invalid structured options: %+v", options)
|
||||
}
|
||||
}
|
||||
if err := validateStructuredOptions(cliOptions{jsonOutput: true, deep: true, specifiedIP: "192.0.2.1", timeout: time.Second}); err != nil {
|
||||
t.Fatalf("valid structured options rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteStructuredReportKeepsStdoutJSONOnly(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
err := writeStructuredReport(context.Background(), &output, "192.0.2.1", bgptools.IPBGPReportConfig{Timeout: time.Second}, func(_ context.Context, ip string, _ bgptools.IPBGPReportConfig) (*bgptools.IPBGPReport, error) {
|
||||
return &bgptools.IPBGPReport{IP: ip, Status: bgptools.ReportAvailable, Sources: []bgptools.IPBGPSourceStatus{}}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var report bgptools.IPBGPReport
|
||||
if err := json.Unmarshal(output.Bytes(), &report); err != nil {
|
||||
t.Fatalf("stdout is not JSON: %v (%q)", err, output.String())
|
||||
}
|
||||
if report.IP != "192.0.2.1" {
|
||||
t.Fatalf("unexpected report: %+v", report)
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var privateErrorPattern = regexp.MustCompile("(?i)(https?://)[^\\s]+")
|
||||
var privateTokenPattern = regexp.MustCompile("(?i)(^|[?&\\s])(token|api[_-]?key|secret|authorization|password|key)=([^&\\s]+)")
|
||||
var privatePathPattern = regexp.MustCompile("(?:[A-Za-z]:\\\\|/)[^\\s]+")
|
||||
|
||||
func indentLegacyOutput(value string) string {
|
||||
lines := strings.Split(value, "\n")
|
||||
for index, line := range lines {
|
||||
if line != "" && line[0] != ' ' && line[0] != '\t' {
|
||||
lines[index] = " " + line
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// sanitizeErrorText keeps remote-source and credential details out of user-visible errors.
|
||||
func sanitizeErrorText(value string) string {
|
||||
value = privateErrorPattern.ReplaceAllString(value, "<remote>")
|
||||
value = privateTokenPattern.ReplaceAllString(value, "$1$2=<redacted>")
|
||||
return privatePathPattern.ReplaceAllString(value, "<path>")
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIndentLegacyOutputLeavesOneLeadingCell(t *testing.T) {
|
||||
got := indentLegacyOutput("alpha\n beta\n\tgamma\n")
|
||||
want := " alpha\n beta\n\tgamma\n"
|
||||
if got != want {
|
||||
t.Fatalf("indentLegacyOutput() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeErrorTextRedactsRemoteSecretsAndPaths(t *testing.T) {
|
||||
input := "load https://private.example/repo/data?token=secret from /private/cache failed: api_key=hidden"
|
||||
got := sanitizeErrorText(input)
|
||||
for _, forbidden := range []string{"private.example", "secret", "hidden", "/private/cache"} {
|
||||
if strings.Contains(got, forbidden) {
|
||||
t.Fatalf("sanitizeErrorText() leaked %q in %q", forbidden, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,207 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/backtrace/bgptools"
|
||||
)
|
||||
|
||||
const defaultSource = "https://raw.githubusercontent.com/xykt/NetQuality/main/ref/AS_Mapping.txt"
|
||||
|
||||
type document struct {
|
||||
Schema string `json:"schema"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Entries []bgptools.ASNMetadata `json:"entries"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Schema string `json:"schema"`
|
||||
File string `json:"file"`
|
||||
Count int `json:"count"`
|
||||
SHA256 string `json:"sha256"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
source := flag.String("source", defaultSource, "ASN name registry URL")
|
||||
output := flag.String("output", "bgptools/data/bgp-asn-map.json", "snapshot output path")
|
||||
minimum := flag.Int("min-count", bgptools.ASNMetadataMinimum, "minimum accepted ASN count")
|
||||
timeout := flag.Duration("timeout", 30*time.Second, "fetch timeout")
|
||||
flag.Parse()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
entries, err := fetchEntries(ctx, http.DefaultClient, *source)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if err := updateSnapshot(*output, entries, *minimum); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchEntries(ctx context.Context, client *http.Client, source string) ([]bgptools.ASNMetadata, error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("User-Agent", "oneclickvirt-backtrace-asn-sync/1")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ASN registry returned HTTP %d", response.StatusCode)
|
||||
}
|
||||
return parseEntries(io.LimitReader(response.Body, 4<<20))
|
||||
}
|
||||
|
||||
func parseEntries(reader io.Reader) ([]bgptools.ASNMetadata, error) {
|
||||
seen := make(map[uint32]string)
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 2 || !strings.HasPrefix(strings.ToUpper(fields[0]), "AS") {
|
||||
continue
|
||||
}
|
||||
number, err := strconv.ParseUint(strings.TrimPrefix(strings.ToUpper(fields[0]), "AS"), 10, 32)
|
||||
if err != nil || number == 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[uint32(number)]; !exists {
|
||||
seen[uint32(number)] = strings.Join(fields[1:], " ")
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := make([]bgptools.ASNMetadata, 0, len(seen))
|
||||
for asn, name := range seen {
|
||||
entries = append(entries, bgptools.ASNMetadata{ASN: asn, Name: name})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].ASN < entries[j].ASN })
|
||||
if len(entries) == 0 {
|
||||
return nil, fmt.Errorf("ASN registry contains no valid entries")
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func updateSnapshot(path string, entries []bgptools.ASNMetadata, minimum int) error {
|
||||
if len(entries) < minimum {
|
||||
return fmt.Errorf("ASN count %d is below minimum %d", len(entries), minimum)
|
||||
}
|
||||
currentData, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var current document
|
||||
if err := json.Unmarshal(currentData, ¤t); err != nil || current.Schema != bgptools.ASNMetadataSchema {
|
||||
return fmt.Errorf("current ASN metadata schema is invalid")
|
||||
}
|
||||
if len(current.Entries) >= minimum && len(entries)*100 < len(current.Entries)*65 {
|
||||
return fmt.Errorf("ASN count dropped from %d to %d", len(current.Entries), len(entries))
|
||||
}
|
||||
if sameEntries(current.Entries, entries) && !current.GeneratedAt.IsZero() {
|
||||
return writeManifest(path, currentData, len(current.Entries))
|
||||
}
|
||||
next, err := json.MarshalIndent(document{Schema: bgptools.ASNMetadataSchema, GeneratedAt: time.Now().UTC(), Entries: entries}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
next = append(next, '\n')
|
||||
temporary, err := os.CreateTemp(filepath.Dir(path), ".asn-metadata-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := temporary.Name()
|
||||
defer os.Remove(name)
|
||||
if _, err := temporary.Write(next); err != nil {
|
||||
temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Chmod(0o644); err != nil {
|
||||
temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(name, path); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeManifest(path, next, len(entries))
|
||||
}
|
||||
|
||||
func writeManifest(snapshotPath string, snapshot []byte, count int) error {
|
||||
var snapshotDocument document
|
||||
if err := json.Unmarshal(snapshot, &snapshotDocument); err != nil {
|
||||
return err
|
||||
}
|
||||
hash := sha256.Sum256(snapshot)
|
||||
value := manifest{
|
||||
Schema: bgptools.ASNMetadataManifestSchema, File: filepath.Base(snapshotPath), Count: count,
|
||||
SHA256: hex.EncodeToString(hash[:]), GeneratedAt: snapshotDocument.GeneratedAt,
|
||||
}
|
||||
data, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
manifestPath := strings.TrimSuffix(snapshotPath, ".json") + ".manifest.json"
|
||||
if current, readErr := os.ReadFile(manifestPath); readErr == nil && string(current) == string(data) {
|
||||
return nil
|
||||
}
|
||||
return writeAtomic(manifestPath, data)
|
||||
}
|
||||
|
||||
func writeAtomic(path string, data []byte) error {
|
||||
temporary, err := os.CreateTemp(filepath.Dir(path), ".asn-metadata-manifest-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := temporary.Name()
|
||||
defer os.Remove(name)
|
||||
if _, err := temporary.Write(data); err != nil {
|
||||
temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Chmod(0o644); err != nil {
|
||||
temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(name, path)
|
||||
}
|
||||
|
||||
func sameEntries(left, right []bgptools.ASNMetadata) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oneclickvirt/backtrace/bgptools"
|
||||
)
|
||||
|
||||
func TestParseEntriesDeduplicatesAndSorts(t *testing.T) {
|
||||
entries, err := parseEntries(strings.NewReader("AS3356 Lumen\nAS174 Cogent\nAS3356 Level3\ninvalid\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 2 || entries[0] != (bgptools.ASNMetadata{ASN: 174, Name: "Cogent"}) || entries[1].Name != "Lumen" {
|
||||
t.Fatalf("unexpected entries: %#v", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSnapshotWritesMatchingManifestWithoutRewritingSnapshot(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "asn.json")
|
||||
current := []byte(`{"schema":"backtrace.asn-metadata/v1","generated_at":"2026-07-20T00:00:00Z","entries":[{"asn":1,"name":"One"},{"asn":2,"name":"Two"}]}`)
|
||||
if err := os.WriteFile(path, current, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries := []bgptools.ASNMetadata{{ASN: 1, Name: "One"}, {ASN: 2, Name: "Two"}}
|
||||
if err := updateSnapshot(path, entries, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, _ := os.ReadFile(path)
|
||||
if string(after) != string(current) {
|
||||
t.Fatal("semantic no-op rewrote snapshot")
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(filepath.Dir(path), "asn.manifest.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var value manifest
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hash := sha256.Sum256(current)
|
||||
if value.Schema != bgptools.ASNMetadataManifestSchema || value.File != "asn.json" || value.Count != 2 || value.SHA256 != hex.EncodeToString(hash[:]) {
|
||||
t.Fatalf("manifest mismatch: %#v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSnapshotRejectsCountDrop(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "asn.json")
|
||||
current := `{"schema":"backtrace.asn-metadata/v1","generated_at":"2026-07-20T00:00:00Z","entries":[{"asn":1,"name":"One"},{"asn":2,"name":"Two"}]}`
|
||||
if err := os.WriteFile(path, []byte(current), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := updateSnapshot(path, []bgptools.ASNMetadata{{ASN: 1, Name: "One"}}, 1); err == nil {
|
||||
t.Fatal("count drop unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
@ -1,280 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var knownASNs = []string{"AS23764", "AS4134", "AS4809", "AS4837", "AS58453", "AS58807", "AS9808", "AS9929"}
|
||||
var errPrefixCountDrop = errors.New("prefix count dropped beyond protection threshold")
|
||||
|
||||
type updateConfig struct {
|
||||
SourceBase string
|
||||
OutputDir string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := updateConfig{}
|
||||
flag.StringVar(&config.SourceBase, "source-base", "https://stat.ripe.net/data/announced-prefixes/data.json", "upstream announced-prefixes API")
|
||||
flag.StringVar(&config.OutputDir, "output-dir", "bk/prefix", "prefix snapshot directory")
|
||||
flag.DurationVar(&config.Timeout, "timeout", 30*time.Second, "upstream request timeout")
|
||||
flag.Parse()
|
||||
if err := updatePrefixes(context.Background(), http.DefaultClient, config); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func updatePrefixes(ctx context.Context, client *http.Client, config updateConfig) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
if config.SourceBase == "" || config.OutputDir == "" || config.Timeout <= 0 {
|
||||
return errors.New("source-base, output-dir, and timeout must be valid")
|
||||
}
|
||||
valuesByASN := make(map[string][]string, len(knownASNs))
|
||||
for _, asn := range knownASNs {
|
||||
requestCtx, cancel := context.WithTimeout(ctx, config.Timeout)
|
||||
endpoint, err := url.Parse(config.SourceBase)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("parse source URL: %w", err)
|
||||
}
|
||||
query := endpoint.Query()
|
||||
query.Set("resource", asn)
|
||||
endpoint.RawQuery = query.Encode()
|
||||
request, err := http.NewRequestWithContext(requestCtx, http.MethodGet, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
request.Header.Set("User-Agent", "oneclickvirt-backtrace-prefix-sync/1")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("fetch %s: %w", asn, err)
|
||||
}
|
||||
body, readErr := io.ReadAll(io.LimitReader(response.Body, 8<<20))
|
||||
response.Body.Close()
|
||||
cancel()
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("fetch %s: HTTP %d", asn, response.StatusCode)
|
||||
}
|
||||
values, err := extractIPv6Prefixes(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse %s: %w", asn, err)
|
||||
}
|
||||
valuesByASN[asn] = values
|
||||
}
|
||||
for _, asn := range knownASNs {
|
||||
path := filepath.Join(config.OutputDir, strings.ToLower(asn)+".txt")
|
||||
if err := writePrefixFile(path, valuesByASN[asn]); err != nil {
|
||||
if errors.Is(err, errPrefixCountDrop) {
|
||||
fmt.Fprintf(os.Stderr, "skip %s: %v\n", asn, err)
|
||||
current, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
valuesByASN[asn] = prefixValuesFromFile(current)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := writePrefixManifest(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type prefixManifest struct {
|
||||
Schema string `json:"schema"`
|
||||
File string `json:"file"`
|
||||
Count int `json:"count"`
|
||||
SHA256 string `json:"sha256"`
|
||||
GeneratedAt string `json:"generated_at"`
|
||||
}
|
||||
|
||||
func prefixValuesFromFile(data []byte) []string {
|
||||
values := make([]string, 0)
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if value := strings.TrimSpace(line); value != "" {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func writePrefixManifest(snapshotPath string) error {
|
||||
snapshot, err := os.ReadFile(snapshotPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values := prefixValuesFromFile(snapshot)
|
||||
if len(values) == 0 {
|
||||
return errors.New("prefix snapshot is empty")
|
||||
}
|
||||
hash := sha256.Sum256(snapshot)
|
||||
fileName := filepath.Base(snapshotPath)
|
||||
sha256Value := hex.EncodeToString(hash[:])
|
||||
manifestPath := snapshotPath + ".manifest.json"
|
||||
if current, readErr := os.ReadFile(manifestPath); readErr == nil {
|
||||
var existing prefixManifest
|
||||
decoder := json.NewDecoder(bytes.NewReader(current))
|
||||
decoder.DisallowUnknownFields()
|
||||
var extra any
|
||||
if decoder.Decode(&existing) == nil && decoder.Decode(&extra) == io.EOF &&
|
||||
existing.Schema == "backtrace.asn-prefixes/v1" && existing.File == fileName &&
|
||||
existing.Count == len(values) && strings.EqualFold(existing.SHA256, sha256Value) {
|
||||
if _, timeErr := time.Parse(time.RFC3339, existing.GeneratedAt); timeErr == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(readErr, os.ErrNotExist) {
|
||||
return readErr
|
||||
}
|
||||
manifest := prefixManifest{Schema: "backtrace.asn-prefixes/v1", File: fileName, Count: len(values), SHA256: sha256Value, GeneratedAt: time.Now().UTC().Format(time.RFC3339)}
|
||||
data, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
return writeAtomicFile(manifestPath, data)
|
||||
}
|
||||
|
||||
func writeAtomicFile(output string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
temporary, err := os.CreateTemp(filepath.Dir(output), ".prefix-manifest-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temporaryName := temporary.Name()
|
||||
defer os.Remove(temporaryName)
|
||||
if err := temporary.Chmod(0o644); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := temporary.Write(data); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Sync(); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(temporaryName, output)
|
||||
}
|
||||
|
||||
func extractIPv6Prefixes(data []byte) ([]string, error) {
|
||||
var response struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Resource string `json:"resource"`
|
||||
Prefixes []struct {
|
||||
Prefix string `json:"prefix"`
|
||||
} `json:"prefixes"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
return nil, fmt.Errorf("decode announced-prefixes response: %w", err)
|
||||
}
|
||||
if response.Status != "ok" || response.Data.Resource == "" || response.Data.Prefixes == nil {
|
||||
return nil, errors.New("announced-prefixes response is missing required fields")
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for _, record := range response.Data.Prefixes {
|
||||
prefix, err := netip.ParsePrefix(strings.TrimSpace(record.Prefix))
|
||||
if err != nil || !prefix.Addr().Is6() || prefix.Bits() < 16 || prefix.Bits() > 128 {
|
||||
continue
|
||||
}
|
||||
seen[prefix.String()] = struct{}{}
|
||||
}
|
||||
values := make([]string, 0, len(seen))
|
||||
for value := range seen {
|
||||
values = append(values, value)
|
||||
}
|
||||
sort.Strings(values)
|
||||
if len(values) == 0 {
|
||||
return nil, errors.New("no IPv6 prefixes found")
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func writePrefixFile(output string, values []string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
candidate := []byte(strings.Join(values, "\n") + "\n")
|
||||
current, readErr := os.ReadFile(output)
|
||||
if readErr == nil {
|
||||
currentCount := countNonemptyLines(current)
|
||||
if currentCount > 0 && len(values)*100 < currentCount*65 {
|
||||
return fmt.Errorf("%w: prefix count dropped from %d to %d for %s", errPrefixCountDrop, currentCount, len(values), filepath.Base(output))
|
||||
}
|
||||
if bytes.Equal(current, candidate) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
|
||||
return readErr
|
||||
}
|
||||
temporary, err := os.CreateTemp(filepath.Dir(output), ".prefix-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
temporaryName := temporary.Name()
|
||||
defer os.Remove(temporaryName)
|
||||
if err := temporary.Chmod(0o644); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := temporary.Write(candidate); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Sync(); err != nil {
|
||||
_ = temporary.Close()
|
||||
return err
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(temporaryName, output)
|
||||
}
|
||||
|
||||
func countNonemptyLines(data []byte) int {
|
||||
count := 0
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.TrimSpace(line) != "" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractIPv6Prefixes(t *testing.T) {
|
||||
values, err := extractIPv6Prefixes([]byte(`{"status":"ok","data":{"resource":"64500","prefixes":[{"prefix":"2409:8000::/32"},{"prefix":"2409:8000::/32"},{"prefix":"192.0.2.0/24"}]}}`))
|
||||
if err != nil || len(values) != 1 || values[0] != "2409:8000::/32" {
|
||||
t.Fatalf("unexpected prefixes: %#v, %v", values, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractIPv6PrefixesRejectsSchemaDrift(t *testing.T) {
|
||||
if _, err := extractIPv6Prefixes([]byte(`{"status":"ok","data":{"resource":"64500"}}`)); err == nil {
|
||||
t.Fatal("missing prefixes unexpectedly accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePrefixFileProtectsLargeCountDrop(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "as64500.txt")
|
||||
if err := os.WriteFile(path, []byte("2001:db8:1::/48\n2001:db8:2::/48\n2001:db8:3::/48\n2001:db8:4::/48\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := writePrefixFile(path, []string{"2001:db8:1::/48"})
|
||||
if !errors.Is(err, errPrefixCountDrop) {
|
||||
t.Fatalf("count drop error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePrefixManifestPreservesGeneratedAtWhenSnapshotIsUnchanged(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
snapshotPath := filepath.Join(directory, "as64500.txt")
|
||||
if err := os.WriteFile(snapshotPath, []byte("2001:db8::/48\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifestPath := snapshotPath + ".manifest.json"
|
||||
old := prefixManifest{Schema: "backtrace.asn-prefixes/v1", File: "as64500.txt", Count: 1, SHA256: "", GeneratedAt: "2020-01-02T03:04:05Z"}
|
||||
data, err := os.ReadFile(snapshotPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Seed a valid manifest with an old timestamp. The updater must retain it.
|
||||
hash := sha256.Sum256(data)
|
||||
old.SHA256 = hex.EncodeToString(hash[:])
|
||||
encoded, err := json.Marshal(old)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(manifestPath, append(encoded, '\n'), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writePrefixManifest(snapshotPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(actual, []byte(`"generated_at":"2020-01-02T03:04:05Z"`)) {
|
||||
t.Fatalf("generated_at was not preserved: %s", actual)
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
const BackTraceVersion = "v0.0.17"
|
||||
const BackTraceVersion = "v0.0.11"
|
||||
|
||||
var EnableLoger = false
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user