Repository: Kisesy/gscan_quic Branch: master Commit: 71e5f5ae0150 Files: 20 Total size: 45.3 KB Directory structure: gitextract_kl6s_cna/ ├── .github/ │ └── workflows/ │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── .travis.yml ├── LICENSE ├── README.md ├── config.json ├── config.user.json.example ├── go.mod ├── go.sum ├── gscan.go ├── iprange.go ├── iprange_quic.txt.example ├── ping.go ├── quic.go ├── scan.go ├── sni.go ├── tls.go └── util.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/go.yml ================================================ # This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go name: Go on: push: branches: ["master"] pull_request: branches: ["master"] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: "1.20" - name: Install dependencies run: go get . - name: Build run: go build -v ./... - name: Test run: go test -v ./... ================================================ FILE: .github/workflows/release.yml ================================================ name: goreleaser on: push: # run only against tags tags: - "*" permissions: contents: write # packages: write # issues: write jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - run: git fetch --force --tags - uses: actions/setup-go@v4 with: go-version: "1.20" # More assembly might be required: Docker logins, GPG, etc. It all depends # on your needs. - uses: goreleaser/goreleaser-action@v4 with: # either 'goreleaser' (default) or 'goreleaser-pro': distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' # distribution: # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib *.zip *.7z out*.txt # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ gscan_quic/ backup/ bin/ ================================================ FILE: .goreleaser.yaml ================================================ archives: - id: "gscan_quic" format_overrides: - goos: windows format: zip files: - LICENSE - README.md - config.json - "*.example" ================================================ FILE: .travis.yml ================================================ language: go go: - "1.10" before_install: - go get github.com/Kisesy/gscan_quic - sudo apt-get install p7zip-full script: - export CGO_ENABLED=0 - export EXE=gscan_quic - export TAR_EXCLUDE="--exclude=*.go --exclude=*.bz2 --exclude=*.7z --exclude=*.xz --exclude=vendor" - echo ${TRAVIS_TAG} > Version.txt - export GOOS=windows GOARCH=386 && go build -ldflags "-w -s" - 7za a ${EXE}_${GOOS}_${GOARCH}-${TRAVIS_TAG}.7z -ms=on -mx=9 -r- -mmt -x'!*.go' -x'!*.7z' -x'!*.zip' -x'!vendor/' * - rm *.exe - export GOOS=windows GOARCH=amd64 && go build -ldflags "-w -s" - 7za a ${EXE}_${GOOS}_${GOARCH}-${TRAVIS_TAG}.7z -ms=on -mx=9 -r- -mmt -x'!*.go' -x'!*.7z' -x'!*.zip' -x'!vendor/' * - rm *.exe - export GOOS=linux GOARCH=386 XZ_OPT=-9 && go build -ldflags "-w -s" - tar cvJpf ${EXE}_${GOOS}_${GOARCH}-${TRAVIS_TAG}.tar.xz * ${TAR_EXCLUDE} - export GOOS=linux GOARCH=amd64 XZ_OPT=-9 && go build -ldflags "-w -s" - tar cvJpf ${EXE}_${GOOS}_${GOARCH}-${TRAVIS_TAG}.tar.xz * ${TAR_EXCLUDE} - export GOOS=linux GOARCH=arm && go build -ldflags "-w -s" - tar cjf ${EXE}_${GOOS}_${GOARCH}-${TRAVIS_TAG}.tar.bz2 * ${TAR_EXCLUDE} - export GOOS=darwin GOARCH=amd64 && go build -ldflags "-w -s" - tar cjf ${EXE}_${GOOS}_${GOARCH}-${TRAVIS_TAG}.tar.bz2 * ${TAR_EXCLUDE} - md5sum *.7z *.xz *.bz2 | tee md5sum - sha1sum *.7z *.xz *.bz2 | tee sha1sum deploy: provider: releases api_key: secure: ${TOKEN} file_glob: true file: - "*.7z" - "*.xz" - "*.bz2" - "md5sum" - "sha1sum" skip_cleanup: true on: tags: true notifications: email: false ================================================ FILE: LICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: README.md ================================================ # gscan-quic 一个 IP 可用性扫描工具 ## 当前支持 - [x] QUIC - [x] SNI - [x] TLS - [x] PING - [ ] SOCKS4/SOCKS4A/SOCKS5 ## 简单说明 因为支持多种类型的IP扫描, 所以 IP 段文件是分开放的. 比如 quic 的IP段文件是 iprange_quic.txt, tls 的就是 iprange_tls.txt 其他类型的都可以在 config.json 文件里看到 **IP段文件格式如下:** # 下面几种都是支持的, 遇到错误IP格式是会跳过的 1.9.23.0 1.9.23.0/24 1.9.0.0/16 1.9.22.0-1.9.23.0/24 1.9.22.0/24-1.9.33.0/24 1.9.22.0-255 1.9.22.0-1.9.22.0 1.9.22.0-1.9.22.255 1.9.22.0-1.9.33.255 1.9.22.111-1.9.22.111 会自动精简成 1.9.22.111 1.9.22.0/24-1.9.22.0/24 会自动精简成 1.9.22.0/24 # 支持 ipv6 格式 2001:db8::1 2001:db8::1/128 # 支持 gop 的 "xxx","xxx" 和 goa 的 xxx|xxx 格式 "1.9.22.0", "1.9.22.1","1.9.22.2", 1.9.22.0|1.9.22.1| # IP段也是会自动去重的 1.9.22.0-255 1.9.0.0/16 1.9.22.0-255 1.9.22.0/24 1.9.22.0-255 1.9.22.0-1.9.22.100 1.9.22.0-1.9.22.255 1.9.0.0/16 3.3.3.0/24 3.3.0.0/16 3.3.3.0-255 1.1.1.0/24 1.9.0.0/16 # 上面几个经过去重只会留下 3.3.0.0/16 1.9.0.0/16 1.1.1.0/24 > **注意:** * 默认是有输出个数限制的, 可以设置配置文件里的 RecordLimit * 在扫描过程中是可以中断的, 只要按 Ctrl+C 就可以中断, 扫过的IP是会保留的 * 扫描IP段是随机的 * 如果IP段是 xx|xx 或 "xxx","xxx" 格式的, 那么一行的字节加起来大小不能超过4MB, 如有超过, 必须分行, 否则会跳过这一行 ## 配置说明 参考 config.json 文件 每次更新时最好覆盖掉 config.json 文件 支持 gop 形式的个人配置 config.user.json, 所以为了防止每次修改, 可以自己新建一个. ## 下载 到 https://github.com/Kisesy/gscan_quic/releases 下载编译好的 ## 感谢 改自 yinqiwen 大神的 https://github.com/yinqiwen/gscan 在此感谢 ================================================ FILE: config.json ================================================ { // 注意: // [扫描并发数] 理论上设置越大扫的越快, 但是并不意味越大就可以扫到更多IP // 大量的并发, 会造成网络堵塞, 甚至触到系统网络的限制或造成路由器宕机而出现更多的问题 // 所以如果在你那里基本扫不到IP, 可以试着减小扫描并发数并增大超时时间 // 新添了 Level 设置 // 可以按照说明进行修改来达到加速扫描的效果, 但是为了结果的准确性最好还是设置为最高 // 扫描并发数 "ScanWorker": 100, // 是否启用 Ping, 每次扫描 Tls、Quic...前都会先 ping 一下 "VerifyPing": false, // Ping 的参数 "ScanMinPingRTT": 80, "ScanMaxPingRTT": 800, // 是否开启备份 // 每次扫到的IP,都会在此目录下备份一份 "EnableBackup": true, "BackupDir": "./backup", // 是否禁用结束扫描时的命令行暂停 "DisablePause": false, // 扫描方式, 可以设置为下面的任意一个, 大小写都可以 "ScanMode": "quic", // 如果设置为 ping, VerifyPing 会自动关闭 "Ping": { "ScanCountPerIP": 1, "ScanMinRTT": 0, "ScanMaxRTT": 1000, "RecordLimit": 10000, "OutputSeparator": "\r\n", // 支持带端口格式的IP, 只是为了使用方便, 并不是真的支持端口, 端口会被自动去除 "InputFile": "./iprange_ping.txt", "OutputFile": "./out_ping.txt", }, "QUIC": { // 每个IP的测试次数 "ScanCountPerIP": 1, // ServerName 可以设置多项, 会随机选择一个 // 默认空列表,随机生成域名格式字符串 // 如果要设置为空, 可以写为 [""] "ServerName": [], // HTTPVerifyHosts 是 http 测试时使用的测试地址 // 如果设置多个, 将会随机选择一个进行测试 "HTTPVerifyHosts": ["dns.google.com"], // 握手超时时间, 单位: 毫秒 "HandshakeTimeout": 2500, "ScanMinRTT": 0, "ScanMaxRTT": 3000, // 结果的限制数量, 比如: 设置成 100, 将会扫到100个IP后自动结束 "RecordLimit": 10000, // 输出结果的分隔符, 比如: 如果想要换行输出, 可以改为: \n // 有个特殊的例外, 如果设置为 gop, 则会输出 "xxx", "xxx" 样式 "OutputSeparator": "gop", // IP 或 IP 段文件 "InputFile": "./iprange_quic.txt", // 输出的文件路径 "OutputFile": "./out_quic.txt", // 验证等级 // 1: 只是测试连接成功, 并且确定证书存在 // 2: 验证证书是否正确 // 3: 测试 HTTP 连接 // 4: 验证是否是 NoSuchBucket 错误 // (2.x版默认等级为3, 所以如果lv2搜到的IP不能用, 可以改为 3) "Level": 4, }, // 暂时只支持 google IP "TLS": { // 同 QUIC 说明 "ScanCountPerIP": 1, "ServerName": [], "HTTPVerifyHosts": ["dns.google.com"], "HandshakeTimeout": 2500, "ScanMinRTT": 0, "ScanMaxRTT": 3000, "RecordLimit": 10000, "OutputSeparator": "|", "InputFile": "./iprange_tls.txt", "OutputFile": "./out_tls.txt", // 1: 测试 tls 连接并握手成功 // 2: 证书验证 // 3: http 测试 "Level": 3, }, "SNI": { "ScanCountPerIP": 1, // 注意, SNI 需要同时验证两个不同域名的 ServerName 才能正确确认 // 当然你也可以改成一个来简单确认 // 还有, 不要填封锁严重的域名, 比如 google 的域名 // 这里的域名最好不用默认的, 防止检测 "ServerName": ["www.bing.com", "duckduckgo.com"], // "HTTPVerifyHosts": ["dns.google.com"], "HandshakeTimeout": 2500, "ScanMinRTT": 0, "ScanMaxRTT": 3000, "RecordLimit": 10000, "OutputSeparator": "\r\n", "InputFile": "./iprange_sni.txt", "OutputFile": "./out_sni.txt", // 1: 测试 tls 连接并握手成功 // 2: 证书验证 // 3: http 测试 "Level": 2, }, } ================================================ FILE: config.user.json.example ================================================ { "ScanWorker": 100, "VerifyPing": false, "ScanMinPingRTT": 80, "ScanMaxPingRTT": 800, "ScanMode": "quic", "Ping": { //"ScanCountPerIP": 1, //"ScanMinRTT": 0, //"ScanMaxRTT": 1000, //"RecordLimit": 10000, //"OutputSeparator": "\r\n", //"InputFile": "./iprange_ping.txt", //"OutputFile": "./out_ping.txt" }, "QUIC": { //"ScanCountPerIP": 1, //"ServerName": [], //"HTTPVerifyHosts": ["dns.google.com"], //"HandshakeTimeout": 2500, //"ScanMinRTT": 0, //"ScanMaxRTT": 3000, //"RecordLimit": 10000, //"OutputSeparator": "gop", //"InputFile": "./iprange_quic.txt", //"OutputFile": "./out_quic.txt", //"Level": 3 }, "TLS": { //"ScanCountPerIP": 1, //"ServerName": [], //"HTTPVerifyHosts": ["dns.google.com"], //"HandshakeTimeout": 2500, //"ScanMinRTT": 0, //"ScanMaxRTT": 3000, //"RecordLimit": 10000, //"OutputSeparator": "|", //"InputFile": "./iprange_tls.txt", //"OutputFile": "./out_tls.txt", //"Level": 3 }, "SNI": { //"ScanCountPerIP": 1, //"ServerName": ["www.bing.com", "duckduckgo.com"], //"HandshakeTimeout": 2500, //"ScanMinRTT": 0, //"ScanMaxRTT": 3000, //"RecordLimit": 10000, //"OutputSeparator": "\r\n", //"InputFile": "./iprange_sni.txt", //"OutputFile": "./out_sni.txt", //"Level": 2 } } ================================================ FILE: go.mod ================================================ module github.com/kisesy/gscan_quic go 1.21 require ( github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 github.com/quic-go/quic-go v0.36.1-0.20230701190300-fd0c9bbf9e1f ) require ( github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.3.0 // indirect golang.org/x/crypto v0.4.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/tools v0.9.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 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/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.3.0 h1:NrCXmDl8BddZwO67vlvEpBTwT89bJfKYygxv4HQvuDk= github.com/quic-go/qtls-go1-20 v0.3.0/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/quic-go v0.36.1-0.20230701190300-fd0c9bbf9e1f h1:I4cT4zLUjYfL6SEqIzZQWe1xTmJRvKZRWrLhmcyaZeA= github.com/quic-go/quic-go v0.36.1-0.20230701190300-fd0c9bbf9e1f/go.mod h1:XtCUOCALTTWbPyd0IxFfHf6h0sEMubRFvEYHl3QxKw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: gscan.go ================================================ package main import ( "bytes" "errors" "flag" "fmt" "log" "math/rand" "os" "os/exec" "path/filepath" "runtime" "sort" "strings" "time" ) type ScanConfig struct { ScanCountPerIP int ServerName []string HTTPVerifyHosts []string HandshakeTimeout time.Duration ScanMinRTT time.Duration ScanMaxRTT time.Duration RecordLimit int InputFile string OutputFile string OutputSeparator string Level int } type GScanner struct { ScanWorker int VerifyPing bool ScanMinPingRTT time.Duration ScanMaxPingRTT time.Duration DisablePause bool EnableBackup bool BackupDir string ScanRecords `json:"-"` ScanMode string PING ScanConfig QUIC ScanConfig TLS ScanConfig SNI ScanConfig } func init() { rand.Seed(time.Now().UnixNano()) log.SetFlags(log.LstdFlags | log.Lshortfile) } func (gs *GScanner) loadConfig(cfgFile string) error { exe, err := os.Executable() if err != nil { return errors.New("could not get executable path") } execFolder := filepath.Dir(exe) if strings.HasPrefix(cfgFile, "./") { cfgFile = filepath.Join(execFolder, cfgFile) } config := gs if err := readJsonConfig(cfgFile, config); err != nil { return fmt.Errorf("could not read config file: %v", err) } if config.EnableBackup { if strings.HasPrefix(config.BackupDir, "./") { config.BackupDir = filepath.Join(execFolder, config.BackupDir) } err := os.MkdirAll(config.BackupDir, 0o644) if err != nil { return fmt.Errorf("could not create backup dir: %v", err) } } config.ScanMode = strings.ToLower(config.ScanMode) if config.ScanMode == "ping" { config.VerifyPing = false } config.ScanMinPingRTT *= time.Millisecond config.ScanMaxPingRTT *= time.Millisecond scanConfigs := []*ScanConfig{&config.QUIC, &config.TLS, &config.SNI, &config.PING} for _, scanConfig := range scanConfigs { if strings.HasPrefix(scanConfig.InputFile, "./") { scanConfig.InputFile = filepath.Join(execFolder, scanConfig.InputFile) } else { scanConfig.InputFile, _ = filepath.Abs(scanConfig.InputFile) } if strings.HasPrefix(scanConfig.OutputFile, "./") { scanConfig.OutputFile = filepath.Join(execFolder, scanConfig.OutputFile) } else { scanConfig.OutputFile, _ = filepath.Abs(scanConfig.OutputFile) } if !pathExist(scanConfig.InputFile) { os.Create(scanConfig.InputFile) } scanConfig.ScanMinRTT *= time.Millisecond scanConfig.ScanMaxRTT *= time.Millisecond scanConfig.HandshakeTimeout *= time.Millisecond } return nil } func main() { var cfgfile string flag.StringVar(&cfgfile, "Config File", "./config.json", "Config file, json format") flag.Parse() scanner := new(GScanner) defer func() { if r := recover(); r != nil { fmt.Println("panic:", r) } fmt.Println() if scanner.DisablePause { return } if runtime.GOOS == "windows" { cmd := exec.Command("cmd", "/C", "pause") cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin // 改为 start, 程序可以正常退出, 这样一些程序监视工具可以正常测到程序结束了 cmd.Start() } else { fmt.Println("Press [Enter] to exit...") fmt.Scanln() } }() err := scanner.loadConfig(cfgfile) if err != nil { log.Println(err) return } scanMode := scanner.ScanMode cfg, _ := scanner.getScanConfig(scanMode) iprangeFile := cfg.InputFile if !pathExist(iprangeFile) { log.Panicf("IP Range file not exist: %s", iprangeFile) } log.Printf("Start loading IP Range file: %s", iprangeFile) ipqueue, err := parseIPRangeFile(iprangeFile) if err != nil { log.Panicln(err) } log.Printf("Start scanning available IP") startTime := time.Now() scanner.StartScan(ipqueue) records := scanner.Records() log.Printf("Scanned %d IP in %s, found %d records", scanner.ScanCount(), time.Since(startTime), len(records)) if len(records) == 0 { return } sort.Slice(records, func(i, j int) bool { return records[i].RTT < records[j].RTT }) a := make([]string, len(records)) for i, r := range records { a[i] = r.IP } b := new(bytes.Buffer) if cfg.OutputSeparator == "gop" { out := strings.Join(a, `", "`) b.WriteString(`"`) b.WriteString(out) b.WriteString(`",`) } else { out := strings.Join(a, cfg.OutputSeparator) b.WriteString(out) } if err := os.WriteFile(cfg.OutputFile, b.Bytes(), 0o644); err != nil { log.Printf("Failed to write output file:%s for reason: %v", cfg.OutputFile, err) } else { log.Printf("All results written to %s", cfg.OutputFile) } if scanner.EnableBackup { filename := fmt.Sprintf("%s_%s_lv%d.txt", scanMode, time.Now().Format("20060102_150405"), cfg.Level) bakfilename := filepath.Join(scanner.BackupDir, filename) if err := os.WriteFile(bakfilename, b.Bytes(), 0o644); err != nil { log.Printf("Failed to write output file:%s for reason: %v\n", bakfilename, err) } else { log.Printf("All results written to %s\n", bakfilename) } } } func (gcfg *GScanner) getScanConfig(scanMode string) (*ScanConfig, testIPFunc) { switch scanMode { case "quic": return &gcfg.QUIC, testQuic case "tls": return &gcfg.TLS, testTls case "sni": return &gcfg.SNI, testSni case "ping": return &gcfg.PING, testPing // case "socks5": // testIPFunc = testSocks5 default: log.Panicln("Unknown scan mode:", scanMode) } return nil, nil } ================================================ FILE: iprange.go ================================================ package main import ( "bufio" "math/rand" "net" "os" "sort" "strings" "github.com/mikioh/ipaddr" ) // 从每组地址中分离出起始IP以及结束IP // 参考自 moonshawdo 的代码 func splitIP(strline string) []ipaddr.Prefix { var begin, end string if strings.Contains(strline, "-") && strings.Contains(strline, "/") { ss := strings.Split(strline, "-") if len(ss) == 2 { iprange1, iprange2 := ss[0], ss[1] // "1.9.22.0/24-1.9.22.0" if strings.Contains(iprange1, "/") && !strings.Contains(iprange2, "/") { begin = iprange1[:strings.Index(iprange1, "/")] if begin == iprange2 { iprange2 = iprange1 } } else if strings.Contains(iprange2, "/") { // 1.9.22.0/24-1.9.22.0/24 begin = iprange1[:strings.Index(iprange1, "/")] } else { // 1.9.22.0-1.9.23.0/24 begin = iprange1 } // c, err := ipaddr.Parse(begin + "," + iprange2) // if err != nil { // panic(err) // } // return ipaddr.Aggregate(c.List()) if c, err := ipaddr.Parse(iprange2); err == nil { end = c.Last().IP.String() } } } else if strings.Contains(strline, "-") { num_regions := strings.Split(strline, ".") if len(num_regions) == 4 { // "xxx.xxx.xxx-xxx.xxx-xxx" for _, region := range num_regions { if strings.Contains(region, "-") { a := strings.Split(region, "-") s, e := a[0], a[1] begin += "." + s end += "." + e } else { begin += "." + region end += "." + region } } begin = begin[1:] end = end[1:] } else { // "xxx.xxx.xxx.xxx-xxx.xxx.xxx.xxx" a := strings.Split(strline, "-") begin, end = a[0], a[1] if 1 <= len(end) && len(end) <= 3 { prefix := begin[0:strings.LastIndex(begin, ".")] end = prefix + "." + end } } } else if strings.HasSuffix(strline, ".") { // "xxx.xxx.xxx." begin = strline + "0" end = strline + "255" } else if strings.Contains(strline, "/") { // "xxx.xxx.xxx.xxx/xx" if c, err := ipaddr.Parse(strline); err == nil { return c.List() } return nil } else { // "xxx.xxx.xxx.xxx" // 如果IP带有端口, 那么就分离端口 if i := strings.LastIndex(strline, ":"); i != -1 { if c, err := ipaddr.Parse(strline[:i]); err == nil { return c.List() } } begin = strline end = strline } return ipaddr.Summarize(net.ParseIP(begin), net.ParseIP(end)) } var sepReplacer = strings.NewReplacer(`","`, ",", `", "`, ",", "|", ",") func parseIPRangeFile(file string) (chan string, error) { f, err := os.Open(file) if err != nil { return nil, err } defer f.Close() ipranges := make([]ipaddr.Prefix, 0) scanner := bufio.NewScanner(f) // 一行最大 4MB buf := make([]byte, 1024*1024*4) scanner.Buffer(buf, len(buf)) for scanner.Scan() { line := strings.TrimFunc(scanner.Text(), func(r rune) bool { switch r { case ',', '|', '"', '\'': return true case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0: return true } return false }) if line == "" || strings.HasPrefix(line, "#") { continue } // 支持 gop 的 "xxx","xxx" 和 goa 的 xxx|xxx 格式 if s := sepReplacer.Replace(line); strings.Contains(s, ",") { if c, err := ipaddr.Parse(s); err == nil { ipranges = append(ipranges, c.List()...) } } else { ipranges = append(ipranges, splitIP(line)...) } } out := make(chan string, 200) go func() { defer close(out) if len(ipranges) > 0 { ipranges = dedup(ipranges) // 打乱IP段扫描顺序 rand.Shuffle(len(ipranges), func(i, j int) { ipranges[i], ipranges[j] = ipranges[j], ipranges[i] }) // 打乱IP的扫描顺序, 不再等待一个IP段扫描完毕再进行下一个, 提高扫描的随机性 // 有时候可以更快的扫到IP n := 15 if len(ipranges) < n { n = len(ipranges) } ops(len(ipranges), n, func(i, _ int) { c := ipaddr.NewCursor([]ipaddr.Prefix{ipranges[i]}) for ip := c.First(); ip != nil; ip = c.Next() { out <- ip.IP.String() } }) } }() return out, nil } /* IP段去重 (此描述对当前算法不适用-2017/09/21) "1.9.22.0-255" "1.9.0.0/16" "1.9.22.0-255" "1.9.22.0/24" "1.9.22.0-255" "1.9.22.0-1.9.22.100" "1.9.22.0-1.9.22.255" "1.9.0.0/16" "3.3.3.0/24" "3.3.0.0/16" "3.3.3.0-255" "1.1.1.0/24" "1.9.0.0/16" "2001:db8::1/128" | | v [1.9.0.0/16 3.3.0.0/16 1.1.1.0/24 203.0.113.0/24 2001:db8::1/128] */ func dedup(s []ipaddr.Prefix) []ipaddr.Prefix { sort.Slice(s, func(i int, j int) bool { return s[i].String() < s[j].String() }) out := s[:1] t := s[0] for _, s := range s[1:] { if !t.Contains(&s) && !t.Equal(&s) { out = append(out, s) t = s } } return out } ================================================ FILE: iprange_quic.txt.example ================================================ 108.177.97.0/24 113.171.202.0/24 113.171.216.0/24 113.171.220.0/24 113.171.232.0/24 113.171.236.0/24 113.171.237.0/24 113.171.238.0/24 121.158.53.0/24 121.78.42.0/24 122.202.129.0/24 123.241.255.0/24 124.216.0.0/24 157.197.92.0/24 157.197.93.0/24 182.163.240.0/24 183.91.238.0/24 203.233.10.0/24 203.233.37.0/24 203.233.88.0/24 203.233.92.0/24 203.233.96.0/24 203.248.132.0/24 203.252.15.0/24 211.49.146.0/24 221.144.116.0/24 223.62.225.0/24 49.231.113.0/24 61.62.98.0/24 61.90.179.0/24 61.90.189.0/24 61.91.8.0/24 61.91.9.0/24 222.251.134.0/24 ================================================ FILE: ping.go ================================================ package main // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // taken from http://golang.org/src/pkg/net/ipraw_test.go import ( "bytes" "context" "errors" "net" "os" "time" ) func testPing(ctx context.Context, ip string, config *ScanConfig, record *ScanRecord) bool { start := time.Now() if err := Pinger(ip, config.ScanMaxRTT); err != nil { return false } if rtt := time.Since(start); rtt > config.ScanMinRTT { record.RTT += rtt return true } return false } const ( icmpv4EchoRequest = 8 icmpv4EchoReply = 0 icmpv6EchoRequest = 128 icmpv6EchoReply = 129 ) var ErrPingConnFailed = errors.New("ping: connect failed") type icmpMessage struct { Type int // type Code int // code Checksum int // checksum Body icmpMessageBody // body } type icmpMessageBody interface { Len() int Marshal() ([]byte, error) } // Marshal returns the binary enconding of the ICMP echo request or // reply message m. func (m *icmpMessage) Marshal() ([]byte, error) { b := []byte{byte(m.Type), byte(m.Code), 0, 0} if m.Body != nil && m.Body.Len() != 0 { mb, err := m.Body.Marshal() if err != nil { return nil, err } b = append(b, mb...) } switch m.Type { case icmpv6EchoRequest, icmpv6EchoReply: return b, nil } csumcv := len(b) - 1 // checksum coverage s := uint32(0) for i := 0; i < csumcv; i += 2 { s += uint32(b[i+1])<<8 | uint32(b[i]) } if csumcv&1 == 0 { s += uint32(b[csumcv]) } s = s>>16 + s&0xffff s = s + s>>16 // Place checksum back in header; using ^= avoids the // assumption the checksum bytes are zero. b[2] ^= byte(^s & 0xff) b[3] ^= byte(^s >> 8) return b, nil } // parseICMPMessage parses b as an ICMP message. func parseICMPMessage(b []byte) (*icmpMessage, error) { msglen := len(b) if msglen < 4 { return nil, errors.New("message too short") } m := &icmpMessage{Type: int(b[0]), Code: int(b[1]), Checksum: int(b[2])<<8 | int(b[3])} if msglen > 4 { var err error switch m.Type { case icmpv4EchoRequest, icmpv4EchoReply, icmpv6EchoRequest, icmpv6EchoReply: m.Body, err = parseICMPEcho(b[4:]) if err != nil { return nil, err } } } return m, nil } // imcpEcho represenets an ICMP echo request or reply message body. type icmpEcho struct { ID int // identifier Seq int // sequence number Data []byte // data } func (p *icmpEcho) Len() int { if p == nil { return 0 } return 4 + len(p.Data) } // Marshal returns the binary enconding of the ICMP echo request or // reply message body p. func (p *icmpEcho) Marshal() ([]byte, error) { b := make([]byte, 4+len(p.Data)) b[0], b[1] = byte(p.ID>>8), byte(p.ID&0xff) b[2], b[3] = byte(p.Seq>>8), byte(p.Seq&0xff) copy(b[4:], p.Data) return b, nil } // parseICMPEcho parses b as an ICMP echo request or reply message body. func parseICMPEcho(b []byte) (*icmpEcho, error) { bodylen := len(b) p := &icmpEcho{ID: int(b[0])<<8 | int(b[1]), Seq: int(b[2])<<8 | int(b[3])} if bodylen > 4 { p.Data = make([]byte, bodylen-4) copy(p.Data, b[4:]) } return p, nil } func Ping(address string, timeout time.Duration) error { return Pinger(address, timeout) } func Pinger(address string, timeout time.Duration) error { typ := icmpv4EchoRequest network := "ip4:icmp" isIpv6 := false if ip := net.ParseIP(address); ip != nil && ip.To4() == nil { typ = icmpv6EchoRequest network = "ip6:ipv6-icmp" isIpv6 = true } c, err := net.Dial(network, address) if err != nil { return ErrPingConnFailed } defer c.Close() deadline := time.Now().Add(timeout) c.SetReadDeadline(deadline) xid, xseq := os.Getpid()&0xffff, 1 wb, err := (&icmpMessage{ Type: typ, Code: 0, Body: &icmpEcho{ ID: xid, Seq: xseq, Data: bytes.Repeat([]byte("Go Go Gadget Ping!!!"), 3), }, }).Marshal() if err != nil { return err } if _, err = c.Write(wb); err != nil { return err } var m *icmpMessage rb := make([]byte, 20+len(wb)) for { // read_timeout := deadline.Sub(time.Now()) // c.SetReadDeadline(time.Now().Add(read_timeout)) if _, err = c.Read(rb); err != nil { return err } if !isIpv6 { rb = ipv4Payload(rb) } if m, err = parseICMPMessage(rb); err != nil { return err } switch m.Type { case icmpv4EchoRequest, icmpv6EchoRequest: continue } break } return nil } func ipv4Payload(b []byte) []byte { if len(b) < 20 { return b } hdrlen := int(b[0]&0x0f) << 2 return b[hdrlen:] } ================================================ FILE: quic.go ================================================ package main import ( "bytes" "context" "crypto/tls" "io" "math/rand" "net" "net/http" "strings" "time" quic "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/http3" ) var errNoSuchBucket = []byte("NoSuchBucketThe specified bucket does not exist.") func testQuic(ctx context.Context, ip string, config *ScanConfig, record *ScanRecord) bool { start := time.Now() quicCfg := &quic.Config{ HandshakeIdleTimeout: config.HandshakeTimeout, KeepAlivePeriod: 0, } serverName := "" if len(config.ServerName) == 0 { serverName = randomHost() } else { serverName = randomChoice(config.ServerName) } tlsCfg := &tls.Config{ InsecureSkipVerify: true, ServerName: serverName, NextProtos: []string{"h3-29", "h3", "hq", "quic"}, } ctx, cancel := context.WithTimeout(ctx, config.ScanMaxRTT) defer cancel() quicConn, err := quic.DialAddrEarly(ctx, net.JoinHostPort(ip, "443"), tlsCfg, quicCfg) if err != nil { return false } defer quicConn.CloseWithError(0, "") // lv1 只会验证证书是否存在 cs := quicConn.ConnectionState().TLS if !cs.HandshakeComplete || len(cs.PeerCertificates) < 2 { return false } // lv2 验证证书是否正确 if config.Level > 1 { pkp := cs.PeerCertificates[1].RawSubjectPublicKeyInfo if !bytes.Equal(gpkp, pkp) { return false } } // lv3 使用 http 访问来验证 if config.Level > 2 { tr := &http3.RoundTripper{DisableCompression: true} defer tr.Close() tr.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { return quicConn, err } // 设置超时 hclient := &http.Client{ Transport: tr, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: config.ScanMaxRTT - time.Since(start), } url := "https://" + config.HTTPVerifyHosts[rand.Intn(len(config.HTTPVerifyHosts))] req, _ := http.NewRequest(http.MethodGet, url, nil) req.Close = true resp, _ := hclient.Do(req) if resp == nil || (resp.StatusCode < 200 || resp.StatusCode >= 400) || !strings.Contains(resp.Header.Get("Alt-Svc"), `quic=":443"`) { return false } if resp.Body != nil { defer resp.Body.Close() // lv4 验证是否是 NoSuchBucket 错误 if config.Level > 3 && resp.Header.Get("Content-Type") == "application/xml; charset=UTF-8" { // 也许条件改为 || 更好 body, err := io.ReadAll(resp.Body) if err != nil || bytes.Equal(body, errNoSuchBucket) { return false } } else { io.Copy(io.Discard, resp.Body) } } } if rtt := time.Since(start); rtt > config.ScanMinRTT { record.RTT += rtt return true } return false } ================================================ FILE: scan.go ================================================ package main import ( "context" "log" "os" "os/signal" "sync" "sync/atomic" "time" ) type ScanRecord struct { IP string RTT time.Duration } type ScanRecords struct { recordMutex sync.Mutex records []*ScanRecord scanCounter int32 } func (srs *ScanRecords) AddRecord(rec *ScanRecord) { srs.recordMutex.Lock() srs.records = append(srs.records, rec) srs.recordMutex.Unlock() log.Printf("Found a record: IP=%s, RTT=%s\n", rec.IP, rec.RTT.String()) } func (srs *ScanRecords) IncScanCounter() { scanCount := atomic.AddInt32(&srs.scanCounter, 1) if scanCount%1000 == 0 { log.Printf("Scanned %d IPs, Found %d records\n", scanCount, srs.RecordSize()) } } func (srs *ScanRecords) RecordSize() int { srs.recordMutex.Lock() defer srs.recordMutex.Unlock() return len(srs.records) } func (srs *ScanRecords) ScanCount() int32 { return atomic.LoadInt32(&srs.scanCounter) } func (srs *ScanRecords) Records() []*ScanRecord { srs.recordMutex.Lock() defer srs.recordMutex.Unlock() return srs.records } type testIPFunc func(ctx context.Context, ip string, config *ScanConfig, record *ScanRecord) bool func testip(ctx context.Context, testFunc testIPFunc, ip string, config *ScanConfig) *ScanRecord { record := new(ScanRecord) for i := 0; i < config.ScanCountPerIP; i++ { if !testFunc(ctx, ip, config, record) { return nil } } record.IP = ip record.RTT = record.RTT / time.Duration(config.ScanCountPerIP) return record } func (gs *GScanner) testIPWorker(ctx context.Context, ipQueue chan string) { cfg, testFunc := gs.getScanConfig(gs.ScanMode) for ip := range ipQueue { // log.Printf("Start testing IP: %s", ip) if gs.VerifyPing { start := time.Now() pingErr := Ping(ip, gs.ScanMaxPingRTT) if pingErr != nil || time.Since(start) < gs.ScanMinPingRTT { continue } } select { case <-ctx.Done(): return default: r := testip(ctx, testFunc, ip, cfg) if r != nil { gs.AddRecord(r) // 这里放到前面,扫描时可能会多出一些记录, 但是不影响 if gs.RecordSize() >= cfg.RecordLimit { return } } gs.IncScanCounter() // 扫描完后才增加计数 } } } func (gs *GScanner) StartScan(ipQueue chan string) { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() n := gs.ScanWorker ops(n, n, func(i, thread int) { gs.testIPWorker(ctx, ipQueue) }) } ================================================ FILE: sni.go ================================================ package main import ( "context" "crypto/tls" "net" "net/http" "net/http/httputil" "time" ) func testSni(ctx context.Context, ip string, config *ScanConfig, record *ScanRecord) bool { tlscfg := &tls.Config{ InsecureSkipVerify: true, } for _, serverName := range config.ServerName { start := time.Now() ctx, cancel := context.WithTimeout(ctx, config.ScanMaxRTT) defer cancel() conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(ip, "443")) if err != nil { return false } tlscfg.ServerName = serverName tlsconn := tls.Client(conn, tlscfg) tlsconn.SetDeadline(time.Now().Add(config.HandshakeTimeout)) if err = tlsconn.Handshake(); err != nil { tlsconn.Close() return false } if config.Level > 1 { pcs := tlsconn.ConnectionState().PeerCertificates if len(pcs) == 0 || pcs[0].Subject.CommonName != serverName { tlsconn.Close() return false } } if config.Level > 2 { req, err := http.NewRequest(http.MethodHead, "https://"+serverName, nil) if err != nil { tlsconn.Close() return false } tlsconn.SetDeadline(time.Now().Add(config.ScanMaxRTT - time.Since(start))) resp, err := httputil.NewClientConn(tlsconn, nil).Do(req) if err != nil { tlsconn.Close() return false } // io.Copy(os.Stdout, resp.Body) // if resp.Body != nil { // io.Copy(io.Discard, resp.Body) // resp.Body.Close() // } if resp.StatusCode >= 400 { tlsconn.Close() return false } } tlsconn.Close() rtt := time.Since(start) if rtt < config.ScanMinRTT { return false } record.RTT += rtt } return true } ================================================ FILE: tls.go ================================================ package main import ( "bytes" "context" "crypto/tls" "encoding/base64" "io" "math/rand" "net" "net/http" "time" ) var gpkp, _ = base64.StdEncoding.DecodeString("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9Yjf52KMHjf4N0KQf2yH0PtlgiX96MtrpP9t6Voj4pn2HOmSA5kTfAkKivpC1l5WJKp6M4Qf0elpu7l07FdMZmiTdzdVU/45EE23NLtfJXc3OxeU6jzlndW8w7RD6y6nR++wRBFj2LRBhd1BMEiTG7+39uBFAiHglkIXz9krZVY0ByYEDaj9fcou7+pIfDdNPwCfg9/vdYQueVdc/FduGpb//Iyappm+Jdl/liwG9xEqAoCA62MYPFBJh+WKyl8ZK1mWgQCg+1HbyncLC8mWT+9wScdcbSD9mbS04soud/0t3Au2axMMjBkrF5aYufCL9qAnu7bjjVGPva7Hm7GJnQIDAQAB") func testTls(ctx context.Context, ip string, config *ScanConfig, record *ScanRecord) bool { start := time.Now() ctx, cancel := context.WithTimeout(ctx, config.ScanMaxRTT) defer cancel() var dialer net.Dialer conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "443")) if err != nil { return false } defer conn.Close() var serverName string if len(config.ServerName) == 0 { serverName = randomHost() } else { serverName = randomChoice(config.ServerName) } tlscfg := &tls.Config{ InsecureSkipVerify: true, MinVersion: tls.VersionTLS11, MaxVersion: tls.VersionTLS12, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, tls.TLS_RSA_WITH_AES_128_CBC_SHA256, tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, }, ServerName: serverName, } tlsconn := tls.Client(conn, tlscfg) defer tlsconn.Close() tlsconn.SetDeadline(time.Now().Add(config.HandshakeTimeout)) if err = tlsconn.Handshake(); err != nil { return false } if config.Level > 1 { pcs := tlsconn.ConnectionState().PeerCertificates if pcs == nil || len(pcs) < 2 { return false } if org := pcs[1].Subject.Organization; len(org) == 0 || org[0] != "Google Trust Services LLC" { return false } pkp := pcs[1].RawSubjectPublicKeyInfo if !bytes.Equal(gpkp, pkp) { return false } } if config.Level > 2 { url := "https://" + config.HTTPVerifyHosts[rand.Intn(len(config.HTTPVerifyHosts))] req, _ := http.NewRequest(http.MethodGet, url, nil) req.Close = true c := http.Client{ Transport: &http.Transport{ DialTLS: func(network, addr string) (net.Conn, error) { return tlsconn, nil }, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: config.ScanMaxRTT - time.Since(start), } resp, _ := c.Do(req) if resp == nil || (resp.StatusCode < 200 || resp.StatusCode >= 400) { return false } if resp.Body != nil { io.Copy(io.Discard, resp.Body) resp.Body.Close() } } if rtt := time.Since(start); rtt > config.ScanMinRTT { record.RTT += rtt return true } return false } ================================================ FILE: util.go ================================================ package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "math/rand" "os" "path" "strings" "sync" ) // 代码来自: goproxy func readJsonConfig(filename string, config interface{}) error { fileext := path.Ext(filename) filename1 := strings.TrimSuffix(filename, fileext) + ".user" + fileext cm := make(map[string]interface{}) for i, name := range []string{filename, filename1} { data, err := os.ReadFile(name) if err != nil { if i == 0 { return err } else { continue } } data = bytes.TrimPrefix(data, []byte("\xef\xbb\xbf")) data, err = readJson(bytes.NewReader(data)) if err != nil { return err } cm1 := make(map[string]interface{}) d := json.NewDecoder(bytes.NewReader(data)) d.UseNumber() if err = d.Decode(&cm1); err != nil { return err } if err = mergeMap(cm, cm1); err != nil { return err } } data, err := json.Marshal(cm) if err != nil { return err } d := json.NewDecoder(bytes.NewReader(data)) d.UseNumber() return d.Decode(config) } func readJson(r io.Reader) ([]byte, error) { scanner := bufio.NewScanner(r) var b bytes.Buffer prev := "" for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "//") { continue } if strings.HasPrefix(line, "}") || strings.HasPrefix(line, "]") { prev = strings.TrimSuffix(prev, ",") } b.WriteString(prev) prev = line } b.WriteString(prev) return b.Bytes(), scanner.Err() } func mergeMap(m1 map[string]interface{}, m2 map[string]interface{}) error { for key, value := range m2 { m1v, m1_has_key := m1[key] m2v, m2v_is_map := value.(map[string]interface{}) m1v1, m1v_is_map := m1v.(map[string]interface{}) switch { case !m1_has_key, !m2v_is_map: m1[key] = value case !m1v_is_map: return fmt.Errorf("m1v=%#v is not a map, but m2v=%#v is a map", m1v, m2v) default: mergeMap(m1v1, m2v) } } return nil } func randInt(l, u int) int { return rand.Intn(u-l) + l } func randomChoice[T any](a []T) T { return a[rand.Intn(len(a))] } // 生成两段或三段的随机字符串当作 host // llm.xadl // unupk.bfrf.pvi func randomHost() string { a := make([][]byte, randInt(2, 4)) for i := range a { m := randInt(3, 7) b := make([]byte, m) for j := 0; j < m; j++ { b[j] = byte(randInt(97, 122)) } a[i] = b } return string(bytes.Join(a, []byte{46})) } func or[T comparable](vals ...T) T { var zero T for _, val := range vals { if val != zero { return val } } return zero } // pathExist 返回文件或文件夹是否存在 func pathExist(name string) bool { _, err := os.Stat(name) return !os.IsNotExist(err) } func ops(count, threads int, op func(i, thread int)) { var wg sync.WaitGroup wg.Add(threads) for i := 0; i < threads; i++ { s, e := count/threads*i, count/threads*(i+1) if i == threads-1 { e = count } go func(i, s, e int) { for j := s; j < e; j++ { op(j, i) } wg.Done() }(i, s, e) } wg.Wait() }