diff --git a/cmd/main.go b/cmd/main.go index 6ec712c..fa15297 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,9 +7,11 @@ import ( "net/http" "os" "runtime" + "time" backtrace "github.com/oneclickvirt/backtrace/bk" "github.com/oneclickvirt/backtrace/model" + "github.com/oneclickvirt/backtrace/utils" . "github.com/oneclickvirt/defaultset" ) @@ -25,7 +27,7 @@ func main() { go func() { http.Get("https://hits.spiritlhl.net/backtrace.svg?action=hit&title=Hits&title_bg=%23555555&count_bg=%230eecf8&edge_flat=false") }() - fmt.Println(Green("项目地址:"), Yellow("https://github.com/oneclickvirt/backtrace")) + fmt.Println(Green("Repo:"), Yellow("https://github.com/oneclickvirt/backtrace")) var showVersion, showIpInfo, help, ipv6 bool backtraceFlag := flag.NewFlagSet("backtrace", flag.ContinueOnError) backtraceFlag.BoolVar(&help, "h", false, "Show help information") @@ -58,7 +60,16 @@ func main() { } } } - backtrace.BackTrace(ipv6) + preCheck := utils.CheckPublicAccess(3 * time.Second) + if preCheck.Connected && preCheck.StackType == "DualStack" { + backtrace.BackTrace(ipv6) + } else if preCheck.Connected && preCheck.StackType == "IPv4" { + backtrace.BackTrace(false) + } else if preCheck.Connected && preCheck.StackType == "IPv6" { + backtrace.BackTrace(true) + } else { + fmt.Println(Red("PreCheck IP Type Failed")) + } fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考")) fmt.Println(Yellow("同一目标地址多个线路时,可能检测已越过汇聚层,除了第一个线路外,后续信息可能无效")) if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..786f716 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,160 @@ +package utils + +import ( + "context" + "net" + "net/http" + "sync" + "time" +) + +type NetCheckResult struct { + HasIPv4 bool + HasIPv6 bool + Connected bool + StackType string // "IPv4", "IPv6", "DualStack", "None" +} + +func makeResolver(proto, dnsAddr string) *net.Resolver { + return &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: 5 * time.Second, + } + return d.DialContext(ctx, proto, dnsAddr) + }, + } +} + +func CheckPublicAccess(timeout time.Duration) NetCheckResult { + if timeout < 2*time.Second { + timeout = 2 * time.Second + } + var wg sync.WaitGroup + resultChan := make(chan string, 8) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + checks := []struct { + Tag string + Addr string + Kind string // udp4, udp6, http4, http6 + }{ + // UDP DNS + {"IPv4", "223.5.5.5:53", "udp4"}, // 阿里 DNS + {"IPv4", "8.8.8.8:53", "udp4"}, // Google DNS + {"IPv6", "[2400:3200::1]:53", "udp6"}, // 阿里 IPv6 DNS + {"IPv6", "[2001:4860:4860::8888]:53", "udp6"}, // Google IPv6 DNS + // HTTP HEAD + {"IPv4", "https://www.baidu.com", "http4"}, // 百度 + {"IPv4", "https://1.1.1.1", "http4"}, // Cloudflare + {"IPv6", "https://[2400:3200::1]", "http6"}, // 阿里 IPv6 + {"IPv6", "https://[2606:4700::1111]", "http6"}, // Cloudflare IPv6 + } + for _, check := range checks { + wg.Add(1) + go func(tag, addr, kind string) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + } + }() + switch kind { + case "udp4", "udp6": + dialer := &net.Dialer{ + Timeout: timeout / 4, + } + conn, err := dialer.DialContext(ctx, kind, addr) + if err == nil && conn != nil { + conn.Close() + select { + case resultChan <- tag: + case <-ctx.Done(): + return + } + } + case "http4", "http6": + var resolver *net.Resolver + if kind == "http4" { + resolver = makeResolver("udp4", "223.5.5.5:53") + } else { + resolver = makeResolver("udp6", "[2400:3200::1]:53") + } + dialer := &net.Dialer{ + Timeout: timeout / 4, + Resolver: resolver, + } + transport := &http.Transport{ + DialContext: dialer.DialContext, + MaxIdleConns: 1, + MaxIdleConnsPerHost: 1, + IdleConnTimeout: time.Second, + TLSHandshakeTimeout: timeout / 4, + ResponseHeaderTimeout: timeout / 4, + DisableKeepAlives: true, + } + client := &http.Client{ + Timeout: timeout / 4, + Transport: transport, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + req, err := http.NewRequestWithContext(ctx, "HEAD", addr, nil) + if err != nil { + return + } + resp, err := client.Do(req) + if err == nil && resp != nil { + if resp.Body != nil { + resp.Body.Close() + } + if resp.StatusCode < 500 { + select { + case resultChan <- tag: + case <-ctx.Done(): + return + } + } + } + } + }(check.Tag, check.Addr, check.Kind) + } + go func() { + wg.Wait() + close(resultChan) + }() + hasV4 := false + hasV6 := false + for { + select { + case res, ok := <-resultChan: + if !ok { + goto result + } + if res == "IPv4" { + hasV4 = true + } + if res == "IPv6" { + hasV6 = true + } + case <-ctx.Done(): + goto result + } + } +result: + stack := "None" + if hasV4 && hasV6 { + stack = "DualStack" + } else if hasV4 { + stack = "IPv4" + } else if hasV6 { + stack = "IPv6" + } + return NetCheckResult{ + HasIPv4: hasV4, + HasIPv6: hasV6, + Connected: hasV4 || hasV6, + StackType: stack, + } +} \ No newline at end of file diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 0000000..09b23c2 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "fmt" + "testing" + "time" +) + +func TestCheckPublicAccess(t *testing.T) { + timeout := 3 * time.Second + result := CheckPublicAccess(timeout) + if result.Connected { + fmt.Printf("✅ 本机有公网连接,类型: %s\n", result.StackType) + } else { + fmt.Println("❌ 本机未检测到公网连接") + } +} \ No newline at end of file