From 50b2c706f17c304cdf8ca8f54561f8919cf8d173 Mon Sep 17 00:00:00 2001 From: spiritlhl <103393591+spiritLHLS@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:30:46 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0POP=E7=82=B9=E6=A3=80?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bgptools/pop.go | 234 +++++++++++++++++++++++++++++++++++++++++++ bgptools/pop_test.go | 20 ++++ 2 files changed, 254 insertions(+) create mode 100644 bgptools/pop.go create mode 100644 bgptools/pop_test.go diff --git a/bgptools/pop.go b/bgptools/pop.go new file mode 100644 index 0000000..310bcf8 --- /dev/null +++ b/bgptools/pop.go @@ -0,0 +1,234 @@ +package bgptools + +import ( + "fmt" + "html" + "io" + "net" + "regexp" + "strings" + + "github.com/imroc/req/v3" +) + +type ASCard struct { + ASN string + Name string + Fill string + Stroke string + ID string +} + +type Arrow struct { + From string + To string +} + +type Upstream struct { + ASN string + Name string + Direct bool + Tier1 bool + Type string // 新增类型字段 +} + +type PoPResult struct { + TargetASN string + Upstreams []Upstream +} + +var tier1Global = map[string]string{ + "174": "Cogent", + "1299": "Arelion", + "3356": "Lumen", + "3257": "GTT", + "7018": "AT&T", + "701": "Verizon", + "2914": "NTT", + "6453": "Tata", + "3320": "DTAG", + "5511": "Orange", + "3491": "PCCW", + "6461": "Zayo", + "6830": "Liberty", + "6762": "Sparkle", + "12956": "Telxius", +} + +func getISPAbbr(asn, name string) string { + if abbr, ok := tier1Global[asn]; ok { + return abbr + } + if idx := strings.Index(name, " "); idx != -1 { + return name[:idx] + } + return name +} + +func getISPType(asn string, tier1 bool) string { + if tier1 { + if _, ok := tier1Global[asn]; ok { + return "Tier1 Global" + } + return "Tier1 Regional" + } + return "Direct" +} + +func isValidIP(ip string) bool { + return net.ParseIP(ip) != nil +} + +func getSVGPath(client *req.Client, ip string) (string, error) { + if !isValidIP(ip) { + return "", fmt.Errorf("invalid IP address: %s", ip) + } + url := fmt.Sprintf("https://bgp.tools/prefix/%s#connectivity", ip) + resp, err := client.R().Get(url) + if err != nil { + return "", fmt.Errorf("failed to fetch BGP info for IP %s: %w", ip, err) + } + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP error %d when fetching BGP info for IP %s", resp.StatusCode, ip) + } + body := resp.String() + re := regexp.MustCompile(`]+id="pathimg"[^>]+src="([^"]+)"`) + matches := re.FindStringSubmatch(body) + if len(matches) < 2 { + return "", fmt.Errorf("SVG path not found for IP %s", ip) + } + return matches[1], nil +} + +func downloadSVG(client *req.Client, svgPath string) (string, error) { + uuid := "fixeduuid123456" + url := fmt.Sprintf("https://bgp.tools%s?%s&loggedin", svgPath, uuid) + resp, err := client.R().Get(url) + if err != nil { + return "", fmt.Errorf("failed to download SVG: %w", err) + } + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP error %d when downloading SVG", resp.StatusCode) + } + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read SVG response body: %w", err) + } + return string(bodyBytes), nil +} + +func parseASAndEdges(svg string) ([]ASCard, []Arrow) { + svg = html.UnescapeString(svg) + var nodes []ASCard + var edges []Arrow + nodeRE := regexp.MustCompile(`(?s)(.*?)`) + edgeRE := regexp.MustCompile(`(?s)(.*?)`) + asnRE := regexp.MustCompile(`AS(\d+)`) + nameRE := regexp.MustCompile(`xlink:title="([^"]+)"`) + fillRE := regexp.MustCompile(`]+fill="([^"]+)"`) + strokeRE := regexp.MustCompile(`]+stroke="([^"]+)"`) + titleRE := regexp.MustCompile(`AS(\d+)->AS(\d+)`) + for _, match := range nodeRE.FindAllStringSubmatch(svg, -1) { + block := match[1] + asn := "" + if a := asnRE.FindStringSubmatch(block); len(a) > 1 { + asn = a[1] + } + name := "unknown" + if n := nameRE.FindStringSubmatch(block); len(n) > 1 { + name = strings.TrimSpace(n[1]) + } + fill := "none" + if f := fillRE.FindStringSubmatch(block); len(f) > 1 { + fill = f[1] + } + stroke := "none" + if s := strokeRE.FindStringSubmatch(block); len(s) > 1 { + stroke = s[1] + } + if asn != "" { + nodes = append(nodes, ASCard{ + ASN: asn, + Name: name, + Fill: fill, + Stroke: stroke, + ID: "", + }) + } + } + for _, match := range edgeRE.FindAllStringSubmatch(svg, -1) { + block := match[1] + if t := titleRE.FindStringSubmatch(block); len(t) == 3 { + edges = append(edges, Arrow{ + From: t[1], + To: t[2], + }) + } + } + return nodes, edges +} + +func findTargetASN(nodes []ASCard) string { + for _, n := range nodes { + if n.Fill == "limegreen" || n.Stroke == "limegreen" || n.Fill == "green" { + return n.ASN + } + } + if len(nodes) > 0 { + return nodes[0].ASN + } + return "" +} + +func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream { + upstreamMap := map[string]bool{} + for _, e := range edges { + if e.From == targetASN { + upstreamMap[e.To] = true + } + } + var upstreams []Upstream + for _, n := range nodes { + if !upstreamMap[n.ASN] { + continue + } + isTier1 := (n.Fill == "white" && n.Stroke == "#005ea5") + upstreamType := getISPType(n.ASN, isTier1) + upstreams = append(upstreams, Upstream{ + ASN: n.ASN, + Name: n.Name, + Direct: true, + Tier1: isTier1, + Type: upstreamType, + }) + } + return upstreams +} + +func GetPoPInfo(ip string) (*PoPResult, error) { + if ip == "" { + return nil, fmt.Errorf("IP address cannot be empty") + } + client := req.C().ImpersonateChrome() + svgPath, err := getSVGPath(client, ip) + if err != nil { + return nil, fmt.Errorf("获取SVG路径失败: %w", err) + } + svg, err := downloadSVG(client, svgPath) + if err != nil { + return nil, fmt.Errorf("下载SVG失败: %w", err) + } + nodes, edges := parseASAndEdges(svg) + if len(nodes) == 0 { + return nil, fmt.Errorf("未找到任何AS节点") + } + targetASN := findTargetASN(nodes) + if targetASN == "" { + return nil, fmt.Errorf("无法识别目标 ASN") + } + upstreams := findUpstreams(targetASN, nodes, edges) + return &PoPResult{ + TargetASN: targetASN, + Upstreams: upstreams, + }, nil +} diff --git a/bgptools/pop_test.go b/bgptools/pop_test.go new file mode 100644 index 0000000..297d48d --- /dev/null +++ b/bgptools/pop_test.go @@ -0,0 +1,20 @@ +package bgptools + +import ( + "fmt" + "testing" +) + +func TestGetPoPInfo(t *testing.T) { + result, err := GetPoPInfo("66.70.153.71") + if err != nil { + fmt.Println(err.Error()) + return + } + fmt.Printf("目标 ASN: %s\n", result.TargetASN) + fmt.Println("上游信息:") + for _, u := range result.Upstreams { + abbr := getISPAbbr(u.ASN, u.Name) + fmt.Printf("AS%s - %s [%s]\n", u.ASN, abbr, u.Type) + } +}