Repository: cedar2025/hysteria Branch: master Commit: 6f4f1d3e9916 Files: 211 Total size: 909.8 KB Directory structure: gitextract_5rjruq7m/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── bug_report.zh.md │ │ ├── feature_request.md │ │ └── feature_request.zh.md │ ├── dependabot.yml │ └── workflows/ │ ├── autotag.yaml │ ├── docker.yml │ ├── master.yml │ ├── release.yml │ └── scripts.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── PROTOCOL.md ├── README.md ├── app/ │ ├── cmd/ │ │ ├── client.go │ │ ├── client_test.go │ │ ├── client_test.yaml │ │ ├── errors.go │ │ ├── ping.go │ │ ├── root.go │ │ ├── server.go │ │ ├── server_test.go │ │ ├── server_test.yaml │ │ ├── share.go │ │ ├── speedtest.go │ │ ├── update.go │ │ └── version.go │ ├── go.mod │ ├── go.sum │ ├── internal/ │ │ ├── forwarding/ │ │ │ ├── tcp.go │ │ │ ├── tcp_test.go │ │ │ ├── udp.go │ │ │ └── udp_test.go │ │ ├── http/ │ │ │ ├── server.go │ │ │ ├── server_test.go │ │ │ ├── server_test.py │ │ │ ├── test.crt │ │ │ └── test.key │ │ ├── proxymux/ │ │ │ ├── .mockery.yaml │ │ │ ├── internal/ │ │ │ │ └── mocks/ │ │ │ │ ├── mock_Conn.go │ │ │ │ └── mock_Listener.go │ │ │ ├── manager.go │ │ │ ├── manager_test.go │ │ │ ├── mux.go │ │ │ └── mux_test.go │ │ ├── redirect/ │ │ │ ├── getsockopt_linux.go │ │ │ ├── getsockopt_linux_386.go │ │ │ ├── syscall_socketcall_linux_386.s │ │ │ ├── tcp_linux.go │ │ │ └── tcp_others.go │ │ ├── sockopts/ │ │ │ ├── fd_control_unix_socket_test.py │ │ │ ├── sockopts.go │ │ │ ├── sockopts_linux.go │ │ │ └── sockopts_linux_test.go │ │ ├── socks5/ │ │ │ ├── server.go │ │ │ ├── server_test.go │ │ │ └── server_test.py │ │ ├── tproxy/ │ │ │ ├── tcp_linux.go │ │ │ ├── tcp_others.go │ │ │ ├── udp_linux.go │ │ │ └── udp_others.go │ │ ├── tun/ │ │ │ ├── log.go │ │ │ └── server.go │ │ ├── url/ │ │ │ ├── url.go │ │ │ └── url_test.go │ │ ├── utils/ │ │ │ ├── bpsconv.go │ │ │ ├── bpsconv_test.go │ │ │ ├── certloader.go │ │ │ ├── certloader_test.go │ │ │ ├── certloader_test_gencert.py │ │ │ ├── certloader_test_tlsclient.py │ │ │ ├── geoloader.go │ │ │ ├── qr.go │ │ │ ├── testcerts/ │ │ │ │ └── .gitignore │ │ │ └── update.go │ │ └── utils_test/ │ │ └── mock.go │ ├── main.go │ ├── misc/ │ │ └── socks5_test.py │ └── pprof.go ├── core/ │ ├── client/ │ │ ├── .mockery.yaml │ │ ├── client.go │ │ ├── config.go │ │ ├── mock_udpIO.go │ │ ├── reconnect.go │ │ ├── udp.go │ │ └── udp_test.go │ ├── errors/ │ │ └── errors.go │ ├── go.mod │ ├── go.sum │ ├── internal/ │ │ ├── congestion/ │ │ │ ├── bbr/ │ │ │ │ ├── bandwidth.go │ │ │ │ ├── bandwidth_sampler.go │ │ │ │ ├── bbr_sender.go │ │ │ │ ├── clock.go │ │ │ │ ├── packet_number_indexed_queue.go │ │ │ │ ├── ringbuffer.go │ │ │ │ └── windowed_filter.go │ │ │ ├── brutal/ │ │ │ │ └── brutal.go │ │ │ ├── common/ │ │ │ │ └── pacer.go │ │ │ └── utils.go │ │ ├── frag/ │ │ │ ├── frag.go │ │ │ └── frag_test.go │ │ ├── integration_tests/ │ │ │ ├── .mockery.yaml │ │ │ ├── close_test.go │ │ │ ├── hook_test.go │ │ │ ├── masq_test.go │ │ │ ├── mocks/ │ │ │ │ ├── mock_Authenticator.go │ │ │ │ ├── mock_Conn.go │ │ │ │ ├── mock_EventLogger.go │ │ │ │ ├── mock_Outbound.go │ │ │ │ ├── mock_RequestHook.go │ │ │ │ ├── mock_TrafficLogger.go │ │ │ │ └── mock_UDPConn.go │ │ │ ├── smoke_test.go │ │ │ ├── stress_test.go │ │ │ ├── test.crt │ │ │ ├── test.key │ │ │ ├── trafficlogger_test.go │ │ │ └── utils_test.go │ │ ├── pmtud/ │ │ │ ├── avail.go │ │ │ └── unavail.go │ │ ├── protocol/ │ │ │ ├── http.go │ │ │ ├── padding.go │ │ │ ├── proxy.go │ │ │ └── proxy_test.go │ │ └── utils/ │ │ ├── atomic.go │ │ └── qstream.go │ └── server/ │ ├── .mockery.yaml │ ├── config.go │ ├── copy.go │ ├── mock_UDPConn.go │ ├── mock_udpEventLogger.go │ ├── mock_udpIO.go │ ├── server.go │ ├── udp.go │ └── udp_test.go ├── entrypoint ├── extras/ │ ├── auth/ │ │ ├── command.go │ │ ├── http.go │ │ ├── http_test.go │ │ ├── http_test.py │ │ ├── password.go │ │ ├── password_test.go │ │ ├── userpass.go │ │ ├── userpass_test.go │ │ └── v2board.go │ ├── correctnet/ │ │ └── correctnet.go │ ├── go.mod │ ├── go.sum │ ├── masq/ │ │ └── server.go │ ├── obfs/ │ │ ├── conn.go │ │ ├── salamander.go │ │ └── salamander_test.go │ ├── outbounds/ │ │ ├── .mockery.yaml │ │ ├── acl/ │ │ │ ├── compile.go │ │ │ ├── compile_test.go │ │ │ ├── matchers.go │ │ │ ├── matchers_test.go │ │ │ ├── matchers_v2geo.go │ │ │ ├── matchers_v2geo_test.go │ │ │ ├── parse.go │ │ │ ├── parse_test.go │ │ │ └── v2geo/ │ │ │ ├── load.go │ │ │ ├── load_test.go │ │ │ ├── v2geo.pb.go │ │ │ └── v2geo.proto │ │ ├── acl.go │ │ ├── acl_test.go │ │ ├── dns_https.go │ │ ├── dns_standard.go │ │ ├── dns_system.go │ │ ├── interface.go │ │ ├── interface_test.go │ │ ├── mock_PluggableOutbound.go │ │ ├── mock_UDPConn.go │ │ ├── ob_direct.go │ │ ├── ob_direct_linux.go │ │ ├── ob_direct_others.go │ │ ├── ob_http.go │ │ ├── ob_socks5.go │ │ ├── speedtest/ │ │ │ ├── client.go │ │ │ ├── protocol.go │ │ │ ├── protocol_test.go │ │ │ └── server.go │ │ ├── speedtest.go │ │ ├── utils.go │ │ └── utils_test.go │ ├── sniff/ │ │ ├── .mockery.yaml │ │ ├── internal/ │ │ │ └── quic/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── header.go │ │ │ ├── packet_protector.go │ │ │ ├── packet_protector_test.go │ │ │ ├── payload.go │ │ │ └── quic.go │ │ ├── mock_Stream.go │ │ ├── sniff.go │ │ └── sniff_test.go │ ├── trafficlogger/ │ │ └── http.go │ ├── transport/ │ │ └── udphop/ │ │ ├── addr.go │ │ └── conn.go │ └── utils/ │ ├── portunion.go │ └── portunion_test.go ├── go.work.sum ├── hyperbole.py ├── platforms.txt ├── requirements.txt └── scripts/ ├── _redirects └── install_server.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ custom: [ 'https://v2.hysteria.network/docs/Donation/' ] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report anything you think is a bug and needs to be fixed. title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **Logs** Attach logs from the client/server when the error occurs. **Device and Operating System** What are you using it on. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.zh.md ================================================ --- name: Bug 反馈 about: 反馈任何你认为是 bug 需要修复的问题。 title: '' labels: bug assignees: '' --- **描述问题** 请尽量清晰精准地描述你遇到的问题。 **如何复现** 复现问题的步骤。 **预期行为** 你认为修复后的行为应该是怎样的。 **日志** 附上客户端/服务器端在错误发生前后的日志。 **设备和操作系统** 你在用什么设备和操作系统。 **额外信息** 其他你认为有助于解决问题的信息。 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project. title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.zh.md ================================================ --- name: 功能请求 about: 为这个项目提出改进意见。 title: '' labels: enhancement assignees: '' --- **你的功能请求是否与某个问题有关?** 请尽量清晰精准地描述你遇到的问题。例如:我家运营商限制 UDP 协议速度,导致 Hysteria 很慢,希望增加 FakeTCP 支持。 **描述你希望的解决方案** 请尽量清晰精准地描述你希望的解决方案。 **有没有其他替代方案** 请尽量清晰精准地描述你认为可能的替代方案。 **额外信息** 其他你认为有助于开发者了解你需求的信息。 ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/autotag.yaml ================================================ name: "Create release tags for nested modules" on: push: tags: - app/v*.*.* permissions: contents: write jobs: tag: name: "Create tags" runs-on: ubuntu-latest steps: - name: "Extract tagbase" id: extract_tagbase uses: actions/github-script@v7 with: script: | const ref = context.ref; core.info(`context.ref: ${ref}`); const refPrefix = 'refs/tags/app/'; if (!ref.startsWith(refPrefix)) { core.setFailed(`context.ref does not start with ${refPrefix}: ${ref}`); return; } const tagbase = ref.slice(refPrefix.length); core.info(`tagbase: ${tagbase}`); core.setOutput('tagbase', tagbase); - name: "Tagging core/*" uses: actions/github-script@v7 env: INPUT_TAGPREFIX: "core/" INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }} with: script: | const tagbase = core.getInput('tagbase', { required: true }); const tagprefix = core.getInput('tagprefix', { required: true }); const refname = `tags/${tagprefix}${tagbase}`; core.info(`creating ref ${refname}`); try { await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `refs/${refname}`, sha: context.sha }); core.info(`created ref ${refname}`); return; } catch (error) { core.info(`failed to create ref ${refname}: ${error}`); } core.info(`updating ref ${refname}`) try { await github.rest.git.updateRef({ owner: context.repo.owner, repo: context.repo.repo, ref: refname, sha: context.sha }); core.info(`updated ref ${refname}`); return; } catch (error) { core.setFailed(`failed to update ref ${refname}: ${error}`); } - name: "Tagging extras/*" uses: actions/github-script@v7 env: INPUT_TAGPREFIX: "extras/" INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }} with: script: | const tagbase = core.getInput('tagbase', { required: true }); const tagprefix = core.getInput('tagprefix', { required: true }); const refname = `tags/${tagprefix}${tagbase}`; core.info(`creating ref ${refname}`); try { await github.rest.git.createRef({ owner: context.repo.owner, repo: context.repo.repo, ref: `refs/${refname}`, sha: context.sha }); core.info(`created ref ${refname}`); return; } catch (error) { core.info(`failed to create ref ${refname}: ${error}`); } core.info(`updating ref ${refname}`) try { await github.rest.git.updateRef({ owner: context.repo.owner, repo: context.repo.repo, ref: refname, sha: context.sha }); core.info(`updated ref ${refname}`); return; } catch (error) { core.setFailed(`failed to update ref ${refname}: ${error}`); } ================================================ FILE: .github/workflows/docker.yml ================================================ name: Docker # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. on: push: branches: [ "master" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write # This is used to complete the identity challenge # with sigstore/fulcio when running outside of PRs. id-token: write steps: - name: Checkout repository uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Install cosign uses: sigstore/cosign-installer@v3.4.0 with: cosign-release: 'v2.2.2' - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.2.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} uses: docker/login-action@v3.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5.5.1 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Get version id: get_version run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT - name: Build and push Docker image id: build-and-push uses: docker/build-push-action@v5.3.0 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: | ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria:latest ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria:${{ steps.get_version.outputs.version }} # Sign the resulting Docker image digest except on PRs. # This will only write to the public Rekor transparency log when the Docker # repository is public to avoid leaking data. If you would like to publish # transparency data even for private images, pass --force to cosign below. # https://github.com/sigstore/cosign - name: Sign the published Docker image env: # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable TAGS: ${{ steps.meta.outputs.tags }} DIGEST: ${{ steps.build-and-push.outputs.digest }} # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} ================================================ FILE: .github/workflows/master.yml ================================================ name: "Build master branch" on: push: branches: - master jobs: build: name: Build runs-on: ubuntu-latest env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - name: Check out uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: "1.23" - name: Setup Python # This is for the build script uses: actions/setup-python@v5 with: python-version: "3.11" - uses: nttld/setup-ndk@v1 id: setup-ndk with: ndk-version: r26b add-to-path: false - name: Run build script env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} run: | export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",") python hyperbole.py build -r - name: Generate hashes run: | for file in build/*; do sha256sum $file >> build/hashes.txt done - name: Archive uses: actions/upload-artifact@v4 with: name: hysteria-master-${{ github.sha }} path: build ================================================ FILE: .github/workflows/release.yml ================================================ name: "Build release" on: push: tags: - app/v*.*.* jobs: build: name: Build runs-on: ubuntu-latest env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - name: Check out uses: actions/checkout@v4 - name: Get version id: get_version run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT - name: Setup Go uses: actions/setup-go@v5 with: go-version: "1.23" - name: Setup Python # This is for the build script uses: actions/setup-python@v5 with: python-version: "3.11" - uses: nttld/setup-ndk@v1 id: setup-ndk with: ndk-version: r26b add-to-path: false - name: Run build script env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} run: | export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",") python hyperbole.py build -r - name: Generate hashes run: | for file in build/*; do sha256sum $file >> build/hashes.txt done - name: Upload GitHub uses: softprops/action-gh-release@v2 with: files: build/* - name: Upload CF bucket uses: shallwefootball/upload-s3-action@v1.3.3 with: aws_key_id: ${{ secrets.CF_KEY_ID }} aws_secret_access_key: ${{ secrets.CF_KEY }} aws_bucket: "hydownload" endpoint: "https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/" source_dir: "build" destination_dir: "app/${{ steps.get_version.outputs.version }}" - name: Publish to API run: | export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }} pip install requests python hyperbole.py publish ================================================ FILE: .github/workflows/scripts.yml ================================================ name: "Publish scripts" on: push: branches: - master paths: - scripts/** jobs: publish: runs-on: ubuntu-latest permissions: contents: read deployments: write name: Publish scripts to Cloudflare Pages steps: - name: Check out uses: actions/checkout@v4 - name: Publish to Cloudflare Pages uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: hy2scripts directory: scripts gitHubToken: ${{ secrets.GITHUB_TOKEN }} branch: main ================================================ FILE: .gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all # Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all ### Go ### # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work ### GoLand+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # SonarLint plugin .idea/sonarlint/ # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### GoLand+all Patch ### # Ignore everything but code style settings and run configurations # that are supposed to be shared within teams. .idea/* !.idea/codeStyles !.idea/runConfigurations ### Intellij+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff # AWS User-specific # Generated files # Sensitive or high-churn files # Gradle # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake # Mongo Explorer plugin # File-based project format # IntelliJ # mpeltonen/sbt-idea plugin # JIRA plugin # Cursive Clojure plugin # SonarLint plugin # Crashlytics plugin (for Android Studio and IntelliJ) # Editor-based Rest Client # Android studio 3.1+ serialized cache file ### Intellij+all Patch ### # Ignore everything but code style settings and run configurations # that are supposed to be shared within teams. ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### macOS Patch ### # iCloud generated files *.icloud ### PyCharm+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff # AWS User-specific # Generated files # Sensitive or high-churn files # Gradle # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake # Mongo Explorer plugin # File-based project format # IntelliJ # mpeltonen/sbt-idea plugin # JIRA plugin # Cursive Clojure plugin # SonarLint plugin # Crashlytics plugin (for Android Studio and IntelliJ) # Editor-based Rest Client # Android studio 3.1+ serialized cache file ### PyCharm+all Patch ### # Ignore everything but code style settings and run configurations # that are supposed to be shared within teams. ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ### Python Patch ### # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration poetry.toml # ruff .ruff_cache/ # LSP config files pyrightconfig.json ### Windows ### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all ================================================ FILE: CHANGELOG.md ================================================ # Changelog https://v2.hysteria.network/docs/Changelog/ ================================================ FILE: Dockerfile ================================================ FROM golang:1-alpine AS builder # GOPROXY is disabled by default, use: # docker build --build-arg GOPROXY="https://goproxy.io" ... # to enable GOPROXY. ARG GOPROXY="" ENV GOPROXY ${GOPROXY} COPY . /go/src/github.com/apernet/hysteria WORKDIR /go/src/github.com/apernet/hysteria RUN set -ex \ && apk add git build-base bash python3 \ && python hyperbole.py build -r \ && mv ./build/hysteria-* /go/bin/hysteria # multi-stage builds to create the final image FROM alpine AS dist # set up nsswitch.conf for Go's "netgo" implementation # - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275 # - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf RUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch.conf; fi # bash is used for debugging, tzdata is used to add timezone information. # Install ca-certificates to ensure no CA certificate errors. # # Do not try to add the "--no-cache" option when there are multiple "apk" # commands, this will cause the build process to become very slow. COPY ./entrypoint /usr/local/bin/entrypoint RUN set -ex \ && apk upgrade \ && apk add bash tzdata ca-certificates \ && rm -rf /var/cache/apk/* \ && chmod +x /usr/local/bin/entrypoint COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria CMD ["entrypoint"] ================================================ FILE: LICENSE.md ================================================ Copyright 2023 Toby Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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 OR COPYRIGHT HOLDERS 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. ================================================ FILE: PROTOCOL.md ================================================ # Hysteria 2 Protocol Specification Hysteria is a TCP & UDP proxy based on QUIC, designed for speed, security and censorship resistance. This document describes the protocol used by Hysteria starting with version 2.0.0, sometimes internally referred to as the "v4" protocol. From here on, we will call it "the protocol" or "the Hysteria protocol". ## Requirements Language The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). ## Underlying Protocol & Wire Format The Hysteria protocol MUST be implemented on top of the standard QUIC transport protocol [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000) with [Unreliable Datagram Extension](https://datatracker.ietf.org/doc/rfc9221/). All multibyte numbers use Big Endian format. All variable-length integers ("varints") are encoded/decoded as defined in QUIC (RFC 9000). ## Authentication & HTTP/3 masquerading One of the key features of the Hysteria protocol is that to a third party without proper authentication credentials (whether it's a middleman or an active prober), a Hysteria proxy server behaves just like a standard HTTP/3 web server. Additionally, the encrypted traffic between the client and the server appears indistinguishable from normal HTTP/3 traffic. Therefore, a Hysteria server MUST implement an HTTP/3 server (as defined by [RFC 9114](https://datatracker.ietf.org/doc/rfc9114/)) and handle HTTP requests as any standard web server would. To prevent active probers from detecting common response patterns in Hysteria servers, implementations SHOULD advise users to either host actual content or set it up as a reverse proxy for other sites. An actual Hysteria client, upon connection, MUST send the following HTTP/3 request to the server: ``` :method: POST :path: /auth :host: hysteria Hysteria-Auth: [string] Hysteria-CC-RX: [uint] Hysteria-Padding: [string] ``` `Hysteria-Auth`: Authentication credentials. `Hysteria-CC-RX`: Client's maximum receive rate in bytes per second. A value of 0 indicates unknown. `Hysteria-Padding`: A random padding string of variable length. The Hysteria server MUST identify this special request, and, instead of attempting to serve content or forwarding it to an upstream site, it MUST authenticate the client using the provided information. If authentication is successful, the server MUST send the following response (HTTP status code 233): ``` :status: 233 HyOK Hysteria-UDP: [true/false] Hysteria-CC-RX: [uint/"auto"] Hysteria-Padding: [string] ``` `Hysteria-UDP`: Whether the server supports UDP relay. `Hysteria-CC-RX`: Server's maximum receive rate in bytes per second. A value of 0 indicates unlimited; "auto" indicates the server refuses to provide a value and ask the client to use congestion control to determine the rate on its own. `Hysteria-Padding`: A random padding string of variable length. See the Congestion Control section for more information on how to use the `Hysteria-CC-RX` values. `Hysteria-Padding` is optional and is only intended to obfuscate the request/response pattern. It SHOULD be ignored by both sides. If authentication fails, the server MUST either act like a standard web server that does not understand the request, or in the case of being a reverse proxy, forward the request to the upstream site and return the response to the client. The client MUST check the status code to determine if the authentication was successful. If the status code is anything other than 233, the client MUST consider authentication to have failed and disconnect from the server. After (and only after) a client passes authentication, the server MUST consider this QUIC connection to be a Hysteria proxy connection. It MUST then start processing proxy requests from the client as described in the next section. ## Proxy Requests ### TCP For each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message: ``` [varint] 0x401 (TCPRequest ID) [varint] Address length [bytes] Address string (host:port) [varint] Padding length [bytes] Random padding ``` The server MUST respond with a TCPResponse message: ``` [uint8] Status (0x00 = OK, 0x01 = Error) [varint] Message length [bytes] Message string [varint] Padding length [bytes] Random padding ``` If the status is OK, the server MUST then begin forwarding data between the client and the specified TCP address until either side closes the connection. If the status is Error, the server MUST close the QUIC stream. ### UDP UDP packets MUST be encapsulated in the following UDPMessage format and sent over QUIC's unreliable datagram (for both client-to-server and server-to-client): ``` [uint32] Session ID [uint16] Packet ID [uint8] Fragment ID [uint8] Fragment count [varint] Address length [bytes] Address string (host:port) [bytes] Payload ``` The client MUST use a unique Session ID for each UDP session. The server SHOULD assign a unique UDP port to each Session ID, unless it has another mechanism to differentiate packets from different sessions (e.g., symmetric NAT, varying outbound IP addresses, etc.). The protocol does not provide an explicit way to close a UDP session. While a client can retain and reuse a Session ID indefinitely, the server SHOULD release and reassign the port associated with the Session ID after a period of inactivity or some other criteria. If the client sends a UDP packet to a Session ID that is no longer recognized by the server, the server MUST treat it as a new session and assign a new port. If a server does not support UDP relay, it SHOULD silently discard all UDP messages received from the client. #### Fragmentation Due to the limit imposed by QUIC's unreliable datagram channel, any UDP packet that exceeds QUIC's maximum datagram size MUST either be fragmented or discarded. For fragmented packets, each fragment MUST carry the same unique Packet ID. The Fragment ID, starting from 0, indicates the index out of the total Fragment Count. Both the server and client MUST wait for all fragments of a fragmented packet to arrive before processing them. If one or more fragments of a packet are lost, the entire packet MUST be discarded. For packets that are not fragmented, the Fragment Count MUST be set to 1. In this case, the values of Packet ID and Fragment ID are irrelevant. ## Congestion Control A unique feature of Hysteria is the ability to set the tx/rx (upload/download) rate on the client side. During authentication, the client sends its rx rate to the server via the `Hysteria-CC-RX` header. The server can use this to determine its transmission rate to the client, and vice versa by returning its rx rate to the client through the same header. Three special cases are: - If the client sends 0, it doesn't know its own rx rate. The server MUST use a congestion control algorithm (e.g., BBR, Cubic) to adjust its transmission rate. - If the server responds with 0, it has no bandwidth limit. The client MAY transmit at any rate it wants. - If the server responds with "auto", it chooses not to specify a rate. The client MUST use a congestion control algorithm to adjust its transmission rate. ## "Salamander" Obfuscation The Hysteria protocol supports an optional obfuscation layer codenamed "Salamander". "Salamander" encapsulates all QUIC packets in the following format: ``` [8 bytes] Salt [bytes] Payload ``` For each QUIC packet, the obfuscator MUST calculate the BLAKE2b-256 hash of a randomly generated 8-byte salt appended to a user-provided pre-shared key. ``` hash = BLAKE2b-256(key + salt) ``` The hash is then used to obfuscate the payload using the following algorithm: ``` for i in range(0, len(payload)): payload[i] ^= hash[i % 32] ``` The deobfuscator MUST use the same algorithms to calculate the salted hash and deobfuscate the payload. Any invalid packet MUST be discarded. ================================================ FILE: README.md ================================================ # ![Hysteria 2](logo.svg) # 支持对接V2board面板的Hysteria2后端 ### 项目说明 本项目基于hysteria官方内核二次开发,添加了从v2b获取节点信息、用户鉴权信息与上报用户流量的功能。 性能方面已经由hysteria2内核作者亲自指导优化过了。 ### TG交流群 欢迎加入交流群 [点击加入](https://t.me/+DcRt8AB2VbI2Yzc1) ### 示例配置 ``` v2board: apiHost: https://面板地址 apiKey: 面板节点密钥 nodeID: 节点ID tls: type: tls cert: /etc/hysteria/tls.crt key: /etc/hysteria/tls.key auth: type: v2board trafficStats: listen: 127.0.0.1:7653 acl: inline: - reject(10.0.0.0/8) - reject(172.16.0.0/12) - reject(192.168.0.0/16) - reject(127.0.0.0/8) - reject(fc00::/7) ``` > 其他配置完全与hysteria文档的一致,可以查看hysteria2官方文档 [点击查看](https://hysteria.network/zh/docs/getting-started/Installation/) ### 快速启动 ``` docker run -itd --restart=always --network=host \ -e apiHost=https://example.com \ -e apiKey=xxxxxxxxxxxxxxxxxxxxx \ -e domain=hy2.example.com \ -e nodeID=1 \ ghcr.io/cedar2025/hysteria:latest ``` ### docker 仓库 ``` docker pull ghcr.io/cedar2025/hysteria:latest ``` ================================================ FILE: app/cmd/client.go ================================================ package cmd import ( "crypto/sha256" "crypto/x509" "encoding/hex" "errors" "fmt" "net" "net/netip" "os" "os/signal" "runtime" "slices" "strconv" "strings" "syscall" "time" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" "github.com/apernet/hysteria/app/v2/internal/forwarding" "github.com/apernet/hysteria/app/v2/internal/http" "github.com/apernet/hysteria/app/v2/internal/proxymux" "github.com/apernet/hysteria/app/v2/internal/redirect" "github.com/apernet/hysteria/app/v2/internal/sockopts" "github.com/apernet/hysteria/app/v2/internal/socks5" "github.com/apernet/hysteria/app/v2/internal/tproxy" "github.com/apernet/hysteria/app/v2/internal/tun" "github.com/apernet/hysteria/app/v2/internal/url" "github.com/apernet/hysteria/app/v2/internal/utils" "github.com/apernet/hysteria/core/v2/client" "github.com/apernet/hysteria/extras/v2/correctnet" "github.com/apernet/hysteria/extras/v2/obfs" "github.com/apernet/hysteria/extras/v2/transport/udphop" ) // Client flags var ( showQR bool ) var clientCmd = &cobra.Command{ Use: "client", Short: "Client mode", Run: runClient, } func init() { initClientFlags() rootCmd.AddCommand(clientCmd) } func initClientFlags() { clientCmd.Flags().BoolVar(&showQR, "qr", false, "show QR code for server config sharing") } type clientConfig struct { Server string `mapstructure:"server"` Auth string `mapstructure:"auth"` Transport clientConfigTransport `mapstructure:"transport"` Obfs clientConfigObfs `mapstructure:"obfs"` TLS clientConfigTLS `mapstructure:"tls"` QUIC clientConfigQUIC `mapstructure:"quic"` Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"` FastOpen bool `mapstructure:"fastOpen"` Lazy bool `mapstructure:"lazy"` SOCKS5 *socks5Config `mapstructure:"socks5"` HTTP *httpConfig `mapstructure:"http"` TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"` UDPForwarding []udpForwardingEntry `mapstructure:"udpForwarding"` TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"` UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"` TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"` TUN *tunConfig `mapstructure:"tun"` } type clientConfigTransportUDP struct { HopInterval time.Duration `mapstructure:"hopInterval"` } type clientConfigTransport struct { Type string `mapstructure:"type"` UDP clientConfigTransportUDP `mapstructure:"udp"` } type clientConfigObfsSalamander struct { Password string `mapstructure:"password"` } type clientConfigObfs struct { Type string `mapstructure:"type"` Salamander clientConfigObfsSalamander `mapstructure:"salamander"` } type clientConfigTLS struct { SNI string `mapstructure:"sni"` Insecure bool `mapstructure:"insecure"` PinSHA256 string `mapstructure:"pinSHA256"` CA string `mapstructure:"ca"` } type clientConfigQUIC struct { InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"` MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"` InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"` MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"` MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"` KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"` DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` Sockopts clientConfigQUICSockopts `mapstructure:"sockopts"` } type clientConfigQUICSockopts struct { BindInterface *string `mapstructure:"bindInterface"` FirewallMark *uint32 `mapstructure:"fwmark"` FdControlUnixSocket *string `mapstructure:"fdControlUnixSocket"` } type clientConfigBandwidth struct { Up string `mapstructure:"up"` Down string `mapstructure:"down"` } type socks5Config struct { Listen string `mapstructure:"listen"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` DisableUDP bool `mapstructure:"disableUDP"` } type httpConfig struct { Listen string `mapstructure:"listen"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` Realm string `mapstructure:"realm"` } type tcpForwardingEntry struct { Listen string `mapstructure:"listen"` Remote string `mapstructure:"remote"` } type udpForwardingEntry struct { Listen string `mapstructure:"listen"` Remote string `mapstructure:"remote"` Timeout time.Duration `mapstructure:"timeout"` } type tcpTProxyConfig struct { Listen string `mapstructure:"listen"` } type udpTProxyConfig struct { Listen string `mapstructure:"listen"` Timeout time.Duration `mapstructure:"timeout"` } type tcpRedirectConfig struct { Listen string `mapstructure:"listen"` } type tunConfig struct { Name string `mapstructure:"name"` MTU uint32 `mapstructure:"mtu"` Timeout time.Duration `mapstructure:"timeout"` Address struct { IPv4 string `mapstructure:"ipv4"` IPv6 string `mapstructure:"ipv6"` } `mapstructure:"address"` Route *struct { Strict bool `mapstructure:"strict"` IPv4 []string `mapstructure:"ipv4"` IPv6 []string `mapstructure:"ipv6"` IPv4Exclude []string `mapstructure:"ipv4Exclude"` IPv6Exclude []string `mapstructure:"ipv6Exclude"` } `mapstructure:"route"` } func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error { if c.Server == "" { return configError{Field: "server", Err: errors.New("server address is empty")} } var addr net.Addr var err error host, port, hostPort := parseServerAddrString(c.Server) if !isPortHoppingPort(port) { addr, err = net.ResolveUDPAddr("udp", hostPort) } else { addr, err = udphop.ResolveUDPHopAddr(hostPort) } if err != nil { return configError{Field: "server", Err: err} } hyConfig.ServerAddr = addr // Special handling for SNI if c.TLS.SNI == "" { // Use server hostname as SNI hyConfig.TLSConfig.ServerName = host } return nil } // fillConnFactory must be called after fillServerAddr, as we have different logic // for ConnFactory depending on whether we have a port hopping address. func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error { so := &sockopts.SocketOptions{ BindInterface: c.QUIC.Sockopts.BindInterface, FirewallMark: c.QUIC.Sockopts.FirewallMark, FdControlUnixSocket: c.QUIC.Sockopts.FdControlUnixSocket, } if err := so.CheckSupported(); err != nil { var unsupportedErr *sockopts.UnsupportedError if errors.As(err, &unsupportedErr) { return configError{ Field: "quic.sockopts." + unsupportedErr.Field, Err: errors.New("unsupported on this platform"), } } return configError{Field: "quic.sockopts", Err: err} } // Inner PacketConn var newFunc func(addr net.Addr) (net.PacketConn, error) switch strings.ToLower(c.Transport.Type) { case "", "udp": if hyConfig.ServerAddr.Network() == "udphop" { hopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr) newFunc = func(addr net.Addr) (net.PacketConn, error) { return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, so.ListenUDP) } } else { newFunc = func(addr net.Addr) (net.PacketConn, error) { return so.ListenUDP() } } default: return configError{Field: "transport.type", Err: errors.New("unsupported transport type")} } // Obfuscation var ob obfs.Obfuscator var err error switch strings.ToLower(c.Obfs.Type) { case "", "plain": // Keep it nil case "salamander": ob, err = obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password)) if err != nil { return configError{Field: "obfs.salamander.password", Err: err} } default: return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")} } hyConfig.ConnFactory = &adaptiveConnFactory{ NewFunc: newFunc, Obfuscator: ob, } return nil } func (c *clientConfig) fillAuth(hyConfig *client.Config) error { hyConfig.Auth = c.Auth return nil } func (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error { if c.TLS.SNI != "" { hyConfig.TLSConfig.ServerName = c.TLS.SNI } hyConfig.TLSConfig.InsecureSkipVerify = c.TLS.Insecure if c.TLS.PinSHA256 != "" { nHash := normalizeCertHash(c.TLS.PinSHA256) hyConfig.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error { for _, cert := range rawCerts { hash := sha256.Sum256(cert) hashHex := hex.EncodeToString(hash[:]) if hashHex == nHash { return nil } } // No match return errors.New("no certificate matches the pinned hash") } } if c.TLS.CA != "" { ca, err := os.ReadFile(c.TLS.CA) if err != nil { return configError{Field: "tls.ca", Err: err} } cPool := x509.NewCertPool() if !cPool.AppendCertsFromPEM(ca) { return configError{Field: "tls.ca", Err: errors.New("failed to parse CA certificate")} } hyConfig.TLSConfig.RootCAs = cPool } return nil } func (c *clientConfig) fillQUICConfig(hyConfig *client.Config) error { hyConfig.QUICConfig = client.QUICConfig{ InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow, MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow, MaxConnectionReceiveWindow: c.QUIC.MaxConnectionReceiveWindow, MaxIdleTimeout: c.QUIC.MaxIdleTimeout, KeepAlivePeriod: c.QUIC.KeepAlivePeriod, DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery, } return nil } func (c *clientConfig) fillBandwidthConfig(hyConfig *client.Config) error { // New core now allows users to omit bandwidth values and use built-in congestion control var err error if c.Bandwidth.Up != "" { hyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up) if err != nil { return configError{Field: "bandwidth.up", Err: err} } } if c.Bandwidth.Down != "" { hyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down) if err != nil { return configError{Field: "bandwidth.down", Err: err} } } return nil } func (c *clientConfig) fillFastOpen(hyConfig *client.Config) error { hyConfig.FastOpen = c.FastOpen return nil } // URI generates a URI for sharing the config with others. // Note that only the bare minimum of information required to // connect to the server is included in the URI, specifically: // - server address // - authentication // - obfuscation type // - obfuscation password // - TLS SNI // - TLS insecure // - TLS pinned SHA256 hash (normalized) func (c *clientConfig) URI() string { q := url.Values{} switch strings.ToLower(c.Obfs.Type) { case "salamander": q.Set("obfs", "salamander") q.Set("obfs-password", c.Obfs.Salamander.Password) } if c.TLS.SNI != "" { q.Set("sni", c.TLS.SNI) } if c.TLS.Insecure { q.Set("insecure", "1") } if c.TLS.PinSHA256 != "" { q.Set("pinSHA256", normalizeCertHash(c.TLS.PinSHA256)) } var user *url.Userinfo if c.Auth != "" { // We need to handle the special case of user:pass pairs rs := strings.SplitN(c.Auth, ":", 2) if len(rs) == 2 { user = url.UserPassword(rs[0], rs[1]) } else { user = url.User(c.Auth) } } u := url.URL{ Scheme: "hysteria2", User: user, Host: c.Server, Path: "/", RawQuery: q.Encode(), } return u.String() } // parseURI tries to parse the server address field as a URI, // and fills the config with the information contained in the URI. // Returns whether the server address field is a valid URI. // This allows a user to use put a URI as the server address and // omit the fields that are already contained in the URI. func (c *clientConfig) parseURI() bool { u, err := url.Parse(c.Server) if err != nil { return false } if u.Scheme != "hysteria2" && u.Scheme != "hy2" { return false } if u.User != nil { auth, err := url.QueryUnescape(u.User.String()) if err != nil { return false } c.Auth = auth } c.Server = u.Host q := u.Query() if obfsType := q.Get("obfs"); obfsType != "" { c.Obfs.Type = obfsType switch strings.ToLower(obfsType) { case "salamander": c.Obfs.Salamander.Password = q.Get("obfs-password") } } if sni := q.Get("sni"); sni != "" { c.TLS.SNI = sni } if insecure, err := strconv.ParseBool(q.Get("insecure")); err == nil { c.TLS.Insecure = insecure } if pinSHA256 := q.Get("pinSHA256"); pinSHA256 != "" { c.TLS.PinSHA256 = pinSHA256 } return true } // Config validates the fields and returns a ready-to-use Hysteria client config func (c *clientConfig) Config() (*client.Config, error) { c.parseURI() hyConfig := &client.Config{} fillers := []func(*client.Config) error{ c.fillServerAddr, c.fillConnFactory, c.fillAuth, c.fillTLSConfig, c.fillQUICConfig, c.fillBandwidthConfig, c.fillFastOpen, } for _, f := range fillers { if err := f(hyConfig); err != nil { return nil, err } } return hyConfig, nil } func runClient(cmd *cobra.Command, args []string) { logger.Info("client mode") if err := viper.ReadInConfig(); err != nil { logger.Fatal("failed to read client config", zap.Error(err)) } var config clientConfig if err := viper.Unmarshal(&config); err != nil { logger.Fatal("failed to parse client config", zap.Error(err)) } c, err := client.NewReconnectableClient( config.Config, func(c client.Client, info *client.HandshakeInfo, count int) { connectLog(info, count) // On the client side, we start checking for updates after we successfully connect // to the server, which, depending on whether lazy mode is enabled, may or may not // be immediately after the client starts. We don't want the update check request // to interfere with the lazy mode option. if count == 1 && !disableUpdateCheck { go runCheckUpdateClient(c) } }, config.Lazy) if err != nil { logger.Fatal("failed to initialize client", zap.Error(err)) } defer c.Close() uri := config.URI() logger.Info("use this URI to share your server", zap.String("uri", uri)) if showQR { utils.PrintQR(uri) } // Register modes var runner clientModeRunner if config.SOCKS5 != nil { runner.Add("SOCKS5 server", func() error { return clientSOCKS5(*config.SOCKS5, c) }) } if config.HTTP != nil { runner.Add("HTTP proxy server", func() error { return clientHTTP(*config.HTTP, c) }) } if len(config.TCPForwarding) > 0 { runner.Add("TCP forwarding", func() error { return clientTCPForwarding(config.TCPForwarding, c) }) } if len(config.UDPForwarding) > 0 { runner.Add("UDP forwarding", func() error { return clientUDPForwarding(config.UDPForwarding, c) }) } if config.TCPTProxy != nil { runner.Add("TCP transparent proxy", func() error { return clientTCPTProxy(*config.TCPTProxy, c) }) } if config.UDPTProxy != nil { runner.Add("UDP transparent proxy", func() error { return clientUDPTProxy(*config.UDPTProxy, c) }) } if config.TCPRedirect != nil { runner.Add("TCP redirect", func() error { return clientTCPRedirect(*config.TCPRedirect, c) }) } if config.TUN != nil { runner.Add("TUN", func() error { return clientTUN(*config.TUN, c) }) } signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) defer signal.Stop(signalChan) runnerChan := make(chan clientModeRunnerResult, 1) go func() { runnerChan <- runner.Run() }() select { case <-signalChan: logger.Info("received signal, shutting down gracefully") case r := <-runnerChan: if r.OK { logger.Info(r.Msg) } else { _ = c.Close() // Close the client here as Fatal will exit the program without running defer if r.Err != nil { logger.Fatal(r.Msg, zap.Error(r.Err)) } else { logger.Fatal(r.Msg) } } } } type clientModeRunner struct { ModeMap map[string]func() error } type clientModeRunnerResult struct { OK bool Msg string Err error } func (r *clientModeRunner) Add(name string, f func() error) { if r.ModeMap == nil { r.ModeMap = make(map[string]func() error) } r.ModeMap[name] = f } func (r *clientModeRunner) Run() clientModeRunnerResult { if len(r.ModeMap) == 0 { return clientModeRunnerResult{OK: false, Msg: "no mode specified"} } type modeError struct { Name string Err error } errChan := make(chan modeError, len(r.ModeMap)) for name, f := range r.ModeMap { go func(name string, f func() error) { err := f() errChan <- modeError{name, err} }(name, f) } // Fatal if any one of the modes fails for i := 0; i < len(r.ModeMap); i++ { e := <-errChan if e.Err != nil { return clientModeRunnerResult{OK: false, Msg: "failed to run " + e.Name, Err: e.Err} } } // We don't really have any such cases, as currently none of our modes would stop on themselves without error. // But we leave the possibility here for future expansion. return clientModeRunnerResult{OK: true, Msg: "finished without error"} } func clientSOCKS5(config socks5Config, c client.Client) error { if config.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} } l, err := proxymux.ListenSOCKS(config.Listen) if err != nil { return configError{Field: "listen", Err: err} } var authFunc func(username, password string) bool username, password := config.Username, config.Password if username != "" && password != "" { authFunc = func(u, p string) bool { return u == username && p == password } } s := socks5.Server{ HyClient: c, AuthFunc: authFunc, DisableUDP: config.DisableUDP, EventLogger: &socks5Logger{}, } logger.Info("SOCKS5 server listening", zap.String("addr", config.Listen)) return s.Serve(l) } func clientHTTP(config httpConfig, c client.Client) error { if config.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} } l, err := proxymux.ListenHTTP(config.Listen) if err != nil { return configError{Field: "listen", Err: err} } var authFunc func(username, password string) bool username, password := config.Username, config.Password if username != "" && password != "" { authFunc = func(u, p string) bool { return u == username && p == password } } if config.Realm == "" { config.Realm = "Hysteria" } h := http.Server{ HyClient: c, AuthFunc: authFunc, AuthRealm: config.Realm, EventLogger: &httpLogger{}, } logger.Info("HTTP proxy server listening", zap.String("addr", config.Listen)) return h.Serve(l) } func clientTCPForwarding(entries []tcpForwardingEntry, c client.Client) error { errChan := make(chan error, len(entries)) for _, e := range entries { if e.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} } if e.Remote == "" { return configError{Field: "remote", Err: errors.New("remote address is empty")} } l, err := correctnet.Listen("tcp", e.Listen) if err != nil { return configError{Field: "listen", Err: err} } logger.Info("TCP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) go func(remote string) { t := &forwarding.TCPTunnel{ HyClient: c, Remote: remote, EventLogger: &tcpLogger{}, } errChan <- t.Serve(l) }(e.Remote) } // Return if any one of the forwarding fails return <-errChan } func clientUDPForwarding(entries []udpForwardingEntry, c client.Client) error { errChan := make(chan error, len(entries)) for _, e := range entries { if e.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} } if e.Remote == "" { return configError{Field: "remote", Err: errors.New("remote address is empty")} } l, err := correctnet.ListenPacket("udp", e.Listen) if err != nil { return configError{Field: "listen", Err: err} } logger.Info("UDP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) go func(remote string, timeout time.Duration) { u := &forwarding.UDPTunnel{ HyClient: c, Remote: remote, Timeout: timeout, EventLogger: &udpLogger{}, } errChan <- u.Serve(l) }(e.Remote, e.Timeout) } // Return if any one of the forwarding fails return <-errChan } func clientTCPTProxy(config tcpTProxyConfig, c client.Client) error { if config.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} } laddr, err := net.ResolveTCPAddr("tcp", config.Listen) if err != nil { return configError{Field: "listen", Err: err} } p := &tproxy.TCPTProxy{ HyClient: c, EventLogger: &tcpTProxyLogger{}, } logger.Info("TCP transparent proxy listening", zap.String("addr", config.Listen)) return p.ListenAndServe(laddr) } func clientUDPTProxy(config udpTProxyConfig, c client.Client) error { if config.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} } laddr, err := net.ResolveUDPAddr("udp", config.Listen) if err != nil { return configError{Field: "listen", Err: err} } p := &tproxy.UDPTProxy{ HyClient: c, Timeout: config.Timeout, EventLogger: &udpTProxyLogger{}, } logger.Info("UDP transparent proxy listening", zap.String("addr", config.Listen)) return p.ListenAndServe(laddr) } func clientTCPRedirect(config tcpRedirectConfig, c client.Client) error { if config.Listen == "" { return configError{Field: "listen", Err: errors.New("listen address is empty")} } laddr, err := net.ResolveTCPAddr("tcp", config.Listen) if err != nil { return configError{Field: "listen", Err: err} } p := &redirect.TCPRedirect{ HyClient: c, EventLogger: &tcpRedirectLogger{}, } logger.Info("TCP redirect listening", zap.String("addr", config.Listen)) return p.ListenAndServe(laddr) } func clientTUN(config tunConfig, c client.Client) error { supportedPlatforms := []string{"linux", "darwin", "windows", "android"} if !slices.Contains(supportedPlatforms, runtime.GOOS) { logger.Error("TUN is not supported on this platform", zap.String("platform", runtime.GOOS)) } if config.Name == "" { return configError{Field: "name", Err: errors.New("name is empty")} } if config.MTU == 0 { config.MTU = 1500 } timeout := int64(config.Timeout.Seconds()) if timeout == 0 { timeout = 300 } if config.Address.IPv4 == "" { config.Address.IPv4 = "100.100.100.101/30" } prefix4, err := netip.ParsePrefix(config.Address.IPv4) if err != nil { return configError{Field: "address.ipv4", Err: err} } if config.Address.IPv6 == "" { config.Address.IPv6 = "2001::ffff:ffff:ffff:fff1/126" } prefix6, err := netip.ParsePrefix(config.Address.IPv6) if err != nil { return configError{Field: "address.ipv6", Err: err} } server := &tun.Server{ HyClient: c, EventLogger: &tunLogger{}, Logger: logger, IfName: config.Name, MTU: config.MTU, Timeout: timeout, Inet4Address: []netip.Prefix{prefix4}, Inet6Address: []netip.Prefix{prefix6}, } if config.Route != nil { server.AutoRoute = true server.StructRoute = config.Route.Strict parsePrefixes := func(field string, ss []string) ([]netip.Prefix, error) { var prefixes []netip.Prefix for i, s := range ss { var p netip.Prefix if strings.Contains(s, "/") { var err error p, err = netip.ParsePrefix(s) if err != nil { return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err} } } else { pa, err := netip.ParseAddr(s) if err != nil { return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err} } p = netip.PrefixFrom(pa, pa.BitLen()) } prefixes = append(prefixes, p) } return prefixes, nil } server.Inet4RouteAddress, err = parsePrefixes("route.ipv4", config.Route.IPv4) if err != nil { return err } server.Inet6RouteAddress, err = parsePrefixes("route.ipv6", config.Route.IPv6) if err != nil { return err } server.Inet4RouteExcludeAddress, err = parsePrefixes("route.ipv4Exclude", config.Route.IPv4Exclude) if err != nil { return err } server.Inet6RouteExcludeAddress, err = parsePrefixes("route.ipv6Exclude", config.Route.IPv6Exclude) if err != nil { return err } } logger.Info("TUN listening", zap.String("interface", config.Name)) return server.Serve() } // parseServerAddrString parses server address string. // Server address can be in either "host:port" or "host" format (in which case we assume port 443). func parseServerAddrString(addrStr string) (host, port, hostPort string) { h, p, err := net.SplitHostPort(addrStr) if err != nil { return addrStr, "443", net.JoinHostPort(addrStr, "443") } return h, p, addrStr } // isPortHoppingPort returns whether the port string is a port hopping port. // We consider a port string to be a port hopping port if it contains "-" or ",". func isPortHoppingPort(port string) bool { return strings.Contains(port, "-") || strings.Contains(port, ",") } // normalizeCertHash normalizes a certificate hash string. // It converts all characters to lowercase and removes possible separators such as ":" and "-". func normalizeCertHash(hash string) string { r := strings.ToLower(hash) r = strings.ReplaceAll(r, ":", "") r = strings.ReplaceAll(r, "-", "") return r } type adaptiveConnFactory struct { NewFunc func(addr net.Addr) (net.PacketConn, error) Obfuscator obfs.Obfuscator // nil if no obfuscation } func (f *adaptiveConnFactory) New(addr net.Addr) (net.PacketConn, error) { if f.Obfuscator == nil { return f.NewFunc(addr) } else { conn, err := f.NewFunc(addr) if err != nil { return nil, err } return obfs.WrapPacketConn(conn, f.Obfuscator), nil } } func connectLog(info *client.HandshakeInfo, count int) { logger.Info("connected to server", zap.Bool("udpEnabled", info.UDPEnabled), zap.Uint64("tx", info.Tx), zap.Int("count", count)) } type socks5Logger struct{} func (l *socks5Logger) TCPRequest(addr net.Addr, reqAddr string) { logger.Debug("SOCKS5 TCP request", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) } func (l *socks5Logger) TCPError(addr net.Addr, reqAddr string, err error) { if err == nil { logger.Debug("SOCKS5 TCP closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) } else { logger.Warn("SOCKS5 TCP error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err)) } } func (l *socks5Logger) UDPRequest(addr net.Addr) { logger.Debug("SOCKS5 UDP request", zap.String("addr", addr.String())) } func (l *socks5Logger) UDPError(addr net.Addr, err error) { if err == nil { logger.Debug("SOCKS5 UDP closed", zap.String("addr", addr.String())) } else { logger.Warn("SOCKS5 UDP error", zap.String("addr", addr.String()), zap.Error(err)) } } type httpLogger struct{} func (l *httpLogger) ConnectRequest(addr net.Addr, reqAddr string) { logger.Debug("HTTP CONNECT request", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) } func (l *httpLogger) ConnectError(addr net.Addr, reqAddr string, err error) { if err == nil { logger.Debug("HTTP CONNECT closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) } else { logger.Warn("HTTP CONNECT error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err)) } } func (l *httpLogger) HTTPRequest(addr net.Addr, reqURL string) { logger.Debug("HTTP request", zap.String("addr", addr.String()), zap.String("reqURL", reqURL)) } func (l *httpLogger) HTTPError(addr net.Addr, reqURL string, err error) { if err == nil { logger.Debug("HTTP closed", zap.String("addr", addr.String()), zap.String("reqURL", reqURL)) } else { logger.Warn("HTTP error", zap.String("addr", addr.String()), zap.String("reqURL", reqURL), zap.Error(err)) } } type tcpLogger struct{} func (l *tcpLogger) Connect(addr net.Addr) { logger.Debug("TCP forwarding connect", zap.String("addr", addr.String())) } func (l *tcpLogger) Error(addr net.Addr, err error) { if err == nil { logger.Debug("TCP forwarding closed", zap.String("addr", addr.String())) } else { logger.Warn("TCP forwarding error", zap.String("addr", addr.String()), zap.Error(err)) } } type udpLogger struct{} func (l *udpLogger) Connect(addr net.Addr) { logger.Debug("UDP forwarding connect", zap.String("addr", addr.String())) } func (l *udpLogger) Error(addr net.Addr, err error) { if err == nil { logger.Debug("UDP forwarding closed", zap.String("addr", addr.String())) } else { logger.Warn("UDP forwarding error", zap.String("addr", addr.String()), zap.Error(err)) } } type tcpTProxyLogger struct{} func (l *tcpTProxyLogger) Connect(addr, reqAddr net.Addr) { logger.Debug("TCP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) } func (l *tcpTProxyLogger) Error(addr, reqAddr net.Addr, err error) { if err == nil { logger.Debug("TCP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) } else { logger.Warn("TCP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) } } type udpTProxyLogger struct{} func (l *udpTProxyLogger) Connect(addr, reqAddr net.Addr) { logger.Debug("UDP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) } func (l *udpTProxyLogger) Error(addr, reqAddr net.Addr, err error) { if err == nil { logger.Debug("UDP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) } else { logger.Warn("UDP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) } } type tcpRedirectLogger struct{} func (l *tcpRedirectLogger) Connect(addr, reqAddr net.Addr) { logger.Debug("TCP redirect connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) } func (l *tcpRedirectLogger) Error(addr, reqAddr net.Addr, err error) { if err == nil { logger.Debug("TCP redirect closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) } else { logger.Warn("TCP redirect error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) } } type tunLogger struct{} func (l *tunLogger) TCPRequest(addr, reqAddr string) { logger.Debug("TUN TCP request", zap.String("addr", addr), zap.String("reqAddr", reqAddr)) } func (l *tunLogger) TCPError(addr, reqAddr string, err error) { if err == nil { logger.Debug("TUN TCP closed", zap.String("addr", addr), zap.String("reqAddr", reqAddr)) } else { logger.Warn("TUN TCP error", zap.String("addr", addr), zap.String("reqAddr", reqAddr), zap.Error(err)) } } func (l *tunLogger) UDPRequest(addr string) { logger.Debug("TUN UDP request", zap.String("addr", addr)) } func (l *tunLogger) UDPError(addr string, err error) { if err == nil { logger.Debug("TUN UDP closed", zap.String("addr", addr)) } else { logger.Warn("TUN UDP error", zap.String("addr", addr), zap.Error(err)) } } ================================================ FILE: app/cmd/client_test.go ================================================ package cmd import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/spf13/viper" ) // TestClientConfig tests the parsing of the client config func TestClientConfig(t *testing.T) { viper.SetConfigFile("client_test.yaml") err := viper.ReadInConfig() assert.NoError(t, err) var config clientConfig err = viper.Unmarshal(&config) assert.NoError(t, err) assert.Equal(t, config, clientConfig{ Server: "example.com", Auth: "weak_ahh_password", Transport: clientConfigTransport{ Type: "udp", UDP: clientConfigTransportUDP{ HopInterval: 30 * time.Second, }, }, Obfs: clientConfigObfs{ Type: "salamander", Salamander: clientConfigObfsSalamander{ Password: "cry_me_a_r1ver", }, }, TLS: clientConfigTLS{ SNI: "another.example.com", Insecure: true, PinSHA256: "114515DEADBEEF", CA: "custom_ca.crt", }, QUIC: clientConfigQUIC{ InitStreamReceiveWindow: 1145141, MaxStreamReceiveWindow: 1145142, InitConnectionReceiveWindow: 1145143, MaxConnectionReceiveWindow: 1145144, MaxIdleTimeout: 10 * time.Second, KeepAlivePeriod: 4 * time.Second, DisablePathMTUDiscovery: true, Sockopts: clientConfigQUICSockopts{ BindInterface: stringRef("eth0"), FirewallMark: uint32Ref(1234), FdControlUnixSocket: stringRef("test.sock"), }, }, Bandwidth: clientConfigBandwidth{ Up: "200 mbps", Down: "1 gbps", }, FastOpen: true, Lazy: true, SOCKS5: &socks5Config{ Listen: "127.0.0.1:1080", Username: "anon", Password: "bro", DisableUDP: true, }, HTTP: &httpConfig{ Listen: "127.0.0.1:8080", Username: "qqq", Password: "bruh", Realm: "martian", }, TCPForwarding: []tcpForwardingEntry{ { Listen: "127.0.0.1:8088", Remote: "internal.example.com:80", }, }, UDPForwarding: []udpForwardingEntry{ { Listen: "127.0.0.1:5353", Remote: "internal.example.com:53", Timeout: 50 * time.Second, }, }, TCPTProxy: &tcpTProxyConfig{ Listen: "127.0.0.1:2500", }, UDPTProxy: &udpTProxyConfig{ Listen: "127.0.0.1:2501", Timeout: 20 * time.Second, }, TCPRedirect: &tcpRedirectConfig{ Listen: "127.0.0.1:3500", }, TUN: &tunConfig{ Name: "hytun", MTU: 1500, Timeout: 60 * time.Second, Address: struct { IPv4 string `mapstructure:"ipv4"` IPv6 string `mapstructure:"ipv6"` }{IPv4: "100.100.100.101/30", IPv6: "2001::ffff:ffff:ffff:fff1/126"}, Route: &struct { Strict bool `mapstructure:"strict"` IPv4 []string `mapstructure:"ipv4"` IPv6 []string `mapstructure:"ipv6"` IPv4Exclude []string `mapstructure:"ipv4Exclude"` IPv6Exclude []string `mapstructure:"ipv6Exclude"` }{ Strict: true, IPv4: []string{"0.0.0.0/0"}, IPv6: []string{"2000::/3"}, IPv4Exclude: []string{"192.0.2.1/32"}, IPv6Exclude: []string{"2001:db8::1/128"}, }, }, }) } // TestClientConfigURI tests URI-related functions of clientConfig func TestClientConfigURI(t *testing.T) { tests := []struct { uri string uriOK bool config *clientConfig }{ { uri: "hysteria2://god@zilla.jp/", uriOK: true, config: &clientConfig{ Server: "zilla.jp", Auth: "god", }, }, { uri: "hysteria2://john:wick@continental.org:4443/", uriOK: true, config: &clientConfig{ Server: "continental.org:4443", Auth: "john:wick", }, }, { uri: "hysteria2://saul@better.call:7000-10000,20000/", uriOK: true, config: &clientConfig{ Server: "better.call:7000-10000,20000", Auth: "saul", }, }, { uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&pinSHA256=deadbeef&sni=crap.cc", uriOK: true, config: &clientConfig{ Server: "noauth.com", Auth: "", Obfs: clientConfigObfs{ Type: "salamander", Salamander: clientConfigObfsSalamander{ Password: "66ccff", }, }, TLS: clientConfigTLS{ SNI: "crap.cc", Insecure: true, PinSHA256: "deadbeef", }, }, }, { uri: "invalid.bs", uriOK: false, config: nil, }, { uri: "https://www.google.com/search?q=test", uriOK: false, config: nil, }, } for _, test := range tests { t.Run(test.uri, func(t *testing.T) { // Test parseURI nc := &clientConfig{Server: test.uri} assert.Equal(t, nc.parseURI(), test.uriOK) if test.uriOK { assert.Equal(t, nc, test.config) } // Test URI generation if test.config != nil { assert.Equal(t, test.config.URI(), test.uri) } }) } } func stringRef(s string) *string { return &s } func uint32Ref(i uint32) *uint32 { return &i } ================================================ FILE: app/cmd/client_test.yaml ================================================ server: example.com auth: weak_ahh_password transport: type: udp udp: hopInterval: 30s obfs: type: salamander salamander: password: cry_me_a_r1ver tls: sni: another.example.com insecure: true pinSHA256: 114515DEADBEEF ca: custom_ca.crt quic: initStreamReceiveWindow: 1145141 maxStreamReceiveWindow: 1145142 initConnReceiveWindow: 1145143 maxConnReceiveWindow: 1145144 maxIdleTimeout: 10s keepAlivePeriod: 4s disablePathMTUDiscovery: true sockopts: bindInterface: eth0 fwmark: 1234 fdControlUnixSocket: test.sock bandwidth: up: 200 mbps down: 1 gbps fastOpen: true lazy: true socks5: listen: 127.0.0.1:1080 username: anon password: bro disableUDP: true http: listen: 127.0.0.1:8080 username: qqq password: bruh realm: martian tcpForwarding: - listen: 127.0.0.1:8088 remote: internal.example.com:80 udpForwarding: - listen: 127.0.0.1:5353 remote: internal.example.com:53 timeout: 50s tcpTProxy: listen: 127.0.0.1:2500 udpTProxy: listen: 127.0.0.1:2501 timeout: 20s tcpRedirect: listen: 127.0.0.1:3500 tun: name: "hytun" mtu: 1500 timeout: 1m address: ipv4: 100.100.100.101/30 ipv6: 2001::ffff:ffff:ffff:fff1/126 route: strict: true ipv4: [ 0.0.0.0/0 ] ipv6: [ "2000::/3" ] ipv4Exclude: [ 192.0.2.1/32 ] ipv6Exclude: [ "2001:db8::1/128" ] ================================================ FILE: app/cmd/errors.go ================================================ package cmd import ( "fmt" ) type configError struct { Field string Err error } func (e configError) Error() string { return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err) } func (e configError) Unwrap() error { return e.Err } ================================================ FILE: app/cmd/ping.go ================================================ package cmd import ( "time" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" "github.com/apernet/hysteria/core/v2/client" ) // pingCmd represents the ping command var pingCmd = &cobra.Command{ Use: "ping address", Short: "Ping mode", Long: "Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.", Run: runPing, } func init() { rootCmd.AddCommand(pingCmd) } func runPing(cmd *cobra.Command, args []string) { logger.Info("ping mode") if len(args) != 1 { logger.Fatal("must specify one and only one address") } addr := args[0] if err := viper.ReadInConfig(); err != nil { logger.Fatal("failed to read client config", zap.Error(err)) } var config clientConfig if err := viper.Unmarshal(&config); err != nil { logger.Fatal("failed to parse client config", zap.Error(err)) } hyConfig, err := config.Config() if err != nil { logger.Fatal("failed to load client config", zap.Error(err)) } c, info, err := client.NewClient(hyConfig) if err != nil { logger.Fatal("failed to initialize client", zap.Error(err)) } defer c.Close() logger.Info("connected to server", zap.Bool("udpEnabled", info.UDPEnabled), zap.Uint64("tx", info.Tx)) logger.Info("connecting", zap.String("addr", addr)) start := time.Now() conn, err := c.TCP(addr) if err != nil { logger.Fatal("failed to connect", zap.Error(err), zap.String("time", time.Since(start).String())) } defer conn.Close() logger.Info("connected", zap.String("time", time.Since(start).String())) } ================================================ FILE: app/cmd/root.go ================================================ package cmd import ( "fmt" "os" "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) const ( appLogo = ` ░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█░░░▀▀▄ ░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█░░░▄▀░ ░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀▀▀ ` appDesc = "a powerful, lightning fast and censorship resistant proxy" appAuthors = "Aperture Internet Laboratory " appLogLevelEnv = "HYSTERIA_LOG_LEVEL" appLogFormatEnv = "HYSTERIA_LOG_FORMAT" appDisableUpdateCheckEnv = "HYSTERIA_DISABLE_UPDATE_CHECK" appACMEDirEnv = "HYSTERIA_ACME_DIR" ) var ( // These values will be injected by the build system appVersion = "Unknown" appDate = "Unknown" appType = "Unknown" // aka channel appCommit = "Unknown" appPlatform = "Unknown" appArch = "Unknown" appVersionLong = fmt.Sprintf("Version:\t%s\n"+ "BuildDate:\t%s\n"+ "BuildType:\t%s\n"+ "CommitHash:\t%s\n"+ "Platform:\t%s\n"+ "Architecture:\t%s", appVersion, appDate, appType, appCommit, appPlatform, appArch) appAboutLong = fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong) ) var logger *zap.Logger // Flags var ( cfgFile string logLevel string logFormat string disableUpdateCheck bool ) var rootCmd = &cobra.Command{ Use: "hysteria", Short: appDesc, Long: appAboutLong, Run: runClient, // Default to client mode } var logLevelMap = map[string]zapcore.Level{ "debug": zapcore.DebugLevel, "info": zapcore.InfoLevel, "warn": zapcore.WarnLevel, "error": zapcore.ErrorLevel, } var logFormatMap = map[string]zapcore.EncoderConfig{ "console": { TimeKey: "time", LevelKey: "level", NameKey: "logger", MessageKey: "msg", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalColorLevelEncoder, EncodeTime: zapcore.RFC3339TimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, }, "json": { TimeKey: "time", LevelKey: "level", NameKey: "logger", MessageKey: "msg", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.EpochMillisTimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, }, } func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { initFlags() cobra.MousetrapHelpText = "" // Disable the mousetrap so Windows users can run the exe directly by double-clicking cobra.OnInitialize(initConfig) cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config } func initFlags() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file") rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", envOrDefaultString(appLogLevelEnv, "info"), "log level") rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "f", envOrDefaultString(appLogFormatEnv, "console"), "log format") rootCmd.PersistentFlags().BoolVar(&disableUpdateCheck, "disable-update-check", envOrDefaultBool(appDisableUpdateCheckEnv, false), "disable update check") } func initConfig() { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { viper.SetConfigName("config") viper.SetConfigType("yaml") viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...) viper.AddConfigPath(".") viper.AddConfigPath("$HOME/.hysteria") viper.AddConfigPath("/etc/hysteria/") } } func initLogger() { level, ok := logLevelMap[strings.ToLower(logLevel)] if !ok { fmt.Printf("unsupported log level: %s\n", logLevel) os.Exit(1) } enc, ok := logFormatMap[strings.ToLower(logFormat)] if !ok { fmt.Printf("unsupported log format: %s\n", logFormat) os.Exit(1) } c := zap.Config{ Level: zap.NewAtomicLevelAt(level), DisableCaller: true, DisableStacktrace: true, Encoding: strings.ToLower(logFormat), EncoderConfig: enc, OutputPaths: []string{"stderr"}, ErrorOutputPaths: []string{"stderr"}, } var err error logger, err = c.Build() if err != nil { fmt.Printf("failed to initialize logger: %s\n", err) os.Exit(1) } } func envOrDefaultString(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func envOrDefaultBool(key string, def bool) bool { if v := os.Getenv(key); v != "" { b, _ := strconv.ParseBool(v) return b } return def } ================================================ FILE: app/cmd/server.go ================================================ package cmd import ( "context" "crypto/tls" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httputil" "net/url" "os" "strconv" "strings" "time" "github.com/caddyserver/certmagic" "github.com/libdns/cloudflare" "github.com/libdns/duckdns" "github.com/libdns/gandi" "github.com/libdns/godaddy" "github.com/libdns/namedotcom" "github.com/libdns/vultr" "github.com/mholt/acmez/acme" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" "github.com/apernet/hysteria/app/v2/internal/utils" "github.com/apernet/hysteria/core/v2/server" "github.com/apernet/hysteria/extras/v2/auth" "github.com/apernet/hysteria/extras/v2/correctnet" "github.com/apernet/hysteria/extras/v2/masq" "github.com/apernet/hysteria/extras/v2/obfs" "github.com/apernet/hysteria/extras/v2/outbounds" "github.com/apernet/hysteria/extras/v2/sniff" "github.com/apernet/hysteria/extras/v2/trafficlogger" eUtils "github.com/apernet/hysteria/extras/v2/utils" ) const ( defaultListenAddr = ":443" ) var serverCmd = &cobra.Command{ Use: "server", Short: "Server mode", Run: runServer, } func init() { rootCmd.AddCommand(serverCmd) } type serverConfig struct { V2board *v2boardConfig `mapstructure:"v2board"` Listen string `mapstructure:"listen"` Obfs serverConfigObfs `mapstructure:"obfs"` TLS *serverConfigTLS `mapstructure:"tls"` ACME *serverConfigACME `mapstructure:"acme"` QUIC serverConfigQUIC `mapstructure:"quic"` Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"` IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"` SpeedTest bool `mapstructure:"speedTest"` DisableUDP bool `mapstructure:"disableUDP"` UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"` Auth serverConfigAuth `mapstructure:"auth"` Resolver serverConfigResolver `mapstructure:"resolver"` Sniff serverConfigSniff `mapstructure:"sniff"` ACL serverConfigACL `mapstructure:"acl"` Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"` TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"` Masquerade serverConfigMasquerade `mapstructure:"masquerade"` } type v2boardConfig struct { ApiHost string `mapstructure:"apiHost"` ApiKey string `mapstructure:"apiKey"` NodeID uint `mapstructure:"nodeID"` PullInterval time.Duration `mapstructure:"pullInterval"` PushInterval time.Duration `mapstructure:"pushInterval"` } type serverConfigObfsSalamander struct { Password string `mapstructure:"password"` } type serverConfigObfs struct { Type string `mapstructure:"type"` Salamander serverConfigObfsSalamander `mapstructure:"salamander"` } type serverConfigTLS struct { Cert string `mapstructure:"cert"` Key string `mapstructure:"key"` SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict" } type serverConfigACME struct { // Common fields Domains []string `mapstructure:"domains"` Email string `mapstructure:"email"` CA string `mapstructure:"ca"` ListenHost string `mapstructure:"listenHost"` Dir string `mapstructure:"dir"` // Type selection Type string `mapstructure:"type"` HTTP serverConfigACMEHTTP `mapstructure:"http"` TLS serverConfigACMETLS `mapstructure:"tls"` DNS serverConfigACMEDNS `mapstructure:"dns"` // Legacy fields for backwards compatibility // Only applicable when Type is empty DisableHTTP bool `mapstructure:"disableHTTP"` DisableTLSALPN bool `mapstructure:"disableTLSALPN"` AltHTTPPort int `mapstructure:"altHTTPPort"` AltTLSALPNPort int `mapstructure:"altTLSALPNPort"` } type serverConfigACMEHTTP struct { AltPort int `mapstructure:"altPort"` } type serverConfigACMETLS struct { AltPort int `mapstructure:"altPort"` } type serverConfigACMEDNS struct { Name string `mapstructure:"name"` Config map[string]string `mapstructure:"config"` } type serverConfigQUIC struct { InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"` MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"` InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"` MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"` MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"` MaxIncomingStreams int64 `mapstructure:"maxIncomingStreams"` DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` } type serverConfigBandwidth struct { Up string `mapstructure:"up"` Down string `mapstructure:"down"` } type serverConfigAuthHTTP struct { URL string `mapstructure:"url"` Insecure bool `mapstructure:"insecure"` } type serverConfigAuth struct { Type string `mapstructure:"type"` Password string `mapstructure:"password"` UserPass map[string]string `mapstructure:"userpass"` HTTP serverConfigAuthHTTP `mapstructure:"http"` Command string `mapstructure:"command"` } type serverConfigResolverTCP struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` } type serverConfigResolverUDP struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` } type serverConfigResolverTLS struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` SNI string `mapstructure:"sni"` Insecure bool `mapstructure:"insecure"` } type serverConfigResolverHTTPS struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` SNI string `mapstructure:"sni"` Insecure bool `mapstructure:"insecure"` } type serverConfigResolver struct { Type string `mapstructure:"type"` TCP serverConfigResolverTCP `mapstructure:"tcp"` UDP serverConfigResolverUDP `mapstructure:"udp"` TLS serverConfigResolverTLS `mapstructure:"tls"` HTTPS serverConfigResolverHTTPS `mapstructure:"https"` } type serverConfigSniff struct { Enable bool `mapstructure:"enable"` Timeout time.Duration `mapstructure:"timeout"` RewriteDomain bool `mapstructure:"rewriteDomain"` TCPPorts string `mapstructure:"tcpPorts"` UDPPorts string `mapstructure:"udpPorts"` } type serverConfigACL struct { File string `mapstructure:"file"` Inline []string `mapstructure:"inline"` GeoIP string `mapstructure:"geoip"` GeoSite string `mapstructure:"geosite"` GeoUpdateInterval time.Duration `mapstructure:"geoUpdateInterval"` } type serverConfigOutboundDirect struct { Mode string `mapstructure:"mode"` BindIPv4 string `mapstructure:"bindIPv4"` BindIPv6 string `mapstructure:"bindIPv6"` BindDevice string `mapstructure:"bindDevice"` } type serverConfigOutboundSOCKS5 struct { Addr string `mapstructure:"addr"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` } type serverConfigOutboundHTTP struct { URL string `mapstructure:"url"` Insecure bool `mapstructure:"insecure"` } type serverConfigOutboundEntry struct { Name string `mapstructure:"name"` Type string `mapstructure:"type"` Direct serverConfigOutboundDirect `mapstructure:"direct"` SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"` HTTP serverConfigOutboundHTTP `mapstructure:"http"` } type serverConfigTrafficStats struct { Listen string `mapstructure:"listen"` Secret string `mapstructure:"secret"` } type serverConfigMasqueradeFile struct { Dir string `mapstructure:"dir"` } type serverConfigMasqueradeProxy struct { URL string `mapstructure:"url"` RewriteHost bool `mapstructure:"rewriteHost"` } type serverConfigMasqueradeString struct { Content string `mapstructure:"content"` Headers map[string]string `mapstructure:"headers"` StatusCode int `mapstructure:"statusCode"` } type serverConfigMasquerade struct { Type string `mapstructure:"type"` File serverConfigMasqueradeFile `mapstructure:"file"` Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"` String serverConfigMasqueradeString `mapstructure:"string"` ListenHTTP string `mapstructure:"listenHTTP"` ListenHTTPS string `mapstructure:"listenHTTPS"` ForceHTTPS bool `mapstructure:"forceHTTPS"` } func (c *serverConfig) fillConn(hyConfig *server.Config) error { listenAddr := c.Listen if listenAddr == "" { listenAddr = defaultListenAddr } uAddr, err := net.ResolveUDPAddr("udp", listenAddr) if err != nil { return configError{Field: "listen", Err: err} } conn, err := correctnet.ListenUDP("udp", uAddr) if err != nil { return configError{Field: "listen", Err: err} } switch strings.ToLower(c.Obfs.Type) { case "", "plain": hyConfig.Conn = conn return nil case "salamander": ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password)) if err != nil { return configError{Field: "obfs.salamander.password", Err: err} } hyConfig.Conn = obfs.WrapPacketConn(conn, ob) return nil default: return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")} } } func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error { if c.TLS == nil && c.ACME == nil { return configError{Field: "tls", Err: errors.New("must set either tls or acme")} } if c.TLS != nil && c.ACME != nil { return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")} } if c.TLS != nil { // SNI guard var sniGuard utils.SNIGuardFunc switch strings.ToLower(c.TLS.SNIGuard) { case "", "dns-san": sniGuard = utils.SNIGuardDNSSAN case "strict": sniGuard = utils.SNIGuardStrict case "disable": sniGuard = nil default: return configError{Field: "tls.sniGuard", Err: errors.New("unsupported SNI guard")} } // Local TLS cert if c.TLS.Cert == "" || c.TLS.Key == "" { return configError{Field: "tls", Err: errors.New("empty cert or key path")} } certLoader := &utils.LocalCertificateLoader{ CertFile: c.TLS.Cert, KeyFile: c.TLS.Key, SNIGuard: sniGuard, } // Try loading the cert-key pair here to catch errors early // (e.g. invalid files or insufficient permissions) err := certLoader.InitializeCache() if err != nil { var pathErr *os.PathError if errors.As(err, &pathErr) { if pathErr.Path == c.TLS.Cert { return configError{Field: "tls.cert", Err: pathErr} } if pathErr.Path == c.TLS.Key { return configError{Field: "tls.key", Err: pathErr} } } return configError{Field: "tls", Err: err} } // Use GetCertificate instead of Certificates so that // users can update the cert without restarting the server. hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate } else { // ACME dataDir := c.ACME.Dir if dataDir == "" { // If not specified in the config, check the environment variable // before resorting to the default "acme" value. The main reason // we have this is so that our setup script can set it to the // user's home directory. dataDir = envOrDefaultString(appACMEDirEnv, "acme") } cmCfg := &certmagic.Config{ RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio, KeySource: certmagic.DefaultKeyGenerator, Storage: &certmagic.FileStorage{Path: dataDir}, Logger: logger, } cmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{ Email: c.ACME.Email, Agreed: true, ListenHost: c.ACME.ListenHost, Logger: logger, }) switch strings.ToLower(c.ACME.CA) { case "letsencrypt", "le", "": // Default to Let's Encrypt cmIssuer.CA = certmagic.LetsEncryptProductionCA case "zerossl", "zero": cmIssuer.CA = certmagic.ZeroSSLProductionCA eab, err := genZeroSSLEAB(c.ACME.Email) if err != nil { return configError{Field: "acme.ca", Err: err} } cmIssuer.ExternalAccount = eab default: return configError{Field: "acme.ca", Err: errors.New("unsupported CA")} } switch strings.ToLower(c.ACME.Type) { case "http": cmIssuer.DisableHTTPChallenge = false cmIssuer.DisableTLSALPNChallenge = true cmIssuer.DNS01Solver = nil cmIssuer.AltHTTPPort = c.ACME.HTTP.AltPort case "tls": cmIssuer.DisableHTTPChallenge = true cmIssuer.DisableTLSALPNChallenge = false cmIssuer.DNS01Solver = nil cmIssuer.AltTLSALPNPort = c.ACME.TLS.AltPort case "dns": cmIssuer.DisableHTTPChallenge = true cmIssuer.DisableTLSALPNChallenge = true if c.ACME.DNS.Name == "" { return configError{Field: "acme.dns.name", Err: errors.New("empty DNS provider name")} } if c.ACME.DNS.Config == nil { return configError{Field: "acme.dns.config", Err: errors.New("empty DNS provider config")} } switch strings.ToLower(c.ACME.DNS.Name) { case "cloudflare": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &cloudflare.Provider{ APIToken: c.ACME.DNS.Config["cloudflare_api_token"], }, } case "duckdns": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &duckdns.Provider{ APIToken: c.ACME.DNS.Config["duckdns_api_token"], OverrideDomain: c.ACME.DNS.Config["duckdns_override_domain"], }, } case "gandi": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &gandi.Provider{ BearerToken: c.ACME.DNS.Config["gandi_api_token"], }, } case "godaddy": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &godaddy.Provider{ APIToken: c.ACME.DNS.Config["godaddy_api_token"], }, } case "namedotcom": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &namedotcom.Provider{ Token: c.ACME.DNS.Config["namedotcom_token"], User: c.ACME.DNS.Config["namedotcom_user"], Server: c.ACME.DNS.Config["namedotcom_server"], }, } case "vultr": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &vultr.Provider{ APIToken: c.ACME.DNS.Config["vultr_api_token"], }, } default: return configError{Field: "acme.dns.name", Err: errors.New("unsupported DNS provider")} } case "": // Legacy compatibility mode cmIssuer.DisableHTTPChallenge = c.ACME.DisableHTTP cmIssuer.DisableTLSALPNChallenge = c.ACME.DisableTLSALPN cmIssuer.AltHTTPPort = c.ACME.AltHTTPPort cmIssuer.AltTLSALPNPort = c.ACME.AltTLSALPNPort default: return configError{Field: "acme.type", Err: errors.New("unsupported ACME type")} } cmCfg.Issuers = []certmagic.Issuer{cmIssuer} cmCache := certmagic.NewCache(certmagic.CacheOptions{ GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { return cmCfg, nil }, Logger: logger, }) cmCfg = certmagic.New(cmCache, *cmCfg) if len(c.ACME.Domains) == 0 { return configError{Field: "acme.domains", Err: errors.New("empty domains")} } err := cmCfg.ManageSync(context.Background(), c.ACME.Domains) if err != nil { return configError{Field: "acme.domains", Err: err} } hyConfig.TLSConfig.GetCertificate = cmCfg.GetCertificate } return nil } func genZeroSSLEAB(email string) (*acme.EAB, error) { req, err := http.NewRequest( http.MethodPost, "https://api.zerossl.com/acme/eab-credentials-email", strings.NewReader(url.Values{"email": []string{email}}.Encode()), ) if err != nil { return nil, fmt.Errorf("failed to creare ZeroSSL EAB request: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", certmagic.UserAgent) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send ZeroSSL EAB request: %w", err) } defer func() { _ = resp.Body.Close() }() var result struct { Success bool `json:"success"` Error struct { Code int `json:"code"` Type string `json:"type"` } `json:"error"` EABKID string `json:"eab_kid"` EABHMACKey string `json:"eab_hmac_key"` } if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed decoding ZeroSSL EAB API response: %w", err) } if result.Error.Code != 0 { return nil, fmt.Errorf("failed getting ZeroSSL EAB credentials: HTTP %d: %s (code %d)", resp.StatusCode, result.Error.Type, result.Error.Code) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode) } return &acme.EAB{ KeyID: result.EABKID, MACKey: result.EABHMACKey, }, nil } func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error { hyConfig.QUICConfig = server.QUICConfig{ InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow, MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow, MaxConnectionReceiveWindow: c.QUIC.MaxConnectionReceiveWindow, MaxIdleTimeout: c.QUIC.MaxIdleTimeout, MaxIncomingStreams: c.QUIC.MaxIncomingStreams, DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery, } return nil } func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) { var mode outbounds.DirectOutboundMode switch strings.ToLower(c.Mode) { case "", "auto": mode = outbounds.DirectOutboundModeAuto case "64": mode = outbounds.DirectOutboundMode64 case "46": mode = outbounds.DirectOutboundMode46 case "6": mode = outbounds.DirectOutboundMode6 case "4": mode = outbounds.DirectOutboundMode4 default: return nil, configError{Field: "outbounds.direct.mode", Err: errors.New("unsupported mode")} } bindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0 bindDevice := len(c.BindDevice) > 0 if bindIP && bindDevice { return nil, configError{Field: "outbounds.direct", Err: errors.New("cannot bind both IP and device")} } if bindIP { ip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6) if len(c.BindIPv4) > 0 && ip4 == nil { return nil, configError{Field: "outbounds.direct.bindIPv4", Err: errors.New("invalid IPv4 address")} } if len(c.BindIPv6) > 0 && ip6 == nil { return nil, configError{Field: "outbounds.direct.bindIPv6", Err: errors.New("invalid IPv6 address")} } return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6) } if bindDevice { return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice) } return outbounds.NewDirectOutboundSimple(mode), nil } func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) { if c.Addr == "" { return nil, configError{Field: "outbounds.socks5.addr", Err: errors.New("empty socks5 address")} } return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil } func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) { if c.URL == "" { return nil, configError{Field: "outbounds.http.url", Err: errors.New("empty http address")} } return outbounds.NewHTTPOutbound(c.URL, c.Insecure) } func (c *serverConfig) fillRequestHook(hyConfig *server.Config) error { if c.Sniff.Enable { s := &sniff.Sniffer{ Timeout: c.Sniff.Timeout, RewriteDomain: c.Sniff.RewriteDomain, } if c.Sniff.TCPPorts != "" { s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts) if s.TCPPorts == nil { return configError{Field: "sniff.tcpPorts", Err: errors.New("invalid port union")} } } if c.Sniff.UDPPorts != "" { s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts) if s.UDPPorts == nil { return configError{Field: "sniff.udpPorts", Err: errors.New("invalid port union")} } } hyConfig.RequestHook = s } return nil } func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { // Resolver, ACL, actual outbound are all implemented through the Outbound interface. // Depending on the config, we build a chain like this: // Resolver(ACL(Outbounds...)) // Outbounds var obs []outbounds.OutboundEntry if len(c.Outbounds) == 0 { // Guarantee we have at least one outbound obs = []outbounds.OutboundEntry{{ Name: "default", Outbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto), }} } else { obs = make([]outbounds.OutboundEntry, len(c.Outbounds)) for i, entry := range c.Outbounds { if entry.Name == "" { return configError{Field: "outbounds.name", Err: errors.New("empty outbound name")} } var ob outbounds.PluggableOutbound var err error switch strings.ToLower(entry.Type) { case "direct": ob, err = serverConfigOutboundDirectToOutbound(entry.Direct) case "socks5": ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5) case "http": ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP) default: err = configError{Field: "outbounds.type", Err: errors.New("unsupported outbound type")} } if err != nil { return err } obs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob} } } var uOb outbounds.PluggableOutbound // "unified" outbound // ACL hasACL := false if c.ACL.File != "" && len(c.ACL.Inline) > 0 { return configError{Field: "acl", Err: errors.New("cannot set both acl.file and acl.inline")} } gLoader := &utils.GeoLoader{ GeoIPFilename: c.ACL.GeoIP, GeoSiteFilename: c.ACL.GeoSite, UpdateInterval: c.ACL.GeoUpdateInterval, DownloadFunc: geoDownloadFunc, DownloadErrFunc: geoDownloadErrFunc, } if c.ACL.File != "" { hasACL = true acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader) if err != nil { return configError{Field: "acl.file", Err: err} } uOb = acl } else if len(c.ACL.Inline) > 0 { hasACL = true acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader) if err != nil { return configError{Field: "acl.inline", Err: err} } uOb = acl } else { // No ACL, use the first outbound uOb = obs[0].Outbound } // Resolver switch strings.ToLower(c.Resolver.Type) { case "", "system": if hasACL { // If the user uses ACL, we must put a resolver in front of it, // for IP rules to work on domain requests. uOb = outbounds.NewSystemResolver(uOb) } // Otherwise we can just rely on outbound handling on its own. case "tcp": if c.Resolver.TCP.Addr == "" { return configError{Field: "resolver.tcp.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, uOb) case "udp": if c.Resolver.UDP.Addr == "" { return configError{Field: "resolver.udp.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, uOb) case "tls", "tcp-tls": if c.Resolver.TLS.Addr == "" { return configError{Field: "resolver.tls.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, uOb) case "https", "http": if c.Resolver.HTTPS.Addr == "" { return configError{Field: "resolver.https.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, uOb) default: return configError{Field: "resolver.type", Err: errors.New("unsupported resolver type")} } // Speed test if c.SpeedTest { uOb = outbounds.NewSpeedtestHandler(uOb) } hyConfig.Outbound = &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb} return nil } func (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error { var err error if c.Bandwidth.Up != "" { hyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up) if err != nil { return configError{Field: "bandwidth.up", Err: err} } } if c.Bandwidth.Down != "" { hyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down) if err != nil { return configError{Field: "bandwidth.down", Err: err} } } return nil } func (c *serverConfig) fillIgnoreClientBandwidth(hyConfig *server.Config) error { hyConfig.IgnoreClientBandwidth = c.IgnoreClientBandwidth return nil } func (c *serverConfig) fillDisableUDP(hyConfig *server.Config) error { hyConfig.DisableUDP = c.DisableUDP return nil } func (c *serverConfig) fillUDPIdleTimeout(hyConfig *server.Config) error { hyConfig.UDPIdleTimeout = c.UDPIdleTimeout return nil } func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error { if c.Auth.Type == "" { return configError{Field: "auth.type", Err: errors.New("empty auth type")} } switch strings.ToLower(c.Auth.Type) { case "password": if c.Auth.Password == "" { return configError{Field: "auth.password", Err: errors.New("empty auth password")} } hyConfig.Authenticator = &auth.PasswordAuthenticator{Password: c.Auth.Password} return nil case "userpass": if len(c.Auth.UserPass) == 0 { return configError{Field: "auth.userpass", Err: errors.New("empty auth userpass")} } hyConfig.Authenticator = &auth.UserPassAuthenticator{Users: c.Auth.UserPass} return nil case "http", "https": if c.Auth.HTTP.URL == "" { return configError{Field: "auth.http.url", Err: errors.New("empty auth http url")} } hyConfig.Authenticator = auth.NewHTTPAuthenticator(c.Auth.HTTP.URL, c.Auth.HTTP.Insecure) return nil case "command", "cmd": if c.Auth.Command == "" { return configError{Field: "auth.command", Err: errors.New("empty auth command")} } hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command} return nil case "v2board": v2boardConfig := c.V2board if v2boardConfig.ApiHost == "" || v2boardConfig.ApiKey == "" || v2boardConfig.NodeID == 0 { return configError{Field: "auth.v2board", Err: errors.New("v2board config error")} } hyConfig.Authenticator = &auth.V2boardApiProvider{URL: fmt.Sprintf("%s?token=%s&node_id=%d&node_type=hysteria", c.V2board.ApiHost+"/api/v1/server/UniProxy/user", c.V2board.ApiKey, c.V2board.NodeID)} return nil default: return configError{Field: "auth.type", Err: errors.New("unsupported auth type")} } } func (c *serverConfig) fillEventLogger(hyConfig *server.Config) error { hyConfig.EventLogger = &serverLogger{} return nil } func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error { pullInterval := time.Second * 5 if c.V2board.PullInterval > 0 { pullInterval = time.Duration(c.V2board.PullInterval) * time.Second } pushInterval := time.Second * 60 if c.V2board.PushInterval > 0 { pushInterval = time.Duration(c.V2board.PushInterval) * time.Second } userURL := fmt.Sprintf("%s?token=%s&node_id=%d&node_type=hysteria", c.V2board.ApiHost+"/api/v1/server/UniProxy/user", c.V2board.ApiKey, c.V2board.NodeID) pushURL := fmt.Sprintf("%s?token=%s&node_id=%d&node_type=hysteria", c.V2board.ApiHost+"/api/v1/server/UniProxy/push", c.V2board.ApiKey, c.V2board.NodeID) if c.TrafficStats.Listen != "" { tss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret) hyConfig.TrafficLogger = tss if c.V2board != nil && c.V2board.ApiHost != "" { go auth.UpdateUsers(userURL, pullInterval, hyConfig.TrafficLogger) go hyConfig.TrafficLogger.PushTrafficToV2boardInterval(pushURL, pushInterval) } go runTrafficStatsServer(c.TrafficStats.Listen, tss) } else { go auth.UpdateUsers(userURL, pullInterval, nil) } return nil } // fillMasqHandler must be called after fillConn, as we may need to extract the QUIC // port number from Conn for MasqTCPServer. func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error { var handler http.Handler switch strings.ToLower(c.Masquerade.Type) { case "", "404": handler = http.NotFoundHandler() case "file": if c.Masquerade.File.Dir == "" { return configError{Field: "masquerade.file.dir", Err: errors.New("empty file directory")} } handler = http.FileServer(http.Dir(c.Masquerade.File.Dir)) case "proxy": if c.Masquerade.Proxy.URL == "" { return configError{Field: "masquerade.proxy.url", Err: errors.New("empty proxy url")} } u, err := url.Parse(c.Masquerade.Proxy.URL) if err != nil { return configError{Field: "masquerade.proxy.url", Err: err} } if u.Scheme != "http" && u.Scheme != "https" { return configError{Field: "masquerade.proxy.url", Err: fmt.Errorf("unsupported protocol scheme \"%s\"", u.Scheme)} } handler = &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(u) // SetURL rewrites the Host header, // but we don't want that if rewriteHost is false if !c.Masquerade.Proxy.RewriteHost { r.Out.Host = r.In.Host } }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { logger.Error("HTTP reverse proxy error", zap.Error(err)) w.WriteHeader(http.StatusBadGateway) }, } case "string": if c.Masquerade.String.Content == "" { return configError{Field: "masquerade.string.content", Err: errors.New("empty string content")} } if c.Masquerade.String.StatusCode != 0 && (c.Masquerade.String.StatusCode < 200 || c.Masquerade.String.StatusCode > 599 || c.Masquerade.String.StatusCode == 233) { // 233 is reserved for Hysteria authentication return configError{Field: "masquerade.string.statusCode", Err: errors.New("invalid status code (must be 200-599, except 233)")} } handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for k, v := range c.Masquerade.String.Headers { w.Header().Set(k, v) } if c.Masquerade.String.StatusCode != 0 { w.WriteHeader(c.Masquerade.String.StatusCode) } else { w.WriteHeader(http.StatusOK) // Use 200 OK by default } _, _ = w.Write([]byte(c.Masquerade.String.Content)) }) default: return configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")} } hyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler, QUIC: true} if c.Masquerade.ListenHTTP != "" || c.Masquerade.ListenHTTPS != "" { if c.Masquerade.ListenHTTP != "" && c.Masquerade.ListenHTTPS == "" { return configError{Field: "masquerade.listenHTTPS", Err: errors.New("having only HTTP server without HTTPS is not supported")} } s := masq.MasqTCPServer{ QUICPort: extractPortFromAddr(hyConfig.Conn.LocalAddr().String()), HTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS), Handler: &masqHandlerLogWrapper{H: handler, QUIC: false}, TLSConfig: &tls.Config{ Certificates: hyConfig.TLSConfig.Certificates, GetCertificate: hyConfig.TLSConfig.GetCertificate, }, ForceHTTPS: c.Masquerade.ForceHTTPS, } go runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS) } return nil } // Config validates the fields and returns a ready-to-use Hysteria server config func (c *serverConfig) Config() (*server.Config, error) { hyConfig := &server.Config{} fillers := []func(*server.Config) error{ c.fillConn, c.fillTLSConfig, c.fillQUICConfig, c.fillRequestHook, c.fillOutboundConfig, c.fillBandwidthConfig, c.fillIgnoreClientBandwidth, c.fillDisableUDP, c.fillUDPIdleTimeout, c.fillAuthenticator, c.fillEventLogger, c.fillTrafficLogger, c.fillMasqHandler, } for _, f := range fillers { if err := f(hyConfig); err != nil { return nil, err } } return hyConfig, nil } type ResponseNodeInfo struct { Host string `json:"host"` ServerPort uint `json:"server_port"` ServerName string `json:"server_name"` UpMbps uint `json:"down_mbps"` DownMbps uint `json:"up_mbps"` Obfs string `json:"obfs"` BaseConfig struct { PushInterval int `json:"push_interval"` PullInterval int `json:"pull_interval"` } `json:"base_config"` } func runServer(cmd *cobra.Command, args []string) { logger.Info("server mode") if err := viper.ReadInConfig(); err != nil { logger.Fatal("failed to read server config", zap.Error(err)) } var config serverConfig if err := viper.Unmarshal(&config); err != nil { logger.Fatal("failed to parse server config", zap.Error(err)) } // 如果配置了v2board 则自动获取监听端口、obfs if config.V2board != nil && config.V2board.ApiHost != "" { // 创建一个url.Values来存储查询参数 queryParams := url.Values{ "token": {config.V2board.ApiKey}, "node_id": {strconv.Itoa(int(config.V2board.NodeID))}, "node_type": {"hysteria"}, } nodeInfoUrl := config.V2board.ApiHost + "/api/v1/server/UniProxy/config?" + queryParams.Encode() resp, err := http.Get(nodeInfoUrl) if err != nil { // 处理错误 fmt.Println("HTTP GET 请求出错:", err) logger.Fatal("failed to client v2board api to get nodeInfo", zap.Error(err)) } defer resp.Body.Close() // 读取响应数据 body, err := io.ReadAll(resp.Body) if err != nil { logger.Fatal("failed to read v2board reaponse", zap.Error(err)) } // 解析JSON数据 var responseNodeInfo ResponseNodeInfo err = json.Unmarshal(body, &responseNodeInfo) if err != nil { logger.Fatal("failed to unmarshal v2board reaponse", zap.Error(err)) } // 给 hy的端口、obfs、上行下行进行赋值 if responseNodeInfo.ServerPort != 0 { config.Listen = ":" + strconv.Itoa(int(responseNodeInfo.ServerPort)) } if responseNodeInfo.DownMbps != 0 { config.Bandwidth.Down = strconv.Itoa(int(responseNodeInfo.DownMbps)) + "Mbps" } if responseNodeInfo.UpMbps != 0 { config.Bandwidth.Up = strconv.Itoa(int(responseNodeInfo.UpMbps)) + "Mbps" } if responseNodeInfo.Obfs != "" { config.Obfs.Type = "salamander" config.Obfs.Salamander.Password = responseNodeInfo.Obfs } } hyConfig, err := config.Config() if err != nil { logger.Fatal("failed to load server config", zap.Error(err)) } s, err := server.NewServer(hyConfig) if err != nil { logger.Fatal("failed to initialize server", zap.Error(err)) } if config.Listen != "" { logger.Info("server up and running", zap.String("listen", config.Listen)) } else { logger.Info("server up and running", zap.String("listen", defaultListenAddr)) } if !disableUpdateCheck { go runCheckUpdateServer() } if err := s.Serve(); err != nil { logger.Fatal("failed to serve", zap.Error(err)) } } func runTrafficStatsServer(listen string, handler http.Handler) { logger.Info("traffic stats server up and running", zap.String("listen", listen)) if err := correctnet.HTTPListenAndServe(listen, handler); err != nil { logger.Fatal("failed to serve traffic stats", zap.Error(err)) } } func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string) { errChan := make(chan error, 2) if httpAddr != "" { go func() { logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr)) errChan <- s.ListenAndServeHTTP(httpAddr) }() } if httpsAddr != "" { go func() { logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr)) errChan <- s.ListenAndServeHTTPS(httpsAddr) }() } err := <-errChan if err != nil { logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err)) } } func geoDownloadFunc(filename, url string) { logger.Info("downloading database", zap.String("filename", filename), zap.String("url", url)) } func geoDownloadErrFunc(err error) { if err != nil { logger.Error("failed to download database", zap.Error(err)) } } type serverLogger struct{} func (l *serverLogger) Connect(addr net.Addr, id string, tx uint64) { logger.Info("client connected", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint64("tx", tx)) } func (l *serverLogger) Disconnect(addr net.Addr, id string, err error) { logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("id", id), zap.Error(err)) } func (l *serverLogger) TCPRequest(addr net.Addr, id, reqAddr string) { logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) } func (l *serverLogger) TCPError(addr net.Addr, id, reqAddr string, err error) { if err == nil { logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) } else { logger.Warn("TCP error", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr), zap.Error(err)) } } func (l *serverLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) { logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.String("reqAddr", reqAddr)) } func (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) { if err == nil { logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID)) } else { logger.Warn("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err)) } } type masqHandlerLogWrapper struct { H http.Handler QUIC bool } func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { logger.Debug("masquerade request", zap.String("addr", r.RemoteAddr), zap.String("method", r.Method), zap.String("host", r.Host), zap.String("url", r.URL.String()), zap.Bool("quic", m.QUIC)) m.H.ServeHTTP(w, r) } func extractPortFromAddr(addr string) int { _, portStr, err := net.SplitHostPort(addr) if err != nil { return 0 } port, err := strconv.Atoi(portStr) if err != nil { return 0 } return port } ================================================ FILE: app/cmd/server_test.go ================================================ package cmd import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/spf13/viper" ) // TestServerConfig tests the parsing of the server config func TestServerConfig(t *testing.T) { viper.SetConfigFile("server_test.yaml") err := viper.ReadInConfig() assert.NoError(t, err) var config serverConfig err = viper.Unmarshal(&config) assert.NoError(t, err) assert.Equal(t, config, serverConfig{ Listen: ":8443", Obfs: serverConfigObfs{ Type: "salamander", Salamander: serverConfigObfsSalamander{ Password: "cry_me_a_r1ver", }, }, TLS: &serverConfigTLS{ Cert: "some.crt", Key: "some.key", SNIGuard: "strict", }, ACME: &serverConfigACME{ Domains: []string{ "sub1.example.com", "sub2.example.com", }, Email: "haha@cringe.net", CA: "zero", ListenHost: "127.0.0.9", Dir: "random_dir", Type: "dns", HTTP: serverConfigACMEHTTP{ AltPort: 8888, }, TLS: serverConfigACMETLS{ AltPort: 44333, }, DNS: serverConfigACMEDNS{ Name: "gomommy", Config: map[string]string{ "key1": "value1", "key2": "value2", }, }, DisableHTTP: true, DisableTLSALPN: true, AltHTTPPort: 8080, AltTLSALPNPort: 4433, }, QUIC: serverConfigQUIC{ InitStreamReceiveWindow: 77881, MaxStreamReceiveWindow: 77882, InitConnectionReceiveWindow: 77883, MaxConnectionReceiveWindow: 77884, MaxIdleTimeout: 999 * time.Second, MaxIncomingStreams: 256, DisablePathMTUDiscovery: true, }, Bandwidth: serverConfigBandwidth{ Up: "500 mbps", Down: "100 mbps", }, IgnoreClientBandwidth: true, SpeedTest: true, DisableUDP: true, UDPIdleTimeout: 120 * time.Second, Auth: serverConfigAuth{ Type: "password", Password: "goofy_ahh_password", UserPass: map[string]string{ "yolo": "swag", "lol": "kek", "foo": "bar", }, HTTP: serverConfigAuthHTTP{ URL: "http://127.0.0.1:5000/auth", Insecure: true, }, Command: "/etc/some_command", }, Resolver: serverConfigResolver{ Type: "udp", TCP: serverConfigResolverTCP{ Addr: "123.123.123.123:5353", Timeout: 4 * time.Second, }, UDP: serverConfigResolverUDP{ Addr: "4.6.8.0:53", Timeout: 2 * time.Second, }, TLS: serverConfigResolverTLS{ Addr: "dot.yolo.com:8853", Timeout: 10 * time.Second, SNI: "server1.yolo.net", Insecure: true, }, HTTPS: serverConfigResolverHTTPS{ Addr: "cringe.ahh.cc", Timeout: 5 * time.Second, SNI: "real.stuff.net", Insecure: true, }, }, Sniff: serverConfigSniff{ Enable: true, Timeout: 1 * time.Second, RewriteDomain: true, TCPPorts: "80,443,1000-2000", UDPPorts: "443", }, ACL: serverConfigACL{ File: "chnroute.txt", Inline: []string{ "lmao(ok)", "kek(cringe,boba,tea)", }, GeoIP: "some.dat", GeoSite: "some_site.dat", GeoUpdateInterval: 168 * time.Hour, }, Outbounds: []serverConfigOutboundEntry{ { Name: "goodstuff", Type: "direct", Direct: serverConfigOutboundDirect{ Mode: "64", BindIPv4: "2.4.6.8", BindIPv6: "0:0:0:0:0:ffff:0204:0608", BindDevice: "eth233", }, }, { Name: "badstuff", Type: "socks5", SOCKS5: serverConfigOutboundSOCKS5{ Addr: "shady.proxy.ru:1080", Username: "hackerman", Password: "Elliot Alderson", }, }, { Name: "weirdstuff", Type: "http", HTTP: serverConfigOutboundHTTP{ URL: "https://eyy.lmao:4443/goofy", Insecure: true, }, }, }, TrafficStats: serverConfigTrafficStats{ Listen: ":9999", Secret: "its_me_mario", }, Masquerade: serverConfigMasquerade{ Type: "proxy", File: serverConfigMasqueradeFile{ Dir: "/www/masq", }, Proxy: serverConfigMasqueradeProxy{ URL: "https://some.site.net", RewriteHost: true, }, String: serverConfigMasqueradeString{ Content: "aint nothin here", Headers: map[string]string{ "content-type": "text/plain", "custom-haha": "lol", }, StatusCode: 418, }, ListenHTTP: ":80", ListenHTTPS: ":443", ForceHTTPS: true, }, }) } ================================================ FILE: app/cmd/server_test.yaml ================================================ listen: :8443 obfs: type: salamander salamander: password: cry_me_a_r1ver tls: cert: some.crt key: some.key sniGuard: strict acme: domains: - sub1.example.com - sub2.example.com email: haha@cringe.net ca: zero listenHost: 127.0.0.9 dir: random_dir type: dns http: altPort: 8888 tls: altPort: 44333 dns: name: gomommy config: key1: value1 key2: value2 disableHTTP: true disableTLSALPN: true altHTTPPort: 8080 altTLSALPNPort: 4433 quic: initStreamReceiveWindow: 77881 maxStreamReceiveWindow: 77882 initConnReceiveWindow: 77883 maxConnReceiveWindow: 77884 maxIdleTimeout: 999s maxIncomingStreams: 256 disablePathMTUDiscovery: true bandwidth: up: 500 mbps down: 100 mbps ignoreClientBandwidth: true speedTest: true disableUDP: true udpIdleTimeout: 120s auth: type: password password: goofy_ahh_password userpass: yolo: swag lol: kek foo: bar http: url: http://127.0.0.1:5000/auth insecure: true command: /etc/some_command resolver: type: udp tcp: addr: 123.123.123.123:5353 timeout: 4s udp: addr: 4.6.8.0:53 timeout: 2s tls: addr: dot.yolo.com:8853 timeout: 10s sni: server1.yolo.net insecure: true https: addr: cringe.ahh.cc timeout: 5s sni: real.stuff.net insecure: true sniff: enable: true timeout: 1s rewriteDomain: true tcpPorts: 80,443,1000-2000 udpPorts: 443 acl: file: chnroute.txt inline: - lmao(ok) - kek(cringe,boba,tea) geoip: some.dat geosite: some_site.dat geoUpdateInterval: 168h outbounds: - name: goodstuff type: direct direct: mode: 64 bindIPv4: 2.4.6.8 bindIPv6: 0:0:0:0:0:ffff:0204:0608 bindDevice: eth233 - name: badstuff type: socks5 socks5: addr: shady.proxy.ru:1080 username: hackerman password: Elliot Alderson - name: weirdstuff type: http http: url: https://eyy.lmao:4443/goofy insecure: true trafficStats: listen: :9999 secret: its_me_mario masquerade: type: proxy file: dir: /www/masq proxy: url: https://some.site.net rewriteHost: true string: content: aint nothin here headers: content-type: text/plain custom-haha: lol statusCode: 418 listenHTTP: :80 listenHTTPS: :443 forceHTTPS: true ================================================ FILE: app/cmd/share.go ================================================ package cmd import ( "fmt" "github.com/apernet/hysteria/app/v2/internal/utils" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" ) var ( noText bool withQR bool ) // shareCmd represents the share command var shareCmd = &cobra.Command{ Use: "share", Short: "Generate share URI", Long: "Generate a hysteria2:// URI from a client config for sharing", Run: runShare, } func init() { initShareFlags() rootCmd.AddCommand(shareCmd) } func initShareFlags() { shareCmd.Flags().BoolVar(&noText, "notext", false, "do not show URI as text") shareCmd.Flags().BoolVar(&withQR, "qr", false, "show URI as QR code") } func runShare(cmd *cobra.Command, args []string) { if err := viper.ReadInConfig(); err != nil { logger.Fatal("failed to read client config", zap.Error(err)) } var config clientConfig if err := viper.Unmarshal(&config); err != nil { logger.Fatal("failed to parse client config", zap.Error(err)) } if _, err := config.Config(); err != nil { logger.Fatal("failed to load client config", zap.Error(err)) } u := config.URI() if !noText { fmt.Println(u) } if withQR { utils.PrintQR(u) } } ================================================ FILE: app/cmd/speedtest.go ================================================ package cmd import ( "errors" "fmt" "os" "os/signal" "syscall" "time" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" "github.com/apernet/hysteria/core/v2/client" hyErrors "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/extras/v2/outbounds" "github.com/apernet/hysteria/extras/v2/outbounds/speedtest" ) var ( skipDownload bool skipUpload bool dataSize uint32 useBytes bool speedtestAddr = fmt.Sprintf("%s:%d", outbounds.SpeedtestDest, 0) ) // speedtestCmd represents the speedtest command var speedtestCmd = &cobra.Command{ Use: "speedtest", Short: "Speed test mode", Long: "Perform a speed test through the proxy server. The server must have speed test support enabled.", Run: runSpeedtest, } func init() { initSpeedtestFlags() rootCmd.AddCommand(speedtestCmd) } func initSpeedtestFlags() { speedtestCmd.Flags().BoolVar(&skipDownload, "skip-download", false, "Skip download test") speedtestCmd.Flags().BoolVar(&skipUpload, "skip-upload", false, "Skip upload test") speedtestCmd.Flags().Uint32Var(&dataSize, "data-size", 1024*1024*100, "Data size for download and upload tests") speedtestCmd.Flags().BoolVar(&useBytes, "use-bytes", false, "Use bytes per second instead of bits per second") } func runSpeedtest(cmd *cobra.Command, args []string) { logger.Info("speed test mode") if err := viper.ReadInConfig(); err != nil { logger.Fatal("failed to read client config", zap.Error(err)) } var config clientConfig if err := viper.Unmarshal(&config); err != nil { logger.Fatal("failed to parse client config", zap.Error(err)) } hyConfig, err := config.Config() if err != nil { logger.Fatal("failed to load client config", zap.Error(err)) } c, info, err := client.NewClient(hyConfig) if err != nil { logger.Fatal("failed to initialize client", zap.Error(err)) } defer c.Close() logger.Info("connected to server", zap.Bool("udpEnabled", info.UDPEnabled), zap.Uint64("tx", info.Tx)) signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) defer signal.Stop(signalChan) runChan := make(chan struct{}, 1) go func() { if !skipDownload { runDownloadTest(c) } if !skipUpload { runUploadTest(c) } runChan <- struct{}{} }() select { case <-signalChan: logger.Info("received signal, shutting down gracefully") case <-runChan: logger.Info("speed test complete") } } func runDownloadTest(c client.Client) { logger.Info("performing download test") downConn, err := c.TCP(speedtestAddr) if err != nil { if errors.As(err, &hyErrors.DialError{}) { logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err)) } else { logger.Fatal("failed to connect", zap.Error(err)) } } defer downConn.Close() downClient := &speedtest.Client{Conn: downConn} currentTotal := uint32(0) err = downClient.Download(dataSize, func(d time.Duration, b uint32, done bool) { if !done { currentTotal += b logger.Info("downloading", zap.Uint32("bytes", b), zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)), zap.String("speed", formatSpeed(b, d, useBytes))) } else { logger.Info("download complete", zap.Uint32("bytes", b), zap.String("speed", formatSpeed(b, d, useBytes))) } }) if err != nil { logger.Fatal("download test failed", zap.Error(err)) } logger.Info("download test complete") } func runUploadTest(c client.Client) { logger.Info("performing upload test") upConn, err := c.TCP(speedtestAddr) if err != nil { if errors.As(err, &hyErrors.DialError{}) { logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err)) } else { logger.Fatal("failed to connect", zap.Error(err)) } } defer upConn.Close() upClient := &speedtest.Client{Conn: upConn} currentTotal := uint32(0) err = upClient.Upload(dataSize, func(d time.Duration, b uint32, done bool) { if !done { currentTotal += b logger.Info("uploading", zap.Uint32("bytes", b), zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)), zap.String("speed", formatSpeed(b, d, useBytes))) } else { logger.Info("upload complete", zap.Uint32("bytes", b), zap.String("speed", formatSpeed(b, d, useBytes))) } }) if err != nil { logger.Fatal("upload test failed", zap.Error(err)) } logger.Info("upload test complete") } func formatSpeed(bytes uint32, duration time.Duration, useBytes bool) string { speed := float64(bytes) / duration.Seconds() var units []string if useBytes { units = []string{"B/s", "KB/s", "MB/s", "GB/s"} } else { units = []string{"bps", "Kbps", "Mbps", "Gbps"} speed *= 8 } unitIndex := 0 for speed > 1000 && unitIndex < len(units)-1 { speed /= 1000 unitIndex++ } return fmt.Sprintf("%.2f %s", speed, units[unitIndex]) } ================================================ FILE: app/cmd/update.go ================================================ package cmd import ( "time" "github.com/spf13/cobra" "go.uber.org/zap" "github.com/apernet/hysteria/app/v2/internal/utils" "github.com/apernet/hysteria/core/v2/client" ) const ( updateCheckInterval = 24 * time.Hour ) // checkUpdateCmd represents the checkUpdate command var checkUpdateCmd = &cobra.Command{ Use: "check-update", Short: "Check for updates", Long: "Check for updates.", Run: runCheckUpdate, } func init() { rootCmd.AddCommand(checkUpdateCmd) } func runCheckUpdate(cmd *cobra.Command, args []string) { logger.Info("checking for updates", zap.String("version", appVersion), zap.String("platform", appPlatform), zap.String("arch", appArch), zap.String("channel", appType), ) checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType) resp, err := checker.Check() if err != nil { logger.Fatal("failed to check for updates", zap.Error(err)) } if resp.HasUpdate { logger.Info("update available", zap.String("version", resp.LatestVersion), zap.String("url", resp.URL), zap.Bool("urgent", resp.Urgent), ) } else { logger.Info("no update available") } } // runCheckUpdateServer is the background update checking routine for server mode func runCheckUpdateServer() { checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType) checkUpdateRoutine(checker) } // runCheckUpdateClient is the background update checking routine for client mode func runCheckUpdateClient(hyClient client.Client) { checker := utils.NewClientUpdateChecker(appVersion, appPlatform, appArch, appType, hyClient) checkUpdateRoutine(checker) } func checkUpdateRoutine(checker *utils.UpdateChecker) { ticker := time.NewTicker(updateCheckInterval) for { logger.Debug("checking for updates", zap.String("version", appVersion), zap.String("platform", appPlatform), zap.String("arch", appArch), zap.String("channel", appType), ) resp, err := checker.Check() if err != nil { logger.Debug("failed to check for updates", zap.Error(err)) } else if resp.HasUpdate { logger.Info("update available", zap.String("version", resp.LatestVersion), zap.String("url", resp.URL), zap.Bool("urgent", resp.Urgent), ) } else { logger.Debug("no update available") } <-ticker.C } } ================================================ FILE: app/cmd/version.go ================================================ package cmd import ( "fmt" "github.com/spf13/cobra" ) // versionCmd represents the version command var versionCmd = &cobra.Command{ Use: "version", Short: "Show version", Long: "Show version.", Run: runVersion, } func init() { rootCmd.AddCommand(versionCmd) } func runVersion(cmd *cobra.Command, args []string) { fmt.Println(appAboutLong) } ================================================ FILE: app/go.mod ================================================ module github.com/apernet/hysteria/app/v2 go 1.22 toolchain go1.23.2 require ( github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000 github.com/apernet/hysteria/extras/v2 v2.0.0-00010101000000-000000000000 github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad github.com/caddyserver/certmagic v0.17.2 github.com/libdns/cloudflare v0.1.1 github.com/libdns/duckdns v0.2.0 github.com/libdns/gandi v1.0.3 github.com/libdns/godaddy v1.0.3 github.com/libdns/namedotcom v0.3.3 github.com/libdns/vultr v1.0.0 github.com/mdp/qrterminal/v3 v3.1.1 github.com/mholt/acmez v1.0.4 github.com/sagernet/sing v0.3.2 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.9.0 github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/sys v0.23.0 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d // indirect github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect github.com/cloudflare/circl v1.3.9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.6 // indirect github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.1.1 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/miekg/dns v1.1.59 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/refraction-networking/utls v1.6.6 // indirect github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect github.com/vultr/govultr/v3 v3.6.4 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) replace github.com/apernet/hysteria/core/v2 => ../core replace github.com/apernet/hysteria/extras/v2 => ../extras ================================================ FILE: app/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY= github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM= github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d h1:KWRCWISqJOgY9/0hhH8Bevjw/k4tCQ7oJlXLyFv8u9s= github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d/go.mod h1:x0paLlmCzNOUDDQIgmgFWmnpWQIEuH1GNfA6NdgSTuM= github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad h1:QzQ2sKpc9o42HNRR8ukM5uMC/RzR2HgZd/Nvaqol2C0= github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad/go.mod h1:S5IydyLSN/QAfvY+r2GoomPJ6hidtXWm/Ad18sJVssk= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 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/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM= github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU= github.com/libdns/duckdns v0.2.0 h1:vd3pE09G2qTx1Zh1o3LmrivWSByD3Z5FbL7csX5vDgE= github.com/libdns/duckdns v0.2.0/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs= github.com/libdns/gandi v1.0.3 h1:FIvipWOg/O4zi75fPRmtcolRKqI6MgrbpFy2p5KYdUk= github.com/libdns/gandi v1.0.3/go.mod h1:G6dw58Xnji2xX+lb+uZxGbtmfxKllm1CGHE2bOPG3WA= github.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44= github.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM= github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE= github.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s= github.com/libdns/vultr v1.0.0 h1:W8B4+k2bm9ro3bZLSZV9hMOQI+uO6Svu+GmD+Olz7ZI= github.com/libdns/vultr v1.0.0/go.mod h1:8K1HJExcbeHS4YPkFHRZpqpXZzZ+DZAA0m0VikJgEqk= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc= github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU= github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo= github.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vultr/govultr/v3 v3.6.4 h1:unvY9eXlBw667ECQZDbBDOIaWB8wkk6Bx+yB0IMKXJ4= github.com/vultr/govultr/v3 v3.6.4/go.mod h1:rt9v2x114jZmmLAE/h5N5jnxTmsK9ewwS2oQZ0UBQzM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: app/internal/forwarding/tcp.go ================================================ package forwarding import ( "io" "net" "github.com/apernet/hysteria/core/v2/client" ) type TCPTunnel struct { HyClient client.Client Remote string EventLogger TCPEventLogger } type TCPEventLogger interface { Connect(addr net.Addr) Error(addr net.Addr, err error) } func (t *TCPTunnel) Serve(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { return err } go t.handle(conn) } } func (t *TCPTunnel) handle(conn net.Conn) { defer conn.Close() if t.EventLogger != nil { t.EventLogger.Connect(conn.RemoteAddr()) } var closeErr error defer func() { if t.EventLogger != nil { t.EventLogger.Error(conn.RemoteAddr(), closeErr) } }() rc, err := t.HyClient.TCP(t.Remote) if err != nil { closeErr = err return } defer rc.Close() // Start forwarding copyErrChan := make(chan error, 2) go func() { _, copyErr := io.Copy(rc, conn) copyErrChan <- copyErr }() go func() { _, copyErr := io.Copy(conn, rc) copyErrChan <- copyErr }() closeErr = <-copyErrChan } ================================================ FILE: app/internal/forwarding/tcp_test.go ================================================ package forwarding import ( "crypto/rand" "net" "testing" "github.com/stretchr/testify/assert" "github.com/apernet/hysteria/app/v2/internal/utils_test" ) func TestTCPTunnel(t *testing.T) { // Start the tunnel l, err := net.Listen("tcp", "127.0.0.1:34567") assert.NoError(t, err) defer l.Close() tunnel := &TCPTunnel{ HyClient: &utils_test.MockEchoHyClient{}, } go tunnel.Serve(l) for i := 0; i < 10; i++ { conn, err := net.Dial("tcp", "127.0.0.1:34567") assert.NoError(t, err) data := make([]byte, 1024) _, _ = rand.Read(data) _, err = conn.Write(data) assert.NoError(t, err) recv := make([]byte, 1024) _, err = conn.Read(recv) assert.NoError(t, err) assert.Equal(t, data, recv) _ = conn.Close() } } ================================================ FILE: app/internal/forwarding/udp.go ================================================ package forwarding import ( "net" "sync" "sync/atomic" "time" "github.com/apernet/hysteria/core/v2/client" ) const ( udpBufferSize = 4096 defaultTimeout = 60 * time.Second idleCleanupInterval = 1 * time.Second ) type atomicTime struct { v atomic.Value } func newAtomicTime(t time.Time) *atomicTime { a := &atomicTime{} a.Set(t) return a } func (t *atomicTime) Set(new time.Time) { t.v.Store(new) } func (t *atomicTime) Get() time.Time { return t.v.Load().(time.Time) } type sessionEntry struct { HyConn client.HyUDPConn Last *atomicTime Timeout bool // true if the session is closed due to timeout } func (e *sessionEntry) Feed(data []byte, addr string) error { e.Last.Set(time.Now()) return e.HyConn.Send(data, addr) } func (e *sessionEntry) ReceiveLoop(pc net.PacketConn, addr net.Addr) error { for { data, _, err := e.HyConn.Receive() if err != nil { return err } _, err = pc.WriteTo(data, addr) if err != nil { return err } e.Last.Set(time.Now()) } } type UDPTunnel struct { HyClient client.Client Remote string Timeout time.Duration EventLogger UDPEventLogger m map[string]*sessionEntry // addr -> HyConn mutex sync.RWMutex } type UDPEventLogger interface { Connect(addr net.Addr) Error(addr net.Addr, err error) } func (t *UDPTunnel) Serve(pc net.PacketConn) error { t.m = make(map[string]*sessionEntry) stopCh := make(chan struct{}) go t.idleCleanupLoop(stopCh) defer close(stopCh) defer t.cleanup(false) buf := make([]byte, udpBufferSize) for { n, addr, err := pc.ReadFrom(buf) if err != nil { return err } t.feed(pc, addr, buf[:n]) } } func (t *UDPTunnel) idleCleanupLoop(stopCh <-chan struct{}) { ticker := time.NewTicker(idleCleanupInterval) defer ticker.Stop() for { select { case <-ticker.C: t.cleanup(true) case <-stopCh: return } } } func (t *UDPTunnel) cleanup(idleOnly bool) { // We use RLock here as we are only scanning the map, not deleting from it. t.mutex.RLock() defer t.mutex.RUnlock() timeout := t.Timeout if timeout == 0 { timeout = defaultTimeout } now := time.Now() for _, entry := range t.m { if !idleOnly || now.Sub(entry.Last.Get()) > timeout { entry.Timeout = true _ = entry.HyConn.Close() // Closing the connection here will cause the ReceiveLoop to exit, // and the session will be removed from the map there. } } } func (t *UDPTunnel) feed(pc net.PacketConn, addr net.Addr, data []byte) { t.mutex.RLock() entry := t.m[addr.String()] t.mutex.RUnlock() // Create a new session if not exists if entry == nil { if t.EventLogger != nil { t.EventLogger.Connect(addr) } hyConn, err := t.HyClient.UDP() if err != nil { if t.EventLogger != nil { t.EventLogger.Error(addr, err) } return } entry = &sessionEntry{ HyConn: hyConn, Last: newAtomicTime(time.Now()), } // Start the receive loop for this session // Local <- Remote go func() { err := entry.ReceiveLoop(pc, addr) if !entry.Timeout { _ = hyConn.Close() if t.EventLogger != nil { t.EventLogger.Error(addr, err) } } else { // Connection already closed by timeout cleanup, // no need to close again here. // Use nil error to indicate timeout. if t.EventLogger != nil { t.EventLogger.Error(addr, nil) } } // Remove the session from the map t.mutex.Lock() delete(t.m, addr.String()) t.mutex.Unlock() }() // Insert the session into the map t.mutex.Lock() t.m[addr.String()] = entry t.mutex.Unlock() } // Feed the message to the session _ = entry.Feed(data, t.Remote) } ================================================ FILE: app/internal/forwarding/udp_test.go ================================================ package forwarding import ( "crypto/rand" "net" "testing" "github.com/stretchr/testify/assert" "github.com/apernet/hysteria/app/v2/internal/utils_test" ) func TestUDPTunnel(t *testing.T) { // Start the tunnel l, err := net.ListenPacket("udp", "127.0.0.1:34567") assert.NoError(t, err) defer l.Close() tunnel := &UDPTunnel{ HyClient: &utils_test.MockEchoHyClient{}, } go tunnel.Serve(l) for i := 0; i < 10; i++ { conn, err := net.Dial("udp", "127.0.0.1:34567") assert.NoError(t, err) data := make([]byte, 1024) _, _ = rand.Read(data) _, err = conn.Write(data) assert.NoError(t, err) recv := make([]byte, 1024) _, err = conn.Read(recv) assert.NoError(t, err) assert.Equal(t, data, recv) _ = conn.Close() } } ================================================ FILE: app/internal/http/server.go ================================================ package http import ( "bufio" "bytes" "context" "encoding/base64" "fmt" "io" "net" "net/http" "strings" "time" "github.com/apernet/hysteria/core/v2/client" ) const ( httpClientTimeout = 10 * time.Second ) // Server is an HTTP server using a Hysteria client as outbound. type Server struct { HyClient client.Client AuthFunc func(username, password string) bool // nil = no authentication AuthRealm string EventLogger EventLogger httpClient *http.Client } type EventLogger interface { ConnectRequest(addr net.Addr, reqAddr string) ConnectError(addr net.Addr, reqAddr string, err error) HTTPRequest(addr net.Addr, reqURL string) HTTPError(addr net.Addr, reqURL string, err error) } func (s *Server) Serve(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { return err } go s.dispatch(conn) } } func (s *Server) dispatch(conn net.Conn) { bufReader := bufio.NewReader(conn) for { req, err := http.ReadRequest(bufReader) if err != nil { // Connection error or invalid request _ = conn.Close() return } if s.AuthFunc != nil { authOK := false // Check the Proxy-Authorization header pAuth := req.Header.Get("Proxy-Authorization") if strings.HasPrefix(pAuth, "Basic ") { userPass, err := base64.URLEncoding.DecodeString(pAuth[6:]) if err == nil { userPassParts := strings.SplitN(string(userPass), ":", 2) if len(userPassParts) == 2 { authOK = s.AuthFunc(userPassParts[0], userPassParts[1]) } } } if !authOK { // Proxy authentication required _ = sendProxyAuthRequired(conn, req, s.AuthRealm) _ = conn.Close() return } } if req.Method == http.MethodConnect { if bufReader.Buffered() > 0 { // There is still data in the buffered reader. // We need to get it out and put it into a cachedConn, // so that handleConnect can read it. data := make([]byte, bufReader.Buffered()) _, err := io.ReadFull(bufReader, data) if err != nil { // Read from buffer failed, is this possible? _ = conn.Close() return } cachedConn := &cachedConn{ Conn: conn, Buffer: *bytes.NewBuffer(data), } s.handleConnect(cachedConn, req) } else { // No data in the buffered reader, we can just pass the original connection. s.handleConnect(conn, req) } // handleConnect will take over the connection, // i.e. it will not return until the connection is closed. // When it returns, there will be no more requests from this connection, // so we simply exit the loop. return } else { // handleRequest on the other hand handles one request at a time, // and returns when the request is done. It returns a bool indicating // whether the connection should be kept alive, but itself never closes // the connection. keepAlive := s.handleRequest(conn, req) if !keepAlive { _ = conn.Close() return } } } } // cachedConn is a net.Conn wrapper that first Read()s from a buffer, // and then from the underlying net.Conn when the buffer is drained. type cachedConn struct { net.Conn Buffer bytes.Buffer } func (c *cachedConn) Read(b []byte) (int, error) { if c.Buffer.Len() > 0 { n, err := c.Buffer.Read(b) if err == io.EOF { // Buffer is drained, hide it from the caller err = nil } return n, err } return c.Conn.Read(b) } func (s *Server) handleConnect(conn net.Conn, req *http.Request) { defer conn.Close() port := req.URL.Port() if port == "" { // HTTP defaults to port 80 port = "80" } reqAddr := net.JoinHostPort(req.URL.Hostname(), port) // Connect request & error log if s.EventLogger != nil { s.EventLogger.ConnectRequest(conn.RemoteAddr(), reqAddr) } var closeErr error defer func() { if s.EventLogger != nil { s.EventLogger.ConnectError(conn.RemoteAddr(), reqAddr, closeErr) } }() // Dial rConn, err := s.HyClient.TCP(reqAddr) if err != nil { _ = sendSimpleResponse(conn, req, http.StatusBadGateway) closeErr = err return } defer rConn.Close() // Send 200 OK response and start relaying _ = sendSimpleResponse(conn, req, http.StatusOK) copyErrChan := make(chan error, 2) go func() { _, err := io.Copy(rConn, conn) copyErrChan <- err }() go func() { _, err := io.Copy(conn, rConn) copyErrChan <- err }() closeErr = <-copyErrChan } func (s *Server) handleRequest(conn net.Conn, req *http.Request) bool { // Some clients use Connection, some use Proxy-Connection // https://www.oreilly.com/library/view/http-the-definitive/1565925092/re40.html keepAlive := req.ProtoAtLeast(1, 1) && (strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" || strings.ToLower(req.Header.Get("Connection")) == "keep-alive") req.RequestURI = "" // Outgoing request should not have RequestURI removeHopByHopHeaders(req.Header) removeExtraHTTPHostPort(req) if req.URL.Scheme == "" || req.URL.Host == "" { _ = sendSimpleResponse(conn, req, http.StatusBadRequest) return false } // Request & error log if s.EventLogger != nil { s.EventLogger.HTTPRequest(conn.RemoteAddr(), req.URL.String()) } var closeErr error defer func() { if s.EventLogger != nil { s.EventLogger.HTTPError(conn.RemoteAddr(), req.URL.String(), closeErr) } }() if s.httpClient == nil { s.initHTTPClient() } // Do the request and send the response back resp, err := s.httpClient.Do(req) if err != nil { closeErr = err _ = sendSimpleResponse(conn, req, http.StatusBadGateway) return false } removeHopByHopHeaders(resp.Header) if keepAlive { resp.Header.Set("Connection", "keep-alive") resp.Header.Set("Proxy-Connection", "keep-alive") resp.Header.Set("Keep-Alive", "timeout=60") } closeErr = resp.Write(conn) return closeErr == nil && keepAlive } func (s *Server) initHTTPClient() { s.httpClient = &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { // HyClient doesn't support context for now return s.HyClient.TCP(addr) }, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: httpClientTimeout, } } func removeHopByHopHeaders(header http.Header) { header.Del("Proxy-Connection") // Not in RFC but common // https://www.ietf.org/rfc/rfc2616.txt header.Del("Connection") header.Del("Keep-Alive") header.Del("Proxy-Authenticate") header.Del("Proxy-Authorization") header.Del("TE") header.Del("Trailers") header.Del("Transfer-Encoding") header.Del("Upgrade") } func removeExtraHTTPHostPort(req *http.Request) { host := req.Host if host == "" { host = req.URL.Host } if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" { host = pHost } req.Host = host req.URL.Host = host } // sendSimpleResponse sends a simple HTTP response with the given status code. func sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error { resp := &http.Response{ StatusCode: statusCode, Status: http.StatusText(statusCode), Proto: req.Proto, ProtoMajor: req.ProtoMajor, ProtoMinor: req.ProtoMinor, Header: http.Header{}, } // Remove the "Content-Length: 0" header, some clients (e.g. ffmpeg) may not like it. resp.ContentLength = -1 // Also, prevent the "Connection: close" header. resp.Close = false resp.Uncompressed = true return resp.Write(conn) } // sendProxyAuthRequired sends a 407 Proxy Authentication Required response. func sendProxyAuthRequired(conn net.Conn, req *http.Request, realm string) error { resp := &http.Response{ StatusCode: http.StatusProxyAuthRequired, Status: http.StatusText(http.StatusProxyAuthRequired), Proto: req.Proto, ProtoMajor: req.ProtoMajor, ProtoMinor: req.ProtoMinor, Header: http.Header{}, } resp.Header.Set("Proxy-Authenticate", fmt.Sprintf("Basic realm=%q", realm)) return resp.Write(conn) } ================================================ FILE: app/internal/http/server_test.go ================================================ package http import ( "errors" "net" "net/http" "os/exec" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/apernet/hysteria/core/v2/client" ) const ( testCertFile = "test.crt" testKeyFile = "test.key" ) type mockHyClient struct{} func (c *mockHyClient) TCP(addr string) (net.Conn, error) { return net.Dial("tcp", addr) } func (c *mockHyClient) UDP() (client.HyUDPConn, error) { return nil, errors.New("not implemented") } func (c *mockHyClient) Close() error { return nil } func TestServer(t *testing.T) { // Start the server l, err := net.Listen("tcp", "127.0.0.1:18080") assert.NoError(t, err) defer l.Close() s := &Server{ HyClient: &mockHyClient{}, } go s.Serve(l) // Start a test HTTP & HTTPS server http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("control is an illusion")) }) go http.ListenAndServe("127.0.0.1:18081", nil) go http.ListenAndServeTLS("127.0.0.1:18082", testCertFile, testKeyFile, nil) // Run the Python test script cmd := exec.Command("python", "server_test.py") // Suppress HTTPS warning text from Python cmd.Env = append(cmd.Env, "PYTHONWARNINGS=ignore:Unverified HTTPS request") out, err := cmd.CombinedOutput() assert.NoError(t, err) assert.Equal(t, "OK", strings.TrimSpace(string(out))) } ================================================ FILE: app/internal/http/server_test.py ================================================ import requests proxies = { "http": "http://127.0.0.1:18080", "https": "http://127.0.0.1:18080", } def test_http(it): for i in range(it): r = requests.get("http://127.0.0.1:18081", proxies=proxies) assert r.status_code == 200 and r.text == "control is an illusion" def test_https(it): for i in range(it): r = requests.get("https://127.0.0.1:18082", proxies=proxies, verify=False) assert r.status_code == 200 and r.text == "control is an illusion" if __name__ == "__main__": test_http(10) test_https(10) print("OK") ================================================ FILE: app/internal/http/test.crt ================================================ -----BEGIN CERTIFICATE----- MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3 DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4 ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf 1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4 FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7 ydYKuI8= -----END CERTIFICATE----- ================================================ FILE: app/internal/http/test.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR 2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr 6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2 wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+ FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U 64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu 1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0 jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2 ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+ menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2 kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ -----END RSA PRIVATE KEY----- ================================================ FILE: app/internal/proxymux/.mockery.yaml ================================================ with-expecter: true dir: internal/mocks outpkg: mocks packages: net: interfaces: Listener: config: mockname: MockListener Conn: config: mockname: MockConn ================================================ FILE: app/internal/proxymux/internal/mocks/mock_Conn.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import ( net "net" mock "github.com/stretchr/testify/mock" time "time" ) // MockConn is an autogenerated mock type for the Conn type type MockConn struct { mock.Mock } type MockConn_Expecter struct { mock *mock.Mock } func (_m *MockConn) EXPECT() *MockConn_Expecter { return &MockConn_Expecter{mock: &_m.Mock} } // Close provides a mock function with given fields: func (_m *MockConn) Close() error { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type MockConn_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *MockConn_Expecter) Close() *MockConn_Close_Call { return &MockConn_Close_Call{Call: _e.mock.On("Close")} } func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call { _c.Call.Return(run) return _c } // LocalAddr provides a mock function with given fields: func (_m *MockConn) LocalAddr() net.Addr { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for LocalAddr") } var r0 net.Addr if rf, ok := ret.Get(0).(func() net.Addr); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Addr) } } return r0 } // MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr' type MockConn_LocalAddr_Call struct { *mock.Call } // LocalAddr is a helper method to define mock.On call func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call { return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")} } func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call { _c.Call.Return(run) return _c } // Read provides a mock function with given fields: b func (_m *MockConn) Read(b []byte) (int, error) { ret := _m.Called(b) if len(ret) == 0 { panic("no return value specified for Read") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { return rf(b) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(b) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) error); ok { r1 = rf(b) } else { r1 = ret.Error(1) } return r0, r1 } // MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' type MockConn_Read_Call struct { *mock.Call } // Read is a helper method to define mock.On call // - b []byte func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call { return &MockConn_Read_Call{Call: _e.mock.On("Read", b)} } func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call { _c.Call.Return(n, err) return _c } func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call { _c.Call.Return(run) return _c } // RemoteAddr provides a mock function with given fields: func (_m *MockConn) RemoteAddr() net.Addr { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for RemoteAddr") } var r0 net.Addr if rf, ok := ret.Get(0).(func() net.Addr); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Addr) } } return r0 } // MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr' type MockConn_RemoteAddr_Call struct { *mock.Call } // RemoteAddr is a helper method to define mock.On call func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call { return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")} } func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call { _c.Call.Return(run) return _c } // SetDeadline provides a mock function with given fields: t func (_m *MockConn) SetDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline' type MockConn_SetDeadline_Call struct { *mock.Call } // SetDeadline is a helper method to define mock.On call // - t time.Time func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call { return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)} } func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call { _c.Call.Return(run) return _c } // SetReadDeadline provides a mock function with given fields: t func (_m *MockConn) SetReadDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetReadDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' type MockConn_SetReadDeadline_Call struct { *mock.Call } // SetReadDeadline is a helper method to define mock.On call // - t time.Time func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call { return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} } func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call { _c.Call.Return(run) return _c } // SetWriteDeadline provides a mock function with given fields: t func (_m *MockConn) SetWriteDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetWriteDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' type MockConn_SetWriteDeadline_Call struct { *mock.Call } // SetWriteDeadline is a helper method to define mock.On call // - t time.Time func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call { return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} } func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call { _c.Call.Return(run) return _c } // Write provides a mock function with given fields: b func (_m *MockConn) Write(b []byte) (int, error) { ret := _m.Called(b) if len(ret) == 0 { panic("no return value specified for Write") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { return rf(b) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(b) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) error); ok { r1 = rf(b) } else { r1 = ret.Error(1) } return r0, r1 } // MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' type MockConn_Write_Call struct { *mock.Call } // Write is a helper method to define mock.On call // - b []byte func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call { return &MockConn_Write_Call{Call: _e.mock.On("Write", b)} } func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call { _c.Call.Return(n, err) return _c } func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call { _c.Call.Return(run) return _c } // NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockConn(t interface { mock.TestingT Cleanup(func()) }) *MockConn { mock := &MockConn{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: app/internal/proxymux/internal/mocks/mock_Listener.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import ( net "net" mock "github.com/stretchr/testify/mock" ) // MockListener is an autogenerated mock type for the Listener type type MockListener struct { mock.Mock } type MockListener_Expecter struct { mock *mock.Mock } func (_m *MockListener) EXPECT() *MockListener_Expecter { return &MockListener_Expecter{mock: &_m.Mock} } // Accept provides a mock function with given fields: func (_m *MockListener) Accept() (net.Conn, error) { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Accept") } var r0 net.Conn var r1 error if rf, ok := ret.Get(0).(func() (net.Conn, error)); ok { return rf() } if rf, ok := ret.Get(0).(func() net.Conn); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Conn) } } if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { r1 = ret.Error(1) } return r0, r1 } // MockListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept' type MockListener_Accept_Call struct { *mock.Call } // Accept is a helper method to define mock.On call func (_e *MockListener_Expecter) Accept() *MockListener_Accept_Call { return &MockListener_Accept_Call{Call: _e.mock.On("Accept")} } func (_c *MockListener_Accept_Call) Run(run func()) *MockListener_Accept_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockListener_Accept_Call) Return(_a0 net.Conn, _a1 error) *MockListener_Accept_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *MockListener_Accept_Call) RunAndReturn(run func() (net.Conn, error)) *MockListener_Accept_Call { _c.Call.Return(run) return _c } // Addr provides a mock function with given fields: func (_m *MockListener) Addr() net.Addr { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Addr") } var r0 net.Addr if rf, ok := ret.Get(0).(func() net.Addr); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Addr) } } return r0 } // MockListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr' type MockListener_Addr_Call struct { *mock.Call } // Addr is a helper method to define mock.On call func (_e *MockListener_Expecter) Addr() *MockListener_Addr_Call { return &MockListener_Addr_Call{Call: _e.mock.On("Addr")} } func (_c *MockListener_Addr_Call) Run(run func()) *MockListener_Addr_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockListener_Addr_Call) Return(_a0 net.Addr) *MockListener_Addr_Call { _c.Call.Return(_a0) return _c } func (_c *MockListener_Addr_Call) RunAndReturn(run func() net.Addr) *MockListener_Addr_Call { _c.Call.Return(run) return _c } // Close provides a mock function with given fields: func (_m *MockListener) Close() error { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // MockListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type MockListener_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *MockListener_Expecter) Close() *MockListener_Close_Call { return &MockListener_Close_Call{Call: _e.mock.On("Close")} } func (_c *MockListener_Close_Call) Run(run func()) *MockListener_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockListener_Close_Call) Return(_a0 error) *MockListener_Close_Call { _c.Call.Return(_a0) return _c } func (_c *MockListener_Close_Call) RunAndReturn(run func() error) *MockListener_Close_Call { _c.Call.Return(run) return _c } // NewMockListener creates a new instance of MockListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockListener(t interface { mock.TestingT Cleanup(func()) }) *MockListener { mock := &MockListener{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: app/internal/proxymux/manager.go ================================================ package proxymux import ( "net" "sync" "github.com/apernet/hysteria/extras/v2/correctnet" ) type muxManager struct { listeners map[string]*muxListener lock sync.Mutex } var globalMuxManager *muxManager func init() { globalMuxManager = &muxManager{ listeners: make(map[string]*muxListener), } } func (m *muxManager) GetOrCreate(address string) (*muxListener, error) { key, err := m.canonicalizeAddrPort(address) if err != nil { return nil, err } m.lock.Lock() defer m.lock.Unlock() if ml, ok := m.listeners[key]; ok { return ml, nil } listener, err := correctnet.Listen("tcp", key) if err != nil { return nil, err } ml := newMuxListener(listener, func() { m.lock.Lock() defer m.lock.Unlock() delete(m.listeners, key) }) m.listeners[key] = ml return ml, nil } func (m *muxManager) canonicalizeAddrPort(address string) (string, error) { taddr, err := net.ResolveTCPAddr("tcp", address) if err != nil { return "", err } return taddr.String(), nil } func ListenHTTP(address string) (net.Listener, error) { ml, err := globalMuxManager.GetOrCreate(address) if err != nil { return nil, err } return ml.ListenHTTP() } func ListenSOCKS(address string) (net.Listener, error) { ml, err := globalMuxManager.GetOrCreate(address) if err != nil { return nil, err } return ml.ListenSOCKS() } ================================================ FILE: app/internal/proxymux/manager_test.go ================================================ package proxymux import ( "net" "testing" "time" "github.com/stretchr/testify/assert" ) func TestListenSOCKS(t *testing.T) { address := "127.2.39.129:11081" sl, err := ListenSOCKS(address) if !assert.NoError(t, err) { return } defer func() { sl.Close() }() hl, err := ListenHTTP(address) if !assert.NoError(t, err) { return } defer hl.Close() _, err = ListenSOCKS(address) if !assert.ErrorIs(t, err, ErrProtocolInUse) { return } sl.Close() sl, err = ListenSOCKS(address) if !assert.NoError(t, err) { return } } func TestListenHTTP(t *testing.T) { address := "127.2.39.129:11082" hl, err := ListenHTTP(address) if !assert.NoError(t, err) { return } defer func() { hl.Close() }() sl, err := ListenSOCKS(address) if !assert.NoError(t, err) { return } defer sl.Close() _, err = ListenHTTP(address) if !assert.ErrorIs(t, err, ErrProtocolInUse) { return } hl.Close() hl, err = ListenHTTP(address) if !assert.NoError(t, err) { return } } func TestRelease(t *testing.T) { address := "127.2.39.129:11083" hl, err := ListenHTTP(address) if !assert.NoError(t, err) { return } sl, err := ListenSOCKS(address) if !assert.NoError(t, err) { return } if !assert.True(t, globalMuxManager.testAddressExists(address)) { return } _, err = net.Listen("tcp", address) if !assert.Error(t, err) { return } hl.Close() sl.Close() // Wait for muxListener released time.Sleep(time.Second) if !assert.False(t, globalMuxManager.testAddressExists(address)) { return } lis, err := net.Listen("tcp", address) if !assert.NoError(t, err) { return } defer lis.Close() } func (m *muxManager) testAddressExists(address string) bool { m.lock.Lock() defer m.lock.Unlock() _, ok := m.listeners[address] return ok } ================================================ FILE: app/internal/proxymux/mux.go ================================================ package proxymux import ( "errors" "fmt" "io" "net" "sync" ) func newMuxListener(listener net.Listener, deleteFunc func()) *muxListener { l := &muxListener{ base: listener, acceptChan: make(chan net.Conn), closeChan: make(chan struct{}), deleteFunc: deleteFunc, } go l.acceptLoop() go l.mainLoop() return l } type muxListener struct { lock sync.Mutex base net.Listener acceptErr error acceptChan chan net.Conn closeChan chan struct{} socksListener *subListener httpListener *subListener deleteFunc func() } func (l *muxListener) acceptLoop() { defer close(l.acceptChan) for { conn, err := l.base.Accept() if err != nil { l.lock.Lock() l.acceptErr = err l.lock.Unlock() return } select { case <-l.closeChan: return case l.acceptChan <- conn: } } } func (l *muxListener) mainLoop() { defer func() { l.deleteFunc() l.base.Close() close(l.closeChan) l.lock.Lock() defer l.lock.Unlock() if sl := l.httpListener; sl != nil { close(sl.acceptChan) l.httpListener = nil } if sl := l.socksListener; sl != nil { close(sl.acceptChan) l.socksListener = nil } }() for { var socksCloseChan, httpCloseChan chan struct{} if l.httpListener != nil { httpCloseChan = l.httpListener.closeChan } if l.socksListener != nil { socksCloseChan = l.socksListener.closeChan } select { case <-l.closeChan: return case conn, ok := <-l.acceptChan: if !ok { return } go l.dispatch(conn) case <-socksCloseChan: l.lock.Lock() if socksCloseChan == l.socksListener.closeChan { // not replaced by another ListenSOCKS() l.socksListener = nil } l.lock.Unlock() if l.checkIdle() { return } case <-httpCloseChan: l.lock.Lock() if httpCloseChan == l.httpListener.closeChan { // not replaced by another ListenHTTP() l.httpListener = nil } l.lock.Unlock() if l.checkIdle() { return } } } } func (l *muxListener) dispatch(conn net.Conn) { var b [1]byte if _, err := io.ReadFull(conn, b[:]); err != nil { conn.Close() return } l.lock.Lock() var target *subListener if b[0] == 5 { target = l.socksListener } else { target = l.httpListener } l.lock.Unlock() if target == nil { conn.Close() return } wconn := &connWithOneByte{Conn: conn, b: b[0]} select { case <-target.closeChan: case target.acceptChan <- wconn: } } func (l *muxListener) checkIdle() bool { l.lock.Lock() defer l.lock.Unlock() return l.httpListener == nil && l.socksListener == nil } func (l *muxListener) getAndClearAcceptError() error { l.lock.Lock() defer l.lock.Unlock() if l.acceptErr == nil { return nil } err := l.acceptErr l.acceptErr = nil return err } func (l *muxListener) ListenHTTP() (net.Listener, error) { l.lock.Lock() defer l.lock.Unlock() if l.httpListener != nil { subListenerPendingClosed := false select { case <-l.httpListener.closeChan: subListenerPendingClosed = true default: } if !subListenerPendingClosed { return nil, OpErr{ Addr: l.base.Addr(), Protocol: "http", Op: "bind-protocol", Err: ErrProtocolInUse, } } l.httpListener = nil } select { case <-l.closeChan: return nil, net.ErrClosed default: } sl := newSubListener(l.getAndClearAcceptError, l.base.Addr) l.httpListener = sl return sl, nil } func (l *muxListener) ListenSOCKS() (net.Listener, error) { l.lock.Lock() defer l.lock.Unlock() if l.socksListener != nil { subListenerPendingClosed := false select { case <-l.socksListener.closeChan: subListenerPendingClosed = true default: } if !subListenerPendingClosed { return nil, OpErr{ Addr: l.base.Addr(), Protocol: "socks", Op: "bind-protocol", Err: ErrProtocolInUse, } } l.socksListener = nil } select { case <-l.closeChan: return nil, net.ErrClosed default: } sl := newSubListener(l.getAndClearAcceptError, l.base.Addr) l.socksListener = sl return sl, nil } func newSubListener(acceptErrorFunc func() error, addrFunc func() net.Addr) *subListener { return &subListener{ acceptChan: make(chan net.Conn), acceptErrorFunc: acceptErrorFunc, closeChan: make(chan struct{}), addrFunc: addrFunc, } } type subListener struct { // receive connections or closure from upstream acceptChan chan net.Conn // get an error of Accept() from upstream acceptErrorFunc func() error // notify upstream that we are closed closeChan chan struct{} // Listener.Addr() implementation of base listener addrFunc func() net.Addr } func (l *subListener) Accept() (net.Conn, error) { select { case <-l.closeChan: // closed by ourselves return nil, net.ErrClosed case conn, ok := <-l.acceptChan: if !ok { // closed by upstream if acceptErr := l.acceptErrorFunc(); acceptErr != nil { return nil, acceptErr } return nil, net.ErrClosed } return conn, nil } } func (l *subListener) Addr() net.Addr { return l.addrFunc() } // Close implements net.Listener.Close. // Upstream should use close(l.acceptChan) instead. func (l *subListener) Close() error { select { case <-l.closeChan: return nil default: } close(l.closeChan) return nil } // connWithOneByte is a net.Conn that returns b for the first read // request, then forwards everything else to Conn. type connWithOneByte struct { net.Conn b byte bRead bool } func (c *connWithOneByte) Read(bs []byte) (int, error) { if c.bRead { return c.Conn.Read(bs) } if len(bs) == 0 { return 0, nil } c.bRead = true bs[0] = c.b return 1, nil } type OpErr struct { Addr net.Addr Protocol string Op string Err error } func (m OpErr) Error() string { return fmt.Sprintf("mux-listen: %s[%s]: %s: %v", m.Addr, m.Protocol, m.Op, m.Err) } func (m OpErr) Unwrap() error { return m.Err } var ErrProtocolInUse = errors.New("protocol already in use") ================================================ FILE: app/internal/proxymux/mux_test.go ================================================ package proxymux import ( "bytes" "net" "sync" "testing" "time" "github.com/apernet/hysteria/app/v2/internal/proxymux/internal/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) //go:generate mockery func testMockListener(t *testing.T, connChan <-chan net.Conn) net.Listener { closedChan := make(chan struct{}) mockListener := mocks.NewMockListener(t) mockListener.EXPECT().Accept().RunAndReturn(func() (net.Conn, error) { select { case <-closedChan: return nil, net.ErrClosed case conn, ok := <-connChan: if !ok { panic("unexpected closed channel (connChan)") } return conn, nil } }) mockListener.EXPECT().Close().RunAndReturn(func() error { select { case <-closedChan: default: close(closedChan) } return nil }) return mockListener } func testMockConn(t *testing.T, b []byte) net.Conn { buf := bytes.NewReader(b) isClosed := false mockConn := mocks.NewMockConn(t) mockConn.EXPECT().Read(mock.Anything).RunAndReturn(func(b []byte) (int, error) { if isClosed { return 0, net.ErrClosed } return buf.Read(b) }) mockConn.EXPECT().Close().RunAndReturn(func() error { isClosed = true return nil }) return mockConn } func TestMuxHTTP(t *testing.T) { connChan := make(chan net.Conn) mockListener := testMockListener(t, connChan) mockConn := testMockConn(t, []byte("CONNECT example.com:443 HTTP/1.1\r\n\r\n")) mux := newMuxListener(mockListener, func() {}) hl, err := mux.ListenHTTP() if !assert.NoError(t, err) { return } sl, err := mux.ListenSOCKS() if !assert.NoError(t, err) { return } connChan <- mockConn var socksConn, httpConn net.Conn var socksErr, httpErr error var wg sync.WaitGroup wg.Add(2) go func() { socksConn, socksErr = sl.Accept() wg.Done() }() go func() { httpConn, httpErr = hl.Accept() wg.Done() }() time.Sleep(time.Second) sl.Close() hl.Close() wg.Wait() assert.Nil(t, socksConn) assert.ErrorIs(t, socksErr, net.ErrClosed) assert.NotNil(t, httpConn) httpConn.Close() assert.NoError(t, httpErr) // Wait for muxListener released <-mux.acceptChan } func TestMuxSOCKS(t *testing.T) { connChan := make(chan net.Conn) mockListener := testMockListener(t, connChan) mockConn := testMockConn(t, []byte{0x05, 0x02, 0x00, 0x01}) // SOCKS5 Connect Request: NOAUTH+GSSAPI mux := newMuxListener(mockListener, func() {}) hl, err := mux.ListenHTTP() if !assert.NoError(t, err) { return } sl, err := mux.ListenSOCKS() if !assert.NoError(t, err) { return } connChan <- mockConn var socksConn, httpConn net.Conn var socksErr, httpErr error var wg sync.WaitGroup wg.Add(2) go func() { socksConn, socksErr = sl.Accept() wg.Done() }() go func() { httpConn, httpErr = hl.Accept() wg.Done() }() time.Sleep(time.Second) sl.Close() hl.Close() wg.Wait() assert.NotNil(t, socksConn) socksConn.Close() assert.NoError(t, socksErr) assert.Nil(t, httpConn) assert.ErrorIs(t, httpErr, net.ErrClosed) // Wait for muxListener released <-mux.acceptChan } ================================================ FILE: app/internal/redirect/getsockopt_linux.go ================================================ //go:build !386 // +build !386 package redirect import ( "syscall" "unsafe" ) func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { _, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) if e != 0 { err = e } return } ================================================ FILE: app/internal/redirect/getsockopt_linux_386.go ================================================ package redirect import ( "syscall" "unsafe" ) const ( sysGetsockopt = 15 ) // On 386 we cannot call socketcall with syscall.Syscall6, as it always fails with EFAULT. // Use our own syscall.socketcall hack instead. func syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { _, e := syscall_socketcall(sysGetsockopt, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) if e != 0 { err = e } return } ================================================ FILE: app/internal/redirect/syscall_socketcall_linux_386.s ================================================ //go:build gc // +build gc #include "textflag.h" TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36 JMP syscall·socketcall(SB) ================================================ FILE: app/internal/redirect/tcp_linux.go ================================================ package redirect import ( "encoding/binary" "errors" "io" "net" "syscall" "unsafe" "github.com/apernet/hysteria/core/v2/client" ) const ( soOriginalDst = 80 soOriginalDstV6 = 80 ) type TCPRedirect struct { HyClient client.Client EventLogger TCPEventLogger } type TCPEventLogger interface { Connect(addr, reqAddr net.Addr) Error(addr, reqAddr net.Addr, err error) } func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error { listener, err := net.ListenTCP("tcp", laddr) if err != nil { return err } defer listener.Close() for { c, err := listener.AcceptTCP() if err != nil { return err } go r.handle(c) } } func (r *TCPRedirect) handle(conn *net.TCPConn) { defer conn.Close() dstAddr, err := getDstAddr(conn) if err != nil { // Fail silently if we can't get the original destination. // Maybe we should print something to the log? return } if r.EventLogger != nil { r.EventLogger.Connect(conn.RemoteAddr(), dstAddr) } var closeErr error defer func() { if r.EventLogger != nil { r.EventLogger.Error(conn.RemoteAddr(), dstAddr, closeErr) } }() rc, err := r.HyClient.TCP(dstAddr.String()) if err != nil { closeErr = err return } defer rc.Close() // Start forwarding copyErrChan := make(chan error, 2) go func() { _, copyErr := io.Copy(rc, conn) copyErrChan <- copyErr }() go func() { _, copyErr := io.Copy(conn, rc) copyErrChan <- copyErr }() closeErr = <-copyErrChan } type sockAddr struct { family uint16 port [2]byte // always big endian regardless of platform data [24]byte // sockaddr_in or sockaddr_in6 } func getOriginalDst(fd uintptr) (*sockAddr, error) { var addr sockAddr addrSize := uint32(unsafe.Sizeof(addr)) // Try IPv6 first err := getsockopt(fd, syscall.SOL_IPV6, soOriginalDstV6, unsafe.Pointer(&addr), &addrSize) if err == nil { return &addr, nil } // Then IPv4 err = getsockopt(fd, syscall.SOL_IP, soOriginalDst, unsafe.Pointer(&addr), &addrSize) return &addr, err } // getDstAddr returns the original destination of a redirected TCP connection. func getDstAddr(conn *net.TCPConn) (*net.TCPAddr, error) { rc, err := conn.SyscallConn() if err != nil { return nil, err } var addr *sockAddr var err2 error err = rc.Control(func(fd uintptr) { addr, err2 = getOriginalDst(fd) }) if err != nil { return nil, err } if err2 != nil { return nil, err2 } switch addr.family { case syscall.AF_INET: return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil case syscall.AF_INET6: return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil default: return nil, errors.New("address family not IPv4 or IPv6") } } ================================================ FILE: app/internal/redirect/tcp_others.go ================================================ //go:build !linux package redirect import ( "errors" "net" "github.com/apernet/hysteria/core/v2/client" ) type TCPRedirect struct { HyClient client.Client EventLogger TCPEventLogger } type TCPEventLogger interface { Connect(addr, reqAddr net.Addr) Error(addr, reqAddr net.Addr, err error) } func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error { return errors.New("not supported on this platform") } ================================================ FILE: app/internal/sockopts/fd_control_unix_socket_test.py ================================================ import socket import array import os import struct import sys def serve(path): try: os.unlink(path) except OSError: if os.path.exists(path): raise server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.bind(path) server.listen() print(f"Listening on {path}") try: while True: connection, client_address = server.accept() print(f"Client connected") try: # Receiving fd from client fds = array.array("i") msg, ancdata, flags, addr = connection.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i'))) for cmsg_level, cmsg_type, cmsg_data in ancdata: if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS: fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) fd = fds[0] # We make a call to setsockopt(2) here, so client can verify we have received the fd # In the real scenario, the server would set things like SO_MARK, # we use SO_RCVBUF as it doesn't require any special capabilities. nbytes = struct.pack("i", 2500) fdsocket = fd_to_socket(fd) fdsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, nbytes) fdsocket.close() # The only protocol-like thing specified in the client implementation. connection.send(b'\x01') finally: connection.close() print("Connection closed") except KeyboardInterrupt: print("Exit") finally: server.close() os.unlink(path) def fd_to_socket(fd): return socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) if __name__ == "__main__": if len(sys.argv) < 2: raise ValueError("unix socket path is required") serve(sys.argv[1]) ================================================ FILE: app/internal/sockopts/sockopts.go ================================================ package sockopts import ( "fmt" "net" ) type SocketOptions struct { BindInterface *string FirewallMark *uint32 FdControlUnixSocket *string } // implemented in platform-specific files var ( bindInterfaceFunc func(c *net.UDPConn, device string) error firewallMarkFunc func(c *net.UDPConn, fwmark uint32) error fdControlUnixSocketFunc func(c *net.UDPConn, path string) error ) func (o *SocketOptions) CheckSupported() (err error) { if o.BindInterface != nil && bindInterfaceFunc == nil { return &UnsupportedError{"bindInterface"} } if o.FirewallMark != nil && firewallMarkFunc == nil { return &UnsupportedError{"fwmark"} } if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc == nil { return &UnsupportedError{"fdControlUnixSocket"} } return nil } type UnsupportedError struct { Field string } func (e *UnsupportedError) Error() string { return fmt.Sprintf("%s is not supported on this platform", e.Field) } func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) { uconn, err = net.ListenUDP("udp", nil) if err != nil { return } err = o.applyToUDPConn(uconn.(*net.UDPConn)) if err != nil { uconn.Close() uconn = nil return } return } func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error { if o.BindInterface != nil && bindInterfaceFunc != nil { err := bindInterfaceFunc(c, *o.BindInterface) if err != nil { return fmt.Errorf("failed to bind to interface: %w", err) } } if o.FirewallMark != nil && firewallMarkFunc != nil { err := firewallMarkFunc(c, *o.FirewallMark) if err != nil { return fmt.Errorf("failed to set fwmark: %w", err) } } if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil { err := fdControlUnixSocketFunc(c, *o.FdControlUnixSocket) if err != nil { return fmt.Errorf("failed to send fd to control unix socket: %w", err) } } return nil } ================================================ FILE: app/internal/sockopts/sockopts_linux.go ================================================ //go:build linux package sockopts import ( "fmt" "net" "time" "golang.org/x/exp/constraints" "golang.org/x/sys/unix" ) const ( fdControlUnixTimeout = 3 * time.Second ) func init() { bindInterfaceFunc = bindInterfaceImpl firewallMarkFunc = firewallMarkImpl fdControlUnixSocketFunc = fdControlUnixSocketImpl } func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) { rconn, err := c.SyscallConn() if err != nil { return } cerr := rconn.Control(func(fd uintptr) { err = cb(int(fd)) }) if err != nil { return } if cerr != nil { err = fmt.Errorf("failed to control fd: %w", cerr) return } return } func bindInterfaceImpl(c *net.UDPConn, device string) error { return controlUDPConn(c, func(fd int) error { return unix.BindToDevice(fd, device) }) } func firewallMarkImpl(c *net.UDPConn, fwmark uint32) error { return controlUDPConn(c, func(fd int) error { return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, int(fwmark)) }) } func fdControlUnixSocketImpl(c *net.UDPConn, path string) error { return controlUDPConn(c, func(fd int) error { socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0) if err != nil { return fmt.Errorf("failed to create unix socket: %w", err) } defer unix.Close(socketFd) var timeout unix.Timeval timeUsec := fdControlUnixTimeout.Microseconds() castAssignInteger(timeUsec/1e6, &timeout.Sec) // Specifying the type explicitly is not necessary here, but it makes GoLand happy. castAssignInteger[int64](timeUsec%1e6, &timeout.Usec) _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout) _ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout) err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path}) if err != nil { return fmt.Errorf("failed to connect: %w", err) } err = unix.Sendmsg(socketFd, nil, unix.UnixRights(fd), nil, 0) if err != nil { return fmt.Errorf("failed to send: %w", err) } dummy := []byte{1} n, err := unix.Read(socketFd, dummy) if err != nil { return fmt.Errorf("failed to receive: %w", err) } if n != 1 { return fmt.Errorf("socket closed unexpectedly") } return nil }) } func castAssignInteger[F, T constraints.Integer](from F, to *T) { *to = T(from) } ================================================ FILE: app/internal/sockopts/sockopts_linux_test.go ================================================ //go:build linux package sockopts import ( "net" "os" "os/exec" "testing" "time" "github.com/stretchr/testify/assert" "golang.org/x/sys/unix" ) func Test_fdControlUnixSocketImpl(t *testing.T) { sockPath := "./fd_control_unix_socket_test.sock" defer os.Remove(sockPath) // Run test server cmd := exec.Command("python", "fd_control_unix_socket_test.py", sockPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Start() if !assert.NoError(t, err) { return } defer cmd.Process.Kill() // Wait for the server to start time.Sleep(1 * time.Second) so := SocketOptions{ FdControlUnixSocket: &sockPath, } conn, err := so.ListenUDP() if !assert.NoError(t, err) { return } defer conn.Close() err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) { rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF) if err != nil { return } // The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500), // and kernel will double this value for getsockopt(). assert.Equal(t, 5000, rcvbuf) return }) assert.NoError(t, err) } ================================================ FILE: app/internal/socks5/server.go ================================================ package socks5 import ( "encoding/binary" "io" "net" "github.com/txthinking/socks5" "github.com/apernet/hysteria/core/v2/client" ) const udpBufferSize = 4096 // Server is a SOCKS5 server using a Hysteria client as outbound. type Server struct { HyClient client.Client AuthFunc func(username, password string) bool // nil = no authentication DisableUDP bool EventLogger EventLogger } type EventLogger interface { TCPRequest(addr net.Addr, reqAddr string) TCPError(addr net.Addr, reqAddr string, err error) UDPRequest(addr net.Addr) UDPError(addr net.Addr, err error) } func (s *Server) Serve(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { return err } go s.dispatch(conn) } } func (s *Server) dispatch(conn net.Conn) { ok, _ := s.negotiate(conn) if !ok { _ = conn.Close() return } // Negotiation ok, get and handle the request req, err := socks5.NewRequestFrom(conn) if err != nil { _ = conn.Close() return } switch req.Cmd { case socks5.CmdConnect: // TCP s.handleTCP(conn, req) case socks5.CmdUDP: // UDP if s.DisableUDP { _ = sendSimpleReply(conn, socks5.RepCommandNotSupported) _ = conn.Close() return } s.handleUDP(conn, req) default: _ = sendSimpleReply(conn, socks5.RepCommandNotSupported) _ = conn.Close() } } func (s *Server) negotiate(conn net.Conn) (bool, error) { req, err := socks5.NewNegotiationRequestFrom(conn) if err != nil { return false, err } var serverMethod byte if s.AuthFunc != nil { serverMethod = socks5.MethodUsernamePassword } else { serverMethod = socks5.MethodNone } // Look for the supported method in the client request supported := false for _, m := range req.Methods { if m == serverMethod { supported = true break } } if !supported { // No supported method found, reject the client rep := socks5.NewNegotiationReply(socks5.MethodUnsupportAll) _, err := rep.WriteTo(conn) return false, err } // OK, send the method we chose rep := socks5.NewNegotiationReply(serverMethod) _, err = rep.WriteTo(conn) if err != nil { return false, err } // If we chose the username/password method, authenticate the client if serverMethod == socks5.MethodUsernamePassword { req, err := socks5.NewUserPassNegotiationRequestFrom(conn) if err != nil { return false, err } ok := s.AuthFunc(string(req.Uname), string(req.Passwd)) if ok { rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess) _, err := rep.WriteTo(conn) if err != nil { return false, err } } else { rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure) _, err := rep.WriteTo(conn) return false, err } } return true, nil } func (s *Server) handleTCP(conn net.Conn, req *socks5.Request) { defer conn.Close() addr := req.Address() // TCP request & error log if s.EventLogger != nil { s.EventLogger.TCPRequest(conn.RemoteAddr(), addr) } var closeErr error defer func() { if s.EventLogger != nil { s.EventLogger.TCPError(conn.RemoteAddr(), addr, closeErr) } }() // Dial rConn, err := s.HyClient.TCP(addr) if err != nil { _ = sendSimpleReply(conn, socks5.RepHostUnreachable) closeErr = err return } defer rConn.Close() // Send reply and start relaying _ = sendSimpleReply(conn, socks5.RepSuccess) copyErrChan := make(chan error, 2) go func() { _, err := io.Copy(rConn, conn) copyErrChan <- err }() go func() { _, err := io.Copy(conn, rConn) copyErrChan <- err }() closeErr = <-copyErrChan } func (s *Server) handleUDP(conn net.Conn, req *socks5.Request) { defer conn.Close() // UDP request & error log if s.EventLogger != nil { s.EventLogger.UDPRequest(conn.RemoteAddr()) } var closeErr error defer func() { if s.EventLogger != nil { s.EventLogger.UDPError(conn.RemoteAddr(), closeErr) } }() // Start UDP relay server // SOCKS5 UDP requires the server to return the UDP bind address and port in the reply. // We bind to the same address that our TCP server listens on (but a different port). host, _, err := net.SplitHostPort(conn.LocalAddr().String()) if err != nil { // Is this even possible? _ = sendSimpleReply(conn, socks5.RepServerFailure) closeErr = err return } udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, "0")) if err != nil { _ = sendSimpleReply(conn, socks5.RepServerFailure) closeErr = err return } udpConn, err := net.ListenUDP("udp", udpAddr) if err != nil { _ = sendSimpleReply(conn, socks5.RepServerFailure) closeErr = err return } defer udpConn.Close() // HyClient UDP session hyUDP, err := s.HyClient.UDP() if err != nil { _ = sendSimpleReply(conn, socks5.RepServerFailure) closeErr = err return } defer hyUDP.Close() // Send reply _ = sendUDPReply(conn, udpConn.LocalAddr().(*net.UDPAddr)) // UDP relay & SOCKS5 connection holder errChan := make(chan error, 2) go func() { err := s.udpServer(udpConn, hyUDP) errChan <- err }() go func() { _, err := io.Copy(io.Discard, conn) errChan <- err }() closeErr = <-errChan } func (s *Server) udpServer(udpConn *net.UDPConn, hyUDP client.HyUDPConn) error { var clientAddr *net.UDPAddr buf := make([]byte, udpBufferSize) // local -> remote for { n, cAddr, err := udpConn.ReadFromUDP(buf) if err != nil { return err } d, err := socks5.NewDatagramFromBytes(buf[:n]) if err != nil || d.Frag != 0 { // Ignore bad packets // Also we don't support SOCKS5 UDP fragmentation for now continue } if clientAddr == nil { // Before the first packet, we don't know what IP the client will use to send us packets, // so we don't know what IP to return packets to. // We treat whoever sends us the first packet as our client. clientAddr = cAddr // Now that we know the client's address, we can start the // remote -> local direction. go func() { for { bs, from, err := hyUDP.Receive() if err != nil { // Close the UDP conn so that the local -> remote direction will exit _ = udpConn.Close() return } atyp, addr, port, err := socks5.ParseAddress(from) if err != nil { continue } if atyp == socks5.ATYPDomain { // socks5.ParseAddress adds a leading byte for domains, // but socks5.NewDatagram will add it again as it expects a raw domain. // So we must remove it here. addr = addr[1:] } d := socks5.NewDatagram(atyp, addr, port, bs) _, _ = udpConn.WriteToUDP(d.Bytes(), clientAddr) } }() } else if !clientAddr.IP.Equal(cAddr.IP) || clientAddr.Port != cAddr.Port { // Not our client, ignore continue } // Send to remote _ = hyUDP.Send(d.Data, d.Address()) } } // sendSimpleReply sends a SOCKS5 reply with the given reply code. // It does not contain bind address or port, so it's not suitable for successful UDP requests. func sendSimpleReply(conn net.Conn, rep byte) error { p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00}) _, err := p.WriteTo(conn) return err } // sendUDPReply sends a SOCKS5 reply with the given reply code and bind address/port. func sendUDPReply(conn net.Conn, addr *net.UDPAddr) error { var atyp byte var bndAddr, bndPort []byte if ip4 := addr.IP.To4(); ip4 != nil { atyp = socks5.ATYPIPv4 bndAddr = ip4 } else { atyp = socks5.ATYPIPv6 bndAddr = addr.IP } bndPort = make([]byte, 2) binary.BigEndian.PutUint16(bndPort, uint16(addr.Port)) p := socks5.NewReply(socks5.RepSuccess, atyp, bndAddr, bndPort) _, err := p.WriteTo(conn) return err } ================================================ FILE: app/internal/socks5/server_test.go ================================================ package socks5 import ( "net" "os/exec" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/apernet/hysteria/app/v2/internal/utils_test" ) func TestServer(t *testing.T) { // Start the server l, err := net.Listen("tcp", "127.0.0.1:11080") assert.NoError(t, err) defer l.Close() s := &Server{ HyClient: &utils_test.MockEchoHyClient{}, } go s.Serve(l) // Run the Python test script cmd := exec.Command("python", "server_test.py") out, err := cmd.CombinedOutput() assert.NoError(t, err) assert.Equal(t, "OK", strings.TrimSpace(string(out))) } ================================================ FILE: app/internal/socks5/server_test.py ================================================ import socket import socks import os ADDR = "127.0.0.1" PORT = 11080 def test_tcp(size, count, it, domain=False): for i in range(it): s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) s.set_proxy(socks.SOCKS5, ADDR, PORT) if domain: s.connect(("test.tcp.com", 12345)) else: s.connect(("1.2.3.4", 12345)) for j in range(count): payload = os.urandom(size) s.send(payload) rsp = s.recv(size) assert rsp == payload s.close() def test_udp(size, count, it, domain=False): for i in range(it): s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM) s.set_proxy(socks.SOCKS5, ADDR, PORT) for j in range(count): payload = os.urandom(size) if domain: s.sendto(payload, ("test.udp.com", 12345)) else: s.sendto(payload, ("1.2.3.4", 12345)) rsp, addr = s.recvfrom(size) assert rsp == payload if domain: assert addr == (b"test.udp.com", 12345) else: assert addr == ("1.2.3.4", 12345) s.close() if __name__ == "__main__": test_tcp(1024, 1024, 10, domain=False) test_tcp(1024, 1024, 10, domain=True) test_udp(1024, 1024, 10, domain=False) test_udp(1024, 1024, 10, domain=True) print("OK") ================================================ FILE: app/internal/tproxy/tcp_linux.go ================================================ package tproxy import ( "io" "net" "github.com/apernet/go-tproxy" "github.com/apernet/hysteria/core/v2/client" ) type TCPTProxy struct { HyClient client.Client EventLogger TCPEventLogger } type TCPEventLogger interface { Connect(addr, reqAddr net.Addr) Error(addr, reqAddr net.Addr, err error) } func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error { listener, err := tproxy.ListenTCP("tcp", laddr) if err != nil { return err } defer listener.Close() for { c, err := listener.Accept() if err != nil { return err } go r.handle(c) } } func (r *TCPTProxy) handle(conn net.Conn) { defer conn.Close() // In TProxy mode, we are masquerading as the remote server. // So LocalAddr is actually the target the user is trying to connect to, // and RemoteAddr is the local address. if r.EventLogger != nil { r.EventLogger.Connect(conn.RemoteAddr(), conn.LocalAddr()) } var closeErr error defer func() { if r.EventLogger != nil { r.EventLogger.Error(conn.RemoteAddr(), conn.LocalAddr(), closeErr) } }() rc, err := r.HyClient.TCP(conn.LocalAddr().String()) if err != nil { closeErr = err return } defer rc.Close() // Start forwarding copyErrChan := make(chan error, 2) go func() { _, copyErr := io.Copy(rc, conn) copyErrChan <- copyErr }() go func() { _, copyErr := io.Copy(conn, rc) copyErrChan <- copyErr }() closeErr = <-copyErrChan } ================================================ FILE: app/internal/tproxy/tcp_others.go ================================================ //go:build !linux package tproxy import ( "errors" "net" "github.com/apernet/hysteria/core/v2/client" ) type TCPTProxy struct { HyClient client.Client EventLogger TCPEventLogger } type TCPEventLogger interface { Connect(addr, reqAddr net.Addr) Error(addr, reqAddr net.Addr, err error) } func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error { return errors.New("not supported on this platform") } ================================================ FILE: app/internal/tproxy/udp_linux.go ================================================ package tproxy import ( "errors" "net" "time" "github.com/apernet/go-tproxy" "github.com/apernet/hysteria/core/v2/client" ) const ( udpBufferSize = 4096 defaultTimeout = 60 * time.Second ) type UDPTProxy struct { HyClient client.Client Timeout time.Duration EventLogger UDPEventLogger } type UDPEventLogger interface { Connect(addr, reqAddr net.Addr) Error(addr, reqAddr net.Addr, err error) } func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error { conn, err := tproxy.ListenUDP("udp", laddr) if err != nil { return err } defer conn.Close() buf := make([]byte, udpBufferSize) for { // We will only get the first packet of each src/dst pair here, // because newPair will create a TProxy connection and take over // the src/dst pair. Later packets will be sent there instead of here. n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf) if err != nil { return err } r.newPair(srcAddr, dstAddr, buf[:n]) } } func (r *UDPTProxy) newPair(srcAddr, dstAddr *net.UDPAddr, initPkt []byte) { if r.EventLogger != nil { r.EventLogger.Connect(srcAddr, dstAddr) } var closeErr error defer func() { // If closeErr is nil, it means we at least successfully sent the first packet // and started forwarding, in which case we don't call the error logger. if r.EventLogger != nil && closeErr != nil { r.EventLogger.Error(srcAddr, dstAddr, closeErr) } }() conn, err := tproxy.DialUDP("udp", dstAddr, srcAddr) if err != nil { closeErr = err return } hyConn, err := r.HyClient.UDP() if err != nil { _ = conn.Close() closeErr = err return } // Send the first packet err = hyConn.Send(initPkt, dstAddr.String()) if err != nil { _ = conn.Close() _ = hyConn.Close() closeErr = err return } // Start forwarding go func() { err := r.forwarding(conn, hyConn, dstAddr.String()) _ = conn.Close() _ = hyConn.Close() if r.EventLogger != nil { var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { // We don't consider deadline exceeded (timeout) an error err = nil } r.EventLogger.Error(srcAddr, dstAddr, err) } }() } func (r *UDPTProxy) forwarding(conn *net.UDPConn, hyConn client.HyUDPConn, dst string) error { errChan := make(chan error, 2) // Local <- Remote go func() { for { bs, _, err := hyConn.Receive() if err != nil { errChan <- err return } _, err = conn.Write(bs) if err != nil { errChan <- err return } _ = r.updateConnDeadline(conn) } }() // Local -> Remote go func() { buf := make([]byte, udpBufferSize) for { _ = r.updateConnDeadline(conn) n, err := conn.Read(buf) if n > 0 { err := hyConn.Send(buf[:n], dst) if err != nil { errChan <- err return } } if err != nil { errChan <- err return } } }() return <-errChan } func (r *UDPTProxy) updateConnDeadline(conn *net.UDPConn) error { if r.Timeout == 0 { return conn.SetReadDeadline(time.Now().Add(defaultTimeout)) } else { return conn.SetReadDeadline(time.Now().Add(r.Timeout)) } } ================================================ FILE: app/internal/tproxy/udp_others.go ================================================ //go:build !linux package tproxy import ( "errors" "net" "time" "github.com/apernet/hysteria/core/v2/client" ) type UDPTProxy struct { HyClient client.Client Timeout time.Duration EventLogger UDPEventLogger } type UDPEventLogger interface { Connect(addr, reqAddr net.Addr) Error(addr, reqAddr net.Addr, err error) } func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error { return errors.New("not supported on this platform") } ================================================ FILE: app/internal/tun/log.go ================================================ package tun import ( "github.com/sagernet/sing/common/logger" "go.uber.org/zap" ) var _ logger.Logger = (*singLogger)(nil) type singLogger struct { tag string zapLogger *zap.Logger } func extractSingExceptions(args []any) { for i, arg := range args { if err, ok := arg.(error); ok { args[i] = err.Error() } } } func (l *singLogger) Trace(args ...any) { if l.zapLogger == nil { return } extractSingExceptions(args) l.zapLogger.Debug(l.tag, zap.Any("args", args)) } func (l *singLogger) Debug(args ...any) { if l.zapLogger == nil { return } extractSingExceptions(args) l.zapLogger.Debug(l.tag, zap.Any("args", args)) } func (l *singLogger) Info(args ...any) { if l.zapLogger == nil { return } extractSingExceptions(args) l.zapLogger.Info(l.tag, zap.Any("args", args)) } func (l *singLogger) Warn(args ...any) { if l.zapLogger == nil { return } extractSingExceptions(args) l.zapLogger.Warn(l.tag, zap.Any("args", args)) } func (l *singLogger) Error(args ...any) { if l.zapLogger == nil { return } extractSingExceptions(args) l.zapLogger.Error(l.tag, zap.Any("args", args)) } func (l *singLogger) Fatal(args ...any) { if l.zapLogger == nil { return } extractSingExceptions(args) l.zapLogger.Fatal(l.tag, zap.Any("args", args)) } func (l *singLogger) Panic(args ...any) { if l.zapLogger == nil { return } extractSingExceptions(args) l.zapLogger.Panic(l.tag, zap.Any("args", args)) } ================================================ FILE: app/internal/tun/server.go ================================================ package tun import ( "context" "fmt" "io" "net" "net/netip" tun "github.com/apernet/sing-tun" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/network" "go.uber.org/zap" "github.com/apernet/hysteria/core/v2/client" ) type Server struct { HyClient client.Client EventLogger EventLogger // for debugging Logger *zap.Logger IfName string MTU uint32 Timeout int64 // in seconds, also applied to TCP in system stack // required by system stack Inet4Address []netip.Prefix Inet6Address []netip.Prefix // auto route AutoRoute bool StructRoute bool Inet4RouteAddress []netip.Prefix Inet6RouteAddress []netip.Prefix Inet4RouteExcludeAddress []netip.Prefix Inet6RouteExcludeAddress []netip.Prefix } type EventLogger interface { TCPRequest(addr, reqAddr string) TCPError(addr, reqAddr string, err error) UDPRequest(addr string) UDPError(addr string, err error) } func (s *Server) Serve() error { tunOpts := tun.Options{ Name: s.IfName, Inet4Address: s.Inet4Address, Inet6Address: s.Inet6Address, MTU: s.MTU, GSO: true, AutoRoute: s.AutoRoute, StrictRoute: s.StructRoute, Inet4RouteAddress: s.Inet4RouteAddress, Inet6RouteAddress: s.Inet6RouteAddress, Inet4RouteExcludeAddress: s.Inet4RouteExcludeAddress, Inet6RouteExcludeAddress: s.Inet6RouteExcludeAddress, Logger: &singLogger{ tag: "tun", zapLogger: s.Logger, }, } tunIf, err := tun.New(tunOpts) if err != nil { return fmt.Errorf("failed to create tun interface: %w", err) } defer tunIf.Close() tunStack, err := tun.NewSystem(tun.StackOptions{ Context: context.Background(), Tun: tunIf, TunOptions: tunOpts, UDPTimeout: s.Timeout, Handler: &tunHandler{s}, Logger: &singLogger{ tag: "tun-stack", zapLogger: s.Logger, }, ForwarderBindInterface: true, InterfaceFinder: &interfaceFinder{}, }) if err != nil { return fmt.Errorf("failed to create tun stack: %w", err) } defer tunStack.Close() return tunStack.(tun.StackRunner).Run() } type tunHandler struct { *Server } var _ tun.Handler = (*tunHandler)(nil) func (t *tunHandler) NewConnection(ctx context.Context, conn net.Conn, m metadata.Metadata) error { addr := m.Source.String() reqAddr := m.Destination.String() if t.EventLogger != nil { t.EventLogger.TCPRequest(addr, reqAddr) } var closeErr error defer func() { if t.EventLogger != nil { t.EventLogger.TCPError(addr, reqAddr, closeErr) } }() rc, err := t.HyClient.TCP(reqAddr) if err != nil { closeErr = err // the returned err is ignored by caller return nil } defer rc.Close() // start forwarding copyErrChan := make(chan error, 3) go func() { <-ctx.Done() copyErrChan <- ctx.Err() }() go func() { _, copyErr := io.Copy(rc, conn) copyErrChan <- copyErr }() go func() { _, copyErr := io.Copy(conn, rc) copyErrChan <- copyErr }() closeErr = <-copyErrChan return nil } func (t *tunHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, m metadata.Metadata) error { addr := m.Source.String() if t.EventLogger != nil { t.EventLogger.UDPRequest(addr) } var closeErr error defer func() { if t.EventLogger != nil { t.EventLogger.UDPError(addr, closeErr) } }() rc, err := t.HyClient.UDP() if err != nil { closeErr = err // the returned err is simply called into NewError again return nil } defer rc.Close() // start forwarding copyErrChan := make(chan error, 3) go func() { <-ctx.Done() copyErrChan <- ctx.Err() }() // local <- remote go func() { for { bs, from, err := rc.Receive() if err != nil { copyErrChan <- err return } var fromAddr metadata.Socksaddr if ap, perr := netip.ParseAddrPort(from); perr == nil { fromAddr = metadata.SocksaddrFromNetIP(ap) } else { fromAddr.Fqdn = from } err = conn.WritePacket(buf.As(bs), fromAddr) if err != nil { copyErrChan <- err return } } }() // local -> remote go func() { buffer := buf.NewPacket() defer buffer.Release() for { buffer.Reset() addr, err := conn.ReadPacket(buffer) if err != nil { copyErrChan <- err return } err = rc.Send(buffer.Bytes(), addr.String()) if err != nil { copyErrChan <- err return } } }() closeErr = <-copyErrChan return nil } func (t *tunHandler) NewError(ctx context.Context, err error) { // unused } type interfaceFinder struct{} var _ control.InterfaceFinder = (*interfaceFinder)(nil) func (f *interfaceFinder) InterfaceIndexByName(name string) (int, error) { ifce, err := net.InterfaceByName(name) if err != nil { return -1, err } return ifce.Index, nil } func (f *interfaceFinder) InterfaceNameByIndex(index int) (string, error) { ifce, err := net.InterfaceByIndex(index) if err != nil { return "", err } return ifce.Name, nil } ================================================ FILE: app/internal/url/url.go ================================================ // 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. // Package url parses URLs and implements query escaping. package url // See RFC 3986. This package generally follows RFC 3986, except where // it deviates for compatibility reasons. When sending changes, first // search old issues for history on decisions. Unit tests should also // contain references to issue numbers with details. // Hysteria fork note: This file is grabbed from the standard Go library, // but with a few modifications to make it support our special port format // when using port hopping. import ( "errors" "fmt" "path" "sort" "strconv" "strings" ) // Error reports an error and the operation and URL that caused it. type Error struct { Op string URL string Err error } func (e *Error) Unwrap() error { return e.Err } func (e *Error) Error() string { return fmt.Sprintf("%s %q: %s", e.Op, e.URL, e.Err) } func (e *Error) Timeout() bool { t, ok := e.Err.(interface { Timeout() bool }) return ok && t.Timeout() } func (e *Error) Temporary() bool { t, ok := e.Err.(interface { Temporary() bool }) return ok && t.Temporary() } const upperhex = "0123456789ABCDEF" func ishex(c byte) bool { switch { case '0' <= c && c <= '9': return true case 'a' <= c && c <= 'f': return true case 'A' <= c && c <= 'F': return true } return false } func unhex(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0 } type encoding int const ( encodePath encoding = 1 + iota encodePathSegment encodeHost encodeZone encodeUserPassword encodeQueryComponent encodeFragment ) type EscapeError string func (e EscapeError) Error() string { return "invalid URL escape " + strconv.Quote(string(e)) } type InvalidHostError string func (e InvalidHostError) Error() string { return "invalid character " + strconv.Quote(string(e)) + " in host name" } // Return true if the specified character should be escaped when // appearing in a URL string, according to RFC 3986. // // Please be informed that for now shouldEscape does not check all // reserved characters correctly. See golang.org/issue/5684. func shouldEscape(c byte, mode encoding) bool { // §2.3 Unreserved characters (alphanum) if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { return false } if mode == encodeHost || mode == encodeZone { // §3.2.2 Host allows // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" // as part of reg-name. // We add : because we include :port as part of host. // We add [ ] because we include [ipv6]:port as part of host. // We add < > because they're the only characters left that // we could possibly allow, and Parse will reject them if we // escape them (because hosts can't use %-encoding for // ASCII bytes). switch c { case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"': return false } } switch c { case '-', '_', '.', '~': // §2.3 Unreserved characters (mark) return false case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved) // Different sections of the URL allow a few of // the reserved characters to appear unescaped. switch mode { case encodePath: // §3.3 // The RFC allows : @ & = + $ but saves / ; , for assigning // meaning to individual path segments. This package // only manipulates the path as a whole, so we allow those // last three as well. That leaves only ? to escape. return c == '?' case encodePathSegment: // §3.3 // The RFC allows : @ & = + $ but saves / ; , for assigning // meaning to individual path segments. return c == '/' || c == ';' || c == ',' || c == '?' case encodeUserPassword: // §3.2.1 // The RFC allows ';', ':', '&', '=', '+', '$', and ',' in // userinfo, so we must escape only '@', '/', and '?'. // The parsing of userinfo treats ':' as special so we must escape // that too. return c == '@' || c == '/' || c == '?' || c == ':' case encodeQueryComponent: // §3.4 // The RFC reserves (so we must escape) everything. return true case encodeFragment: // §4.1 // The RFC text is silent but the grammar allows // everything, so escape nothing. return false } } if mode == encodeFragment { // RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are // included in reserved from RFC 2396 §2.2. The remaining sub-delims do not // need to be escaped. To minimize potential breakage, we apply two restrictions: // (1) we always escape sub-delims outside of the fragment, and (2) we always // escape single quote to avoid breaking callers that had previously assumed that // single quotes would be escaped. See issue #19917. switch c { case '!', '(', ')', '*': return false } } // Everything else must be escaped. return true } // QueryUnescape does the inverse transformation of QueryEscape, // converting each 3-byte encoded substring of the form "%AB" into the // hex-decoded byte 0xAB. // It returns an error if any % is not followed by two hexadecimal // digits. func QueryUnescape(s string) (string, error) { return unescape(s, encodeQueryComponent) } // PathUnescape does the inverse transformation of PathEscape, // converting each 3-byte encoded substring of the form "%AB" into the // hex-decoded byte 0xAB. It returns an error if any % is not followed // by two hexadecimal digits. // // PathUnescape is identical to QueryUnescape except that it does not // unescape '+' to ' ' (space). func PathUnescape(s string) (string, error) { return unescape(s, encodePathSegment) } // unescape unescapes a string; the mode specifies // which section of the URL string is being unescaped. func unescape(s string, mode encoding) (string, error) { // Count %, check that they're well-formed. n := 0 hasPlus := false for i := 0; i < len(s); { switch s[i] { case '%': n++ if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { s = s[i:] if len(s) > 3 { s = s[:3] } return "", EscapeError(s) } // Per https://tools.ietf.org/html/rfc3986#page-21 // in the host component %-encoding can only be used // for non-ASCII bytes. // But https://tools.ietf.org/html/rfc6874#section-2 // introduces %25 being allowed to escape a percent sign // in IPv6 scoped-address literals. Yay. if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" { return "", EscapeError(s[i : i+3]) } if mode == encodeZone { // RFC 6874 says basically "anything goes" for zone identifiers // and that even non-ASCII can be redundantly escaped, // but it seems prudent to restrict %-escaped bytes here to those // that are valid host name bytes in their unescaped form. // That is, you can use escaping in the zone identifier but not // to introduce bytes you couldn't just write directly. // But Windows puts spaces here! Yay. v := unhex(s[i+1])<<4 | unhex(s[i+2]) if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) { return "", EscapeError(s[i : i+3]) } } i += 3 case '+': hasPlus = mode == encodeQueryComponent i++ default: if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) { return "", InvalidHostError(s[i : i+1]) } i++ } } if n == 0 && !hasPlus { return s, nil } var t strings.Builder t.Grow(len(s) - 2*n) for i := 0; i < len(s); i++ { switch s[i] { case '%': t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2])) i += 2 case '+': if mode == encodeQueryComponent { t.WriteByte(' ') } else { t.WriteByte('+') } default: t.WriteByte(s[i]) } } return t.String(), nil } // QueryEscape escapes the string so it can be safely placed // inside a URL query. func QueryEscape(s string) string { return escape(s, encodeQueryComponent) } // PathEscape escapes the string so it can be safely placed inside a URL path segment, // replacing special characters (including /) with %XX sequences as needed. func PathEscape(s string) string { return escape(s, encodePathSegment) } func escape(s string, mode encoding) string { spaceCount, hexCount := 0, 0 for i := 0; i < len(s); i++ { c := s[i] if shouldEscape(c, mode) { if c == ' ' && mode == encodeQueryComponent { spaceCount++ } else { hexCount++ } } } if spaceCount == 0 && hexCount == 0 { return s } var buf [64]byte var t []byte required := len(s) + 2*hexCount if required <= len(buf) { t = buf[:required] } else { t = make([]byte, required) } if hexCount == 0 { copy(t, s) for i := 0; i < len(s); i++ { if s[i] == ' ' { t[i] = '+' } } return string(t) } j := 0 for i := 0; i < len(s); i++ { switch c := s[i]; { case c == ' ' && mode == encodeQueryComponent: t[j] = '+' j++ case shouldEscape(c, mode): t[j] = '%' t[j+1] = upperhex[c>>4] t[j+2] = upperhex[c&15] j += 3 default: t[j] = s[i] j++ } } return string(t) } // A URL represents a parsed URL (technically, a URI reference). // // The general form represented is: // // [scheme:][//[userinfo@]host][/]path[?query][#fragment] // // URLs that do not start with a slash after the scheme are interpreted as: // // scheme:opaque[?query][#fragment] // // Note that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/. // A consequence is that it is impossible to tell which slashes in the Path were // slashes in the raw URL and which were %2f. This distinction is rarely important, // but when it is, the code should use the EscapedPath method, which preserves // the original encoding of Path. // // The RawPath field is an optional field which is only set when the default // encoding of Path is different from the escaped path. See the EscapedPath method // for more details. // // URL's String method uses the EscapedPath method to obtain the path. type URL struct { Scheme string Opaque string // encoded opaque data User *Userinfo // username and password information Host string // host or host:port Path string // path (relative paths may omit leading slash) RawPath string // encoded path hint (see EscapedPath method) OmitHost bool // do not emit empty host (authority) ForceQuery bool // append a query ('?') even if RawQuery is empty RawQuery string // encoded query values, without '?' Fragment string // fragment for references, without '#' RawFragment string // encoded fragment hint (see EscapedFragment method) } // User returns a Userinfo containing the provided username // and no password set. func User(username string) *Userinfo { return &Userinfo{username, "", false} } // UserPassword returns a Userinfo containing the provided username // and password. // // This functionality should only be used with legacy web sites. // RFC 2396 warns that interpreting Userinfo this way // “is NOT RECOMMENDED, because the passing of authentication // information in clear text (such as URI) has proven to be a // security risk in almost every case where it has been used.” func UserPassword(username, password string) *Userinfo { return &Userinfo{username, password, true} } // The Userinfo type is an immutable encapsulation of username and // password details for a URL. An existing Userinfo value is guaranteed // to have a username set (potentially empty, as allowed by RFC 2396), // and optionally a password. type Userinfo struct { username string password string passwordSet bool } // Username returns the username. func (u *Userinfo) Username() string { if u == nil { return "" } return u.username } // Password returns the password in case it is set, and whether it is set. func (u *Userinfo) Password() (string, bool) { if u == nil { return "", false } return u.password, u.passwordSet } // String returns the encoded userinfo information in the standard form // of "username[:password]". func (u *Userinfo) String() string { if u == nil { return "" } s := escape(u.username, encodeUserPassword) if u.passwordSet { s += ":" + escape(u.password, encodeUserPassword) } return s } // Maybe rawURL is of the form scheme:path. // (Scheme must be [a-zA-Z][a-zA-Z0-9+.-]*) // If so, return scheme, path; else return "", rawURL. func getScheme(rawURL string) (scheme, path string, err error) { for i := 0; i < len(rawURL); i++ { c := rawURL[i] switch { case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': // do nothing case '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.': if i == 0 { return "", rawURL, nil } case c == ':': if i == 0 { return "", "", errors.New("missing protocol scheme") } return rawURL[:i], rawURL[i+1:], nil default: // we have encountered an invalid character, // so there is no valid scheme return "", rawURL, nil } } return "", rawURL, nil } // Parse parses a raw url into a URL structure. // // The url may be relative (a path, without a host) or absolute // (starting with a scheme). Trying to parse a hostname and path // without a scheme is invalid but may not necessarily return an // error, due to parsing ambiguities. func Parse(rawURL string) (*URL, error) { // Cut off #frag u, frag, _ := strings.Cut(rawURL, "#") url, err := parse(u, false) if err != nil { return nil, &Error{"parse", u, err} } if frag == "" { return url, nil } if err = url.setFragment(frag); err != nil { return nil, &Error{"parse", rawURL, err} } return url, nil } // ParseRequestURI parses a raw url into a URL structure. It assumes that // url was received in an HTTP request, so the url is interpreted // only as an absolute URI or an absolute path. // The string url is assumed not to have a #fragment suffix. // (Web browsers strip #fragment before sending the URL to a web server.) func ParseRequestURI(rawURL string) (*URL, error) { url, err := parse(rawURL, true) if err != nil { return nil, &Error{"parse", rawURL, err} } return url, nil } // parse parses a URL from a string in one of two contexts. If // viaRequest is true, the URL is assumed to have arrived via an HTTP request, // in which case only absolute URLs or path-absolute relative URLs are allowed. // If viaRequest is false, all forms of relative URLs are allowed. func parse(rawURL string, viaRequest bool) (*URL, error) { var rest string var err error if stringContainsCTLByte(rawURL) { return nil, errors.New("net/url: invalid control character in URL") } if rawURL == "" && viaRequest { return nil, errors.New("empty url") } url := new(URL) if rawURL == "*" { url.Path = "*" return url, nil } // Split off possible leading "http:", "mailto:", etc. // Cannot contain escaped characters. if url.Scheme, rest, err = getScheme(rawURL); err != nil { return nil, err } url.Scheme = strings.ToLower(url.Scheme) if strings.HasSuffix(rest, "?") && strings.Count(rest, "?") == 1 { url.ForceQuery = true rest = rest[:len(rest)-1] } else { rest, url.RawQuery, _ = strings.Cut(rest, "?") } if !strings.HasPrefix(rest, "/") { if url.Scheme != "" { // We consider rootless paths per RFC 3986 as opaque. url.Opaque = rest return url, nil } if viaRequest { return nil, errors.New("invalid URI for request") } // Avoid confusion with malformed schemes, like cache_object:foo/bar. // See golang.org/issue/16822. // // RFC 3986, §3.3: // In addition, a URI reference (Section 4.1) may be a relative-path reference, // in which case the first path segment cannot contain a colon (":") character. if segment, _, _ := strings.Cut(rest, "/"); strings.Contains(segment, ":") { // First path segment has colon. Not allowed in relative URL. return nil, errors.New("first path segment in URL cannot contain colon") } } if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") { var authority string authority, rest = rest[2:], "" if i := strings.Index(authority, "/"); i >= 0 { authority, rest = authority[:i], authority[i:] } url.User, url.Host, err = parseAuthority(authority) if err != nil { return nil, err } } else if url.Scheme != "" && strings.HasPrefix(rest, "/") { // OmitHost is set to true when rawURL has an empty host (authority). // See golang.org/issue/46059. url.OmitHost = true } // Set Path and, optionally, RawPath. // RawPath is a hint of the encoding of Path. We don't want to set it if // the default escaping of Path is equivalent, to help make sure that people // don't rely on it in general. if err := url.setPath(rest); err != nil { return nil, err } return url, nil } func parseAuthority(authority string) (user *Userinfo, host string, err error) { i := strings.LastIndex(authority, "@") if i < 0 { host, err = parseHost(authority) } else { host, err = parseHost(authority[i+1:]) } if err != nil { return nil, "", err } if i < 0 { return nil, host, nil } userinfo := authority[:i] if !validUserinfo(userinfo) { return nil, "", errors.New("net/url: invalid userinfo") } if !strings.Contains(userinfo, ":") { if userinfo, err = unescape(userinfo, encodeUserPassword); err != nil { return nil, "", err } user = User(userinfo) } else { username, password, _ := strings.Cut(userinfo, ":") if username, err = unescape(username, encodeUserPassword); err != nil { return nil, "", err } if password, err = unescape(password, encodeUserPassword); err != nil { return nil, "", err } user = UserPassword(username, password) } return user, host, nil } // parseHost parses host as an authority without user // information. That is, as host[:port]. func parseHost(host string) (string, error) { if strings.HasPrefix(host, "[") { // Parse an IP-Literal in RFC 3986 and RFC 6874. // E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80". i := strings.LastIndex(host, "]") if i < 0 { return "", errors.New("missing ']' in host") } colonPort := host[i+1:] if !validOptionalPort(colonPort) { return "", fmt.Errorf("invalid port %q after host", colonPort) } // RFC 6874 defines that %25 (%-encoded percent) introduces // the zone identifier, and the zone identifier can use basically // any %-encoding it likes. That's different from the host, which // can only %-encode non-ASCII bytes. // We do impose some restrictions on the zone, to avoid stupidity // like newlines. zone := strings.Index(host[:i], "%25") if zone >= 0 { host1, err := unescape(host[:zone], encodeHost) if err != nil { return "", err } host2, err := unescape(host[zone:i], encodeZone) if err != nil { return "", err } host3, err := unescape(host[i:], encodeHost) if err != nil { return "", err } return host1 + host2 + host3, nil } } else if i := strings.LastIndex(host, ":"); i != -1 { colonPort := host[i:] if !validOptionalPort(colonPort) { return "", fmt.Errorf("invalid port %q after host", colonPort) } } var err error if host, err = unescape(host, encodeHost); err != nil { return "", err } return host, nil } // setPath sets the Path and RawPath fields of the URL based on the provided // escaped path p. It maintains the invariant that RawPath is only specified // when it differs from the default encoding of the path. // For example: // - setPath("/foo/bar") will set Path="/foo/bar" and RawPath="" // - setPath("/foo%2fbar") will set Path="/foo/bar" and RawPath="/foo%2fbar" // setPath will return an error only if the provided path contains an invalid // escaping. func (u *URL) setPath(p string) error { path, err := unescape(p, encodePath) if err != nil { return err } u.Path = path if escp := escape(path, encodePath); p == escp { // Default encoding is fine. u.RawPath = "" } else { u.RawPath = p } return nil } // EscapedPath returns the escaped form of u.Path. // In general there are multiple possible escaped forms of any path. // EscapedPath returns u.RawPath when it is a valid escaping of u.Path. // Otherwise EscapedPath ignores u.RawPath and computes an escaped // form on its own. // The String and RequestURI methods use EscapedPath to construct // their results. // In general, code should call EscapedPath instead of // reading u.RawPath directly. func (u *URL) EscapedPath() string { if u.RawPath != "" && validEncoded(u.RawPath, encodePath) { p, err := unescape(u.RawPath, encodePath) if err == nil && p == u.Path { return u.RawPath } } if u.Path == "*" { return "*" // don't escape (Issue 11202) } return escape(u.Path, encodePath) } // validEncoded reports whether s is a valid encoded path or fragment, // according to mode. // It must not contain any bytes that require escaping during encoding. func validEncoded(s string, mode encoding) bool { for i := 0; i < len(s); i++ { // RFC 3986, Appendix A. // pchar = unreserved / pct-encoded / sub-delims / ":" / "@". // shouldEscape is not quite compliant with the RFC, // so we check the sub-delims ourselves and let // shouldEscape handle the others. switch s[i] { case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@': // ok case '[', ']': // ok - not specified in RFC 3986 but left alone by modern browsers case '%': // ok - percent encoded, will decode default: if shouldEscape(s[i], mode) { return false } } } return true } // setFragment is like setPath but for Fragment/RawFragment. func (u *URL) setFragment(f string) error { frag, err := unescape(f, encodeFragment) if err != nil { return err } u.Fragment = frag if escf := escape(frag, encodeFragment); f == escf { // Default encoding is fine. u.RawFragment = "" } else { u.RawFragment = f } return nil } // EscapedFragment returns the escaped form of u.Fragment. // In general there are multiple possible escaped forms of any fragment. // EscapedFragment returns u.RawFragment when it is a valid escaping of u.Fragment. // Otherwise EscapedFragment ignores u.RawFragment and computes an escaped // form on its own. // The String method uses EscapedFragment to construct its result. // In general, code should call EscapedFragment instead of // reading u.RawFragment directly. func (u *URL) EscapedFragment() string { if u.RawFragment != "" && validEncoded(u.RawFragment, encodeFragment) { f, err := unescape(u.RawFragment, encodeFragment) if err == nil && f == u.Fragment { return u.RawFragment } } return escape(u.Fragment, encodeFragment) } // validOptionalPort reports whether port is either an empty string // or matches /^:\d*$/ func validOptionalPort(port string) bool { if port == "" { return true } if port[0] != ':' { return false } for _, b := range port[1:] { if (b < '0' || b > '9') && (b != '-' && b != ',') { // Neither a digit nor a valid separator character. return false } } return true } // String reassembles the URL into a valid URL string. // The general form of the result is one of: // // scheme:opaque?query#fragment // scheme://userinfo@host/path?query#fragment // // If u.Opaque is non-empty, String uses the first form; // otherwise it uses the second form. // Any non-ASCII characters in host are escaped. // To obtain the path, String uses u.EscapedPath(). // // In the second form, the following rules apply: // - if u.Scheme is empty, scheme: is omitted. // - if u.User is nil, userinfo@ is omitted. // - if u.Host is empty, host/ is omitted. // - if u.Scheme and u.Host are empty and u.User is nil, // the entire scheme://userinfo@host/ is omitted. // - if u.Host is non-empty and u.Path begins with a /, // the form host/path does not add its own /. // - if u.RawQuery is empty, ?query is omitted. // - if u.Fragment is empty, #fragment is omitted. func (u *URL) String() string { var buf strings.Builder if u.Scheme != "" { buf.WriteString(u.Scheme) buf.WriteByte(':') } if u.Opaque != "" { buf.WriteString(u.Opaque) } else { if u.Scheme != "" || u.Host != "" || u.User != nil { if u.OmitHost && u.Host == "" && u.User == nil { // omit empty host } else { if u.Host != "" || u.Path != "" || u.User != nil { buf.WriteString("//") } if ui := u.User; ui != nil { buf.WriteString(ui.String()) buf.WriteByte('@') } if h := u.Host; h != "" { buf.WriteString(escape(h, encodeHost)) } } } path := u.EscapedPath() if path != "" && path[0] != '/' && u.Host != "" { buf.WriteByte('/') } if buf.Len() == 0 { // RFC 3986 §4.2 // A path segment that contains a colon character (e.g., "this:that") // cannot be used as the first segment of a relative-path reference, as // it would be mistaken for a scheme name. Such a segment must be // preceded by a dot-segment (e.g., "./this:that") to make a relative- // path reference. if segment, _, _ := strings.Cut(path, "/"); strings.Contains(segment, ":") { buf.WriteString("./") } } buf.WriteString(path) } if u.ForceQuery || u.RawQuery != "" { buf.WriteByte('?') buf.WriteString(u.RawQuery) } if u.Fragment != "" { buf.WriteByte('#') buf.WriteString(u.EscapedFragment()) } return buf.String() } // Redacted is like String but replaces any password with "xxxxx". // Only the password in u.User is redacted. func (u *URL) Redacted() string { if u == nil { return "" } ru := *u if _, has := ru.User.Password(); has { ru.User = UserPassword(ru.User.Username(), "xxxxx") } return ru.String() } // Values maps a string key to a list of values. // It is typically used for query parameters and form values. // Unlike in the http.Header map, the keys in a Values map // are case-sensitive. type Values map[string][]string // Get gets the first value associated with the given key. // If there are no values associated with the key, Get returns // the empty string. To access multiple values, use the map // directly. func (v Values) Get(key string) string { vs := v[key] if len(vs) == 0 { return "" } return vs[0] } // Set sets the key to value. It replaces any existing // values. func (v Values) Set(key, value string) { v[key] = []string{value} } // Add adds the value to key. It appends to any existing // values associated with key. func (v Values) Add(key, value string) { v[key] = append(v[key], value) } // Del deletes the values associated with key. func (v Values) Del(key string) { delete(v, key) } // Has checks whether a given key is set. func (v Values) Has(key string) bool { _, ok := v[key] return ok } // ParseQuery parses the URL-encoded query string and returns // a map listing the values specified for each key. // ParseQuery always returns a non-nil map containing all the // valid query parameters found; err describes the first decoding error // encountered, if any. // // Query is expected to be a list of key=value settings separated by ampersands. // A setting without an equals sign is interpreted as a key set to an empty // value. // Settings containing a non-URL-encoded semicolon are considered invalid. func ParseQuery(query string) (Values, error) { m := make(Values) err := parseQuery(m, query) return m, err } func parseQuery(m Values, query string) (err error) { for query != "" { var key string key, query, _ = strings.Cut(query, "&") if strings.Contains(key, ";") { err = fmt.Errorf("invalid semicolon separator in query") continue } if key == "" { continue } key, value, _ := strings.Cut(key, "=") key, err1 := QueryUnescape(key) if err1 != nil { if err == nil { err = err1 } continue } value, err1 = QueryUnescape(value) if err1 != nil { if err == nil { err = err1 } continue } m[key] = append(m[key], value) } return err } // Encode encodes the values into “URL encoded” form // ("bar=baz&foo=quux") sorted by key. func (v Values) Encode() string { if v == nil { return "" } var buf strings.Builder keys := make([]string, 0, len(v)) for k := range v { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { vs := v[k] keyEscaped := QueryEscape(k) for _, v := range vs { if buf.Len() > 0 { buf.WriteByte('&') } buf.WriteString(keyEscaped) buf.WriteByte('=') buf.WriteString(QueryEscape(v)) } } return buf.String() } // resolvePath applies special path segments from refs and applies // them to base, per RFC 3986. func resolvePath(base, ref string) string { var full string if ref == "" { full = base } else if ref[0] != '/' { i := strings.LastIndex(base, "/") full = base[:i+1] + ref } else { full = ref } if full == "" { return "" } var ( elem string dst strings.Builder ) first := true remaining := full // We want to return a leading '/', so write it now. dst.WriteByte('/') found := true for found { elem, remaining, found = strings.Cut(remaining, "/") if elem == "." { first = false // drop continue } if elem == ".." { // Ignore the leading '/' we already wrote. str := dst.String()[1:] index := strings.LastIndexByte(str, '/') dst.Reset() dst.WriteByte('/') if index == -1 { first = true } else { dst.WriteString(str[:index]) } } else { if !first { dst.WriteByte('/') } dst.WriteString(elem) first = false } } if elem == "." || elem == ".." { dst.WriteByte('/') } // We wrote an initial '/', but we don't want two. r := dst.String() if len(r) > 1 && r[1] == '/' { r = r[1:] } return r } // IsAbs reports whether the URL is absolute. // Absolute means that it has a non-empty scheme. func (u *URL) IsAbs() bool { return u.Scheme != "" } // Parse parses a URL in the context of the receiver. The provided URL // may be relative or absolute. Parse returns nil, err on parse // failure, otherwise its return value is the same as ResolveReference. func (u *URL) Parse(ref string) (*URL, error) { refURL, err := Parse(ref) if err != nil { return nil, err } return u.ResolveReference(refURL), nil } // ResolveReference resolves a URI reference to an absolute URI from // an absolute base URI u, per RFC 3986 Section 5.2. The URI reference // may be relative or absolute. ResolveReference always returns a new // URL instance, even if the returned URL is identical to either the // base or reference. If ref is an absolute URL, then ResolveReference // ignores base and returns a copy of ref. func (u *URL) ResolveReference(ref *URL) *URL { url := *ref if ref.Scheme == "" { url.Scheme = u.Scheme } if ref.Scheme != "" || ref.Host != "" || ref.User != nil { // The "absoluteURI" or "net_path" cases. // We can ignore the error from setPath since we know we provided a // validly-escaped path. url.setPath(resolvePath(ref.EscapedPath(), "")) return &url } if ref.Opaque != "" { url.User = nil url.Host = "" url.Path = "" return &url } if ref.Path == "" && !ref.ForceQuery && ref.RawQuery == "" { url.RawQuery = u.RawQuery if ref.Fragment == "" { url.Fragment = u.Fragment url.RawFragment = u.RawFragment } } // The "abs_path" or "rel_path" cases. url.Host = u.Host url.User = u.User url.setPath(resolvePath(u.EscapedPath(), ref.EscapedPath())) return &url } // Query parses RawQuery and returns the corresponding values. // It silently discards malformed value pairs. // To check errors use ParseQuery. func (u *URL) Query() Values { v, _ := ParseQuery(u.RawQuery) return v } // RequestURI returns the encoded path?query or opaque?query // string that would be used in an HTTP request for u. func (u *URL) RequestURI() string { result := u.Opaque if result == "" { result = u.EscapedPath() if result == "" { result = "/" } } else { if strings.HasPrefix(result, "//") { result = u.Scheme + ":" + result } } if u.ForceQuery || u.RawQuery != "" { result += "?" + u.RawQuery } return result } // Hostname returns u.Host, stripping any valid port number if present. // // If the result is enclosed in square brackets, as literal IPv6 addresses are, // the square brackets are removed from the result. func (u *URL) Hostname() string { host, _ := splitHostPort(u.Host) return host } // Port returns the port part of u.Host, without the leading colon. // // If u.Host doesn't contain a valid numeric port, Port returns an empty string. func (u *URL) Port() string { _, port := splitHostPort(u.Host) return port } // splitHostPort separates host and port. If the port is not valid, it returns // the entire input as host, and it doesn't check the validity of the host. // Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. func splitHostPort(hostPort string) (host, port string) { host = hostPort colon := strings.LastIndexByte(host, ':') if colon != -1 && validOptionalPort(host[colon:]) { host, port = host[:colon], host[colon+1:] } if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { host = host[1 : len(host)-1] } return } // Marshaling interface implementations. // Would like to implement MarshalText/UnmarshalText but that will change the JSON representation of URLs. func (u *URL) MarshalBinary() (text []byte, err error) { return []byte(u.String()), nil } func (u *URL) UnmarshalBinary(text []byte) error { u1, err := Parse(string(text)) if err != nil { return err } *u = *u1 return nil } // JoinPath returns a new URL with the provided path elements joined to // any existing path and the resulting path cleaned of any ./ or ../ elements. // Any sequences of multiple / characters will be reduced to a single /. func (u *URL) JoinPath(elem ...string) *URL { elem = append([]string{u.EscapedPath()}, elem...) var p string if !strings.HasPrefix(elem[0], "/") { // Return a relative path if u is relative, // but ensure that it contains no ../ elements. elem[0] = "/" + elem[0] p = path.Join(elem...)[1:] } else { p = path.Join(elem...) } // path.Join will remove any trailing slashes. // Preserve at least one. if strings.HasSuffix(elem[len(elem)-1], "/") && !strings.HasSuffix(p, "/") { p += "/" } url := *u url.setPath(p) return &url } // validUserinfo reports whether s is a valid userinfo string per RFC 3986 // Section 3.2.1: // // userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" // / "*" / "+" / "," / ";" / "=" // // It doesn't validate pct-encoded. The caller does that via func unescape. func validUserinfo(s string) bool { for _, r := range s { if 'A' <= r && r <= 'Z' { continue } if 'a' <= r && r <= 'z' { continue } if '0' <= r && r <= '9' { continue } switch r { case '-', '.', '_', ':', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', '%', '@': continue default: return false } } return true } // stringContainsCTLByte reports whether s contains any ASCII control character. func stringContainsCTLByte(s string) bool { for i := 0; i < len(s); i++ { b := s[i] if b < ' ' || b == 0x7f { return true } } return false } // JoinPath returns a URL string with the provided path elements joined to // the existing path of base and the resulting path cleaned of any ./ or ../ elements. func JoinPath(base string, elem ...string) (result string, err error) { url, err := Parse(base) if err != nil { return } result = url.JoinPath(elem...).String() return } ================================================ FILE: app/internal/url/url_test.go ================================================ package url import ( "reflect" "testing" ) func TestParse(t *testing.T) { type args struct { rawURL string } tests := []struct { name string args args want *URL wantErr bool }{ { name: "no port", args: args{ rawURL: "hysteria2://ganggang@icecreamsogood/", }, want: &URL{ Scheme: "hysteria2", User: User("ganggang"), Host: "icecreamsogood", Path: "/", }, }, { name: "single port", args: args{ rawURL: "hysteria2://yesyes@icecreamsogood:8888/", }, want: &URL{ Scheme: "hysteria2", User: User("yesyes"), Host: "icecreamsogood:8888", Path: "/", }, }, { name: "multi port", args: args{ rawURL: "hysteria2://darkness@laplus.org:8888,9999,11111/", }, want: &URL{ Scheme: "hysteria2", User: User("darkness"), Host: "laplus.org:8888,9999,11111", Path: "/", }, }, { name: "range port", args: args{ rawURL: "hysteria2://darkness@laplus.org:8888-9999/", }, want: &URL{ Scheme: "hysteria2", User: User("darkness"), Host: "laplus.org:8888-9999", Path: "/", }, }, { name: "both", args: args{ rawURL: "hysteria2://gawr:gura@atlantis.moe:443,7788-8899,10010/", }, want: &URL{ Scheme: "hysteria2", User: UserPassword("gawr", "gura"), Host: "atlantis.moe:443,7788-8899,10010", Path: "/", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Parse(tt.args.rawURL) if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Parse() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: app/internal/utils/bpsconv.go ================================================ package utils import ( "errors" "fmt" "strconv" "strings" ) const ( Byte = 1 Kilobyte = Byte * 1000 Megabyte = Kilobyte * 1000 Gigabyte = Megabyte * 1000 Terabyte = Gigabyte * 1000 ) // StringToBps converts a string to a bandwidth value in bytes per second. // E.g. "100 Mbps", "512 kbps", "1g" are all valid. func StringToBps(s string) (uint64, error) { s = strings.ToLower(strings.TrimSpace(s)) spl := 0 for i, c := range s { if c < '0' || c > '9' { spl = i break } } if spl == 0 { // No unit or no value return 0, errors.New("invalid format") } v, err := strconv.ParseUint(s[:spl], 10, 64) if err != nil { return 0, err } unit := strings.TrimSpace(s[spl:]) switch strings.ToLower(unit) { case "b", "bps": return v * Byte / 8, nil case "k", "kb", "kbps": return v * Kilobyte / 8, nil case "m", "mb", "mbps": return v * Megabyte / 8, nil case "g", "gb", "gbps": return v * Gigabyte / 8, nil case "t", "tb", "tbps": return v * Terabyte / 8, nil default: return 0, errors.New("unsupported unit") } } // ConvBandwidth handles both string and int types for bandwidth. // When using string, it will be parsed as a bandwidth string with units. // When using int, it will be parsed as a raw bandwidth in bytes per second. // It does NOT support float types. func ConvBandwidth(bw interface{}) (uint64, error) { switch bwT := bw.(type) { case string: return StringToBps(bwT) case int: return uint64(bwT), nil default: return 0, fmt.Errorf("invalid type %T for bandwidth", bwT) } } ================================================ FILE: app/internal/utils/bpsconv_test.go ================================================ package utils import "testing" func TestStringToBps(t *testing.T) { type args struct { s string } tests := []struct { name string args args want uint64 wantErr bool }{ {"bps", args{"800 bps"}, 100, false}, {"kbps", args{"800 kbps"}, 100_000, false}, {"mbps", args{"800 mbps"}, 100_000_000, false}, {"gbps", args{"800 gbps"}, 100_000_000_000, false}, {"tbps", args{"800 tbps"}, 100_000_000_000_000, false}, {"mbps simp", args{"100m"}, 12_500_000, false}, {"gbps simp upper", args{"2G"}, 250_000_000, false}, {"invalid 1", args{"damn"}, 0, true}, {"invalid 2", args{"6444"}, 0, true}, {"invalid 3", args{"5.4 mbps"}, 0, true}, {"invalid 4", args{"kbps"}, 0, true}, {"invalid 5", args{"1234 5678 gbps"}, 0, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := StringToBps(tt.args.s) if (err != nil) != tt.wantErr { t.Errorf("StringToBps() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("StringToBps() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: app/internal/utils/certloader.go ================================================ package utils import ( "crypto/tls" "crypto/x509" "fmt" "net" "os" "strings" "sync" "sync/atomic" "time" ) type LocalCertificateLoader struct { CertFile string KeyFile string SNIGuard SNIGuardFunc lock sync.Mutex cache atomic.Pointer[localCertificateCache] } type SNIGuardFunc func(info *tls.ClientHelloInfo, cert *tls.Certificate) error // localCertificateCache holds the certificate and its mod times. // this struct is designed to be read-only. // // to update the cache, use LocalCertificateLoader.makeCache and // update the LocalCertificateLoader.cache field. type localCertificateCache struct { certificate *tls.Certificate certModTime time.Time keyModTime time.Time } func (l *LocalCertificateLoader) InitializeCache() error { l.lock.Lock() defer l.lock.Unlock() cache, err := l.makeCache() if err != nil { return err } l.cache.Store(cache) return nil } func (l *LocalCertificateLoader) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { cert, err := l.getCertificateWithCache() if err != nil { return nil, err } if l.SNIGuard == nil { return cert, nil } err = l.SNIGuard(info, cert) if err != nil { return nil, err } return cert, nil } func (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Time, err error) { fi, err := os.Stat(l.CertFile) if err != nil { err = fmt.Errorf("failed to stat certificate file: %w", err) return } certModTime = fi.ModTime() fi, err = os.Stat(l.KeyFile) if err != nil { err = fmt.Errorf("failed to stat key file: %w", err) return } keyModTime = fi.ModTime() return } func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) { c := &localCertificateCache{} c.certModTime, c.keyModTime, err = l.checkModTime() if err != nil { return } cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile) if err != nil { return } c.certificate = &cert if c.certificate.Leaf == nil { // certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23 c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) if err != nil { return } } cache = c return } func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) { cache := l.cache.Load() certModTime, keyModTime, terr := l.checkModTime() if terr != nil { if cache != nil { // use cache when file is temporarily unavailable return cache.certificate, nil } return nil, terr } if cache != nil && cache.certModTime.Equal(certModTime) && cache.keyModTime.Equal(keyModTime) { // cache is up-to-date return cache.certificate, nil } if cache != nil { if !l.lock.TryLock() { // another goroutine is updating the cache return cache.certificate, nil } } else { l.lock.Lock() } defer l.lock.Unlock() if l.cache.Load() != cache { // another goroutine updated the cache return l.cache.Load().certificate, nil } newCache, err := l.makeCache() if err != nil { if cache != nil { // use cache when loading failed return cache.certificate, nil } return nil, err } l.cache.Store(newCache) return newCache.certificate, nil } // getNameFromClientHello returns a normalized form of hello.ServerName. // If hello.ServerName is empty (i.e. client did not use SNI), then the // associated connection's local address is used to extract an IP address. // // ref: https://github.com/caddyserver/certmagic/blob/3bad5b6bb595b09c14bd86ff0b365d302faaf5e2/handshake.go#L838 func getNameFromClientHello(hello *tls.ClientHelloInfo) string { normalizedName := func(serverName string) string { return strings.ToLower(strings.TrimSpace(serverName)) } localIPFromConn := func(c net.Conn) string { if c == nil { return "" } localAddr := c.LocalAddr().String() ip, _, err := net.SplitHostPort(localAddr) if err != nil { ip = localAddr } if scopeIDStart := strings.Index(ip, "%"); scopeIDStart > -1 { ip = ip[:scopeIDStart] } return ip } if name := normalizedName(hello.ServerName); name != "" { return name } return localIPFromConn(hello.Conn) } func SNIGuardDNSSAN(info *tls.ClientHelloInfo, cert *tls.Certificate) error { if len(cert.Leaf.DNSNames) == 0 { return nil } return SNIGuardStrict(info, cert) } func SNIGuardStrict(info *tls.ClientHelloInfo, cert *tls.Certificate) error { hostname := getNameFromClientHello(info) err := cert.Leaf.VerifyHostname(hostname) if err != nil { return fmt.Errorf("sni guard: %w", err) } return nil } ================================================ FILE: app/internal/utils/certloader_test.go ================================================ package utils import ( "crypto/tls" "log" "net/http" "os" "os/exec" "strings" "testing" "github.com/stretchr/testify/assert" ) const ( testListen = "127.82.39.147:12947" testCAFile = "./testcerts/ca" testCertFile = "./testcerts/cert" testKeyFile = "./testcerts/key" ) func TestCertificateLoaderPathError(t *testing.T) { assert.NoError(t, os.RemoveAll(testCertFile)) assert.NoError(t, os.RemoveAll(testKeyFile)) loader := LocalCertificateLoader{ CertFile: testCertFile, KeyFile: testKeyFile, SNIGuard: SNIGuardStrict, } err := loader.InitializeCache() var pathErr *os.PathError assert.ErrorAs(t, err, &pathErr) } func TestCertificateLoaderFullChain(t *testing.T) { assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain")) loader := LocalCertificateLoader{ CertFile: testCertFile, KeyFile: testKeyFile, SNIGuard: SNIGuardStrict, } assert.NoError(t, loader.InitializeCache()) lis, err := tls.Listen("tcp", testListen, &tls.Config{ GetCertificate: loader.GetCertificate, }) assert.NoError(t, err) defer lis.Close() go http.Serve(lis, nil) assert.Error(t, runTestTLSClient("unmatched-sni.example.com")) assert.Error(t, runTestTLSClient("")) assert.NoError(t, runTestTLSClient("example.com")) } func TestCertificateLoaderNoSAN(t *testing.T) { assert.NoError(t, generateTestCertificate(nil, "selfsign")) loader := LocalCertificateLoader{ CertFile: testCertFile, KeyFile: testKeyFile, SNIGuard: SNIGuardDNSSAN, } assert.NoError(t, loader.InitializeCache()) lis, err := tls.Listen("tcp", testListen, &tls.Config{ GetCertificate: loader.GetCertificate, }) assert.NoError(t, err) defer lis.Close() go http.Serve(lis, nil) assert.NoError(t, runTestTLSClient("")) } func TestCertificateLoaderReplaceCertificate(t *testing.T) { assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain")) loader := LocalCertificateLoader{ CertFile: testCertFile, KeyFile: testKeyFile, SNIGuard: SNIGuardStrict, } assert.NoError(t, loader.InitializeCache()) lis, err := tls.Listen("tcp", testListen, &tls.Config{ GetCertificate: loader.GetCertificate, }) assert.NoError(t, err) defer lis.Close() go http.Serve(lis, nil) assert.NoError(t, runTestTLSClient("example.com")) assert.Error(t, runTestTLSClient("2.example.com")) assert.NoError(t, generateTestCertificate([]string{"2.example.com"}, "fullchain")) assert.Error(t, runTestTLSClient("example.com")) assert.NoError(t, runTestTLSClient("2.example.com")) } func generateTestCertificate(dnssan []string, certType string) error { args := []string{ "certloader_test_gencert.py", "--ca", testCAFile, "--cert", testCertFile, "--key", testKeyFile, "--type", certType, } if len(dnssan) > 0 { args = append(args, "--dnssan", strings.Join(dnssan, ",")) } cmd := exec.Command("python", args...) out, err := cmd.CombinedOutput() if err != nil { log.Printf("Failed to generate test certificate: %s", out) return err } return nil } func runTestTLSClient(sni string) error { args := []string{ "certloader_test_tlsclient.py", "--server", testListen, "--ca", testCAFile, } if sni != "" { args = append(args, "--sni", sni) } cmd := exec.Command("python", args...) out, err := cmd.CombinedOutput() if err != nil { log.Printf("Failed to run test TLS client: %s", out) return err } return nil } ================================================ FILE: app/internal/utils/certloader_test_gencert.py ================================================ import argparse import datetime from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption def create_key(): return ec.generate_private_key(ec.SECP256R1()) def create_certificate(cert_type, subject, issuer, private_key, public_key, dns_san=None): serial_number = x509.random_serial_number() not_valid_before = datetime.datetime.now(datetime.UTC) not_valid_after = not_valid_before + datetime.timedelta(days=365) subject_name = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, subject.get('C', 'ZZ')), x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject.get('O', 'No Organization')), x509.NameAttribute(NameOID.COMMON_NAME, subject.get('CN', 'No CommonName')), ]) issuer_name = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, issuer.get('C', 'ZZ')), x509.NameAttribute(NameOID.ORGANIZATION_NAME, issuer.get('O', 'No Organization')), x509.NameAttribute(NameOID.COMMON_NAME, issuer.get('CN', 'No CommonName')), ]) builder = x509.CertificateBuilder() builder = builder.subject_name(subject_name) builder = builder.issuer_name(issuer_name) builder = builder.public_key(public_key) builder = builder.serial_number(serial_number) builder = builder.not_valid_before(not_valid_before) builder = builder.not_valid_after(not_valid_after) if cert_type == 'root': builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True ) elif cert_type == 'intermediate': builder = builder.add_extension( x509.BasicConstraints(ca=True, path_length=0), critical=True ) elif cert_type == 'leaf': builder = builder.add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True ) else: raise ValueError(f'Invalid cert_type: {cert_type}') if dns_san: builder = builder.add_extension( x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_san.split(',')]), critical=False ) return builder.sign(private_key=private_key, algorithm=hashes.SHA256()) def main(): parser = argparse.ArgumentParser(description='Generate HTTPS server certificate.') parser.add_argument('--ca', required=True, help='Path to write the X509 CA certificate in PEM format') parser.add_argument('--cert', required=True, help='Path to write the X509 certificate in PEM format') parser.add_argument('--key', required=True, help='Path to write the private key in PEM format') parser.add_argument('--dnssan', required=False, default=None, help='Comma-separated list of DNS SANs') parser.add_argument('--type', required=True, choices=['selfsign', 'fullchain'], help='Type of certificate to generate') args = parser.parse_args() key = create_key() public_key = key.public_key() if args.type == 'selfsign': subject = {"C": "ZZ", "O": "Certificate", "CN": "Certificate"} cert = create_certificate( cert_type='root', subject=subject, issuer=subject, private_key=key, public_key=public_key, dns_san=args.dnssan) with open(args.ca, 'wb') as f: f.write(cert.public_bytes(Encoding.PEM)) with open(args.cert, 'wb') as f: f.write(cert.public_bytes(Encoding.PEM)) with open(args.key, 'wb') as f: f.write( key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption())) elif args.type == 'fullchain': ca_key = create_key() ca_public_key = ca_key.public_key() ca_subject = {"C": "ZZ", "O": "Root CA", "CN": "Root CA"} ca_cert = create_certificate( cert_type='root', subject=ca_subject, issuer=ca_subject, private_key=ca_key, public_key=ca_public_key) intermediate_key = create_key() intermediate_public_key = intermediate_key.public_key() intermediate_subject = {"C": "ZZ", "O": "Intermediate CA", "CN": "Intermediate CA"} intermediate_cert = create_certificate( cert_type='intermediate', subject=intermediate_subject, issuer=ca_subject, private_key=ca_key, public_key=intermediate_public_key) leaf_subject = {"C": "ZZ", "O": "Leaf Certificate", "CN": "Leaf Certificate"} cert = create_certificate( cert_type='leaf', subject=leaf_subject, issuer=intermediate_subject, private_key=intermediate_key, public_key=public_key, dns_san=args.dnssan) with open(args.ca, 'wb') as f: f.write(ca_cert.public_bytes(Encoding.PEM)) with open(args.cert, 'wb') as f: f.write(cert.public_bytes(Encoding.PEM)) f.write(intermediate_cert.public_bytes(Encoding.PEM)) with open(args.key, 'wb') as f: f.write( key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption())) if __name__ == "__main__": main() ================================================ FILE: app/internal/utils/certloader_test_tlsclient.py ================================================ import argparse import ssl import socket import sys def check_tls(server, ca_cert, sni, alpn): try: host, port = server.split(":") port = int(port) if ca_cert: context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_cert) context.check_hostname = sni is not None context.verify_mode = ssl.CERT_REQUIRED else: context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE if alpn: context.set_alpn_protocols([p for p in alpn.split(",")]) with socket.create_connection((host, port)) as sock: with context.wrap_socket(sock, server_hostname=sni) as ssock: # Verify handshake and certificate print(f'Connected to {ssock.version()} using {ssock.cipher()}') print(f'Server certificate validated and details: {ssock.getpeercert()}') print("OK") return 0 except Exception as e: print(f"Error: {e}") return 1 def main(): parser = argparse.ArgumentParser(description="Test TLS Server") parser.add_argument("--server", required=True, help="Server address to test (e.g., 127.1.2.3:8443)") parser.add_argument("--ca", required=False, default=None, help="CA certificate file used to validate the server certificate" "Omit to use insecure connection") parser.add_argument("--sni", required=False, default=None, help="SNI to send in ClientHello") parser.add_argument("--alpn", required=False, default='h2', help="ALPN to send in ClientHello") args = parser.parse_args() exit_status = check_tls( server=args.server, ca_cert=args.ca, sni=args.sni, alpn=args.alpn) sys.exit(exit_status) if __name__ == "__main__": main() ================================================ FILE: app/internal/utils/geoloader.go ================================================ package utils import ( "fmt" "io" "net/http" "os" "time" "github.com/apernet/hysteria/extras/v2/outbounds/acl" "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" ) const ( geoipFilename = "geoip.dat" geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat" geositeFilename = "geosite.dat" geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat" geoDlTmpPattern = ".hysteria-geoloader.dlpart.*" geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days ) var _ acl.GeoLoader = (*GeoLoader)(nil) // GeoLoader provides the on-demand GeoIP/GeoSite database // loading functionality required by the ACL engine. // Empty filenames = automatic download from built-in URLs. type GeoLoader struct { GeoIPFilename string GeoSiteFilename string UpdateInterval time.Duration DownloadFunc func(filename, url string) DownloadErrFunc func(err error) geoipMap map[string]*v2geo.GeoIP geositeMap map[string]*v2geo.GeoSite } func (l *GeoLoader) shouldDownload(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return true } if info.Size() == 0 { // empty files are loadable by v2geo, but we consider it broken return true } dt := time.Now().Sub(info.ModTime()) if l.UpdateInterval == 0 { return dt > geoDefaultUpdateInterval } else { return dt > l.UpdateInterval } } func (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error { l.DownloadFunc(filename, url) resp, err := http.Get(url) if err != nil { l.DownloadErrFunc(err) return err } defer resp.Body.Close() f, err := os.CreateTemp(".", geoDlTmpPattern) if err != nil { l.DownloadErrFunc(err) return err } defer os.Remove(f.Name()) _, err = io.Copy(f, resp.Body) if err != nil { f.Close() l.DownloadErrFunc(err) return err } f.Close() err = checkFunc(f.Name()) if err != nil { l.DownloadErrFunc(fmt.Errorf("integrity check failed: %w", err)) return err } err = os.Rename(f.Name(), filename) if err != nil { l.DownloadErrFunc(fmt.Errorf("rename failed: %w", err)) return err } return nil } func (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) { if l.geoipMap != nil { return l.geoipMap, nil } autoDL := false filename := l.GeoIPFilename if filename == "" { autoDL = true filename = geoipFilename } if autoDL { if !l.shouldDownload(filename) { m, err := v2geo.LoadGeoIP(filename) if err == nil { l.geoipMap = m return m, nil } // file is broken, download it again } err := l.downloadAndCheck(filename, geoipURL, func(filename string) error { _, err := v2geo.LoadGeoIP(filename) return err }) if err != nil { // as long as the previous download exists, fallback to it if _, serr := os.Stat(filename); os.IsNotExist(serr) { return nil, err } } } m, err := v2geo.LoadGeoIP(filename) if err != nil { return nil, err } l.geoipMap = m return m, nil } func (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) { if l.geositeMap != nil { return l.geositeMap, nil } autoDL := false filename := l.GeoSiteFilename if filename == "" { autoDL = true filename = geositeFilename } if autoDL { if !l.shouldDownload(filename) { m, err := v2geo.LoadGeoSite(filename) if err == nil { l.geositeMap = m return m, nil } // file is broken, download it again } err := l.downloadAndCheck(filename, geositeURL, func(filename string) error { _, err := v2geo.LoadGeoSite(filename) return err }) if err != nil { // as long as the previous download exists, fallback to it if _, serr := os.Stat(filename); os.IsNotExist(serr) { return nil, err } } } m, err := v2geo.LoadGeoSite(filename) if err != nil { return nil, err } l.geositeMap = m return m, nil } ================================================ FILE: app/internal/utils/qr.go ================================================ package utils import ( "os" "github.com/mdp/qrterminal/v3" ) func PrintQR(str string) { qrterminal.GenerateWithConfig(str, qrterminal.Config{ Level: qrterminal.L, Writer: os.Stdout, BlackChar: qrterminal.BLACK, WhiteChar: qrterminal.WHITE, }) } ================================================ FILE: app/internal/utils/testcerts/.gitignore ================================================ # This directory is used for certificate generation in certloader_test.go /* !/.gitignore ================================================ FILE: app/internal/utils/update.go ================================================ package utils import ( "context" "encoding/json" "fmt" "net" "net/http" "time" "github.com/apernet/hysteria/core/v2/client" ) const ( updateCheckEndpoint = "https://api.hy2.io/v1/update" updateCheckTimeout = 10 * time.Second ) type UpdateChecker struct { CurrentVersion string Platform string Architecture string Channel string Side string Client *http.Client } func NewServerUpdateChecker(currentVersion, platform, architecture, channel string) *UpdateChecker { return &UpdateChecker{ CurrentVersion: currentVersion, Platform: platform, Architecture: architecture, Channel: channel, Side: "server", Client: &http.Client{ Timeout: updateCheckTimeout, }, } } // NewClientUpdateChecker ensures that update checks are routed through a HyClient, // not being sent directly. This safeguard is CRITICAL, especially in scenarios where // users use Hysteria to bypass censorship. Making direct HTTPS requests to the API // endpoint could be easily spotted by censors (through SNI, for example), and could // serve as a signal to identify and penalize Hysteria users. func NewClientUpdateChecker(currentVersion, platform, architecture, channel string, hyClient client.Client) *UpdateChecker { return &UpdateChecker{ CurrentVersion: currentVersion, Platform: platform, Architecture: architecture, Channel: channel, Side: "client", Client: &http.Client{ Timeout: updateCheckTimeout, Transport: &http.Transport{ DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { // Unfortunately HyClient doesn't support context for now return hyClient.TCP(addr) }, }, }, } } type UpdateResponse struct { HasUpdate bool `json:"update"` LatestVersion string `json:"lver"` URL string `json:"url"` Urgent bool `json:"urgent"` } func (uc *UpdateChecker) Check() (*UpdateResponse, error) { url := fmt.Sprintf("%s?cver=%s&plat=%s&arch=%s&chan=%s&side=%s", updateCheckEndpoint, uc.CurrentVersion, uc.Platform, uc.Architecture, uc.Channel, uc.Side, ) resp, err := uc.Client.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var uResp UpdateResponse decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&uResp); err != nil { return nil, err } return &uResp, nil } ================================================ FILE: app/internal/utils_test/mock.go ================================================ package utils_test import ( "io" "net" "time" "github.com/apernet/hysteria/core/v2/client" ) type MockEchoHyClient struct{} func (c *MockEchoHyClient) TCP(addr string) (net.Conn, error) { return &mockEchoTCPConn{ BufChan: make(chan []byte, 10), }, nil } func (c *MockEchoHyClient) UDP() (client.HyUDPConn, error) { return &mockEchoUDPConn{ BufChan: make(chan mockEchoUDPPacket, 10), }, nil } func (c *MockEchoHyClient) Close() error { return nil } type mockEchoTCPConn struct { BufChan chan []byte } func (c *mockEchoTCPConn) Read(b []byte) (n int, err error) { buf := <-c.BufChan if buf == nil { // EOF return 0, io.EOF } return copy(b, buf), nil } func (c *mockEchoTCPConn) Write(b []byte) (n int, err error) { c.BufChan <- b return len(b), nil } func (c *mockEchoTCPConn) Close() error { close(c.BufChan) return nil } func (c *mockEchoTCPConn) LocalAddr() net.Addr { // Not implemented return nil } func (c *mockEchoTCPConn) RemoteAddr() net.Addr { // Not implemented return nil } func (c *mockEchoTCPConn) SetDeadline(t time.Time) error { // Not implemented return nil } func (c *mockEchoTCPConn) SetReadDeadline(t time.Time) error { // Not implemented return nil } func (c *mockEchoTCPConn) SetWriteDeadline(t time.Time) error { // Not implemented return nil } type mockEchoUDPPacket struct { Data []byte Addr string } type mockEchoUDPConn struct { BufChan chan mockEchoUDPPacket } func (c *mockEchoUDPConn) Receive() ([]byte, string, error) { p := <-c.BufChan if p.Data == nil { // EOF return nil, "", io.EOF } return p.Data, p.Addr, nil } func (c *mockEchoUDPConn) Send(bytes []byte, s string) error { c.BufChan <- mockEchoUDPPacket{ Data: bytes, Addr: s, } return nil } func (c *mockEchoUDPConn) Close() error { close(c.BufChan) return nil } ================================================ FILE: app/main.go ================================================ package main import "github.com/apernet/hysteria/app/v2/cmd" func main() { cmd.Execute() } ================================================ FILE: app/misc/socks5_test.py ================================================ import socket import socks import time TARGET = "1.1.1.1" def test_tcp() -> None: s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) print(f"TCP - Sending HTTP request to {TARGET}") start = time.time() s.connect((TARGET, 80)) s.send(b"GET / HTTP/1.1\r\nHost: " + TARGET.encode() + b"\r\n\r\n") data = s.recv(1024) if not data: print("No data received") elif not data.startswith(b"HTTP/1.1 "): print("Invalid response received") else: print("TCP test passed") end = time.time() s.close() print(f"Time: {round((end - start) * 1000, 2)} ms") def test_udp() -> None: s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM) s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) req = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05\x62\x61\x69\x64\x75\x03\x63\x6f\x6d\x00\x00\x01\x00\x01" print(f"UDP - Sending DNS request to {TARGET}") start = time.time() s.sendto(req, (TARGET, 53)) (rsp, address) = s.recvfrom(4096) if address[0] == TARGET and address[1] == 53 and rsp[0] == req[0] and rsp[1] == req[1]: print("UDP test passed") else: print("Invalid response received") end = time.time() s.close() print(f"Time: {round((end - start) * 1000, 2)} ms") if __name__ == "__main__": test_tcp() test_udp() ================================================ FILE: app/pprof.go ================================================ //go:build pprof package main import ( "fmt" "net/http" _ "net/http/pprof" ) const ( pprofListenAddr = ":6060" ) func init() { fmt.Printf("!!! pprof enabled, listening on %s\n", pprofListenAddr) go func() { if err := http.ListenAndServe(pprofListenAddr, nil); err != nil { panic(err) } }() } ================================================ FILE: core/client/.mockery.yaml ================================================ with-expecter: true inpackage: true dir: . packages: github.com/apernet/hysteria/core/v2/client: interfaces: udpIO: config: mockname: mockUDPIO ================================================ FILE: core/client/client.go ================================================ package client import ( "context" "crypto/tls" "net" "net/http" "net/url" "time" coreErrs "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/congestion" "github.com/apernet/hysteria/core/v2/internal/protocol" "github.com/apernet/hysteria/core/v2/internal/utils" "github.com/apernet/quic-go" "github.com/apernet/quic-go/http3" ) const ( closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError closeErrCodeProtocolError = 0x101 // HTTP3 ErrCodeGeneralProtocolError ) type Client interface { TCP(addr string) (net.Conn, error) UDP() (HyUDPConn, error) Close() error } type HyUDPConn interface { Receive() ([]byte, string, error) Send([]byte, string) error Close() error } type HandshakeInfo struct { UDPEnabled bool Tx uint64 // 0 if using BBR } func NewClient(config *Config) (Client, *HandshakeInfo, error) { if err := config.verifyAndFill(); err != nil { return nil, nil, err } c := &clientImpl{ config: config, } info, err := c.connect() if err != nil { return nil, nil, err } return c, info, nil } type clientImpl struct { config *Config pktConn net.PacketConn conn quic.Connection udpSM *udpSessionManager } func (c *clientImpl) connect() (*HandshakeInfo, error) { pktConn, err := c.config.ConnFactory.New(c.config.ServerAddr) if err != nil { return nil, err } // Convert config to TLS config & QUIC config tlsConfig := &tls.Config{ ServerName: c.config.TLSConfig.ServerName, InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify, VerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate, RootCAs: c.config.TLSConfig.RootCAs, } quicConfig := &quic.Config{ InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow, MaxStreamReceiveWindow: c.config.QUICConfig.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: c.config.QUICConfig.InitialConnectionReceiveWindow, MaxConnectionReceiveWindow: c.config.QUICConfig.MaxConnectionReceiveWindow, MaxIdleTimeout: c.config.QUICConfig.MaxIdleTimeout, KeepAlivePeriod: c.config.QUICConfig.KeepAlivePeriod, DisablePathMTUDiscovery: c.config.QUICConfig.DisablePathMTUDiscovery, EnableDatagrams: true, } // Prepare RoundTripper var conn quic.EarlyConnection rt := &http3.RoundTripper{ TLSClientConfig: tlsConfig, QUICConfig: quicConfig, Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { qc, err := quic.DialEarly(ctx, pktConn, c.config.ServerAddr, tlsCfg, cfg) if err != nil { return nil, err } conn = qc return qc, nil }, } // Send auth HTTP request req := &http.Request{ Method: http.MethodPost, URL: &url.URL{ Scheme: "https", Host: protocol.URLHost, Path: protocol.URLPath, }, Header: make(http.Header), } protocol.AuthRequestToHeader(req.Header, protocol.AuthRequest{ Auth: c.config.Auth, Rx: c.config.BandwidthConfig.MaxRx, }) resp, err := rt.RoundTrip(req) if err != nil { if conn != nil { _ = conn.CloseWithError(closeErrCodeProtocolError, "") } _ = pktConn.Close() return nil, coreErrs.ConnectError{Err: err} } if resp.StatusCode != protocol.StatusAuthOK { _ = conn.CloseWithError(closeErrCodeProtocolError, "") _ = pktConn.Close() return nil, coreErrs.AuthError{StatusCode: resp.StatusCode} } // Auth OK authResp := protocol.AuthResponseFromHeader(resp.Header) var actualTx uint64 if authResp.RxAuto { // Server asks client to use bandwidth detection, // ignore local bandwidth config and use BBR congestion.UseBBR(conn) } else { // actualTx = min(serverRx, clientTx) actualTx = authResp.Rx if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx { // Server doesn't have a limit, or our clientTx is smaller than serverRx actualTx = c.config.BandwidthConfig.MaxTx } if actualTx > 0 { congestion.UseBrutal(conn, actualTx) } else { // We don't know our own bandwidth either, use BBR congestion.UseBBR(conn) } } _ = resp.Body.Close() c.pktConn = pktConn c.conn = conn if authResp.UDPEnabled { c.udpSM = newUDPSessionManager(&udpIOImpl{Conn: conn}) } return &HandshakeInfo{ UDPEnabled: authResp.UDPEnabled, Tx: actualTx, }, nil } // openStream wraps the stream with QStream, which handles Close() properly func (c *clientImpl) openStream() (quic.Stream, error) { stream, err := c.conn.OpenStream() if err != nil { return nil, err } return &utils.QStream{Stream: stream}, nil } func (c *clientImpl) TCP(addr string) (net.Conn, error) { stream, err := c.openStream() if err != nil { return nil, wrapIfConnectionClosed(err) } // Send request err = protocol.WriteTCPRequest(stream, addr) if err != nil { _ = stream.Close() return nil, wrapIfConnectionClosed(err) } if c.config.FastOpen { // Don't wait for the response when fast open is enabled. // Return the connection immediately, defer the response handling // to the first Read() call. return &tcpConn{ Orig: stream, PseudoLocalAddr: c.conn.LocalAddr(), PseudoRemoteAddr: c.conn.RemoteAddr(), Established: false, }, nil } // Read response ok, msg, err := protocol.ReadTCPResponse(stream) if err != nil { _ = stream.Close() return nil, wrapIfConnectionClosed(err) } if !ok { _ = stream.Close() return nil, coreErrs.DialError{Message: msg} } return &tcpConn{ Orig: stream, PseudoLocalAddr: c.conn.LocalAddr(), PseudoRemoteAddr: c.conn.RemoteAddr(), Established: true, }, nil } func (c *clientImpl) UDP() (HyUDPConn, error) { if c.udpSM == nil { return nil, coreErrs.DialError{Message: "UDP not enabled"} } return c.udpSM.NewUDP() } func (c *clientImpl) Close() error { _ = c.conn.CloseWithError(closeErrCodeOK, "") _ = c.pktConn.Close() return nil } // wrapIfConnectionClosed checks if the error returned by quic-go // indicates that the QUIC connection has been permanently closed, // and if so, wraps the error with coreErrs.ClosedError. // PITFALL: sometimes quic-go has "internal errors" that are not net.Error, // but we still need to treat them as ClosedError. func wrapIfConnectionClosed(err error) error { netErr, ok := err.(net.Error) if !ok || !netErr.Temporary() { return coreErrs.ClosedError{Err: err} } else { return err } } type tcpConn struct { Orig quic.Stream PseudoLocalAddr net.Addr PseudoRemoteAddr net.Addr Established bool } func (c *tcpConn) Read(b []byte) (n int, err error) { if !c.Established { // Read response ok, msg, err := protocol.ReadTCPResponse(c.Orig) if err != nil { return 0, err } if !ok { return 0, coreErrs.DialError{Message: msg} } c.Established = true } return c.Orig.Read(b) } func (c *tcpConn) Write(b []byte) (n int, err error) { return c.Orig.Write(b) } func (c *tcpConn) Close() error { return c.Orig.Close() } func (c *tcpConn) LocalAddr() net.Addr { return c.PseudoLocalAddr } func (c *tcpConn) RemoteAddr() net.Addr { return c.PseudoRemoteAddr } func (c *tcpConn) SetDeadline(t time.Time) error { return c.Orig.SetDeadline(t) } func (c *tcpConn) SetReadDeadline(t time.Time) error { return c.Orig.SetReadDeadline(t) } func (c *tcpConn) SetWriteDeadline(t time.Time) error { return c.Orig.SetWriteDeadline(t) } type udpIOImpl struct { Conn quic.Connection } func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) { for { msg, err := io.Conn.ReceiveDatagram(context.Background()) if err != nil { // Connection error, this will stop the session manager return nil, err } udpMsg, err := protocol.ParseUDPMessage(msg) if err != nil { // Invalid message, this is fine - just wait for the next continue } return udpMsg, nil } } func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error { msgN := msg.Serialize(buf) if msgN < 0 { // Message larger than buffer, silent drop return nil } return io.Conn.SendDatagram(buf[:msgN]) } ================================================ FILE: core/client/config.go ================================================ package client import ( "crypto/x509" "net" "time" "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/pmtud" ) const ( defaultStreamReceiveWindow = 8388608 // 8MB defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB defaultMaxIdleTimeout = 30 * time.Second defaultKeepAlivePeriod = 10 * time.Second ) type Config struct { ConnFactory ConnFactory ServerAddr net.Addr Auth string TLSConfig TLSConfig QUICConfig QUICConfig BandwidthConfig BandwidthConfig FastOpen bool filled bool // whether the fields have been verified and filled } // verifyAndFill fills the fields that are not set by the user with default values when possible, // and returns an error if the user has not set a required field or has set an invalid value. func (c *Config) verifyAndFill() error { if c.filled { return nil } if c.ConnFactory == nil { c.ConnFactory = &udpConnFactory{} } if c.ServerAddr == nil { return errors.ConfigError{Field: "ServerAddr", Reason: "must be set"} } if c.QUICConfig.InitialStreamReceiveWindow == 0 { c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow } else if c.QUICConfig.InitialStreamReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.MaxStreamReceiveWindow == 0 { c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow } else if c.QUICConfig.MaxStreamReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.InitialConnectionReceiveWindow == 0 { c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow } else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.MaxConnectionReceiveWindow == 0 { c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow } else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.MaxIdleTimeout == 0 { c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout } else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second { return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"} } if c.QUICConfig.KeepAlivePeriod == 0 { c.QUICConfig.KeepAlivePeriod = defaultKeepAlivePeriod } else if c.QUICConfig.KeepAlivePeriod < 2*time.Second || c.QUICConfig.KeepAlivePeriod > 60*time.Second { return errors.ConfigError{Field: "QUICConfig.KeepAlivePeriod", Reason: "must be between 2s and 60s"} } c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery c.filled = true return nil } type ConnFactory interface { New(net.Addr) (net.PacketConn, error) } type udpConnFactory struct{} func (f *udpConnFactory) New(addr net.Addr) (net.PacketConn, error) { return net.ListenUDP("udp", nil) } // TLSConfig contains the TLS configuration fields that we want to expose to the user. type TLSConfig struct { ServerName string InsecureSkipVerify bool VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error RootCAs *x509.CertPool } // QUICConfig contains the QUIC configuration fields that we want to expose to the user. type QUICConfig struct { InitialStreamReceiveWindow uint64 MaxStreamReceiveWindow uint64 InitialConnectionReceiveWindow uint64 MaxConnectionReceiveWindow uint64 MaxIdleTimeout time.Duration KeepAlivePeriod time.Duration DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. } // BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second. type BandwidthConfig struct { MaxTx uint64 MaxRx uint64 } ================================================ FILE: core/client/mock_udpIO.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package client import ( protocol "github.com/apernet/hysteria/core/v2/internal/protocol" mock "github.com/stretchr/testify/mock" ) // mockUDPIO is an autogenerated mock type for the udpIO type type mockUDPIO struct { mock.Mock } type mockUDPIO_Expecter struct { mock *mock.Mock } func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter { return &mockUDPIO_Expecter{mock: &_m.Mock} } // ReceiveMessage provides a mock function with given fields: func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for ReceiveMessage") } var r0 *protocol.UDPMessage var r1 error if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok { return rf() } if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*protocol.UDPMessage) } } if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { r1 = ret.Error(1) } return r0, r1 } // mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage' type mockUDPIO_ReceiveMessage_Call struct { *mock.Call } // ReceiveMessage is a helper method to define mock.On call func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call { return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")} } func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call { _c.Call.Return(run) return _c } // SendMessage provides a mock function with given fields: _a0, _a1 func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error { ret := _m.Called(_a0, _a1) if len(ret) == 0 { panic("no return value specified for SendMessage") } var r0 error if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok { r0 = rf(_a0, _a1) } else { r0 = ret.Error(0) } return r0 } // mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage' type mockUDPIO_SendMessage_Call struct { *mock.Call } // SendMessage is a helper method to define mock.On call // - _a0 []byte // - _a1 *protocol.UDPMessage func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call { return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)} } func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte), args[1].(*protocol.UDPMessage)) }) return _c } func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call { _c.Call.Return(_a0) return _c } func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call { _c.Call.Return(run) return _c } // newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockUDPIO(t interface { mock.TestingT Cleanup(func()) }) *mockUDPIO { mock := &mockUDPIO{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/client/reconnect.go ================================================ package client import ( "net" "sync" coreErrs "github.com/apernet/hysteria/core/v2/errors" ) // reconnectableClientImpl is a wrapper of Client, which can reconnect when the connection is closed, // except when the caller explicitly calls Close() to permanently close this client. type reconnectableClientImpl struct { configFunc func() (*Config, error) // called before connecting connectedFunc func(Client, *HandshakeInfo, int) // called when successfully connected client Client count int m sync.Mutex closed bool // permanent close } // NewReconnectableClient creates a reconnectable client. // If lazy is true, the client will not connect until the first call to TCP() or UDP(). // We use a function for config mainly to delay config evaluation // (which involves DNS resolution) until the actual connection attempt. func NewReconnectableClient(configFunc func() (*Config, error), connectedFunc func(Client, *HandshakeInfo, int), lazy bool) (Client, error) { rc := &reconnectableClientImpl{ configFunc: configFunc, connectedFunc: connectedFunc, } if !lazy { if err := rc.reconnect(); err != nil { return nil, err } } return rc, nil } func (rc *reconnectableClientImpl) reconnect() error { if rc.client != nil { _ = rc.client.Close() } var info *HandshakeInfo config, err := rc.configFunc() if err != nil { return err } rc.client, info, err = NewClient(config) if err != nil { return err } else { rc.count++ if rc.connectedFunc != nil { rc.connectedFunc(rc, info, rc.count) } return nil } } // clientDo calls f with the current client. // If the client is nil, it will first reconnect. // It will also detect if the client is closed, and if so, // set it to nil for reconnect next time. func (rc *reconnectableClientImpl) clientDo(f func(Client) (interface{}, error)) (interface{}, error) { rc.m.Lock() if rc.closed { rc.m.Unlock() return nil, coreErrs.ClosedError{} } if rc.client == nil { // No active connection, connect first if err := rc.reconnect(); err != nil { rc.m.Unlock() return nil, err } } client := rc.client rc.m.Unlock() ret, err := f(client) if _, ok := err.(coreErrs.ClosedError); ok { // Connection closed, set client to nil for reconnect next time rc.m.Lock() if rc.client == client { // This check is in case the client is already changed by another goroutine rc.client = nil } rc.m.Unlock() } return ret, err } func (rc *reconnectableClientImpl) TCP(addr string) (net.Conn, error) { if c, err := rc.clientDo(func(client Client) (interface{}, error) { return client.TCP(addr) }); err != nil { return nil, err } else { return c.(net.Conn), nil } } func (rc *reconnectableClientImpl) UDP() (HyUDPConn, error) { if c, err := rc.clientDo(func(client Client) (interface{}, error) { return client.UDP() }); err != nil { return nil, err } else { return c.(HyUDPConn), nil } } func (rc *reconnectableClientImpl) Close() error { rc.m.Lock() defer rc.m.Unlock() rc.closed = true if rc.client != nil { return rc.client.Close() } return nil } ================================================ FILE: core/client/udp.go ================================================ package client import ( "errors" "io" "math/rand" "sync" "github.com/apernet/quic-go" coreErrs "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/frag" "github.com/apernet/hysteria/core/v2/internal/protocol" ) const ( udpMessageChanSize = 1024 ) type udpIO interface { ReceiveMessage() (*protocol.UDPMessage, error) SendMessage([]byte, *protocol.UDPMessage) error } type udpConn struct { ID uint32 D *frag.Defragger ReceiveCh chan *protocol.UDPMessage SendBuf []byte SendFunc func([]byte, *protocol.UDPMessage) error CloseFunc func() Closed bool } func (u *udpConn) Receive() ([]byte, string, error) { for { msg := <-u.ReceiveCh if msg == nil { // Closed return nil, "", io.EOF } dfMsg := u.D.Feed(msg) if dfMsg == nil { // Incomplete message, wait for more continue } return dfMsg.Data, dfMsg.Addr, nil } } // Send is not thread-safe, as it uses a shared SendBuf. func (u *udpConn) Send(data []byte, addr string) error { // Try no frag first msg := &protocol.UDPMessage{ SessionID: u.ID, PacketID: 0, FragID: 0, FragCount: 1, Addr: addr, Data: data, } err := u.SendFunc(u.SendBuf, msg) var errTooLarge *quic.DatagramTooLargeError if errors.As(err, &errTooLarge) { // Message too large, try fragmentation msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDataLen)) for _, fMsg := range fMsgs { err := u.SendFunc(u.SendBuf, &fMsg) if err != nil { return err } } return nil } else { return err } } func (u *udpConn) Close() error { u.CloseFunc() return nil } type udpSessionManager struct { io udpIO mutex sync.RWMutex m map[uint32]*udpConn nextID uint32 closed bool } func newUDPSessionManager(io udpIO) *udpSessionManager { m := &udpSessionManager{ io: io, m: make(map[uint32]*udpConn), nextID: 1, } go m.run() return m } func (m *udpSessionManager) run() error { defer m.closeCleanup() for { msg, err := m.io.ReceiveMessage() if err != nil { return err } m.feed(msg) } } func (m *udpSessionManager) closeCleanup() { m.mutex.Lock() defer m.mutex.Unlock() for _, conn := range m.m { m.close(conn) } m.closed = true } func (m *udpSessionManager) feed(msg *protocol.UDPMessage) { m.mutex.RLock() defer m.mutex.RUnlock() conn, ok := m.m[msg.SessionID] if !ok { // Ignore message from unknown session return } select { case conn.ReceiveCh <- msg: // OK default: // Channel full, drop the message } } // NewUDP creates a new UDP session. func (m *udpSessionManager) NewUDP() (HyUDPConn, error) { m.mutex.Lock() defer m.mutex.Unlock() if m.closed { return nil, coreErrs.ClosedError{} } id := m.nextID m.nextID++ conn := &udpConn{ ID: id, D: &frag.Defragger{}, ReceiveCh: make(chan *protocol.UDPMessage, udpMessageChanSize), SendBuf: make([]byte, protocol.MaxUDPSize), SendFunc: m.io.SendMessage, } conn.CloseFunc = func() { m.mutex.Lock() defer m.mutex.Unlock() m.close(conn) } m.m[id] = conn return conn, nil } func (m *udpSessionManager) close(conn *udpConn) { if !conn.Closed { conn.Closed = true close(conn.ReceiveCh) delete(m.m, conn.ID) } } func (m *udpSessionManager) Count() int { m.mutex.RLock() defer m.mutex.RUnlock() return len(m.m) } ================================================ FILE: core/client/udp_test.go ================================================ package client import ( "errors" io2 "io" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/goleak" coreErrs "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/protocol" ) func TestUDPSessionManager(t *testing.T) { io := newMockUDPIO(t) receiveCh := make(chan *protocol.UDPMessage, 4) io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) { m := <-receiveCh if m == nil { return nil, errors.New("closed") } return m, nil }) sm := newUDPSessionManager(io) // Test UDP session IO udpConn1, err := sm.NewUDP() assert.NoError(t, err) udpConn2, err := sm.NewUDP() assert.NoError(t, err) msg1 := &protocol.UDPMessage{ SessionID: 1, PacketID: 0, FragID: 0, FragCount: 1, Addr: "random.site.com:9000", Data: []byte("hello friend"), } io.EXPECT().SendMessage(mock.Anything, msg1).Return(nil).Once() err = udpConn1.Send(msg1.Data, msg1.Addr) assert.NoError(t, err) msg2 := &protocol.UDPMessage{ SessionID: 2, PacketID: 0, FragID: 0, FragCount: 1, Addr: "another.site.org:8000", Data: []byte("mr robot"), } io.EXPECT().SendMessage(mock.Anything, msg2).Return(nil).Once() err = udpConn2.Send(msg2.Data, msg2.Addr) assert.NoError(t, err) respMsg1 := &protocol.UDPMessage{ SessionID: 1, PacketID: 0, FragID: 0, FragCount: 1, Addr: msg1.Addr, Data: []byte("goodbye captain price"), } receiveCh <- respMsg1 data, addr, err := udpConn1.Receive() assert.NoError(t, err) assert.Equal(t, data, respMsg1.Data) assert.Equal(t, addr, respMsg1.Addr) respMsg2 := &protocol.UDPMessage{ SessionID: 2, PacketID: 0, FragID: 0, FragCount: 1, Addr: msg2.Addr, Data: []byte("white rose"), } receiveCh <- respMsg2 data, addr, err = udpConn2.Receive() assert.NoError(t, err) assert.Equal(t, data, respMsg2.Data) assert.Equal(t, addr, respMsg2.Addr) respMsg3 := &protocol.UDPMessage{ SessionID: 55, // Bogus session ID that doesn't exist PacketID: 0, FragID: 0, FragCount: 1, Addr: "burgerking.com:27017", Data: []byte("impossible whopper"), } receiveCh <- respMsg3 // No test for this, just make sure it doesn't panic // Test close UDP connection unblocks Receive() errChan := make(chan error, 1) go func() { _, _, err := udpConn1.Receive() errChan <- err }() assert.NoError(t, udpConn1.Close()) assert.Equal(t, <-errChan, io2.EOF) // Test close IO unblocks Receive() and blocks new UDP creation errChan = make(chan error, 1) go func() { _, _, err := udpConn2.Receive() errChan <- err }() close(receiveCh) assert.Equal(t, <-errChan, io2.EOF) _, err = sm.NewUDP() assert.Equal(t, err, coreErrs.ClosedError{}) // Leak checks time.Sleep(1 * time.Second) assert.Zero(t, sm.Count(), "session count should be 0") goleak.VerifyNone(t) } ================================================ FILE: core/errors/errors.go ================================================ package errors import ( "fmt" "strconv" ) // ConfigError is returned when a configuration field is invalid. type ConfigError struct { Field string Reason string } func (c ConfigError) Error() string { return fmt.Sprintf("invalid config: %s: %s", c.Field, c.Reason) } // ConnectError is returned when the client fails to connect to the server. type ConnectError struct { Err error } func (c ConnectError) Error() string { return "connect error: " + c.Err.Error() } func (c ConnectError) Unwrap() error { return c.Err } // AuthError is returned when the client fails to authenticate with the server. type AuthError struct { StatusCode int } func (a AuthError) Error() string { return "authentication error, HTTP status code: " + strconv.Itoa(a.StatusCode) } // DialError is returned when the server rejects the client's dial request. // This applies to both TCP and UDP. type DialError struct { Message string } func (c DialError) Error() string { return "dial error: " + c.Message } // ClosedError is returned when the client attempts to use a closed connection. type ClosedError struct { Err error // Can be nil } func (c ClosedError) Error() string { if c.Err == nil { return "connection closed" } else { return "connection closed: " + c.Err.Error() } } func (c ClosedError) Unwrap() error { return c.Err } // ProtocolError is returned when the server/client runs into an unexpected // or malformed request/response/message. type ProtocolError struct { Message string } func (p ProtocolError) Error() string { return "protocol error: " + p.Message } ================================================ FILE: core/go.mod ================================================ module github.com/apernet/hysteria/core/v2 go 1.22 toolchain go1.23.2 require ( github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d github.com/stretchr/testify v1.9.0 go.uber.org/goleak v1.2.1 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/time v0.5.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/objx v0.5.2 // indirect go.uber.org/mock v0.4.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: core/go.sum ================================================ github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d h1:KWRCWISqJOgY9/0hhH8Bevjw/k4tCQ7oJlXLyFv8u9s= github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d/go.mod h1:x0paLlmCzNOUDDQIgmgFWmnpWQIEuH1GNfA6NdgSTuM= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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: core/internal/congestion/bbr/bandwidth.go ================================================ package bbr import ( "math" "time" "github.com/apernet/quic-go/congestion" ) const ( infBandwidth = Bandwidth(math.MaxUint64) ) // Bandwidth of a connection type Bandwidth uint64 const ( // BitsPerSecond is 1 bit per second BitsPerSecond Bandwidth = 1 // BytesPerSecond is 1 byte per second BytesPerSecond = 8 * BitsPerSecond ) // BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth { return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond } ================================================ FILE: core/internal/congestion/bbr/bandwidth_sampler.go ================================================ package bbr import ( "math" "time" "github.com/apernet/quic-go/congestion" ) const ( infRTT = time.Duration(math.MaxInt64) defaultConnectionStateMapQueueSize = 256 defaultCandidatesBufferSize = 256 ) type roundTripCount uint64 // SendTimeState is a subset of ConnectionStateOnSentPacket which is returned // to the caller when the packet is acked or lost. type sendTimeState struct { // Whether other states in this object is valid. isValid bool // Whether the sender is app limited at the time the packet was sent. // App limited bandwidth sample might be artificially low because the sender // did not have enough data to send in order to saturate the link. isAppLimited bool // Total number of sent bytes at the time the packet was sent. // Includes the packet itself. totalBytesSent congestion.ByteCount // Total number of acked bytes at the time the packet was sent. totalBytesAcked congestion.ByteCount // Total number of lost bytes at the time the packet was sent. totalBytesLost congestion.ByteCount // Total number of inflight bytes at the time the packet was sent. // Includes the packet itself. // It should be equal to |total_bytes_sent| minus the sum of // |total_bytes_acked|, |total_bytes_lost| and total neutered bytes. bytesInFlight congestion.ByteCount } func newSendTimeState( isAppLimited bool, totalBytesSent congestion.ByteCount, totalBytesAcked congestion.ByteCount, totalBytesLost congestion.ByteCount, bytesInFlight congestion.ByteCount, ) *sendTimeState { return &sendTimeState{ isValid: true, isAppLimited: isAppLimited, totalBytesSent: totalBytesSent, totalBytesAcked: totalBytesAcked, totalBytesLost: totalBytesLost, bytesInFlight: bytesInFlight, } } type extraAckedEvent struct { // The excess bytes acknowlwedged in the time delta for this event. extraAcked congestion.ByteCount // The bytes acknowledged and time delta from the event. bytesAcked congestion.ByteCount timeDelta time.Duration // The round trip of the event. round roundTripCount } func maxExtraAckedEventFunc(a, b extraAckedEvent) int { if a.extraAcked > b.extraAcked { return 1 } else if a.extraAcked < b.extraAcked { return -1 } return 0 } // BandwidthSample type bandwidthSample struct { // The bandwidth at that particular sample. Zero if no valid bandwidth sample // is available. bandwidth Bandwidth // The RTT measurement at this particular sample. Zero if no RTT sample is // available. Does not correct for delayed ack time. rtt time.Duration // |send_rate| is computed from the current packet being acked('P') and an // earlier packet that is acked before P was sent. sendRate Bandwidth // States captured when the packet was sent. stateAtSend sendTimeState } func newBandwidthSample() *bandwidthSample { return &bandwidthSample{ sendRate: infBandwidth, } } // MaxAckHeightTracker is part of the BandwidthSampler. It is called after every // ack event to keep track the degree of ack aggregation(a.k.a "ack height"). type maxAckHeightTracker struct { // Tracks the maximum number of bytes acked faster than the estimated // bandwidth. maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount] // The time this aggregation started and the number of bytes acked during it. aggregationEpochStartTime time.Time aggregationEpochBytes congestion.ByteCount // The last sent packet number before the current aggregation epoch started. lastSentPacketNumberBeforeEpoch congestion.PacketNumber // The number of ack aggregation epochs ever started, including the ongoing // one. Stats only. numAckAggregationEpochs uint64 ackAggregationBandwidthThreshold float64 startNewAggregationEpochAfterFullRound bool reduceExtraAckedOnBandwidthIncrease bool } func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker { return &maxAckHeightTracker{ maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc), lastSentPacketNumberBeforeEpoch: invalidPacketNumber, ackAggregationBandwidthThreshold: 1.0, } } func (m *maxAckHeightTracker) Get() congestion.ByteCount { return m.maxAckHeightFilter.GetBest().extraAcked } func (m *maxAckHeightTracker) Update( bandwidthEstimate Bandwidth, isNewMaxBandwidth bool, roundTripCount roundTripCount, lastSentPacketNumber congestion.PacketNumber, lastAckedPacketNumber congestion.PacketNumber, ackTime time.Time, bytesAcked congestion.ByteCount, ) congestion.ByteCount { forceNewEpoch := false if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth { // Save and clear existing entries. best := m.maxAckHeightFilter.GetBest() secondBest := m.maxAckHeightFilter.GetSecondBest() thirdBest := m.maxAckHeightFilter.GetThirdBest() m.maxAckHeightFilter.Clear() // Reinsert the heights into the filter after recalculating. expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta) if expectedBytesAcked < best.bytesAcked { best.extraAcked = best.bytesAcked - expectedBytesAcked m.maxAckHeightFilter.Update(best, best.round) } expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta) if expectedBytesAcked < secondBest.bytesAcked { secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked m.maxAckHeightFilter.Update(secondBest, secondBest.round) } expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta) if expectedBytesAcked < thirdBest.bytesAcked { thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked m.maxAckHeightFilter.Update(thirdBest, thirdBest.round) } } // If any packet sent after the start of the epoch has been acked, start a new // epoch. if m.startNewAggregationEpochAfterFullRound && m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber && lastAckedPacketNumber != invalidPacketNumber && lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch { forceNewEpoch = true } if m.aggregationEpochStartTime.IsZero() || forceNewEpoch { m.aggregationEpochBytes = bytesAcked m.aggregationEpochStartTime = ackTime m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber m.numAckAggregationEpochs++ return 0 } // Compute how many bytes are expected to be delivered, assuming max bandwidth // is correct. aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime) expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta) // Reset the current aggregation epoch as soon as the ack arrival rate is less // than or equal to the max bandwidth. if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) { // Reset to start measuring a new aggregation epoch. m.aggregationEpochBytes = bytesAcked m.aggregationEpochStartTime = ackTime m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber m.numAckAggregationEpochs++ return 0 } m.aggregationEpochBytes += bytesAcked // Compute how many extra bytes were delivered vs max bandwidth. extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked newEvent := extraAckedEvent{ extraAcked: expectedBytesAcked, bytesAcked: m.aggregationEpochBytes, timeDelta: aggregationDelta, } m.maxAckHeightFilter.Update(newEvent, roundTripCount) return extraBytesAcked } func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) { m.maxAckHeightFilter.SetWindowLength(length) } func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) { newEvent := extraAckedEvent{ extraAcked: newHeight, round: newTime, } m.maxAckHeightFilter.Reset(newEvent, newTime) } func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) { m.ackAggregationBandwidthThreshold = threshold } func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) { m.startNewAggregationEpochAfterFullRound = value } func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) { m.reduceExtraAckedOnBandwidthIncrease = value } func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 { return m.ackAggregationBandwidthThreshold } func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 { return m.numAckAggregationEpochs } // AckPoint represents a point on the ack line. type ackPoint struct { ackTime time.Time totalBytesAcked congestion.ByteCount } // RecentAckPoints maintains the most recent 2 ack points at distinct times. type recentAckPoints struct { ackPoints [2]ackPoint } func (r *recentAckPoints) Update(ackTime time.Time, totalBytesAcked congestion.ByteCount) { if ackTime.Before(r.ackPoints[1].ackTime) { r.ackPoints[1].ackTime = ackTime } else if ackTime.After(r.ackPoints[1].ackTime) { r.ackPoints[0] = r.ackPoints[1] r.ackPoints[1].ackTime = ackTime } r.ackPoints[1].totalBytesAcked = totalBytesAcked } func (r *recentAckPoints) Clear() { r.ackPoints[0] = ackPoint{} r.ackPoints[1] = ackPoint{} } func (r *recentAckPoints) MostRecentPoint() *ackPoint { return &r.ackPoints[1] } func (r *recentAckPoints) LessRecentPoint() *ackPoint { if r.ackPoints[0].totalBytesAcked != 0 { return &r.ackPoints[0] } return &r.ackPoints[1] } // ConnectionStateOnSentPacket represents the information about a sent packet // and the state of the connection at the moment the packet was sent, // specifically the information about the most recently acknowledged packet at // that moment. type connectionStateOnSentPacket struct { // Time at which the packet is sent. sentTime time.Time // Size of the packet. size congestion.ByteCount // The value of |totalBytesSentAtLastAckedPacket| at the time the // packet was sent. totalBytesSentAtLastAckedPacket congestion.ByteCount // The value of |lastAckedPacketSentTime| at the time the packet was // sent. lastAckedPacketSentTime time.Time // The value of |lastAckedPacketAckTime| at the time the packet was // sent. lastAckedPacketAckTime time.Time // Send time states that are returned to the congestion controller when the // packet is acked or lost. sendTimeState sendTimeState } // Snapshot constructor. Records the current state of the bandwidth // sampler. // |bytes_in_flight| is the bytes in flight right after the packet is sent. func newConnectionStateOnSentPacket( sentTime time.Time, size congestion.ByteCount, bytesInFlight congestion.ByteCount, sampler *bandwidthSampler, ) *connectionStateOnSentPacket { return &connectionStateOnSentPacket{ sentTime: sentTime, size: size, totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket, lastAckedPacketSentTime: sampler.lastAckedPacketSentTime, lastAckedPacketAckTime: sampler.lastAckedPacketAckTime, sendTimeState: *newSendTimeState( sampler.isAppLimited, sampler.totalBytesSent, sampler.totalBytesAcked, sampler.totalBytesLost, bytesInFlight, ), } } // BandwidthSampler keeps track of sent and acknowledged packets and outputs a // bandwidth sample for every packet acknowledged. The samples are taken for // individual packets, and are not filtered; the consumer has to filter the // bandwidth samples itself. In certain cases, the sampler will locally severely // underestimate the bandwidth, hence a maximum filter with a size of at least // one RTT is recommended. // // This class bases its samples on the slope of two curves: the number of bytes // sent over time, and the number of bytes acknowledged as received over time. // It produces a sample of both slopes for every packet that gets acknowledged, // based on a slope between two points on each of the corresponding curves. Note // that due to the packet loss, the number of bytes on each curve might get // further and further away from each other, meaning that it is not feasible to // compare byte values coming from different curves with each other. // // The obvious points for measuring slope sample are the ones corresponding to // the packet that was just acknowledged. Let us denote them as S_1 (point at // which the current packet was sent) and A_1 (point at which the current packet // was acknowledged). However, taking a slope requires two points on each line, // so estimating bandwidth requires picking a packet in the past with respect to // which the slope is measured. // // For that purpose, BandwidthSampler always keeps track of the most recently // acknowledged packet, and records it together with every outgoing packet. // When a packet gets acknowledged (A_1), it has not only information about when // it itself was sent (S_1), but also the information about the latest // acknowledged packet right before it was sent (S_0 and A_0). // // Based on that data, send and ack rate are estimated as: // // send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0)) // ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0)) // // Here, the ack rate is intuitively the rate we want to treat as bandwidth. // However, in certain cases (e.g. ack compression) the ack rate at a point may // end up higher than the rate at which the data was originally sent, which is // not indicative of the real bandwidth. Hence, we use the send rate as an upper // bound, and the sample value is // // rate_sample = min(send_rate, ack_rate) // // An important edge case handled by the sampler is tracking the app-limited // samples. There are multiple meaning of "app-limited" used interchangeably, // hence it is important to understand and to be able to distinguish between // them. // // Meaning 1: connection state. The connection is said to be app-limited when // there is no outstanding data to send. This means that certain bandwidth // samples in the future would not be an accurate indication of the link // capacity, and it is important to inform consumer about that. Whenever // connection becomes app-limited, the sampler is notified via OnAppLimited() // method. // // Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth // sampler becomes notified about the connection being app-limited, it enters // app-limited phase. In that phase, all *sent* packets are marked as // app-limited. Note that the connection itself does not have to be // app-limited during the app-limited phase, and in fact it will not be // (otherwise how would it send packets?). The boolean flag below indicates // whether the sampler is in that phase. // // Meaning 3: a flag on the sent packet and on the sample. If a sent packet is // sent during the app-limited phase, the resulting sample related to the // packet will be marked as app-limited. // // With the terminology issue out of the way, let us consider the question of // what kind of situation it addresses. // // Consider a scenario where we first send packets 1 to 20 at a regular // bandwidth, and then immediately run out of data. After a few seconds, we send // packets 21 to 60, and only receive ack for 21 between sending packets 40 and // 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0 // we use to compute the slope is going to be packet 20, a few seconds apart // from the current packet, hence the resulting estimate would be extremely low // and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21, // meaning that the bandwidth sample would exclude the quiescence. // // Based on the analysis of that scenario, we implement the following rule: once // OnAppLimited() is called, all sent packets will produce app-limited samples // up until an ack for a packet that was sent after OnAppLimited() was called. // Note that while the scenario above is not the only scenario when the // connection is app-limited, the approach works in other cases too. type congestionEventSample struct { // The maximum bandwidth sample from all acked packets. // QuicBandwidth::Zero() if no samples are available. sampleMaxBandwidth Bandwidth // Whether |sample_max_bandwidth| is from a app-limited sample. sampleIsAppLimited bool // The minimum rtt sample from all acked packets. // QuicTime::Delta::Infinite() if no samples are available. sampleRtt time.Duration // For each packet p in acked packets, this is the max value of INFLIGHT(p), // where INFLIGHT(p) is the number of bytes acked while p is inflight. sampleMaxInflight congestion.ByteCount // The send state of the largest packet in acked_packets, unless it is // empty. If acked_packets is empty, it's the send state of the largest // packet in lost_packets. lastPacketSendState sendTimeState // The number of extra bytes acked from this ack event, compared to what is // expected from the flow's bandwidth. Larger value means more ack // aggregation. extraAcked congestion.ByteCount } func newCongestionEventSample() *congestionEventSample { return &congestionEventSample{ sampleRtt: infRTT, } } type bandwidthSampler struct { // The total number of congestion controlled bytes sent during the connection. totalBytesSent congestion.ByteCount // The total number of congestion controlled bytes which were acknowledged. totalBytesAcked congestion.ByteCount // The total number of congestion controlled bytes which were lost. totalBytesLost congestion.ByteCount // The total number of congestion controlled bytes which have been neutered. totalBytesNeutered congestion.ByteCount // The value of |total_bytes_sent_| at the time the last acknowledged packet // was sent. Valid only when |last_acked_packet_sent_time_| is valid. totalBytesSentAtLastAckedPacket congestion.ByteCount // The time at which the last acknowledged packet was sent. Set to // QuicTime::Zero() if no valid timestamp is available. lastAckedPacketSentTime time.Time // The time at which the most recent packet was acknowledged. lastAckedPacketAckTime time.Time // The most recently sent packet. lastSentPacket congestion.PacketNumber // The most recently acked packet. lastAckedPacket congestion.PacketNumber // Indicates whether the bandwidth sampler is currently in an app-limited // phase. isAppLimited bool // The packet that will be acknowledged after this one will cause the sampler // to exit the app-limited phase. endOfAppLimitedPhase congestion.PacketNumber // Record of the connection state at the point where each packet in flight was // sent, indexed by the packet number. connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket] recentAckPoints recentAckPoints a0Candidates RingBuffer[ackPoint] // Maximum number of tracked packets. maxTrackedPackets congestion.ByteCount maxAckHeightTracker *maxAckHeightTracker totalBytesAckedAfterLastAckEvent congestion.ByteCount // True if connection option 'BSAO' is set. overestimateAvoidance bool // True if connection option 'BBRB' is set. limitMaxAckHeightTrackerBySendRate bool } func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler { b := &bandwidthSampler{ maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength), connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize), lastSentPacket: invalidPacketNumber, lastAckedPacket: invalidPacketNumber, endOfAppLimitedPhase: invalidPacketNumber, } b.a0Candidates.Init(defaultCandidatesBufferSize) return b } func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount { return b.maxAckHeightTracker.Get() } func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 { return b.maxAckHeightTracker.NumAckAggregationEpochs() } func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) { b.maxAckHeightTracker.SetFilterWindowLength(length) } func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) { b.maxAckHeightTracker.Reset(newHeight, newTime) } func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) { b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value) } func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) { b.limitMaxAckHeightTrackerBySendRate = value } func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) { b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value) } func (b *bandwidthSampler) EnableOverestimateAvoidance() { if b.overestimateAvoidance { return } b.overestimateAvoidance = true b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0) } func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool { return b.overestimateAvoidance } func (b *bandwidthSampler) OnPacketSent( sentTime time.Time, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, bytesInFlight congestion.ByteCount, isRetransmittable bool, ) { b.lastSentPacket = packetNumber if !isRetransmittable { return } b.totalBytesSent += bytes // If there are no packets in flight, the time at which the new transmission // opens can be treated as the A_0 point for the purpose of bandwidth // sampling. This underestimates bandwidth to some extent, and produces some // artificially low samples for most packets in flight, but it provides with // samples at important points where we would not have them otherwise, most // importantly at the beginning of the connection. if bytesInFlight == 0 { b.lastAckedPacketAckTime = sentTime if b.overestimateAvoidance { b.recentAckPoints.Clear() b.recentAckPoints.Update(sentTime, b.totalBytesAcked) b.a0Candidates.Clear() b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint()) } b.totalBytesSentAtLastAckedPacket = b.totalBytesSent // In this situation ack compression is not a concern, set send rate to // effectively infinite. b.lastAckedPacketSentTime = sentTime } b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket( sentTime, bytes, bytesInFlight+bytes, b, )) } func (b *bandwidthSampler) OnCongestionEvent( ackTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo, maxBandwidth Bandwidth, estBandwidthUpperBound Bandwidth, roundTripCount roundTripCount, ) congestionEventSample { eventSample := newCongestionEventSample() var lastLostPacketSendState sendTimeState for _, p := range lostPackets { sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost) if sendState.isValid { lastLostPacketSendState = sendState } } if len(ackedPackets) == 0 { // Only populate send state for a loss-only event. eventSample.lastPacketSendState = lastLostPacketSendState return *eventSample } var lastAckedPacketSendState sendTimeState var maxSendRate Bandwidth for _, p := range ackedPackets { sample := b.onPacketAcknowledged(ackTime, p.PacketNumber) if !sample.stateAtSend.isValid { continue } lastAckedPacketSendState = sample.stateAtSend if sample.rtt != 0 { eventSample.sampleRtt = min(eventSample.sampleRtt, sample.rtt) } if sample.bandwidth > eventSample.sampleMaxBandwidth { eventSample.sampleMaxBandwidth = sample.bandwidth eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited } if sample.sendRate != infBandwidth { maxSendRate = max(maxSendRate, sample.sendRate) } inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked if inflightSample > eventSample.sampleMaxInflight { eventSample.sampleMaxInflight = inflightSample } } if !lastLostPacketSendState.isValid { eventSample.lastPacketSendState = lastAckedPacketSendState } else if !lastAckedPacketSendState.isValid { eventSample.lastPacketSendState = lastLostPacketSendState } else { // If two packets are inflight and an alarm is armed to lose a packet and it // wakes up late, then the first of two in flight packets could have been // acknowledged before the wakeup, which re-evaluates loss detection, and // could declare the later of the two lost. if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber { eventSample.lastPacketSendState = lastLostPacketSendState } else { eventSample.lastPacketSendState = lastAckedPacketSendState } } isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth maxBandwidth = max(maxBandwidth, eventSample.sampleMaxBandwidth) if b.limitMaxAckHeightTrackerBySendRate { maxBandwidth = max(maxBandwidth, maxSendRate) } eventSample.extraAcked = b.onAckEventEnd(min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount) return *eventSample } func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) { b.totalBytesLost += bytesLost if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil { sentPacketToSendTimeState(sentPacketPointer, &s) } return s } func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) { b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) { b.totalBytesNeutered += sentPacket.size }) } func (b *bandwidthSampler) OnAppLimited() { b.isAppLimited = true b.endOfAppLimitedPhase = b.lastSentPacket } func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) { // A packet can become obsolete when it is removed from QuicUnackedPacketMap's // view of inflight before it is acked or marked as lost. For example, when // QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet, // the packet is removed from QuicUnackedPacketMap's inflight, but is not // marked as acked or lost in the BandwidthSampler. b.connectionStateMap.RemoveUpTo(leastUnacked) } func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount { return b.totalBytesSent } func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount { return b.totalBytesLost } func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount { return b.totalBytesAcked } func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount { return b.totalBytesNeutered } func (b *bandwidthSampler) IsAppLimited() bool { return b.isAppLimited } func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber { return b.endOfAppLimitedPhase } func (b *bandwidthSampler) max_ack_height() congestion.ByteCount { return b.maxAckHeightTracker.Get() } func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool { if b.a0Candidates.Empty() { return false } if b.a0Candidates.Len() == 1 { *a0 = *b.a0Candidates.Front() return true } for i := 1; i < b.a0Candidates.Len(); i++ { if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked { *a0 = *b.a0Candidates.Offset(i - 1) if i > 1 { for j := 0; j < i-1; j++ { b.a0Candidates.PopFront() } } return true } } *a0 = *b.a0Candidates.Back() for k := 0; k < b.a0Candidates.Len()-1; k++ { b.a0Candidates.PopFront() } return true } func (b *bandwidthSampler) onPacketAcknowledged(ackTime time.Time, packetNumber congestion.PacketNumber) bandwidthSample { sample := newBandwidthSample() b.lastAckedPacket = packetNumber sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber) if sentPacketPointer == nil { return *sample } // OnPacketAcknowledgedInner b.totalBytesAcked += sentPacketPointer.size b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent b.lastAckedPacketSentTime = sentPacketPointer.sentTime b.lastAckedPacketAckTime = ackTime if b.overestimateAvoidance { b.recentAckPoints.Update(ackTime, b.totalBytesAcked) } if b.isAppLimited { // Exit app-limited phase in two cases: // (1) end_of_app_limited_phase_ is not initialized, i.e., so far all // packets are sent while there are buffered packets or pending data. // (2) The current acked packet is after the sent packet marked as the end // of the app limit phase. if b.endOfAppLimitedPhase == invalidPacketNumber || packetNumber > b.endOfAppLimitedPhase { b.isAppLimited = false } } // There might have been no packets acknowledged at the moment when the // current packet was sent. In that case, there is no bandwidth sample to // make. if sentPacketPointer.lastAckedPacketSentTime.IsZero() { return *sample } // Infinite rate indicates that the sampler is supposed to discard the // current send rate sample and use only the ack rate. sendRate := infBandwidth if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) { sendRate = BandwidthFromDelta( sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket, sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime)) } var a0 ackPoint if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) { } else { a0.ackTime = sentPacketPointer.lastAckedPacketAckTime a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked } // During the slope calculation, ensure that ack time of the current packet is // always larger than the time of the previous packet, otherwise division by // zero or integer underflow can occur. if ackTime.Sub(a0.ackTime) <= 0 { return *sample } ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime)) sample.bandwidth = min(sendRate, ackRate) // Note: this sample does not account for delayed acknowledgement time. This // means that the RTT measurements here can be artificially high, especially // on low bandwidth connections. sample.rtt = ackTime.Sub(sentPacketPointer.sentTime) sample.sendRate = sendRate sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend) return *sample } func (b *bandwidthSampler) onAckEventEnd( bandwidthEstimate Bandwidth, isNewMaxBandwidth bool, roundTripCount roundTripCount, ) congestion.ByteCount { newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent if newlyAckedBytes == 0 { return 0 } b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked extraAcked := b.maxAckHeightTracker.Update( bandwidthEstimate, isNewMaxBandwidth, roundTripCount, b.lastSentPacket, b.lastAckedPacket, b.lastAckedPacketAckTime, newlyAckedBytes) // If |extra_acked| is zero, i.e. this ack event marks the start of a new ack // aggregation epoch, save LessRecentPoint, which is the last ack point of the // previous epoch, as a A0 candidate. if b.overestimateAvoidance && extraAcked == 0 { b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint()) } return extraAcked } func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) { *sendTimeState = sentPacket.sendTimeState sendTimeState.isValid = true } // BytesFromBandwidthAndTimeDelta calculates the bytes // from a bandwidth(bits per second) and a time delta func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount { return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) / (congestion.ByteCount(time.Second) * 8) } func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration { return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth) } ================================================ FILE: core/internal/congestion/bbr/bbr_sender.go ================================================ package bbr import ( "fmt" "math/rand" "net" "os" "strconv" "time" "github.com/apernet/quic-go/congestion" "github.com/apernet/hysteria/core/v2/internal/congestion/common" ) // BbrSender implements BBR congestion control algorithm. BBR aims to estimate // the current available Bottleneck Bandwidth and RTT (hence the name), and // regulates the pacing rate and the size of the congestion window based on // those signals. // // BBR relies on pacing in order to function properly. Do not use BBR when // pacing is disabled. // const ( minBps = 65536 // 64 kbps invalidPacketNumber = -1 initialCongestionWindowPackets = 32 // Constants based on TCP defaults. // The minimum CWND to ensure delayed acks don't reduce bandwidth measurements. // Does not inflate the pacing rate. defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSizeIPv4) // The gain used for the STARTUP, equal to 2/ln(2). defaultHighGain = 2.885 // The newly derived gain for STARTUP, equal to 4 * ln(2) derivedHighGain = 2.773 // The newly derived CWND gain for STARTUP, 2. derivedHighCWNDGain = 2.0 debugEnv = "HYSTERIA_BBR_DEBUG" ) // The cycle of gains used during the PROBE_BW stage. var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0} const ( // The length of the gain cycle. gainCycleLength = len(pacingGain) // The size of the bandwidth filter window, in round-trips. bandwidthWindowSize = gainCycleLength + 2 // The time after which the current min_rtt value expires. minRttExpiry = 10 * time.Second // The minimum time the connection can spend in PROBE_RTT mode. probeRttTime = 200 * time.Millisecond // If the bandwidth does not increase by the factor of |kStartupGrowthTarget| // within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection // will exit the STARTUP mode. startupGrowthTarget = 1.25 roundTripsWithoutGrowthBeforeExitingStartup = int64(3) // Flag. defaultStartupFullLossCount = 8 quicBbr2DefaultLossThreshold = 0.02 maxBbrBurstPackets = 10 ) type bbrMode int const ( // Startup phase of the connection. bbrModeStartup = iota // After achieving the highest possible bandwidth during the startup, lower // the pacing rate in order to drain the queue. bbrModeDrain // Cruising mode. bbrModeProbeBw // Temporarily slow down sending in order to empty the buffer and measure // the real minimum RTT. bbrModeProbeRtt ) // Indicates how the congestion control limits the amount of bytes in flight. type bbrRecoveryState int const ( // Do not limit. bbrRecoveryStateNotInRecovery = iota // Allow an extra outstanding byte for each byte acknowledged. bbrRecoveryStateConservation // Allow two extra outstanding bytes for each byte acknowledged (slow // start). bbrRecoveryStateGrowth ) type bbrSender struct { rttStats congestion.RTTStatsProvider clock Clock pacer *common.Pacer mode bbrMode // Bandwidth sampler provides BBR with the bandwidth measurements at // individual points. sampler *bandwidthSampler // The number of the round trips that have occurred during the connection. roundTripCount roundTripCount // The packet number of the most recently sent packet. lastSentPacket congestion.PacketNumber // Acknowledgement of any packet after |current_round_trip_end_| will cause // the round trip counter to advance. currentRoundTripEnd congestion.PacketNumber // Number of congestion events with some losses, in the current round. numLossEventsInRound uint64 // Number of total bytes lost in the current round. bytesLostInRound congestion.ByteCount // The filter that tracks the maximum bandwidth over the multiple recent // round-trips. maxBandwidth *WindowedFilter[Bandwidth, roundTripCount] // Minimum RTT estimate. Automatically expires within 10 seconds (and // triggers PROBE_RTT mode) if no new value is sampled during that period. minRtt time.Duration // The time at which the current value of |min_rtt_| was assigned. minRttTimestamp time.Time // The maximum allowed number of bytes in flight. congestionWindow congestion.ByteCount // The initial value of the |congestion_window_|. initialCongestionWindow congestion.ByteCount // The largest value the |congestion_window_| can achieve. maxCongestionWindow congestion.ByteCount // The smallest value the |congestion_window_| can achieve. minCongestionWindow congestion.ByteCount // The pacing gain applied during the STARTUP phase. highGain float64 // The CWND gain applied during the STARTUP phase. highCwndGain float64 // The pacing gain applied during the DRAIN phase. drainGain float64 // The current pacing rate of the connection. pacingRate Bandwidth // The gain currently applied to the pacing rate. pacingGain float64 // The gain currently applied to the congestion window. congestionWindowGain float64 // The gain used for the congestion window during PROBE_BW. Latched from // quic_bbr_cwnd_gain flag. congestionWindowGainConstant float64 // The number of RTTs to stay in STARTUP mode. Defaults to 3. numStartupRtts int64 // Number of round-trips in PROBE_BW mode, used for determining the current // pacing gain cycle. cycleCurrentOffset int // The time at which the last pacing gain cycle was started. lastCycleStart time.Time // Indicates whether the connection has reached the full bandwidth mode. isAtFullBandwidth bool // Number of rounds during which there was no significant bandwidth increase. roundsWithoutBandwidthGain int64 // The bandwidth compared to which the increase is measured. bandwidthAtLastRound Bandwidth // Set to true upon exiting quiescence. exitingQuiescence bool // Time at which PROBE_RTT has to be exited. Setting it to zero indicates // that the time is yet unknown as the number of packets in flight has not // reached the required value. exitProbeRttAt time.Time // Indicates whether a round-trip has passed since PROBE_RTT became active. probeRttRoundPassed bool // Indicates whether the most recent bandwidth sample was marked as // app-limited. lastSampleIsAppLimited bool // Indicates whether any non app-limited samples have been recorded. hasNoAppLimitedSample bool // Current state of recovery. recoveryState bbrRecoveryState // Receiving acknowledgement of a packet after |end_recovery_at_| will cause // BBR to exit the recovery mode. A value above zero indicates at least one // loss has been detected, so it must not be set back to zero. endRecoveryAt congestion.PacketNumber // A window used to limit the number of bytes in flight during loss recovery. recoveryWindow congestion.ByteCount // If true, consider all samples in recovery app-limited. isAppLimitedRecovery bool // not used // When true, pace at 1.5x and disable packet conservation in STARTUP. slowerStartup bool // not used // When true, disables packet conservation in STARTUP. rateBasedStartup bool // not used // When true, add the most recent ack aggregation measurement during STARTUP. enableAckAggregationDuringStartup bool // When true, expire the windowed ack aggregation values in STARTUP when // bandwidth increases more than 25%. expireAckAggregationInStartup bool // If true, will not exit low gain mode until bytes_in_flight drops below BDP // or it's time for high gain mode. drainToTarget bool // If true, slow down pacing rate in STARTUP when overshooting is detected. detectOvershooting bool // Bytes lost while detect_overshooting_ is true. bytesLostWhileDetectingOvershooting congestion.ByteCount // Slow down pacing rate if // bytes_lost_while_detecting_overshooting_ * // bytes_lost_multiplier_while_detecting_overshooting_ > IW. bytesLostMultiplierWhileDetectingOvershooting uint8 // When overshooting is detected, do not drop pacing_rate_ below this value / // min_rtt. cwndToCalculateMinPacingRate congestion.ByteCount // Max congestion window when adjusting network parameters. maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used // Params. maxDatagramSize congestion.ByteCount // Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()| bytesInFlight congestion.ByteCount debug bool } var _ congestion.CongestionControl = &bbrSender{} func NewBbrSender( clock Clock, initialMaxDatagramSize congestion.ByteCount, ) *bbrSender { return newBbrSender( clock, initialMaxDatagramSize, initialCongestionWindowPackets*initialMaxDatagramSize, congestion.MaxCongestionWindowPackets*initialMaxDatagramSize, ) } func newBbrSender( clock Clock, initialMaxDatagramSize, initialCongestionWindow, initialMaxCongestionWindow congestion.ByteCount, ) *bbrSender { debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) b := &bbrSender{ clock: clock, mode: bbrModeStartup, sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)), lastSentPacket: invalidPacketNumber, currentRoundTripEnd: invalidPacketNumber, maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]), congestionWindow: initialCongestionWindow, initialCongestionWindow: initialCongestionWindow, maxCongestionWindow: initialMaxCongestionWindow, minCongestionWindow: defaultMinimumCongestionWindow, highGain: defaultHighGain, highCwndGain: defaultHighGain, drainGain: 1.0 / defaultHighGain, pacingGain: 1.0, congestionWindowGain: 1.0, congestionWindowGainConstant: 2.0, numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup, recoveryState: bbrRecoveryStateNotInRecovery, endRecoveryAt: invalidPacketNumber, recoveryWindow: initialMaxCongestionWindow, bytesLostMultiplierWhileDetectingOvershooting: 2, cwndToCalculateMinPacingRate: initialCongestionWindow, maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow, maxDatagramSize: initialMaxDatagramSize, debug: debug, } b.pacer = common.NewPacer(b.bandwidthForPacer) /* if b.tracer != nil { b.lastState = logging.CongestionStateStartup b.tracer.UpdatedCongestionState(logging.CongestionStateStartup) } */ b.enterStartupMode(b.clock.Now()) b.setHighCwndGain(derivedHighCWNDGain) return b } func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) { b.rttStats = provider } // TimeUntilSend implements the SendAlgorithm interface. func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { return b.pacer.TimeUntilSend() } // HasPacingBudget implements the SendAlgorithm interface. func (b *bbrSender) HasPacingBudget(now time.Time) bool { return b.pacer.Budget(now) >= b.maxDatagramSize } // OnPacketSent implements the SendAlgorithm interface. func (b *bbrSender) OnPacketSent( sentTime time.Time, bytesInFlight congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, ) { b.pacer.SentPacket(sentTime, bytes) b.lastSentPacket = packetNumber b.bytesInFlight = bytesInFlight if bytesInFlight == 0 { b.exitingQuiescence = true } b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable) } // CanSend implements the SendAlgorithm interface. func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool { return bytesInFlight < b.GetCongestionWindow() } // MaybeExitSlowStart implements the SendAlgorithm interface. func (b *bbrSender) MaybeExitSlowStart() { // Do nothing } // OnPacketAcked implements the SendAlgorithm interface. func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime time.Time) { // Do nothing. } // OnPacketLost implements the SendAlgorithm interface. func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { // Do nothing. } // OnRetransmissionTimeout implements the SendAlgorithm interface. func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) { // Do nothing. } // SetMaxDatagramSize implements the SendAlgorithm interface. func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) { if s < b.maxDatagramSize { panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s)) } cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow b.maxDatagramSize = s if cwndIsMinCwnd { b.congestionWindow = b.minCongestionWindow } b.pacer.SetMaxDatagramSize(s) } // InSlowStart implements the SendAlgorithmWithDebugInfos interface. func (b *bbrSender) InSlowStart() bool { return b.mode == bbrModeStartup } // InRecovery implements the SendAlgorithmWithDebugInfos interface. func (b *bbrSender) InRecovery() bool { return b.recoveryState != bbrRecoveryStateNotInRecovery } // GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface. func (b *bbrSender) GetCongestionWindow() congestion.ByteCount { if b.mode == bbrModeProbeRtt { return b.probeRttCongestionWindow() } if b.InRecovery() { return min(b.congestionWindow, b.recoveryWindow) } return b.congestionWindow } func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) { // Do nothing. } func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { totalBytesAckedBefore := b.sampler.TotalBytesAcked() totalBytesLostBefore := b.sampler.TotalBytesLost() var isRoundStart, minRttExpired bool var excessAcked, bytesLost congestion.ByteCount // The send state of the largest packet in acked_packets, unless it is // empty. If acked_packets is empty, it's the send state of the largest // packet in lost_packets. var lastPacketSendState sendTimeState b.maybeAppLimited(priorInFlight) // Update bytesInFlight b.bytesInFlight = priorInFlight for _, p := range ackedPackets { b.bytesInFlight -= p.BytesAcked } for _, p := range lostPackets { b.bytesInFlight -= p.BytesLost } if len(ackedPackets) != 0 { lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber isRoundStart = b.updateRoundTripCounter(lastAckedPacket) b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart) } sample := b.sampler.OnCongestionEvent(eventTime, ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount) if sample.lastPacketSendState.isValid { b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited } // Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all // packets in |acked_packets| did not generate valid samples. (e.g. ack of // ack-only packets). In both cases, sampler_.total_bytes_acked() will not // change. if totalBytesAckedBefore != b.sampler.TotalBytesAcked() { if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() { b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount) } } if sample.sampleRtt != infRTT { minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt) } bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore excessAcked = sample.extraAcked lastPacketSendState = sample.lastPacketSendState if len(lostPackets) != 0 { b.numLossEventsInRound++ b.bytesLostInRound += bytesLost } // Handle logic specific to PROBE_BW mode. if b.mode == bbrModeProbeBw { b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0) } // Handle logic specific to STARTUP and DRAIN modes. if isRoundStart && !b.isAtFullBandwidth { b.checkIfFullBandwidthReached(&lastPacketSendState) } b.maybeExitStartupOrDrain(eventTime) // Handle logic specific to PROBE_RTT. b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired) // Calculate number of packets acked and lost. bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore // After the model is updated, recalculate the pacing rate and congestion // window. b.calculatePacingRate(bytesLost) b.calculateCongestionWindow(bytesAcked, excessAcked) b.calculateRecoveryWindow(bytesAcked, bytesLost) // Cleanup internal state. // This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler. // The "least unacked" should actually be FirstOutstanding, but since we are not passing // that through OnCongestionEventEx, we will only do an estimate using acked/lost packets // for now. Because of fast retransmission, they should differ by no more than 2 packets. // (this is controlled by packetThreshold in quic-go's sentPacketHandler) var leastUnacked congestion.PacketNumber if len(ackedPackets) != 0 { leastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2 } else { leastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1 } b.sampler.RemoveObsoletePackets(leastUnacked) if isRoundStart { b.numLossEventsInRound = 0 b.bytesLostInRound = 0 } } func (b *bbrSender) PacingRate() Bandwidth { if b.pacingRate == 0 { return Bandwidth(b.highGain * float64( BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt()))) } return b.pacingRate } func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool { return b.hasNonAppLimitedSample() } func (b *bbrSender) hasNonAppLimitedSample() bool { return b.hasNoAppLimitedSample } // Sets the pacing gain used in STARTUP. Must be greater than 1. func (b *bbrSender) setHighGain(highGain float64) { b.highGain = highGain if b.mode == bbrModeStartup { b.pacingGain = highGain } } // Sets the CWND gain used in STARTUP. Must be greater than 1. func (b *bbrSender) setHighCwndGain(highCwndGain float64) { b.highCwndGain = highCwndGain if b.mode == bbrModeStartup { b.congestionWindowGain = highCwndGain } } // Sets the gain used in DRAIN. Must be less than 1. func (b *bbrSender) setDrainGain(drainGain float64) { b.drainGain = drainGain } // Get the current bandwidth estimate. Note that Bandwidth is in bits per second. func (b *bbrSender) bandwidthEstimate() Bandwidth { return b.maxBandwidth.GetBest() } func (b *bbrSender) bandwidthForPacer() congestion.ByteCount { bps := congestion.ByteCount(float64(b.bandwidthEstimate()) * b.congestionWindowGain / float64(BytesPerSecond)) if bps < minBps { // We need to make sure that the bandwidth value for pacer is never zero, // otherwise it will go into an edge case where HasPacingBudget = false // but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck. return minBps } return bps } // Returns the current estimate of the RTT of the connection. Outside of the // edge cases, this is minimum RTT. func (b *bbrSender) getMinRtt() time.Duration { if b.minRtt != 0 { return b.minRtt } // min_rtt could be available if the handshake packet gets neutered then // gets acknowledged. This could only happen for QUIC crypto where we do not // drop keys. minRtt := b.rttStats.MinRTT() if minRtt == 0 { return 100 * time.Millisecond } else { return minRtt } } // Computes the target congestion window using the specified gain. func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount { bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate()) congestionWindow := congestion.ByteCount(gain * float64(bdp)) // BDP estimate will be zero if no bandwidth samples are available yet. if congestionWindow == 0 { congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow)) } return max(congestionWindow, b.minCongestionWindow) } // The target congestion window during PROBE_RTT. func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount { return b.minCongestionWindow } func (b *bbrSender) maybeUpdateMinRtt(now time.Time, sampleMinRtt time.Duration) bool { // Do not expire min_rtt if none was ever available. minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry)) if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 { b.minRtt = sampleMinRtt b.minRttTimestamp = now } return minRttExpired } // Enters the STARTUP mode. func (b *bbrSender) enterStartupMode(now time.Time) { b.mode = bbrModeStartup // b.maybeTraceStateChange(logging.CongestionStateStartup) b.pacingGain = b.highGain b.congestionWindowGain = b.highCwndGain if b.debug { b.debugPrint("Phase: STARTUP") } } // Enters the PROBE_BW mode. func (b *bbrSender) enterProbeBandwidthMode(now time.Time) { b.mode = bbrModeProbeBw // b.maybeTraceStateChange(logging.CongestionStateProbeBw) b.congestionWindowGain = b.congestionWindowGainConstant // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is // excluded because in that case increased gain and decreased gain would not // follow each other. b.cycleCurrentOffset = int(rand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1) if b.cycleCurrentOffset >= 1 { b.cycleCurrentOffset += 1 } b.lastCycleStart = now b.pacingGain = pacingGain[b.cycleCurrentOffset] if b.debug { b.debugPrint("Phase: PROBE_BW") } } // Updates the round-trip counter if a round-trip has passed. Returns true if // the counter has been advanced. func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool { if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd { b.roundTripCount++ b.currentRoundTripEnd = b.lastSentPacket return true } return false } // Updates the current gain used in PROBE_BW mode. func (b *bbrSender) updateGainCyclePhase(now time.Time, priorInFlight congestion.ByteCount, hasLosses bool) { // In most cases, the cycle is advanced after an RTT passes. shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt())) // If the pacing gain is above 1.0, the connection is trying to probe the // bandwidth by increasing the number of bytes in flight to at least // pacing_gain * BDP. Make sure that it actually reaches the target, as long // as there are no losses suggesting that the buffers are not able to hold // that much. if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) { shouldAdvanceGainCycling = false } // If pacing gain is below 1.0, the connection is trying to drain the extra // queue which could have been incurred by probing prior to it. If the number // of bytes in flight falls down to the estimated BDP value earlier, conclude // that the queue has been successfully drained and exit this cycle early. if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) { shouldAdvanceGainCycling = true } if shouldAdvanceGainCycling { b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength b.lastCycleStart = now // Stay in low gain mode until the target BDP is hit. // Low gain mode will be exited immediately when the target BDP is achieved. if b.drainToTarget && b.pacingGain < 1 && pacingGain[b.cycleCurrentOffset] == 1 && b.bytesInFlight > b.getTargetCongestionWindow(1) { return } b.pacingGain = pacingGain[b.cycleCurrentOffset] } } // Tracks for how many round-trips the bandwidth has not increased // significantly. func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) { if b.lastSampleIsAppLimited { return } target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget) if b.bandwidthEstimate() >= target { b.bandwidthAtLastRound = b.bandwidthEstimate() b.roundsWithoutBandwidthGain = 0 if b.expireAckAggregationInStartup { // Expire old excess delivery measurements now that bandwidth increased. b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount) } return } b.roundsWithoutBandwidthGain++ if b.roundsWithoutBandwidthGain >= b.numStartupRtts || b.shouldExitStartupDueToLoss(lastPacketSendState) { b.isAtFullBandwidth = true } } func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) { if bytesInFlight < b.getTargetCongestionWindow(1) { b.sampler.OnAppLimited() } } // Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if // appropriate. func (b *bbrSender) maybeExitStartupOrDrain(now time.Time) { if b.mode == bbrModeStartup && b.isAtFullBandwidth { b.mode = bbrModeDrain // b.maybeTraceStateChange(logging.CongestionStateDrain) b.pacingGain = b.drainGain b.congestionWindowGain = b.highCwndGain if b.debug { b.debugPrint("Phase: DRAIN") } } if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) { b.enterProbeBandwidthMode(now) } } // Decides whether to enter or exit PROBE_RTT. func (b *bbrSender) maybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRttExpired bool) { if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt { b.mode = bbrModeProbeRtt // b.maybeTraceStateChange(logging.CongestionStateProbRtt) b.pacingGain = 1.0 // Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight| // is at the target small value. b.exitProbeRttAt = time.Time{} if b.debug { b.debugPrint("BandwidthEstimate: %s, CongestionWindowGain: %.2f, PacingGain: %.2f, PacingRate: %s", formatSpeed(b.bandwidthEstimate()), b.congestionWindowGain, b.pacingGain, formatSpeed(b.PacingRate())) b.debugPrint("Phase: PROBE_RTT") } } if b.mode == bbrModeProbeRtt { b.sampler.OnAppLimited() // b.maybeTraceStateChange(logging.CongestionStateApplicationLimited) if b.exitProbeRttAt.IsZero() { // If the window has reached the appropriate size, schedule exiting // PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but // we allow an extra packet since QUIC checks CWND before sending a // packet. if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize { b.exitProbeRttAt = now.Add(probeRttTime) b.probeRttRoundPassed = false } } else { if isRoundStart { b.probeRttRoundPassed = true } if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed { b.minRttTimestamp = now if b.debug { b.debugPrint("MinRTT: %s", b.getMinRtt()) } if !b.isAtFullBandwidth { b.enterStartupMode(now) } else { b.enterProbeBandwidthMode(now) } } } } b.exitingQuiescence = false } // Determines whether BBR needs to enter, exit or advance state of the // recovery. func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) { // Disable recovery in startup, if loss-based exit is enabled. if !b.isAtFullBandwidth { return } // Exit recovery when there are no losses for a round. if hasLosses { b.endRecoveryAt = b.lastSentPacket } switch b.recoveryState { case bbrRecoveryStateNotInRecovery: if hasLosses { b.recoveryState = bbrRecoveryStateConservation // This will cause the |recovery_window_| to be set to the correct // value in CalculateRecoveryWindow(). b.recoveryWindow = 0 // Since the conservation phase is meant to be lasting for a whole // round, extend the current round as if it were started right now. b.currentRoundTripEnd = b.lastSentPacket } case bbrRecoveryStateConservation: if isRoundStart { b.recoveryState = bbrRecoveryStateGrowth } fallthrough case bbrRecoveryStateGrowth: // Exit recovery if appropriate. if !hasLosses && lastAckedPacket > b.endRecoveryAt { b.recoveryState = bbrRecoveryStateNotInRecovery } } } // Determines the appropriate pacing rate for the connection. func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) { if b.bandwidthEstimate() == 0 { return } targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate())) if b.isAtFullBandwidth { b.pacingRate = targetRate return } // Pace at the rate of initial_window / RTT as soon as RTT measurements are // available. if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 { b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT()) return } if b.detectOvershooting { b.bytesLostWhileDetectingOvershooting += bytesLost // Check for overshooting with network parameters adjusted when pacing rate // > target_rate and loss has been detected. if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 { if b.hasNoAppLimitedSample || b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow { // We are fairly sure overshoot happens if 1) there is at least one // non app-limited bw sample or 2) half of IW gets lost. Slow pacing // rate. b.pacingRate = max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT())) b.bytesLostWhileDetectingOvershooting = 0 b.detectOvershooting = false } } } // Do not decrease the pacing rate during startup. b.pacingRate = max(b.pacingRate, targetRate) } // Determines the appropriate congestion window for the connection. func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) { if b.mode == bbrModeProbeRtt { return } targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain) if b.isAtFullBandwidth { // Add the max recently measured ack aggregation to CWND. targetWindow += b.sampler.MaxAckHeight() } else if b.enableAckAggregationDuringStartup { // Add the most recent excess acked. Because CWND never decreases in // STARTUP, this will automatically create a very localized max filter. targetWindow += excessAcked } // Instead of immediately setting the target CWND as the new one, BBR grows // the CWND towards |target_window| by only increasing it |bytes_acked| at a // time. if b.isAtFullBandwidth { b.congestionWindow = min(targetWindow, b.congestionWindow+bytesAcked) } else if b.congestionWindow < targetWindow || b.sampler.TotalBytesAcked() < b.initialCongestionWindow { // If the connection is not yet out of startup phase, do not decrease the // window. b.congestionWindow += bytesAcked } // Enforce the limits on the congestion window. b.congestionWindow = max(b.congestionWindow, b.minCongestionWindow) b.congestionWindow = min(b.congestionWindow, b.maxCongestionWindow) } // Determines the appropriate window that constrains the in-flight during recovery. func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) { if b.recoveryState == bbrRecoveryStateNotInRecovery { return } // Set up the initial recovery window. if b.recoveryWindow == 0 { b.recoveryWindow = b.bytesInFlight + bytesAcked b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) return } // Remove losses from the recovery window, while accounting for a potential // integer underflow. if b.recoveryWindow >= bytesLost { b.recoveryWindow = b.recoveryWindow - bytesLost } else { b.recoveryWindow = b.maxDatagramSize } // In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH, // release additional |bytes_acked| to achieve a slow-start-like behavior. if b.recoveryState == bbrRecoveryStateGrowth { b.recoveryWindow += bytesAcked } // Always allow sending at least |bytes_acked| in response. b.recoveryWindow = max(b.recoveryWindow, b.bytesInFlight+bytesAcked) b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow) } // Return whether we should exit STARTUP due to excessive loss. func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool { if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid { return false } inflightAtSend := lastPacketSendState.bytesInFlight if inflightAtSend > 0 && b.bytesLostInRound > 0 { if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) { return true } return false } return false } func (b *bbrSender) debugPrint(format string, a ...any) { fmt.Printf("[BBRSender] [%s] %s\n", time.Now().Format("15:04:05"), fmt.Sprintf(format, a...)) } func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount { return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second) } func GetInitialPacketSize(addr net.Addr) congestion.ByteCount { // If this is not a UDP address, we don't know anything about the MTU. // Use the minimum size of an Initial packet as the max packet size. if udpAddr, ok := addr.(*net.UDPAddr); ok { if udpAddr.IP.To4() != nil { return congestion.InitialPacketSizeIPv4 } else { return congestion.InitialPacketSizeIPv6 } } else { return congestion.MinInitialPacketSize } } func formatSpeed(bw Bandwidth) string { bwf := float64(bw) units := []string{"bps", "Kbps", "Mbps", "Gbps"} unitIndex := 0 for bwf > 1000 && unitIndex < len(units)-1 { bwf /= 1000 unitIndex++ } return fmt.Sprintf("%.2f %s", bwf, units[unitIndex]) } ================================================ FILE: core/internal/congestion/bbr/clock.go ================================================ package bbr import "time" // A Clock returns the current time type Clock interface { Now() time.Time } // DefaultClock implements the Clock interface using the Go stdlib clock. type DefaultClock struct{} var _ Clock = DefaultClock{} // Now gets the current time func (DefaultClock) Now() time.Time { return time.Now() } ================================================ FILE: core/internal/congestion/bbr/packet_number_indexed_queue.go ================================================ package bbr import ( "github.com/apernet/quic-go/congestion" ) // packetNumberIndexedQueue is a queue of mostly continuous numbered entries // which supports the following operations: // - adding elements to the end of the queue, or at some point past the end // - removing elements in any order // - retrieving elements // If all elements are inserted in order, all of the operations above are // amortized O(1) time. // // Internally, the data structure is a deque where each element is marked as // present or not. The deque starts at the lowest present index. Whenever an // element is removed, it's marked as not present, and the front of the deque is // cleared of elements that are not present. // // The tail of the queue is not cleared due to the assumption of entries being // inserted in order, though removing all elements of the queue will return it // to its initial state. // // Note that this data structure is inherently hazardous, since an addition of // just two entries will cause it to consume all of the memory available. // Because of that, it is not a general-purpose container and should not be used // as one. type entryWrapper[T any] struct { present bool entry T } type packetNumberIndexedQueue[T any] struct { entries RingBuffer[entryWrapper[T]] numberOfPresentEntries int firstPacket congestion.PacketNumber } func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] { q := &packetNumberIndexedQueue[T]{ firstPacket: invalidPacketNumber, } q.entries.Init(size) return q } // Emplace inserts data associated |packet_number| into (or past) the end of the // queue, filling up the missing intermediate entries as necessary. Returns // true if the element has been inserted successfully, false if it was already // in the queue or inserted out of order. func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool { if packetNumber == invalidPacketNumber || entry == nil { return false } if p.IsEmpty() { p.entries.PushBack(entryWrapper[T]{ present: true, entry: *entry, }) p.numberOfPresentEntries = 1 p.firstPacket = packetNumber return true } // Do not allow insertion out-of-order. if packetNumber <= p.LastPacket() { return false } // Handle potentially missing elements. offset := int(packetNumber - p.FirstPacket()) if gap := offset - p.entries.Len(); gap > 0 { for i := 0; i < gap; i++ { p.entries.PushBack(entryWrapper[T]{}) } } p.entries.PushBack(entryWrapper[T]{ present: true, entry: *entry, }) p.numberOfPresentEntries++ return true } // GetEntry Retrieve the entry associated with the packet number. Returns the pointer // to the entry in case of success, or nullptr if the entry does not exist. func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T { ew := p.getEntryWraper(packetNumber) if ew == nil { return nil } return &ew.entry } // Remove, Same as above, but if an entry is present in the queue, also call f(entry) // before removing it. func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool { ew := p.getEntryWraper(packetNumber) if ew == nil { return false } if f != nil { f(ew.entry) } ew.present = false p.numberOfPresentEntries-- if packetNumber == p.FirstPacket() { p.clearup() } return true } // RemoveUpTo, but not including |packet_number|. // Unused slots in the front are also removed, which means when the function // returns, |first_packet()| can be larger than |packet_number|. func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) { for !p.entries.Empty() && p.firstPacket != invalidPacketNumber && p.firstPacket < packetNumber { if p.entries.Front().present { p.numberOfPresentEntries-- } p.entries.PopFront() p.firstPacket++ } p.clearup() return } // IsEmpty return if queue is empty. func (p *packetNumberIndexedQueue[T]) IsEmpty() bool { return p.numberOfPresentEntries == 0 } // NumberOfPresentEntries returns the number of entries in the queue. func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int { return p.numberOfPresentEntries } // EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is // proportional to the memory usage of the queue. func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int { return p.entries.Len() } // LastPacket returns packet number of the first entry in the queue. func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) { return p.firstPacket } // LastPacket returns packet number of the last entry ever inserted in the queue. Note that the // entry in question may have already been removed. Zero if the queue is // empty. func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) { if p.IsEmpty() { return invalidPacketNumber } return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1) } func (p *packetNumberIndexedQueue[T]) clearup() { for !p.entries.Empty() && !p.entries.Front().present { p.entries.PopFront() p.firstPacket++ } if p.entries.Empty() { p.firstPacket = invalidPacketNumber } } func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] { if packetNumber == invalidPacketNumber || p.IsEmpty() || packetNumber < p.firstPacket { return nil } offset := int(packetNumber - p.firstPacket) if offset >= p.entries.Len() { return nil } ew := p.entries.Offset(offset) if ew == nil || !ew.present { return nil } return ew } ================================================ FILE: core/internal/congestion/bbr/ringbuffer.go ================================================ package bbr // A RingBuffer is a ring buffer. // It acts as a heap that doesn't cause any allocations. type RingBuffer[T any] struct { ring []T headPos, tailPos int full bool } // Init preallocs a buffer with a certain size. func (r *RingBuffer[T]) Init(size int) { r.ring = make([]T, size) } // Len returns the number of elements in the ring buffer. func (r *RingBuffer[T]) Len() int { if r.full { return len(r.ring) } if r.tailPos >= r.headPos { return r.tailPos - r.headPos } return r.tailPos - r.headPos + len(r.ring) } // Empty says if the ring buffer is empty. func (r *RingBuffer[T]) Empty() bool { return !r.full && r.headPos == r.tailPos } // PushBack adds a new element. // If the ring buffer is full, its capacity is increased first. func (r *RingBuffer[T]) PushBack(t T) { if r.full || len(r.ring) == 0 { r.grow() } r.ring[r.tailPos] = t r.tailPos++ if r.tailPos == len(r.ring) { r.tailPos = 0 } if r.tailPos == r.headPos { r.full = true } } // PopFront returns the next element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first. func (r *RingBuffer[T]) PopFront() T { if r.Empty() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue") } r.full = false t := r.ring[r.headPos] r.ring[r.headPos] = *new(T) r.headPos++ if r.headPos == len(r.ring) { r.headPos = 0 } return t } // Offset returns the offset element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first // and check if the index larger than buffer length. func (r *RingBuffer[T]) Offset(index int) *T { if r.Empty() || index >= r.Len() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index") } offset := (r.headPos + index) % len(r.ring) return &r.ring[offset] } // Front returns the front element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first. func (r *RingBuffer[T]) Front() *T { if r.Empty() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue") } return &r.ring[r.headPos] } // Back returns the back element. // It must not be called when the buffer is empty, that means that // callers might need to check if there are elements in the buffer first. func (r *RingBuffer[T]) Back() *T { if r.Empty() { panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue") } return r.Offset(r.Len() - 1) } // Grow the maximum size of the queue. // This method assume the queue is full. func (r *RingBuffer[T]) grow() { oldRing := r.ring newSize := len(oldRing) * 2 if newSize == 0 { newSize = 1 } r.ring = make([]T, newSize) headLen := copy(r.ring, oldRing[r.headPos:]) copy(r.ring[headLen:], oldRing[:r.headPos]) r.headPos, r.tailPos, r.full = 0, len(oldRing), false } // Clear removes all elements. func (r *RingBuffer[T]) Clear() { var zeroValue T for i := range r.ring { r.ring[i] = zeroValue } r.headPos, r.tailPos, r.full = 0, 0, false } ================================================ FILE: core/internal/congestion/bbr/windowed_filter.go ================================================ package bbr import ( "golang.org/x/exp/constraints" ) // Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum) // estimate of a stream of samples over some fixed time interval. (E.g., // the minimum RTT over the past five minutes.) The algorithm keeps track of // the best, second best, and third best min (or max) estimates, maintaining an // invariant that the measurement time of the n'th best >= n-1'th best. // The algorithm works as follows. On a reset, all three estimates are set to // the same sample. The second best estimate is then recorded in the second // quarter of the window, and a third best estimate is recorded in the second // half of the window, bounding the worst case error when the true min is // monotonically increasing (or true max is monotonically decreasing) over the // window. // // A new best sample replaces all three estimates, since the new best is lower // (or higher) than everything else in the window and it is the most recent. // The window thus effectively gets reset on every new min. The same property // holds true for second best and third best estimates. Specifically, when a // sample arrives that is better than the second best but not better than the // best, it replaces the second and third best estimates but not the best // estimate. Similarly, a sample that is better than the third best estimate // but not the other estimates replaces only the third best estimate. // // Finally, when the best expires, it is replaced by the second best, which in // turn is replaced by the third best. The newest sample replaces the third // best. type WindowedFilterValue interface { any } type WindowedFilterTime interface { constraints.Integer | constraints.Float } type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct { // Time length of window. windowLength T estimates []entry[V, T] comparator func(V, V) int } type entry[V WindowedFilterValue, T WindowedFilterTime] struct { sample V time T } // Compares two values and returns true if the first is greater than or equal // to the second. func MaxFilter[O constraints.Ordered](a, b O) int { if a > b { return 1 } else if a < b { return -1 } return 0 } // Compares two values and returns true if the first is less than or equal // to the second. func MinFilter[O constraints.Ordered](a, b O) int { if a < b { return 1 } else if a > b { return -1 } return 0 } func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] { return &WindowedFilter[V, T]{ windowLength: windowLength, estimates: make([]entry[V, T], 3, 3), comparator: comparator, } } // Changes the window length. Does not update any current samples. func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) { f.windowLength = windowLength } func (f *WindowedFilter[V, T]) GetBest() V { return f.estimates[0].sample } func (f *WindowedFilter[V, T]) GetSecondBest() V { return f.estimates[1].sample } func (f *WindowedFilter[V, T]) GetThirdBest() V { return f.estimates[2].sample } // Updates best estimates with |sample|, and expires and updates best // estimates as necessary. func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) { // Reset all estimates if they have not yet been initialized, if new sample // is a new best, or if the newest recorded estimate is too old. if f.comparator(f.estimates[0].sample, *new(V)) == 0 || f.comparator(newSample, f.estimates[0].sample) >= 0 || newTime-f.estimates[2].time > f.windowLength { f.Reset(newSample, newTime) return } if f.comparator(newSample, f.estimates[1].sample) >= 0 { f.estimates[1] = entry[V, T]{newSample, newTime} f.estimates[2] = f.estimates[1] } else if f.comparator(newSample, f.estimates[2].sample) >= 0 { f.estimates[2] = entry[V, T]{newSample, newTime} } // Expire and update estimates as necessary. if newTime-f.estimates[0].time > f.windowLength { // The best estimate hasn't been updated for an entire window, so promote // second and third best estimates. f.estimates[0] = f.estimates[1] f.estimates[1] = f.estimates[2] f.estimates[2] = entry[V, T]{newSample, newTime} // Need to iterate one more time. Check if the new best estimate is // outside the window as well, since it may also have been recorded a // long time ago. Don't need to iterate once more since we cover that // case at the beginning of the method. if newTime-f.estimates[0].time > f.windowLength { f.estimates[0] = f.estimates[1] f.estimates[1] = f.estimates[2] } return } if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 && newTime-f.estimates[1].time > f.windowLength/4 { // A quarter of the window has passed without a better sample, so the // second-best estimate is taken from the second quarter of the window. f.estimates[1] = entry[V, T]{newSample, newTime} f.estimates[2] = f.estimates[1] return } if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 && newTime-f.estimates[2].time > f.windowLength/2 { // We've passed a half of the window without a better estimate, so take // a third-best estimate from the second half of the window. f.estimates[2] = entry[V, T]{newSample, newTime} } } // Resets all estimates to new sample. func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) { f.estimates[2] = entry[V, T]{newSample, newTime} f.estimates[1] = f.estimates[2] f.estimates[0] = f.estimates[1] } func (f *WindowedFilter[V, T]) Clear() { f.estimates = make([]entry[V, T], 3, 3) } ================================================ FILE: core/internal/congestion/brutal/brutal.go ================================================ package brutal import ( "fmt" "os" "strconv" "time" "github.com/apernet/hysteria/core/v2/internal/congestion/common" "github.com/apernet/quic-go/congestion" ) const ( pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample minSampleCount = 50 minAckRate = 0.8 congestionWindowMultiplier = 2 debugEnv = "HYSTERIA_BRUTAL_DEBUG" debugPrintInterval = 2 ) var _ congestion.CongestionControl = &BrutalSender{} type BrutalSender struct { rttStats congestion.RTTStatsProvider bps congestion.ByteCount maxDatagramSize congestion.ByteCount pacer *common.Pacer pktInfoSlots [pktInfoSlotCount]pktInfo ackRate float64 debug bool lastAckPrintTimestamp int64 } type pktInfo struct { Timestamp int64 AckCount uint64 LossCount uint64 } func NewBrutalSender(bps uint64) *BrutalSender { debug, _ := strconv.ParseBool(os.Getenv(debugEnv)) bs := &BrutalSender{ bps: congestion.ByteCount(bps), maxDatagramSize: congestion.InitialPacketSizeIPv4, ackRate: 1, debug: debug, } bs.pacer = common.NewPacer(func() congestion.ByteCount { return congestion.ByteCount(float64(bs.bps) / bs.ackRate) }) return bs } func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) { b.rttStats = rttStats } func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time { return b.pacer.TimeUntilSend() } func (b *BrutalSender) HasPacingBudget(now time.Time) bool { return b.pacer.Budget(now) >= b.maxDatagramSize } func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { return bytesInFlight <= b.GetCongestionWindow() } func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { rtt := b.rttStats.SmoothedRTT() if rtt <= 0 { return 10240 } cwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate) if cwnd < b.maxDatagramSize { cwnd = b.maxDatagramSize } return cwnd } func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount, packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool, ) { b.pacer.SentPacket(sentTime, bytes) } func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, priorInFlight congestion.ByteCount, eventTime time.Time, ) { // Stub } func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount, priorInFlight congestion.ByteCount, ) { // Stub } func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) { currentTimestamp := eventTime.Unix() slot := currentTimestamp % pktInfoSlotCount if b.pktInfoSlots[slot].Timestamp == currentTimestamp { b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets)) b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets)) } else { // uninitialized slot or too old, reset b.pktInfoSlots[slot].Timestamp = currentTimestamp b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets)) b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets)) } b.updateAckRate(currentTimestamp) } func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) { b.maxDatagramSize = size b.pacer.SetMaxDatagramSize(size) if b.debug { b.debugPrint("SetMaxDatagramSize: %d", size) } } func (b *BrutalSender) updateAckRate(currentTimestamp int64) { minTimestamp := currentTimestamp - pktInfoSlotCount var ackCount, lossCount uint64 for _, info := range b.pktInfoSlots { if info.Timestamp < minTimestamp { continue } ackCount += info.AckCount lossCount += info.LossCount } if ackCount+lossCount < minSampleCount { b.ackRate = 1 if b.canPrintAckRate(currentTimestamp) { b.lastAckPrintTimestamp = currentTimestamp b.debugPrint("Not enough samples (total=%d, ack=%d, loss=%d, rtt=%d)", ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) } return } rate := float64(ackCount) / float64(ackCount+lossCount) if rate < minAckRate { b.ackRate = minAckRate if b.canPrintAckRate(currentTimestamp) { b.lastAckPrintTimestamp = currentTimestamp b.debugPrint("ACK rate too low: %.2f, clamped to %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", rate, minAckRate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) } return } b.ackRate = rate if b.canPrintAckRate(currentTimestamp) { b.lastAckPrintTimestamp = currentTimestamp b.debugPrint("ACK rate: %.2f (total=%d, ack=%d, loss=%d, rtt=%d)", rate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds()) } } func (b *BrutalSender) InSlowStart() bool { return false } func (b *BrutalSender) InRecovery() bool { return false } func (b *BrutalSender) MaybeExitSlowStart() {} func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} func (b *BrutalSender) canPrintAckRate(currentTimestamp int64) bool { return b.debug && currentTimestamp-b.lastAckPrintTimestamp >= debugPrintInterval } func (b *BrutalSender) debugPrint(format string, a ...any) { fmt.Printf("[BrutalSender] [%s] %s\n", time.Now().Format("15:04:05"), fmt.Sprintf(format, a...)) } ================================================ FILE: core/internal/congestion/common/pacer.go ================================================ package common import ( "time" "github.com/apernet/quic-go/congestion" ) const ( maxBurstPackets = 10 maxBurstPacingDelayMultiplier = 4 ) // Pacer implements a token bucket pacing algorithm. type Pacer struct { budgetAtLastSent congestion.ByteCount maxDatagramSize congestion.ByteCount lastSentTime time.Time getBandwidth func() congestion.ByteCount // in bytes/s } func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer { p := &Pacer{ budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSizeIPv4, maxDatagramSize: congestion.InitialPacketSizeIPv4, getBandwidth: getBandwidth, } return p } func (p *Pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) { budget := p.Budget(sendTime) if size > budget { p.budgetAtLastSent = 0 } else { p.budgetAtLastSent = budget - size } p.lastSentTime = sendTime } func (p *Pacer) Budget(now time.Time) congestion.ByteCount { if p.lastSentTime.IsZero() { return p.maxBurstSize() } budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9 if budget < 0 { // protect against overflows budget = congestion.ByteCount(1<<62 - 1) } return min(p.maxBurstSize(), budget) } func (p *Pacer) maxBurstSize() congestion.ByteCount { return max( congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9, maxBurstPackets*p.maxDatagramSize, ) } // TimeUntilSend returns when the next packet should be sent. // It returns the zero value of time.Time if a packet can be sent immediately. func (p *Pacer) TimeUntilSend() time.Time { if p.budgetAtLastSent >= p.maxDatagramSize { return time.Time{} } diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent) bw := uint64(p.getBandwidth()) // We might need to round up this value. // Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires. d := diff / bw // this is effectively a math.Ceil, but using only integer math if diff%bw > 0 { d++ } return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond)) } func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) { p.maxDatagramSize = s } ================================================ FILE: core/internal/congestion/utils.go ================================================ package congestion import ( "github.com/apernet/hysteria/core/v2/internal/congestion/bbr" "github.com/apernet/hysteria/core/v2/internal/congestion/brutal" "github.com/apernet/quic-go" ) func UseBBR(conn quic.Connection) { conn.SetCongestionControl(bbr.NewBbrSender( bbr.DefaultClock{}, bbr.GetInitialPacketSize(conn.RemoteAddr()), )) } func UseBrutal(conn quic.Connection, tx uint64) { conn.SetCongestionControl(brutal.NewBrutalSender(tx)) } ================================================ FILE: core/internal/frag/frag.go ================================================ package frag import ( "github.com/apernet/hysteria/core/v2/internal/protocol" ) func FragUDPMessage(m *protocol.UDPMessage, maxSize int) []protocol.UDPMessage { if m.Size() <= maxSize { return []protocol.UDPMessage{*m} } fullPayload := m.Data maxPayloadSize := maxSize - m.HeaderSize() off := 0 fragID := uint8(0) fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up frags := make([]protocol.UDPMessage, fragCount) for off < len(fullPayload) { payloadSize := len(fullPayload) - off if payloadSize > maxPayloadSize { payloadSize = maxPayloadSize } frag := *m frag.FragID = fragID frag.FragCount = fragCount frag.Data = fullPayload[off : off+payloadSize] frags[fragID] = frag off += payloadSize fragID++ } return frags } // Defragger handles the defragmentation of UDP messages. // The current implementation can only handle one packet ID at a time. // If another packet arrives before a packet has received all fragments // in their entirety, any previous state is discarded. type Defragger struct { pktID uint16 frags []*protocol.UDPMessage count uint8 size int // data size } func (d *Defragger) Feed(m *protocol.UDPMessage) *protocol.UDPMessage { if m.FragCount <= 1 { return m } if m.FragID >= m.FragCount { // wtf is this? return nil } if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) { // new message, clear previous state d.pktID = m.PacketID d.frags = make([]*protocol.UDPMessage, m.FragCount) d.frags[m.FragID] = m d.count = 1 d.size = len(m.Data) } else if d.frags[m.FragID] == nil { d.frags[m.FragID] = m d.count++ d.size += len(m.Data) if int(d.count) == len(d.frags) { // all fragments received, assemble data := make([]byte, d.size) off := 0 for _, frag := range d.frags { off += copy(data[off:], frag.Data) } m.Data = data m.FragID = 0 m.FragCount = 1 return m } } return nil } ================================================ FILE: core/internal/frag/frag_test.go ================================================ package frag import ( "reflect" "testing" "github.com/apernet/hysteria/core/v2/internal/protocol" ) func TestFragUDPMessage(t *testing.T) { type args struct { m *protocol.UDPMessage maxSize int } tests := []struct { name string args args want []protocol.UDPMessage }{ { "no frag", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 123, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("hello"), }, 100, }, []protocol.UDPMessage{ { SessionID: 123, PacketID: 123, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("hello"), }, }, }, { "2 frags", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 123, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("hello"), }, 20, }, []protocol.UDPMessage{ { SessionID: 123, PacketID: 123, FragID: 0, FragCount: 2, Addr: "test:123", Data: []byte("hel"), }, { SessionID: 123, PacketID: 123, FragID: 1, FragCount: 2, Addr: "test:123", Data: []byte("lo"), }, }, }, { "4 frags", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 123, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("abcdefgh"), }, 19, }, []protocol.UDPMessage{ { SessionID: 123, PacketID: 123, FragID: 0, FragCount: 4, Addr: "test:123", Data: []byte("ab"), }, { SessionID: 123, PacketID: 123, FragID: 1, FragCount: 4, Addr: "test:123", Data: []byte("cd"), }, { SessionID: 123, PacketID: 123, FragID: 2, FragCount: 4, Addr: "test:123", Data: []byte("ef"), }, { SessionID: 123, PacketID: 123, FragID: 3, FragCount: 4, Addr: "test:123", Data: []byte("gh"), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := FragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) { t.Errorf("FragUDPMessage() = %v, want %v", got, tt.want) } }) } } func TestDefragger(t *testing.T) { type args struct { m *protocol.UDPMessage } tests := []struct { name string args args want *protocol.UDPMessage }{ { "no frag", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("hello"), }, }, &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("hello"), }, }, { "frag 0 - 1/2", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 0, FragCount: 2, Addr: "test:123", Data: []byte("hello "), }, }, nil, }, { "frag 0 - 2/2", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 1, FragCount: 2, Addr: "test:123", Data: []byte("moto"), }, }, &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("hello moto"), }, }, { "frag 1 - 1/3", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 0, FragCount: 3, Addr: "test:123", Data: []byte("deco"), }, }, nil, }, { "frag 1 - 2/3", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 1, FragCount: 3, Addr: "test:123", Data: []byte("*"), }, }, nil, }, { "frag 1 - 3/3", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 2, FragCount: 3, Addr: "test:123", Data: []byte("27"), }, }, &protocol.UDPMessage{ SessionID: 123, PacketID: 987, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("deco*27"), }, }, { "frag 2 - 1/2", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 233, FragID: 1, FragCount: 2, Addr: "test:123", Data: []byte("shinsekai"), }, }, nil, }, { "frag 3 - 2/2", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 244, FragID: 1, FragCount: 2, Addr: "test:123", Data: []byte("what???"), }, }, nil, }, { "frag 2 - 2/2", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 233, FragID: 1, FragCount: 2, Addr: "test:123", Data: []byte(" annaijo"), }, }, nil, }, { "invalid id", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 233, FragID: 88, FragCount: 2, Addr: "test:123", Data: []byte("shinsekai"), }, }, nil, }, { "frag 2 - 1/2 re", args{ &protocol.UDPMessage{ SessionID: 123, PacketID: 233, FragID: 0, FragCount: 2, Addr: "test:123", Data: []byte("shinsekai"), }, }, &protocol.UDPMessage{ SessionID: 123, PacketID: 233, FragID: 0, FragCount: 1, Addr: "test:123", Data: []byte("shinsekai annaijo"), }, }, } d := &Defragger{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) { t.Errorf("Feed() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: core/internal/integration_tests/.mockery.yaml ================================================ with-expecter: true dir: mocks outpkg: mocks packages: net: interfaces: Conn: config: mockname: MockConn github.com/apernet/hysteria/core/v2/server: interfaces: Outbound: config: mockname: MockOutbound UDPConn: config: mockname: MockUDPConn Authenticator: config: mockname: MockAuthenticator EventLogger: config: mockname: MockEventLogger TrafficLogger: config: mockname: MockTrafficLogger RequestHook: config: mockname: MockRequestHook ================================================ FILE: core/internal/integration_tests/close_test.go ================================================ package integration_tests import ( "io" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/apernet/hysteria/core/v2/client" "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" "github.com/apernet/hysteria/core/v2/server" ) // TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly. // Closing one side of the connection should close the other side as well. func TestClientServerTCPClose(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) serverOb := mocks.NewMockOutbound(t) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Outbound: serverOb, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() addr := "hi-and-goodbye:2333" // Test close from client side: // Client creates a connection, writes something, then closes it. // Server outbound connection should write the same thing, then close. sobConn := mocks.NewMockConn(t) sobConnCh := make(chan struct{}) // For close signal only sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { <-sobConnCh return 0, io.EOF }) sobConn.EXPECT().Write([]byte("happy")).Return(5, nil) sobConn.EXPECT().Close().RunAndReturn(func() error { sobConnChCloseFunc() return nil }) serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once() conn, err := c.TCP(addr) assert.NoError(t, err) _, err = conn.Write([]byte("happy")) assert.NoError(t, err) err = conn.Close() assert.NoError(t, err) time.Sleep(1 * time.Second) mock.AssertExpectationsForObjects(t, sobConn, serverOb) // Test close from server side: // Client creates a connection. // Server outbound connection reads something, then closes. // Client connection should read the same thing, then close. sobConn = mocks.NewMockConn(t) sobConnCh2 := make(chan []byte, 1) sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { d := <-sobConnCh2 if d == nil { return 0, io.EOF } else { return copy(bs, d), nil } }) sobConn.EXPECT().Close().Return(nil) serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once() conn, err = c.TCP(addr) assert.NoError(t, err) sobConnCh2 <- []byte("happy") close(sobConnCh2) bs, err := io.ReadAll(conn) assert.NoError(t, err) assert.Equal(t, "happy", string(bs)) } // TestClientServerUDPIdleTimeout tests whether the server's UDP idle timeout works correctly. func TestClientServerUDPIdleTimeout(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) serverOb := mocks.NewMockOutbound(t) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") eventLogger := mocks.NewMockEventLogger(t) eventLogger.EXPECT().Connect(mock.Anything, "nobody", mock.Anything).Once() eventLogger.EXPECT().Disconnect(mock.Anything, "nobody", mock.Anything).Maybe() // Depends on the timing, don't care s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Outbound: serverOb, UDPIdleTimeout: 2 * time.Second, Authenticator: auth, EventLogger: eventLogger, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() addr := "spy.x.family:2023" // On the client side, create a UDP session and send a packet every 1 second, // 4 packets in total. The server should have one UDP session and receive all // 4 packets. Then the UDP connection on the server side will receive a packet // every 1 second, 4 packets in total. The client session should receive all // 4 packets. Then the session will be idle for 3 seconds - should be enough // to trigger the server's UDP idle timeout. sobConn := mocks.NewMockUDPConn(t) sobConnCh := make(chan []byte, 1) sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) sobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) { d := <-sobConnCh if d == nil { return 0, "", io.EOF } else { return copy(bs, d), addr, nil } }) sobConn.EXPECT().WriteTo([]byte("happy"), addr).Return(5, nil).Times(4) serverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once() eventLogger.EXPECT().UDPRequest(mock.Anything, mock.Anything, uint32(1), addr).Once() cu, err := c.UDP() assert.NoError(t, err) // Client sends 4 packets for i := 0; i < 4; i++ { err = cu.Send([]byte("happy"), addr) assert.NoError(t, err) time.Sleep(1 * time.Second) } // Client receives 4 packets go func() { for i := 0; i < 4; i++ { sobConnCh <- []byte("sad") time.Sleep(1 * time.Second) } }() for i := 0; i < 4; i++ { bs, rAddr, err := cu.Receive() assert.NoError(t, err) assert.Equal(t, "sad", string(bs)) assert.Equal(t, addr, rAddr) } // Now we wait for 3 seconds, the server should close the UDP session. sobConn.EXPECT().Close().RunAndReturn(func() error { sobConnChCloseFunc() return nil }) eventLogger.EXPECT().UDPError(mock.Anything, mock.Anything, uint32(1), nil).Once() time.Sleep(3 * time.Second) } // TestClientServerClientShutdown tests whether the server can handle the client's shutdown correctly. func TestClientServerClientShutdown(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") eventLogger := mocks.NewMockEventLogger(t) eventLogger.EXPECT().Connect(mock.Anything, "nobody", mock.Anything).Once() s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, EventLogger: eventLogger, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) // Close the client - expect disconnect event on the server side. // Since client.Close() sends HTTP3 ErrCodeNoError, the error should be nil. eventLogger.EXPECT().Disconnect(mock.Anything, "nobody", nil).Once() _ = c.Close() time.Sleep(1 * time.Second) } // TestClientServerServerShutdown tests whether the client can handle the server's shutdown correctly. func TestClientServerServerShutdown(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) go s.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, QUICConfig: client.QUICConfig{ MaxIdleTimeout: 4 * time.Second, }, }) assert.NoError(t, err) // Close the server - expect the client to return ClosedError for both TCP & UDP calls. _ = s.Close() _, err = c.TCP("whatever") _, ok := err.(errors.ClosedError) assert.True(t, ok) time.Sleep(1 * time.Second) // Allow some time for the error to be propagated to the UDP session manager _, err = c.UDP() _, ok = err.(errors.ClosedError) assert.True(t, ok) assert.NoError(t, c.Close()) } ================================================ FILE: core/internal/integration_tests/hook_test.go ================================================ package integration_tests import ( "io" "net" "testing" "github.com/apernet/hysteria/core/v2/client" "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" "github.com/apernet/hysteria/core/v2/server" "github.com/apernet/quic-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestClientServerHookTCP(t *testing.T) { fakeEchoAddr := "hahanope:6666" realEchoAddr := "127.0.0.1:22333" // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") hook := mocks.NewMockRequestHook(t) hook.EXPECT().Check(false, fakeEchoAddr).Return(true).Once() hook.EXPECT().TCP(mock.Anything, mock.Anything).RunAndReturn(func(stream quic.Stream, s *string) ([]byte, error) { assert.Equal(t, fakeEchoAddr, *s) // Change the address *s = realEchoAddr // Read the first 5 bytes and replace them with "byeee" data := make([]byte, 5) _, err := io.ReadFull(stream, data) if err != nil { return nil, err } assert.Equal(t, []byte("hello"), data) return []byte("byeee"), nil }).Once() s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, RequestHook: hook, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create TCP echo server echoListener, err := net.Listen("tcp", realEchoAddr) assert.NoError(t, err) echoServer := &tcpEchoServer{Listener: echoListener} defer echoServer.Close() go echoServer.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() // Dial TCP conn, err := c.TCP(fakeEchoAddr) assert.NoError(t, err) defer conn.Close() // Send and receive data sData := []byte("hello world") _, err = conn.Write(sData) assert.NoError(t, err) rData := make([]byte, len(sData)) _, err = io.ReadFull(conn, rData) assert.NoError(t, err) assert.Equal(t, []byte("byeee world"), rData) } func TestClientServerHookUDP(t *testing.T) { fakeEchoAddr := "hahanope:6666" realEchoAddr := "127.0.0.1:22333" // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") hook := mocks.NewMockRequestHook(t) hook.EXPECT().Check(true, fakeEchoAddr).Return(true).Once() hook.EXPECT().UDP(mock.Anything, mock.Anything).RunAndReturn(func(bytes []byte, s *string) error { assert.Equal(t, fakeEchoAddr, *s) assert.Equal(t, []byte("hello world"), bytes) // Change the address *s = realEchoAddr return nil }).Once() s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, RequestHook: hook, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create UDP echo server echoConn, err := net.ListenPacket("udp", realEchoAddr) assert.NoError(t, err) echoServer := &udpEchoServer{Conn: echoConn} defer echoServer.Close() go echoServer.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() // Listen UDP conn, err := c.UDP() assert.NoError(t, err) defer conn.Close() // Send and receive data sData := []byte("hello world") err = conn.Send(sData, fakeEchoAddr) assert.NoError(t, err) rData, rAddr, err := conn.Receive() assert.NoError(t, err) assert.Equal(t, sData, rData) // Hook address change is transparent, // the client should still see the fake echo address it sent packets to assert.Equal(t, fakeEchoAddr, rAddr) // Subsequent packets should also be sent to the real echo server sData = []byte("never stop fighting") err = conn.Send(sData, fakeEchoAddr) assert.NoError(t, err) rData, rAddr, err = conn.Receive() assert.NoError(t, err) assert.Equal(t, sData, rData) assert.Equal(t, fakeEchoAddr, rAddr) } ================================================ FILE: core/internal/integration_tests/masq_test.go ================================================ package integration_tests import ( "context" "crypto/tls" "net" "net/http" "net/url" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" "github.com/apernet/hysteria/core/v2/internal/protocol" "github.com/apernet/hysteria/core/v2/server" "github.com/apernet/quic-go" "github.com/apernet/quic-go/http3" ) // TestServerMasquerade is a test to ensure that the server behaves as a normal // HTTP/3 server when dealing with an unauthenticated client. This is mainly to // confirm that the server does not expose itself to active probing. func TestServerMasquerade(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, "", uint64(0)).Return(false, "").Once() s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // QUIC connection & RoundTripper var conn quic.EarlyConnection rt := &http3.RoundTripper{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { qc, err := quic.DialAddrEarly(ctx, udpAddr.String(), tlsCfg, cfg) if err != nil { return nil, err } conn = qc return qc, nil }, } defer rt.Close() // This will close the QUIC connection // Send the bogus request // We expect 404 (from the default handler) req := &http.Request{ Method: http.MethodPost, URL: &url.URL{ Scheme: "https", Host: protocol.URLHost, Path: protocol.URLPath, }, Header: make(http.Header), } resp, err := rt.RoundTrip(req) assert.NoError(t, err) assert.Equal(t, http.StatusNotFound, resp.StatusCode) for k := range resp.Header { // Make sure no strange headers are sent by the server assert.NotContains(t, k, "Hysteria") } buf := make([]byte, 1024) // We send a TCP request anyway, see if we get a response tcpStream, err := conn.OpenStream() assert.NoError(t, err) defer tcpStream.Close() err = protocol.WriteTCPRequest(tcpStream, "www.google.com:443") assert.NoError(t, err) // We should receive nothing _ = tcpStream.SetReadDeadline(time.Now().Add(2 * time.Second)) n, err := tcpStream.Read(buf) assert.Equal(t, 0, n) nErr, ok := err.(net.Error) assert.True(t, ok) assert.True(t, nErr.Timeout()) } ================================================ FILE: core/internal/integration_tests/mocks/mock_Authenticator.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import ( net "net" mock "github.com/stretchr/testify/mock" ) // MockAuthenticator is an autogenerated mock type for the Authenticator type type MockAuthenticator struct { mock.Mock } type MockAuthenticator_Expecter struct { mock *mock.Mock } func (_m *MockAuthenticator) EXPECT() *MockAuthenticator_Expecter { return &MockAuthenticator_Expecter{mock: &_m.Mock} } // Authenticate provides a mock function with given fields: addr, auth, tx func (_m *MockAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (bool, string) { ret := _m.Called(addr, auth, tx) if len(ret) == 0 { panic("no return value specified for Authenticate") } var r0 bool var r1 string if rf, ok := ret.Get(0).(func(net.Addr, string, uint64) (bool, string)); ok { return rf(addr, auth, tx) } if rf, ok := ret.Get(0).(func(net.Addr, string, uint64) bool); ok { r0 = rf(addr, auth, tx) } else { r0 = ret.Get(0).(bool) } if rf, ok := ret.Get(1).(func(net.Addr, string, uint64) string); ok { r1 = rf(addr, auth, tx) } else { r1 = ret.Get(1).(string) } return r0, r1 } // MockAuthenticator_Authenticate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authenticate' type MockAuthenticator_Authenticate_Call struct { *mock.Call } // Authenticate is a helper method to define mock.On call // - addr net.Addr // - auth string // - tx uint64 func (_e *MockAuthenticator_Expecter) Authenticate(addr interface{}, auth interface{}, tx interface{}) *MockAuthenticator_Authenticate_Call { return &MockAuthenticator_Authenticate_Call{Call: _e.mock.On("Authenticate", addr, auth, tx)} } func (_c *MockAuthenticator_Authenticate_Call) Run(run func(addr net.Addr, auth string, tx uint64)) *MockAuthenticator_Authenticate_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(net.Addr), args[1].(string), args[2].(uint64)) }) return _c } func (_c *MockAuthenticator_Authenticate_Call) Return(ok bool, id string) *MockAuthenticator_Authenticate_Call { _c.Call.Return(ok, id) return _c } func (_c *MockAuthenticator_Authenticate_Call) RunAndReturn(run func(net.Addr, string, uint64) (bool, string)) *MockAuthenticator_Authenticate_Call { _c.Call.Return(run) return _c } // NewMockAuthenticator creates a new instance of MockAuthenticator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockAuthenticator(t interface { mock.TestingT Cleanup(func()) }) *MockAuthenticator { mock := &MockAuthenticator{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/internal/integration_tests/mocks/mock_Conn.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import ( net "net" mock "github.com/stretchr/testify/mock" time "time" ) // MockConn is an autogenerated mock type for the Conn type type MockConn struct { mock.Mock } type MockConn_Expecter struct { mock *mock.Mock } func (_m *MockConn) EXPECT() *MockConn_Expecter { return &MockConn_Expecter{mock: &_m.Mock} } // Close provides a mock function with given fields: func (_m *MockConn) Close() error { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type MockConn_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *MockConn_Expecter) Close() *MockConn_Close_Call { return &MockConn_Close_Call{Call: _e.mock.On("Close")} } func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call { _c.Call.Return(run) return _c } // LocalAddr provides a mock function with given fields: func (_m *MockConn) LocalAddr() net.Addr { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for LocalAddr") } var r0 net.Addr if rf, ok := ret.Get(0).(func() net.Addr); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Addr) } } return r0 } // MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr' type MockConn_LocalAddr_Call struct { *mock.Call } // LocalAddr is a helper method to define mock.On call func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call { return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")} } func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call { _c.Call.Return(run) return _c } // Read provides a mock function with given fields: b func (_m *MockConn) Read(b []byte) (int, error) { ret := _m.Called(b) if len(ret) == 0 { panic("no return value specified for Read") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { return rf(b) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(b) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) error); ok { r1 = rf(b) } else { r1 = ret.Error(1) } return r0, r1 } // MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' type MockConn_Read_Call struct { *mock.Call } // Read is a helper method to define mock.On call // - b []byte func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call { return &MockConn_Read_Call{Call: _e.mock.On("Read", b)} } func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call { _c.Call.Return(n, err) return _c } func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call { _c.Call.Return(run) return _c } // RemoteAddr provides a mock function with given fields: func (_m *MockConn) RemoteAddr() net.Addr { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for RemoteAddr") } var r0 net.Addr if rf, ok := ret.Get(0).(func() net.Addr); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Addr) } } return r0 } // MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr' type MockConn_RemoteAddr_Call struct { *mock.Call } // RemoteAddr is a helper method to define mock.On call func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call { return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")} } func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call { _c.Call.Return(run) return _c } // SetDeadline provides a mock function with given fields: t func (_m *MockConn) SetDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline' type MockConn_SetDeadline_Call struct { *mock.Call } // SetDeadline is a helper method to define mock.On call // - t time.Time func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call { return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)} } func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call { _c.Call.Return(run) return _c } // SetReadDeadline provides a mock function with given fields: t func (_m *MockConn) SetReadDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetReadDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' type MockConn_SetReadDeadline_Call struct { *mock.Call } // SetReadDeadline is a helper method to define mock.On call // - t time.Time func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call { return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} } func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call { _c.Call.Return(run) return _c } // SetWriteDeadline provides a mock function with given fields: t func (_m *MockConn) SetWriteDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetWriteDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' type MockConn_SetWriteDeadline_Call struct { *mock.Call } // SetWriteDeadline is a helper method to define mock.On call // - t time.Time func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call { return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} } func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call { _c.Call.Return(run) return _c } // Write provides a mock function with given fields: b func (_m *MockConn) Write(b []byte) (int, error) { ret := _m.Called(b) if len(ret) == 0 { panic("no return value specified for Write") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { return rf(b) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(b) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) error); ok { r1 = rf(b) } else { r1 = ret.Error(1) } return r0, r1 } // MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' type MockConn_Write_Call struct { *mock.Call } // Write is a helper method to define mock.On call // - b []byte func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call { return &MockConn_Write_Call{Call: _e.mock.On("Write", b)} } func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call { _c.Call.Return(n, err) return _c } func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call { _c.Call.Return(run) return _c } // NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockConn(t interface { mock.TestingT Cleanup(func()) }) *MockConn { mock := &MockConn{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/internal/integration_tests/mocks/mock_EventLogger.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import ( net "net" mock "github.com/stretchr/testify/mock" ) // MockEventLogger is an autogenerated mock type for the EventLogger type type MockEventLogger struct { mock.Mock } type MockEventLogger_Expecter struct { mock *mock.Mock } func (_m *MockEventLogger) EXPECT() *MockEventLogger_Expecter { return &MockEventLogger_Expecter{mock: &_m.Mock} } // Connect provides a mock function with given fields: addr, id, tx func (_m *MockEventLogger) Connect(addr net.Addr, id string, tx uint64) { _m.Called(addr, id, tx) } // MockEventLogger_Connect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Connect' type MockEventLogger_Connect_Call struct { *mock.Call } // Connect is a helper method to define mock.On call // - addr net.Addr // - id string // - tx uint64 func (_e *MockEventLogger_Expecter) Connect(addr interface{}, id interface{}, tx interface{}) *MockEventLogger_Connect_Call { return &MockEventLogger_Connect_Call{Call: _e.mock.On("Connect", addr, id, tx)} } func (_c *MockEventLogger_Connect_Call) Run(run func(addr net.Addr, id string, tx uint64)) *MockEventLogger_Connect_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(net.Addr), args[1].(string), args[2].(uint64)) }) return _c } func (_c *MockEventLogger_Connect_Call) Return() *MockEventLogger_Connect_Call { _c.Call.Return() return _c } func (_c *MockEventLogger_Connect_Call) RunAndReturn(run func(net.Addr, string, uint64)) *MockEventLogger_Connect_Call { _c.Call.Return(run) return _c } // Disconnect provides a mock function with given fields: addr, id, err func (_m *MockEventLogger) Disconnect(addr net.Addr, id string, err error) { _m.Called(addr, id, err) } // MockEventLogger_Disconnect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Disconnect' type MockEventLogger_Disconnect_Call struct { *mock.Call } // Disconnect is a helper method to define mock.On call // - addr net.Addr // - id string // - err error func (_e *MockEventLogger_Expecter) Disconnect(addr interface{}, id interface{}, err interface{}) *MockEventLogger_Disconnect_Call { return &MockEventLogger_Disconnect_Call{Call: _e.mock.On("Disconnect", addr, id, err)} } func (_c *MockEventLogger_Disconnect_Call) Run(run func(addr net.Addr, id string, err error)) *MockEventLogger_Disconnect_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(net.Addr), args[1].(string), args[2].(error)) }) return _c } func (_c *MockEventLogger_Disconnect_Call) Return() *MockEventLogger_Disconnect_Call { _c.Call.Return() return _c } func (_c *MockEventLogger_Disconnect_Call) RunAndReturn(run func(net.Addr, string, error)) *MockEventLogger_Disconnect_Call { _c.Call.Return(run) return _c } // TCPError provides a mock function with given fields: addr, id, reqAddr, err func (_m *MockEventLogger) TCPError(addr net.Addr, id string, reqAddr string, err error) { _m.Called(addr, id, reqAddr, err) } // MockEventLogger_TCPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPError' type MockEventLogger_TCPError_Call struct { *mock.Call } // TCPError is a helper method to define mock.On call // - addr net.Addr // - id string // - reqAddr string // - err error func (_e *MockEventLogger_Expecter) TCPError(addr interface{}, id interface{}, reqAddr interface{}, err interface{}) *MockEventLogger_TCPError_Call { return &MockEventLogger_TCPError_Call{Call: _e.mock.On("TCPError", addr, id, reqAddr, err)} } func (_c *MockEventLogger_TCPError_Call) Run(run func(addr net.Addr, id string, reqAddr string, err error)) *MockEventLogger_TCPError_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(net.Addr), args[1].(string), args[2].(string), args[3].(error)) }) return _c } func (_c *MockEventLogger_TCPError_Call) Return() *MockEventLogger_TCPError_Call { _c.Call.Return() return _c } func (_c *MockEventLogger_TCPError_Call) RunAndReturn(run func(net.Addr, string, string, error)) *MockEventLogger_TCPError_Call { _c.Call.Return(run) return _c } // TCPRequest provides a mock function with given fields: addr, id, reqAddr func (_m *MockEventLogger) TCPRequest(addr net.Addr, id string, reqAddr string) { _m.Called(addr, id, reqAddr) } // MockEventLogger_TCPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPRequest' type MockEventLogger_TCPRequest_Call struct { *mock.Call } // TCPRequest is a helper method to define mock.On call // - addr net.Addr // - id string // - reqAddr string func (_e *MockEventLogger_Expecter) TCPRequest(addr interface{}, id interface{}, reqAddr interface{}) *MockEventLogger_TCPRequest_Call { return &MockEventLogger_TCPRequest_Call{Call: _e.mock.On("TCPRequest", addr, id, reqAddr)} } func (_c *MockEventLogger_TCPRequest_Call) Run(run func(addr net.Addr, id string, reqAddr string)) *MockEventLogger_TCPRequest_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(net.Addr), args[1].(string), args[2].(string)) }) return _c } func (_c *MockEventLogger_TCPRequest_Call) Return() *MockEventLogger_TCPRequest_Call { _c.Call.Return() return _c } func (_c *MockEventLogger_TCPRequest_Call) RunAndReturn(run func(net.Addr, string, string)) *MockEventLogger_TCPRequest_Call { _c.Call.Return(run) return _c } // UDPError provides a mock function with given fields: addr, id, sessionID, err func (_m *MockEventLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) { _m.Called(addr, id, sessionID, err) } // MockEventLogger_UDPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPError' type MockEventLogger_UDPError_Call struct { *mock.Call } // UDPError is a helper method to define mock.On call // - addr net.Addr // - id string // - sessionID uint32 // - err error func (_e *MockEventLogger_Expecter) UDPError(addr interface{}, id interface{}, sessionID interface{}, err interface{}) *MockEventLogger_UDPError_Call { return &MockEventLogger_UDPError_Call{Call: _e.mock.On("UDPError", addr, id, sessionID, err)} } func (_c *MockEventLogger_UDPError_Call) Run(run func(addr net.Addr, id string, sessionID uint32, err error)) *MockEventLogger_UDPError_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(error)) }) return _c } func (_c *MockEventLogger_UDPError_Call) Return() *MockEventLogger_UDPError_Call { _c.Call.Return() return _c } func (_c *MockEventLogger_UDPError_Call) RunAndReturn(run func(net.Addr, string, uint32, error)) *MockEventLogger_UDPError_Call { _c.Call.Return(run) return _c } // UDPRequest provides a mock function with given fields: addr, id, sessionID, reqAddr func (_m *MockEventLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) { _m.Called(addr, id, sessionID, reqAddr) } // MockEventLogger_UDPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPRequest' type MockEventLogger_UDPRequest_Call struct { *mock.Call } // UDPRequest is a helper method to define mock.On call // - addr net.Addr // - id string // - sessionID uint32 // - reqAddr string func (_e *MockEventLogger_Expecter) UDPRequest(addr interface{}, id interface{}, sessionID interface{}, reqAddr interface{}) *MockEventLogger_UDPRequest_Call { return &MockEventLogger_UDPRequest_Call{Call: _e.mock.On("UDPRequest", addr, id, sessionID, reqAddr)} } func (_c *MockEventLogger_UDPRequest_Call) Run(run func(addr net.Addr, id string, sessionID uint32, reqAddr string)) *MockEventLogger_UDPRequest_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(string)) }) return _c } func (_c *MockEventLogger_UDPRequest_Call) Return() *MockEventLogger_UDPRequest_Call { _c.Call.Return() return _c } func (_c *MockEventLogger_UDPRequest_Call) RunAndReturn(run func(net.Addr, string, uint32, string)) *MockEventLogger_UDPRequest_Call { _c.Call.Return(run) return _c } // NewMockEventLogger creates a new instance of MockEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockEventLogger(t interface { mock.TestingT Cleanup(func()) }) *MockEventLogger { mock := &MockEventLogger{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/internal/integration_tests/mocks/mock_Outbound.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import ( net "net" mock "github.com/stretchr/testify/mock" server "github.com/apernet/hysteria/core/v2/server" ) // MockOutbound is an autogenerated mock type for the Outbound type type MockOutbound struct { mock.Mock } type MockOutbound_Expecter struct { mock *mock.Mock } func (_m *MockOutbound) EXPECT() *MockOutbound_Expecter { return &MockOutbound_Expecter{mock: &_m.Mock} } // TCP provides a mock function with given fields: reqAddr func (_m *MockOutbound) TCP(reqAddr string) (net.Conn, error) { ret := _m.Called(reqAddr) if len(ret) == 0 { panic("no return value specified for TCP") } var r0 net.Conn var r1 error if rf, ok := ret.Get(0).(func(string) (net.Conn, error)); ok { return rf(reqAddr) } if rf, ok := ret.Get(0).(func(string) net.Conn); ok { r0 = rf(reqAddr) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Conn) } } if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(reqAddr) } else { r1 = ret.Error(1) } return r0, r1 } // MockOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP' type MockOutbound_TCP_Call struct { *mock.Call } // TCP is a helper method to define mock.On call // - reqAddr string func (_e *MockOutbound_Expecter) TCP(reqAddr interface{}) *MockOutbound_TCP_Call { return &MockOutbound_TCP_Call{Call: _e.mock.On("TCP", reqAddr)} } func (_c *MockOutbound_TCP_Call) Run(run func(reqAddr string)) *MockOutbound_TCP_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string)) }) return _c } func (_c *MockOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *MockOutbound_TCP_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *MockOutbound_TCP_Call) RunAndReturn(run func(string) (net.Conn, error)) *MockOutbound_TCP_Call { _c.Call.Return(run) return _c } // UDP provides a mock function with given fields: reqAddr func (_m *MockOutbound) UDP(reqAddr string) (server.UDPConn, error) { ret := _m.Called(reqAddr) if len(ret) == 0 { panic("no return value specified for UDP") } var r0 server.UDPConn var r1 error if rf, ok := ret.Get(0).(func(string) (server.UDPConn, error)); ok { return rf(reqAddr) } if rf, ok := ret.Get(0).(func(string) server.UDPConn); ok { r0 = rf(reqAddr) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(server.UDPConn) } } if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(reqAddr) } else { r1 = ret.Error(1) } return r0, r1 } // MockOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' type MockOutbound_UDP_Call struct { *mock.Call } // UDP is a helper method to define mock.On call // - reqAddr string func (_e *MockOutbound_Expecter) UDP(reqAddr interface{}) *MockOutbound_UDP_Call { return &MockOutbound_UDP_Call{Call: _e.mock.On("UDP", reqAddr)} } func (_c *MockOutbound_UDP_Call) Run(run func(reqAddr string)) *MockOutbound_UDP_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string)) }) return _c } func (_c *MockOutbound_UDP_Call) Return(_a0 server.UDPConn, _a1 error) *MockOutbound_UDP_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *MockOutbound_UDP_Call) RunAndReturn(run func(string) (server.UDPConn, error)) *MockOutbound_UDP_Call { _c.Call.Return(run) return _c } // NewMockOutbound creates a new instance of MockOutbound. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockOutbound(t interface { mock.TestingT Cleanup(func()) }) *MockOutbound { mock := &MockOutbound{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/internal/integration_tests/mocks/mock_RequestHook.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import ( quic "github.com/apernet/quic-go" mock "github.com/stretchr/testify/mock" ) // MockRequestHook is an autogenerated mock type for the RequestHook type type MockRequestHook struct { mock.Mock } type MockRequestHook_Expecter struct { mock *mock.Mock } func (_m *MockRequestHook) EXPECT() *MockRequestHook_Expecter { return &MockRequestHook_Expecter{mock: &_m.Mock} } // Check provides a mock function with given fields: isUDP, reqAddr func (_m *MockRequestHook) Check(isUDP bool, reqAddr string) bool { ret := _m.Called(isUDP, reqAddr) if len(ret) == 0 { panic("no return value specified for Check") } var r0 bool if rf, ok := ret.Get(0).(func(bool, string) bool); ok { r0 = rf(isUDP, reqAddr) } else { r0 = ret.Get(0).(bool) } return r0 } // MockRequestHook_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check' type MockRequestHook_Check_Call struct { *mock.Call } // Check is a helper method to define mock.On call // - isUDP bool // - reqAddr string func (_e *MockRequestHook_Expecter) Check(isUDP interface{}, reqAddr interface{}) *MockRequestHook_Check_Call { return &MockRequestHook_Check_Call{Call: _e.mock.On("Check", isUDP, reqAddr)} } func (_c *MockRequestHook_Check_Call) Run(run func(isUDP bool, reqAddr string)) *MockRequestHook_Check_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(bool), args[1].(string)) }) return _c } func (_c *MockRequestHook_Check_Call) Return(_a0 bool) *MockRequestHook_Check_Call { _c.Call.Return(_a0) return _c } func (_c *MockRequestHook_Check_Call) RunAndReturn(run func(bool, string) bool) *MockRequestHook_Check_Call { _c.Call.Return(run) return _c } // TCP provides a mock function with given fields: stream, reqAddr func (_m *MockRequestHook) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { ret := _m.Called(stream, reqAddr) if len(ret) == 0 { panic("no return value specified for TCP") } var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(quic.Stream, *string) ([]byte, error)); ok { return rf(stream, reqAddr) } if rf, ok := ret.Get(0).(func(quic.Stream, *string) []byte); ok { r0 = rf(stream, reqAddr) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]byte) } } if rf, ok := ret.Get(1).(func(quic.Stream, *string) error); ok { r1 = rf(stream, reqAddr) } else { r1 = ret.Error(1) } return r0, r1 } // MockRequestHook_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP' type MockRequestHook_TCP_Call struct { *mock.Call } // TCP is a helper method to define mock.On call // - stream quic.Stream // - reqAddr *string func (_e *MockRequestHook_Expecter) TCP(stream interface{}, reqAddr interface{}) *MockRequestHook_TCP_Call { return &MockRequestHook_TCP_Call{Call: _e.mock.On("TCP", stream, reqAddr)} } func (_c *MockRequestHook_TCP_Call) Run(run func(stream quic.Stream, reqAddr *string)) *MockRequestHook_TCP_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(quic.Stream), args[1].(*string)) }) return _c } func (_c *MockRequestHook_TCP_Call) Return(_a0 []byte, _a1 error) *MockRequestHook_TCP_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *MockRequestHook_TCP_Call) RunAndReturn(run func(quic.Stream, *string) ([]byte, error)) *MockRequestHook_TCP_Call { _c.Call.Return(run) return _c } // UDP provides a mock function with given fields: data, reqAddr func (_m *MockRequestHook) UDP(data []byte, reqAddr *string) error { ret := _m.Called(data, reqAddr) if len(ret) == 0 { panic("no return value specified for UDP") } var r0 error if rf, ok := ret.Get(0).(func([]byte, *string) error); ok { r0 = rf(data, reqAddr) } else { r0 = ret.Error(0) } return r0 } // MockRequestHook_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' type MockRequestHook_UDP_Call struct { *mock.Call } // UDP is a helper method to define mock.On call // - data []byte // - reqAddr *string func (_e *MockRequestHook_Expecter) UDP(data interface{}, reqAddr interface{}) *MockRequestHook_UDP_Call { return &MockRequestHook_UDP_Call{Call: _e.mock.On("UDP", data, reqAddr)} } func (_c *MockRequestHook_UDP_Call) Run(run func(data []byte, reqAddr *string)) *MockRequestHook_UDP_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte), args[1].(*string)) }) return _c } func (_c *MockRequestHook_UDP_Call) Return(_a0 error) *MockRequestHook_UDP_Call { _c.Call.Return(_a0) return _c } func (_c *MockRequestHook_UDP_Call) RunAndReturn(run func([]byte, *string) error) *MockRequestHook_UDP_Call { _c.Call.Return(run) return _c } // NewMockRequestHook creates a new instance of MockRequestHook. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockRequestHook(t interface { mock.TestingT Cleanup(func()) }) *MockRequestHook { mock := &MockRequestHook{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/internal/integration_tests/mocks/mock_TrafficLogger.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import mock "github.com/stretchr/testify/mock" // MockTrafficLogger is an autogenerated mock type for the TrafficLogger type type MockTrafficLogger struct { mock.Mock } type MockTrafficLogger_Expecter struct { mock *mock.Mock } func (_m *MockTrafficLogger) EXPECT() *MockTrafficLogger_Expecter { return &MockTrafficLogger_Expecter{mock: &_m.Mock} } // LogOnlineState provides a mock function with given fields: id, online func (_m *MockTrafficLogger) LogOnlineState(id string, online bool) { _m.Called(id, online) } // MockTrafficLogger_LogOnlineState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogOnlineState' type MockTrafficLogger_LogOnlineState_Call struct { *mock.Call } // LogOnlineState is a helper method to define mock.On call // - id string // - online bool func (_e *MockTrafficLogger_Expecter) LogOnlineState(id interface{}, online interface{}) *MockTrafficLogger_LogOnlineState_Call { return &MockTrafficLogger_LogOnlineState_Call{Call: _e.mock.On("LogOnlineState", id, online)} } func (_c *MockTrafficLogger_LogOnlineState_Call) Run(run func(id string, online bool)) *MockTrafficLogger_LogOnlineState_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].(bool)) }) return _c } func (_c *MockTrafficLogger_LogOnlineState_Call) Return() *MockTrafficLogger_LogOnlineState_Call { _c.Call.Return() return _c } func (_c *MockTrafficLogger_LogOnlineState_Call) RunAndReturn(run func(string, bool)) *MockTrafficLogger_LogOnlineState_Call { _c.Call.Return(run) return _c } // LogTraffic provides a mock function with given fields: id, tx, rx func (_m *MockTrafficLogger) LogTraffic(id string, tx uint64, rx uint64) bool { ret := _m.Called(id, tx, rx) if len(ret) == 0 { panic("no return value specified for LogTraffic") } var r0 bool if rf, ok := ret.Get(0).(func(string, uint64, uint64) bool); ok { r0 = rf(id, tx, rx) } else { r0 = ret.Get(0).(bool) } return r0 } // MockTrafficLogger_LogTraffic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogTraffic' type MockTrafficLogger_LogTraffic_Call struct { *mock.Call } // LogTraffic is a helper method to define mock.On call // - id string // - tx uint64 // - rx uint64 func (_e *MockTrafficLogger_Expecter) LogTraffic(id interface{}, tx interface{}, rx interface{}) *MockTrafficLogger_LogTraffic_Call { return &MockTrafficLogger_LogTraffic_Call{Call: _e.mock.On("LogTraffic", id, tx, rx)} } func (_c *MockTrafficLogger_LogTraffic_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_LogTraffic_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].(uint64), args[2].(uint64)) }) return _c } func (_c *MockTrafficLogger_LogTraffic_Call) Return(ok bool) *MockTrafficLogger_LogTraffic_Call { _c.Call.Return(ok) return _c } func (_c *MockTrafficLogger_LogTraffic_Call) RunAndReturn(run func(string, uint64, uint64) bool) *MockTrafficLogger_LogTraffic_Call { _c.Call.Return(run) return _c } // NewMockTrafficLogger creates a new instance of MockTrafficLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockTrafficLogger(t interface { mock.TestingT Cleanup(func()) }) *MockTrafficLogger { mock := &MockTrafficLogger{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/internal/integration_tests/mocks/mock_UDPConn.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package mocks import mock "github.com/stretchr/testify/mock" // MockUDPConn is an autogenerated mock type for the UDPConn type type MockUDPConn struct { mock.Mock } type MockUDPConn_Expecter struct { mock *mock.Mock } func (_m *MockUDPConn) EXPECT() *MockUDPConn_Expecter { return &MockUDPConn_Expecter{mock: &_m.Mock} } // Close provides a mock function with given fields: func (_m *MockUDPConn) Close() error { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // MockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type MockUDPConn_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *MockUDPConn_Expecter) Close() *MockUDPConn_Close_Call { return &MockUDPConn_Close_Call{Call: _e.mock.On("Close")} } func (_c *MockUDPConn_Close_Call) Run(run func()) *MockUDPConn_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *MockUDPConn_Close_Call) Return(_a0 error) *MockUDPConn_Close_Call { _c.Call.Return(_a0) return _c } func (_c *MockUDPConn_Close_Call) RunAndReturn(run func() error) *MockUDPConn_Close_Call { _c.Call.Return(run) return _c } // ReadFrom provides a mock function with given fields: b func (_m *MockUDPConn) ReadFrom(b []byte) (int, string, error) { ret := _m.Called(b) if len(ret) == 0 { panic("no return value specified for ReadFrom") } var r0 int var r1 string var r2 error if rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok { return rf(b) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(b) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) string); ok { r1 = rf(b) } else { r1 = ret.Get(1).(string) } if rf, ok := ret.Get(2).(func([]byte) error); ok { r2 = rf(b) } else { r2 = ret.Error(2) } return r0, r1, r2 } // MockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom' type MockUDPConn_ReadFrom_Call struct { *mock.Call } // ReadFrom is a helper method to define mock.On call // - b []byte func (_e *MockUDPConn_Expecter) ReadFrom(b interface{}) *MockUDPConn_ReadFrom_Call { return &MockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)} } func (_c *MockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *MockUDPConn_ReadFrom_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *MockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *MockUDPConn_ReadFrom_Call { _c.Call.Return(_a0, _a1, _a2) return _c } func (_c *MockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *MockUDPConn_ReadFrom_Call { _c.Call.Return(run) return _c } // WriteTo provides a mock function with given fields: b, addr func (_m *MockUDPConn) WriteTo(b []byte, addr string) (int, error) { ret := _m.Called(b, addr) if len(ret) == 0 { panic("no return value specified for WriteTo") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok { return rf(b, addr) } if rf, ok := ret.Get(0).(func([]byte, string) int); ok { r0 = rf(b, addr) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte, string) error); ok { r1 = rf(b, addr) } else { r1 = ret.Error(1) } return r0, r1 } // MockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo' type MockUDPConn_WriteTo_Call struct { *mock.Call } // WriteTo is a helper method to define mock.On call // - b []byte // - addr string func (_e *MockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *MockUDPConn_WriteTo_Call { return &MockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)} } func (_c *MockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *MockUDPConn_WriteTo_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte), args[1].(string)) }) return _c } func (_c *MockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *MockUDPConn_WriteTo_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *MockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *MockUDPConn_WriteTo_Call { _c.Call.Return(run) return _c } // NewMockUDPConn creates a new instance of MockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockUDPConn(t interface { mock.TestingT Cleanup(func()) }) *MockUDPConn { mock := &MockUDPConn{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/internal/integration_tests/smoke_test.go ================================================ package integration_tests import ( "io" "net" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/apernet/hysteria/core/v2/client" coreErrs "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" "github.com/apernet/hysteria/core/v2/server" ) // Smoke tests that act as a sanity check for client & server to ensure they can talk to each other correctly. // TestClientNoServer tests how the client handles a server address it cannot connect to. // NewClient should return a ConnectError. func TestClientNoServer(t *testing.T) { c, _, err := client.NewClient(&client.Config{ ServerAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 55666}, }) assert.Nil(t, c) _, ok := err.(coreErrs.ConnectError) assert.True(t, ok) } // TestClientServerBadAuth tests two things: // - The server uses Authenticator when a client connects. // - How the client handles failed authentication. func TestClientServerBadAuth(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, "badpassword", uint64(0)).Return(false, "").Once() s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, Auth: "badpassword", TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.Nil(t, c) _, ok := err.(coreErrs.AuthError) assert.True(t, ok) } // TestClientServerUDPDisabled tests how the client handles a server that does not support UDP. // UDP should return a DialError. func TestClientServerUDPDisabled(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, DisableUDP: true, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() conn, err := c.UDP() assert.Nil(t, conn) _, ok := err.(coreErrs.DialError) assert.True(t, ok) } // TestClientServerTCPEcho tests TCP forwarding using a TCP echo server. func TestClientServerTCPEcho(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create TCP echo server echoAddr := "127.0.0.1:22333" echoListener, err := net.Listen("tcp", echoAddr) assert.NoError(t, err) echoServer := &tcpEchoServer{Listener: echoListener} defer echoServer.Close() go echoServer.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() // Dial TCP conn, err := c.TCP(echoAddr) assert.NoError(t, err) defer conn.Close() // Send and receive data sData := []byte("hello world") _, err = conn.Write(sData) assert.NoError(t, err) rData := make([]byte, len(sData)) _, err = io.ReadFull(conn, rData) assert.NoError(t, err) assert.Equal(t, sData, rData) } // TestClientServerUDPEcho tests UDP forwarding using a UDP echo server. func TestClientServerUDPEcho(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create UDP echo server echoAddr := "127.0.0.1:22333" echoConn, err := net.ListenPacket("udp", echoAddr) assert.NoError(t, err) echoServer := &udpEchoServer{Conn: echoConn} defer echoServer.Close() go echoServer.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() // Listen UDP conn, err := c.UDP() assert.NoError(t, err) defer conn.Close() // Send and receive data sData := []byte("hello world") err = conn.Send(sData, echoAddr) assert.NoError(t, err) rData, rAddr, err := conn.Receive() assert.NoError(t, err) assert.Equal(t, sData, rData) assert.Equal(t, echoAddr, rAddr) } // TestClientServerHandshakeInfo tests that the client returns the correct handshake info. func TestClientServerHandshakeInfo(t *testing.T) { // Create server 1, UDP enabled, unlimited bandwidth udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) go s.Serve() // Create client 1, with specified tx bandwidth c, info, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, BandwidthConfig: client.BandwidthConfig{ MaxTx: 123456, }, }) assert.NoError(t, err) assert.Equal(t, &client.HandshakeInfo{ UDPEnabled: true, Tx: 123456, }, info) // Close server 1 and client 1 _ = s.Close() _ = c.Close() // Create server 2, UDP disabled, limited rx bandwidth udpConn, udpAddr, err = serverConn() assert.NoError(t, err) s, err = server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, BandwidthConfig: server.BandwidthConfig{ MaxRx: 100000, }, DisableUDP: true, Authenticator: auth, }) assert.NoError(t, err) go s.Serve() // Create client 2, with specified tx bandwidth c, info, err = client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, BandwidthConfig: client.BandwidthConfig{ MaxTx: 123456, }, }) assert.NoError(t, err) assert.Equal(t, &client.HandshakeInfo{ UDPEnabled: false, Tx: 100000, }, info) // Close server 2 and client 2 _ = s.Close() _ = c.Close() // Create server 3, UDP enabled, ignore client bandwidth udpConn, udpAddr, err = serverConn() assert.NoError(t, err) s, err = server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, IgnoreClientBandwidth: true, Authenticator: auth, }) assert.NoError(t, err) go s.Serve() // Create client 3, with specified tx bandwidth c, info, err = client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, BandwidthConfig: client.BandwidthConfig{ MaxTx: 123456, }, }) assert.NoError(t, err) assert.Equal(t, &client.HandshakeInfo{ UDPEnabled: true, Tx: 0, }, info) // Close server 3 and client 3 _ = s.Close() _ = c.Close() } ================================================ FILE: core/internal/integration_tests/stress_test.go ================================================ package integration_tests import ( "context" "crypto/rand" "fmt" "io" "net" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "golang.org/x/time/rate" "github.com/apernet/hysteria/core/v2/client" "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" "github.com/apernet/hysteria/core/v2/server" ) type tcpStressor struct { DialFunc func() (net.Conn, error) Size int Parallel int Iterations int } func (s *tcpStressor) Run(t *testing.T) { // Make some random data sData := make([]byte, s.Size) _, err := rand.Read(sData) assert.NoError(t, err) // Run iterations for i := 0; i < s.Iterations; i++ { var wg sync.WaitGroup errChan := make(chan error, s.Parallel) for j := 0; j < s.Parallel; j++ { wg.Add(1) go func() { defer wg.Done() conn, err := s.DialFunc() if err != nil { errChan <- err return } defer conn.Close() go conn.Write(sData) rData := make([]byte, len(sData)) _, err = io.ReadFull(conn, rData) if err != nil { errChan <- err return } }() } wg.Wait() assert.Empty(t, errChan) } } type udpStressor struct { ListenFunc func() (client.HyUDPConn, error) ServerAddr string Size int Count int Parallel int Iterations int } func (s *udpStressor) Run(t *testing.T) { // Make some random data sData := make([]byte, s.Size) _, err := rand.Read(sData) assert.NoError(t, err) // Due to UDP's unreliability, we need to limit the rate of sending // to reduce packet loss. This is hardcoded to 1 MiB/s for now. limiter := rate.NewLimiter(1048576, 1048576) // Run iterations for i := 0; i < s.Iterations; i++ { var wg sync.WaitGroup errChan := make(chan error, s.Parallel) for j := 0; j < s.Parallel; j++ { wg.Add(1) go func() { defer wg.Done() conn, err := s.ListenFunc() if err != nil { errChan <- err return } defer conn.Close() go func() { // Sending routine for i := 0; i < s.Count; i++ { _ = limiter.WaitN(context.Background(), len(sData)) _ = conn.Send(sData, s.ServerAddr) } }() minCount := s.Count * 8 / 10 // Tolerate 20% packet loss for i := 0; i < minCount; i++ { rData, _, err := conn.Receive() if err != nil { errChan <- err return } if len(rData) != len(sData) { errChan <- fmt.Errorf("incomplete data received: %d/%d bytes", len(rData), len(sData)) return } } }() } wg.Wait() assert.Empty(t, errChan) } } func TestClientServerTCPStress(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create TCP echo server echoAddr := "127.0.0.1:22333" echoListener, err := net.Listen("tcp", echoAddr) assert.NoError(t, err) echoServer := &tcpEchoServer{Listener: echoListener} defer echoServer.Close() go echoServer.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() dialFunc := func() (net.Conn, error) { return c.TCP(echoAddr) } t.Run("Single 500m", (&tcpStressor{DialFunc: dialFunc, Size: 524288000, Parallel: 1, Iterations: 1}).Run) t.Run("Sequential 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1, Iterations: 1000}).Run) t.Run("Sequential 10000x100k", (&tcpStressor{DialFunc: dialFunc, Size: 102400, Parallel: 1, Iterations: 10000}).Run) t.Run("Parallel 100x10m", (&tcpStressor{DialFunc: dialFunc, Size: 10485760, Parallel: 100, Iterations: 1}).Run) t.Run("Parallel 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1000, Iterations: 1}).Run) } func TestClientServerUDPStress(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Authenticator: auth, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create UDP echo server echoAddr := "127.0.0.1:22333" echoConn, err := net.ListenPacket("udp", echoAddr) assert.NoError(t, err) echoServer := &udpEchoServer{Conn: echoConn} defer echoServer.Close() go echoServer.Serve() // Create client c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() t.Run("Single 1000x100b", (&udpStressor{ ListenFunc: c.UDP, ServerAddr: echoAddr, Size: 100, Count: 1000, Parallel: 1, Iterations: 1, }).Run) t.Run("Single 1000x3k", (&udpStressor{ ListenFunc: c.UDP, ServerAddr: echoAddr, Size: 3000, Count: 1000, Parallel: 1, Iterations: 1, }).Run) t.Run("5 Sequential 1000x100b", (&udpStressor{ ListenFunc: c.UDP, ServerAddr: echoAddr, Size: 100, Count: 1000, Parallel: 1, Iterations: 5, }).Run) t.Run("5 Sequential 200x3k", (&udpStressor{ ListenFunc: c.UDP, ServerAddr: echoAddr, Size: 3000, Count: 200, Parallel: 1, Iterations: 5, }).Run) t.Run("2 Sequential 5 Parallel 1000x100b", (&udpStressor{ ListenFunc: c.UDP, ServerAddr: echoAddr, Size: 100, Count: 1000, Parallel: 5, Iterations: 2, }).Run) t.Run("2 Sequential 5 Parallel 200x3k", (&udpStressor{ ListenFunc: c.UDP, ServerAddr: echoAddr, Size: 3000, Count: 200, Parallel: 5, Iterations: 2, }).Run) } ================================================ FILE: core/internal/integration_tests/test.crt ================================================ -----BEGIN CERTIFICATE----- MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3 DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4 ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf 1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4 FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7 ydYKuI8= -----END CERTIFICATE----- ================================================ FILE: core/internal/integration_tests/test.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR 2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr 6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2 wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+ FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U 64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu 1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0 jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2 ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+ menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2 kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ -----END RSA PRIVATE KEY----- ================================================ FILE: core/internal/integration_tests/trafficlogger_test.go ================================================ package integration_tests import ( "io" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/apernet/hysteria/core/v2/client" "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks" "github.com/apernet/hysteria/core/v2/server" ) // TestClientServerTrafficLoggerTCP tests that the traffic logger is correctly called for TCP connections, // and that the client is disconnected when the traffic logger returns false. func TestClientServerTrafficLoggerTCP(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) serverOb := mocks.NewMockOutbound(t) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") trafficLogger := mocks.NewMockTrafficLogger(t) s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Outbound: serverOb, Authenticator: auth, TrafficLogger: trafficLogger, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create client trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once() c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() addr := "dontcare.cc:4455" sobConn := mocks.NewMockConn(t) sobConnCh := make(chan []byte, 1) sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { b := <-sobConnCh if b == nil { return 0, io.EOF } else { return copy(bs, b), nil } }) sobConn.EXPECT().Close().RunAndReturn(func() error { sobConnChCloseFunc() return nil }) serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once() conn, err := c.TCP(addr) assert.NoError(t, err) // Client reads from server trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(11)).Return(true).Once() sobConnCh <- []byte("knock knock") buf := make([]byte, 100) n, err := conn.Read(buf) assert.NoError(t, err) assert.Equal(t, 11, n) assert.Equal(t, "knock knock", string(buf[:n])) // Client writes to server trafficLogger.EXPECT().LogTraffic("nobody", uint64(12), uint64(0)).Return(true).Once() sobConn.EXPECT().Write([]byte("who is there")).Return(12, nil).Once() n, err = conn.Write([]byte("who is there")) assert.NoError(t, err) assert.Equal(t, 12, n) time.Sleep(1 * time.Second) // Need some time for the server to receive the data // Client reads from server again but blocked trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once() trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once() sobConnCh <- []byte("nope") n, err = conn.Read(buf) assert.Zero(t, n) assert.Error(t, err) // The client should be disconnected _, err = c.TCP("whatever") assert.Error(t, err) } // TestClientServerTrafficLoggerUDP tests that the traffic logger is correctly called for UDP sessions, // and that the client is disconnected when the traffic logger returns false. func TestClientServerTrafficLoggerUDP(t *testing.T) { // Create server udpConn, udpAddr, err := serverConn() assert.NoError(t, err) serverOb := mocks.NewMockOutbound(t) auth := mocks.NewMockAuthenticator(t) auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody") trafficLogger := mocks.NewMockTrafficLogger(t) s, err := server.NewServer(&server.Config{ TLSConfig: serverTLSConfig(), Conn: udpConn, Outbound: serverOb, Authenticator: auth, TrafficLogger: trafficLogger, }) assert.NoError(t, err) defer s.Close() go s.Serve() // Create client trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once() c, _, err := client.NewClient(&client.Config{ ServerAddr: udpAddr, TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, }) assert.NoError(t, err) defer c.Close() addr := "shady.org:43211" sobConn := mocks.NewMockUDPConn(t) sobConnCh := make(chan []byte, 1) sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) }) sobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) { b := <-sobConnCh if b == nil { return 0, "", io.EOF } else { return copy(bs, b), addr, nil } }) sobConn.EXPECT().Close().RunAndReturn(func() error { sobConnChCloseFunc() return nil }) serverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once() conn, err := c.UDP() assert.NoError(t, err) // Client writes to server trafficLogger.EXPECT().LogTraffic("nobody", uint64(9), uint64(0)).Return(true).Once() sobConn.EXPECT().WriteTo([]byte("small sad"), addr).Return(9, nil).Once() err = conn.Send([]byte("small sad"), addr) assert.NoError(t, err) time.Sleep(1 * time.Second) // Need some time for the server to receive the data // Client reads from server trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(7)).Return(true).Once() sobConnCh <- []byte("big mad") bs, rAddr, err := conn.Receive() assert.NoError(t, err) assert.Equal(t, rAddr, addr) assert.Equal(t, "big mad", string(bs)) // Client reads from server again but blocked trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once() trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once() sobConnCh <- []byte("nope") bs, rAddr, err = conn.Receive() assert.Equal(t, err, io.EOF) assert.Empty(t, rAddr) assert.Empty(t, bs) // The client should be disconnected _, err = c.UDP() assert.Error(t, err) } ================================================ FILE: core/internal/integration_tests/utils_test.go ================================================ package integration_tests import ( "crypto/tls" "io" "net" "github.com/apernet/hysteria/core/v2/server" ) // This file provides utilities for the integration tests. const ( testCertFile = "test.crt" testKeyFile = "test.key" ) func serverTLSConfig() server.TLSConfig { cert, err := tls.LoadX509KeyPair(testCertFile, testKeyFile) if err != nil { panic(err) } return server.TLSConfig{ Certificates: []tls.Certificate{cert}, } } func serverConn() (net.PacketConn, net.Addr, error) { udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} udpConn, err := net.ListenUDP("udp", udpAddr) if err != nil { return nil, nil, err } return udpConn, udpAddr, nil } // tcpEchoServer is a TCP server that echoes what it reads from the connection. // It will never actively close the connection. type tcpEchoServer struct { Listener net.Listener } func (s *tcpEchoServer) Serve() error { for { conn, err := s.Listener.Accept() if err != nil { return err } go func() { _, _ = io.Copy(conn, conn) _ = conn.Close() }() } } func (s *tcpEchoServer) Close() error { return s.Listener.Close() } // udpEchoServer is a UDP server that echoes what it reads from the connection. // It will never actively close the connection. type udpEchoServer struct { Conn net.PacketConn } func (s *udpEchoServer) Serve() error { buf := make([]byte, 65536) for { n, addr, err := s.Conn.ReadFrom(buf) if err != nil { return err } _, err = s.Conn.WriteTo(buf[:n], addr) if err != nil { return err } } } func (s *udpEchoServer) Close() error { return s.Conn.Close() } ================================================ FILE: core/internal/pmtud/avail.go ================================================ //go:build linux || windows || darwin package pmtud const ( DisablePathMTUDiscovery = false ) ================================================ FILE: core/internal/pmtud/unavail.go ================================================ //go:build !linux && !windows && !darwin package pmtud // quic-go's MTU detection is enabled by default on all platforms. // However, it only actually sets the DF bit on 3 supported platforms (Windows, macOS, Linux). // As a result, on other platforms, probe packets that should never be fragmented will still // be fragmented and transmitted. So we're only enabling it for platforms where we've verified // its functionality for now. const ( DisablePathMTUDiscovery = true ) ================================================ FILE: core/internal/protocol/http.go ================================================ package protocol import ( "net/http" "strconv" ) const ( URLHost = "hysteria" URLPath = "/auth" RequestHeaderAuth = "Hysteria-Auth" ResponseHeaderUDPEnabled = "Hysteria-UDP" CommonHeaderCCRX = "Hysteria-CC-RX" CommonHeaderPadding = "Hysteria-Padding" StatusAuthOK = 233 ) // AuthRequest is what client sends to server for authentication. type AuthRequest struct { Auth string Rx uint64 // 0 = unknown, client asks server to use bandwidth detection } // AuthResponse is what server sends to client when authentication is passed. type AuthResponse struct { UDPEnabled bool Rx uint64 // 0 = unlimited RxAuto bool // true = server asks client to use bandwidth detection } func AuthRequestFromHeader(h http.Header) AuthRequest { rx, _ := strconv.ParseUint(h.Get(CommonHeaderCCRX), 10, 64) return AuthRequest{ Auth: h.Get(RequestHeaderAuth), Rx: rx, } } func AuthRequestToHeader(h http.Header, req AuthRequest) { h.Set(RequestHeaderAuth, req.Auth) h.Set(CommonHeaderCCRX, strconv.FormatUint(req.Rx, 10)) h.Set(CommonHeaderPadding, authRequestPadding.String()) } func AuthResponseFromHeader(h http.Header) AuthResponse { resp := AuthResponse{} resp.UDPEnabled, _ = strconv.ParseBool(h.Get(ResponseHeaderUDPEnabled)) rxStr := h.Get(CommonHeaderCCRX) if rxStr == "auto" { // Special case for server requesting client to use bandwidth detection resp.RxAuto = true } else { resp.Rx, _ = strconv.ParseUint(rxStr, 10, 64) } return resp } func AuthResponseToHeader(h http.Header, resp AuthResponse) { h.Set(ResponseHeaderUDPEnabled, strconv.FormatBool(resp.UDPEnabled)) if resp.RxAuto { h.Set(CommonHeaderCCRX, "auto") } else { h.Set(CommonHeaderCCRX, strconv.FormatUint(resp.Rx, 10)) } h.Set(CommonHeaderPadding, authResponsePadding.String()) } ================================================ FILE: core/internal/protocol/padding.go ================================================ package protocol import ( "math/rand" ) const ( paddingChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) // padding specifies a half-open range [Min, Max). type padding struct { Min int Max int } func (p padding) String() string { n := p.Min + rand.Intn(p.Max-p.Min) bs := make([]byte, n) for i := range bs { bs[i] = paddingChars[rand.Intn(len(paddingChars))] } return string(bs) } var ( authRequestPadding = padding{Min: 256, Max: 2048} authResponsePadding = padding{Min: 256, Max: 2048} tcpRequestPadding = padding{Min: 64, Max: 512} tcpResponsePadding = padding{Min: 128, Max: 1024} ) ================================================ FILE: core/internal/protocol/proxy.go ================================================ package protocol import ( "bytes" "encoding/binary" "fmt" "io" "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/quic-go/quicvarint" ) const ( FrameTypeTCPRequest = 0x401 // Max length values are for preventing DoS attacks MaxAddressLength = 2048 MaxMessageLength = 2048 MaxPaddingLength = 4096 MaxUDPSize = 4096 maxVarInt1 = 63 maxVarInt2 = 16383 maxVarInt4 = 1073741823 maxVarInt8 = 4611686018427387903 ) // TCPRequest format: // 0x401 (QUIC varint) // Address length (QUIC varint) // Address (bytes) // Padding length (QUIC varint) // Padding (bytes) func ReadTCPRequest(r io.Reader) (string, error) { bReader := quicvarint.NewReader(r) addrLen, err := quicvarint.Read(bReader) if err != nil { return "", err } if addrLen == 0 || addrLen > MaxAddressLength { return "", errors.ProtocolError{Message: "invalid address length"} } addrBuf := make([]byte, addrLen) _, err = io.ReadFull(r, addrBuf) if err != nil { return "", err } paddingLen, err := quicvarint.Read(bReader) if err != nil { return "", err } if paddingLen > MaxPaddingLength { return "", errors.ProtocolError{Message: "invalid padding length"} } if paddingLen > 0 { _, err = io.CopyN(io.Discard, r, int64(paddingLen)) if err != nil { return "", err } } return string(addrBuf), nil } func WriteTCPRequest(w io.Writer, addr string) error { padding := tcpRequestPadding.String() paddingLen := len(padding) addrLen := len(addr) sz := int(quicvarint.Len(FrameTypeTCPRequest)) + int(quicvarint.Len(uint64(addrLen))) + addrLen + int(quicvarint.Len(uint64(paddingLen))) + paddingLen buf := make([]byte, sz) i := varintPut(buf, FrameTypeTCPRequest) i += varintPut(buf[i:], uint64(addrLen)) i += copy(buf[i:], addr) i += varintPut(buf[i:], uint64(paddingLen)) copy(buf[i:], padding) _, err := w.Write(buf) return err } // TCPResponse format: // Status (byte, 0=ok, 1=error) // Message length (QUIC varint) // Message (bytes) // Padding length (QUIC varint) // Padding (bytes) func ReadTCPResponse(r io.Reader) (bool, string, error) { var status [1]byte if _, err := io.ReadFull(r, status[:]); err != nil { return false, "", err } bReader := quicvarint.NewReader(r) msgLen, err := quicvarint.Read(bReader) if err != nil { return false, "", err } if msgLen > MaxMessageLength { return false, "", errors.ProtocolError{Message: "invalid message length"} } var msgBuf []byte // No message is fine if msgLen > 0 { msgBuf = make([]byte, msgLen) _, err = io.ReadFull(r, msgBuf) if err != nil { return false, "", err } } paddingLen, err := quicvarint.Read(bReader) if err != nil { return false, "", err } if paddingLen > MaxPaddingLength { return false, "", errors.ProtocolError{Message: "invalid padding length"} } if paddingLen > 0 { _, err = io.CopyN(io.Discard, r, int64(paddingLen)) if err != nil { return false, "", err } } return status[0] == 0, string(msgBuf), nil } func WriteTCPResponse(w io.Writer, ok bool, msg string) error { padding := tcpResponsePadding.String() paddingLen := len(padding) msgLen := len(msg) sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen + int(quicvarint.Len(uint64(paddingLen))) + paddingLen buf := make([]byte, sz) if ok { buf[0] = 0 } else { buf[0] = 1 } i := varintPut(buf[1:], uint64(msgLen)) i += copy(buf[1+i:], msg) i += varintPut(buf[1+i:], uint64(paddingLen)) copy(buf[1+i:], padding) _, err := w.Write(buf) return err } // UDPMessage format: // Session ID (uint32 BE) // Packet ID (uint16 BE) // Fragment ID (uint8) // Fragment count (uint8) // Address length (QUIC varint) // Address (bytes) // Data... type UDPMessage struct { SessionID uint32 // 4 PacketID uint16 // 2 FragID uint8 // 1 FragCount uint8 // 1 Addr string // varint + bytes Data []byte } func (m *UDPMessage) HeaderSize() int { lAddr := len(m.Addr) return 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr } func (m *UDPMessage) Size() int { return m.HeaderSize() + len(m.Data) } func (m *UDPMessage) Serialize(buf []byte) int { // Make sure the buffer is big enough if len(buf) < m.Size() { return -1 } binary.BigEndian.PutUint32(buf, m.SessionID) binary.BigEndian.PutUint16(buf[4:], m.PacketID) buf[6] = m.FragID buf[7] = m.FragCount i := varintPut(buf[8:], uint64(len(m.Addr))) i += copy(buf[8+i:], m.Addr) i += copy(buf[8+i:], m.Data) return 8 + i } func ParseUDPMessage(msg []byte) (*UDPMessage, error) { m := &UDPMessage{} buf := bytes.NewBuffer(msg) if err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil { return nil, err } if err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil { return nil, err } if err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil { return nil, err } if err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil { return nil, err } lAddr, err := quicvarint.Read(buf) if err != nil { return nil, err } if lAddr == 0 || lAddr > MaxMessageLength { return nil, errors.ProtocolError{Message: "invalid address length"} } bs := buf.Bytes() if len(bs) <= int(lAddr) { // We use <= instead of < here as we expect at least one byte of data after the address return nil, errors.ProtocolError{Message: "invalid message length"} } m.Addr = string(bs[:lAddr]) m.Data = bs[lAddr:] return m, nil } // varintPut is like quicvarint.Append, but instead of appending to a slice, // it writes to a fixed-size buffer. Returns the number of bytes written. func varintPut(b []byte, i uint64) int { if i <= maxVarInt1 { b[0] = uint8(i) return 1 } if i <= maxVarInt2 { b[0] = uint8(i>>8) | 0x40 b[1] = uint8(i) return 2 } if i <= maxVarInt4 { b[0] = uint8(i>>24) | 0x80 b[1] = uint8(i >> 16) b[2] = uint8(i >> 8) b[3] = uint8(i) return 4 } if i <= maxVarInt8 { b[0] = uint8(i>>56) | 0xc0 b[1] = uint8(i >> 48) b[2] = uint8(i >> 40) b[3] = uint8(i >> 32) b[4] = uint8(i >> 24) b[5] = uint8(i >> 16) b[6] = uint8(i >> 8) b[7] = uint8(i) return 8 } panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i)) } ================================================ FILE: core/internal/protocol/proxy_test.go ================================================ package protocol import ( "bytes" "reflect" "strings" "testing" ) func TestUDPMessage(t *testing.T) { t.Run("buffer too small", func(t *testing.T) { // Make sure Serialize returns -1 when the buffer is too small. tBuf := make([]byte, 20) if (&UDPMessage{ SessionID: 66, PacketID: 77, FragID: 2, FragCount: 5, Addr: "random_addr", Data: []byte("random_data"), }).Serialize(tBuf) != -1 { t.Error("Serialize() did not return -1 when the buffer was too small") } }) type fields struct { SessionID uint32 PacketID uint16 FragID uint8 FragCount uint8 Addr string Data []byte } tests := []struct { name string fields fields want []byte }{ { name: "test 1", fields: fields{ SessionID: 1, PacketID: 1, FragID: 0, FragCount: 1, Addr: "example.com:80", Data: []byte("GET /nothing HTTP/1.1\r\n"), }, want: []byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0xe, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x38, 0x30, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31, 0xd, 0xa}, }, { name: "test 2", fields: fields{ SessionID: 1329655244, Addr: "some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long:9000", PacketID: 62233, FragID: 8, FragCount: 19, Data: []byte("God is great, beer is good, and people are crazy."), }, want: []byte{0x4f, 0x40, 0xed, 0xcc, 0xf3, 0x19, 0x8, 0x13, 0x41, 0xee, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x3a, 0x39, 0x30, 0x30, 0x30, 0x47, 0x6f, 0x64, 0x20, 0x69, 0x73, 0x20, 0x67, 0x72, 0x65, 0x61, 0x74, 0x2c, 0x20, 0x62, 0x65, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x67, 0x6f, 0x6f, 0x64, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x20, 0x61, 0x72, 0x65, 0x20, 0x63, 0x72, 0x61, 0x7a, 0x79, 0x2e}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &UDPMessage{ SessionID: tt.fields.SessionID, Addr: tt.fields.Addr, PacketID: tt.fields.PacketID, FragID: tt.fields.FragID, FragCount: tt.fields.FragCount, Data: tt.fields.Data, } // Serialize buf := make([]byte, MaxUDPSize) n := m.Serialize(buf) if got := buf[:n]; !reflect.DeepEqual(got, tt.want) { t.Errorf("Serialize() = %v, want %v", got, tt.want) } // Parse back if m2, err := ParseUDPMessage(tt.want); err != nil { t.Errorf("ParseUDPMessage() error = %v", err) } else { if !reflect.DeepEqual(m2, m) { t.Errorf("ParseUDPMessage() = %v, want %v", m2, m) } } }) } } // TestUDPMessageMalformed is to make sure ParseUDPMessage() fails (but not panic) on malformed data. func TestUDPMessageMalformed(t *testing.T) { tests := []struct { name string data []byte }{ { name: "empty", data: []byte{}, }, { name: "zeroes 1", data: []byte{0, 0, 0, 0}, }, { name: "zeroes 2", data: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }, { name: "incomplete 1", data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55}, }, { name: "incomplete 2", data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x90, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if _, err := ParseUDPMessage(tt.data); err == nil { t.Errorf("ParseUDPMessage() should fail") } }) } } func TestReadTCPRequest(t *testing.T) { tests := []struct { name string data []byte want string wantErr bool }{ { name: "normal no padding", data: []byte("\x0egoogle.com:443\x00"), want: "google.com:443", wantErr: false, }, { name: "normal with padding", data: []byte("\x0bholy.cc:443\x02gg"), want: "holy.cc:443", wantErr: false, }, { name: "incomplete 1", data: []byte("\x0bhoho"), want: "", wantErr: true, }, { name: "incomplete 2", data: []byte("\x0bholy.cc:443\x05x"), want: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := bytes.NewReader(tt.data) got, err := ReadTCPRequest(r) if (err != nil) != tt.wantErr { t.Errorf("ReadTCPRequest() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ReadTCPRequest() got = %v, want %v", got, tt.want) } }) } } func TestWriteTCPRequest(t *testing.T) { tests := []struct { name string addr string wantW string // Just a prefix, we don't care about the padding wantErr bool }{ { name: "normal 1", addr: "google.com:443", wantW: "\x44\x01\x0egoogle.com:443", wantErr: false, }, { name: "normal 2", addr: "client-api.arkoselabs.com:8080", wantW: "\x44\x01\x1eclient-api.arkoselabs.com:8080", wantErr: false, }, { name: "empty", addr: "", wantW: "\x44\x01\x00", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} err := WriteTCPRequest(w, tt.addr) if (err != nil) != tt.wantErr { t.Errorf("WriteTCPRequest() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) { t.Errorf("WriteTCPRequest() gotW = %v, want %v", gotW, tt.wantW) } }) } } func TestReadTCPResponse(t *testing.T) { tests := []struct { name string data []byte want bool want1 string wantErr bool }{ { name: "normal ok no padding", data: []byte("\x00\x0bhello world\x00"), want: true, want1: "hello world", wantErr: false, }, { name: "normal error with padding", data: []byte("\x01\x06stop!!\x05xxxxx"), want1: "stop!!", wantErr: false, }, { name: "normal ok no message with padding", data: []byte("\x01\x00\x05xxxxx"), want1: "", wantErr: false, }, { name: "incomplete 1", data: []byte("\x00\x0bhoho"), want1: "", wantErr: true, }, { name: "incomplete 2", data: []byte("\x01\x05jesus\x05x"), want1: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := bytes.NewReader(tt.data) got, got1, err := ReadTCPResponse(r) if (err != nil) != tt.wantErr { t.Errorf("ReadTCPResponse() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("ReadTCPResponse() got = %v, want %v", got, tt.want) } if got1 != tt.want1 { t.Errorf("ReadTCPResponse() got1 = %v, want %v", got1, tt.want1) } }) } } func TestWriteTCPResponse(t *testing.T) { type args struct { ok bool msg string } tests := []struct { name string args args wantW string // Just a prefix, we don't care about the padding wantErr bool }{ { name: "normal ok", args: args{ok: true, msg: "hello world"}, wantW: "\x00\x0bhello world", wantErr: false, }, { name: "normal error", args: args{ok: false, msg: "stop!!"}, wantW: "\x01\x06stop!!", wantErr: false, }, { name: "empty", args: args{ok: true, msg: ""}, wantW: "\x00\x00", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} err := WriteTCPResponse(w, tt.args.ok, tt.args.msg) if (err != nil) != tt.wantErr { t.Errorf("WriteTCPResponse() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) { t.Errorf("WriteTCPResponse() gotW = %v, want %v", gotW, tt.wantW) } }) } } ================================================ FILE: core/internal/utils/atomic.go ================================================ package utils import ( "sync/atomic" "time" ) type AtomicTime struct { v atomic.Value } func NewAtomicTime(t time.Time) *AtomicTime { a := &AtomicTime{} a.Set(t) return a } func (t *AtomicTime) Set(new time.Time) { t.v.Store(new) } func (t *AtomicTime) Get() time.Time { return t.v.Load().(time.Time) } ================================================ FILE: core/internal/utils/qstream.go ================================================ package utils import ( "context" "time" "github.com/apernet/quic-go" ) // QStream is a wrapper of quic.Stream that handles Close() in a way that // makes more sense to us. By default, quic.Stream's Close() only closes // the write side of the stream, not the read side. And if there is unread // data, the stream is not really considered closed until either the data // is drained or CancelRead() is called. // References: // - https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/stream.go // - https://github.com/quic-go/quic-go/issues/3558 // - https://github.com/quic-go/quic-go/issues/1599 type QStream struct { Stream quic.Stream } func (s *QStream) StreamID() quic.StreamID { return s.Stream.StreamID() } func (s *QStream) Read(p []byte) (n int, err error) { return s.Stream.Read(p) } func (s *QStream) CancelRead(code quic.StreamErrorCode) { s.Stream.CancelRead(code) } func (s *QStream) SetReadDeadline(t time.Time) error { return s.Stream.SetReadDeadline(t) } func (s *QStream) Write(p []byte) (n int, err error) { return s.Stream.Write(p) } func (s *QStream) Close() error { s.Stream.CancelRead(0) return s.Stream.Close() } func (s *QStream) CancelWrite(code quic.StreamErrorCode) { s.Stream.CancelWrite(code) } func (s *QStream) Context() context.Context { return s.Stream.Context() } func (s *QStream) SetWriteDeadline(t time.Time) error { return s.Stream.SetWriteDeadline(t) } func (s *QStream) SetDeadline(t time.Time) error { return s.Stream.SetDeadline(t) } ================================================ FILE: core/server/.mockery.yaml ================================================ with-expecter: true inpackage: true dir: . packages: github.com/apernet/hysteria/core/v2/server: interfaces: udpIO: config: mockname: mockUDPIO udpEventLogger: config: mockname: mockUDPEventLogger UDPConn: config: mockname: mockUDPConn ================================================ FILE: core/server/config.go ================================================ package server import ( "crypto/tls" "net" "net/http" "time" "github.com/apernet/hysteria/core/v2/errors" "github.com/apernet/hysteria/core/v2/internal/pmtud" "github.com/apernet/quic-go" ) const ( defaultStreamReceiveWindow = 8388608 // 8MB defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB defaultMaxIdleTimeout = 30 * time.Second defaultMaxIncomingStreams = 1024 defaultUDPIdleTimeout = 60 * time.Second ) type Config struct { TLSConfig TLSConfig QUICConfig QUICConfig Conn net.PacketConn RequestHook RequestHook Outbound Outbound BandwidthConfig BandwidthConfig IgnoreClientBandwidth bool DisableUDP bool UDPIdleTimeout time.Duration Authenticator Authenticator EventLogger EventLogger TrafficLogger TrafficLogger MasqHandler http.Handler } // fill fills the fields that are not set by the user with default values when possible, // and returns an error if the user has not set a required field, or if a field is invalid. func (c *Config) fill() error { if len(c.TLSConfig.Certificates) == 0 && c.TLSConfig.GetCertificate == nil { return errors.ConfigError{Field: "TLSConfig", Reason: "must set at least one of Certificates or GetCertificate"} } if c.QUICConfig.InitialStreamReceiveWindow == 0 { c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow } else if c.QUICConfig.InitialStreamReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.MaxStreamReceiveWindow == 0 { c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow } else if c.QUICConfig.MaxStreamReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.InitialConnectionReceiveWindow == 0 { c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow } else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.MaxConnectionReceiveWindow == 0 { c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow } else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 { return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"} } if c.QUICConfig.MaxIdleTimeout == 0 { c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout } else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second { return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"} } if c.QUICConfig.MaxIncomingStreams == 0 { c.QUICConfig.MaxIncomingStreams = defaultMaxIncomingStreams } else if c.QUICConfig.MaxIncomingStreams < 8 { return errors.ConfigError{Field: "QUICConfig.MaxIncomingStreams", Reason: "must be at least 8"} } c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery if c.Conn == nil { return errors.ConfigError{Field: "Conn", Reason: "must be set"} } if c.Outbound == nil { c.Outbound = &defaultOutbound{} } if c.BandwidthConfig.MaxTx != 0 && c.BandwidthConfig.MaxTx < 65536 { return errors.ConfigError{Field: "BandwidthConfig.MaxTx", Reason: "must be at least 65536"} } if c.BandwidthConfig.MaxRx != 0 && c.BandwidthConfig.MaxRx < 65536 { return errors.ConfigError{Field: "BandwidthConfig.MaxRx", Reason: "must be at least 65536"} } if c.UDPIdleTimeout == 0 { c.UDPIdleTimeout = defaultUDPIdleTimeout } else if c.UDPIdleTimeout < 2*time.Second || c.UDPIdleTimeout > 600*time.Second { return errors.ConfigError{Field: "UDPIdleTimeout", Reason: "must be between 2s and 600s"} } if c.Authenticator == nil { return errors.ConfigError{Field: "Authenticator", Reason: "must be set"} } return nil } // TLSConfig contains the TLS configuration fields that we want to expose to the user. type TLSConfig struct { Certificates []tls.Certificate GetCertificate func(info *tls.ClientHelloInfo) (*tls.Certificate, error) } // QUICConfig contains the QUIC configuration fields that we want to expose to the user. type QUICConfig struct { InitialStreamReceiveWindow uint64 MaxStreamReceiveWindow uint64 InitialConnectionReceiveWindow uint64 MaxConnectionReceiveWindow uint64 MaxIdleTimeout time.Duration MaxIncomingStreams int64 DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. } // RequestHook allows filtering and modifying requests before the server connects to the remote. // A request will only be hooked if Check returns true. // The returned byte slice, if not empty, will be sent to the remote before proxying - this is // mainly for "putting back" the content read from the client for sniffing, etc. // Return a non-nil error to abort the connection. // Note that due to the current architectural limitations, it can only inspect the first packet // of a UDP connection. It also cannot put back any data as the first packet is always sent as-is. type RequestHook interface { Check(isUDP bool, reqAddr string) bool TCP(stream quic.Stream, reqAddr *string) ([]byte, error) UDP(data []byte, reqAddr *string) error } // Outbound provides the implementation of how the server should connect to remote servers. // Although UDP includes a reqAddr, the implementation does not necessarily have to use it // to make a "connected" UDP connection that does not accept packets from other addresses. // In fact, the default implementation simply uses net.ListenUDP for a "full-cone" behavior. type Outbound interface { TCP(reqAddr string) (net.Conn, error) UDP(reqAddr string) (UDPConn, error) } // UDPConn is like net.PacketConn, but uses string for addresses. type UDPConn interface { ReadFrom(b []byte) (int, string, error) WriteTo(b []byte, addr string) (int, error) Close() error } type defaultOutbound struct{} var defaultOutboundDialer = net.Dialer{ Timeout: 10 * time.Second, } func (o *defaultOutbound) TCP(reqAddr string) (net.Conn, error) { return defaultOutboundDialer.Dial("tcp", reqAddr) } func (o *defaultOutbound) UDP(reqAddr string) (UDPConn, error) { conn, err := net.ListenUDP("udp", nil) if err != nil { return nil, err } return &defaultUDPConn{conn}, nil } type defaultUDPConn struct { *net.UDPConn } func (c *defaultUDPConn) ReadFrom(b []byte) (int, string, error) { n, addr, err := c.UDPConn.ReadFrom(b) if addr != nil { return n, addr.String(), err } else { return n, "", err } } func (c *defaultUDPConn) WriteTo(b []byte, addr string) (int, error) { uAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { return 0, err } return c.UDPConn.WriteTo(b, uAddr) } // BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second. type BandwidthConfig struct { MaxTx uint64 MaxRx uint64 } // Authenticator is an interface that provides authentication logic. type Authenticator interface { Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) } // EventLogger is an interface that provides logging logic. type EventLogger interface { Connect(addr net.Addr, id string, tx uint64) Disconnect(addr net.Addr, id string, err error) TCPRequest(addr net.Addr, id, reqAddr string) TCPError(addr net.Addr, id, reqAddr string, err error) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) UDPError(addr net.Addr, id string, sessionID uint32, err error) } // TrafficLogger is an interface that provides traffic logging logic. // Tx/Rx in this context refers to the server-remote (proxy target) perspective. // Tx is the bytes sent from the server to the remote. // Rx is the bytes received by the server from the remote. // Apart from logging, the Log function can also return false to signal // that the client should be disconnected. This can be used to implement // bandwidth limits or post-connection authentication, for example. // The implementation of this interface must be thread-safe. type TrafficLogger interface { LogTraffic(id string, tx, rx uint64) (ok bool) PushTrafficToV2boardInterval(url string, interval time.Duration) LogOnlineState(id string, online bool) NewKick(id string) (ok bool) } ================================================ FILE: core/server/copy.go ================================================ package server import ( "errors" "io" ) var errDisconnect = errors.New("traffic logger requested disconnect") func copyBufferLog(dst io.Writer, src io.Reader, log func(n uint64) bool) error { buf := make([]byte, 32*1024) for { nr, er := src.Read(buf) if nr > 0 { if !log(uint64(nr)) { // Log returns false, which means that the client should be disconnected return errDisconnect } _, ew := dst.Write(buf[0:nr]) if ew != nil { return ew } } if er != nil { if er == io.EOF { // EOF should not be considered as an error return nil } return er } } } func copyTwoWayWithLogger(id string, serverRw, remoteRw io.ReadWriter, l TrafficLogger) error { errChan := make(chan error, 2) go func() { errChan <- copyBufferLog(serverRw, remoteRw, func(n uint64) bool { return l.LogTraffic(id, 0, n) }) }() go func() { errChan <- copyBufferLog(remoteRw, serverRw, func(n uint64) bool { return l.LogTraffic(id, n, 0) }) }() // Block until one of the two goroutines returns return <-errChan } // copyTwoWay is the "fast-path" version of copyTwoWayWithLogger that does not log traffic. // It uses the built-in io.Copy instead of our own copyBufferLog. func copyTwoWay(serverRw, remoteRw io.ReadWriter) error { errChan := make(chan error, 2) go func() { _, err := io.Copy(serverRw, remoteRw) errChan <- err }() go func() { _, err := io.Copy(remoteRw, serverRw) errChan <- err }() // Block until one of the two goroutines returns return <-errChan } ================================================ FILE: core/server/mock_UDPConn.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package server import mock "github.com/stretchr/testify/mock" // mockUDPConn is an autogenerated mock type for the UDPConn type type mockUDPConn struct { mock.Mock } type mockUDPConn_Expecter struct { mock *mock.Mock } func (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter { return &mockUDPConn_Expecter{mock: &_m.Mock} } // Close provides a mock function with given fields: func (_m *mockUDPConn) Close() error { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type mockUDPConn_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call { return &mockUDPConn_Close_Call{Call: _e.mock.On("Close")} } func (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call { _c.Call.Return(_a0) return _c } func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call { _c.Call.Return(run) return _c } // ReadFrom provides a mock function with given fields: b func (_m *mockUDPConn) ReadFrom(b []byte) (int, string, error) { ret := _m.Called(b) if len(ret) == 0 { panic("no return value specified for ReadFrom") } var r0 int var r1 string var r2 error if rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok { return rf(b) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(b) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) string); ok { r1 = rf(b) } else { r1 = ret.Get(1).(string) } if rf, ok := ret.Get(2).(func([]byte) error); ok { r2 = rf(b) } else { r2 = ret.Error(2) } return r0, r1, r2 } // mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom' type mockUDPConn_ReadFrom_Call struct { *mock.Call } // ReadFrom is a helper method to define mock.On call // - b []byte func (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call { return &mockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)} } func (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *mockUDPConn_ReadFrom_Call { _c.Call.Return(_a0, _a1, _a2) return _c } func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *mockUDPConn_ReadFrom_Call { _c.Call.Return(run) return _c } // WriteTo provides a mock function with given fields: b, addr func (_m *mockUDPConn) WriteTo(b []byte, addr string) (int, error) { ret := _m.Called(b, addr) if len(ret) == 0 { panic("no return value specified for WriteTo") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok { return rf(b, addr) } if rf, ok := ret.Get(0).(func([]byte, string) int); ok { r0 = rf(b, addr) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte, string) error); ok { r1 = rf(b, addr) } else { r1 = ret.Error(1) } return r0, r1 } // mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo' type mockUDPConn_WriteTo_Call struct { *mock.Call } // WriteTo is a helper method to define mock.On call // - b []byte // - addr string func (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call { return &mockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)} } func (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *mockUDPConn_WriteTo_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte), args[1].(string)) }) return _c } func (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *mockUDPConn_WriteTo_Call { _c.Call.Return(run) return _c } // newMockUDPConn creates a new instance of mockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockUDPConn(t interface { mock.TestingT Cleanup(func()) }) *mockUDPConn { mock := &mockUDPConn{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/server/mock_udpEventLogger.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package server import mock "github.com/stretchr/testify/mock" // mockUDPEventLogger is an autogenerated mock type for the udpEventLogger type type mockUDPEventLogger struct { mock.Mock } type mockUDPEventLogger_Expecter struct { mock *mock.Mock } func (_m *mockUDPEventLogger) EXPECT() *mockUDPEventLogger_Expecter { return &mockUDPEventLogger_Expecter{mock: &_m.Mock} } // Close provides a mock function with given fields: sessionID, err func (_m *mockUDPEventLogger) Close(sessionID uint32, err error) { _m.Called(sessionID, err) } // mockUDPEventLogger_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type mockUDPEventLogger_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call // - sessionID uint32 // - err error func (_e *mockUDPEventLogger_Expecter) Close(sessionID interface{}, err interface{}) *mockUDPEventLogger_Close_Call { return &mockUDPEventLogger_Close_Call{Call: _e.mock.On("Close", sessionID, err)} } func (_c *mockUDPEventLogger_Close_Call) Run(run func(sessionID uint32, err error)) *mockUDPEventLogger_Close_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(uint32), args[1].(error)) }) return _c } func (_c *mockUDPEventLogger_Close_Call) Return() *mockUDPEventLogger_Close_Call { _c.Call.Return() return _c } func (_c *mockUDPEventLogger_Close_Call) RunAndReturn(run func(uint32, error)) *mockUDPEventLogger_Close_Call { _c.Call.Return(run) return _c } // New provides a mock function with given fields: sessionID, reqAddr func (_m *mockUDPEventLogger) New(sessionID uint32, reqAddr string) { _m.Called(sessionID, reqAddr) } // mockUDPEventLogger_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New' type mockUDPEventLogger_New_Call struct { *mock.Call } // New is a helper method to define mock.On call // - sessionID uint32 // - reqAddr string func (_e *mockUDPEventLogger_Expecter) New(sessionID interface{}, reqAddr interface{}) *mockUDPEventLogger_New_Call { return &mockUDPEventLogger_New_Call{Call: _e.mock.On("New", sessionID, reqAddr)} } func (_c *mockUDPEventLogger_New_Call) Run(run func(sessionID uint32, reqAddr string)) *mockUDPEventLogger_New_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(uint32), args[1].(string)) }) return _c } func (_c *mockUDPEventLogger_New_Call) Return() *mockUDPEventLogger_New_Call { _c.Call.Return() return _c } func (_c *mockUDPEventLogger_New_Call) RunAndReturn(run func(uint32, string)) *mockUDPEventLogger_New_Call { _c.Call.Return(run) return _c } // newMockUDPEventLogger creates a new instance of mockUDPEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockUDPEventLogger(t interface { mock.TestingT Cleanup(func()) }) *mockUDPEventLogger { mock := &mockUDPEventLogger{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/server/mock_udpIO.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package server import ( protocol "github.com/apernet/hysteria/core/v2/internal/protocol" mock "github.com/stretchr/testify/mock" ) // mockUDPIO is an autogenerated mock type for the udpIO type type mockUDPIO struct { mock.Mock } type mockUDPIO_Expecter struct { mock *mock.Mock } func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter { return &mockUDPIO_Expecter{mock: &_m.Mock} } // Hook provides a mock function with given fields: data, reqAddr func (_m *mockUDPIO) Hook(data []byte, reqAddr *string) error { ret := _m.Called(data, reqAddr) if len(ret) == 0 { panic("no return value specified for Hook") } var r0 error if rf, ok := ret.Get(0).(func([]byte, *string) error); ok { r0 = rf(data, reqAddr) } else { r0 = ret.Error(0) } return r0 } // mockUDPIO_Hook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Hook' type mockUDPIO_Hook_Call struct { *mock.Call } // Hook is a helper method to define mock.On call // - data []byte // - reqAddr *string func (_e *mockUDPIO_Expecter) Hook(data interface{}, reqAddr interface{}) *mockUDPIO_Hook_Call { return &mockUDPIO_Hook_Call{Call: _e.mock.On("Hook", data, reqAddr)} } func (_c *mockUDPIO_Hook_Call) Run(run func(data []byte, reqAddr *string)) *mockUDPIO_Hook_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte), args[1].(*string)) }) return _c } func (_c *mockUDPIO_Hook_Call) Return(_a0 error) *mockUDPIO_Hook_Call { _c.Call.Return(_a0) return _c } func (_c *mockUDPIO_Hook_Call) RunAndReturn(run func([]byte, *string) error) *mockUDPIO_Hook_Call { _c.Call.Return(run) return _c } // ReceiveMessage provides a mock function with given fields: func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for ReceiveMessage") } var r0 *protocol.UDPMessage var r1 error if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok { return rf() } if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*protocol.UDPMessage) } } if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { r1 = ret.Error(1) } return r0, r1 } // mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage' type mockUDPIO_ReceiveMessage_Call struct { *mock.Call } // ReceiveMessage is a helper method to define mock.On call func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call { return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")} } func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call { _c.Call.Return(run) return _c } // SendMessage provides a mock function with given fields: _a0, _a1 func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error { ret := _m.Called(_a0, _a1) if len(ret) == 0 { panic("no return value specified for SendMessage") } var r0 error if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok { r0 = rf(_a0, _a1) } else { r0 = ret.Error(0) } return r0 } // mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage' type mockUDPIO_SendMessage_Call struct { *mock.Call } // SendMessage is a helper method to define mock.On call // - _a0 []byte // - _a1 *protocol.UDPMessage func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call { return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)} } func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte), args[1].(*protocol.UDPMessage)) }) return _c } func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call { _c.Call.Return(_a0) return _c } func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call { _c.Call.Return(run) return _c } // UDP provides a mock function with given fields: reqAddr func (_m *mockUDPIO) UDP(reqAddr string) (UDPConn, error) { ret := _m.Called(reqAddr) if len(ret) == 0 { panic("no return value specified for UDP") } var r0 UDPConn var r1 error if rf, ok := ret.Get(0).(func(string) (UDPConn, error)); ok { return rf(reqAddr) } if rf, ok := ret.Get(0).(func(string) UDPConn); ok { r0 = rf(reqAddr) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(UDPConn) } } if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(reqAddr) } else { r1 = ret.Error(1) } return r0, r1 } // mockUDPIO_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' type mockUDPIO_UDP_Call struct { *mock.Call } // UDP is a helper method to define mock.On call // - reqAddr string func (_e *mockUDPIO_Expecter) UDP(reqAddr interface{}) *mockUDPIO_UDP_Call { return &mockUDPIO_UDP_Call{Call: _e.mock.On("UDP", reqAddr)} } func (_c *mockUDPIO_UDP_Call) Run(run func(reqAddr string)) *mockUDPIO_UDP_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string)) }) return _c } func (_c *mockUDPIO_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockUDPIO_UDP_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *mockUDPIO_UDP_Call) RunAndReturn(run func(string) (UDPConn, error)) *mockUDPIO_UDP_Call { _c.Call.Return(run) return _c } // newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockUDPIO(t interface { mock.TestingT Cleanup(func()) }) *mockUDPIO { mock := &mockUDPIO{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: core/server/server.go ================================================ package server import ( "context" "crypto/tls" "net/http" "sync" "github.com/apernet/quic-go" "github.com/apernet/quic-go/http3" "github.com/apernet/hysteria/core/v2/internal/congestion" "github.com/apernet/hysteria/core/v2/internal/protocol" "github.com/apernet/hysteria/core/v2/internal/utils" ) const ( closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError closeErrCodeTrafficLimitReached = 0x107 // HTTP3 ErrCodeExcessiveLoad ) type Server interface { Serve() error Close() error } func NewServer(config *Config) (Server, error) { if err := config.fill(); err != nil { return nil, err } tlsConfig := http3.ConfigureTLSConfig(&tls.Config{ Certificates: config.TLSConfig.Certificates, GetCertificate: config.TLSConfig.GetCertificate, }) quicConfig := &quic.Config{ InitialStreamReceiveWindow: config.QUICConfig.InitialStreamReceiveWindow, MaxStreamReceiveWindow: config.QUICConfig.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: config.QUICConfig.InitialConnectionReceiveWindow, MaxConnectionReceiveWindow: config.QUICConfig.MaxConnectionReceiveWindow, MaxIdleTimeout: config.QUICConfig.MaxIdleTimeout, MaxIncomingStreams: config.QUICConfig.MaxIncomingStreams, DisablePathMTUDiscovery: config.QUICConfig.DisablePathMTUDiscovery, EnableDatagrams: true, } listener, err := quic.Listen(config.Conn, tlsConfig, quicConfig) if err != nil { _ = config.Conn.Close() return nil, err } return &serverImpl{ config: config, listener: listener, }, nil } type serverImpl struct { config *Config listener *quic.Listener } func (s *serverImpl) Serve() error { for { conn, err := s.listener.Accept(context.Background()) if err != nil { return err } go s.handleClient(conn) } } func (s *serverImpl) Close() error { err := s.listener.Close() _ = s.config.Conn.Close() return err } func (s *serverImpl) handleClient(conn quic.Connection) { handler := newH3sHandler(s.config, conn) h3s := http3.Server{ Handler: handler, StreamHijacker: handler.ProxyStreamHijacker, } err := h3s.ServeQUICConn(conn) // If the client is authenticated, we need to log the disconnect event if handler.authenticated { if tl := s.config.TrafficLogger; tl != nil { tl.LogOnlineState(handler.authID, false) } if el := s.config.EventLogger; el != nil { el.Disconnect(conn.RemoteAddr(), handler.authID, err) } } _ = conn.CloseWithError(closeErrCodeOK, "") } type h3sHandler struct { config *Config conn quic.Connection authenticated bool authMutex sync.Mutex authID string udpSM *udpSessionManager // Only set after authentication } func newH3sHandler(config *Config, conn quic.Connection) *h3sHandler { return &h3sHandler{ config: config, conn: conn, } } func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost && r.Host == protocol.URLHost && r.URL.Path == protocol.URLPath { h.authMutex.Lock() defer h.authMutex.Unlock() if h.authenticated { // Already authenticated protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{ UDPEnabled: !h.config.DisableUDP, Rx: h.config.BandwidthConfig.MaxRx, RxAuto: h.config.IgnoreClientBandwidth, }) w.WriteHeader(protocol.StatusAuthOK) return } authReq := protocol.AuthRequestFromHeader(r.Header) actualTx := authReq.Rx ok, id := h.config.Authenticator.Authenticate(h.conn.RemoteAddr(), authReq.Auth, actualTx) if ok { // Set authenticated flag h.authenticated = true h.authID = id if h.config.IgnoreClientBandwidth { // Ignore client bandwidth, always use BBR congestion.UseBBR(h.conn) actualTx = 0 } else { // actualTx = min(serverTx, clientRx) if h.config.BandwidthConfig.MaxTx > 0 && actualTx > h.config.BandwidthConfig.MaxTx { // We have a maxTx limit and the client is asking for more than that, // return and use the limit instead actualTx = h.config.BandwidthConfig.MaxTx } if actualTx > 0 { congestion.UseBrutal(h.conn, actualTx) } else { // Client doesn't know its own bandwidth, use BBR congestion.UseBBR(h.conn) } } // Auth OK, send response protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{ UDPEnabled: !h.config.DisableUDP, Rx: h.config.BandwidthConfig.MaxRx, RxAuto: h.config.IgnoreClientBandwidth, }) w.WriteHeader(protocol.StatusAuthOK) // Call event logger if tl := h.config.TrafficLogger; tl != nil { tl.LogOnlineState(id, true) } if el := h.config.EventLogger; el != nil { el.Connect(h.conn.RemoteAddr(), id, actualTx) } // Initialize UDP session manager (if UDP is enabled) // We use sync.Once to make sure that only one goroutine is started, // as ServeHTTP may be called by multiple goroutines simultaneously if !h.config.DisableUDP { go func() { sm := newUDPSessionManager( &udpIOImpl{h.conn, id, h.config.TrafficLogger, h.config.RequestHook, h.config.Outbound}, &udpEventLoggerImpl{h.conn, id, h.config.EventLogger}, h.config.UDPIdleTimeout) h.udpSM = sm go sm.Run() }() } } else { // Auth failed, pretend to be a normal HTTP server h.masqHandler(w, r) } } else { // Not an auth request, pretend to be a normal HTTP server h.masqHandler(w, r) } } func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, id quic.ConnectionTracingID, stream quic.Stream, err error) (bool, error) { if err != nil || !h.authenticated { return false, nil } // Wraps the stream with QStream, which handles Close() properly stream = &utils.QStream{Stream: stream} switch ft { case protocol.FrameTypeTCPRequest: go h.handleTCPRequest(stream) return true, nil default: return false, nil } } func (h *h3sHandler) handleTCPRequest(stream quic.Stream) { // Read request reqAddr, err := protocol.ReadTCPRequest(stream) if err != nil { _ = stream.Close() return } // Call the hook if set var putback []byte var hooked bool if h.config.RequestHook != nil { hooked = h.config.RequestHook.Check(false, reqAddr) // When the hook is enabled, the server should always accept a connection // so that the client will send whatever request the hook wants to see. // This is essentially a server-side fast-open. if hooked { _ = protocol.WriteTCPResponse(stream, true, "RequestHook enabled") putback, err = h.config.RequestHook.TCP(stream, &reqAddr) if err != nil { _ = stream.Close() return } } } // Log the event if h.config.EventLogger != nil { h.config.EventLogger.TCPRequest(h.conn.RemoteAddr(), h.authID, reqAddr) } // Dial target tConn, err := h.config.Outbound.TCP(reqAddr) if err != nil { if !hooked { _ = protocol.WriteTCPResponse(stream, false, err.Error()) } _ = stream.Close() // Log the error if h.config.EventLogger != nil { h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err) } return } if !hooked { _ = protocol.WriteTCPResponse(stream, true, "Connected") } // Put back the data if the hook requested if len(putback) > 0 { _, _ = tConn.Write(putback) } // Start proxying if h.config.TrafficLogger != nil { err = copyTwoWayWithLogger(h.authID, stream, tConn, h.config.TrafficLogger) } else { // Use the fast path if no traffic logger is set err = copyTwoWay(stream, tConn) } if h.config.EventLogger != nil { h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err) } // Cleanup _ = tConn.Close() _ = stream.Close() // Disconnect the client if TrafficLogger requested if err == errDisconnect { _ = h.conn.CloseWithError(closeErrCodeTrafficLimitReached, "") } } func (h *h3sHandler) masqHandler(w http.ResponseWriter, r *http.Request) { if h.config.MasqHandler != nil { h.config.MasqHandler.ServeHTTP(w, r) } else { // Return 404 for everything http.NotFound(w, r) } } // udpIOImpl is the IO implementation for udpSessionManager with TrafficLogger support type udpIOImpl struct { Conn quic.Connection AuthID string TrafficLogger TrafficLogger RequestHook RequestHook Outbound Outbound } func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) { for { msg, err := io.Conn.ReceiveDatagram(context.Background()) if err != nil { // Connection error, this will stop the session manager return nil, err } udpMsg, err := protocol.ParseUDPMessage(msg) if err != nil { // Invalid message, this is fine - just wait for the next continue } if io.TrafficLogger != nil { ok := io.TrafficLogger.LogTraffic(io.AuthID, uint64(len(udpMsg.Data)), 0) if !ok { // TrafficLogger requested to disconnect the client _ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "") return nil, errDisconnect } } return udpMsg, nil } } func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error { if io.TrafficLogger != nil { ok := io.TrafficLogger.LogTraffic(io.AuthID, 0, uint64(len(msg.Data))) if !ok { // TrafficLogger requested to disconnect the client _ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "") return errDisconnect } } msgN := msg.Serialize(buf) if msgN < 0 { // Message larger than buffer, silent drop return nil } return io.Conn.SendDatagram(buf[:msgN]) } func (io *udpIOImpl) Hook(data []byte, reqAddr *string) error { if io.RequestHook != nil && io.RequestHook.Check(true, *reqAddr) { return io.RequestHook.UDP(data, reqAddr) } else { return nil } } func (io *udpIOImpl) UDP(reqAddr string) (UDPConn, error) { return io.Outbound.UDP(reqAddr) } type udpEventLoggerImpl struct { Conn quic.Connection AuthID string EventLogger EventLogger } func (l *udpEventLoggerImpl) New(sessionID uint32, reqAddr string) { if l.EventLogger != nil { l.EventLogger.UDPRequest(l.Conn.RemoteAddr(), l.AuthID, sessionID, reqAddr) } } func (l *udpEventLoggerImpl) Close(sessionID uint32, err error) { if l.EventLogger != nil { l.EventLogger.UDPError(l.Conn.RemoteAddr(), l.AuthID, sessionID, err) } } ================================================ FILE: core/server/udp.go ================================================ package server import ( "errors" "math/rand" "sync" "time" "github.com/apernet/quic-go" "github.com/apernet/hysteria/core/v2/internal/frag" "github.com/apernet/hysteria/core/v2/internal/protocol" "github.com/apernet/hysteria/core/v2/internal/utils" ) const ( idleCleanupInterval = 1 * time.Second ) type udpIO interface { ReceiveMessage() (*protocol.UDPMessage, error) SendMessage([]byte, *protocol.UDPMessage) error Hook(data []byte, reqAddr *string) error UDP(reqAddr string) (UDPConn, error) } type udpEventLogger interface { New(sessionID uint32, reqAddr string) Close(sessionID uint32, err error) } type udpSessionEntry struct { ID uint32 OverrideAddr string // Ignore the address in the UDP message, always use this if not empty OriginalAddr string // The original address in the UDP message D *frag.Defragger Last *utils.AtomicTime IO udpIO DialFunc func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error) ExitFunc func(err error) conn UDPConn connLock sync.Mutex closed bool } func newUDPSessionEntry( id uint32, io udpIO, dialFunc func(string, []byte) (UDPConn, string, error), exitFunc func(error), ) (e *udpSessionEntry) { e = &udpSessionEntry{ ID: id, D: &frag.Defragger{}, Last: utils.NewAtomicTime(time.Now()), IO: io, DialFunc: dialFunc, ExitFunc: exitFunc, } return } // CloseWithErr closes the session and calls ExitFunc with the given error. // A nil error indicates the session is cleaned up due to timeout. func (e *udpSessionEntry) CloseWithErr(err error) { // We need this lock to ensure not to create conn after session exit e.connLock.Lock() if e.closed { // Already closed e.connLock.Unlock() return } e.closed = true if e.conn != nil { _ = e.conn.Close() } e.connLock.Unlock() e.ExitFunc(err) } // Feed feeds a UDP message to the session. // If the message itself is a complete message, or it completes a fragmented message, // the message is written to the session's UDP connection, and the number of bytes // written is returned. // Otherwise, 0 and nil are returned. func (e *udpSessionEntry) Feed(msg *protocol.UDPMessage) (int, error) { e.Last.Set(time.Now()) dfMsg := e.D.Feed(msg) if dfMsg == nil { return 0, nil } if e.conn == nil { err := e.initConn(dfMsg) if err != nil { return 0, err } } addr := dfMsg.Addr if e.OverrideAddr != "" { addr = e.OverrideAddr } return e.conn.WriteTo(dfMsg.Data, addr) } // initConn initializes the UDP connection of the session. // If no error is returned, the e.conn is set to the new connection. func (e *udpSessionEntry) initConn(firstMsg *protocol.UDPMessage) error { // We need this lock to ensure not to create conn after session exit e.connLock.Lock() if e.closed { e.connLock.Unlock() return errors.New("session is closed") } conn, actualAddr, err := e.DialFunc(firstMsg.Addr, firstMsg.Data) if err != nil { // Fail fast if DialFunc failed // (usually indicates the connection has been rejected by the ACL) e.connLock.Unlock() // CloseWithErr acquires the connLock again e.CloseWithErr(err) return err } e.conn = conn if firstMsg.Addr != actualAddr { // Hook changed the address, enable address override e.OverrideAddr = actualAddr e.OriginalAddr = firstMsg.Addr } go e.receiveLoop() e.connLock.Unlock() return nil } // receiveLoop receives incoming UDP packets, packs them into UDP messages, // and sends using the IO. // Exit when either the underlying UDP connection returns error (e.g. closed), // or the IO returns error when sending. func (e *udpSessionEntry) receiveLoop() { udpBuf := make([]byte, protocol.MaxUDPSize) msgBuf := make([]byte, protocol.MaxUDPSize) for { udpN, rAddr, err := e.conn.ReadFrom(udpBuf) if err != nil { e.CloseWithErr(err) return } e.Last.Set(time.Now()) if e.OriginalAddr != "" { // Use the original address in the opposite direction, // otherwise the QUIC clients or NAT on the client side // may not treat it as the same UDP session. rAddr = e.OriginalAddr } msg := &protocol.UDPMessage{ SessionID: e.ID, PacketID: 0, FragID: 0, FragCount: 1, Addr: rAddr, Data: udpBuf[:udpN], } err = sendMessageAutoFrag(e.IO, msgBuf, msg) if err != nil { e.CloseWithErr(err) return } } } // sendMessageAutoFrag tries to send a UDP message as a whole first, // but if it fails due to quic.ErrMessageTooLarge, it tries again by // fragmenting the message. func sendMessageAutoFrag(io udpIO, buf []byte, msg *protocol.UDPMessage) error { err := io.SendMessage(buf, msg) var errTooLarge *quic.DatagramTooLargeError if errors.As(err, &errTooLarge) { // Message too large, try fragmentation msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDataLen)) for _, fMsg := range fMsgs { err := io.SendMessage(buf, &fMsg) if err != nil { return err } } return nil } else { return err } } // udpSessionManager manages the lifecycle of UDP sessions. // Each UDP session is identified by a SessionID, and corresponds to a UDP connection. // A UDP session is created when a UDP message with a new SessionID is received. // Similar to standard NAT, a UDP session is destroyed when no UDP message is received // for a certain period of time (specified by idleTimeout). type udpSessionManager struct { io udpIO eventLogger udpEventLogger idleTimeout time.Duration mutex sync.RWMutex m map[uint32]*udpSessionEntry } func newUDPSessionManager(io udpIO, eventLogger udpEventLogger, idleTimeout time.Duration) *udpSessionManager { return &udpSessionManager{ io: io, eventLogger: eventLogger, idleTimeout: idleTimeout, m: make(map[uint32]*udpSessionEntry), } } // Run runs the session manager main loop. // Exit and returns error when the underlying io returns error (e.g. closed). func (m *udpSessionManager) Run() error { stopCh := make(chan struct{}) go m.idleCleanupLoop(stopCh) defer close(stopCh) defer m.cleanup(false) for { msg, err := m.io.ReceiveMessage() if err != nil { return err } m.feed(msg) } } func (m *udpSessionManager) idleCleanupLoop(stopCh <-chan struct{}) { ticker := time.NewTicker(idleCleanupInterval) defer ticker.Stop() for { select { case <-ticker.C: m.cleanup(true) case <-stopCh: return } } } func (m *udpSessionManager) cleanup(idleOnly bool) { timeoutEntry := make([]*udpSessionEntry, 0, len(m.m)) // We use RLock here as we are only scanning the map, not deleting from it. m.mutex.RLock() now := time.Now() for _, entry := range m.m { if !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout { timeoutEntry = append(timeoutEntry, entry) } } m.mutex.RUnlock() for _, entry := range timeoutEntry { // This eventually calls entry.ExitFunc, // where the m.mutex will be locked again to remove the entry from the map. entry.CloseWithErr(nil) } } func (m *udpSessionManager) feed(msg *protocol.UDPMessage) { m.mutex.RLock() entry := m.m[msg.SessionID] m.mutex.RUnlock() // Create a new session if not exists if entry == nil { dialFunc := func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error) { // Call the hook err = m.io.Hook(firstMsgData, &addr) if err != nil { return } actualAddr = addr // Log the event m.eventLogger.New(msg.SessionID, addr) // Dial target conn, err = m.io.UDP(addr) return } exitFunc := func(err error) { // Log the event m.eventLogger.Close(entry.ID, err) // Remove the session from the map m.mutex.Lock() delete(m.m, entry.ID) m.mutex.Unlock() } entry = newUDPSessionEntry(msg.SessionID, m.io, dialFunc, exitFunc) // Insert the session into the map m.mutex.Lock() m.m[msg.SessionID] = entry m.mutex.Unlock() } // Feed the message to the session // Feed (send) errors are ignored for now, // as some are temporary (e.g. invalid address) _, _ = entry.Feed(msg) } func (m *udpSessionManager) Count() int { m.mutex.RLock() defer m.mutex.RUnlock() return len(m.m) } ================================================ FILE: core/server/udp_test.go ================================================ package server import ( "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/goleak" "github.com/apernet/hysteria/core/v2/internal/protocol" ) func TestUDPSessionManager(t *testing.T) { io := newMockUDPIO(t) eventLogger := newMockUDPEventLogger(t) sm := newUDPSessionManager(io, eventLogger, 2*time.Second) msgCh := make(chan *protocol.UDPMessage, 4) io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) { m := <-msgCh if m == nil { return nil, errors.New("closed") } return m, nil }) go sm.Run() udpReadFunc := func(addr string, ch chan []byte, b []byte) (int, string, error) { bs := <-ch if bs == nil { return 0, "", errors.New("closed") } n := copy(b, bs) return n, addr, nil } // Test normal session creation & timeout msg1 := &protocol.UDPMessage{ SessionID: 1234, PacketID: 0, FragID: 0, FragCount: 1, Addr: "address1.com:9000", Data: []byte("hello"), } eventLogger.EXPECT().New(msg1.SessionID, msg1.Addr).Return().Once() udpConn1 := newMockUDPConn(t) udpConn1Ch := make(chan []byte, 1) io.EXPECT().Hook(msg1.Data, &msg1.Addr).Return(nil).Once() io.EXPECT().UDP(msg1.Addr).Return(udpConn1, nil).Once() udpConn1.EXPECT().WriteTo(msg1.Data, msg1.Addr).Return(5, nil).Once() udpConn1.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) { return udpReadFunc(msg1.Addr, udpConn1Ch, b) }) io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ SessionID: msg1.SessionID, PacketID: 0, FragID: 0, FragCount: 1, Addr: msg1.Addr, Data: []byte("hi back"), }).Return(nil).Once() msgCh <- msg1 udpConn1Ch <- []byte("hi back") msg2data := []byte("how are you doing?") msg2_1 := &protocol.UDPMessage{ SessionID: 5678, PacketID: 0, FragID: 0, FragCount: 2, Addr: "address2.net:12450", Data: msg2data[:6], } msg2_2 := &protocol.UDPMessage{ SessionID: 5678, PacketID: 0, FragID: 1, FragCount: 2, Addr: "address2.net:12450", Data: msg2data[6:], } eventLogger.EXPECT().New(msg2_1.SessionID, msg2_1.Addr).Return().Once() udpConn2 := newMockUDPConn(t) udpConn2Ch := make(chan []byte, 1) // On fragmentation, make sure hook gets the whole message io.EXPECT().Hook(msg2data, &msg2_1.Addr).Return(nil).Once() io.EXPECT().UDP(msg2_1.Addr).Return(udpConn2, nil).Once() udpConn2.EXPECT().WriteTo(msg2data, msg2_1.Addr).Return(11, nil).Once() udpConn2.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) { return udpReadFunc(msg2_1.Addr, udpConn2Ch, b) }) io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ SessionID: msg2_1.SessionID, PacketID: 0, FragID: 0, FragCount: 1, Addr: msg2_1.Addr, Data: []byte("im fine"), }).Return(nil).Once() msgCh <- msg2_1 msgCh <- msg2_2 udpConn2Ch <- []byte("im fine") msg3 := &protocol.UDPMessage{ SessionID: 1234, PacketID: 0, FragID: 0, FragCount: 1, Addr: "address1.com:9000", Data: []byte("who are you?"), } udpConn1.EXPECT().WriteTo(msg3.Data, msg3.Addr).Return(12, nil).Once() io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{ SessionID: msg3.SessionID, PacketID: 0, FragID: 0, FragCount: 1, Addr: msg3.Addr, Data: []byte("im your father"), }).Return(nil).Once() msgCh <- msg3 udpConn1Ch <- []byte("im your father") // Make sure timeout works (connections closed & close events emitted) udpConn1.EXPECT().Close().RunAndReturn(func() error { close(udpConn1Ch) return nil }).Once() udpConn2.EXPECT().Close().RunAndReturn(func() error { close(udpConn2Ch) return nil }).Once() eventLogger.EXPECT().Close(msg1.SessionID, nil).Once() eventLogger.EXPECT().Close(msg2_1.SessionID, nil).Once() time.Sleep(3 * time.Second) // Wait for timeout mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn1, udpConn2) // Test UDP connection close error propagation errUDPClosed := errors.New("UDP connection closed") msg4 := &protocol.UDPMessage{ SessionID: 666, PacketID: 0, FragID: 0, FragCount: 1, Addr: "oh-no.com:27015", Data: []byte("dont say bye"), } eventLogger.EXPECT().New(msg4.SessionID, msg4.Addr).Return().Once() udpConn4 := newMockUDPConn(t) io.EXPECT().Hook(msg4.Data, &msg4.Addr).Return(nil).Once() io.EXPECT().UDP(msg4.Addr).Return(udpConn4, nil).Once() udpConn4.EXPECT().WriteTo(msg4.Data, msg4.Addr).Return(12, nil).Once() udpConn4.EXPECT().ReadFrom(mock.Anything).Return(0, "", errUDPClosed).Once() udpConn4.EXPECT().Close().Return(nil).Once() eventLogger.EXPECT().Close(msg4.SessionID, errUDPClosed).Once() msgCh <- msg4 time.Sleep(1 * time.Second) mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn4) // Test UDP connection creation error propagation errUDPIO := errors.New("UDP IO error") msg5 := &protocol.UDPMessage{ SessionID: 777, PacketID: 0, FragID: 0, FragCount: 1, Addr: "callmemaybe.com:15353", Data: []byte("babe i miss you"), } eventLogger.EXPECT().New(msg5.SessionID, msg5.Addr).Return().Once() io.EXPECT().Hook(msg5.Data, &msg5.Addr).Return(nil).Once() io.EXPECT().UDP(msg5.Addr).Return(nil, errUDPIO).Once() eventLogger.EXPECT().Close(msg5.SessionID, errUDPIO).Once() msgCh <- msg5 time.Sleep(1 * time.Second) mock.AssertExpectationsForObjects(t, io, eventLogger) // Leak checks close(msgCh) // This will return error from ReceiveMessage(), should stop the session manager time.Sleep(1 * time.Second) // Wait one more second just to be sure assert.Zero(t, sm.Count(), "session count should be 0") goleak.VerifyNone(t) } ================================================ FILE: entrypoint ================================================ #!/bin/sh CONFIG_FILE="/etc/hysteria/server.yaml" # 判断配置文件是否存存在,如果不存在走不存在的逻辑 if [ ! -f "$CONFIG_FILE" ]; then echo "Creating configuration file $CONFIG_FILE" mkdir -p /etc/hysteria cat <"$CONFIG_FILE" v2board: apiHost: ${apiHost} apiKey: ${apiKey} nodeID: ${nodeID} acme: domains: - ${domain} email: your@email.com auth: type: v2board trafficStats: listen: 127.0.0.1:7653 acl: inline: - reject(10.0.0.0/8) - reject(172.16.0.0/12) - reject(192.168.0.0/16) - reject(127.0.0.0/8) - reject(fc00::/7) EOF fi hysteria server -c $CONFIG_FILE 2>&1 | tee & # 获取HYSTERIA server命令的进程组ID(Process Group ID) HYSTERIA_PID=$! # 定义一个函数来处理Ctrl+C信号 cleanup() { echo "接收到Ctrl+C信号,正在停止HYSTERIA..." # 向HYSTERIA进程组发送终止信号 kill -SIGINT $HYSTERIA_PID exit 0 } # 捕获Ctrl+C信号并调用cleanup函数 trap cleanup INT trap cleanup SIGTERM # 等待HYSTERIA进程结束 wait ================================================ FILE: extras/auth/command.go ================================================ package auth import ( "net" "os/exec" "strconv" "strings" "github.com/apernet/hysteria/core/v2/server" ) var _ server.Authenticator = &CommandAuthenticator{} type CommandAuthenticator struct { Cmd string } func (a *CommandAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { cmd := exec.Command(a.Cmd, addr.String(), auth, strconv.Itoa(int(tx))) out, err := cmd.Output() if err != nil { // This includes failing to execute the command, // or the command exiting with a non-zero exit code. return false, "" } else { return true, strings.TrimSpace(string(out)) } } ================================================ FILE: extras/auth/http.go ================================================ package auth import ( "bytes" "crypto/tls" "encoding/json" "errors" "io" "net" "net/http" "time" "github.com/apernet/hysteria/core/v2/server" ) const ( httpAuthTimeout = 10 * time.Second ) var _ server.Authenticator = &HTTPAuthenticator{} var errInvalidStatusCode = errors.New("invalid status code") type HTTPAuthenticator struct { Client *http.Client URL string } func NewHTTPAuthenticator(url string, insecure bool) *HTTPAuthenticator { tr := http.DefaultTransport.(*http.Transport).Clone() tr.TLSClientConfig = &tls.Config{ InsecureSkipVerify: insecure, } return &HTTPAuthenticator{ Client: &http.Client{ Transport: tr, Timeout: httpAuthTimeout, }, URL: url, } } type httpAuthRequest struct { Addr string `json:"addr"` Auth string `json:"auth"` Tx uint64 `json:"tx"` } type httpAuthResponse struct { OK bool `json:"ok"` ID string `json:"id"` } func (a *HTTPAuthenticator) post(req *httpAuthRequest) (*httpAuthResponse, error) { bs, err := json.Marshal(req) if err != nil { return nil, err } resp, err := a.Client.Post(a.URL, "application/json", bytes.NewReader(bs)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errInvalidStatusCode } respData, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var authResp httpAuthResponse err = json.Unmarshal(respData, &authResp) if err != nil { return nil, err } return &authResp, nil } func (a *HTTPAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { req := &httpAuthRequest{ Addr: addr.String(), Auth: auth, Tx: tx, } resp, err := a.post(req) if err != nil { return false, "" } return resp.OK, resp.ID } ================================================ FILE: extras/auth/http_test.go ================================================ package auth import ( "net" "os/exec" "testing" "time" "github.com/stretchr/testify/assert" ) func TestHTTPAuthenticator(t *testing.T) { // Run the Python test auth server cmd := exec.Command("python", "http_test.py") err := cmd.Start() assert.NoError(t, err) defer cmd.Process.Kill() time.Sleep(1 * time.Second) // Wait for the server to start auth := NewHTTPAuthenticator("http://127.0.0.1:5000/auth", false) ok, id := auth.Authenticate(&net.UDPAddr{ IP: net.ParseIP("1.2.3.4"), Port: 34567, }, "idk", 123) assert.False(t, ok) assert.Equal(t, "", id) ok, id = auth.Authenticate(&net.UDPAddr{ IP: net.ParseIP("123.123.123.123"), Port: 5566, }, "wahaha", 12345) assert.True(t, ok) assert.Equal(t, "some_unique_id", id) } ================================================ FILE: extras/auth/http_test.py ================================================ from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/auth", methods=["POST"]) def auth(): data = request.json if data is None: return jsonify({"ok": False, "id": ""}), 400 addr = data.get("addr", "") auth = data.get("auth", "") tx = data.get("tx", 0) if addr == "123.123.123.123:5566" and auth == "wahaha" and tx == 12345: return jsonify({"ok": True, "id": "some_unique_id"}) else: return jsonify({"ok": False, "id": ""}) if __name__ == "__main__": app.run() ================================================ FILE: extras/auth/password.go ================================================ package auth import ( "net" "github.com/apernet/hysteria/core/v2/server" ) var _ server.Authenticator = &PasswordAuthenticator{} // PasswordAuthenticator is a simple authenticator that checks the password against a single string. type PasswordAuthenticator struct { Password string } func (a *PasswordAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { if auth == a.Password { return true, "user" } else { return false, "" } } ================================================ FILE: extras/auth/password_test.go ================================================ package auth import ( "net" "testing" ) func TestPasswordAuthenticator(t *testing.T) { type fields struct { Password string } type args struct { addr net.Addr auth string tx uint64 } tests := []struct { name string fields fields args args wantOk bool wantId string }{ { name: "correct", fields: fields{ Password: "yes,yes", }, args: args{ addr: nil, auth: "yes,yes", tx: 0, }, wantOk: true, wantId: "user", }, { name: "incorrect", fields: fields{ Password: "something_somehow", }, args: args{ addr: nil, auth: "random", tx: 0, }, wantOk: false, wantId: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &PasswordAuthenticator{ Password: tt.fields.Password, } gotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx) if gotOk != tt.wantOk { t.Errorf("Authenticate() gotOk = %v, want %v", gotOk, tt.wantOk) } if gotId != tt.wantId { t.Errorf("Authenticate() gotId = %v, want %v", gotId, tt.wantId) } }) } } ================================================ FILE: extras/auth/userpass.go ================================================ package auth import ( "net" "strings" "github.com/apernet/hysteria/core/v2/server" ) const ( userPassSeparator = ":" ) var _ server.Authenticator = &UserPassAuthenticator{} // UserPassAuthenticator checks the provided auth string against a map of username/password pairs. // The format of the auth string must be "username:password". type UserPassAuthenticator struct { Users map[string]string } func (a *UserPassAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { u, p, ok := splitUserPass(auth) if !ok { return false, "" } rp, ok := a.Users[u] if !ok || rp != p { return false, "" } return true, u } func splitUserPass(auth string) (user, pass string, ok bool) { rs := strings.SplitN(auth, userPassSeparator, 2) if len(rs) != 2 { return "", "", false } return rs[0], rs[1], true } ================================================ FILE: extras/auth/userpass_test.go ================================================ package auth import ( "net" "testing" ) func TestUserPassAuthenticator(t *testing.T) { type fields struct { Users map[string]string } type args struct { addr net.Addr auth string tx uint64 } tests := []struct { name string fields fields args args wantOk bool wantId string }{ { name: "correct 1", fields: fields{ Users: map[string]string{ "saul": "goodman", "wang": "123", }, }, args: args{ addr: nil, auth: "wang:123", tx: 0, }, wantOk: true, wantId: "wang", }, { name: "correct 2", fields: fields{ Users: map[string]string{ "gawr": "gura", "fubuki": "shirakami", }, }, args: args{ addr: nil, auth: "gawr:gura", tx: 0, }, wantOk: true, wantId: "gawr", }, { name: "incorrect 1", fields: fields{ Users: map[string]string{ "gawr": "gura", "fubuki": "shirakami", }, }, args: args{ addr: nil, auth: "random:stranger", tx: 0, }, wantOk: false, wantId: "", }, { name: "incorrect 2", fields: fields{ Users: map[string]string{ "gawr": "gura", "fubuki": "shirakami", }, }, args: args{ addr: nil, auth: "poop", tx: 0, }, wantOk: false, wantId: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &UserPassAuthenticator{ Users: tt.fields.Users, } gotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx) if gotOk != tt.wantOk { t.Errorf("Authenticate() gotOk = %v, want %v", gotOk, tt.wantOk) } if gotId != tt.wantId { t.Errorf("Authenticate() gotId = %v, want %v", gotId, tt.wantId) } }) } } ================================================ FILE: extras/auth/v2board.go ================================================ package auth import ( "encoding/json" "fmt" "net" "net/http" "strconv" "sync" "time" "github.com/apernet/hysteria/core/v2/server" ) var _ server.Authenticator = &V2boardApiProvider{} type V2boardApiProvider struct { Client *http.Client URL string } // 用户列表 var ( usersMap map[string]User lock sync.Mutex ) type User struct { ID int `json:"id"` UUID string `json:"uuid"` SpeedLimit *uint32 `json:"speed_limit"` } type ResponseData struct { Users []User `json:"users"` } func getUserList(url string) ([]User, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() var responseData ResponseData err = json.NewDecoder(resp.Body).Decode(&responseData) if err != nil { return nil, err } return responseData.Users, nil } func UpdateUsers(url string, interval time.Duration, trafficlogger server.TrafficLogger) { fmt.Println("用户列表自动更新服务已激活,更新周期为", interval) // 先立即执行一次更新 userList, err := getUserList(url) if err != nil { fmt.Println("Error:", err) return // 如果第一次获取失败,退出函数 } processUserList(userList, trafficlogger) // 设置定时器 ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { userList, err := getUserList(url) if err != nil { fmt.Println("Error:", err) continue } processUserList(userList, trafficlogger) } } // 处理用户列表的逻辑 func processUserList(userList []User, trafficlogger server.TrafficLogger) { lock.Lock() defer lock.Unlock() newUsersMap := make(map[string]User) for _, user := range userList { newUsersMap[user.UUID] = user } if trafficlogger != nil { for uuid := range usersMap { if _, exists := newUsersMap[uuid]; !exists { fmt.Println(usersMap[uuid].ID) trafficlogger.NewKick(strconv.Itoa(usersMap[uuid].ID)) } } } usersMap = newUsersMap } // 验证代码 func (v *V2boardApiProvider) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { // 获取判断连接用户是否在用户列表内 lock.Lock() defer lock.Unlock() if user, exists := usersMap[auth]; exists { return true, strconv.Itoa(user.ID) } return false, "" } ================================================ FILE: extras/correctnet/correctnet.go ================================================ package correctnet import ( "net" "net/http" "strings" ) func extractIPFamily(ip net.IP) (family string) { if len(ip) == 0 { // real family independent wildcard address, such as ":443" return "" } if p4 := ip.To4(); len(p4) == net.IPv4len { return "4" } return "6" } func tcpAddrNetwork(addr *net.TCPAddr) (network string) { if addr == nil { return "tcp" } return "tcp" + extractIPFamily(addr.IP) } func udpAddrNetwork(addr *net.UDPAddr) (network string) { if addr == nil { return "udp" } return "udp" + extractIPFamily(addr.IP) } func ipAddrNetwork(addr *net.IPAddr) (network string) { if addr == nil { return "ip" } return "ip" + extractIPFamily(addr.IP) } func Listen(network, address string) (net.Listener, error) { if network == "tcp" { tcpAddr, err := net.ResolveTCPAddr(network, address) if err != nil { return nil, err } return ListenTCP(network, tcpAddr) } return net.Listen(network, address) } func ListenTCP(network string, laddr *net.TCPAddr) (*net.TCPListener, error) { if network == "tcp" { return net.ListenTCP(tcpAddrNetwork(laddr), laddr) } return net.ListenTCP(network, laddr) } func ListenPacket(network, address string) (listener net.PacketConn, err error) { if network == "udp" { udpAddr, err := net.ResolveUDPAddr(network, address) if err != nil { return nil, err } return ListenUDP(network, udpAddr) } if strings.HasPrefix(network, "ip:") { proto := network[3:] ipAddr, err := net.ResolveIPAddr(proto, address) if err != nil { return nil, err } return net.ListenIP(ipAddrNetwork(ipAddr)+":"+proto, ipAddr) } return net.ListenPacket(network, address) } func ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error) { if network == "udp" { return net.ListenUDP(udpAddrNetwork(laddr), laddr) } return net.ListenUDP(network, laddr) } func HTTPListenAndServe(address string, handler http.Handler) error { listener, err := Listen("tcp", address) if err != nil { return err } defer listener.Close() return http.Serve(listener, handler) } ================================================ FILE: extras/go.mod ================================================ module github.com/apernet/hysteria/extras/v2 go 1.22 toolchain go1.23.2 require ( github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000 github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 github.com/hashicorp/golang-lru/v2 v2.0.5 github.com/miekg/dns v1.1.59 github.com/refraction-networking/utls v1.6.6 github.com/stretchr/testify v1.9.0 github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 golang.org/x/crypto v0.26.0 golang.org/x/net v0.28.0 google.golang.org/protobuf v1.34.1 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/cloudflare/circl v1.3.9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/kr/text v0.2.0 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect go.uber.org/mock v0.4.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/apernet/hysteria/core/v2 => ../core ================================================ FILE: extras/go.sum ================================================ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d h1:KWRCWISqJOgY9/0hhH8Bevjw/k4tCQ7oJlXLyFv8u9s= github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d/go.mod h1:x0paLlmCzNOUDDQIgmgFWmnpWQIEuH1GNfA6NdgSTuM= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0= github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg= 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/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/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/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= 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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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: extras/masq/server.go ================================================ package masq import ( "bufio" "crypto/tls" "fmt" "net" "net/http" "github.com/apernet/hysteria/extras/v2/correctnet" ) // MasqTCPServer covers the TCP parts of a standard web server (TCP based HTTP/HTTPS). // We provide this as an option for masquerading, as some may consider a server // "suspicious" if it only serves the QUIC protocol and not standard HTTP/HTTPS. type MasqTCPServer struct { QUICPort int HTTPSPort int Handler http.Handler TLSConfig *tls.Config ForceHTTPS bool // Always 301 redirect from HTTP to HTTPS } func (s *MasqTCPServer) ListenAndServeHTTP(addr string) error { return correctnet.HTTPListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.ForceHTTPS { if s.HTTPSPort == 0 || s.HTTPSPort == 443 { // Omit port if it's the default http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) } else { http.Redirect(w, r, fmt.Sprintf("https://%s:%d%s", r.Host, s.HTTPSPort, r.RequestURI), http.StatusMovedPermanently) } return } s.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r) })) } func (s *MasqTCPServer) ListenAndServeHTTPS(addr string) error { server := &http.Server{ Addr: addr, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r) }), TLSConfig: s.TLSConfig, } listener, err := correctnet.Listen("tcp", addr) if err != nil { return err } defer listener.Close() return server.ServeTLS(listener, "", "") } var _ http.ResponseWriter = (*altSvcHijackResponseWriter)(nil) // altSvcHijackResponseWriter makes sure that the Alt-Svc's port // is always set with our own value, no matter what the handler sets. type altSvcHijackResponseWriter struct { Port int http.ResponseWriter } func (w *altSvcHijackResponseWriter) WriteHeader(statusCode int) { w.Header().Set("Alt-Svc", fmt.Sprintf(`h3=":%d"; ma=2592000`, w.Port)) w.ResponseWriter.WriteHeader(statusCode) } var _ http.Hijacker = (*altSvcHijackResponseWriterHijacker)(nil) // altSvcHijackResponseWriterHijacker is a wrapper around altSvcHijackResponseWriter // that also implements http.Hijacker. This is needed for WebSocket support. type altSvcHijackResponseWriterHijacker struct { altSvcHijackResponseWriter } func (w *altSvcHijackResponseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { return w.ResponseWriter.(http.Hijacker).Hijack() } func newAltSvcHijackResponseWriter(w http.ResponseWriter, port int) http.ResponseWriter { if _, ok := w.(http.Hijacker); ok { return &altSvcHijackResponseWriterHijacker{ altSvcHijackResponseWriter: altSvcHijackResponseWriter{ Port: port, ResponseWriter: w, }, } } return &altSvcHijackResponseWriter{ Port: port, ResponseWriter: w, } } ================================================ FILE: extras/obfs/conn.go ================================================ package obfs import ( "net" "sync" "syscall" "time" ) const udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough // Obfuscator is the interface that wraps the Obfuscate and Deobfuscate methods. // Both methods return the number of bytes written to out. // If a packet is not valid, the methods should return 0. type Obfuscator interface { Obfuscate(in, out []byte) int Deobfuscate(in, out []byte) int } var _ net.PacketConn = (*obfsPacketConn)(nil) type obfsPacketConn struct { Conn net.PacketConn Obfs Obfuscator readBuf []byte readMutex sync.Mutex writeBuf []byte writeMutex sync.Mutex } // obfsPacketConnUDP is a special case of obfsPacketConn that uses a UDPConn // as the underlying connection. We pass additional methods to quic-go to // enable UDP-specific optimizations. type obfsPacketConnUDP struct { *obfsPacketConn UDPConn *net.UDPConn } // WrapPacketConn enables obfuscation on a net.PacketConn. // The obfuscation is transparent to the caller - the n bytes returned by // ReadFrom and WriteTo are the number of original bytes, not after // obfuscation/deobfuscation. func WrapPacketConn(conn net.PacketConn, obfs Obfuscator) net.PacketConn { opc := &obfsPacketConn{ Conn: conn, Obfs: obfs, readBuf: make([]byte, udpBufferSize), writeBuf: make([]byte, udpBufferSize), } if udpConn, ok := conn.(*net.UDPConn); ok { return &obfsPacketConnUDP{ obfsPacketConn: opc, UDPConn: udpConn, } } else { return opc } } func (c *obfsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { for { c.readMutex.Lock() n, addr, err = c.Conn.ReadFrom(c.readBuf) if n <= 0 { c.readMutex.Unlock() return } n = c.Obfs.Deobfuscate(c.readBuf[:n], p) c.readMutex.Unlock() if n > 0 || err != nil { return } // Invalid packet, try again } } func (c *obfsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { c.writeMutex.Lock() nn := c.Obfs.Obfuscate(p, c.writeBuf) _, err = c.Conn.WriteTo(c.writeBuf[:nn], addr) c.writeMutex.Unlock() if err == nil { n = len(p) } return } func (c *obfsPacketConn) Close() error { return c.Conn.Close() } func (c *obfsPacketConn) LocalAddr() net.Addr { return c.Conn.LocalAddr() } func (c *obfsPacketConn) SetDeadline(t time.Time) error { return c.Conn.SetDeadline(t) } func (c *obfsPacketConn) SetReadDeadline(t time.Time) error { return c.Conn.SetReadDeadline(t) } func (c *obfsPacketConn) SetWriteDeadline(t time.Time) error { return c.Conn.SetWriteDeadline(t) } // UDP-specific methods below func (c *obfsPacketConnUDP) SetReadBuffer(bytes int) error { return c.UDPConn.SetReadBuffer(bytes) } func (c *obfsPacketConnUDP) SetWriteBuffer(bytes int) error { return c.UDPConn.SetWriteBuffer(bytes) } func (c *obfsPacketConnUDP) SyscallConn() (syscall.RawConn, error) { return c.UDPConn.SyscallConn() } ================================================ FILE: extras/obfs/salamander.go ================================================ package obfs import ( "fmt" "math/rand" "sync" "time" "golang.org/x/crypto/blake2b" ) const ( smPSKMinLen = 4 smSaltLen = 8 smKeyLen = blake2b.Size256 ) var _ Obfuscator = (*SalamanderObfuscator)(nil) var ErrPSKTooShort = fmt.Errorf("PSK must be at least %d bytes", smPSKMinLen) // SalamanderObfuscator is an obfuscator that obfuscates each packet with // the BLAKE2b-256 hash of a pre-shared key combined with a random salt. // Packet format: [8-byte salt][payload] type SalamanderObfuscator struct { PSK []byte RandSrc *rand.Rand lk sync.Mutex } func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) { if len(psk) < smPSKMinLen { return nil, ErrPSKTooShort } return &SalamanderObfuscator{ PSK: psk, RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), }, nil } func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int { outLen := len(in) + smSaltLen if len(out) < outLen { return 0 } o.lk.Lock() _, _ = o.RandSrc.Read(out[:smSaltLen]) o.lk.Unlock() key := o.key(out[:smSaltLen]) for i, c := range in { out[i+smSaltLen] = c ^ key[i%smKeyLen] } return outLen } func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int { outLen := len(in) - smSaltLen if outLen <= 0 || len(out) < outLen { return 0 } key := o.key(in[:smSaltLen]) for i, c := range in[smSaltLen:] { out[i] = c ^ key[i%smKeyLen] } return outLen } func (o *SalamanderObfuscator) key(salt []byte) [smKeyLen]byte { return blake2b.Sum256(append(o.PSK, salt...)) } ================================================ FILE: extras/obfs/salamander_test.go ================================================ package obfs import ( "crypto/rand" "testing" "github.com/stretchr/testify/assert" ) func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) { o, _ := NewSalamanderObfuscator([]byte("average_password")) in := make([]byte, 1200) _, _ = rand.Read(in) out := make([]byte, 2048) b.ResetTimer() for i := 0; i < b.N; i++ { o.Obfuscate(in, out) } } func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) { o, _ := NewSalamanderObfuscator([]byte("average_password")) in := make([]byte, 1200) _, _ = rand.Read(in) out := make([]byte, 2048) b.ResetTimer() for i := 0; i < b.N; i++ { o.Deobfuscate(in, out) } } func TestSalamanderObfuscator(t *testing.T) { o, _ := NewSalamanderObfuscator([]byte("average_password")) in := make([]byte, 1200) oOut := make([]byte, 2048) dOut := make([]byte, 2048) for i := 0; i < 1000; i++ { _, _ = rand.Read(in) n := o.Obfuscate(in, oOut) assert.Equal(t, len(in)+smSaltLen, n) n = o.Deobfuscate(oOut[:n], dOut) assert.Equal(t, len(in), n) assert.Equal(t, in, dOut[:n]) } } ================================================ FILE: extras/outbounds/.mockery.yaml ================================================ with-expecter: true inpackage: true dir: . packages: github.com/apernet/hysteria/extras/v2/outbounds: interfaces: PluggableOutbound: config: mockname: mockPluggableOutbound UDPConn: config: mockname: mockUDPConn ================================================ FILE: extras/outbounds/acl/compile.go ================================================ package acl import ( "fmt" "net" "strconv" "strings" "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" lru "github.com/hashicorp/golang-lru/v2" ) type Protocol int const ( ProtocolBoth Protocol = iota ProtocolTCP ProtocolUDP ) type Outbound interface { any } type HostInfo struct { Name string IPv4 net.IP IPv6 net.IP } func (h HostInfo) String() string { return fmt.Sprintf("%s|%s|%s", h.Name, h.IPv4, h.IPv6) } type CompiledRuleSet[O Outbound] interface { Match(host HostInfo, proto Protocol, port uint16) (O, net.IP) } type compiledRule[O Outbound] struct { Outbound O HostMatcher hostMatcher Protocol Protocol StartPort uint16 EndPort uint16 HijackAddress net.IP } func (r *compiledRule[O]) Match(host HostInfo, proto Protocol, port uint16) bool { if r.Protocol != ProtocolBoth && r.Protocol != proto { return false } if r.StartPort != 0 && (port < r.StartPort || port > r.EndPort) { return false } return r.HostMatcher.Match(host) } type matchResult[O Outbound] struct { Outbound O HijackAddress net.IP } type compiledRuleSetImpl[O Outbound] struct { Rules []compiledRule[O] Cache *lru.Cache[string, matchResult[O]] // key: HostInfo.String() } func (s *compiledRuleSetImpl[O]) Match(host HostInfo, proto Protocol, port uint16) (O, net.IP) { host.Name = strings.ToLower(host.Name) // Normalize host name to lower case key := host.String() if result, ok := s.Cache.Get(key); ok { return result.Outbound, result.HijackAddress } for _, rule := range s.Rules { if rule.Match(host, proto, port) { result := matchResult[O]{rule.Outbound, rule.HijackAddress} s.Cache.Add(key, result) return result.Outbound, result.HijackAddress } } // No match should also be cached var zero O s.Cache.Add(key, matchResult[O]{zero, nil}) return zero, nil } type CompilationError struct { LineNum int Message string } func (e *CompilationError) Error() string { return fmt.Sprintf("error at line %d: %s", e.LineNum, e.Message) } type GeoLoader interface { LoadGeoIP() (map[string]*v2geo.GeoIP, error) LoadGeoSite() (map[string]*v2geo.GeoSite, error) } // Compile compiles TextRules into a CompiledRuleSet. // Names in the outbounds map MUST be in all lower case. // We want on-demand loading of GeoIP/GeoSite databases, so instead of passing the // databases directly, we use a GeoLoader interface to load them only when needed // by at least one rule. func Compile[O Outbound](rules []TextRule, outbounds map[string]O, cacheSize int, geoLoader GeoLoader, ) (CompiledRuleSet[O], error) { compiledRules := make([]compiledRule[O], len(rules)) for i, rule := range rules { outbound, ok := outbounds[strings.ToLower(rule.Outbound)] if !ok { return nil, &CompilationError{rule.LineNum, fmt.Sprintf("outbound %s not found", rule.Outbound)} } hm, errStr := compileHostMatcher(rule.Address, geoLoader) if errStr != "" { return nil, &CompilationError{rule.LineNum, errStr} } proto, startPort, endPort, ok := parseProtoPort(rule.ProtoPort) if !ok { return nil, &CompilationError{rule.LineNum, fmt.Sprintf("invalid protocol/port: %s", rule.ProtoPort)} } var hijackAddress net.IP if rule.HijackAddress != "" { hijackAddress = net.ParseIP(rule.HijackAddress) if hijackAddress == nil { return nil, &CompilationError{rule.LineNum, fmt.Sprintf("invalid hijack address (must be an IP address): %s", rule.HijackAddress)} } } compiledRules[i] = compiledRule[O]{outbound, hm, proto, startPort, endPort, hijackAddress} } cache, err := lru.New[string, matchResult[O]](cacheSize) if err != nil { return nil, err } return &compiledRuleSetImpl[O]{compiledRules, cache}, nil } // parseProtoPort parses the protocol and port from a protoPort string. // protoPort must be in one of the following formats: // // proto/port // proto/* // proto // */port // */* // * // [empty] (same as *) // // proto must be either "tcp" or "udp", case-insensitive. func parseProtoPort(protoPort string) (Protocol, uint16, uint16, bool) { protoPort = strings.ToLower(protoPort) if protoPort == "" || protoPort == "*" || protoPort == "*/*" { return ProtocolBoth, 0, 0, true } parts := strings.SplitN(protoPort, "/", 2) if len(parts) == 1 { // No port, only protocol switch parts[0] { case "tcp": return ProtocolTCP, 0, 0, true case "udp": return ProtocolUDP, 0, 0, true default: return ProtocolBoth, 0, 0, false } } else { // Both protocol and port var proto Protocol var startPort, endPort uint16 switch parts[0] { case "tcp": proto = ProtocolTCP case "udp": proto = ProtocolUDP case "*": proto = ProtocolBoth default: return ProtocolBoth, 0, 0, false } if parts[1] != "*" { // We allow either a single port or a range (e.g. "1000-2000") ports := strings.SplitN(strings.TrimSpace(parts[1]), "-", 2) if len(ports) == 1 { p64, err := strconv.ParseUint(parts[1], 10, 16) if err != nil { return ProtocolBoth, 0, 0, false } startPort = uint16(p64) endPort = startPort } else { p64, err := strconv.ParseUint(ports[0], 10, 16) if err != nil { return ProtocolBoth, 0, 0, false } startPort = uint16(p64) p64, err = strconv.ParseUint(ports[1], 10, 16) if err != nil { return ProtocolBoth, 0, 0, false } endPort = uint16(p64) if startPort > endPort { return ProtocolBoth, 0, 0, false } } } return proto, startPort, endPort, true } } func compileHostMatcher(addr string, geoLoader GeoLoader) (hostMatcher, string) { addr = strings.ToLower(addr) // Normalize to lower case if addr == "*" || addr == "all" { // Match all hosts return &allMatcher{}, "" } if strings.HasPrefix(addr, "geoip:") { // GeoIP matcher country := addr[6:] if len(country) == 0 { return nil, "empty GeoIP country code" } gMap, err := geoLoader.LoadGeoIP() if err != nil { return nil, err.Error() } list, ok := gMap[country] if !ok || list == nil { return nil, fmt.Sprintf("GeoIP country code %s not found", country) } m, err := newGeoIPMatcher(list) if err != nil { return nil, err.Error() } return m, "" } if strings.HasPrefix(addr, "geosite:") { // GeoSite matcher name, attrs := parseGeoSiteName(addr[8:]) if len(name) == 0 { return nil, "empty GeoSite name" } gMap, err := geoLoader.LoadGeoSite() if err != nil { return nil, err.Error() } list, ok := gMap[name] if !ok || list == nil { return nil, fmt.Sprintf("GeoSite name %s not found", name) } m, err := newGeositeMatcher(list, attrs) if err != nil { return nil, err.Error() } return m, "" } if strings.HasPrefix(addr, "suffix:") { // Domain suffix matcher suffix := addr[7:] if len(suffix) == 0 { return nil, "empty domain suffix" } return &domainMatcher{ Pattern: suffix, Mode: domainMatchSuffix, }, "" } if strings.Contains(addr, "/") { // CIDR matcher _, ipnet, err := net.ParseCIDR(addr) if err != nil { return nil, fmt.Sprintf("invalid CIDR address: %s", addr) } return &cidrMatcher{ipnet}, "" } if ip := net.ParseIP(addr); ip != nil { // Single IP matcher return &ipMatcher{ip}, "" } if strings.Contains(addr, "*") { // Wildcard domain matcher return &domainMatcher{ Pattern: addr, Mode: domainMatchWildcard, }, "" } // Nothing else matched, treat it as a non-wildcard domain return &domainMatcher{ Pattern: addr, Mode: domainMatchExact, }, "" } func parseGeoSiteName(s string) (string, []string) { parts := strings.Split(s, "@") base := strings.TrimSpace(parts[0]) attrs := parts[1:] for i := range attrs { attrs[i] = strings.TrimSpace(attrs[i]) } return base, attrs } ================================================ FILE: extras/outbounds/acl/compile_test.go ================================================ package acl import ( "net" "testing" "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" "github.com/stretchr/testify/assert" ) var _ GeoLoader = (*testGeoLoader)(nil) type testGeoLoader struct{} func (l *testGeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) { return v2geo.LoadGeoIP("v2geo/geoip.dat") } func (l *testGeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) { return v2geo.LoadGeoSite("v2geo/geosite.dat") } func TestCompile(t *testing.T) { ob1, ob2, ob3, ob4, ob5, ob6 := 1, 2, 3, 4, 5, 6 rules := []TextRule{ { Outbound: "ob1", Address: "1.2.3.4", ProtoPort: "", HijackAddress: "", }, { Outbound: "ob2", Address: "8.8.8.0/24", ProtoPort: "*", HijackAddress: "1.1.1.1", }, { Outbound: "ob3", Address: "all", ProtoPort: "udp/443", HijackAddress: "", }, { Outbound: "ob1", Address: "2606:4700::6810:85e5", ProtoPort: "tcp", HijackAddress: "2606:4700::6810:85e6", }, { Outbound: "ob2", Address: "2606:4700::/44", ProtoPort: "*/8888", HijackAddress: "", }, { Outbound: "ob3", Address: "*.v2ex.com", ProtoPort: "udp", HijackAddress: "", }, { Outbound: "ob1", Address: "crap.v2ex.com", ProtoPort: "tcp/80", HijackAddress: "2.2.2.2", }, { Outbound: "ob2", Address: "geoip:JP", ProtoPort: "*/*", HijackAddress: "", }, { Outbound: "ob4", Address: "geosite:4chan", ProtoPort: "*/*", HijackAddress: "", }, { Outbound: "ob4", Address: "geosite:google @cn", ProtoPort: "*/*", HijackAddress: "", }, { Outbound: "ob5", Address: "suffix:microsoft.com", ProtoPort: "*/*", HijackAddress: "", }, { Outbound: "ob6", Address: "all", ProtoPort: "tcp/6881-6889", HijackAddress: "", }, } comp, err := Compile[int](rules, map[string]int{ "ob1": ob1, "ob2": ob2, "ob3": ob3, "ob4": ob4, "ob5": ob5, "ob6": ob6, }, 100, &testGeoLoader{}) assert.NoError(t, err) tests := []struct { host HostInfo proto Protocol port uint16 wantOutbound int wantIP net.IP }{ { host: HostInfo{ IPv4: net.ParseIP("1.2.3.4"), }, proto: ProtocolTCP, port: 1234, wantOutbound: ob1, wantIP: nil, }, { host: HostInfo{ IPv4: net.ParseIP("8.8.8.4"), }, proto: ProtocolUDP, port: 5353, wantOutbound: ob2, wantIP: net.ParseIP("1.1.1.1"), }, { host: HostInfo{ Name: "lean.delicious.com", }, proto: ProtocolUDP, port: 443, wantOutbound: ob3, wantIP: nil, }, { host: HostInfo{ IPv6: net.ParseIP("2606:4700::6810:85e5"), }, proto: ProtocolTCP, port: 80, wantOutbound: ob1, wantIP: net.ParseIP("2606:4700::6810:85e6"), }, { host: HostInfo{ IPv6: net.ParseIP("2606:4700:0:0:0:0:0:1"), }, proto: ProtocolUDP, port: 8888, wantOutbound: ob2, wantIP: nil, }, { host: HostInfo{ Name: "www.v2ex.com", }, proto: ProtocolUDP, port: 1234, wantOutbound: ob3, wantIP: nil, }, { host: HostInfo{ Name: "crap.v2ex.com", }, proto: ProtocolTCP, port: 80, wantOutbound: ob1, wantIP: net.ParseIP("2.2.2.2"), }, { host: HostInfo{ IPv4: net.ParseIP("210.140.92.187"), }, proto: ProtocolTCP, port: 25, wantOutbound: ob2, wantIP: nil, }, { host: HostInfo{ IPv4: net.ParseIP("175.45.176.73"), }, proto: ProtocolTCP, port: 80, wantOutbound: 0, // no match default wantIP: nil, }, { host: HostInfo{ Name: "boards.4channel.org", }, proto: ProtocolTCP, port: 443, wantOutbound: ob4, wantIP: nil, }, { host: HostInfo{ Name: "gstatic-cn.com", }, proto: ProtocolUDP, port: 9999, wantOutbound: ob4, wantIP: nil, }, { host: HostInfo{ Name: "hoho.waymo.com", }, proto: ProtocolUDP, port: 9999, wantOutbound: 0, // no match default wantIP: nil, }, { host: HostInfo{ Name: "microsoft.com", }, proto: ProtocolTCP, port: 6000, wantOutbound: ob5, wantIP: nil, }, { host: HostInfo{ Name: "real.microsoft.com", }, proto: ProtocolUDP, port: 5353, wantOutbound: ob5, wantIP: nil, }, { host: HostInfo{ Name: "fakemicrosoft.com", }, proto: ProtocolTCP, port: 5000, wantOutbound: 0, // no match default wantIP: nil, }, { host: HostInfo{ IPv4: net.ParseIP("223.1.1.1"), }, proto: ProtocolTCP, port: 6883, wantOutbound: ob6, // match range port rule 6881-6889 wantIP: nil, }, } for _, test := range tests { gotOutbound, gotIP := comp.Match(test.host, test.proto, test.port) assert.Equal(t, test.wantOutbound, gotOutbound) assert.Equal(t, test.wantIP, gotIP) } // Test Invalid Port Range Rule eb1 := 1 invalidRules := []TextRule{ { Outbound: "eb1", Address: "1.1.2.0/24", ProtoPort: "*/3-1", HijackAddress: "", }, } _, err = Compile[int](invalidRules, map[string]int{ "eb1": eb1, }, 100, &testGeoLoader{}) assert.Error(t, err) } func Test_parseGeoSiteName(t *testing.T) { tests := []struct { name string s string want string want1 []string }{ { name: "no attrs", s: "pornhub", want: "pornhub", want1: []string{}, }, { name: "one attr 1", s: "xiaomi@cn", want: "xiaomi", want1: []string{"cn"}, }, { name: "one attr 2", s: " google @jp ", want: "google", want1: []string{"jp"}, }, { name: "two attrs 1", s: "netflix@jp@kr", want: "netflix", want1: []string{"jp", "kr"}, }, { name: "two attrs 2", s: "netflix @xixi @haha ", want: "netflix", want1: []string{"xixi", "haha"}, }, { name: "empty", s: "", want: "", want1: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, got1 := parseGeoSiteName(tt.s) assert.Equalf(t, tt.want, got, "parseGeoSiteName(%v)", tt.s) assert.Equalf(t, tt.want1, got1, "parseGeoSiteName(%v)", tt.s) }) } } ================================================ FILE: extras/outbounds/acl/matchers.go ================================================ package acl import ( "net" "strings" "golang.org/x/net/idna" ) const ( domainMatchExact = uint8(iota) domainMatchWildcard domainMatchSuffix ) type hostMatcher interface { Match(HostInfo) bool } type ipMatcher struct { IP net.IP } func (m *ipMatcher) Match(host HostInfo) bool { return m.IP.Equal(host.IPv4) || m.IP.Equal(host.IPv6) } type cidrMatcher struct { IPNet *net.IPNet } func (m *cidrMatcher) Match(host HostInfo) bool { return m.IPNet.Contains(host.IPv4) || m.IPNet.Contains(host.IPv6) } type domainMatcher struct { Pattern string Mode uint8 } func (m *domainMatcher) Match(host HostInfo) bool { name, err := idna.ToUnicode(host.Name) if err != nil { name = host.Name } switch m.Mode { case domainMatchExact: return name == m.Pattern case domainMatchWildcard: return deepMatchRune([]rune(name), []rune(m.Pattern)) case domainMatchSuffix: return name == m.Pattern || strings.HasSuffix(name, "."+m.Pattern) default: return false // Invalid mode } } func deepMatchRune(str, pattern []rune) bool { for len(pattern) > 0 { switch pattern[0] { default: if len(str) == 0 || str[0] != pattern[0] { return false } case '*': return deepMatchRune(str, pattern[1:]) || (len(str) > 0 && deepMatchRune(str[1:], pattern)) } str = str[1:] pattern = pattern[1:] } return len(str) == 0 && len(pattern) == 0 } type allMatcher struct{} func (m *allMatcher) Match(host HostInfo) bool { return true } ================================================ FILE: extras/outbounds/acl/matchers_test.go ================================================ package acl import ( "net" "testing" ) func Test_ipMatcher_Match(t *testing.T) { tests := []struct { name string IP net.IP host HostInfo want bool }{ { name: "ipv4 match", IP: net.IPv4(127, 0, 0, 1), host: HostInfo{ IPv4: net.IPv4(127, 0, 0, 1), IPv6: nil, }, want: true, }, { name: "ipv6 match", IP: net.IPv6loopback, host: HostInfo{ IPv4: nil, IPv6: net.IPv6loopback, }, want: true, }, { name: "no match", IP: net.IPv4(127, 0, 0, 1), host: HostInfo{ IPv4: net.IPv4(127, 0, 0, 2), IPv6: net.IPv6loopback, }, want: false, }, { name: "both nil", IP: net.IPv4(127, 0, 0, 1), host: HostInfo{ IPv4: nil, IPv6: nil, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &ipMatcher{ IP: tt.IP, } if got := m.Match(tt.host); got != tt.want { t.Errorf("Match() = %v, want %v", got, tt.want) } }) } } func Test_cidrMatcher_Match(t *testing.T) { _, cidr1, _ := net.ParseCIDR("192.168.1.0/24") _, cidr2, _ := net.ParseCIDR("::1/128") _, cidr3, _ := net.ParseCIDR("0.0.0.0/0") _, cidr4, _ := net.ParseCIDR("::/0") tests := []struct { name string IPNet *net.IPNet host HostInfo want bool }{ { name: "ipv4 match", IPNet: cidr1, host: HostInfo{ IPv4: net.ParseIP("192.168.1.100"), IPv6: net.ParseIP("::1"), }, want: true, }, { name: "ipv6 match", IPNet: cidr2, host: HostInfo{ IPv4: net.ParseIP("10.0.0.1"), IPv6: net.ParseIP("::1"), }, want: true, }, { name: "no match", IPNet: cidr1, host: HostInfo{ IPv4: net.ParseIP("10.0.0.1"), IPv6: net.ParseIP("2001:db8::2:1"), }, want: false, }, { name: "ipv4 broad", IPNet: cidr3, host: HostInfo{ IPv4: net.ParseIP("10.0.0.1"), IPv6: net.ParseIP("::1"), }, want: true, }, { name: "ipv6 broad", IPNet: cidr4, host: HostInfo{ IPv4: net.ParseIP("10.0.0.1"), IPv6: net.ParseIP("2001:db8::2:1"), }, want: true, }, { name: "both nil", IPNet: cidr1, host: HostInfo{ IPv4: nil, IPv6: nil, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &cidrMatcher{ IPNet: tt.IPNet, } if got := m.Match(tt.host); got != tt.want { t.Errorf("Match() = %v, want %v", got, tt.want) } }) } } func Test_domainMatcher_Match(t *testing.T) { type fields struct { Pattern string Mode uint8 } tests := []struct { name string fields fields host HostInfo want bool }{ { name: "non-wildcard match", fields: fields{ Pattern: "example.com", Mode: domainMatchExact, }, host: HostInfo{ Name: "example.com", }, want: true, }, { name: "non-wildcard IDN match", fields: fields{ Pattern: "政府.中国", Mode: domainMatchExact, }, host: HostInfo{ Name: "xn--mxtq1m.xn--fiqs8s", }, want: true, }, { name: "non-wildcard no match", fields: fields{ Pattern: "example.com", Mode: domainMatchExact, }, host: HostInfo{ Name: "example.org", }, want: false, }, { name: "non-wildcard IDN no match", fields: fields{ Pattern: "政府.中国", Mode: domainMatchExact, }, host: HostInfo{ Name: "xn--mxtq1m.xn--yfro4i67o", }, want: false, }, { name: "wildcard match 1", fields: fields{ Pattern: "*.example.com", Mode: domainMatchWildcard, }, host: HostInfo{ Name: "www.example.com", }, want: true, }, { name: "wildcard match 2", fields: fields{ Pattern: "example*.com", Mode: domainMatchWildcard, }, host: HostInfo{ Name: "example2.com", }, want: true, }, { name: "wildcard IDN match 1", fields: fields{ Pattern: "战狼*.com", Mode: domainMatchWildcard, }, host: HostInfo{ Name: "xn--2-x14by21c.com", }, want: true, }, { name: "wildcard IDN match 2", fields: fields{ Pattern: "*大学*", Mode: domainMatchWildcard, }, host: HostInfo{ Name: "xn--xkry9kk1bz66a.xn--ses554g", }, want: true, }, { name: "wildcard no match", fields: fields{ Pattern: "*.example.com", Mode: domainMatchWildcard, }, host: HostInfo{ Name: "example.com", }, want: false, }, { name: "wildcard IDN no match", fields: fields{ Pattern: "*呵呵*", Mode: domainMatchWildcard, }, host: HostInfo{ Name: "xn--6qqt7juua.cn", }, want: false, }, { name: "suffix match 1", fields: fields{ Pattern: "apple.com", Mode: domainMatchSuffix, }, host: HostInfo{ Name: "apple.com", }, want: true, }, { name: "suffix match 2", fields: fields{ Pattern: "apple.com", Mode: domainMatchSuffix, }, host: HostInfo{ Name: "store.apple.com", }, want: true, }, { name: "suffix IDN match 1", fields: fields{ Pattern: "中国", Mode: domainMatchSuffix, }, host: HostInfo{ Name: "中国", }, want: true, }, { name: "suffix IDN match 2", fields: fields{ Pattern: "中国", Mode: domainMatchSuffix, }, host: HostInfo{ Name: "天安门.中国", }, want: true, }, { name: "suffix no match", fields: fields{ Pattern: "news.com", }, host: HostInfo{ Name: "fakenews.com", }, want: false, }, { name: "suffix IDN no match", fields: fields{ Pattern: "冲浪", }, host: HostInfo{ Name: "666.网上冲浪", }, want: false, }, { name: "empty", fields: fields{ Pattern: "*.example.com", Mode: domainMatchWildcard, }, host: HostInfo{ Name: "", }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &domainMatcher{ Pattern: tt.fields.Pattern, Mode: tt.fields.Mode, } if got := m.Match(tt.host); got != tt.want { t.Errorf("Match() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: extras/outbounds/acl/matchers_v2geo.go ================================================ package acl import ( "bytes" "errors" "net" "regexp" "sort" "strings" "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" ) var _ hostMatcher = (*geoipMatcher)(nil) type geoipMatcher struct { N4 []*net.IPNet // sorted N6 []*net.IPNet // sorted Inverse bool } // matchIP tries to match the given IP address with the corresponding IPNets. // Note that this function does NOT handle the Inverse flag. func (m *geoipMatcher) matchIP(ip net.IP) bool { var n []*net.IPNet if ip4 := ip.To4(); ip4 != nil { // N4 stores IPv4 addresses in 4-byte form. // Make sure we use it here too, otherwise bytes.Compare will fail. ip = ip4 n = m.N4 } else { n = m.N6 } left, right := 0, len(n)-1 for left <= right { mid := (left + right) / 2 if n[mid].Contains(ip) { return true } else if bytes.Compare(n[mid].IP, ip) < 0 { left = mid + 1 } else { right = mid - 1 } } return false } func (m *geoipMatcher) Match(host HostInfo) bool { if host.IPv4 != nil { if m.matchIP(host.IPv4) { return !m.Inverse } } if host.IPv6 != nil { if m.matchIP(host.IPv6) { return !m.Inverse } } return m.Inverse } func newGeoIPMatcher(list *v2geo.GeoIP) (*geoipMatcher, error) { n4 := make([]*net.IPNet, 0) n6 := make([]*net.IPNet, 0) for _, cidr := range list.Cidr { if len(cidr.Ip) == 4 { // IPv4 n4 = append(n4, &net.IPNet{ IP: cidr.Ip, Mask: net.CIDRMask(int(cidr.Prefix), 32), }) } else if len(cidr.Ip) == 16 { // IPv6 n6 = append(n6, &net.IPNet{ IP: cidr.Ip, Mask: net.CIDRMask(int(cidr.Prefix), 128), }) } else { return nil, errors.New("invalid IP length") } } // Sort the IPNets, so we can do binary search later. sort.Slice(n4, func(i, j int) bool { return bytes.Compare(n4[i].IP, n4[j].IP) < 0 }) sort.Slice(n6, func(i, j int) bool { return bytes.Compare(n6[i].IP, n6[j].IP) < 0 }) return &geoipMatcher{ N4: n4, N6: n6, Inverse: list.InverseMatch, }, nil } var _ hostMatcher = (*geositeMatcher)(nil) type geositeDomainType int const ( geositeDomainPlain geositeDomainType = iota geositeDomainRegex geositeDomainRoot geositeDomainFull ) type geositeDomain struct { Type geositeDomainType Value string Regex *regexp.Regexp Attrs map[string]bool } type geositeMatcher struct { Domains []geositeDomain // Attributes are matched using "and" logic - if you have multiple attributes here, // a domain must have all of those attributes to be considered a match. Attrs []string } func (m *geositeMatcher) matchDomain(domain geositeDomain, host HostInfo) bool { // Match attributes first if len(m.Attrs) > 0 { if len(domain.Attrs) == 0 { return false } for _, attr := range m.Attrs { if !domain.Attrs[attr] { return false } } } switch domain.Type { case geositeDomainPlain: return strings.Contains(host.Name, domain.Value) case geositeDomainRegex: if domain.Regex != nil { return domain.Regex.MatchString(host.Name) } case geositeDomainFull: return host.Name == domain.Value case geositeDomainRoot: if host.Name == domain.Value { return true } return strings.HasSuffix(host.Name, "."+domain.Value) default: return false } return false } func (m *geositeMatcher) Match(host HostInfo) bool { for _, domain := range m.Domains { if m.matchDomain(domain, host) { return true } } return false } func newGeositeMatcher(list *v2geo.GeoSite, attrs []string) (*geositeMatcher, error) { domains := make([]geositeDomain, len(list.Domain)) for i, domain := range list.Domain { switch domain.Type { case v2geo.Domain_Plain: domains[i] = geositeDomain{ Type: geositeDomainPlain, Value: domain.Value, Attrs: domainAttributeToMap(domain.Attribute), } case v2geo.Domain_Regex: regex, err := regexp.Compile(domain.Value) if err != nil { return nil, err } domains[i] = geositeDomain{ Type: geositeDomainRegex, Regex: regex, Attrs: domainAttributeToMap(domain.Attribute), } case v2geo.Domain_Full: domains[i] = geositeDomain{ Type: geositeDomainFull, Value: domain.Value, Attrs: domainAttributeToMap(domain.Attribute), } case v2geo.Domain_RootDomain: domains[i] = geositeDomain{ Type: geositeDomainRoot, Value: domain.Value, Attrs: domainAttributeToMap(domain.Attribute), } default: return nil, errors.New("unsupported domain type") } } return &geositeMatcher{ Domains: domains, Attrs: attrs, }, nil } func domainAttributeToMap(attrs []*v2geo.Domain_Attribute) map[string]bool { m := make(map[string]bool) for _, attr := range attrs { // Supposedly there are also int attributes, // but nobody seems to use them, so we treat everything as boolean for now. m[attr.Key] = true } return m } ================================================ FILE: extras/outbounds/acl/matchers_v2geo_test.go ================================================ package acl import ( "net" "testing" "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo" "github.com/stretchr/testify/assert" ) func Test_geoipMatcher_Match(t *testing.T) { geoipMap, err := v2geo.LoadGeoIP("v2geo/geoip.dat") assert.NoError(t, err) m, err := newGeoIPMatcher(geoipMap["us"]) assert.NoError(t, err) tests := []struct { name string host HostInfo want bool }{ { name: "IPv4 match", host: HostInfo{ IPv4: net.ParseIP("73.222.1.100"), }, want: true, }, { name: "IPv4 no match", host: HostInfo{ IPv4: net.ParseIP("123.123.123.123"), }, want: false, }, { name: "IPv6 match", host: HostInfo{ IPv6: net.ParseIP("2607:f8b0:4005:80c::2004"), }, want: true, }, { name: "IPv6 no match", host: HostInfo{ IPv6: net.ParseIP("240e:947:6001::1f8"), }, want: false, }, { name: "both nil", host: HostInfo{ IPv4: nil, IPv6: nil, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equalf(t, tt.want, m.Match(tt.host), "Match(%v)", tt.host) }) } } func Test_geositeMatcher_Match(t *testing.T) { geositeMap, err := v2geo.LoadGeoSite("v2geo/geosite.dat") assert.NoError(t, err) m, err := newGeositeMatcher(geositeMap["apple"], nil) assert.NoError(t, err) tests := []struct { name string attrs []string host HostInfo want bool }{ { name: "subdomain", attrs: nil, host: HostInfo{ Name: "poop.i-book.com", }, want: true, }, { name: "subdomain root", attrs: nil, host: HostInfo{ Name: "applepaycash.net", }, want: true, }, { name: "full", attrs: nil, host: HostInfo{ Name: "courier-push-apple.com.akadns.net", }, want: true, }, { name: "regexp", attrs: nil, host: HostInfo{ Name: "cdn4.apple-mapkit.com", }, want: true, }, { name: "attr match", attrs: []string{"cn"}, host: HostInfo{ Name: "bag.itunes.apple.com", }, want: true, }, { name: "attr multi no match", attrs: []string{"cn", "haha"}, host: HostInfo{ Name: "bag.itunes.apple.com", }, want: false, }, { name: "attr no match", attrs: []string{"cn"}, host: HostInfo{ Name: "mr-apple.com.tw", }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m.Attrs = tt.attrs assert.Equalf(t, tt.want, m.Match(tt.host), "Match(%v)", tt.host) }) } } ================================================ FILE: extras/outbounds/acl/parse.go ================================================ package acl import ( "fmt" "regexp" "strings" ) var linePattern = regexp.MustCompile(`^(\w+)\s*\(([^,]+)(?:,([^,]+))?(?:,([^,]+))?\)$`) type InvalidSyntaxError struct { Line string LineNum int } func (e *InvalidSyntaxError) Error() string { return fmt.Sprintf("invalid syntax at line %d: %s", e.LineNum, e.Line) } // TextRule is the struct representation of a (non-comment) line parsed from an ACL file. // A line can be parsed into a TextRule as long as it matches one of the following patterns: // // outbound(address) // outbound(address,protoPort) // outbound(address,protoPort,hijackAddress) // // It does not check whether any of the fields is valid - it's up to the compiler to do so. type TextRule struct { Outbound string Address string ProtoPort string HijackAddress string LineNum int } func parseLine(line string, num int) *TextRule { matches := linePattern.FindStringSubmatch(line) if matches == nil { return nil } return &TextRule{ Outbound: matches[1], Address: strings.TrimSpace(matches[2]), ProtoPort: strings.TrimSpace(matches[3]), HijackAddress: strings.TrimSpace(matches[4]), LineNum: num, } } func ParseTextRules(text string) ([]TextRule, error) { rules := make([]TextRule, 0) lineNum := 0 for _, line := range strings.Split(text, "\n") { lineNum++ // Remove comments if i := strings.Index(line, "#"); i >= 0 { line = line[:i] } line = strings.TrimSpace(line) // Skip empty lines if len(line) == 0 { continue } // Parse line rule := parseLine(line, lineNum) if rule == nil { return nil, &InvalidSyntaxError{line, lineNum} } rules = append(rules, *rule) } return rules, nil } ================================================ FILE: extras/outbounds/acl/parse_test.go ================================================ package acl import ( "reflect" "testing" ) func TestParseTextRules(t *testing.T) { tests := []struct { name string text string want []TextRule wantErr bool }{ { name: "empty", text: "", want: []TextRule{}, wantErr: false, }, { name: "ok", text: ` # just a comment # another comment direct(1.1.1.1) direct(8.8.8.0/24) reject(all, udp/443) # inline comment reject(geoip:cn) reject(*.v2ex.com) my_custom_outbound1(9.9.9.9,*, 8.8.8.8) # bebop my_custom_outbound2(all) `, want: []TextRule{ {Outbound: "direct", Address: "1.1.1.1", LineNum: 4}, {Outbound: "direct", Address: "8.8.8.0/24", LineNum: 5}, {Outbound: "reject", Address: "all", ProtoPort: "udp/443", LineNum: 6}, {Outbound: "reject", Address: "geoip:cn", LineNum: 7}, {Outbound: "reject", Address: "*.v2ex.com", LineNum: 8}, {Outbound: "my_custom_outbound1", Address: "9.9.9.9", ProtoPort: "*", HijackAddress: "8.8.8.8", LineNum: 9}, {Outbound: "my_custom_outbound2", Address: "all", LineNum: 10}, }, wantErr: false, }, { name: "fail 1", text: `boom()`, want: nil, wantErr: true, }, { name: "fail 2", text: `lol(1,1,1,1)`, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseTextRules(tt.text) if (err != nil) != tt.wantErr { t.Errorf("ParseTextRules() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("ParseTextRules() got = %v, want %v", got, tt.want) } }) } } ================================================ FILE: extras/outbounds/acl/v2geo/load.go ================================================ package v2geo import ( "os" "strings" "google.golang.org/protobuf/proto" ) // LoadGeoIP loads a GeoIP data file and converts it to a map. // The keys of the map (country codes) are all normalized to lowercase. func LoadGeoIP(filename string) (map[string]*GeoIP, error) { bs, err := os.ReadFile(filename) if err != nil { return nil, err } var list GeoIPList if err := proto.Unmarshal(bs, &list); err != nil { return nil, err } m := make(map[string]*GeoIP) for _, entry := range list.Entry { m[strings.ToLower(entry.CountryCode)] = entry } return m, nil } // LoadGeoSite loads a GeoSite data file and converts it to a map. // The keys of the map (site keys) are all normalized to lowercase. func LoadGeoSite(filename string) (map[string]*GeoSite, error) { bs, err := os.ReadFile(filename) if err != nil { return nil, err } var list GeoSiteList if err := proto.Unmarshal(bs, &list); err != nil { return nil, err } m := make(map[string]*GeoSite) for _, entry := range list.Entry { m[strings.ToLower(entry.CountryCode)] = entry } return m, nil } ================================================ FILE: extras/outbounds/acl/v2geo/load_test.go ================================================ package v2geo import ( "testing" "github.com/stretchr/testify/assert" ) func TestLoadGeoIP(t *testing.T) { m, err := LoadGeoIP("geoip.dat") assert.NoError(t, err) // Exact checks since we know the data. assert.Len(t, m, 252) assert.Equal(t, m["cn"].CountryCode, "CN") assert.Len(t, m["cn"].Cidr, 10407) assert.Equal(t, m["us"].CountryCode, "US") assert.Len(t, m["us"].Cidr, 193171) assert.Equal(t, m["private"].CountryCode, "PRIVATE") assert.Len(t, m["private"].Cidr, 18) assert.Contains(t, m["private"].Cidr, &CIDR{ Ip: []byte("\xc0\xa8\x00\x00"), Prefix: 16, }) } func TestLoadGeoSite(t *testing.T) { m, err := LoadGeoSite("geosite.dat") assert.NoError(t, err) // Exact checks since we know the data. assert.Len(t, m, 1204) assert.Equal(t, m["netflix"].CountryCode, "NETFLIX") assert.Len(t, m["netflix"].Domain, 25) assert.Contains(t, m["netflix"].Domain, &Domain{ Type: Domain_Full, Value: "netflix.com.edgesuite.net", }) assert.Contains(t, m["netflix"].Domain, &Domain{ Type: Domain_RootDomain, Value: "fast.com", }) assert.Len(t, m["google"].Domain, 1066) assert.Contains(t, m["google"].Domain, &Domain{ Type: Domain_RootDomain, Value: "ggpht.cn", Attribute: []*Domain_Attribute{ { Key: "cn", TypedValue: &Domain_Attribute_BoolValue{BoolValue: true}, }, }, }) } ================================================ FILE: extras/outbounds/acl/v2geo/v2geo.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 // protoc v4.24.4 // source: v2geo.proto package v2geo import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // Type of domain value. type Domain_Type int32 const ( // The value is used as is. Domain_Plain Domain_Type = 0 // The value is used as a regular expression. Domain_Regex Domain_Type = 1 // The value is a root domain. Domain_RootDomain Domain_Type = 2 // The value is a domain. Domain_Full Domain_Type = 3 ) // Enum value maps for Domain_Type. var ( Domain_Type_name = map[int32]string{ 0: "Plain", 1: "Regex", 2: "RootDomain", 3: "Full", } Domain_Type_value = map[string]int32{ "Plain": 0, "Regex": 1, "RootDomain": 2, "Full": 3, } ) func (x Domain_Type) Enum() *Domain_Type { p := new(Domain_Type) *p = x return p } func (x Domain_Type) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (Domain_Type) Descriptor() protoreflect.EnumDescriptor { return file_v2geo_proto_enumTypes[0].Descriptor() } func (Domain_Type) Type() protoreflect.EnumType { return &file_v2geo_proto_enumTypes[0] } func (x Domain_Type) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use Domain_Type.Descriptor instead. func (Domain_Type) EnumDescriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{0, 0} } // Domain for routing decision. type Domain struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Domain matching type. Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Domain_Type" json:"type,omitempty"` // Domain value. Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` // Attributes of this domain. May be used for filtering. Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"` } func (x *Domain) Reset() { *x = Domain{} if protoimpl.UnsafeEnabled { mi := &file_v2geo_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Domain) String() string { return protoimpl.X.MessageStringOf(x) } func (*Domain) ProtoMessage() {} func (x *Domain) ProtoReflect() protoreflect.Message { mi := &file_v2geo_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Domain.ProtoReflect.Descriptor instead. func (*Domain) Descriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{0} } func (x *Domain) GetType() Domain_Type { if x != nil { return x.Type } return Domain_Plain } func (x *Domain) GetValue() string { if x != nil { return x.Value } return "" } func (x *Domain) GetAttribute() []*Domain_Attribute { if x != nil { return x.Attribute } return nil } // IP for routing decision, in CIDR form. type CIDR struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // IP address, should be either 4 or 16 bytes. Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` // Number of leading ones in the network mask. Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"` } func (x *CIDR) Reset() { *x = CIDR{} if protoimpl.UnsafeEnabled { mi := &file_v2geo_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *CIDR) String() string { return protoimpl.X.MessageStringOf(x) } func (*CIDR) ProtoMessage() {} func (x *CIDR) ProtoReflect() protoreflect.Message { mi := &file_v2geo_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use CIDR.ProtoReflect.Descriptor instead. func (*CIDR) Descriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{1} } func (x *CIDR) GetIp() []byte { if x != nil { return x.Ip } return nil } func (x *CIDR) GetPrefix() uint32 { if x != nil { return x.Prefix } return 0 } type GeoIP struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"` InverseMatch bool `protobuf:"varint,3,opt,name=inverse_match,json=inverseMatch,proto3" json:"inverse_match,omitempty"` // resource_hash instruct simplified config converter to load domain from geo file. ResourceHash []byte `protobuf:"bytes,4,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"` Code string `protobuf:"bytes,5,opt,name=code,proto3" json:"code,omitempty"` } func (x *GeoIP) Reset() { *x = GeoIP{} if protoimpl.UnsafeEnabled { mi := &file_v2geo_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoIP) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoIP) ProtoMessage() {} func (x *GeoIP) ProtoReflect() protoreflect.Message { mi := &file_v2geo_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoIP.ProtoReflect.Descriptor instead. func (*GeoIP) Descriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{2} } func (x *GeoIP) GetCountryCode() string { if x != nil { return x.CountryCode } return "" } func (x *GeoIP) GetCidr() []*CIDR { if x != nil { return x.Cidr } return nil } func (x *GeoIP) GetInverseMatch() bool { if x != nil { return x.InverseMatch } return false } func (x *GeoIP) GetResourceHash() []byte { if x != nil { return x.ResourceHash } return nil } func (x *GeoIP) GetCode() string { if x != nil { return x.Code } return "" } type GeoIPList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` } func (x *GeoIPList) Reset() { *x = GeoIPList{} if protoimpl.UnsafeEnabled { mi := &file_v2geo_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoIPList) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoIPList) ProtoMessage() {} func (x *GeoIPList) ProtoReflect() protoreflect.Message { mi := &file_v2geo_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead. func (*GeoIPList) Descriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{3} } func (x *GeoIPList) GetEntry() []*GeoIP { if x != nil { return x.Entry } return nil } type GeoSite struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` // resource_hash instruct simplified config converter to load domain from geo file. ResourceHash []byte `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"` Code string `protobuf:"bytes,4,opt,name=code,proto3" json:"code,omitempty"` } func (x *GeoSite) Reset() { *x = GeoSite{} if protoimpl.UnsafeEnabled { mi := &file_v2geo_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoSite) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoSite) ProtoMessage() {} func (x *GeoSite) ProtoReflect() protoreflect.Message { mi := &file_v2geo_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoSite.ProtoReflect.Descriptor instead. func (*GeoSite) Descriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{4} } func (x *GeoSite) GetCountryCode() string { if x != nil { return x.CountryCode } return "" } func (x *GeoSite) GetDomain() []*Domain { if x != nil { return x.Domain } return nil } func (x *GeoSite) GetResourceHash() []byte { if x != nil { return x.ResourceHash } return nil } func (x *GeoSite) GetCode() string { if x != nil { return x.Code } return "" } type GeoSiteList struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"` } func (x *GeoSiteList) Reset() { *x = GeoSiteList{} if protoimpl.UnsafeEnabled { mi := &file_v2geo_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GeoSiteList) String() string { return protoimpl.X.MessageStringOf(x) } func (*GeoSiteList) ProtoMessage() {} func (x *GeoSiteList) ProtoReflect() protoreflect.Message { mi := &file_v2geo_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead. func (*GeoSiteList) Descriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{5} } func (x *GeoSiteList) GetEntry() []*GeoSite { if x != nil { return x.Entry } return nil } type Domain_Attribute struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Types that are assignable to TypedValue: // // *Domain_Attribute_BoolValue // *Domain_Attribute_IntValue TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"` } func (x *Domain_Attribute) Reset() { *x = Domain_Attribute{} if protoimpl.UnsafeEnabled { mi := &file_v2geo_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Domain_Attribute) String() string { return protoimpl.X.MessageStringOf(x) } func (*Domain_Attribute) ProtoMessage() {} func (x *Domain_Attribute) ProtoReflect() protoreflect.Message { mi := &file_v2geo_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead. func (*Domain_Attribute) Descriptor() ([]byte, []int) { return file_v2geo_proto_rawDescGZIP(), []int{0, 0} } func (x *Domain_Attribute) GetKey() string { if x != nil { return x.Key } return "" } func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue { if m != nil { return m.TypedValue } return nil } func (x *Domain_Attribute) GetBoolValue() bool { if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok { return x.BoolValue } return false } func (x *Domain_Attribute) GetIntValue() int64 { if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok { return x.IntValue } return 0 } type isDomain_Attribute_TypedValue interface { isDomain_Attribute_TypedValue() } type Domain_Attribute_BoolValue struct { BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"` } type Domain_Attribute_IntValue struct { IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"` } func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {} func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {} var File_v2geo_proto protoreflect.FileDescriptor var file_v2geo_proto_rawDesc = []byte{ 0x0a, 0x0b, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x02, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x6f, 0x6f, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xa3, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a, 0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x86, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x2d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_v2geo_proto_rawDescOnce sync.Once file_v2geo_proto_rawDescData = file_v2geo_proto_rawDesc ) func file_v2geo_proto_rawDescGZIP() []byte { file_v2geo_proto_rawDescOnce.Do(func() { file_v2geo_proto_rawDescData = protoimpl.X.CompressGZIP(file_v2geo_proto_rawDescData) }) return file_v2geo_proto_rawDescData } var file_v2geo_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_v2geo_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_v2geo_proto_goTypes = []interface{}{ (Domain_Type)(0), // 0: Domain.Type (*Domain)(nil), // 1: Domain (*CIDR)(nil), // 2: CIDR (*GeoIP)(nil), // 3: GeoIP (*GeoIPList)(nil), // 4: GeoIPList (*GeoSite)(nil), // 5: GeoSite (*GeoSiteList)(nil), // 6: GeoSiteList (*Domain_Attribute)(nil), // 7: Domain.Attribute } var file_v2geo_proto_depIdxs = []int32{ 0, // 0: Domain.type:type_name -> Domain.Type 7, // 1: Domain.attribute:type_name -> Domain.Attribute 2, // 2: GeoIP.cidr:type_name -> CIDR 3, // 3: GeoIPList.entry:type_name -> GeoIP 1, // 4: GeoSite.domain:type_name -> Domain 5, // 5: GeoSiteList.entry:type_name -> GeoSite 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name } func init() { file_v2geo_proto_init() } func file_v2geo_proto_init() { if File_v2geo_proto != nil { return } if !protoimpl.UnsafeEnabled { file_v2geo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Domain); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v2geo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CIDR); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v2geo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoIP); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v2geo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoIPList); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v2geo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoSite); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v2geo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GeoSiteList); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_v2geo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Domain_Attribute); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } file_v2geo_proto_msgTypes[6].OneofWrappers = []interface{}{ (*Domain_Attribute_BoolValue)(nil), (*Domain_Attribute_IntValue)(nil), } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_v2geo_proto_rawDesc, NumEnums: 1, NumMessages: 7, NumExtensions: 0, NumServices: 0, }, GoTypes: file_v2geo_proto_goTypes, DependencyIndexes: file_v2geo_proto_depIdxs, EnumInfos: file_v2geo_proto_enumTypes, MessageInfos: file_v2geo_proto_msgTypes, }.Build() File_v2geo_proto = out.File file_v2geo_proto_rawDesc = nil file_v2geo_proto_goTypes = nil file_v2geo_proto_depIdxs = nil } ================================================ FILE: extras/outbounds/acl/v2geo/v2geo.proto ================================================ syntax = "proto3"; option go_package = "./v2geo"; // This file is copied from // https://github.com/v2fly/v2ray-core/blob/master/app/router/routercommon/common.proto // with some modifications. // Domain for routing decision. message Domain { // Type of domain value. enum Type { // The value is used as is. Plain = 0; // The value is used as a regular expression. Regex = 1; // The value is a root domain. RootDomain = 2; // The value is a domain. Full = 3; } // Domain matching type. Type type = 1; // Domain value. string value = 2; message Attribute { string key = 1; oneof typed_value { bool bool_value = 2; int64 int_value = 3; } } // Attributes of this domain. May be used for filtering. repeated Attribute attribute = 3; } // IP for routing decision, in CIDR form. message CIDR { // IP address, should be either 4 or 16 bytes. bytes ip = 1; // Number of leading ones in the network mask. uint32 prefix = 2; } message GeoIP { string country_code = 1; repeated CIDR cidr = 2; bool inverse_match = 3; // resource_hash instruct simplified config converter to load domain from geo file. bytes resource_hash = 4; string code = 5; } message GeoIPList { repeated GeoIP entry = 1; } message GeoSite { string country_code = 1; repeated Domain domain = 2; // resource_hash instruct simplified config converter to load domain from geo file. bytes resource_hash = 3; string code = 4; } message GeoSiteList { repeated GeoSite entry = 1; } ================================================ FILE: extras/outbounds/acl.go ================================================ package outbounds import ( "errors" "net" "os" "strings" "github.com/apernet/hysteria/extras/v2/outbounds/acl" ) const ( aclCacheSize = 1024 ) var errRejected = errors.New("rejected") // aclEngine is a PluggableOutbound that dispatches connections to different // outbounds based on ACL rules. // There are 3 built-in outbounds: // - direct: directOutbound, auto mode // - reject: reject the connection // - default: first outbound in the list, or if the list is empty, equal to direct // If the user-defined outbounds contain any of the above names, they will // override the built-in outbounds. type aclEngine struct { RuleSet acl.CompiledRuleSet[PluggableOutbound] Default PluggableOutbound } type OutboundEntry struct { Name string Outbound PluggableOutbound } func NewACLEngineFromString(rules string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) { trs, err := acl.ParseTextRules(rules) if err != nil { return nil, err } obMap := outboundsToMap(outbounds) rs, err := acl.Compile[PluggableOutbound](trs, obMap, aclCacheSize, geoLoader) if err != nil { return nil, err } return &aclEngine{rs, obMap["default"]}, nil } func NewACLEngineFromFile(filename string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) { bs, err := os.ReadFile(filename) if err != nil { return nil, err } return NewACLEngineFromString(string(bs), outbounds, geoLoader) } func outboundsToMap(outbounds []OutboundEntry) map[string]PluggableOutbound { obMap := make(map[string]PluggableOutbound) for _, ob := range outbounds { obMap[strings.ToLower(ob.Name)] = ob.Outbound } // Add built-in outbounds if not overridden if _, ok := obMap["direct"]; !ok { obMap["direct"] = NewDirectOutboundSimple(DirectOutboundModeAuto) } if _, ok := obMap["reject"]; !ok { obMap["reject"] = &aclRejectOutbound{} } if _, ok := obMap["default"]; !ok { if len(outbounds) > 0 { obMap["default"] = outbounds[0].Outbound } else { obMap["default"] = obMap["direct"] } } return obMap } func (a *aclEngine) handle(reqAddr *AddrEx, proto acl.Protocol) PluggableOutbound { hostInfo := acl.HostInfo{Name: reqAddr.Host} if reqAddr.ResolveInfo != nil { hostInfo.IPv4 = reqAddr.ResolveInfo.IPv4 hostInfo.IPv6 = reqAddr.ResolveInfo.IPv6 } ob, hijackIP := a.RuleSet.Match(hostInfo, proto, reqAddr.Port) if ob == nil { // No match, use default outbound return a.Default } if hijackIP != nil { // We must rewrite both Host & ResolveInfo, // as some outbounds only care about Host. reqAddr.Host = hijackIP.String() if ip4 := hijackIP.To4(); ip4 != nil { reqAddr.ResolveInfo = &ResolveInfo{IPv4: ip4} } else { reqAddr.ResolveInfo = &ResolveInfo{IPv6: hijackIP} } } return ob } func (a *aclEngine) TCP(reqAddr *AddrEx) (net.Conn, error) { ob := a.handle(reqAddr, acl.ProtocolTCP) return ob.TCP(reqAddr) } func (a *aclEngine) UDP(reqAddr *AddrEx) (UDPConn, error) { ob := a.handle(reqAddr, acl.ProtocolUDP) return ob.UDP(reqAddr) } type aclRejectOutbound struct{} func (a *aclRejectOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { return nil, errRejected } func (a *aclRejectOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { return nil, errRejected } ================================================ FILE: extras/outbounds/acl_test.go ================================================ package outbounds import ( "net" "testing" "github.com/stretchr/testify/assert" ) func TestACLEngine(t *testing.T) { ob1, ob2, ob3 := &mockPluggableOutbound{}, &mockPluggableOutbound{}, &mockPluggableOutbound{} obs := []OutboundEntry{ {"ob1", ob1}, {"ob2", ob2}, {"ob3", ob3}, {"direct", ob2}, } acl, err := NewACLEngineFromString(` ob2(google.com,tcp) ob3(youtube.com,udp) ob1 (1.1.1.1/24,*,8.8.8.8) Direct(cia.gov) reJect(nsa.gov) `, obs, nil) assert.NoError(t, err) // No match, default, should be the first (ob1) ob1.EXPECT().TCP(&AddrEx{Host: "example.com"}).Return(nil, nil).Once() conn, err := acl.TCP(&AddrEx{Host: "example.com"}) assert.NoError(t, err) assert.Nil(t, conn) // Match ob2 ob2.EXPECT().TCP(&AddrEx{Host: "google.com"}).Return(nil, nil).Once() conn, err = acl.TCP(&AddrEx{Host: "google.com"}) assert.NoError(t, err) assert.Nil(t, conn) // Match ob3 ob3.EXPECT().UDP(&AddrEx{Host: "youtube.com"}).Return(nil, nil).Once() udpConn, err := acl.UDP(&AddrEx{Host: "youtube.com"}) assert.NoError(t, err) assert.Nil(t, udpConn) // Match ob1 hijack IP ob1.EXPECT().TCP(&AddrEx{Host: "8.8.8.8", ResolveInfo: &ResolveInfo{IPv4: net.ParseIP("8.8.8.8").To4()}}).Return(nil, nil).Once() conn, err = acl.TCP(&AddrEx{ResolveInfo: &ResolveInfo{IPv4: net.ParseIP("1.1.1.22")}}) assert.NoError(t, err) assert.Nil(t, conn) // direct should be ob2 as we override it ob2.EXPECT().TCP(&AddrEx{Host: "cia.gov"}).Return(nil, nil).Once() conn, err = acl.TCP(&AddrEx{Host: "cia.gov"}) assert.NoError(t, err) assert.Nil(t, conn) // reject conn, err = acl.TCP(&AddrEx{Host: "nsa.gov"}) assert.Error(t, err) assert.Nil(t, conn) } ================================================ FILE: extras/outbounds/dns_https.go ================================================ package outbounds import ( "crypto/tls" "net" "net/http" "time" "github.com/babolivier/go-doh-client" ) // dohResolver is a PluggableOutbound DNS resolver that resolves hostnames // using the user-provided DNS-over-HTTPS server. type dohResolver struct { Resolver *doh.Resolver Next PluggableOutbound } func NewDoHResolver(host string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound { tr := http.DefaultTransport.(*http.Transport).Clone() tr.TLSClientConfig = &tls.Config{ ServerName: sni, InsecureSkipVerify: insecure, } return &dohResolver{ Resolver: &doh.Resolver{ Host: host, Class: doh.IN, HTTPClient: &http.Client{ Transport: tr, Timeout: timeoutOrDefault(timeout), }, }, Next: next, } } func (r *dohResolver) resolve(reqAddr *AddrEx) { if tryParseIP(reqAddr) { // The host is already an IP address, we don't need to resolve it. return } type lookupResult struct { ip net.IP err error } ch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1) go func() { recs, _, err := r.Resolver.LookupA(reqAddr.Host) var ip net.IP if err == nil && len(recs) > 0 { ip = net.ParseIP(recs[0].IP4).To4() } ch4 <- lookupResult{ip, err} }() go func() { recs, _, err := r.Resolver.LookupAAAA(reqAddr.Host) var ip net.IP if err == nil && len(recs) > 0 { ip = net.ParseIP(recs[0].IP6).To16() } ch6 <- lookupResult{ip, err} }() result4, result6 := <-ch4, <-ch6 reqAddr.ResolveInfo = &ResolveInfo{ IPv4: result4.ip, IPv6: result6.ip, } if result4.err != nil { reqAddr.ResolveInfo.Err = result4.err } else if result6.err != nil { reqAddr.ResolveInfo.Err = result6.err } } func (r *dohResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { r.resolve(reqAddr) return r.Next.TCP(reqAddr) } func (r *dohResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { r.resolve(reqAddr) return r.Next.UDP(reqAddr) } ================================================ FILE: extras/outbounds/dns_standard.go ================================================ package outbounds import ( "crypto/tls" "net" "time" "github.com/miekg/dns" ) const ( resolverDefaultTimeout = 2 * time.Second standardResolverRetryTimes = 2 ) // standardResolver is a PluggableOutbound DNS resolver that resolves hostnames // using the user-provided DNS server. // Based on "github.com/miekg/dns", it supports UDP, TCP & DNS-over-TLS (TCP). type standardResolver struct { Addr string Client *dns.Client Next PluggableOutbound } func NewStandardResolverUDP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound { return &standardResolver{ Addr: addDefaultPort(addr), Client: &dns.Client{ Timeout: timeoutOrDefault(timeout), }, Next: next, } } func NewStandardResolverTCP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound { return &standardResolver{ Addr: addDefaultPort(addr), Client: &dns.Client{ Net: "tcp", Timeout: timeoutOrDefault(timeout), }, Next: next, } } func NewStandardResolverTLS(addr string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound { return &standardResolver{ Addr: addDefaultPortTLS(addr), Client: &dns.Client{ Net: "tcp-tls", Timeout: timeoutOrDefault(timeout), TLSConfig: &tls.Config{ ServerName: sni, InsecureSkipVerify: insecure, }, }, Next: next, } } // addDefaultPort adds the default DNS port (53) to the address if not present. func addDefaultPort(addr string) string { if _, _, err := net.SplitHostPort(addr); err != nil { return net.JoinHostPort(addr, "53") } return addr } // addDefaultPortTLS adds the default DNS-over-TLS port (853) to the address if not present. func addDefaultPortTLS(addr string) string { if _, _, err := net.SplitHostPort(addr); err != nil { return net.JoinHostPort(addr, "853") } return addr } func timeoutOrDefault(timeout time.Duration) time.Duration { if timeout == 0 { return resolverDefaultTimeout } return timeout } // skipCNAMEChain skips the CNAME chain and returns the last CNAME target. // Sometimes the DNS server returns a CNAME chain like this, in one packet: // domain1.com. CNAME domain2.com. // domain2.com. CNAME domain3.com. // In this case, we should avoid sending a query for domain2.com and go // straight to domain3.com. func (r *standardResolver) skipCNAMEChain(answers []dns.RR) string { var lastCNAME string for _, a := range answers { if cname, ok := a.(*dns.CNAME); ok { if lastCNAME == "" { // First CNAME lastCNAME = cname.Target } else if cname.Hdr.Name == lastCNAME { // CNAME chain lastCNAME = cname.Target } else { // CNAME chain ends return lastCNAME } } } return lastCNAME } // lookup4 resolves a hostname to an IPv4 address. // If there's no IPv4 address, it returns (nil, nil), no error. func (r *standardResolver) lookup4(host string) (net.IP, error) { m := new(dns.Msg) m.SetQuestion(dns.Fqdn(host), dns.TypeA) m.RecursionDesired = true resp, _, err := r.Client.Exchange(m, r.Addr) if err != nil { return nil, err } if len(resp.Answer) == 0 { return nil, nil } // Sometimes the DNS server returns both CNAME and A records in one packet. hasCNAME := false for _, a := range resp.Answer { if aa, ok := a.(*dns.A); ok { return aa.A.To4(), nil } else if _, ok := a.(*dns.CNAME); ok { hasCNAME = true } } if hasCNAME { return r.lookup4(r.skipCNAMEChain(resp.Answer)) } else { // Should not happen return nil, nil } } // lookup6 resolves a hostname to an IPv6 address. // If there's no IPv6 address, it returns (nil, nil), no error. func (r *standardResolver) lookup6(host string) (net.IP, error) { m := new(dns.Msg) m.SetQuestion(dns.Fqdn(host), dns.TypeAAAA) m.RecursionDesired = true resp, _, err := r.Client.Exchange(m, r.Addr) if err != nil { return nil, err } if len(resp.Answer) == 0 { return nil, nil } // Sometimes the DNS server returns both CNAME and AAAA records in one packet. hasCNAME := false for _, a := range resp.Answer { if aa, ok := a.(*dns.AAAA); ok { return aa.AAAA.To16(), nil } else if _, ok := a.(*dns.CNAME); ok { hasCNAME = true } } if hasCNAME { return r.lookup6(r.skipCNAMEChain(resp.Answer)) } else { // Should not happen return nil, nil } } func (r *standardResolver) resolve(reqAddr *AddrEx) { if tryParseIP(reqAddr) { // The host is already an IP address, we don't need to resolve it. return } type lookupResult struct { ip net.IP err error } ch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1) go func() { var ip net.IP var err error for i := 0; i < standardResolverRetryTimes; i++ { ip, err = r.lookup4(reqAddr.Host) if err == nil { break } } ch4 <- lookupResult{ip, err} }() go func() { var ip net.IP var err error for i := 0; i < standardResolverRetryTimes; i++ { ip, err = r.lookup6(reqAddr.Host) if err == nil { break } } ch6 <- lookupResult{ip, err} }() result4, result6 := <-ch4, <-ch6 reqAddr.ResolveInfo = &ResolveInfo{ IPv4: result4.ip, IPv6: result6.ip, } if result4.err != nil { reqAddr.ResolveInfo.Err = result4.err } else if result6.err != nil { reqAddr.ResolveInfo.Err = result6.err } } func (r *standardResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { r.resolve(reqAddr) return r.Next.TCP(reqAddr) } func (r *standardResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { r.resolve(reqAddr) return r.Next.UDP(reqAddr) } ================================================ FILE: extras/outbounds/dns_system.go ================================================ package outbounds import ( "net" ) // systemResolver is a PluggableOutbound DNS resolver that resolves hostnames // using the default system DNS server. // Outbounds typically don't require a resolver, as they can do DNS resolution // themselves. However, when using ACL, it's necessary to place a resolver in // front of it in the pipeline (for IP rules to work on domain requests). type systemResolver struct { Next PluggableOutbound } func NewSystemResolver(next PluggableOutbound) PluggableOutbound { return &systemResolver{ Next: next, } } func (r *systemResolver) resolve(reqAddr *AddrEx) { ips, err := net.LookupIP(reqAddr.Host) if err != nil { reqAddr.ResolveInfo = &ResolveInfo{Err: err} return } info := &ResolveInfo{} info.IPv4, info.IPv6 = splitIPv4IPv6(ips) reqAddr.ResolveInfo = info } func (r *systemResolver) TCP(reqAddr *AddrEx) (net.Conn, error) { r.resolve(reqAddr) return r.Next.TCP(reqAddr) } func (r *systemResolver) UDP(reqAddr *AddrEx) (UDPConn, error) { r.resolve(reqAddr) return r.Next.UDP(reqAddr) } ================================================ FILE: extras/outbounds/interface.go ================================================ package outbounds import ( "net" "strconv" "github.com/apernet/hysteria/core/v2/server" ) // The PluggableOutbound system is designed to function in a chain-like manner. // Not every outbound is an actual outbound; some are just wrappers around other // outbounds, such as custom resolvers, ACL engine, etc. It is a pipeline where // each stage can check (and optionally modify) the request before passing it // on to the next stage. The last stage in the pipeline is always a real outbound // that actually implements the logic of connecting to the remote server. // There can also be instances of branching, where requests can be sent to // different outbound sub-pipelines based on some criteria. // PluggableOutbound differs from the built-in Outbound interface from Hysteria core // in that it uses an AddrEx struct for addresses instead of a string. Because of this // difference, we need a special PluggableOutboundAdapter to convert between the two // for use in Hysteria core config. type PluggableOutbound interface { TCP(reqAddr *AddrEx) (net.Conn, error) UDP(reqAddr *AddrEx) (UDPConn, error) } type UDPConn interface { ReadFrom(b []byte) (int, *AddrEx, error) WriteTo(b []byte, addr *AddrEx) (int, error) Close() error } // AddrEx keeps both the original string representation of the address and // the resolved IP addresses from the resolver, if any. // The actual outbound implementations can choose to use either the string // representation or the resolved IP addresses, depending on their capabilities. // A SOCKS5 outbound, for example, should prefer the string representation // because SOCKS5 protocol supports sending the hostname to the proxy server // and let the proxy server do the DNS resolution. type AddrEx struct { Host string // String representation of the host, can be an IP or a domain name Port uint16 ResolveInfo *ResolveInfo // Only set if there's a resolver in the pipeline } func (a *AddrEx) String() string { return net.JoinHostPort(a.Host, strconv.Itoa(int(a.Port))) } // ResolveInfo contains the resolved IP addresses from the resolver, and any // error that occurred during the resolution. // Note that there could be no error but also no resolved IP addresses, // or there could be an error but also some resolved IP addresses. // It's up to the actual outbound implementation to decide how to handle // these cases. type ResolveInfo struct { IPv4 net.IP IPv6 net.IP Err error } var _ server.Outbound = (*PluggableOutboundAdapter)(nil) type PluggableOutboundAdapter struct { PluggableOutbound } func (a *PluggableOutboundAdapter) TCP(reqAddr string) (net.Conn, error) { host, port, err := net.SplitHostPort(reqAddr) if err != nil { return nil, err } portInt, err := strconv.Atoi(port) if err != nil { return nil, err } return a.PluggableOutbound.TCP(&AddrEx{ Host: host, Port: uint16(portInt), }) } func (a *PluggableOutboundAdapter) UDP(reqAddr string) (server.UDPConn, error) { host, port, err := net.SplitHostPort(reqAddr) if err != nil { return nil, err } portInt, err := strconv.Atoi(port) if err != nil { return nil, err } conn, err := a.PluggableOutbound.UDP(&AddrEx{ Host: host, Port: uint16(portInt), }) if err != nil { return nil, err } return &udpConnAdapter{conn}, nil } type udpConnAdapter struct { UDPConn } func (u *udpConnAdapter) ReadFrom(b []byte) (int, string, error) { n, addr, err := u.UDPConn.ReadFrom(b) if addr != nil { return n, addr.String(), err } else { return n, "", err } } func (u *udpConnAdapter) WriteTo(b []byte, addr string) (int, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return 0, err } portInt, err := strconv.Atoi(port) if err != nil { return 0, err } return u.UDPConn.WriteTo(b, &AddrEx{ Host: host, Port: uint16(portInt), }) } func (u *udpConnAdapter) Close() error { return u.UDPConn.Close() } ================================================ FILE: extras/outbounds/interface_test.go ================================================ package outbounds import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestPluggableOutboundAdapter(t *testing.T) { ob := newMockPluggableOutbound(t) adapter := &PluggableOutboundAdapter{ob} ob.EXPECT().TCP(&AddrEx{ Host: "only.fans", Port: 443, }).Return(nil, nil).Once() conn, err := adapter.TCP("only.fans:443") assert.Nil(t, conn) assert.Nil(t, err) mc := newMockUDPConn(t) mc.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, *AddrEx, error) { return copy(bs, "gura"), &AddrEx{ Host: "gura.com", Port: 2333, }, nil }).Once() mc.EXPECT().WriteTo([]byte("gawr"), &AddrEx{ Host: "another.hololive.tv", Port: 1551, }).Return(4, nil).Once() ob.EXPECT().UDP(&AddrEx{ Host: "hololive.tv", Port: 8999, }).Return(mc, nil).Once() uConn, err := adapter.UDP("hololive.tv:8999") assert.Nil(t, err) assert.NotNil(t, uConn) n, err := uConn.WriteTo([]byte("gawr"), "another.hololive.tv:1551") assert.Nil(t, err) assert.Equal(t, 4, n) bs := make([]byte, 1024) n, addr, err := uConn.ReadFrom(bs) assert.Nil(t, err) assert.Equal(t, 4, n) assert.Equal(t, "gura", string(bs[:n])) assert.Equal(t, "gura.com:2333", addr) } ================================================ FILE: extras/outbounds/mock_PluggableOutbound.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package outbounds import ( net "net" mock "github.com/stretchr/testify/mock" ) // mockPluggableOutbound is an autogenerated mock type for the PluggableOutbound type type mockPluggableOutbound struct { mock.Mock } type mockPluggableOutbound_Expecter struct { mock *mock.Mock } func (_m *mockPluggableOutbound) EXPECT() *mockPluggableOutbound_Expecter { return &mockPluggableOutbound_Expecter{mock: &_m.Mock} } // TCP provides a mock function with given fields: reqAddr func (_m *mockPluggableOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { ret := _m.Called(reqAddr) if len(ret) == 0 { panic("no return value specified for TCP") } var r0 net.Conn var r1 error if rf, ok := ret.Get(0).(func(*AddrEx) (net.Conn, error)); ok { return rf(reqAddr) } if rf, ok := ret.Get(0).(func(*AddrEx) net.Conn); ok { r0 = rf(reqAddr) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(net.Conn) } } if rf, ok := ret.Get(1).(func(*AddrEx) error); ok { r1 = rf(reqAddr) } else { r1 = ret.Error(1) } return r0, r1 } // mockPluggableOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP' type mockPluggableOutbound_TCP_Call struct { *mock.Call } // TCP is a helper method to define mock.On call // - reqAddr *AddrEx func (_e *mockPluggableOutbound_Expecter) TCP(reqAddr interface{}) *mockPluggableOutbound_TCP_Call { return &mockPluggableOutbound_TCP_Call{Call: _e.mock.On("TCP", reqAddr)} } func (_c *mockPluggableOutbound_TCP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_TCP_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(*AddrEx)) }) return _c } func (_c *mockPluggableOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *mockPluggableOutbound_TCP_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *mockPluggableOutbound_TCP_Call) RunAndReturn(run func(*AddrEx) (net.Conn, error)) *mockPluggableOutbound_TCP_Call { _c.Call.Return(run) return _c } // UDP provides a mock function with given fields: reqAddr func (_m *mockPluggableOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { ret := _m.Called(reqAddr) if len(ret) == 0 { panic("no return value specified for UDP") } var r0 UDPConn var r1 error if rf, ok := ret.Get(0).(func(*AddrEx) (UDPConn, error)); ok { return rf(reqAddr) } if rf, ok := ret.Get(0).(func(*AddrEx) UDPConn); ok { r0 = rf(reqAddr) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(UDPConn) } } if rf, ok := ret.Get(1).(func(*AddrEx) error); ok { r1 = rf(reqAddr) } else { r1 = ret.Error(1) } return r0, r1 } // mockPluggableOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP' type mockPluggableOutbound_UDP_Call struct { *mock.Call } // UDP is a helper method to define mock.On call // - reqAddr *AddrEx func (_e *mockPluggableOutbound_Expecter) UDP(reqAddr interface{}) *mockPluggableOutbound_UDP_Call { return &mockPluggableOutbound_UDP_Call{Call: _e.mock.On("UDP", reqAddr)} } func (_c *mockPluggableOutbound_UDP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_UDP_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(*AddrEx)) }) return _c } func (_c *mockPluggableOutbound_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockPluggableOutbound_UDP_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *mockPluggableOutbound_UDP_Call) RunAndReturn(run func(*AddrEx) (UDPConn, error)) *mockPluggableOutbound_UDP_Call { _c.Call.Return(run) return _c } // newMockPluggableOutbound creates a new instance of mockPluggableOutbound. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockPluggableOutbound(t interface { mock.TestingT Cleanup(func()) }) *mockPluggableOutbound { mock := &mockPluggableOutbound{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: extras/outbounds/mock_UDPConn.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package outbounds import mock "github.com/stretchr/testify/mock" // mockUDPConn is an autogenerated mock type for the UDPConn type type mockUDPConn struct { mock.Mock } type mockUDPConn_Expecter struct { mock *mock.Mock } func (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter { return &mockUDPConn_Expecter{mock: &_m.Mock} } // Close provides a mock function with given fields: func (_m *mockUDPConn) Close() error { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type mockUDPConn_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call { return &mockUDPConn_Close_Call{Call: _e.mock.On("Close")} } func (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call { _c.Call.Return(_a0) return _c } func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call { _c.Call.Return(run) return _c } // ReadFrom provides a mock function with given fields: b func (_m *mockUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) { ret := _m.Called(b) if len(ret) == 0 { panic("no return value specified for ReadFrom") } var r0 int var r1 *AddrEx var r2 error if rf, ok := ret.Get(0).(func([]byte) (int, *AddrEx, error)); ok { return rf(b) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(b) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) *AddrEx); ok { r1 = rf(b) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(*AddrEx) } } if rf, ok := ret.Get(2).(func([]byte) error); ok { r2 = rf(b) } else { r2 = ret.Error(2) } return r0, r1, r2 } // mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom' type mockUDPConn_ReadFrom_Call struct { *mock.Call } // ReadFrom is a helper method to define mock.On call // - b []byte func (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call { return &mockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)} } func (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 *AddrEx, _a2 error) *mockUDPConn_ReadFrom_Call { _c.Call.Return(_a0, _a1, _a2) return _c } func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, *AddrEx, error)) *mockUDPConn_ReadFrom_Call { _c.Call.Return(run) return _c } // WriteTo provides a mock function with given fields: b, addr func (_m *mockUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { ret := _m.Called(b, addr) if len(ret) == 0 { panic("no return value specified for WriteTo") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte, *AddrEx) (int, error)); ok { return rf(b, addr) } if rf, ok := ret.Get(0).(func([]byte, *AddrEx) int); ok { r0 = rf(b, addr) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte, *AddrEx) error); ok { r1 = rf(b, addr) } else { r1 = ret.Error(1) } return r0, r1 } // mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo' type mockUDPConn_WriteTo_Call struct { *mock.Call } // WriteTo is a helper method to define mock.On call // - b []byte // - addr *AddrEx func (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call { return &mockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)} } func (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr *AddrEx)) *mockUDPConn_WriteTo_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte), args[1].(*AddrEx)) }) return _c } func (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call { _c.Call.Return(_a0, _a1) return _c } func (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, *AddrEx) (int, error)) *mockUDPConn_WriteTo_Call { _c.Call.Return(run) return _c } // newMockUDPConn creates a new instance of mockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockUDPConn(t interface { mock.TestingT Cleanup(func()) }) *mockUDPConn { mock := &mockUDPConn{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: extras/outbounds/ob_direct.go ================================================ package outbounds import ( "errors" "net" "strconv" "time" ) type DirectOutboundMode int type udpConnState int const ( DirectOutboundModeAuto DirectOutboundMode = iota // Dual-stack "happy eyeballs"-like mode DirectOutboundMode64 // Use IPv6 address when available, otherwise IPv4 DirectOutboundMode46 // Use IPv4 address when available, otherwise IPv6 DirectOutboundMode6 // Use IPv6 only, fail if not available DirectOutboundMode4 // Use IPv4 only, fail if not available defaultDialerTimeout = 10 * time.Second ) const ( udpConnStateDualStack udpConnState = iota udpConnStateIPv4 udpConnStateIPv6 ) // directOutbound is a PluggableOutbound that connects directly to the target // using the local network (as opposed to using a proxy, for example). // It prefers to use ResolveInfo in AddrEx if available. But if it's nil, // it will fall back to resolving Host using Go's built-in DNS resolver. type directOutbound struct { Mode DirectOutboundMode // Dialer4 and Dialer6 are used for IPv4 and IPv6 TCP connections respectively. Dialer4 *net.Dialer Dialer6 *net.Dialer // DeviceName & BindIPs are for UDP connections. They don't use dialers, so we // need to bind them when creating the connection. DeviceName string BindIP4 net.IP BindIP6 net.IP } type noAddressError struct { IPv4 bool IPv6 bool } func (e noAddressError) Error() string { if e.IPv4 && e.IPv6 { return "no IPv4 or IPv6 address available" } else if e.IPv4 { return "no IPv4 address available" } else if e.IPv6 { return "no IPv6 address available" } else { return "no address available" } } type invalidOutboundModeError struct{} func (e invalidOutboundModeError) Error() string { return "invalid outbound mode" } type resolveError struct { Err error } func (e resolveError) Error() string { if e.Err == nil { return "resolve error" } else { return "resolve error: " + e.Err.Error() } } func (e resolveError) Unwrap() error { return e.Err } // NewDirectOutboundSimple creates a new directOutbound with the given mode, // without binding to a specific device. Works on all platforms. func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound { d := &net.Dialer{ Timeout: defaultDialerTimeout, } return &directOutbound{ Mode: mode, Dialer4: d, Dialer6: d, } } // NewDirectOutboundBindToIPs creates a new directOutbound with the given mode, // and binds to the given IPv4 and IPv6 addresses. Either or both of the addresses // can be nil, in which case the directOutbound will not bind to a specific address // for that family. func NewDirectOutboundBindToIPs(mode DirectOutboundMode, bindIP4, bindIP6 net.IP) (PluggableOutbound, error) { if bindIP4 != nil && bindIP4.To4() == nil { return nil, errors.New("bindIP4 must be an IPv4 address") } if bindIP6 != nil && bindIP6.To4() != nil { return nil, errors.New("bindIP6 must be an IPv6 address") } ob := &directOutbound{ Mode: mode, Dialer4: &net.Dialer{ Timeout: defaultDialerTimeout, }, Dialer6: &net.Dialer{ Timeout: defaultDialerTimeout, }, BindIP4: bindIP4, BindIP6: bindIP6, } if bindIP4 != nil { ob.Dialer4.LocalAddr = &net.TCPAddr{ IP: bindIP4, } } if bindIP6 != nil { ob.Dialer6.LocalAddr = &net.TCPAddr{ IP: bindIP6, } } return ob, nil } // resolve is our built-in DNS resolver for handling the case when // AddrEx.ResolveInfo is nil. func (d *directOutbound) resolve(reqAddr *AddrEx) { ips, err := net.LookupIP(reqAddr.Host) if err != nil { reqAddr.ResolveInfo = &ResolveInfo{Err: err} return } r := &ResolveInfo{} r.IPv4, r.IPv6 = splitIPv4IPv6(ips) if r.IPv4 == nil && r.IPv6 == nil { r.Err = noAddressError{IPv4: true, IPv6: true} } reqAddr.ResolveInfo = r } func (d *directOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { if reqAddr.ResolveInfo == nil { // AddrEx.ResolveInfo is nil (no resolver in the pipeline), // we need to resolve the address ourselves. d.resolve(reqAddr) } r := reqAddr.ResolveInfo if r.IPv4 == nil && r.IPv6 == nil { // ResolveInfo not nil but no address available, // this can only mean that the resolver failed. // Return the error from the resolver. return nil, resolveError{Err: r.Err} } switch d.Mode { case DirectOutboundModeAuto: if r.IPv4 != nil && r.IPv6 != nil { return d.dualStackDialTCP(r.IPv4, r.IPv6, reqAddr.Port) } else if r.IPv4 != nil { return d.dialTCP(r.IPv4, reqAddr.Port) } else { return d.dialTCP(r.IPv6, reqAddr.Port) } case DirectOutboundMode64: if r.IPv6 != nil { return d.dialTCP(r.IPv6, reqAddr.Port) } else { return d.dialTCP(r.IPv4, reqAddr.Port) } case DirectOutboundMode46: if r.IPv4 != nil { return d.dialTCP(r.IPv4, reqAddr.Port) } else { return d.dialTCP(r.IPv6, reqAddr.Port) } case DirectOutboundMode6: if r.IPv6 != nil { return d.dialTCP(r.IPv6, reqAddr.Port) } else { return nil, noAddressError{IPv6: true} } case DirectOutboundMode4: if r.IPv4 != nil { return d.dialTCP(r.IPv4, reqAddr.Port) } else { return nil, noAddressError{IPv4: true} } default: return nil, invalidOutboundModeError{} } } func (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) { if ip.To4() != nil { return d.Dialer4.Dial("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) } else { return d.Dialer6.Dial("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port)))) } } type dialResult struct { Conn net.Conn Err error } // dualStackDialTCP dials the target using both IPv4 and IPv6 addresses simultaneously. // It returns the first successful connection and drops the other one. // If both connections fail, it returns the last error. func (d *directOutbound) dualStackDialTCP(ipv4, ipv6 net.IP, port uint16) (net.Conn, error) { ch := make(chan dialResult, 2) go func() { conn, err := d.dialTCP(ipv4, port) ch <- dialResult{Conn: conn, Err: err} }() go func() { conn, err := d.dialTCP(ipv6, port) ch <- dialResult{Conn: conn, Err: err} }() // Get the first result, check if it's successful if r := <-ch; r.Err == nil { // Yes. Return this and close the other connection when it's done go func() { r2 := <-ch if r2.Conn != nil { _ = r2.Conn.Close() } }() return r.Conn, nil } else { // No. Return the other result, which may or may not be successful r2 := <-ch return r2.Conn, r2.Err } } type directOutboundUDPConn struct { *directOutbound *net.UDPConn State udpConnState } func (u *directOutboundUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) { n, addr, err := u.UDPConn.ReadFromUDP(b) if addr != nil { return n, &AddrEx{ Host: addr.IP.String(), Port: uint16(addr.Port), }, err } else { return n, nil, err } } func (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { if addr.ResolveInfo == nil { u.directOutbound.resolve(addr) } r := addr.ResolveInfo if r.IPv4 == nil && r.IPv6 == nil { return 0, resolveError{Err: r.Err} } if u.State == udpConnStateIPv4 { if r.IPv4 != nil { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv4, Port: int(addr.Port), }) } else { return 0, noAddressError{IPv4: true} } } else if u.State == udpConnStateIPv6 { if r.IPv6 != nil { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv6, Port: int(addr.Port), }) } else { return 0, noAddressError{IPv6: true} } } else { // Dual stack switch u.directOutbound.Mode { case DirectOutboundModeAuto: // This is a special case. // We must make a decision here, so we prefer IPv4 for maximum compatibility. if r.IPv4 != nil { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv4, Port: int(addr.Port), }) } else { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv6, Port: int(addr.Port), }) } case DirectOutboundMode64: if r.IPv6 != nil { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv6, Port: int(addr.Port), }) } else { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv4, Port: int(addr.Port), }) } case DirectOutboundMode46: if r.IPv4 != nil { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv4, Port: int(addr.Port), }) } else { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv6, Port: int(addr.Port), }) } case DirectOutboundMode6: if r.IPv6 != nil { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv6, Port: int(addr.Port), }) } else { return 0, noAddressError{IPv6: true} } case DirectOutboundMode4: if r.IPv4 != nil { return u.UDPConn.WriteToUDP(b, &net.UDPAddr{ IP: r.IPv4, Port: int(addr.Port), }) } else { return 0, noAddressError{IPv4: true} } default: return 0, invalidOutboundModeError{} } } } func (u *directOutboundUDPConn) Close() error { return u.UDPConn.Close() } func (d *directOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { if d.BindIP4 == nil && d.BindIP6 == nil { // No bind address specified, use default dual stack implementation c, err := net.ListenUDP("udp", nil) if err != nil { return nil, err } if d.DeviceName != "" { if err := udpConnBindToDevice(c, d.DeviceName); err != nil { // Don't forget to close the UDPConn if binding fails _ = c.Close() return nil, err } } return &directOutboundUDPConn{ directOutbound: d, UDPConn: c, State: udpConnStateDualStack, }, nil } else { // Bind address specified, // need to check what kind of address is in reqAddr // to determine which address family to bind to if reqAddr.ResolveInfo == nil { d.resolve(reqAddr) } r := reqAddr.ResolveInfo if r.IPv4 == nil && r.IPv6 == nil { return nil, resolveError{Err: r.Err} } var bindIP net.IP // can be nil, in which case we still lock the address family but don't bind to any address var state udpConnState // either IPv4 or IPv6 switch d.Mode { case DirectOutboundModeAuto: // This is a special case. // We must make a decision here, so we prefer IPv4 for maximum compatibility. if r.IPv4 != nil { bindIP = d.BindIP4 state = udpConnStateIPv4 } else { bindIP = d.BindIP6 state = udpConnStateIPv6 } case DirectOutboundMode64: if r.IPv6 != nil { bindIP = d.BindIP6 state = udpConnStateIPv6 } else { bindIP = d.BindIP4 state = udpConnStateIPv4 } case DirectOutboundMode46: if r.IPv4 != nil { bindIP = d.BindIP4 state = udpConnStateIPv4 } else { bindIP = d.BindIP6 state = udpConnStateIPv6 } case DirectOutboundMode6: if r.IPv6 != nil { bindIP = d.BindIP6 state = udpConnStateIPv6 } else { return nil, noAddressError{IPv6: true} } case DirectOutboundMode4: if r.IPv4 != nil { bindIP = d.BindIP4 state = udpConnStateIPv4 } else { return nil, noAddressError{IPv4: true} } default: return nil, invalidOutboundModeError{} } var network string var c *net.UDPConn var err error if state == udpConnStateIPv4 { network = "udp4" } else { network = "udp6" } if bindIP != nil { c, err = net.ListenUDP(network, &net.UDPAddr{ IP: bindIP, }) } else { c, err = net.ListenUDP(network, nil) } if err != nil { return nil, err } // We don't support binding to both device & address at the same time, // so d.DeviceName is ignored in this case. return &directOutboundUDPConn{ directOutbound: d, UDPConn: c, State: state, }, nil } } ================================================ FILE: extras/outbounds/ob_direct_linux.go ================================================ package outbounds import ( "errors" "net" "syscall" ) // NewDirectOutboundBindToDevice creates a new directOutbound with the given mode, // and binds to the given device. Only works on Linux. func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) { if err := verifyDeviceName(deviceName); err != nil { return nil, err } d := &net.Dialer{ Timeout: defaultDialerTimeout, Control: func(network, address string, c syscall.RawConn) error { var errBind error err := c.Control(func(fd uintptr) { errBind = syscall.BindToDevice(int(fd), deviceName) }) if err != nil { return err } return errBind }, } return &directOutbound{ Mode: mode, Dialer4: d, Dialer6: d, DeviceName: deviceName, }, nil } func verifyDeviceName(deviceName string) error { if deviceName == "" { return errors.New("device name cannot be empty") } _, err := net.InterfaceByName(deviceName) return err } func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error { sc, err := conn.SyscallConn() if err != nil { return err } var errBind error err = sc.Control(func(fd uintptr) { errBind = syscall.BindToDevice(int(fd), deviceName) }) if err != nil { return err } return errBind } ================================================ FILE: extras/outbounds/ob_direct_others.go ================================================ //go:build !linux package outbounds import ( "errors" "net" ) // NewDirectOutboundBindToDevice creates a new directOutbound with the given mode, // and binds to the given device. This doesn't work on non-Linux platforms, so this // is just a stub function that always returns an error. func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) { return nil, errors.New("binding to device is not supported on this platform") } func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error { return errors.New("binding to device is not supported on this platform") } ================================================ FILE: extras/outbounds/ob_http.go ================================================ package outbounds import ( "bufio" "bytes" "crypto/tls" "encoding/base64" "errors" "fmt" "io" "net" "net/http" "net/url" "strconv" "time" ) const ( httpRequestTimeout = 10 * time.Second ) var ( errHTTPUDPNotSupported = errors.New("UDP not supported by HTTP proxy") errHTTPUnsupportedScheme = errors.New("unsupported scheme for HTTP proxy (use http:// or https://)") ) type errHTTPRequestFailed struct { Status int } func (e errHTTPRequestFailed) Error() string { return fmt.Sprintf("HTTP request failed: %d", e.Status) } // httpOutbound is a PluggableOutbound that connects to the target using // an HTTP/HTTPS proxy server (that supports the CONNECT method). // HTTP proxies don't support UDP by design, so this outbound will reject // any UDP request with errHTTPUDPNotSupported. // Since HTTP proxies support using either IP or domain name as the target // address, it will ignore ResolveInfo in AddrEx and always only use Host. type httpOutbound struct { Dialer *net.Dialer Addr string HTTPS bool Insecure bool ServerName string BasicAuth string // This is after Base64 encoding } func NewHTTPOutbound(proxyURL string, insecure bool) (PluggableOutbound, error) { u, err := url.Parse(proxyURL) if err != nil { return nil, err } if u.Scheme != "http" && u.Scheme != "https" { return nil, errHTTPUnsupportedScheme } addr := u.Host if u.Port() == "" { if u.Scheme == "http" { addr = net.JoinHostPort(u.Host, "80") } else { addr = net.JoinHostPort(u.Host, "443") } } var basicAuth string if u.User != nil { username := u.User.Username() password, _ := u.User.Password() basicAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) } return &httpOutbound{ Dialer: &net.Dialer{Timeout: defaultDialerTimeout}, Addr: addr, HTTPS: u.Scheme == "https", Insecure: insecure, ServerName: u.Hostname(), BasicAuth: basicAuth, }, nil } func (o *httpOutbound) dial() (net.Conn, error) { conn, err := o.Dialer.Dial("tcp", o.Addr) if err != nil { return nil, err } if o.HTTPS { // Wrap the connection with TLS if the proxy is HTTPS. conn = tls.Client(conn, &tls.Config{ InsecureSkipVerify: o.Insecure, ServerName: o.Addr, }) } return conn, nil } func (o *httpOutbound) addrExToRequest(reqAddr *AddrEx) (*http.Request, error) { req := &http.Request{ Method: http.MethodConnect, URL: &url.URL{ Host: net.JoinHostPort(reqAddr.Host, strconv.Itoa(int(reqAddr.Port))), }, Header: http.Header{ "Proxy-Connection": []string{"Keep-Alive"}, }, } if o.BasicAuth != "" { req.Header.Add("Proxy-Authorization", o.BasicAuth) } return req, nil } func (o *httpOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) { req, err := o.addrExToRequest(reqAddr) if err != nil { return nil, err } conn, err := o.dial() if err != nil { return nil, err } if err := req.Write(conn); err != nil { _ = conn.Close() return nil, err } if err := conn.SetDeadline(time.Now().Add(httpRequestTimeout)); err != nil { _ = conn.Close() return nil, err } bufReader := bufio.NewReader(conn) resp, err := http.ReadResponse(bufReader, req) if resp != nil { // Don't need response body here. _ = resp.Body.Close() } if err != nil { _ = conn.Close() return nil, err } if resp.StatusCode != http.StatusOK { _ = conn.Close() return nil, errHTTPRequestFailed{resp.StatusCode} } if err := conn.SetDeadline(time.Time{}); err != nil { _ = conn.Close() return nil, err } if bufReader.Buffered() > 0 { // There is still data in the buffered reader. // We need to get it out and put it into a cachedConn, // so that handleConnect can read it. data := make([]byte, bufReader.Buffered()) _, err := io.ReadFull(bufReader, data) if err != nil { // Read from buffer failed, is this possible? _ = conn.Close() return nil, err } cachedConn := &cachedConn{ Conn: conn, Buffer: *bytes.NewBuffer(data), } return cachedConn, nil } else { return conn, nil } } func (o *httpOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) { return nil, errHTTPUDPNotSupported } // cachedConn is a net.Conn wrapper that first Read()s from a buffer, // and then from the underlying net.Conn when the buffer is drained. type cachedConn struct { net.Conn Buffer bytes.Buffer } func (c *cachedConn) Read(b []byte) (int, error) { if c.Buffer.Len() > 0 { n, err := c.Buffer.Read(b) if err == io.EOF { // Buffer is drained, hide it from the caller err = nil } return n, err } return c.Conn.Read(b) } ================================================ FILE: extras/outbounds/ob_socks5.go ================================================ package outbounds import ( "encoding/binary" "errors" "fmt" "io" "net" "time" "github.com/txthinking/socks5" ) const ( socks5NegotiationTimeout = 10 * time.Second socks5RequestTimeout = 10 * time.Second ) var errSOCKS5AuthFailed = errors.New("SOCKS5 authentication failed") type errSOCKS5UnsupportedAuthMethod struct { Method byte } func (e errSOCKS5UnsupportedAuthMethod) Error() string { return fmt.Sprintf("unsupported SOCKS5 authentication method: %d", e.Method) } type errSOCKS5RequestFailed struct { Rep byte } func (e errSOCKS5RequestFailed) Error() string { var msg string // RFC 1928 switch e.Rep { case 0x00: msg = "succeeded" case 0x01: msg = "general SOCKS server failure" case 0x02: msg = "connection not allowed by ruleset" case 0x03: msg = "Network unreachable" case 0x04: msg = "Host unreachable" case 0x05: msg = "Connection refused" case 0x06: msg = "TTL expired" case 0x07: msg = "Command not supported" case 0x08: msg = "Address type not supported" default: msg = "undefined" } return fmt.Sprintf("SOCKS5 request failed: %s (%d)", msg, e.Rep) } // socks5Outbound is a PluggableOutbound that connects to the target using // a SOCKS5 proxy server. // Since SOCKS5 supports using either IP or domain name as the target address, // it will ignore ResolveInfo in AddrEx and always only use Host. type socks5Outbound struct { Dialer *net.Dialer Addr string Username string Password string } func NewSOCKS5Outbound(addr, username, password string) PluggableOutbound { return &socks5Outbound{ Dialer: &net.Dialer{ Timeout: defaultDialerTimeout, }, Addr: addr, Username: username, Password: password, } } // dialAndNegotiate creates a new TCP connection to the SOCKS5 proxy server // and performs the negotiation. Returns an established connection ready to // handle requests, or an error if the process fails. func (o *socks5Outbound) dialAndNegotiate() (net.Conn, error) { conn, err := o.Dialer.Dial("tcp", o.Addr) if err != nil { return nil, err } if err := conn.SetDeadline(time.Now().Add(socks5NegotiationTimeout)); err != nil { _ = conn.Close() return nil, err } authMethods := []byte{socks5.MethodNone} if o.Username != "" && o.Password != "" { authMethods = append(authMethods, socks5.MethodUsernamePassword) } req := socks5.NewNegotiationRequest(authMethods) if _, err := req.WriteTo(conn); err != nil { _ = conn.Close() return nil, err } resp, err := socks5.NewNegotiationReplyFrom(conn) if err != nil { _ = conn.Close() return nil, err } if resp.Method == socks5.MethodUsernamePassword { upReq := socks5.NewUserPassNegotiationRequest([]byte(o.Username), []byte(o.Password)) if _, err := upReq.WriteTo(conn); err != nil { _ = conn.Close() return nil, err } upResp, err := socks5.NewUserPassNegotiationReplyFrom(conn) if err != nil { _ = conn.Close() return nil, err } if upResp.Status != socks5.UserPassStatusSuccess { _ = conn.Close() return nil, errSOCKS5AuthFailed } } else if resp.Method != socks5.MethodNone { // We only support none & username/password authentication methods. _ = conn.Close() return nil, errSOCKS5UnsupportedAuthMethod{resp.Method} } // Negotiation succeeded, reset the deadline. if err := conn.SetDeadline(time.Time{}); err != nil { _ = conn.Close() return nil, err } return conn, nil } // request sends a SOCKS5 request to the proxy server and returns the reply. // Note that it will return an error if the reply from the server indicates // a failure. func (o *socks5Outbound) request(conn net.Conn, req *socks5.Request) (*socks5.Reply, error) { if err := conn.SetDeadline(time.Now().Add(socks5RequestTimeout)); err != nil { return nil, err } if _, err := req.WriteTo(conn); err != nil { return nil, err } resp, err := socks5.NewReplyFrom(conn) if err != nil { return nil, err } if resp.Rep != socks5.RepSuccess { return nil, errSOCKS5RequestFailed{resp.Rep} } if err := conn.SetDeadline(time.Time{}); err != nil { return nil, err } return resp, nil } func (s *socks5Outbound) TCP(reqAddr *AddrEx) (net.Conn, error) { conn, err := s.dialAndNegotiate() if err != nil { return nil, err } atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr) req := socks5.NewRequest(socks5.CmdConnect, atyp, dstAddr, dstPort) if _, err := s.request(conn, req); err != nil { _ = conn.Close() return nil, err } return conn, nil } func (s *socks5Outbound) UDP(reqAddr *AddrEx) (UDPConn, error) { conn, err := s.dialAndNegotiate() if err != nil { return nil, err } atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr) req := socks5.NewRequest(socks5.CmdUDP, atyp, dstAddr, dstPort) resp, err := s.request(conn, req) if err != nil { _ = conn.Close() return nil, err } return newSOCKS5UDPConn(conn, resp.Address()) } type socks5UDPConn struct { tcpConn net.Conn udpConn net.Conn } func newSOCKS5UDPConn(tcpConn net.Conn, udpAddr string) (*socks5UDPConn, error) { udpConn, err := net.Dial("udp", udpAddr) if err != nil { return nil, err } sc := &socks5UDPConn{ tcpConn: tcpConn, udpConn: udpConn, } go sc.hold() return sc, nil } func (c *socks5UDPConn) hold() { _, _ = io.Copy(io.Discard, c.tcpConn) _ = c.tcpConn.Close() _ = c.udpConn.Close() } func (c *socks5UDPConn) ReadFrom(b []byte) (int, *AddrEx, error) { n, err := c.udpConn.Read(b) if err != nil { return 0, nil, err } d, err := socks5.NewDatagramFromBytes(b[:n]) if err != nil { return 0, nil, err } addr := socks5AddrToAddrEx(d.Atyp, d.DstAddr, d.DstPort) n = copy(b, d.Data) return n, addr, nil } func (c *socks5UDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { atyp, dstAddr, dstPort := addrExToSOCKS5Addr(addr) d := socks5.NewDatagram(atyp, dstAddr, dstPort, b) _, err := c.udpConn.Write(d.Bytes()) if err != nil { return 0, err } return len(b), nil } func (c *socks5UDPConn) Close() error { _ = c.tcpConn.Close() _ = c.udpConn.Close() return nil } func addrExToSOCKS5Addr(addr *AddrEx) (atyp byte, dstAddr, dstPort []byte) { // Host ip := net.ParseIP(addr.Host) if ip != nil { if ip.To4() != nil { atyp = socks5.ATYPIPv4 dstAddr = ip.To4() } else { atyp = socks5.ATYPIPv6 dstAddr = ip.To16() } } else { atyp = socks5.ATYPDomain dstAddr = []byte(addr.Host) } // Port dstPort = make([]byte, 2) binary.BigEndian.PutUint16(dstPort, addr.Port) return } func socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx { // Host var host string if atyp == socks5.ATYPIPv4 { host = net.IP(dstAddr).To4().String() } else if atyp == socks5.ATYPIPv6 { host = net.IP(dstAddr).To16().String() } else if atyp == socks5.ATYPDomain { // Need to strip the first byte which is the domain length. host = string(dstAddr[1:]) } // Port port := binary.BigEndian.Uint16(dstPort) return &AddrEx{ Host: host, Port: port, } } ================================================ FILE: extras/outbounds/speedtest/client.go ================================================ package speedtest import ( "fmt" "io" "net" "sync/atomic" "time" ) type Client struct { Conn net.Conn } // Download requests the server to send l bytes of data. // The callback function cb is called every second with the time since the last call, // and the number of bytes received in that time. func (c *Client) Download(l uint32, cb func(time.Duration, uint32, bool)) error { err := writeDownloadRequest(c.Conn, l) if err != nil { return err } ok, msg, err := readDownloadResponse(c.Conn) if err != nil { return err } if !ok { return fmt.Errorf("server rejected download request: %s", msg) } var counter uint32 stopChan := make(chan struct{}) defer close(stopChan) // Call the callback function every second, // with the time since the last call and the number of bytes received in that time. go func() { ticker := time.NewTicker(time.Second) defer ticker.Stop() t := time.Now() for { select { case <-stopChan: return case <-ticker.C: cb(time.Since(t), atomic.SwapUint32(&counter, 0), false) t = time.Now() } } }() buf := make([]byte, chunkSize) startTime := time.Now() remaining := l for remaining > 0 { n := remaining if n > chunkSize { n = chunkSize } rn, err := c.Conn.Read(buf[:n]) remaining -= uint32(rn) atomic.AddUint32(&counter, uint32(rn)) if err != nil && !(remaining == 0 && err == io.EOF) { return err } } // One last call to the callback function to report the total time and bytes received. cb(time.Since(startTime), l, true) return nil } // Upload requests the server to receive l bytes of data. // The callback function cb is called every second with the time since the last call, // and the number of bytes sent in that time. func (c *Client) Upload(l uint32, cb func(time.Duration, uint32, bool)) error { err := writeUploadRequest(c.Conn, l) if err != nil { return err } ok, msg, err := readUploadResponse(c.Conn) if err != nil { return err } if !ok { return fmt.Errorf("server rejected upload request: %s", msg) } var counter uint32 stopChan := make(chan struct{}) defer close(stopChan) // Call the callback function every second, // with the time since the last call and the number of bytes sent in that time. go func() { ticker := time.NewTicker(time.Second) defer ticker.Stop() t := time.Now() for { select { case <-stopChan: return case <-ticker.C: cb(time.Since(t), atomic.SwapUint32(&counter, 0), false) t = time.Now() } } }() buf := make([]byte, chunkSize) remaining := l for remaining > 0 { n := remaining if n > chunkSize { n = chunkSize } _, err := c.Conn.Write(buf[:n]) if err != nil { return err } remaining -= n atomic.AddUint32(&counter, n) } // Now we should receive the upload summary from the server. elapsed, received, err := readUploadSummary(c.Conn) if err != nil { return err } // One last call to the callback function to report the total time and bytes sent. cb(elapsed, received, true) return nil } ================================================ FILE: extras/outbounds/speedtest/protocol.go ================================================ package speedtest import ( "encoding/binary" "io" "time" ) const ( typeDownload = 0x1 typeUpload = 0x2 ) // DownloadRequest format: // 0x1 (byte) // Request data length (uint32 BE) func readDownloadRequest(r io.Reader) (uint32, error) { var l uint32 err := binary.Read(r, binary.BigEndian, &l) return l, err } func writeDownloadRequest(w io.Writer, l uint32) error { buf := make([]byte, 5) buf[0] = typeDownload binary.BigEndian.PutUint32(buf[1:], l) _, err := w.Write(buf) return err } // DownloadResponse format: // Status (byte, 0=ok, 1=error) // Message length (uint16 BE) // Message (bytes) func readDownloadResponse(r io.Reader) (bool, string, error) { var status [1]byte if _, err := io.ReadFull(r, status[:]); err != nil { return false, "", err } var msgLen uint16 if err := binary.Read(r, binary.BigEndian, &msgLen); err != nil { return false, "", err } // No message is fine if msgLen == 0 { return status[0] == 0, "", nil } msgBuf := make([]byte, msgLen) _, err := io.ReadFull(r, msgBuf) if err != nil { return false, "", err } return status[0] == 0, string(msgBuf), nil } func writeDownloadResponse(w io.Writer, ok bool, msg string) error { sz := 1 + 2 + len(msg) buf := make([]byte, sz) if ok { buf[0] = 0 } else { buf[0] = 1 } binary.BigEndian.PutUint16(buf[1:], uint16(len(msg))) copy(buf[3:], msg) _, err := w.Write(buf) return err } // UploadRequest format: // 0x2 (byte) // Upload data length (uint32 BE) func readUploadRequest(r io.Reader) (uint32, error) { var l uint32 err := binary.Read(r, binary.BigEndian, &l) return l, err } func writeUploadRequest(w io.Writer, l uint32) error { buf := make([]byte, 5) buf[0] = typeUpload binary.BigEndian.PutUint32(buf[1:], l) _, err := w.Write(buf) return err } // UploadResponse format: // Status (byte, 0=ok, 1=error) // Message length (uint16 BE) // Message (bytes) func readUploadResponse(r io.Reader) (bool, string, error) { var status [1]byte if _, err := io.ReadFull(r, status[:]); err != nil { return false, "", err } var msgLen uint16 if err := binary.Read(r, binary.BigEndian, &msgLen); err != nil { return false, "", err } // No message is fine if msgLen == 0 { return status[0] == 0, "", nil } msgBuf := make([]byte, msgLen) _, err := io.ReadFull(r, msgBuf) if err != nil { return false, "", err } return status[0] == 0, string(msgBuf), nil } func writeUploadResponse(w io.Writer, ok bool, msg string) error { sz := 1 + 2 + len(msg) buf := make([]byte, sz) if ok { buf[0] = 0 } else { buf[0] = 1 } binary.BigEndian.PutUint16(buf[1:], uint16(len(msg))) copy(buf[3:], msg) _, err := w.Write(buf) return err } // UploadSummary format: // Duration (in milliseconds, uint32 BE) // Received data length (uint32 BE) func readUploadSummary(r io.Reader) (time.Duration, uint32, error) { var duration uint32 if err := binary.Read(r, binary.BigEndian, &duration); err != nil { return 0, 0, err } var l uint32 if err := binary.Read(r, binary.BigEndian, &l); err != nil { return 0, 0, err } return time.Duration(duration) * time.Millisecond, l, nil } func writeUploadSummary(w io.Writer, duration time.Duration, l uint32) error { buf := make([]byte, 8) binary.BigEndian.PutUint32(buf, uint32(duration/time.Millisecond)) binary.BigEndian.PutUint32(buf[4:], l) _, err := w.Write(buf) return err } ================================================ FILE: extras/outbounds/speedtest/protocol_test.go ================================================ package speedtest import ( "bytes" "testing" "time" ) func TestReadDownloadRequest(t *testing.T) { tests := []struct { name string data []byte want uint32 wantErr bool }{ { name: "normal", data: []byte{0x0, 0x1, 0xBD, 0xC2}, want: 114114, wantErr: false, }, { name: "normal zero", data: []byte{0x0, 0x0, 0x0, 0x0}, want: 0, wantErr: false, }, { name: "incomplete", data: []byte{0x0, 0x1, 0x2}, want: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := bytes.NewReader(tt.data) got, err := readDownloadRequest(r) if (err != nil) != tt.wantErr { t.Errorf("readDownloadRequest() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("readDownloadRequest() got = %v, want %v", got, tt.want) } }) } } func TestWriteDownloadRequest(t *testing.T) { tests := []struct { name string l uint32 wantW string wantErr bool }{ { name: "normal", l: 78909912, wantW: "\x01\x04\xB4\x11\xD8", wantErr: false, }, { name: "normal zero", l: 0, wantW: "\x01\x00\x00\x00\x00", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} err := writeDownloadRequest(w, tt.l) if (err != nil) != tt.wantErr { t.Errorf("writeDownloadRequest() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); gotW != tt.wantW { t.Errorf("writeDownloadRequest() gotW = %v, want %v", gotW, tt.wantW) } }) } } func TestReadDownloadResponse(t *testing.T) { tests := []struct { name string data []byte want bool want1 string wantErr bool }{ { name: "normal ok", data: []byte{0x0, 0x0, 0x2, 0x41, 0x42}, want: true, want1: "AB", wantErr: false, }, { name: "normal ok no message", data: []byte{0x0, 0x0, 0x0, 0x0}, want: true, want1: "", wantErr: false, }, { name: "normal error", data: []byte{0x1, 0x0, 0x2, 0x43, 0x44}, want: false, want1: "CD", wantErr: false, }, { name: "incomplete", data: []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47}, want: false, want1: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := bytes.NewReader(tt.data) got, got1, err := readDownloadResponse(r) if (err != nil) != tt.wantErr { t.Errorf("readDownloadResponse() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("readDownloadResponse() got = %v, want %v", got, tt.want) } if got1 != tt.want1 { t.Errorf("readDownloadResponse() got1 = %v, want %v", got1, tt.want1) } }) } } func TestWriteDownloadResponse(t *testing.T) { type args struct { ok bool msg string } tests := []struct { name string args args wantW string wantErr bool }{ { name: "normal ok", args: args{ok: true, msg: "wahaha"}, wantW: "\x00\x00\x06wahaha", wantErr: false, }, { name: "normal error", args: args{ok: false, msg: "bullbull"}, wantW: "\x01\x00\x08bullbull", wantErr: false, }, { name: "empty ok", args: args{ok: true, msg: ""}, wantW: "\x00\x00\x00", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} err := writeDownloadResponse(w, tt.args.ok, tt.args.msg) if (err != nil) != tt.wantErr { t.Errorf("writeDownloadResponse() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); gotW != tt.wantW { t.Errorf("writeDownloadResponse() gotW = %v, want %v", gotW, tt.wantW) } }) } } func TestReadUploadRequest(t *testing.T) { tests := []struct { name string data []byte want uint32 wantErr bool }{ { name: "normal", data: []byte{0x0, 0x0, 0x26, 0xEE}, want: 9966, wantErr: false, }, { name: "normal zero", data: []byte{0x0, 0x0, 0x0, 0x0}, want: 0, wantErr: false, }, { name: "incomplete", data: []byte{0x1}, want: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := bytes.NewReader(tt.data) got, err := readUploadRequest(r) if (err != nil) != tt.wantErr { t.Errorf("readUploadRequest() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("readUploadRequest() got = %v, want %v", got, tt.want) } }) } } func TestWriteUploadRequest(t *testing.T) { tests := []struct { name string l uint32 wantW string wantErr bool }{ { name: "normal", l: 2291758882, wantW: "\x02\x88\x99\x77\x22", wantErr: false, }, { name: "normal zero", l: 0, wantW: "\x02\x00\x00\x00\x00", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} err := writeUploadRequest(w, tt.l) if (err != nil) != tt.wantErr { t.Errorf("writeUploadRequest() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); gotW != tt.wantW { t.Errorf("writeUploadRequest() gotW = %v, want %v", gotW, tt.wantW) } }) } } func TestReadUploadResponse(t *testing.T) { tests := []struct { name string data []byte want bool want1 string wantErr bool }{ { name: "normal ok", data: []byte{0x0, 0x0, 0x2, 0x41, 0x42}, want: true, want1: "AB", wantErr: false, }, { name: "normal ok no message", data: []byte{0x0, 0x0, 0x0}, want: true, want1: "", wantErr: false, }, { name: "normal error", data: []byte{0x1, 0x0, 0x2, 0x43, 0x44}, want: false, want1: "CD", wantErr: false, }, { name: "incomplete", data: []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47}, want: false, want1: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := bytes.NewReader(tt.data) got, got1, err := readUploadResponse(r) if (err != nil) != tt.wantErr { t.Errorf("readUploadResponse() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("readUploadResponse() got = %v, want %v", got, tt.want) } if got1 != tt.want1 { t.Errorf("readUploadResponse() got1 = %v, want %v", got1, tt.want1) } }) } } func TestWriteUploadResponse(t *testing.T) { type args struct { ok bool msg string } tests := []struct { name string args args wantW string wantErr bool }{ { name: "normal ok", args: args{ok: true, msg: "lul"}, wantW: "\x00\x00\x03lul", wantErr: false, }, { name: "normal error", args: args{ok: false, msg: "notforu"}, wantW: "\x01\x00\x07notforu", wantErr: false, }, { name: "empty ok", args: args{ok: true, msg: ""}, wantW: "\x00\x00\x00", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} err := writeUploadResponse(w, tt.args.ok, tt.args.msg) if (err != nil) != tt.wantErr { t.Errorf("writeUploadResponse() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); gotW != tt.wantW { t.Errorf("writeUploadResponse() gotW = %v, want %v", gotW, tt.wantW) } }) } } func TestReadUploadSummary(t *testing.T) { tests := []struct { name string data []byte want time.Duration want1 uint32 wantErr bool }{ { name: "normal", data: []byte{0x0, 0x0, 0x14, 0x6E, 0x0, 0x26, 0x25, 0xA0}, want: 5230 * time.Millisecond, want1: 2500000, wantErr: false, }, { name: "zero", data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, want: 0, want1: 0, wantErr: false, }, { name: "incomplete", data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, want: 0, want1: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := bytes.NewReader(tt.data) got, got1, err := readUploadSummary(r) if (err != nil) != tt.wantErr { t.Errorf("readUploadSummary() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("readUploadSummary() got = %v, want %v", got, tt.want) } if got1 != tt.want1 { t.Errorf("readUploadSummary() got1 = %v, want %v", got1, tt.want1) } }) } } func TestWriteUploadSummary(t *testing.T) { type args struct { duration time.Duration l uint32 } tests := []struct { name string args args wantW string wantErr bool }{ { name: "normal", args: args{duration: 5230 * time.Millisecond, l: 2500000}, wantW: "\x00\x00\x14\x6E\x00\x26\x25\xA0", wantErr: false, }, { name: "zero", args: args{duration: 0, l: 0}, wantW: "\x00\x00\x00\x00\x00\x00\x00\x00", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} err := writeUploadSummary(w, tt.args.duration, tt.args.l) if (err != nil) != tt.wantErr { t.Errorf("writeUploadSummary() error = %v, wantErr %v", err, tt.wantErr) return } if gotW := w.String(); gotW != tt.wantW { t.Errorf("writeUploadSummary() gotW = %v, want %v", gotW, tt.wantW) } }) } } ================================================ FILE: extras/outbounds/speedtest/server.go ================================================ package speedtest import ( "crypto/rand" "fmt" "io" "net" "time" ) const ( chunkSize = 64 * 1024 ) // NewServerConn creates a new "pseudo" connection that implements the speed test protocol. // It's called "pseudo" because it's not a real TCP connection - everything is done in memory. func NewServerConn() net.Conn { rConn, iConn := net.Pipe() // return conn & internal conn // Start the server logic go server(iConn) return rConn } func server(conn net.Conn) error { defer conn.Close() // First byte determines the request type var typ [1]byte if _, err := io.ReadFull(conn, typ[:]); err != nil { return err } switch typ[0] { case typeDownload: return handleDownload(conn) case typeUpload: return handleUpload(conn) default: return fmt.Errorf("unknown request type: %d", typ[0]) } } // handleDownload reads the download request and sends the requested amount of data. func handleDownload(conn net.Conn) error { l, err := readDownloadRequest(conn) if err != nil { return err } err = writeDownloadResponse(conn, true, "OK") if err != nil { return err } buf := make([]byte, chunkSize) // Fill the buffer with random data. // For now, we only do it once and repeat the same data for performance reasons. _, err = rand.Read(buf) if err != nil { return err } remaining := l for remaining > 0 { n := remaining if n > chunkSize { n = chunkSize } _, err := conn.Write(buf[:n]) if err != nil { return err } remaining -= n } return nil } // handleUpload reads the upload request, reads & discards the requested amount of data, // and sends the upload summary. func handleUpload(conn net.Conn) error { l, err := readUploadRequest(conn) if err != nil { return err } err = writeUploadResponse(conn, true, "OK") if err != nil { return err } buf := make([]byte, chunkSize) startTime := time.Now() remaining := l for remaining > 0 { n := remaining if n > chunkSize { n = chunkSize } rn, err := conn.Read(buf[:n]) remaining -= uint32(rn) if err != nil && !(remaining == 0 && err == io.EOF) { return err } } return writeUploadSummary(conn, time.Since(startTime), l) } ================================================ FILE: extras/outbounds/speedtest.go ================================================ package outbounds import ( "net" "github.com/apernet/hysteria/extras/v2/outbounds/speedtest" ) const ( SpeedtestDest = "@SpeedTest" ) // speedtestHandler is a PluggableOutbound that handles speed test requests. // It's used to intercept speed test requests and return a pseudo connection that // implements the speed test protocol. type speedtestHandler struct { Next PluggableOutbound } func NewSpeedtestHandler(next PluggableOutbound) PluggableOutbound { return &speedtestHandler{ Next: next, } } func (s *speedtestHandler) TCP(reqAddr *AddrEx) (net.Conn, error) { if reqAddr.Host == SpeedtestDest { return speedtest.NewServerConn(), nil } else { return s.Next.TCP(reqAddr) } } func (s *speedtestHandler) UDP(reqAddr *AddrEx) (UDPConn, error) { return s.Next.UDP(reqAddr) } ================================================ FILE: extras/outbounds/utils.go ================================================ package outbounds import "net" // splitIPv4IPv6 gets the first IPv4 and IPv6 address from a list of IP addresses. // Both of the return values can be nil when no IPv4 or IPv6 address is found. func splitIPv4IPv6(ips []net.IP) (ipv4, ipv6 net.IP) { for _, ip := range ips { if ip.To4() != nil { if ipv4 == nil { ipv4 = ip } } else { if ipv6 == nil { ipv6 = ip } } if ipv4 != nil && ipv6 != nil { // We have everything we need. break } } return } // tryParseIP tries to parse the host string in the AddrEx as an IP address. // If the host is indeed an IP address, it will fill the ResolveInfo with the // parsed IP address and return true. Otherwise, it will return false. func tryParseIP(addr *AddrEx) bool { if ip := net.ParseIP(addr.Host); ip != nil { addr.ResolveInfo = &ResolveInfo{} if ip.To4() != nil { addr.ResolveInfo.IPv4 = ip } else { addr.ResolveInfo.IPv6 = ip } return true } return false } ================================================ FILE: extras/outbounds/utils_test.go ================================================ package outbounds import ( "net" "testing" "github.com/stretchr/testify/assert" ) func TestSplitIPv4IPv6(t *testing.T) { type args struct { ips []net.IP } tests := []struct { name string args args wantIpv4 net.IP wantIpv6 net.IP }{ { name: "IPv4 only", args: args{ ips: []net.IP{ net.ParseIP("4.5.6.7"), net.ParseIP("9.9.9.9"), }, }, wantIpv4: net.ParseIP("4.5.6.7"), wantIpv6: nil, }, { name: "IPv6 only", args: args{ ips: []net.IP{ net.ParseIP("2001:db8::68"), net.ParseIP("2001:db8::69"), }, }, wantIpv4: nil, wantIpv6: net.ParseIP("2001:db8::68"), }, { name: "Both 1", args: args{ ips: []net.IP{ net.ParseIP("2001:db8::68"), net.ParseIP("2001:db8::69"), net.ParseIP("4.5.6.7"), net.ParseIP("9.9.9.9"), }, }, wantIpv4: net.ParseIP("4.5.6.7"), wantIpv6: net.ParseIP("2001:db8::68"), }, { name: "Both 2", args: args{ ips: []net.IP{ net.ParseIP("2001:db8::69"), net.ParseIP("9.9.9.9"), net.ParseIP("2001:db8::68"), net.ParseIP("4.5.6.7"), }, }, wantIpv4: net.ParseIP("9.9.9.9"), wantIpv6: net.ParseIP("2001:db8::69"), }, { name: "Empty", args: args{ ips: []net.IP{}, }, wantIpv4: nil, wantIpv6: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotIpv4, gotIpv6 := splitIPv4IPv6(tt.args.ips) assert.Equalf(t, tt.wantIpv4, gotIpv4, "splitIPv4IPv6(%v)", tt.args.ips) assert.Equalf(t, tt.wantIpv6, gotIpv6, "splitIPv4IPv6(%v)", tt.args.ips) }) } } ================================================ FILE: extras/sniff/.mockery.yaml ================================================ with-expecter: true dir: . outpkg: sniff packages: github.com/apernet/quic-go: interfaces: Stream: config: mockname: mockStream replace-type: # internal package alias dirty fix - github.com/apernet/quic-go/internal/protocol=github.com/apernet/quic-go - github.com/apernet/quic-go/internal/qerr=github.com/apernet/quic-go ================================================ FILE: extras/sniff/internal/quic/LICENSE ================================================ Author:: Cuong Manh Le Copyright:: Copyright (c) 2023, Cuong Manh Le All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the @organization@ nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: extras/sniff/internal/quic/README.md ================================================ The code here is from https://github.com/cuonglm/quicsni with various modifications. ================================================ FILE: extras/sniff/internal/quic/header.go ================================================ package quic import ( "bytes" "encoding/binary" "errors" "io" "github.com/apernet/quic-go/quicvarint" ) // The Header represents a QUIC header. type Header struct { Type uint8 Version uint32 SrcConnectionID []byte DestConnectionID []byte Length int64 Token []byte } // ParseInitialHeader parses the initial packet of a QUIC connection, // return the initial header and number of bytes read so far. func ParseInitialHeader(data []byte) (*Header, int64, error) { br := bytes.NewReader(data) hdr, err := parseLongHeader(br) if err != nil { return nil, 0, err } n := int64(len(data) - br.Len()) return hdr, n, nil } func parseLongHeader(b *bytes.Reader) (*Header, error) { typeByte, err := b.ReadByte() if err != nil { return nil, err } h := &Header{} ver, err := beUint32(b) if err != nil { return nil, err } h.Version = ver if h.Version != 0 && typeByte&0x40 == 0 { return nil, errors.New("not a QUIC packet") } destConnIDLen, err := b.ReadByte() if err != nil { return nil, err } h.DestConnectionID = make([]byte, int(destConnIDLen)) if err := readConnectionID(b, h.DestConnectionID); err != nil { return nil, err } srcConnIDLen, err := b.ReadByte() if err != nil { return nil, err } h.SrcConnectionID = make([]byte, int(srcConnIDLen)) if err := readConnectionID(b, h.SrcConnectionID); err != nil { return nil, err } initialPacketType := byte(0b00) if h.Version == V2 { initialPacketType = 0b01 } if (typeByte >> 4 & 0b11) == initialPacketType { tokenLen, err := quicvarint.Read(b) if err != nil { return nil, err } if tokenLen > uint64(b.Len()) { return nil, io.EOF } h.Token = make([]byte, tokenLen) if _, err := io.ReadFull(b, h.Token); err != nil { return nil, err } } pl, err := quicvarint.Read(b) if err != nil { return nil, err } h.Length = int64(pl) return h, err } func readConnectionID(r io.Reader, cid []byte) error { _, err := io.ReadFull(r, cid) if err == io.ErrUnexpectedEOF { return io.EOF } return nil } func beUint32(r io.Reader) (uint32, error) { b := make([]byte, 4) if _, err := io.ReadFull(r, b); err != nil { return 0, err } return binary.BigEndian.Uint32(b), nil } ================================================ FILE: extras/sniff/internal/quic/packet_protector.go ================================================ package quic import ( "crypto" "crypto/aes" "crypto/cipher" "crypto/sha256" "crypto/tls" "encoding/binary" "errors" "fmt" "hash" "golang.org/x/crypto/chacha20" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/hkdf" ) // NewProtectionKey creates a new ProtectionKey. func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) { return newProtectionKey(suite, secret, v) } // NewInitialProtectionKey is like NewProtectionKey, but the returned protection key // is used for encrypt/decrypt Initial Packet only. // // See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) { return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v) } // NewPacketProtector creates a new PacketProtector. func NewPacketProtector(key *ProtectionKey) *PacketProtector { return &PacketProtector{key: key} } // PacketProtector is used for protecting a QUIC packet. // // See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection type PacketProtector struct { key *ProtectionKey } // UnProtect decrypts a QUIC packet. func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) { if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 { return nil, errors.New("packet with long header is too small") } // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample sampleOffset := pnOffset + 4 sample := packet[sampleOffset : sampleOffset+16] // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati mask := pp.key.headerProtection(sample) if isLongHeader(packet[0]) { // Long header: 4 bits masked packet[0] ^= mask[0] & 0x0f } else { // Short header: 5 bits masked packet[0] ^= mask[0] & 0x1f } pnLen := packet[0]&0x3 + 1 pn := int64(0) for i := uint8(0); i < pnLen; i++ { packet[pnOffset:][i] ^= mask[1+i] pn = (pn << 8) | int64(packet[pnOffset:][i]) } pn = decodePacketNumber(pnMax, pn, pnLen) hdr := packet[:pnOffset+int64(pnLen)] payload := packet[pnOffset:][pnLen:] dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr) if err != nil { return nil, fmt.Errorf("decryption failed: %w", err) } return dec, nil } // ProtectionKey is the key used to protect a QUIC packet. type ProtectionKey struct { aead cipher.AEAD headerProtection func(sample []byte) (mask []byte) iv []byte } // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage // // "The 62 bits of the reconstructed QUIC packet number in network byte order are // left-padded with zeros to the size of the IV. The exclusive OR of the padded // packet number and the IV forms the AEAD nonce." func (pk *ProtectionKey) nonce(pn int64) []byte { nonce := make([]byte, len(pk.iv)) binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn)) for i := range pk.iv { nonce[i] ^= pk.iv[i] } return nonce } func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) { switch suite { case tls.TLS_AES_128_GCM_SHA256: key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16) c, err := aes.NewCipher(key) if err != nil { panic(err) } aead, err := cipher.NewGCM(c) if err != nil { panic(err) } iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize()) hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16) hp, err := aes.NewCipher(hpKey) if err != nil { panic(err) } k := &ProtectionKey{} k.aead = aead // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection k.headerProtection = func(sample []byte) []byte { mask := make([]byte, hp.BlockSize()) hp.Encrypt(mask, sample) return mask } k.iv = iv return k, nil case tls.TLS_CHACHA20_POLY1305_SHA256: key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize) aead, err := chacha20poly1305.New(key) if err != nil { return nil, err } iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize()) hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize) k := &ProtectionKey{} k.aead = aead // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote k.headerProtection = func(sample []byte) []byte { nonce := sample[4:16] c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce) if err != nil { panic(err) } c.SetCounter(binary.LittleEndian.Uint32(sample[:4])) mask := make([]byte, 5) c.XORKeyStream(mask, mask) return mask } k.iv = iv return k, nil } return nil, errors.New("not supported cipher suite") } // decodePacketNumber decode the packet number after header protection removed. // // See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a func decodePacketNumber(largest, truncated int64, nbits uint8) int64 { expected := largest + 1 win := int64(1 << (nbits * 8)) hwin := win / 2 mask := win - 1 candidate := (expected &^ mask) | truncated switch { case candidate <= expected-hwin && candidate < (1<<62)-win: return candidate + win case candidate > expected+hwin && candidate >= win: return candidate - win } return candidate } // Copied from crypto/tls/key_schedule.go. func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte { var hkdfLabel cryptobyte.Builder hkdfLabel.AddUint16(uint16(length)) hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes([]byte("tls13 ")) b.AddBytes([]byte(label)) }) hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(context) }) out := make([]byte, length) n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out) if err != nil || n != length { panic("quic: HKDF-Expand-Label invocation failed unexpectedly") } return out } ================================================ FILE: extras/sniff/internal/quic/packet_protector_test.go ================================================ package quic import ( "bytes" "crypto" "crypto/tls" "encoding/hex" "strings" "testing" "unicode" "golang.org/x/crypto/hkdf" ) func TestInitialPacketProtector_UnProtect(t *testing.T) { // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial protect := mustHexDecodeString(` c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7 6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498 44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5 be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e fd8142fafc0f76 `) unProtect := mustHexDecodeString(` 02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739 88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94 0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00 020304 `) connID := mustHexDecodeString(`8394c8f03e515708`) packet := append([]byte{}, protect...) hdr, offset, err := ParseInitialHeader(packet) if err != nil { t.Fatal(err) } initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version)) serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size()) key, err := NewInitialProtectionKey(serverSecret, hdr.Version) if err != nil { t.Fatal(err) } pp := NewPacketProtector(key) got, err := pp.UnProtect(protect, offset, 1) if err != nil { t.Fatal(err) } if !bytes.Equal(got, unProtect) { t.Error("UnProtect returns wrong result") } } func TestPacketProtectorShortHeader_UnProtect(t *testing.T) { // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`) unProtect := mustHexDecodeString(`01`) hdr := mustHexDecodeString(`4200bff4`) secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`) k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1) if err != nil { t.Fatal(err) } pnLen := int(hdr[0]&0x03) + 1 offset := len(hdr) - pnLen pp := NewPacketProtector(k) got, err := pp.UnProtect(protect, int64(offset), 654360564) if err != nil { t.Fatal(err) } if !bytes.Equal(got, unProtect) { t.Error("UnProtect returns wrong result") } } func mustHexDecodeString(s string) []byte { b, err := hex.DecodeString(normalizeHex(s)) if err != nil { panic(err) } return b } func normalizeHex(s string) string { return strings.Map(func(c rune) rune { if unicode.IsSpace(c) { return -1 } return c }, s) } ================================================ FILE: extras/sniff/internal/quic/payload.go ================================================ package quic import ( "bytes" "crypto" "errors" "fmt" "io" "sort" "github.com/apernet/quic-go/quicvarint" "golang.org/x/crypto/hkdf" ) func ReadCryptoPayload(packet []byte) ([]byte, error) { hdr, offset, err := ParseInitialHeader(packet) if err != nil { return nil, err } // Some sanity checks if hdr.Version != V1 && hdr.Version != V2 { return nil, fmt.Errorf("unsupported version: %x", hdr.Version) } if offset == 0 || hdr.Length == 0 { return nil, errors.New("invalid packet") } initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version)) clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size()) key, err := NewInitialProtectionKey(clientSecret, hdr.Version) if err != nil { return nil, fmt.Errorf("NewInitialProtectionKey: %w", err) } pp := NewPacketProtector(key) // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial // // "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2" if int64(len(packet)) < offset+hdr.Length { return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length) } unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2) if err != nil { return nil, err } frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload)) if err != nil { return nil, err } data := assembleCryptoFrames(frs) if data == nil { return nil, errors.New("unable to assemble crypto frames") } return data, nil } const ( paddingFrameType = 0x00 pingFrameType = 0x01 cryptoFrameType = 0x06 ) type cryptoFrame struct { Offset int64 Data []byte } func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) { var frames []cryptoFrame for r.Len() > 0 { typ, err := quicvarint.Read(r) if err != nil { return nil, err } if typ == paddingFrameType || typ == pingFrameType { continue } if typ != cryptoFrameType { return nil, fmt.Errorf("encountered unexpected frame type: %d", typ) } var frame cryptoFrame offset, err := quicvarint.Read(r) if err != nil { return nil, err } frame.Offset = int64(offset) dataLen, err := quicvarint.Read(r) if err != nil { return nil, err } frame.Data = make([]byte, dataLen) if _, err := io.ReadFull(r, frame.Data); err != nil { return nil, err } frames = append(frames, frame) } return frames, nil } // assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible). // It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous. func assembleCryptoFrames(frames []cryptoFrame) []byte { if len(frames) == 0 { return nil } if len(frames) == 1 { return frames[0].Data } // sort the frames by offset sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset }) // check if the frames are contiguous for i := 1; i < len(frames); i++ { if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) { return nil } } // concatenate the frames data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data))) for _, frame := range frames { copy(data[frame.Offset:], frame.Data) } return data } ================================================ FILE: extras/sniff/internal/quic/quic.go ================================================ package quic const ( V1 uint32 = 0x1 V2 uint32 = 0x6b3343cf hkdfLabelKeyV1 = "quic key" hkdfLabelKeyV2 = "quicv2 key" hkdfLabelIVV1 = "quic iv" hkdfLabelIVV2 = "quicv2 iv" hkdfLabelHPV1 = "quic hp" hkdfLabelHPV2 = "quicv2 hp" ) var ( quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99} // https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a} // https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2 quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9} ) // isLongHeader reports whether b is the first byte of a long header packet. func isLongHeader(b byte) bool { return b&0x80 > 0 } func getSalt(v uint32) []byte { switch v { case V1: return quicSaltV1 case V2: return quicSaltV2 } return quicSaltOld } func keyLabel(v uint32) string { kl := hkdfLabelKeyV1 if v == V2 { kl = hkdfLabelKeyV2 } return kl } func ivLabel(v uint32) string { ivl := hkdfLabelIVV1 if v == V2 { ivl = hkdfLabelIVV2 } return ivl } func headerProtectionLabel(v uint32) string { if v == V2 { return hkdfLabelHPV2 } return hkdfLabelHPV1 } ================================================ FILE: extras/sniff/mock_Stream.go ================================================ // Code generated by mockery v2.43.0. DO NOT EDIT. package sniff import ( context "context" qerr "github.com/apernet/quic-go" mock "github.com/stretchr/testify/mock" time "time" ) // mockStream is an autogenerated mock type for the Stream type type mockStream struct { mock.Mock } type mockStream_Expecter struct { mock *mock.Mock } func (_m *mockStream) EXPECT() *mockStream_Expecter { return &mockStream_Expecter{mock: &_m.Mock} } // CancelRead provides a mock function with given fields: _a0 func (_m *mockStream) CancelRead(_a0 qerr.StreamErrorCode) { _m.Called(_a0) } // mockStream_CancelRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelRead' type mockStream_CancelRead_Call struct { *mock.Call } // CancelRead is a helper method to define mock.On call // - _a0 qerr.StreamErrorCode func (_e *mockStream_Expecter) CancelRead(_a0 interface{}) *mockStream_CancelRead_Call { return &mockStream_CancelRead_Call{Call: _e.mock.On("CancelRead", _a0)} } func (_c *mockStream_CancelRead_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelRead_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(qerr.StreamErrorCode)) }) return _c } func (_c *mockStream_CancelRead_Call) Return() *mockStream_CancelRead_Call { _c.Call.Return() return _c } func (_c *mockStream_CancelRead_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelRead_Call { _c.Call.Return(run) return _c } // CancelWrite provides a mock function with given fields: _a0 func (_m *mockStream) CancelWrite(_a0 qerr.StreamErrorCode) { _m.Called(_a0) } // mockStream_CancelWrite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelWrite' type mockStream_CancelWrite_Call struct { *mock.Call } // CancelWrite is a helper method to define mock.On call // - _a0 qerr.StreamErrorCode func (_e *mockStream_Expecter) CancelWrite(_a0 interface{}) *mockStream_CancelWrite_Call { return &mockStream_CancelWrite_Call{Call: _e.mock.On("CancelWrite", _a0)} } func (_c *mockStream_CancelWrite_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelWrite_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(qerr.StreamErrorCode)) }) return _c } func (_c *mockStream_CancelWrite_Call) Return() *mockStream_CancelWrite_Call { _c.Call.Return() return _c } func (_c *mockStream_CancelWrite_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelWrite_Call { _c.Call.Return(run) return _c } // Close provides a mock function with given fields: func (_m *mockStream) Close() error { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // mockStream_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' type mockStream_Close_Call struct { *mock.Call } // Close is a helper method to define mock.On call func (_e *mockStream_Expecter) Close() *mockStream_Close_Call { return &mockStream_Close_Call{Call: _e.mock.On("Close")} } func (_c *mockStream_Close_Call) Run(run func()) *mockStream_Close_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *mockStream_Close_Call) Return(_a0 error) *mockStream_Close_Call { _c.Call.Return(_a0) return _c } func (_c *mockStream_Close_Call) RunAndReturn(run func() error) *mockStream_Close_Call { _c.Call.Return(run) return _c } // Context provides a mock function with given fields: func (_m *mockStream) Context() context.Context { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Context") } var r0 context.Context if rf, ok := ret.Get(0).(func() context.Context); ok { r0 = rf() } else { if ret.Get(0) != nil { r0 = ret.Get(0).(context.Context) } } return r0 } // mockStream_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' type mockStream_Context_Call struct { *mock.Call } // Context is a helper method to define mock.On call func (_e *mockStream_Expecter) Context() *mockStream_Context_Call { return &mockStream_Context_Call{Call: _e.mock.On("Context")} } func (_c *mockStream_Context_Call) Run(run func()) *mockStream_Context_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *mockStream_Context_Call) Return(_a0 context.Context) *mockStream_Context_Call { _c.Call.Return(_a0) return _c } func (_c *mockStream_Context_Call) RunAndReturn(run func() context.Context) *mockStream_Context_Call { _c.Call.Return(run) return _c } // Read provides a mock function with given fields: p func (_m *mockStream) Read(p []byte) (int, error) { ret := _m.Called(p) if len(ret) == 0 { panic("no return value specified for Read") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { return rf(p) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(p) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) error); ok { r1 = rf(p) } else { r1 = ret.Error(1) } return r0, r1 } // mockStream_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' type mockStream_Read_Call struct { *mock.Call } // Read is a helper method to define mock.On call // - p []byte func (_e *mockStream_Expecter) Read(p interface{}) *mockStream_Read_Call { return &mockStream_Read_Call{Call: _e.mock.On("Read", p)} } func (_c *mockStream_Read_Call) Run(run func(p []byte)) *mockStream_Read_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *mockStream_Read_Call) Return(n int, err error) *mockStream_Read_Call { _c.Call.Return(n, err) return _c } func (_c *mockStream_Read_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Read_Call { _c.Call.Return(run) return _c } // SetDeadline provides a mock function with given fields: t func (_m *mockStream) SetDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // mockStream_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline' type mockStream_SetDeadline_Call struct { *mock.Call } // SetDeadline is a helper method to define mock.On call // - t time.Time func (_e *mockStream_Expecter) SetDeadline(t interface{}) *mockStream_SetDeadline_Call { return &mockStream_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)} } func (_c *mockStream_SetDeadline_Call) Run(run func(t time.Time)) *mockStream_SetDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *mockStream_SetDeadline_Call) Return(_a0 error) *mockStream_SetDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *mockStream_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetDeadline_Call { _c.Call.Return(run) return _c } // SetReadDeadline provides a mock function with given fields: t func (_m *mockStream) SetReadDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetReadDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // mockStream_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline' type mockStream_SetReadDeadline_Call struct { *mock.Call } // SetReadDeadline is a helper method to define mock.On call // - t time.Time func (_e *mockStream_Expecter) SetReadDeadline(t interface{}) *mockStream_SetReadDeadline_Call { return &mockStream_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)} } func (_c *mockStream_SetReadDeadline_Call) Run(run func(t time.Time)) *mockStream_SetReadDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *mockStream_SetReadDeadline_Call) Return(_a0 error) *mockStream_SetReadDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *mockStream_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetReadDeadline_Call { _c.Call.Return(run) return _c } // SetWriteDeadline provides a mock function with given fields: t func (_m *mockStream) SetWriteDeadline(t time.Time) error { ret := _m.Called(t) if len(ret) == 0 { panic("no return value specified for SetWriteDeadline") } var r0 error if rf, ok := ret.Get(0).(func(time.Time) error); ok { r0 = rf(t) } else { r0 = ret.Error(0) } return r0 } // mockStream_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline' type mockStream_SetWriteDeadline_Call struct { *mock.Call } // SetWriteDeadline is a helper method to define mock.On call // - t time.Time func (_e *mockStream_Expecter) SetWriteDeadline(t interface{}) *mockStream_SetWriteDeadline_Call { return &mockStream_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)} } func (_c *mockStream_SetWriteDeadline_Call) Run(run func(t time.Time)) *mockStream_SetWriteDeadline_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(time.Time)) }) return _c } func (_c *mockStream_SetWriteDeadline_Call) Return(_a0 error) *mockStream_SetWriteDeadline_Call { _c.Call.Return(_a0) return _c } func (_c *mockStream_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetWriteDeadline_Call { _c.Call.Return(run) return _c } // StreamID provides a mock function with given fields: func (_m *mockStream) StreamID() qerr.StreamID { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for StreamID") } var r0 qerr.StreamID if rf, ok := ret.Get(0).(func() qerr.StreamID); ok { r0 = rf() } else { r0 = ret.Get(0).(qerr.StreamID) } return r0 } // mockStream_StreamID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamID' type mockStream_StreamID_Call struct { *mock.Call } // StreamID is a helper method to define mock.On call func (_e *mockStream_Expecter) StreamID() *mockStream_StreamID_Call { return &mockStream_StreamID_Call{Call: _e.mock.On("StreamID")} } func (_c *mockStream_StreamID_Call) Run(run func()) *mockStream_StreamID_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } func (_c *mockStream_StreamID_Call) Return(_a0 qerr.StreamID) *mockStream_StreamID_Call { _c.Call.Return(_a0) return _c } func (_c *mockStream_StreamID_Call) RunAndReturn(run func() qerr.StreamID) *mockStream_StreamID_Call { _c.Call.Return(run) return _c } // Write provides a mock function with given fields: p func (_m *mockStream) Write(p []byte) (int, error) { ret := _m.Called(p) if len(ret) == 0 { panic("no return value specified for Write") } var r0 int var r1 error if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { return rf(p) } if rf, ok := ret.Get(0).(func([]byte) int); ok { r0 = rf(p) } else { r0 = ret.Get(0).(int) } if rf, ok := ret.Get(1).(func([]byte) error); ok { r1 = rf(p) } else { r1 = ret.Error(1) } return r0, r1 } // mockStream_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' type mockStream_Write_Call struct { *mock.Call } // Write is a helper method to define mock.On call // - p []byte func (_e *mockStream_Expecter) Write(p interface{}) *mockStream_Write_Call { return &mockStream_Write_Call{Call: _e.mock.On("Write", p)} } func (_c *mockStream_Write_Call) Run(run func(p []byte)) *mockStream_Write_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].([]byte)) }) return _c } func (_c *mockStream_Write_Call) Return(n int, err error) *mockStream_Write_Call { _c.Call.Return(n, err) return _c } func (_c *mockStream_Write_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Write_Call { _c.Call.Return(run) return _c } // newMockStream creates a new instance of mockStream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockStream(t interface { mock.TestingT Cleanup(func()) }) *mockStream { mock := &mockStream{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: extras/sniff/sniff.go ================================================ package sniff import ( "bufio" "io" "net" "net/http" "strconv" "strings" "time" "github.com/apernet/quic-go" utls "github.com/refraction-networking/utls" "github.com/apernet/hysteria/core/v2/server" quicInternal "github.com/apernet/hysteria/extras/v2/sniff/internal/quic" "github.com/apernet/hysteria/extras/v2/utils" ) const ( sniffDefaultTimeout = 4 * time.Second ) var _ server.RequestHook = (*Sniffer)(nil) // Sniffer is a server core RequestHook that performs packet inspection and possibly // rewrites the request address based on what's in the protocol header. // This is mainly for inbounds that inherently cannot get domain information (e.g. TUN), // in which case sniffing can restore the domains and apply ACLs correctly. // Currently supports HTTP, HTTPS (TLS) and QUIC. type Sniffer struct { Timeout time.Duration RewriteDomain bool // Whether to rewrite the address even when it's already a domain TCPPorts utils.PortUnion UDPPorts utils.PortUnion } func (h *Sniffer) isDomain(addr string) bool { host, _, err := net.SplitHostPort(addr) if err != nil { return false } return net.ParseIP(host) == nil } func (h *Sniffer) isHTTP(buf []byte) bool { if len(buf) < 3 { return false } // First 3 bytes should be English letters (whatever HTTP method) for _, b := range buf[:3] { if (b < 'A' || b > 'Z') && (b < 'a' || b > 'z') { return false } } return true } func (h *Sniffer) isTLS(buf []byte) bool { if len(buf) < 3 { return false } return buf[0] >= 0x16 && buf[0] <= 0x17 && buf[1] == 0x03 && buf[2] <= 0x09 } func (h *Sniffer) Check(isUDP bool, reqAddr string) bool { // @ means it's internal (e.g. speed test) if strings.HasPrefix(reqAddr, "@") { return false } host, port, err := net.SplitHostPort(reqAddr) if err != nil { return false } if !h.RewriteDomain && net.ParseIP(host) == nil { // Is a domain and domain rewriting is disabled return false } portNum, err := strconv.Atoi(port) if err != nil { return false } if isUDP { return h.UDPPorts == nil || h.UDPPorts.Contains(uint16(portNum)) } else { return h.TCPPorts == nil || h.TCPPorts.Contains(uint16(portNum)) } } func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) { var err error if h.Timeout == 0 { err = stream.SetReadDeadline(time.Now().Add(sniffDefaultTimeout)) } else { err = stream.SetReadDeadline(time.Now().Add(h.Timeout)) } if err != nil { return nil, err } // Make sure to reset the deadline after sniffing defer stream.SetReadDeadline(time.Time{}) // Read 3 bytes to determine the protocol pre := make([]byte, 3) n, err := io.ReadFull(stream, pre) if err != nil { // Not enough within the timeout, just return what we have return pre[:n], nil } if h.isHTTP(pre) { // HTTP tr := &teeReader{Stream: stream, Pre: pre} req, _ := http.ReadRequest(bufio.NewReader(tr)) if req != nil && req.Host != "" { // req.Host can be host:port, in which case we need to extract the host part host, _, err := net.SplitHostPort(req.Host) if err != nil { // No port, just use the whole string host = req.Host } _, port, err := net.SplitHostPort(*reqAddr) if err != nil { return nil, err } *reqAddr = net.JoinHostPort(host, port) } return tr.Buffer(), nil } else if h.isTLS(pre) { // TLS // Need to read 2 more bytes (content length) pre = append(pre, make([]byte, 2)...) n, err = io.ReadFull(stream, pre[3:]) if err != nil { // Not enough within the timeout, just return what we have return pre[:3+n], nil } contentLength := int(pre[3])<<8 | int(pre[4]) pre = append(pre, make([]byte, contentLength)...) n, err = io.ReadFull(stream, pre[5:]) if err != nil { // Not enough within the timeout, just return what we have return pre[:5+n], nil } clientHello := utls.UnmarshalClientHello(pre[5:]) if clientHello != nil && clientHello.ServerName != "" { _, port, err := net.SplitHostPort(*reqAddr) if err != nil { return nil, err } *reqAddr = net.JoinHostPort(clientHello.ServerName, port) } return pre, nil } else { // Unrecognized protocol, just return what we have return pre, nil } } func (h *Sniffer) UDP(data []byte, reqAddr *string) error { pl, err := quicInternal.ReadCryptoPayload(data) if err != nil || len(pl) < 4 || pl[0] != 0x01 { // Unrecognized protocol, incomplete payload or not a client hello return nil } clientHello := utls.UnmarshalClientHello(pl) if clientHello != nil && clientHello.ServerName != "" { _, port, err := net.SplitHostPort(*reqAddr) if err != nil { return err } *reqAddr = net.JoinHostPort(clientHello.ServerName, port) } return nil } type teeReader struct { Stream quic.Stream Pre []byte buf []byte } func (c *teeReader) Read(b []byte) (n int, err error) { if len(c.Pre) > 0 { n = copy(b, c.Pre) c.Pre = c.Pre[n:] c.buf = append(c.buf, b[:n]...) return n, nil } n, err = c.Stream.Read(b) if n > 0 { c.buf = append(c.buf, b[:n]...) } return n, err } func (c *teeReader) Buffer() []byte { return append(c.Pre, c.buf...) } ================================================ FILE: extras/sniff/sniff_test.go ================================================ package sniff import ( "encoding/base64" "io" "testing" "time" "github.com/apernet/hysteria/extras/v2/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestSnifferCheck(t *testing.T) { sniffer := &Sniffer{ Timeout: 1 * time.Second, RewriteDomain: false, TCPPorts: nil, // nil = all UDPPorts: nil, // nil = all } assert.True(t, sniffer.Check(false, "1.1.1.1:80")) assert.False(t, sniffer.Check(false, "example.com:443")) sniffer.RewriteDomain = true assert.True(t, sniffer.Check(false, "example.com:443")) sniffer.TCPPorts = []utils.PortRange{{80, 80}} assert.True(t, sniffer.Check(false, "google.com:80")) assert.False(t, sniffer.Check(false, "google.com:443")) sniffer.UDPPorts = []utils.PortRange{{443, 443}} assert.True(t, sniffer.Check(true, "google.com:443")) assert.False(t, sniffer.Check(true, "google.com:80")) } func TestSnifferTCP(t *testing.T) { sniffer := &Sniffer{ Timeout: 1 * time.Second, RewriteDomain: false, } buf := &[]byte{} // Test HTTP *buf = []byte("POST /hello HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: mamamiya\r\n" + "Content-Length: 27\r\n" + "Connection: keep-alive\r\n\r\n" + "param1=value1¶m2=value2") index := 0 stream := &mockStream{} stream.EXPECT().SetReadDeadline(mock.Anything).Return(nil) stream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { if index < len(*buf) { n := copy(bs, (*buf)[index:]) index += n return n, nil } else { return 0, io.EOF } }) // Rewrite IP to domain reqAddr := "111.111.111.111:80" putback, err := sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, *buf, putback) assert.Equal(t, "example.com:80", reqAddr) // Test HTTP with Host as host:port *buf = []byte("GET / HTTP/1.1\r\n" + "Host: example.com:8080\r\n" + "User-Agent: test-agent\r\n" + "Accept: */*\r\n\r\n") index = 0 reqAddr = "222.222.222.222:10086" putback, err = sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, *buf, putback) assert.Equal(t, "example.com:10086", reqAddr) // Test TLS *buf, err = base64.StdEncoding.DecodeString("FgMBARcBAAETAwPJL2jlt1OAo+Rslkjv/aqKiTthKMaCKg2Gvd+uALDbDCDdY+UIk8ouadEB9fC3j52Y1i7SJZqGIgBRIS6kKieYrAAoEwITAcAswCvAMMAvwCTAI8AowCfACsAJwBTAEwCdAJwAPQA8ADUALwEAAKIAAAAOAAwAAAlpcGluZm8uaW8ABQAFAQAAAAAAKwAJCAMEAwMDAgMBAA0AGgAYCAQIBQgGBAEFAQIBBAMFAwIDAgIGAQYDACMAAAAKAAgABgAdABcAGAAQAAsACQhodHRwLzEuMQAzACYAJAAdACBguQbqNJNyamYxYcrBFpBP7pWv5TgZsP9gwGtMYNKVBQAxAAAAFwAA/wEAAQAALQACAQE=") assert.NoError(t, err) index = 0 reqAddr = "222.222.222.222:443" putback, err = sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, *buf, putback) assert.Equal(t, "ipinfo.io:443", reqAddr) // Test unrecognized 1 *buf = []byte("Wait It's All Ohio? Always Has Been.") index = 0 reqAddr = "123.123.123.123:123" putback, err = sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, *buf, putback) assert.Equal(t, "123.123.123.123:123", reqAddr) // Test unrecognized 2 *buf = []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a") index = 0 reqAddr = "45.45.45.45:45" putback, err = sniffer.TCP(stream, &reqAddr) assert.NoError(t, err) assert.Equal(t, []byte("\x01\x02\x03"), putback) assert.Equal(t, "45.45.45.45:45", reqAddr) // Test timeout blockStream := &mockStream{} blockStream.EXPECT().SetReadDeadline(mock.Anything).Return(nil) blockStream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) { time.Sleep(2 * time.Second) return 0, io.EOF }) reqAddr = "66.66.66.66:66" putback, err = sniffer.TCP(blockStream, &reqAddr) assert.NoError(t, err) assert.Equal(t, []byte{}, putback) assert.Equal(t, "66.66.66.66:66", reqAddr) } func TestSnifferUDP(t *testing.T) { sniffer := &Sniffer{ Timeout: 1 * time.Second, RewriteDomain: false, } // Test QUIC reqAddr := "2.3.4.5:443" pkt, err := base64.StdEncoding.DecodeString("ygAAAAEIwugWgPS7ulYAAES8hY891uwgGE9GG4CPOLd+nsDe28raso24lCSFmlFwYQG1uF39ikbL13/R9ZTghYmTl+jEbr6F9TxxRiOgpTmKRmh6aKZiIiVfy5pVRckovaI8lq0WRoW9xoFNTyYtQP8TVJ3bLCK+zUqpquEQSyWf7CE43ywayyMpE9UlIoPXFWCoopXLM1SvzdQ+17P51N9KR7m4emti4DWWTBLMQOvrwd2HEEkbiZdRO1wf6ZXJlIat5dN0R/6uod60OFPO+u+awvq67MoMReC7+5I/xWI+xx6o4JpnZNn6YPG8Gqi8hS6doNcAAdtD8h5eMLuHCCgkpX3QVjjfWtcOhtw9xKjU43HhUPwzUTv+JDLgwuTQCTmlfYlb3B+pk4b2I9si0tJ0SBuYaZ2VQPtZbj2hpGXw3gn11pbN8xsbKkQL50+Scd4dGJxWQlGaJHeaU5WOCkxLXc635z8m5XO/CBHVYPGp4pfwfwNUgbe5WF+3MaUIlDB8dMfsnrO0BmZPo379jVx0SFLTAiS8wAdHib1WNEY8qKYnTWuiyxYg1GZEhJt0nXmI+8f0eJq42DgHBWC+Rf5rRBr/Sf25o3mFAmTUaul0Woo9/CIrpT73B63N91xd9A77i4ru995YG8l9Hen+eLtpDU9Q9376nwMDYBzeYG9U/Rn0Urbm6q4hmAgV/xlNJ2rAyDS+yLnwqD6I0PRy8bZJEttcidb/SkOyrpgMiAzWeT+SO+c/k+Y8H0UTRa05faZUrhuUaym9wAcaIVRA6nFI+fejfjVp+7afFv+kWn3vCqQEij+CRHuxkltrixZMD2rfYj6NUW7TTYBtPRtuV/V0ZIDjRR26vr4K+0D84+l3c0mA/l6nmpP5kkco3nmpdjtQN6sGXL7+5o0nnsftX5d6/n5mLyEpP+AEDl1zk3iqkS62RsITwql6DMMoGbSDdUpMclCIeM0vlo3CkxGMO7QA9ruVeNddkL3EWMivl+uxO43sXEEqYQHVl4N75y63t05GOf7/gm9Kb/BJ8MpG9ViEkVYaskQCzi3D8bVpzo8FfTj8te8B6c3ikc/cm7r8k0ZcZpr+YiLGDYq+0ilHxpqJfmq8dPkSvxdzLcUSvy7+LMQ/TTobRSF7L4JhtDKck0+00vl9H35Tkh9N+MsVtpKdWyoqZ4XaK2Nx1M6AieczXpdFc0y7lYPoUfF4IeW8WzeVUclol5ElYjkyFz/lDOGAe1bF2g5AYaGWCPiGleVZknNdD5ihB8W8Mfkt1pEwq2S97AHrppqkf/VoIfZzeqH8wUFw8fDDrZIpnoa0rW7HfwIQaqJhPCyB9Z6TVbV4x9UWmaHfVAcinCK/7o10dtaj3rvEqcUC/iPceGq3Tqv/p9GGNJ+Ci2JBjXqNxYr893Llk75VdPD9pM6y1SM0P80oXNy32VMtafkFFST8GpvvqWcxUJ93kzaY8RmU1g3XFOImSU2utU6+FUQ2Pn5uLwcfT2cTYfTpPGh+WXjSbZ6trqdEMEsLHybuPo2UN4WpVLXVQma3kSaHQggcLlEip8GhEUAy/xCb2eKqhI4HkDpDjwDnDVKufWlnRaOHf58cc8Woi+WT8JTOkHC+nBEG6fKRPHDG08U5yayIQIjI") assert.NoError(t, err) err = sniffer.UDP(pkt, &reqAddr) assert.NoError(t, err) assert.Equal(t, "www.notion.so:443", reqAddr) // Test unrecognized pkt = []byte("oh my sweet summer child") reqAddr = "90.90.90.90:90" err = sniffer.UDP(pkt, &reqAddr) assert.NoError(t, err) assert.Equal(t, "90.90.90.90:90", reqAddr) } ================================================ FILE: extras/trafficlogger/http.go ================================================ package trafficlogger import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "strconv" "sync" "time" "github.com/apernet/hysteria/core/v2/server" ) const ( indexHTML = ` Hysteria Traffic Stats API Server

This is a Hysteria Traffic Stats API server.

Check the documentation for usage.

` ) // TrafficStatsServer implements both server.TrafficLogger and http.Handler // to provide a simple HTTP API to get the traffic stats per user. type TrafficStatsServer interface { server.TrafficLogger http.Handler } func NewTrafficStatsServer(secret string) TrafficStatsServer { return &trafficStatsServerImpl{ StatsMap: make(map[string]*trafficStatsEntry), KickMap: make(map[string]struct{}), OnlineMap: make(map[string]int), Secret: secret, } } type TrafficPushRequest struct { Data map[string][2]int64 } // 定时提交用户流量情况 func (s *trafficStatsServerImpl) PushTrafficToV2boardInterval(url string, interval time.Duration) { fmt.Println("用户流量情况监控已启动,提交周期为:", interval) ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { if err := s.PushTrafficToV2board(url); err != nil { fmt.Println("用户流量信息提交失败:", err) } } } // 向v2board 提交用户流量使用情况 func (s *trafficStatsServerImpl) PushTrafficToV2board(url string) error { s.Mutex.Lock() // 写锁,阻止其他操作 StatsMap 的并发访问 defer s.Mutex.Unlock() // 确保在函数退出时释放写锁 request := TrafficPushRequest{ Data: make(map[string][2]int64), } for id, stats := range s.StatsMap { request.Data[id] = [2]int64{int64(stats.Tx), int64(stats.Rx)} } if len(request.Data) == 0 { return nil } jsonData, err := json.Marshal(request.Data) if err != nil { return err } resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) if err != nil { fmt.Println(resp) return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return errors.New("HTTP request failed with status code: " + resp.Status) } s.StatsMap = make(map[string]*trafficStatsEntry) return nil } type trafficStatsServerImpl struct { Mutex sync.RWMutex StatsMap map[string]*trafficStatsEntry OnlineMap map[string]int KickMap map[string]struct{} Secret string } type trafficStatsEntry struct { Tx uint64 `json:"tx"` Rx uint64 `json:"rx"` } func (s *trafficStatsServerImpl) LogTraffic(id string, tx, rx uint64) (ok bool) { s.Mutex.Lock() defer s.Mutex.Unlock() _, ok = s.KickMap[id] if ok { delete(s.KickMap, id) return false } entry, ok := s.StatsMap[id] if !ok { entry = &trafficStatsEntry{} s.StatsMap[id] = entry } entry.Tx += tx entry.Rx += rx return true } // LogOnlineStateChanged updates the online state to the online map. func (s *trafficStatsServerImpl) LogOnlineState(id string, online bool) { s.Mutex.Lock() defer s.Mutex.Unlock() if online { s.OnlineMap[id]++ } else { s.OnlineMap[id]-- if s.OnlineMap[id] <= 0 { delete(s.OnlineMap, id) } } } func (s *trafficStatsServerImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.Secret != "" && r.Header.Get("Authorization") != s.Secret { http.Error(w, "unauthorized", http.StatusUnauthorized) return } if r.Method == http.MethodGet && r.URL.Path == "/" { _, _ = w.Write([]byte(indexHTML)) return } if r.Method == http.MethodGet && r.URL.Path == "/traffic" { s.getTraffic(w, r) return } if r.Method == http.MethodPost && r.URL.Path == "/kick" { s.kick(w, r) return } if r.Method == http.MethodGet && r.URL.Path == "/online" { s.getOnline(w, r) return } http.NotFound(w, r) } func (s *trafficStatsServerImpl) getTraffic(w http.ResponseWriter, r *http.Request) { bClear, _ := strconv.ParseBool(r.URL.Query().Get("clear")) var jb []byte var err error if bClear { s.Mutex.Lock() jb, err = json.Marshal(s.StatsMap) s.StatsMap = make(map[string]*trafficStatsEntry) s.Mutex.Unlock() } else { s.Mutex.RLock() jb, err = json.Marshal(s.StatsMap) s.Mutex.RUnlock() } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") _, _ = w.Write(jb) } func (s *trafficStatsServerImpl) getOnline(w http.ResponseWriter, r *http.Request) { s.Mutex.RLock() defer s.Mutex.RUnlock() jb, err := json.Marshal(s.OnlineMap) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") _, _ = w.Write(jb) } func (s *trafficStatsServerImpl) kick(w http.ResponseWriter, r *http.Request) { var ids []string err := json.NewDecoder(r.Body).Decode(&ids) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } s.Mutex.Lock() for _, id := range ids { s.KickMap[id] = struct{}{} } s.Mutex.Unlock() w.WriteHeader(http.StatusOK) } // 踢出用户名单 func (s *trafficStatsServerImpl) NewKick(id string) bool { s.Mutex.Lock() s.KickMap[id] = struct{}{} s.Mutex.Unlock() return true } ================================================ FILE: extras/transport/udphop/addr.go ================================================ package udphop import ( "fmt" "net" "github.com/apernet/hysteria/extras/v2/utils" ) type InvalidPortError struct { PortStr string } func (e InvalidPortError) Error() string { return fmt.Sprintf("%s is not a valid port number or range", e.PortStr) } // UDPHopAddr contains an IP address and a list of ports. type UDPHopAddr struct { IP net.IP Ports []uint16 PortStr string } func (a *UDPHopAddr) Network() string { return "udphop" } func (a *UDPHopAddr) String() string { return net.JoinHostPort(a.IP.String(), a.PortStr) } // addrs returns a list of net.Addr's, one for each port. func (a *UDPHopAddr) addrs() ([]net.Addr, error) { var addrs []net.Addr for _, port := range a.Ports { addr := &net.UDPAddr{ IP: a.IP, Port: int(port), } addrs = append(addrs, addr) } return addrs, nil } func ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) { host, portStr, err := net.SplitHostPort(addr) if err != nil { return nil, err } ip, err := net.ResolveIPAddr("ip", host) if err != nil { return nil, err } result := &UDPHopAddr{ IP: ip.IP, PortStr: portStr, } pu := utils.ParsePortUnion(portStr) if pu == nil { return nil, InvalidPortError{portStr} } result.Ports = pu.Ports() return result, nil } ================================================ FILE: extras/transport/udphop/conn.go ================================================ package udphop import ( "errors" "math/rand" "net" "sync" "syscall" "time" ) const ( packetQueueSize = 1024 udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough defaultHopInterval = 30 * time.Second ) type udpHopPacketConn struct { Addr net.Addr Addrs []net.Addr HopInterval time.Duration ListenUDPFunc ListenUDPFunc connMutex sync.RWMutex prevConn net.PacketConn currentConn net.PacketConn addrIndex int readBufferSize int writeBufferSize int recvQueue chan *udpPacket closeChan chan struct{} closed bool bufPool sync.Pool } type udpPacket struct { Buf []byte N int Addr net.Addr Err error } type ListenUDPFunc = func() (net.PacketConn, error) func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc) (net.PacketConn, error) { if hopInterval == 0 { hopInterval = defaultHopInterval } else if hopInterval < 5*time.Second { return nil, errors.New("hop interval must be at least 5 seconds") } if listenUDPFunc == nil { listenUDPFunc = func() (net.PacketConn, error) { return net.ListenUDP("udp", nil) } } addrs, err := addr.addrs() if err != nil { return nil, err } curConn, err := listenUDPFunc() if err != nil { return nil, err } hConn := &udpHopPacketConn{ Addr: addr, Addrs: addrs, HopInterval: hopInterval, ListenUDPFunc: listenUDPFunc, prevConn: nil, currentConn: curConn, addrIndex: rand.Intn(len(addrs)), recvQueue: make(chan *udpPacket, packetQueueSize), closeChan: make(chan struct{}), bufPool: sync.Pool{ New: func() interface{} { return make([]byte, udpBufferSize) }, }, } go hConn.recvLoop(curConn) go hConn.hopLoop() return hConn, nil } func (u *udpHopPacketConn) recvLoop(conn net.PacketConn) { for { buf := u.bufPool.Get().([]byte) n, addr, err := conn.ReadFrom(buf) if err != nil { u.bufPool.Put(buf) var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { // Only pass through timeout errors here, not permanent errors // like connection closed. Connection close is normal as we close // the old connection to exit this loop every time we hop. u.recvQueue <- &udpPacket{nil, 0, nil, netErr} } return } select { case u.recvQueue <- &udpPacket{buf, n, addr, nil}: // Packet successfully queued default: // Queue is full, drop the packet u.bufPool.Put(buf) } } } func (u *udpHopPacketConn) hopLoop() { ticker := time.NewTicker(u.HopInterval) defer ticker.Stop() for { select { case <-ticker.C: u.hop() case <-u.closeChan: return } } } func (u *udpHopPacketConn) hop() { u.connMutex.Lock() defer u.connMutex.Unlock() if u.closed { return } newConn, err := u.ListenUDPFunc() if err != nil { // Could be temporary, just skip this hop return } // We need to keep receiving packets from the previous connection, // because otherwise there will be packet loss due to the time gap // between we hop to a new port and the server acknowledges this change. // So we do the following: // Close prevConn, // move currentConn to prevConn, // set newConn as currentConn, // start recvLoop on newConn. if u.prevConn != nil { _ = u.prevConn.Close() // recvLoop for this conn will exit } u.prevConn = u.currentConn u.currentConn = newConn // Set buffer sizes if previously set if u.readBufferSize > 0 { _ = trySetReadBuffer(u.currentConn, u.readBufferSize) } if u.writeBufferSize > 0 { _ = trySetWriteBuffer(u.currentConn, u.writeBufferSize) } go u.recvLoop(newConn) // Update addrIndex to a new random value u.addrIndex = rand.Intn(len(u.Addrs)) } func (u *udpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { for { select { case p := <-u.recvQueue: if p.Err != nil { return 0, nil, p.Err } // Currently we do not check whether the packet is from // the server or not due to performance reasons. n := copy(b, p.Buf[:p.N]) u.bufPool.Put(p.Buf) return n, u.Addr, nil case <-u.closeChan: return 0, nil, net.ErrClosed } } } func (u *udpHopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { u.connMutex.RLock() defer u.connMutex.RUnlock() if u.closed { return 0, net.ErrClosed } // Skip the check for now, always write to the server, // for the same reason as in ReadFrom. return u.currentConn.WriteTo(b, u.Addrs[u.addrIndex]) } func (u *udpHopPacketConn) Close() error { u.connMutex.Lock() defer u.connMutex.Unlock() if u.closed { return nil } // Close prevConn and currentConn // Close closeChan to unblock ReadFrom & hopLoop // Set closed flag to true to prevent double close if u.prevConn != nil { _ = u.prevConn.Close() } err := u.currentConn.Close() close(u.closeChan) u.closed = true u.Addrs = nil // For GC return err } func (u *udpHopPacketConn) LocalAddr() net.Addr { u.connMutex.RLock() defer u.connMutex.RUnlock() return u.currentConn.LocalAddr() } func (u *udpHopPacketConn) SetDeadline(t time.Time) error { u.connMutex.RLock() defer u.connMutex.RUnlock() if u.prevConn != nil { _ = u.prevConn.SetDeadline(t) } return u.currentConn.SetDeadline(t) } func (u *udpHopPacketConn) SetReadDeadline(t time.Time) error { u.connMutex.RLock() defer u.connMutex.RUnlock() if u.prevConn != nil { _ = u.prevConn.SetReadDeadline(t) } return u.currentConn.SetReadDeadline(t) } func (u *udpHopPacketConn) SetWriteDeadline(t time.Time) error { u.connMutex.RLock() defer u.connMutex.RUnlock() if u.prevConn != nil { _ = u.prevConn.SetWriteDeadline(t) } return u.currentConn.SetWriteDeadline(t) } // UDP-specific methods below func (u *udpHopPacketConn) SetReadBuffer(bytes int) error { u.connMutex.Lock() defer u.connMutex.Unlock() u.readBufferSize = bytes if u.prevConn != nil { _ = trySetReadBuffer(u.prevConn, bytes) } return trySetReadBuffer(u.currentConn, bytes) } func (u *udpHopPacketConn) SetWriteBuffer(bytes int) error { u.connMutex.Lock() defer u.connMutex.Unlock() u.writeBufferSize = bytes if u.prevConn != nil { _ = trySetWriteBuffer(u.prevConn, bytes) } return trySetWriteBuffer(u.currentConn, bytes) } func (u *udpHopPacketConn) SyscallConn() (syscall.RawConn, error) { u.connMutex.RLock() defer u.connMutex.RUnlock() sc, ok := u.currentConn.(syscall.Conn) if !ok { return nil, errors.New("not supported") } return sc.SyscallConn() } func trySetReadBuffer(pc net.PacketConn, bytes int) error { sc, ok := pc.(interface { SetReadBuffer(bytes int) error }) if ok { return sc.SetReadBuffer(bytes) } return nil } func trySetWriteBuffer(pc net.PacketConn, bytes int) error { sc, ok := pc.(interface { SetWriteBuffer(bytes int) error }) if ok { return sc.SetWriteBuffer(bytes) } return nil } ================================================ FILE: extras/utils/portunion.go ================================================ package utils import ( "sort" "strconv" "strings" ) // PortUnion is a collection of multiple port ranges. type PortUnion []PortRange // PortRange represents a range of ports. // Start and End are inclusive. [Start, End] type PortRange struct { Start, End uint16 } // ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion. // Returns nil if the input is invalid. // The returned PortUnion is guaranteed to be normalized. func ParsePortUnion(s string) PortUnion { if s == "all" || s == "*" { // Wildcard special case return PortUnion{PortRange{0, 65535}} } var result PortUnion portStrs := strings.Split(s, ",") for _, portStr := range portStrs { if strings.Contains(portStr, "-") { // Port range portRange := strings.Split(portStr, "-") if len(portRange) != 2 { return nil } start, err := strconv.ParseUint(portRange[0], 10, 16) if err != nil { return nil } end, err := strconv.ParseUint(portRange[1], 10, 16) if err != nil { return nil } if start > end { start, end = end, start } result = append(result, PortRange{uint16(start), uint16(end)}) } else { // Single port port, err := strconv.ParseUint(portStr, 10, 16) if err != nil { return nil } result = append(result, PortRange{uint16(port), uint16(port)}) } } if result == nil { return nil } return result.Normalize() } // Normalize normalizes a PortUnion. // No overlapping ranges, ranges are sorted from low to high. func (u PortUnion) Normalize() PortUnion { if len(u) == 0 { return u } sort.Slice(u, func(i, j int) bool { if u[i].Start == u[j].Start { return u[i].End < u[j].End } return u[i].Start < u[j].Start }) normalized := PortUnion{u[0]} for _, current := range u[1:] { last := &normalized[len(normalized)-1] if current.Start <= last.End+1 { if current.End > last.End { last.End = current.End } } else { normalized = append(normalized, current) } } return normalized } // Ports returns all ports in the PortUnion as a slice. func (u PortUnion) Ports() []uint16 { var ports []uint16 for _, r := range u { for i := r.Start; i <= r.End; i++ { ports = append(ports, i) } } return ports } // Contains returns true if the PortUnion contains the given port. func (u PortUnion) Contains(port uint16) bool { for _, r := range u { if port >= r.Start && port <= r.End { return true } } return false } ================================================ FILE: extras/utils/portunion_test.go ================================================ package utils import ( "reflect" "testing" ) func TestParsePortUnion(t *testing.T) { tests := []struct { name string s string want PortUnion }{ { name: "empty", s: "", want: nil, }, { name: "all 1", s: "all", want: PortUnion{{0, 65535}}, }, { name: "all 2", s: "*", want: PortUnion{{0, 65535}}, }, { name: "single port", s: "1234", want: PortUnion{{1234, 1234}}, }, { name: "multiple ports (unsorted)", s: "5678,1234,9012", want: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}}, }, { name: "one range", s: "1234-1240", want: PortUnion{{1234, 1240}}, }, { name: "one range (reversed)", s: "1240-1234", want: PortUnion{{1234, 1240}}, }, { name: "multiple ports and ranges (reversed, unsorted, overlapping)", s: "5678,1200-1236,9100-9012,1234-1240", want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}}, }, { name: "invalid 1", s: "1234-", want: nil, }, { name: "invalid 2", s: "1234-ggez", want: nil, }, { name: "invalid 3", s: "233,", want: nil, }, { name: "invalid 4", s: "1234-1240-1250", want: nil, }, { name: "invalid 5", s: "-,,", want: nil, }, { name: "invalid 6", s: "http", want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) { t.Errorf("ParsePortUnion() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: go.work.sum ================================================ cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0 h1:SPOUaucgtVls75mg+X7CXigS71EnsfVUK/2CgVrwqgw= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412 h1:GvWw74lx5noHocd+f6HBMXK6DuggBB1dhVkuGZbv7qM= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c h1:ivON6cwHK1OH26MZyWDCnbTRZZf0IhNsENoNAKFS1g4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:OR8VhtwhcAI3U48/rzBsVOuHi0zDPzYI1xASVcdSgR8= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q= github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad h1:EmNYJhPYy0pOFjCx2PrgtaBXmee0iUX9hLlxE1xHOJE= github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/consul/api v1.18.0 h1:R7PPNzTCeN6VuQNDwwhZWJvzCtGSrNpJqfb22h3yH9g= github.com/hashicorp/consul/api v1.18.0/go.mod h1:owRRGJ9M5xReDC5nfT8FTJrNAPbT4NM6p/k+d03q2v4= github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/sagikazarmark/crypt v0.9.0 h1:fipzMFW34hFUEc4D7fsLQFtE7yElkpgyS2zruedRdZk= github.com/sagikazarmark/crypt v0.9.0/go.mod h1:RnH7sEhxfdnPm1z+XMgSLjWTEIjyK4z2dw6+4vHTMuo= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d h1:Yoy/IzG4lULT6qZg62sVC+qyBL8DQkmD2zv6i7OImrc= github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c h1:UOk+nlt1BJtTcH15CT7iNO7YVWTfTv/DNwEAQHLIaDQ= github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw= github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20 h1:7pDq9pAMCQgRohFmd25X8hIH8VxmT3TaDm+r9LHxgBk= github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9 h1:MPblCbqA5+z6XARjScMfz1TqtJC7TuTRj0U9VqIBs6k= github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50 h1:crYRwvwjdVh1biHzzciFHe8DrZcYrVcZFlJtykhRctg= github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc h1:eHRtZoIi6n9Wo1uR+RU44C247msLWwyA89hVKwRLkMk= github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9 h1:fxoFD0in0/CBzXoyNhMTjvBZYW6ilSnTw7N7y/8vkmM= github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191 h1:T4wuULTrzCKMFlg3HmKHgXAF8oStFb/+lOIupLV2v+o= github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241 h1:Y+TeIabU8sJD10Qwd/zMty2/LEaT9GNDaA6nyZf+jgo= github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122 h1:TQVQrsyNaimGwF7bIhzoVC9QkKm4KsWd8cECGzFx8gI= github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2 h1:bu666BQci+y4S0tVRVjsHUeRon6vUXmsGBwdowgMrg4= github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/AkPDU3AkqMxnMYL+imaqkpflHu73us8= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= go.etcd.io/etcd/api/v3 v3.5.6 h1:Cy2qx3npLcYqTKqGJzMypnMv2tiRyifZJ17BlWIWA7A= go.etcd.io/etcd/api/v3 v3.5.6/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8= go.etcd.io/etcd/client/pkg/v3 v3.5.6 h1:TXQWYceBKqLp4sa87rcPs11SXxUA/mHwH975v+BDvLU= go.etcd.io/etcd/client/pkg/v3 v3.5.6/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ= go.etcd.io/etcd/client/v2 v2.305.6 h1:fIDR0p4KMjw01MJMfUIDWdQbjo06PD6CeYM5z4EHLi0= go.etcd.io/etcd/client/v2 v2.305.6/go.mod h1:BHha8XJGe8vCIBfWBpbBLVZ4QjOIlfoouvOwydu63E0= go.etcd.io/etcd/client/v3 v3.5.6 h1:coLs69PWCXE9G4FKquzNaSHrRyMCAXwF+IX1tAPVO8E= go.etcd.io/etcd/client/v3 v3.5.6/go.mod h1:f6GRinRMCsFVv9Ht42EyY7nfsVGwrNO0WEoS2pRKzQk= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE= golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= google.golang.org/api v0.107.0 h1:I2SlFjD8ZWabaIFOfeEDg3pf0BHJDh6iYQ1ic3Yu/UU= google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo= honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= sourcegraph.com/sourcegraph/go-diff v0.5.0 h1:eTiIR0CoWjGzJcnQ3OkhIl/b9GJovq4lSAVRt0ZFEG8= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= ================================================ FILE: hyperbole.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import argparse import os import re import sys import subprocess import datetime import shutil # Hyperbole is the official build script for Hysteria. # Available environment variables for controlling the build: # - HY_APP_VERSION: App version # - HY_APP_COMMIT: App commit hash # - HY_APP_PLATFORMS: Platforms to build for (e.g. "windows/amd64,linux/arm") LOGO = """ ░█░█░█░█░█▀█░█▀▀░█▀▄░█▀▄░█▀█░█░░░█▀▀ ░█▀█░░█░░█▀▀░█▀▀░█▀▄░█▀▄░█░█░█░░░█▀▀ ░▀░▀░░▀░░▀░░░▀▀▀░▀░▀░▀▀░░▀▀▀░▀▀▀░▀▀▀ """ DESC = "Hyperbole is the official build script for Hysteria." BUILD_DIR = "build" CORE_SRC_DIR = "./core" EXTRAS_SRC_DIR = "./extras" APP_SRC_DIR = "./app" APP_SRC_CMD_PKG = "github.com/apernet/hysteria/app/v2/cmd" MODULE_SRC_DIRS = [CORE_SRC_DIR, EXTRAS_SRC_DIR, APP_SRC_DIR] ARCH_ALIASES = { "arm": { "GOARCH": "arm", "GOARM": "7", }, "armv5": { "GOARCH": "arm", "GOARM": "5", }, "armv6": { "GOARCH": "arm", "GOARM": "6", }, "armv7": { "GOARCH": "arm", "GOARM": "7", }, "mips": { "GOARCH": "mips", "GOMIPS": "", }, "mipsle": { "GOARCH": "mipsle", "GOMIPS": "", }, "mips-sf": { "GOARCH": "mips", "GOMIPS": "softfloat", }, "mipsle-sf": { "GOARCH": "mipsle", "GOMIPS": "softfloat", }, "amd64": { "GOARCH": "amd64", "GOAMD64": "", }, "amd64-avx": { "GOARCH": "amd64", "GOAMD64": "v3", }, } def check_command(args): try: subprocess.check_call( args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) return True except Exception: return False def check_build_env(): if not check_command(["git", "--version"]): print("Git is not installed. Please install Git and try again.") return False if not check_command(["git", "rev-parse", "--is-inside-work-tree"]): print("Not in a Git repository. Please go to the project root and try again.") return False if not check_command(["go", "version"]): print("Go is not installed. Please install Go and try again.") return False return True def get_app_version(): app_version = os.environ.get("HY_APP_VERSION") if not app_version: try: output = ( subprocess.check_output( ["git", "describe", "--tags", "--always", "--match", "app/v*"] ) .decode() .strip() ) app_version = output.split("/")[-1] except Exception: app_version = "Unknown" return app_version def get_app_version_code(str=None): if not str: str = get_app_version() match = re.search(r"v(\d+)\.(\d+)\.(\d+)", str) if match: major, minor, patch = match.groups() major = major.zfill(2)[:2] minor = minor.zfill(2)[:2] patch = patch.zfill(2)[:2] return int(f"{major}{minor}{patch[:2]}") else: return 0 def get_app_commit(): app_commit = os.environ.get("HY_APP_COMMIT") if not app_commit: try: app_commit = ( subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip() ) except Exception: app_commit = "Unknown" return app_commit def get_current_os_arch(): d_os = subprocess.check_output(["go", "env", "GOOS"]).decode().strip() d_arch = subprocess.check_output(["go", "env", "GOARCH"]).decode().strip() return (d_os, d_arch) def get_app_platforms(): platforms = os.environ.get("HY_APP_PLATFORMS") if not platforms: d_os, d_arch = get_current_os_arch() return [(d_os, d_arch)] result = [] for platform in platforms.split(","): platform = platform.strip() if not platform: continue parts = platform.split("/") if len(parts) != 2: continue result.append((parts[0], parts[1])) return result def cmd_build(pprof=False, release=False, race=False): if not check_build_env(): return os.makedirs(BUILD_DIR, exist_ok=True) app_version = get_app_version() app_date = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") app_commit = get_app_commit() ldflags = [ "-X", APP_SRC_CMD_PKG + ".appVersion=" + app_version, "-X", APP_SRC_CMD_PKG + ".appDate=" + app_date, "-X", APP_SRC_CMD_PKG + ".appType=" + ("release" if release else "dev") + ("-pprof" if pprof else ""), "-X", APP_SRC_CMD_PKG + ".appCommit=" + app_commit, ] if release: ldflags.append("-s") ldflags.append("-w") for os_name, arch in get_app_platforms(): print("Building for %s/%s..." % (os_name, arch)) out_name = "hysteria-%s-%s" % (os_name, arch) if os_name == "windows": out_name += ".exe" env = os.environ.copy() env["GOOS"] = os_name if arch in ARCH_ALIASES: for k, v in ARCH_ALIASES[arch].items(): env[k] = v else: env["GOARCH"] = arch if os_name == "android": env["CGO_ENABLED"] = "1" ANDROID_NDK_HOME = ( os.environ.get("ANDROID_NDK_HOME") + "/toolchains/llvm/prebuilt/linux-x86_64/bin" ) if arch == "arm64": env["CC"] = ANDROID_NDK_HOME + "/aarch64-linux-android29-clang" elif arch == "armv7": env["CC"] = ANDROID_NDK_HOME + "/armv7a-linux-androideabi29-clang" elif arch == "386": env["CC"] = ANDROID_NDK_HOME + "/i686-linux-android29-clang" elif arch == "amd64": env["CC"] = ANDROID_NDK_HOME + "/x86_64-linux-android29-clang" else: print("Unsupported arch for android: %s" % arch) return else: env["CGO_ENABLED"] = "1" if race else "0" # Race detector requires cgo plat_ldflags = ldflags.copy() plat_ldflags.append("-X") plat_ldflags.append(APP_SRC_CMD_PKG + ".appPlatform=" + os_name) plat_ldflags.append("-X") plat_ldflags.append(APP_SRC_CMD_PKG + ".appArch=" + arch) cmd = [ "go", "build", "-o", os.path.join(BUILD_DIR, out_name), "-ldflags", " ".join(plat_ldflags), ] if pprof: cmd.append("-tags") cmd.append("pprof") if race: cmd.append("-race") if release: cmd.append("-trimpath") cmd.append(APP_SRC_DIR) try: subprocess.check_call(cmd, env=env) except Exception: print("Failed to build for %s/%s" % (os_name, arch)) sys.exit(1) print("Built %s" % out_name) def cmd_run(args, pprof=False, race=False): if not check_build_env(): return app_version = get_app_version() app_date = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") app_commit = get_app_commit() current_os, current_arch = get_current_os_arch() ldflags = [ "-X", APP_SRC_CMD_PKG + ".appVersion=" + app_version, "-X", APP_SRC_CMD_PKG + ".appDate=" + app_date, "-X", APP_SRC_CMD_PKG + ".appType=dev-run", "-X", APP_SRC_CMD_PKG + ".appCommit=" + app_commit, "-X", APP_SRC_CMD_PKG + ".appPlatform=" + current_os, "-X", APP_SRC_CMD_PKG + ".appArch=" + current_arch, ] cmd = ["go", "run", "-ldflags", " ".join(ldflags)] if pprof: cmd.append("-tags") cmd.append("pprof") if race: cmd.append("-race") cmd.append(APP_SRC_DIR) cmd.extend(args) try: subprocess.check_call(cmd) except KeyboardInterrupt: pass except subprocess.CalledProcessError as e: # Pass through the exit code sys.exit(e.returncode) def cmd_format(): if not check_command(["gofumpt", "-version"]): print("gofumpt is not installed. Please install gofumpt and try again.") return try: subprocess.check_call(["gofumpt", "-w", "-l", "-extra", "."]) except Exception: print("Failed to format code") def cmd_mockgen(): if not check_command(["mockery", "--version"]): print("mockery is not installed. Please install mockery and try again.") return for dirpath, dirnames, filenames in os.walk("."): dirnames[:] = [d for d in dirnames if not d.startswith(".")] if ".mockery.yaml" in filenames: print("Generating mocks for %s..." % dirpath) try: subprocess.check_call(["mockery"], cwd=dirpath) except Exception: print("Failed to generate mocks for %s" % dirpath) def cmd_protogen(): if not check_command(["protoc", "--version"]): print("protoc is not installed. Please install protoc and try again.") return for dirpath, dirnames, filenames in os.walk("."): dirnames[:] = [d for d in dirnames if not d.startswith(".")] proto_files = [f for f in filenames if f.endswith(".proto")] if len(proto_files) > 0: for proto_file in proto_files: print("Generating protobuf for %s..." % proto_file) try: subprocess.check_call( ["protoc", "--go_out=paths=source_relative:.", proto_file], cwd=dirpath, ) except Exception: print("Failed to generate protobuf for %s" % proto_file) def cmd_tidy(): if not check_build_env(): return for dir in MODULE_SRC_DIRS: print("Tidying %s..." % dir) try: subprocess.check_call(["go", "mod", "tidy"], cwd=dir) except Exception: print("Failed to tidy %s" % dir) print("Syncing go work...") try: subprocess.check_call(["go", "work", "sync"]) except Exception: print("Failed to sync go work") def cmd_test(module=None): if not check_build_env(): return if module: print("Testing %s..." % module) try: subprocess.check_call(["go", "test", "-v", "./..."], cwd=module) except Exception: print("Failed to test %s" % module) else: for dir in MODULE_SRC_DIRS: print("Testing %s..." % dir) try: subprocess.check_call(["go", "test", "-v", "./..."], cwd=dir) except Exception: print("Failed to test %s" % dir) def cmd_publish(urgent=False): import requests if not check_build_env(): return app_version = get_app_version() app_version_code = get_app_version_code(app_version) if app_version_code == 0: print("Invalid app version") return payload = { "code": app_version_code, "ver": app_version, "chan": "release", "url": "https://github.com/apernet/hysteria/releases", "urgent": urgent, } headers = { "Content-Type": "application/json", "Authorization": os.environ.get("HY_API_POST_KEY"), } resp = requests.post("https://api.hy2.io/v1/update", json=payload, headers=headers) if resp.status_code == 200: print("Published %s" % app_version) else: print("Failed to publish %s, status code: %d" % (app_version, resp.status_code)) def cmd_clean(): shutil.rmtree(BUILD_DIR, ignore_errors=True) def cmd_about(): print(LOGO) print(DESC) def main(): parser = argparse.ArgumentParser() p_cmd = parser.add_subparsers(dest="command") p_cmd.required = True # Run p_run = p_cmd.add_parser("run", help="Run the app") p_run.add_argument( "-p", "--pprof", action="store_true", help="Run with pprof enabled" ) p_run.add_argument( "-d", "--race", action="store_true", help="Build with data race detection" ) p_run.add_argument("args", nargs=argparse.REMAINDER) # Build p_build = p_cmd.add_parser("build", help="Build the app") p_build.add_argument( "-p", "--pprof", action="store_true", help="Build with pprof enabled" ) p_build.add_argument( "-r", "--release", action="store_true", help="Build a release version" ) p_build.add_argument( "-d", "--race", action="store_true", help="Build with data race detection" ) # Format p_cmd.add_parser("format", help="Format the code") # Mockgen p_cmd.add_parser("mockgen", help="Generate mock interfaces") # Protogen p_cmd.add_parser("protogen", help="Generate protobuf interfaces") # Tidy p_cmd.add_parser("tidy", help="Tidy the go modules") # Test p_test = p_cmd.add_parser("test", help="Test the code") p_test.add_argument("module", nargs="?", help="Module to test") # Publish p_pub = p_cmd.add_parser("publish", help="Publish the current version") p_pub.add_argument( "-u", "--urgent", action="store_true", help="Publish as an urgent update" ) # Clean p_cmd.add_parser("clean", help="Clean the build directory") # About p_cmd.add_parser("about", help="Print about information") args = parser.parse_args() if args.command == "run": cmd_run(args.args, args.pprof, args.race) elif args.command == "build": cmd_build(args.pprof, args.release, args.race) elif args.command == "format": cmd_format() elif args.command == "mockgen": cmd_mockgen() elif args.command == "protogen": cmd_protogen() elif args.command == "tidy": cmd_tidy() elif args.command == "test": cmd_test(args.module) elif args.command == "publish": cmd_publish(args.urgent) elif args.command == "clean": cmd_clean() elif args.command == "about": cmd_about() if __name__ == "__main__": main() ================================================ FILE: platforms.txt ================================================ # This file controls what platform/architecture combinations we build for a release. # Windows windows/amd64 windows/amd64-avx windows/386 windows/arm64 # macOS darwin/amd64 darwin/amd64-avx darwin/arm64 # Linux linux/amd64 linux/amd64-avx linux/386 linux/arm linux/armv5 linux/arm64 linux/s390x linux/mipsle linux/mipsle-sf linux/riscv64 # Android android/386 android/amd64 android/armv7 android/arm64 # FreeBSD freebsd/amd64 freebsd/amd64-avx freebsd/386 freebsd/arm freebsd/arm64 ================================================ FILE: requirements.txt ================================================ blinker==1.8.2 cffi==1.17.0 click==8.1.7 cryptography==43.0.0 Flask==3.0.3 itsdangerous==2.2.0 Jinja2==3.1.4 MarkupSafe==2.1.5 pycparser==2.22 PySocks==1.7.1 Werkzeug==3.0.4 ================================================ FILE: scripts/_redirects ================================================ / /install_server.sh 301 ================================================ FILE: scripts/install_server.sh ================================================ #!/usr/bin/env bash # # install_server.sh - hysteria server install script # Try `install_server.sh --help` for usage. # # SPDX-License-Identifier: MIT # Copyright (c) 2023 Aperture Internet Laboratory # set -e ### # SCRIPT CONFIGURATION ### # Basename of this script SCRIPT_NAME="$(basename "$0")" # Command line arguments of this script SCRIPT_ARGS=("$@") # Path for installing executable EXECUTABLE_INSTALL_PATH="/usr/local/bin/hysteria" # Paths to install systemd files SYSTEMD_SERVICES_DIR="/etc/systemd/system" # Directory to store hysteria config file CONFIG_DIR="/etc/hysteria" # URLs of GitHub REPO_URL="https://github.com/apernet/hysteria" # URL of Hysteria 2 API HY2_API_BASE_URL="https://api.hy2.io/v1" # curl command line flags. # To using a proxy, please specify ALL_PROXY in the environ variable, such like: # export ALL_PROXY=socks5h://192.0.2.1:1080 CURL_FLAGS=(-L -f -q --retry 5 --retry-delay 10 --retry-max-time 60) ### # AUTO DETECTED GLOBAL VARIABLE ### # Package manager PACKAGE_MANAGEMENT_INSTALL="${PACKAGE_MANAGEMENT_INSTALL:-}" # Operating System of current machine, supported: linux OPERATING_SYSTEM="${OPERATING_SYSTEM:-}" # Architecture of current machine, supported: 386, amd64, arm, arm64, mipsle, s390x ARCHITECTURE="${ARCHITECTURE:-}" # User for running hysteria HYSTERIA_USER="${HYSTERIA_USER:-}" # Directory for ACME certificates storage HYSTERIA_HOME_DIR="${HYSTERIA_HOME_DIR:-}" # SELinux context of systemd unit files SECONTEXT_SYSTEMD_UNIT="${SECONTEXT_SYSTEMD_UNIT:-}" ### # ARGUMENTS ### # Supported operation: install, remove, check_update OPERATION= # User specified version to install VERSION= # Force install even if installed FORCE= # User specified binary to install LOCAL_FILE= ### # COMMAND REPLACEMENT & UTILITIES ### has_command() { local _command=$1 type -P "$_command" > /dev/null 2>&1 } curl() { command curl "${CURL_FLAGS[@]}" "$@" } mktemp() { command mktemp "$@" "/tmp/hyservinst.XXXXXXXXXX" } tput() { if has_command tput; then command tput "$@" fi } tred() { tput setaf 1 } tgreen() { tput setaf 2 } tyellow() { tput setaf 3 } tblue() { tput setaf 4 } taoi() { tput setaf 6 } tbold() { tput bold } treset() { tput sgr0 } note() { local _msg="$1" echo -e "$SCRIPT_NAME: $(tbold)note: $_msg$(treset)" } warning() { local _msg="$1" echo -e "$SCRIPT_NAME: $(tyellow)warning: $_msg$(treset)" } error() { local _msg="$1" echo -e "$SCRIPT_NAME: $(tred)error: $_msg$(treset)" } has_prefix() { local _s="$1" local _prefix="$2" if [[ -z "$_prefix" ]]; then return 0 fi if [[ -z "$_s" ]]; then return 1 fi [[ "x$_s" != "x${_s#"$_prefix"}" ]] } generate_random_password() { dd if=/dev/random bs=18 count=1 status=none | base64 } systemctl() { if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]] || ! has_command systemctl; then warning "Ignored systemd command: systemctl $@" return fi command systemctl "$@" } chcon() { if ! has_command chcon || [[ "x$FORCE_NO_SELINUX" == "x1" ]]; then return fi command chcon "$@" } get_systemd_version() { if ! has_command systemctl; then return fi command systemctl --version | head -1 | cut -d ' ' -f 2 } systemd_unit_working_directory() { local _systemd_version="$(get_systemd_version || true)" # WorkingDirectory=~ requires systemd v227 or later. # (released on Oct 2015, only CentOS 7 use an earlier version) # ref: systemd/systemd@5f5d8eab1f2f5f5e088bc301533b3e4636de96c7 if [[ -n "$_systemd_version" && "$_systemd_version" -lt "227" ]]; then echo "$HYSTERIA_HOME_DIR" return fi echo "~" } get_selinux_context() { local _file="$1" local _lsres="$(ls -dZ "$_file" | head -1)" local _sectx='' case "$(echo "$_lsres" | wc -w)" in 2) _sectx="$(echo "$_lsres" | cut -d ' ' -f 1)" ;; 5) _sectx="$(echo "$_lsres" | cut -d ' ' -f 4)" ;; *) ;; esac if [[ "x$_sectx" == "x?" ]]; then _sectx="" fi echo "$_sectx" } show_argument_error_and_exit() { local _error_msg="$1" error "$_error_msg" echo "Try \"$0 --help\" for usage." >&2 exit 22 } install_content() { local _install_flags="$1" local _content="$2" local _destination="$3" local _overwrite="$4" local _tmpfile="$(mktemp)" echo -ne "Install $_destination ... " echo "$_content" > "$_tmpfile" if [[ -z "$_overwrite" && -e "$_destination" ]]; then echo -e "exists" elif install "$_install_flags" "$_tmpfile" "$_destination"; then echo -e "ok" fi rm -f "$_tmpfile" } remove_file() { local _target="$1" echo -ne "Remove $_target ... " if rm "$_target"; then echo -e "ok" fi } exec_sudo() { # exec sudo with configurable environ preserved. local _saved_ifs="$IFS" IFS=$'\n' local _preserved_env=( $(env | grep "^PACKAGE_MANAGEMENT_INSTALL=" || true) $(env | grep "^OPERATING_SYSTEM=" || true) $(env | grep "^ARCHITECTURE=" || true) $(env | grep "^HYSTERIA_\w*=" || true) $(env | grep "^SECONTEXT_SYSTEMD_UNIT=" || true) $(env | grep "^FORCE_\w*=" || true) ) IFS="$_saved_ifs" exec sudo env \ "${_preserved_env[@]}" \ "$@" } detect_package_manager() { if [[ -n "$PACKAGE_MANAGEMENT_INSTALL" ]]; then return 0 fi if has_command apt; then apt update PACKAGE_MANAGEMENT_INSTALL='apt -y --no-install-recommends install' return 0 fi if has_command dnf; then PACKAGE_MANAGEMENT_INSTALL='dnf -y install' return 0 fi if has_command yum; then PACKAGE_MANAGEMENT_INSTALL='yum -y install' return 0 fi if has_command zypper; then PACKAGE_MANAGEMENT_INSTALL='zypper install -y --no-recommends' return 0 fi if has_command pacman; then PACKAGE_MANAGEMENT_INSTALL='pacman -Syu --noconfirm' return 0 fi return 1 } install_software() { local _package_name="$1" if ! detect_package_manager; then error "Supported package manager is not detected, please install the following package manually:" echo echo -e "\t* $_package_name" echo exit 65 fi echo "Installing missing dependence '$_package_name' with '$PACKAGE_MANAGEMENT_INSTALL' ... " if $PACKAGE_MANAGEMENT_INSTALL "$_package_name"; then echo "ok" else error "Cannot install '$_package_name' with detected package manager, please install it manually." exit 65 fi } is_user_exists() { local _user="$1" id "$_user" > /dev/null 2>&1 } rerun_with_sudo() { if ! has_command sudo; then return 13 fi local _target_script if has_prefix "$0" "/dev/" || has_prefix "$0" "/proc/"; then local _tmp_script="$(mktemp)" chmod +x "$_tmp_script" if has_command curl; then curl -o "$_tmp_script" 'https://get.hy2.sh/' elif has_command wget; then wget -O "$_tmp_script" 'https://get.hy2.sh' else return 127 fi _target_script="$_tmp_script" else _target_script="$0" fi note "Re-running this script with sudo. You can also specify FORCE_NO_ROOT=1 to force this script to run as the current user." exec_sudo "$_target_script" "${SCRIPT_ARGS[@]}" } check_permission() { if [[ "$UID" -eq '0' ]]; then return fi note "The user running this script is not root." case "$FORCE_NO_ROOT" in '1') warning "FORCE_NO_ROOT=1 detected, we will proceed without root, but you may get insufficient privileges errors." ;; *) if ! rerun_with_sudo; then error "Please run this script with root or specify FORCE_NO_ROOT=1 to force this script to run as the current user." exit 13 fi ;; esac } check_environment_operating_system() { if [[ -n "$OPERATING_SYSTEM" ]]; then warning "OPERATING_SYSTEM=$OPERATING_SYSTEM detected, operating system detection will not be performed." return fi if [[ "x$(uname)" == "xLinux" ]]; then OPERATING_SYSTEM=linux return fi error "This script only supports Linux." note "Specify OPERATING_SYSTEM=[linux|darwin|freebsd|windows] to bypass this check and force this script to run on this $(uname)." exit 95 } check_environment_architecture() { if [[ -n "$ARCHITECTURE" ]]; then warning "ARCHITECTURE=$ARCHITECTURE detected, architecture detection will not be performed." return fi case "$(uname -m)" in 'i386' | 'i686') ARCHITECTURE='386' ;; 'amd64' | 'x86_64') ARCHITECTURE='amd64' ;; 'armv5tel' | 'armv6l' | 'armv7' | 'armv7l') ARCHITECTURE='arm' ;; 'armv8' | 'aarch64') ARCHITECTURE='arm64' ;; 'mips' | 'mipsle' | 'mips64' | 'mips64le') ARCHITECTURE='mipsle' ;; 's390x') ARCHITECTURE='s390x' ;; *) error "The architecture '$(uname -a)' is not supported." note "Specify ARCHITECTURE= to bypass this check and force this script to run on this $(uname -m)." exit 8 ;; esac } check_environment_systemd() { if [[ -d "/run/systemd/system" ]] || grep -q systemd <(ls -l /sbin/init); then return fi case "$FORCE_NO_SYSTEMD" in '1') warning "FORCE_NO_SYSTEMD=1, we will proceed as normal even if systemd is not detected." ;; '2') warning "FORCE_NO_SYSTEMD=2, we will proceed but skip all systemd related commands." ;; *) error "This script only supports Linux distributions with systemd." note "Specify FORCE_NO_SYSTEMD=1 to disable this check and force this script to run as if systemd exists." note "Specify FORCE_NO_SYSTEMD=2 to disable this check and skip all systemd related commands." ;; esac } check_environment_selinux() { if ! has_command getenforce; then return fi note "SELinux is detected" if [[ "x$FORCE_NO_SELINUX" == "x1" ]]; then warning "FORCE_NO_SELINUX=1, we will skip all SELinux related commands." return fi if [[ -z "$SECONTEXT_SYSTEMD_UNIT" ]]; then if [[ -z "$FORCE_NO_SYSTEMD" ]] && [[ -e "$SYSTEMD_SERVICES_DIR" ]]; then local _sectx="$(get_selinux_context "$SYSTEMD_SERVICES_DIR")" if [[ -z "$_sectx" ]]; then warning "Failed to obtain SEContext of $SYSTEMD_SERVICES_DIR" else SECONTEXT_SYSTEMD_UNIT="$_sectx" fi fi fi } check_environment_curl() { if has_command curl; then return fi install_software curl } check_environment_grep() { if has_command grep; then return fi install_software grep } check_environment() { check_environment_operating_system check_environment_architecture check_environment_systemd check_environment_selinux check_environment_curl check_environment_grep } vercmp_segment() { local _lhs="$1" local _rhs="$2" if [[ "x$_lhs" == "x$_rhs" ]]; then echo 0 return fi if [[ -z "$_lhs" ]]; then echo -1 return fi if [[ -z "$_rhs" ]]; then echo 1 return fi local _lhs_num="${_lhs//[A-Za-z]*/}" local _rhs_num="${_rhs//[A-Za-z]*/}" if [[ "x$_lhs_num" == "x$_rhs_num" ]]; then echo 0 return fi if [[ -z "$_lhs_num" ]]; then echo -1 return fi if [[ -z "$_rhs_num" ]]; then echo 1 return fi local _numcmp=$(($_lhs_num - $_rhs_num)) if [[ "$_numcmp" -ne 0 ]]; then echo "$_numcmp" return fi local _lhs_suffix="${_lhs#"$_lhs_num"}" local _rhs_suffix="${_rhs#"$_rhs_num"}" if [[ "x$_lhs_suffix" == "x$_rhs_suffix" ]]; then echo 0 return fi if [[ -z "$_lhs_suffix" ]]; then echo 1 return fi if [[ -z "$_rhs_suffix" ]]; then echo -1 return fi if [[ "$_lhs_suffix" < "$_rhs_suffix" ]]; then echo -1 return fi echo 1 } vercmp() { local _lhs=${1#v} local _rhs=${2#v} while [[ -n "$_lhs" && -n "$_rhs" ]]; do local _clhs="${_lhs/.*/}" local _crhs="${_rhs/.*/}" local _segcmp="$(vercmp_segment "$_clhs" "$_crhs")" if [[ "$_segcmp" -ne 0 ]]; then echo "$_segcmp" return fi _lhs="${_lhs#"$_clhs"}" _lhs="${_lhs#.}" _rhs="${_rhs#"$_crhs"}" _rhs="${_rhs#.}" done if [[ "x$_lhs" == "x$_rhs" ]]; then echo 0 return fi if [[ -z "$_lhs" ]]; then echo -1 return fi if [[ -z "$_rhs" ]]; then echo 1 return fi return } check_hysteria_user() { local _default_hysteria_user="$1" if [[ -n "$HYSTERIA_USER" ]]; then return fi if [[ ! -e "$SYSTEMD_SERVICES_DIR/hysteria-server.service" ]]; then HYSTERIA_USER="$_default_hysteria_user" return fi HYSTERIA_USER="$(grep -o '^User=\w*' "$SYSTEMD_SERVICES_DIR/hysteria-server.service" | tail -1 | cut -d '=' -f 2 || true)" if [[ -z "$HYSTERIA_USER" ]]; then HYSTERIA_USER="$_default_hysteria_user" fi } check_hysteria_homedir() { local _default_hysteria_homedir="$1" if [[ -n "$HYSTERIA_HOME_DIR" ]]; then return fi if ! is_user_exists "$HYSTERIA_USER"; then HYSTERIA_HOME_DIR="$_default_hysteria_homedir" return fi HYSTERIA_HOME_DIR="$(eval echo ~"$HYSTERIA_USER")" } ### # ARGUMENTS PARSER ### show_usage_and_exit() { echo echo -e "\t$(tbold)$SCRIPT_NAME$(treset) - hysteria server install script" echo echo -e "Usage:" echo echo -e "$(tbold)Install hysteria$(treset)" echo -e "\t$0 [ -f | -l | --version ]" echo -e "Flags:" echo -e "\t-f, --force\tForce re-install latest or specified version even if it has been installed." echo -e "\t-l, --local \tInstall specified hysteria binary instead of download it." echo -e "\t--version \tInstall specified version instead of the latest." echo echo -e "$(tbold)Remove hysteria$(treset)" echo -e "\t$0 --remove" echo echo -e "$(tbold)Check for the update$(treset)" echo -e "\t$0 -c" echo -e "\t$0 --check" echo echo -e "$(tbold)Show this help$(treset)" echo -e "\t$0 -h" echo -e "\t$0 --help" exit 0 } parse_arguments() { while [[ "$#" -gt '0' ]]; do case "$1" in '--remove') if [[ -n "$OPERATION" && "$OPERATION" != 'remove' ]]; then show_argument_error_and_exit "Option '--remove' is in conflict with other options." fi OPERATION='remove' ;; '--version') VERSION="$2" if [[ -z "$VERSION" ]]; then show_argument_error_and_exit "Please specify the version for option '--version'." fi shift if ! has_prefix "$VERSION" 'v'; then show_argument_error_and_exit "Version numbers should begin with 'v' (such as 'v2.0.0'), got '$VERSION'" fi ;; '-c' | '--check') if [[ -n "$OPERATION" && "$OPERATION" != 'check' ]]; then show_argument_error_and_exit "Option '-c' or '--check' is in conflict with other options." fi OPERATION='check_update' ;; '-f' | '--force') FORCE='1' ;; '-h' | '--help') show_usage_and_exit ;; '-l' | '--local') LOCAL_FILE="$2" if [[ -z "$LOCAL_FILE" ]]; then show_argument_error_and_exit "Please specify the local binary to install for option '-l' or '--local'." fi break ;; *) show_argument_error_and_exit "Unknown option '$1'" ;; esac shift done if [[ -z "$OPERATION" ]]; then OPERATION='install' fi # validate arguments case "$OPERATION" in 'install') if [[ -n "$VERSION" && -n "$LOCAL_FILE" ]]; then show_argument_error_and_exit '--version and --local cannot be used together.' fi ;; *) if [[ -n "$VERSION" ]]; then show_argument_error_and_exit "--version is only valid for install operation." fi if [[ -n "$LOCAL_FILE" ]]; then show_argument_error_and_exit "--local is only valid for install operation." fi ;; esac } ### # FILE TEMPLATES ### # /etc/systemd/system/hysteria-server.service tpl_hysteria_server_service_base() { local _config_name="$1" cat << EOF [Unit] Description=Hysteria Server Service (${_config_name}.yaml) After=network.target [Service] Type=simple ExecStart=$EXECUTABLE_INSTALL_PATH server --config ${CONFIG_DIR}/${_config_name}.yaml WorkingDirectory=$(systemd_unit_working_directory) User=$HYSTERIA_USER Group=$HYSTERIA_USER Environment=HYSTERIA_LOG_LEVEL=info CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW NoNewPrivileges=true [Install] WantedBy=multi-user.target EOF } # /etc/systemd/system/hysteria-server.service tpl_hysteria_server_service() { tpl_hysteria_server_service_base 'config' } # /etc/systemd/system/hysteria-server@.service tpl_hysteria_server_x_service() { tpl_hysteria_server_service_base '%i' } # /etc/hysteria/config.yaml tpl_etc_hysteria_config_yaml() { cat << EOF # listen: :443 acme: domains: - your.domain.net email: your@email.com auth: type: password password: $(generate_random_password) masquerade: type: proxy proxy: url: https://news.ycombinator.com/ rewriteHost: true EOF } ### # SYSTEMD ### get_running_services() { if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then return fi systemctl list-units --state=active --plain --no-legend \ | grep -o "hysteria-server@*[^\s]*.service" || true } restart_running_services() { if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then return fi echo "Restarting running service ... " for service in $(get_running_services); do echo -ne "Restarting $service ... " systemctl restart "$service" echo "done" done } stop_running_services() { if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then return fi echo "Stopping running service ... " for service in $(get_running_services); do echo -ne "Stopping $service ... " systemctl stop "$service" echo "done" done } ### # HYSTERIA & GITHUB API ### is_hysteria_installed() { # RETURN VALUE # 0: hysteria is installed # 1: hysteria is not installed if [[ -f "$EXECUTABLE_INSTALL_PATH" || -h "$EXECUTABLE_INSTALL_PATH" ]]; then return 0 fi return 1 } is_hysteria1_version() { local _version="$1" has_prefix "$_version" "v1." || has_prefix "$_version" "v0." } get_installed_version() { if is_hysteria_installed; then if "$EXECUTABLE_INSTALL_PATH" version > /dev/null 2>&1; then "$EXECUTABLE_INSTALL_PATH" version | grep Version | grep -o 'v[.0-9]*' elif "$EXECUTABLE_INSTALL_PATH" -v > /dev/null 2>&1; then # hysteria 1 "$EXECUTABLE_INSTALL_PATH" -v | cut -d ' ' -f 3 fi fi } get_latest_version() { if [[ -n "$VERSION" ]]; then echo "$VERSION" return fi local _tmpfile=$(mktemp) if ! curl -sS "$HY2_API_BASE_URL/update?cver=installscript&plat=${OPERATING_SYSTEM}&arch=${ARCHITECTURE}&chan=release&side=server" -o "$_tmpfile"; then error "Failed to get the latest version from Hysteria 2 API, please check your network and try again." exit 11 fi local _latest_version=$(grep -oP '"lver":\s*\K"v.*?"' "$_tmpfile" | head -1) _latest_version=${_latest_version#'"'} _latest_version=${_latest_version%'"'} if [[ -n "$_latest_version" ]]; then echo "$_latest_version" fi rm -f "$_tmpfile" } download_hysteria() { local _version="$1" local _destination="$2" local _download_url="$REPO_URL/releases/download/app/$_version/hysteria-$OPERATING_SYSTEM-$ARCHITECTURE" echo "Downloading hysteria binary: $_download_url ..." if ! curl -R -H 'Cache-Control: no-cache' "$_download_url" -o "$_destination"; then error "Download failed, please check your network and try again." return 11 fi return 0 } check_update() { # RETURN VALUE # 0: update available # 1: installed version is latest echo -ne "Checking for installed version ... " local _installed_version="$(get_installed_version)" if [[ -n "$_installed_version" ]]; then echo "$_installed_version" else echo "not installed" fi echo -ne "Checking for latest version ... " local _latest_version="$(get_latest_version)" if [[ -n "$_latest_version" ]]; then echo "$_latest_version" VERSION="$_latest_version" else echo "failed" return 1 fi local _vercmp="$(vercmp "$_installed_version" "$_latest_version")" if [[ "$_vercmp" -lt 0 ]]; then return 0 fi return 1 } ### # ENTRY ### perform_install_hysteria_binary() { if [[ -n "$LOCAL_FILE" ]]; then note "Performing local install: $LOCAL_FILE" echo -ne "Installing hysteria executable ... " if install -Dm755 "$LOCAL_FILE" "$EXECUTABLE_INSTALL_PATH"; then echo "ok" else exit 2 fi return fi local _tmpfile=$(mktemp) if ! download_hysteria "$VERSION" "$_tmpfile"; then rm -f "$_tmpfile" exit 11 fi echo -ne "Installing hysteria executable ... " if install -Dm755 "$_tmpfile" "$EXECUTABLE_INSTALL_PATH"; then echo "ok" else exit 13 fi rm -f "$_tmpfile" } perform_remove_hysteria_binary() { remove_file "$EXECUTABLE_INSTALL_PATH" } perform_install_hysteria_example_config() { install_content -Dm644 "$(tpl_etc_hysteria_config_yaml)" "$CONFIG_DIR/config.yaml" "" } perform_install_hysteria_systemd() { if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then return fi install_content -Dm644 "$(tpl_hysteria_server_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server.service" "1" install_content -Dm644 "$(tpl_hysteria_server_x_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" "1" if [[ -n "$SECONTEXT_SYSTEMD_UNIT" ]]; then chcon "$SECONTEXT_SYSTEMD_UNIT" "$SYSTEMD_SERVICES_DIR/hysteria-server.service" chcon "$SECONTEXT_SYSTEMD_UNIT" "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" fi systemctl daemon-reload } perform_remove_hysteria_systemd() { remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server.service" remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" systemctl daemon-reload } perform_install_hysteria_home_legacy() { if ! is_user_exists "$HYSTERIA_USER"; then echo -ne "Creating user $HYSTERIA_USER ... " useradd -r -d "$HYSTERIA_HOME_DIR" -m "$HYSTERIA_USER" echo "ok" fi } perform_install() { local _is_frash_install local _is_upgrade_from_hysteria1 if ! is_hysteria_installed; then _is_frash_install=1 elif is_hysteria1_version "$(get_installed_version)"; then _is_upgrade_from_hysteria1=1 fi local _is_update_required if [[ -n "$LOCAL_FILE" ]] || [[ -n "$VERSION" ]] || check_update; then _is_update_required=1 fi if [[ "x$FORCE" == "x1" ]]; then if [[ -z "$_is_update_required" ]]; then note "Option '--force' detected, re-install even if installed version is the latest." fi _is_update_required=1 fi if is_hysteria1_version "$VERSION"; then error "This script can only install Hysteria 2." exit 95 fi if [[ -n "$_is_update_required" ]]; then perform_install_hysteria_binary fi # Always install additional files, regardless of $_is_update_required. # This allows changes to be made with environment variables (e.g. change HYSTERIA_USER without --force). perform_install_hysteria_example_config perform_install_hysteria_home_legacy perform_install_hysteria_systemd if [[ -z "$_is_update_required" ]]; then echo echo "$(tgreen)Installed version is up-to-date, there is nothing to do.$(treset)" echo elif [[ -n "$_is_frash_install" ]]; then echo echo -e "$(tbold)Congratulation! Hysteria 2 has been successfully installed on your server.$(treset)" echo echo -e "What's next?" echo echo -e "\t+ Take a look at the differences between Hysteria 2 and Hysteria 1 at https://hysteria.network/docs/misc/2-vs-1/" echo -e "\t+ Check out the quick server config guide at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)" echo -e "\t+ Edit server config file at $(tred)$CONFIG_DIR/config.yaml$(treset)" echo -e "\t+ Start your hysteria server with $(tred)systemctl start hysteria-server.service$(treset)" echo -e "\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)" echo elif [[ -n "$_is_upgrade_from_hysteria1" ]]; then echo -e "Skip automatic service restart due to $(tred)incompatible$(treset) upgrade." echo echo -e "$(tbold)Hysteria has been successfully update to $VERSION from Hysteria 1.$(treset)" echo echo -e "$(tred)Hysteria 2 uses a completely redesigned protocol & config, which is NOT compatible with the version 1.x.x in any way.$(treset)" echo echo -e "\t+ Take a look at the behavior changes in Hysteria 2 at $(tblue)https://hysteria.network/docs/misc/2-vs-1/$(treset)" echo -e "\t+ Check out the quick server configuration guide for Hysteria 2 at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)" echo -e "\t+ Migrate server config file to the Hysteria 2 at $(tred)$CONFIG_DIR/config.yaml$(treset)" echo -e "\t+ Start your hysteria server with $(tred)systemctl restart hysteria-server.service$(treset)" echo -e "\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)" else restart_running_services echo echo -e "$(tbold)Hysteria has been successfully update to $VERSION.$(treset)" echo echo -e "Check out the latest changelog $(tblue)https://github.com/apernet/hysteria/blob/master/CHANGELOG.md$(treset)" echo fi } perform_remove() { perform_remove_hysteria_binary stop_running_services perform_remove_hysteria_systemd echo echo -e "$(tbold)Congratulation! Hysteria has been successfully removed from your server.$(treset)" echo echo -e "You still need to remove configuration files and ACME certificates manually with the following commands:" echo echo -e "\t$(tred)rm -rf "$CONFIG_DIR"$(treset)" if [[ "x$HYSTERIA_USER" != "xroot" ]]; then echo -e "\t$(tred)userdel -r "$HYSTERIA_USER"$(treset)" fi if [[ "x$FORCE_NO_SYSTEMD" != "x2" ]]; then echo echo -e "You still might need to disable all related systemd services with the following commands:" echo echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server.service$(treset)" echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server@*.service$(treset)" echo -e "\t$(tred)systemctl daemon-reload$(treset)" fi echo } perform_check_update() { if check_update; then echo echo -e "$(tbold)Update available: $VERSION$(treset)" echo echo -e "$(tgreen)You can download and install the latest version by execute this script without any arguments.$(treset)" echo else echo echo "$(tgreen)Installed version is up-to-date.$(treset)" echo fi } main() { parse_arguments "$@" check_permission check_environment check_hysteria_user "hysteria" check_hysteria_homedir "/var/lib/$HYSTERIA_USER" case "$OPERATION" in "install") perform_install ;; "remove") perform_remove ;; "check_update") perform_check_update ;; *) error "Unknown operation '$OPERATION'." ;; esac } main "$@" # vim:set ft=bash ts=2 sw=2 sts=2 et: