mirror of
				https://github.com/oneclickvirt/backtrace.git
				synced 2025-11-04 15:52:37 +08:00 
			
		
		
		
	fix: 添加POP点检测
This commit is contained in:
		
							parent
							
								
									a9d1782d53
								
							
						
					
					
						commit
						50b2c706f1
					
				
							
								
								
									
										234
									
								
								bgptools/pop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								bgptools/pop.go
									
									
									
									
									
										Normal file
									
								
							@ -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(`<img[^>]+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)<g id="node\d+" class="node">(.*?)</g>`)
 | 
			
		||||
	edgeRE := regexp.MustCompile(`(?s)<g id="edge\d+" class="edge">(.*?)</g>`)
 | 
			
		||||
	asnRE := regexp.MustCompile(`<title>AS(\d+)</title>`)
 | 
			
		||||
	nameRE := regexp.MustCompile(`xlink:title="([^"]+)"`)
 | 
			
		||||
	fillRE := regexp.MustCompile(`<polygon[^>]+fill="([^"]+)"`)
 | 
			
		||||
	strokeRE := regexp.MustCompile(`<polygon[^>]+stroke="([^"]+)"`)
 | 
			
		||||
	titleRE := regexp.MustCompile(`<title>AS(\d+)->AS(\d+)</title>`)
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								bgptools/pop_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								bgptools/pop_test.go
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user