From 16e551b631af396924441a289c99b4462be8d9f6 Mon Sep 17 00:00:00 2001 From: spiritlhl <103393591+spiritLHLS@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:37:44 +0800 Subject: [PATCH] feat(dns): prepare backtrace v0.0.21 --- .github/workflows/build.yml | 3 - .github/workflows/ci.yaml | 64 +++----- .github/workflows/main.yaml | 243 ++++++++++++++--------------- .github/workflows/release.yaml | 78 +++++++++ README.md | 11 +- backtrace_install.sh | 41 +++-- bk/prefix/as9808.txt | 4 + bk/prefix/as9808.txt.manifest.json | 6 +- cmd/main.go | 73 ++++++++- cmd/main_test.go | 108 ++++++++++++- go.mod | 12 +- go.sum | 26 +-- model/model.go | 2 +- utils/utils.go | 157 +------------------ utils/utils_test.go | 6 +- 15 files changed, 461 insertions(+), 373 deletions(-) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f1c1ee..09b937a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,9 +9,6 @@ concurrency: group: backtrace-ipv6-prefix-sync cancel-in-progress: false -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - permissions: contents: write diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e59a40d..3f383a9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,58 +1,34 @@ name: CI on: + push: + branches: [main] + pull_request: + branches: [main] workflow_dispatch: -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" +permissions: + contents: read + +concurrency: + group: backtrace-ci-${{ github.ref }} + cancel-in-progress: true jobs: test: - strategy: - matrix: - go: ['stable'] - os: [ubuntu-latest] - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup Go - uses: actions/setup-go@v7 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version-file: go.mod check-latest: true - - - name: Test + - name: Verify module and test source + shell: bash run: | - # Run all tests without generating a combined coverage profile set -euo pipefail - go test ./... - - - name: Create Tag - if: success() - run: | - 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 }} + go mod verify + go test -race -count=1 ./... + go vet ./... + go build -o "$RUNNER_TEMP/backtrace" ./cmd + git diff --check diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8431342..8238564 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,139 +1,128 @@ name: Build and Release on: - # push: - # branches: [ main ] - # pull_request: - # branches: [ main ] - # release: - # types: [published] + push: + tags: ["v*"] workflow_dispatch: + inputs: + release_tag: + description: Existing semantic version tag to build and release + required: true + type: string -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" +permissions: + contents: write 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: - name: Build and Test + needs: [verify, release] 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: + fail-fast: false matrix: - goos: [windows, freebsd, openbsd, linux, darwin] - goarch: [amd64, 386] - exclude: - - goarch: 386 - goos: darwin include: - - goos: windows - goarch: 386 - - goos: windows - goarch: amd64 - - goos: windows - goarch: arm64 - - goos: darwin - goarch: arm64 - - goos: linux - goarch: arm - goarm: 7 - - goos: linux - goarch: arm64 - - goos: linux - goarch: riscv64 - - goos: linux - goarch: mips64 - - goos: linux - goarch: mips64le - - goos: linux - goarch: mipsle - - goos: linux - goarch: mips - - goos: linux - goarch: ppc64 - - goos: linux - goarch: ppc64le - - goos: freebsd - goarch: arm64 - - goos: freebsd - goarch: arm - goarm: 7 - - goos: openbsd - goarch: arm64 - - goos: openbsd - goarch: arm - goarm: 7 - # - goos: linux - # goarch: mipsle - # gomips: softfloat - # - goos: linux - # goarch: mips - # gomips: softfloat - # - goos: linux - # goarch: arm - # goarm: 6 - # - goos: linux - # goarch: arm - # goarm: 5 + - {goos: linux, goarch: amd64} + - {goos: linux, goarch: "386"} + - {goos: linux, goarch: arm, goarm: "7"} + - {goos: linux, goarch: arm64} + - {goos: linux, goarch: riscv64} + - {goos: linux, goarch: mips} + - {goos: linux, goarch: mipsle} + - {goos: linux, goarch: mips64} + - {goos: linux, goarch: mips64le} + - {goos: linux, goarch: ppc64} + - {goos: linux, goarch: ppc64le} + - {goos: freebsd, goarch: amd64} + - {goos: freebsd, goarch: "386"} + - {goos: freebsd, goarch: arm, goarm: "7"} + - {goos: freebsd, goarch: arm64} + - {goos: openbsd, goarch: amd64} + - {goos: openbsd, goarch: "386"} + - {goos: openbsd, goarch: arm, goarm: "7"} + - {goos: openbsd, goarch: arm64} + - {goos: darwin, goarch: amd64} + - {goos: darwin, goarch: arm64} + - {goos: windows, goarch: "386"} + - {goos: windows, goarch: amd64} + - {goos: windows, goarch: arm64} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref }} + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + check-latest: true + - name: Build binary + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarm }} + CGO_ENABLED: "0" + shell: bash + run: | + set -euo pipefail + suffix="" + [[ "$GOOS" == "windows" ]] && suffix=".exe" + artifact="backtrace-${GOOS}-${GOARCH}${suffix}" + go build -trimpath -buildvcs=false -ldflags="-s -w" -o "$artifact" ./cmd + echo "ARTIFACT=$artifact" >> "$GITHUB_ENV" + - name: Upload release asset + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.verify.outputs.tag }} + run: gh release upload "$RELEASE_TAG" "$ARTIFACT" --repo "$GITHUB_REPOSITORY" --clobber diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..70d7a04 --- /dev/null +++ b/.github/workflows/release.yaml @@ -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" diff --git a/README.md b/README.md index c884ec2..9f2fdc4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [x] 多次并发路由检测以分析平均多次路由,避免单次路由因网络波动未能准确检测 - [x] 增加对全平台的编译支持,原版[backtrace](https://github.com/zhanghanyun/backtrace)仅支持linux平台的amd64和arm64架构 - [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) @@ -50,7 +51,7 @@ backtrace 进行测试 -无环境依赖,理论上适配所有系统和主流架构,更多架构请查看 https://github.com/oneclickvirt/backtrace/releases/tag/output +无环境依赖,理论上适配所有系统和主流架构,更多架构请查看 https://github.com/oneclickvirt/backtrace/releases ``` Usage: backtrace [options] @@ -59,6 +60,8 @@ Usage: backtrace [options] Specify IP address for bgptools -ipv6 Enable ipv6 testing + -dns-mode string + DNS mode (auto, system, doh, or dot) (default "auto") -log Enable logging -s Disabe show ip info (default true) @@ -75,9 +78,13 @@ rm -rf /usr/bin/backtrace ## 在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) diff --git a/backtrace_install.sh b/backtrace_install.sh index 7d91e19..4979182 100644 --- a/backtrace_install.sh +++ b/backtrace_install.sh @@ -34,13 +34,16 @@ case $os in Linux) case $arch in "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") - 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") - wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-linux-arm64" + "armv7l" | "armv8" | "armv8l") + 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" @@ -51,13 +54,13 @@ case $os in Darwin) case $arch in "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") - 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") - wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-darwin-arm64" + "aarch64" | "arm64") + wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/latest/download/backtrace-darwin-arm64" ;; *) echo "Unsupported architecture: $arch" @@ -68,13 +71,16 @@ case $os in FreeBSD) case $arch in 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") - 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") - wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-freebsd-arm64" + "armv7l" | "armv8" | "armv8l") + 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" @@ -85,13 +91,16 @@ case $os in OpenBSD) case $arch in 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") - 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") - wget -O backtrace "${cdn_success_url}https://github.com/oneclickvirt/backtrace/releases/download/output/backtrace-openbsd-arm64" + "armv7l" | "armv8" | "armv8l") + 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" diff --git a/bk/prefix/as9808.txt b/bk/prefix/as9808.txt index 398611e..c03317f 100644 --- a/bk/prefix/as9808.txt +++ b/bk/prefix/as9808.txt @@ -381,6 +381,7 @@ 2409:804c:18::/48 2409:804c:19::/48 2409:804c:2000::/40 +2409:804c:2000::/48 2409:804c:22::/48 2409:804c:24::/48 2409:804c:27::/48 @@ -555,6 +556,7 @@ 2409:805a:2b02::/48 2409:805a:2b05::/48 2409:805a:2b08::/48 +2409:805a:2b0b::/48 2409:805a:2c01::/48 2409:805b:2807::/48 2409:805b:280e::/48 @@ -688,6 +690,8 @@ 2409:8062:1015::/48 2409:8062:1800::/40 2409:8062:2000::/40 +2409:8062:2001::/48 +2409:8062:2002::/48 2409:8062:2800::/40 2409:8062:2900::/40 2409:8062:2b00::/40 diff --git a/bk/prefix/as9808.txt.manifest.json b/bk/prefix/as9808.txt.manifest.json index ec7b6ad..a18005d 100644 --- a/bk/prefix/as9808.txt.manifest.json +++ b/bk/prefix/as9808.txt.manifest.json @@ -1,7 +1,7 @@ { "schema": "backtrace.asn-prefixes/v1", "file": "as9808.txt", - "count": 7166, - "sha256": "90cc93ec88ebc7e4f97bfb9eadc2032e9c9ca803e618a2aa811af6f7d0eac1f7", - "generated_at": "2026-08-22T01:20:00Z" + "count": 7170, + "sha256": "b74a4ff30b2f92e153643684758ba892efc426c3f8c6ad355581288fbec71bee", + "generated_at": "2026-08-26T07:30:15Z" } diff --git a/cmd/main.go b/cmd/main.go index d3eb580..da01d83 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "runtime" + "strings" "sync" "time" @@ -16,6 +17,7 @@ import ( backtrace "github.com/oneclickvirt/backtrace/bk" "github.com/oneclickvirt/backtrace/model" "github.com/oneclickvirt/backtrace/utils" + "github.com/oneclickvirt/basics/network/resolver" . "github.com/oneclickvirt/defaultset" ) @@ -44,9 +46,16 @@ type cliOptions struct { deep bool routeTries int specifiedIP string + dnsMode string timeout time.Duration } +var ( + backtraceDNSConfigureFn = resolver.Configure + backtraceDNSShutdownFn = resolver.Shutdown + backtraceDNSBootstrapReachableFn = resolver.BootstrapReachable +) + func newBacktraceFlagSet(options *cliOptions) *flag.FlagSet { set := flag.NewFlagSet("backtrace", flag.ContinueOnError) 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.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.StringVar(&options.dnsMode, "dns-mode", "auto", "DNS mode (auto, system, doh, or dot)") return set } @@ -75,13 +85,40 @@ func safeGo(wg *sync.WaitGroup, fn func()) { }() } -func main() { - 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() +func configureBacktraceResolver(ctx context.Context, mode resolver.Mode, preCheck *utils.NetCheckResult) resolver.Status { + requested := resolver.ParseMode(string(mode)) + config := resolver.Config{Mode: requested} + status := resolver.Status{Requested: requested, Active: resolver.ModeUnavailable, Reason: "network unavailable"} + 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 backtraceFlag := newBacktraceFlagSet(&options) if err := backtraceFlag.Parse(os.Args[1:]); err != nil { @@ -99,10 +136,31 @@ func main() { fmt.Println(model.BackTraceVersion) 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 { fmt.Fprintln(os.Stderr, sanitizeErrorText(err.Error())) 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 { config := bgptools.IPBGPReportConfig{ Timeout: options.timeout, @@ -146,8 +204,7 @@ func main() { } } } - preCheck := utils.CheckPublicAccess(3 * time.Second) - if !preCheck.Connected { + if preCheck == nil || !preCheck.Connected { fmt.Println(Red("PreCheck IP Type Failed")) if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { fmt.Println("Press Enter to exit...") diff --git a/cmd/main_test.go b/cmd/main_test.go index a9a8122..9ca8b8c 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -4,11 +4,14 @@ import ( "bytes" "context" "encoding/json" + "strings" "testing" "time" "github.com/oneclickvirt/backtrace/bgptools" backtrace "github.com/oneclickvirt/backtrace/bk" + "github.com/oneclickvirt/backtrace/utils" + "github.com/oneclickvirt/basics/network/resolver" ) func TestBacktraceStructuredFlagParsing(t *testing.T) { @@ -27,11 +30,114 @@ func TestBacktraceDefaultsPreserveLegacyMode(t *testing.T) { if err := newBacktraceFlagSet(&options).Parse(nil); err != nil { 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) } } +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) { var options cliOptions set := newBacktraceFlagSet(&options) diff --git a/go.mod b/go.mod index 272327d..a249651 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,10 @@ go 1.25.3 require ( github.com/google/uuid v1.6.0 github.com/imroc/req/v3 v3.59.0 - github.com/oneclickvirt/defaultset v0.0.0-20240624051018-30a50859e1b5 - golang.org/x/net v0.55.0 - golang.org/x/sys v0.45.0 + github.com/oneclickvirt/basics v0.0.34 + github.com/oneclickvirt/defaultset v0.0.2-20240624082446 + golang.org/x/net v0.57.0 + golang.org/x/sys v0.47.0 ) require ( @@ -15,11 +16,12 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/icholy/digest v1.1.0 // 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/quic-go v0.60.0 // indirect github.com/refraction-networking/utls v1.8.2 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/text v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index 58e177a..c986e13 100644 --- a/go.sum +++ b/go.sum @@ -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/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= 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/oneclickvirt/defaultset v0.0.0-20240624051018-30a50859e1b5/go.mod h1:e9Jt4tf2sbemCtc84/XgKcHy9EZ2jkc5x2sW1NiJS+E= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 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.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +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= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/model/model.go b/model/model.go index 8170f7b..c92d812 100644 --- a/model/model.go +++ b/model/model.go @@ -2,7 +2,7 @@ package model import "time" -const BackTraceVersion = "v0.0.20" +const BackTraceVersion = "v0.0.21" var EnableLoger = false diff --git a/utils/utils.go b/utils/utils.go index 786f716..98df8e2 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,160 +1,13 @@ package utils import ( - "context" - "net" - "net/http" - "sync" "time" + + basicsutils "github.com/oneclickvirt/basics/utils" ) -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) - }, - } -} +type NetCheckResult = basicsutils.NetCheckResult 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 + return basicsutils.CheckPublicAccess(timeout) +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 09b23c2..bb129dd 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -2,11 +2,15 @@ package utils import ( "fmt" + "os" "testing" "time" ) 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 result := CheckPublicAccess(timeout) if result.Connected { @@ -14,4 +18,4 @@ func TestCheckPublicAccess(t *testing.T) { } else { fmt.Println("❌ 本机未检测到公网连接") } -} \ No newline at end of file +}