Compare commits

...

15 Commits

Author SHA1 Message Date
github-actions
289739fff5 Update README.md with new tag v0.0.6-20250801151556
Some checks failed
创建IPv6检测的前缀 / fetch-ipv6-prefixes (push) Has been cancelled
2025-08-01 15:15:57 +00:00
spiritlhl
be67c981ad fix: 修复颜色和内容 2025-08-01 15:13:59 +00:00
github-actions
80a27a005b Update README.md with new tag v0.0.6-20250801150719 2025-08-01 15:07:20 +00:00
spiritlhl
a6adf423ae fix: 删除突兀的说明 2025-08-01 15:05:00 +00:00
spiritlhl
d6ad936901 fix: 添加颜色显示增加区分度 2025-08-01 15:03:40 +00:00
github-actions[bot]
e089430958 chore: 更新多个 ASN 的 IPv6 前缀 2025-08-01 14:45:05 +00:00
spiritlhl
799d653089 fix: 更新版本 2025-08-01 14:41:33 +00:00
spiritlhl
cfed3329d2 fix: 修复间接上游识别 2025-08-01 14:33:40 +00:00
spiritlhl
461c28f3d1 fix: 修复类型分配 2025-08-01 13:59:14 +00:00
spiritlhl
bf0dc1c95f fix: 修复类型分配 2025-08-01 13:58:55 +00:00
spiritlhl
a36c3eff82 fix: 修复文本生成 2025-08-01 13:41:56 +00:00
spiritlhl
4d5b3fd5cb fix: 修复uuid随机生成 2025-08-01 13:35:42 +00:00
spiritlhl
50b2c706f1 fix: 添加POP点检测 2025-08-01 13:30:46 +00:00
spiritlhl
a9d1782d53
fix: 汇聚层无法通过路由信息查询得到 2025-08-01 19:21:58 +08:00
github-actions
30c9877cdc Update README.md with new tag v0.0.5-20250727160732 2025-07-27 16:07:33 +00:00
9 changed files with 430 additions and 9 deletions

View File

@ -27,7 +27,7 @@ jobs:
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
TAG="v0.0.5-$(date +'%Y%m%d%H%M%S')"
TAG="v0.0.6-$(date +'%Y%m%d%H%M%S')"
git tag $TAG
git push origin $TAG
echo "TAG=$TAG" >> $GITHUB_ENV

View File

@ -15,14 +15,10 @@
- [x] 支持对```CTGNET```、```CN2GIA```和```CN2GT```线路的判断
- [x] 支持对```CMIN2```和```CMI```线路的判断
- [x] 支持对整个回程路由进行线路分析一个目标IP可能会分析出多种线路
- [x] 支持对主流接入点的线路检测,方便分析国际互联能力
- [x] 增加对全平台的编译支持,原版[backtrace](https://github.com/zhanghanyun/backtrace)仅支持linux平台的amd64和arm64架构
- [x] 兼容额外的ICMP地址获取若当前目标IP无法查询路由尝试额外的IP地址
## TODO
- [ ] 自动检测汇聚层,裁剪结果不输出汇聚层后的线路(区分境内外段)
- [ ] 添加对主流ISP的POP点检测区分国际互联能力
## 使用
下载、安装、更新
@ -74,7 +70,7 @@ rm -rf /usr/bin/backtrace
## 在Golang中使用
```
go get github.com/oneclickvirt/backtrace@v0.0.5-20250629024536
go get github.com/oneclickvirt/backtrace@v0.0.6-20250801151556
```
## 概览图

315
bgptools/pop.go Normal file
View File

@ -0,0 +1,315 @@
package bgptools
import (
"fmt"
"html"
"io"
"net"
"regexp"
"strings"
"github.com/google/uuid"
"github.com/imroc/req/v3"
"github.com/oneclickvirt/backtrace/model"
"github.com/oneclickvirt/defaultset"
)
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
Result string
}
func getISPAbbr(asn, name string) string {
if abbr, ok := model.Tier1Global[asn]; ok {
return abbr
}
if idx := strings.Index(name, " "); idx != -1 && idx > 18 {
return name[:idx]
}
return name
}
func getISPType(asn string, tier1 bool, direct bool) string {
switch {
case tier1 && model.Tier1Global[asn] != "":
return "Tier1 Global"
case model.Tier1Regional[asn] != "":
return "Tier1 Regional"
case model.Tier2[asn] != "":
return "Tier2"
case model.ContentProviders[asn] != "":
return "CDN Provider"
case model.IXPS[asn] != "":
return "IXP"
case direct:
return "Direct"
default:
return "Indirect"
}
}
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 := uuid.NewString()
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, true)
upstreams = append(upstreams, Upstream{
ASN: n.ASN,
Name: n.Name,
Direct: true,
Tier1: isTier1,
Type: upstreamType,
})
}
if len(upstreams) == 1 {
currentASN := upstreams[0].ASN
for {
nextUpstreams := map[string]bool{}
for _, e := range edges {
if e.From == currentASN {
nextUpstreams[e.To] = true
}
}
if len(nextUpstreams) != 1 {
break
}
var nextASN string
for asn := range nextUpstreams {
nextASN = asn
break
}
found := false
for _, existing := range upstreams {
if existing.ASN == nextASN {
found = true
break
}
}
if found {
break
}
var nextNode *ASCard
for _, n := range nodes {
if n.ASN == nextASN {
nextNode = &n
break
}
}
if nextNode == nil {
break
}
isTier1 := (nextNode.Fill == "white" && nextNode.Stroke == "#005ea5")
upstreamType := getISPType(nextNode.ASN, isTier1, false)
upstreams = append(upstreams, Upstream{
ASN: nextNode.ASN,
Name: nextNode.Name,
Direct: false,
Tier1: isTier1,
Type: upstreamType,
})
currentASN = nextASN
}
}
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)
colWidth := 18
center := func(s string) string {
runeLen := len([]rune(s))
if runeLen >= colWidth {
return string([]rune(s)[:colWidth])
}
padding := colWidth - runeLen
left := padding / 2
right := padding - left
return strings.Repeat(" ", left) + s + strings.Repeat(" ", right)
}
var result strings.Builder
perLine := 5
for i := 0; i < len(upstreams); i += perLine {
end := i + perLine
if end > len(upstreams) {
end = len(upstreams)
}
batch := upstreams[i:end]
var line1, line2, line3 []string
for _, u := range batch {
abbr := getISPAbbr(u.ASN, u.Name)
asStr := center("AS" + u.ASN)
abbrStr := center(abbr)
typeStr := center(u.Type)
line1 = append(line1, defaultset.White(asStr))
line2 = append(line2, abbrStr)
line3 = append(line3, defaultset.Blue(typeStr))
}
result.WriteString(strings.Join(line1, ""))
result.WriteString("\n")
result.WriteString(strings.Join(line2, ""))
result.WriteString("\n")
result.WriteString(strings.Join(line3, ""))
result.WriteString("\n")
}
return &PoPResult{
TargetASN: targetASN,
Upstreams: upstreams,
Result: result.String(),
}, nil
}

19
bgptools/pop_test.go Normal file
View File

@ -0,0 +1,19 @@
package bgptools
import (
"fmt"
"testing"
)
func TestGetPoPInfo(t *testing.T) {
result, err := GetPoPInfo("23.128.228.123")
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Printf("目标 ASN: %s\n", result.TargetASN)
fmt.Println(len(result.Upstreams))
fmt.Println(result.Upstreams)
fmt.Println("上游信息:")
fmt.Print(result.Result)
}

View File

@ -33,7 +33,9 @@
2409:8004:3821
2409:8004:3822
2409:8004:3841
2409:8004:3842
2409:8004:3843
2409:8004:3844
2409:8004:38c0
2409:8004:801
2409:8004:807

View File

@ -9,6 +9,7 @@ import (
"runtime"
"time"
"github.com/oneclickvirt/backtrace/bgptools"
backtrace "github.com/oneclickvirt/backtrace/bk"
"github.com/oneclickvirt/backtrace/model"
"github.com/oneclickvirt/backtrace/utils"
@ -45,12 +46,12 @@ func main() {
fmt.Println(model.BackTraceVersion)
return
}
info := IpInfo{}
if showIpInfo {
rsp, err := http.Get("http://ipinfo.io")
if err != nil {
fmt.Errorf("Get ip info err %v \n", err.Error())
} else {
info := IpInfo{}
err = json.NewDecoder(rsp.Body).Decode(&info)
if err != nil {
fmt.Errorf("json decode err %v \n", err.Error())
@ -61,6 +62,12 @@ func main() {
}
}
preCheck := utils.CheckPublicAccess(3 * time.Second)
if preCheck.Connected && info.Ip != "" {
result, err := bgptools.GetPoPInfo(info.Ip)
if err == nil {
fmt.Print(result.Result)
}
}
if preCheck.Connected && preCheck.StackType == "DualStack" {
backtrace.BackTrace(ipv6)
} else if preCheck.Connected && preCheck.StackType == "IPv4" {

1
go.mod
View File

@ -3,6 +3,7 @@ module github.com/oneclickvirt/backtrace
go 1.24.5
require (
github.com/google/uuid v1.6.0
github.com/imroc/req/v3 v3.54.0
github.com/oneclickvirt/defaultset v0.0.0-20240624051018-30a50859e1b5
golang.org/x/net v0.41.0

2
go.sum
View File

@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=

View File

@ -2,7 +2,7 @@ package model
import "time"
const BackTraceVersion = "v0.0.5"
const BackTraceVersion = "v0.0.6"
var EnableLoger = false
@ -76,3 +76,82 @@ var (
CachedIcmpDataFetchTime time.Time
ParsedIcmpTargets []IcmpTarget
)
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",
"702": "Verizon",
}
var Tier1Regional = map[string]string{
"4134": "ChinaNet",
"4837": "China Unicom",
"9808": "China Mobile",
"4766": "Korea Telecom",
"2516": "KDDI",
"7713": "Telkomnet",
"9121": "Etisalat",
"7473": "SingTel",
"4637": "Telstra",
"5400": "British Telecom",
"2497": "IIJ",
"3462": "Chunghwa Telecom",
"3463": "TWNIC",
"12389": "SoftBank",
"3303": "MTS",
"45609": "Reliance Jio",
}
var Tier2 = map[string]string{
"6939": "HurricaneElectric",
"20485": "Transtelecom",
"1273": "Vodafone",
"1239": "Sprint",
"6453": "Tata",
"6762": "Sparkle",
"9002": "RETN",
"7922": "Comcast",
"23754": "Rostelecom",
"3320": "DTAG",
}
var ContentProviders = map[string]string{
"15169": "Google",
"32934": "Facebook",
"54113": "Fastly",
"20940": "Akamai",
"13335": "Cloudflare",
"14618": "Amazon AWS",
"55102": "Netflix CDN",
"4685": "CacheFly",
"16509": "Amazon",
"36040": "Amazon CloudFront",
"36459": "EdgeCast",
"24940": "CDNetworks",
}
var IXPS = map[string]string{
"5539": "IX.br",
"25291": "HKIX",
"1200": "AMS-IX",
"6695": "DE-CIX",
"58558": "LINX",
"395848": "France-IX",
"4713": "JPNAP",
"4635": "SIX",
"2906": "MSK-IX",
"1273": "NIX.CZ",
}