Compare commits

...

11 Commits

Author SHA1 Message Date
spiritlhl
a011cf049f
fix: 更新说明
Some checks failed
创建IPv6检测的前缀 / fetch-ipv6-prefixes (push) Has been cancelled
2025-08-05 21:08:46 +08:00
spiritlhl
b68a7e8a20
fix 2025-08-05 21:02:54 +08:00
spiritlhl
cd99074950
fix: 更新说明 2025-08-05 21:00:24 +08:00
spiritlhl
9737562e39
fix: 修复IP指定问题 2025-08-05 20:59:22 +08:00
github-actions
6f679f403f Update README.md with new tag v0.0.6-20250805091811 2025-08-05 09:18:12 +00:00
spiritlhl
a74e6c177e
fix: 区分直接接入和间接接入的Tier1 2025-08-05 17:14:07 +08:00
github-actions
02804c1b21 Update README.md with new tag v0.0.6-20250805082719 2025-08-05 08:27:20 +00:00
spiritlhl
476aac782d
fix: 减少重试次数 2025-08-05 16:26:03 +08:00
spiritlhl
4c859e663e
fix: 减少等待时长 2025-08-05 16:21:53 +08:00
github-actions
1fbb0758b6 Update README.md with new tag v0.0.6-20250805081606 2025-08-05 08:16:06 +00:00
spiritlhl
7677003308 fix: 加入重试机制避免一次请求失败则全部失败 2025-08-05 08:12:52 +00:00
3 changed files with 152 additions and 54 deletions

View File

@ -19,6 +19,8 @@
- [x] 增加对全平台的编译支持,原版[backtrace](https://github.com/zhanghanyun/backtrace)仅支持linux平台的amd64和arm64架构 - [x] 增加对全平台的编译支持,原版[backtrace](https://github.com/zhanghanyun/backtrace)仅支持linux平台的amd64和arm64架构
- [x] 兼容额外的ICMP地址获取若当前目标IP无法查询路由尝试额外的IP地址 - [x] 兼容额外的ICMP地址获取若当前目标IP无法查询路由尝试额外的IP地址
相关输出和查询结果的说明:[跳转](https://github.com/oneclickvirt/ecs/blob/master/README_NEW_USER.md#%E4%B8%8A%E6%B8%B8%E5%8F%8A%E5%9B%9E%E7%A8%8B%E7%BA%BF%E8%B7%AF%E6%A3%80%E6%B5%8B)
## 使用 ## 使用
下载、安装、更新 下载、安装、更新
@ -52,6 +54,8 @@ backtrace
``` ```
Usage: backtrace [options] Usage: backtrace [options]
-h Show help information -h Show help information
-ip string
Specify IP address for bgptools
-ipv6 -ipv6
Enable ipv6 testing Enable ipv6 testing
-log -log
@ -70,7 +74,7 @@ rm -rf /usr/bin/backtrace
## 在Golang中使用 ## 在Golang中使用
``` ```
go get github.com/oneclickvirt/backtrace@v0.0.6-20250801151556 go get github.com/oneclickvirt/backtrace@v0.0.6-20250805091811
``` ```
## 概览图 ## 概览图

View File

@ -7,6 +7,7 @@ import (
"net" "net"
"regexp" "regexp"
"strings" "strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
@ -41,22 +42,55 @@ type PoPResult struct {
Result string Result string
} }
type retryConfig struct {
maxRetries int
timeouts []time.Duration
}
var defaultRetryConfig = retryConfig{
maxRetries: 2,
timeouts: []time.Duration{5 * time.Second, 6 * time.Second},
}
func executeWithRetry(client *req.Client, url string, config retryConfig) (*req.Response, error) {
var lastErr error
for attempt := 0; attempt < config.maxRetries; attempt++ {
timeout := config.timeouts[attempt]
resp, err := client.SetTimeout(timeout).R().
Get(url)
if err == nil && resp.StatusCode == 200 {
return resp, nil
}
if err != nil {
lastErr = fmt.Errorf("attempt %d failed with timeout %v: %w", attempt+1, timeout, err)
} else {
lastErr = fmt.Errorf("attempt %d failed with HTTP status %d (timeout %v)", attempt+1, resp.StatusCode, timeout)
}
if attempt < config.maxRetries-1 {
time.Sleep(1 * time.Second)
}
}
return nil, fmt.Errorf("all %d attempts failed, last error: %w", config.maxRetries, lastErr)
}
func getISPAbbr(asn, name string) string { func getISPAbbr(asn, name string) string {
if abbr, ok := model.Tier1Global[asn]; ok { if abbr, ok := model.Tier1Global[asn]; ok {
return abbr return abbr
} }
if idx := strings.Index(name, " "); idx != -1 && idx > 18 { if idx := strings.Index(name, " "); idx != -1 && idx >= 18 {
return name[:idx] return name[:idx]
} }
return name return strings.TrimSpace(name)
} }
func getISPType(asn string, tier1 bool, direct bool) string { func getISPType(asn string, tier1 bool, direct bool) string {
switch { switch {
case tier1 && model.Tier1Global[asn] != "": case tier1 && direct && model.Tier1Global[asn] != "":
return "Tier1 Global" return "Tier1 Global"
case model.Tier1Regional[asn] != "": case tier1 && direct && model.Tier1Regional[asn] != "":
return "Tier1 Regional" return "Tier1 Regional"
case tier1 && !direct:
return "Tier1 Indirect"
case model.Tier2[asn] != "": case model.Tier2[asn] != "":
return "Tier2" return "Tier2"
case model.ContentProviders[asn] != "": case model.ContentProviders[asn] != "":
@ -74,42 +108,54 @@ func isValidIP(ip string) bool {
return net.ParseIP(ip) != nil return net.ParseIP(ip) != nil
} }
func getSVGPath(client *req.Client, ip string) (string, error) { func getSVGPath(ip string) (string, error) {
if !isValidIP(ip) { if !isValidIP(ip) {
return "", fmt.Errorf("invalid IP address: %s", ip) return "", fmt.Errorf("invalid IP address: %s", ip)
} }
var lastErr error
for attempt := 0; attempt < defaultRetryConfig.maxRetries; attempt++ {
client := req.C().ImpersonateChrome()
url := fmt.Sprintf("https://bgp.tools/prefix/%s#connectivity", ip) url := fmt.Sprintf("https://bgp.tools/prefix/%s#connectivity", ip)
resp, err := client.R().Get(url) resp, err := executeWithRetry(client, url, defaultRetryConfig)
if err != nil { 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() body := resp.String()
re := regexp.MustCompile(`<img[^>]+id="pathimg"[^>]+src="([^"]+)"`) re := regexp.MustCompile(`<img[^>]+id="pathimg"[^>]+src="([^"]+)"`)
matches := re.FindStringSubmatch(body) matches := re.FindStringSubmatch(body)
if len(matches) < 2 { if len(matches) >= 2 {
return "", fmt.Errorf("SVG path not found for IP %s", ip)
}
return matches[1], nil return matches[1], nil
}
lastErr = fmt.Errorf("SVG path not found for IP %s", ip)
} else {
lastErr = fmt.Errorf("failed to fetch BGP info for IP %s: %w", ip, err)
}
if attempt < defaultRetryConfig.maxRetries-1 {
time.Sleep(1 * time.Second)
}
}
return "", fmt.Errorf("failed to get SVG path after %d retries: %w", defaultRetryConfig.maxRetries, lastErr)
} }
func downloadSVG(client *req.Client, svgPath string) (string, error) { func downloadSVG(svgPath string) (string, error) {
var lastErr error
for attempt := 0; attempt < defaultRetryConfig.maxRetries; attempt++ {
client := req.C().ImpersonateChrome()
uuid := uuid.NewString() uuid := uuid.NewString()
url := fmt.Sprintf("https://bgp.tools%s?%s&loggedin", svgPath, uuid) url := fmt.Sprintf("https://bgp.tools%s?%s&loggedin", svgPath, uuid)
resp, err := client.R().Get(url) resp, err := executeWithRetry(client, url, defaultRetryConfig)
if err != nil { 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) bodyBytes, err := io.ReadAll(resp.Body)
if err != nil { if err == nil {
return "", fmt.Errorf("failed to read SVG response body: %w", err)
}
return string(bodyBytes), nil return string(bodyBytes), nil
}
lastErr = fmt.Errorf("failed to read SVG response body: %w", err)
} else {
lastErr = fmt.Errorf("failed to download SVG: %w", err)
}
if attempt < defaultRetryConfig.maxRetries-1 {
time.Sleep(1 * time.Second)
}
}
return "", fmt.Errorf("failed to download SVG after %d retries: %w", defaultRetryConfig.maxRetries, lastErr)
} }
func parseASAndEdges(svg string) ([]ASCard, []Arrow) { func parseASAndEdges(svg string) ([]ASCard, []Arrow) {
@ -183,6 +229,7 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream {
} }
} }
var upstreams []Upstream var upstreams []Upstream
addedASNs := map[string]bool{}
for _, n := range nodes { for _, n := range nodes {
if !upstreamMap[n.ASN] { if !upstreamMap[n.ASN] {
continue continue
@ -196,6 +243,7 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream {
Tier1: isTier1, Tier1: isTier1,
Type: upstreamType, Type: upstreamType,
}) })
addedASNs[n.ASN] = true
} }
if len(upstreams) == 1 { if len(upstreams) == 1 {
currentASN := upstreams[0].ASN currentASN := upstreams[0].ASN
@ -214,14 +262,7 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream {
nextASN = asn nextASN = asn
break break
} }
found := false if addedASNs[nextASN] {
for _, existing := range upstreams {
if existing.ASN == nextASN {
found = true
break
}
}
if found {
break break
} }
var nextNode *ASCard var nextNode *ASCard
@ -243,8 +284,56 @@ func findUpstreams(targetASN string, nodes []ASCard, edges []Arrow) []Upstream {
Tier1: isTier1, Tier1: isTier1,
Type: upstreamType, Type: upstreamType,
}) })
addedASNs[nextNode.ASN] = true
currentASN = nextASN currentASN = nextASN
} }
} else if len(upstreams) > 1 {
for _, directUpstream := range upstreams {
currentASN := directUpstream.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
}
if addedASNs[nextASN] {
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")
if isTier1 {
upstreamType := getISPType(nextNode.ASN, isTier1, false)
upstreams = append(upstreams, Upstream{
ASN: nextNode.ASN,
Name: nextNode.Name,
Direct: false,
Tier1: isTier1,
Type: upstreamType,
})
addedASNs[nextNode.ASN] = true
break
}
currentASN = nextASN
}
}
} }
return upstreams return upstreams
} }
@ -253,12 +342,11 @@ func GetPoPInfo(ip string) (*PoPResult, error) {
if ip == "" { if ip == "" {
return nil, fmt.Errorf("IP address cannot be empty") return nil, fmt.Errorf("IP address cannot be empty")
} }
client := req.C().ImpersonateChrome() svgPath, err := getSVGPath(ip)
svgPath, err := getSVGPath(client, ip)
if err != nil { if err != nil {
return nil, fmt.Errorf("获取SVG路径失败: %w", err) return nil, fmt.Errorf("获取SVG路径失败: %w", err)
} }
svg, err := downloadSVG(client, svgPath) svg, err := downloadSVG(svgPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("下载SVG失败: %w", err) return nil, fmt.Errorf("下载SVG失败: %w", err)
} }

View File

@ -1,5 +1,4 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
@ -8,14 +7,12 @@ import (
"os" "os"
"runtime" "runtime"
"time" "time"
"github.com/oneclickvirt/backtrace/bgptools" "github.com/oneclickvirt/backtrace/bgptools"
backtrace "github.com/oneclickvirt/backtrace/bk" backtrace "github.com/oneclickvirt/backtrace/bk"
"github.com/oneclickvirt/backtrace/model" "github.com/oneclickvirt/backtrace/model"
"github.com/oneclickvirt/backtrace/utils" "github.com/oneclickvirt/backtrace/utils"
. "github.com/oneclickvirt/defaultset" . "github.com/oneclickvirt/defaultset"
) )
type IpInfo struct { type IpInfo struct {
Ip string `json:"ip"` Ip string `json:"ip"`
City string `json:"city"` City string `json:"city"`
@ -23,19 +20,20 @@ type IpInfo struct {
Country string `json:"country"` Country string `json:"country"`
Org string `json:"org"` Org string `json:"org"`
} }
func main() { func main() {
go func() { go func() {
http.Get("https://hits.spiritlhl.net/backtrace.svg?action=hit&title=Hits&title_bg=%23555555&count_bg=%230eecf8&edge_flat=false") http.Get("https://hits.spiritlhl.net/backtrace.svg?action=hit&title=Hits&title_bg=%23555555&count_bg=%230eecf8&edge_flat=false")
}() }()
fmt.Println(Green("Repo:"), Yellow("https://github.com/oneclickvirt/backtrace")) fmt.Println(Green("Repo:"), Yellow("https://github.com/oneclickvirt/backtrace"))
var showVersion, showIpInfo, help, ipv6 bool var showVersion, showIpInfo, help, ipv6 bool
var specifiedIP string
backtraceFlag := flag.NewFlagSet("backtrace", flag.ContinueOnError) backtraceFlag := flag.NewFlagSet("backtrace", flag.ContinueOnError)
backtraceFlag.BoolVar(&help, "h", false, "Show help information") backtraceFlag.BoolVar(&help, "h", false, "Show help information")
backtraceFlag.BoolVar(&showVersion, "v", false, "Show version") backtraceFlag.BoolVar(&showVersion, "v", false, "Show version")
backtraceFlag.BoolVar(&showIpInfo, "s", true, "Disabe show ip info") backtraceFlag.BoolVar(&showIpInfo, "s", true, "Disabe show ip info")
backtraceFlag.BoolVar(&model.EnableLoger, "log", false, "Enable logging") backtraceFlag.BoolVar(&model.EnableLoger, "log", false, "Enable logging")
backtraceFlag.BoolVar(&ipv6, "ipv6", false, "Enable ipv6 testing") backtraceFlag.BoolVar(&ipv6, "ipv6", false, "Enable ipv6 testing")
backtraceFlag.StringVar(&specifiedIP, "ip", "", "Specify IP address for bgptools")
backtraceFlag.Parse(os.Args[1:]) backtraceFlag.Parse(os.Args[1:])
if help { if help {
fmt.Printf("Usage: %s [options]\n", os.Args[0]) fmt.Printf("Usage: %s [options]\n", os.Args[0])
@ -62,12 +60,20 @@ func main() {
} }
} }
preCheck := utils.CheckPublicAccess(3 * time.Second) preCheck := utils.CheckPublicAccess(3 * time.Second)
if preCheck.Connected && info.Ip != "" { if preCheck.Connected {
result, err := bgptools.GetPoPInfo(info.Ip) var targetIP string
if specifiedIP != "" {
targetIP = specifiedIP
} else if info.Ip != "" {
targetIP = info.Ip
}
if targetIP != "" {
result, err := bgptools.GetPoPInfo(targetIP)
if err == nil { if err == nil {
fmt.Print(result.Result) fmt.Print(result.Result)
} }
} }
}
if preCheck.Connected && preCheck.StackType == "DualStack" { if preCheck.Connected && preCheck.StackType == "DualStack" {
backtrace.BackTrace(ipv6) backtrace.BackTrace(ipv6)
} else if preCheck.Connected && preCheck.StackType == "IPv4" { } else if preCheck.Connected && preCheck.StackType == "IPv4" {
@ -78,7 +84,7 @@ func main() {
fmt.Println(Red("PreCheck IP Type Failed")) fmt.Println(Red("PreCheck IP Type Failed"))
} }
fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考")) fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考"))
fmt.Println(Yellow("同一目标地址多个线路时,可能检测已越过汇聚层,除第一个线路外,后续信息可能无效")) fmt.Println(Yellow("同一目标地址多个线路时,检测可能已越过汇聚层,除第一个线路外,后续信息可能无效"))
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
fmt.Println("Press Enter to exit...") fmt.Println("Press Enter to exit...")
fmt.Scanln() fmt.Scanln()