feat(dns): prepare backtrace v0.0.21
Some checks failed
CI / test (push) Waiting to run
Sync ASN metadata / sync (push) Has been cancelled

This commit is contained in:
spiritlhl 2026-08-26 15:37:44 +08:00
parent e83eb23302
commit 16e551b631
15 changed files with 461 additions and 373 deletions

View File

@ -9,9 +9,6 @@ concurrency:
group: backtrace-ipv6-prefix-sync group: backtrace-ipv6-prefix-sync
cancel-in-progress: false cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
permissions: permissions:
contents: write contents: write

View File

@ -1,58 +1,34 @@
name: CI name: CI
on: on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: workflow_dispatch:
env: permissions:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" contents: read
concurrency:
group: backtrace-ci-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
test: test:
strategy: runs-on: ubuntu-latest
matrix:
go: ['stable']
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - uses: actions/checkout@v7
uses: actions/checkout@v7 - uses: actions/setup-go@v7
- name: Setup Go
uses: actions/setup-go@v7
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- name: Verify module and test source
- name: Test shell: bash
run: | run: |
# Run all tests without generating a combined coverage profile
set -euo pipefail set -euo pipefail
go test ./... go mod verify
go test -race -count=1 ./...
- name: Create Tag go vet ./...
if: success() go build -o "$RUNNER_TEMP/backtrace" ./cmd
run: | git diff --check
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
TAG="v0.0.11-$(date +'%Y%m%d%H%M%S')"
git tag $TAG
git push origin $TAG
echo "TAG=$TAG" >> $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update README.md
if: success()
run: |
sed -i "s|go get github.com/oneclickvirt/backtrace@.*|go get github.com/oneclickvirt/backtrace@${TAG}|" README.md
env:
TAG: ${{ env.TAG }}
- name: Commit and Push README.md
if: success()
run: |
git add README.md
git commit -m "Update README.md with new tag ${TAG}"
git push origin main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,139 +1,128 @@
name: Build and Release name: Build and Release
on: on:
# push: push:
# branches: [ main ] tags: ["v*"]
# pull_request:
# branches: [ main ]
# release:
# types: [published]
workflow_dispatch: workflow_dispatch:
inputs:
release_tag:
description: Existing semantic version tag to build and release
required: true
type: string
env: permissions:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" contents: write
jobs: jobs:
verify:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.release.outputs.tag }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref }}
fetch-depth: 0
fetch-tags: true
- id: release
name: Verify release tag and source version
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
tag="${{ inputs.release_tag }}"
else
tag="${GITHUB_REF#refs/tags/}"
fi
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
git fetch --force --tags origin
[[ "$(git rev-list -n 1 "$tag")" == "$(git rev-parse HEAD)" ]]
version="$(sed -n 's/^const BackTraceVersion = "\(v[0-9.]*\)"$/\1/p' model/model.go)"
[[ "$version" == "$tag" ]]
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
check-latest: true
- name: Validate release source
shell: bash
run: |
set -euo pipefail
go mod verify
go test -race -count=1 ./...
go vet ./...
go build -o "$RUNNER_TEMP/backtrace" ./cmd
release:
needs: verify
runs-on: ubuntu-latest
steps:
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.verify.outputs.tag }}
shell: bash
run: |
set -euo pipefail
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \
gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \
--title "$RELEASE_TAG" --generate-notes
build: build:
name: Build and Test needs: [verify, release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
check-latest: true
- name: Build and Test on Default Platform
run: |
go build -v ./...
go test -v ./...
- name: Delete Existing Release Assets
run: |
release_id=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/oneclickvirt/backtrace/releases/tags/output" | jq -r '.id')
echo "Deleting existing release assets..."
assets=$(curl -s -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/oneclickvirt/backtrace/releases/$release_id/assets" | jq -r '.[] | .id')
for asset in $assets; do
echo "Deleting asset with ID: $asset"
curl -X DELETE -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/oneclickvirt/backtrace/releases/assets/$asset"
done
sleep 30
release-binary:
name: Release Go Binary
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
check-latest: true
- name: Build and Release
run: |
mkdir -p bin
cd cmd
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} GOARM=${{ matrix.goarm }} go build \
-o ../bin/backtrace-${{ matrix.goos }}-${{ matrix.goarch }} \
-v \
-ldflags="-extldflags=-static -s -w" \
-trimpath \
.
- name: Upload New Assets
run: |
release_id=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/oneclickvirt/backtrace/releases/tags/output" | jq -r '.id')
echo "Uploading new assets to release..."
for file in ./bin/*; do
echo "Uploading $file to release..."
curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$file" \
"https://uploads.github.com/repos/oneclickvirt/backtrace/releases/$release_id/assets?name=$(basename "$file")"
rm -rf $file
done
strategy: strategy:
fail-fast: false
matrix: matrix:
goos: [windows, freebsd, openbsd, linux, darwin]
goarch: [amd64, 386]
exclude:
- goarch: 386
goos: darwin
include: include:
- goos: windows - {goos: linux, goarch: amd64}
goarch: 386 - {goos: linux, goarch: "386"}
- goos: windows - {goos: linux, goarch: arm, goarm: "7"}
goarch: amd64 - {goos: linux, goarch: arm64}
- goos: windows - {goos: linux, goarch: riscv64}
goarch: arm64 - {goos: linux, goarch: mips}
- goos: darwin - {goos: linux, goarch: mipsle}
goarch: arm64 - {goos: linux, goarch: mips64}
- goos: linux - {goos: linux, goarch: mips64le}
goarch: arm - {goos: linux, goarch: ppc64}
goarm: 7 - {goos: linux, goarch: ppc64le}
- goos: linux - {goos: freebsd, goarch: amd64}
goarch: arm64 - {goos: freebsd, goarch: "386"}
- goos: linux - {goos: freebsd, goarch: arm, goarm: "7"}
goarch: riscv64 - {goos: freebsd, goarch: arm64}
- goos: linux - {goos: openbsd, goarch: amd64}
goarch: mips64 - {goos: openbsd, goarch: "386"}
- goos: linux - {goos: openbsd, goarch: arm, goarm: "7"}
goarch: mips64le - {goos: openbsd, goarch: arm64}
- goos: linux - {goos: darwin, goarch: amd64}
goarch: mipsle - {goos: darwin, goarch: arm64}
- goos: linux - {goos: windows, goarch: "386"}
goarch: mips - {goos: windows, goarch: amd64}
- goos: linux - {goos: windows, goarch: arm64}
goarch: ppc64 steps:
- goos: linux - uses: actions/checkout@v7
goarch: ppc64le with:
- goos: freebsd ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref }}
goarch: arm64 - uses: actions/setup-go@v7
- goos: freebsd with:
goarch: arm go-version-file: go.mod
goarm: 7 check-latest: true
- goos: openbsd - name: Build binary
goarch: arm64 env:
- goos: openbsd GOOS: ${{ matrix.goos }}
goarch: arm GOARCH: ${{ matrix.goarch }}
goarm: 7 GOARM: ${{ matrix.goarm }}
# - goos: linux CGO_ENABLED: "0"
# goarch: mipsle shell: bash
# gomips: softfloat run: |
# - goos: linux set -euo pipefail
# goarch: mips suffix=""
# gomips: softfloat [[ "$GOOS" == "windows" ]] && suffix=".exe"
# - goos: linux artifact="backtrace-${GOOS}-${GOARCH}${suffix}"
# goarch: arm go build -trimpath -buildvcs=false -ldflags="-s -w" -o "$artifact" ./cmd
# goarm: 6 echo "ARTIFACT=$artifact" >> "$GITHUB_ENV"
# - goos: linux - name: Upload release asset
# goarch: arm env:
# goarm: 5 GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.verify.outputs.tag }}
run: gh release upload "$RELEASE_TAG" "$ARTIFACT" --repo "$GITHUB_REPOSITORY" --clobber

78
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,78 @@
name: Prepare and Tag Release
on:
workflow_dispatch:
inputs:
release_tag:
description: Semantic version tag; leave empty to use BackTraceVersion
required: false
type: string
permissions:
contents: write
concurrency:
group: backtrace-release-data
cancel-in-progress: false
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
check-latest: true
- id: release
name: Resolve release tag
shell: bash
run: |
set -euo pipefail
version="$(sed -n 's/^const BackTraceVersion = "\(v[0-9.]*\)"$/\1/p' model/model.go)"
tag="${{ inputs.release_tag }}"
[[ -n "$tag" ]] || tag="$version"
[[ "$tag" == "$version" ]]
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
git fetch --force --tags origin
! git rev-parse --verify --quiet "refs/tags/$tag"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Refresh embedded routing datasets
shell: bash
run: |
set -euo pipefail
go run ./cmd/update-prefixes -output-dir bk/prefix
go run ./cmd/update-asn-metadata
- name: Validate release source
shell: bash
run: |
set -euo pipefail
go mod verify
go test -race -count=1 ./...
go vet ./...
go build -o "$RUNNER_TEMP/backtrace" ./cmd
git diff --check
- name: Commit refreshed routing datasets
shell: bash
run: |
set -euo pipefail
if [[ -z "$(git status --porcelain -- bk/prefix bgptools/data)" ]]; then
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add bk/prefix bgptools/data
git commit -m "chore(data): refresh routing datasets"
git push origin HEAD:main
- name: Create immutable release tag
env:
RELEASE_TAG: ${{ steps.release.outputs.tag }}
shell: bash
run: |
set -euo pipefail
git tag -a "$RELEASE_TAG" -m "Release $RELEASE_TAG"
git push origin "refs/tags/$RELEASE_TAG"

View File

@ -19,6 +19,7 @@
- [x] 多次并发路由检测以分析平均多次路由,避免单次路由因网络波动未能准确检测 - [x] 多次并发路由检测以分析平均多次路由,避免单次路由因网络波动未能准确检测
- [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地址
- [x] 在线但本地 DNS 确认缺失时,在当前进程内使用经过固定地址 TLS 查询校验的最快 DoH/DoT 上游瞬时超时、SERVFAIL 或丢包不会切换系统解析
相关输出和查询结果的说明:[跳转](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) 相关输出和查询结果的说明:[跳转](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)
@ -50,7 +51,7 @@ backtrace
进行测试 进行测试
无环境依赖,理论上适配所有系统和主流架构,更多架构请查看 https://github.com/oneclickvirt/backtrace/releases/tag/output 无环境依赖,理论上适配所有系统和主流架构,更多架构请查看 https://github.com/oneclickvirt/backtrace/releases
``` ```
Usage: backtrace [options] Usage: backtrace [options]
@ -59,6 +60,8 @@ Usage: backtrace [options]
Specify IP address for bgptools Specify IP address for bgptools
-ipv6 -ipv6
Enable ipv6 testing Enable ipv6 testing
-dns-mode string
DNS mode (auto, system, doh, or dot) (default "auto")
-log -log
Enable logging Enable logging
-s Disabe show ip info (default true) -s Disabe show ip info (default true)
@ -75,9 +78,13 @@ rm -rf /usr/bin/backtrace
## 在Golang中使用 ## 在Golang中使用
``` ```
go get github.com/oneclickvirt/backtrace@v0.0.10-20260702203359 go get github.com/oneclickvirt/backtrace@v0.0.21
``` ```
## DNS 解析回退
`-dns-mode=auto` 是默认值。它会先保留系统 DNS只有独立探测都确认本地解析器不可用时才在本次进程内启用内置加密 DNS并选择实测延迟最低的 DoH 或 DoT 上游。正常响应、NXDOMAIN、短暂超时、SERVFAIL 和丢包都不会被误判为 DNS 缺失。`system` 禁用回退,`doh` 和 `dot` 可显式强制相应协议;不会修改 `/etc/resolv.conf`、`/etc/hosts` 或其他系统配置。
## 概览图 ## 概览图
![图片](https://github.com/oneclickvirt/backtrace/assets/103393591/4688f99f-0f02-486f-8ffc-78d30f2c2f95) ![图片](https://github.com/oneclickvirt/backtrace/assets/103393591/4688f99f-0f02-486f-8ffc-78d30f2c2f95)

View File

@ -34,13 +34,16 @@ case $os in
Linux) Linux)
case $arch in case $arch in
"x86_64" | "x86" | "amd64" | "x64") "x86_64" | "x86" | "amd64" | "x64")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-linux-amd64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-linux-amd64"
;; ;;
"i386" | "i686") "i386" | "i686")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-linux-386" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-linux-386"
;; ;;
"armv7l" | "armv8" | "armv8l" | "aarch64" | "arm64") "armv7l" | "armv8" | "armv8l")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-linux-arm64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-linux-arm"
;;
"aarch64" | "arm64")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-linux-arm64"
;; ;;
*) *)
echo "Unsupported architecture: $arch" echo "Unsupported architecture: $arch"
@ -51,13 +54,13 @@ case $os in
Darwin) Darwin)
case $arch in case $arch in
"x86_64" | "x86" | "amd64" | "x64") "x86_64" | "x86" | "amd64" | "x64")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-darwin-amd64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-darwin-amd64"
;; ;;
"i386" | "i686") "i386" | "i686")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-darwin-386" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-darwin-386"
;; ;;
"armv7l" | "armv8" | "armv8l" | "aarch64" | "arm64") "aarch64" | "arm64")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-darwin-arm64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-darwin-arm64"
;; ;;
*) *)
echo "Unsupported architecture: $arch" echo "Unsupported architecture: $arch"
@ -68,13 +71,16 @@ case $os in
FreeBSD) FreeBSD)
case $arch in case $arch in
amd64) amd64)
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-freebsd-amd64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-freebsd-amd64"
;; ;;
"i386" | "i686") "i386" | "i686")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-freebsd-386" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-freebsd-386"
;; ;;
"armv7l" | "armv8" | "armv8l" | "aarch64" | "arm64") "armv7l" | "armv8" | "armv8l")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-freebsd-arm64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-freebsd-arm"
;;
"aarch64" | "arm64")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-freebsd-arm64"
;; ;;
*) *)
echo "Unsupported architecture: $arch" echo "Unsupported architecture: $arch"
@ -85,13 +91,16 @@ case $os in
OpenBSD) OpenBSD)
case $arch in case $arch in
amd64) amd64)
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-openbsd-amd64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-openbsd-amd64"
;; ;;
"i386" | "i686") "i386" | "i686")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-openbsd-386" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-openbsd-386"
;; ;;
"armv7l" | "armv8" | "armv8l" | "aarch64" | "arm64") "armv7l" | "armv8" | "armv8l")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-openbsd-arm64" wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-openbsd-arm"
;;
"aarch64" | "arm64")
wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-openbsd-arm64"
;; ;;
*) *)
echo "Unsupported architecture: $arch" echo "Unsupported architecture: $arch"

View File

@ -381,6 +381,7 @@
2409:804c:18::/48 2409:804c:18::/48
2409:804c:19::/48 2409:804c:19::/48
2409:804c:2000::/40 2409:804c:2000::/40
2409:804c:2000::/48
2409:804c:22::/48 2409:804c:22::/48
2409:804c:24::/48 2409:804c:24::/48
2409:804c:27::/48 2409:804c:27::/48
@ -555,6 +556,7 @@
2409:805a:2b02::/48 2409:805a:2b02::/48
2409:805a:2b05::/48 2409:805a:2b05::/48
2409:805a:2b08::/48 2409:805a:2b08::/48
2409:805a:2b0b::/48
2409:805a:2c01::/48 2409:805a:2c01::/48
2409:805b:2807::/48 2409:805b:2807::/48
2409:805b:280e::/48 2409:805b:280e::/48
@ -688,6 +690,8 @@
2409:8062:1015::/48 2409:8062:1015::/48
2409:8062:1800::/40 2409:8062:1800::/40
2409:8062:2000::/40 2409:8062:2000::/40
2409:8062:2001::/48
2409:8062:2002::/48
2409:8062:2800::/40 2409:8062:2800::/40
2409:8062:2900::/40 2409:8062:2900::/40
2409:8062:2b00::/40 2409:8062:2b00::/40

View File

@ -1,7 +1,7 @@
{ {
"schema": "backtrace.asn-prefixes/v1", "schema": "backtrace.asn-prefixes/v1",
"file": "as9808.txt", "file": "as9808.txt",
"count": 7166, "count": 7170,
"sha256": "90cc93ec88ebc7e4f97bfb9eadc2032e9c9ca803e618a2aa811af6f7d0eac1f7", "sha256": "b74a4ff30b2f92e153643684758ba892efc426c3f8c6ad355581288fbec71bee",
"generated_at": "2026-08-22T01:20:00Z" "generated_at": "2026-08-26T07:30:15Z"
} }

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"strings"
"sync" "sync"
"time" "time"
@ -16,6 +17,7 @@ import (
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/basics/network/resolver"
. "github.com/oneclickvirt/defaultset" . "github.com/oneclickvirt/defaultset"
) )
@ -44,9 +46,16 @@ type cliOptions struct {
deep bool deep bool
routeTries int routeTries int
specifiedIP string specifiedIP string
dnsMode string
timeout time.Duration timeout time.Duration
} }
var (
backtraceDNSConfigureFn = resolver.Configure
backtraceDNSShutdownFn = resolver.Shutdown
backtraceDNSBootstrapReachableFn = resolver.BootstrapReachable
)
func newBacktraceFlagSet(options *cliOptions) *flag.FlagSet { func newBacktraceFlagSet(options *cliOptions) *flag.FlagSet {
set := flag.NewFlagSet("backtrace", flag.ContinueOnError) set := flag.NewFlagSet("backtrace", flag.ContinueOnError)
set.BoolVar(&options.help, "h", false, "Show help information") set.BoolVar(&options.help, "h", false, "Show help information")
@ -61,6 +70,7 @@ func newBacktraceFlagSet(options *cliOptions) *flag.FlagSet {
set.BoolVar(&options.deep, "deep", false, "Fetch geofeed and enable WHOIS fallback") set.BoolVar(&options.deep, "deep", false, "Fetch geofeed and enable WHOIS fallback")
set.IntVar(&options.routeTries, "route-attempts", 3, "Traceroute attempts per target (1-5)") set.IntVar(&options.routeTries, "route-attempts", 3, "Traceroute attempts per target (1-5)")
set.DurationVar(&options.timeout, "timeout", 15*time.Second, "Structured or route report timeout") set.DurationVar(&options.timeout, "timeout", 15*time.Second, "Structured or route report timeout")
set.StringVar(&options.dnsMode, "dns-mode", "auto", "DNS mode (auto, system, doh, or dot)")
return set return set
} }
@ -75,13 +85,40 @@ func safeGo(wg *sync.WaitGroup, fn func()) {
}() }()
} }
func main() { func configureBacktraceResolver(ctx context.Context, mode resolver.Mode, preCheck *utils.NetCheckResult) resolver.Status {
go func() { requested := resolver.ParseMode(string(mode))
resp, err := http.Get("https://hits.spiritlhl.net/backtrace.svg?action=hit&title=Hits&title_bg=%23555555&count_bg=%230eecf8&edge_flat=false") config := resolver.Config{Mode: requested}
if err == nil && resp != nil && resp.Body != nil { status := resolver.Status{Requested: requested, Active: resolver.ModeUnavailable, Reason: "network unavailable"}
resp.Body.Close() if preCheck == nil || preCheck.Connected || requested == resolver.ModeDoH || requested == resolver.ModeDoT {
return backtraceDNSConfigureFn(ctx, config)
}
if requested == resolver.ModeAuto {
if _, reachable := backtraceDNSBootstrapReachableFn(ctx, config); reachable {
return backtraceDNSConfigureFn(ctx, config)
} }
}() status.Reason = "encrypted DNS endpoint unreachable"
}
backtraceDNSShutdownFn()
return status
}
func promoteEncryptedDNSConnectivity(preCheck *utils.NetCheckResult, status resolver.Status) {
if preCheck == nil || preCheck.Connected || (status.Active != resolver.ModeDoH && status.Active != resolver.ModeDoT) {
return
}
preCheck.Connected = true
switch status.Stack {
case "IPv4":
preCheck.HasIPv4 = true
case "IPv6":
preCheck.HasIPv6 = true
}
if status.Stack != "" {
preCheck.StackType = status.Stack
}
}
func main() {
var options cliOptions var options cliOptions
backtraceFlag := newBacktraceFlagSet(&options) backtraceFlag := newBacktraceFlagSet(&options)
if err := backtraceFlag.Parse(os.Args[1:]); err != nil { if err := backtraceFlag.Parse(os.Args[1:]); err != nil {
@ -99,10 +136,31 @@ func main() {
fmt.Println(model.BackTraceVersion) fmt.Println(model.BackTraceVersion)
return return
} }
options.dnsMode = strings.ToLower(strings.TrimSpace(options.dnsMode))
if options.dnsMode != "auto" && options.dnsMode != "system" && options.dnsMode != "doh" && options.dnsMode != "dot" {
fmt.Fprintln(os.Stderr, "dns-mode must be auto, system, doh, or dot")
os.Exit(2)
}
if err := validateStructuredOptions(options); err != nil { if err := validateStructuredOptions(options); err != nil {
fmt.Fprintln(os.Stderr, sanitizeErrorText(err.Error())) fmt.Fprintln(os.Stderr, sanitizeErrorText(err.Error()))
os.Exit(2) os.Exit(2)
} }
var preCheck *utils.NetCheckResult
if !options.jsonOutput && !options.routeJSON {
check := utils.CheckPublicAccess(3 * time.Second)
preCheck = &check
}
status := configureBacktraceResolver(context.Background(), resolver.ParseMode(options.dnsMode), preCheck)
promoteEncryptedDNSConnectivity(preCheck, status)
if status.Active == resolver.ModeDoH || status.Active == resolver.ModeDoT {
defer backtraceDNSShutdownFn()
}
go func() {
resp, err := http.Get("https://hits.spiritlhl.net/backtrace.svg?action=hit&title=Hits&title_bg=%23555555&count_bg=%230eecf8&edge_flat=false")
if err == nil && resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if options.jsonOutput { if options.jsonOutput {
config := bgptools.IPBGPReportConfig{ config := bgptools.IPBGPReportConfig{
Timeout: options.timeout, Timeout: options.timeout,
@ -146,8 +204,7 @@ func main() {
} }
} }
} }
preCheck := utils.CheckPublicAccess(3 * time.Second) if preCheck == nil || !preCheck.Connected {
if !preCheck.Connected {
fmt.Println(Red("PreCheck IP Type Failed")) fmt.Println(Red("PreCheck IP Type Failed"))
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...")

View File

@ -4,11 +4,14 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"strings"
"testing" "testing"
"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/utils"
"github.com/oneclickvirt/basics/network/resolver"
) )
func TestBacktraceStructuredFlagParsing(t *testing.T) { func TestBacktraceStructuredFlagParsing(t *testing.T) {
@ -27,11 +30,114 @@ func TestBacktraceDefaultsPreserveLegacyMode(t *testing.T) {
if err := newBacktraceFlagSet(&options).Parse(nil); err != nil { if err := newBacktraceFlagSet(&options).Parse(nil); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !options.showIPInfo || options.jsonOutput || options.routeJSON || options.deep || options.ipv6 || options.specifiedIP != "" || options.timeout != 15*time.Second || options.routeTries != 3 { if !options.showIPInfo || options.jsonOutput || options.routeJSON || options.deep || options.ipv6 || options.specifiedIP != "" || options.dnsMode != "auto" || options.timeout != 15*time.Second || options.routeTries != 3 {
t.Fatalf("legacy defaults changed: %+v", options) t.Fatalf("legacy defaults changed: %+v", options)
} }
} }
func TestBacktraceDNSModeParsing(t *testing.T) {
var options cliOptions
if err := newBacktraceFlagSet(&options).Parse([]string{"-dns-mode", "DoT"}); err != nil {
t.Fatal(err)
}
options.dnsMode = strings.ToLower(strings.TrimSpace(options.dnsMode))
if options.dnsMode != "dot" {
t.Fatalf("dns mode = %q, want dot", options.dnsMode)
}
}
func TestConfigureBacktraceResolverScopesBootstrapFallback(t *testing.T) {
originalConfigure := backtraceDNSConfigureFn
originalShutdown := backtraceDNSShutdownFn
originalBootstrapReachable := backtraceDNSBootstrapReachableFn
t.Cleanup(func() {
backtraceDNSConfigureFn = originalConfigure
backtraceDNSShutdownFn = originalShutdown
backtraceDNSBootstrapReachableFn = originalBootstrapReachable
})
tests := []struct {
name string
mode resolver.Mode
connected bool
bootstrapReachable bool
configuredStatus resolver.Status
wantConfigure int
wantBootstrap int
wantShutdown int
}{
{
name: "connected auto uses normal resolver path",
mode: resolver.ModeAuto,
connected: true,
configuredStatus: resolver.Status{Requested: resolver.ModeAuto, Active: resolver.ModeSystem, SystemAvailable: true},
wantConfigure: 1,
},
{
name: "offline auto stops when bootstrap is unreachable",
mode: resolver.ModeAuto,
wantBootstrap: 1,
wantShutdown: 1,
},
{
name: "offline auto retries after reachable bootstrap",
mode: resolver.ModeAuto,
bootstrapReachable: true,
configuredStatus: resolver.Status{Requested: resolver.ModeAuto, Active: resolver.ModeDoH, DoHAvailable: true, Fallback: true, Stack: "IPv4"},
wantConfigure: 1,
wantBootstrap: 1,
},
{
name: "forced DoH bypasses bootstrap gate",
mode: resolver.ModeDoH,
configuredStatus: resolver.Status{Requested: resolver.ModeDoH, Active: resolver.ModeDoH, DoHAvailable: true},
wantConfigure: 1,
},
{
name: "system mode never falls back",
mode: resolver.ModeSystem,
wantShutdown: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
configureCalls := 0
bootstrapCalls := 0
shutdownCalls := 0
backtraceDNSConfigureFn = func(_ context.Context, config resolver.Config) resolver.Status {
configureCalls++
if config.Mode != test.mode {
t.Fatalf("resolver mode = %q, want %q", config.Mode, test.mode)
}
return test.configuredStatus
}
backtraceDNSBootstrapReachableFn = func(_ context.Context, config resolver.Config) (string, bool) {
bootstrapCalls++
if config.Mode != resolver.ModeAuto {
t.Fatalf("bootstrap mode = %q, want auto", config.Mode)
}
return "IPv4", test.bootstrapReachable
}
backtraceDNSShutdownFn = func() { shutdownCalls++ }
status := configureBacktraceResolver(context.Background(), test.mode, &utils.NetCheckResult{Connected: test.connected})
if configureCalls != test.wantConfigure || bootstrapCalls != test.wantBootstrap || shutdownCalls != test.wantShutdown {
t.Fatalf("DNS calls = configure:%d bootstrap:%d shutdown:%d, want configure:%d bootstrap:%d shutdown:%d", configureCalls, bootstrapCalls, shutdownCalls, test.wantConfigure, test.wantBootstrap, test.wantShutdown)
}
if test.wantConfigure == 0 && status.Active != resolver.ModeUnavailable {
t.Fatalf("status = %#v, want unavailable", status)
}
})
}
}
func TestPromoteEncryptedDNSConnectivity(t *testing.T) {
preCheck := &utils.NetCheckResult{StackType: "None"}
promoteEncryptedDNSConnectivity(preCheck, resolver.Status{Active: resolver.ModeDoT, Stack: "IPv6"})
if !preCheck.Connected || !preCheck.HasIPv6 || preCheck.HasIPv4 || preCheck.StackType != "IPv6" {
t.Fatalf("successful encrypted DNS did not promote network state: %#v", preCheck)
}
}
func TestBacktraceRouteStructuredFlagParsing(t *testing.T) { func TestBacktraceRouteStructuredFlagParsing(t *testing.T) {
var options cliOptions var options cliOptions
set := newBacktraceFlagSet(&options) set := newBacktraceFlagSet(&options)

12
go.mod
View File

@ -5,9 +5,10 @@ go 1.25.3
require ( require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/imroc/req/v3 v3.59.0 github.com/imroc/req/v3 v3.59.0
github.com/oneclickvirt/defaultset v0.0.0-20240624051018-30a50859e1b5 github.com/oneclickvirt/basics v0.0.34
golang.org/x/net v0.55.0 github.com/oneclickvirt/defaultset v0.0.2-20240624082446
golang.org/x/sys v0.45.0 golang.org/x/net v0.57.0
golang.org/x/sys v0.47.0
) )
require ( require (
@ -15,11 +16,12 @@ require (
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/icholy/digest v1.1.0 // indirect github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/miekg/dns v1.1.73 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.60.0 // indirect github.com/quic-go/quic-go v0.60.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect github.com/refraction-networking/utls v1.8.2 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.51.0 // indirect golang.org/x/crypto v0.54.0 // indirect
golang.org/x/text v0.37.0 // indirect golang.org/x/text v0.40.0 // indirect
) )

26
go.sum
View File

@ -15,8 +15,12 @@ github.com/imroc/req/v3 v3.59.0 h1:PqKhJHyBmJYob47LVuTHwRZE00ZO6icbLHe5Zra13jo=
github.com/imroc/req/v3 v3.59.0/go.mod h1:cwLwwaE90Pq6ZMmKuHA9FivJbg3avnCH8/RA5lUUs74= github.com/imroc/req/v3 v3.59.0/go.mod h1:cwLwwaE90Pq6ZMmKuHA9FivJbg3avnCH8/RA5lUUs74=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/oneclickvirt/defaultset v0.0.0-20240624051018-30a50859e1b5 h1:TUM6XzOB7Z7OxyXi3fwlZY9KfuVbvUBusYiNbSfX208= github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
github.com/oneclickvirt/defaultset v0.0.0-20240624051018-30a50859e1b5/go.mod h1:e9Jt4tf2sbemCtc84/XgKcHy9EZ2jkc5x2sW1NiJS+E= github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/oneclickvirt/basics v0.0.34 h1:iT2lFMrWcxw0UwnFyw4Gasa2CDp4ERULsckBTFVzSUQ=
github.com/oneclickvirt/basics v0.0.34/go.mod h1:AqnE9cDa4ThZRvTq7UlUmLYSZBY6CPBzbUhxGT3JWyU=
github.com/oneclickvirt/defaultset v0.0.2-20240624082446 h1:5Pg3mK/u/vQvSz7anu0nxzrNdELi/AcDAU1mMsmPzyc=
github.com/oneclickvirt/defaultset v0.0.2-20240624082446/go.mod h1:e9Jt4tf2sbemCtc84/XgKcHy9EZ2jkc5x2sW1NiJS+E=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
@ -39,14 +43,16 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -2,7 +2,7 @@ package model
import "time" import "time"
const BackTraceVersion = "v0.0.20" const BackTraceVersion = "v0.0.21"
var EnableLoger = false var EnableLoger = false

View File

@ -1,160 +1,13 @@
package utils package utils
import ( import (
"context"
"net"
"net/http"
"sync"
"time" "time"
basicsutils "github.com/oneclickvirt/basics/utils"
) )
type NetCheckResult struct { type NetCheckResult = basicsutils.NetCheckResult
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 { func CheckPublicAccess(timeout time.Duration) NetCheckResult {
if timeout < 2*time.Second { return basicsutils.CheckPublicAccess(timeout)
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,
}
}

View File

@ -2,11 +2,15 @@ package utils
import ( import (
"fmt" "fmt"
"os"
"testing" "testing"
"time" "time"
) )
func TestCheckPublicAccess(t *testing.T) { func TestCheckPublicAccess(t *testing.T) {
if os.Getenv("BACKTRACE_INTEGRATION") != "1" {
t.Skip("set BACKTRACE_INTEGRATION=1 to run live public-access checks")
}
timeout := 3 * time.Second timeout := 3 * time.Second
result := CheckPublicAccess(timeout) result := CheckPublicAccess(timeout)
if result.Connected { if result.Connected {
@ -14,4 +18,4 @@ func TestCheckPublicAccess(t *testing.T) {
} else { } else {
fmt.Println("❌ 本机未检测到公网连接") fmt.Println("❌ 本机未检测到公网连接")
} }
} }