mirror of
https://github.com/oneclickvirt/backtrace.git
synced 2025-09-09 03:47:10 +08:00
Compare commits
11 Commits
289739fff5
...
a011cf049f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a011cf049f | ||
![]() |
b68a7e8a20 | ||
![]() |
cd99074950 | ||
![]() |
9737562e39 | ||
![]() |
6f679f403f | ||
![]() |
a74e6c177e | ||
![]() |
02804c1b21 | ||
![]() |
476aac782d | ||
![]() |
4c859e663e | ||
![]() |
1fbb0758b6 | ||
![]() |
7677003308 |
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 概览图
|
## 概览图
|
||||||
|
162
bgptools/pop.go
162
bgptools/pop.go
@ -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)
|
||||||
}
|
}
|
||||||
|
20
cmd/main.go
20
cmd/main.go
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user