[
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: [ 'https://v2.hysteria.network/docs/Donation/' ]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Report anything you think is a bug and needs to be fixed.\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Logs**\nAttach logs from the client/server when the error occurs.\n\n**Device and Operating System**\nWhat are you using it on.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.zh.md",
    "content": "---\nname: Bug 反馈\nabout: 反馈任何你认为是 bug 需要修复的问题。\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**描述问题**\n请尽量清晰精准地描述你遇到的问题。\n\n**如何复现**\n复现问题的步骤。\n\n**预期行为**\n你认为修复后的行为应该是怎样的。\n\n**日志**\n附上客户端/服务器端在错误发生前后的日志。\n\n**设备和操作系统**\n你在用什么设备和操作系统。\n\n**额外信息**\n其他你认为有助于解决问题的信息。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project.\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.zh.md",
    "content": "---\nname: 功能请求\nabout: 为这个项目提出改进意见。\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**你的功能请求是否与某个问题有关？**\n请尽量清晰精准地描述你遇到的问题。例如：我家运营商限制 UDP 协议速度，导致 Hysteria 很慢，希望增加 FakeTCP 支持。\n\n**描述你希望的解决方案**\n请尽量清晰精准地描述你希望的解决方案。\n\n**有没有其他替代方案**\n请尽量清晰精准地描述你认为可能的替代方案。\n\n**额外信息**\n其他你认为有助于开发者了解你需求的信息。\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\""
  },
  {
    "path": ".github/workflows/autotag.yaml",
    "content": "name: \"Create release tags for nested modules\"\n\non:\n  push:\n    tags:\n      - app/v*.*.*\n\npermissions:\n  contents: write\n\njobs:\n  tag:\n    name: \"Create tags\"\n    runs-on: ubuntu-latest\n    steps:\n      - name: \"Extract tagbase\"\n        id: extract_tagbase\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const ref = context.ref;\n            core.info(`context.ref: ${ref}`);\n            const refPrefix = 'refs/tags/app/';\n            if (!ref.startsWith(refPrefix)) {\n              core.setFailed(`context.ref does not start with ${refPrefix}: ${ref}`);\n              return;\n            }\n            const tagbase = ref.slice(refPrefix.length);\n            core.info(`tagbase: ${tagbase}`);\n            core.setOutput('tagbase', tagbase);\n\n      - name: \"Tagging core/*\"\n        uses: actions/github-script@v7\n        env:\n          INPUT_TAGPREFIX: \"core/\"\n          INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}\n        with:\n          script: |\n            const tagbase = core.getInput('tagbase', { required: true });\n            const tagprefix = core.getInput('tagprefix', { required: true });\n            const refname = `tags/${tagprefix}${tagbase}`;\n            core.info(`creating ref ${refname}`);\n            try {\n              await github.rest.git.createRef({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                ref: `refs/${refname}`,\n                sha: context.sha\n              });\n              core.info(`created ref ${refname}`);\n              return;\n            } catch (error) {\n              core.info(`failed to create ref ${refname}: ${error}`);\n            }\n            core.info(`updating ref ${refname}`)\n            try {\n              await github.rest.git.updateRef({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                ref: refname,\n                sha: context.sha\n              });\n              core.info(`updated ref ${refname}`);\n              return;\n            } catch (error) {\n              core.setFailed(`failed to update ref ${refname}: ${error}`);\n            }\n\n      - name: \"Tagging extras/*\"\n        uses: actions/github-script@v7\n        env:\n          INPUT_TAGPREFIX: \"extras/\"\n          INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}\n        with:\n          script: |\n            const tagbase = core.getInput('tagbase', { required: true });\n            const tagprefix = core.getInput('tagprefix', { required: true });\n            const refname = `tags/${tagprefix}${tagbase}`;\n            core.info(`creating ref ${refname}`);\n            try {\n              await github.rest.git.createRef({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                ref: `refs/${refname}`,\n                sha: context.sha\n              });\n              core.info(`created ref ${refname}`);\n              return;\n            } catch (error) {\n              core.info(`failed to create ref ${refname}: ${error}`);\n            }\n            core.info(`updating ref ${refname}`)\n            try {\n              await github.rest.git.updateRef({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                ref: refname,\n                sha: context.sha\n              });\n              core.info(`updated ref ${refname}`);\n              return;\n            } catch (error) {\n              core.setFailed(`failed to update ref ${refname}: ${error}`);\n            }\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Docker\n\n# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\non:\n  push:\n    branches: [ \"master\" ]\n    # Publish semver tags as releases.\n    tags: [ 'v*.*.*' ]\n\nenv:\n  # Use docker.io for Docker Hub if empty\n  REGISTRY: ghcr.io\n  # github.repository as <account>/<repo>\n  IMAGE_NAME: ${{ github.repository }}\n\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n      # This is used to complete the identity challenge\n      # with sigstore/fulcio when running outside of PRs.\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Install cosign\n        uses: sigstore/cosign-installer@v3.4.0\n        with:\n          cosign-release: 'v2.2.2'\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3.2.0\n\n      # Login against a Docker registry except on PR\n      # https://github.com/docker/login-action\n      - name: Log into registry ${{ env.REGISTRY }}\n        uses: docker/login-action@v3.1.0\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract Docker metadata\n        id: meta\n        uses: docker/metadata-action@v5.5.1\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n      \n      - name: Get version\n        id: get_version\n        run: echo \"version=$(git describe --tags --always)\" >> $GITHUB_OUTPUT\n\n      - name: Build and push Docker image\n        id: build-and-push\n        uses: docker/build-push-action@v5.3.0\n        with:\n          context: .\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: |\n            ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria\n            ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria:latest\n            ${{ env.REGISTRY }}/${{ github.repository_owner }}/hysteria:${{ steps.get_version.outputs.version }}\n      # Sign the resulting Docker image digest except on PRs.\n      # This will only write to the public Rekor transparency log when the Docker\n      # repository is public to avoid leaking data.  If you would like to publish\n      # transparency data even for private images, pass --force to cosign below.\n      # https://github.com/sigstore/cosign\n      - name: Sign the published Docker image\n        env:\n          # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable\n          TAGS: ${{ steps.meta.outputs.tags }}\n          DIGEST: ${{ steps.build-and-push.outputs.digest }}\n        # This step uses the identity token to provision an ephemeral certificate\n        # against the sigstore community Fulcio instance.\n        run: echo \"${TAGS}\" | xargs -I {} cosign sign --yes {}@${DIGEST}\n        \n"
  },
  {
    "path": ".github/workflows/master.yml",
    "content": "name: \"Build master branch\"\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    env:\n      ACTIONS_ALLOW_UNSECURE_COMMANDS: true\n\n    steps:\n      - name: Check out\n        uses: actions/checkout@v4\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.23\"\n\n      - name: Setup Python # This is for the build script\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.11\"\n\n      - uses: nttld/setup-ndk@v1\n        id: setup-ndk\n        with:\n          ndk-version: r26b\n          add-to-path: false\n\n      - name: Run build script\n        env:\n          ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}\n        run: |\n          export HY_APP_PLATFORMS=$(sed 's/\\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd \",\")\n          python hyperbole.py build -r\n\n      - name: Generate hashes\n        run: |\n          for file in build/*; do\n            sha256sum $file >> build/hashes.txt\n          done\n\n      - name: Archive\n        uses: actions/upload-artifact@v4\n        with:\n          name: hysteria-master-${{ github.sha }}\n          path: build\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: \"Build release\"\n\non:\n  push:\n    tags:\n      - app/v*.*.*\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    env:\n      ACTIONS_ALLOW_UNSECURE_COMMANDS: true\n\n    steps:\n      - name: Check out\n        uses: actions/checkout@v4\n\n      - name: Get version\n        id: get_version\n        run: echo \"version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\\([^/-]*\\)\\(-.*\\)\\{0,1\\}|\\1|p')\" >> $GITHUB_OUTPUT\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.23\"\n\n      - name: Setup Python # This is for the build script\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.11\"\n\n      - uses: nttld/setup-ndk@v1\n        id: setup-ndk\n        with:\n          ndk-version: r26b\n          add-to-path: false\n\n      - name: Run build script\n        env:\n          ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}\n        run: |\n          export HY_APP_PLATFORMS=$(sed 's/\\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd \",\")\n          python hyperbole.py build -r\n\n      - name: Generate hashes\n        run: |\n          for file in build/*; do\n            sha256sum $file >> build/hashes.txt\n          done\n\n      - name: Upload GitHub\n        uses: softprops/action-gh-release@v2\n        with:\n          files: build/*\n\n      - name: Upload CF bucket\n        uses: shallwefootball/upload-s3-action@v1.3.3\n        with:\n          aws_key_id: ${{ secrets.CF_KEY_ID }}\n          aws_secret_access_key: ${{ secrets.CF_KEY }}\n          aws_bucket: \"hydownload\"\n          endpoint: \"https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/\"\n          source_dir: \"build\"\n          destination_dir: \"app/${{ steps.get_version.outputs.version }}\"\n\n      - name: Publish to API\n        run: |\n          export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }}\n          pip install requests\n          python hyperbole.py publish\n"
  },
  {
    "path": ".github/workflows/scripts.yml",
    "content": "name: \"Publish scripts\"\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - scripts/**\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      deployments: write\n    name: Publish scripts to Cloudflare Pages\n    steps:\n      - name: Check out\n        uses: actions/checkout@v4\n\n      - name: Publish to Cloudflare Pages\n        uses: cloudflare/pages-action@v1\n        with:\n          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\n          projectName: hy2scripts\n          directory: scripts\n          gitHubToken: ${{ secrets.GITHUB_TOKEN }}\n          branch: main\n"
  },
  {
    "path": ".gitignore",
    "content": "# Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all\n\n### Go ###\n# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\n\n### GoLand+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### GoLand+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n### Intellij+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n\n# AWS User-specific\n\n# Generated files\n\n# Sensitive or high-churn files\n\n# Gradle\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\n\n# Mongo Explorer plugin\n\n# File-based project format\n\n# IntelliJ\n\n# mpeltonen/sbt-idea plugin\n\n# JIRA plugin\n\n# Cursive Clojure plugin\n\n# SonarLint plugin\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\n\n# Editor-based Rest Client\n\n# Android studio 3.1+ serialized cache file\n\n### Intellij+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n\n\n### Linux ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### macOS ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### macOS Patch ###\n# iCloud generated files\n*.icloud\n\n### PyCharm+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n\n# AWS User-specific\n\n# Generated files\n\n# Sensitive or high-churn files\n\n# Gradle\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\n\n# Mongo Explorer plugin\n\n# File-based project format\n\n# IntelliJ\n\n# mpeltonen/sbt-idea plugin\n\n# JIRA plugin\n\n# Cursive Clojure plugin\n\n# SonarLint plugin\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\n\n# Editor-based Rest Client\n\n# Android studio 3.1+ serialized cache file\n\n### PyCharm+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n### Python Patch ###\n# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration\npoetry.toml\n\n# ruff\n.ruff_cache/\n\n# LSP config files\npyrightconfig.json\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nThumbs.db:encryptable\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nhttps://v2.hysteria.network/docs/Changelog/\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1-alpine AS builder\n\n# GOPROXY is disabled by default, use:\n# docker build --build-arg GOPROXY=\"https://goproxy.io\" ...\n# to enable GOPROXY.\nARG GOPROXY=\"\"\n\nENV GOPROXY ${GOPROXY}\n\nCOPY . /go/src/github.com/apernet/hysteria\n\nWORKDIR /go/src/github.com/apernet/hysteria\n\nRUN set -ex \\\n    && apk add git build-base bash python3 \\\n    && python hyperbole.py build -r \\\n    && mv ./build/hysteria-* /go/bin/hysteria\n\n# multi-stage builds to create the final image\nFROM alpine AS dist\n\n# set up nsswitch.conf for Go's \"netgo\" implementation\n# - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275\n# - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf\nRUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch.conf; fi\n\n# bash is used for debugging, tzdata is used to add timezone information.\n# Install ca-certificates to ensure no CA certificate errors.\n#\n# Do not try to add the \"--no-cache\" option when there are multiple \"apk\"\n# commands, this will cause the build process to become very slow.\nCOPY ./entrypoint /usr/local/bin/entrypoint\nRUN set -ex \\\n    && apk upgrade \\\n    && apk add bash tzdata ca-certificates \\\n    && rm -rf /var/cache/apk/* \\\n    && chmod +x /usr/local/bin/entrypoint\n\nCOPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria\nCMD [\"entrypoint\"]"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright 2023 Toby\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n"
  },
  {
    "path": "PROTOCOL.md",
    "content": "# Hysteria 2 Protocol Specification\n\nHysteria 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\".\n\n## Requirements Language\n\nThe 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).\n\n## Underlying Protocol & Wire Format\n\nThe 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/).\n\nAll multibyte numbers use Big Endian format.\n\nAll variable-length integers (\"varints\") are encoded/decoded as defined in QUIC (RFC 9000).\n\n## Authentication & HTTP/3 masquerading\n\nOne 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.\n\nTherefore, 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.\n\nAn actual Hysteria client, upon connection, MUST send the following HTTP/3 request to the server:\n\n```\n:method: POST\n:path: /auth\n:host: hysteria\nHysteria-Auth: [string]\nHysteria-CC-RX: [uint]\nHysteria-Padding: [string]\n```\n\n`Hysteria-Auth`: Authentication credentials.\n\n`Hysteria-CC-RX`: Client's maximum receive rate in bytes per second. A value of 0 indicates unknown.\n\n`Hysteria-Padding`: A random padding string of variable length.\n\nThe 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):\n\n```\n:status: 233 HyOK\nHysteria-UDP: [true/false]\nHysteria-CC-RX: [uint/\"auto\"]\nHysteria-Padding: [string]\n```\n\n`Hysteria-UDP`: Whether the server supports UDP relay.\n\n`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.\n\n`Hysteria-Padding`: A random padding string of variable length.\n\nSee the Congestion Control section for more information on how to use the `Hysteria-CC-RX` values.\n\n`Hysteria-Padding` is optional and is only intended to obfuscate the request/response pattern. It SHOULD be ignored by both sides.\n\nIf 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.\n\nThe 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.\n\nAfter (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.\n\n## Proxy Requests\n\n### TCP\n\nFor each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message:\n\n```\n[varint] 0x401 (TCPRequest ID)\n[varint] Address length\n[bytes] Address string (host:port)\n[varint] Padding length\n[bytes] Random padding\n```\n\nThe server MUST respond with a TCPResponse message:\n\n```\n[uint8] Status (0x00 = OK, 0x01 = Error)\n[varint] Message length\n[bytes] Message string\n[varint] Padding length\n[bytes] Random padding\n```\n\nIf 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.\n\n### UDP\n\nUDP 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):\n\n```\n[uint32] Session ID\n[uint16] Packet ID\n[uint8] Fragment ID\n[uint8] Fragment count\n[varint] Address length\n[bytes] Address string (host:port)\n[bytes] Payload\n```\n\nThe 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.).\n\nThe 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.\n\nIf a server does not support UDP relay, it SHOULD silently discard all UDP messages received from the client.\n\n#### Fragmentation\n\nDue 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.\n\nFor 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.\n\nFor 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.\n\n## Congestion Control\n\nA 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.\n\nThree special cases are:\n\n- 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.\n- If the server responds with 0, it has no bandwidth limit. The client MAY transmit at any rate it wants.\n- 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.\n\n## \"Salamander\" Obfuscation\n\nThe Hysteria protocol supports an optional obfuscation layer codenamed \"Salamander\".\n\n\"Salamander\" encapsulates all QUIC packets in the following format:\n\n```\n[8 bytes] Salt\n[bytes] Payload\n```\n\nFor 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.\n\n```\nhash = BLAKE2b-256(key + salt)\n```\n\nThe hash is then used to obfuscate the payload using the following algorithm:\n\n```\nfor i in range(0, len(payload)):\n    payload[i] ^= hash[i % 32]\n```\n\nThe deobfuscator MUST use the same algorithms to calculate the salted hash and deobfuscate the payload. Any invalid packet MUST be discarded.\n"
  },
  {
    "path": "README.md",
    "content": "# ![Hysteria 2](logo.svg)\n\n# 支持对接V2board面板的Hysteria2后端\n\n### 项目说明\n本项目基于hysteria官方内核二次开发，添加了从v2b获取节点信息、用户鉴权信息与上报用户流量的功能。\n性能方面已经由hysteria2内核作者亲自指导优化过了。\n\n### TG交流群\n欢迎加入交流群 [点击加入](https://t.me/+DcRt8AB2VbI2Yzc1)\n\n\n### 示例配置\n```\nv2board:\n  apiHost: https://面板地址\n  apiKey: 面板节点密钥\n  nodeID: 节点ID\ntls:\n  type: tls\n  cert: /etc/hysteria/tls.crt\n  key: /etc/hysteria/tls.key\nauth:\n  type: v2board\ntrafficStats:\n  listen: 127.0.0.1:7653\nacl: \n  inline: \n    - reject(10.0.0.0/8)\n    - reject(172.16.0.0/12)\n    - reject(192.168.0.0/16)\n    - reject(127.0.0.0/8)\n    - reject(fc00::/7)\n```\n> 其他配置完全与hysteria文档的一致，可以查看hysteria2官方文档 [点击查看](https://hysteria.network/zh/docs/getting-started/Installation/) \n\n### 快速启动\n```\ndocker run -itd --restart=always  --network=host \\\n -e apiHost=https://example.com \\\n -e apiKey=xxxxxxxxxxxxxxxxxxxxx \\\n -e domain=hy2.example.com  \\\n -e nodeID=1 \\\nghcr.io/cedar2025/hysteria:latest\n```\n### docker 仓库\n```\ndocker pull ghcr.io/cedar2025/hysteria:latest\n```"
  },
  {
    "path": "app/cmd/client.go",
    "content": "package cmd\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"go.uber.org/zap\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/forwarding\"\n\t\"github.com/apernet/hysteria/app/v2/internal/http\"\n\t\"github.com/apernet/hysteria/app/v2/internal/proxymux\"\n\t\"github.com/apernet/hysteria/app/v2/internal/redirect\"\n\t\"github.com/apernet/hysteria/app/v2/internal/sockopts\"\n\t\"github.com/apernet/hysteria/app/v2/internal/socks5\"\n\t\"github.com/apernet/hysteria/app/v2/internal/tproxy\"\n\t\"github.com/apernet/hysteria/app/v2/internal/tun\"\n\t\"github.com/apernet/hysteria/app/v2/internal/url\"\n\t\"github.com/apernet/hysteria/app/v2/internal/utils\"\n\t\"github.com/apernet/hysteria/core/v2/client\"\n\t\"github.com/apernet/hysteria/extras/v2/correctnet\"\n\t\"github.com/apernet/hysteria/extras/v2/obfs\"\n\t\"github.com/apernet/hysteria/extras/v2/transport/udphop\"\n)\n\n// Client flags\nvar (\n\tshowQR bool\n)\n\nvar clientCmd = &cobra.Command{\n\tUse:   \"client\",\n\tShort: \"Client mode\",\n\tRun:   runClient,\n}\n\nfunc init() {\n\tinitClientFlags()\n\trootCmd.AddCommand(clientCmd)\n}\n\nfunc initClientFlags() {\n\tclientCmd.Flags().BoolVar(&showQR, \"qr\", false, \"show QR code for server config sharing\")\n}\n\ntype clientConfig struct {\n\tServer        string                `mapstructure:\"server\"`\n\tAuth          string                `mapstructure:\"auth\"`\n\tTransport     clientConfigTransport `mapstructure:\"transport\"`\n\tObfs          clientConfigObfs      `mapstructure:\"obfs\"`\n\tTLS           clientConfigTLS       `mapstructure:\"tls\"`\n\tQUIC          clientConfigQUIC      `mapstructure:\"quic\"`\n\tBandwidth     clientConfigBandwidth `mapstructure:\"bandwidth\"`\n\tFastOpen      bool                  `mapstructure:\"fastOpen\"`\n\tLazy          bool                  `mapstructure:\"lazy\"`\n\tSOCKS5        *socks5Config         `mapstructure:\"socks5\"`\n\tHTTP          *httpConfig           `mapstructure:\"http\"`\n\tTCPForwarding []tcpForwardingEntry  `mapstructure:\"tcpForwarding\"`\n\tUDPForwarding []udpForwardingEntry  `mapstructure:\"udpForwarding\"`\n\tTCPTProxy     *tcpTProxyConfig      `mapstructure:\"tcpTProxy\"`\n\tUDPTProxy     *udpTProxyConfig      `mapstructure:\"udpTProxy\"`\n\tTCPRedirect   *tcpRedirectConfig    `mapstructure:\"tcpRedirect\"`\n\tTUN           *tunConfig            `mapstructure:\"tun\"`\n}\n\ntype clientConfigTransportUDP struct {\n\tHopInterval time.Duration `mapstructure:\"hopInterval\"`\n}\n\ntype clientConfigTransport struct {\n\tType string                   `mapstructure:\"type\"`\n\tUDP  clientConfigTransportUDP `mapstructure:\"udp\"`\n}\n\ntype clientConfigObfsSalamander struct {\n\tPassword string `mapstructure:\"password\"`\n}\n\ntype clientConfigObfs struct {\n\tType       string                     `mapstructure:\"type\"`\n\tSalamander clientConfigObfsSalamander `mapstructure:\"salamander\"`\n}\n\ntype clientConfigTLS struct {\n\tSNI       string `mapstructure:\"sni\"`\n\tInsecure  bool   `mapstructure:\"insecure\"`\n\tPinSHA256 string `mapstructure:\"pinSHA256\"`\n\tCA        string `mapstructure:\"ca\"`\n}\n\ntype clientConfigQUIC struct {\n\tInitStreamReceiveWindow     uint64                   `mapstructure:\"initStreamReceiveWindow\"`\n\tMaxStreamReceiveWindow      uint64                   `mapstructure:\"maxStreamReceiveWindow\"`\n\tInitConnectionReceiveWindow uint64                   `mapstructure:\"initConnReceiveWindow\"`\n\tMaxConnectionReceiveWindow  uint64                   `mapstructure:\"maxConnReceiveWindow\"`\n\tMaxIdleTimeout              time.Duration            `mapstructure:\"maxIdleTimeout\"`\n\tKeepAlivePeriod             time.Duration            `mapstructure:\"keepAlivePeriod\"`\n\tDisablePathMTUDiscovery     bool                     `mapstructure:\"disablePathMTUDiscovery\"`\n\tSockopts                    clientConfigQUICSockopts `mapstructure:\"sockopts\"`\n}\n\ntype clientConfigQUICSockopts struct {\n\tBindInterface       *string `mapstructure:\"bindInterface\"`\n\tFirewallMark        *uint32 `mapstructure:\"fwmark\"`\n\tFdControlUnixSocket *string `mapstructure:\"fdControlUnixSocket\"`\n}\n\ntype clientConfigBandwidth struct {\n\tUp   string `mapstructure:\"up\"`\n\tDown string `mapstructure:\"down\"`\n}\n\ntype socks5Config struct {\n\tListen     string `mapstructure:\"listen\"`\n\tUsername   string `mapstructure:\"username\"`\n\tPassword   string `mapstructure:\"password\"`\n\tDisableUDP bool   `mapstructure:\"disableUDP\"`\n}\n\ntype httpConfig struct {\n\tListen   string `mapstructure:\"listen\"`\n\tUsername string `mapstructure:\"username\"`\n\tPassword string `mapstructure:\"password\"`\n\tRealm    string `mapstructure:\"realm\"`\n}\n\ntype tcpForwardingEntry struct {\n\tListen string `mapstructure:\"listen\"`\n\tRemote string `mapstructure:\"remote\"`\n}\n\ntype udpForwardingEntry struct {\n\tListen  string        `mapstructure:\"listen\"`\n\tRemote  string        `mapstructure:\"remote\"`\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n}\n\ntype tcpTProxyConfig struct {\n\tListen string `mapstructure:\"listen\"`\n}\n\ntype udpTProxyConfig struct {\n\tListen  string        `mapstructure:\"listen\"`\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n}\n\ntype tcpRedirectConfig struct {\n\tListen string `mapstructure:\"listen\"`\n}\n\ntype tunConfig struct {\n\tName    string        `mapstructure:\"name\"`\n\tMTU     uint32        `mapstructure:\"mtu\"`\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n\tAddress struct {\n\t\tIPv4 string `mapstructure:\"ipv4\"`\n\t\tIPv6 string `mapstructure:\"ipv6\"`\n\t} `mapstructure:\"address\"`\n\tRoute *struct {\n\t\tStrict      bool     `mapstructure:\"strict\"`\n\t\tIPv4        []string `mapstructure:\"ipv4\"`\n\t\tIPv6        []string `mapstructure:\"ipv6\"`\n\t\tIPv4Exclude []string `mapstructure:\"ipv4Exclude\"`\n\t\tIPv6Exclude []string `mapstructure:\"ipv6Exclude\"`\n\t} `mapstructure:\"route\"`\n}\n\nfunc (c *clientConfig) fillServerAddr(hyConfig *client.Config) error {\n\tif c.Server == \"\" {\n\t\treturn configError{Field: \"server\", Err: errors.New(\"server address is empty\")}\n\t}\n\tvar addr net.Addr\n\tvar err error\n\thost, port, hostPort := parseServerAddrString(c.Server)\n\tif !isPortHoppingPort(port) {\n\t\taddr, err = net.ResolveUDPAddr(\"udp\", hostPort)\n\t} else {\n\t\taddr, err = udphop.ResolveUDPHopAddr(hostPort)\n\t}\n\tif err != nil {\n\t\treturn configError{Field: \"server\", Err: err}\n\t}\n\thyConfig.ServerAddr = addr\n\t// Special handling for SNI\n\tif c.TLS.SNI == \"\" {\n\t\t// Use server hostname as SNI\n\t\thyConfig.TLSConfig.ServerName = host\n\t}\n\treturn nil\n}\n\n// fillConnFactory must be called after fillServerAddr, as we have different logic\n// for ConnFactory depending on whether we have a port hopping address.\nfunc (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {\n\tso := &sockopts.SocketOptions{\n\t\tBindInterface:       c.QUIC.Sockopts.BindInterface,\n\t\tFirewallMark:        c.QUIC.Sockopts.FirewallMark,\n\t\tFdControlUnixSocket: c.QUIC.Sockopts.FdControlUnixSocket,\n\t}\n\tif err := so.CheckSupported(); err != nil {\n\t\tvar unsupportedErr *sockopts.UnsupportedError\n\t\tif errors.As(err, &unsupportedErr) {\n\t\t\treturn configError{\n\t\t\t\tField: \"quic.sockopts.\" + unsupportedErr.Field,\n\t\t\t\tErr:   errors.New(\"unsupported on this platform\"),\n\t\t\t}\n\t\t}\n\t\treturn configError{Field: \"quic.sockopts\", Err: err}\n\t}\n\t// Inner PacketConn\n\tvar newFunc func(addr net.Addr) (net.PacketConn, error)\n\tswitch strings.ToLower(c.Transport.Type) {\n\tcase \"\", \"udp\":\n\t\tif hyConfig.ServerAddr.Network() == \"udphop\" {\n\t\t\thopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr)\n\t\t\tnewFunc = func(addr net.Addr) (net.PacketConn, error) {\n\t\t\t\treturn udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, so.ListenUDP)\n\t\t\t}\n\t\t} else {\n\t\t\tnewFunc = func(addr net.Addr) (net.PacketConn, error) {\n\t\t\t\treturn so.ListenUDP()\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn configError{Field: \"transport.type\", Err: errors.New(\"unsupported transport type\")}\n\t}\n\t// Obfuscation\n\tvar ob obfs.Obfuscator\n\tvar err error\n\tswitch strings.ToLower(c.Obfs.Type) {\n\tcase \"\", \"plain\":\n\t\t// Keep it nil\n\tcase \"salamander\":\n\t\tob, err = obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"obfs.salamander.password\", Err: err}\n\t\t}\n\tdefault:\n\t\treturn configError{Field: \"obfs.type\", Err: errors.New(\"unsupported obfuscation type\")}\n\t}\n\thyConfig.ConnFactory = &adaptiveConnFactory{\n\t\tNewFunc:    newFunc,\n\t\tObfuscator: ob,\n\t}\n\treturn nil\n}\n\nfunc (c *clientConfig) fillAuth(hyConfig *client.Config) error {\n\thyConfig.Auth = c.Auth\n\treturn nil\n}\n\nfunc (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error {\n\tif c.TLS.SNI != \"\" {\n\t\thyConfig.TLSConfig.ServerName = c.TLS.SNI\n\t}\n\thyConfig.TLSConfig.InsecureSkipVerify = c.TLS.Insecure\n\tif c.TLS.PinSHA256 != \"\" {\n\t\tnHash := normalizeCertHash(c.TLS.PinSHA256)\n\t\thyConfig.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {\n\t\t\tfor _, cert := range rawCerts {\n\t\t\t\thash := sha256.Sum256(cert)\n\t\t\t\thashHex := hex.EncodeToString(hash[:])\n\t\t\t\tif hashHex == nHash {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\t// No match\n\t\t\treturn errors.New(\"no certificate matches the pinned hash\")\n\t\t}\n\t}\n\tif c.TLS.CA != \"\" {\n\t\tca, err := os.ReadFile(c.TLS.CA)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"tls.ca\", Err: err}\n\t\t}\n\t\tcPool := x509.NewCertPool()\n\t\tif !cPool.AppendCertsFromPEM(ca) {\n\t\t\treturn configError{Field: \"tls.ca\", Err: errors.New(\"failed to parse CA certificate\")}\n\t\t}\n\t\thyConfig.TLSConfig.RootCAs = cPool\n\t}\n\treturn nil\n}\n\nfunc (c *clientConfig) fillQUICConfig(hyConfig *client.Config) error {\n\thyConfig.QUICConfig = client.QUICConfig{\n\t\tInitialStreamReceiveWindow:     c.QUIC.InitStreamReceiveWindow,\n\t\tMaxStreamReceiveWindow:         c.QUIC.MaxStreamReceiveWindow,\n\t\tInitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow,\n\t\tMaxConnectionReceiveWindow:     c.QUIC.MaxConnectionReceiveWindow,\n\t\tMaxIdleTimeout:                 c.QUIC.MaxIdleTimeout,\n\t\tKeepAlivePeriod:                c.QUIC.KeepAlivePeriod,\n\t\tDisablePathMTUDiscovery:        c.QUIC.DisablePathMTUDiscovery,\n\t}\n\treturn nil\n}\n\nfunc (c *clientConfig) fillBandwidthConfig(hyConfig *client.Config) error {\n\t// New core now allows users to omit bandwidth values and use built-in congestion control\n\tvar err error\n\tif c.Bandwidth.Up != \"\" {\n\t\thyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"bandwidth.up\", Err: err}\n\t\t}\n\t}\n\tif c.Bandwidth.Down != \"\" {\n\t\thyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"bandwidth.down\", Err: err}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *clientConfig) fillFastOpen(hyConfig *client.Config) error {\n\thyConfig.FastOpen = c.FastOpen\n\treturn nil\n}\n\n// URI generates a URI for sharing the config with others.\n// Note that only the bare minimum of information required to\n// connect to the server is included in the URI, specifically:\n// - server address\n// - authentication\n// - obfuscation type\n// - obfuscation password\n// - TLS SNI\n// - TLS insecure\n// - TLS pinned SHA256 hash (normalized)\nfunc (c *clientConfig) URI() string {\n\tq := url.Values{}\n\tswitch strings.ToLower(c.Obfs.Type) {\n\tcase \"salamander\":\n\t\tq.Set(\"obfs\", \"salamander\")\n\t\tq.Set(\"obfs-password\", c.Obfs.Salamander.Password)\n\t}\n\tif c.TLS.SNI != \"\" {\n\t\tq.Set(\"sni\", c.TLS.SNI)\n\t}\n\tif c.TLS.Insecure {\n\t\tq.Set(\"insecure\", \"1\")\n\t}\n\tif c.TLS.PinSHA256 != \"\" {\n\t\tq.Set(\"pinSHA256\", normalizeCertHash(c.TLS.PinSHA256))\n\t}\n\tvar user *url.Userinfo\n\tif c.Auth != \"\" {\n\t\t// We need to handle the special case of user:pass pairs\n\t\trs := strings.SplitN(c.Auth, \":\", 2)\n\t\tif len(rs) == 2 {\n\t\t\tuser = url.UserPassword(rs[0], rs[1])\n\t\t} else {\n\t\t\tuser = url.User(c.Auth)\n\t\t}\n\t}\n\tu := url.URL{\n\t\tScheme:   \"hysteria2\",\n\t\tUser:     user,\n\t\tHost:     c.Server,\n\t\tPath:     \"/\",\n\t\tRawQuery: q.Encode(),\n\t}\n\treturn u.String()\n}\n\n// parseURI tries to parse the server address field as a URI,\n// and fills the config with the information contained in the URI.\n// Returns whether the server address field is a valid URI.\n// This allows a user to use put a URI as the server address and\n// omit the fields that are already contained in the URI.\nfunc (c *clientConfig) parseURI() bool {\n\tu, err := url.Parse(c.Server)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif u.Scheme != \"hysteria2\" && u.Scheme != \"hy2\" {\n\t\treturn false\n\t}\n\tif u.User != nil {\n\t\tauth, err := url.QueryUnescape(u.User.String())\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tc.Auth = auth\n\t}\n\tc.Server = u.Host\n\tq := u.Query()\n\tif obfsType := q.Get(\"obfs\"); obfsType != \"\" {\n\t\tc.Obfs.Type = obfsType\n\t\tswitch strings.ToLower(obfsType) {\n\t\tcase \"salamander\":\n\t\t\tc.Obfs.Salamander.Password = q.Get(\"obfs-password\")\n\t\t}\n\t}\n\tif sni := q.Get(\"sni\"); sni != \"\" {\n\t\tc.TLS.SNI = sni\n\t}\n\tif insecure, err := strconv.ParseBool(q.Get(\"insecure\")); err == nil {\n\t\tc.TLS.Insecure = insecure\n\t}\n\tif pinSHA256 := q.Get(\"pinSHA256\"); pinSHA256 != \"\" {\n\t\tc.TLS.PinSHA256 = pinSHA256\n\t}\n\treturn true\n}\n\n// Config validates the fields and returns a ready-to-use Hysteria client config\nfunc (c *clientConfig) Config() (*client.Config, error) {\n\tc.parseURI()\n\thyConfig := &client.Config{}\n\tfillers := []func(*client.Config) error{\n\t\tc.fillServerAddr,\n\t\tc.fillConnFactory,\n\t\tc.fillAuth,\n\t\tc.fillTLSConfig,\n\t\tc.fillQUICConfig,\n\t\tc.fillBandwidthConfig,\n\t\tc.fillFastOpen,\n\t}\n\tfor _, f := range fillers {\n\t\tif err := f(hyConfig); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn hyConfig, nil\n}\n\nfunc runClient(cmd *cobra.Command, args []string) {\n\tlogger.Info(\"client mode\")\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\tlogger.Fatal(\"failed to read client config\", zap.Error(err))\n\t}\n\tvar config clientConfig\n\tif err := viper.Unmarshal(&config); err != nil {\n\t\tlogger.Fatal(\"failed to parse client config\", zap.Error(err))\n\t}\n\n\tc, err := client.NewReconnectableClient(\n\t\tconfig.Config,\n\t\tfunc(c client.Client, info *client.HandshakeInfo, count int) {\n\t\t\tconnectLog(info, count)\n\t\t\t// On the client side, we start checking for updates after we successfully connect\n\t\t\t// to the server, which, depending on whether lazy mode is enabled, may or may not\n\t\t\t// be immediately after the client starts. We don't want the update check request\n\t\t\t// to interfere with the lazy mode option.\n\t\t\tif count == 1 && !disableUpdateCheck {\n\t\t\t\tgo runCheckUpdateClient(c)\n\t\t\t}\n\t\t}, config.Lazy)\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to initialize client\", zap.Error(err))\n\t}\n\tdefer c.Close()\n\n\turi := config.URI()\n\tlogger.Info(\"use this URI to share your server\", zap.String(\"uri\", uri))\n\tif showQR {\n\t\tutils.PrintQR(uri)\n\t}\n\n\t// Register modes\n\tvar runner clientModeRunner\n\tif config.SOCKS5 != nil {\n\t\trunner.Add(\"SOCKS5 server\", func() error {\n\t\t\treturn clientSOCKS5(*config.SOCKS5, c)\n\t\t})\n\t}\n\tif config.HTTP != nil {\n\t\trunner.Add(\"HTTP proxy server\", func() error {\n\t\t\treturn clientHTTP(*config.HTTP, c)\n\t\t})\n\t}\n\tif len(config.TCPForwarding) > 0 {\n\t\trunner.Add(\"TCP forwarding\", func() error {\n\t\t\treturn clientTCPForwarding(config.TCPForwarding, c)\n\t\t})\n\t}\n\tif len(config.UDPForwarding) > 0 {\n\t\trunner.Add(\"UDP forwarding\", func() error {\n\t\t\treturn clientUDPForwarding(config.UDPForwarding, c)\n\t\t})\n\t}\n\tif config.TCPTProxy != nil {\n\t\trunner.Add(\"TCP transparent proxy\", func() error {\n\t\t\treturn clientTCPTProxy(*config.TCPTProxy, c)\n\t\t})\n\t}\n\tif config.UDPTProxy != nil {\n\t\trunner.Add(\"UDP transparent proxy\", func() error {\n\t\t\treturn clientUDPTProxy(*config.UDPTProxy, c)\n\t\t})\n\t}\n\tif config.TCPRedirect != nil {\n\t\trunner.Add(\"TCP redirect\", func() error {\n\t\t\treturn clientTCPRedirect(*config.TCPRedirect, c)\n\t\t})\n\t}\n\tif config.TUN != nil {\n\t\trunner.Add(\"TUN\", func() error {\n\t\t\treturn clientTUN(*config.TUN, c)\n\t\t})\n\t}\n\n\tsignalChan := make(chan os.Signal, 1)\n\tsignal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)\n\tdefer signal.Stop(signalChan)\n\n\trunnerChan := make(chan clientModeRunnerResult, 1)\n\tgo func() {\n\t\trunnerChan <- runner.Run()\n\t}()\n\n\tselect {\n\tcase <-signalChan:\n\t\tlogger.Info(\"received signal, shutting down gracefully\")\n\tcase r := <-runnerChan:\n\t\tif r.OK {\n\t\t\tlogger.Info(r.Msg)\n\t\t} else {\n\t\t\t_ = c.Close() // Close the client here as Fatal will exit the program without running defer\n\t\t\tif r.Err != nil {\n\t\t\t\tlogger.Fatal(r.Msg, zap.Error(r.Err))\n\t\t\t} else {\n\t\t\t\tlogger.Fatal(r.Msg)\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype clientModeRunner struct {\n\tModeMap map[string]func() error\n}\n\ntype clientModeRunnerResult struct {\n\tOK  bool\n\tMsg string\n\tErr error\n}\n\nfunc (r *clientModeRunner) Add(name string, f func() error) {\n\tif r.ModeMap == nil {\n\t\tr.ModeMap = make(map[string]func() error)\n\t}\n\tr.ModeMap[name] = f\n}\n\nfunc (r *clientModeRunner) Run() clientModeRunnerResult {\n\tif len(r.ModeMap) == 0 {\n\t\treturn clientModeRunnerResult{OK: false, Msg: \"no mode specified\"}\n\t}\n\n\ttype modeError struct {\n\t\tName string\n\t\tErr  error\n\t}\n\terrChan := make(chan modeError, len(r.ModeMap))\n\tfor name, f := range r.ModeMap {\n\t\tgo func(name string, f func() error) {\n\t\t\terr := f()\n\t\t\terrChan <- modeError{name, err}\n\t\t}(name, f)\n\t}\n\t// Fatal if any one of the modes fails\n\tfor i := 0; i < len(r.ModeMap); i++ {\n\t\te := <-errChan\n\t\tif e.Err != nil {\n\t\t\treturn clientModeRunnerResult{OK: false, Msg: \"failed to run \" + e.Name, Err: e.Err}\n\t\t}\n\t}\n\n\t// We don't really have any such cases, as currently none of our modes would stop on themselves without error.\n\t// But we leave the possibility here for future expansion.\n\treturn clientModeRunnerResult{OK: true, Msg: \"finished without error\"}\n}\n\nfunc clientSOCKS5(config socks5Config, c client.Client) error {\n\tif config.Listen == \"\" {\n\t\treturn configError{Field: \"listen\", Err: errors.New(\"listen address is empty\")}\n\t}\n\tl, err := proxymux.ListenSOCKS(config.Listen)\n\tif err != nil {\n\t\treturn configError{Field: \"listen\", Err: err}\n\t}\n\tvar authFunc func(username, password string) bool\n\tusername, password := config.Username, config.Password\n\tif username != \"\" && password != \"\" {\n\t\tauthFunc = func(u, p string) bool {\n\t\t\treturn u == username && p == password\n\t\t}\n\t}\n\ts := socks5.Server{\n\t\tHyClient:    c,\n\t\tAuthFunc:    authFunc,\n\t\tDisableUDP:  config.DisableUDP,\n\t\tEventLogger: &socks5Logger{},\n\t}\n\tlogger.Info(\"SOCKS5 server listening\", zap.String(\"addr\", config.Listen))\n\treturn s.Serve(l)\n}\n\nfunc clientHTTP(config httpConfig, c client.Client) error {\n\tif config.Listen == \"\" {\n\t\treturn configError{Field: \"listen\", Err: errors.New(\"listen address is empty\")}\n\t}\n\tl, err := proxymux.ListenHTTP(config.Listen)\n\tif err != nil {\n\t\treturn configError{Field: \"listen\", Err: err}\n\t}\n\tvar authFunc func(username, password string) bool\n\tusername, password := config.Username, config.Password\n\tif username != \"\" && password != \"\" {\n\t\tauthFunc = func(u, p string) bool {\n\t\t\treturn u == username && p == password\n\t\t}\n\t}\n\tif config.Realm == \"\" {\n\t\tconfig.Realm = \"Hysteria\"\n\t}\n\th := http.Server{\n\t\tHyClient:    c,\n\t\tAuthFunc:    authFunc,\n\t\tAuthRealm:   config.Realm,\n\t\tEventLogger: &httpLogger{},\n\t}\n\tlogger.Info(\"HTTP proxy server listening\", zap.String(\"addr\", config.Listen))\n\treturn h.Serve(l)\n}\n\nfunc clientTCPForwarding(entries []tcpForwardingEntry, c client.Client) error {\n\terrChan := make(chan error, len(entries))\n\tfor _, e := range entries {\n\t\tif e.Listen == \"\" {\n\t\t\treturn configError{Field: \"listen\", Err: errors.New(\"listen address is empty\")}\n\t\t}\n\t\tif e.Remote == \"\" {\n\t\t\treturn configError{Field: \"remote\", Err: errors.New(\"remote address is empty\")}\n\t\t}\n\t\tl, err := correctnet.Listen(\"tcp\", e.Listen)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"listen\", Err: err}\n\t\t}\n\t\tlogger.Info(\"TCP forwarding listening\", zap.String(\"addr\", e.Listen), zap.String(\"remote\", e.Remote))\n\t\tgo func(remote string) {\n\t\t\tt := &forwarding.TCPTunnel{\n\t\t\t\tHyClient:    c,\n\t\t\t\tRemote:      remote,\n\t\t\t\tEventLogger: &tcpLogger{},\n\t\t\t}\n\t\t\terrChan <- t.Serve(l)\n\t\t}(e.Remote)\n\t}\n\t// Return if any one of the forwarding fails\n\treturn <-errChan\n}\n\nfunc clientUDPForwarding(entries []udpForwardingEntry, c client.Client) error {\n\terrChan := make(chan error, len(entries))\n\tfor _, e := range entries {\n\t\tif e.Listen == \"\" {\n\t\t\treturn configError{Field: \"listen\", Err: errors.New(\"listen address is empty\")}\n\t\t}\n\t\tif e.Remote == \"\" {\n\t\t\treturn configError{Field: \"remote\", Err: errors.New(\"remote address is empty\")}\n\t\t}\n\t\tl, err := correctnet.ListenPacket(\"udp\", e.Listen)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"listen\", Err: err}\n\t\t}\n\t\tlogger.Info(\"UDP forwarding listening\", zap.String(\"addr\", e.Listen), zap.String(\"remote\", e.Remote))\n\t\tgo func(remote string, timeout time.Duration) {\n\t\t\tu := &forwarding.UDPTunnel{\n\t\t\t\tHyClient:    c,\n\t\t\t\tRemote:      remote,\n\t\t\t\tTimeout:     timeout,\n\t\t\t\tEventLogger: &udpLogger{},\n\t\t\t}\n\t\t\terrChan <- u.Serve(l)\n\t\t}(e.Remote, e.Timeout)\n\t}\n\t// Return if any one of the forwarding fails\n\treturn <-errChan\n}\n\nfunc clientTCPTProxy(config tcpTProxyConfig, c client.Client) error {\n\tif config.Listen == \"\" {\n\t\treturn configError{Field: \"listen\", Err: errors.New(\"listen address is empty\")}\n\t}\n\tladdr, err := net.ResolveTCPAddr(\"tcp\", config.Listen)\n\tif err != nil {\n\t\treturn configError{Field: \"listen\", Err: err}\n\t}\n\tp := &tproxy.TCPTProxy{\n\t\tHyClient:    c,\n\t\tEventLogger: &tcpTProxyLogger{},\n\t}\n\tlogger.Info(\"TCP transparent proxy listening\", zap.String(\"addr\", config.Listen))\n\treturn p.ListenAndServe(laddr)\n}\n\nfunc clientUDPTProxy(config udpTProxyConfig, c client.Client) error {\n\tif config.Listen == \"\" {\n\t\treturn configError{Field: \"listen\", Err: errors.New(\"listen address is empty\")}\n\t}\n\tladdr, err := net.ResolveUDPAddr(\"udp\", config.Listen)\n\tif err != nil {\n\t\treturn configError{Field: \"listen\", Err: err}\n\t}\n\tp := &tproxy.UDPTProxy{\n\t\tHyClient:    c,\n\t\tTimeout:     config.Timeout,\n\t\tEventLogger: &udpTProxyLogger{},\n\t}\n\tlogger.Info(\"UDP transparent proxy listening\", zap.String(\"addr\", config.Listen))\n\treturn p.ListenAndServe(laddr)\n}\n\nfunc clientTCPRedirect(config tcpRedirectConfig, c client.Client) error {\n\tif config.Listen == \"\" {\n\t\treturn configError{Field: \"listen\", Err: errors.New(\"listen address is empty\")}\n\t}\n\tladdr, err := net.ResolveTCPAddr(\"tcp\", config.Listen)\n\tif err != nil {\n\t\treturn configError{Field: \"listen\", Err: err}\n\t}\n\tp := &redirect.TCPRedirect{\n\t\tHyClient:    c,\n\t\tEventLogger: &tcpRedirectLogger{},\n\t}\n\tlogger.Info(\"TCP redirect listening\", zap.String(\"addr\", config.Listen))\n\treturn p.ListenAndServe(laddr)\n}\n\nfunc clientTUN(config tunConfig, c client.Client) error {\n\tsupportedPlatforms := []string{\"linux\", \"darwin\", \"windows\", \"android\"}\n\tif !slices.Contains(supportedPlatforms, runtime.GOOS) {\n\t\tlogger.Error(\"TUN is not supported on this platform\", zap.String(\"platform\", runtime.GOOS))\n\t}\n\tif config.Name == \"\" {\n\t\treturn configError{Field: \"name\", Err: errors.New(\"name is empty\")}\n\t}\n\tif config.MTU == 0 {\n\t\tconfig.MTU = 1500\n\t}\n\ttimeout := int64(config.Timeout.Seconds())\n\tif timeout == 0 {\n\t\ttimeout = 300\n\t}\n\tif config.Address.IPv4 == \"\" {\n\t\tconfig.Address.IPv4 = \"100.100.100.101/30\"\n\t}\n\tprefix4, err := netip.ParsePrefix(config.Address.IPv4)\n\tif err != nil {\n\t\treturn configError{Field: \"address.ipv4\", Err: err}\n\t}\n\tif config.Address.IPv6 == \"\" {\n\t\tconfig.Address.IPv6 = \"2001::ffff:ffff:ffff:fff1/126\"\n\t}\n\tprefix6, err := netip.ParsePrefix(config.Address.IPv6)\n\tif err != nil {\n\t\treturn configError{Field: \"address.ipv6\", Err: err}\n\t}\n\tserver := &tun.Server{\n\t\tHyClient:     c,\n\t\tEventLogger:  &tunLogger{},\n\t\tLogger:       logger,\n\t\tIfName:       config.Name,\n\t\tMTU:          config.MTU,\n\t\tTimeout:      timeout,\n\t\tInet4Address: []netip.Prefix{prefix4},\n\t\tInet6Address: []netip.Prefix{prefix6},\n\t}\n\tif config.Route != nil {\n\t\tserver.AutoRoute = true\n\t\tserver.StructRoute = config.Route.Strict\n\n\t\tparsePrefixes := func(field string, ss []string) ([]netip.Prefix, error) {\n\t\t\tvar prefixes []netip.Prefix\n\t\t\tfor i, s := range ss {\n\t\t\t\tvar p netip.Prefix\n\t\t\t\tif strings.Contains(s, \"/\") {\n\t\t\t\t\tvar err error\n\t\t\t\t\tp, err = netip.ParsePrefix(s)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, configError{Field: fmt.Sprintf(\"%s[%d]\", field, i), Err: err}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tpa, err := netip.ParseAddr(s)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, configError{Field: fmt.Sprintf(\"%s[%d]\", field, i), Err: err}\n\t\t\t\t\t}\n\t\t\t\t\tp = netip.PrefixFrom(pa, pa.BitLen())\n\t\t\t\t}\n\t\t\t\tprefixes = append(prefixes, p)\n\t\t\t}\n\t\t\treturn prefixes, nil\n\t\t}\n\n\t\tserver.Inet4RouteAddress, err = parsePrefixes(\"route.ipv4\", config.Route.IPv4)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.Inet6RouteAddress, err = parsePrefixes(\"route.ipv6\", config.Route.IPv6)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.Inet4RouteExcludeAddress, err = parsePrefixes(\"route.ipv4Exclude\", config.Route.IPv4Exclude)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.Inet6RouteExcludeAddress, err = parsePrefixes(\"route.ipv6Exclude\", config.Route.IPv6Exclude)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tlogger.Info(\"TUN listening\", zap.String(\"interface\", config.Name))\n\treturn server.Serve()\n}\n\n// parseServerAddrString parses server address string.\n// Server address can be in either \"host:port\" or \"host\" format (in which case we assume port 443).\nfunc parseServerAddrString(addrStr string) (host, port, hostPort string) {\n\th, p, err := net.SplitHostPort(addrStr)\n\tif err != nil {\n\t\treturn addrStr, \"443\", net.JoinHostPort(addrStr, \"443\")\n\t}\n\treturn h, p, addrStr\n}\n\n// isPortHoppingPort returns whether the port string is a port hopping port.\n// We consider a port string to be a port hopping port if it contains \"-\" or \",\".\nfunc isPortHoppingPort(port string) bool {\n\treturn strings.Contains(port, \"-\") || strings.Contains(port, \",\")\n}\n\n// normalizeCertHash normalizes a certificate hash string.\n// It converts all characters to lowercase and removes possible separators such as \":\" and \"-\".\nfunc normalizeCertHash(hash string) string {\n\tr := strings.ToLower(hash)\n\tr = strings.ReplaceAll(r, \":\", \"\")\n\tr = strings.ReplaceAll(r, \"-\", \"\")\n\treturn r\n}\n\ntype adaptiveConnFactory struct {\n\tNewFunc    func(addr net.Addr) (net.PacketConn, error)\n\tObfuscator obfs.Obfuscator // nil if no obfuscation\n}\n\nfunc (f *adaptiveConnFactory) New(addr net.Addr) (net.PacketConn, error) {\n\tif f.Obfuscator == nil {\n\t\treturn f.NewFunc(addr)\n\t} else {\n\t\tconn, err := f.NewFunc(addr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn obfs.WrapPacketConn(conn, f.Obfuscator), nil\n\t}\n}\n\nfunc connectLog(info *client.HandshakeInfo, count int) {\n\tlogger.Info(\"connected to server\",\n\t\tzap.Bool(\"udpEnabled\", info.UDPEnabled),\n\t\tzap.Uint64(\"tx\", info.Tx),\n\t\tzap.Int(\"count\", count))\n}\n\ntype socks5Logger struct{}\n\nfunc (l *socks5Logger) TCPRequest(addr net.Addr, reqAddr string) {\n\tlogger.Debug(\"SOCKS5 TCP request\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr))\n}\n\nfunc (l *socks5Logger) TCPError(addr net.Addr, reqAddr string, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"SOCKS5 TCP closed\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr))\n\t} else {\n\t\tlogger.Warn(\"SOCKS5 TCP error\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr), zap.Error(err))\n\t}\n}\n\nfunc (l *socks5Logger) UDPRequest(addr net.Addr) {\n\tlogger.Debug(\"SOCKS5 UDP request\", zap.String(\"addr\", addr.String()))\n}\n\nfunc (l *socks5Logger) UDPError(addr net.Addr, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"SOCKS5 UDP closed\", zap.String(\"addr\", addr.String()))\n\t} else {\n\t\tlogger.Warn(\"SOCKS5 UDP error\", zap.String(\"addr\", addr.String()), zap.Error(err))\n\t}\n}\n\ntype httpLogger struct{}\n\nfunc (l *httpLogger) ConnectRequest(addr net.Addr, reqAddr string) {\n\tlogger.Debug(\"HTTP CONNECT request\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr))\n}\n\nfunc (l *httpLogger) ConnectError(addr net.Addr, reqAddr string, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"HTTP CONNECT closed\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr))\n\t} else {\n\t\tlogger.Warn(\"HTTP CONNECT error\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr), zap.Error(err))\n\t}\n}\n\nfunc (l *httpLogger) HTTPRequest(addr net.Addr, reqURL string) {\n\tlogger.Debug(\"HTTP request\", zap.String(\"addr\", addr.String()), zap.String(\"reqURL\", reqURL))\n}\n\nfunc (l *httpLogger) HTTPError(addr net.Addr, reqURL string, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"HTTP closed\", zap.String(\"addr\", addr.String()), zap.String(\"reqURL\", reqURL))\n\t} else {\n\t\tlogger.Warn(\"HTTP error\", zap.String(\"addr\", addr.String()), zap.String(\"reqURL\", reqURL), zap.Error(err))\n\t}\n}\n\ntype tcpLogger struct{}\n\nfunc (l *tcpLogger) Connect(addr net.Addr) {\n\tlogger.Debug(\"TCP forwarding connect\", zap.String(\"addr\", addr.String()))\n}\n\nfunc (l *tcpLogger) Error(addr net.Addr, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"TCP forwarding closed\", zap.String(\"addr\", addr.String()))\n\t} else {\n\t\tlogger.Warn(\"TCP forwarding error\", zap.String(\"addr\", addr.String()), zap.Error(err))\n\t}\n}\n\ntype udpLogger struct{}\n\nfunc (l *udpLogger) Connect(addr net.Addr) {\n\tlogger.Debug(\"UDP forwarding connect\", zap.String(\"addr\", addr.String()))\n}\n\nfunc (l *udpLogger) Error(addr net.Addr, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"UDP forwarding closed\", zap.String(\"addr\", addr.String()))\n\t} else {\n\t\tlogger.Warn(\"UDP forwarding error\", zap.String(\"addr\", addr.String()), zap.Error(err))\n\t}\n}\n\ntype tcpTProxyLogger struct{}\n\nfunc (l *tcpTProxyLogger) Connect(addr, reqAddr net.Addr) {\n\tlogger.Debug(\"TCP transparent proxy connect\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()))\n}\n\nfunc (l *tcpTProxyLogger) Error(addr, reqAddr net.Addr, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"TCP transparent proxy closed\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()))\n\t} else {\n\t\tlogger.Warn(\"TCP transparent proxy error\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()), zap.Error(err))\n\t}\n}\n\ntype udpTProxyLogger struct{}\n\nfunc (l *udpTProxyLogger) Connect(addr, reqAddr net.Addr) {\n\tlogger.Debug(\"UDP transparent proxy connect\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()))\n}\n\nfunc (l *udpTProxyLogger) Error(addr, reqAddr net.Addr, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"UDP transparent proxy closed\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()))\n\t} else {\n\t\tlogger.Warn(\"UDP transparent proxy error\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()), zap.Error(err))\n\t}\n}\n\ntype tcpRedirectLogger struct{}\n\nfunc (l *tcpRedirectLogger) Connect(addr, reqAddr net.Addr) {\n\tlogger.Debug(\"TCP redirect connect\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()))\n}\n\nfunc (l *tcpRedirectLogger) Error(addr, reqAddr net.Addr, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"TCP redirect closed\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()))\n\t} else {\n\t\tlogger.Warn(\"TCP redirect error\", zap.String(\"addr\", addr.String()), zap.String(\"reqAddr\", reqAddr.String()), zap.Error(err))\n\t}\n}\n\ntype tunLogger struct{}\n\nfunc (l *tunLogger) TCPRequest(addr, reqAddr string) {\n\tlogger.Debug(\"TUN TCP request\", zap.String(\"addr\", addr), zap.String(\"reqAddr\", reqAddr))\n}\n\nfunc (l *tunLogger) TCPError(addr, reqAddr string, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"TUN TCP closed\", zap.String(\"addr\", addr), zap.String(\"reqAddr\", reqAddr))\n\t} else {\n\t\tlogger.Warn(\"TUN TCP error\", zap.String(\"addr\", addr), zap.String(\"reqAddr\", reqAddr), zap.Error(err))\n\t}\n}\n\nfunc (l *tunLogger) UDPRequest(addr string) {\n\tlogger.Debug(\"TUN UDP request\", zap.String(\"addr\", addr))\n}\n\nfunc (l *tunLogger) UDPError(addr string, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"TUN UDP closed\", zap.String(\"addr\", addr))\n\t} else {\n\t\tlogger.Warn(\"TUN UDP error\", zap.String(\"addr\", addr), zap.Error(err))\n\t}\n}\n"
  },
  {
    "path": "app/cmd/client_test.go",
    "content": "package cmd\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/spf13/viper\"\n)\n\n// TestClientConfig tests the parsing of the client config\nfunc TestClientConfig(t *testing.T) {\n\tviper.SetConfigFile(\"client_test.yaml\")\n\terr := viper.ReadInConfig()\n\tassert.NoError(t, err)\n\tvar config clientConfig\n\terr = viper.Unmarshal(&config)\n\tassert.NoError(t, err)\n\tassert.Equal(t, config, clientConfig{\n\t\tServer: \"example.com\",\n\t\tAuth:   \"weak_ahh_password\",\n\t\tTransport: clientConfigTransport{\n\t\t\tType: \"udp\",\n\t\t\tUDP: clientConfigTransportUDP{\n\t\t\t\tHopInterval: 30 * time.Second,\n\t\t\t},\n\t\t},\n\t\tObfs: clientConfigObfs{\n\t\t\tType: \"salamander\",\n\t\t\tSalamander: clientConfigObfsSalamander{\n\t\t\t\tPassword: \"cry_me_a_r1ver\",\n\t\t\t},\n\t\t},\n\t\tTLS: clientConfigTLS{\n\t\t\tSNI:       \"another.example.com\",\n\t\t\tInsecure:  true,\n\t\t\tPinSHA256: \"114515DEADBEEF\",\n\t\t\tCA:        \"custom_ca.crt\",\n\t\t},\n\t\tQUIC: clientConfigQUIC{\n\t\t\tInitStreamReceiveWindow:     1145141,\n\t\t\tMaxStreamReceiveWindow:      1145142,\n\t\t\tInitConnectionReceiveWindow: 1145143,\n\t\t\tMaxConnectionReceiveWindow:  1145144,\n\t\t\tMaxIdleTimeout:              10 * time.Second,\n\t\t\tKeepAlivePeriod:             4 * time.Second,\n\t\t\tDisablePathMTUDiscovery:     true,\n\t\t\tSockopts: clientConfigQUICSockopts{\n\t\t\t\tBindInterface:       stringRef(\"eth0\"),\n\t\t\t\tFirewallMark:        uint32Ref(1234),\n\t\t\t\tFdControlUnixSocket: stringRef(\"test.sock\"),\n\t\t\t},\n\t\t},\n\t\tBandwidth: clientConfigBandwidth{\n\t\t\tUp:   \"200 mbps\",\n\t\t\tDown: \"1 gbps\",\n\t\t},\n\t\tFastOpen: true,\n\t\tLazy:     true,\n\t\tSOCKS5: &socks5Config{\n\t\t\tListen:     \"127.0.0.1:1080\",\n\t\t\tUsername:   \"anon\",\n\t\t\tPassword:   \"bro\",\n\t\t\tDisableUDP: true,\n\t\t},\n\t\tHTTP: &httpConfig{\n\t\t\tListen:   \"127.0.0.1:8080\",\n\t\t\tUsername: \"qqq\",\n\t\t\tPassword: \"bruh\",\n\t\t\tRealm:    \"martian\",\n\t\t},\n\t\tTCPForwarding: []tcpForwardingEntry{\n\t\t\t{\n\t\t\t\tListen: \"127.0.0.1:8088\",\n\t\t\t\tRemote: \"internal.example.com:80\",\n\t\t\t},\n\t\t},\n\t\tUDPForwarding: []udpForwardingEntry{\n\t\t\t{\n\t\t\t\tListen:  \"127.0.0.1:5353\",\n\t\t\t\tRemote:  \"internal.example.com:53\",\n\t\t\t\tTimeout: 50 * time.Second,\n\t\t\t},\n\t\t},\n\t\tTCPTProxy: &tcpTProxyConfig{\n\t\t\tListen: \"127.0.0.1:2500\",\n\t\t},\n\t\tUDPTProxy: &udpTProxyConfig{\n\t\t\tListen:  \"127.0.0.1:2501\",\n\t\t\tTimeout: 20 * time.Second,\n\t\t},\n\t\tTCPRedirect: &tcpRedirectConfig{\n\t\t\tListen: \"127.0.0.1:3500\",\n\t\t},\n\t\tTUN: &tunConfig{\n\t\t\tName:    \"hytun\",\n\t\t\tMTU:     1500,\n\t\t\tTimeout: 60 * time.Second,\n\t\t\tAddress: struct {\n\t\t\t\tIPv4 string `mapstructure:\"ipv4\"`\n\t\t\t\tIPv6 string `mapstructure:\"ipv6\"`\n\t\t\t}{IPv4: \"100.100.100.101/30\", IPv6: \"2001::ffff:ffff:ffff:fff1/126\"},\n\t\t\tRoute: &struct {\n\t\t\t\tStrict      bool     `mapstructure:\"strict\"`\n\t\t\t\tIPv4        []string `mapstructure:\"ipv4\"`\n\t\t\t\tIPv6        []string `mapstructure:\"ipv6\"`\n\t\t\t\tIPv4Exclude []string `mapstructure:\"ipv4Exclude\"`\n\t\t\t\tIPv6Exclude []string `mapstructure:\"ipv6Exclude\"`\n\t\t\t}{\n\t\t\t\tStrict:      true,\n\t\t\t\tIPv4:        []string{\"0.0.0.0/0\"},\n\t\t\t\tIPv6:        []string{\"2000::/3\"},\n\t\t\t\tIPv4Exclude: []string{\"192.0.2.1/32\"},\n\t\t\t\tIPv6Exclude: []string{\"2001:db8::1/128\"},\n\t\t\t},\n\t\t},\n\t})\n}\n\n// TestClientConfigURI tests URI-related functions of clientConfig\nfunc TestClientConfigURI(t *testing.T) {\n\ttests := []struct {\n\t\turi    string\n\t\turiOK  bool\n\t\tconfig *clientConfig\n\t}{\n\t\t{\n\t\t\turi:   \"hysteria2://god@zilla.jp/\",\n\t\t\turiOK: true,\n\t\t\tconfig: &clientConfig{\n\t\t\t\tServer: \"zilla.jp\",\n\t\t\t\tAuth:   \"god\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\turi:   \"hysteria2://john:wick@continental.org:4443/\",\n\t\t\turiOK: true,\n\t\t\tconfig: &clientConfig{\n\t\t\t\tServer: \"continental.org:4443\",\n\t\t\t\tAuth:   \"john:wick\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\turi:   \"hysteria2://saul@better.call:7000-10000,20000/\",\n\t\t\turiOK: true,\n\t\t\tconfig: &clientConfig{\n\t\t\t\tServer: \"better.call:7000-10000,20000\",\n\t\t\t\tAuth:   \"saul\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\turi:   \"hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&pinSHA256=deadbeef&sni=crap.cc\",\n\t\t\turiOK: true,\n\t\t\tconfig: &clientConfig{\n\t\t\t\tServer: \"noauth.com\",\n\t\t\t\tAuth:   \"\",\n\t\t\t\tObfs: clientConfigObfs{\n\t\t\t\t\tType: \"salamander\",\n\t\t\t\t\tSalamander: clientConfigObfsSalamander{\n\t\t\t\t\t\tPassword: \"66ccff\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTLS: clientConfigTLS{\n\t\t\t\t\tSNI:       \"crap.cc\",\n\t\t\t\t\tInsecure:  true,\n\t\t\t\t\tPinSHA256: \"deadbeef\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\turi:    \"invalid.bs\",\n\t\t\turiOK:  false,\n\t\t\tconfig: nil,\n\t\t},\n\t\t{\n\t\t\turi:    \"https://www.google.com/search?q=test\",\n\t\t\turiOK:  false,\n\t\t\tconfig: nil,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.uri, func(t *testing.T) {\n\t\t\t// Test parseURI\n\t\t\tnc := &clientConfig{Server: test.uri}\n\t\t\tassert.Equal(t, nc.parseURI(), test.uriOK)\n\t\t\tif test.uriOK {\n\t\t\t\tassert.Equal(t, nc, test.config)\n\t\t\t}\n\t\t\t// Test URI generation\n\t\t\tif test.config != nil {\n\t\t\t\tassert.Equal(t, test.config.URI(), test.uri)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc stringRef(s string) *string {\n\treturn &s\n}\n\nfunc uint32Ref(i uint32) *uint32 {\n\treturn &i\n}\n"
  },
  {
    "path": "app/cmd/client_test.yaml",
    "content": "server: example.com\n\nauth: weak_ahh_password\n\ntransport:\n  type: udp\n  udp:\n    hopInterval: 30s\n\nobfs:\n  type: salamander\n  salamander:\n    password: cry_me_a_r1ver\n\ntls:\n  sni: another.example.com\n  insecure: true\n  pinSHA256: 114515DEADBEEF\n  ca: custom_ca.crt\n\nquic:\n  initStreamReceiveWindow: 1145141\n  maxStreamReceiveWindow: 1145142\n  initConnReceiveWindow: 1145143\n  maxConnReceiveWindow: 1145144\n  maxIdleTimeout: 10s\n  keepAlivePeriod: 4s\n  disablePathMTUDiscovery: true\n  sockopts:\n    bindInterface: eth0\n    fwmark: 1234\n    fdControlUnixSocket: test.sock\n\nbandwidth:\n  up: 200 mbps\n  down: 1 gbps\n\nfastOpen: true\n\nlazy: true\n\nsocks5:\n  listen: 127.0.0.1:1080\n  username: anon\n  password: bro\n  disableUDP: true\n\nhttp:\n  listen: 127.0.0.1:8080\n  username: qqq\n  password: bruh\n  realm: martian\n\ntcpForwarding:\n  - listen: 127.0.0.1:8088\n    remote: internal.example.com:80\n\nudpForwarding:\n  - listen: 127.0.0.1:5353\n    remote: internal.example.com:53\n    timeout: 50s\n\ntcpTProxy:\n  listen: 127.0.0.1:2500\n\nudpTProxy:\n  listen: 127.0.0.1:2501\n  timeout: 20s\n\ntcpRedirect:\n  listen: 127.0.0.1:3500\n\ntun:\n  name: \"hytun\"\n  mtu: 1500\n  timeout: 1m\n  address:\n    ipv4: 100.100.100.101/30\n    ipv6: 2001::ffff:ffff:ffff:fff1/126\n  route:\n    strict: true\n    ipv4: [ 0.0.0.0/0 ]\n    ipv6: [ \"2000::/3\" ]\n    ipv4Exclude: [ 192.0.2.1/32 ]\n    ipv6Exclude: [ \"2001:db8::1/128\" ]\n"
  },
  {
    "path": "app/cmd/errors.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n)\n\ntype configError struct {\n\tField string\n\tErr   error\n}\n\nfunc (e configError) Error() string {\n\treturn fmt.Sprintf(\"invalid config: %s: %s\", e.Field, e.Err)\n}\n\nfunc (e configError) Unwrap() error {\n\treturn e.Err\n}\n"
  },
  {
    "path": "app/cmd/ping.go",
    "content": "package cmd\n\nimport (\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"go.uber.org/zap\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\n// pingCmd represents the ping command\nvar pingCmd = &cobra.Command{\n\tUse:   \"ping address\",\n\tShort: \"Ping mode\",\n\tLong:  \"Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.\",\n\tRun:   runPing,\n}\n\nfunc init() {\n\trootCmd.AddCommand(pingCmd)\n}\n\nfunc runPing(cmd *cobra.Command, args []string) {\n\tlogger.Info(\"ping mode\")\n\n\tif len(args) != 1 {\n\t\tlogger.Fatal(\"must specify one and only one address\")\n\t}\n\taddr := args[0]\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\tlogger.Fatal(\"failed to read client config\", zap.Error(err))\n\t}\n\tvar config clientConfig\n\tif err := viper.Unmarshal(&config); err != nil {\n\t\tlogger.Fatal(\"failed to parse client config\", zap.Error(err))\n\t}\n\thyConfig, err := config.Config()\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to load client config\", zap.Error(err))\n\t}\n\n\tc, info, err := client.NewClient(hyConfig)\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to initialize client\", zap.Error(err))\n\t}\n\tdefer c.Close()\n\tlogger.Info(\"connected to server\",\n\t\tzap.Bool(\"udpEnabled\", info.UDPEnabled),\n\t\tzap.Uint64(\"tx\", info.Tx))\n\n\tlogger.Info(\"connecting\", zap.String(\"addr\", addr))\n\tstart := time.Now()\n\tconn, err := c.TCP(addr)\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to connect\", zap.Error(err), zap.String(\"time\", time.Since(start).String()))\n\t}\n\tdefer conn.Close()\n\n\tlogger.Info(\"connected\", zap.String(\"time\", time.Since(start).String()))\n}\n"
  },
  {
    "path": "app/cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\nconst (\n\tappLogo = `\n░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█░░░▀▀▄\n░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█░░░▄▀░\n░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀▀▀\n`\n\tappDesc    = \"a powerful, lightning fast and censorship resistant proxy\"\n\tappAuthors = \"Aperture Internet Laboratory <https://github.com/apernet>\"\n\n\tappLogLevelEnv           = \"HYSTERIA_LOG_LEVEL\"\n\tappLogFormatEnv          = \"HYSTERIA_LOG_FORMAT\"\n\tappDisableUpdateCheckEnv = \"HYSTERIA_DISABLE_UPDATE_CHECK\"\n\tappACMEDirEnv            = \"HYSTERIA_ACME_DIR\"\n)\n\nvar (\n\t// These values will be injected by the build system\n\tappVersion  = \"Unknown\"\n\tappDate     = \"Unknown\"\n\tappType     = \"Unknown\" // aka channel\n\tappCommit   = \"Unknown\"\n\tappPlatform = \"Unknown\"\n\tappArch     = \"Unknown\"\n\n\tappVersionLong = fmt.Sprintf(\"Version:\\t%s\\n\"+\n\t\t\"BuildDate:\\t%s\\n\"+\n\t\t\"BuildType:\\t%s\\n\"+\n\t\t\"CommitHash:\\t%s\\n\"+\n\t\t\"Platform:\\t%s\\n\"+\n\t\t\"Architecture:\\t%s\",\n\t\tappVersion, appDate, appType, appCommit, appPlatform, appArch)\n\n\tappAboutLong = fmt.Sprintf(\"%s\\n%s\\n%s\\n\\n%s\", appLogo, appDesc, appAuthors, appVersionLong)\n)\n\nvar logger *zap.Logger\n\n// Flags\nvar (\n\tcfgFile            string\n\tlogLevel           string\n\tlogFormat          string\n\tdisableUpdateCheck bool\n)\n\nvar rootCmd = &cobra.Command{\n\tUse:   \"hysteria\",\n\tShort: appDesc,\n\tLong:  appAboutLong,\n\tRun:   runClient, // Default to client mode\n}\n\nvar logLevelMap = map[string]zapcore.Level{\n\t\"debug\": zapcore.DebugLevel,\n\t\"info\":  zapcore.InfoLevel,\n\t\"warn\":  zapcore.WarnLevel,\n\t\"error\": zapcore.ErrorLevel,\n}\n\nvar logFormatMap = map[string]zapcore.EncoderConfig{\n\t\"console\": {\n\t\tTimeKey:        \"time\",\n\t\tLevelKey:       \"level\",\n\t\tNameKey:        \"logger\",\n\t\tMessageKey:     \"msg\",\n\t\tLineEnding:     zapcore.DefaultLineEnding,\n\t\tEncodeLevel:    zapcore.CapitalColorLevelEncoder,\n\t\tEncodeTime:     zapcore.RFC3339TimeEncoder,\n\t\tEncodeDuration: zapcore.SecondsDurationEncoder,\n\t},\n\t\"json\": {\n\t\tTimeKey:        \"time\",\n\t\tLevelKey:       \"level\",\n\t\tNameKey:        \"logger\",\n\t\tMessageKey:     \"msg\",\n\t\tLineEnding:     zapcore.DefaultLineEnding,\n\t\tEncodeLevel:    zapcore.LowercaseLevelEncoder,\n\t\tEncodeTime:     zapcore.EpochMillisTimeEncoder,\n\t\tEncodeDuration: zapcore.SecondsDurationEncoder,\n\t},\n}\n\nfunc Execute() {\n\terr := rootCmd.Execute()\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\tinitFlags()\n\tcobra.MousetrapHelpText = \"\" // Disable the mousetrap so Windows users can run the exe directly by double-clicking\n\tcobra.OnInitialize(initConfig)\n\tcobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config\n}\n\nfunc initFlags() {\n\trootCmd.PersistentFlags().StringVarP(&cfgFile, \"config\", \"c\", \"\", \"config file\")\n\trootCmd.PersistentFlags().StringVarP(&logLevel, \"log-level\", \"l\", envOrDefaultString(appLogLevelEnv, \"info\"), \"log level\")\n\trootCmd.PersistentFlags().StringVarP(&logFormat, \"log-format\", \"f\", envOrDefaultString(appLogFormatEnv, \"console\"), \"log format\")\n\trootCmd.PersistentFlags().BoolVar(&disableUpdateCheck, \"disable-update-check\", envOrDefaultBool(appDisableUpdateCheckEnv, false), \"disable update check\")\n}\n\nfunc initConfig() {\n\tif cfgFile != \"\" {\n\t\tviper.SetConfigFile(cfgFile)\n\t} else {\n\t\tviper.SetConfigName(\"config\")\n\t\tviper.SetConfigType(\"yaml\")\n\t\tviper.SupportedExts = append([]string{\"yaml\", \"yml\"}, viper.SupportedExts...)\n\t\tviper.AddConfigPath(\".\")\n\t\tviper.AddConfigPath(\"$HOME/.hysteria\")\n\t\tviper.AddConfigPath(\"/etc/hysteria/\")\n\t}\n}\n\nfunc initLogger() {\n\tlevel, ok := logLevelMap[strings.ToLower(logLevel)]\n\tif !ok {\n\t\tfmt.Printf(\"unsupported log level: %s\\n\", logLevel)\n\t\tos.Exit(1)\n\t}\n\tenc, ok := logFormatMap[strings.ToLower(logFormat)]\n\tif !ok {\n\t\tfmt.Printf(\"unsupported log format: %s\\n\", logFormat)\n\t\tos.Exit(1)\n\t}\n\tc := zap.Config{\n\t\tLevel:             zap.NewAtomicLevelAt(level),\n\t\tDisableCaller:     true,\n\t\tDisableStacktrace: true,\n\t\tEncoding:          strings.ToLower(logFormat),\n\t\tEncoderConfig:     enc,\n\t\tOutputPaths:       []string{\"stderr\"},\n\t\tErrorOutputPaths:  []string{\"stderr\"},\n\t}\n\tvar err error\n\tlogger, err = c.Build()\n\tif err != nil {\n\t\tfmt.Printf(\"failed to initialize logger: %s\\n\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc envOrDefaultString(key, def string) string {\n\tif v := os.Getenv(key); v != \"\" {\n\t\treturn v\n\t}\n\treturn def\n}\n\nfunc envOrDefaultBool(key string, def bool) bool {\n\tif v := os.Getenv(key); v != \"\" {\n\t\tb, _ := strconv.ParseBool(v)\n\t\treturn b\n\t}\n\treturn def\n}\n"
  },
  {
    "path": "app/cmd/server.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/caddyserver/certmagic\"\n\t\"github.com/libdns/cloudflare\"\n\t\"github.com/libdns/duckdns\"\n\t\"github.com/libdns/gandi\"\n\t\"github.com/libdns/godaddy\"\n\t\"github.com/libdns/namedotcom\"\n\t\"github.com/libdns/vultr\"\n\t\"github.com/mholt/acmez/acme\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"go.uber.org/zap\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/utils\"\n\t\"github.com/apernet/hysteria/core/v2/server\"\n\t\"github.com/apernet/hysteria/extras/v2/auth\"\n\t\"github.com/apernet/hysteria/extras/v2/correctnet\"\n\t\"github.com/apernet/hysteria/extras/v2/masq\"\n\t\"github.com/apernet/hysteria/extras/v2/obfs\"\n\t\"github.com/apernet/hysteria/extras/v2/outbounds\"\n\t\"github.com/apernet/hysteria/extras/v2/sniff\"\n\t\"github.com/apernet/hysteria/extras/v2/trafficlogger\"\n\teUtils \"github.com/apernet/hysteria/extras/v2/utils\"\n)\n\nconst (\n\tdefaultListenAddr = \":443\"\n)\n\nvar serverCmd = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"Server mode\",\n\tRun:   runServer,\n}\n\nfunc init() {\n\trootCmd.AddCommand(serverCmd)\n}\n\ntype serverConfig struct {\n\tV2board               *v2boardConfig              `mapstructure:\"v2board\"`\n\tListen                string                      `mapstructure:\"listen\"`\n\tObfs                  serverConfigObfs            `mapstructure:\"obfs\"`\n\tTLS                   *serverConfigTLS            `mapstructure:\"tls\"`\n\tACME                  *serverConfigACME           `mapstructure:\"acme\"`\n\tQUIC                  serverConfigQUIC            `mapstructure:\"quic\"`\n\tBandwidth             serverConfigBandwidth       `mapstructure:\"bandwidth\"`\n\tIgnoreClientBandwidth bool                        `mapstructure:\"ignoreClientBandwidth\"`\n\tSpeedTest             bool                        `mapstructure:\"speedTest\"`\n\tDisableUDP            bool                        `mapstructure:\"disableUDP\"`\n\tUDPIdleTimeout        time.Duration               `mapstructure:\"udpIdleTimeout\"`\n\tAuth                  serverConfigAuth            `mapstructure:\"auth\"`\n\tResolver              serverConfigResolver        `mapstructure:\"resolver\"`\n\tSniff                 serverConfigSniff           `mapstructure:\"sniff\"`\n\tACL                   serverConfigACL             `mapstructure:\"acl\"`\n\tOutbounds             []serverConfigOutboundEntry `mapstructure:\"outbounds\"`\n\tTrafficStats          serverConfigTrafficStats    `mapstructure:\"trafficStats\"`\n\tMasquerade            serverConfigMasquerade      `mapstructure:\"masquerade\"`\n}\n\ntype v2boardConfig struct {\n\tApiHost      string        `mapstructure:\"apiHost\"`\n\tApiKey       string        `mapstructure:\"apiKey\"`\n\tNodeID       uint          `mapstructure:\"nodeID\"`\n\tPullInterval time.Duration `mapstructure:\"pullInterval\"`\n\tPushInterval time.Duration `mapstructure:\"pushInterval\"`\n}\n\ntype serverConfigObfsSalamander struct {\n\tPassword string `mapstructure:\"password\"`\n}\n\ntype serverConfigObfs struct {\n\tType       string                     `mapstructure:\"type\"`\n\tSalamander serverConfigObfsSalamander `mapstructure:\"salamander\"`\n}\n\ntype serverConfigTLS struct {\n\tCert     string `mapstructure:\"cert\"`\n\tKey      string `mapstructure:\"key\"`\n\tSNIGuard string `mapstructure:\"sniGuard\"` // \"disable\", \"dns-san\", \"strict\"\n}\n\ntype serverConfigACME struct {\n\t// Common fields\n\tDomains    []string `mapstructure:\"domains\"`\n\tEmail      string   `mapstructure:\"email\"`\n\tCA         string   `mapstructure:\"ca\"`\n\tListenHost string   `mapstructure:\"listenHost\"`\n\tDir        string   `mapstructure:\"dir\"`\n\n\t// Type selection\n\tType string               `mapstructure:\"type\"`\n\tHTTP serverConfigACMEHTTP `mapstructure:\"http\"`\n\tTLS  serverConfigACMETLS  `mapstructure:\"tls\"`\n\tDNS  serverConfigACMEDNS  `mapstructure:\"dns\"`\n\n\t// Legacy fields for backwards compatibility\n\t// Only applicable when Type is empty\n\tDisableHTTP    bool `mapstructure:\"disableHTTP\"`\n\tDisableTLSALPN bool `mapstructure:\"disableTLSALPN\"`\n\tAltHTTPPort    int  `mapstructure:\"altHTTPPort\"`\n\tAltTLSALPNPort int  `mapstructure:\"altTLSALPNPort\"`\n}\n\ntype serverConfigACMEHTTP struct {\n\tAltPort int `mapstructure:\"altPort\"`\n}\n\ntype serverConfigACMETLS struct {\n\tAltPort int `mapstructure:\"altPort\"`\n}\n\ntype serverConfigACMEDNS struct {\n\tName   string            `mapstructure:\"name\"`\n\tConfig map[string]string `mapstructure:\"config\"`\n}\n\ntype serverConfigQUIC struct {\n\tInitStreamReceiveWindow     uint64        `mapstructure:\"initStreamReceiveWindow\"`\n\tMaxStreamReceiveWindow      uint64        `mapstructure:\"maxStreamReceiveWindow\"`\n\tInitConnectionReceiveWindow uint64        `mapstructure:\"initConnReceiveWindow\"`\n\tMaxConnectionReceiveWindow  uint64        `mapstructure:\"maxConnReceiveWindow\"`\n\tMaxIdleTimeout              time.Duration `mapstructure:\"maxIdleTimeout\"`\n\tMaxIncomingStreams          int64         `mapstructure:\"maxIncomingStreams\"`\n\tDisablePathMTUDiscovery     bool          `mapstructure:\"disablePathMTUDiscovery\"`\n}\n\ntype serverConfigBandwidth struct {\n\tUp   string `mapstructure:\"up\"`\n\tDown string `mapstructure:\"down\"`\n}\n\ntype serverConfigAuthHTTP struct {\n\tURL      string `mapstructure:\"url\"`\n\tInsecure bool   `mapstructure:\"insecure\"`\n}\n\ntype serverConfigAuth struct {\n\tType     string               `mapstructure:\"type\"`\n\tPassword string               `mapstructure:\"password\"`\n\tUserPass map[string]string    `mapstructure:\"userpass\"`\n\tHTTP     serverConfigAuthHTTP `mapstructure:\"http\"`\n\tCommand  string               `mapstructure:\"command\"`\n}\n\ntype serverConfigResolverTCP struct {\n\tAddr    string        `mapstructure:\"addr\"`\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n}\n\ntype serverConfigResolverUDP struct {\n\tAddr    string        `mapstructure:\"addr\"`\n\tTimeout time.Duration `mapstructure:\"timeout\"`\n}\n\ntype serverConfigResolverTLS struct {\n\tAddr     string        `mapstructure:\"addr\"`\n\tTimeout  time.Duration `mapstructure:\"timeout\"`\n\tSNI      string        `mapstructure:\"sni\"`\n\tInsecure bool          `mapstructure:\"insecure\"`\n}\n\ntype serverConfigResolverHTTPS struct {\n\tAddr     string        `mapstructure:\"addr\"`\n\tTimeout  time.Duration `mapstructure:\"timeout\"`\n\tSNI      string        `mapstructure:\"sni\"`\n\tInsecure bool          `mapstructure:\"insecure\"`\n}\n\ntype serverConfigResolver struct {\n\tType  string                    `mapstructure:\"type\"`\n\tTCP   serverConfigResolverTCP   `mapstructure:\"tcp\"`\n\tUDP   serverConfigResolverUDP   `mapstructure:\"udp\"`\n\tTLS   serverConfigResolverTLS   `mapstructure:\"tls\"`\n\tHTTPS serverConfigResolverHTTPS `mapstructure:\"https\"`\n}\n\ntype serverConfigSniff struct {\n\tEnable        bool          `mapstructure:\"enable\"`\n\tTimeout       time.Duration `mapstructure:\"timeout\"`\n\tRewriteDomain bool          `mapstructure:\"rewriteDomain\"`\n\tTCPPorts      string        `mapstructure:\"tcpPorts\"`\n\tUDPPorts      string        `mapstructure:\"udpPorts\"`\n}\n\ntype serverConfigACL struct {\n\tFile              string        `mapstructure:\"file\"`\n\tInline            []string      `mapstructure:\"inline\"`\n\tGeoIP             string        `mapstructure:\"geoip\"`\n\tGeoSite           string        `mapstructure:\"geosite\"`\n\tGeoUpdateInterval time.Duration `mapstructure:\"geoUpdateInterval\"`\n}\n\ntype serverConfigOutboundDirect struct {\n\tMode       string `mapstructure:\"mode\"`\n\tBindIPv4   string `mapstructure:\"bindIPv4\"`\n\tBindIPv6   string `mapstructure:\"bindIPv6\"`\n\tBindDevice string `mapstructure:\"bindDevice\"`\n}\n\ntype serverConfigOutboundSOCKS5 struct {\n\tAddr     string `mapstructure:\"addr\"`\n\tUsername string `mapstructure:\"username\"`\n\tPassword string `mapstructure:\"password\"`\n}\n\ntype serverConfigOutboundHTTP struct {\n\tURL      string `mapstructure:\"url\"`\n\tInsecure bool   `mapstructure:\"insecure\"`\n}\n\ntype serverConfigOutboundEntry struct {\n\tName   string                     `mapstructure:\"name\"`\n\tType   string                     `mapstructure:\"type\"`\n\tDirect serverConfigOutboundDirect `mapstructure:\"direct\"`\n\tSOCKS5 serverConfigOutboundSOCKS5 `mapstructure:\"socks5\"`\n\tHTTP   serverConfigOutboundHTTP   `mapstructure:\"http\"`\n}\n\ntype serverConfigTrafficStats struct {\n\tListen string `mapstructure:\"listen\"`\n\tSecret string `mapstructure:\"secret\"`\n}\n\ntype serverConfigMasqueradeFile struct {\n\tDir string `mapstructure:\"dir\"`\n}\n\ntype serverConfigMasqueradeProxy struct {\n\tURL         string `mapstructure:\"url\"`\n\tRewriteHost bool   `mapstructure:\"rewriteHost\"`\n}\n\ntype serverConfigMasqueradeString struct {\n\tContent    string            `mapstructure:\"content\"`\n\tHeaders    map[string]string `mapstructure:\"headers\"`\n\tStatusCode int               `mapstructure:\"statusCode\"`\n}\n\ntype serverConfigMasquerade struct {\n\tType        string                       `mapstructure:\"type\"`\n\tFile        serverConfigMasqueradeFile   `mapstructure:\"file\"`\n\tProxy       serverConfigMasqueradeProxy  `mapstructure:\"proxy\"`\n\tString      serverConfigMasqueradeString `mapstructure:\"string\"`\n\tListenHTTP  string                       `mapstructure:\"listenHTTP\"`\n\tListenHTTPS string                       `mapstructure:\"listenHTTPS\"`\n\tForceHTTPS  bool                         `mapstructure:\"forceHTTPS\"`\n}\n\nfunc (c *serverConfig) fillConn(hyConfig *server.Config) error {\n\tlistenAddr := c.Listen\n\tif listenAddr == \"\" {\n\t\tlistenAddr = defaultListenAddr\n\t}\n\tuAddr, err := net.ResolveUDPAddr(\"udp\", listenAddr)\n\tif err != nil {\n\t\treturn configError{Field: \"listen\", Err: err}\n\t}\n\tconn, err := correctnet.ListenUDP(\"udp\", uAddr)\n\tif err != nil {\n\t\treturn configError{Field: \"listen\", Err: err}\n\t}\n\tswitch strings.ToLower(c.Obfs.Type) {\n\tcase \"\", \"plain\":\n\t\thyConfig.Conn = conn\n\t\treturn nil\n\tcase \"salamander\":\n\t\tob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"obfs.salamander.password\", Err: err}\n\t\t}\n\t\thyConfig.Conn = obfs.WrapPacketConn(conn, ob)\n\t\treturn nil\n\tdefault:\n\t\treturn configError{Field: \"obfs.type\", Err: errors.New(\"unsupported obfuscation type\")}\n\t}\n}\n\nfunc (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {\n\tif c.TLS == nil && c.ACME == nil {\n\t\treturn configError{Field: \"tls\", Err: errors.New(\"must set either tls or acme\")}\n\t}\n\tif c.TLS != nil && c.ACME != nil {\n\t\treturn configError{Field: \"tls\", Err: errors.New(\"cannot set both tls and acme\")}\n\t}\n\tif c.TLS != nil {\n\t\t// SNI guard\n\t\tvar sniGuard utils.SNIGuardFunc\n\t\tswitch strings.ToLower(c.TLS.SNIGuard) {\n\t\tcase \"\", \"dns-san\":\n\t\t\tsniGuard = utils.SNIGuardDNSSAN\n\t\tcase \"strict\":\n\t\t\tsniGuard = utils.SNIGuardStrict\n\t\tcase \"disable\":\n\t\t\tsniGuard = nil\n\t\tdefault:\n\t\t\treturn configError{Field: \"tls.sniGuard\", Err: errors.New(\"unsupported SNI guard\")}\n\t\t}\n\t\t// Local TLS cert\n\t\tif c.TLS.Cert == \"\" || c.TLS.Key == \"\" {\n\t\t\treturn configError{Field: \"tls\", Err: errors.New(\"empty cert or key path\")}\n\t\t}\n\t\tcertLoader := &utils.LocalCertificateLoader{\n\t\t\tCertFile: c.TLS.Cert,\n\t\t\tKeyFile:  c.TLS.Key,\n\t\t\tSNIGuard: sniGuard,\n\t\t}\n\t\t// Try loading the cert-key pair here to catch errors early\n\t\t// (e.g. invalid files or insufficient permissions)\n\t\terr := certLoader.InitializeCache()\n\t\tif err != nil {\n\t\t\tvar pathErr *os.PathError\n\t\t\tif errors.As(err, &pathErr) {\n\t\t\t\tif pathErr.Path == c.TLS.Cert {\n\t\t\t\t\treturn configError{Field: \"tls.cert\", Err: pathErr}\n\t\t\t\t}\n\t\t\t\tif pathErr.Path == c.TLS.Key {\n\t\t\t\t\treturn configError{Field: \"tls.key\", Err: pathErr}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn configError{Field: \"tls\", Err: err}\n\t\t}\n\t\t// Use GetCertificate instead of Certificates so that\n\t\t// users can update the cert without restarting the server.\n\t\thyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate\n\t} else {\n\t\t// ACME\n\t\tdataDir := c.ACME.Dir\n\t\tif dataDir == \"\" {\n\t\t\t// If not specified in the config, check the environment variable\n\t\t\t// before resorting to the default \"acme\" value. The main reason\n\t\t\t// we have this is so that our setup script can set it to the\n\t\t\t// user's home directory.\n\t\t\tdataDir = envOrDefaultString(appACMEDirEnv, \"acme\")\n\t\t}\n\t\tcmCfg := &certmagic.Config{\n\t\t\tRenewalWindowRatio: certmagic.DefaultRenewalWindowRatio,\n\t\t\tKeySource:          certmagic.DefaultKeyGenerator,\n\t\t\tStorage:            &certmagic.FileStorage{Path: dataDir},\n\t\t\tLogger:             logger,\n\t\t}\n\t\tcmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{\n\t\t\tEmail:      c.ACME.Email,\n\t\t\tAgreed:     true,\n\t\t\tListenHost: c.ACME.ListenHost,\n\t\t\tLogger:     logger,\n\t\t})\n\t\tswitch strings.ToLower(c.ACME.CA) {\n\t\tcase \"letsencrypt\", \"le\", \"\":\n\t\t\t// Default to Let's Encrypt\n\t\t\tcmIssuer.CA = certmagic.LetsEncryptProductionCA\n\t\tcase \"zerossl\", \"zero\":\n\t\t\tcmIssuer.CA = certmagic.ZeroSSLProductionCA\n\t\t\teab, err := genZeroSSLEAB(c.ACME.Email)\n\t\t\tif err != nil {\n\t\t\t\treturn configError{Field: \"acme.ca\", Err: err}\n\t\t\t}\n\t\t\tcmIssuer.ExternalAccount = eab\n\t\tdefault:\n\t\t\treturn configError{Field: \"acme.ca\", Err: errors.New(\"unsupported CA\")}\n\t\t}\n\n\t\tswitch strings.ToLower(c.ACME.Type) {\n\t\tcase \"http\":\n\t\t\tcmIssuer.DisableHTTPChallenge = false\n\t\t\tcmIssuer.DisableTLSALPNChallenge = true\n\t\t\tcmIssuer.DNS01Solver = nil\n\t\t\tcmIssuer.AltHTTPPort = c.ACME.HTTP.AltPort\n\t\tcase \"tls\":\n\t\t\tcmIssuer.DisableHTTPChallenge = true\n\t\t\tcmIssuer.DisableTLSALPNChallenge = false\n\t\t\tcmIssuer.DNS01Solver = nil\n\t\t\tcmIssuer.AltTLSALPNPort = c.ACME.TLS.AltPort\n\t\tcase \"dns\":\n\t\t\tcmIssuer.DisableHTTPChallenge = true\n\t\t\tcmIssuer.DisableTLSALPNChallenge = true\n\t\t\tif c.ACME.DNS.Name == \"\" {\n\t\t\t\treturn configError{Field: \"acme.dns.name\", Err: errors.New(\"empty DNS provider name\")}\n\t\t\t}\n\t\t\tif c.ACME.DNS.Config == nil {\n\t\t\t\treturn configError{Field: \"acme.dns.config\", Err: errors.New(\"empty DNS provider config\")}\n\t\t\t}\n\t\t\tswitch strings.ToLower(c.ACME.DNS.Name) {\n\t\t\tcase \"cloudflare\":\n\t\t\t\tcmIssuer.DNS01Solver = &certmagic.DNS01Solver{\n\t\t\t\t\tDNSProvider: &cloudflare.Provider{\n\t\t\t\t\t\tAPIToken: c.ACME.DNS.Config[\"cloudflare_api_token\"],\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\tcase \"duckdns\":\n\t\t\t\tcmIssuer.DNS01Solver = &certmagic.DNS01Solver{\n\t\t\t\t\tDNSProvider: &duckdns.Provider{\n\t\t\t\t\t\tAPIToken:       c.ACME.DNS.Config[\"duckdns_api_token\"],\n\t\t\t\t\t\tOverrideDomain: c.ACME.DNS.Config[\"duckdns_override_domain\"],\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\tcase \"gandi\":\n\t\t\t\tcmIssuer.DNS01Solver = &certmagic.DNS01Solver{\n\t\t\t\t\tDNSProvider: &gandi.Provider{\n\t\t\t\t\t\tBearerToken: c.ACME.DNS.Config[\"gandi_api_token\"],\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\tcase \"godaddy\":\n\t\t\t\tcmIssuer.DNS01Solver = &certmagic.DNS01Solver{\n\t\t\t\t\tDNSProvider: &godaddy.Provider{\n\t\t\t\t\t\tAPIToken: c.ACME.DNS.Config[\"godaddy_api_token\"],\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\tcase \"namedotcom\":\n\t\t\t\tcmIssuer.DNS01Solver = &certmagic.DNS01Solver{\n\t\t\t\t\tDNSProvider: &namedotcom.Provider{\n\t\t\t\t\t\tToken:  c.ACME.DNS.Config[\"namedotcom_token\"],\n\t\t\t\t\t\tUser:   c.ACME.DNS.Config[\"namedotcom_user\"],\n\t\t\t\t\t\tServer: c.ACME.DNS.Config[\"namedotcom_server\"],\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\tcase \"vultr\":\n\t\t\t\tcmIssuer.DNS01Solver = &certmagic.DNS01Solver{\n\t\t\t\t\tDNSProvider: &vultr.Provider{\n\t\t\t\t\t\tAPIToken: c.ACME.DNS.Config[\"vultr_api_token\"],\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn configError{Field: \"acme.dns.name\", Err: errors.New(\"unsupported DNS provider\")}\n\t\t\t}\n\t\tcase \"\":\n\t\t\t// Legacy compatibility mode\n\t\t\tcmIssuer.DisableHTTPChallenge = c.ACME.DisableHTTP\n\t\t\tcmIssuer.DisableTLSALPNChallenge = c.ACME.DisableTLSALPN\n\t\t\tcmIssuer.AltHTTPPort = c.ACME.AltHTTPPort\n\t\t\tcmIssuer.AltTLSALPNPort = c.ACME.AltTLSALPNPort\n\t\tdefault:\n\t\t\treturn configError{Field: \"acme.type\", Err: errors.New(\"unsupported ACME type\")}\n\t\t}\n\n\t\tcmCfg.Issuers = []certmagic.Issuer{cmIssuer}\n\t\tcmCache := certmagic.NewCache(certmagic.CacheOptions{\n\t\t\tGetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {\n\t\t\t\treturn cmCfg, nil\n\t\t\t},\n\t\t\tLogger: logger,\n\t\t})\n\t\tcmCfg = certmagic.New(cmCache, *cmCfg)\n\n\t\tif len(c.ACME.Domains) == 0 {\n\t\t\treturn configError{Field: \"acme.domains\", Err: errors.New(\"empty domains\")}\n\t\t}\n\t\terr := cmCfg.ManageSync(context.Background(), c.ACME.Domains)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"acme.domains\", Err: err}\n\t\t}\n\t\thyConfig.TLSConfig.GetCertificate = cmCfg.GetCertificate\n\t}\n\treturn nil\n}\n\nfunc genZeroSSLEAB(email string) (*acme.EAB, error) {\n\treq, err := http.NewRequest(\n\t\thttp.MethodPost,\n\t\t\"https://api.zerossl.com/acme/eab-credentials-email\",\n\t\tstrings.NewReader(url.Values{\"email\": []string{email}}.Encode()),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to creare ZeroSSL EAB request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"User-Agent\", certmagic.UserAgent)\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send ZeroSSL EAB request: %w\", err)\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tvar result struct {\n\t\tSuccess bool `json:\"success\"`\n\t\tError   struct {\n\t\t\tCode int    `json:\"code\"`\n\t\t\tType string `json:\"type\"`\n\t\t} `json:\"error\"`\n\t\tEABKID     string `json:\"eab_kid\"`\n\t\tEABHMACKey string `json:\"eab_hmac_key\"`\n\t}\n\tif err = json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed decoding ZeroSSL EAB API response: %w\", err)\n\t}\n\tif result.Error.Code != 0 {\n\t\treturn nil, fmt.Errorf(\"failed getting ZeroSSL EAB credentials: HTTP %d: %s (code %d)\", resp.StatusCode, result.Error.Type, result.Error.Code)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"failed getting EAB credentials: HTTP %d\", resp.StatusCode)\n\t}\n\n\treturn &acme.EAB{\n\t\tKeyID:  result.EABKID,\n\t\tMACKey: result.EABHMACKey,\n\t}, nil\n}\n\nfunc (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error {\n\thyConfig.QUICConfig = server.QUICConfig{\n\t\tInitialStreamReceiveWindow:     c.QUIC.InitStreamReceiveWindow,\n\t\tMaxStreamReceiveWindow:         c.QUIC.MaxStreamReceiveWindow,\n\t\tInitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow,\n\t\tMaxConnectionReceiveWindow:     c.QUIC.MaxConnectionReceiveWindow,\n\t\tMaxIdleTimeout:                 c.QUIC.MaxIdleTimeout,\n\t\tMaxIncomingStreams:             c.QUIC.MaxIncomingStreams,\n\t\tDisablePathMTUDiscovery:        c.QUIC.DisablePathMTUDiscovery,\n\t}\n\treturn nil\n}\n\nfunc serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) {\n\tvar mode outbounds.DirectOutboundMode\n\tswitch strings.ToLower(c.Mode) {\n\tcase \"\", \"auto\":\n\t\tmode = outbounds.DirectOutboundModeAuto\n\tcase \"64\":\n\t\tmode = outbounds.DirectOutboundMode64\n\tcase \"46\":\n\t\tmode = outbounds.DirectOutboundMode46\n\tcase \"6\":\n\t\tmode = outbounds.DirectOutboundMode6\n\tcase \"4\":\n\t\tmode = outbounds.DirectOutboundMode4\n\tdefault:\n\t\treturn nil, configError{Field: \"outbounds.direct.mode\", Err: errors.New(\"unsupported mode\")}\n\t}\n\tbindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0\n\tbindDevice := len(c.BindDevice) > 0\n\tif bindIP && bindDevice {\n\t\treturn nil, configError{Field: \"outbounds.direct\", Err: errors.New(\"cannot bind both IP and device\")}\n\t}\n\tif bindIP {\n\t\tip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6)\n\t\tif len(c.BindIPv4) > 0 && ip4 == nil {\n\t\t\treturn nil, configError{Field: \"outbounds.direct.bindIPv4\", Err: errors.New(\"invalid IPv4 address\")}\n\t\t}\n\t\tif len(c.BindIPv6) > 0 && ip6 == nil {\n\t\t\treturn nil, configError{Field: \"outbounds.direct.bindIPv6\", Err: errors.New(\"invalid IPv6 address\")}\n\t\t}\n\t\treturn outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6)\n\t}\n\tif bindDevice {\n\t\treturn outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice)\n\t}\n\treturn outbounds.NewDirectOutboundSimple(mode), nil\n}\n\nfunc serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) {\n\tif c.Addr == \"\" {\n\t\treturn nil, configError{Field: \"outbounds.socks5.addr\", Err: errors.New(\"empty socks5 address\")}\n\t}\n\treturn outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil\n}\n\nfunc serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) {\n\tif c.URL == \"\" {\n\t\treturn nil, configError{Field: \"outbounds.http.url\", Err: errors.New(\"empty http address\")}\n\t}\n\treturn outbounds.NewHTTPOutbound(c.URL, c.Insecure)\n}\n\nfunc (c *serverConfig) fillRequestHook(hyConfig *server.Config) error {\n\tif c.Sniff.Enable {\n\t\ts := &sniff.Sniffer{\n\t\t\tTimeout:       c.Sniff.Timeout,\n\t\t\tRewriteDomain: c.Sniff.RewriteDomain,\n\t\t}\n\t\tif c.Sniff.TCPPorts != \"\" {\n\t\t\ts.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts)\n\t\t\tif s.TCPPorts == nil {\n\t\t\t\treturn configError{Field: \"sniff.tcpPorts\", Err: errors.New(\"invalid port union\")}\n\t\t\t}\n\t\t}\n\t\tif c.Sniff.UDPPorts != \"\" {\n\t\t\ts.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts)\n\t\t\tif s.UDPPorts == nil {\n\t\t\t\treturn configError{Field: \"sniff.udpPorts\", Err: errors.New(\"invalid port union\")}\n\t\t\t}\n\t\t}\n\t\thyConfig.RequestHook = s\n\t}\n\treturn nil\n}\n\nfunc (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {\n\t// Resolver, ACL, actual outbound are all implemented through the Outbound interface.\n\t// Depending on the config, we build a chain like this:\n\t// Resolver(ACL(Outbounds...))\n\n\t// Outbounds\n\tvar obs []outbounds.OutboundEntry\n\tif len(c.Outbounds) == 0 {\n\t\t// Guarantee we have at least one outbound\n\t\tobs = []outbounds.OutboundEntry{{\n\t\t\tName:     \"default\",\n\t\t\tOutbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto),\n\t\t}}\n\t} else {\n\t\tobs = make([]outbounds.OutboundEntry, len(c.Outbounds))\n\t\tfor i, entry := range c.Outbounds {\n\t\t\tif entry.Name == \"\" {\n\t\t\t\treturn configError{Field: \"outbounds.name\", Err: errors.New(\"empty outbound name\")}\n\t\t\t}\n\t\t\tvar ob outbounds.PluggableOutbound\n\t\t\tvar err error\n\t\t\tswitch strings.ToLower(entry.Type) {\n\t\t\tcase \"direct\":\n\t\t\t\tob, err = serverConfigOutboundDirectToOutbound(entry.Direct)\n\t\t\tcase \"socks5\":\n\t\t\t\tob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5)\n\t\t\tcase \"http\":\n\t\t\t\tob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP)\n\t\t\tdefault:\n\t\t\t\terr = configError{Field: \"outbounds.type\", Err: errors.New(\"unsupported outbound type\")}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tobs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob}\n\t\t}\n\t}\n\n\tvar uOb outbounds.PluggableOutbound // \"unified\" outbound\n\n\t// ACL\n\thasACL := false\n\tif c.ACL.File != \"\" && len(c.ACL.Inline) > 0 {\n\t\treturn configError{Field: \"acl\", Err: errors.New(\"cannot set both acl.file and acl.inline\")}\n\t}\n\tgLoader := &utils.GeoLoader{\n\t\tGeoIPFilename:   c.ACL.GeoIP,\n\t\tGeoSiteFilename: c.ACL.GeoSite,\n\t\tUpdateInterval:  c.ACL.GeoUpdateInterval,\n\t\tDownloadFunc:    geoDownloadFunc,\n\t\tDownloadErrFunc: geoDownloadErrFunc,\n\t}\n\tif c.ACL.File != \"\" {\n\t\thasACL = true\n\t\tacl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"acl.file\", Err: err}\n\t\t}\n\t\tuOb = acl\n\t} else if len(c.ACL.Inline) > 0 {\n\t\thasACL = true\n\t\tacl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, \"\\n\"), obs, gLoader)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"acl.inline\", Err: err}\n\t\t}\n\t\tuOb = acl\n\t} else {\n\t\t// No ACL, use the first outbound\n\t\tuOb = obs[0].Outbound\n\t}\n\n\t// Resolver\n\tswitch strings.ToLower(c.Resolver.Type) {\n\tcase \"\", \"system\":\n\t\tif hasACL {\n\t\t\t// If the user uses ACL, we must put a resolver in front of it,\n\t\t\t// for IP rules to work on domain requests.\n\t\t\tuOb = outbounds.NewSystemResolver(uOb)\n\t\t}\n\t\t// Otherwise we can just rely on outbound handling on its own.\n\tcase \"tcp\":\n\t\tif c.Resolver.TCP.Addr == \"\" {\n\t\t\treturn configError{Field: \"resolver.tcp.addr\", Err: errors.New(\"empty resolver address\")}\n\t\t}\n\t\tuOb = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, uOb)\n\tcase \"udp\":\n\t\tif c.Resolver.UDP.Addr == \"\" {\n\t\t\treturn configError{Field: \"resolver.udp.addr\", Err: errors.New(\"empty resolver address\")}\n\t\t}\n\t\tuOb = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, uOb)\n\tcase \"tls\", \"tcp-tls\":\n\t\tif c.Resolver.TLS.Addr == \"\" {\n\t\t\treturn configError{Field: \"resolver.tls.addr\", Err: errors.New(\"empty resolver address\")}\n\t\t}\n\t\tuOb = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, uOb)\n\tcase \"https\", \"http\":\n\t\tif c.Resolver.HTTPS.Addr == \"\" {\n\t\t\treturn configError{Field: \"resolver.https.addr\", Err: errors.New(\"empty resolver address\")}\n\t\t}\n\t\tuOb = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, uOb)\n\tdefault:\n\t\treturn configError{Field: \"resolver.type\", Err: errors.New(\"unsupported resolver type\")}\n\t}\n\n\t// Speed test\n\tif c.SpeedTest {\n\t\tuOb = outbounds.NewSpeedtestHandler(uOb)\n\t}\n\n\thyConfig.Outbound = &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb}\n\treturn nil\n}\n\nfunc (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error {\n\tvar err error\n\tif c.Bandwidth.Up != \"\" {\n\t\thyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"bandwidth.up\", Err: err}\n\t\t}\n\t}\n\tif c.Bandwidth.Down != \"\" {\n\t\thyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"bandwidth.down\", Err: err}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *serverConfig) fillIgnoreClientBandwidth(hyConfig *server.Config) error {\n\thyConfig.IgnoreClientBandwidth = c.IgnoreClientBandwidth\n\treturn nil\n}\n\nfunc (c *serverConfig) fillDisableUDP(hyConfig *server.Config) error {\n\thyConfig.DisableUDP = c.DisableUDP\n\treturn nil\n}\n\nfunc (c *serverConfig) fillUDPIdleTimeout(hyConfig *server.Config) error {\n\thyConfig.UDPIdleTimeout = c.UDPIdleTimeout\n\treturn nil\n}\n\nfunc (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error {\n\tif c.Auth.Type == \"\" {\n\t\treturn configError{Field: \"auth.type\", Err: errors.New(\"empty auth type\")}\n\t}\n\tswitch strings.ToLower(c.Auth.Type) {\n\tcase \"password\":\n\t\tif c.Auth.Password == \"\" {\n\t\t\treturn configError{Field: \"auth.password\", Err: errors.New(\"empty auth password\")}\n\t\t}\n\t\thyConfig.Authenticator = &auth.PasswordAuthenticator{Password: c.Auth.Password}\n\t\treturn nil\n\tcase \"userpass\":\n\t\tif len(c.Auth.UserPass) == 0 {\n\t\t\treturn configError{Field: \"auth.userpass\", Err: errors.New(\"empty auth userpass\")}\n\t\t}\n\t\thyConfig.Authenticator = &auth.UserPassAuthenticator{Users: c.Auth.UserPass}\n\t\treturn nil\n\tcase \"http\", \"https\":\n\t\tif c.Auth.HTTP.URL == \"\" {\n\t\t\treturn configError{Field: \"auth.http.url\", Err: errors.New(\"empty auth http url\")}\n\t\t}\n\t\thyConfig.Authenticator = auth.NewHTTPAuthenticator(c.Auth.HTTP.URL, c.Auth.HTTP.Insecure)\n\t\treturn nil\n\tcase \"command\", \"cmd\":\n\t\tif c.Auth.Command == \"\" {\n\t\t\treturn configError{Field: \"auth.command\", Err: errors.New(\"empty auth command\")}\n\t\t}\n\t\thyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command}\n\t\treturn nil\n\tcase \"v2board\":\n\t\tv2boardConfig := c.V2board\n\t\tif v2boardConfig.ApiHost == \"\" || v2boardConfig.ApiKey == \"\" || v2boardConfig.NodeID == 0 {\n\t\t\treturn configError{Field: \"auth.v2board\", Err: errors.New(\"v2board config error\")}\n\t\t}\n\t\thyConfig.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)}\n\n\t\treturn nil\n\tdefault:\n\t\treturn configError{Field: \"auth.type\", Err: errors.New(\"unsupported auth type\")}\n\t}\n}\n\nfunc (c *serverConfig) fillEventLogger(hyConfig *server.Config) error {\n\thyConfig.EventLogger = &serverLogger{}\n\treturn nil\n}\n\nfunc (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error {\n\tpullInterval := time.Second * 5\n\tif c.V2board.PullInterval > 0 {\n\t\tpullInterval = time.Duration(c.V2board.PullInterval) * time.Second\n\t}\n\tpushInterval := time.Second * 60\n\tif c.V2board.PushInterval > 0 {\n\t\tpushInterval = time.Duration(c.V2board.PushInterval) * time.Second\n\t}\n\tuserURL := 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)\n\tpushURL := 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)\n\tif c.TrafficStats.Listen != \"\" {\n\t\ttss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret)\n\t\thyConfig.TrafficLogger = tss\n\t\tif c.V2board != nil && c.V2board.ApiHost != \"\" {\n\t\t\tgo auth.UpdateUsers(userURL, pullInterval, hyConfig.TrafficLogger)\n\t\t\tgo hyConfig.TrafficLogger.PushTrafficToV2boardInterval(pushURL, pushInterval)\n\t\t}\n\t\tgo runTrafficStatsServer(c.TrafficStats.Listen, tss)\n\t} else {\n\t\tgo auth.UpdateUsers(userURL, pullInterval, nil)\n\t}\n\n\treturn nil\n}\n\n// fillMasqHandler must be called after fillConn, as we may need to extract the QUIC\n// port number from Conn for MasqTCPServer.\nfunc (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error {\n\tvar handler http.Handler\n\tswitch strings.ToLower(c.Masquerade.Type) {\n\tcase \"\", \"404\":\n\t\thandler = http.NotFoundHandler()\n\tcase \"file\":\n\t\tif c.Masquerade.File.Dir == \"\" {\n\t\t\treturn configError{Field: \"masquerade.file.dir\", Err: errors.New(\"empty file directory\")}\n\t\t}\n\t\thandler = http.FileServer(http.Dir(c.Masquerade.File.Dir))\n\tcase \"proxy\":\n\t\tif c.Masquerade.Proxy.URL == \"\" {\n\t\t\treturn configError{Field: \"masquerade.proxy.url\", Err: errors.New(\"empty proxy url\")}\n\t\t}\n\t\tu, err := url.Parse(c.Masquerade.Proxy.URL)\n\t\tif err != nil {\n\t\t\treturn configError{Field: \"masquerade.proxy.url\", Err: err}\n\t\t}\n\t\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\t\treturn configError{Field: \"masquerade.proxy.url\", Err: fmt.Errorf(\"unsupported protocol scheme \\\"%s\\\"\", u.Scheme)}\n\t\t}\n\t\thandler = &httputil.ReverseProxy{\n\t\t\tRewrite: func(r *httputil.ProxyRequest) {\n\t\t\t\tr.SetURL(u)\n\t\t\t\t// SetURL rewrites the Host header,\n\t\t\t\t// but we don't want that if rewriteHost is false\n\t\t\t\tif !c.Masquerade.Proxy.RewriteHost {\n\t\t\t\t\tr.Out.Host = r.In.Host\n\t\t\t\t}\n\t\t\t},\n\t\t\tErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {\n\t\t\t\tlogger.Error(\"HTTP reverse proxy error\", zap.Error(err))\n\t\t\t\tw.WriteHeader(http.StatusBadGateway)\n\t\t\t},\n\t\t}\n\tcase \"string\":\n\t\tif c.Masquerade.String.Content == \"\" {\n\t\t\treturn configError{Field: \"masquerade.string.content\", Err: errors.New(\"empty string content\")}\n\t\t}\n\t\tif c.Masquerade.String.StatusCode != 0 &&\n\t\t\t(c.Masquerade.String.StatusCode < 200 ||\n\t\t\t\tc.Masquerade.String.StatusCode > 599 ||\n\t\t\t\tc.Masquerade.String.StatusCode == 233) {\n\t\t\t// 233 is reserved for Hysteria authentication\n\t\t\treturn configError{Field: \"masquerade.string.statusCode\", Err: errors.New(\"invalid status code (must be 200-599, except 233)\")}\n\t\t}\n\t\thandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tfor k, v := range c.Masquerade.String.Headers {\n\t\t\t\tw.Header().Set(k, v)\n\t\t\t}\n\t\t\tif c.Masquerade.String.StatusCode != 0 {\n\t\t\t\tw.WriteHeader(c.Masquerade.String.StatusCode)\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(http.StatusOK) // Use 200 OK by default\n\t\t\t}\n\t\t\t_, _ = w.Write([]byte(c.Masquerade.String.Content))\n\t\t})\n\tdefault:\n\t\treturn configError{Field: \"masquerade.type\", Err: errors.New(\"unsupported masquerade type\")}\n\t}\n\thyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler, QUIC: true}\n\n\tif c.Masquerade.ListenHTTP != \"\" || c.Masquerade.ListenHTTPS != \"\" {\n\t\tif c.Masquerade.ListenHTTP != \"\" && c.Masquerade.ListenHTTPS == \"\" {\n\t\t\treturn configError{Field: \"masquerade.listenHTTPS\", Err: errors.New(\"having only HTTP server without HTTPS is not supported\")}\n\t\t}\n\t\ts := masq.MasqTCPServer{\n\t\t\tQUICPort:  extractPortFromAddr(hyConfig.Conn.LocalAddr().String()),\n\t\t\tHTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS),\n\t\t\tHandler:   &masqHandlerLogWrapper{H: handler, QUIC: false},\n\t\t\tTLSConfig: &tls.Config{\n\t\t\t\tCertificates:   hyConfig.TLSConfig.Certificates,\n\t\t\t\tGetCertificate: hyConfig.TLSConfig.GetCertificate,\n\t\t\t},\n\t\t\tForceHTTPS: c.Masquerade.ForceHTTPS,\n\t\t}\n\t\tgo runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS)\n\t}\n\treturn nil\n}\n\n// Config validates the fields and returns a ready-to-use Hysteria server config\nfunc (c *serverConfig) Config() (*server.Config, error) {\n\thyConfig := &server.Config{}\n\tfillers := []func(*server.Config) error{\n\t\tc.fillConn,\n\t\tc.fillTLSConfig,\n\t\tc.fillQUICConfig,\n\t\tc.fillRequestHook,\n\t\tc.fillOutboundConfig,\n\t\tc.fillBandwidthConfig,\n\t\tc.fillIgnoreClientBandwidth,\n\t\tc.fillDisableUDP,\n\t\tc.fillUDPIdleTimeout,\n\t\tc.fillAuthenticator,\n\t\tc.fillEventLogger,\n\t\tc.fillTrafficLogger,\n\t\tc.fillMasqHandler,\n\t}\n\tfor _, f := range fillers {\n\t\tif err := f(hyConfig); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn hyConfig, nil\n}\n\ntype ResponseNodeInfo struct {\n\tHost       string `json:\"host\"`\n\tServerPort uint   `json:\"server_port\"`\n\tServerName string `json:\"server_name\"`\n\tUpMbps     uint   `json:\"down_mbps\"`\n\tDownMbps   uint   `json:\"up_mbps\"`\n\tObfs       string `json:\"obfs\"`\n\tBaseConfig struct {\n\t\tPushInterval int `json:\"push_interval\"`\n\t\tPullInterval int `json:\"pull_interval\"`\n\t} `json:\"base_config\"`\n}\n\nfunc runServer(cmd *cobra.Command, args []string) {\n\tlogger.Info(\"server mode\")\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\tlogger.Fatal(\"failed to read server config\", zap.Error(err))\n\t}\n\tvar config serverConfig\n\tif err := viper.Unmarshal(&config); err != nil {\n\t\tlogger.Fatal(\"failed to parse server config\", zap.Error(err))\n\t}\n\t// 如果配置了v2board 则自动获取监听端口、obfs\n\tif config.V2board != nil && config.V2board.ApiHost != \"\" {\n\t\t// 创建一个url.Values来存储查询参数\n\t\tqueryParams := url.Values{\n\t\t\t\"token\":     {config.V2board.ApiKey},\n\t\t\t\"node_id\":   {strconv.Itoa(int(config.V2board.NodeID))},\n\t\t\t\"node_type\": {\"hysteria\"},\n\t\t}\n\t\tnodeInfoUrl := config.V2board.ApiHost + \"/api/v1/server/UniProxy/config?\" + queryParams.Encode()\n\t\tresp, err := http.Get(nodeInfoUrl)\n\t\tif err != nil {\n\t\t\t// 处理错误\n\t\t\tfmt.Println(\"HTTP GET 请求出错:\", err)\n\t\t\tlogger.Fatal(\"failed to client v2board api to get nodeInfo\", zap.Error(err))\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\t// 读取响应数据\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tlogger.Fatal(\"failed to read v2board reaponse\", zap.Error(err))\n\t\t}\n\t\t// 解析JSON数据\n\t\tvar responseNodeInfo ResponseNodeInfo\n\t\terr = json.Unmarshal(body, &responseNodeInfo)\n\t\tif err != nil {\n\t\t\tlogger.Fatal(\"failed to unmarshal v2board reaponse\", zap.Error(err))\n\t\t}\n\t\t// 给 hy的端口、obfs、上行下行进行赋值\n\t\tif responseNodeInfo.ServerPort != 0 {\n\t\t\tconfig.Listen = \":\" + strconv.Itoa(int(responseNodeInfo.ServerPort))\n\t\t}\n\t\tif responseNodeInfo.DownMbps != 0 {\n\t\t\tconfig.Bandwidth.Down = strconv.Itoa(int(responseNodeInfo.DownMbps)) + \"Mbps\"\n\t\t}\n\t\tif responseNodeInfo.UpMbps != 0 {\n\t\t\tconfig.Bandwidth.Up = strconv.Itoa(int(responseNodeInfo.UpMbps)) + \"Mbps\"\n\t\t}\n\t\tif responseNodeInfo.Obfs != \"\" {\n\t\t\tconfig.Obfs.Type = \"salamander\"\n\t\t\tconfig.Obfs.Salamander.Password = responseNodeInfo.Obfs\n\t\t}\n\t}\n\thyConfig, err := config.Config()\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to load server config\", zap.Error(err))\n\t}\n\n\ts, err := server.NewServer(hyConfig)\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to initialize server\", zap.Error(err))\n\t}\n\tif config.Listen != \"\" {\n\t\tlogger.Info(\"server up and running\", zap.String(\"listen\", config.Listen))\n\t} else {\n\t\tlogger.Info(\"server up and running\", zap.String(\"listen\", defaultListenAddr))\n\t}\n\n\tif !disableUpdateCheck {\n\t\tgo runCheckUpdateServer()\n\t}\n\n\tif err := s.Serve(); err != nil {\n\t\tlogger.Fatal(\"failed to serve\", zap.Error(err))\n\t}\n}\n\nfunc runTrafficStatsServer(listen string, handler http.Handler) {\n\tlogger.Info(\"traffic stats server up and running\", zap.String(\"listen\", listen))\n\tif err := correctnet.HTTPListenAndServe(listen, handler); err != nil {\n\t\tlogger.Fatal(\"failed to serve traffic stats\", zap.Error(err))\n\t}\n}\n\nfunc runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string) {\n\terrChan := make(chan error, 2)\n\tif httpAddr != \"\" {\n\t\tgo func() {\n\t\t\tlogger.Info(\"masquerade HTTP server up and running\", zap.String(\"listen\", httpAddr))\n\t\t\terrChan <- s.ListenAndServeHTTP(httpAddr)\n\t\t}()\n\t}\n\tif httpsAddr != \"\" {\n\t\tgo func() {\n\t\t\tlogger.Info(\"masquerade HTTPS server up and running\", zap.String(\"listen\", httpsAddr))\n\t\t\terrChan <- s.ListenAndServeHTTPS(httpsAddr)\n\t\t}()\n\t}\n\terr := <-errChan\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to serve masquerade HTTP(S)\", zap.Error(err))\n\t}\n}\n\nfunc geoDownloadFunc(filename, url string) {\n\tlogger.Info(\"downloading database\", zap.String(\"filename\", filename), zap.String(\"url\", url))\n}\n\nfunc geoDownloadErrFunc(err error) {\n\tif err != nil {\n\t\tlogger.Error(\"failed to download database\", zap.Error(err))\n\t}\n}\n\ntype serverLogger struct{}\n\nfunc (l *serverLogger) Connect(addr net.Addr, id string, tx uint64) {\n\tlogger.Info(\"client connected\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.Uint64(\"tx\", tx))\n}\n\nfunc (l *serverLogger) Disconnect(addr net.Addr, id string, err error) {\n\tlogger.Info(\"client disconnected\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.Error(err))\n}\n\nfunc (l *serverLogger) TCPRequest(addr net.Addr, id, reqAddr string) {\n\tlogger.Debug(\"TCP request\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.String(\"reqAddr\", reqAddr))\n}\n\nfunc (l *serverLogger) TCPError(addr net.Addr, id, reqAddr string, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"TCP closed\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.String(\"reqAddr\", reqAddr))\n\t} else {\n\t\tlogger.Warn(\"TCP error\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.String(\"reqAddr\", reqAddr), zap.Error(err))\n\t}\n}\n\nfunc (l *serverLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) {\n\tlogger.Debug(\"UDP request\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.Uint32(\"sessionID\", sessionID), zap.String(\"reqAddr\", reqAddr))\n}\n\nfunc (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) {\n\tif err == nil {\n\t\tlogger.Debug(\"UDP closed\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.Uint32(\"sessionID\", sessionID))\n\t} else {\n\t\tlogger.Warn(\"UDP error\", zap.String(\"addr\", addr.String()), zap.String(\"id\", id), zap.Uint32(\"sessionID\", sessionID), zap.Error(err))\n\t}\n}\n\ntype masqHandlerLogWrapper struct {\n\tH    http.Handler\n\tQUIC bool\n}\n\nfunc (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tlogger.Debug(\"masquerade request\",\n\t\tzap.String(\"addr\", r.RemoteAddr),\n\t\tzap.String(\"method\", r.Method),\n\t\tzap.String(\"host\", r.Host),\n\t\tzap.String(\"url\", r.URL.String()),\n\t\tzap.Bool(\"quic\", m.QUIC))\n\tm.H.ServeHTTP(w, r)\n}\n\nfunc extractPortFromAddr(addr string) int {\n\t_, portStr, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn 0\n\t}\n\tport, err := strconv.Atoi(portStr)\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn port\n}\n"
  },
  {
    "path": "app/cmd/server_test.go",
    "content": "package cmd\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/spf13/viper\"\n)\n\n// TestServerConfig tests the parsing of the server config\nfunc TestServerConfig(t *testing.T) {\n\tviper.SetConfigFile(\"server_test.yaml\")\n\terr := viper.ReadInConfig()\n\tassert.NoError(t, err)\n\tvar config serverConfig\n\terr = viper.Unmarshal(&config)\n\tassert.NoError(t, err)\n\tassert.Equal(t, config, serverConfig{\n\t\tListen: \":8443\",\n\t\tObfs: serverConfigObfs{\n\t\t\tType: \"salamander\",\n\t\t\tSalamander: serverConfigObfsSalamander{\n\t\t\t\tPassword: \"cry_me_a_r1ver\",\n\t\t\t},\n\t\t},\n\t\tTLS: &serverConfigTLS{\n\t\t\tCert:     \"some.crt\",\n\t\t\tKey:      \"some.key\",\n\t\t\tSNIGuard: \"strict\",\n\t\t},\n\t\tACME: &serverConfigACME{\n\t\t\tDomains: []string{\n\t\t\t\t\"sub1.example.com\",\n\t\t\t\t\"sub2.example.com\",\n\t\t\t},\n\t\t\tEmail:      \"haha@cringe.net\",\n\t\t\tCA:         \"zero\",\n\t\t\tListenHost: \"127.0.0.9\",\n\t\t\tDir:        \"random_dir\",\n\t\t\tType:       \"dns\",\n\t\t\tHTTP: serverConfigACMEHTTP{\n\t\t\t\tAltPort: 8888,\n\t\t\t},\n\t\t\tTLS: serverConfigACMETLS{\n\t\t\t\tAltPort: 44333,\n\t\t\t},\n\t\t\tDNS: serverConfigACMEDNS{\n\t\t\t\tName: \"gomommy\",\n\t\t\t\tConfig: map[string]string{\n\t\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\t\"key2\": \"value2\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tDisableHTTP:    true,\n\t\t\tDisableTLSALPN: true,\n\t\t\tAltHTTPPort:    8080,\n\t\t\tAltTLSALPNPort: 4433,\n\t\t},\n\t\tQUIC: serverConfigQUIC{\n\t\t\tInitStreamReceiveWindow:     77881,\n\t\t\tMaxStreamReceiveWindow:      77882,\n\t\t\tInitConnectionReceiveWindow: 77883,\n\t\t\tMaxConnectionReceiveWindow:  77884,\n\t\t\tMaxIdleTimeout:              999 * time.Second,\n\t\t\tMaxIncomingStreams:          256,\n\t\t\tDisablePathMTUDiscovery:     true,\n\t\t},\n\t\tBandwidth: serverConfigBandwidth{\n\t\t\tUp:   \"500 mbps\",\n\t\t\tDown: \"100 mbps\",\n\t\t},\n\t\tIgnoreClientBandwidth: true,\n\t\tSpeedTest:             true,\n\t\tDisableUDP:            true,\n\t\tUDPIdleTimeout:        120 * time.Second,\n\t\tAuth: serverConfigAuth{\n\t\t\tType:     \"password\",\n\t\t\tPassword: \"goofy_ahh_password\",\n\t\t\tUserPass: map[string]string{\n\t\t\t\t\"yolo\": \"swag\",\n\t\t\t\t\"lol\":  \"kek\",\n\t\t\t\t\"foo\":  \"bar\",\n\t\t\t},\n\t\t\tHTTP: serverConfigAuthHTTP{\n\t\t\t\tURL:      \"http://127.0.0.1:5000/auth\",\n\t\t\t\tInsecure: true,\n\t\t\t},\n\t\t\tCommand: \"/etc/some_command\",\n\t\t},\n\t\tResolver: serverConfigResolver{\n\t\t\tType: \"udp\",\n\t\t\tTCP: serverConfigResolverTCP{\n\t\t\t\tAddr:    \"123.123.123.123:5353\",\n\t\t\t\tTimeout: 4 * time.Second,\n\t\t\t},\n\t\t\tUDP: serverConfigResolverUDP{\n\t\t\t\tAddr:    \"4.6.8.0:53\",\n\t\t\t\tTimeout: 2 * time.Second,\n\t\t\t},\n\t\t\tTLS: serverConfigResolverTLS{\n\t\t\t\tAddr:     \"dot.yolo.com:8853\",\n\t\t\t\tTimeout:  10 * time.Second,\n\t\t\t\tSNI:      \"server1.yolo.net\",\n\t\t\t\tInsecure: true,\n\t\t\t},\n\t\t\tHTTPS: serverConfigResolverHTTPS{\n\t\t\t\tAddr:     \"cringe.ahh.cc\",\n\t\t\t\tTimeout:  5 * time.Second,\n\t\t\t\tSNI:      \"real.stuff.net\",\n\t\t\t\tInsecure: true,\n\t\t\t},\n\t\t},\n\t\tSniff: serverConfigSniff{\n\t\t\tEnable:        true,\n\t\t\tTimeout:       1 * time.Second,\n\t\t\tRewriteDomain: true,\n\t\t\tTCPPorts:      \"80,443,1000-2000\",\n\t\t\tUDPPorts:      \"443\",\n\t\t},\n\t\tACL: serverConfigACL{\n\t\t\tFile: \"chnroute.txt\",\n\t\t\tInline: []string{\n\t\t\t\t\"lmao(ok)\",\n\t\t\t\t\"kek(cringe,boba,tea)\",\n\t\t\t},\n\t\t\tGeoIP:             \"some.dat\",\n\t\t\tGeoSite:           \"some_site.dat\",\n\t\t\tGeoUpdateInterval: 168 * time.Hour,\n\t\t},\n\t\tOutbounds: []serverConfigOutboundEntry{\n\t\t\t{\n\t\t\t\tName: \"goodstuff\",\n\t\t\t\tType: \"direct\",\n\t\t\t\tDirect: serverConfigOutboundDirect{\n\t\t\t\t\tMode:       \"64\",\n\t\t\t\t\tBindIPv4:   \"2.4.6.8\",\n\t\t\t\t\tBindIPv6:   \"0:0:0:0:0:ffff:0204:0608\",\n\t\t\t\t\tBindDevice: \"eth233\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"badstuff\",\n\t\t\t\tType: \"socks5\",\n\t\t\t\tSOCKS5: serverConfigOutboundSOCKS5{\n\t\t\t\t\tAddr:     \"shady.proxy.ru:1080\",\n\t\t\t\t\tUsername: \"hackerman\",\n\t\t\t\t\tPassword: \"Elliot Alderson\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"weirdstuff\",\n\t\t\t\tType: \"http\",\n\t\t\t\tHTTP: serverConfigOutboundHTTP{\n\t\t\t\t\tURL:      \"https://eyy.lmao:4443/goofy\",\n\t\t\t\t\tInsecure: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tTrafficStats: serverConfigTrafficStats{\n\t\t\tListen: \":9999\",\n\t\t\tSecret: \"its_me_mario\",\n\t\t},\n\t\tMasquerade: serverConfigMasquerade{\n\t\t\tType: \"proxy\",\n\t\t\tFile: serverConfigMasqueradeFile{\n\t\t\t\tDir: \"/www/masq\",\n\t\t\t},\n\t\t\tProxy: serverConfigMasqueradeProxy{\n\t\t\t\tURL:         \"https://some.site.net\",\n\t\t\t\tRewriteHost: true,\n\t\t\t},\n\t\t\tString: serverConfigMasqueradeString{\n\t\t\t\tContent: \"aint nothin here\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"content-type\": \"text/plain\",\n\t\t\t\t\t\"custom-haha\":  \"lol\",\n\t\t\t\t},\n\t\t\t\tStatusCode: 418,\n\t\t\t},\n\t\t\tListenHTTP:  \":80\",\n\t\t\tListenHTTPS: \":443\",\n\t\t\tForceHTTPS:  true,\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "app/cmd/server_test.yaml",
    "content": "listen: :8443\n\nobfs:\n  type: salamander\n  salamander:\n    password: cry_me_a_r1ver\n\ntls:\n  cert: some.crt\n  key: some.key\n  sniGuard: strict\n\nacme:\n  domains:\n    - sub1.example.com\n    - sub2.example.com\n  email: haha@cringe.net\n  ca: zero\n  listenHost: 127.0.0.9\n  dir: random_dir\n  type: dns\n  http:\n    altPort: 8888\n  tls:\n    altPort: 44333\n  dns:\n    name: gomommy\n    config:\n      key1: value1\n      key2: value2\n  disableHTTP: true\n  disableTLSALPN: true\n  altHTTPPort: 8080\n  altTLSALPNPort: 4433\n\nquic:\n  initStreamReceiveWindow: 77881\n  maxStreamReceiveWindow: 77882\n  initConnReceiveWindow: 77883\n  maxConnReceiveWindow: 77884\n  maxIdleTimeout: 999s\n  maxIncomingStreams: 256\n  disablePathMTUDiscovery: true\n\nbandwidth:\n  up: 500 mbps\n  down: 100 mbps\n\nignoreClientBandwidth: true\n\nspeedTest: true\n\ndisableUDP: true\nudpIdleTimeout: 120s\n\nauth:\n  type: password\n  password: goofy_ahh_password\n  userpass:\n    yolo: swag\n    lol: kek\n    foo: bar\n  http:\n    url: http://127.0.0.1:5000/auth\n    insecure: true\n  command: /etc/some_command\n\nresolver:\n  type: udp\n  tcp:\n    addr: 123.123.123.123:5353\n    timeout: 4s\n  udp:\n    addr: 4.6.8.0:53\n    timeout: 2s\n  tls:\n    addr: dot.yolo.com:8853\n    timeout: 10s\n    sni: server1.yolo.net\n    insecure: true\n  https:\n    addr: cringe.ahh.cc\n    timeout: 5s\n    sni: real.stuff.net\n    insecure: true\n\nsniff:\n  enable: true\n  timeout: 1s\n  rewriteDomain: true\n  tcpPorts: 80,443,1000-2000\n  udpPorts: 443\n\nacl:\n  file: chnroute.txt\n  inline:\n    - lmao(ok)\n    - kek(cringe,boba,tea)\n  geoip: some.dat\n  geosite: some_site.dat\n  geoUpdateInterval: 168h\n\noutbounds:\n  - name: goodstuff\n    type: direct\n    direct:\n      mode: 64\n      bindIPv4: 2.4.6.8\n      bindIPv6: 0:0:0:0:0:ffff:0204:0608\n      bindDevice: eth233\n  - name: badstuff\n    type: socks5\n    socks5:\n      addr: shady.proxy.ru:1080\n      username: hackerman\n      password: Elliot Alderson\n  - name: weirdstuff\n    type: http\n    http:\n      url: https://eyy.lmao:4443/goofy\n      insecure: true\n\ntrafficStats:\n  listen: :9999\n  secret: its_me_mario\n\nmasquerade:\n  type: proxy\n  file:\n    dir: /www/masq\n  proxy:\n    url: https://some.site.net\n    rewriteHost: true\n  string:\n    content: aint nothin here\n    headers:\n      content-type: text/plain\n      custom-haha: lol\n    statusCode: 418\n  listenHTTP: :80\n  listenHTTPS: :443\n  forceHTTPS: true\n"
  },
  {
    "path": "app/cmd/share.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/utils\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"go.uber.org/zap\"\n)\n\nvar (\n\tnoText bool\n\twithQR bool\n)\n\n// shareCmd represents the share command\nvar shareCmd = &cobra.Command{\n\tUse:   \"share\",\n\tShort: \"Generate share URI\",\n\tLong:  \"Generate a hysteria2:// URI from a client config for sharing\",\n\tRun:   runShare,\n}\n\nfunc init() {\n\tinitShareFlags()\n\trootCmd.AddCommand(shareCmd)\n}\n\nfunc initShareFlags() {\n\tshareCmd.Flags().BoolVar(&noText, \"notext\", false, \"do not show URI as text\")\n\tshareCmd.Flags().BoolVar(&withQR, \"qr\", false, \"show URI as QR code\")\n}\n\nfunc runShare(cmd *cobra.Command, args []string) {\n\tif err := viper.ReadInConfig(); err != nil {\n\t\tlogger.Fatal(\"failed to read client config\", zap.Error(err))\n\t}\n\tvar config clientConfig\n\tif err := viper.Unmarshal(&config); err != nil {\n\t\tlogger.Fatal(\"failed to parse client config\", zap.Error(err))\n\t}\n\tif _, err := config.Config(); err != nil {\n\t\tlogger.Fatal(\"failed to load client config\", zap.Error(err))\n\t}\n\n\tu := config.URI()\n\n\tif !noText {\n\t\tfmt.Println(u)\n\t}\n\tif withQR {\n\t\tutils.PrintQR(u)\n\t}\n}\n"
  },
  {
    "path": "app/cmd/speedtest.go",
    "content": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"go.uber.org/zap\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n\thyErrors \"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/extras/v2/outbounds\"\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/speedtest\"\n)\n\nvar (\n\tskipDownload bool\n\tskipUpload   bool\n\tdataSize     uint32\n\tuseBytes     bool\n\n\tspeedtestAddr = fmt.Sprintf(\"%s:%d\", outbounds.SpeedtestDest, 0)\n)\n\n// speedtestCmd represents the speedtest command\nvar speedtestCmd = &cobra.Command{\n\tUse:   \"speedtest\",\n\tShort: \"Speed test mode\",\n\tLong:  \"Perform a speed test through the proxy server. The server must have speed test support enabled.\",\n\tRun:   runSpeedtest,\n}\n\nfunc init() {\n\tinitSpeedtestFlags()\n\trootCmd.AddCommand(speedtestCmd)\n}\n\nfunc initSpeedtestFlags() {\n\tspeedtestCmd.Flags().BoolVar(&skipDownload, \"skip-download\", false, \"Skip download test\")\n\tspeedtestCmd.Flags().BoolVar(&skipUpload, \"skip-upload\", false, \"Skip upload test\")\n\tspeedtestCmd.Flags().Uint32Var(&dataSize, \"data-size\", 1024*1024*100, \"Data size for download and upload tests\")\n\tspeedtestCmd.Flags().BoolVar(&useBytes, \"use-bytes\", false, \"Use bytes per second instead of bits per second\")\n}\n\nfunc runSpeedtest(cmd *cobra.Command, args []string) {\n\tlogger.Info(\"speed test mode\")\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\tlogger.Fatal(\"failed to read client config\", zap.Error(err))\n\t}\n\tvar config clientConfig\n\tif err := viper.Unmarshal(&config); err != nil {\n\t\tlogger.Fatal(\"failed to parse client config\", zap.Error(err))\n\t}\n\thyConfig, err := config.Config()\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to load client config\", zap.Error(err))\n\t}\n\n\tc, info, err := client.NewClient(hyConfig)\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to initialize client\", zap.Error(err))\n\t}\n\tdefer c.Close()\n\tlogger.Info(\"connected to server\",\n\t\tzap.Bool(\"udpEnabled\", info.UDPEnabled),\n\t\tzap.Uint64(\"tx\", info.Tx))\n\n\tsignalChan := make(chan os.Signal, 1)\n\tsignal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)\n\tdefer signal.Stop(signalChan)\n\n\trunChan := make(chan struct{}, 1)\n\tgo func() {\n\t\tif !skipDownload {\n\t\t\trunDownloadTest(c)\n\t\t}\n\t\tif !skipUpload {\n\t\t\trunUploadTest(c)\n\t\t}\n\t\trunChan <- struct{}{}\n\t}()\n\n\tselect {\n\tcase <-signalChan:\n\t\tlogger.Info(\"received signal, shutting down gracefully\")\n\tcase <-runChan:\n\t\tlogger.Info(\"speed test complete\")\n\t}\n}\n\nfunc runDownloadTest(c client.Client) {\n\tlogger.Info(\"performing download test\")\n\tdownConn, err := c.TCP(speedtestAddr)\n\tif err != nil {\n\t\tif errors.As(err, &hyErrors.DialError{}) {\n\t\t\tlogger.Fatal(\"failed to connect (server may not support speed test)\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Fatal(\"failed to connect\", zap.Error(err))\n\t\t}\n\t}\n\tdefer downConn.Close()\n\n\tdownClient := &speedtest.Client{Conn: downConn}\n\tcurrentTotal := uint32(0)\n\terr = downClient.Download(dataSize, func(d time.Duration, b uint32, done bool) {\n\t\tif !done {\n\t\t\tcurrentTotal += b\n\t\t\tlogger.Info(\"downloading\",\n\t\t\t\tzap.Uint32(\"bytes\", b),\n\t\t\t\tzap.String(\"progress\", fmt.Sprintf(\"%.2f%%\", float64(currentTotal)/float64(dataSize)*100)),\n\t\t\t\tzap.String(\"speed\", formatSpeed(b, d, useBytes)))\n\t\t} else {\n\t\t\tlogger.Info(\"download complete\",\n\t\t\t\tzap.Uint32(\"bytes\", b),\n\t\t\t\tzap.String(\"speed\", formatSpeed(b, d, useBytes)))\n\t\t}\n\t})\n\tif err != nil {\n\t\tlogger.Fatal(\"download test failed\", zap.Error(err))\n\t}\n\tlogger.Info(\"download test complete\")\n}\n\nfunc runUploadTest(c client.Client) {\n\tlogger.Info(\"performing upload test\")\n\tupConn, err := c.TCP(speedtestAddr)\n\tif err != nil {\n\t\tif errors.As(err, &hyErrors.DialError{}) {\n\t\t\tlogger.Fatal(\"failed to connect (server may not support speed test)\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Fatal(\"failed to connect\", zap.Error(err))\n\t\t}\n\t}\n\tdefer upConn.Close()\n\n\tupClient := &speedtest.Client{Conn: upConn}\n\tcurrentTotal := uint32(0)\n\terr = upClient.Upload(dataSize, func(d time.Duration, b uint32, done bool) {\n\t\tif !done {\n\t\t\tcurrentTotal += b\n\t\t\tlogger.Info(\"uploading\",\n\t\t\t\tzap.Uint32(\"bytes\", b),\n\t\t\t\tzap.String(\"progress\", fmt.Sprintf(\"%.2f%%\", float64(currentTotal)/float64(dataSize)*100)),\n\t\t\t\tzap.String(\"speed\", formatSpeed(b, d, useBytes)))\n\t\t} else {\n\t\t\tlogger.Info(\"upload complete\",\n\t\t\t\tzap.Uint32(\"bytes\", b),\n\t\t\t\tzap.String(\"speed\", formatSpeed(b, d, useBytes)))\n\t\t}\n\t})\n\tif err != nil {\n\t\tlogger.Fatal(\"upload test failed\", zap.Error(err))\n\t}\n\tlogger.Info(\"upload test complete\")\n}\n\nfunc formatSpeed(bytes uint32, duration time.Duration, useBytes bool) string {\n\tspeed := float64(bytes) / duration.Seconds()\n\tvar units []string\n\tif useBytes {\n\t\tunits = []string{\"B/s\", \"KB/s\", \"MB/s\", \"GB/s\"}\n\t} else {\n\t\tunits = []string{\"bps\", \"Kbps\", \"Mbps\", \"Gbps\"}\n\t\tspeed *= 8\n\t}\n\tunitIndex := 0\n\tfor speed > 1000 && unitIndex < len(units)-1 {\n\t\tspeed /= 1000\n\t\tunitIndex++\n\t}\n\treturn fmt.Sprintf(\"%.2f %s\", speed, units[unitIndex])\n}\n"
  },
  {
    "path": "app/cmd/update.go",
    "content": "package cmd\n\nimport (\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"go.uber.org/zap\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/utils\"\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst (\n\tupdateCheckInterval = 24 * time.Hour\n)\n\n// checkUpdateCmd represents the checkUpdate command\nvar checkUpdateCmd = &cobra.Command{\n\tUse:   \"check-update\",\n\tShort: \"Check for updates\",\n\tLong:  \"Check for updates.\",\n\tRun:   runCheckUpdate,\n}\n\nfunc init() {\n\trootCmd.AddCommand(checkUpdateCmd)\n}\n\nfunc runCheckUpdate(cmd *cobra.Command, args []string) {\n\tlogger.Info(\"checking for updates\",\n\t\tzap.String(\"version\", appVersion),\n\t\tzap.String(\"platform\", appPlatform),\n\t\tzap.String(\"arch\", appArch),\n\t\tzap.String(\"channel\", appType),\n\t)\n\n\tchecker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)\n\tresp, err := checker.Check()\n\tif err != nil {\n\t\tlogger.Fatal(\"failed to check for updates\", zap.Error(err))\n\t}\n\tif resp.HasUpdate {\n\t\tlogger.Info(\"update available\",\n\t\t\tzap.String(\"version\", resp.LatestVersion),\n\t\t\tzap.String(\"url\", resp.URL),\n\t\t\tzap.Bool(\"urgent\", resp.Urgent),\n\t\t)\n\t} else {\n\t\tlogger.Info(\"no update available\")\n\t}\n}\n\n// runCheckUpdateServer is the background update checking routine for server mode\nfunc runCheckUpdateServer() {\n\tchecker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)\n\tcheckUpdateRoutine(checker)\n}\n\n// runCheckUpdateClient is the background update checking routine for client mode\nfunc runCheckUpdateClient(hyClient client.Client) {\n\tchecker := utils.NewClientUpdateChecker(appVersion, appPlatform, appArch, appType, hyClient)\n\tcheckUpdateRoutine(checker)\n}\n\nfunc checkUpdateRoutine(checker *utils.UpdateChecker) {\n\tticker := time.NewTicker(updateCheckInterval)\n\tfor {\n\t\tlogger.Debug(\"checking for updates\",\n\t\t\tzap.String(\"version\", appVersion),\n\t\t\tzap.String(\"platform\", appPlatform),\n\t\t\tzap.String(\"arch\", appArch),\n\t\t\tzap.String(\"channel\", appType),\n\t\t)\n\t\tresp, err := checker.Check()\n\t\tif err != nil {\n\t\t\tlogger.Debug(\"failed to check for updates\", zap.Error(err))\n\t\t} else if resp.HasUpdate {\n\t\t\tlogger.Info(\"update available\",\n\t\t\t\tzap.String(\"version\", resp.LatestVersion),\n\t\t\t\tzap.String(\"url\", resp.URL),\n\t\t\t\tzap.Bool(\"urgent\", resp.Urgent),\n\t\t\t)\n\t\t} else {\n\t\t\tlogger.Debug(\"no update available\")\n\t\t}\n\t\t<-ticker.C\n\t}\n}\n"
  },
  {
    "path": "app/cmd/version.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// versionCmd represents the version command\nvar versionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Show version\",\n\tLong:  \"Show version.\",\n\tRun:   runVersion,\n}\n\nfunc init() {\n\trootCmd.AddCommand(versionCmd)\n}\n\nfunc runVersion(cmd *cobra.Command, args []string) {\n\tfmt.Println(appAboutLong)\n}\n"
  },
  {
    "path": "app/go.mod",
    "content": "module github.com/apernet/hysteria/app/v2\n\ngo 1.22\n\ntoolchain go1.23.2\n\nrequire (\n\tgithub.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f\n\tgithub.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000\n\tgithub.com/apernet/hysteria/extras/v2 v2.0.0-00010101000000-000000000000\n\tgithub.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad\n\tgithub.com/caddyserver/certmagic v0.17.2\n\tgithub.com/libdns/cloudflare v0.1.1\n\tgithub.com/libdns/duckdns v0.2.0\n\tgithub.com/libdns/gandi v1.0.3\n\tgithub.com/libdns/godaddy v1.0.3\n\tgithub.com/libdns/namedotcom v0.3.3\n\tgithub.com/libdns/vultr v1.0.0\n\tgithub.com/mdp/qrterminal/v3 v3.1.1\n\tgithub.com/mholt/acmez v1.0.4\n\tgithub.com/sagernet/sing v0.3.2\n\tgithub.com/spf13/cobra v1.7.0\n\tgithub.com/spf13/viper v1.15.0\n\tgithub.com/stretchr/testify v1.9.0\n\tgithub.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301\n\tgo.uber.org/zap v1.24.0\n\tgolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842\n\tgolang.org/x/sys v0.23.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.1.0 // indirect\n\tgithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d // indirect\n\tgithub.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect\n\tgithub.com/cloudflare/circl v1.3.9 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/fsnotify/fsnotify v1.7.0 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.6 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.5 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/klauspost/compress v1.17.9 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.1.1 // indirect\n\tgithub.com/libdns/libdns v0.2.2 // indirect\n\tgithub.com/magiconair/properties v1.8.7 // indirect\n\tgithub.com/miekg/dns v1.1.59 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/onsi/ginkgo/v2 v2.9.5 // indirect\n\tgithub.com/patrickmn/go-cache v2.1.0+incompatible // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.0.6 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/refraction-networking/utls v1.6.6 // indirect\n\tgithub.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect\n\tgithub.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect\n\tgithub.com/spf13/afero v1.9.3 // indirect\n\tgithub.com/spf13/cast v1.5.0 // indirect\n\tgithub.com/spf13/jwalterweatherman v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/subosito/gotenv v1.4.2 // indirect\n\tgithub.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect\n\tgithub.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect\n\tgithub.com/vultr/govultr/v3 v3.6.4 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgo.uber.org/mock v0.4.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect\n\tgolang.org/x/crypto v0.26.0 // indirect\n\tgolang.org/x/mod v0.17.0 // indirect\n\tgolang.org/x/net v0.28.0 // indirect\n\tgolang.org/x/oauth2 v0.20.0 // indirect\n\tgolang.org/x/sync v0.8.0 // indirect\n\tgolang.org/x/text v0.17.0 // indirect\n\tgolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect\n\tgoogle.golang.org/protobuf v1.34.1 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\trsc.io/qr v0.2.0 // indirect\n)\n\nreplace github.com/apernet/hysteria/core/v2 => ../core\n\nreplace github.com/apernet/hysteria/extras/v2 => ../extras\n"
  },
  {
    "path": "app/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=\ngithub.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=\ngithub.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY=\ngithub.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM=\ngithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d h1:KWRCWISqJOgY9/0hhH8Bevjw/k4tCQ7oJlXLyFv8u9s=\ngithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d/go.mod h1:x0paLlmCzNOUDDQIgmgFWmnpWQIEuH1GNfA6NdgSTuM=\ngithub.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad h1:QzQ2sKpc9o42HNRR8ukM5uMC/RzR2HgZd/Nvaqol2C0=\ngithub.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad/go.mod h1:S5IydyLSN/QAfvY+r2GoomPJ6hidtXWm/Ad18sJVssk=\ngithub.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=\ngithub.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=\ngithub.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=\ngithub.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=\ngithub.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=\ngithub.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=\ngithub.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=\ngithub.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=\ngithub.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=\ngithub.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=\ngithub.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=\ngithub.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=\ngithub.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0=\ngithub.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=\ngithub.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=\ngithub.com/libdns/duckdns v0.2.0 h1:vd3pE09G2qTx1Zh1o3LmrivWSByD3Z5FbL7csX5vDgE=\ngithub.com/libdns/duckdns v0.2.0/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs=\ngithub.com/libdns/gandi v1.0.3 h1:FIvipWOg/O4zi75fPRmtcolRKqI6MgrbpFy2p5KYdUk=\ngithub.com/libdns/gandi v1.0.3/go.mod h1:G6dw58Xnji2xX+lb+uZxGbtmfxKllm1CGHE2bOPG3WA=\ngithub.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44=\ngithub.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM=\ngithub.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=\ngithub.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=\ngithub.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=\ngithub.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE=\ngithub.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s=\ngithub.com/libdns/vultr v1.0.0 h1:W8B4+k2bm9ro3bZLSZV9hMOQI+uO6Svu+GmD+Olz7ZI=\ngithub.com/libdns/vultr v1.0.0/go.mod h1:8K1HJExcbeHS4YPkFHRZpqpXZzZ+DZAA0m0VikJgEqk=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc=\ngithub.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU=\ngithub.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=\ngithub.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=\ngithub.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=\ngithub.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=\ngithub.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=\ngithub.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=\ngithub.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=\ngithub.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=\ngithub.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=\ngithub.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=\ngithub.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=\ngithub.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=\ngithub.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=\ngithub.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=\ngithub.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo=\ngithub.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE=\ngithub.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=\ngithub.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=\ngithub.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=\ngithub.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=\ngithub.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=\ngithub.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=\ngithub.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=\ngithub.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=\ngithub.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=\ngithub.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=\ngithub.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=\ngithub.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=\ngithub.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM=\ngithub.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=\ngithub.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=\ngithub.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/vultr/govultr/v3 v3.6.4 h1:unvY9eXlBw667ECQZDbBDOIaWB8wkk6Bx+yB0IMKXJ4=\ngithub.com/vultr/govultr/v3 v3.6.4/go.mod h1:rt9v2x114jZmmLAE/h5N5jnxTmsK9ewwS2oQZ0UBQzM=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=\ngo.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=\ngo.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=\ngo.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=\ngo.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=\ngo.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=\ngolang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=\ngolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=\ngolang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=\ngolang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=\ngolang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=\ngolang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=\ngolang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\ngolang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=\nrsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "app/internal/forwarding/tcp.go",
    "content": "package forwarding\n\nimport (\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\ntype TCPTunnel struct {\n\tHyClient    client.Client\n\tRemote      string\n\tEventLogger TCPEventLogger\n}\n\ntype TCPEventLogger interface {\n\tConnect(addr net.Addr)\n\tError(addr net.Addr, err error)\n}\n\nfunc (t *TCPTunnel) Serve(listener net.Listener) error {\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo t.handle(conn)\n\t}\n}\n\nfunc (t *TCPTunnel) handle(conn net.Conn) {\n\tdefer conn.Close()\n\n\tif t.EventLogger != nil {\n\t\tt.EventLogger.Connect(conn.RemoteAddr())\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif t.EventLogger != nil {\n\t\t\tt.EventLogger.Error(conn.RemoteAddr(), closeErr)\n\t\t}\n\t}()\n\n\trc, err := t.HyClient.TCP(t.Remote)\n\tif err != nil {\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tdefer rc.Close()\n\n\t// Start forwarding\n\tcopyErrChan := make(chan error, 2)\n\tgo func() {\n\t\t_, copyErr := io.Copy(rc, conn)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tgo func() {\n\t\t_, copyErr := io.Copy(conn, rc)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tcloseErr = <-copyErrChan\n}\n"
  },
  {
    "path": "app/internal/forwarding/tcp_test.go",
    "content": "package forwarding\n\nimport (\n\t\"crypto/rand\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/utils_test\"\n)\n\nfunc TestTCPTunnel(t *testing.T) {\n\t// Start the tunnel\n\tl, err := net.Listen(\"tcp\", \"127.0.0.1:34567\")\n\tassert.NoError(t, err)\n\tdefer l.Close()\n\ttunnel := &TCPTunnel{\n\t\tHyClient: &utils_test.MockEchoHyClient{},\n\t}\n\tgo tunnel.Serve(l)\n\n\tfor i := 0; i < 10; i++ {\n\t\tconn, err := net.Dial(\"tcp\", \"127.0.0.1:34567\")\n\t\tassert.NoError(t, err)\n\n\t\tdata := make([]byte, 1024)\n\t\t_, _ = rand.Read(data)\n\t\t_, err = conn.Write(data)\n\t\tassert.NoError(t, err)\n\n\t\trecv := make([]byte, 1024)\n\t\t_, err = conn.Read(recv)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, data, recv)\n\t\t_ = conn.Close()\n\t}\n}\n"
  },
  {
    "path": "app/internal/forwarding/udp.go",
    "content": "package forwarding\n\nimport (\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst (\n\tudpBufferSize = 4096\n\n\tdefaultTimeout      = 60 * time.Second\n\tidleCleanupInterval = 1 * time.Second\n)\n\ntype atomicTime struct {\n\tv atomic.Value\n}\n\nfunc newAtomicTime(t time.Time) *atomicTime {\n\ta := &atomicTime{}\n\ta.Set(t)\n\treturn a\n}\n\nfunc (t *atomicTime) Set(new time.Time) {\n\tt.v.Store(new)\n}\n\nfunc (t *atomicTime) Get() time.Time {\n\treturn t.v.Load().(time.Time)\n}\n\ntype sessionEntry struct {\n\tHyConn  client.HyUDPConn\n\tLast    *atomicTime\n\tTimeout bool // true if the session is closed due to timeout\n}\n\nfunc (e *sessionEntry) Feed(data []byte, addr string) error {\n\te.Last.Set(time.Now())\n\treturn e.HyConn.Send(data, addr)\n}\n\nfunc (e *sessionEntry) ReceiveLoop(pc net.PacketConn, addr net.Addr) error {\n\tfor {\n\t\tdata, _, err := e.HyConn.Receive()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = pc.WriteTo(data, addr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\te.Last.Set(time.Now())\n\t}\n}\n\ntype UDPTunnel struct {\n\tHyClient    client.Client\n\tRemote      string\n\tTimeout     time.Duration\n\tEventLogger UDPEventLogger\n\n\tm     map[string]*sessionEntry // addr -> HyConn\n\tmutex sync.RWMutex\n}\n\ntype UDPEventLogger interface {\n\tConnect(addr net.Addr)\n\tError(addr net.Addr, err error)\n}\n\nfunc (t *UDPTunnel) Serve(pc net.PacketConn) error {\n\tt.m = make(map[string]*sessionEntry)\n\n\tstopCh := make(chan struct{})\n\tgo t.idleCleanupLoop(stopCh)\n\tdefer close(stopCh)\n\tdefer t.cleanup(false)\n\n\tbuf := make([]byte, udpBufferSize)\n\tfor {\n\t\tn, addr, err := pc.ReadFrom(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tt.feed(pc, addr, buf[:n])\n\t}\n}\n\nfunc (t *UDPTunnel) idleCleanupLoop(stopCh <-chan struct{}) {\n\tticker := time.NewTicker(idleCleanupInterval)\n\tdefer ticker.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tt.cleanup(true)\n\t\tcase <-stopCh:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (t *UDPTunnel) cleanup(idleOnly bool) {\n\t// We use RLock here as we are only scanning the map, not deleting from it.\n\tt.mutex.RLock()\n\tdefer t.mutex.RUnlock()\n\n\ttimeout := t.Timeout\n\tif timeout == 0 {\n\t\ttimeout = defaultTimeout\n\t}\n\n\tnow := time.Now()\n\tfor _, entry := range t.m {\n\t\tif !idleOnly || now.Sub(entry.Last.Get()) > timeout {\n\t\t\tentry.Timeout = true\n\t\t\t_ = entry.HyConn.Close()\n\t\t\t// Closing the connection here will cause the ReceiveLoop to exit,\n\t\t\t// and the session will be removed from the map there.\n\t\t}\n\t}\n}\n\nfunc (t *UDPTunnel) feed(pc net.PacketConn, addr net.Addr, data []byte) {\n\tt.mutex.RLock()\n\tentry := t.m[addr.String()]\n\tt.mutex.RUnlock()\n\n\t// Create a new session if not exists\n\tif entry == nil {\n\t\tif t.EventLogger != nil {\n\t\t\tt.EventLogger.Connect(addr)\n\t\t}\n\t\thyConn, err := t.HyClient.UDP()\n\t\tif err != nil {\n\t\t\tif t.EventLogger != nil {\n\t\t\t\tt.EventLogger.Error(addr, err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tentry = &sessionEntry{\n\t\t\tHyConn: hyConn,\n\t\t\tLast:   newAtomicTime(time.Now()),\n\t\t}\n\t\t// Start the receive loop for this session\n\t\t// Local <- Remote\n\t\tgo func() {\n\t\t\terr := entry.ReceiveLoop(pc, addr)\n\t\t\tif !entry.Timeout {\n\t\t\t\t_ = hyConn.Close()\n\t\t\t\tif t.EventLogger != nil {\n\t\t\t\t\tt.EventLogger.Error(addr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Connection already closed by timeout cleanup,\n\t\t\t\t// no need to close again here.\n\t\t\t\t// Use nil error to indicate timeout.\n\t\t\t\tif t.EventLogger != nil {\n\t\t\t\t\tt.EventLogger.Error(addr, nil)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Remove the session from the map\n\t\t\tt.mutex.Lock()\n\t\t\tdelete(t.m, addr.String())\n\t\t\tt.mutex.Unlock()\n\t\t}()\n\t\t// Insert the session into the map\n\t\tt.mutex.Lock()\n\t\tt.m[addr.String()] = entry\n\t\tt.mutex.Unlock()\n\t}\n\n\t// Feed the message to the session\n\t_ = entry.Feed(data, t.Remote)\n}\n"
  },
  {
    "path": "app/internal/forwarding/udp_test.go",
    "content": "package forwarding\n\nimport (\n\t\"crypto/rand\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/utils_test\"\n)\n\nfunc TestUDPTunnel(t *testing.T) {\n\t// Start the tunnel\n\tl, err := net.ListenPacket(\"udp\", \"127.0.0.1:34567\")\n\tassert.NoError(t, err)\n\tdefer l.Close()\n\ttunnel := &UDPTunnel{\n\t\tHyClient: &utils_test.MockEchoHyClient{},\n\t}\n\tgo tunnel.Serve(l)\n\n\tfor i := 0; i < 10; i++ {\n\t\tconn, err := net.Dial(\"udp\", \"127.0.0.1:34567\")\n\t\tassert.NoError(t, err)\n\n\t\tdata := make([]byte, 1024)\n\t\t_, _ = rand.Read(data)\n\t\t_, err = conn.Write(data)\n\t\tassert.NoError(t, err)\n\n\t\trecv := make([]byte, 1024)\n\t\t_, err = conn.Read(recv)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, data, recv)\n\t\t_ = conn.Close()\n\t}\n}\n"
  },
  {
    "path": "app/internal/http/server.go",
    "content": "package http\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst (\n\thttpClientTimeout = 10 * time.Second\n)\n\n// Server is an HTTP server using a Hysteria client as outbound.\ntype Server struct {\n\tHyClient    client.Client\n\tAuthFunc    func(username, password string) bool // nil = no authentication\n\tAuthRealm   string\n\tEventLogger EventLogger\n\n\thttpClient *http.Client\n}\n\ntype EventLogger interface {\n\tConnectRequest(addr net.Addr, reqAddr string)\n\tConnectError(addr net.Addr, reqAddr string, err error)\n\tHTTPRequest(addr net.Addr, reqURL string)\n\tHTTPError(addr net.Addr, reqURL string, err error)\n}\n\nfunc (s *Server) Serve(listener net.Listener) error {\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo s.dispatch(conn)\n\t}\n}\n\nfunc (s *Server) dispatch(conn net.Conn) {\n\tbufReader := bufio.NewReader(conn)\n\tfor {\n\t\treq, err := http.ReadRequest(bufReader)\n\t\tif err != nil {\n\t\t\t// Connection error or invalid request\n\t\t\t_ = conn.Close()\n\t\t\treturn\n\t\t}\n\t\tif s.AuthFunc != nil {\n\t\t\tauthOK := false\n\t\t\t// Check the Proxy-Authorization header\n\t\t\tpAuth := req.Header.Get(\"Proxy-Authorization\")\n\t\t\tif strings.HasPrefix(pAuth, \"Basic \") {\n\t\t\t\tuserPass, err := base64.URLEncoding.DecodeString(pAuth[6:])\n\t\t\t\tif err == nil {\n\t\t\t\t\tuserPassParts := strings.SplitN(string(userPass), \":\", 2)\n\t\t\t\t\tif len(userPassParts) == 2 {\n\t\t\t\t\t\tauthOK = s.AuthFunc(userPassParts[0], userPassParts[1])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !authOK {\n\t\t\t\t// Proxy authentication required\n\t\t\t\t_ = sendProxyAuthRequired(conn, req, s.AuthRealm)\n\t\t\t\t_ = conn.Close()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif req.Method == http.MethodConnect {\n\t\t\tif bufReader.Buffered() > 0 {\n\t\t\t\t// There is still data in the buffered reader.\n\t\t\t\t// We need to get it out and put it into a cachedConn,\n\t\t\t\t// so that handleConnect can read it.\n\t\t\t\tdata := make([]byte, bufReader.Buffered())\n\t\t\t\t_, err := io.ReadFull(bufReader, data)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// Read from buffer failed, is this possible?\n\t\t\t\t\t_ = conn.Close()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcachedConn := &cachedConn{\n\t\t\t\t\tConn:   conn,\n\t\t\t\t\tBuffer: *bytes.NewBuffer(data),\n\t\t\t\t}\n\t\t\t\ts.handleConnect(cachedConn, req)\n\t\t\t} else {\n\t\t\t\t// No data in the buffered reader, we can just pass the original connection.\n\t\t\t\ts.handleConnect(conn, req)\n\t\t\t}\n\t\t\t// handleConnect will take over the connection,\n\t\t\t// i.e. it will not return until the connection is closed.\n\t\t\t// When it returns, there will be no more requests from this connection,\n\t\t\t// so we simply exit the loop.\n\t\t\treturn\n\t\t} else {\n\t\t\t// handleRequest on the other hand handles one request at a time,\n\t\t\t// and returns when the request is done. It returns a bool indicating\n\t\t\t// whether the connection should be kept alive, but itself never closes\n\t\t\t// the connection.\n\t\t\tkeepAlive := s.handleRequest(conn, req)\n\t\t\tif !keepAlive {\n\t\t\t\t_ = conn.Close()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// cachedConn is a net.Conn wrapper that first Read()s from a buffer,\n// and then from the underlying net.Conn when the buffer is drained.\ntype cachedConn struct {\n\tnet.Conn\n\tBuffer bytes.Buffer\n}\n\nfunc (c *cachedConn) Read(b []byte) (int, error) {\n\tif c.Buffer.Len() > 0 {\n\t\tn, err := c.Buffer.Read(b)\n\t\tif err == io.EOF {\n\t\t\t// Buffer is drained, hide it from the caller\n\t\t\terr = nil\n\t\t}\n\t\treturn n, err\n\t}\n\treturn c.Conn.Read(b)\n}\n\nfunc (s *Server) handleConnect(conn net.Conn, req *http.Request) {\n\tdefer conn.Close()\n\n\tport := req.URL.Port()\n\tif port == \"\" {\n\t\t// HTTP defaults to port 80\n\t\tport = \"80\"\n\t}\n\treqAddr := net.JoinHostPort(req.URL.Hostname(), port)\n\n\t// Connect request & error log\n\tif s.EventLogger != nil {\n\t\ts.EventLogger.ConnectRequest(conn.RemoteAddr(), reqAddr)\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif s.EventLogger != nil {\n\t\t\ts.EventLogger.ConnectError(conn.RemoteAddr(), reqAddr, closeErr)\n\t\t}\n\t}()\n\n\t// Dial\n\trConn, err := s.HyClient.TCP(reqAddr)\n\tif err != nil {\n\t\t_ = sendSimpleResponse(conn, req, http.StatusBadGateway)\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tdefer rConn.Close()\n\n\t// Send 200 OK response and start relaying\n\t_ = sendSimpleResponse(conn, req, http.StatusOK)\n\tcopyErrChan := make(chan error, 2)\n\tgo func() {\n\t\t_, err := io.Copy(rConn, conn)\n\t\tcopyErrChan <- err\n\t}()\n\tgo func() {\n\t\t_, err := io.Copy(conn, rConn)\n\t\tcopyErrChan <- err\n\t}()\n\tcloseErr = <-copyErrChan\n}\n\nfunc (s *Server) handleRequest(conn net.Conn, req *http.Request) bool {\n\t// Some clients use Connection, some use Proxy-Connection\n\t// https://www.oreilly.com/library/view/http-the-definitive/1565925092/re40.html\n\tkeepAlive := req.ProtoAtLeast(1, 1) &&\n\t\t(strings.ToLower(req.Header.Get(\"Proxy-Connection\")) == \"keep-alive\" ||\n\t\t\tstrings.ToLower(req.Header.Get(\"Connection\")) == \"keep-alive\")\n\treq.RequestURI = \"\" // Outgoing request should not have RequestURI\n\n\tremoveHopByHopHeaders(req.Header)\n\tremoveExtraHTTPHostPort(req)\n\n\tif req.URL.Scheme == \"\" || req.URL.Host == \"\" {\n\t\t_ = sendSimpleResponse(conn, req, http.StatusBadRequest)\n\t\treturn false\n\t}\n\n\t// Request & error log\n\tif s.EventLogger != nil {\n\t\ts.EventLogger.HTTPRequest(conn.RemoteAddr(), req.URL.String())\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif s.EventLogger != nil {\n\t\t\ts.EventLogger.HTTPError(conn.RemoteAddr(), req.URL.String(), closeErr)\n\t\t}\n\t}()\n\n\tif s.httpClient == nil {\n\t\ts.initHTTPClient()\n\t}\n\n\t// Do the request and send the response back\n\tresp, err := s.httpClient.Do(req)\n\tif err != nil {\n\t\tcloseErr = err\n\t\t_ = sendSimpleResponse(conn, req, http.StatusBadGateway)\n\t\treturn false\n\t}\n\n\tremoveHopByHopHeaders(resp.Header)\n\tif keepAlive {\n\t\tresp.Header.Set(\"Connection\", \"keep-alive\")\n\t\tresp.Header.Set(\"Proxy-Connection\", \"keep-alive\")\n\t\tresp.Header.Set(\"Keep-Alive\", \"timeout=60\")\n\t}\n\n\tcloseErr = resp.Write(conn)\n\treturn closeErr == nil && keepAlive\n}\n\nfunc (s *Server) initHTTPClient() {\n\ts.httpClient = &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t\t// HyClient doesn't support context for now\n\t\t\t\treturn s.HyClient.TCP(addr)\n\t\t\t},\n\t\t},\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\treturn http.ErrUseLastResponse\n\t\t},\n\t\tTimeout: httpClientTimeout,\n\t}\n}\n\nfunc removeHopByHopHeaders(header http.Header) {\n\theader.Del(\"Proxy-Connection\") // Not in RFC but common\n\t// https://www.ietf.org/rfc/rfc2616.txt\n\theader.Del(\"Connection\")\n\theader.Del(\"Keep-Alive\")\n\theader.Del(\"Proxy-Authenticate\")\n\theader.Del(\"Proxy-Authorization\")\n\theader.Del(\"TE\")\n\theader.Del(\"Trailers\")\n\theader.Del(\"Transfer-Encoding\")\n\theader.Del(\"Upgrade\")\n}\n\nfunc removeExtraHTTPHostPort(req *http.Request) {\n\thost := req.Host\n\tif host == \"\" {\n\t\thost = req.URL.Host\n\t}\n\tif pHost, port, err := net.SplitHostPort(host); err == nil && port == \"80\" {\n\t\thost = pHost\n\t}\n\treq.Host = host\n\treq.URL.Host = host\n}\n\n// sendSimpleResponse sends a simple HTTP response with the given status code.\nfunc sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error {\n\tresp := &http.Response{\n\t\tStatusCode: statusCode,\n\t\tStatus:     http.StatusText(statusCode),\n\t\tProto:      req.Proto,\n\t\tProtoMajor: req.ProtoMajor,\n\t\tProtoMinor: req.ProtoMinor,\n\t\tHeader:     http.Header{},\n\t}\n\t// Remove the \"Content-Length: 0\" header, some clients (e.g. ffmpeg) may not like it.\n\tresp.ContentLength = -1\n\t// Also, prevent the \"Connection: close\" header.\n\tresp.Close = false\n\tresp.Uncompressed = true\n\treturn resp.Write(conn)\n}\n\n// sendProxyAuthRequired sends a 407 Proxy Authentication Required response.\nfunc sendProxyAuthRequired(conn net.Conn, req *http.Request, realm string) error {\n\tresp := &http.Response{\n\t\tStatusCode: http.StatusProxyAuthRequired,\n\t\tStatus:     http.StatusText(http.StatusProxyAuthRequired),\n\t\tProto:      req.Proto,\n\t\tProtoMajor: req.ProtoMajor,\n\t\tProtoMinor: req.ProtoMinor,\n\t\tHeader:     http.Header{},\n\t}\n\tresp.Header.Set(\"Proxy-Authenticate\", fmt.Sprintf(\"Basic realm=%q\", realm))\n\treturn resp.Write(conn)\n}\n"
  },
  {
    "path": "app/internal/http/server_test.go",
    "content": "package http\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst (\n\ttestCertFile = \"test.crt\"\n\ttestKeyFile  = \"test.key\"\n)\n\ntype mockHyClient struct{}\n\nfunc (c *mockHyClient) TCP(addr string) (net.Conn, error) {\n\treturn net.Dial(\"tcp\", addr)\n}\n\nfunc (c *mockHyClient) UDP() (client.HyUDPConn, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n\nfunc (c *mockHyClient) Close() error {\n\treturn nil\n}\n\nfunc TestServer(t *testing.T) {\n\t// Start the server\n\tl, err := net.Listen(\"tcp\", \"127.0.0.1:18080\")\n\tassert.NoError(t, err)\n\tdefer l.Close()\n\ts := &Server{\n\t\tHyClient: &mockHyClient{},\n\t}\n\tgo s.Serve(l)\n\n\t// Start a test HTTP & HTTPS server\n\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"control is an illusion\"))\n\t})\n\tgo http.ListenAndServe(\"127.0.0.1:18081\", nil)\n\tgo http.ListenAndServeTLS(\"127.0.0.1:18082\", testCertFile, testKeyFile, nil)\n\n\t// Run the Python test script\n\tcmd := exec.Command(\"python\", \"server_test.py\")\n\t// Suppress HTTPS warning text from Python\n\tcmd.Env = append(cmd.Env, \"PYTHONWARNINGS=ignore:Unverified HTTPS request\")\n\tout, err := cmd.CombinedOutput()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"OK\", strings.TrimSpace(string(out)))\n}\n"
  },
  {
    "path": "app/internal/http/server_test.py",
    "content": "import requests\n\nproxies = {\n    \"http\": \"http://127.0.0.1:18080\",\n    \"https\": \"http://127.0.0.1:18080\",\n}\n\n\ndef test_http(it):\n    for i in range(it):\n        r = requests.get(\"http://127.0.0.1:18081\", proxies=proxies)\n        assert r.status_code == 200 and r.text == \"control is an illusion\"\n\n\ndef test_https(it):\n    for i in range(it):\n        r = requests.get(\"https://127.0.0.1:18082\", proxies=proxies, verify=False)\n        assert r.status_code == 200 and r.text == \"control is an illusion\"\n\n\nif __name__ == \"__main__\":\n    test_http(10)\n    test_https(10)\n    print(\"OK\")\n"
  },
  {
    "path": "app/internal/http/test.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL\nBQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM\nEFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3\nDQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy\nMDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE\nCgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI\nhvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4\nZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W\nDKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW\nCbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf\njICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N\no0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v\nM29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD\nMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf\n1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc\nluDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4\nFjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD\nUMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD\nOLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7\nydYKuI8=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "app/internal/http/test.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR\n2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr\n6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc\nL9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2\nwiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C\nLolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI\nMdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+\nFLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U\n64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX\nerwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu\n1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW\nT3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA\ng4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA\no/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO\nUj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY\nZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V\nrFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k\nAnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI\nj8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0\njS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2\nZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g\nuhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+\nmenJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2\nkh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB\nT7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "app/internal/proxymux/.mockery.yaml",
    "content": "with-expecter: true\ndir: internal/mocks\noutpkg: mocks\npackages:\n  net:\n    interfaces:\n      Listener:\n        config:\n          mockname: MockListener\n      Conn:\n        config:\n          mockname: MockConn\n"
  },
  {
    "path": "app/internal/proxymux/internal/mocks/mock_Conn.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tnet \"net\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttime \"time\"\n)\n\n// MockConn is an autogenerated mock type for the Conn type\ntype MockConn struct {\n\tmock.Mock\n}\n\ntype MockConn_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockConn) EXPECT() *MockConn_Expecter {\n\treturn &MockConn_Expecter{mock: &_m.Mock}\n}\n\n// Close provides a mock function with given fields:\nfunc (_m *MockConn) Close() error {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Close\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype MockConn_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\nfunc (_e *MockConn_Expecter) Close() *MockConn_Close_Call {\n\treturn &MockConn_Close_Call{Call: _e.mock.On(\"Close\")}\n}\n\nfunc (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// LocalAddr provides a mock function with given fields:\nfunc (_m *MockConn) LocalAddr() net.Addr {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for LocalAddr\")\n\t}\n\n\tvar r0 net.Addr\n\tif rf, ok := ret.Get(0).(func() net.Addr); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Addr)\n\t\t}\n\t}\n\n\treturn r0\n}\n\n// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr'\ntype MockConn_LocalAddr_Call struct {\n\t*mock.Call\n}\n\n// LocalAddr is a helper method to define mock.On call\nfunc (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call {\n\treturn &MockConn_LocalAddr_Call{Call: _e.mock.On(\"LocalAddr\")}\n}\n\nfunc (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Read provides a mock function with given fields: b\nfunc (_m *MockConn) Read(b []byte) (int, error) {\n\tret := _m.Called(b)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Read\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {\n\t\treturn rf(b)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(b)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) error); ok {\n\t\tr1 = rf(b)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'\ntype MockConn_Read_Call struct {\n\t*mock.Call\n}\n\n// Read is a helper method to define mock.On call\n//   - b []byte\nfunc (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call {\n\treturn &MockConn_Read_Call{Call: _e.mock.On(\"Read\", b)}\n}\n\nfunc (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call {\n\t_c.Call.Return(n, err)\n\treturn _c\n}\n\nfunc (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// RemoteAddr provides a mock function with given fields:\nfunc (_m *MockConn) RemoteAddr() net.Addr {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for RemoteAddr\")\n\t}\n\n\tvar r0 net.Addr\n\tif rf, ok := ret.Get(0).(func() net.Addr); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Addr)\n\t\t}\n\t}\n\n\treturn r0\n}\n\n// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr'\ntype MockConn_RemoteAddr_Call struct {\n\t*mock.Call\n}\n\n// RemoteAddr is a helper method to define mock.On call\nfunc (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call {\n\treturn &MockConn_RemoteAddr_Call{Call: _e.mock.On(\"RemoteAddr\")}\n}\n\nfunc (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetDeadline provides a mock function with given fields: t\nfunc (_m *MockConn) SetDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'\ntype MockConn_SetDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call {\n\treturn &MockConn_SetDeadline_Call{Call: _e.mock.On(\"SetDeadline\", t)}\n}\n\nfunc (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetReadDeadline provides a mock function with given fields: t\nfunc (_m *MockConn) SetReadDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetReadDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'\ntype MockConn_SetReadDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetReadDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call {\n\treturn &MockConn_SetReadDeadline_Call{Call: _e.mock.On(\"SetReadDeadline\", t)}\n}\n\nfunc (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetWriteDeadline provides a mock function with given fields: t\nfunc (_m *MockConn) SetWriteDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetWriteDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'\ntype MockConn_SetWriteDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetWriteDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call {\n\treturn &MockConn_SetWriteDeadline_Call{Call: _e.mock.On(\"SetWriteDeadline\", t)}\n}\n\nfunc (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Write provides a mock function with given fields: b\nfunc (_m *MockConn) Write(b []byte) (int, error) {\n\tret := _m.Called(b)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Write\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {\n\t\treturn rf(b)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(b)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) error); ok {\n\t\tr1 = rf(b)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'\ntype MockConn_Write_Call struct {\n\t*mock.Call\n}\n\n// Write is a helper method to define mock.On call\n//   - b []byte\nfunc (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call {\n\treturn &MockConn_Write_Call{Call: _e.mock.On(\"Write\", b)}\n}\n\nfunc (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call {\n\t_c.Call.Return(n, err)\n\treturn _c\n}\n\nfunc (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockConn(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockConn {\n\tmock := &MockConn{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "app/internal/proxymux/internal/mocks/mock_Listener.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tnet \"net\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// MockListener is an autogenerated mock type for the Listener type\ntype MockListener struct {\n\tmock.Mock\n}\n\ntype MockListener_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockListener) EXPECT() *MockListener_Expecter {\n\treturn &MockListener_Expecter{mock: &_m.Mock}\n}\n\n// Accept provides a mock function with given fields:\nfunc (_m *MockListener) Accept() (net.Conn, error) {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Accept\")\n\t}\n\n\tvar r0 net.Conn\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func() (net.Conn, error)); ok {\n\t\treturn rf()\n\t}\n\tif rf, ok := ret.Get(0).(func() net.Conn); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Conn)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = rf()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'\ntype MockListener_Accept_Call struct {\n\t*mock.Call\n}\n\n// Accept is a helper method to define mock.On call\nfunc (_e *MockListener_Expecter) Accept() *MockListener_Accept_Call {\n\treturn &MockListener_Accept_Call{Call: _e.mock.On(\"Accept\")}\n}\n\nfunc (_c *MockListener_Accept_Call) Run(run func()) *MockListener_Accept_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockListener_Accept_Call) Return(_a0 net.Conn, _a1 error) *MockListener_Accept_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *MockListener_Accept_Call) RunAndReturn(run func() (net.Conn, error)) *MockListener_Accept_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Addr provides a mock function with given fields:\nfunc (_m *MockListener) Addr() net.Addr {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Addr\")\n\t}\n\n\tvar r0 net.Addr\n\tif rf, ok := ret.Get(0).(func() net.Addr); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Addr)\n\t\t}\n\t}\n\n\treturn r0\n}\n\n// MockListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'\ntype MockListener_Addr_Call struct {\n\t*mock.Call\n}\n\n// Addr is a helper method to define mock.On call\nfunc (_e *MockListener_Expecter) Addr() *MockListener_Addr_Call {\n\treturn &MockListener_Addr_Call{Call: _e.mock.On(\"Addr\")}\n}\n\nfunc (_c *MockListener_Addr_Call) Run(run func()) *MockListener_Addr_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockListener_Addr_Call) Return(_a0 net.Addr) *MockListener_Addr_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockListener_Addr_Call) RunAndReturn(run func() net.Addr) *MockListener_Addr_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Close provides a mock function with given fields:\nfunc (_m *MockListener) Close() error {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Close\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype MockListener_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\nfunc (_e *MockListener_Expecter) Close() *MockListener_Close_Call {\n\treturn &MockListener_Close_Call{Call: _e.mock.On(\"Close\")}\n}\n\nfunc (_c *MockListener_Close_Call) Run(run func()) *MockListener_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockListener_Close_Call) Return(_a0 error) *MockListener_Close_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockListener_Close_Call) RunAndReturn(run func() error) *MockListener_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockListener(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockListener {\n\tmock := &MockListener{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "app/internal/proxymux/manager.go",
    "content": "package proxymux\n\nimport (\n\t\"net\"\n\t\"sync\"\n\n\t\"github.com/apernet/hysteria/extras/v2/correctnet\"\n)\n\ntype muxManager struct {\n\tlisteners map[string]*muxListener\n\tlock      sync.Mutex\n}\n\nvar globalMuxManager *muxManager\n\nfunc init() {\n\tglobalMuxManager = &muxManager{\n\t\tlisteners: make(map[string]*muxListener),\n\t}\n}\n\nfunc (m *muxManager) GetOrCreate(address string) (*muxListener, error) {\n\tkey, err := m.canonicalizeAddrPort(address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\tif ml, ok := m.listeners[key]; ok {\n\t\treturn ml, nil\n\t}\n\n\tlistener, err := correctnet.Listen(\"tcp\", key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tml := newMuxListener(listener, func() {\n\t\tm.lock.Lock()\n\t\tdefer m.lock.Unlock()\n\t\tdelete(m.listeners, key)\n\t})\n\tm.listeners[key] = ml\n\treturn ml, nil\n}\n\nfunc (m *muxManager) canonicalizeAddrPort(address string) (string, error) {\n\ttaddr, err := net.ResolveTCPAddr(\"tcp\", address)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn taddr.String(), nil\n}\n\nfunc ListenHTTP(address string) (net.Listener, error) {\n\tml, err := globalMuxManager.GetOrCreate(address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ml.ListenHTTP()\n}\n\nfunc ListenSOCKS(address string) (net.Listener, error) {\n\tml, err := globalMuxManager.GetOrCreate(address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ml.ListenSOCKS()\n}\n"
  },
  {
    "path": "app/internal/proxymux/manager_test.go",
    "content": "package proxymux\n\nimport (\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestListenSOCKS(t *testing.T) {\n\taddress := \"127.2.39.129:11081\"\n\n\tsl, err := ListenSOCKS(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tdefer func() {\n\t\tsl.Close()\n\t}()\n\n\thl, err := ListenHTTP(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tdefer hl.Close()\n\n\t_, err = ListenSOCKS(address)\n\tif !assert.ErrorIs(t, err, ErrProtocolInUse) {\n\t\treturn\n\t}\n\tsl.Close()\n\n\tsl, err = ListenSOCKS(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n}\n\nfunc TestListenHTTP(t *testing.T) {\n\taddress := \"127.2.39.129:11082\"\n\n\thl, err := ListenHTTP(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tdefer func() {\n\t\thl.Close()\n\t}()\n\n\tsl, err := ListenSOCKS(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tdefer sl.Close()\n\n\t_, err = ListenHTTP(address)\n\tif !assert.ErrorIs(t, err, ErrProtocolInUse) {\n\t\treturn\n\t}\n\thl.Close()\n\n\thl, err = ListenHTTP(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n}\n\nfunc TestRelease(t *testing.T) {\n\taddress := \"127.2.39.129:11083\"\n\n\thl, err := ListenHTTP(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tsl, err := ListenSOCKS(address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\n\tif !assert.True(t, globalMuxManager.testAddressExists(address)) {\n\t\treturn\n\t}\n\t_, err = net.Listen(\"tcp\", address)\n\tif !assert.Error(t, err) {\n\t\treturn\n\t}\n\n\thl.Close()\n\tsl.Close()\n\n\t// Wait for muxListener released\n\ttime.Sleep(time.Second)\n\tif !assert.False(t, globalMuxManager.testAddressExists(address)) {\n\t\treturn\n\t}\n\tlis, err := net.Listen(\"tcp\", address)\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tdefer lis.Close()\n}\n\nfunc (m *muxManager) testAddressExists(address string) bool {\n\tm.lock.Lock()\n\tdefer m.lock.Unlock()\n\n\t_, ok := m.listeners[address]\n\treturn ok\n}\n"
  },
  {
    "path": "app/internal/proxymux/mux.go",
    "content": "package proxymux\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n)\n\nfunc newMuxListener(listener net.Listener, deleteFunc func()) *muxListener {\n\tl := &muxListener{\n\t\tbase:       listener,\n\t\tacceptChan: make(chan net.Conn),\n\t\tcloseChan:  make(chan struct{}),\n\t\tdeleteFunc: deleteFunc,\n\t}\n\tgo l.acceptLoop()\n\tgo l.mainLoop()\n\treturn l\n}\n\ntype muxListener struct {\n\tlock      sync.Mutex\n\tbase      net.Listener\n\tacceptErr error\n\n\tacceptChan chan net.Conn\n\tcloseChan  chan struct{}\n\n\tsocksListener *subListener\n\thttpListener  *subListener\n\n\tdeleteFunc func()\n}\n\nfunc (l *muxListener) acceptLoop() {\n\tdefer close(l.acceptChan)\n\n\tfor {\n\t\tconn, err := l.base.Accept()\n\t\tif err != nil {\n\t\t\tl.lock.Lock()\n\t\t\tl.acceptErr = err\n\t\t\tl.lock.Unlock()\n\t\t\treturn\n\t\t}\n\t\tselect {\n\t\tcase <-l.closeChan:\n\t\t\treturn\n\t\tcase l.acceptChan <- conn:\n\t\t}\n\t}\n}\n\nfunc (l *muxListener) mainLoop() {\n\tdefer func() {\n\t\tl.deleteFunc()\n\t\tl.base.Close()\n\n\t\tclose(l.closeChan)\n\n\t\tl.lock.Lock()\n\t\tdefer l.lock.Unlock()\n\n\t\tif sl := l.httpListener; sl != nil {\n\t\t\tclose(sl.acceptChan)\n\t\t\tl.httpListener = nil\n\t\t}\n\t\tif sl := l.socksListener; sl != nil {\n\t\t\tclose(sl.acceptChan)\n\t\t\tl.socksListener = nil\n\t\t}\n\t}()\n\n\tfor {\n\t\tvar socksCloseChan, httpCloseChan chan struct{}\n\t\tif l.httpListener != nil {\n\t\t\thttpCloseChan = l.httpListener.closeChan\n\t\t}\n\t\tif l.socksListener != nil {\n\t\t\tsocksCloseChan = l.socksListener.closeChan\n\t\t}\n\t\tselect {\n\t\tcase <-l.closeChan:\n\t\t\treturn\n\t\tcase conn, ok := <-l.acceptChan:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgo l.dispatch(conn)\n\t\tcase <-socksCloseChan:\n\t\t\tl.lock.Lock()\n\t\t\tif socksCloseChan == l.socksListener.closeChan {\n\t\t\t\t// not replaced by another ListenSOCKS()\n\t\t\t\tl.socksListener = nil\n\t\t\t}\n\t\t\tl.lock.Unlock()\n\t\t\tif l.checkIdle() {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-httpCloseChan:\n\t\t\tl.lock.Lock()\n\t\t\tif httpCloseChan == l.httpListener.closeChan {\n\t\t\t\t// not replaced by another ListenHTTP()\n\t\t\t\tl.httpListener = nil\n\t\t\t}\n\t\t\tl.lock.Unlock()\n\t\t\tif l.checkIdle() {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (l *muxListener) dispatch(conn net.Conn) {\n\tvar b [1]byte\n\tif _, err := io.ReadFull(conn, b[:]); err != nil {\n\t\tconn.Close()\n\t\treturn\n\t}\n\n\tl.lock.Lock()\n\tvar target *subListener\n\tif b[0] == 5 {\n\t\ttarget = l.socksListener\n\t} else {\n\t\ttarget = l.httpListener\n\t}\n\tl.lock.Unlock()\n\n\tif target == nil {\n\t\tconn.Close()\n\t\treturn\n\t}\n\n\twconn := &connWithOneByte{Conn: conn, b: b[0]}\n\n\tselect {\n\tcase <-target.closeChan:\n\tcase target.acceptChan <- wconn:\n\t}\n}\n\nfunc (l *muxListener) checkIdle() bool {\n\tl.lock.Lock()\n\tdefer l.lock.Unlock()\n\n\treturn l.httpListener == nil && l.socksListener == nil\n}\n\nfunc (l *muxListener) getAndClearAcceptError() error {\n\tl.lock.Lock()\n\tdefer l.lock.Unlock()\n\n\tif l.acceptErr == nil {\n\t\treturn nil\n\t}\n\terr := l.acceptErr\n\tl.acceptErr = nil\n\treturn err\n}\n\nfunc (l *muxListener) ListenHTTP() (net.Listener, error) {\n\tl.lock.Lock()\n\tdefer l.lock.Unlock()\n\n\tif l.httpListener != nil {\n\t\tsubListenerPendingClosed := false\n\t\tselect {\n\t\tcase <-l.httpListener.closeChan:\n\t\t\tsubListenerPendingClosed = true\n\t\tdefault:\n\t\t}\n\t\tif !subListenerPendingClosed {\n\t\t\treturn nil, OpErr{\n\t\t\t\tAddr:     l.base.Addr(),\n\t\t\t\tProtocol: \"http\",\n\t\t\t\tOp:       \"bind-protocol\",\n\t\t\t\tErr:      ErrProtocolInUse,\n\t\t\t}\n\t\t}\n\t\tl.httpListener = nil\n\t}\n\n\tselect {\n\tcase <-l.closeChan:\n\t\treturn nil, net.ErrClosed\n\tdefault:\n\t}\n\n\tsl := newSubListener(l.getAndClearAcceptError, l.base.Addr)\n\tl.httpListener = sl\n\treturn sl, nil\n}\n\nfunc (l *muxListener) ListenSOCKS() (net.Listener, error) {\n\tl.lock.Lock()\n\tdefer l.lock.Unlock()\n\n\tif l.socksListener != nil {\n\t\tsubListenerPendingClosed := false\n\t\tselect {\n\t\tcase <-l.socksListener.closeChan:\n\t\t\tsubListenerPendingClosed = true\n\t\tdefault:\n\t\t}\n\t\tif !subListenerPendingClosed {\n\t\t\treturn nil, OpErr{\n\t\t\t\tAddr:     l.base.Addr(),\n\t\t\t\tProtocol: \"socks\",\n\t\t\t\tOp:       \"bind-protocol\",\n\t\t\t\tErr:      ErrProtocolInUse,\n\t\t\t}\n\t\t}\n\t\tl.socksListener = nil\n\t}\n\n\tselect {\n\tcase <-l.closeChan:\n\t\treturn nil, net.ErrClosed\n\tdefault:\n\t}\n\n\tsl := newSubListener(l.getAndClearAcceptError, l.base.Addr)\n\tl.socksListener = sl\n\treturn sl, nil\n}\n\nfunc newSubListener(acceptErrorFunc func() error, addrFunc func() net.Addr) *subListener {\n\treturn &subListener{\n\t\tacceptChan:      make(chan net.Conn),\n\t\tacceptErrorFunc: acceptErrorFunc,\n\t\tcloseChan:       make(chan struct{}),\n\t\taddrFunc:        addrFunc,\n\t}\n}\n\ntype subListener struct {\n\t// receive connections or closure from upstream\n\tacceptChan chan net.Conn\n\t// get an error of Accept() from upstream\n\tacceptErrorFunc func() error\n\t// notify upstream that we are closed\n\tcloseChan chan struct{}\n\n\t// Listener.Addr() implementation of base listener\n\taddrFunc func() net.Addr\n}\n\nfunc (l *subListener) Accept() (net.Conn, error) {\n\tselect {\n\tcase <-l.closeChan:\n\t\t// closed by ourselves\n\t\treturn nil, net.ErrClosed\n\tcase conn, ok := <-l.acceptChan:\n\t\tif !ok {\n\t\t\t// closed by upstream\n\t\t\tif acceptErr := l.acceptErrorFunc(); acceptErr != nil {\n\t\t\t\treturn nil, acceptErr\n\t\t\t}\n\t\t\treturn nil, net.ErrClosed\n\t\t}\n\t\treturn conn, nil\n\t}\n}\n\nfunc (l *subListener) Addr() net.Addr {\n\treturn l.addrFunc()\n}\n\n// Close implements net.Listener.Close.\n// Upstream should use close(l.acceptChan) instead.\nfunc (l *subListener) Close() error {\n\tselect {\n\tcase <-l.closeChan:\n\t\treturn nil\n\tdefault:\n\t}\n\tclose(l.closeChan)\n\treturn nil\n}\n\n// connWithOneByte is a net.Conn that returns b for the first read\n// request, then forwards everything else to Conn.\ntype connWithOneByte struct {\n\tnet.Conn\n\n\tb     byte\n\tbRead bool\n}\n\nfunc (c *connWithOneByte) Read(bs []byte) (int, error) {\n\tif c.bRead {\n\t\treturn c.Conn.Read(bs)\n\t}\n\tif len(bs) == 0 {\n\t\treturn 0, nil\n\t}\n\tc.bRead = true\n\tbs[0] = c.b\n\treturn 1, nil\n}\n\ntype OpErr struct {\n\tAddr     net.Addr\n\tProtocol string\n\tOp       string\n\tErr      error\n}\n\nfunc (m OpErr) Error() string {\n\treturn fmt.Sprintf(\"mux-listen: %s[%s]: %s: %v\", m.Addr, m.Protocol, m.Op, m.Err)\n}\n\nfunc (m OpErr) Unwrap() error {\n\treturn m.Err\n}\n\nvar ErrProtocolInUse = errors.New(\"protocol already in use\")\n"
  },
  {
    "path": "app/internal/proxymux/mux_test.go",
    "content": "package proxymux\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/proxymux/internal/mocks\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\n//go:generate mockery\n\nfunc testMockListener(t *testing.T, connChan <-chan net.Conn) net.Listener {\n\tclosedChan := make(chan struct{})\n\tmockListener := mocks.NewMockListener(t)\n\tmockListener.EXPECT().Accept().RunAndReturn(func() (net.Conn, error) {\n\t\tselect {\n\t\tcase <-closedChan:\n\t\t\treturn nil, net.ErrClosed\n\t\tcase conn, ok := <-connChan:\n\t\t\tif !ok {\n\t\t\t\tpanic(\"unexpected closed channel (connChan)\")\n\t\t\t}\n\t\t\treturn conn, nil\n\t\t}\n\t})\n\tmockListener.EXPECT().Close().RunAndReturn(func() error {\n\t\tselect {\n\t\tcase <-closedChan:\n\t\tdefault:\n\t\t\tclose(closedChan)\n\t\t}\n\t\treturn nil\n\t})\n\treturn mockListener\n}\n\nfunc testMockConn(t *testing.T, b []byte) net.Conn {\n\tbuf := bytes.NewReader(b)\n\tisClosed := false\n\tmockConn := mocks.NewMockConn(t)\n\tmockConn.EXPECT().Read(mock.Anything).RunAndReturn(func(b []byte) (int, error) {\n\t\tif isClosed {\n\t\t\treturn 0, net.ErrClosed\n\t\t}\n\t\treturn buf.Read(b)\n\t})\n\tmockConn.EXPECT().Close().RunAndReturn(func() error {\n\t\tisClosed = true\n\t\treturn nil\n\t})\n\treturn mockConn\n}\n\nfunc TestMuxHTTP(t *testing.T) {\n\tconnChan := make(chan net.Conn)\n\tmockListener := testMockListener(t, connChan)\n\tmockConn := testMockConn(t, []byte(\"CONNECT example.com:443 HTTP/1.1\\r\\n\\r\\n\"))\n\n\tmux := newMuxListener(mockListener, func() {})\n\thl, err := mux.ListenHTTP()\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tsl, err := mux.ListenSOCKS()\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\n\tconnChan <- mockConn\n\n\tvar socksConn, httpConn net.Conn\n\tvar socksErr, httpErr error\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tsocksConn, socksErr = sl.Accept()\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\thttpConn, httpErr = hl.Accept()\n\t\twg.Done()\n\t}()\n\n\ttime.Sleep(time.Second)\n\n\tsl.Close()\n\thl.Close()\n\n\twg.Wait()\n\n\tassert.Nil(t, socksConn)\n\tassert.ErrorIs(t, socksErr, net.ErrClosed)\n\tassert.NotNil(t, httpConn)\n\thttpConn.Close()\n\tassert.NoError(t, httpErr)\n\n\t// Wait for muxListener released\n\t<-mux.acceptChan\n}\n\nfunc TestMuxSOCKS(t *testing.T) {\n\tconnChan := make(chan net.Conn)\n\tmockListener := testMockListener(t, connChan)\n\tmockConn := testMockConn(t, []byte{0x05, 0x02, 0x00, 0x01}) // SOCKS5 Connect Request: NOAUTH+GSSAPI\n\n\tmux := newMuxListener(mockListener, func() {})\n\thl, err := mux.ListenHTTP()\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tsl, err := mux.ListenSOCKS()\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\n\tconnChan <- mockConn\n\n\tvar socksConn, httpConn net.Conn\n\tvar socksErr, httpErr error\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tsocksConn, socksErr = sl.Accept()\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\thttpConn, httpErr = hl.Accept()\n\t\twg.Done()\n\t}()\n\n\ttime.Sleep(time.Second)\n\n\tsl.Close()\n\thl.Close()\n\n\twg.Wait()\n\n\tassert.NotNil(t, socksConn)\n\tsocksConn.Close()\n\tassert.NoError(t, socksErr)\n\tassert.Nil(t, httpConn)\n\tassert.ErrorIs(t, httpErr, net.ErrClosed)\n\n\t// Wait for muxListener released\n\t<-mux.acceptChan\n}\n"
  },
  {
    "path": "app/internal/redirect/getsockopt_linux.go",
    "content": "//go:build !386\n// +build !386\n\npackage redirect\n\nimport (\n\t\"syscall\"\n\t\"unsafe\"\n)\n\nfunc getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) {\n\t_, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)\n\tif e != 0 {\n\t\terr = e\n\t}\n\treturn\n}\n"
  },
  {
    "path": "app/internal/redirect/getsockopt_linux_386.go",
    "content": "package redirect\n\nimport (\n\t\"syscall\"\n\t\"unsafe\"\n)\n\nconst (\n\tsysGetsockopt = 15\n)\n\n// On 386 we cannot call socketcall with syscall.Syscall6, as it always fails with EFAULT.\n// Use our own syscall.socketcall hack instead.\n\nfunc syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno)\n\nfunc getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) {\n\t_, e := syscall_socketcall(sysGetsockopt, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)\n\tif e != 0 {\n\t\terr = e\n\t}\n\treturn\n}\n"
  },
  {
    "path": "app/internal/redirect/syscall_socketcall_linux_386.s",
    "content": "//go:build gc\n// +build gc\n\n#include \"textflag.h\"\n\nTEXT ·syscall_socketcall(SB),NOSPLIT,$0-36\n\tJMP\tsyscall·socketcall(SB)\n"
  },
  {
    "path": "app/internal/redirect/tcp_linux.go",
    "content": "package redirect\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"syscall\"\n\t\"unsafe\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst (\n\tsoOriginalDst   = 80\n\tsoOriginalDstV6 = 80\n)\n\ntype TCPRedirect struct {\n\tHyClient    client.Client\n\tEventLogger TCPEventLogger\n}\n\ntype TCPEventLogger interface {\n\tConnect(addr, reqAddr net.Addr)\n\tError(addr, reqAddr net.Addr, err error)\n}\n\nfunc (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error {\n\tlistener, err := net.ListenTCP(\"tcp\", laddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer listener.Close()\n\tfor {\n\t\tc, err := listener.AcceptTCP()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo r.handle(c)\n\t}\n}\n\nfunc (r *TCPRedirect) handle(conn *net.TCPConn) {\n\tdefer conn.Close()\n\tdstAddr, err := getDstAddr(conn)\n\tif err != nil {\n\t\t// Fail silently if we can't get the original destination.\n\t\t// Maybe we should print something to the log?\n\t\treturn\n\t}\n\tif r.EventLogger != nil {\n\t\tr.EventLogger.Connect(conn.RemoteAddr(), dstAddr)\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif r.EventLogger != nil {\n\t\t\tr.EventLogger.Error(conn.RemoteAddr(), dstAddr, closeErr)\n\t\t}\n\t}()\n\n\trc, err := r.HyClient.TCP(dstAddr.String())\n\tif err != nil {\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tdefer rc.Close()\n\n\t// Start forwarding\n\tcopyErrChan := make(chan error, 2)\n\tgo func() {\n\t\t_, copyErr := io.Copy(rc, conn)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tgo func() {\n\t\t_, copyErr := io.Copy(conn, rc)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tcloseErr = <-copyErrChan\n}\n\ntype sockAddr struct {\n\tfamily uint16\n\tport   [2]byte  // always big endian regardless of platform\n\tdata   [24]byte // sockaddr_in or sockaddr_in6\n}\n\nfunc getOriginalDst(fd uintptr) (*sockAddr, error) {\n\tvar addr sockAddr\n\taddrSize := uint32(unsafe.Sizeof(addr))\n\t// Try IPv6 first\n\terr := getsockopt(fd, syscall.SOL_IPV6, soOriginalDstV6, unsafe.Pointer(&addr), &addrSize)\n\tif err == nil {\n\t\treturn &addr, nil\n\t}\n\t// Then IPv4\n\terr = getsockopt(fd, syscall.SOL_IP, soOriginalDst, unsafe.Pointer(&addr), &addrSize)\n\treturn &addr, err\n}\n\n// getDstAddr returns the original destination of a redirected TCP connection.\nfunc getDstAddr(conn *net.TCPConn) (*net.TCPAddr, error) {\n\trc, err := conn.SyscallConn()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar addr *sockAddr\n\tvar err2 error\n\terr = rc.Control(func(fd uintptr) {\n\t\taddr, err2 = getOriginalDst(fd)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err2 != nil {\n\t\treturn nil, err2\n\t}\n\tswitch addr.family {\n\tcase syscall.AF_INET:\n\t\treturn &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil\n\tcase syscall.AF_INET6:\n\t\treturn &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil\n\tdefault:\n\t\treturn nil, errors.New(\"address family not IPv4 or IPv6\")\n\t}\n}\n"
  },
  {
    "path": "app/internal/redirect/tcp_others.go",
    "content": "//go:build !linux\n\npackage redirect\n\nimport (\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\ntype TCPRedirect struct {\n\tHyClient    client.Client\n\tEventLogger TCPEventLogger\n}\n\ntype TCPEventLogger interface {\n\tConnect(addr, reqAddr net.Addr)\n\tError(addr, reqAddr net.Addr, err error)\n}\n\nfunc (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error {\n\treturn errors.New(\"not supported on this platform\")\n}\n"
  },
  {
    "path": "app/internal/sockopts/fd_control_unix_socket_test.py",
    "content": "import socket\nimport array\nimport os\nimport struct\nimport sys\n\n\ndef serve(path):\n    try:\n        os.unlink(path)\n    except OSError:\n        if os.path.exists(path):\n            raise\n\n    server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n    server.bind(path)\n    server.listen()\n    print(f\"Listening on {path}\")\n\n    try:\n        while True:\n            connection, client_address = server.accept()\n            print(f\"Client connected\")\n\n            try:\n                # Receiving fd from client\n                fds = array.array(\"i\")\n                msg, ancdata, flags, addr = connection.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i')))\n                for cmsg_level, cmsg_type, cmsg_data in ancdata:\n                    if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:\n                        fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])\n\n                fd = fds[0]\n\n                # We make a call to setsockopt(2) here, so client can verify we have received the fd\n                # In the real scenario, the server would set things like SO_MARK,\n                # we use SO_RCVBUF as it doesn't require any special capabilities.\n                nbytes = struct.pack(\"i\", 2500)\n                fdsocket = fd_to_socket(fd)\n                fdsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, nbytes)\n                fdsocket.close()\n\n                # The only protocol-like thing specified in the client implementation.\n                connection.send(b'\\x01')\n            finally:\n                connection.close()\n                print(\"Connection closed\")\n\n    except KeyboardInterrupt:\n        print(\"Exit\")\n\n    finally:\n        server.close()\n        os.unlink(path)\n\n\ndef fd_to_socket(fd):\n    return socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) < 2:\n        raise ValueError(\"unix socket path is required\")\n\n    serve(sys.argv[1])\n"
  },
  {
    "path": "app/internal/sockopts/sockopts.go",
    "content": "package sockopts\n\nimport (\n\t\"fmt\"\n\t\"net\"\n)\n\ntype SocketOptions struct {\n\tBindInterface       *string\n\tFirewallMark        *uint32\n\tFdControlUnixSocket *string\n}\n\n// implemented in platform-specific files\nvar (\n\tbindInterfaceFunc       func(c *net.UDPConn, device string) error\n\tfirewallMarkFunc        func(c *net.UDPConn, fwmark uint32) error\n\tfdControlUnixSocketFunc func(c *net.UDPConn, path string) error\n)\n\nfunc (o *SocketOptions) CheckSupported() (err error) {\n\tif o.BindInterface != nil && bindInterfaceFunc == nil {\n\t\treturn &UnsupportedError{\"bindInterface\"}\n\t}\n\tif o.FirewallMark != nil && firewallMarkFunc == nil {\n\t\treturn &UnsupportedError{\"fwmark\"}\n\t}\n\tif o.FdControlUnixSocket != nil && fdControlUnixSocketFunc == nil {\n\t\treturn &UnsupportedError{\"fdControlUnixSocket\"}\n\t}\n\treturn nil\n}\n\ntype UnsupportedError struct {\n\tField string\n}\n\nfunc (e *UnsupportedError) Error() string {\n\treturn fmt.Sprintf(\"%s is not supported on this platform\", e.Field)\n}\n\nfunc (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) {\n\tuconn, err = net.ListenUDP(\"udp\", nil)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = o.applyToUDPConn(uconn.(*net.UDPConn))\n\tif err != nil {\n\t\tuconn.Close()\n\t\tuconn = nil\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error {\n\tif o.BindInterface != nil && bindInterfaceFunc != nil {\n\t\terr := bindInterfaceFunc(c, *o.BindInterface)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to bind to interface: %w\", err)\n\t\t}\n\t}\n\tif o.FirewallMark != nil && firewallMarkFunc != nil {\n\t\terr := firewallMarkFunc(c, *o.FirewallMark)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set fwmark: %w\", err)\n\t\t}\n\t}\n\tif o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil {\n\t\terr := fdControlUnixSocketFunc(c, *o.FdControlUnixSocket)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to send fd to control unix socket: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "app/internal/sockopts/sockopts_linux.go",
    "content": "//go:build linux\n\npackage sockopts\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\t\"golang.org/x/exp/constraints\"\n\t\"golang.org/x/sys/unix\"\n)\n\nconst (\n\tfdControlUnixTimeout = 3 * time.Second\n)\n\nfunc init() {\n\tbindInterfaceFunc = bindInterfaceImpl\n\tfirewallMarkFunc = firewallMarkImpl\n\tfdControlUnixSocketFunc = fdControlUnixSocketImpl\n}\n\nfunc controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) {\n\trconn, err := c.SyscallConn()\n\tif err != nil {\n\t\treturn\n\t}\n\tcerr := rconn.Control(func(fd uintptr) {\n\t\terr = cb(int(fd))\n\t})\n\tif err != nil {\n\t\treturn\n\t}\n\tif cerr != nil {\n\t\terr = fmt.Errorf(\"failed to control fd: %w\", cerr)\n\t\treturn\n\t}\n\treturn\n}\n\nfunc bindInterfaceImpl(c *net.UDPConn, device string) error {\n\treturn controlUDPConn(c, func(fd int) error {\n\t\treturn unix.BindToDevice(fd, device)\n\t})\n}\n\nfunc firewallMarkImpl(c *net.UDPConn, fwmark uint32) error {\n\treturn controlUDPConn(c, func(fd int) error {\n\t\treturn unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, int(fwmark))\n\t})\n}\n\nfunc fdControlUnixSocketImpl(c *net.UDPConn, path string) error {\n\treturn controlUDPConn(c, func(fd int) error {\n\t\tsocketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create unix socket: %w\", err)\n\t\t}\n\t\tdefer unix.Close(socketFd)\n\n\t\tvar timeout unix.Timeval\n\t\ttimeUsec := fdControlUnixTimeout.Microseconds()\n\t\tcastAssignInteger(timeUsec/1e6, &timeout.Sec)\n\t\t// Specifying the type explicitly is not necessary here, but it makes GoLand happy.\n\t\tcastAssignInteger[int64](timeUsec%1e6, &timeout.Usec)\n\n\t\t_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout)\n\t\t_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout)\n\n\t\terr = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to connect: %w\", err)\n\t\t}\n\n\t\terr = unix.Sendmsg(socketFd, nil, unix.UnixRights(fd), nil, 0)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to send: %w\", err)\n\t\t}\n\n\t\tdummy := []byte{1}\n\t\tn, err := unix.Read(socketFd, dummy)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to receive: %w\", err)\n\t\t}\n\t\tif n != 1 {\n\t\t\treturn fmt.Errorf(\"socket closed unexpectedly\")\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc castAssignInteger[F, T constraints.Integer](from F, to *T) {\n\t*to = T(from)\n}\n"
  },
  {
    "path": "app/internal/sockopts/sockopts_linux_test.go",
    "content": "//go:build linux\n\npackage sockopts\n\nimport (\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc Test_fdControlUnixSocketImpl(t *testing.T) {\n\tsockPath := \"./fd_control_unix_socket_test.sock\"\n\tdefer os.Remove(sockPath)\n\n\t// Run test server\n\tcmd := exec.Command(\"python\", \"fd_control_unix_socket_test.py\", sockPath)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\terr := cmd.Start()\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tdefer cmd.Process.Kill()\n\n\t// Wait for the server to start\n\ttime.Sleep(1 * time.Second)\n\n\tso := SocketOptions{\n\t\tFdControlUnixSocket: &sockPath,\n\t}\n\tconn, err := so.ListenUDP()\n\tif !assert.NoError(t, err) {\n\t\treturn\n\t}\n\tdefer conn.Close()\n\n\terr = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) {\n\t\trcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\t// The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500),\n\t\t// and kernel will double this value for getsockopt().\n\t\tassert.Equal(t, 5000, rcvbuf)\n\t\treturn\n\t})\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "app/internal/socks5/server.go",
    "content": "package socks5\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/txthinking/socks5\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst udpBufferSize = 4096\n\n// Server is a SOCKS5 server using a Hysteria client as outbound.\ntype Server struct {\n\tHyClient    client.Client\n\tAuthFunc    func(username, password string) bool // nil = no authentication\n\tDisableUDP  bool\n\tEventLogger EventLogger\n}\n\ntype EventLogger interface {\n\tTCPRequest(addr net.Addr, reqAddr string)\n\tTCPError(addr net.Addr, reqAddr string, err error)\n\tUDPRequest(addr net.Addr)\n\tUDPError(addr net.Addr, err error)\n}\n\nfunc (s *Server) Serve(listener net.Listener) error {\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo s.dispatch(conn)\n\t}\n}\n\nfunc (s *Server) dispatch(conn net.Conn) {\n\tok, _ := s.negotiate(conn)\n\tif !ok {\n\t\t_ = conn.Close()\n\t\treturn\n\t}\n\t// Negotiation ok, get and handle the request\n\treq, err := socks5.NewRequestFrom(conn)\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\treturn\n\t}\n\tswitch req.Cmd {\n\tcase socks5.CmdConnect: // TCP\n\t\ts.handleTCP(conn, req)\n\tcase socks5.CmdUDP: // UDP\n\t\tif s.DisableUDP {\n\t\t\t_ = sendSimpleReply(conn, socks5.RepCommandNotSupported)\n\t\t\t_ = conn.Close()\n\t\t\treturn\n\t\t}\n\t\ts.handleUDP(conn, req)\n\tdefault:\n\t\t_ = sendSimpleReply(conn, socks5.RepCommandNotSupported)\n\t\t_ = conn.Close()\n\t}\n}\n\nfunc (s *Server) negotiate(conn net.Conn) (bool, error) {\n\treq, err := socks5.NewNegotiationRequestFrom(conn)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tvar serverMethod byte\n\tif s.AuthFunc != nil {\n\t\tserverMethod = socks5.MethodUsernamePassword\n\t} else {\n\t\tserverMethod = socks5.MethodNone\n\t}\n\t// Look for the supported method in the client request\n\tsupported := false\n\tfor _, m := range req.Methods {\n\t\tif m == serverMethod {\n\t\t\tsupported = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !supported {\n\t\t// No supported method found, reject the client\n\t\trep := socks5.NewNegotiationReply(socks5.MethodUnsupportAll)\n\t\t_, err := rep.WriteTo(conn)\n\t\treturn false, err\n\t}\n\t// OK, send the method we chose\n\trep := socks5.NewNegotiationReply(serverMethod)\n\t_, err = rep.WriteTo(conn)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t// If we chose the username/password method, authenticate the client\n\tif serverMethod == socks5.MethodUsernamePassword {\n\t\treq, err := socks5.NewUserPassNegotiationRequestFrom(conn)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tok := s.AuthFunc(string(req.Uname), string(req.Passwd))\n\t\tif ok {\n\t\t\trep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess)\n\t\t\t_, err := rep.WriteTo(conn)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t} else {\n\t\t\trep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure)\n\t\t\t_, err := rep.WriteTo(conn)\n\t\t\treturn false, err\n\t\t}\n\t}\n\treturn true, nil\n}\n\nfunc (s *Server) handleTCP(conn net.Conn, req *socks5.Request) {\n\tdefer conn.Close()\n\n\taddr := req.Address()\n\n\t// TCP request & error log\n\tif s.EventLogger != nil {\n\t\ts.EventLogger.TCPRequest(conn.RemoteAddr(), addr)\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif s.EventLogger != nil {\n\t\t\ts.EventLogger.TCPError(conn.RemoteAddr(), addr, closeErr)\n\t\t}\n\t}()\n\n\t// Dial\n\trConn, err := s.HyClient.TCP(addr)\n\tif err != nil {\n\t\t_ = sendSimpleReply(conn, socks5.RepHostUnreachable)\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tdefer rConn.Close()\n\n\t// Send reply and start relaying\n\t_ = sendSimpleReply(conn, socks5.RepSuccess)\n\tcopyErrChan := make(chan error, 2)\n\tgo func() {\n\t\t_, err := io.Copy(rConn, conn)\n\t\tcopyErrChan <- err\n\t}()\n\tgo func() {\n\t\t_, err := io.Copy(conn, rConn)\n\t\tcopyErrChan <- err\n\t}()\n\tcloseErr = <-copyErrChan\n}\n\nfunc (s *Server) handleUDP(conn net.Conn, req *socks5.Request) {\n\tdefer conn.Close()\n\n\t// UDP request & error log\n\tif s.EventLogger != nil {\n\t\ts.EventLogger.UDPRequest(conn.RemoteAddr())\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif s.EventLogger != nil {\n\t\t\ts.EventLogger.UDPError(conn.RemoteAddr(), closeErr)\n\t\t}\n\t}()\n\n\t// Start UDP relay server\n\t// SOCKS5 UDP requires the server to return the UDP bind address and port in the reply.\n\t// We bind to the same address that our TCP server listens on (but a different port).\n\thost, _, err := net.SplitHostPort(conn.LocalAddr().String())\n\tif err != nil {\n\t\t// Is this even possible?\n\t\t_ = sendSimpleReply(conn, socks5.RepServerFailure)\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tudpAddr, err := net.ResolveUDPAddr(\"udp\", net.JoinHostPort(host, \"0\"))\n\tif err != nil {\n\t\t_ = sendSimpleReply(conn, socks5.RepServerFailure)\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tudpConn, err := net.ListenUDP(\"udp\", udpAddr)\n\tif err != nil {\n\t\t_ = sendSimpleReply(conn, socks5.RepServerFailure)\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tdefer udpConn.Close()\n\n\t// HyClient UDP session\n\thyUDP, err := s.HyClient.UDP()\n\tif err != nil {\n\t\t_ = sendSimpleReply(conn, socks5.RepServerFailure)\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tdefer hyUDP.Close()\n\n\t// Send reply\n\t_ = sendUDPReply(conn, udpConn.LocalAddr().(*net.UDPAddr))\n\n\t// UDP relay & SOCKS5 connection holder\n\terrChan := make(chan error, 2)\n\tgo func() {\n\t\terr := s.udpServer(udpConn, hyUDP)\n\t\terrChan <- err\n\t}()\n\tgo func() {\n\t\t_, err := io.Copy(io.Discard, conn)\n\t\terrChan <- err\n\t}()\n\tcloseErr = <-errChan\n}\n\nfunc (s *Server) udpServer(udpConn *net.UDPConn, hyUDP client.HyUDPConn) error {\n\tvar clientAddr *net.UDPAddr\n\tbuf := make([]byte, udpBufferSize)\n\t// local -> remote\n\tfor {\n\t\tn, cAddr, err := udpConn.ReadFromUDP(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\td, err := socks5.NewDatagramFromBytes(buf[:n])\n\t\tif err != nil || d.Frag != 0 {\n\t\t\t// Ignore bad packets\n\t\t\t// Also we don't support SOCKS5 UDP fragmentation for now\n\t\t\tcontinue\n\t\t}\n\t\tif clientAddr == nil {\n\t\t\t// Before the first packet, we don't know what IP the client will use to send us packets,\n\t\t\t// so we don't know what IP to return packets to.\n\t\t\t// We treat whoever sends us the first packet as our client.\n\t\t\tclientAddr = cAddr\n\t\t\t// Now that we know the client's address, we can start the\n\t\t\t// remote -> local direction.\n\t\t\tgo func() {\n\t\t\t\tfor {\n\t\t\t\t\tbs, from, err := hyUDP.Receive()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t// Close the UDP conn so that the local -> remote direction will exit\n\t\t\t\t\t\t_ = udpConn.Close()\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tatyp, addr, port, err := socks5.ParseAddress(from)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif atyp == socks5.ATYPDomain {\n\t\t\t\t\t\t// socks5.ParseAddress adds a leading byte for domains,\n\t\t\t\t\t\t// but socks5.NewDatagram will add it again as it expects a raw domain.\n\t\t\t\t\t\t// So we must remove it here.\n\t\t\t\t\t\taddr = addr[1:]\n\t\t\t\t\t}\n\t\t\t\t\td := socks5.NewDatagram(atyp, addr, port, bs)\n\t\t\t\t\t_, _ = udpConn.WriteToUDP(d.Bytes(), clientAddr)\n\t\t\t\t}\n\t\t\t}()\n\t\t} else if !clientAddr.IP.Equal(cAddr.IP) || clientAddr.Port != cAddr.Port {\n\t\t\t// Not our client, ignore\n\t\t\tcontinue\n\t\t}\n\t\t// Send to remote\n\t\t_ = hyUDP.Send(d.Data, d.Address())\n\t}\n}\n\n// sendSimpleReply sends a SOCKS5 reply with the given reply code.\n// It does not contain bind address or port, so it's not suitable for successful UDP requests.\nfunc sendSimpleReply(conn net.Conn, rep byte) error {\n\tp := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00})\n\t_, err := p.WriteTo(conn)\n\treturn err\n}\n\n// sendUDPReply sends a SOCKS5 reply with the given reply code and bind address/port.\nfunc sendUDPReply(conn net.Conn, addr *net.UDPAddr) error {\n\tvar atyp byte\n\tvar bndAddr, bndPort []byte\n\tif ip4 := addr.IP.To4(); ip4 != nil {\n\t\tatyp = socks5.ATYPIPv4\n\t\tbndAddr = ip4\n\t} else {\n\t\tatyp = socks5.ATYPIPv6\n\t\tbndAddr = addr.IP\n\t}\n\tbndPort = make([]byte, 2)\n\tbinary.BigEndian.PutUint16(bndPort, uint16(addr.Port))\n\tp := socks5.NewReply(socks5.RepSuccess, atyp, bndAddr, bndPort)\n\t_, err := p.WriteTo(conn)\n\treturn err\n}\n"
  },
  {
    "path": "app/internal/socks5/server_test.go",
    "content": "package socks5\n\nimport (\n\t\"net\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/apernet/hysteria/app/v2/internal/utils_test\"\n)\n\nfunc TestServer(t *testing.T) {\n\t// Start the server\n\tl, err := net.Listen(\"tcp\", \"127.0.0.1:11080\")\n\tassert.NoError(t, err)\n\tdefer l.Close()\n\ts := &Server{\n\t\tHyClient: &utils_test.MockEchoHyClient{},\n\t}\n\tgo s.Serve(l)\n\n\t// Run the Python test script\n\tcmd := exec.Command(\"python\", \"server_test.py\")\n\tout, err := cmd.CombinedOutput()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"OK\", strings.TrimSpace(string(out)))\n}\n"
  },
  {
    "path": "app/internal/socks5/server_test.py",
    "content": "import socket\nimport socks\nimport os\n\nADDR = \"127.0.0.1\"\nPORT = 11080\n\n\ndef test_tcp(size, count, it, domain=False):\n    for i in range(it):\n        s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)\n        s.set_proxy(socks.SOCKS5, ADDR, PORT)\n\n        if domain:\n            s.connect((\"test.tcp.com\", 12345))\n        else:\n            s.connect((\"1.2.3.4\", 12345))\n\n        for j in range(count):\n            payload = os.urandom(size)\n            s.send(payload)\n            rsp = s.recv(size)\n            assert rsp == payload\n\n        s.close()\n\n\ndef test_udp(size, count, it, domain=False):\n    for i in range(it):\n        s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)\n        s.set_proxy(socks.SOCKS5, ADDR, PORT)\n\n        for j in range(count):\n            payload = os.urandom(size)\n\n            if domain:\n                s.sendto(payload, (\"test.udp.com\", 12345))\n            else:\n                s.sendto(payload, (\"1.2.3.4\", 12345))\n\n            rsp, addr = s.recvfrom(size)\n            assert rsp == payload\n\n            if domain:\n                assert addr == (b\"test.udp.com\", 12345)\n            else:\n                assert addr == (\"1.2.3.4\", 12345)\n\n        s.close()\n\n\nif __name__ == \"__main__\":\n    test_tcp(1024, 1024, 10, domain=False)\n    test_tcp(1024, 1024, 10, domain=True)\n    test_udp(1024, 1024, 10, domain=False)\n    test_udp(1024, 1024, 10, domain=True)\n    print(\"OK\")\n"
  },
  {
    "path": "app/internal/tproxy/tcp_linux.go",
    "content": "package tproxy\n\nimport (\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/apernet/go-tproxy\"\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\ntype TCPTProxy struct {\n\tHyClient    client.Client\n\tEventLogger TCPEventLogger\n}\n\ntype TCPEventLogger interface {\n\tConnect(addr, reqAddr net.Addr)\n\tError(addr, reqAddr net.Addr, err error)\n}\n\nfunc (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error {\n\tlistener, err := tproxy.ListenTCP(\"tcp\", laddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer listener.Close()\n\tfor {\n\t\tc, err := listener.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo r.handle(c)\n\t}\n}\n\nfunc (r *TCPTProxy) handle(conn net.Conn) {\n\tdefer conn.Close()\n\t// In TProxy mode, we are masquerading as the remote server.\n\t// So LocalAddr is actually the target the user is trying to connect to,\n\t// and RemoteAddr is the local address.\n\tif r.EventLogger != nil {\n\t\tr.EventLogger.Connect(conn.RemoteAddr(), conn.LocalAddr())\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif r.EventLogger != nil {\n\t\t\tr.EventLogger.Error(conn.RemoteAddr(), conn.LocalAddr(), closeErr)\n\t\t}\n\t}()\n\n\trc, err := r.HyClient.TCP(conn.LocalAddr().String())\n\tif err != nil {\n\t\tcloseErr = err\n\t\treturn\n\t}\n\tdefer rc.Close()\n\n\t// Start forwarding\n\tcopyErrChan := make(chan error, 2)\n\tgo func() {\n\t\t_, copyErr := io.Copy(rc, conn)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tgo func() {\n\t\t_, copyErr := io.Copy(conn, rc)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tcloseErr = <-copyErrChan\n}\n"
  },
  {
    "path": "app/internal/tproxy/tcp_others.go",
    "content": "//go:build !linux\n\npackage tproxy\n\nimport (\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\ntype TCPTProxy struct {\n\tHyClient    client.Client\n\tEventLogger TCPEventLogger\n}\n\ntype TCPEventLogger interface {\n\tConnect(addr, reqAddr net.Addr)\n\tError(addr, reqAddr net.Addr, err error)\n}\n\nfunc (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error {\n\treturn errors.New(\"not supported on this platform\")\n}\n"
  },
  {
    "path": "app/internal/tproxy/udp_linux.go",
    "content": "package tproxy\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/apernet/go-tproxy\"\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst (\n\tudpBufferSize  = 4096\n\tdefaultTimeout = 60 * time.Second\n)\n\ntype UDPTProxy struct {\n\tHyClient    client.Client\n\tTimeout     time.Duration\n\tEventLogger UDPEventLogger\n}\n\ntype UDPEventLogger interface {\n\tConnect(addr, reqAddr net.Addr)\n\tError(addr, reqAddr net.Addr, err error)\n}\n\nfunc (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error {\n\tconn, err := tproxy.ListenUDP(\"udp\", laddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer conn.Close()\n\tbuf := make([]byte, udpBufferSize)\n\tfor {\n\t\t// We will only get the first packet of each src/dst pair here,\n\t\t// because newPair will create a TProxy connection and take over\n\t\t// the src/dst pair. Later packets will be sent there instead of here.\n\t\tn, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr.newPair(srcAddr, dstAddr, buf[:n])\n\t}\n}\n\nfunc (r *UDPTProxy) newPair(srcAddr, dstAddr *net.UDPAddr, initPkt []byte) {\n\tif r.EventLogger != nil {\n\t\tr.EventLogger.Connect(srcAddr, dstAddr)\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\t// If closeErr is nil, it means we at least successfully sent the first packet\n\t\t// and started forwarding, in which case we don't call the error logger.\n\t\tif r.EventLogger != nil && closeErr != nil {\n\t\t\tr.EventLogger.Error(srcAddr, dstAddr, closeErr)\n\t\t}\n\t}()\n\tconn, err := tproxy.DialUDP(\"udp\", dstAddr, srcAddr)\n\tif err != nil {\n\t\tcloseErr = err\n\t\treturn\n\t}\n\thyConn, err := r.HyClient.UDP()\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\tcloseErr = err\n\t\treturn\n\t}\n\t// Send the first packet\n\terr = hyConn.Send(initPkt, dstAddr.String())\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\t_ = hyConn.Close()\n\t\tcloseErr = err\n\t\treturn\n\t}\n\t// Start forwarding\n\tgo func() {\n\t\terr := r.forwarding(conn, hyConn, dstAddr.String())\n\t\t_ = conn.Close()\n\t\t_ = hyConn.Close()\n\t\tif r.EventLogger != nil {\n\t\t\tvar netErr net.Error\n\t\t\tif errors.As(err, &netErr) && netErr.Timeout() {\n\t\t\t\t// We don't consider deadline exceeded (timeout) an error\n\t\t\t\terr = nil\n\t\t\t}\n\t\t\tr.EventLogger.Error(srcAddr, dstAddr, err)\n\t\t}\n\t}()\n}\n\nfunc (r *UDPTProxy) forwarding(conn *net.UDPConn, hyConn client.HyUDPConn, dst string) error {\n\terrChan := make(chan error, 2)\n\t// Local <- Remote\n\tgo func() {\n\t\tfor {\n\t\t\tbs, _, err := hyConn.Receive()\n\t\t\tif err != nil {\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_, err = conn.Write(bs)\n\t\t\tif err != nil {\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_ = r.updateConnDeadline(conn)\n\t\t}\n\t}()\n\t// Local -> Remote\n\tgo func() {\n\t\tbuf := make([]byte, udpBufferSize)\n\t\tfor {\n\t\t\t_ = r.updateConnDeadline(conn)\n\t\t\tn, err := conn.Read(buf)\n\t\t\tif n > 0 {\n\t\t\t\terr := hyConn.Send(buf[:n], dst)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrChan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\treturn <-errChan\n}\n\nfunc (r *UDPTProxy) updateConnDeadline(conn *net.UDPConn) error {\n\tif r.Timeout == 0 {\n\t\treturn conn.SetReadDeadline(time.Now().Add(defaultTimeout))\n\t} else {\n\t\treturn conn.SetReadDeadline(time.Now().Add(r.Timeout))\n\t}\n}\n"
  },
  {
    "path": "app/internal/tproxy/udp_others.go",
    "content": "//go:build !linux\n\npackage tproxy\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\ntype UDPTProxy struct {\n\tHyClient    client.Client\n\tTimeout     time.Duration\n\tEventLogger UDPEventLogger\n}\n\ntype UDPEventLogger interface {\n\tConnect(addr, reqAddr net.Addr)\n\tError(addr, reqAddr net.Addr, err error)\n}\n\nfunc (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error {\n\treturn errors.New(\"not supported on this platform\")\n}\n"
  },
  {
    "path": "app/internal/tun/log.go",
    "content": "package tun\n\nimport (\n\t\"github.com/sagernet/sing/common/logger\"\n\t\"go.uber.org/zap\"\n)\n\nvar _ logger.Logger = (*singLogger)(nil)\n\ntype singLogger struct {\n\ttag       string\n\tzapLogger *zap.Logger\n}\n\nfunc extractSingExceptions(args []any) {\n\tfor i, arg := range args {\n\t\tif err, ok := arg.(error); ok {\n\t\t\targs[i] = err.Error()\n\t\t}\n\t}\n}\n\nfunc (l *singLogger) Trace(args ...any) {\n\tif l.zapLogger == nil {\n\t\treturn\n\t}\n\textractSingExceptions(args)\n\tl.zapLogger.Debug(l.tag, zap.Any(\"args\", args))\n}\n\nfunc (l *singLogger) Debug(args ...any) {\n\tif l.zapLogger == nil {\n\t\treturn\n\t}\n\textractSingExceptions(args)\n\tl.zapLogger.Debug(l.tag, zap.Any(\"args\", args))\n}\n\nfunc (l *singLogger) Info(args ...any) {\n\tif l.zapLogger == nil {\n\t\treturn\n\t}\n\textractSingExceptions(args)\n\tl.zapLogger.Info(l.tag, zap.Any(\"args\", args))\n}\n\nfunc (l *singLogger) Warn(args ...any) {\n\tif l.zapLogger == nil {\n\t\treturn\n\t}\n\textractSingExceptions(args)\n\tl.zapLogger.Warn(l.tag, zap.Any(\"args\", args))\n}\n\nfunc (l *singLogger) Error(args ...any) {\n\tif l.zapLogger == nil {\n\t\treturn\n\t}\n\textractSingExceptions(args)\n\tl.zapLogger.Error(l.tag, zap.Any(\"args\", args))\n}\n\nfunc (l *singLogger) Fatal(args ...any) {\n\tif l.zapLogger == nil {\n\t\treturn\n\t}\n\textractSingExceptions(args)\n\tl.zapLogger.Fatal(l.tag, zap.Any(\"args\", args))\n}\n\nfunc (l *singLogger) Panic(args ...any) {\n\tif l.zapLogger == nil {\n\t\treturn\n\t}\n\textractSingExceptions(args)\n\tl.zapLogger.Panic(l.tag, zap.Any(\"args\", args))\n}\n"
  },
  {
    "path": "app/internal/tun/server.go",
    "content": "package tun\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/netip\"\n\n\ttun \"github.com/apernet/sing-tun\"\n\t\"github.com/sagernet/sing/common/buf\"\n\t\"github.com/sagernet/sing/common/control\"\n\t\"github.com/sagernet/sing/common/metadata\"\n\t\"github.com/sagernet/sing/common/network\"\n\t\"go.uber.org/zap\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\ntype Server struct {\n\tHyClient    client.Client\n\tEventLogger EventLogger\n\n\t// for debugging\n\tLogger *zap.Logger\n\n\tIfName  string\n\tMTU     uint32\n\tTimeout int64 // in seconds, also applied to TCP in system stack\n\n\t// required by system stack\n\tInet4Address []netip.Prefix\n\tInet6Address []netip.Prefix\n\n\t// auto route\n\tAutoRoute                bool\n\tStructRoute              bool\n\tInet4RouteAddress        []netip.Prefix\n\tInet6RouteAddress        []netip.Prefix\n\tInet4RouteExcludeAddress []netip.Prefix\n\tInet6RouteExcludeAddress []netip.Prefix\n}\n\ntype EventLogger interface {\n\tTCPRequest(addr, reqAddr string)\n\tTCPError(addr, reqAddr string, err error)\n\tUDPRequest(addr string)\n\tUDPError(addr string, err error)\n}\n\nfunc (s *Server) Serve() error {\n\ttunOpts := tun.Options{\n\t\tName:                     s.IfName,\n\t\tInet4Address:             s.Inet4Address,\n\t\tInet6Address:             s.Inet6Address,\n\t\tMTU:                      s.MTU,\n\t\tGSO:                      true,\n\t\tAutoRoute:                s.AutoRoute,\n\t\tStrictRoute:              s.StructRoute,\n\t\tInet4RouteAddress:        s.Inet4RouteAddress,\n\t\tInet6RouteAddress:        s.Inet6RouteAddress,\n\t\tInet4RouteExcludeAddress: s.Inet4RouteExcludeAddress,\n\t\tInet6RouteExcludeAddress: s.Inet6RouteExcludeAddress,\n\t\tLogger: &singLogger{\n\t\t\ttag:       \"tun\",\n\t\t\tzapLogger: s.Logger,\n\t\t},\n\t}\n\ttunIf, err := tun.New(tunOpts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create tun interface: %w\", err)\n\t}\n\tdefer tunIf.Close()\n\n\ttunStack, err := tun.NewSystem(tun.StackOptions{\n\t\tContext:    context.Background(),\n\t\tTun:        tunIf,\n\t\tTunOptions: tunOpts,\n\t\tUDPTimeout: s.Timeout,\n\t\tHandler:    &tunHandler{s},\n\t\tLogger: &singLogger{\n\t\t\ttag:       \"tun-stack\",\n\t\t\tzapLogger: s.Logger,\n\t\t},\n\t\tForwarderBindInterface: true,\n\t\tInterfaceFinder:        &interfaceFinder{},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create tun stack: %w\", err)\n\t}\n\tdefer tunStack.Close()\n\treturn tunStack.(tun.StackRunner).Run()\n}\n\ntype tunHandler struct {\n\t*Server\n}\n\nvar _ tun.Handler = (*tunHandler)(nil)\n\nfunc (t *tunHandler) NewConnection(ctx context.Context, conn net.Conn, m metadata.Metadata) error {\n\taddr := m.Source.String()\n\treqAddr := m.Destination.String()\n\tif t.EventLogger != nil {\n\t\tt.EventLogger.TCPRequest(addr, reqAddr)\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif t.EventLogger != nil {\n\t\t\tt.EventLogger.TCPError(addr, reqAddr, closeErr)\n\t\t}\n\t}()\n\trc, err := t.HyClient.TCP(reqAddr)\n\tif err != nil {\n\t\tcloseErr = err\n\t\t// the returned err is ignored by caller\n\t\treturn nil\n\t}\n\tdefer rc.Close()\n\n\t// start forwarding\n\tcopyErrChan := make(chan error, 3)\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tcopyErrChan <- ctx.Err()\n\t}()\n\tgo func() {\n\t\t_, copyErr := io.Copy(rc, conn)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tgo func() {\n\t\t_, copyErr := io.Copy(conn, rc)\n\t\tcopyErrChan <- copyErr\n\t}()\n\tcloseErr = <-copyErrChan\n\treturn nil\n}\n\nfunc (t *tunHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, m metadata.Metadata) error {\n\taddr := m.Source.String()\n\tif t.EventLogger != nil {\n\t\tt.EventLogger.UDPRequest(addr)\n\t}\n\tvar closeErr error\n\tdefer func() {\n\t\tif t.EventLogger != nil {\n\t\t\tt.EventLogger.UDPError(addr, closeErr)\n\t\t}\n\t}()\n\trc, err := t.HyClient.UDP()\n\tif err != nil {\n\t\tcloseErr = err\n\t\t// the returned err is simply called into NewError again\n\t\treturn nil\n\t}\n\tdefer rc.Close()\n\n\t// start forwarding\n\tcopyErrChan := make(chan error, 3)\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tcopyErrChan <- ctx.Err()\n\t}()\n\t// local <- remote\n\tgo func() {\n\t\tfor {\n\t\t\tbs, from, err := rc.Receive()\n\t\t\tif err != nil {\n\t\t\t\tcopyErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar fromAddr metadata.Socksaddr\n\t\t\tif ap, perr := netip.ParseAddrPort(from); perr == nil {\n\t\t\t\tfromAddr = metadata.SocksaddrFromNetIP(ap)\n\t\t\t} else {\n\t\t\t\tfromAddr.Fqdn = from\n\t\t\t}\n\t\t\terr = conn.WritePacket(buf.As(bs), fromAddr)\n\t\t\tif err != nil {\n\t\t\t\tcopyErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\t// local -> remote\n\tgo func() {\n\t\tbuffer := buf.NewPacket()\n\t\tdefer buffer.Release()\n\n\t\tfor {\n\t\t\tbuffer.Reset()\n\t\t\taddr, err := conn.ReadPacket(buffer)\n\t\t\tif err != nil {\n\t\t\t\tcopyErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\terr = rc.Send(buffer.Bytes(), addr.String())\n\t\t\tif err != nil {\n\t\t\t\tcopyErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\tcloseErr = <-copyErrChan\n\treturn nil\n}\n\nfunc (t *tunHandler) NewError(ctx context.Context, err error) {\n\t// unused\n}\n\ntype interfaceFinder struct{}\n\nvar _ control.InterfaceFinder = (*interfaceFinder)(nil)\n\nfunc (f *interfaceFinder) InterfaceIndexByName(name string) (int, error) {\n\tifce, err := net.InterfaceByName(name)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\treturn ifce.Index, nil\n}\n\nfunc (f *interfaceFinder) InterfaceNameByIndex(index int) (string, error) {\n\tifce, err := net.InterfaceByIndex(index)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn ifce.Name, nil\n}\n"
  },
  {
    "path": "app/internal/url/url.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package url parses URLs and implements query escaping.\npackage url\n\n// See RFC 3986. This package generally follows RFC 3986, except where\n// it deviates for compatibility reasons. When sending changes, first\n// search old issues for history on decisions. Unit tests should also\n// contain references to issue numbers with details.\n\n// Hysteria fork note: This file is grabbed from the standard Go library,\n// but with a few modifications to make it support our special port format\n// when using port hopping.\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"path\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Error reports an error and the operation and URL that caused it.\ntype Error struct {\n\tOp  string\n\tURL string\n\tErr error\n}\n\nfunc (e *Error) Unwrap() error { return e.Err }\nfunc (e *Error) Error() string { return fmt.Sprintf(\"%s %q: %s\", e.Op, e.URL, e.Err) }\n\nfunc (e *Error) Timeout() bool {\n\tt, ok := e.Err.(interface {\n\t\tTimeout() bool\n\t})\n\treturn ok && t.Timeout()\n}\n\nfunc (e *Error) Temporary() bool {\n\tt, ok := e.Err.(interface {\n\t\tTemporary() bool\n\t})\n\treturn ok && t.Temporary()\n}\n\nconst upperhex = \"0123456789ABCDEF\"\n\nfunc ishex(c byte) bool {\n\tswitch {\n\tcase '0' <= c && c <= '9':\n\t\treturn true\n\tcase 'a' <= c && c <= 'f':\n\t\treturn true\n\tcase 'A' <= c && c <= 'F':\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc unhex(c byte) byte {\n\tswitch {\n\tcase '0' <= c && c <= '9':\n\t\treturn c - '0'\n\tcase 'a' <= c && c <= 'f':\n\t\treturn c - 'a' + 10\n\tcase 'A' <= c && c <= 'F':\n\t\treturn c - 'A' + 10\n\t}\n\treturn 0\n}\n\ntype encoding int\n\nconst (\n\tencodePath encoding = 1 + iota\n\tencodePathSegment\n\tencodeHost\n\tencodeZone\n\tencodeUserPassword\n\tencodeQueryComponent\n\tencodeFragment\n)\n\ntype EscapeError string\n\nfunc (e EscapeError) Error() string {\n\treturn \"invalid URL escape \" + strconv.Quote(string(e))\n}\n\ntype InvalidHostError string\n\nfunc (e InvalidHostError) Error() string {\n\treturn \"invalid character \" + strconv.Quote(string(e)) + \" in host name\"\n}\n\n// Return true if the specified character should be escaped when\n// appearing in a URL string, according to RFC 3986.\n//\n// Please be informed that for now shouldEscape does not check all\n// reserved characters correctly. See golang.org/issue/5684.\nfunc shouldEscape(c byte, mode encoding) bool {\n\t// §2.3 Unreserved characters (alphanum)\n\tif 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {\n\t\treturn false\n\t}\n\n\tif mode == encodeHost || mode == encodeZone {\n\t\t// §3.2.2 Host allows\n\t\t//\tsub-delims = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t\t// as part of reg-name.\n\t\t// We add : because we include :port as part of host.\n\t\t// We add [ ] because we include [ipv6]:port as part of host.\n\t\t// We add < > because they're the only characters left that\n\t\t// we could possibly allow, and Parse will reject them if we\n\t\t// escape them (because hosts can't use %-encoding for\n\t\t// ASCII bytes).\n\t\tswitch c {\n\t\tcase '!', '$', '&', '\\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '\"':\n\t\t\treturn false\n\t\t}\n\t}\n\n\tswitch c {\n\tcase '-', '_', '.', '~': // §2.3 Unreserved characters (mark)\n\t\treturn false\n\n\tcase '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)\n\t\t// Different sections of the URL allow a few of\n\t\t// the reserved characters to appear unescaped.\n\t\tswitch mode {\n\t\tcase encodePath: // §3.3\n\t\t\t// The RFC allows : @ & = + $ but saves / ; , for assigning\n\t\t\t// meaning to individual path segments. This package\n\t\t\t// only manipulates the path as a whole, so we allow those\n\t\t\t// last three as well. That leaves only ? to escape.\n\t\t\treturn c == '?'\n\n\t\tcase encodePathSegment: // §3.3\n\t\t\t// The RFC allows : @ & = + $ but saves / ; , for assigning\n\t\t\t// meaning to individual path segments.\n\t\t\treturn c == '/' || c == ';' || c == ',' || c == '?'\n\n\t\tcase encodeUserPassword: // §3.2.1\n\t\t\t// The RFC allows ';', ':', '&', '=', '+', '$', and ',' in\n\t\t\t// userinfo, so we must escape only '@', '/', and '?'.\n\t\t\t// The parsing of userinfo treats ':' as special so we must escape\n\t\t\t// that too.\n\t\t\treturn c == '@' || c == '/' || c == '?' || c == ':'\n\n\t\tcase encodeQueryComponent: // §3.4\n\t\t\t// The RFC reserves (so we must escape) everything.\n\t\t\treturn true\n\n\t\tcase encodeFragment: // §4.1\n\t\t\t// The RFC text is silent but the grammar allows\n\t\t\t// everything, so escape nothing.\n\t\t\treturn false\n\t\t}\n\t}\n\n\tif mode == encodeFragment {\n\t\t// RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are\n\t\t// included in reserved from RFC 2396 §2.2. The remaining sub-delims do not\n\t\t// need to be escaped. To minimize potential breakage, we apply two restrictions:\n\t\t// (1) we always escape sub-delims outside of the fragment, and (2) we always\n\t\t// escape single quote to avoid breaking callers that had previously assumed that\n\t\t// single quotes would be escaped. See issue #19917.\n\t\tswitch c {\n\t\tcase '!', '(', ')', '*':\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Everything else must be escaped.\n\treturn true\n}\n\n// QueryUnescape does the inverse transformation of QueryEscape,\n// converting each 3-byte encoded substring of the form \"%AB\" into the\n// hex-decoded byte 0xAB.\n// It returns an error if any % is not followed by two hexadecimal\n// digits.\nfunc QueryUnescape(s string) (string, error) {\n\treturn unescape(s, encodeQueryComponent)\n}\n\n// PathUnescape does the inverse transformation of PathEscape,\n// converting each 3-byte encoded substring of the form \"%AB\" into the\n// hex-decoded byte 0xAB. It returns an error if any % is not followed\n// by two hexadecimal digits.\n//\n// PathUnescape is identical to QueryUnescape except that it does not\n// unescape '+' to ' ' (space).\nfunc PathUnescape(s string) (string, error) {\n\treturn unescape(s, encodePathSegment)\n}\n\n// unescape unescapes a string; the mode specifies\n// which section of the URL string is being unescaped.\nfunc unescape(s string, mode encoding) (string, error) {\n\t// Count %, check that they're well-formed.\n\tn := 0\n\thasPlus := false\n\tfor i := 0; i < len(s); {\n\t\tswitch s[i] {\n\t\tcase '%':\n\t\t\tn++\n\t\t\tif i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {\n\t\t\t\ts = s[i:]\n\t\t\t\tif len(s) > 3 {\n\t\t\t\t\ts = s[:3]\n\t\t\t\t}\n\t\t\t\treturn \"\", EscapeError(s)\n\t\t\t}\n\t\t\t// Per https://tools.ietf.org/html/rfc3986#page-21\n\t\t\t// in the host component %-encoding can only be used\n\t\t\t// for non-ASCII bytes.\n\t\t\t// But https://tools.ietf.org/html/rfc6874#section-2\n\t\t\t// introduces %25 being allowed to escape a percent sign\n\t\t\t// in IPv6 scoped-address literals. Yay.\n\t\t\tif mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != \"%25\" {\n\t\t\t\treturn \"\", EscapeError(s[i : i+3])\n\t\t\t}\n\t\t\tif mode == encodeZone {\n\t\t\t\t// RFC 6874 says basically \"anything goes\" for zone identifiers\n\t\t\t\t// and that even non-ASCII can be redundantly escaped,\n\t\t\t\t// but it seems prudent to restrict %-escaped bytes here to those\n\t\t\t\t// that are valid host name bytes in their unescaped form.\n\t\t\t\t// That is, you can use escaping in the zone identifier but not\n\t\t\t\t// to introduce bytes you couldn't just write directly.\n\t\t\t\t// But Windows puts spaces here! Yay.\n\t\t\t\tv := unhex(s[i+1])<<4 | unhex(s[i+2])\n\t\t\t\tif s[i:i+3] != \"%25\" && v != ' ' && shouldEscape(v, encodeHost) {\n\t\t\t\t\treturn \"\", EscapeError(s[i : i+3])\n\t\t\t\t}\n\t\t\t}\n\t\t\ti += 3\n\t\tcase '+':\n\t\t\thasPlus = mode == encodeQueryComponent\n\t\t\ti++\n\t\tdefault:\n\t\t\tif (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) {\n\t\t\t\treturn \"\", InvalidHostError(s[i : i+1])\n\t\t\t}\n\t\t\ti++\n\t\t}\n\t}\n\n\tif n == 0 && !hasPlus {\n\t\treturn s, nil\n\t}\n\n\tvar t strings.Builder\n\tt.Grow(len(s) - 2*n)\n\tfor i := 0; i < len(s); i++ {\n\t\tswitch s[i] {\n\t\tcase '%':\n\t\t\tt.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2]))\n\t\t\ti += 2\n\t\tcase '+':\n\t\t\tif mode == encodeQueryComponent {\n\t\t\t\tt.WriteByte(' ')\n\t\t\t} else {\n\t\t\t\tt.WriteByte('+')\n\t\t\t}\n\t\tdefault:\n\t\t\tt.WriteByte(s[i])\n\t\t}\n\t}\n\treturn t.String(), nil\n}\n\n// QueryEscape escapes the string so it can be safely placed\n// inside a URL query.\nfunc QueryEscape(s string) string {\n\treturn escape(s, encodeQueryComponent)\n}\n\n// PathEscape escapes the string so it can be safely placed inside a URL path segment,\n// replacing special characters (including /) with %XX sequences as needed.\nfunc PathEscape(s string) string {\n\treturn escape(s, encodePathSegment)\n}\n\nfunc escape(s string, mode encoding) string {\n\tspaceCount, hexCount := 0, 0\n\tfor i := 0; i < len(s); i++ {\n\t\tc := s[i]\n\t\tif shouldEscape(c, mode) {\n\t\t\tif c == ' ' && mode == encodeQueryComponent {\n\t\t\t\tspaceCount++\n\t\t\t} else {\n\t\t\t\thexCount++\n\t\t\t}\n\t\t}\n\t}\n\n\tif spaceCount == 0 && hexCount == 0 {\n\t\treturn s\n\t}\n\n\tvar buf [64]byte\n\tvar t []byte\n\n\trequired := len(s) + 2*hexCount\n\tif required <= len(buf) {\n\t\tt = buf[:required]\n\t} else {\n\t\tt = make([]byte, required)\n\t}\n\n\tif hexCount == 0 {\n\t\tcopy(t, s)\n\t\tfor i := 0; i < len(s); i++ {\n\t\t\tif s[i] == ' ' {\n\t\t\t\tt[i] = '+'\n\t\t\t}\n\t\t}\n\t\treturn string(t)\n\t}\n\n\tj := 0\n\tfor i := 0; i < len(s); i++ {\n\t\tswitch c := s[i]; {\n\t\tcase c == ' ' && mode == encodeQueryComponent:\n\t\t\tt[j] = '+'\n\t\t\tj++\n\t\tcase shouldEscape(c, mode):\n\t\t\tt[j] = '%'\n\t\t\tt[j+1] = upperhex[c>>4]\n\t\t\tt[j+2] = upperhex[c&15]\n\t\t\tj += 3\n\t\tdefault:\n\t\t\tt[j] = s[i]\n\t\t\tj++\n\t\t}\n\t}\n\treturn string(t)\n}\n\n// A URL represents a parsed URL (technically, a URI reference).\n//\n// The general form represented is:\n//\n//\t[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n//\n// URLs that do not start with a slash after the scheme are interpreted as:\n//\n//\tscheme:opaque[?query][#fragment]\n//\n// Note that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\n// A consequence is that it is impossible to tell which slashes in the Path were\n// slashes in the raw URL and which were %2f. This distinction is rarely important,\n// but when it is, the code should use the EscapedPath method, which preserves\n// the original encoding of Path.\n//\n// The RawPath field is an optional field which is only set when the default\n// encoding of Path is different from the escaped path. See the EscapedPath method\n// for more details.\n//\n// URL's String method uses the EscapedPath method to obtain the path.\ntype URL struct {\n\tScheme      string\n\tOpaque      string    // encoded opaque data\n\tUser        *Userinfo // username and password information\n\tHost        string    // host or host:port\n\tPath        string    // path (relative paths may omit leading slash)\n\tRawPath     string    // encoded path hint (see EscapedPath method)\n\tOmitHost    bool      // do not emit empty host (authority)\n\tForceQuery  bool      // append a query ('?') even if RawQuery is empty\n\tRawQuery    string    // encoded query values, without '?'\n\tFragment    string    // fragment for references, without '#'\n\tRawFragment string    // encoded fragment hint (see EscapedFragment method)\n}\n\n// User returns a Userinfo containing the provided username\n// and no password set.\nfunc User(username string) *Userinfo {\n\treturn &Userinfo{username, \"\", false}\n}\n\n// UserPassword returns a Userinfo containing the provided username\n// and password.\n//\n// This functionality should only be used with legacy web sites.\n// RFC 2396 warns that interpreting Userinfo this way\n// “is NOT RECOMMENDED, because the passing of authentication\n// information in clear text (such as URI) has proven to be a\n// security risk in almost every case where it has been used.”\nfunc UserPassword(username, password string) *Userinfo {\n\treturn &Userinfo{username, password, true}\n}\n\n// The Userinfo type is an immutable encapsulation of username and\n// password details for a URL. An existing Userinfo value is guaranteed\n// to have a username set (potentially empty, as allowed by RFC 2396),\n// and optionally a password.\ntype Userinfo struct {\n\tusername    string\n\tpassword    string\n\tpasswordSet bool\n}\n\n// Username returns the username.\nfunc (u *Userinfo) Username() string {\n\tif u == nil {\n\t\treturn \"\"\n\t}\n\treturn u.username\n}\n\n// Password returns the password in case it is set, and whether it is set.\nfunc (u *Userinfo) Password() (string, bool) {\n\tif u == nil {\n\t\treturn \"\", false\n\t}\n\treturn u.password, u.passwordSet\n}\n\n// String returns the encoded userinfo information in the standard form\n// of \"username[:password]\".\nfunc (u *Userinfo) String() string {\n\tif u == nil {\n\t\treturn \"\"\n\t}\n\ts := escape(u.username, encodeUserPassword)\n\tif u.passwordSet {\n\t\ts += \":\" + escape(u.password, encodeUserPassword)\n\t}\n\treturn s\n}\n\n// Maybe rawURL is of the form scheme:path.\n// (Scheme must be [a-zA-Z][a-zA-Z0-9+.-]*)\n// If so, return scheme, path; else return \"\", rawURL.\nfunc getScheme(rawURL string) (scheme, path string, err error) {\n\tfor i := 0; i < len(rawURL); i++ {\n\t\tc := rawURL[i]\n\t\tswitch {\n\t\tcase 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':\n\t\t// do nothing\n\t\tcase '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.':\n\t\t\tif i == 0 {\n\t\t\t\treturn \"\", rawURL, nil\n\t\t\t}\n\t\tcase c == ':':\n\t\t\tif i == 0 {\n\t\t\t\treturn \"\", \"\", errors.New(\"missing protocol scheme\")\n\t\t\t}\n\t\t\treturn rawURL[:i], rawURL[i+1:], nil\n\t\tdefault:\n\t\t\t// we have encountered an invalid character,\n\t\t\t// so there is no valid scheme\n\t\t\treturn \"\", rawURL, nil\n\t\t}\n\t}\n\treturn \"\", rawURL, nil\n}\n\n// Parse parses a raw url into a URL structure.\n//\n// The url may be relative (a path, without a host) or absolute\n// (starting with a scheme). Trying to parse a hostname and path\n// without a scheme is invalid but may not necessarily return an\n// error, due to parsing ambiguities.\nfunc Parse(rawURL string) (*URL, error) {\n\t// Cut off #frag\n\tu, frag, _ := strings.Cut(rawURL, \"#\")\n\turl, err := parse(u, false)\n\tif err != nil {\n\t\treturn nil, &Error{\"parse\", u, err}\n\t}\n\tif frag == \"\" {\n\t\treturn url, nil\n\t}\n\tif err = url.setFragment(frag); err != nil {\n\t\treturn nil, &Error{\"parse\", rawURL, err}\n\t}\n\treturn url, nil\n}\n\n// ParseRequestURI parses a raw url into a URL structure. It assumes that\n// url was received in an HTTP request, so the url is interpreted\n// only as an absolute URI or an absolute path.\n// The string url is assumed not to have a #fragment suffix.\n// (Web browsers strip #fragment before sending the URL to a web server.)\nfunc ParseRequestURI(rawURL string) (*URL, error) {\n\turl, err := parse(rawURL, true)\n\tif err != nil {\n\t\treturn nil, &Error{\"parse\", rawURL, err}\n\t}\n\treturn url, nil\n}\n\n// parse parses a URL from a string in one of two contexts. If\n// viaRequest is true, the URL is assumed to have arrived via an HTTP request,\n// in which case only absolute URLs or path-absolute relative URLs are allowed.\n// If viaRequest is false, all forms of relative URLs are allowed.\nfunc parse(rawURL string, viaRequest bool) (*URL, error) {\n\tvar rest string\n\tvar err error\n\n\tif stringContainsCTLByte(rawURL) {\n\t\treturn nil, errors.New(\"net/url: invalid control character in URL\")\n\t}\n\n\tif rawURL == \"\" && viaRequest {\n\t\treturn nil, errors.New(\"empty url\")\n\t}\n\turl := new(URL)\n\n\tif rawURL == \"*\" {\n\t\turl.Path = \"*\"\n\t\treturn url, nil\n\t}\n\n\t// Split off possible leading \"http:\", \"mailto:\", etc.\n\t// Cannot contain escaped characters.\n\tif url.Scheme, rest, err = getScheme(rawURL); err != nil {\n\t\treturn nil, err\n\t}\n\turl.Scheme = strings.ToLower(url.Scheme)\n\n\tif strings.HasSuffix(rest, \"?\") && strings.Count(rest, \"?\") == 1 {\n\t\turl.ForceQuery = true\n\t\trest = rest[:len(rest)-1]\n\t} else {\n\t\trest, url.RawQuery, _ = strings.Cut(rest, \"?\")\n\t}\n\n\tif !strings.HasPrefix(rest, \"/\") {\n\t\tif url.Scheme != \"\" {\n\t\t\t// We consider rootless paths per RFC 3986 as opaque.\n\t\t\turl.Opaque = rest\n\t\t\treturn url, nil\n\t\t}\n\t\tif viaRequest {\n\t\t\treturn nil, errors.New(\"invalid URI for request\")\n\t\t}\n\n\t\t// Avoid confusion with malformed schemes, like cache_object:foo/bar.\n\t\t// See golang.org/issue/16822.\n\t\t//\n\t\t// RFC 3986, §3.3:\n\t\t// In addition, a URI reference (Section 4.1) may be a relative-path reference,\n\t\t// in which case the first path segment cannot contain a colon (\":\") character.\n\t\tif segment, _, _ := strings.Cut(rest, \"/\"); strings.Contains(segment, \":\") {\n\t\t\t// First path segment has colon. Not allowed in relative URL.\n\t\t\treturn nil, errors.New(\"first path segment in URL cannot contain colon\")\n\t\t}\n\t}\n\n\tif (url.Scheme != \"\" || !viaRequest && !strings.HasPrefix(rest, \"///\")) && strings.HasPrefix(rest, \"//\") {\n\t\tvar authority string\n\t\tauthority, rest = rest[2:], \"\"\n\t\tif i := strings.Index(authority, \"/\"); i >= 0 {\n\t\t\tauthority, rest = authority[:i], authority[i:]\n\t\t}\n\t\turl.User, url.Host, err = parseAuthority(authority)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else if url.Scheme != \"\" && strings.HasPrefix(rest, \"/\") {\n\t\t// OmitHost is set to true when rawURL has an empty host (authority).\n\t\t// See golang.org/issue/46059.\n\t\turl.OmitHost = true\n\t}\n\n\t// Set Path and, optionally, RawPath.\n\t// RawPath is a hint of the encoding of Path. We don't want to set it if\n\t// the default escaping of Path is equivalent, to help make sure that people\n\t// don't rely on it in general.\n\tif err := url.setPath(rest); err != nil {\n\t\treturn nil, err\n\t}\n\treturn url, nil\n}\n\nfunc parseAuthority(authority string) (user *Userinfo, host string, err error) {\n\ti := strings.LastIndex(authority, \"@\")\n\tif i < 0 {\n\t\thost, err = parseHost(authority)\n\t} else {\n\t\thost, err = parseHost(authority[i+1:])\n\t}\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tif i < 0 {\n\t\treturn nil, host, nil\n\t}\n\tuserinfo := authority[:i]\n\tif !validUserinfo(userinfo) {\n\t\treturn nil, \"\", errors.New(\"net/url: invalid userinfo\")\n\t}\n\tif !strings.Contains(userinfo, \":\") {\n\t\tif userinfo, err = unescape(userinfo, encodeUserPassword); err != nil {\n\t\t\treturn nil, \"\", err\n\t\t}\n\t\tuser = User(userinfo)\n\t} else {\n\t\tusername, password, _ := strings.Cut(userinfo, \":\")\n\t\tif username, err = unescape(username, encodeUserPassword); err != nil {\n\t\t\treturn nil, \"\", err\n\t\t}\n\t\tif password, err = unescape(password, encodeUserPassword); err != nil {\n\t\t\treturn nil, \"\", err\n\t\t}\n\t\tuser = UserPassword(username, password)\n\t}\n\treturn user, host, nil\n}\n\n// parseHost parses host as an authority without user\n// information. That is, as host[:port].\nfunc parseHost(host string) (string, error) {\n\tif strings.HasPrefix(host, \"[\") {\n\t\t// Parse an IP-Literal in RFC 3986 and RFC 6874.\n\t\t// E.g., \"[fe80::1]\", \"[fe80::1%25en0]\", \"[fe80::1]:80\".\n\t\ti := strings.LastIndex(host, \"]\")\n\t\tif i < 0 {\n\t\t\treturn \"\", errors.New(\"missing ']' in host\")\n\t\t}\n\t\tcolonPort := host[i+1:]\n\t\tif !validOptionalPort(colonPort) {\n\t\t\treturn \"\", fmt.Errorf(\"invalid port %q after host\", colonPort)\n\t\t}\n\n\t\t// RFC 6874 defines that %25 (%-encoded percent) introduces\n\t\t// the zone identifier, and the zone identifier can use basically\n\t\t// any %-encoding it likes. That's different from the host, which\n\t\t// can only %-encode non-ASCII bytes.\n\t\t// We do impose some restrictions on the zone, to avoid stupidity\n\t\t// like newlines.\n\t\tzone := strings.Index(host[:i], \"%25\")\n\t\tif zone >= 0 {\n\t\t\thost1, err := unescape(host[:zone], encodeHost)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\thost2, err := unescape(host[zone:i], encodeZone)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\thost3, err := unescape(host[i:], encodeHost)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\treturn host1 + host2 + host3, nil\n\t\t}\n\t} else if i := strings.LastIndex(host, \":\"); i != -1 {\n\t\tcolonPort := host[i:]\n\t\tif !validOptionalPort(colonPort) {\n\t\t\treturn \"\", fmt.Errorf(\"invalid port %q after host\", colonPort)\n\t\t}\n\t}\n\n\tvar err error\n\tif host, err = unescape(host, encodeHost); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn host, nil\n}\n\n// setPath sets the Path and RawPath fields of the URL based on the provided\n// escaped path p. It maintains the invariant that RawPath is only specified\n// when it differs from the default encoding of the path.\n// For example:\n// - setPath(\"/foo/bar\")   will set Path=\"/foo/bar\" and RawPath=\"\"\n// - setPath(\"/foo%2fbar\") will set Path=\"/foo/bar\" and RawPath=\"/foo%2fbar\"\n// setPath will return an error only if the provided path contains an invalid\n// escaping.\nfunc (u *URL) setPath(p string) error {\n\tpath, err := unescape(p, encodePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tu.Path = path\n\tif escp := escape(path, encodePath); p == escp {\n\t\t// Default encoding is fine.\n\t\tu.RawPath = \"\"\n\t} else {\n\t\tu.RawPath = p\n\t}\n\treturn nil\n}\n\n// EscapedPath returns the escaped form of u.Path.\n// In general there are multiple possible escaped forms of any path.\n// EscapedPath returns u.RawPath when it is a valid escaping of u.Path.\n// Otherwise EscapedPath ignores u.RawPath and computes an escaped\n// form on its own.\n// The String and RequestURI methods use EscapedPath to construct\n// their results.\n// In general, code should call EscapedPath instead of\n// reading u.RawPath directly.\nfunc (u *URL) EscapedPath() string {\n\tif u.RawPath != \"\" && validEncoded(u.RawPath, encodePath) {\n\t\tp, err := unescape(u.RawPath, encodePath)\n\t\tif err == nil && p == u.Path {\n\t\t\treturn u.RawPath\n\t\t}\n\t}\n\tif u.Path == \"*\" {\n\t\treturn \"*\" // don't escape (Issue 11202)\n\t}\n\treturn escape(u.Path, encodePath)\n}\n\n// validEncoded reports whether s is a valid encoded path or fragment,\n// according to mode.\n// It must not contain any bytes that require escaping during encoding.\nfunc validEncoded(s string, mode encoding) bool {\n\tfor i := 0; i < len(s); i++ {\n\t\t// RFC 3986, Appendix A.\n\t\t// pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\".\n\t\t// shouldEscape is not quite compliant with the RFC,\n\t\t// so we check the sub-delims ourselves and let\n\t\t// shouldEscape handle the others.\n\t\tswitch s[i] {\n\t\tcase '!', '$', '&', '\\'', '(', ')', '*', '+', ',', ';', '=', ':', '@':\n\t\t\t// ok\n\t\tcase '[', ']':\n\t\t\t// ok - not specified in RFC 3986 but left alone by modern browsers\n\t\tcase '%':\n\t\t\t// ok - percent encoded, will decode\n\t\tdefault:\n\t\t\tif shouldEscape(s[i], mode) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n// setFragment is like setPath but for Fragment/RawFragment.\nfunc (u *URL) setFragment(f string) error {\n\tfrag, err := unescape(f, encodeFragment)\n\tif err != nil {\n\t\treturn err\n\t}\n\tu.Fragment = frag\n\tif escf := escape(frag, encodeFragment); f == escf {\n\t\t// Default encoding is fine.\n\t\tu.RawFragment = \"\"\n\t} else {\n\t\tu.RawFragment = f\n\t}\n\treturn nil\n}\n\n// EscapedFragment returns the escaped form of u.Fragment.\n// In general there are multiple possible escaped forms of any fragment.\n// EscapedFragment returns u.RawFragment when it is a valid escaping of u.Fragment.\n// Otherwise EscapedFragment ignores u.RawFragment and computes an escaped\n// form on its own.\n// The String method uses EscapedFragment to construct its result.\n// In general, code should call EscapedFragment instead of\n// reading u.RawFragment directly.\nfunc (u *URL) EscapedFragment() string {\n\tif u.RawFragment != \"\" && validEncoded(u.RawFragment, encodeFragment) {\n\t\tf, err := unescape(u.RawFragment, encodeFragment)\n\t\tif err == nil && f == u.Fragment {\n\t\t\treturn u.RawFragment\n\t\t}\n\t}\n\treturn escape(u.Fragment, encodeFragment)\n}\n\n// validOptionalPort reports whether port is either an empty string\n// or matches /^:\\d*$/\nfunc validOptionalPort(port string) bool {\n\tif port == \"\" {\n\t\treturn true\n\t}\n\tif port[0] != ':' {\n\t\treturn false\n\t}\n\tfor _, b := range port[1:] {\n\t\tif (b < '0' || b > '9') && (b != '-' && b != ',') {\n\t\t\t// Neither a digit nor a valid separator character.\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// String reassembles the URL into a valid URL string.\n// The general form of the result is one of:\n//\n//\tscheme:opaque?query#fragment\n//\tscheme://userinfo@host/path?query#fragment\n//\n// If u.Opaque is non-empty, String uses the first form;\n// otherwise it uses the second form.\n// Any non-ASCII characters in host are escaped.\n// To obtain the path, String uses u.EscapedPath().\n//\n// In the second form, the following rules apply:\n//   - if u.Scheme is empty, scheme: is omitted.\n//   - if u.User is nil, userinfo@ is omitted.\n//   - if u.Host is empty, host/ is omitted.\n//   - if u.Scheme and u.Host are empty and u.User is nil,\n//     the entire scheme://userinfo@host/ is omitted.\n//   - if u.Host is non-empty and u.Path begins with a /,\n//     the form host/path does not add its own /.\n//   - if u.RawQuery is empty, ?query is omitted.\n//   - if u.Fragment is empty, #fragment is omitted.\nfunc (u *URL) String() string {\n\tvar buf strings.Builder\n\tif u.Scheme != \"\" {\n\t\tbuf.WriteString(u.Scheme)\n\t\tbuf.WriteByte(':')\n\t}\n\tif u.Opaque != \"\" {\n\t\tbuf.WriteString(u.Opaque)\n\t} else {\n\t\tif u.Scheme != \"\" || u.Host != \"\" || u.User != nil {\n\t\t\tif u.OmitHost && u.Host == \"\" && u.User == nil {\n\t\t\t\t// omit empty host\n\t\t\t} else {\n\t\t\t\tif u.Host != \"\" || u.Path != \"\" || u.User != nil {\n\t\t\t\t\tbuf.WriteString(\"//\")\n\t\t\t\t}\n\t\t\t\tif ui := u.User; ui != nil {\n\t\t\t\t\tbuf.WriteString(ui.String())\n\t\t\t\t\tbuf.WriteByte('@')\n\t\t\t\t}\n\t\t\t\tif h := u.Host; h != \"\" {\n\t\t\t\t\tbuf.WriteString(escape(h, encodeHost))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tpath := u.EscapedPath()\n\t\tif path != \"\" && path[0] != '/' && u.Host != \"\" {\n\t\t\tbuf.WriteByte('/')\n\t\t}\n\t\tif buf.Len() == 0 {\n\t\t\t// RFC 3986 §4.2\n\t\t\t// A path segment that contains a colon character (e.g., \"this:that\")\n\t\t\t// cannot be used as the first segment of a relative-path reference, as\n\t\t\t// it would be mistaken for a scheme name. Such a segment must be\n\t\t\t// preceded by a dot-segment (e.g., \"./this:that\") to make a relative-\n\t\t\t// path reference.\n\t\t\tif segment, _, _ := strings.Cut(path, \"/\"); strings.Contains(segment, \":\") {\n\t\t\t\tbuf.WriteString(\"./\")\n\t\t\t}\n\t\t}\n\t\tbuf.WriteString(path)\n\t}\n\tif u.ForceQuery || u.RawQuery != \"\" {\n\t\tbuf.WriteByte('?')\n\t\tbuf.WriteString(u.RawQuery)\n\t}\n\tif u.Fragment != \"\" {\n\t\tbuf.WriteByte('#')\n\t\tbuf.WriteString(u.EscapedFragment())\n\t}\n\treturn buf.String()\n}\n\n// Redacted is like String but replaces any password with \"xxxxx\".\n// Only the password in u.User is redacted.\nfunc (u *URL) Redacted() string {\n\tif u == nil {\n\t\treturn \"\"\n\t}\n\n\tru := *u\n\tif _, has := ru.User.Password(); has {\n\t\tru.User = UserPassword(ru.User.Username(), \"xxxxx\")\n\t}\n\treturn ru.String()\n}\n\n// Values maps a string key to a list of values.\n// It is typically used for query parameters and form values.\n// Unlike in the http.Header map, the keys in a Values map\n// are case-sensitive.\ntype Values map[string][]string\n\n// Get gets the first value associated with the given key.\n// If there are no values associated with the key, Get returns\n// the empty string. To access multiple values, use the map\n// directly.\nfunc (v Values) Get(key string) string {\n\tvs := v[key]\n\tif len(vs) == 0 {\n\t\treturn \"\"\n\t}\n\treturn vs[0]\n}\n\n// Set sets the key to value. It replaces any existing\n// values.\nfunc (v Values) Set(key, value string) {\n\tv[key] = []string{value}\n}\n\n// Add adds the value to key. It appends to any existing\n// values associated with key.\nfunc (v Values) Add(key, value string) {\n\tv[key] = append(v[key], value)\n}\n\n// Del deletes the values associated with key.\nfunc (v Values) Del(key string) {\n\tdelete(v, key)\n}\n\n// Has checks whether a given key is set.\nfunc (v Values) Has(key string) bool {\n\t_, ok := v[key]\n\treturn ok\n}\n\n// ParseQuery parses the URL-encoded query string and returns\n// a map listing the values specified for each key.\n// ParseQuery always returns a non-nil map containing all the\n// valid query parameters found; err describes the first decoding error\n// encountered, if any.\n//\n// Query is expected to be a list of key=value settings separated by ampersands.\n// A setting without an equals sign is interpreted as a key set to an empty\n// value.\n// Settings containing a non-URL-encoded semicolon are considered invalid.\nfunc ParseQuery(query string) (Values, error) {\n\tm := make(Values)\n\terr := parseQuery(m, query)\n\treturn m, err\n}\n\nfunc parseQuery(m Values, query string) (err error) {\n\tfor query != \"\" {\n\t\tvar key string\n\t\tkey, query, _ = strings.Cut(query, \"&\")\n\t\tif strings.Contains(key, \";\") {\n\t\t\terr = fmt.Errorf(\"invalid semicolon separator in query\")\n\t\t\tcontinue\n\t\t}\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkey, value, _ := strings.Cut(key, \"=\")\n\t\tkey, err1 := QueryUnescape(key)\n\t\tif err1 != nil {\n\t\t\tif err == nil {\n\t\t\t\terr = err1\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tvalue, err1 = QueryUnescape(value)\n\t\tif err1 != nil {\n\t\t\tif err == nil {\n\t\t\t\terr = err1\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tm[key] = append(m[key], value)\n\t}\n\treturn err\n}\n\n// Encode encodes the values into “URL encoded” form\n// (\"bar=baz&foo=quux\") sorted by key.\nfunc (v Values) Encode() string {\n\tif v == nil {\n\t\treturn \"\"\n\t}\n\tvar buf strings.Builder\n\tkeys := make([]string, 0, len(v))\n\tfor k := range v {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\tfor _, k := range keys {\n\t\tvs := v[k]\n\t\tkeyEscaped := QueryEscape(k)\n\t\tfor _, v := range vs {\n\t\t\tif buf.Len() > 0 {\n\t\t\t\tbuf.WriteByte('&')\n\t\t\t}\n\t\t\tbuf.WriteString(keyEscaped)\n\t\t\tbuf.WriteByte('=')\n\t\t\tbuf.WriteString(QueryEscape(v))\n\t\t}\n\t}\n\treturn buf.String()\n}\n\n// resolvePath applies special path segments from refs and applies\n// them to base, per RFC 3986.\nfunc resolvePath(base, ref string) string {\n\tvar full string\n\tif ref == \"\" {\n\t\tfull = base\n\t} else if ref[0] != '/' {\n\t\ti := strings.LastIndex(base, \"/\")\n\t\tfull = base[:i+1] + ref\n\t} else {\n\t\tfull = ref\n\t}\n\tif full == \"\" {\n\t\treturn \"\"\n\t}\n\n\tvar (\n\t\telem string\n\t\tdst  strings.Builder\n\t)\n\tfirst := true\n\tremaining := full\n\t// We want to return a leading '/', so write it now.\n\tdst.WriteByte('/')\n\tfound := true\n\tfor found {\n\t\telem, remaining, found = strings.Cut(remaining, \"/\")\n\t\tif elem == \".\" {\n\t\t\tfirst = false\n\t\t\t// drop\n\t\t\tcontinue\n\t\t}\n\n\t\tif elem == \"..\" {\n\t\t\t// Ignore the leading '/' we already wrote.\n\t\t\tstr := dst.String()[1:]\n\t\t\tindex := strings.LastIndexByte(str, '/')\n\n\t\t\tdst.Reset()\n\t\t\tdst.WriteByte('/')\n\t\t\tif index == -1 {\n\t\t\t\tfirst = true\n\t\t\t} else {\n\t\t\t\tdst.WriteString(str[:index])\n\t\t\t}\n\t\t} else {\n\t\t\tif !first {\n\t\t\t\tdst.WriteByte('/')\n\t\t\t}\n\t\t\tdst.WriteString(elem)\n\t\t\tfirst = false\n\t\t}\n\t}\n\n\tif elem == \".\" || elem == \"..\" {\n\t\tdst.WriteByte('/')\n\t}\n\n\t// We wrote an initial '/', but we don't want two.\n\tr := dst.String()\n\tif len(r) > 1 && r[1] == '/' {\n\t\tr = r[1:]\n\t}\n\treturn r\n}\n\n// IsAbs reports whether the URL is absolute.\n// Absolute means that it has a non-empty scheme.\nfunc (u *URL) IsAbs() bool {\n\treturn u.Scheme != \"\"\n}\n\n// Parse parses a URL in the context of the receiver. The provided URL\n// may be relative or absolute. Parse returns nil, err on parse\n// failure, otherwise its return value is the same as ResolveReference.\nfunc (u *URL) Parse(ref string) (*URL, error) {\n\trefURL, err := Parse(ref)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn u.ResolveReference(refURL), nil\n}\n\n// ResolveReference resolves a URI reference to an absolute URI from\n// an absolute base URI u, per RFC 3986 Section 5.2. The URI reference\n// may be relative or absolute. ResolveReference always returns a new\n// URL instance, even if the returned URL is identical to either the\n// base or reference. If ref is an absolute URL, then ResolveReference\n// ignores base and returns a copy of ref.\nfunc (u *URL) ResolveReference(ref *URL) *URL {\n\turl := *ref\n\tif ref.Scheme == \"\" {\n\t\turl.Scheme = u.Scheme\n\t}\n\tif ref.Scheme != \"\" || ref.Host != \"\" || ref.User != nil {\n\t\t// The \"absoluteURI\" or \"net_path\" cases.\n\t\t// We can ignore the error from setPath since we know we provided a\n\t\t// validly-escaped path.\n\t\turl.setPath(resolvePath(ref.EscapedPath(), \"\"))\n\t\treturn &url\n\t}\n\tif ref.Opaque != \"\" {\n\t\turl.User = nil\n\t\turl.Host = \"\"\n\t\turl.Path = \"\"\n\t\treturn &url\n\t}\n\tif ref.Path == \"\" && !ref.ForceQuery && ref.RawQuery == \"\" {\n\t\turl.RawQuery = u.RawQuery\n\t\tif ref.Fragment == \"\" {\n\t\t\turl.Fragment = u.Fragment\n\t\t\turl.RawFragment = u.RawFragment\n\t\t}\n\t}\n\t// The \"abs_path\" or \"rel_path\" cases.\n\turl.Host = u.Host\n\turl.User = u.User\n\turl.setPath(resolvePath(u.EscapedPath(), ref.EscapedPath()))\n\treturn &url\n}\n\n// Query parses RawQuery and returns the corresponding values.\n// It silently discards malformed value pairs.\n// To check errors use ParseQuery.\nfunc (u *URL) Query() Values {\n\tv, _ := ParseQuery(u.RawQuery)\n\treturn v\n}\n\n// RequestURI returns the encoded path?query or opaque?query\n// string that would be used in an HTTP request for u.\nfunc (u *URL) RequestURI() string {\n\tresult := u.Opaque\n\tif result == \"\" {\n\t\tresult = u.EscapedPath()\n\t\tif result == \"\" {\n\t\t\tresult = \"/\"\n\t\t}\n\t} else {\n\t\tif strings.HasPrefix(result, \"//\") {\n\t\t\tresult = u.Scheme + \":\" + result\n\t\t}\n\t}\n\tif u.ForceQuery || u.RawQuery != \"\" {\n\t\tresult += \"?\" + u.RawQuery\n\t}\n\treturn result\n}\n\n// Hostname returns u.Host, stripping any valid port number if present.\n//\n// If the result is enclosed in square brackets, as literal IPv6 addresses are,\n// the square brackets are removed from the result.\nfunc (u *URL) Hostname() string {\n\thost, _ := splitHostPort(u.Host)\n\treturn host\n}\n\n// Port returns the port part of u.Host, without the leading colon.\n//\n// If u.Host doesn't contain a valid numeric port, Port returns an empty string.\nfunc (u *URL) Port() string {\n\t_, port := splitHostPort(u.Host)\n\treturn port\n}\n\n// splitHostPort separates host and port. If the port is not valid, it returns\n// the entire input as host, and it doesn't check the validity of the host.\n// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.\nfunc splitHostPort(hostPort string) (host, port string) {\n\thost = hostPort\n\n\tcolon := strings.LastIndexByte(host, ':')\n\tif colon != -1 && validOptionalPort(host[colon:]) {\n\t\thost, port = host[:colon], host[colon+1:]\n\t}\n\n\tif strings.HasPrefix(host, \"[\") && strings.HasSuffix(host, \"]\") {\n\t\thost = host[1 : len(host)-1]\n\t}\n\n\treturn\n}\n\n// Marshaling interface implementations.\n// Would like to implement MarshalText/UnmarshalText but that will change the JSON representation of URLs.\n\nfunc (u *URL) MarshalBinary() (text []byte, err error) {\n\treturn []byte(u.String()), nil\n}\n\nfunc (u *URL) UnmarshalBinary(text []byte) error {\n\tu1, err := Parse(string(text))\n\tif err != nil {\n\t\treturn err\n\t}\n\t*u = *u1\n\treturn nil\n}\n\n// JoinPath returns a new URL with the provided path elements joined to\n// any existing path and the resulting path cleaned of any ./ or ../ elements.\n// Any sequences of multiple / characters will be reduced to a single /.\nfunc (u *URL) JoinPath(elem ...string) *URL {\n\telem = append([]string{u.EscapedPath()}, elem...)\n\tvar p string\n\tif !strings.HasPrefix(elem[0], \"/\") {\n\t\t// Return a relative path if u is relative,\n\t\t// but ensure that it contains no ../ elements.\n\t\telem[0] = \"/\" + elem[0]\n\t\tp = path.Join(elem...)[1:]\n\t} else {\n\t\tp = path.Join(elem...)\n\t}\n\t// path.Join will remove any trailing slashes.\n\t// Preserve at least one.\n\tif strings.HasSuffix(elem[len(elem)-1], \"/\") && !strings.HasSuffix(p, \"/\") {\n\t\tp += \"/\"\n\t}\n\turl := *u\n\turl.setPath(p)\n\treturn &url\n}\n\n// validUserinfo reports whether s is a valid userinfo string per RFC 3986\n// Section 3.2.1:\n//\n//\tuserinfo    = *( unreserved / pct-encoded / sub-delims / \":\" )\n//\tunreserved  = ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"\n//\tsub-delims  = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\"\n//\t              / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n//\n// It doesn't validate pct-encoded. The caller does that via func unescape.\nfunc validUserinfo(s string) bool {\n\tfor _, r := range s {\n\t\tif 'A' <= r && r <= 'Z' {\n\t\t\tcontinue\n\t\t}\n\t\tif 'a' <= r && r <= 'z' {\n\t\t\tcontinue\n\t\t}\n\t\tif '0' <= r && r <= '9' {\n\t\t\tcontinue\n\t\t}\n\t\tswitch r {\n\t\tcase '-', '.', '_', ':', '~', '!', '$', '&', '\\'',\n\t\t\t'(', ')', '*', '+', ',', ';', '=', '%', '@':\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// stringContainsCTLByte reports whether s contains any ASCII control character.\nfunc stringContainsCTLByte(s string) bool {\n\tfor i := 0; i < len(s); i++ {\n\t\tb := s[i]\n\t\tif b < ' ' || b == 0x7f {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// JoinPath returns a URL string with the provided path elements joined to\n// the existing path of base and the resulting path cleaned of any ./ or ../ elements.\nfunc JoinPath(base string, elem ...string) (result string, err error) {\n\turl, err := Parse(base)\n\tif err != nil {\n\t\treturn\n\t}\n\tresult = url.JoinPath(elem...).String()\n\treturn\n}\n"
  },
  {
    "path": "app/internal/url/url_test.go",
    "content": "package url\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttype args struct {\n\t\trawURL string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *URL\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"no port\",\n\t\t\targs: args{\n\t\t\t\trawURL: \"hysteria2://ganggang@icecreamsogood/\",\n\t\t\t},\n\t\t\twant: &URL{\n\t\t\t\tScheme: \"hysteria2\",\n\t\t\t\tUser:   User(\"ganggang\"),\n\t\t\t\tHost:   \"icecreamsogood\",\n\t\t\t\tPath:   \"/\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single port\",\n\t\t\targs: args{\n\t\t\t\trawURL: \"hysteria2://yesyes@icecreamsogood:8888/\",\n\t\t\t},\n\t\t\twant: &URL{\n\t\t\t\tScheme: \"hysteria2\",\n\t\t\t\tUser:   User(\"yesyes\"),\n\t\t\t\tHost:   \"icecreamsogood:8888\",\n\t\t\t\tPath:   \"/\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multi port\",\n\t\t\targs: args{\n\t\t\t\trawURL: \"hysteria2://darkness@laplus.org:8888,9999,11111/\",\n\t\t\t},\n\t\t\twant: &URL{\n\t\t\t\tScheme: \"hysteria2\",\n\t\t\t\tUser:   User(\"darkness\"),\n\t\t\t\tHost:   \"laplus.org:8888,9999,11111\",\n\t\t\t\tPath:   \"/\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"range port\",\n\t\t\targs: args{\n\t\t\t\trawURL: \"hysteria2://darkness@laplus.org:8888-9999/\",\n\t\t\t},\n\t\t\twant: &URL{\n\t\t\t\tScheme: \"hysteria2\",\n\t\t\t\tUser:   User(\"darkness\"),\n\t\t\t\tHost:   \"laplus.org:8888-9999\",\n\t\t\t\tPath:   \"/\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"both\",\n\t\t\targs: args{\n\t\t\t\trawURL: \"hysteria2://gawr:gura@atlantis.moe:443,7788-8899,10010/\",\n\t\t\t},\n\t\t\twant: &URL{\n\t\t\t\tScheme: \"hysteria2\",\n\t\t\t\tUser:   UserPassword(\"gawr\", \"gura\"),\n\t\t\t\tHost:   \"atlantis.moe:443,7788-8899,10010\",\n\t\t\t\tPath:   \"/\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := Parse(tt.args.rawURL)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Parse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Parse() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/internal/utils/bpsconv.go",
    "content": "package utils\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tByte     = 1\n\tKilobyte = Byte * 1000\n\tMegabyte = Kilobyte * 1000\n\tGigabyte = Megabyte * 1000\n\tTerabyte = Gigabyte * 1000\n)\n\n// StringToBps converts a string to a bandwidth value in bytes per second.\n// E.g. \"100 Mbps\", \"512 kbps\", \"1g\" are all valid.\nfunc StringToBps(s string) (uint64, error) {\n\ts = strings.ToLower(strings.TrimSpace(s))\n\tspl := 0\n\tfor i, c := range s {\n\t\tif c < '0' || c > '9' {\n\t\t\tspl = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif spl == 0 {\n\t\t// No unit or no value\n\t\treturn 0, errors.New(\"invalid format\")\n\t}\n\tv, err := strconv.ParseUint(s[:spl], 10, 64)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tunit := strings.TrimSpace(s[spl:])\n\n\tswitch strings.ToLower(unit) {\n\tcase \"b\", \"bps\":\n\t\treturn v * Byte / 8, nil\n\tcase \"k\", \"kb\", \"kbps\":\n\t\treturn v * Kilobyte / 8, nil\n\tcase \"m\", \"mb\", \"mbps\":\n\t\treturn v * Megabyte / 8, nil\n\tcase \"g\", \"gb\", \"gbps\":\n\t\treturn v * Gigabyte / 8, nil\n\tcase \"t\", \"tb\", \"tbps\":\n\t\treturn v * Terabyte / 8, nil\n\tdefault:\n\t\treturn 0, errors.New(\"unsupported unit\")\n\t}\n}\n\n// ConvBandwidth handles both string and int types for bandwidth.\n// When using string, it will be parsed as a bandwidth string with units.\n// When using int, it will be parsed as a raw bandwidth in bytes per second.\n// It does NOT support float types.\nfunc ConvBandwidth(bw interface{}) (uint64, error) {\n\tswitch bwT := bw.(type) {\n\tcase string:\n\t\treturn StringToBps(bwT)\n\tcase int:\n\t\treturn uint64(bwT), nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"invalid type %T for bandwidth\", bwT)\n\t}\n}\n"
  },
  {
    "path": "app/internal/utils/bpsconv_test.go",
    "content": "package utils\n\nimport \"testing\"\n\nfunc TestStringToBps(t *testing.T) {\n\ttype args struct {\n\t\ts string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    uint64\n\t\twantErr bool\n\t}{\n\t\t{\"bps\", args{\"800 bps\"}, 100, false},\n\t\t{\"kbps\", args{\"800 kbps\"}, 100_000, false},\n\t\t{\"mbps\", args{\"800 mbps\"}, 100_000_000, false},\n\t\t{\"gbps\", args{\"800 gbps\"}, 100_000_000_000, false},\n\t\t{\"tbps\", args{\"800 tbps\"}, 100_000_000_000_000, false},\n\t\t{\"mbps simp\", args{\"100m\"}, 12_500_000, false},\n\t\t{\"gbps simp upper\", args{\"2G\"}, 250_000_000, false},\n\t\t{\"invalid 1\", args{\"damn\"}, 0, true},\n\t\t{\"invalid 2\", args{\"6444\"}, 0, true},\n\t\t{\"invalid 3\", args{\"5.4 mbps\"}, 0, true},\n\t\t{\"invalid 4\", args{\"kbps\"}, 0, true},\n\t\t{\"invalid 5\", args{\"1234 5678 gbps\"}, 0, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := StringToBps(tt.args.s)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"StringToBps() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"StringToBps() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/internal/utils/certloader.go",
    "content": "package utils\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype LocalCertificateLoader struct {\n\tCertFile string\n\tKeyFile  string\n\tSNIGuard SNIGuardFunc\n\n\tlock  sync.Mutex\n\tcache atomic.Pointer[localCertificateCache]\n}\n\ntype SNIGuardFunc func(info *tls.ClientHelloInfo, cert *tls.Certificate) error\n\n// localCertificateCache holds the certificate and its mod times.\n// this struct is designed to be read-only.\n//\n// to update the cache, use LocalCertificateLoader.makeCache and\n// update the LocalCertificateLoader.cache field.\ntype localCertificateCache struct {\n\tcertificate *tls.Certificate\n\tcertModTime time.Time\n\tkeyModTime  time.Time\n}\n\nfunc (l *LocalCertificateLoader) InitializeCache() error {\n\tl.lock.Lock()\n\tdefer l.lock.Unlock()\n\n\tcache, err := l.makeCache()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tl.cache.Store(cache)\n\treturn nil\n}\n\nfunc (l *LocalCertificateLoader) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\tcert, err := l.getCertificateWithCache()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif l.SNIGuard == nil {\n\t\treturn cert, nil\n\t}\n\terr = l.SNIGuard(info, cert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cert, nil\n}\n\nfunc (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Time, err error) {\n\tfi, err := os.Stat(l.CertFile)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"failed to stat certificate file: %w\", err)\n\t\treturn\n\t}\n\tcertModTime = fi.ModTime()\n\n\tfi, err = os.Stat(l.KeyFile)\n\tif err != nil {\n\t\terr = fmt.Errorf(\"failed to stat key file: %w\", err)\n\t\treturn\n\t}\n\tkeyModTime = fi.ModTime()\n\treturn\n}\n\nfunc (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) {\n\tc := &localCertificateCache{}\n\n\tc.certModTime, c.keyModTime, err = l.checkModTime()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile)\n\tif err != nil {\n\t\treturn\n\t}\n\tc.certificate = &cert\n\tif c.certificate.Leaf == nil {\n\t\t// certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23\n\t\tc.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0])\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\tcache = c\n\treturn\n}\n\nfunc (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) {\n\tcache := l.cache.Load()\n\n\tcertModTime, keyModTime, terr := l.checkModTime()\n\tif terr != nil {\n\t\tif cache != nil {\n\t\t\t// use cache when file is temporarily unavailable\n\t\t\treturn cache.certificate, nil\n\t\t}\n\t\treturn nil, terr\n\t}\n\n\tif cache != nil && cache.certModTime.Equal(certModTime) && cache.keyModTime.Equal(keyModTime) {\n\t\t// cache is up-to-date\n\t\treturn cache.certificate, nil\n\t}\n\n\tif cache != nil {\n\t\tif !l.lock.TryLock() {\n\t\t\t// another goroutine is updating the cache\n\t\t\treturn cache.certificate, nil\n\t\t}\n\t} else {\n\t\tl.lock.Lock()\n\t}\n\tdefer l.lock.Unlock()\n\n\tif l.cache.Load() != cache {\n\t\t// another goroutine updated the cache\n\t\treturn l.cache.Load().certificate, nil\n\t}\n\n\tnewCache, err := l.makeCache()\n\tif err != nil {\n\t\tif cache != nil {\n\t\t\t// use cache when loading failed\n\t\t\treturn cache.certificate, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tl.cache.Store(newCache)\n\treturn newCache.certificate, nil\n}\n\n// getNameFromClientHello returns a normalized form of hello.ServerName.\n// If hello.ServerName is empty (i.e. client did not use SNI), then the\n// associated connection's local address is used to extract an IP address.\n//\n// ref: https://github.com/caddyserver/certmagic/blob/3bad5b6bb595b09c14bd86ff0b365d302faaf5e2/handshake.go#L838\nfunc getNameFromClientHello(hello *tls.ClientHelloInfo) string {\n\tnormalizedName := func(serverName string) string {\n\t\treturn strings.ToLower(strings.TrimSpace(serverName))\n\t}\n\tlocalIPFromConn := func(c net.Conn) string {\n\t\tif c == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tlocalAddr := c.LocalAddr().String()\n\t\tip, _, err := net.SplitHostPort(localAddr)\n\t\tif err != nil {\n\t\t\tip = localAddr\n\t\t}\n\t\tif scopeIDStart := strings.Index(ip, \"%\"); scopeIDStart > -1 {\n\t\t\tip = ip[:scopeIDStart]\n\t\t}\n\t\treturn ip\n\t}\n\n\tif name := normalizedName(hello.ServerName); name != \"\" {\n\t\treturn name\n\t}\n\treturn localIPFromConn(hello.Conn)\n}\n\nfunc SNIGuardDNSSAN(info *tls.ClientHelloInfo, cert *tls.Certificate) error {\n\tif len(cert.Leaf.DNSNames) == 0 {\n\t\treturn nil\n\t}\n\treturn SNIGuardStrict(info, cert)\n}\n\nfunc SNIGuardStrict(info *tls.ClientHelloInfo, cert *tls.Certificate) error {\n\thostname := getNameFromClientHello(info)\n\terr := cert.Leaf.VerifyHostname(hostname)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"sni guard: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "app/internal/utils/certloader_test.go",
    "content": "package utils\n\nimport (\n\t\"crypto/tls\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst (\n\ttestListen   = \"127.82.39.147:12947\"\n\ttestCAFile   = \"./testcerts/ca\"\n\ttestCertFile = \"./testcerts/cert\"\n\ttestKeyFile  = \"./testcerts/key\"\n)\n\nfunc TestCertificateLoaderPathError(t *testing.T) {\n\tassert.NoError(t, os.RemoveAll(testCertFile))\n\tassert.NoError(t, os.RemoveAll(testKeyFile))\n\tloader := LocalCertificateLoader{\n\t\tCertFile: testCertFile,\n\t\tKeyFile:  testKeyFile,\n\t\tSNIGuard: SNIGuardStrict,\n\t}\n\terr := loader.InitializeCache()\n\tvar pathErr *os.PathError\n\tassert.ErrorAs(t, err, &pathErr)\n}\n\nfunc TestCertificateLoaderFullChain(t *testing.T) {\n\tassert.NoError(t, generateTestCertificate([]string{\"example.com\"}, \"fullchain\"))\n\n\tloader := LocalCertificateLoader{\n\t\tCertFile: testCertFile,\n\t\tKeyFile:  testKeyFile,\n\t\tSNIGuard: SNIGuardStrict,\n\t}\n\tassert.NoError(t, loader.InitializeCache())\n\n\tlis, err := tls.Listen(\"tcp\", testListen, &tls.Config{\n\t\tGetCertificate: loader.GetCertificate,\n\t})\n\tassert.NoError(t, err)\n\tdefer lis.Close()\n\tgo http.Serve(lis, nil)\n\n\tassert.Error(t, runTestTLSClient(\"unmatched-sni.example.com\"))\n\tassert.Error(t, runTestTLSClient(\"\"))\n\tassert.NoError(t, runTestTLSClient(\"example.com\"))\n}\n\nfunc TestCertificateLoaderNoSAN(t *testing.T) {\n\tassert.NoError(t, generateTestCertificate(nil, \"selfsign\"))\n\n\tloader := LocalCertificateLoader{\n\t\tCertFile: testCertFile,\n\t\tKeyFile:  testKeyFile,\n\t\tSNIGuard: SNIGuardDNSSAN,\n\t}\n\tassert.NoError(t, loader.InitializeCache())\n\n\tlis, err := tls.Listen(\"tcp\", testListen, &tls.Config{\n\t\tGetCertificate: loader.GetCertificate,\n\t})\n\tassert.NoError(t, err)\n\tdefer lis.Close()\n\tgo http.Serve(lis, nil)\n\n\tassert.NoError(t, runTestTLSClient(\"\"))\n}\n\nfunc TestCertificateLoaderReplaceCertificate(t *testing.T) {\n\tassert.NoError(t, generateTestCertificate([]string{\"example.com\"}, \"fullchain\"))\n\n\tloader := LocalCertificateLoader{\n\t\tCertFile: testCertFile,\n\t\tKeyFile:  testKeyFile,\n\t\tSNIGuard: SNIGuardStrict,\n\t}\n\tassert.NoError(t, loader.InitializeCache())\n\n\tlis, err := tls.Listen(\"tcp\", testListen, &tls.Config{\n\t\tGetCertificate: loader.GetCertificate,\n\t})\n\tassert.NoError(t, err)\n\tdefer lis.Close()\n\tgo http.Serve(lis, nil)\n\n\tassert.NoError(t, runTestTLSClient(\"example.com\"))\n\tassert.Error(t, runTestTLSClient(\"2.example.com\"))\n\n\tassert.NoError(t, generateTestCertificate([]string{\"2.example.com\"}, \"fullchain\"))\n\n\tassert.Error(t, runTestTLSClient(\"example.com\"))\n\tassert.NoError(t, runTestTLSClient(\"2.example.com\"))\n}\n\nfunc generateTestCertificate(dnssan []string, certType string) error {\n\targs := []string{\n\t\t\"certloader_test_gencert.py\",\n\t\t\"--ca\", testCAFile,\n\t\t\"--cert\", testCertFile,\n\t\t\"--key\", testKeyFile,\n\t\t\"--type\", certType,\n\t}\n\tif len(dnssan) > 0 {\n\t\targs = append(args, \"--dnssan\", strings.Join(dnssan, \",\"))\n\t}\n\tcmd := exec.Command(\"python\", args...)\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"Failed to generate test certificate: %s\", out)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc runTestTLSClient(sni string) error {\n\targs := []string{\n\t\t\"certloader_test_tlsclient.py\",\n\t\t\"--server\", testListen,\n\t\t\"--ca\", testCAFile,\n\t}\n\tif sni != \"\" {\n\t\targs = append(args, \"--sni\", sni)\n\t}\n\tcmd := exec.Command(\"python\", args...)\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"Failed to run test TLS client: %s\", out)\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "app/internal/utils/certloader_test_gencert.py",
    "content": "import argparse\nimport datetime\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption\n\n\ndef create_key():\n    return ec.generate_private_key(ec.SECP256R1())\n\n\ndef create_certificate(cert_type, subject, issuer, private_key, public_key, dns_san=None):\n    serial_number = x509.random_serial_number()\n    not_valid_before = datetime.datetime.now(datetime.UTC)\n    not_valid_after = not_valid_before + datetime.timedelta(days=365)\n\n    subject_name = x509.Name([\n        x509.NameAttribute(NameOID.COUNTRY_NAME, subject.get('C', 'ZZ')),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject.get('O', 'No Organization')),\n        x509.NameAttribute(NameOID.COMMON_NAME, subject.get('CN', 'No CommonName')),\n    ])\n    issuer_name = x509.Name([\n        x509.NameAttribute(NameOID.COUNTRY_NAME, issuer.get('C', 'ZZ')),\n        x509.NameAttribute(NameOID.ORGANIZATION_NAME, issuer.get('O', 'No Organization')),\n        x509.NameAttribute(NameOID.COMMON_NAME, issuer.get('CN', 'No CommonName')),\n    ])\n    builder = x509.CertificateBuilder()\n    builder = builder.subject_name(subject_name)\n    builder = builder.issuer_name(issuer_name)\n    builder = builder.public_key(public_key)\n    builder = builder.serial_number(serial_number)\n    builder = builder.not_valid_before(not_valid_before)\n    builder = builder.not_valid_after(not_valid_after)\n    if cert_type == 'root':\n        builder = builder.add_extension(\n            x509.BasicConstraints(ca=True, path_length=None), critical=True\n        )\n    elif cert_type == 'intermediate':\n        builder = builder.add_extension(\n            x509.BasicConstraints(ca=True, path_length=0), critical=True\n        )\n    elif cert_type == 'leaf':\n        builder = builder.add_extension(\n            x509.BasicConstraints(ca=False, path_length=None), critical=True\n        )\n    else:\n        raise ValueError(f'Invalid cert_type: {cert_type}')\n    if dns_san:\n        builder = builder.add_extension(\n            x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_san.split(',')]),\n            critical=False\n        )\n    return builder.sign(private_key=private_key, algorithm=hashes.SHA256())\n\n\ndef main():\n    parser = argparse.ArgumentParser(description='Generate HTTPS server certificate.')\n    parser.add_argument('--ca', required=True,\n                        help='Path to write the X509 CA certificate in PEM format')\n    parser.add_argument('--cert', required=True,\n                        help='Path to write the X509 certificate in PEM format')\n    parser.add_argument('--key', required=True,\n                        help='Path to write the private key in PEM format')\n    parser.add_argument('--dnssan', required=False, default=None,\n                        help='Comma-separated list of DNS SANs')\n    parser.add_argument('--type', required=True, choices=['selfsign', 'fullchain'],\n                        help='Type of certificate to generate')\n\n    args = parser.parse_args()\n\n    key = create_key()\n    public_key = key.public_key()\n\n    if args.type == 'selfsign':\n        subject = {\"C\": \"ZZ\", \"O\": \"Certificate\", \"CN\": \"Certificate\"}\n        cert = create_certificate(\n            cert_type='root',\n            subject=subject,\n            issuer=subject,\n            private_key=key,\n            public_key=public_key,\n            dns_san=args.dnssan)\n        with open(args.ca, 'wb') as f:\n            f.write(cert.public_bytes(Encoding.PEM))\n        with open(args.cert, 'wb') as f:\n            f.write(cert.public_bytes(Encoding.PEM))\n        with open(args.key, 'wb') as f:\n            f.write(\n                key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))\n\n    elif args.type == 'fullchain':\n        ca_key = create_key()\n        ca_public_key = ca_key.public_key()\n        ca_subject = {\"C\": \"ZZ\", \"O\": \"Root CA\", \"CN\": \"Root CA\"}\n        ca_cert = create_certificate(\n            cert_type='root',\n            subject=ca_subject,\n            issuer=ca_subject,\n            private_key=ca_key,\n            public_key=ca_public_key)\n\n        intermediate_key = create_key()\n        intermediate_public_key = intermediate_key.public_key()\n        intermediate_subject = {\"C\": \"ZZ\", \"O\": \"Intermediate CA\", \"CN\": \"Intermediate CA\"}\n        intermediate_cert = create_certificate(\n            cert_type='intermediate',\n            subject=intermediate_subject,\n            issuer=ca_subject,\n            private_key=ca_key,\n            public_key=intermediate_public_key)\n\n        leaf_subject = {\"C\": \"ZZ\", \"O\": \"Leaf Certificate\", \"CN\": \"Leaf Certificate\"}\n        cert = create_certificate(\n            cert_type='leaf',\n            subject=leaf_subject,\n            issuer=intermediate_subject,\n            private_key=intermediate_key,\n            public_key=public_key,\n            dns_san=args.dnssan)\n\n        with open(args.ca, 'wb') as f:\n            f.write(ca_cert.public_bytes(Encoding.PEM))\n        with open(args.cert, 'wb') as f:\n            f.write(cert.public_bytes(Encoding.PEM))\n            f.write(intermediate_cert.public_bytes(Encoding.PEM))\n        with open(args.key, 'wb') as f:\n            f.write(\n                key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "app/internal/utils/certloader_test_tlsclient.py",
    "content": "import argparse\nimport ssl\nimport socket\nimport sys\n\n\ndef check_tls(server, ca_cert, sni, alpn):\n    try:\n        host, port = server.split(\":\")\n        port = int(port)\n\n        if ca_cert:\n            context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_cert)\n            context.check_hostname = sni is not None\n            context.verify_mode = ssl.CERT_REQUIRED\n        else:\n            context = ssl.create_default_context()\n            context.check_hostname = False\n            context.verify_mode = ssl.CERT_NONE\n\n        if alpn:\n            context.set_alpn_protocols([p for p in alpn.split(\",\")])\n\n        with socket.create_connection((host, port)) as sock:\n            with context.wrap_socket(sock, server_hostname=sni) as ssock:\n                # Verify handshake and certificate\n                print(f'Connected to {ssock.version()} using {ssock.cipher()}')\n                print(f'Server certificate validated and details: {ssock.getpeercert()}')\n                print(\"OK\")\n                return 0\n    except Exception as e:\n        print(f\"Error: {e}\")\n        return 1\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Test TLS Server\")\n    parser.add_argument(\"--server\", required=True,\n                        help=\"Server address to test (e.g., 127.1.2.3:8443)\")\n    parser.add_argument(\"--ca\", required=False, default=None,\n                        help=\"CA certificate file used to validate the server certificate\"\n                        \"Omit to use insecure connection\")\n    parser.add_argument(\"--sni\", required=False, default=None,\n                        help=\"SNI to send in ClientHello\")\n    parser.add_argument(\"--alpn\", required=False, default='h2',\n                        help=\"ALPN to send in ClientHello\")\n\n    args = parser.parse_args()\n\n    exit_status = check_tls(\n        server=args.server,\n        ca_cert=args.ca,\n        sni=args.sni,\n        alpn=args.alpn)\n\n    sys.exit(exit_status)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "app/internal/utils/geoloader.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/acl\"\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo\"\n)\n\nconst (\n\tgeoipFilename   = \"geoip.dat\"\n\tgeoipURL        = \"https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat\"\n\tgeositeFilename = \"geosite.dat\"\n\tgeositeURL      = \"https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat\"\n\tgeoDlTmpPattern = \".hysteria-geoloader.dlpart.*\"\n\n\tgeoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days\n)\n\nvar _ acl.GeoLoader = (*GeoLoader)(nil)\n\n// GeoLoader provides the on-demand GeoIP/GeoSite database\n// loading functionality required by the ACL engine.\n// Empty filenames = automatic download from built-in URLs.\ntype GeoLoader struct {\n\tGeoIPFilename   string\n\tGeoSiteFilename string\n\tUpdateInterval  time.Duration\n\n\tDownloadFunc    func(filename, url string)\n\tDownloadErrFunc func(err error)\n\n\tgeoipMap   map[string]*v2geo.GeoIP\n\tgeositeMap map[string]*v2geo.GeoSite\n}\n\nfunc (l *GeoLoader) shouldDownload(filename string) bool {\n\tinfo, err := os.Stat(filename)\n\tif os.IsNotExist(err) {\n\t\treturn true\n\t}\n\tif info.Size() == 0 {\n\t\t// empty files are loadable by v2geo, but we consider it broken\n\t\treturn true\n\t}\n\tdt := time.Now().Sub(info.ModTime())\n\tif l.UpdateInterval == 0 {\n\t\treturn dt > geoDefaultUpdateInterval\n\t} else {\n\t\treturn dt > l.UpdateInterval\n\t}\n}\n\nfunc (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error {\n\tl.DownloadFunc(filename, url)\n\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\tl.DownloadErrFunc(err)\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tf, err := os.CreateTemp(\".\", geoDlTmpPattern)\n\tif err != nil {\n\t\tl.DownloadErrFunc(err)\n\t\treturn err\n\t}\n\tdefer os.Remove(f.Name())\n\n\t_, err = io.Copy(f, resp.Body)\n\tif err != nil {\n\t\tf.Close()\n\t\tl.DownloadErrFunc(err)\n\t\treturn err\n\t}\n\tf.Close()\n\n\terr = checkFunc(f.Name())\n\tif err != nil {\n\t\tl.DownloadErrFunc(fmt.Errorf(\"integrity check failed: %w\", err))\n\t\treturn err\n\t}\n\n\terr = os.Rename(f.Name(), filename)\n\tif err != nil {\n\t\tl.DownloadErrFunc(fmt.Errorf(\"rename failed: %w\", err))\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {\n\tif l.geoipMap != nil {\n\t\treturn l.geoipMap, nil\n\t}\n\tautoDL := false\n\tfilename := l.GeoIPFilename\n\tif filename == \"\" {\n\t\tautoDL = true\n\t\tfilename = geoipFilename\n\t}\n\tif autoDL {\n\t\tif !l.shouldDownload(filename) {\n\t\t\tm, err := v2geo.LoadGeoIP(filename)\n\t\t\tif err == nil {\n\t\t\t\tl.geoipMap = m\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\t// file is broken, download it again\n\t\t}\n\t\terr := l.downloadAndCheck(filename, geoipURL, func(filename string) error {\n\t\t\t_, err := v2geo.LoadGeoIP(filename)\n\t\t\treturn err\n\t\t})\n\t\tif err != nil {\n\t\t\t// as long as the previous download exists, fallback to it\n\t\t\tif _, serr := os.Stat(filename); os.IsNotExist(serr) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\tm, err := v2geo.LoadGeoIP(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl.geoipMap = m\n\treturn m, nil\n}\n\nfunc (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {\n\tif l.geositeMap != nil {\n\t\treturn l.geositeMap, nil\n\t}\n\tautoDL := false\n\tfilename := l.GeoSiteFilename\n\tif filename == \"\" {\n\t\tautoDL = true\n\t\tfilename = geositeFilename\n\t}\n\tif autoDL {\n\t\tif !l.shouldDownload(filename) {\n\t\t\tm, err := v2geo.LoadGeoSite(filename)\n\t\t\tif err == nil {\n\t\t\t\tl.geositeMap = m\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\t// file is broken, download it again\n\t\t}\n\t\terr := l.downloadAndCheck(filename, geositeURL, func(filename string) error {\n\t\t\t_, err := v2geo.LoadGeoSite(filename)\n\t\t\treturn err\n\t\t})\n\t\tif err != nil {\n\t\t\t// as long as the previous download exists, fallback to it\n\t\t\tif _, serr := os.Stat(filename); os.IsNotExist(serr) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\tm, err := v2geo.LoadGeoSite(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl.geositeMap = m\n\treturn m, nil\n}\n"
  },
  {
    "path": "app/internal/utils/qr.go",
    "content": "package utils\n\nimport (\n\t\"os\"\n\n\t\"github.com/mdp/qrterminal/v3\"\n)\n\nfunc PrintQR(str string) {\n\tqrterminal.GenerateWithConfig(str, qrterminal.Config{\n\t\tLevel:     qrterminal.L,\n\t\tWriter:    os.Stdout,\n\t\tBlackChar: qrterminal.BLACK,\n\t\tWhiteChar: qrterminal.WHITE,\n\t})\n}\n"
  },
  {
    "path": "app/internal/utils/testcerts/.gitignore",
    "content": "# This directory is used for certificate generation in certloader_test.go\n/*\n!/.gitignore\n"
  },
  {
    "path": "app/internal/utils/update.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\nconst (\n\tupdateCheckEndpoint = \"https://api.hy2.io/v1/update\"\n\tupdateCheckTimeout  = 10 * time.Second\n)\n\ntype UpdateChecker struct {\n\tCurrentVersion string\n\tPlatform       string\n\tArchitecture   string\n\tChannel        string\n\tSide           string\n\tClient         *http.Client\n}\n\nfunc NewServerUpdateChecker(currentVersion, platform, architecture, channel string) *UpdateChecker {\n\treturn &UpdateChecker{\n\t\tCurrentVersion: currentVersion,\n\t\tPlatform:       platform,\n\t\tArchitecture:   architecture,\n\t\tChannel:        channel,\n\t\tSide:           \"server\",\n\t\tClient: &http.Client{\n\t\t\tTimeout: updateCheckTimeout,\n\t\t},\n\t}\n}\n\n// NewClientUpdateChecker ensures that update checks are routed through a HyClient,\n// not being sent directly. This safeguard is CRITICAL, especially in scenarios where\n// users use Hysteria to bypass censorship. Making direct HTTPS requests to the API\n// endpoint could be easily spotted by censors (through SNI, for example), and could\n// serve as a signal to identify and penalize Hysteria users.\nfunc NewClientUpdateChecker(currentVersion, platform, architecture, channel string, hyClient client.Client) *UpdateChecker {\n\treturn &UpdateChecker{\n\t\tCurrentVersion: currentVersion,\n\t\tPlatform:       platform,\n\t\tArchitecture:   architecture,\n\t\tChannel:        channel,\n\t\tSide:           \"client\",\n\t\tClient: &http.Client{\n\t\t\tTimeout: updateCheckTimeout,\n\t\t\tTransport: &http.Transport{\n\t\t\t\tDialContext: func(_ context.Context, network, addr string) (net.Conn, error) {\n\t\t\t\t\t// Unfortunately HyClient doesn't support context for now\n\t\t\t\t\treturn hyClient.TCP(addr)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\ntype UpdateResponse struct {\n\tHasUpdate     bool   `json:\"update\"`\n\tLatestVersion string `json:\"lver\"`\n\tURL           string `json:\"url\"`\n\tUrgent        bool   `json:\"urgent\"`\n}\n\nfunc (uc *UpdateChecker) Check() (*UpdateResponse, error) {\n\turl := fmt.Sprintf(\"%s?cver=%s&plat=%s&arch=%s&chan=%s&side=%s\",\n\t\tupdateCheckEndpoint,\n\t\tuc.CurrentVersion,\n\t\tuc.Platform,\n\t\tuc.Architecture,\n\t\tuc.Channel,\n\t\tuc.Side,\n\t)\n\tresp, err := uc.Client.Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\tvar uResp UpdateResponse\n\tdecoder := json.NewDecoder(resp.Body)\n\tif err := decoder.Decode(&uResp); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &uResp, nil\n}\n"
  },
  {
    "path": "app/internal/utils_test/mock.go",
    "content": "package utils_test\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n)\n\ntype MockEchoHyClient struct{}\n\nfunc (c *MockEchoHyClient) TCP(addr string) (net.Conn, error) {\n\treturn &mockEchoTCPConn{\n\t\tBufChan: make(chan []byte, 10),\n\t}, nil\n}\n\nfunc (c *MockEchoHyClient) UDP() (client.HyUDPConn, error) {\n\treturn &mockEchoUDPConn{\n\t\tBufChan: make(chan mockEchoUDPPacket, 10),\n\t}, nil\n}\n\nfunc (c *MockEchoHyClient) Close() error {\n\treturn nil\n}\n\ntype mockEchoTCPConn struct {\n\tBufChan chan []byte\n}\n\nfunc (c *mockEchoTCPConn) Read(b []byte) (n int, err error) {\n\tbuf := <-c.BufChan\n\tif buf == nil {\n\t\t// EOF\n\t\treturn 0, io.EOF\n\t}\n\treturn copy(b, buf), nil\n}\n\nfunc (c *mockEchoTCPConn) Write(b []byte) (n int, err error) {\n\tc.BufChan <- b\n\treturn len(b), nil\n}\n\nfunc (c *mockEchoTCPConn) Close() error {\n\tclose(c.BufChan)\n\treturn nil\n}\n\nfunc (c *mockEchoTCPConn) LocalAddr() net.Addr {\n\t// Not implemented\n\treturn nil\n}\n\nfunc (c *mockEchoTCPConn) RemoteAddr() net.Addr {\n\t// Not implemented\n\treturn nil\n}\n\nfunc (c *mockEchoTCPConn) SetDeadline(t time.Time) error {\n\t// Not implemented\n\treturn nil\n}\n\nfunc (c *mockEchoTCPConn) SetReadDeadline(t time.Time) error {\n\t// Not implemented\n\treturn nil\n}\n\nfunc (c *mockEchoTCPConn) SetWriteDeadline(t time.Time) error {\n\t// Not implemented\n\treturn nil\n}\n\ntype mockEchoUDPPacket struct {\n\tData []byte\n\tAddr string\n}\n\ntype mockEchoUDPConn struct {\n\tBufChan chan mockEchoUDPPacket\n}\n\nfunc (c *mockEchoUDPConn) Receive() ([]byte, string, error) {\n\tp := <-c.BufChan\n\tif p.Data == nil {\n\t\t// EOF\n\t\treturn nil, \"\", io.EOF\n\t}\n\treturn p.Data, p.Addr, nil\n}\n\nfunc (c *mockEchoUDPConn) Send(bytes []byte, s string) error {\n\tc.BufChan <- mockEchoUDPPacket{\n\t\tData: bytes,\n\t\tAddr: s,\n\t}\n\treturn nil\n}\n\nfunc (c *mockEchoUDPConn) Close() error {\n\tclose(c.BufChan)\n\treturn nil\n}\n"
  },
  {
    "path": "app/main.go",
    "content": "package main\n\nimport \"github.com/apernet/hysteria/app/v2/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "app/misc/socks5_test.py",
    "content": "import socket\nimport socks\nimport time\n\nTARGET = \"1.1.1.1\"\n\n\ndef test_tcp() -> None:\n    s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)\n    s.set_proxy(socks.SOCKS5, \"127.0.0.1\", 1080)\n\n    print(f\"TCP - Sending HTTP request to {TARGET}\")\n    start = time.time()\n    s.connect((TARGET, 80))\n    s.send(b\"GET / HTTP/1.1\\r\\nHost: \" + TARGET.encode() + b\"\\r\\n\\r\\n\")\n    data = s.recv(1024)\n    if not data:\n        print(\"No data received\")\n    elif not data.startswith(b\"HTTP/1.1 \"):\n        print(\"Invalid response received\")\n    else:\n        print(\"TCP test passed\")\n    end = time.time()\n    s.close()\n\n    print(f\"Time: {round((end - start) * 1000, 2)} ms\")\n\n\ndef test_udp() -> None:\n    s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)\n    s.set_proxy(socks.SOCKS5, \"127.0.0.1\", 1080)\n\n    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\"\n    print(f\"UDP - Sending DNS request to {TARGET}\")\n    start = time.time()\n    s.sendto(req, (TARGET, 53))\n    (rsp, address) = s.recvfrom(4096)\n    if address[0] == TARGET and address[1] == 53 and rsp[0] == req[0] and rsp[1] == req[1]:\n        print(\"UDP test passed\")\n    else:\n        print(\"Invalid response received\")\n    end = time.time()\n    s.close()\n\n    print(f\"Time: {round((end - start) * 1000, 2)} ms\")\n\n\nif __name__ == \"__main__\":\n    test_tcp()\n    test_udp()\n"
  },
  {
    "path": "app/pprof.go",
    "content": "//go:build pprof\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t_ \"net/http/pprof\"\n)\n\nconst (\n\tpprofListenAddr = \":6060\"\n)\n\nfunc init() {\n\tfmt.Printf(\"!!! pprof enabled, listening on %s\\n\", pprofListenAddr)\n\tgo func() {\n\t\tif err := http.ListenAndServe(pprofListenAddr, nil); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "core/client/.mockery.yaml",
    "content": "with-expecter: true\ninpackage: true\ndir: .\npackages:\n  github.com/apernet/hysteria/core/v2/client:\n    interfaces:\n      udpIO:\n        config:\n          mockname: mockUDPIO\n"
  },
  {
    "path": "core/client/client.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\tcoreErrs \"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/core/v2/internal/congestion\"\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n\t\"github.com/apernet/hysteria/core/v2/internal/utils\"\n\n\t\"github.com/apernet/quic-go\"\n\t\"github.com/apernet/quic-go/http3\"\n)\n\nconst (\n\tcloseErrCodeOK            = 0x100 // HTTP3 ErrCodeNoError\n\tcloseErrCodeProtocolError = 0x101 // HTTP3 ErrCodeGeneralProtocolError\n)\n\ntype Client interface {\n\tTCP(addr string) (net.Conn, error)\n\tUDP() (HyUDPConn, error)\n\tClose() error\n}\n\ntype HyUDPConn interface {\n\tReceive() ([]byte, string, error)\n\tSend([]byte, string) error\n\tClose() error\n}\n\ntype HandshakeInfo struct {\n\tUDPEnabled bool\n\tTx         uint64 // 0 if using BBR\n}\n\nfunc NewClient(config *Config) (Client, *HandshakeInfo, error) {\n\tif err := config.verifyAndFill(); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tc := &clientImpl{\n\t\tconfig: config,\n\t}\n\tinfo, err := c.connect()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn c, info, nil\n}\n\ntype clientImpl struct {\n\tconfig *Config\n\n\tpktConn net.PacketConn\n\tconn    quic.Connection\n\n\tudpSM *udpSessionManager\n}\n\nfunc (c *clientImpl) connect() (*HandshakeInfo, error) {\n\tpktConn, err := c.config.ConnFactory.New(c.config.ServerAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Convert config to TLS config & QUIC config\n\ttlsConfig := &tls.Config{\n\t\tServerName:            c.config.TLSConfig.ServerName,\n\t\tInsecureSkipVerify:    c.config.TLSConfig.InsecureSkipVerify,\n\t\tVerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate,\n\t\tRootCAs:               c.config.TLSConfig.RootCAs,\n\t}\n\tquicConfig := &quic.Config{\n\t\tInitialStreamReceiveWindow:     c.config.QUICConfig.InitialStreamReceiveWindow,\n\t\tMaxStreamReceiveWindow:         c.config.QUICConfig.MaxStreamReceiveWindow,\n\t\tInitialConnectionReceiveWindow: c.config.QUICConfig.InitialConnectionReceiveWindow,\n\t\tMaxConnectionReceiveWindow:     c.config.QUICConfig.MaxConnectionReceiveWindow,\n\t\tMaxIdleTimeout:                 c.config.QUICConfig.MaxIdleTimeout,\n\t\tKeepAlivePeriod:                c.config.QUICConfig.KeepAlivePeriod,\n\t\tDisablePathMTUDiscovery:        c.config.QUICConfig.DisablePathMTUDiscovery,\n\t\tEnableDatagrams:                true,\n\t}\n\t// Prepare RoundTripper\n\tvar conn quic.EarlyConnection\n\trt := &http3.RoundTripper{\n\t\tTLSClientConfig: tlsConfig,\n\t\tQUICConfig:      quicConfig,\n\t\tDial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {\n\t\t\tqc, err := quic.DialEarly(ctx, pktConn, c.config.ServerAddr, tlsCfg, cfg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tconn = qc\n\t\t\treturn qc, nil\n\t\t},\n\t}\n\t// Send auth HTTP request\n\treq := &http.Request{\n\t\tMethod: http.MethodPost,\n\t\tURL: &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   protocol.URLHost,\n\t\t\tPath:   protocol.URLPath,\n\t\t},\n\t\tHeader: make(http.Header),\n\t}\n\tprotocol.AuthRequestToHeader(req.Header, protocol.AuthRequest{\n\t\tAuth: c.config.Auth,\n\t\tRx:   c.config.BandwidthConfig.MaxRx,\n\t})\n\tresp, err := rt.RoundTrip(req)\n\tif err != nil {\n\t\tif conn != nil {\n\t\t\t_ = conn.CloseWithError(closeErrCodeProtocolError, \"\")\n\t\t}\n\t\t_ = pktConn.Close()\n\t\treturn nil, coreErrs.ConnectError{Err: err}\n\t}\n\tif resp.StatusCode != protocol.StatusAuthOK {\n\t\t_ = conn.CloseWithError(closeErrCodeProtocolError, \"\")\n\t\t_ = pktConn.Close()\n\t\treturn nil, coreErrs.AuthError{StatusCode: resp.StatusCode}\n\t}\n\t// Auth OK\n\tauthResp := protocol.AuthResponseFromHeader(resp.Header)\n\tvar actualTx uint64\n\tif authResp.RxAuto {\n\t\t// Server asks client to use bandwidth detection,\n\t\t// ignore local bandwidth config and use BBR\n\t\tcongestion.UseBBR(conn)\n\t} else {\n\t\t// actualTx = min(serverRx, clientTx)\n\t\tactualTx = authResp.Rx\n\t\tif actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx {\n\t\t\t// Server doesn't have a limit, or our clientTx is smaller than serverRx\n\t\t\tactualTx = c.config.BandwidthConfig.MaxTx\n\t\t}\n\t\tif actualTx > 0 {\n\t\t\tcongestion.UseBrutal(conn, actualTx)\n\t\t} else {\n\t\t\t// We don't know our own bandwidth either, use BBR\n\t\t\tcongestion.UseBBR(conn)\n\t\t}\n\t}\n\t_ = resp.Body.Close()\n\n\tc.pktConn = pktConn\n\tc.conn = conn\n\tif authResp.UDPEnabled {\n\t\tc.udpSM = newUDPSessionManager(&udpIOImpl{Conn: conn})\n\t}\n\treturn &HandshakeInfo{\n\t\tUDPEnabled: authResp.UDPEnabled,\n\t\tTx:         actualTx,\n\t}, nil\n}\n\n// openStream wraps the stream with QStream, which handles Close() properly\nfunc (c *clientImpl) openStream() (quic.Stream, error) {\n\tstream, err := c.conn.OpenStream()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &utils.QStream{Stream: stream}, nil\n}\n\nfunc (c *clientImpl) TCP(addr string) (net.Conn, error) {\n\tstream, err := c.openStream()\n\tif err != nil {\n\t\treturn nil, wrapIfConnectionClosed(err)\n\t}\n\t// Send request\n\terr = protocol.WriteTCPRequest(stream, addr)\n\tif err != nil {\n\t\t_ = stream.Close()\n\t\treturn nil, wrapIfConnectionClosed(err)\n\t}\n\tif c.config.FastOpen {\n\t\t// Don't wait for the response when fast open is enabled.\n\t\t// Return the connection immediately, defer the response handling\n\t\t// to the first Read() call.\n\t\treturn &tcpConn{\n\t\t\tOrig:             stream,\n\t\t\tPseudoLocalAddr:  c.conn.LocalAddr(),\n\t\t\tPseudoRemoteAddr: c.conn.RemoteAddr(),\n\t\t\tEstablished:      false,\n\t\t}, nil\n\t}\n\t// Read response\n\tok, msg, err := protocol.ReadTCPResponse(stream)\n\tif err != nil {\n\t\t_ = stream.Close()\n\t\treturn nil, wrapIfConnectionClosed(err)\n\t}\n\tif !ok {\n\t\t_ = stream.Close()\n\t\treturn nil, coreErrs.DialError{Message: msg}\n\t}\n\treturn &tcpConn{\n\t\tOrig:             stream,\n\t\tPseudoLocalAddr:  c.conn.LocalAddr(),\n\t\tPseudoRemoteAddr: c.conn.RemoteAddr(),\n\t\tEstablished:      true,\n\t}, nil\n}\n\nfunc (c *clientImpl) UDP() (HyUDPConn, error) {\n\tif c.udpSM == nil {\n\t\treturn nil, coreErrs.DialError{Message: \"UDP not enabled\"}\n\t}\n\treturn c.udpSM.NewUDP()\n}\n\nfunc (c *clientImpl) Close() error {\n\t_ = c.conn.CloseWithError(closeErrCodeOK, \"\")\n\t_ = c.pktConn.Close()\n\treturn nil\n}\n\n// wrapIfConnectionClosed checks if the error returned by quic-go\n// indicates that the QUIC connection has been permanently closed,\n// and if so, wraps the error with coreErrs.ClosedError.\n// PITFALL: sometimes quic-go has \"internal errors\" that are not net.Error,\n// but we still need to treat them as ClosedError.\nfunc wrapIfConnectionClosed(err error) error {\n\tnetErr, ok := err.(net.Error)\n\tif !ok || !netErr.Temporary() {\n\t\treturn coreErrs.ClosedError{Err: err}\n\t} else {\n\t\treturn err\n\t}\n}\n\ntype tcpConn struct {\n\tOrig             quic.Stream\n\tPseudoLocalAddr  net.Addr\n\tPseudoRemoteAddr net.Addr\n\tEstablished      bool\n}\n\nfunc (c *tcpConn) Read(b []byte) (n int, err error) {\n\tif !c.Established {\n\t\t// Read response\n\t\tok, msg, err := protocol.ReadTCPResponse(c.Orig)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif !ok {\n\t\t\treturn 0, coreErrs.DialError{Message: msg}\n\t\t}\n\t\tc.Established = true\n\t}\n\treturn c.Orig.Read(b)\n}\n\nfunc (c *tcpConn) Write(b []byte) (n int, err error) {\n\treturn c.Orig.Write(b)\n}\n\nfunc (c *tcpConn) Close() error {\n\treturn c.Orig.Close()\n}\n\nfunc (c *tcpConn) LocalAddr() net.Addr {\n\treturn c.PseudoLocalAddr\n}\n\nfunc (c *tcpConn) RemoteAddr() net.Addr {\n\treturn c.PseudoRemoteAddr\n}\n\nfunc (c *tcpConn) SetDeadline(t time.Time) error {\n\treturn c.Orig.SetDeadline(t)\n}\n\nfunc (c *tcpConn) SetReadDeadline(t time.Time) error {\n\treturn c.Orig.SetReadDeadline(t)\n}\n\nfunc (c *tcpConn) SetWriteDeadline(t time.Time) error {\n\treturn c.Orig.SetWriteDeadline(t)\n}\n\ntype udpIOImpl struct {\n\tConn quic.Connection\n}\n\nfunc (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {\n\tfor {\n\t\tmsg, err := io.Conn.ReceiveDatagram(context.Background())\n\t\tif err != nil {\n\t\t\t// Connection error, this will stop the session manager\n\t\t\treturn nil, err\n\t\t}\n\t\tudpMsg, err := protocol.ParseUDPMessage(msg)\n\t\tif err != nil {\n\t\t\t// Invalid message, this is fine - just wait for the next\n\t\t\tcontinue\n\t\t}\n\t\treturn udpMsg, nil\n\t}\n}\n\nfunc (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {\n\tmsgN := msg.Serialize(buf)\n\tif msgN < 0 {\n\t\t// Message larger than buffer, silent drop\n\t\treturn nil\n\t}\n\treturn io.Conn.SendDatagram(buf[:msgN])\n}\n"
  },
  {
    "path": "core/client/config.go",
    "content": "package client\n\nimport (\n\t\"crypto/x509\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/core/v2/internal/pmtud\"\n)\n\nconst (\n\tdefaultStreamReceiveWindow = 8388608                            // 8MB\n\tdefaultConnReceiveWindow   = defaultStreamReceiveWindow * 5 / 2 // 20MB\n\tdefaultMaxIdleTimeout      = 30 * time.Second\n\tdefaultKeepAlivePeriod     = 10 * time.Second\n)\n\ntype Config struct {\n\tConnFactory     ConnFactory\n\tServerAddr      net.Addr\n\tAuth            string\n\tTLSConfig       TLSConfig\n\tQUICConfig      QUICConfig\n\tBandwidthConfig BandwidthConfig\n\tFastOpen        bool\n\n\tfilled bool // whether the fields have been verified and filled\n}\n\n// verifyAndFill fills the fields that are not set by the user with default values when possible,\n// and returns an error if the user has not set a required field or has set an invalid value.\nfunc (c *Config) verifyAndFill() error {\n\tif c.filled {\n\t\treturn nil\n\t}\n\tif c.ConnFactory == nil {\n\t\tc.ConnFactory = &udpConnFactory{}\n\t}\n\tif c.ServerAddr == nil {\n\t\treturn errors.ConfigError{Field: \"ServerAddr\", Reason: \"must be set\"}\n\t}\n\tif c.QUICConfig.InitialStreamReceiveWindow == 0 {\n\t\tc.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow\n\t} else if c.QUICConfig.InitialStreamReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.InitialStreamReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.MaxStreamReceiveWindow == 0 {\n\t\tc.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow\n\t} else if c.QUICConfig.MaxStreamReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.MaxStreamReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.InitialConnectionReceiveWindow == 0 {\n\t\tc.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow\n\t} else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.InitialConnectionReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.MaxConnectionReceiveWindow == 0 {\n\t\tc.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow\n\t} else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.MaxConnectionReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.MaxIdleTimeout == 0 {\n\t\tc.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout\n\t} else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.MaxIdleTimeout\", Reason: \"must be between 4s and 120s\"}\n\t}\n\tif c.QUICConfig.KeepAlivePeriod == 0 {\n\t\tc.QUICConfig.KeepAlivePeriod = defaultKeepAlivePeriod\n\t} else if c.QUICConfig.KeepAlivePeriod < 2*time.Second || c.QUICConfig.KeepAlivePeriod > 60*time.Second {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.KeepAlivePeriod\", Reason: \"must be between 2s and 60s\"}\n\t}\n\tc.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery\n\n\tc.filled = true\n\treturn nil\n}\n\ntype ConnFactory interface {\n\tNew(net.Addr) (net.PacketConn, error)\n}\n\ntype udpConnFactory struct{}\n\nfunc (f *udpConnFactory) New(addr net.Addr) (net.PacketConn, error) {\n\treturn net.ListenUDP(\"udp\", nil)\n}\n\n// TLSConfig contains the TLS configuration fields that we want to expose to the user.\ntype TLSConfig struct {\n\tServerName            string\n\tInsecureSkipVerify    bool\n\tVerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error\n\tRootCAs               *x509.CertPool\n}\n\n// QUICConfig contains the QUIC configuration fields that we want to expose to the user.\ntype QUICConfig struct {\n\tInitialStreamReceiveWindow     uint64\n\tMaxStreamReceiveWindow         uint64\n\tInitialConnectionReceiveWindow uint64\n\tMaxConnectionReceiveWindow     uint64\n\tMaxIdleTimeout                 time.Duration\n\tKeepAlivePeriod                time.Duration\n\tDisablePathMTUDiscovery        bool // The server may still override this to true on unsupported platforms.\n}\n\n// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second.\ntype BandwidthConfig struct {\n\tMaxTx uint64\n\tMaxRx uint64\n}\n"
  },
  {
    "path": "core/client/mock_udpIO.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage client\n\nimport (\n\tprotocol \"github.com/apernet/hysteria/core/v2/internal/protocol\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// mockUDPIO is an autogenerated mock type for the udpIO type\ntype mockUDPIO struct {\n\tmock.Mock\n}\n\ntype mockUDPIO_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter {\n\treturn &mockUDPIO_Expecter{mock: &_m.Mock}\n}\n\n// ReceiveMessage provides a mock function with given fields:\nfunc (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for ReceiveMessage\")\n\t}\n\n\tvar r0 *protocol.UDPMessage\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok {\n\t\treturn rf()\n\t}\n\tif rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*protocol.UDPMessage)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = rf()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage'\ntype mockUDPIO_ReceiveMessage_Call struct {\n\t*mock.Call\n}\n\n// ReceiveMessage is a helper method to define mock.On call\nfunc (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call {\n\treturn &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On(\"ReceiveMessage\")}\n}\n\nfunc (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SendMessage provides a mock function with given fields: _a0, _a1\nfunc (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error {\n\tret := _m.Called(_a0, _a1)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SendMessage\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok {\n\t\tr0 = rf(_a0, _a1)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage'\ntype mockUDPIO_SendMessage_Call struct {\n\t*mock.Call\n}\n\n// SendMessage is a helper method to define mock.On call\n//   - _a0 []byte\n//   - _a1 *protocol.UDPMessage\nfunc (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call {\n\treturn &mockUDPIO_SendMessage_Call{Call: _e.mock.On(\"SendMessage\", _a0, _a1)}\n}\n\nfunc (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte), args[1].(*protocol.UDPMessage))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc newMockUDPIO(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *mockUDPIO {\n\tmock := &mockUDPIO{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/client/reconnect.go",
    "content": "package client\n\nimport (\n\t\"net\"\n\t\"sync\"\n\n\tcoreErrs \"github.com/apernet/hysteria/core/v2/errors\"\n)\n\n// reconnectableClientImpl is a wrapper of Client, which can reconnect when the connection is closed,\n// except when the caller explicitly calls Close() to permanently close this client.\ntype reconnectableClientImpl struct {\n\tconfigFunc    func() (*Config, error)           // called before connecting\n\tconnectedFunc func(Client, *HandshakeInfo, int) // called when successfully connected\n\tclient        Client\n\tcount         int\n\tm             sync.Mutex\n\tclosed        bool // permanent close\n}\n\n// NewReconnectableClient creates a reconnectable client.\n// If lazy is true, the client will not connect until the first call to TCP() or UDP().\n// We use a function for config mainly to delay config evaluation\n// (which involves DNS resolution) until the actual connection attempt.\nfunc NewReconnectableClient(configFunc func() (*Config, error), connectedFunc func(Client, *HandshakeInfo, int), lazy bool) (Client, error) {\n\trc := &reconnectableClientImpl{\n\t\tconfigFunc:    configFunc,\n\t\tconnectedFunc: connectedFunc,\n\t}\n\tif !lazy {\n\t\tif err := rc.reconnect(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn rc, nil\n}\n\nfunc (rc *reconnectableClientImpl) reconnect() error {\n\tif rc.client != nil {\n\t\t_ = rc.client.Close()\n\t}\n\tvar info *HandshakeInfo\n\tconfig, err := rc.configFunc()\n\tif err != nil {\n\t\treturn err\n\t}\n\trc.client, info, err = NewClient(config)\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\trc.count++\n\t\tif rc.connectedFunc != nil {\n\t\t\trc.connectedFunc(rc, info, rc.count)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// clientDo calls f with the current client.\n// If the client is nil, it will first reconnect.\n// It will also detect if the client is closed, and if so,\n// set it to nil for reconnect next time.\nfunc (rc *reconnectableClientImpl) clientDo(f func(Client) (interface{}, error)) (interface{}, error) {\n\trc.m.Lock()\n\tif rc.closed {\n\t\trc.m.Unlock()\n\t\treturn nil, coreErrs.ClosedError{}\n\t}\n\tif rc.client == nil {\n\t\t// No active connection, connect first\n\t\tif err := rc.reconnect(); err != nil {\n\t\t\trc.m.Unlock()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tclient := rc.client\n\trc.m.Unlock()\n\n\tret, err := f(client)\n\tif _, ok := err.(coreErrs.ClosedError); ok {\n\t\t// Connection closed, set client to nil for reconnect next time\n\t\trc.m.Lock()\n\t\tif rc.client == client {\n\t\t\t// This check is in case the client is already changed by another goroutine\n\t\t\trc.client = nil\n\t\t}\n\t\trc.m.Unlock()\n\t}\n\treturn ret, err\n}\n\nfunc (rc *reconnectableClientImpl) TCP(addr string) (net.Conn, error) {\n\tif c, err := rc.clientDo(func(client Client) (interface{}, error) {\n\t\treturn client.TCP(addr)\n\t}); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn c.(net.Conn), nil\n\t}\n}\n\nfunc (rc *reconnectableClientImpl) UDP() (HyUDPConn, error) {\n\tif c, err := rc.clientDo(func(client Client) (interface{}, error) {\n\t\treturn client.UDP()\n\t}); err != nil {\n\t\treturn nil, err\n\t} else {\n\t\treturn c.(HyUDPConn), nil\n\t}\n}\n\nfunc (rc *reconnectableClientImpl) Close() error {\n\trc.m.Lock()\n\tdefer rc.m.Unlock()\n\trc.closed = true\n\tif rc.client != nil {\n\t\treturn rc.client.Close()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/client/udp.go",
    "content": "package client\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"math/rand\"\n\t\"sync\"\n\n\t\"github.com/apernet/quic-go\"\n\n\tcoreErrs \"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/core/v2/internal/frag\"\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n)\n\nconst (\n\tudpMessageChanSize = 1024\n)\n\ntype udpIO interface {\n\tReceiveMessage() (*protocol.UDPMessage, error)\n\tSendMessage([]byte, *protocol.UDPMessage) error\n}\n\ntype udpConn struct {\n\tID        uint32\n\tD         *frag.Defragger\n\tReceiveCh chan *protocol.UDPMessage\n\tSendBuf   []byte\n\tSendFunc  func([]byte, *protocol.UDPMessage) error\n\tCloseFunc func()\n\tClosed    bool\n}\n\nfunc (u *udpConn) Receive() ([]byte, string, error) {\n\tfor {\n\t\tmsg := <-u.ReceiveCh\n\t\tif msg == nil {\n\t\t\t// Closed\n\t\t\treturn nil, \"\", io.EOF\n\t\t}\n\t\tdfMsg := u.D.Feed(msg)\n\t\tif dfMsg == nil {\n\t\t\t// Incomplete message, wait for more\n\t\t\tcontinue\n\t\t}\n\t\treturn dfMsg.Data, dfMsg.Addr, nil\n\t}\n}\n\n// Send is not thread-safe, as it uses a shared SendBuf.\nfunc (u *udpConn) Send(data []byte, addr string) error {\n\t// Try no frag first\n\tmsg := &protocol.UDPMessage{\n\t\tSessionID: u.ID,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      addr,\n\t\tData:      data,\n\t}\n\terr := u.SendFunc(u.SendBuf, msg)\n\tvar errTooLarge *quic.DatagramTooLargeError\n\tif errors.As(err, &errTooLarge) {\n\t\t// Message too large, try fragmentation\n\t\tmsg.PacketID = uint16(rand.Intn(0xFFFF)) + 1\n\t\tfMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDataLen))\n\t\tfor _, fMsg := range fMsgs {\n\t\t\terr := u.SendFunc(u.SendBuf, &fMsg)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t} else {\n\t\treturn err\n\t}\n}\n\nfunc (u *udpConn) Close() error {\n\tu.CloseFunc()\n\treturn nil\n}\n\ntype udpSessionManager struct {\n\tio udpIO\n\n\tmutex  sync.RWMutex\n\tm      map[uint32]*udpConn\n\tnextID uint32\n\n\tclosed bool\n}\n\nfunc newUDPSessionManager(io udpIO) *udpSessionManager {\n\tm := &udpSessionManager{\n\t\tio:     io,\n\t\tm:      make(map[uint32]*udpConn),\n\t\tnextID: 1,\n\t}\n\tgo m.run()\n\treturn m\n}\n\nfunc (m *udpSessionManager) run() error {\n\tdefer m.closeCleanup()\n\tfor {\n\t\tmsg, err := m.io.ReceiveMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.feed(msg)\n\t}\n}\n\nfunc (m *udpSessionManager) closeCleanup() {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\tfor _, conn := range m.m {\n\t\tm.close(conn)\n\t}\n\tm.closed = true\n}\n\nfunc (m *udpSessionManager) feed(msg *protocol.UDPMessage) {\n\tm.mutex.RLock()\n\tdefer m.mutex.RUnlock()\n\n\tconn, ok := m.m[msg.SessionID]\n\tif !ok {\n\t\t// Ignore message from unknown session\n\t\treturn\n\t}\n\n\tselect {\n\tcase conn.ReceiveCh <- msg:\n\t\t// OK\n\tdefault:\n\t\t// Channel full, drop the message\n\t}\n}\n\n// NewUDP creates a new UDP session.\nfunc (m *udpSessionManager) NewUDP() (HyUDPConn, error) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\tif m.closed {\n\t\treturn nil, coreErrs.ClosedError{}\n\t}\n\n\tid := m.nextID\n\tm.nextID++\n\n\tconn := &udpConn{\n\t\tID:        id,\n\t\tD:         &frag.Defragger{},\n\t\tReceiveCh: make(chan *protocol.UDPMessage, udpMessageChanSize),\n\t\tSendBuf:   make([]byte, protocol.MaxUDPSize),\n\t\tSendFunc:  m.io.SendMessage,\n\t}\n\tconn.CloseFunc = func() {\n\t\tm.mutex.Lock()\n\t\tdefer m.mutex.Unlock()\n\t\tm.close(conn)\n\t}\n\tm.m[id] = conn\n\n\treturn conn, nil\n}\n\nfunc (m *udpSessionManager) close(conn *udpConn) {\n\tif !conn.Closed {\n\t\tconn.Closed = true\n\t\tclose(conn.ReceiveCh)\n\t\tdelete(m.m, conn.ID)\n\t}\n}\n\nfunc (m *udpSessionManager) Count() int {\n\tm.mutex.RLock()\n\tdefer m.mutex.RUnlock()\n\treturn len(m.m)\n}\n"
  },
  {
    "path": "core/client/udp_test.go",
    "content": "package client\n\nimport (\n\t\"errors\"\n\tio2 \"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"go.uber.org/goleak\"\n\n\tcoreErrs \"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n)\n\nfunc TestUDPSessionManager(t *testing.T) {\n\tio := newMockUDPIO(t)\n\treceiveCh := make(chan *protocol.UDPMessage, 4)\n\tio.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) {\n\t\tm := <-receiveCh\n\t\tif m == nil {\n\t\t\treturn nil, errors.New(\"closed\")\n\t\t}\n\t\treturn m, nil\n\t})\n\tsm := newUDPSessionManager(io)\n\n\t// Test UDP session IO\n\tudpConn1, err := sm.NewUDP()\n\tassert.NoError(t, err)\n\tudpConn2, err := sm.NewUDP()\n\tassert.NoError(t, err)\n\n\tmsg1 := &protocol.UDPMessage{\n\t\tSessionID: 1,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      \"random.site.com:9000\",\n\t\tData:      []byte(\"hello friend\"),\n\t}\n\tio.EXPECT().SendMessage(mock.Anything, msg1).Return(nil).Once()\n\terr = udpConn1.Send(msg1.Data, msg1.Addr)\n\tassert.NoError(t, err)\n\n\tmsg2 := &protocol.UDPMessage{\n\t\tSessionID: 2,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      \"another.site.org:8000\",\n\t\tData:      []byte(\"mr robot\"),\n\t}\n\tio.EXPECT().SendMessage(mock.Anything, msg2).Return(nil).Once()\n\terr = udpConn2.Send(msg2.Data, msg2.Addr)\n\tassert.NoError(t, err)\n\n\trespMsg1 := &protocol.UDPMessage{\n\t\tSessionID: 1,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      msg1.Addr,\n\t\tData:      []byte(\"goodbye captain price\"),\n\t}\n\treceiveCh <- respMsg1\n\tdata, addr, err := udpConn1.Receive()\n\tassert.NoError(t, err)\n\tassert.Equal(t, data, respMsg1.Data)\n\tassert.Equal(t, addr, respMsg1.Addr)\n\n\trespMsg2 := &protocol.UDPMessage{\n\t\tSessionID: 2,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      msg2.Addr,\n\t\tData:      []byte(\"white rose\"),\n\t}\n\treceiveCh <- respMsg2\n\tdata, addr, err = udpConn2.Receive()\n\tassert.NoError(t, err)\n\tassert.Equal(t, data, respMsg2.Data)\n\tassert.Equal(t, addr, respMsg2.Addr)\n\n\trespMsg3 := &protocol.UDPMessage{\n\t\tSessionID: 55, // Bogus session ID that doesn't exist\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      \"burgerking.com:27017\",\n\t\tData:      []byte(\"impossible whopper\"),\n\t}\n\treceiveCh <- respMsg3\n\t// No test for this, just make sure it doesn't panic\n\n\t// Test close UDP connection unblocks Receive()\n\terrChan := make(chan error, 1)\n\tgo func() {\n\t\t_, _, err := udpConn1.Receive()\n\t\terrChan <- err\n\t}()\n\tassert.NoError(t, udpConn1.Close())\n\tassert.Equal(t, <-errChan, io2.EOF)\n\n\t// Test close IO unblocks Receive() and blocks new UDP creation\n\terrChan = make(chan error, 1)\n\tgo func() {\n\t\t_, _, err := udpConn2.Receive()\n\t\terrChan <- err\n\t}()\n\tclose(receiveCh)\n\tassert.Equal(t, <-errChan, io2.EOF)\n\t_, err = sm.NewUDP()\n\tassert.Equal(t, err, coreErrs.ClosedError{})\n\n\t// Leak checks\n\ttime.Sleep(1 * time.Second)\n\tassert.Zero(t, sm.Count(), \"session count should be 0\")\n\tgoleak.VerifyNone(t)\n}\n"
  },
  {
    "path": "core/errors/errors.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n)\n\n// ConfigError is returned when a configuration field is invalid.\ntype ConfigError struct {\n\tField  string\n\tReason string\n}\n\nfunc (c ConfigError) Error() string {\n\treturn fmt.Sprintf(\"invalid config: %s: %s\", c.Field, c.Reason)\n}\n\n// ConnectError is returned when the client fails to connect to the server.\ntype ConnectError struct {\n\tErr error\n}\n\nfunc (c ConnectError) Error() string {\n\treturn \"connect error: \" + c.Err.Error()\n}\n\nfunc (c ConnectError) Unwrap() error {\n\treturn c.Err\n}\n\n// AuthError is returned when the client fails to authenticate with the server.\ntype AuthError struct {\n\tStatusCode int\n}\n\nfunc (a AuthError) Error() string {\n\treturn \"authentication error, HTTP status code: \" + strconv.Itoa(a.StatusCode)\n}\n\n// DialError is returned when the server rejects the client's dial request.\n// This applies to both TCP and UDP.\ntype DialError struct {\n\tMessage string\n}\n\nfunc (c DialError) Error() string {\n\treturn \"dial error: \" + c.Message\n}\n\n// ClosedError is returned when the client attempts to use a closed connection.\ntype ClosedError struct {\n\tErr error // Can be nil\n}\n\nfunc (c ClosedError) Error() string {\n\tif c.Err == nil {\n\t\treturn \"connection closed\"\n\t} else {\n\t\treturn \"connection closed: \" + c.Err.Error()\n\t}\n}\n\nfunc (c ClosedError) Unwrap() error {\n\treturn c.Err\n}\n\n// ProtocolError is returned when the server/client runs into an unexpected\n// or malformed request/response/message.\ntype ProtocolError struct {\n\tMessage string\n}\n\nfunc (p ProtocolError) Error() string {\n\treturn \"protocol error: \" + p.Message\n}\n"
  },
  {
    "path": "core/go.mod",
    "content": "module github.com/apernet/hysteria/core/v2\n\ngo 1.22\n\ntoolchain go1.23.2\n\nrequire (\n\tgithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d\n\tgithub.com/stretchr/testify v1.9.0\n\tgo.uber.org/goleak v1.2.1\n\tgolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842\n\tgolang.org/x/time v0.5.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/onsi/ginkgo/v2 v2.9.5 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/rogpeppe/go-internal v1.12.0 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgo.uber.org/mock v0.4.0 // indirect\n\tgolang.org/x/crypto v0.26.0 // indirect\n\tgolang.org/x/mod v0.17.0 // indirect\n\tgolang.org/x/net v0.28.0 // indirect\n\tgolang.org/x/sys v0.23.0 // indirect\n\tgolang.org/x/text v0.17.0 // indirect\n\tgolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect\n\tgoogle.golang.org/protobuf v1.34.1 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "core/go.sum",
    "content": "github.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d h1:KWRCWISqJOgY9/0hhH8Bevjw/k4tCQ7oJlXLyFv8u9s=\ngithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d/go.mod h1:x0paLlmCzNOUDDQIgmgFWmnpWQIEuH1GNfA6NdgSTuM=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=\ngithub.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=\ngithub.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=\ngithub.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=\ngithub.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=\ngithub.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngo.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=\ngo.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=\ngo.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=\ngo.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=\ngolang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=\ngolang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=\ngolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=\ngolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=\ngolang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=\ngolang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=\ngolang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=\ngolang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=\ngolang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=\ngolang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\ngolang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngoogle.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "core/internal/congestion/bbr/bandwidth.go",
    "content": "package bbr\n\nimport (\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/apernet/quic-go/congestion\"\n)\n\nconst (\n\tinfBandwidth = Bandwidth(math.MaxUint64)\n)\n\n// Bandwidth of a connection\ntype Bandwidth uint64\n\nconst (\n\t// BitsPerSecond is 1 bit per second\n\tBitsPerSecond Bandwidth = 1\n\t// BytesPerSecond is 1 byte per second\n\tBytesPerSecond = 8 * BitsPerSecond\n)\n\n// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta\nfunc BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth {\n\treturn Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond\n}\n"
  },
  {
    "path": "core/internal/congestion/bbr/bandwidth_sampler.go",
    "content": "package bbr\n\nimport (\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/apernet/quic-go/congestion\"\n)\n\nconst (\n\tinfRTT                             = time.Duration(math.MaxInt64)\n\tdefaultConnectionStateMapQueueSize = 256\n\tdefaultCandidatesBufferSize        = 256\n)\n\ntype roundTripCount uint64\n\n// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned\n// to the caller when the packet is acked or lost.\ntype sendTimeState struct {\n\t// Whether other states in this object is valid.\n\tisValid bool\n\t// Whether the sender is app limited at the time the packet was sent.\n\t// App limited bandwidth sample might be artificially low because the sender\n\t// did not have enough data to send in order to saturate the link.\n\tisAppLimited bool\n\t// Total number of sent bytes at the time the packet was sent.\n\t// Includes the packet itself.\n\ttotalBytesSent congestion.ByteCount\n\t// Total number of acked bytes at the time the packet was sent.\n\ttotalBytesAcked congestion.ByteCount\n\t// Total number of lost bytes at the time the packet was sent.\n\ttotalBytesLost congestion.ByteCount\n\t// Total number of inflight bytes at the time the packet was sent.\n\t// Includes the packet itself.\n\t// It should be equal to |total_bytes_sent| minus the sum of\n\t// |total_bytes_acked|, |total_bytes_lost| and total neutered bytes.\n\tbytesInFlight congestion.ByteCount\n}\n\nfunc newSendTimeState(\n\tisAppLimited bool,\n\ttotalBytesSent congestion.ByteCount,\n\ttotalBytesAcked congestion.ByteCount,\n\ttotalBytesLost congestion.ByteCount,\n\tbytesInFlight congestion.ByteCount,\n) *sendTimeState {\n\treturn &sendTimeState{\n\t\tisValid:         true,\n\t\tisAppLimited:    isAppLimited,\n\t\ttotalBytesSent:  totalBytesSent,\n\t\ttotalBytesAcked: totalBytesAcked,\n\t\ttotalBytesLost:  totalBytesLost,\n\t\tbytesInFlight:   bytesInFlight,\n\t}\n}\n\ntype extraAckedEvent struct {\n\t// The excess bytes acknowlwedged in the time delta for this event.\n\textraAcked congestion.ByteCount\n\n\t// The bytes acknowledged and time delta from the event.\n\tbytesAcked congestion.ByteCount\n\ttimeDelta  time.Duration\n\t// The round trip of the event.\n\tround roundTripCount\n}\n\nfunc maxExtraAckedEventFunc(a, b extraAckedEvent) int {\n\tif a.extraAcked > b.extraAcked {\n\t\treturn 1\n\t} else if a.extraAcked < b.extraAcked {\n\t\treturn -1\n\t}\n\treturn 0\n}\n\n// BandwidthSample\ntype bandwidthSample struct {\n\t// The bandwidth at that particular sample. Zero if no valid bandwidth sample\n\t// is available.\n\tbandwidth Bandwidth\n\t// The RTT measurement at this particular sample.  Zero if no RTT sample is\n\t// available.  Does not correct for delayed ack time.\n\trtt time.Duration\n\t// |send_rate| is computed from the current packet being acked('P') and an\n\t// earlier packet that is acked before P was sent.\n\tsendRate Bandwidth\n\t// States captured when the packet was sent.\n\tstateAtSend sendTimeState\n}\n\nfunc newBandwidthSample() *bandwidthSample {\n\treturn &bandwidthSample{\n\t\tsendRate: infBandwidth,\n\t}\n}\n\n// MaxAckHeightTracker is part of the BandwidthSampler. It is called after every\n// ack event to keep track the degree of ack aggregation(a.k.a \"ack height\").\ntype maxAckHeightTracker struct {\n\t// Tracks the maximum number of bytes acked faster than the estimated\n\t// bandwidth.\n\tmaxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount]\n\t// The time this aggregation started and the number of bytes acked during it.\n\taggregationEpochStartTime time.Time\n\taggregationEpochBytes     congestion.ByteCount\n\t// The last sent packet number before the current aggregation epoch started.\n\tlastSentPacketNumberBeforeEpoch congestion.PacketNumber\n\t// The number of ack aggregation epochs ever started, including the ongoing\n\t// one. Stats only.\n\tnumAckAggregationEpochs                uint64\n\tackAggregationBandwidthThreshold       float64\n\tstartNewAggregationEpochAfterFullRound bool\n\treduceExtraAckedOnBandwidthIncrease    bool\n}\n\nfunc newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker {\n\treturn &maxAckHeightTracker{\n\t\tmaxAckHeightFilter:               NewWindowedFilter(windowLength, maxExtraAckedEventFunc),\n\t\tlastSentPacketNumberBeforeEpoch:  invalidPacketNumber,\n\t\tackAggregationBandwidthThreshold: 1.0,\n\t}\n}\n\nfunc (m *maxAckHeightTracker) Get() congestion.ByteCount {\n\treturn m.maxAckHeightFilter.GetBest().extraAcked\n}\n\nfunc (m *maxAckHeightTracker) Update(\n\tbandwidthEstimate Bandwidth,\n\tisNewMaxBandwidth bool,\n\troundTripCount roundTripCount,\n\tlastSentPacketNumber congestion.PacketNumber,\n\tlastAckedPacketNumber congestion.PacketNumber,\n\tackTime time.Time,\n\tbytesAcked congestion.ByteCount,\n) congestion.ByteCount {\n\tforceNewEpoch := false\n\n\tif m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth {\n\t\t// Save and clear existing entries.\n\t\tbest := m.maxAckHeightFilter.GetBest()\n\t\tsecondBest := m.maxAckHeightFilter.GetSecondBest()\n\t\tthirdBest := m.maxAckHeightFilter.GetThirdBest()\n\t\tm.maxAckHeightFilter.Clear()\n\n\t\t// Reinsert the heights into the filter after recalculating.\n\t\texpectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta)\n\t\tif expectedBytesAcked < best.bytesAcked {\n\t\t\tbest.extraAcked = best.bytesAcked - expectedBytesAcked\n\t\t\tm.maxAckHeightFilter.Update(best, best.round)\n\t\t}\n\t\texpectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta)\n\t\tif expectedBytesAcked < secondBest.bytesAcked {\n\t\t\tsecondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked\n\t\t\tm.maxAckHeightFilter.Update(secondBest, secondBest.round)\n\t\t}\n\t\texpectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta)\n\t\tif expectedBytesAcked < thirdBest.bytesAcked {\n\t\t\tthirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked\n\t\t\tm.maxAckHeightFilter.Update(thirdBest, thirdBest.round)\n\t\t}\n\t}\n\n\t// If any packet sent after the start of the epoch has been acked, start a new\n\t// epoch.\n\tif m.startNewAggregationEpochAfterFullRound &&\n\t\tm.lastSentPacketNumberBeforeEpoch != invalidPacketNumber &&\n\t\tlastAckedPacketNumber != invalidPacketNumber &&\n\t\tlastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch {\n\t\tforceNewEpoch = true\n\t}\n\tif m.aggregationEpochStartTime.IsZero() || forceNewEpoch {\n\t\tm.aggregationEpochBytes = bytesAcked\n\t\tm.aggregationEpochStartTime = ackTime\n\t\tm.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber\n\t\tm.numAckAggregationEpochs++\n\t\treturn 0\n\t}\n\n\t// Compute how many bytes are expected to be delivered, assuming max bandwidth\n\t// is correct.\n\taggregationDelta := ackTime.Sub(m.aggregationEpochStartTime)\n\texpectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta)\n\t// Reset the current aggregation epoch as soon as the ack arrival rate is less\n\t// than or equal to the max bandwidth.\n\tif m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) {\n\t\t// Reset to start measuring a new aggregation epoch.\n\t\tm.aggregationEpochBytes = bytesAcked\n\t\tm.aggregationEpochStartTime = ackTime\n\t\tm.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber\n\t\tm.numAckAggregationEpochs++\n\t\treturn 0\n\t}\n\n\tm.aggregationEpochBytes += bytesAcked\n\n\t// Compute how many extra bytes were delivered vs max bandwidth.\n\textraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked\n\tnewEvent := extraAckedEvent{\n\t\textraAcked: expectedBytesAcked,\n\t\tbytesAcked: m.aggregationEpochBytes,\n\t\ttimeDelta:  aggregationDelta,\n\t}\n\tm.maxAckHeightFilter.Update(newEvent, roundTripCount)\n\treturn extraBytesAcked\n}\n\nfunc (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) {\n\tm.maxAckHeightFilter.SetWindowLength(length)\n}\n\nfunc (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) {\n\tnewEvent := extraAckedEvent{\n\t\textraAcked: newHeight,\n\t\tround:      newTime,\n\t}\n\tm.maxAckHeightFilter.Reset(newEvent, newTime)\n}\n\nfunc (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) {\n\tm.ackAggregationBandwidthThreshold = threshold\n}\n\nfunc (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) {\n\tm.startNewAggregationEpochAfterFullRound = value\n}\n\nfunc (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) {\n\tm.reduceExtraAckedOnBandwidthIncrease = value\n}\n\nfunc (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 {\n\treturn m.ackAggregationBandwidthThreshold\n}\n\nfunc (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 {\n\treturn m.numAckAggregationEpochs\n}\n\n// AckPoint represents a point on the ack line.\ntype ackPoint struct {\n\tackTime         time.Time\n\ttotalBytesAcked congestion.ByteCount\n}\n\n// RecentAckPoints maintains the most recent 2 ack points at distinct times.\ntype recentAckPoints struct {\n\tackPoints [2]ackPoint\n}\n\nfunc (r *recentAckPoints) Update(ackTime time.Time, totalBytesAcked congestion.ByteCount) {\n\tif ackTime.Before(r.ackPoints[1].ackTime) {\n\t\tr.ackPoints[1].ackTime = ackTime\n\t} else if ackTime.After(r.ackPoints[1].ackTime) {\n\t\tr.ackPoints[0] = r.ackPoints[1]\n\t\tr.ackPoints[1].ackTime = ackTime\n\t}\n\n\tr.ackPoints[1].totalBytesAcked = totalBytesAcked\n}\n\nfunc (r *recentAckPoints) Clear() {\n\tr.ackPoints[0] = ackPoint{}\n\tr.ackPoints[1] = ackPoint{}\n}\n\nfunc (r *recentAckPoints) MostRecentPoint() *ackPoint {\n\treturn &r.ackPoints[1]\n}\n\nfunc (r *recentAckPoints) LessRecentPoint() *ackPoint {\n\tif r.ackPoints[0].totalBytesAcked != 0 {\n\t\treturn &r.ackPoints[0]\n\t}\n\n\treturn &r.ackPoints[1]\n}\n\n// ConnectionStateOnSentPacket represents the information about a sent packet\n// and the state of the connection at the moment the packet was sent,\n// specifically the information about the most recently acknowledged packet at\n// that moment.\ntype connectionStateOnSentPacket struct {\n\t// Time at which the packet is sent.\n\tsentTime time.Time\n\t// Size of the packet.\n\tsize congestion.ByteCount\n\t// The value of |totalBytesSentAtLastAckedPacket| at the time the\n\t// packet was sent.\n\ttotalBytesSentAtLastAckedPacket congestion.ByteCount\n\t// The value of |lastAckedPacketSentTime| at the time the packet was\n\t// sent.\n\tlastAckedPacketSentTime time.Time\n\t// The value of |lastAckedPacketAckTime| at the time the packet was\n\t// sent.\n\tlastAckedPacketAckTime time.Time\n\t// Send time states that are returned to the congestion controller when the\n\t// packet is acked or lost.\n\tsendTimeState sendTimeState\n}\n\n// Snapshot constructor. Records the current state of the bandwidth\n// sampler.\n// |bytes_in_flight| is the bytes in flight right after the packet is sent.\nfunc newConnectionStateOnSentPacket(\n\tsentTime time.Time,\n\tsize congestion.ByteCount,\n\tbytesInFlight congestion.ByteCount,\n\tsampler *bandwidthSampler,\n) *connectionStateOnSentPacket {\n\treturn &connectionStateOnSentPacket{\n\t\tsentTime:                        sentTime,\n\t\tsize:                            size,\n\t\ttotalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket,\n\t\tlastAckedPacketSentTime:         sampler.lastAckedPacketSentTime,\n\t\tlastAckedPacketAckTime:          sampler.lastAckedPacketAckTime,\n\t\tsendTimeState: *newSendTimeState(\n\t\t\tsampler.isAppLimited,\n\t\t\tsampler.totalBytesSent,\n\t\t\tsampler.totalBytesAcked,\n\t\t\tsampler.totalBytesLost,\n\t\t\tbytesInFlight,\n\t\t),\n\t}\n}\n\n// BandwidthSampler keeps track of sent and acknowledged packets and outputs a\n// bandwidth sample for every packet acknowledged. The samples are taken for\n// individual packets, and are not filtered; the consumer has to filter the\n// bandwidth samples itself. In certain cases, the sampler will locally severely\n// underestimate the bandwidth, hence a maximum filter with a size of at least\n// one RTT is recommended.\n//\n// This class bases its samples on the slope of two curves: the number of bytes\n// sent over time, and the number of bytes acknowledged as received over time.\n// It produces a sample of both slopes for every packet that gets acknowledged,\n// based on a slope between two points on each of the corresponding curves. Note\n// that due to the packet loss, the number of bytes on each curve might get\n// further and further away from each other, meaning that it is not feasible to\n// compare byte values coming from different curves with each other.\n//\n// The obvious points for measuring slope sample are the ones corresponding to\n// the packet that was just acknowledged. Let us denote them as S_1 (point at\n// which the current packet was sent) and A_1 (point at which the current packet\n// was acknowledged). However, taking a slope requires two points on each line,\n// so estimating bandwidth requires picking a packet in the past with respect to\n// which the slope is measured.\n//\n// For that purpose, BandwidthSampler always keeps track of the most recently\n// acknowledged packet, and records it together with every outgoing packet.\n// When a packet gets acknowledged (A_1), it has not only information about when\n// it itself was sent (S_1), but also the information about the latest\n// acknowledged packet right before it was sent (S_0 and A_0).\n//\n// Based on that data, send and ack rate are estimated as:\n//\n//\tsend_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0))\n//\tack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0))\n//\n// Here, the ack rate is intuitively the rate we want to treat as bandwidth.\n// However, in certain cases (e.g. ack compression) the ack rate at a point may\n// end up higher than the rate at which the data was originally sent, which is\n// not indicative of the real bandwidth. Hence, we use the send rate as an upper\n// bound, and the sample value is\n//\n//\trate_sample = min(send_rate, ack_rate)\n//\n// An important edge case handled by the sampler is tracking the app-limited\n// samples. There are multiple meaning of \"app-limited\" used interchangeably,\n// hence it is important to understand and to be able to distinguish between\n// them.\n//\n// Meaning 1: connection state. The connection is said to be app-limited when\n// there is no outstanding data to send. This means that certain bandwidth\n// samples in the future would not be an accurate indication of the link\n// capacity, and it is important to inform consumer about that. Whenever\n// connection becomes app-limited, the sampler is notified via OnAppLimited()\n// method.\n//\n// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth\n// sampler becomes notified about the connection being app-limited, it enters\n// app-limited phase. In that phase, all *sent* packets are marked as\n// app-limited. Note that the connection itself does not have to be\n// app-limited during the app-limited phase, and in fact it will not be\n// (otherwise how would it send packets?). The boolean flag below indicates\n// whether the sampler is in that phase.\n//\n// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is\n// sent during the app-limited phase, the resulting sample related to the\n// packet will be marked as app-limited.\n//\n// With the terminology issue out of the way, let us consider the question of\n// what kind of situation it addresses.\n//\n// Consider a scenario where we first send packets 1 to 20 at a regular\n// bandwidth, and then immediately run out of data. After a few seconds, we send\n// packets 21 to 60, and only receive ack for 21 between sending packets 40 and\n// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0\n// we use to compute the slope is going to be packet 20, a few seconds apart\n// from the current packet, hence the resulting estimate would be extremely low\n// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21,\n// meaning that the bandwidth sample would exclude the quiescence.\n//\n// Based on the analysis of that scenario, we implement the following rule: once\n// OnAppLimited() is called, all sent packets will produce app-limited samples\n// up until an ack for a packet that was sent after OnAppLimited() was called.\n// Note that while the scenario above is not the only scenario when the\n// connection is app-limited, the approach works in other cases too.\n\ntype congestionEventSample struct {\n\t// The maximum bandwidth sample from all acked packets.\n\t// QuicBandwidth::Zero() if no samples are available.\n\tsampleMaxBandwidth Bandwidth\n\t// Whether |sample_max_bandwidth| is from a app-limited sample.\n\tsampleIsAppLimited bool\n\t// The minimum rtt sample from all acked packets.\n\t// QuicTime::Delta::Infinite() if no samples are available.\n\tsampleRtt time.Duration\n\t// For each packet p in acked packets, this is the max value of INFLIGHT(p),\n\t// where INFLIGHT(p) is the number of bytes acked while p is inflight.\n\tsampleMaxInflight congestion.ByteCount\n\t// The send state of the largest packet in acked_packets, unless it is\n\t// empty. If acked_packets is empty, it's the send state of the largest\n\t// packet in lost_packets.\n\tlastPacketSendState sendTimeState\n\t// The number of extra bytes acked from this ack event, compared to what is\n\t// expected from the flow's bandwidth. Larger value means more ack\n\t// aggregation.\n\textraAcked congestion.ByteCount\n}\n\nfunc newCongestionEventSample() *congestionEventSample {\n\treturn &congestionEventSample{\n\t\tsampleRtt: infRTT,\n\t}\n}\n\ntype bandwidthSampler struct {\n\t// The total number of congestion controlled bytes sent during the connection.\n\ttotalBytesSent congestion.ByteCount\n\n\t// The total number of congestion controlled bytes which were acknowledged.\n\ttotalBytesAcked congestion.ByteCount\n\n\t// The total number of congestion controlled bytes which were lost.\n\ttotalBytesLost congestion.ByteCount\n\n\t// The total number of congestion controlled bytes which have been neutered.\n\ttotalBytesNeutered congestion.ByteCount\n\n\t// The value of |total_bytes_sent_| at the time the last acknowledged packet\n\t// was sent. Valid only when |last_acked_packet_sent_time_| is valid.\n\ttotalBytesSentAtLastAckedPacket congestion.ByteCount\n\n\t// The time at which the last acknowledged packet was sent. Set to\n\t// QuicTime::Zero() if no valid timestamp is available.\n\tlastAckedPacketSentTime time.Time\n\n\t// The time at which the most recent packet was acknowledged.\n\tlastAckedPacketAckTime time.Time\n\n\t// The most recently sent packet.\n\tlastSentPacket congestion.PacketNumber\n\n\t// The most recently acked packet.\n\tlastAckedPacket congestion.PacketNumber\n\n\t// Indicates whether the bandwidth sampler is currently in an app-limited\n\t// phase.\n\tisAppLimited bool\n\n\t// The packet that will be acknowledged after this one will cause the sampler\n\t// to exit the app-limited phase.\n\tendOfAppLimitedPhase congestion.PacketNumber\n\n\t// Record of the connection state at the point where each packet in flight was\n\t// sent, indexed by the packet number.\n\tconnectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket]\n\n\trecentAckPoints recentAckPoints\n\ta0Candidates    RingBuffer[ackPoint]\n\n\t// Maximum number of tracked packets.\n\tmaxTrackedPackets congestion.ByteCount\n\n\tmaxAckHeightTracker              *maxAckHeightTracker\n\ttotalBytesAckedAfterLastAckEvent congestion.ByteCount\n\n\t// True if connection option 'BSAO' is set.\n\toverestimateAvoidance bool\n\n\t// True if connection option 'BBRB' is set.\n\tlimitMaxAckHeightTrackerBySendRate bool\n}\n\nfunc newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler {\n\tb := &bandwidthSampler{\n\t\tmaxAckHeightTracker:  newMaxAckHeightTracker(maxAckHeightTrackerWindowLength),\n\t\tconnectionStateMap:   newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize),\n\t\tlastSentPacket:       invalidPacketNumber,\n\t\tlastAckedPacket:      invalidPacketNumber,\n\t\tendOfAppLimitedPhase: invalidPacketNumber,\n\t}\n\n\tb.a0Candidates.Init(defaultCandidatesBufferSize)\n\n\treturn b\n}\n\nfunc (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount {\n\treturn b.maxAckHeightTracker.Get()\n}\n\nfunc (b *bandwidthSampler) NumAckAggregationEpochs() uint64 {\n\treturn b.maxAckHeightTracker.NumAckAggregationEpochs()\n}\n\nfunc (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) {\n\tb.maxAckHeightTracker.SetFilterWindowLength(length)\n}\n\nfunc (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) {\n\tb.maxAckHeightTracker.Reset(newHeight, newTime)\n}\n\nfunc (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) {\n\tb.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value)\n}\n\nfunc (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) {\n\tb.limitMaxAckHeightTrackerBySendRate = value\n}\n\nfunc (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) {\n\tb.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value)\n}\n\nfunc (b *bandwidthSampler) EnableOverestimateAvoidance() {\n\tif b.overestimateAvoidance {\n\t\treturn\n\t}\n\n\tb.overestimateAvoidance = true\n\tb.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0)\n}\n\nfunc (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool {\n\treturn b.overestimateAvoidance\n}\n\nfunc (b *bandwidthSampler) OnPacketSent(\n\tsentTime time.Time,\n\tpacketNumber congestion.PacketNumber,\n\tbytes congestion.ByteCount,\n\tbytesInFlight congestion.ByteCount,\n\tisRetransmittable bool,\n) {\n\tb.lastSentPacket = packetNumber\n\n\tif !isRetransmittable {\n\t\treturn\n\t}\n\n\tb.totalBytesSent += bytes\n\n\t// If there are no packets in flight, the time at which the new transmission\n\t// opens can be treated as the A_0 point for the purpose of bandwidth\n\t// sampling. This underestimates bandwidth to some extent, and produces some\n\t// artificially low samples for most packets in flight, but it provides with\n\t// samples at important points where we would not have them otherwise, most\n\t// importantly at the beginning of the connection.\n\tif bytesInFlight == 0 {\n\t\tb.lastAckedPacketAckTime = sentTime\n\t\tif b.overestimateAvoidance {\n\t\t\tb.recentAckPoints.Clear()\n\t\t\tb.recentAckPoints.Update(sentTime, b.totalBytesAcked)\n\t\t\tb.a0Candidates.Clear()\n\t\t\tb.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint())\n\t\t}\n\t\tb.totalBytesSentAtLastAckedPacket = b.totalBytesSent\n\n\t\t// In this situation ack compression is not a concern, set send rate to\n\t\t// effectively infinite.\n\t\tb.lastAckedPacketSentTime = sentTime\n\t}\n\n\tb.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket(\n\t\tsentTime,\n\t\tbytes,\n\t\tbytesInFlight+bytes,\n\t\tb,\n\t))\n}\n\nfunc (b *bandwidthSampler) OnCongestionEvent(\n\tackTime time.Time,\n\tackedPackets []congestion.AckedPacketInfo,\n\tlostPackets []congestion.LostPacketInfo,\n\tmaxBandwidth Bandwidth,\n\testBandwidthUpperBound Bandwidth,\n\troundTripCount roundTripCount,\n) congestionEventSample {\n\teventSample := newCongestionEventSample()\n\n\tvar lastLostPacketSendState sendTimeState\n\n\tfor _, p := range lostPackets {\n\t\tsendState := b.OnPacketLost(p.PacketNumber, p.BytesLost)\n\t\tif sendState.isValid {\n\t\t\tlastLostPacketSendState = sendState\n\t\t}\n\t}\n\n\tif len(ackedPackets) == 0 {\n\t\t// Only populate send state for a loss-only event.\n\t\teventSample.lastPacketSendState = lastLostPacketSendState\n\t\treturn *eventSample\n\t}\n\n\tvar lastAckedPacketSendState sendTimeState\n\tvar maxSendRate Bandwidth\n\n\tfor _, p := range ackedPackets {\n\t\tsample := b.onPacketAcknowledged(ackTime, p.PacketNumber)\n\t\tif !sample.stateAtSend.isValid {\n\t\t\tcontinue\n\t\t}\n\n\t\tlastAckedPacketSendState = sample.stateAtSend\n\n\t\tif sample.rtt != 0 {\n\t\t\teventSample.sampleRtt = min(eventSample.sampleRtt, sample.rtt)\n\t\t}\n\t\tif sample.bandwidth > eventSample.sampleMaxBandwidth {\n\t\t\teventSample.sampleMaxBandwidth = sample.bandwidth\n\t\t\teventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited\n\t\t}\n\t\tif sample.sendRate != infBandwidth {\n\t\t\tmaxSendRate = max(maxSendRate, sample.sendRate)\n\t\t}\n\t\tinflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked\n\t\tif inflightSample > eventSample.sampleMaxInflight {\n\t\t\teventSample.sampleMaxInflight = inflightSample\n\t\t}\n\t}\n\n\tif !lastLostPacketSendState.isValid {\n\t\teventSample.lastPacketSendState = lastAckedPacketSendState\n\t} else if !lastAckedPacketSendState.isValid {\n\t\teventSample.lastPacketSendState = lastLostPacketSendState\n\t} else {\n\t\t// If two packets are inflight and an alarm is armed to lose a packet and it\n\t\t// wakes up late, then the first of two in flight packets could have been\n\t\t// acknowledged before the wakeup, which re-evaluates loss detection, and\n\t\t// could declare the later of the two lost.\n\t\tif lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber {\n\t\t\teventSample.lastPacketSendState = lastLostPacketSendState\n\t\t} else {\n\t\t\teventSample.lastPacketSendState = lastAckedPacketSendState\n\t\t}\n\t}\n\n\tisNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth\n\tmaxBandwidth = max(maxBandwidth, eventSample.sampleMaxBandwidth)\n\tif b.limitMaxAckHeightTrackerBySendRate {\n\t\tmaxBandwidth = max(maxBandwidth, maxSendRate)\n\t}\n\n\teventSample.extraAcked = b.onAckEventEnd(min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount)\n\n\treturn *eventSample\n}\n\nfunc (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) {\n\tb.totalBytesLost += bytesLost\n\tif sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil {\n\t\tsentPacketToSendTimeState(sentPacketPointer, &s)\n\t}\n\treturn s\n}\n\nfunc (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) {\n\tb.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) {\n\t\tb.totalBytesNeutered += sentPacket.size\n\t})\n}\n\nfunc (b *bandwidthSampler) OnAppLimited() {\n\tb.isAppLimited = true\n\tb.endOfAppLimitedPhase = b.lastSentPacket\n}\n\nfunc (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) {\n\t// A packet can become obsolete when it is removed from QuicUnackedPacketMap's\n\t// view of inflight before it is acked or marked as lost. For example, when\n\t// QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet,\n\t// the packet is removed from QuicUnackedPacketMap's inflight, but is not\n\t// marked as acked or lost in the BandwidthSampler.\n\tb.connectionStateMap.RemoveUpTo(leastUnacked)\n}\n\nfunc (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount {\n\treturn b.totalBytesSent\n}\n\nfunc (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount {\n\treturn b.totalBytesLost\n}\n\nfunc (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount {\n\treturn b.totalBytesAcked\n}\n\nfunc (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount {\n\treturn b.totalBytesNeutered\n}\n\nfunc (b *bandwidthSampler) IsAppLimited() bool {\n\treturn b.isAppLimited\n}\n\nfunc (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber {\n\treturn b.endOfAppLimitedPhase\n}\n\nfunc (b *bandwidthSampler) max_ack_height() congestion.ByteCount {\n\treturn b.maxAckHeightTracker.Get()\n}\n\nfunc (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool {\n\tif b.a0Candidates.Empty() {\n\t\treturn false\n\t}\n\n\tif b.a0Candidates.Len() == 1 {\n\t\t*a0 = *b.a0Candidates.Front()\n\t\treturn true\n\t}\n\n\tfor i := 1; i < b.a0Candidates.Len(); i++ {\n\t\tif b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked {\n\t\t\t*a0 = *b.a0Candidates.Offset(i - 1)\n\t\t\tif i > 1 {\n\t\t\t\tfor j := 0; j < i-1; j++ {\n\t\t\t\t\tb.a0Candidates.PopFront()\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t}\n\n\t*a0 = *b.a0Candidates.Back()\n\tfor k := 0; k < b.a0Candidates.Len()-1; k++ {\n\t\tb.a0Candidates.PopFront()\n\t}\n\treturn true\n}\n\nfunc (b *bandwidthSampler) onPacketAcknowledged(ackTime time.Time, packetNumber congestion.PacketNumber) bandwidthSample {\n\tsample := newBandwidthSample()\n\tb.lastAckedPacket = packetNumber\n\tsentPacketPointer := b.connectionStateMap.GetEntry(packetNumber)\n\tif sentPacketPointer == nil {\n\t\treturn *sample\n\t}\n\n\t// OnPacketAcknowledgedInner\n\tb.totalBytesAcked += sentPacketPointer.size\n\tb.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent\n\tb.lastAckedPacketSentTime = sentPacketPointer.sentTime\n\tb.lastAckedPacketAckTime = ackTime\n\tif b.overestimateAvoidance {\n\t\tb.recentAckPoints.Update(ackTime, b.totalBytesAcked)\n\t}\n\n\tif b.isAppLimited {\n\t\t// Exit app-limited phase in two cases:\n\t\t// (1) end_of_app_limited_phase_ is not initialized, i.e., so far all\n\t\t// packets are sent while there are buffered packets or pending data.\n\t\t// (2) The current acked packet is after the sent packet marked as the end\n\t\t// of the app limit phase.\n\t\tif b.endOfAppLimitedPhase == invalidPacketNumber ||\n\t\t\tpacketNumber > b.endOfAppLimitedPhase {\n\t\t\tb.isAppLimited = false\n\t\t}\n\t}\n\n\t// There might have been no packets acknowledged at the moment when the\n\t// current packet was sent. In that case, there is no bandwidth sample to\n\t// make.\n\tif sentPacketPointer.lastAckedPacketSentTime.IsZero() {\n\t\treturn *sample\n\t}\n\n\t// Infinite rate indicates that the sampler is supposed to discard the\n\t// current send rate sample and use only the ack rate.\n\tsendRate := infBandwidth\n\tif sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) {\n\t\tsendRate = BandwidthFromDelta(\n\t\t\tsentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket,\n\t\t\tsentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime))\n\t}\n\n\tvar a0 ackPoint\n\tif b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) {\n\t} else {\n\t\ta0.ackTime = sentPacketPointer.lastAckedPacketAckTime\n\t\ta0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked\n\t}\n\n\t// During the slope calculation, ensure that ack time of the current packet is\n\t// always larger than the time of the previous packet, otherwise division by\n\t// zero or integer underflow can occur.\n\tif ackTime.Sub(a0.ackTime) <= 0 {\n\t\treturn *sample\n\t}\n\n\tackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime))\n\n\tsample.bandwidth = min(sendRate, ackRate)\n\t// Note: this sample does not account for delayed acknowledgement time.  This\n\t// means that the RTT measurements here can be artificially high, especially\n\t// on low bandwidth connections.\n\tsample.rtt = ackTime.Sub(sentPacketPointer.sentTime)\n\tsample.sendRate = sendRate\n\tsentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend)\n\n\treturn *sample\n}\n\nfunc (b *bandwidthSampler) onAckEventEnd(\n\tbandwidthEstimate Bandwidth,\n\tisNewMaxBandwidth bool,\n\troundTripCount roundTripCount,\n) congestion.ByteCount {\n\tnewlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent\n\tif newlyAckedBytes == 0 {\n\t\treturn 0\n\t}\n\tb.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked\n\textraAcked := b.maxAckHeightTracker.Update(\n\t\tbandwidthEstimate,\n\t\tisNewMaxBandwidth,\n\t\troundTripCount,\n\t\tb.lastSentPacket,\n\t\tb.lastAckedPacket,\n\t\tb.lastAckedPacketAckTime,\n\t\tnewlyAckedBytes)\n\t// If |extra_acked| is zero, i.e. this ack event marks the start of a new ack\n\t// aggregation epoch, save LessRecentPoint, which is the last ack point of the\n\t// previous epoch, as a A0 candidate.\n\tif b.overestimateAvoidance && extraAcked == 0 {\n\t\tb.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint())\n\t}\n\treturn extraAcked\n}\n\nfunc sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) {\n\t*sendTimeState = sentPacket.sendTimeState\n\tsendTimeState.isValid = true\n}\n\n// BytesFromBandwidthAndTimeDelta calculates the bytes\n// from a bandwidth(bits per second) and a time delta\nfunc bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount {\n\treturn (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) /\n\t\t(congestion.ByteCount(time.Second) * 8)\n}\n\nfunc timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration {\n\treturn time.Duration(bytes*8) * time.Second / time.Duration(bandwidth)\n}\n"
  },
  {
    "path": "core/internal/congestion/bbr/bbr_sender.go",
    "content": "package bbr\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/apernet/quic-go/congestion\"\n\n\t\"github.com/apernet/hysteria/core/v2/internal/congestion/common\"\n)\n\n// BbrSender implements BBR congestion control algorithm.  BBR aims to estimate\n// the current available Bottleneck Bandwidth and RTT (hence the name), and\n// regulates the pacing rate and the size of the congestion window based on\n// those signals.\n//\n// BBR relies on pacing in order to function properly.  Do not use BBR when\n// pacing is disabled.\n//\n\nconst (\n\tminBps = 65536 // 64 kbps\n\n\tinvalidPacketNumber            = -1\n\tinitialCongestionWindowPackets = 32\n\n\t// Constants based on TCP defaults.\n\t// The minimum CWND to ensure delayed acks don't reduce bandwidth measurements.\n\t// Does not inflate the pacing rate.\n\tdefaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSizeIPv4)\n\n\t// The gain used for the STARTUP, equal to 2/ln(2).\n\tdefaultHighGain = 2.885\n\t// The newly derived gain for STARTUP, equal to 4 * ln(2)\n\tderivedHighGain = 2.773\n\t// The newly derived CWND gain for STARTUP, 2.\n\tderivedHighCWNDGain = 2.0\n\n\tdebugEnv = \"HYSTERIA_BBR_DEBUG\"\n)\n\n// The cycle of gains used during the PROBE_BW stage.\nvar pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}\n\nconst (\n\t// The length of the gain cycle.\n\tgainCycleLength = len(pacingGain)\n\t// The size of the bandwidth filter window, in round-trips.\n\tbandwidthWindowSize = gainCycleLength + 2\n\n\t// The time after which the current min_rtt value expires.\n\tminRttExpiry = 10 * time.Second\n\t// The minimum time the connection can spend in PROBE_RTT mode.\n\tprobeRttTime = 200 * time.Millisecond\n\t// If the bandwidth does not increase by the factor of |kStartupGrowthTarget|\n\t// within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection\n\t// will exit the STARTUP mode.\n\tstartupGrowthTarget                         = 1.25\n\troundTripsWithoutGrowthBeforeExitingStartup = int64(3)\n\n\t// Flag.\n\tdefaultStartupFullLossCount  = 8\n\tquicBbr2DefaultLossThreshold = 0.02\n\tmaxBbrBurstPackets           = 10\n)\n\ntype bbrMode int\n\nconst (\n\t// Startup phase of the connection.\n\tbbrModeStartup = iota\n\t// After achieving the highest possible bandwidth during the startup, lower\n\t// the pacing rate in order to drain the queue.\n\tbbrModeDrain\n\t// Cruising mode.\n\tbbrModeProbeBw\n\t// Temporarily slow down sending in order to empty the buffer and measure\n\t// the real minimum RTT.\n\tbbrModeProbeRtt\n)\n\n// Indicates how the congestion control limits the amount of bytes in flight.\ntype bbrRecoveryState int\n\nconst (\n\t// Do not limit.\n\tbbrRecoveryStateNotInRecovery = iota\n\t// Allow an extra outstanding byte for each byte acknowledged.\n\tbbrRecoveryStateConservation\n\t// Allow two extra outstanding bytes for each byte acknowledged (slow\n\t// start).\n\tbbrRecoveryStateGrowth\n)\n\ntype bbrSender struct {\n\trttStats congestion.RTTStatsProvider\n\tclock    Clock\n\tpacer    *common.Pacer\n\n\tmode bbrMode\n\n\t// Bandwidth sampler provides BBR with the bandwidth measurements at\n\t// individual points.\n\tsampler *bandwidthSampler\n\n\t// The number of the round trips that have occurred during the connection.\n\troundTripCount roundTripCount\n\n\t// The packet number of the most recently sent packet.\n\tlastSentPacket congestion.PacketNumber\n\t// Acknowledgement of any packet after |current_round_trip_end_| will cause\n\t// the round trip counter to advance.\n\tcurrentRoundTripEnd congestion.PacketNumber\n\n\t// Number of congestion events with some losses, in the current round.\n\tnumLossEventsInRound uint64\n\n\t// Number of total bytes lost in the current round.\n\tbytesLostInRound congestion.ByteCount\n\n\t// The filter that tracks the maximum bandwidth over the multiple recent\n\t// round-trips.\n\tmaxBandwidth *WindowedFilter[Bandwidth, roundTripCount]\n\n\t// Minimum RTT estimate.  Automatically expires within 10 seconds (and\n\t// triggers PROBE_RTT mode) if no new value is sampled during that period.\n\tminRtt time.Duration\n\t// The time at which the current value of |min_rtt_| was assigned.\n\tminRttTimestamp time.Time\n\n\t// The maximum allowed number of bytes in flight.\n\tcongestionWindow congestion.ByteCount\n\n\t// The initial value of the |congestion_window_|.\n\tinitialCongestionWindow congestion.ByteCount\n\n\t// The largest value the |congestion_window_| can achieve.\n\tmaxCongestionWindow congestion.ByteCount\n\n\t// The smallest value the |congestion_window_| can achieve.\n\tminCongestionWindow congestion.ByteCount\n\n\t// The pacing gain applied during the STARTUP phase.\n\thighGain float64\n\n\t// The CWND gain applied during the STARTUP phase.\n\thighCwndGain float64\n\n\t// The pacing gain applied during the DRAIN phase.\n\tdrainGain float64\n\n\t// The current pacing rate of the connection.\n\tpacingRate Bandwidth\n\n\t// The gain currently applied to the pacing rate.\n\tpacingGain float64\n\t// The gain currently applied to the congestion window.\n\tcongestionWindowGain float64\n\n\t// The gain used for the congestion window during PROBE_BW.  Latched from\n\t// quic_bbr_cwnd_gain flag.\n\tcongestionWindowGainConstant float64\n\t// The number of RTTs to stay in STARTUP mode.  Defaults to 3.\n\tnumStartupRtts int64\n\n\t// Number of round-trips in PROBE_BW mode, used for determining the current\n\t// pacing gain cycle.\n\tcycleCurrentOffset int\n\t// The time at which the last pacing gain cycle was started.\n\tlastCycleStart time.Time\n\n\t// Indicates whether the connection has reached the full bandwidth mode.\n\tisAtFullBandwidth bool\n\t// Number of rounds during which there was no significant bandwidth increase.\n\troundsWithoutBandwidthGain int64\n\t// The bandwidth compared to which the increase is measured.\n\tbandwidthAtLastRound Bandwidth\n\n\t// Set to true upon exiting quiescence.\n\texitingQuiescence bool\n\n\t// Time at which PROBE_RTT has to be exited.  Setting it to zero indicates\n\t// that the time is yet unknown as the number of packets in flight has not\n\t// reached the required value.\n\texitProbeRttAt time.Time\n\t// Indicates whether a round-trip has passed since PROBE_RTT became active.\n\tprobeRttRoundPassed bool\n\n\t// Indicates whether the most recent bandwidth sample was marked as\n\t// app-limited.\n\tlastSampleIsAppLimited bool\n\t// Indicates whether any non app-limited samples have been recorded.\n\thasNoAppLimitedSample bool\n\n\t// Current state of recovery.\n\trecoveryState bbrRecoveryState\n\t// Receiving acknowledgement of a packet after |end_recovery_at_| will cause\n\t// BBR to exit the recovery mode.  A value above zero indicates at least one\n\t// loss has been detected, so it must not be set back to zero.\n\tendRecoveryAt congestion.PacketNumber\n\t// A window used to limit the number of bytes in flight during loss recovery.\n\trecoveryWindow congestion.ByteCount\n\t// If true, consider all samples in recovery app-limited.\n\tisAppLimitedRecovery bool // not used\n\n\t// When true, pace at 1.5x and disable packet conservation in STARTUP.\n\tslowerStartup bool // not used\n\t// When true, disables packet conservation in STARTUP.\n\trateBasedStartup bool // not used\n\n\t// When true, add the most recent ack aggregation measurement during STARTUP.\n\tenableAckAggregationDuringStartup bool\n\t// When true, expire the windowed ack aggregation values in STARTUP when\n\t// bandwidth increases more than 25%.\n\texpireAckAggregationInStartup bool\n\n\t// If true, will not exit low gain mode until bytes_in_flight drops below BDP\n\t// or it's time for high gain mode.\n\tdrainToTarget bool\n\n\t// If true, slow down pacing rate in STARTUP when overshooting is detected.\n\tdetectOvershooting bool\n\t// Bytes lost while detect_overshooting_ is true.\n\tbytesLostWhileDetectingOvershooting congestion.ByteCount\n\t// Slow down pacing rate if\n\t// bytes_lost_while_detecting_overshooting_ *\n\t// bytes_lost_multiplier_while_detecting_overshooting_ > IW.\n\tbytesLostMultiplierWhileDetectingOvershooting uint8\n\t// When overshooting is detected, do not drop pacing_rate_ below this value /\n\t// min_rtt.\n\tcwndToCalculateMinPacingRate congestion.ByteCount\n\n\t// Max congestion window when adjusting network parameters.\n\tmaxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used\n\n\t// Params.\n\tmaxDatagramSize congestion.ByteCount\n\t// Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()|\n\tbytesInFlight congestion.ByteCount\n\n\tdebug bool\n}\n\nvar _ congestion.CongestionControl = &bbrSender{}\n\nfunc NewBbrSender(\n\tclock Clock,\n\tinitialMaxDatagramSize congestion.ByteCount,\n) *bbrSender {\n\treturn newBbrSender(\n\t\tclock,\n\t\tinitialMaxDatagramSize,\n\t\tinitialCongestionWindowPackets*initialMaxDatagramSize,\n\t\tcongestion.MaxCongestionWindowPackets*initialMaxDatagramSize,\n\t)\n}\n\nfunc newBbrSender(\n\tclock Clock,\n\tinitialMaxDatagramSize,\n\tinitialCongestionWindow,\n\tinitialMaxCongestionWindow congestion.ByteCount,\n) *bbrSender {\n\tdebug, _ := strconv.ParseBool(os.Getenv(debugEnv))\n\tb := &bbrSender{\n\t\tclock:                        clock,\n\t\tmode:                         bbrModeStartup,\n\t\tsampler:                      newBandwidthSampler(roundTripCount(bandwidthWindowSize)),\n\t\tlastSentPacket:               invalidPacketNumber,\n\t\tcurrentRoundTripEnd:          invalidPacketNumber,\n\t\tmaxBandwidth:                 NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]),\n\t\tcongestionWindow:             initialCongestionWindow,\n\t\tinitialCongestionWindow:      initialCongestionWindow,\n\t\tmaxCongestionWindow:          initialMaxCongestionWindow,\n\t\tminCongestionWindow:          defaultMinimumCongestionWindow,\n\t\thighGain:                     defaultHighGain,\n\t\thighCwndGain:                 defaultHighGain,\n\t\tdrainGain:                    1.0 / defaultHighGain,\n\t\tpacingGain:                   1.0,\n\t\tcongestionWindowGain:         1.0,\n\t\tcongestionWindowGainConstant: 2.0,\n\t\tnumStartupRtts:               roundTripsWithoutGrowthBeforeExitingStartup,\n\t\trecoveryState:                bbrRecoveryStateNotInRecovery,\n\t\tendRecoveryAt:                invalidPacketNumber,\n\t\trecoveryWindow:               initialMaxCongestionWindow,\n\t\tbytesLostMultiplierWhileDetectingOvershooting:    2,\n\t\tcwndToCalculateMinPacingRate:                     initialCongestionWindow,\n\t\tmaxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow,\n\t\tmaxDatagramSize: initialMaxDatagramSize,\n\t\tdebug:           debug,\n\t}\n\tb.pacer = common.NewPacer(b.bandwidthForPacer)\n\n\t/*\n\t\tif b.tracer != nil {\n\t\t\tb.lastState = logging.CongestionStateStartup\n\t\t\tb.tracer.UpdatedCongestionState(logging.CongestionStateStartup)\n\t\t}\n\t*/\n\n\tb.enterStartupMode(b.clock.Now())\n\tb.setHighCwndGain(derivedHighCWNDGain)\n\n\treturn b\n}\n\nfunc (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) {\n\tb.rttStats = provider\n}\n\n// TimeUntilSend implements the SendAlgorithm interface.\nfunc (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time {\n\treturn b.pacer.TimeUntilSend()\n}\n\n// HasPacingBudget implements the SendAlgorithm interface.\nfunc (b *bbrSender) HasPacingBudget(now time.Time) bool {\n\treturn b.pacer.Budget(now) >= b.maxDatagramSize\n}\n\n// OnPacketSent implements the SendAlgorithm interface.\nfunc (b *bbrSender) OnPacketSent(\n\tsentTime time.Time,\n\tbytesInFlight congestion.ByteCount,\n\tpacketNumber congestion.PacketNumber,\n\tbytes congestion.ByteCount,\n\tisRetransmittable bool,\n) {\n\tb.pacer.SentPacket(sentTime, bytes)\n\n\tb.lastSentPacket = packetNumber\n\tb.bytesInFlight = bytesInFlight\n\n\tif bytesInFlight == 0 {\n\t\tb.exitingQuiescence = true\n\t}\n\n\tb.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable)\n}\n\n// CanSend implements the SendAlgorithm interface.\nfunc (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool {\n\treturn bytesInFlight < b.GetCongestionWindow()\n}\n\n// MaybeExitSlowStart implements the SendAlgorithm interface.\nfunc (b *bbrSender) MaybeExitSlowStart() {\n\t// Do nothing\n}\n\n// OnPacketAcked implements the SendAlgorithm interface.\nfunc (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime time.Time) {\n\t// Do nothing.\n}\n\n// OnPacketLost implements the SendAlgorithm interface.\nfunc (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) {\n\t// Do nothing.\n}\n\n// OnRetransmissionTimeout implements the SendAlgorithm interface.\nfunc (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) {\n\t// Do nothing.\n}\n\n// SetMaxDatagramSize implements the SendAlgorithm interface.\nfunc (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) {\n\tif s < b.maxDatagramSize {\n\t\tpanic(fmt.Sprintf(\"congestion BUG: decreased max datagram size from %d to %d\", b.maxDatagramSize, s))\n\t}\n\tcwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow\n\tb.maxDatagramSize = s\n\tif cwndIsMinCwnd {\n\t\tb.congestionWindow = b.minCongestionWindow\n\t}\n\tb.pacer.SetMaxDatagramSize(s)\n}\n\n// InSlowStart implements the SendAlgorithmWithDebugInfos interface.\nfunc (b *bbrSender) InSlowStart() bool {\n\treturn b.mode == bbrModeStartup\n}\n\n// InRecovery implements the SendAlgorithmWithDebugInfos interface.\nfunc (b *bbrSender) InRecovery() bool {\n\treturn b.recoveryState != bbrRecoveryStateNotInRecovery\n}\n\n// GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface.\nfunc (b *bbrSender) GetCongestionWindow() congestion.ByteCount {\n\tif b.mode == bbrModeProbeRtt {\n\t\treturn b.probeRttCongestionWindow()\n\t}\n\n\tif b.InRecovery() {\n\t\treturn min(b.congestionWindow, b.recoveryWindow)\n\t}\n\n\treturn b.congestionWindow\n}\n\nfunc (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) {\n\t// Do nothing.\n}\n\nfunc (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {\n\ttotalBytesAckedBefore := b.sampler.TotalBytesAcked()\n\ttotalBytesLostBefore := b.sampler.TotalBytesLost()\n\n\tvar isRoundStart, minRttExpired bool\n\tvar excessAcked, bytesLost congestion.ByteCount\n\n\t// The send state of the largest packet in acked_packets, unless it is\n\t// empty. If acked_packets is empty, it's the send state of the largest\n\t// packet in lost_packets.\n\tvar lastPacketSendState sendTimeState\n\n\tb.maybeAppLimited(priorInFlight)\n\n\t// Update bytesInFlight\n\tb.bytesInFlight = priorInFlight\n\tfor _, p := range ackedPackets {\n\t\tb.bytesInFlight -= p.BytesAcked\n\t}\n\tfor _, p := range lostPackets {\n\t\tb.bytesInFlight -= p.BytesLost\n\t}\n\n\tif len(ackedPackets) != 0 {\n\t\tlastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber\n\t\tisRoundStart = b.updateRoundTripCounter(lastAckedPacket)\n\t\tb.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart)\n\t}\n\n\tsample := b.sampler.OnCongestionEvent(eventTime,\n\t\tackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount)\n\tif sample.lastPacketSendState.isValid {\n\t\tb.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited\n\t\tb.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited\n\t}\n\t// Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all\n\t// packets in |acked_packets| did not generate valid samples. (e.g. ack of\n\t// ack-only packets). In both cases, sampler_.total_bytes_acked() will not\n\t// change.\n\tif totalBytesAckedBefore != b.sampler.TotalBytesAcked() {\n\t\tif !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() {\n\t\t\tb.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount)\n\t\t}\n\t}\n\n\tif sample.sampleRtt != infRTT {\n\t\tminRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt)\n\t}\n\tbytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore\n\n\texcessAcked = sample.extraAcked\n\tlastPacketSendState = sample.lastPacketSendState\n\n\tif len(lostPackets) != 0 {\n\t\tb.numLossEventsInRound++\n\t\tb.bytesLostInRound += bytesLost\n\t}\n\n\t// Handle logic specific to PROBE_BW mode.\n\tif b.mode == bbrModeProbeBw {\n\t\tb.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0)\n\t}\n\n\t// Handle logic specific to STARTUP and DRAIN modes.\n\tif isRoundStart && !b.isAtFullBandwidth {\n\t\tb.checkIfFullBandwidthReached(&lastPacketSendState)\n\t}\n\n\tb.maybeExitStartupOrDrain(eventTime)\n\n\t// Handle logic specific to PROBE_RTT.\n\tb.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired)\n\n\t// Calculate number of packets acked and lost.\n\tbytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore\n\n\t// After the model is updated, recalculate the pacing rate and congestion\n\t// window.\n\tb.calculatePacingRate(bytesLost)\n\tb.calculateCongestionWindow(bytesAcked, excessAcked)\n\tb.calculateRecoveryWindow(bytesAcked, bytesLost)\n\n\t// Cleanup internal state.\n\t// This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler.\n\t// The \"least unacked\" should actually be FirstOutstanding, but since we are not passing\n\t// that through OnCongestionEventEx, we will only do an estimate using acked/lost packets\n\t// for now. Because of fast retransmission, they should differ by no more than 2 packets.\n\t// (this is controlled by packetThreshold in quic-go's sentPacketHandler)\n\tvar leastUnacked congestion.PacketNumber\n\tif len(ackedPackets) != 0 {\n\t\tleastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2\n\t} else {\n\t\tleastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1\n\t}\n\tb.sampler.RemoveObsoletePackets(leastUnacked)\n\n\tif isRoundStart {\n\t\tb.numLossEventsInRound = 0\n\t\tb.bytesLostInRound = 0\n\t}\n}\n\nfunc (b *bbrSender) PacingRate() Bandwidth {\n\tif b.pacingRate == 0 {\n\t\treturn Bandwidth(b.highGain * float64(\n\t\t\tBandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt())))\n\t}\n\n\treturn b.pacingRate\n}\n\nfunc (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool {\n\treturn b.hasNonAppLimitedSample()\n}\n\nfunc (b *bbrSender) hasNonAppLimitedSample() bool {\n\treturn b.hasNoAppLimitedSample\n}\n\n// Sets the pacing gain used in STARTUP.  Must be greater than 1.\nfunc (b *bbrSender) setHighGain(highGain float64) {\n\tb.highGain = highGain\n\tif b.mode == bbrModeStartup {\n\t\tb.pacingGain = highGain\n\t}\n}\n\n// Sets the CWND gain used in STARTUP.  Must be greater than 1.\nfunc (b *bbrSender) setHighCwndGain(highCwndGain float64) {\n\tb.highCwndGain = highCwndGain\n\tif b.mode == bbrModeStartup {\n\t\tb.congestionWindowGain = highCwndGain\n\t}\n}\n\n// Sets the gain used in DRAIN.  Must be less than 1.\nfunc (b *bbrSender) setDrainGain(drainGain float64) {\n\tb.drainGain = drainGain\n}\n\n// Get the current bandwidth estimate. Note that Bandwidth is in bits per second.\nfunc (b *bbrSender) bandwidthEstimate() Bandwidth {\n\treturn b.maxBandwidth.GetBest()\n}\n\nfunc (b *bbrSender) bandwidthForPacer() congestion.ByteCount {\n\tbps := congestion.ByteCount(float64(b.bandwidthEstimate()) * b.congestionWindowGain / float64(BytesPerSecond))\n\tif bps < minBps {\n\t\t// We need to make sure that the bandwidth value for pacer is never zero,\n\t\t// otherwise it will go into an edge case where HasPacingBudget = false\n\t\t// but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck.\n\t\treturn minBps\n\t}\n\treturn bps\n}\n\n// Returns the current estimate of the RTT of the connection.  Outside of the\n// edge cases, this is minimum RTT.\nfunc (b *bbrSender) getMinRtt() time.Duration {\n\tif b.minRtt != 0 {\n\t\treturn b.minRtt\n\t}\n\t// min_rtt could be available if the handshake packet gets neutered then\n\t// gets acknowledged. This could only happen for QUIC crypto where we do not\n\t// drop keys.\n\tminRtt := b.rttStats.MinRTT()\n\tif minRtt == 0 {\n\t\treturn 100 * time.Millisecond\n\t} else {\n\t\treturn minRtt\n\t}\n}\n\n// Computes the target congestion window using the specified gain.\nfunc (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount {\n\tbdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate())\n\tcongestionWindow := congestion.ByteCount(gain * float64(bdp))\n\n\t// BDP estimate will be zero if no bandwidth samples are available yet.\n\tif congestionWindow == 0 {\n\t\tcongestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow))\n\t}\n\n\treturn max(congestionWindow, b.minCongestionWindow)\n}\n\n// The target congestion window during PROBE_RTT.\nfunc (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount {\n\treturn b.minCongestionWindow\n}\n\nfunc (b *bbrSender) maybeUpdateMinRtt(now time.Time, sampleMinRtt time.Duration) bool {\n\t// Do not expire min_rtt if none was ever available.\n\tminRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry))\n\tif minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 {\n\t\tb.minRtt = sampleMinRtt\n\t\tb.minRttTimestamp = now\n\t}\n\n\treturn minRttExpired\n}\n\n// Enters the STARTUP mode.\nfunc (b *bbrSender) enterStartupMode(now time.Time) {\n\tb.mode = bbrModeStartup\n\t// b.maybeTraceStateChange(logging.CongestionStateStartup)\n\tb.pacingGain = b.highGain\n\tb.congestionWindowGain = b.highCwndGain\n\n\tif b.debug {\n\t\tb.debugPrint(\"Phase: STARTUP\")\n\t}\n}\n\n// Enters the PROBE_BW mode.\nfunc (b *bbrSender) enterProbeBandwidthMode(now time.Time) {\n\tb.mode = bbrModeProbeBw\n\t// b.maybeTraceStateChange(logging.CongestionStateProbeBw)\n\tb.congestionWindowGain = b.congestionWindowGainConstant\n\n\t// Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is\n\t// excluded because in that case increased gain and decreased gain would not\n\t// follow each other.\n\tb.cycleCurrentOffset = int(rand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1)\n\tif b.cycleCurrentOffset >= 1 {\n\t\tb.cycleCurrentOffset += 1\n\t}\n\n\tb.lastCycleStart = now\n\tb.pacingGain = pacingGain[b.cycleCurrentOffset]\n\n\tif b.debug {\n\t\tb.debugPrint(\"Phase: PROBE_BW\")\n\t}\n}\n\n// Updates the round-trip counter if a round-trip has passed.  Returns true if\n// the counter has been advanced.\nfunc (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool {\n\tif b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd {\n\t\tb.roundTripCount++\n\t\tb.currentRoundTripEnd = b.lastSentPacket\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Updates the current gain used in PROBE_BW mode.\nfunc (b *bbrSender) updateGainCyclePhase(now time.Time, priorInFlight congestion.ByteCount, hasLosses bool) {\n\t// In most cases, the cycle is advanced after an RTT passes.\n\tshouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt()))\n\t// If the pacing gain is above 1.0, the connection is trying to probe the\n\t// bandwidth by increasing the number of bytes in flight to at least\n\t// pacing_gain * BDP.  Make sure that it actually reaches the target, as long\n\t// as there are no losses suggesting that the buffers are not able to hold\n\t// that much.\n\tif b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) {\n\t\tshouldAdvanceGainCycling = false\n\t}\n\n\t// If pacing gain is below 1.0, the connection is trying to drain the extra\n\t// queue which could have been incurred by probing prior to it.  If the number\n\t// of bytes in flight falls down to the estimated BDP value earlier, conclude\n\t// that the queue has been successfully drained and exit this cycle early.\n\tif b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) {\n\t\tshouldAdvanceGainCycling = true\n\t}\n\n\tif shouldAdvanceGainCycling {\n\t\tb.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength\n\t\tb.lastCycleStart = now\n\t\t// Stay in low gain mode until the target BDP is hit.\n\t\t// Low gain mode will be exited immediately when the target BDP is achieved.\n\t\tif b.drainToTarget && b.pacingGain < 1 &&\n\t\t\tpacingGain[b.cycleCurrentOffset] == 1 &&\n\t\t\tb.bytesInFlight > b.getTargetCongestionWindow(1) {\n\t\t\treturn\n\t\t}\n\t\tb.pacingGain = pacingGain[b.cycleCurrentOffset]\n\t}\n}\n\n// Tracks for how many round-trips the bandwidth has not increased\n// significantly.\nfunc (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) {\n\tif b.lastSampleIsAppLimited {\n\t\treturn\n\t}\n\n\ttarget := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget)\n\tif b.bandwidthEstimate() >= target {\n\t\tb.bandwidthAtLastRound = b.bandwidthEstimate()\n\t\tb.roundsWithoutBandwidthGain = 0\n\t\tif b.expireAckAggregationInStartup {\n\t\t\t// Expire old excess delivery measurements now that bandwidth increased.\n\t\t\tb.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount)\n\t\t}\n\t\treturn\n\t}\n\n\tb.roundsWithoutBandwidthGain++\n\tif b.roundsWithoutBandwidthGain >= b.numStartupRtts ||\n\t\tb.shouldExitStartupDueToLoss(lastPacketSendState) {\n\t\tb.isAtFullBandwidth = true\n\t}\n}\n\nfunc (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) {\n\tif bytesInFlight < b.getTargetCongestionWindow(1) {\n\t\tb.sampler.OnAppLimited()\n\t}\n}\n\n// Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if\n// appropriate.\nfunc (b *bbrSender) maybeExitStartupOrDrain(now time.Time) {\n\tif b.mode == bbrModeStartup && b.isAtFullBandwidth {\n\t\tb.mode = bbrModeDrain\n\t\t// b.maybeTraceStateChange(logging.CongestionStateDrain)\n\t\tb.pacingGain = b.drainGain\n\t\tb.congestionWindowGain = b.highCwndGain\n\n\t\tif b.debug {\n\t\t\tb.debugPrint(\"Phase: DRAIN\")\n\t\t}\n\t}\n\tif b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) {\n\t\tb.enterProbeBandwidthMode(now)\n\t}\n}\n\n// Decides whether to enter or exit PROBE_RTT.\nfunc (b *bbrSender) maybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRttExpired bool) {\n\tif minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt {\n\t\tb.mode = bbrModeProbeRtt\n\t\t// b.maybeTraceStateChange(logging.CongestionStateProbRtt)\n\t\tb.pacingGain = 1.0\n\t\t// Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight|\n\t\t// is at the target small value.\n\t\tb.exitProbeRttAt = time.Time{}\n\n\t\tif b.debug {\n\t\t\tb.debugPrint(\"BandwidthEstimate: %s, CongestionWindowGain: %.2f, PacingGain: %.2f, PacingRate: %s\",\n\t\t\t\tformatSpeed(b.bandwidthEstimate()), b.congestionWindowGain, b.pacingGain, formatSpeed(b.PacingRate()))\n\t\t\tb.debugPrint(\"Phase: PROBE_RTT\")\n\t\t}\n\t}\n\n\tif b.mode == bbrModeProbeRtt {\n\t\tb.sampler.OnAppLimited()\n\t\t// b.maybeTraceStateChange(logging.CongestionStateApplicationLimited)\n\n\t\tif b.exitProbeRttAt.IsZero() {\n\t\t\t// If the window has reached the appropriate size, schedule exiting\n\t\t\t// PROBE_RTT.  The CWND during PROBE_RTT is kMinimumCongestionWindow, but\n\t\t\t// we allow an extra packet since QUIC checks CWND before sending a\n\t\t\t// packet.\n\t\t\tif b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize {\n\t\t\t\tb.exitProbeRttAt = now.Add(probeRttTime)\n\t\t\t\tb.probeRttRoundPassed = false\n\t\t\t}\n\t\t} else {\n\t\t\tif isRoundStart {\n\t\t\t\tb.probeRttRoundPassed = true\n\t\t\t}\n\t\t\tif now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed {\n\t\t\t\tb.minRttTimestamp = now\n\t\t\t\tif b.debug {\n\t\t\t\t\tb.debugPrint(\"MinRTT: %s\", b.getMinRtt())\n\t\t\t\t}\n\t\t\t\tif !b.isAtFullBandwidth {\n\t\t\t\t\tb.enterStartupMode(now)\n\t\t\t\t} else {\n\t\t\t\t\tb.enterProbeBandwidthMode(now)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tb.exitingQuiescence = false\n}\n\n// Determines whether BBR needs to enter, exit or advance state of the\n// recovery.\nfunc (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) {\n\t// Disable recovery in startup, if loss-based exit is enabled.\n\tif !b.isAtFullBandwidth {\n\t\treturn\n\t}\n\n\t// Exit recovery when there are no losses for a round.\n\tif hasLosses {\n\t\tb.endRecoveryAt = b.lastSentPacket\n\t}\n\n\tswitch b.recoveryState {\n\tcase bbrRecoveryStateNotInRecovery:\n\t\tif hasLosses {\n\t\t\tb.recoveryState = bbrRecoveryStateConservation\n\t\t\t// This will cause the |recovery_window_| to be set to the correct\n\t\t\t// value in CalculateRecoveryWindow().\n\t\t\tb.recoveryWindow = 0\n\t\t\t// Since the conservation phase is meant to be lasting for a whole\n\t\t\t// round, extend the current round as if it were started right now.\n\t\t\tb.currentRoundTripEnd = b.lastSentPacket\n\t\t}\n\tcase bbrRecoveryStateConservation:\n\t\tif isRoundStart {\n\t\t\tb.recoveryState = bbrRecoveryStateGrowth\n\t\t}\n\t\tfallthrough\n\tcase bbrRecoveryStateGrowth:\n\t\t// Exit recovery if appropriate.\n\t\tif !hasLosses && lastAckedPacket > b.endRecoveryAt {\n\t\t\tb.recoveryState = bbrRecoveryStateNotInRecovery\n\t\t}\n\t}\n}\n\n// Determines the appropriate pacing rate for the connection.\nfunc (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) {\n\tif b.bandwidthEstimate() == 0 {\n\t\treturn\n\t}\n\n\ttargetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate()))\n\tif b.isAtFullBandwidth {\n\t\tb.pacingRate = targetRate\n\t\treturn\n\t}\n\n\t// Pace at the rate of initial_window / RTT as soon as RTT measurements are\n\t// available.\n\tif b.pacingRate == 0 && b.rttStats.MinRTT() != 0 {\n\t\tb.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT())\n\t\treturn\n\t}\n\n\tif b.detectOvershooting {\n\t\tb.bytesLostWhileDetectingOvershooting += bytesLost\n\t\t// Check for overshooting with network parameters adjusted when pacing rate\n\t\t// > target_rate and loss has been detected.\n\t\tif b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 {\n\t\t\tif b.hasNoAppLimitedSample ||\n\t\t\t\tb.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow {\n\t\t\t\t// We are fairly sure overshoot happens if 1) there is at least one\n\t\t\t\t// non app-limited bw sample or 2) half of IW gets lost. Slow pacing\n\t\t\t\t// rate.\n\t\t\t\tb.pacingRate = max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT()))\n\t\t\t\tb.bytesLostWhileDetectingOvershooting = 0\n\t\t\t\tb.detectOvershooting = false\n\t\t\t}\n\t\t}\n\t}\n\n\t// Do not decrease the pacing rate during startup.\n\tb.pacingRate = max(b.pacingRate, targetRate)\n}\n\n// Determines the appropriate congestion window for the connection.\nfunc (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) {\n\tif b.mode == bbrModeProbeRtt {\n\t\treturn\n\t}\n\n\ttargetWindow := b.getTargetCongestionWindow(b.congestionWindowGain)\n\tif b.isAtFullBandwidth {\n\t\t// Add the max recently measured ack aggregation to CWND.\n\t\ttargetWindow += b.sampler.MaxAckHeight()\n\t} else if b.enableAckAggregationDuringStartup {\n\t\t// Add the most recent excess acked.  Because CWND never decreases in\n\t\t// STARTUP, this will automatically create a very localized max filter.\n\t\ttargetWindow += excessAcked\n\t}\n\n\t// Instead of immediately setting the target CWND as the new one, BBR grows\n\t// the CWND towards |target_window| by only increasing it |bytes_acked| at a\n\t// time.\n\tif b.isAtFullBandwidth {\n\t\tb.congestionWindow = min(targetWindow, b.congestionWindow+bytesAcked)\n\t} else if b.congestionWindow < targetWindow ||\n\t\tb.sampler.TotalBytesAcked() < b.initialCongestionWindow {\n\t\t// If the connection is not yet out of startup phase, do not decrease the\n\t\t// window.\n\t\tb.congestionWindow += bytesAcked\n\t}\n\n\t// Enforce the limits on the congestion window.\n\tb.congestionWindow = max(b.congestionWindow, b.minCongestionWindow)\n\tb.congestionWindow = min(b.congestionWindow, b.maxCongestionWindow)\n}\n\n// Determines the appropriate window that constrains the in-flight during recovery.\nfunc (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) {\n\tif b.recoveryState == bbrRecoveryStateNotInRecovery {\n\t\treturn\n\t}\n\n\t// Set up the initial recovery window.\n\tif b.recoveryWindow == 0 {\n\t\tb.recoveryWindow = b.bytesInFlight + bytesAcked\n\t\tb.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow)\n\t\treturn\n\t}\n\n\t// Remove losses from the recovery window, while accounting for a potential\n\t// integer underflow.\n\tif b.recoveryWindow >= bytesLost {\n\t\tb.recoveryWindow = b.recoveryWindow - bytesLost\n\t} else {\n\t\tb.recoveryWindow = b.maxDatagramSize\n\t}\n\n\t// In CONSERVATION mode, just subtracting losses is sufficient.  In GROWTH,\n\t// release additional |bytes_acked| to achieve a slow-start-like behavior.\n\tif b.recoveryState == bbrRecoveryStateGrowth {\n\t\tb.recoveryWindow += bytesAcked\n\t}\n\n\t// Always allow sending at least |bytes_acked| in response.\n\tb.recoveryWindow = max(b.recoveryWindow, b.bytesInFlight+bytesAcked)\n\tb.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow)\n}\n\n// Return whether we should exit STARTUP due to excessive loss.\nfunc (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool {\n\tif b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid {\n\t\treturn false\n\t}\n\n\tinflightAtSend := lastPacketSendState.bytesInFlight\n\n\tif inflightAtSend > 0 && b.bytesLostInRound > 0 {\n\t\tif b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\treturn false\n}\n\nfunc (b *bbrSender) debugPrint(format string, a ...any) {\n\tfmt.Printf(\"[BBRSender] [%s] %s\\n\",\n\t\ttime.Now().Format(\"15:04:05\"),\n\t\tfmt.Sprintf(format, a...))\n}\n\nfunc bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount {\n\treturn congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second)\n}\n\nfunc GetInitialPacketSize(addr net.Addr) congestion.ByteCount {\n\t// If this is not a UDP address, we don't know anything about the MTU.\n\t// Use the minimum size of an Initial packet as the max packet size.\n\tif udpAddr, ok := addr.(*net.UDPAddr); ok {\n\t\tif udpAddr.IP.To4() != nil {\n\t\t\treturn congestion.InitialPacketSizeIPv4\n\t\t} else {\n\t\t\treturn congestion.InitialPacketSizeIPv6\n\t\t}\n\t} else {\n\t\treturn congestion.MinInitialPacketSize\n\t}\n}\n\nfunc formatSpeed(bw Bandwidth) string {\n\tbwf := float64(bw)\n\tunits := []string{\"bps\", \"Kbps\", \"Mbps\", \"Gbps\"}\n\tunitIndex := 0\n\tfor bwf > 1000 && unitIndex < len(units)-1 {\n\t\tbwf /= 1000\n\t\tunitIndex++\n\t}\n\treturn fmt.Sprintf(\"%.2f %s\", bwf, units[unitIndex])\n}\n"
  },
  {
    "path": "core/internal/congestion/bbr/clock.go",
    "content": "package bbr\n\nimport \"time\"\n\n// A Clock returns the current time\ntype Clock interface {\n\tNow() time.Time\n}\n\n// DefaultClock implements the Clock interface using the Go stdlib clock.\ntype DefaultClock struct{}\n\nvar _ Clock = DefaultClock{}\n\n// Now gets the current time\nfunc (DefaultClock) Now() time.Time {\n\treturn time.Now()\n}\n"
  },
  {
    "path": "core/internal/congestion/bbr/packet_number_indexed_queue.go",
    "content": "package bbr\n\nimport (\n\t\"github.com/apernet/quic-go/congestion\"\n)\n\n// packetNumberIndexedQueue is a queue of mostly continuous numbered entries\n// which supports the following operations:\n// - adding elements to the end of the queue, or at some point past the end\n// - removing elements in any order\n// - retrieving elements\n// If all elements are inserted in order, all of the operations above are\n// amortized O(1) time.\n//\n// Internally, the data structure is a deque where each element is marked as\n// present or not.  The deque starts at the lowest present index.  Whenever an\n// element is removed, it's marked as not present, and the front of the deque is\n// cleared of elements that are not present.\n//\n// The tail of the queue is not cleared due to the assumption of entries being\n// inserted in order, though removing all elements of the queue will return it\n// to its initial state.\n//\n// Note that this data structure is inherently hazardous, since an addition of\n// just two entries will cause it to consume all of the memory available.\n// Because of that, it is not a general-purpose container and should not be used\n// as one.\n\ntype entryWrapper[T any] struct {\n\tpresent bool\n\tentry   T\n}\n\ntype packetNumberIndexedQueue[T any] struct {\n\tentries                RingBuffer[entryWrapper[T]]\n\tnumberOfPresentEntries int\n\tfirstPacket            congestion.PacketNumber\n}\n\nfunc newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] {\n\tq := &packetNumberIndexedQueue[T]{\n\t\tfirstPacket: invalidPacketNumber,\n\t}\n\n\tq.entries.Init(size)\n\n\treturn q\n}\n\n// Emplace inserts data associated |packet_number| into (or past) the end of the\n// queue, filling up the missing intermediate entries as necessary.  Returns\n// true if the element has been inserted successfully, false if it was already\n// in the queue or inserted out of order.\nfunc (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool {\n\tif packetNumber == invalidPacketNumber || entry == nil {\n\t\treturn false\n\t}\n\n\tif p.IsEmpty() {\n\t\tp.entries.PushBack(entryWrapper[T]{\n\t\t\tpresent: true,\n\t\t\tentry:   *entry,\n\t\t})\n\t\tp.numberOfPresentEntries = 1\n\t\tp.firstPacket = packetNumber\n\t\treturn true\n\t}\n\n\t// Do not allow insertion out-of-order.\n\tif packetNumber <= p.LastPacket() {\n\t\treturn false\n\t}\n\n\t// Handle potentially missing elements.\n\toffset := int(packetNumber - p.FirstPacket())\n\tif gap := offset - p.entries.Len(); gap > 0 {\n\t\tfor i := 0; i < gap; i++ {\n\t\t\tp.entries.PushBack(entryWrapper[T]{})\n\t\t}\n\t}\n\n\tp.entries.PushBack(entryWrapper[T]{\n\t\tpresent: true,\n\t\tentry:   *entry,\n\t})\n\tp.numberOfPresentEntries++\n\treturn true\n}\n\n// GetEntry Retrieve the entry associated with the packet number.  Returns the pointer\n// to the entry in case of success, or nullptr if the entry does not exist.\nfunc (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T {\n\tew := p.getEntryWraper(packetNumber)\n\tif ew == nil {\n\t\treturn nil\n\t}\n\n\treturn &ew.entry\n}\n\n// Remove, Same as above, but if an entry is present in the queue, also call f(entry)\n// before removing it.\nfunc (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool {\n\tew := p.getEntryWraper(packetNumber)\n\tif ew == nil {\n\t\treturn false\n\t}\n\tif f != nil {\n\t\tf(ew.entry)\n\t}\n\tew.present = false\n\tp.numberOfPresentEntries--\n\n\tif packetNumber == p.FirstPacket() {\n\t\tp.clearup()\n\t}\n\n\treturn true\n}\n\n// RemoveUpTo, but not including |packet_number|.\n// Unused slots in the front are also removed, which means when the function\n// returns, |first_packet()| can be larger than |packet_number|.\nfunc (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) {\n\tfor !p.entries.Empty() &&\n\t\tp.firstPacket != invalidPacketNumber &&\n\t\tp.firstPacket < packetNumber {\n\t\tif p.entries.Front().present {\n\t\t\tp.numberOfPresentEntries--\n\t\t}\n\t\tp.entries.PopFront()\n\t\tp.firstPacket++\n\t}\n\tp.clearup()\n\n\treturn\n}\n\n// IsEmpty return if queue is empty.\nfunc (p *packetNumberIndexedQueue[T]) IsEmpty() bool {\n\treturn p.numberOfPresentEntries == 0\n}\n\n// NumberOfPresentEntries returns the number of entries in the queue.\nfunc (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int {\n\treturn p.numberOfPresentEntries\n}\n\n// EntrySlotsUsed returns the number of entries allocated in the underlying deque.  This is\n// proportional to the memory usage of the queue.\nfunc (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int {\n\treturn p.entries.Len()\n}\n\n// LastPacket returns packet number of the first entry in the queue.\nfunc (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) {\n\treturn p.firstPacket\n}\n\n// LastPacket returns packet number of the last entry ever inserted in the queue.  Note that the\n// entry in question may have already been removed.  Zero if the queue is\n// empty.\nfunc (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) {\n\tif p.IsEmpty() {\n\t\treturn invalidPacketNumber\n\t}\n\n\treturn p.firstPacket + congestion.PacketNumber(p.entries.Len()-1)\n}\n\nfunc (p *packetNumberIndexedQueue[T]) clearup() {\n\tfor !p.entries.Empty() && !p.entries.Front().present {\n\t\tp.entries.PopFront()\n\t\tp.firstPacket++\n\t}\n\tif p.entries.Empty() {\n\t\tp.firstPacket = invalidPacketNumber\n\t}\n}\n\nfunc (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] {\n\tif packetNumber == invalidPacketNumber ||\n\t\tp.IsEmpty() ||\n\t\tpacketNumber < p.firstPacket {\n\t\treturn nil\n\t}\n\n\toffset := int(packetNumber - p.firstPacket)\n\tif offset >= p.entries.Len() {\n\t\treturn nil\n\t}\n\n\tew := p.entries.Offset(offset)\n\tif ew == nil || !ew.present {\n\t\treturn nil\n\t}\n\n\treturn ew\n}\n"
  },
  {
    "path": "core/internal/congestion/bbr/ringbuffer.go",
    "content": "package bbr\n\n// A RingBuffer is a ring buffer.\n// It acts as a heap that doesn't cause any allocations.\ntype RingBuffer[T any] struct {\n\tring             []T\n\theadPos, tailPos int\n\tfull             bool\n}\n\n// Init preallocs a buffer with a certain size.\nfunc (r *RingBuffer[T]) Init(size int) {\n\tr.ring = make([]T, size)\n}\n\n// Len returns the number of elements in the ring buffer.\nfunc (r *RingBuffer[T]) Len() int {\n\tif r.full {\n\t\treturn len(r.ring)\n\t}\n\tif r.tailPos >= r.headPos {\n\t\treturn r.tailPos - r.headPos\n\t}\n\treturn r.tailPos - r.headPos + len(r.ring)\n}\n\n// Empty says if the ring buffer is empty.\nfunc (r *RingBuffer[T]) Empty() bool {\n\treturn !r.full && r.headPos == r.tailPos\n}\n\n// PushBack adds a new element.\n// If the ring buffer is full, its capacity is increased first.\nfunc (r *RingBuffer[T]) PushBack(t T) {\n\tif r.full || len(r.ring) == 0 {\n\t\tr.grow()\n\t}\n\tr.ring[r.tailPos] = t\n\tr.tailPos++\n\tif r.tailPos == len(r.ring) {\n\t\tr.tailPos = 0\n\t}\n\tif r.tailPos == r.headPos {\n\t\tr.full = true\n\t}\n}\n\n// PopFront returns the next element.\n// It must not be called when the buffer is empty, that means that\n// callers might need to check if there are elements in the buffer first.\nfunc (r *RingBuffer[T]) PopFront() T {\n\tif r.Empty() {\n\t\tpanic(\"github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue\")\n\t}\n\tr.full = false\n\tt := r.ring[r.headPos]\n\tr.ring[r.headPos] = *new(T)\n\tr.headPos++\n\tif r.headPos == len(r.ring) {\n\t\tr.headPos = 0\n\t}\n\treturn t\n}\n\n// Offset returns the offset element.\n// It must not be called when the buffer is empty, that means that\n// callers might need to check if there are elements in the buffer first\n// and check if the index larger than buffer length.\nfunc (r *RingBuffer[T]) Offset(index int) *T {\n\tif r.Empty() || index >= r.Len() {\n\t\tpanic(\"github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index\")\n\t}\n\toffset := (r.headPos + index) % len(r.ring)\n\treturn &r.ring[offset]\n}\n\n// Front returns the front element.\n// It must not be called when the buffer is empty, that means that\n// callers might need to check if there are elements in the buffer first.\nfunc (r *RingBuffer[T]) Front() *T {\n\tif r.Empty() {\n\t\tpanic(\"github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue\")\n\t}\n\treturn &r.ring[r.headPos]\n}\n\n// Back returns the back element.\n// It must not be called when the buffer is empty, that means that\n// callers might need to check if there are elements in the buffer first.\nfunc (r *RingBuffer[T]) Back() *T {\n\tif r.Empty() {\n\t\tpanic(\"github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue\")\n\t}\n\treturn r.Offset(r.Len() - 1)\n}\n\n// Grow the maximum size of the queue.\n// This method assume the queue is full.\nfunc (r *RingBuffer[T]) grow() {\n\toldRing := r.ring\n\tnewSize := len(oldRing) * 2\n\tif newSize == 0 {\n\t\tnewSize = 1\n\t}\n\tr.ring = make([]T, newSize)\n\theadLen := copy(r.ring, oldRing[r.headPos:])\n\tcopy(r.ring[headLen:], oldRing[:r.headPos])\n\tr.headPos, r.tailPos, r.full = 0, len(oldRing), false\n}\n\n// Clear removes all elements.\nfunc (r *RingBuffer[T]) Clear() {\n\tvar zeroValue T\n\tfor i := range r.ring {\n\t\tr.ring[i] = zeroValue\n\t}\n\tr.headPos, r.tailPos, r.full = 0, 0, false\n}\n"
  },
  {
    "path": "core/internal/congestion/bbr/windowed_filter.go",
    "content": "package bbr\n\nimport (\n\t\"golang.org/x/exp/constraints\"\n)\n\n// Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum)\n// estimate of a stream of samples over some fixed time interval. (E.g.,\n// the minimum RTT over the past five minutes.) The algorithm keeps track of\n// the best, second best, and third best min (or max) estimates, maintaining an\n// invariant that the measurement time of the n'th best >= n-1'th best.\n\n// The algorithm works as follows. On a reset, all three estimates are set to\n// the same sample. The second best estimate is then recorded in the second\n// quarter of the window, and a third best estimate is recorded in the second\n// half of the window, bounding the worst case error when the true min is\n// monotonically increasing (or true max is monotonically decreasing) over the\n// window.\n//\n// A new best sample replaces all three estimates, since the new best is lower\n// (or higher) than everything else in the window and it is the most recent.\n// The window thus effectively gets reset on every new min. The same property\n// holds true for second best and third best estimates. Specifically, when a\n// sample arrives that is better than the second best but not better than the\n// best, it replaces the second and third best estimates but not the best\n// estimate. Similarly, a sample that is better than the third best estimate\n// but not the other estimates replaces only the third best estimate.\n//\n// Finally, when the best expires, it is replaced by the second best, which in\n// turn is replaced by the third best. The newest sample replaces the third\n// best.\n\ntype WindowedFilterValue interface {\n\tany\n}\n\ntype WindowedFilterTime interface {\n\tconstraints.Integer | constraints.Float\n}\n\ntype WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct {\n\t// Time length of window.\n\twindowLength T\n\testimates    []entry[V, T]\n\tcomparator   func(V, V) int\n}\n\ntype entry[V WindowedFilterValue, T WindowedFilterTime] struct {\n\tsample V\n\ttime   T\n}\n\n// Compares two values and returns true if the first is greater than or equal\n// to the second.\nfunc MaxFilter[O constraints.Ordered](a, b O) int {\n\tif a > b {\n\t\treturn 1\n\t} else if a < b {\n\t\treturn -1\n\t}\n\treturn 0\n}\n\n// Compares two values and returns true if the first is less than or equal\n// to the second.\nfunc MinFilter[O constraints.Ordered](a, b O) int {\n\tif a < b {\n\t\treturn 1\n\t} else if a > b {\n\t\treturn -1\n\t}\n\treturn 0\n}\n\nfunc NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] {\n\treturn &WindowedFilter[V, T]{\n\t\twindowLength: windowLength,\n\t\testimates:    make([]entry[V, T], 3, 3),\n\t\tcomparator:   comparator,\n\t}\n}\n\n// Changes the window length.  Does not update any current samples.\nfunc (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) {\n\tf.windowLength = windowLength\n}\n\nfunc (f *WindowedFilter[V, T]) GetBest() V {\n\treturn f.estimates[0].sample\n}\n\nfunc (f *WindowedFilter[V, T]) GetSecondBest() V {\n\treturn f.estimates[1].sample\n}\n\nfunc (f *WindowedFilter[V, T]) GetThirdBest() V {\n\treturn f.estimates[2].sample\n}\n\n// Updates best estimates with |sample|, and expires and updates best\n// estimates as necessary.\nfunc (f *WindowedFilter[V, T]) Update(newSample V, newTime T) {\n\t// Reset all estimates if they have not yet been initialized, if new sample\n\t// is a new best, or if the newest recorded estimate is too old.\n\tif f.comparator(f.estimates[0].sample, *new(V)) == 0 ||\n\t\tf.comparator(newSample, f.estimates[0].sample) >= 0 ||\n\t\tnewTime-f.estimates[2].time > f.windowLength {\n\t\tf.Reset(newSample, newTime)\n\t\treturn\n\t}\n\n\tif f.comparator(newSample, f.estimates[1].sample) >= 0 {\n\t\tf.estimates[1] = entry[V, T]{newSample, newTime}\n\t\tf.estimates[2] = f.estimates[1]\n\t} else if f.comparator(newSample, f.estimates[2].sample) >= 0 {\n\t\tf.estimates[2] = entry[V, T]{newSample, newTime}\n\t}\n\n\t// Expire and update estimates as necessary.\n\tif newTime-f.estimates[0].time > f.windowLength {\n\t\t// The best estimate hasn't been updated for an entire window, so promote\n\t\t// second and third best estimates.\n\t\tf.estimates[0] = f.estimates[1]\n\t\tf.estimates[1] = f.estimates[2]\n\t\tf.estimates[2] = entry[V, T]{newSample, newTime}\n\t\t// Need to iterate one more time. Check if the new best estimate is\n\t\t// outside the window as well, since it may also have been recorded a\n\t\t// long time ago. Don't need to iterate once more since we cover that\n\t\t// case at the beginning of the method.\n\t\tif newTime-f.estimates[0].time > f.windowLength {\n\t\t\tf.estimates[0] = f.estimates[1]\n\t\t\tf.estimates[1] = f.estimates[2]\n\t\t}\n\t\treturn\n\t}\n\tif f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 &&\n\t\tnewTime-f.estimates[1].time > f.windowLength/4 {\n\t\t// A quarter of the window has passed without a better sample, so the\n\t\t// second-best estimate is taken from the second quarter of the window.\n\t\tf.estimates[1] = entry[V, T]{newSample, newTime}\n\t\tf.estimates[2] = f.estimates[1]\n\t\treturn\n\t}\n\n\tif f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 &&\n\t\tnewTime-f.estimates[2].time > f.windowLength/2 {\n\t\t// We've passed a half of the window without a better estimate, so take\n\t\t// a third-best estimate from the second half of the window.\n\t\tf.estimates[2] = entry[V, T]{newSample, newTime}\n\t}\n}\n\n// Resets all estimates to new sample.\nfunc (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) {\n\tf.estimates[2] = entry[V, T]{newSample, newTime}\n\tf.estimates[1] = f.estimates[2]\n\tf.estimates[0] = f.estimates[1]\n}\n\nfunc (f *WindowedFilter[V, T]) Clear() {\n\tf.estimates = make([]entry[V, T], 3, 3)\n}\n"
  },
  {
    "path": "core/internal/congestion/brutal/brutal.go",
    "content": "package brutal\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/internal/congestion/common\"\n\n\t\"github.com/apernet/quic-go/congestion\"\n)\n\nconst (\n\tpktInfoSlotCount           = 5 // slot index is based on seconds, so this is basically how many seconds we sample\n\tminSampleCount             = 50\n\tminAckRate                 = 0.8\n\tcongestionWindowMultiplier = 2\n\n\tdebugEnv           = \"HYSTERIA_BRUTAL_DEBUG\"\n\tdebugPrintInterval = 2\n)\n\nvar _ congestion.CongestionControl = &BrutalSender{}\n\ntype BrutalSender struct {\n\trttStats        congestion.RTTStatsProvider\n\tbps             congestion.ByteCount\n\tmaxDatagramSize congestion.ByteCount\n\tpacer           *common.Pacer\n\n\tpktInfoSlots [pktInfoSlotCount]pktInfo\n\tackRate      float64\n\n\tdebug                 bool\n\tlastAckPrintTimestamp int64\n}\n\ntype pktInfo struct {\n\tTimestamp int64\n\tAckCount  uint64\n\tLossCount uint64\n}\n\nfunc NewBrutalSender(bps uint64) *BrutalSender {\n\tdebug, _ := strconv.ParseBool(os.Getenv(debugEnv))\n\tbs := &BrutalSender{\n\t\tbps:             congestion.ByteCount(bps),\n\t\tmaxDatagramSize: congestion.InitialPacketSizeIPv4,\n\t\tackRate:         1,\n\t\tdebug:           debug,\n\t}\n\tbs.pacer = common.NewPacer(func() congestion.ByteCount {\n\t\treturn congestion.ByteCount(float64(bs.bps) / bs.ackRate)\n\t})\n\treturn bs\n}\n\nfunc (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) {\n\tb.rttStats = rttStats\n}\n\nfunc (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time {\n\treturn b.pacer.TimeUntilSend()\n}\n\nfunc (b *BrutalSender) HasPacingBudget(now time.Time) bool {\n\treturn b.pacer.Budget(now) >= b.maxDatagramSize\n}\n\nfunc (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {\n\treturn bytesInFlight <= b.GetCongestionWindow()\n}\n\nfunc (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {\n\trtt := b.rttStats.SmoothedRTT()\n\tif rtt <= 0 {\n\t\treturn 10240\n\t}\n\tcwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate)\n\tif cwnd < b.maxDatagramSize {\n\t\tcwnd = b.maxDatagramSize\n\t}\n\treturn cwnd\n}\n\nfunc (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount,\n\tpacketNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool,\n) {\n\tb.pacer.SentPacket(sentTime, bytes)\n}\n\nfunc (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount,\n\tpriorInFlight congestion.ByteCount, eventTime time.Time,\n) {\n\t// Stub\n}\n\nfunc (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount,\n\tpriorInFlight congestion.ByteCount,\n) {\n\t// Stub\n}\n\nfunc (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {\n\tcurrentTimestamp := eventTime.Unix()\n\tslot := currentTimestamp % pktInfoSlotCount\n\tif b.pktInfoSlots[slot].Timestamp == currentTimestamp {\n\t\tb.pktInfoSlots[slot].LossCount += uint64(len(lostPackets))\n\t\tb.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets))\n\t} else {\n\t\t// uninitialized slot or too old, reset\n\t\tb.pktInfoSlots[slot].Timestamp = currentTimestamp\n\t\tb.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets))\n\t\tb.pktInfoSlots[slot].LossCount = uint64(len(lostPackets))\n\t}\n\tb.updateAckRate(currentTimestamp)\n}\n\nfunc (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {\n\tb.maxDatagramSize = size\n\tb.pacer.SetMaxDatagramSize(size)\n\tif b.debug {\n\t\tb.debugPrint(\"SetMaxDatagramSize: %d\", size)\n\t}\n}\n\nfunc (b *BrutalSender) updateAckRate(currentTimestamp int64) {\n\tminTimestamp := currentTimestamp - pktInfoSlotCount\n\tvar ackCount, lossCount uint64\n\tfor _, info := range b.pktInfoSlots {\n\t\tif info.Timestamp < minTimestamp {\n\t\t\tcontinue\n\t\t}\n\t\tackCount += info.AckCount\n\t\tlossCount += info.LossCount\n\t}\n\tif ackCount+lossCount < minSampleCount {\n\t\tb.ackRate = 1\n\t\tif b.canPrintAckRate(currentTimestamp) {\n\t\t\tb.lastAckPrintTimestamp = currentTimestamp\n\t\t\tb.debugPrint(\"Not enough samples (total=%d, ack=%d, loss=%d, rtt=%d)\",\n\t\t\t\tackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())\n\t\t}\n\t\treturn\n\t}\n\trate := float64(ackCount) / float64(ackCount+lossCount)\n\tif rate < minAckRate {\n\t\tb.ackRate = minAckRate\n\t\tif b.canPrintAckRate(currentTimestamp) {\n\t\t\tb.lastAckPrintTimestamp = currentTimestamp\n\t\t\tb.debugPrint(\"ACK rate too low: %.2f, clamped to %.2f (total=%d, ack=%d, loss=%d, rtt=%d)\",\n\t\t\t\trate, minAckRate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())\n\t\t}\n\t\treturn\n\t}\n\tb.ackRate = rate\n\tif b.canPrintAckRate(currentTimestamp) {\n\t\tb.lastAckPrintTimestamp = currentTimestamp\n\t\tb.debugPrint(\"ACK rate: %.2f (total=%d, ack=%d, loss=%d, rtt=%d)\",\n\t\t\trate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())\n\t}\n}\n\nfunc (b *BrutalSender) InSlowStart() bool {\n\treturn false\n}\n\nfunc (b *BrutalSender) InRecovery() bool {\n\treturn false\n}\n\nfunc (b *BrutalSender) MaybeExitSlowStart() {}\n\nfunc (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {}\n\nfunc (b *BrutalSender) canPrintAckRate(currentTimestamp int64) bool {\n\treturn b.debug && currentTimestamp-b.lastAckPrintTimestamp >= debugPrintInterval\n}\n\nfunc (b *BrutalSender) debugPrint(format string, a ...any) {\n\tfmt.Printf(\"[BrutalSender] [%s] %s\\n\",\n\t\ttime.Now().Format(\"15:04:05\"),\n\t\tfmt.Sprintf(format, a...))\n}\n"
  },
  {
    "path": "core/internal/congestion/common/pacer.go",
    "content": "package common\n\nimport (\n\t\"time\"\n\n\t\"github.com/apernet/quic-go/congestion\"\n)\n\nconst (\n\tmaxBurstPackets               = 10\n\tmaxBurstPacingDelayMultiplier = 4\n)\n\n// Pacer implements a token bucket pacing algorithm.\ntype Pacer struct {\n\tbudgetAtLastSent congestion.ByteCount\n\tmaxDatagramSize  congestion.ByteCount\n\tlastSentTime     time.Time\n\tgetBandwidth     func() congestion.ByteCount // in bytes/s\n}\n\nfunc NewPacer(getBandwidth func() congestion.ByteCount) *Pacer {\n\tp := &Pacer{\n\t\tbudgetAtLastSent: maxBurstPackets * congestion.InitialPacketSizeIPv4,\n\t\tmaxDatagramSize:  congestion.InitialPacketSizeIPv4,\n\t\tgetBandwidth:     getBandwidth,\n\t}\n\treturn p\n}\n\nfunc (p *Pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) {\n\tbudget := p.Budget(sendTime)\n\tif size > budget {\n\t\tp.budgetAtLastSent = 0\n\t} else {\n\t\tp.budgetAtLastSent = budget - size\n\t}\n\tp.lastSentTime = sendTime\n}\n\nfunc (p *Pacer) Budget(now time.Time) congestion.ByteCount {\n\tif p.lastSentTime.IsZero() {\n\t\treturn p.maxBurstSize()\n\t}\n\tbudget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9\n\tif budget < 0 { // protect against overflows\n\t\tbudget = congestion.ByteCount(1<<62 - 1)\n\t}\n\treturn min(p.maxBurstSize(), budget)\n}\n\nfunc (p *Pacer) maxBurstSize() congestion.ByteCount {\n\treturn max(\n\t\tcongestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9,\n\t\tmaxBurstPackets*p.maxDatagramSize,\n\t)\n}\n\n// TimeUntilSend returns when the next packet should be sent.\n// It returns the zero value of time.Time if a packet can be sent immediately.\nfunc (p *Pacer) TimeUntilSend() time.Time {\n\tif p.budgetAtLastSent >= p.maxDatagramSize {\n\t\treturn time.Time{}\n\t}\n\tdiff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent)\n\tbw := uint64(p.getBandwidth())\n\t// We might need to round up this value.\n\t// Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires.\n\td := diff / bw\n\t// this is effectively a math.Ceil, but using only integer math\n\tif diff%bw > 0 {\n\t\td++\n\t}\n\treturn p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond))\n}\n\nfunc (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) {\n\tp.maxDatagramSize = s\n}\n"
  },
  {
    "path": "core/internal/congestion/utils.go",
    "content": "package congestion\n\nimport (\n\t\"github.com/apernet/hysteria/core/v2/internal/congestion/bbr\"\n\t\"github.com/apernet/hysteria/core/v2/internal/congestion/brutal\"\n\t\"github.com/apernet/quic-go\"\n)\n\nfunc UseBBR(conn quic.Connection) {\n\tconn.SetCongestionControl(bbr.NewBbrSender(\n\t\tbbr.DefaultClock{},\n\t\tbbr.GetInitialPacketSize(conn.RemoteAddr()),\n\t))\n}\n\nfunc UseBrutal(conn quic.Connection, tx uint64) {\n\tconn.SetCongestionControl(brutal.NewBrutalSender(tx))\n}\n"
  },
  {
    "path": "core/internal/frag/frag.go",
    "content": "package frag\n\nimport (\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n)\n\nfunc FragUDPMessage(m *protocol.UDPMessage, maxSize int) []protocol.UDPMessage {\n\tif m.Size() <= maxSize {\n\t\treturn []protocol.UDPMessage{*m}\n\t}\n\tfullPayload := m.Data\n\tmaxPayloadSize := maxSize - m.HeaderSize()\n\toff := 0\n\tfragID := uint8(0)\n\tfragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up\n\tfrags := make([]protocol.UDPMessage, fragCount)\n\tfor off < len(fullPayload) {\n\t\tpayloadSize := len(fullPayload) - off\n\t\tif payloadSize > maxPayloadSize {\n\t\t\tpayloadSize = maxPayloadSize\n\t\t}\n\t\tfrag := *m\n\t\tfrag.FragID = fragID\n\t\tfrag.FragCount = fragCount\n\t\tfrag.Data = fullPayload[off : off+payloadSize]\n\t\tfrags[fragID] = frag\n\t\toff += payloadSize\n\t\tfragID++\n\t}\n\treturn frags\n}\n\n// Defragger handles the defragmentation of UDP messages.\n// The current implementation can only handle one packet ID at a time.\n// If another packet arrives before a packet has received all fragments\n// in their entirety, any previous state is discarded.\ntype Defragger struct {\n\tpktID uint16\n\tfrags []*protocol.UDPMessage\n\tcount uint8\n\tsize  int // data size\n}\n\nfunc (d *Defragger) Feed(m *protocol.UDPMessage) *protocol.UDPMessage {\n\tif m.FragCount <= 1 {\n\t\treturn m\n\t}\n\tif m.FragID >= m.FragCount {\n\t\t// wtf is this?\n\t\treturn nil\n\t}\n\tif m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {\n\t\t// new message, clear previous state\n\t\td.pktID = m.PacketID\n\t\td.frags = make([]*protocol.UDPMessage, m.FragCount)\n\t\td.frags[m.FragID] = m\n\t\td.count = 1\n\t\td.size = len(m.Data)\n\t} else if d.frags[m.FragID] == nil {\n\t\td.frags[m.FragID] = m\n\t\td.count++\n\t\td.size += len(m.Data)\n\t\tif int(d.count) == len(d.frags) {\n\t\t\t// all fragments received, assemble\n\t\t\tdata := make([]byte, d.size)\n\t\t\toff := 0\n\t\t\tfor _, frag := range d.frags {\n\t\t\t\toff += copy(data[off:], frag.Data)\n\t\t\t}\n\t\t\tm.Data = data\n\t\t\tm.FragID = 0\n\t\t\tm.FragCount = 1\n\t\t\treturn m\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/internal/frag/frag_test.go",
    "content": "package frag\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n)\n\nfunc TestFragUDPMessage(t *testing.T) {\n\ttype args struct {\n\t\tm       *protocol.UDPMessage\n\t\tmaxSize int\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant []protocol.UDPMessage\n\t}{\n\t\t{\n\t\t\t\"no frag\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 1,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"hello\"),\n\t\t\t\t},\n\t\t\t\t100,\n\t\t\t},\n\t\t\t[]protocol.UDPMessage{\n\t\t\t\t{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 1,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"hello\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"2 frags\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 1,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"hello\"),\n\t\t\t\t},\n\t\t\t\t20,\n\t\t\t},\n\t\t\t[]protocol.UDPMessage{\n\t\t\t\t{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"hel\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    1,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"lo\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"4 frags\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 1,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"abcdefgh\"),\n\t\t\t\t},\n\t\t\t\t19,\n\t\t\t},\n\t\t\t[]protocol.UDPMessage{\n\t\t\t\t{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 4,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"ab\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    1,\n\t\t\t\t\tFragCount: 4,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"cd\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    2,\n\t\t\t\t\tFragCount: 4,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"ef\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  123,\n\t\t\t\t\tFragID:    3,\n\t\t\t\t\tFragCount: 4,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"gh\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := FragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"FragUDPMessage() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefragger(t *testing.T) {\n\ttype args struct {\n\t\tm *protocol.UDPMessage\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *protocol.UDPMessage\n\t}{\n\t\t{\n\t\t\t\"no frag\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  987,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 1,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"hello\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&protocol.UDPMessage{\n\t\t\t\tSessionID: 123,\n\t\t\t\tPacketID:  987,\n\t\t\t\tFragID:    0,\n\t\t\t\tFragCount: 1,\n\t\t\t\tAddr:      \"test:123\",\n\t\t\t\tData:      []byte(\"hello\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"frag 0 - 1/2\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  987,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"hello \"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"frag 0 - 2/2\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  987,\n\t\t\t\t\tFragID:    1,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"moto\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&protocol.UDPMessage{\n\t\t\t\tSessionID: 123,\n\t\t\t\tPacketID:  987,\n\t\t\t\tFragID:    0,\n\t\t\t\tFragCount: 1,\n\t\t\t\tAddr:      \"test:123\",\n\t\t\t\tData:      []byte(\"hello moto\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"frag 1 - 1/3\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  987,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 3,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"deco\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"frag 1 - 2/3\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  987,\n\t\t\t\t\tFragID:    1,\n\t\t\t\t\tFragCount: 3,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"*\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"frag 1 - 3/3\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  987,\n\t\t\t\t\tFragID:    2,\n\t\t\t\t\tFragCount: 3,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"27\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&protocol.UDPMessage{\n\t\t\t\tSessionID: 123,\n\t\t\t\tPacketID:  987,\n\t\t\t\tFragID:    0,\n\t\t\t\tFragCount: 1,\n\t\t\t\tAddr:      \"test:123\",\n\t\t\t\tData:      []byte(\"deco*27\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"frag 2 - 1/2\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  233,\n\t\t\t\t\tFragID:    1,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"shinsekai\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"frag 3 - 2/2\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  244,\n\t\t\t\t\tFragID:    1,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"what???\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"frag 2 - 2/2\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  233,\n\t\t\t\t\tFragID:    1,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\" annaijo\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"invalid id\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  233,\n\t\t\t\t\tFragID:    88,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"shinsekai\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"frag 2 - 1/2 re\",\n\t\t\targs{\n\t\t\t\t&protocol.UDPMessage{\n\t\t\t\t\tSessionID: 123,\n\t\t\t\t\tPacketID:  233,\n\t\t\t\t\tFragID:    0,\n\t\t\t\t\tFragCount: 2,\n\t\t\t\t\tAddr:      \"test:123\",\n\t\t\t\t\tData:      []byte(\"shinsekai\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\t&protocol.UDPMessage{\n\t\t\t\tSessionID: 123,\n\t\t\t\tPacketID:  233,\n\t\t\t\tFragID:    0,\n\t\t\t\tFragCount: 1,\n\t\t\t\tAddr:      \"test:123\",\n\t\t\t\tData:      []byte(\"shinsekai annaijo\"),\n\t\t\t},\n\t\t},\n\t}\n\n\td := &Defragger{}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Feed() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/internal/integration_tests/.mockery.yaml",
    "content": "with-expecter: true\ndir: mocks\noutpkg: mocks\npackages:\n  net:\n    interfaces:\n      Conn:\n        config:\n          mockname: MockConn\n  github.com/apernet/hysteria/core/v2/server:\n    interfaces:\n      Outbound:\n        config:\n          mockname: MockOutbound\n      UDPConn:\n        config:\n          mockname: MockUDPConn\n      Authenticator:\n        config:\n          mockname: MockAuthenticator\n      EventLogger:\n        config:\n          mockname: MockEventLogger\n      TrafficLogger:\n        config:\n          mockname: MockTrafficLogger\n      RequestHook:\n        config:\n          mockname: MockRequestHook"
  },
  {
    "path": "core/internal/integration_tests/close_test.go",
    "content": "package integration_tests\n\nimport (\n\t\"io\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n\t\"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks\"\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\n// TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly.\n// Closing one side of the connection should close the other side as well.\nfunc TestClientServerTCPClose(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tserverOb := mocks.NewMockOutbound(t)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tOutbound:      serverOb,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\taddr := \"hi-and-goodbye:2333\"\n\n\t// Test close from client side:\n\t// Client creates a connection, writes something, then closes it.\n\t// Server outbound connection should write the same thing, then close.\n\tsobConn := mocks.NewMockConn(t)\n\tsobConnCh := make(chan struct{}) // For close signal only\n\tsobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })\n\tsobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {\n\t\t<-sobConnCh\n\t\treturn 0, io.EOF\n\t})\n\tsobConn.EXPECT().Write([]byte(\"happy\")).Return(5, nil)\n\tsobConn.EXPECT().Close().RunAndReturn(func() error {\n\t\tsobConnChCloseFunc()\n\t\treturn nil\n\t})\n\tserverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()\n\tconn, err := c.TCP(addr)\n\tassert.NoError(t, err)\n\t_, err = conn.Write([]byte(\"happy\"))\n\tassert.NoError(t, err)\n\terr = conn.Close()\n\tassert.NoError(t, err)\n\ttime.Sleep(1 * time.Second)\n\tmock.AssertExpectationsForObjects(t, sobConn, serverOb)\n\n\t// Test close from server side:\n\t// Client creates a connection.\n\t// Server outbound connection reads something, then closes.\n\t// Client connection should read the same thing, then close.\n\tsobConn = mocks.NewMockConn(t)\n\tsobConnCh2 := make(chan []byte, 1)\n\tsobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {\n\t\td := <-sobConnCh2\n\t\tif d == nil {\n\t\t\treturn 0, io.EOF\n\t\t} else {\n\t\t\treturn copy(bs, d), nil\n\t\t}\n\t})\n\tsobConn.EXPECT().Close().Return(nil)\n\tserverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()\n\tconn, err = c.TCP(addr)\n\tassert.NoError(t, err)\n\tsobConnCh2 <- []byte(\"happy\")\n\tclose(sobConnCh2)\n\tbs, err := io.ReadAll(conn)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"happy\", string(bs))\n}\n\n// TestClientServerUDPIdleTimeout tests whether the server's UDP idle timeout works correctly.\nfunc TestClientServerUDPIdleTimeout(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tserverOb := mocks.NewMockOutbound(t)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\teventLogger := mocks.NewMockEventLogger(t)\n\teventLogger.EXPECT().Connect(mock.Anything, \"nobody\", mock.Anything).Once()\n\teventLogger.EXPECT().Disconnect(mock.Anything, \"nobody\", mock.Anything).Maybe() // Depends on the timing, don't care\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:      serverTLSConfig(),\n\t\tConn:           udpConn,\n\t\tOutbound:       serverOb,\n\t\tUDPIdleTimeout: 2 * time.Second,\n\t\tAuthenticator:  auth,\n\t\tEventLogger:    eventLogger,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\taddr := \"spy.x.family:2023\"\n\n\t// On the client side, create a UDP session and send a packet every 1 second,\n\t// 4 packets in total. The server should have one UDP session and receive all\n\t// 4 packets. Then the UDP connection on the server side will receive a packet\n\t// every 1 second, 4 packets in total. The client session should receive all\n\t// 4 packets. Then the session will be idle for 3 seconds - should be enough\n\t// to trigger the server's UDP idle timeout.\n\tsobConn := mocks.NewMockUDPConn(t)\n\tsobConnCh := make(chan []byte, 1)\n\tsobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })\n\tsobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) {\n\t\td := <-sobConnCh\n\t\tif d == nil {\n\t\t\treturn 0, \"\", io.EOF\n\t\t} else {\n\t\t\treturn copy(bs, d), addr, nil\n\t\t}\n\t})\n\tsobConn.EXPECT().WriteTo([]byte(\"happy\"), addr).Return(5, nil).Times(4)\n\tserverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once()\n\teventLogger.EXPECT().UDPRequest(mock.Anything, mock.Anything, uint32(1), addr).Once()\n\tcu, err := c.UDP()\n\tassert.NoError(t, err)\n\t// Client sends 4 packets\n\tfor i := 0; i < 4; i++ {\n\t\terr = cu.Send([]byte(\"happy\"), addr)\n\t\tassert.NoError(t, err)\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\t// Client receives 4 packets\n\tgo func() {\n\t\tfor i := 0; i < 4; i++ {\n\t\t\tsobConnCh <- []byte(\"sad\")\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\t}()\n\tfor i := 0; i < 4; i++ {\n\t\tbs, rAddr, err := cu.Receive()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"sad\", string(bs))\n\t\tassert.Equal(t, addr, rAddr)\n\t}\n\t// Now we wait for 3 seconds, the server should close the UDP session.\n\tsobConn.EXPECT().Close().RunAndReturn(func() error {\n\t\tsobConnChCloseFunc()\n\t\treturn nil\n\t})\n\teventLogger.EXPECT().UDPError(mock.Anything, mock.Anything, uint32(1), nil).Once()\n\ttime.Sleep(3 * time.Second)\n}\n\n// TestClientServerClientShutdown tests whether the server can handle the client's shutdown correctly.\nfunc TestClientServerClientShutdown(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\teventLogger := mocks.NewMockEventLogger(t)\n\teventLogger.EXPECT().Connect(mock.Anything, \"nobody\", mock.Anything).Once()\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t\tEventLogger:   eventLogger,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\n\t// Close the client - expect disconnect event on the server side.\n\t// Since client.Close() sends HTTP3 ErrCodeNoError, the error should be nil.\n\teventLogger.EXPECT().Disconnect(mock.Anything, \"nobody\", nil).Once()\n\t_ = c.Close()\n\ttime.Sleep(1 * time.Second)\n}\n\n// TestClientServerServerShutdown tests whether the client can handle the server's shutdown correctly.\nfunc TestClientServerServerShutdown(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tgo s.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t\tQUICConfig: client.QUICConfig{\n\t\t\tMaxIdleTimeout: 4 * time.Second,\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\n\t// Close the server - expect the client to return ClosedError for both TCP & UDP calls.\n\t_ = s.Close()\n\n\t_, err = c.TCP(\"whatever\")\n\t_, ok := err.(errors.ClosedError)\n\tassert.True(t, ok)\n\n\ttime.Sleep(1 * time.Second) // Allow some time for the error to be propagated to the UDP session manager\n\n\t_, err = c.UDP()\n\t_, ok = err.(errors.ClosedError)\n\tassert.True(t, ok)\n\n\tassert.NoError(t, c.Close())\n}\n"
  },
  {
    "path": "core/internal/integration_tests/hook_test.go",
    "content": "package integration_tests\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n\t\"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks\"\n\t\"github.com/apernet/hysteria/core/v2/server\"\n\t\"github.com/apernet/quic-go\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\nfunc TestClientServerHookTCP(t *testing.T) {\n\tfakeEchoAddr := \"hahanope:6666\"\n\trealEchoAddr := \"127.0.0.1:22333\"\n\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\thook := mocks.NewMockRequestHook(t)\n\thook.EXPECT().Check(false, fakeEchoAddr).Return(true).Once()\n\thook.EXPECT().TCP(mock.Anything, mock.Anything).RunAndReturn(func(stream quic.Stream, s *string) ([]byte, error) {\n\t\tassert.Equal(t, fakeEchoAddr, *s)\n\t\t// Change the address\n\t\t*s = realEchoAddr\n\t\t// Read the first 5 bytes and replace them with \"byeee\"\n\t\tdata := make([]byte, 5)\n\t\t_, err := io.ReadFull(stream, data)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tassert.Equal(t, []byte(\"hello\"), data)\n\t\treturn []byte(\"byeee\"), nil\n\t}).Once()\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tRequestHook:   hook,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create TCP echo server\n\techoListener, err := net.Listen(\"tcp\", realEchoAddr)\n\tassert.NoError(t, err)\n\techoServer := &tcpEchoServer{Listener: echoListener}\n\tdefer echoServer.Close()\n\tgo echoServer.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\t// Dial TCP\n\tconn, err := c.TCP(fakeEchoAddr)\n\tassert.NoError(t, err)\n\tdefer conn.Close()\n\n\t// Send and receive data\n\tsData := []byte(\"hello world\")\n\t_, err = conn.Write(sData)\n\tassert.NoError(t, err)\n\trData := make([]byte, len(sData))\n\t_, err = io.ReadFull(conn, rData)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []byte(\"byeee world\"), rData)\n}\n\nfunc TestClientServerHookUDP(t *testing.T) {\n\tfakeEchoAddr := \"hahanope:6666\"\n\trealEchoAddr := \"127.0.0.1:22333\"\n\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\thook := mocks.NewMockRequestHook(t)\n\thook.EXPECT().Check(true, fakeEchoAddr).Return(true).Once()\n\thook.EXPECT().UDP(mock.Anything, mock.Anything).RunAndReturn(func(bytes []byte, s *string) error {\n\t\tassert.Equal(t, fakeEchoAddr, *s)\n\t\tassert.Equal(t, []byte(\"hello world\"), bytes)\n\t\t// Change the address\n\t\t*s = realEchoAddr\n\t\treturn nil\n\t}).Once()\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tRequestHook:   hook,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create UDP echo server\n\techoConn, err := net.ListenPacket(\"udp\", realEchoAddr)\n\tassert.NoError(t, err)\n\techoServer := &udpEchoServer{Conn: echoConn}\n\tdefer echoServer.Close()\n\tgo echoServer.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\t// Listen UDP\n\tconn, err := c.UDP()\n\tassert.NoError(t, err)\n\tdefer conn.Close()\n\n\t// Send and receive data\n\tsData := []byte(\"hello world\")\n\terr = conn.Send(sData, fakeEchoAddr)\n\tassert.NoError(t, err)\n\trData, rAddr, err := conn.Receive()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sData, rData)\n\t// Hook address change is transparent,\n\t// the client should still see the fake echo address it sent packets to\n\tassert.Equal(t, fakeEchoAddr, rAddr)\n\n\t// Subsequent packets should also be sent to the real echo server\n\tsData = []byte(\"never stop fighting\")\n\terr = conn.Send(sData, fakeEchoAddr)\n\tassert.NoError(t, err)\n\trData, rAddr, err = conn.Receive()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sData, rData)\n\tassert.Equal(t, fakeEchoAddr, rAddr)\n}\n"
  },
  {
    "path": "core/internal/integration_tests/masq_test.go",
    "content": "package integration_tests\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\n\t\"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks\"\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n\t\"github.com/apernet/hysteria/core/v2/server\"\n\n\t\"github.com/apernet/quic-go\"\n\t\"github.com/apernet/quic-go/http3\"\n)\n\n// TestServerMasquerade is a test to ensure that the server behaves as a normal\n// HTTP/3 server when dealing with an unauthenticated client. This is mainly to\n// confirm that the server does not expose itself to active probing.\nfunc TestServerMasquerade(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, \"\", uint64(0)).Return(false, \"\").Once()\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// QUIC connection & RoundTripper\n\tvar conn quic.EarlyConnection\n\trt := &http3.RoundTripper{\n\t\tTLSClientConfig: &tls.Config{\n\t\t\tInsecureSkipVerify: true,\n\t\t},\n\t\tDial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {\n\t\t\tqc, err := quic.DialAddrEarly(ctx, udpAddr.String(), tlsCfg, cfg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tconn = qc\n\t\t\treturn qc, nil\n\t\t},\n\t}\n\tdefer rt.Close() // This will close the QUIC connection\n\n\t// Send the bogus request\n\t// We expect 404 (from the default handler)\n\treq := &http.Request{\n\t\tMethod: http.MethodPost,\n\t\tURL: &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   protocol.URLHost,\n\t\t\tPath:   protocol.URLPath,\n\t\t},\n\t\tHeader: make(http.Header),\n\t}\n\tresp, err := rt.RoundTrip(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n\tfor k := range resp.Header {\n\t\t// Make sure no strange headers are sent by the server\n\t\tassert.NotContains(t, k, \"Hysteria\")\n\t}\n\n\tbuf := make([]byte, 1024)\n\n\t// We send a TCP request anyway, see if we get a response\n\ttcpStream, err := conn.OpenStream()\n\tassert.NoError(t, err)\n\tdefer tcpStream.Close()\n\terr = protocol.WriteTCPRequest(tcpStream, \"www.google.com:443\")\n\tassert.NoError(t, err)\n\n\t// We should receive nothing\n\t_ = tcpStream.SetReadDeadline(time.Now().Add(2 * time.Second))\n\tn, err := tcpStream.Read(buf)\n\tassert.Equal(t, 0, n)\n\tnErr, ok := err.(net.Error)\n\tassert.True(t, ok)\n\tassert.True(t, nErr.Timeout())\n}\n"
  },
  {
    "path": "core/internal/integration_tests/mocks/mock_Authenticator.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tnet \"net\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// MockAuthenticator is an autogenerated mock type for the Authenticator type\ntype MockAuthenticator struct {\n\tmock.Mock\n}\n\ntype MockAuthenticator_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockAuthenticator) EXPECT() *MockAuthenticator_Expecter {\n\treturn &MockAuthenticator_Expecter{mock: &_m.Mock}\n}\n\n// Authenticate provides a mock function with given fields: addr, auth, tx\nfunc (_m *MockAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (bool, string) {\n\tret := _m.Called(addr, auth, tx)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Authenticate\")\n\t}\n\n\tvar r0 bool\n\tvar r1 string\n\tif rf, ok := ret.Get(0).(func(net.Addr, string, uint64) (bool, string)); ok {\n\t\treturn rf(addr, auth, tx)\n\t}\n\tif rf, ok := ret.Get(0).(func(net.Addr, string, uint64) bool); ok {\n\t\tr0 = rf(addr, auth, tx)\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\n\tif rf, ok := ret.Get(1).(func(net.Addr, string, uint64) string); ok {\n\t\tr1 = rf(addr, auth, tx)\n\t} else {\n\t\tr1 = ret.Get(1).(string)\n\t}\n\n\treturn r0, r1\n}\n\n// MockAuthenticator_Authenticate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authenticate'\ntype MockAuthenticator_Authenticate_Call struct {\n\t*mock.Call\n}\n\n// Authenticate is a helper method to define mock.On call\n//   - addr net.Addr\n//   - auth string\n//   - tx uint64\nfunc (_e *MockAuthenticator_Expecter) Authenticate(addr interface{}, auth interface{}, tx interface{}) *MockAuthenticator_Authenticate_Call {\n\treturn &MockAuthenticator_Authenticate_Call{Call: _e.mock.On(\"Authenticate\", addr, auth, tx)}\n}\n\nfunc (_c *MockAuthenticator_Authenticate_Call) Run(run func(addr net.Addr, auth string, tx uint64)) *MockAuthenticator_Authenticate_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(net.Addr), args[1].(string), args[2].(uint64))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockAuthenticator_Authenticate_Call) Return(ok bool, id string) *MockAuthenticator_Authenticate_Call {\n\t_c.Call.Return(ok, id)\n\treturn _c\n}\n\nfunc (_c *MockAuthenticator_Authenticate_Call) RunAndReturn(run func(net.Addr, string, uint64) (bool, string)) *MockAuthenticator_Authenticate_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockAuthenticator(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockAuthenticator {\n\tmock := &MockAuthenticator{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/internal/integration_tests/mocks/mock_Conn.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tnet \"net\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttime \"time\"\n)\n\n// MockConn is an autogenerated mock type for the Conn type\ntype MockConn struct {\n\tmock.Mock\n}\n\ntype MockConn_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockConn) EXPECT() *MockConn_Expecter {\n\treturn &MockConn_Expecter{mock: &_m.Mock}\n}\n\n// Close provides a mock function with given fields:\nfunc (_m *MockConn) Close() error {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Close\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype MockConn_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\nfunc (_e *MockConn_Expecter) Close() *MockConn_Close_Call {\n\treturn &MockConn_Close_Call{Call: _e.mock.On(\"Close\")}\n}\n\nfunc (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// LocalAddr provides a mock function with given fields:\nfunc (_m *MockConn) LocalAddr() net.Addr {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for LocalAddr\")\n\t}\n\n\tvar r0 net.Addr\n\tif rf, ok := ret.Get(0).(func() net.Addr); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Addr)\n\t\t}\n\t}\n\n\treturn r0\n}\n\n// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr'\ntype MockConn_LocalAddr_Call struct {\n\t*mock.Call\n}\n\n// LocalAddr is a helper method to define mock.On call\nfunc (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call {\n\treturn &MockConn_LocalAddr_Call{Call: _e.mock.On(\"LocalAddr\")}\n}\n\nfunc (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Read provides a mock function with given fields: b\nfunc (_m *MockConn) Read(b []byte) (int, error) {\n\tret := _m.Called(b)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Read\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {\n\t\treturn rf(b)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(b)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) error); ok {\n\t\tr1 = rf(b)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'\ntype MockConn_Read_Call struct {\n\t*mock.Call\n}\n\n// Read is a helper method to define mock.On call\n//   - b []byte\nfunc (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call {\n\treturn &MockConn_Read_Call{Call: _e.mock.On(\"Read\", b)}\n}\n\nfunc (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call {\n\t_c.Call.Return(n, err)\n\treturn _c\n}\n\nfunc (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// RemoteAddr provides a mock function with given fields:\nfunc (_m *MockConn) RemoteAddr() net.Addr {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for RemoteAddr\")\n\t}\n\n\tvar r0 net.Addr\n\tif rf, ok := ret.Get(0).(func() net.Addr); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Addr)\n\t\t}\n\t}\n\n\treturn r0\n}\n\n// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr'\ntype MockConn_RemoteAddr_Call struct {\n\t*mock.Call\n}\n\n// RemoteAddr is a helper method to define mock.On call\nfunc (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call {\n\treturn &MockConn_RemoteAddr_Call{Call: _e.mock.On(\"RemoteAddr\")}\n}\n\nfunc (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetDeadline provides a mock function with given fields: t\nfunc (_m *MockConn) SetDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'\ntype MockConn_SetDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call {\n\treturn &MockConn_SetDeadline_Call{Call: _e.mock.On(\"SetDeadline\", t)}\n}\n\nfunc (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetReadDeadline provides a mock function with given fields: t\nfunc (_m *MockConn) SetReadDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetReadDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'\ntype MockConn_SetReadDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetReadDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call {\n\treturn &MockConn_SetReadDeadline_Call{Call: _e.mock.On(\"SetReadDeadline\", t)}\n}\n\nfunc (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetWriteDeadline provides a mock function with given fields: t\nfunc (_m *MockConn) SetWriteDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetWriteDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'\ntype MockConn_SetWriteDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetWriteDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call {\n\treturn &MockConn_SetWriteDeadline_Call{Call: _e.mock.On(\"SetWriteDeadline\", t)}\n}\n\nfunc (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Write provides a mock function with given fields: b\nfunc (_m *MockConn) Write(b []byte) (int, error) {\n\tret := _m.Called(b)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Write\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {\n\t\treturn rf(b)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(b)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) error); ok {\n\t\tr1 = rf(b)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'\ntype MockConn_Write_Call struct {\n\t*mock.Call\n}\n\n// Write is a helper method to define mock.On call\n//   - b []byte\nfunc (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call {\n\treturn &MockConn_Write_Call{Call: _e.mock.On(\"Write\", b)}\n}\n\nfunc (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call {\n\t_c.Call.Return(n, err)\n\treturn _c\n}\n\nfunc (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockConn(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockConn {\n\tmock := &MockConn{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/internal/integration_tests/mocks/mock_EventLogger.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tnet \"net\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// MockEventLogger is an autogenerated mock type for the EventLogger type\ntype MockEventLogger struct {\n\tmock.Mock\n}\n\ntype MockEventLogger_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockEventLogger) EXPECT() *MockEventLogger_Expecter {\n\treturn &MockEventLogger_Expecter{mock: &_m.Mock}\n}\n\n// Connect provides a mock function with given fields: addr, id, tx\nfunc (_m *MockEventLogger) Connect(addr net.Addr, id string, tx uint64) {\n\t_m.Called(addr, id, tx)\n}\n\n// MockEventLogger_Connect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Connect'\ntype MockEventLogger_Connect_Call struct {\n\t*mock.Call\n}\n\n// Connect is a helper method to define mock.On call\n//   - addr net.Addr\n//   - id string\n//   - tx uint64\nfunc (_e *MockEventLogger_Expecter) Connect(addr interface{}, id interface{}, tx interface{}) *MockEventLogger_Connect_Call {\n\treturn &MockEventLogger_Connect_Call{Call: _e.mock.On(\"Connect\", addr, id, tx)}\n}\n\nfunc (_c *MockEventLogger_Connect_Call) Run(run func(addr net.Addr, id string, tx uint64)) *MockEventLogger_Connect_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(net.Addr), args[1].(string), args[2].(uint64))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_Connect_Call) Return() *MockEventLogger_Connect_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_Connect_Call) RunAndReturn(run func(net.Addr, string, uint64)) *MockEventLogger_Connect_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Disconnect provides a mock function with given fields: addr, id, err\nfunc (_m *MockEventLogger) Disconnect(addr net.Addr, id string, err error) {\n\t_m.Called(addr, id, err)\n}\n\n// MockEventLogger_Disconnect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Disconnect'\ntype MockEventLogger_Disconnect_Call struct {\n\t*mock.Call\n}\n\n// Disconnect is a helper method to define mock.On call\n//   - addr net.Addr\n//   - id string\n//   - err error\nfunc (_e *MockEventLogger_Expecter) Disconnect(addr interface{}, id interface{}, err interface{}) *MockEventLogger_Disconnect_Call {\n\treturn &MockEventLogger_Disconnect_Call{Call: _e.mock.On(\"Disconnect\", addr, id, err)}\n}\n\nfunc (_c *MockEventLogger_Disconnect_Call) Run(run func(addr net.Addr, id string, err error)) *MockEventLogger_Disconnect_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(net.Addr), args[1].(string), args[2].(error))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_Disconnect_Call) Return() *MockEventLogger_Disconnect_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_Disconnect_Call) RunAndReturn(run func(net.Addr, string, error)) *MockEventLogger_Disconnect_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// TCPError provides a mock function with given fields: addr, id, reqAddr, err\nfunc (_m *MockEventLogger) TCPError(addr net.Addr, id string, reqAddr string, err error) {\n\t_m.Called(addr, id, reqAddr, err)\n}\n\n// MockEventLogger_TCPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPError'\ntype MockEventLogger_TCPError_Call struct {\n\t*mock.Call\n}\n\n// TCPError is a helper method to define mock.On call\n//   - addr net.Addr\n//   - id string\n//   - reqAddr string\n//   - err error\nfunc (_e *MockEventLogger_Expecter) TCPError(addr interface{}, id interface{}, reqAddr interface{}, err interface{}) *MockEventLogger_TCPError_Call {\n\treturn &MockEventLogger_TCPError_Call{Call: _e.mock.On(\"TCPError\", addr, id, reqAddr, err)}\n}\n\nfunc (_c *MockEventLogger_TCPError_Call) Run(run func(addr net.Addr, id string, reqAddr string, err error)) *MockEventLogger_TCPError_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(net.Addr), args[1].(string), args[2].(string), args[3].(error))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_TCPError_Call) Return() *MockEventLogger_TCPError_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_TCPError_Call) RunAndReturn(run func(net.Addr, string, string, error)) *MockEventLogger_TCPError_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// TCPRequest provides a mock function with given fields: addr, id, reqAddr\nfunc (_m *MockEventLogger) TCPRequest(addr net.Addr, id string, reqAddr string) {\n\t_m.Called(addr, id, reqAddr)\n}\n\n// MockEventLogger_TCPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPRequest'\ntype MockEventLogger_TCPRequest_Call struct {\n\t*mock.Call\n}\n\n// TCPRequest is a helper method to define mock.On call\n//   - addr net.Addr\n//   - id string\n//   - reqAddr string\nfunc (_e *MockEventLogger_Expecter) TCPRequest(addr interface{}, id interface{}, reqAddr interface{}) *MockEventLogger_TCPRequest_Call {\n\treturn &MockEventLogger_TCPRequest_Call{Call: _e.mock.On(\"TCPRequest\", addr, id, reqAddr)}\n}\n\nfunc (_c *MockEventLogger_TCPRequest_Call) Run(run func(addr net.Addr, id string, reqAddr string)) *MockEventLogger_TCPRequest_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(net.Addr), args[1].(string), args[2].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_TCPRequest_Call) Return() *MockEventLogger_TCPRequest_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_TCPRequest_Call) RunAndReturn(run func(net.Addr, string, string)) *MockEventLogger_TCPRequest_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UDPError provides a mock function with given fields: addr, id, sessionID, err\nfunc (_m *MockEventLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) {\n\t_m.Called(addr, id, sessionID, err)\n}\n\n// MockEventLogger_UDPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPError'\ntype MockEventLogger_UDPError_Call struct {\n\t*mock.Call\n}\n\n// UDPError is a helper method to define mock.On call\n//   - addr net.Addr\n//   - id string\n//   - sessionID uint32\n//   - err error\nfunc (_e *MockEventLogger_Expecter) UDPError(addr interface{}, id interface{}, sessionID interface{}, err interface{}) *MockEventLogger_UDPError_Call {\n\treturn &MockEventLogger_UDPError_Call{Call: _e.mock.On(\"UDPError\", addr, id, sessionID, err)}\n}\n\nfunc (_c *MockEventLogger_UDPError_Call) Run(run func(addr net.Addr, id string, sessionID uint32, err error)) *MockEventLogger_UDPError_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(error))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_UDPError_Call) Return() *MockEventLogger_UDPError_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_UDPError_Call) RunAndReturn(run func(net.Addr, string, uint32, error)) *MockEventLogger_UDPError_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UDPRequest provides a mock function with given fields: addr, id, sessionID, reqAddr\nfunc (_m *MockEventLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) {\n\t_m.Called(addr, id, sessionID, reqAddr)\n}\n\n// MockEventLogger_UDPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPRequest'\ntype MockEventLogger_UDPRequest_Call struct {\n\t*mock.Call\n}\n\n// UDPRequest is a helper method to define mock.On call\n//   - addr net.Addr\n//   - id string\n//   - sessionID uint32\n//   - reqAddr string\nfunc (_e *MockEventLogger_Expecter) UDPRequest(addr interface{}, id interface{}, sessionID interface{}, reqAddr interface{}) *MockEventLogger_UDPRequest_Call {\n\treturn &MockEventLogger_UDPRequest_Call{Call: _e.mock.On(\"UDPRequest\", addr, id, sessionID, reqAddr)}\n}\n\nfunc (_c *MockEventLogger_UDPRequest_Call) Run(run func(addr net.Addr, id string, sessionID uint32, reqAddr string)) *MockEventLogger_UDPRequest_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_UDPRequest_Call) Return() *MockEventLogger_UDPRequest_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockEventLogger_UDPRequest_Call) RunAndReturn(run func(net.Addr, string, uint32, string)) *MockEventLogger_UDPRequest_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockEventLogger(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockEventLogger {\n\tmock := &MockEventLogger{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/internal/integration_tests/mocks/mock_Outbound.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tnet \"net\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n\n\tserver \"github.com/apernet/hysteria/core/v2/server\"\n)\n\n// MockOutbound is an autogenerated mock type for the Outbound type\ntype MockOutbound struct {\n\tmock.Mock\n}\n\ntype MockOutbound_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockOutbound) EXPECT() *MockOutbound_Expecter {\n\treturn &MockOutbound_Expecter{mock: &_m.Mock}\n}\n\n// TCP provides a mock function with given fields: reqAddr\nfunc (_m *MockOutbound) TCP(reqAddr string) (net.Conn, error) {\n\tret := _m.Called(reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for TCP\")\n\t}\n\n\tvar r0 net.Conn\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(string) (net.Conn, error)); ok {\n\t\treturn rf(reqAddr)\n\t}\n\tif rf, ok := ret.Get(0).(func(string) net.Conn); ok {\n\t\tr0 = rf(reqAddr)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Conn)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(reqAddr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP'\ntype MockOutbound_TCP_Call struct {\n\t*mock.Call\n}\n\n// TCP is a helper method to define mock.On call\n//   - reqAddr string\nfunc (_e *MockOutbound_Expecter) TCP(reqAddr interface{}) *MockOutbound_TCP_Call {\n\treturn &MockOutbound_TCP_Call{Call: _e.mock.On(\"TCP\", reqAddr)}\n}\n\nfunc (_c *MockOutbound_TCP_Call) Run(run func(reqAddr string)) *MockOutbound_TCP_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *MockOutbound_TCP_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *MockOutbound_TCP_Call) RunAndReturn(run func(string) (net.Conn, error)) *MockOutbound_TCP_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UDP provides a mock function with given fields: reqAddr\nfunc (_m *MockOutbound) UDP(reqAddr string) (server.UDPConn, error) {\n\tret := _m.Called(reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UDP\")\n\t}\n\n\tvar r0 server.UDPConn\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(string) (server.UDPConn, error)); ok {\n\t\treturn rf(reqAddr)\n\t}\n\tif rf, ok := ret.Get(0).(func(string) server.UDPConn); ok {\n\t\tr0 = rf(reqAddr)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(server.UDPConn)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(reqAddr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'\ntype MockOutbound_UDP_Call struct {\n\t*mock.Call\n}\n\n// UDP is a helper method to define mock.On call\n//   - reqAddr string\nfunc (_e *MockOutbound_Expecter) UDP(reqAddr interface{}) *MockOutbound_UDP_Call {\n\treturn &MockOutbound_UDP_Call{Call: _e.mock.On(\"UDP\", reqAddr)}\n}\n\nfunc (_c *MockOutbound_UDP_Call) Run(run func(reqAddr string)) *MockOutbound_UDP_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockOutbound_UDP_Call) Return(_a0 server.UDPConn, _a1 error) *MockOutbound_UDP_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *MockOutbound_UDP_Call) RunAndReturn(run func(string) (server.UDPConn, error)) *MockOutbound_UDP_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockOutbound(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockOutbound {\n\tmock := &MockOutbound{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/internal/integration_tests/mocks/mock_RequestHook.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tquic \"github.com/apernet/quic-go\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// MockRequestHook is an autogenerated mock type for the RequestHook type\ntype MockRequestHook struct {\n\tmock.Mock\n}\n\ntype MockRequestHook_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockRequestHook) EXPECT() *MockRequestHook_Expecter {\n\treturn &MockRequestHook_Expecter{mock: &_m.Mock}\n}\n\n// Check provides a mock function with given fields: isUDP, reqAddr\nfunc (_m *MockRequestHook) Check(isUDP bool, reqAddr string) bool {\n\tret := _m.Called(isUDP, reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Check\")\n\t}\n\n\tvar r0 bool\n\tif rf, ok := ret.Get(0).(func(bool, string) bool); ok {\n\t\tr0 = rf(isUDP, reqAddr)\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\n\treturn r0\n}\n\n// MockRequestHook_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check'\ntype MockRequestHook_Check_Call struct {\n\t*mock.Call\n}\n\n// Check is a helper method to define mock.On call\n//   - isUDP bool\n//   - reqAddr string\nfunc (_e *MockRequestHook_Expecter) Check(isUDP interface{}, reqAddr interface{}) *MockRequestHook_Check_Call {\n\treturn &MockRequestHook_Check_Call{Call: _e.mock.On(\"Check\", isUDP, reqAddr)}\n}\n\nfunc (_c *MockRequestHook_Check_Call) Run(run func(isUDP bool, reqAddr string)) *MockRequestHook_Check_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(bool), args[1].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockRequestHook_Check_Call) Return(_a0 bool) *MockRequestHook_Check_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockRequestHook_Check_Call) RunAndReturn(run func(bool, string) bool) *MockRequestHook_Check_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// TCP provides a mock function with given fields: stream, reqAddr\nfunc (_m *MockRequestHook) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) {\n\tret := _m.Called(stream, reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for TCP\")\n\t}\n\n\tvar r0 []byte\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(quic.Stream, *string) ([]byte, error)); ok {\n\t\treturn rf(stream, reqAddr)\n\t}\n\tif rf, ok := ret.Get(0).(func(quic.Stream, *string) []byte); ok {\n\t\tr0 = rf(stream, reqAddr)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]byte)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(quic.Stream, *string) error); ok {\n\t\tr1 = rf(stream, reqAddr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockRequestHook_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP'\ntype MockRequestHook_TCP_Call struct {\n\t*mock.Call\n}\n\n// TCP is a helper method to define mock.On call\n//   - stream quic.Stream\n//   - reqAddr *string\nfunc (_e *MockRequestHook_Expecter) TCP(stream interface{}, reqAddr interface{}) *MockRequestHook_TCP_Call {\n\treturn &MockRequestHook_TCP_Call{Call: _e.mock.On(\"TCP\", stream, reqAddr)}\n}\n\nfunc (_c *MockRequestHook_TCP_Call) Run(run func(stream quic.Stream, reqAddr *string)) *MockRequestHook_TCP_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(quic.Stream), args[1].(*string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockRequestHook_TCP_Call) Return(_a0 []byte, _a1 error) *MockRequestHook_TCP_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *MockRequestHook_TCP_Call) RunAndReturn(run func(quic.Stream, *string) ([]byte, error)) *MockRequestHook_TCP_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UDP provides a mock function with given fields: data, reqAddr\nfunc (_m *MockRequestHook) UDP(data []byte, reqAddr *string) error {\n\tret := _m.Called(data, reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UDP\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]byte, *string) error); ok {\n\t\tr0 = rf(data, reqAddr)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockRequestHook_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'\ntype MockRequestHook_UDP_Call struct {\n\t*mock.Call\n}\n\n// UDP is a helper method to define mock.On call\n//   - data []byte\n//   - reqAddr *string\nfunc (_e *MockRequestHook_Expecter) UDP(data interface{}, reqAddr interface{}) *MockRequestHook_UDP_Call {\n\treturn &MockRequestHook_UDP_Call{Call: _e.mock.On(\"UDP\", data, reqAddr)}\n}\n\nfunc (_c *MockRequestHook_UDP_Call) Run(run func(data []byte, reqAddr *string)) *MockRequestHook_UDP_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte), args[1].(*string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockRequestHook_UDP_Call) Return(_a0 error) *MockRequestHook_UDP_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockRequestHook_UDP_Call) RunAndReturn(run func([]byte, *string) error) *MockRequestHook_UDP_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockRequestHook(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockRequestHook {\n\tmock := &MockRequestHook{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/internal/integration_tests/mocks/mock_TrafficLogger.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport mock \"github.com/stretchr/testify/mock\"\n\n// MockTrafficLogger is an autogenerated mock type for the TrafficLogger type\ntype MockTrafficLogger struct {\n\tmock.Mock\n}\n\ntype MockTrafficLogger_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockTrafficLogger) EXPECT() *MockTrafficLogger_Expecter {\n\treturn &MockTrafficLogger_Expecter{mock: &_m.Mock}\n}\n\n// LogOnlineState provides a mock function with given fields: id, online\nfunc (_m *MockTrafficLogger) LogOnlineState(id string, online bool) {\n\t_m.Called(id, online)\n}\n\n// MockTrafficLogger_LogOnlineState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogOnlineState'\ntype MockTrafficLogger_LogOnlineState_Call struct {\n\t*mock.Call\n}\n\n// LogOnlineState is a helper method to define mock.On call\n//   - id string\n//   - online bool\nfunc (_e *MockTrafficLogger_Expecter) LogOnlineState(id interface{}, online interface{}) *MockTrafficLogger_LogOnlineState_Call {\n\treturn &MockTrafficLogger_LogOnlineState_Call{Call: _e.mock.On(\"LogOnlineState\", id, online)}\n}\n\nfunc (_c *MockTrafficLogger_LogOnlineState_Call) Run(run func(id string, online bool)) *MockTrafficLogger_LogOnlineState_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(string), args[1].(bool))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockTrafficLogger_LogOnlineState_Call) Return() *MockTrafficLogger_LogOnlineState_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *MockTrafficLogger_LogOnlineState_Call) RunAndReturn(run func(string, bool)) *MockTrafficLogger_LogOnlineState_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// LogTraffic provides a mock function with given fields: id, tx, rx\nfunc (_m *MockTrafficLogger) LogTraffic(id string, tx uint64, rx uint64) bool {\n\tret := _m.Called(id, tx, rx)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for LogTraffic\")\n\t}\n\n\tvar r0 bool\n\tif rf, ok := ret.Get(0).(func(string, uint64, uint64) bool); ok {\n\t\tr0 = rf(id, tx, rx)\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\n\treturn r0\n}\n\n// MockTrafficLogger_LogTraffic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogTraffic'\ntype MockTrafficLogger_LogTraffic_Call struct {\n\t*mock.Call\n}\n\n// LogTraffic is a helper method to define mock.On call\n//   - id string\n//   - tx uint64\n//   - rx uint64\nfunc (_e *MockTrafficLogger_Expecter) LogTraffic(id interface{}, tx interface{}, rx interface{}) *MockTrafficLogger_LogTraffic_Call {\n\treturn &MockTrafficLogger_LogTraffic_Call{Call: _e.mock.On(\"LogTraffic\", id, tx, rx)}\n}\n\nfunc (_c *MockTrafficLogger_LogTraffic_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_LogTraffic_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(string), args[1].(uint64), args[2].(uint64))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockTrafficLogger_LogTraffic_Call) Return(ok bool) *MockTrafficLogger_LogTraffic_Call {\n\t_c.Call.Return(ok)\n\treturn _c\n}\n\nfunc (_c *MockTrafficLogger_LogTraffic_Call) RunAndReturn(run func(string, uint64, uint64) bool) *MockTrafficLogger_LogTraffic_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockTrafficLogger(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockTrafficLogger {\n\tmock := &MockTrafficLogger{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/internal/integration_tests/mocks/mock_UDPConn.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage mocks\n\nimport mock \"github.com/stretchr/testify/mock\"\n\n// MockUDPConn is an autogenerated mock type for the UDPConn type\ntype MockUDPConn struct {\n\tmock.Mock\n}\n\ntype MockUDPConn_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockUDPConn) EXPECT() *MockUDPConn_Expecter {\n\treturn &MockUDPConn_Expecter{mock: &_m.Mock}\n}\n\n// Close provides a mock function with given fields:\nfunc (_m *MockUDPConn) Close() error {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Close\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// MockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype MockUDPConn_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\nfunc (_e *MockUDPConn_Expecter) Close() *MockUDPConn_Close_Call {\n\treturn &MockUDPConn_Close_Call{Call: _e.mock.On(\"Close\")}\n}\n\nfunc (_c *MockUDPConn_Close_Call) Run(run func()) *MockUDPConn_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockUDPConn_Close_Call) Return(_a0 error) *MockUDPConn_Close_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *MockUDPConn_Close_Call) RunAndReturn(run func() error) *MockUDPConn_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// ReadFrom provides a mock function with given fields: b\nfunc (_m *MockUDPConn) ReadFrom(b []byte) (int, string, error) {\n\tret := _m.Called(b)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for ReadFrom\")\n\t}\n\n\tvar r0 int\n\tvar r1 string\n\tvar r2 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok {\n\t\treturn rf(b)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(b)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) string); ok {\n\t\tr1 = rf(b)\n\t} else {\n\t\tr1 = ret.Get(1).(string)\n\t}\n\n\tif rf, ok := ret.Get(2).(func([]byte) error); ok {\n\t\tr2 = rf(b)\n\t} else {\n\t\tr2 = ret.Error(2)\n\t}\n\n\treturn r0, r1, r2\n}\n\n// MockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom'\ntype MockUDPConn_ReadFrom_Call struct {\n\t*mock.Call\n}\n\n// ReadFrom is a helper method to define mock.On call\n//   - b []byte\nfunc (_e *MockUDPConn_Expecter) ReadFrom(b interface{}) *MockUDPConn_ReadFrom_Call {\n\treturn &MockUDPConn_ReadFrom_Call{Call: _e.mock.On(\"ReadFrom\", b)}\n}\n\nfunc (_c *MockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *MockUDPConn_ReadFrom_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *MockUDPConn_ReadFrom_Call {\n\t_c.Call.Return(_a0, _a1, _a2)\n\treturn _c\n}\n\nfunc (_c *MockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *MockUDPConn_ReadFrom_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// WriteTo provides a mock function with given fields: b, addr\nfunc (_m *MockUDPConn) WriteTo(b []byte, addr string) (int, error) {\n\tret := _m.Called(b, addr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for WriteTo\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok {\n\t\treturn rf(b, addr)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte, string) int); ok {\n\t\tr0 = rf(b, addr)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte, string) error); ok {\n\t\tr1 = rf(b, addr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// MockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo'\ntype MockUDPConn_WriteTo_Call struct {\n\t*mock.Call\n}\n\n// WriteTo is a helper method to define mock.On call\n//   - b []byte\n//   - addr string\nfunc (_e *MockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *MockUDPConn_WriteTo_Call {\n\treturn &MockUDPConn_WriteTo_Call{Call: _e.mock.On(\"WriteTo\", b, addr)}\n}\n\nfunc (_c *MockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *MockUDPConn_WriteTo_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte), args[1].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *MockUDPConn_WriteTo_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *MockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *MockUDPConn_WriteTo_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewMockUDPConn(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockUDPConn {\n\tmock := &MockUDPConn{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/internal/integration_tests/smoke_test.go",
    "content": "package integration_tests\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n\tcoreErrs \"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks\"\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\n// Smoke tests that act as a sanity check for client & server to ensure they can talk to each other correctly.\n\n// TestClientNoServer tests how the client handles a server address it cannot connect to.\n// NewClient should return a ConnectError.\nfunc TestClientNoServer(t *testing.T) {\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 55666},\n\t})\n\tassert.Nil(t, c)\n\t_, ok := err.(coreErrs.ConnectError)\n\tassert.True(t, ok)\n}\n\n// TestClientServerBadAuth tests two things:\n// - The server uses Authenticator when a client connects.\n// - How the client handles failed authentication.\nfunc TestClientServerBadAuth(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, \"badpassword\", uint64(0)).Return(false, \"\").Once()\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tAuth:       \"badpassword\",\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.Nil(t, c)\n\t_, ok := err.(coreErrs.AuthError)\n\tassert.True(t, ok)\n}\n\n// TestClientServerUDPDisabled tests how the client handles a server that does not support UDP.\n// UDP should return a DialError.\nfunc TestClientServerUDPDisabled(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tDisableUDP:    true,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\tconn, err := c.UDP()\n\tassert.Nil(t, conn)\n\t_, ok := err.(coreErrs.DialError)\n\tassert.True(t, ok)\n}\n\n// TestClientServerTCPEcho tests TCP forwarding using a TCP echo server.\nfunc TestClientServerTCPEcho(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create TCP echo server\n\techoAddr := \"127.0.0.1:22333\"\n\techoListener, err := net.Listen(\"tcp\", echoAddr)\n\tassert.NoError(t, err)\n\techoServer := &tcpEchoServer{Listener: echoListener}\n\tdefer echoServer.Close()\n\tgo echoServer.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\t// Dial TCP\n\tconn, err := c.TCP(echoAddr)\n\tassert.NoError(t, err)\n\tdefer conn.Close()\n\n\t// Send and receive data\n\tsData := []byte(\"hello world\")\n\t_, err = conn.Write(sData)\n\tassert.NoError(t, err)\n\trData := make([]byte, len(sData))\n\t_, err = io.ReadFull(conn, rData)\n\tassert.NoError(t, err)\n\tassert.Equal(t, sData, rData)\n}\n\n// TestClientServerUDPEcho tests UDP forwarding using a UDP echo server.\nfunc TestClientServerUDPEcho(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create UDP echo server\n\techoAddr := \"127.0.0.1:22333\"\n\techoConn, err := net.ListenPacket(\"udp\", echoAddr)\n\tassert.NoError(t, err)\n\techoServer := &udpEchoServer{Conn: echoConn}\n\tdefer echoServer.Close()\n\tgo echoServer.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\t// Listen UDP\n\tconn, err := c.UDP()\n\tassert.NoError(t, err)\n\tdefer conn.Close()\n\n\t// Send and receive data\n\tsData := []byte(\"hello world\")\n\terr = conn.Send(sData, echoAddr)\n\tassert.NoError(t, err)\n\trData, rAddr, err := conn.Receive()\n\tassert.NoError(t, err)\n\tassert.Equal(t, sData, rData)\n\tassert.Equal(t, echoAddr, rAddr)\n}\n\n// TestClientServerHandshakeInfo tests that the client returns the correct handshake info.\nfunc TestClientServerHandshakeInfo(t *testing.T) {\n\t// Create server 1, UDP enabled, unlimited bandwidth\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tgo s.Serve()\n\n\t// Create client 1, with specified tx bandwidth\n\tc, info, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t\tBandwidthConfig: client.BandwidthConfig{\n\t\t\tMaxTx: 123456,\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, &client.HandshakeInfo{\n\t\tUDPEnabled: true,\n\t\tTx:         123456,\n\t}, info)\n\n\t// Close server 1 and client 1\n\t_ = s.Close()\n\t_ = c.Close()\n\n\t// Create server 2, UDP disabled, limited rx bandwidth\n\tudpConn, udpAddr, err = serverConn()\n\tassert.NoError(t, err)\n\ts, err = server.NewServer(&server.Config{\n\t\tTLSConfig: serverTLSConfig(),\n\t\tConn:      udpConn,\n\t\tBandwidthConfig: server.BandwidthConfig{\n\t\t\tMaxRx: 100000,\n\t\t},\n\t\tDisableUDP:    true,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tgo s.Serve()\n\n\t// Create client 2, with specified tx bandwidth\n\tc, info, err = client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t\tBandwidthConfig: client.BandwidthConfig{\n\t\t\tMaxTx: 123456,\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, &client.HandshakeInfo{\n\t\tUDPEnabled: false,\n\t\tTx:         100000,\n\t}, info)\n\n\t// Close server 2 and client 2\n\t_ = s.Close()\n\t_ = c.Close()\n\n\t// Create server 3, UDP enabled, ignore client bandwidth\n\tudpConn, udpAddr, err = serverConn()\n\tassert.NoError(t, err)\n\ts, err = server.NewServer(&server.Config{\n\t\tTLSConfig:             serverTLSConfig(),\n\t\tConn:                  udpConn,\n\t\tIgnoreClientBandwidth: true,\n\t\tAuthenticator:         auth,\n\t})\n\tassert.NoError(t, err)\n\tgo s.Serve()\n\n\t// Create client 3, with specified tx bandwidth\n\tc, info, err = client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t\tBandwidthConfig: client.BandwidthConfig{\n\t\t\tMaxTx: 123456,\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\tassert.Equal(t, &client.HandshakeInfo{\n\t\tUDPEnabled: true,\n\t\tTx:         0,\n\t}, info)\n\n\t// Close server 3 and client 3\n\t_ = s.Close()\n\t_ = c.Close()\n}\n"
  },
  {
    "path": "core/internal/integration_tests/stress_test.go",
    "content": "package integration_tests\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"golang.org/x/time/rate\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n\t\"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks\"\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\ntype tcpStressor struct {\n\tDialFunc   func() (net.Conn, error)\n\tSize       int\n\tParallel   int\n\tIterations int\n}\n\nfunc (s *tcpStressor) Run(t *testing.T) {\n\t// Make some random data\n\tsData := make([]byte, s.Size)\n\t_, err := rand.Read(sData)\n\tassert.NoError(t, err)\n\n\t// Run iterations\n\tfor i := 0; i < s.Iterations; i++ {\n\t\tvar wg sync.WaitGroup\n\t\terrChan := make(chan error, s.Parallel)\n\t\tfor j := 0; j < s.Parallel; j++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\tconn, err := s.DialFunc()\n\t\t\t\tif err != nil {\n\t\t\t\t\terrChan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tdefer conn.Close()\n\t\t\t\tgo conn.Write(sData)\n\n\t\t\t\trData := make([]byte, len(sData))\n\t\t\t\t_, err = io.ReadFull(conn, rData)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrChan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\tassert.Empty(t, errChan)\n\t}\n}\n\ntype udpStressor struct {\n\tListenFunc func() (client.HyUDPConn, error)\n\tServerAddr string\n\tSize       int\n\tCount      int\n\tParallel   int\n\tIterations int\n}\n\nfunc (s *udpStressor) Run(t *testing.T) {\n\t// Make some random data\n\tsData := make([]byte, s.Size)\n\t_, err := rand.Read(sData)\n\tassert.NoError(t, err)\n\n\t// Due to UDP's unreliability, we need to limit the rate of sending\n\t// to reduce packet loss. This is hardcoded to 1 MiB/s for now.\n\tlimiter := rate.NewLimiter(1048576, 1048576)\n\n\t// Run iterations\n\tfor i := 0; i < s.Iterations; i++ {\n\t\tvar wg sync.WaitGroup\n\t\terrChan := make(chan error, s.Parallel)\n\t\tfor j := 0; j < s.Parallel; j++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\tconn, err := s.ListenFunc()\n\t\t\t\tif err != nil {\n\t\t\t\t\terrChan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tdefer conn.Close()\n\t\t\t\tgo func() {\n\t\t\t\t\t// Sending routine\n\t\t\t\t\tfor i := 0; i < s.Count; i++ {\n\t\t\t\t\t\t_ = limiter.WaitN(context.Background(), len(sData))\n\t\t\t\t\t\t_ = conn.Send(sData, s.ServerAddr)\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tminCount := s.Count * 8 / 10 // Tolerate 20% packet loss\n\t\t\t\tfor i := 0; i < minCount; i++ {\n\t\t\t\t\trData, _, err := conn.Receive()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrChan <- err\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif len(rData) != len(sData) {\n\t\t\t\t\t\terrChan <- fmt.Errorf(\"incomplete data received: %d/%d bytes\", len(rData), len(sData))\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\n\t\tassert.Empty(t, errChan)\n\t}\n}\n\nfunc TestClientServerTCPStress(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create TCP echo server\n\techoAddr := \"127.0.0.1:22333\"\n\techoListener, err := net.Listen(\"tcp\", echoAddr)\n\tassert.NoError(t, err)\n\techoServer := &tcpEchoServer{Listener: echoListener}\n\tdefer echoServer.Close()\n\tgo echoServer.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\tdialFunc := func() (net.Conn, error) {\n\t\treturn c.TCP(echoAddr)\n\t}\n\n\tt.Run(\"Single 500m\", (&tcpStressor{DialFunc: dialFunc, Size: 524288000, Parallel: 1, Iterations: 1}).Run)\n\n\tt.Run(\"Sequential 1000x1m\", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1, Iterations: 1000}).Run)\n\tt.Run(\"Sequential 10000x100k\", (&tcpStressor{DialFunc: dialFunc, Size: 102400, Parallel: 1, Iterations: 10000}).Run)\n\n\tt.Run(\"Parallel 100x10m\", (&tcpStressor{DialFunc: dialFunc, Size: 10485760, Parallel: 100, Iterations: 1}).Run)\n\tt.Run(\"Parallel 1000x1m\", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1000, Iterations: 1}).Run)\n}\n\nfunc TestClientServerUDPStress(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tAuthenticator: auth,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create UDP echo server\n\techoAddr := \"127.0.0.1:22333\"\n\techoConn, err := net.ListenPacket(\"udp\", echoAddr)\n\tassert.NoError(t, err)\n\techoServer := &udpEchoServer{Conn: echoConn}\n\tdefer echoServer.Close()\n\tgo echoServer.Serve()\n\n\t// Create client\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\tt.Run(\"Single 1000x100b\", (&udpStressor{\n\t\tListenFunc: c.UDP,\n\t\tServerAddr: echoAddr,\n\t\tSize:       100,\n\t\tCount:      1000,\n\t\tParallel:   1,\n\t\tIterations: 1,\n\t}).Run)\n\tt.Run(\"Single 1000x3k\", (&udpStressor{\n\t\tListenFunc: c.UDP,\n\t\tServerAddr: echoAddr,\n\t\tSize:       3000,\n\t\tCount:      1000,\n\t\tParallel:   1,\n\t\tIterations: 1,\n\t}).Run)\n\n\tt.Run(\"5 Sequential 1000x100b\", (&udpStressor{\n\t\tListenFunc: c.UDP,\n\t\tServerAddr: echoAddr,\n\t\tSize:       100,\n\t\tCount:      1000,\n\t\tParallel:   1,\n\t\tIterations: 5,\n\t}).Run)\n\tt.Run(\"5 Sequential 200x3k\", (&udpStressor{\n\t\tListenFunc: c.UDP,\n\t\tServerAddr: echoAddr,\n\t\tSize:       3000,\n\t\tCount:      200,\n\t\tParallel:   1,\n\t\tIterations: 5,\n\t}).Run)\n\n\tt.Run(\"2 Sequential 5 Parallel 1000x100b\", (&udpStressor{\n\t\tListenFunc: c.UDP,\n\t\tServerAddr: echoAddr,\n\t\tSize:       100,\n\t\tCount:      1000,\n\t\tParallel:   5,\n\t\tIterations: 2,\n\t}).Run)\n\tt.Run(\"2 Sequential 5 Parallel 200x3k\", (&udpStressor{\n\t\tListenFunc: c.UDP,\n\t\tServerAddr: echoAddr,\n\t\tSize:       3000,\n\t\tCount:      200,\n\t\tParallel:   5,\n\t\tIterations: 2,\n\t}).Run)\n}\n"
  },
  {
    "path": "core/internal/integration_tests/test.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL\nBQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM\nEFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3\nDQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy\nMDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE\nCgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI\nhvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4\nZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W\nDKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW\nCbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf\njICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N\no0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v\nM29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD\nMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf\n1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc\nluDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4\nFjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD\nUMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD\nOLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7\nydYKuI8=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "core/internal/integration_tests/test.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR\n2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr\n6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc\nL9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2\nwiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C\nLolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI\nMdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+\nFLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U\n64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX\nerwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu\n1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW\nT3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA\ng4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA\no/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO\nUj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY\nZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V\nrFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k\nAnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI\nj8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0\njS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2\nZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g\nuhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+\nmenJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2\nkh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB\nT7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "core/internal/integration_tests/trafficlogger_test.go",
    "content": "package integration_tests\n\nimport (\n\t\"io\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\n\t\"github.com/apernet/hysteria/core/v2/client\"\n\t\"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks\"\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\n// TestClientServerTrafficLoggerTCP tests that the traffic logger is correctly called for TCP connections,\n// and that the client is disconnected when the traffic logger returns false.\nfunc TestClientServerTrafficLoggerTCP(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tserverOb := mocks.NewMockOutbound(t)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ttrafficLogger := mocks.NewMockTrafficLogger(t)\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tOutbound:      serverOb,\n\t\tAuthenticator: auth,\n\t\tTrafficLogger: trafficLogger,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create client\n\ttrafficLogger.EXPECT().LogOnlineState(\"nobody\", true).Return().Once()\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\taddr := \"dontcare.cc:4455\"\n\n\tsobConn := mocks.NewMockConn(t)\n\tsobConnCh := make(chan []byte, 1)\n\tsobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })\n\tsobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {\n\t\tb := <-sobConnCh\n\t\tif b == nil {\n\t\t\treturn 0, io.EOF\n\t\t} else {\n\t\t\treturn copy(bs, b), nil\n\t\t}\n\t})\n\tsobConn.EXPECT().Close().RunAndReturn(func() error {\n\t\tsobConnChCloseFunc()\n\t\treturn nil\n\t})\n\tserverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()\n\n\tconn, err := c.TCP(addr)\n\tassert.NoError(t, err)\n\n\t// Client reads from server\n\ttrafficLogger.EXPECT().LogTraffic(\"nobody\", uint64(0), uint64(11)).Return(true).Once()\n\tsobConnCh <- []byte(\"knock knock\")\n\tbuf := make([]byte, 100)\n\tn, err := conn.Read(buf)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 11, n)\n\tassert.Equal(t, \"knock knock\", string(buf[:n]))\n\n\t// Client writes to server\n\ttrafficLogger.EXPECT().LogTraffic(\"nobody\", uint64(12), uint64(0)).Return(true).Once()\n\tsobConn.EXPECT().Write([]byte(\"who is there\")).Return(12, nil).Once()\n\tn, err = conn.Write([]byte(\"who is there\"))\n\tassert.NoError(t, err)\n\tassert.Equal(t, 12, n)\n\ttime.Sleep(1 * time.Second) // Need some time for the server to receive the data\n\n\t// Client reads from server again but blocked\n\ttrafficLogger.EXPECT().LogTraffic(\"nobody\", uint64(0), uint64(4)).Return(false).Once()\n\ttrafficLogger.EXPECT().LogOnlineState(\"nobody\", false).Return().Once()\n\tsobConnCh <- []byte(\"nope\")\n\tn, err = conn.Read(buf)\n\tassert.Zero(t, n)\n\tassert.Error(t, err)\n\n\t// The client should be disconnected\n\t_, err = c.TCP(\"whatever\")\n\tassert.Error(t, err)\n}\n\n// TestClientServerTrafficLoggerUDP tests that the traffic logger is correctly called for UDP sessions,\n// and that the client is disconnected when the traffic logger returns false.\nfunc TestClientServerTrafficLoggerUDP(t *testing.T) {\n\t// Create server\n\tudpConn, udpAddr, err := serverConn()\n\tassert.NoError(t, err)\n\tserverOb := mocks.NewMockOutbound(t)\n\tauth := mocks.NewMockAuthenticator(t)\n\tauth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, \"nobody\")\n\ttrafficLogger := mocks.NewMockTrafficLogger(t)\n\ts, err := server.NewServer(&server.Config{\n\t\tTLSConfig:     serverTLSConfig(),\n\t\tConn:          udpConn,\n\t\tOutbound:      serverOb,\n\t\tAuthenticator: auth,\n\t\tTrafficLogger: trafficLogger,\n\t})\n\tassert.NoError(t, err)\n\tdefer s.Close()\n\tgo s.Serve()\n\n\t// Create client\n\ttrafficLogger.EXPECT().LogOnlineState(\"nobody\", true).Return().Once()\n\tc, _, err := client.NewClient(&client.Config{\n\t\tServerAddr: udpAddr,\n\t\tTLSConfig:  client.TLSConfig{InsecureSkipVerify: true},\n\t})\n\tassert.NoError(t, err)\n\tdefer c.Close()\n\n\taddr := \"shady.org:43211\"\n\n\tsobConn := mocks.NewMockUDPConn(t)\n\tsobConnCh := make(chan []byte, 1)\n\tsobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })\n\tsobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) {\n\t\tb := <-sobConnCh\n\t\tif b == nil {\n\t\t\treturn 0, \"\", io.EOF\n\t\t} else {\n\t\t\treturn copy(bs, b), addr, nil\n\t\t}\n\t})\n\tsobConn.EXPECT().Close().RunAndReturn(func() error {\n\t\tsobConnChCloseFunc()\n\t\treturn nil\n\t})\n\tserverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once()\n\n\tconn, err := c.UDP()\n\tassert.NoError(t, err)\n\n\t// Client writes to server\n\ttrafficLogger.EXPECT().LogTraffic(\"nobody\", uint64(9), uint64(0)).Return(true).Once()\n\tsobConn.EXPECT().WriteTo([]byte(\"small sad\"), addr).Return(9, nil).Once()\n\terr = conn.Send([]byte(\"small sad\"), addr)\n\tassert.NoError(t, err)\n\ttime.Sleep(1 * time.Second) // Need some time for the server to receive the data\n\n\t// Client reads from server\n\ttrafficLogger.EXPECT().LogTraffic(\"nobody\", uint64(0), uint64(7)).Return(true).Once()\n\tsobConnCh <- []byte(\"big mad\")\n\tbs, rAddr, err := conn.Receive()\n\tassert.NoError(t, err)\n\tassert.Equal(t, rAddr, addr)\n\tassert.Equal(t, \"big mad\", string(bs))\n\n\t// Client reads from server again but blocked\n\ttrafficLogger.EXPECT().LogTraffic(\"nobody\", uint64(0), uint64(4)).Return(false).Once()\n\ttrafficLogger.EXPECT().LogOnlineState(\"nobody\", false).Return().Once()\n\tsobConnCh <- []byte(\"nope\")\n\tbs, rAddr, err = conn.Receive()\n\tassert.Equal(t, err, io.EOF)\n\tassert.Empty(t, rAddr)\n\tassert.Empty(t, bs)\n\n\t// The client should be disconnected\n\t_, err = c.UDP()\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "core/internal/integration_tests/utils_test.go",
    "content": "package integration_tests\n\nimport (\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\n// This file provides utilities for the integration tests.\n\nconst (\n\ttestCertFile = \"test.crt\"\n\ttestKeyFile  = \"test.key\"\n)\n\nfunc serverTLSConfig() server.TLSConfig {\n\tcert, err := tls.LoadX509KeyPair(testCertFile, testKeyFile)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn server.TLSConfig{\n\t\tCertificates: []tls.Certificate{cert},\n\t}\n}\n\nfunc serverConn() (net.PacketConn, net.Addr, error) {\n\tudpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}\n\tudpConn, err := net.ListenUDP(\"udp\", udpAddr)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn udpConn, udpAddr, nil\n}\n\n// tcpEchoServer is a TCP server that echoes what it reads from the connection.\n// It will never actively close the connection.\ntype tcpEchoServer struct {\n\tListener net.Listener\n}\n\nfunc (s *tcpEchoServer) Serve() error {\n\tfor {\n\t\tconn, err := s.Listener.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo func() {\n\t\t\t_, _ = io.Copy(conn, conn)\n\t\t\t_ = conn.Close()\n\t\t}()\n\t}\n}\n\nfunc (s *tcpEchoServer) Close() error {\n\treturn s.Listener.Close()\n}\n\n// udpEchoServer is a UDP server that echoes what it reads from the connection.\n// It will never actively close the connection.\ntype udpEchoServer struct {\n\tConn net.PacketConn\n}\n\nfunc (s *udpEchoServer) Serve() error {\n\tbuf := make([]byte, 65536)\n\tfor {\n\t\tn, addr, err := s.Conn.ReadFrom(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = s.Conn.WriteTo(buf[:n], addr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (s *udpEchoServer) Close() error {\n\treturn s.Conn.Close()\n}\n"
  },
  {
    "path": "core/internal/pmtud/avail.go",
    "content": "//go:build linux || windows || darwin\n\npackage pmtud\n\nconst (\n\tDisablePathMTUDiscovery = false\n)\n"
  },
  {
    "path": "core/internal/pmtud/unavail.go",
    "content": "//go:build !linux && !windows && !darwin\n\npackage pmtud\n\n// quic-go's MTU detection is enabled by default on all platforms.\n// However, it only actually sets the DF bit on 3 supported platforms (Windows, macOS, Linux).\n// As a result, on other platforms, probe packets that should never be fragmented will still\n// be fragmented and transmitted. So we're only enabling it for platforms where we've verified\n// its functionality for now.\n\nconst (\n\tDisablePathMTUDiscovery = true\n)\n"
  },
  {
    "path": "core/internal/protocol/http.go",
    "content": "package protocol\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n)\n\nconst (\n\tURLHost = \"hysteria\"\n\tURLPath = \"/auth\"\n\n\tRequestHeaderAuth        = \"Hysteria-Auth\"\n\tResponseHeaderUDPEnabled = \"Hysteria-UDP\"\n\tCommonHeaderCCRX         = \"Hysteria-CC-RX\"\n\tCommonHeaderPadding      = \"Hysteria-Padding\"\n\n\tStatusAuthOK = 233\n)\n\n// AuthRequest is what client sends to server for authentication.\ntype AuthRequest struct {\n\tAuth string\n\tRx   uint64 // 0 = unknown, client asks server to use bandwidth detection\n}\n\n// AuthResponse is what server sends to client when authentication is passed.\ntype AuthResponse struct {\n\tUDPEnabled bool\n\tRx         uint64 // 0 = unlimited\n\tRxAuto     bool   // true = server asks client to use bandwidth detection\n}\n\nfunc AuthRequestFromHeader(h http.Header) AuthRequest {\n\trx, _ := strconv.ParseUint(h.Get(CommonHeaderCCRX), 10, 64)\n\treturn AuthRequest{\n\t\tAuth: h.Get(RequestHeaderAuth),\n\t\tRx:   rx,\n\t}\n}\n\nfunc AuthRequestToHeader(h http.Header, req AuthRequest) {\n\th.Set(RequestHeaderAuth, req.Auth)\n\th.Set(CommonHeaderCCRX, strconv.FormatUint(req.Rx, 10))\n\th.Set(CommonHeaderPadding, authRequestPadding.String())\n}\n\nfunc AuthResponseFromHeader(h http.Header) AuthResponse {\n\tresp := AuthResponse{}\n\tresp.UDPEnabled, _ = strconv.ParseBool(h.Get(ResponseHeaderUDPEnabled))\n\trxStr := h.Get(CommonHeaderCCRX)\n\tif rxStr == \"auto\" {\n\t\t// Special case for server requesting client to use bandwidth detection\n\t\tresp.RxAuto = true\n\t} else {\n\t\tresp.Rx, _ = strconv.ParseUint(rxStr, 10, 64)\n\t}\n\treturn resp\n}\n\nfunc AuthResponseToHeader(h http.Header, resp AuthResponse) {\n\th.Set(ResponseHeaderUDPEnabled, strconv.FormatBool(resp.UDPEnabled))\n\tif resp.RxAuto {\n\t\th.Set(CommonHeaderCCRX, \"auto\")\n\t} else {\n\t\th.Set(CommonHeaderCCRX, strconv.FormatUint(resp.Rx, 10))\n\t}\n\th.Set(CommonHeaderPadding, authResponsePadding.String())\n}\n"
  },
  {
    "path": "core/internal/protocol/padding.go",
    "content": "package protocol\n\nimport (\n\t\"math/rand\"\n)\n\nconst (\n\tpaddingChars = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n)\n\n// padding specifies a half-open range [Min, Max).\ntype padding struct {\n\tMin int\n\tMax int\n}\n\nfunc (p padding) String() string {\n\tn := p.Min + rand.Intn(p.Max-p.Min)\n\tbs := make([]byte, n)\n\tfor i := range bs {\n\t\tbs[i] = paddingChars[rand.Intn(len(paddingChars))]\n\t}\n\treturn string(bs)\n}\n\nvar (\n\tauthRequestPadding  = padding{Min: 256, Max: 2048}\n\tauthResponsePadding = padding{Min: 256, Max: 2048}\n\ttcpRequestPadding   = padding{Min: 64, Max: 512}\n\ttcpResponsePadding  = padding{Min: 128, Max: 1024}\n)\n"
  },
  {
    "path": "core/internal/protocol/proxy.go",
    "content": "package protocol\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/apernet/hysteria/core/v2/errors\"\n\n\t\"github.com/apernet/quic-go/quicvarint\"\n)\n\nconst (\n\tFrameTypeTCPRequest = 0x401\n\n\t// Max length values are for preventing DoS attacks\n\n\tMaxAddressLength = 2048\n\tMaxMessageLength = 2048\n\tMaxPaddingLength = 4096\n\n\tMaxUDPSize = 4096\n\n\tmaxVarInt1 = 63\n\tmaxVarInt2 = 16383\n\tmaxVarInt4 = 1073741823\n\tmaxVarInt8 = 4611686018427387903\n)\n\n// TCPRequest format:\n// 0x401 (QUIC varint)\n// Address length (QUIC varint)\n// Address (bytes)\n// Padding length (QUIC varint)\n// Padding (bytes)\n\nfunc ReadTCPRequest(r io.Reader) (string, error) {\n\tbReader := quicvarint.NewReader(r)\n\taddrLen, err := quicvarint.Read(bReader)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif addrLen == 0 || addrLen > MaxAddressLength {\n\t\treturn \"\", errors.ProtocolError{Message: \"invalid address length\"}\n\t}\n\taddrBuf := make([]byte, addrLen)\n\t_, err = io.ReadFull(r, addrBuf)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tpaddingLen, err := quicvarint.Read(bReader)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif paddingLen > MaxPaddingLength {\n\t\treturn \"\", errors.ProtocolError{Message: \"invalid padding length\"}\n\t}\n\tif paddingLen > 0 {\n\t\t_, err = io.CopyN(io.Discard, r, int64(paddingLen))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn string(addrBuf), nil\n}\n\nfunc WriteTCPRequest(w io.Writer, addr string) error {\n\tpadding := tcpRequestPadding.String()\n\tpaddingLen := len(padding)\n\taddrLen := len(addr)\n\tsz := int(quicvarint.Len(FrameTypeTCPRequest)) +\n\t\tint(quicvarint.Len(uint64(addrLen))) + addrLen +\n\t\tint(quicvarint.Len(uint64(paddingLen))) + paddingLen\n\tbuf := make([]byte, sz)\n\ti := varintPut(buf, FrameTypeTCPRequest)\n\ti += varintPut(buf[i:], uint64(addrLen))\n\ti += copy(buf[i:], addr)\n\ti += varintPut(buf[i:], uint64(paddingLen))\n\tcopy(buf[i:], padding)\n\t_, err := w.Write(buf)\n\treturn err\n}\n\n// TCPResponse format:\n// Status (byte, 0=ok, 1=error)\n// Message length (QUIC varint)\n// Message (bytes)\n// Padding length (QUIC varint)\n// Padding (bytes)\n\nfunc ReadTCPResponse(r io.Reader) (bool, string, error) {\n\tvar status [1]byte\n\tif _, err := io.ReadFull(r, status[:]); err != nil {\n\t\treturn false, \"\", err\n\t}\n\tbReader := quicvarint.NewReader(r)\n\tmsgLen, err := quicvarint.Read(bReader)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\tif msgLen > MaxMessageLength {\n\t\treturn false, \"\", errors.ProtocolError{Message: \"invalid message length\"}\n\t}\n\tvar msgBuf []byte\n\t// No message is fine\n\tif msgLen > 0 {\n\t\tmsgBuf = make([]byte, msgLen)\n\t\t_, err = io.ReadFull(r, msgBuf)\n\t\tif err != nil {\n\t\t\treturn false, \"\", err\n\t\t}\n\t}\n\tpaddingLen, err := quicvarint.Read(bReader)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\tif paddingLen > MaxPaddingLength {\n\t\treturn false, \"\", errors.ProtocolError{Message: \"invalid padding length\"}\n\t}\n\tif paddingLen > 0 {\n\t\t_, err = io.CopyN(io.Discard, r, int64(paddingLen))\n\t\tif err != nil {\n\t\t\treturn false, \"\", err\n\t\t}\n\t}\n\treturn status[0] == 0, string(msgBuf), nil\n}\n\nfunc WriteTCPResponse(w io.Writer, ok bool, msg string) error {\n\tpadding := tcpResponsePadding.String()\n\tpaddingLen := len(padding)\n\tmsgLen := len(msg)\n\tsz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen +\n\t\tint(quicvarint.Len(uint64(paddingLen))) + paddingLen\n\tbuf := make([]byte, sz)\n\tif ok {\n\t\tbuf[0] = 0\n\t} else {\n\t\tbuf[0] = 1\n\t}\n\ti := varintPut(buf[1:], uint64(msgLen))\n\ti += copy(buf[1+i:], msg)\n\ti += varintPut(buf[1+i:], uint64(paddingLen))\n\tcopy(buf[1+i:], padding)\n\t_, err := w.Write(buf)\n\treturn err\n}\n\n// UDPMessage format:\n// Session ID (uint32 BE)\n// Packet ID (uint16 BE)\n// Fragment ID (uint8)\n// Fragment count (uint8)\n// Address length (QUIC varint)\n// Address (bytes)\n// Data...\n\ntype UDPMessage struct {\n\tSessionID uint32 // 4\n\tPacketID  uint16 // 2\n\tFragID    uint8  // 1\n\tFragCount uint8  // 1\n\tAddr      string // varint + bytes\n\tData      []byte\n}\n\nfunc (m *UDPMessage) HeaderSize() int {\n\tlAddr := len(m.Addr)\n\treturn 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr\n}\n\nfunc (m *UDPMessage) Size() int {\n\treturn m.HeaderSize() + len(m.Data)\n}\n\nfunc (m *UDPMessage) Serialize(buf []byte) int {\n\t// Make sure the buffer is big enough\n\tif len(buf) < m.Size() {\n\t\treturn -1\n\t}\n\tbinary.BigEndian.PutUint32(buf, m.SessionID)\n\tbinary.BigEndian.PutUint16(buf[4:], m.PacketID)\n\tbuf[6] = m.FragID\n\tbuf[7] = m.FragCount\n\ti := varintPut(buf[8:], uint64(len(m.Addr)))\n\ti += copy(buf[8+i:], m.Addr)\n\ti += copy(buf[8+i:], m.Data)\n\treturn 8 + i\n}\n\nfunc ParseUDPMessage(msg []byte) (*UDPMessage, error) {\n\tm := &UDPMessage{}\n\tbuf := bytes.NewBuffer(msg)\n\tif err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil {\n\t\treturn nil, err\n\t}\n\tlAddr, err := quicvarint.Read(buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif lAddr == 0 || lAddr > MaxMessageLength {\n\t\treturn nil, errors.ProtocolError{Message: \"invalid address length\"}\n\t}\n\tbs := buf.Bytes()\n\tif len(bs) <= int(lAddr) {\n\t\t// We use <= instead of < here as we expect at least one byte of data after the address\n\t\treturn nil, errors.ProtocolError{Message: \"invalid message length\"}\n\t}\n\tm.Addr = string(bs[:lAddr])\n\tm.Data = bs[lAddr:]\n\treturn m, nil\n}\n\n// varintPut is like quicvarint.Append, but instead of appending to a slice,\n// it writes to a fixed-size buffer. Returns the number of bytes written.\nfunc varintPut(b []byte, i uint64) int {\n\tif i <= maxVarInt1 {\n\t\tb[0] = uint8(i)\n\t\treturn 1\n\t}\n\tif i <= maxVarInt2 {\n\t\tb[0] = uint8(i>>8) | 0x40\n\t\tb[1] = uint8(i)\n\t\treturn 2\n\t}\n\tif i <= maxVarInt4 {\n\t\tb[0] = uint8(i>>24) | 0x80\n\t\tb[1] = uint8(i >> 16)\n\t\tb[2] = uint8(i >> 8)\n\t\tb[3] = uint8(i)\n\t\treturn 4\n\t}\n\tif i <= maxVarInt8 {\n\t\tb[0] = uint8(i>>56) | 0xc0\n\t\tb[1] = uint8(i >> 48)\n\t\tb[2] = uint8(i >> 40)\n\t\tb[3] = uint8(i >> 32)\n\t\tb[4] = uint8(i >> 24)\n\t\tb[5] = uint8(i >> 16)\n\t\tb[6] = uint8(i >> 8)\n\t\tb[7] = uint8(i)\n\t\treturn 8\n\t}\n\tpanic(fmt.Sprintf(\"%#x doesn't fit into 62 bits\", i))\n}\n"
  },
  {
    "path": "core/internal/protocol/proxy_test.go",
    "content": "package protocol\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestUDPMessage(t *testing.T) {\n\tt.Run(\"buffer too small\", func(t *testing.T) {\n\t\t// Make sure Serialize returns -1 when the buffer is too small.\n\t\ttBuf := make([]byte, 20)\n\t\tif (&UDPMessage{\n\t\t\tSessionID: 66,\n\t\t\tPacketID:  77,\n\t\t\tFragID:    2,\n\t\t\tFragCount: 5,\n\t\t\tAddr:      \"random_addr\",\n\t\t\tData:      []byte(\"random_data\"),\n\t\t}).Serialize(tBuf) != -1 {\n\t\t\tt.Error(\"Serialize() did not return -1 when the buffer was too small\")\n\t\t}\n\t})\n\n\ttype fields struct {\n\t\tSessionID uint32\n\t\tPacketID  uint16\n\t\tFragID    uint8\n\t\tFragCount uint8\n\t\tAddr      string\n\t\tData      []byte\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   []byte\n\t}{\n\t\t{\n\t\t\tname: \"test 1\",\n\t\t\tfields: fields{\n\t\t\t\tSessionID: 1,\n\t\t\t\tPacketID:  1,\n\t\t\t\tFragID:    0,\n\t\t\t\tFragCount: 1,\n\t\t\t\tAddr:      \"example.com:80\",\n\t\t\t\tData:      []byte(\"GET /nothing HTTP/1.1\\r\\n\"),\n\t\t\t},\n\t\t\twant: []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},\n\t\t},\n\t\t{\n\t\t\tname: \"test 2\",\n\t\t\tfields: fields{\n\t\t\t\tSessionID: 1329655244,\n\t\t\t\tAddr:      \"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\",\n\t\t\t\tPacketID:  62233,\n\t\t\t\tFragID:    8,\n\t\t\t\tFragCount: 19,\n\t\t\t\tData:      []byte(\"God is great, beer is good, and people are crazy.\"),\n\t\t\t},\n\t\t\twant: []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},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &UDPMessage{\n\t\t\t\tSessionID: tt.fields.SessionID,\n\t\t\t\tAddr:      tt.fields.Addr,\n\t\t\t\tPacketID:  tt.fields.PacketID,\n\t\t\t\tFragID:    tt.fields.FragID,\n\t\t\t\tFragCount: tt.fields.FragCount,\n\t\t\t\tData:      tt.fields.Data,\n\t\t\t}\n\t\t\t// Serialize\n\t\t\tbuf := make([]byte, MaxUDPSize)\n\t\t\tn := m.Serialize(buf)\n\t\t\tif got := buf[:n]; !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Serialize() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\t// Parse back\n\t\t\tif m2, err := ParseUDPMessage(tt.want); err != nil {\n\t\t\t\tt.Errorf(\"ParseUDPMessage() error = %v\", err)\n\t\t\t} else {\n\t\t\t\tif !reflect.DeepEqual(m2, m) {\n\t\t\t\t\tt.Errorf(\"ParseUDPMessage() = %v, want %v\", m2, m)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestUDPMessageMalformed is to make sure ParseUDPMessage() fails (but not panic) on malformed data.\nfunc TestUDPMessageMalformed(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tdata: []byte{},\n\t\t},\n\t\t{\n\t\t\tname: \"zeroes 1\",\n\t\t\tdata: []byte{0, 0, 0, 0},\n\t\t},\n\t\t{\n\t\t\tname: \"zeroes 2\",\n\t\t\tdata: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n\t\t},\n\t\t{\n\t\t\tname: \"incomplete 1\",\n\t\t\tdata: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55},\n\t\t},\n\t\t{\n\t\t\tname: \"incomplete 2\",\n\t\t\tdata: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x90, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := ParseUDPMessage(tt.data); err == nil {\n\t\t\t\tt.Errorf(\"ParseUDPMessage() should fail\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadTCPRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdata    []byte\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal no padding\",\n\t\t\tdata:    []byte(\"\\x0egoogle.com:443\\x00\"),\n\t\t\twant:    \"google.com:443\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal with padding\",\n\t\t\tdata:    []byte(\"\\x0bholy.cc:443\\x02gg\"),\n\t\t\twant:    \"holy.cc:443\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete 1\",\n\t\t\tdata:    []byte(\"\\x0bhoho\"),\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete 2\",\n\t\t\tdata:    []byte(\"\\x0bholy.cc:443\\x05x\"),\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := bytes.NewReader(tt.data)\n\t\t\tgot, err := ReadTCPRequest(r)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ReadTCPRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"ReadTCPRequest() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteTCPRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\taddr    string\n\t\twantW   string // Just a prefix, we don't care about the padding\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal 1\",\n\t\t\taddr:    \"google.com:443\",\n\t\t\twantW:   \"\\x44\\x01\\x0egoogle.com:443\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal 2\",\n\t\t\taddr:    \"client-api.arkoselabs.com:8080\",\n\t\t\twantW:   \"\\x44\\x01\\x1eclient-api.arkoselabs.com:8080\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty\",\n\t\t\taddr:    \"\",\n\t\t\twantW:   \"\\x44\\x01\\x00\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &bytes.Buffer{}\n\t\t\terr := WriteTCPRequest(w, tt.addr)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"WriteTCPRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) {\n\t\t\t\tt.Errorf(\"WriteTCPRequest() gotW = %v, want %v\", gotW, tt.wantW)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadTCPResponse(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdata    []byte\n\t\twant    bool\n\t\twant1   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal ok no padding\",\n\t\t\tdata:    []byte(\"\\x00\\x0bhello world\\x00\"),\n\t\t\twant:    true,\n\t\t\twant1:   \"hello world\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal error with padding\",\n\t\t\tdata:    []byte(\"\\x01\\x06stop!!\\x05xxxxx\"),\n\t\t\twant1:   \"stop!!\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal ok no message with padding\",\n\t\t\tdata:    []byte(\"\\x01\\x00\\x05xxxxx\"),\n\t\t\twant1:   \"\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete 1\",\n\t\t\tdata:    []byte(\"\\x00\\x0bhoho\"),\n\t\t\twant1:   \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete 2\",\n\t\t\tdata:    []byte(\"\\x01\\x05jesus\\x05x\"),\n\t\t\twant1:   \"\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := bytes.NewReader(tt.data)\n\t\t\tgot, got1, err := ReadTCPResponse(r)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ReadTCPResponse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"ReadTCPResponse() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"ReadTCPResponse() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteTCPResponse(t *testing.T) {\n\ttype args struct {\n\t\tok  bool\n\t\tmsg string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantW   string // Just a prefix, we don't care about the padding\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal ok\",\n\t\t\targs:    args{ok: true, msg: \"hello world\"},\n\t\t\twantW:   \"\\x00\\x0bhello world\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal error\",\n\t\t\targs:    args{ok: false, msg: \"stop!!\"},\n\t\t\twantW:   \"\\x01\\x06stop!!\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty\",\n\t\t\targs:    args{ok: true, msg: \"\"},\n\t\t\twantW:   \"\\x00\\x00\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &bytes.Buffer{}\n\t\t\terr := WriteTCPResponse(w, tt.args.ok, tt.args.msg)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"WriteTCPResponse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) {\n\t\t\t\tt.Errorf(\"WriteTCPResponse() gotW = %v, want %v\", gotW, tt.wantW)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "core/internal/utils/atomic.go",
    "content": "package utils\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype AtomicTime struct {\n\tv atomic.Value\n}\n\nfunc NewAtomicTime(t time.Time) *AtomicTime {\n\ta := &AtomicTime{}\n\ta.Set(t)\n\treturn a\n}\n\nfunc (t *AtomicTime) Set(new time.Time) {\n\tt.v.Store(new)\n}\n\nfunc (t *AtomicTime) Get() time.Time {\n\treturn t.v.Load().(time.Time)\n}\n"
  },
  {
    "path": "core/internal/utils/qstream.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/apernet/quic-go\"\n)\n\n// QStream is a wrapper of quic.Stream that handles Close() in a way that\n// makes more sense to us. By default, quic.Stream's Close() only closes\n// the write side of the stream, not the read side. And if there is unread\n// data, the stream is not really considered closed until either the data\n// is drained or CancelRead() is called.\n// References:\n// - https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/stream.go\n// - https://github.com/quic-go/quic-go/issues/3558\n// - https://github.com/quic-go/quic-go/issues/1599\ntype QStream struct {\n\tStream quic.Stream\n}\n\nfunc (s *QStream) StreamID() quic.StreamID {\n\treturn s.Stream.StreamID()\n}\n\nfunc (s *QStream) Read(p []byte) (n int, err error) {\n\treturn s.Stream.Read(p)\n}\n\nfunc (s *QStream) CancelRead(code quic.StreamErrorCode) {\n\ts.Stream.CancelRead(code)\n}\n\nfunc (s *QStream) SetReadDeadline(t time.Time) error {\n\treturn s.Stream.SetReadDeadline(t)\n}\n\nfunc (s *QStream) Write(p []byte) (n int, err error) {\n\treturn s.Stream.Write(p)\n}\n\nfunc (s *QStream) Close() error {\n\ts.Stream.CancelRead(0)\n\treturn s.Stream.Close()\n}\n\nfunc (s *QStream) CancelWrite(code quic.StreamErrorCode) {\n\ts.Stream.CancelWrite(code)\n}\n\nfunc (s *QStream) Context() context.Context {\n\treturn s.Stream.Context()\n}\n\nfunc (s *QStream) SetWriteDeadline(t time.Time) error {\n\treturn s.Stream.SetWriteDeadline(t)\n}\n\nfunc (s *QStream) SetDeadline(t time.Time) error {\n\treturn s.Stream.SetDeadline(t)\n}\n"
  },
  {
    "path": "core/server/.mockery.yaml",
    "content": "with-expecter: true\ninpackage: true\ndir: .\npackages:\n  github.com/apernet/hysteria/core/v2/server:\n    interfaces:\n      udpIO:\n        config:\n          mockname: mockUDPIO\n      udpEventLogger:\n        config:\n          mockname: mockUDPEventLogger\n      UDPConn:\n        config:\n          mockname: mockUDPConn\n"
  },
  {
    "path": "core/server/config.go",
    "content": "package server\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/errors\"\n\t\"github.com/apernet/hysteria/core/v2/internal/pmtud\"\n\t\"github.com/apernet/quic-go\"\n)\n\nconst (\n\tdefaultStreamReceiveWindow = 8388608                            // 8MB\n\tdefaultConnReceiveWindow   = defaultStreamReceiveWindow * 5 / 2 // 20MB\n\tdefaultMaxIdleTimeout      = 30 * time.Second\n\tdefaultMaxIncomingStreams  = 1024\n\tdefaultUDPIdleTimeout      = 60 * time.Second\n)\n\ntype Config struct {\n\tTLSConfig             TLSConfig\n\tQUICConfig            QUICConfig\n\tConn                  net.PacketConn\n\tRequestHook           RequestHook\n\tOutbound              Outbound\n\tBandwidthConfig       BandwidthConfig\n\tIgnoreClientBandwidth bool\n\tDisableUDP            bool\n\tUDPIdleTimeout        time.Duration\n\tAuthenticator         Authenticator\n\tEventLogger           EventLogger\n\tTrafficLogger         TrafficLogger\n\tMasqHandler           http.Handler\n}\n\n// fill fills the fields that are not set by the user with default values when possible,\n// and returns an error if the user has not set a required field, or if a field is invalid.\nfunc (c *Config) fill() error {\n\tif len(c.TLSConfig.Certificates) == 0 && c.TLSConfig.GetCertificate == nil {\n\t\treturn errors.ConfigError{Field: \"TLSConfig\", Reason: \"must set at least one of Certificates or GetCertificate\"}\n\t}\n\tif c.QUICConfig.InitialStreamReceiveWindow == 0 {\n\t\tc.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow\n\t} else if c.QUICConfig.InitialStreamReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.InitialStreamReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.MaxStreamReceiveWindow == 0 {\n\t\tc.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow\n\t} else if c.QUICConfig.MaxStreamReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.MaxStreamReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.InitialConnectionReceiveWindow == 0 {\n\t\tc.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow\n\t} else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.InitialConnectionReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.MaxConnectionReceiveWindow == 0 {\n\t\tc.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow\n\t} else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.MaxConnectionReceiveWindow\", Reason: \"must be at least 16384\"}\n\t}\n\tif c.QUICConfig.MaxIdleTimeout == 0 {\n\t\tc.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout\n\t} else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.MaxIdleTimeout\", Reason: \"must be between 4s and 120s\"}\n\t}\n\tif c.QUICConfig.MaxIncomingStreams == 0 {\n\t\tc.QUICConfig.MaxIncomingStreams = defaultMaxIncomingStreams\n\t} else if c.QUICConfig.MaxIncomingStreams < 8 {\n\t\treturn errors.ConfigError{Field: \"QUICConfig.MaxIncomingStreams\", Reason: \"must be at least 8\"}\n\t}\n\tc.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery\n\tif c.Conn == nil {\n\t\treturn errors.ConfigError{Field: \"Conn\", Reason: \"must be set\"}\n\t}\n\tif c.Outbound == nil {\n\t\tc.Outbound = &defaultOutbound{}\n\t}\n\tif c.BandwidthConfig.MaxTx != 0 && c.BandwidthConfig.MaxTx < 65536 {\n\t\treturn errors.ConfigError{Field: \"BandwidthConfig.MaxTx\", Reason: \"must be at least 65536\"}\n\t}\n\tif c.BandwidthConfig.MaxRx != 0 && c.BandwidthConfig.MaxRx < 65536 {\n\t\treturn errors.ConfigError{Field: \"BandwidthConfig.MaxRx\", Reason: \"must be at least 65536\"}\n\t}\n\tif c.UDPIdleTimeout == 0 {\n\t\tc.UDPIdleTimeout = defaultUDPIdleTimeout\n\t} else if c.UDPIdleTimeout < 2*time.Second || c.UDPIdleTimeout > 600*time.Second {\n\t\treturn errors.ConfigError{Field: \"UDPIdleTimeout\", Reason: \"must be between 2s and 600s\"}\n\t}\n\tif c.Authenticator == nil {\n\t\treturn errors.ConfigError{Field: \"Authenticator\", Reason: \"must be set\"}\n\t}\n\treturn nil\n}\n\n// TLSConfig contains the TLS configuration fields that we want to expose to the user.\ntype TLSConfig struct {\n\tCertificates   []tls.Certificate\n\tGetCertificate func(info *tls.ClientHelloInfo) (*tls.Certificate, error)\n}\n\n// QUICConfig contains the QUIC configuration fields that we want to expose to the user.\ntype QUICConfig struct {\n\tInitialStreamReceiveWindow     uint64\n\tMaxStreamReceiveWindow         uint64\n\tInitialConnectionReceiveWindow uint64\n\tMaxConnectionReceiveWindow     uint64\n\tMaxIdleTimeout                 time.Duration\n\tMaxIncomingStreams             int64\n\tDisablePathMTUDiscovery        bool // The server may still override this to true on unsupported platforms.\n}\n\n// RequestHook allows filtering and modifying requests before the server connects to the remote.\n// A request will only be hooked if Check returns true.\n// The returned byte slice, if not empty, will be sent to the remote before proxying - this is\n// mainly for \"putting back\" the content read from the client for sniffing, etc.\n// Return a non-nil error to abort the connection.\n// Note that due to the current architectural limitations, it can only inspect the first packet\n// of a UDP connection. It also cannot put back any data as the first packet is always sent as-is.\ntype RequestHook interface {\n\tCheck(isUDP bool, reqAddr string) bool\n\tTCP(stream quic.Stream, reqAddr *string) ([]byte, error)\n\tUDP(data []byte, reqAddr *string) error\n}\n\n// Outbound provides the implementation of how the server should connect to remote servers.\n// Although UDP includes a reqAddr, the implementation does not necessarily have to use it\n// to make a \"connected\" UDP connection that does not accept packets from other addresses.\n// In fact, the default implementation simply uses net.ListenUDP for a \"full-cone\" behavior.\ntype Outbound interface {\n\tTCP(reqAddr string) (net.Conn, error)\n\tUDP(reqAddr string) (UDPConn, error)\n}\n\n// UDPConn is like net.PacketConn, but uses string for addresses.\ntype UDPConn interface {\n\tReadFrom(b []byte) (int, string, error)\n\tWriteTo(b []byte, addr string) (int, error)\n\tClose() error\n}\n\ntype defaultOutbound struct{}\n\nvar defaultOutboundDialer = net.Dialer{\n\tTimeout: 10 * time.Second,\n}\n\nfunc (o *defaultOutbound) TCP(reqAddr string) (net.Conn, error) {\n\treturn defaultOutboundDialer.Dial(\"tcp\", reqAddr)\n}\n\nfunc (o *defaultOutbound) UDP(reqAddr string) (UDPConn, error) {\n\tconn, err := net.ListenUDP(\"udp\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &defaultUDPConn{conn}, nil\n}\n\ntype defaultUDPConn struct {\n\t*net.UDPConn\n}\n\nfunc (c *defaultUDPConn) ReadFrom(b []byte) (int, string, error) {\n\tn, addr, err := c.UDPConn.ReadFrom(b)\n\tif addr != nil {\n\t\treturn n, addr.String(), err\n\t} else {\n\t\treturn n, \"\", err\n\t}\n}\n\nfunc (c *defaultUDPConn) WriteTo(b []byte, addr string) (int, error) {\n\tuAddr, err := net.ResolveUDPAddr(\"udp\", addr)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn c.UDPConn.WriteTo(b, uAddr)\n}\n\n// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second.\ntype BandwidthConfig struct {\n\tMaxTx uint64\n\tMaxRx uint64\n}\n\n// Authenticator is an interface that provides authentication logic.\ntype Authenticator interface {\n\tAuthenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string)\n}\n\n// EventLogger is an interface that provides logging logic.\ntype EventLogger interface {\n\tConnect(addr net.Addr, id string, tx uint64)\n\tDisconnect(addr net.Addr, id string, err error)\n\tTCPRequest(addr net.Addr, id, reqAddr string)\n\tTCPError(addr net.Addr, id, reqAddr string, err error)\n\tUDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string)\n\tUDPError(addr net.Addr, id string, sessionID uint32, err error)\n}\n\n// TrafficLogger is an interface that provides traffic logging logic.\n// Tx/Rx in this context refers to the server-remote (proxy target) perspective.\n// Tx is the bytes sent from the server to the remote.\n// Rx is the bytes received by the server from the remote.\n// Apart from logging, the Log function can also return false to signal\n// that the client should be disconnected. This can be used to implement\n// bandwidth limits or post-connection authentication, for example.\n// The implementation of this interface must be thread-safe.\ntype TrafficLogger interface {\n\tLogTraffic(id string, tx, rx uint64) (ok bool)\n\tPushTrafficToV2boardInterval(url string, interval time.Duration)\n\tLogOnlineState(id string, online bool)\n\tNewKick(id string) (ok bool)\n}\n"
  },
  {
    "path": "core/server/copy.go",
    "content": "package server\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\nvar errDisconnect = errors.New(\"traffic logger requested disconnect\")\n\nfunc copyBufferLog(dst io.Writer, src io.Reader, log func(n uint64) bool) error {\n\tbuf := make([]byte, 32*1024)\n\tfor {\n\t\tnr, er := src.Read(buf)\n\t\tif nr > 0 {\n\t\t\tif !log(uint64(nr)) {\n\t\t\t\t// Log returns false, which means that the client should be disconnected\n\t\t\t\treturn errDisconnect\n\t\t\t}\n\t\t\t_, ew := dst.Write(buf[0:nr])\n\t\t\tif ew != nil {\n\t\t\t\treturn ew\n\t\t\t}\n\t\t}\n\t\tif er != nil {\n\t\t\tif er == io.EOF {\n\t\t\t\t// EOF should not be considered as an error\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn er\n\t\t}\n\t}\n}\n\nfunc copyTwoWayWithLogger(id string, serverRw, remoteRw io.ReadWriter, l TrafficLogger) error {\n\terrChan := make(chan error, 2)\n\tgo func() {\n\t\terrChan <- copyBufferLog(serverRw, remoteRw, func(n uint64) bool {\n\t\t\treturn l.LogTraffic(id, 0, n)\n\t\t})\n\t}()\n\tgo func() {\n\t\terrChan <- copyBufferLog(remoteRw, serverRw, func(n uint64) bool {\n\t\t\treturn l.LogTraffic(id, n, 0)\n\t\t})\n\t}()\n\t// Block until one of the two goroutines returns\n\treturn <-errChan\n}\n\n// copyTwoWay is the \"fast-path\" version of copyTwoWayWithLogger that does not log traffic.\n// It uses the built-in io.Copy instead of our own copyBufferLog.\nfunc copyTwoWay(serverRw, remoteRw io.ReadWriter) error {\n\terrChan := make(chan error, 2)\n\tgo func() {\n\t\t_, err := io.Copy(serverRw, remoteRw)\n\t\terrChan <- err\n\t}()\n\tgo func() {\n\t\t_, err := io.Copy(remoteRw, serverRw)\n\t\terrChan <- err\n\t}()\n\t// Block until one of the two goroutines returns\n\treturn <-errChan\n}\n"
  },
  {
    "path": "core/server/mock_UDPConn.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage server\n\nimport mock \"github.com/stretchr/testify/mock\"\n\n// mockUDPConn is an autogenerated mock type for the UDPConn type\ntype mockUDPConn struct {\n\tmock.Mock\n}\n\ntype mockUDPConn_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter {\n\treturn &mockUDPConn_Expecter{mock: &_m.Mock}\n}\n\n// Close provides a mock function with given fields:\nfunc (_m *mockUDPConn) Close() error {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Close\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype mockUDPConn_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\nfunc (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call {\n\treturn &mockUDPConn_Close_Call{Call: _e.mock.On(\"Close\")}\n}\n\nfunc (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// ReadFrom provides a mock function with given fields: b\nfunc (_m *mockUDPConn) ReadFrom(b []byte) (int, string, error) {\n\tret := _m.Called(b)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for ReadFrom\")\n\t}\n\n\tvar r0 int\n\tvar r1 string\n\tvar r2 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok {\n\t\treturn rf(b)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(b)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) string); ok {\n\t\tr1 = rf(b)\n\t} else {\n\t\tr1 = ret.Get(1).(string)\n\t}\n\n\tif rf, ok := ret.Get(2).(func([]byte) error); ok {\n\t\tr2 = rf(b)\n\t} else {\n\t\tr2 = ret.Error(2)\n\t}\n\n\treturn r0, r1, r2\n}\n\n// mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom'\ntype mockUDPConn_ReadFrom_Call struct {\n\t*mock.Call\n}\n\n// ReadFrom is a helper method to define mock.On call\n//   - b []byte\nfunc (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call {\n\treturn &mockUDPConn_ReadFrom_Call{Call: _e.mock.On(\"ReadFrom\", b)}\n}\n\nfunc (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *mockUDPConn_ReadFrom_Call {\n\t_c.Call.Return(_a0, _a1, _a2)\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *mockUDPConn_ReadFrom_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// WriteTo provides a mock function with given fields: b, addr\nfunc (_m *mockUDPConn) WriteTo(b []byte, addr string) (int, error) {\n\tret := _m.Called(b, addr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for WriteTo\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok {\n\t\treturn rf(b, addr)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte, string) int); ok {\n\t\tr0 = rf(b, addr)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte, string) error); ok {\n\t\tr1 = rf(b, addr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo'\ntype mockUDPConn_WriteTo_Call struct {\n\t*mock.Call\n}\n\n// WriteTo is a helper method to define mock.On call\n//   - b []byte\n//   - addr string\nfunc (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call {\n\treturn &mockUDPConn_WriteTo_Call{Call: _e.mock.On(\"WriteTo\", b, addr)}\n}\n\nfunc (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *mockUDPConn_WriteTo_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte), args[1].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *mockUDPConn_WriteTo_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc newMockUDPConn(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *mockUDPConn {\n\tmock := &mockUDPConn{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/server/mock_udpEventLogger.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage server\n\nimport mock \"github.com/stretchr/testify/mock\"\n\n// mockUDPEventLogger is an autogenerated mock type for the udpEventLogger type\ntype mockUDPEventLogger struct {\n\tmock.Mock\n}\n\ntype mockUDPEventLogger_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *mockUDPEventLogger) EXPECT() *mockUDPEventLogger_Expecter {\n\treturn &mockUDPEventLogger_Expecter{mock: &_m.Mock}\n}\n\n// Close provides a mock function with given fields: sessionID, err\nfunc (_m *mockUDPEventLogger) Close(sessionID uint32, err error) {\n\t_m.Called(sessionID, err)\n}\n\n// mockUDPEventLogger_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype mockUDPEventLogger_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\n//   - sessionID uint32\n//   - err error\nfunc (_e *mockUDPEventLogger_Expecter) Close(sessionID interface{}, err interface{}) *mockUDPEventLogger_Close_Call {\n\treturn &mockUDPEventLogger_Close_Call{Call: _e.mock.On(\"Close\", sessionID, err)}\n}\n\nfunc (_c *mockUDPEventLogger_Close_Call) Run(run func(sessionID uint32, err error)) *mockUDPEventLogger_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(uint32), args[1].(error))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPEventLogger_Close_Call) Return() *mockUDPEventLogger_Close_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *mockUDPEventLogger_Close_Call) RunAndReturn(run func(uint32, error)) *mockUDPEventLogger_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// New provides a mock function with given fields: sessionID, reqAddr\nfunc (_m *mockUDPEventLogger) New(sessionID uint32, reqAddr string) {\n\t_m.Called(sessionID, reqAddr)\n}\n\n// mockUDPEventLogger_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New'\ntype mockUDPEventLogger_New_Call struct {\n\t*mock.Call\n}\n\n// New is a helper method to define mock.On call\n//   - sessionID uint32\n//   - reqAddr string\nfunc (_e *mockUDPEventLogger_Expecter) New(sessionID interface{}, reqAddr interface{}) *mockUDPEventLogger_New_Call {\n\treturn &mockUDPEventLogger_New_Call{Call: _e.mock.On(\"New\", sessionID, reqAddr)}\n}\n\nfunc (_c *mockUDPEventLogger_New_Call) Run(run func(sessionID uint32, reqAddr string)) *mockUDPEventLogger_New_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(uint32), args[1].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPEventLogger_New_Call) Return() *mockUDPEventLogger_New_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *mockUDPEventLogger_New_Call) RunAndReturn(run func(uint32, string)) *mockUDPEventLogger_New_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc newMockUDPEventLogger(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *mockUDPEventLogger {\n\tmock := &mockUDPEventLogger{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/server/mock_udpIO.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage server\n\nimport (\n\tprotocol \"github.com/apernet/hysteria/core/v2/internal/protocol\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// mockUDPIO is an autogenerated mock type for the udpIO type\ntype mockUDPIO struct {\n\tmock.Mock\n}\n\ntype mockUDPIO_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter {\n\treturn &mockUDPIO_Expecter{mock: &_m.Mock}\n}\n\n// Hook provides a mock function with given fields: data, reqAddr\nfunc (_m *mockUDPIO) Hook(data []byte, reqAddr *string) error {\n\tret := _m.Called(data, reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Hook\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]byte, *string) error); ok {\n\t\tr0 = rf(data, reqAddr)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockUDPIO_Hook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Hook'\ntype mockUDPIO_Hook_Call struct {\n\t*mock.Call\n}\n\n// Hook is a helper method to define mock.On call\n//   - data []byte\n//   - reqAddr *string\nfunc (_e *mockUDPIO_Expecter) Hook(data interface{}, reqAddr interface{}) *mockUDPIO_Hook_Call {\n\treturn &mockUDPIO_Hook_Call{Call: _e.mock.On(\"Hook\", data, reqAddr)}\n}\n\nfunc (_c *mockUDPIO_Hook_Call) Run(run func(data []byte, reqAddr *string)) *mockUDPIO_Hook_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte), args[1].(*string))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_Hook_Call) Return(_a0 error) *mockUDPIO_Hook_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_Hook_Call) RunAndReturn(run func([]byte, *string) error) *mockUDPIO_Hook_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// ReceiveMessage provides a mock function with given fields:\nfunc (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for ReceiveMessage\")\n\t}\n\n\tvar r0 *protocol.UDPMessage\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok {\n\t\treturn rf()\n\t}\n\tif rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*protocol.UDPMessage)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func() error); ok {\n\t\tr1 = rf()\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage'\ntype mockUDPIO_ReceiveMessage_Call struct {\n\t*mock.Call\n}\n\n// ReceiveMessage is a helper method to define mock.On call\nfunc (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call {\n\treturn &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On(\"ReceiveMessage\")}\n}\n\nfunc (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SendMessage provides a mock function with given fields: _a0, _a1\nfunc (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error {\n\tret := _m.Called(_a0, _a1)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SendMessage\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok {\n\t\tr0 = rf(_a0, _a1)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage'\ntype mockUDPIO_SendMessage_Call struct {\n\t*mock.Call\n}\n\n// SendMessage is a helper method to define mock.On call\n//   - _a0 []byte\n//   - _a1 *protocol.UDPMessage\nfunc (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call {\n\treturn &mockUDPIO_SendMessage_Call{Call: _e.mock.On(\"SendMessage\", _a0, _a1)}\n}\n\nfunc (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte), args[1].(*protocol.UDPMessage))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UDP provides a mock function with given fields: reqAddr\nfunc (_m *mockUDPIO) UDP(reqAddr string) (UDPConn, error) {\n\tret := _m.Called(reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UDP\")\n\t}\n\n\tvar r0 UDPConn\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(string) (UDPConn, error)); ok {\n\t\treturn rf(reqAddr)\n\t}\n\tif rf, ok := ret.Get(0).(func(string) UDPConn); ok {\n\t\tr0 = rf(reqAddr)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(UDPConn)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(reqAddr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockUDPIO_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'\ntype mockUDPIO_UDP_Call struct {\n\t*mock.Call\n}\n\n// UDP is a helper method to define mock.On call\n//   - reqAddr string\nfunc (_e *mockUDPIO_Expecter) UDP(reqAddr interface{}) *mockUDPIO_UDP_Call {\n\treturn &mockUDPIO_UDP_Call{Call: _e.mock.On(\"UDP\", reqAddr)}\n}\n\nfunc (_c *mockUDPIO_UDP_Call) Run(run func(reqAddr string)) *mockUDPIO_UDP_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(string))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockUDPIO_UDP_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *mockUDPIO_UDP_Call) RunAndReturn(run func(string) (UDPConn, error)) *mockUDPIO_UDP_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc newMockUDPIO(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *mockUDPIO {\n\tmock := &mockUDPIO{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "core/server/server.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/apernet/quic-go\"\n\t\"github.com/apernet/quic-go/http3\"\n\n\t\"github.com/apernet/hysteria/core/v2/internal/congestion\"\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n\t\"github.com/apernet/hysteria/core/v2/internal/utils\"\n)\n\nconst (\n\tcloseErrCodeOK                  = 0x100 // HTTP3 ErrCodeNoError\n\tcloseErrCodeTrafficLimitReached = 0x107 // HTTP3 ErrCodeExcessiveLoad\n)\n\ntype Server interface {\n\tServe() error\n\tClose() error\n}\n\nfunc NewServer(config *Config) (Server, error) {\n\tif err := config.fill(); err != nil {\n\t\treturn nil, err\n\t}\n\ttlsConfig := http3.ConfigureTLSConfig(&tls.Config{\n\t\tCertificates:   config.TLSConfig.Certificates,\n\t\tGetCertificate: config.TLSConfig.GetCertificate,\n\t})\n\tquicConfig := &quic.Config{\n\t\tInitialStreamReceiveWindow:     config.QUICConfig.InitialStreamReceiveWindow,\n\t\tMaxStreamReceiveWindow:         config.QUICConfig.MaxStreamReceiveWindow,\n\t\tInitialConnectionReceiveWindow: config.QUICConfig.InitialConnectionReceiveWindow,\n\t\tMaxConnectionReceiveWindow:     config.QUICConfig.MaxConnectionReceiveWindow,\n\t\tMaxIdleTimeout:                 config.QUICConfig.MaxIdleTimeout,\n\t\tMaxIncomingStreams:             config.QUICConfig.MaxIncomingStreams,\n\t\tDisablePathMTUDiscovery:        config.QUICConfig.DisablePathMTUDiscovery,\n\t\tEnableDatagrams:                true,\n\t}\n\tlistener, err := quic.Listen(config.Conn, tlsConfig, quicConfig)\n\tif err != nil {\n\t\t_ = config.Conn.Close()\n\t\treturn nil, err\n\t}\n\treturn &serverImpl{\n\t\tconfig:   config,\n\t\tlistener: listener,\n\t}, nil\n}\n\ntype serverImpl struct {\n\tconfig   *Config\n\tlistener *quic.Listener\n}\n\nfunc (s *serverImpl) Serve() error {\n\tfor {\n\t\tconn, err := s.listener.Accept(context.Background())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo s.handleClient(conn)\n\t}\n}\n\nfunc (s *serverImpl) Close() error {\n\terr := s.listener.Close()\n\t_ = s.config.Conn.Close()\n\treturn err\n}\n\nfunc (s *serverImpl) handleClient(conn quic.Connection) {\n\thandler := newH3sHandler(s.config, conn)\n\th3s := http3.Server{\n\t\tHandler:        handler,\n\t\tStreamHijacker: handler.ProxyStreamHijacker,\n\t}\n\terr := h3s.ServeQUICConn(conn)\n\t// If the client is authenticated, we need to log the disconnect event\n\tif handler.authenticated {\n\t\tif tl := s.config.TrafficLogger; tl != nil {\n\t\t\ttl.LogOnlineState(handler.authID, false)\n\t\t}\n\t\tif el := s.config.EventLogger; el != nil {\n\t\t\tel.Disconnect(conn.RemoteAddr(), handler.authID, err)\n\t\t}\n\t}\n\t_ = conn.CloseWithError(closeErrCodeOK, \"\")\n}\n\ntype h3sHandler struct {\n\tconfig *Config\n\tconn   quic.Connection\n\n\tauthenticated bool\n\tauthMutex     sync.Mutex\n\tauthID        string\n\n\tudpSM *udpSessionManager // Only set after authentication\n}\n\nfunc newH3sHandler(config *Config, conn quic.Connection) *h3sHandler {\n\treturn &h3sHandler{\n\t\tconfig: config,\n\t\tconn:   conn,\n\t}\n}\n\nfunc (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif r.Method == http.MethodPost && r.Host == protocol.URLHost && r.URL.Path == protocol.URLPath {\n\t\th.authMutex.Lock()\n\t\tdefer h.authMutex.Unlock()\n\t\tif h.authenticated {\n\t\t\t// Already authenticated\n\t\t\tprotocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{\n\t\t\t\tUDPEnabled: !h.config.DisableUDP,\n\t\t\t\tRx:         h.config.BandwidthConfig.MaxRx,\n\t\t\t\tRxAuto:     h.config.IgnoreClientBandwidth,\n\t\t\t})\n\t\t\tw.WriteHeader(protocol.StatusAuthOK)\n\t\t\treturn\n\t\t}\n\t\tauthReq := protocol.AuthRequestFromHeader(r.Header)\n\t\tactualTx := authReq.Rx\n\t\tok, id := h.config.Authenticator.Authenticate(h.conn.RemoteAddr(), authReq.Auth, actualTx)\n\t\tif ok {\n\t\t\t// Set authenticated flag\n\t\t\th.authenticated = true\n\t\t\th.authID = id\n\t\t\tif h.config.IgnoreClientBandwidth {\n\t\t\t\t// Ignore client bandwidth, always use BBR\n\t\t\t\tcongestion.UseBBR(h.conn)\n\t\t\t\tactualTx = 0\n\t\t\t} else {\n\t\t\t\t// actualTx = min(serverTx, clientRx)\n\t\t\t\tif h.config.BandwidthConfig.MaxTx > 0 && actualTx > h.config.BandwidthConfig.MaxTx {\n\t\t\t\t\t// We have a maxTx limit and the client is asking for more than that,\n\t\t\t\t\t// return and use the limit instead\n\t\t\t\t\tactualTx = h.config.BandwidthConfig.MaxTx\n\t\t\t\t}\n\t\t\t\tif actualTx > 0 {\n\t\t\t\t\tcongestion.UseBrutal(h.conn, actualTx)\n\t\t\t\t} else {\n\t\t\t\t\t// Client doesn't know its own bandwidth, use BBR\n\t\t\t\t\tcongestion.UseBBR(h.conn)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Auth OK, send response\n\t\t\tprotocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{\n\t\t\t\tUDPEnabled: !h.config.DisableUDP,\n\t\t\t\tRx:         h.config.BandwidthConfig.MaxRx,\n\t\t\t\tRxAuto:     h.config.IgnoreClientBandwidth,\n\t\t\t})\n\t\t\tw.WriteHeader(protocol.StatusAuthOK)\n\t\t\t// Call event logger\n\t\t\tif tl := h.config.TrafficLogger; tl != nil {\n\t\t\t\ttl.LogOnlineState(id, true)\n\t\t\t}\n\t\t\tif el := h.config.EventLogger; el != nil {\n\t\t\t\tel.Connect(h.conn.RemoteAddr(), id, actualTx)\n\t\t\t}\n\t\t\t// Initialize UDP session manager (if UDP is enabled)\n\t\t\t// We use sync.Once to make sure that only one goroutine is started,\n\t\t\t// as ServeHTTP may be called by multiple goroutines simultaneously\n\t\t\tif !h.config.DisableUDP {\n\t\t\t\tgo func() {\n\t\t\t\t\tsm := newUDPSessionManager(\n\t\t\t\t\t\t&udpIOImpl{h.conn, id, h.config.TrafficLogger, h.config.RequestHook, h.config.Outbound},\n\t\t\t\t\t\t&udpEventLoggerImpl{h.conn, id, h.config.EventLogger},\n\t\t\t\t\t\th.config.UDPIdleTimeout)\n\t\t\t\t\th.udpSM = sm\n\t\t\t\t\tgo sm.Run()\n\t\t\t\t}()\n\t\t\t}\n\t\t} else {\n\t\t\t// Auth failed, pretend to be a normal HTTP server\n\t\t\th.masqHandler(w, r)\n\t\t}\n\t} else {\n\t\t// Not an auth request, pretend to be a normal HTTP server\n\t\th.masqHandler(w, r)\n\t}\n}\n\nfunc (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, id quic.ConnectionTracingID, stream quic.Stream, err error) (bool, error) {\n\tif err != nil || !h.authenticated {\n\t\treturn false, nil\n\t}\n\n\t// Wraps the stream with QStream, which handles Close() properly\n\tstream = &utils.QStream{Stream: stream}\n\n\tswitch ft {\n\tcase protocol.FrameTypeTCPRequest:\n\t\tgo h.handleTCPRequest(stream)\n\t\treturn true, nil\n\tdefault:\n\t\treturn false, nil\n\t}\n}\n\nfunc (h *h3sHandler) handleTCPRequest(stream quic.Stream) {\n\t// Read request\n\treqAddr, err := protocol.ReadTCPRequest(stream)\n\tif err != nil {\n\t\t_ = stream.Close()\n\t\treturn\n\t}\n\t// Call the hook if set\n\tvar putback []byte\n\tvar hooked bool\n\tif h.config.RequestHook != nil {\n\t\thooked = h.config.RequestHook.Check(false, reqAddr)\n\t\t// When the hook is enabled, the server should always accept a connection\n\t\t// so that the client will send whatever request the hook wants to see.\n\t\t// This is essentially a server-side fast-open.\n\t\tif hooked {\n\t\t\t_ = protocol.WriteTCPResponse(stream, true, \"RequestHook enabled\")\n\t\t\tputback, err = h.config.RequestHook.TCP(stream, &reqAddr)\n\t\t\tif err != nil {\n\t\t\t\t_ = stream.Close()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\t// Log the event\n\tif h.config.EventLogger != nil {\n\t\th.config.EventLogger.TCPRequest(h.conn.RemoteAddr(), h.authID, reqAddr)\n\t}\n\t// Dial target\n\ttConn, err := h.config.Outbound.TCP(reqAddr)\n\tif err != nil {\n\t\tif !hooked {\n\t\t\t_ = protocol.WriteTCPResponse(stream, false, err.Error())\n\t\t}\n\t\t_ = stream.Close()\n\t\t// Log the error\n\t\tif h.config.EventLogger != nil {\n\t\t\th.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err)\n\t\t}\n\t\treturn\n\t}\n\tif !hooked {\n\t\t_ = protocol.WriteTCPResponse(stream, true, \"Connected\")\n\t}\n\t// Put back the data if the hook requested\n\tif len(putback) > 0 {\n\t\t_, _ = tConn.Write(putback)\n\t}\n\t// Start proxying\n\tif h.config.TrafficLogger != nil {\n\t\terr = copyTwoWayWithLogger(h.authID, stream, tConn, h.config.TrafficLogger)\n\t} else {\n\t\t// Use the fast path if no traffic logger is set\n\t\terr = copyTwoWay(stream, tConn)\n\t}\n\tif h.config.EventLogger != nil {\n\t\th.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err)\n\t}\n\t// Cleanup\n\t_ = tConn.Close()\n\t_ = stream.Close()\n\t// Disconnect the client if TrafficLogger requested\n\tif err == errDisconnect {\n\t\t_ = h.conn.CloseWithError(closeErrCodeTrafficLimitReached, \"\")\n\t}\n}\n\nfunc (h *h3sHandler) masqHandler(w http.ResponseWriter, r *http.Request) {\n\tif h.config.MasqHandler != nil {\n\t\th.config.MasqHandler.ServeHTTP(w, r)\n\t} else {\n\t\t// Return 404 for everything\n\t\thttp.NotFound(w, r)\n\t}\n}\n\n// udpIOImpl is the IO implementation for udpSessionManager with TrafficLogger support\ntype udpIOImpl struct {\n\tConn          quic.Connection\n\tAuthID        string\n\tTrafficLogger TrafficLogger\n\tRequestHook   RequestHook\n\tOutbound      Outbound\n}\n\nfunc (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {\n\tfor {\n\t\tmsg, err := io.Conn.ReceiveDatagram(context.Background())\n\t\tif err != nil {\n\t\t\t// Connection error, this will stop the session manager\n\t\t\treturn nil, err\n\t\t}\n\t\tudpMsg, err := protocol.ParseUDPMessage(msg)\n\t\tif err != nil {\n\t\t\t// Invalid message, this is fine - just wait for the next\n\t\t\tcontinue\n\t\t}\n\t\tif io.TrafficLogger != nil {\n\t\t\tok := io.TrafficLogger.LogTraffic(io.AuthID, uint64(len(udpMsg.Data)), 0)\n\t\t\tif !ok {\n\t\t\t\t// TrafficLogger requested to disconnect the client\n\t\t\t\t_ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, \"\")\n\t\t\t\treturn nil, errDisconnect\n\t\t\t}\n\t\t}\n\t\treturn udpMsg, nil\n\t}\n}\n\nfunc (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {\n\tif io.TrafficLogger != nil {\n\t\tok := io.TrafficLogger.LogTraffic(io.AuthID, 0, uint64(len(msg.Data)))\n\t\tif !ok {\n\t\t\t// TrafficLogger requested to disconnect the client\n\t\t\t_ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, \"\")\n\t\t\treturn errDisconnect\n\t\t}\n\t}\n\tmsgN := msg.Serialize(buf)\n\tif msgN < 0 {\n\t\t// Message larger than buffer, silent drop\n\t\treturn nil\n\t}\n\treturn io.Conn.SendDatagram(buf[:msgN])\n}\n\nfunc (io *udpIOImpl) Hook(data []byte, reqAddr *string) error {\n\tif io.RequestHook != nil && io.RequestHook.Check(true, *reqAddr) {\n\t\treturn io.RequestHook.UDP(data, reqAddr)\n\t} else {\n\t\treturn nil\n\t}\n}\n\nfunc (io *udpIOImpl) UDP(reqAddr string) (UDPConn, error) {\n\treturn io.Outbound.UDP(reqAddr)\n}\n\ntype udpEventLoggerImpl struct {\n\tConn        quic.Connection\n\tAuthID      string\n\tEventLogger EventLogger\n}\n\nfunc (l *udpEventLoggerImpl) New(sessionID uint32, reqAddr string) {\n\tif l.EventLogger != nil {\n\t\tl.EventLogger.UDPRequest(l.Conn.RemoteAddr(), l.AuthID, sessionID, reqAddr)\n\t}\n}\n\nfunc (l *udpEventLoggerImpl) Close(sessionID uint32, err error) {\n\tif l.EventLogger != nil {\n\t\tl.EventLogger.UDPError(l.Conn.RemoteAddr(), l.AuthID, sessionID, err)\n\t}\n}\n"
  },
  {
    "path": "core/server/udp.go",
    "content": "package server\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/apernet/quic-go\"\n\n\t\"github.com/apernet/hysteria/core/v2/internal/frag\"\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n\t\"github.com/apernet/hysteria/core/v2/internal/utils\"\n)\n\nconst (\n\tidleCleanupInterval = 1 * time.Second\n)\n\ntype udpIO interface {\n\tReceiveMessage() (*protocol.UDPMessage, error)\n\tSendMessage([]byte, *protocol.UDPMessage) error\n\tHook(data []byte, reqAddr *string) error\n\tUDP(reqAddr string) (UDPConn, error)\n}\n\ntype udpEventLogger interface {\n\tNew(sessionID uint32, reqAddr string)\n\tClose(sessionID uint32, err error)\n}\n\ntype udpSessionEntry struct {\n\tID           uint32\n\tOverrideAddr string // Ignore the address in the UDP message, always use this if not empty\n\tOriginalAddr string // The original address in the UDP message\n\tD            *frag.Defragger\n\tLast         *utils.AtomicTime\n\tIO           udpIO\n\n\tDialFunc func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error)\n\tExitFunc func(err error)\n\n\tconn     UDPConn\n\tconnLock sync.Mutex\n\tclosed   bool\n}\n\nfunc newUDPSessionEntry(\n\tid uint32, io udpIO,\n\tdialFunc func(string, []byte) (UDPConn, string, error),\n\texitFunc func(error),\n) (e *udpSessionEntry) {\n\te = &udpSessionEntry{\n\t\tID:   id,\n\t\tD:    &frag.Defragger{},\n\t\tLast: utils.NewAtomicTime(time.Now()),\n\t\tIO:   io,\n\n\t\tDialFunc: dialFunc,\n\t\tExitFunc: exitFunc,\n\t}\n\n\treturn\n}\n\n// CloseWithErr closes the session and calls ExitFunc with the given error.\n// A nil error indicates the session is cleaned up due to timeout.\nfunc (e *udpSessionEntry) CloseWithErr(err error) {\n\t// We need this lock to ensure not to create conn after session exit\n\te.connLock.Lock()\n\n\tif e.closed {\n\t\t// Already closed\n\t\te.connLock.Unlock()\n\t\treturn\n\t}\n\n\te.closed = true\n\tif e.conn != nil {\n\t\t_ = e.conn.Close()\n\t}\n\te.connLock.Unlock()\n\n\te.ExitFunc(err)\n}\n\n// Feed feeds a UDP message to the session.\n// If the message itself is a complete message, or it completes a fragmented message,\n// the message is written to the session's UDP connection, and the number of bytes\n// written is returned.\n// Otherwise, 0 and nil are returned.\nfunc (e *udpSessionEntry) Feed(msg *protocol.UDPMessage) (int, error) {\n\te.Last.Set(time.Now())\n\tdfMsg := e.D.Feed(msg)\n\tif dfMsg == nil {\n\t\treturn 0, nil\n\t}\n\n\tif e.conn == nil {\n\t\terr := e.initConn(dfMsg)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\n\taddr := dfMsg.Addr\n\tif e.OverrideAddr != \"\" {\n\t\taddr = e.OverrideAddr\n\t}\n\n\treturn e.conn.WriteTo(dfMsg.Data, addr)\n}\n\n// initConn initializes the UDP connection of the session.\n// If no error is returned, the e.conn is set to the new connection.\nfunc (e *udpSessionEntry) initConn(firstMsg *protocol.UDPMessage) error {\n\t// We need this lock to ensure not to create conn after session exit\n\te.connLock.Lock()\n\n\tif e.closed {\n\t\te.connLock.Unlock()\n\t\treturn errors.New(\"session is closed\")\n\t}\n\n\tconn, actualAddr, err := e.DialFunc(firstMsg.Addr, firstMsg.Data)\n\tif err != nil {\n\t\t// Fail fast if DialFunc failed\n\t\t// (usually indicates the connection has been rejected by the ACL)\n\t\te.connLock.Unlock()\n\t\t// CloseWithErr acquires the connLock again\n\t\te.CloseWithErr(err)\n\t\treturn err\n\t}\n\n\te.conn = conn\n\n\tif firstMsg.Addr != actualAddr {\n\t\t// Hook changed the address, enable address override\n\t\te.OverrideAddr = actualAddr\n\t\te.OriginalAddr = firstMsg.Addr\n\t}\n\tgo e.receiveLoop()\n\n\te.connLock.Unlock()\n\treturn nil\n}\n\n// receiveLoop receives incoming UDP packets, packs them into UDP messages,\n// and sends using the IO.\n// Exit when either the underlying UDP connection returns error (e.g. closed),\n// or the IO returns error when sending.\nfunc (e *udpSessionEntry) receiveLoop() {\n\tudpBuf := make([]byte, protocol.MaxUDPSize)\n\tmsgBuf := make([]byte, protocol.MaxUDPSize)\n\tfor {\n\t\tudpN, rAddr, err := e.conn.ReadFrom(udpBuf)\n\t\tif err != nil {\n\t\t\te.CloseWithErr(err)\n\t\t\treturn\n\t\t}\n\t\te.Last.Set(time.Now())\n\n\t\tif e.OriginalAddr != \"\" {\n\t\t\t// Use the original address in the opposite direction,\n\t\t\t// otherwise the QUIC clients or NAT on the client side\n\t\t\t// may not treat it as the same UDP session.\n\t\t\trAddr = e.OriginalAddr\n\t\t}\n\n\t\tmsg := &protocol.UDPMessage{\n\t\t\tSessionID: e.ID,\n\t\t\tPacketID:  0,\n\t\t\tFragID:    0,\n\t\t\tFragCount: 1,\n\t\t\tAddr:      rAddr,\n\t\t\tData:      udpBuf[:udpN],\n\t\t}\n\t\terr = sendMessageAutoFrag(e.IO, msgBuf, msg)\n\t\tif err != nil {\n\t\t\te.CloseWithErr(err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// sendMessageAutoFrag tries to send a UDP message as a whole first,\n// but if it fails due to quic.ErrMessageTooLarge, it tries again by\n// fragmenting the message.\nfunc sendMessageAutoFrag(io udpIO, buf []byte, msg *protocol.UDPMessage) error {\n\terr := io.SendMessage(buf, msg)\n\tvar errTooLarge *quic.DatagramTooLargeError\n\tif errors.As(err, &errTooLarge) {\n\t\t// Message too large, try fragmentation\n\t\tmsg.PacketID = uint16(rand.Intn(0xFFFF)) + 1\n\t\tfMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDataLen))\n\t\tfor _, fMsg := range fMsgs {\n\t\t\terr := io.SendMessage(buf, &fMsg)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t} else {\n\t\treturn err\n\t}\n}\n\n// udpSessionManager manages the lifecycle of UDP sessions.\n// Each UDP session is identified by a SessionID, and corresponds to a UDP connection.\n// A UDP session is created when a UDP message with a new SessionID is received.\n// Similar to standard NAT, a UDP session is destroyed when no UDP message is received\n// for a certain period of time (specified by idleTimeout).\ntype udpSessionManager struct {\n\tio          udpIO\n\teventLogger udpEventLogger\n\tidleTimeout time.Duration\n\n\tmutex sync.RWMutex\n\tm     map[uint32]*udpSessionEntry\n}\n\nfunc newUDPSessionManager(io udpIO, eventLogger udpEventLogger, idleTimeout time.Duration) *udpSessionManager {\n\treturn &udpSessionManager{\n\t\tio:          io,\n\t\teventLogger: eventLogger,\n\t\tidleTimeout: idleTimeout,\n\t\tm:           make(map[uint32]*udpSessionEntry),\n\t}\n}\n\n// Run runs the session manager main loop.\n// Exit and returns error when the underlying io returns error (e.g. closed).\nfunc (m *udpSessionManager) Run() error {\n\tstopCh := make(chan struct{})\n\tgo m.idleCleanupLoop(stopCh)\n\tdefer close(stopCh)\n\tdefer m.cleanup(false)\n\n\tfor {\n\t\tmsg, err := m.io.ReceiveMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.feed(msg)\n\t}\n}\n\nfunc (m *udpSessionManager) idleCleanupLoop(stopCh <-chan struct{}) {\n\tticker := time.NewTicker(idleCleanupInterval)\n\tdefer ticker.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tm.cleanup(true)\n\t\tcase <-stopCh:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (m *udpSessionManager) cleanup(idleOnly bool) {\n\ttimeoutEntry := make([]*udpSessionEntry, 0, len(m.m))\n\n\t// We use RLock here as we are only scanning the map, not deleting from it.\n\tm.mutex.RLock()\n\tnow := time.Now()\n\tfor _, entry := range m.m {\n\t\tif !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout {\n\t\t\ttimeoutEntry = append(timeoutEntry, entry)\n\t\t}\n\t}\n\tm.mutex.RUnlock()\n\n\tfor _, entry := range timeoutEntry {\n\t\t// This eventually calls entry.ExitFunc,\n\t\t// where the m.mutex will be locked again to remove the entry from the map.\n\t\tentry.CloseWithErr(nil)\n\t}\n}\n\nfunc (m *udpSessionManager) feed(msg *protocol.UDPMessage) {\n\tm.mutex.RLock()\n\tentry := m.m[msg.SessionID]\n\tm.mutex.RUnlock()\n\n\t// Create a new session if not exists\n\tif entry == nil {\n\t\tdialFunc := func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error) {\n\t\t\t// Call the hook\n\t\t\terr = m.io.Hook(firstMsgData, &addr)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tactualAddr = addr\n\t\t\t// Log the event\n\t\t\tm.eventLogger.New(msg.SessionID, addr)\n\t\t\t// Dial target\n\t\t\tconn, err = m.io.UDP(addr)\n\t\t\treturn\n\t\t}\n\t\texitFunc := func(err error) {\n\t\t\t// Log the event\n\t\t\tm.eventLogger.Close(entry.ID, err)\n\n\t\t\t// Remove the session from the map\n\t\t\tm.mutex.Lock()\n\t\t\tdelete(m.m, entry.ID)\n\t\t\tm.mutex.Unlock()\n\t\t}\n\n\t\tentry = newUDPSessionEntry(msg.SessionID, m.io, dialFunc, exitFunc)\n\n\t\t// Insert the session into the map\n\t\tm.mutex.Lock()\n\t\tm.m[msg.SessionID] = entry\n\t\tm.mutex.Unlock()\n\t}\n\n\t// Feed the message to the session\n\t// Feed (send) errors are ignored for now,\n\t// as some are temporary (e.g. invalid address)\n\t_, _ = entry.Feed(msg)\n}\n\nfunc (m *udpSessionManager) Count() int {\n\tm.mutex.RLock()\n\tdefer m.mutex.RUnlock()\n\treturn len(m.m)\n}\n"
  },
  {
    "path": "core/server/udp_test.go",
    "content": "package server\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"go.uber.org/goleak\"\n\n\t\"github.com/apernet/hysteria/core/v2/internal/protocol\"\n)\n\nfunc TestUDPSessionManager(t *testing.T) {\n\tio := newMockUDPIO(t)\n\teventLogger := newMockUDPEventLogger(t)\n\tsm := newUDPSessionManager(io, eventLogger, 2*time.Second)\n\n\tmsgCh := make(chan *protocol.UDPMessage, 4)\n\tio.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) {\n\t\tm := <-msgCh\n\t\tif m == nil {\n\t\t\treturn nil, errors.New(\"closed\")\n\t\t}\n\t\treturn m, nil\n\t})\n\n\tgo sm.Run()\n\n\tudpReadFunc := func(addr string, ch chan []byte, b []byte) (int, string, error) {\n\t\tbs := <-ch\n\t\tif bs == nil {\n\t\t\treturn 0, \"\", errors.New(\"closed\")\n\t\t}\n\t\tn := copy(b, bs)\n\t\treturn n, addr, nil\n\t}\n\n\t// Test normal session creation & timeout\n\tmsg1 := &protocol.UDPMessage{\n\t\tSessionID: 1234,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      \"address1.com:9000\",\n\t\tData:      []byte(\"hello\"),\n\t}\n\teventLogger.EXPECT().New(msg1.SessionID, msg1.Addr).Return().Once()\n\tudpConn1 := newMockUDPConn(t)\n\tudpConn1Ch := make(chan []byte, 1)\n\tio.EXPECT().Hook(msg1.Data, &msg1.Addr).Return(nil).Once()\n\tio.EXPECT().UDP(msg1.Addr).Return(udpConn1, nil).Once()\n\tudpConn1.EXPECT().WriteTo(msg1.Data, msg1.Addr).Return(5, nil).Once()\n\tudpConn1.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) {\n\t\treturn udpReadFunc(msg1.Addr, udpConn1Ch, b)\n\t})\n\tio.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{\n\t\tSessionID: msg1.SessionID,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      msg1.Addr,\n\t\tData:      []byte(\"hi back\"),\n\t}).Return(nil).Once()\n\tmsgCh <- msg1\n\tudpConn1Ch <- []byte(\"hi back\")\n\n\tmsg2data := []byte(\"how are you doing?\")\n\tmsg2_1 := &protocol.UDPMessage{\n\t\tSessionID: 5678,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 2,\n\t\tAddr:      \"address2.net:12450\",\n\t\tData:      msg2data[:6],\n\t}\n\tmsg2_2 := &protocol.UDPMessage{\n\t\tSessionID: 5678,\n\t\tPacketID:  0,\n\t\tFragID:    1,\n\t\tFragCount: 2,\n\t\tAddr:      \"address2.net:12450\",\n\t\tData:      msg2data[6:],\n\t}\n\n\teventLogger.EXPECT().New(msg2_1.SessionID, msg2_1.Addr).Return().Once()\n\tudpConn2 := newMockUDPConn(t)\n\tudpConn2Ch := make(chan []byte, 1)\n\t// On fragmentation, make sure hook gets the whole message\n\tio.EXPECT().Hook(msg2data, &msg2_1.Addr).Return(nil).Once()\n\tio.EXPECT().UDP(msg2_1.Addr).Return(udpConn2, nil).Once()\n\tudpConn2.EXPECT().WriteTo(msg2data, msg2_1.Addr).Return(11, nil).Once()\n\tudpConn2.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) {\n\t\treturn udpReadFunc(msg2_1.Addr, udpConn2Ch, b)\n\t})\n\tio.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{\n\t\tSessionID: msg2_1.SessionID,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      msg2_1.Addr,\n\t\tData:      []byte(\"im fine\"),\n\t}).Return(nil).Once()\n\tmsgCh <- msg2_1\n\tmsgCh <- msg2_2\n\tudpConn2Ch <- []byte(\"im fine\")\n\n\tmsg3 := &protocol.UDPMessage{\n\t\tSessionID: 1234,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      \"address1.com:9000\",\n\t\tData:      []byte(\"who are you?\"),\n\t}\n\tudpConn1.EXPECT().WriteTo(msg3.Data, msg3.Addr).Return(12, nil).Once()\n\tio.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{\n\t\tSessionID: msg3.SessionID,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      msg3.Addr,\n\t\tData:      []byte(\"im your father\"),\n\t}).Return(nil).Once()\n\tmsgCh <- msg3\n\tudpConn1Ch <- []byte(\"im your father\")\n\n\t// Make sure timeout works (connections closed & close events emitted)\n\tudpConn1.EXPECT().Close().RunAndReturn(func() error {\n\t\tclose(udpConn1Ch)\n\t\treturn nil\n\t}).Once()\n\tudpConn2.EXPECT().Close().RunAndReturn(func() error {\n\t\tclose(udpConn2Ch)\n\t\treturn nil\n\t}).Once()\n\teventLogger.EXPECT().Close(msg1.SessionID, nil).Once()\n\teventLogger.EXPECT().Close(msg2_1.SessionID, nil).Once()\n\n\ttime.Sleep(3 * time.Second) // Wait for timeout\n\tmock.AssertExpectationsForObjects(t, io, eventLogger, udpConn1, udpConn2)\n\n\t// Test UDP connection close error propagation\n\terrUDPClosed := errors.New(\"UDP connection closed\")\n\tmsg4 := &protocol.UDPMessage{\n\t\tSessionID: 666,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      \"oh-no.com:27015\",\n\t\tData:      []byte(\"dont say bye\"),\n\t}\n\teventLogger.EXPECT().New(msg4.SessionID, msg4.Addr).Return().Once()\n\tudpConn4 := newMockUDPConn(t)\n\tio.EXPECT().Hook(msg4.Data, &msg4.Addr).Return(nil).Once()\n\tio.EXPECT().UDP(msg4.Addr).Return(udpConn4, nil).Once()\n\tudpConn4.EXPECT().WriteTo(msg4.Data, msg4.Addr).Return(12, nil).Once()\n\tudpConn4.EXPECT().ReadFrom(mock.Anything).Return(0, \"\", errUDPClosed).Once()\n\tudpConn4.EXPECT().Close().Return(nil).Once()\n\teventLogger.EXPECT().Close(msg4.SessionID, errUDPClosed).Once()\n\tmsgCh <- msg4\n\n\ttime.Sleep(1 * time.Second)\n\tmock.AssertExpectationsForObjects(t, io, eventLogger, udpConn4)\n\n\t// Test UDP connection creation error propagation\n\terrUDPIO := errors.New(\"UDP IO error\")\n\tmsg5 := &protocol.UDPMessage{\n\t\tSessionID: 777,\n\t\tPacketID:  0,\n\t\tFragID:    0,\n\t\tFragCount: 1,\n\t\tAddr:      \"callmemaybe.com:15353\",\n\t\tData:      []byte(\"babe i miss you\"),\n\t}\n\teventLogger.EXPECT().New(msg5.SessionID, msg5.Addr).Return().Once()\n\tio.EXPECT().Hook(msg5.Data, &msg5.Addr).Return(nil).Once()\n\tio.EXPECT().UDP(msg5.Addr).Return(nil, errUDPIO).Once()\n\teventLogger.EXPECT().Close(msg5.SessionID, errUDPIO).Once()\n\tmsgCh <- msg5\n\n\ttime.Sleep(1 * time.Second)\n\tmock.AssertExpectationsForObjects(t, io, eventLogger)\n\n\t// Leak checks\n\tclose(msgCh)                // This will return error from ReceiveMessage(), should stop the session manager\n\ttime.Sleep(1 * time.Second) // Wait one more second just to be sure\n\tassert.Zero(t, sm.Count(), \"session count should be 0\")\n\tgoleak.VerifyNone(t)\n}\n"
  },
  {
    "path": "entrypoint",
    "content": "#!/bin/sh\n\nCONFIG_FILE=\"/etc/hysteria/server.yaml\"\n\n# 判断配置文件是否存存在，如果不存在走不存在的逻辑\nif [ ! -f \"$CONFIG_FILE\" ]; then\n    echo \"Creating configuration file $CONFIG_FILE\"\n    mkdir -p /etc/hysteria\n    cat <<EOF >\"$CONFIG_FILE\"\nv2board:\n  apiHost: ${apiHost}\n  apiKey: ${apiKey}\n  nodeID: ${nodeID}\nacme:\n  domains:\n    - ${domain}\n  email: your@email.com\nauth:\n  type: v2board\ntrafficStats:\n  listen: 127.0.0.1:7653\nacl:\n  inline:\n    - reject(10.0.0.0/8)\n    - reject(172.16.0.0/12)\n    - reject(192.168.0.0/16)\n    - reject(127.0.0.0/8)\n    - reject(fc00::/7)\nEOF\nfi\n\nhysteria server -c $CONFIG_FILE 2>&1 | tee &\n\n# 获取HYSTERIA server命令的进程组ID（Process Group ID）\nHYSTERIA_PID=$!\n\n# 定义一个函数来处理Ctrl+C信号\ncleanup() {\n    echo \"接收到Ctrl+C信号，正在停止HYSTERIA...\"\n    # 向HYSTERIA进程组发送终止信号\n    kill -SIGINT $HYSTERIA_PID\n    exit 0\n}\n\n# 捕获Ctrl+C信号并调用cleanup函数\ntrap cleanup INT\ntrap cleanup SIGTERM\n\n# 等待HYSTERIA进程结束\nwait"
  },
  {
    "path": "extras/auth/command.go",
    "content": "package auth\n\nimport (\n\t\"net\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\nvar _ server.Authenticator = &CommandAuthenticator{}\n\ntype CommandAuthenticator struct {\n\tCmd string\n}\n\nfunc (a *CommandAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {\n\tcmd := exec.Command(a.Cmd, addr.String(), auth, strconv.Itoa(int(tx)))\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\t// This includes failing to execute the command,\n\t\t// or the command exiting with a non-zero exit code.\n\t\treturn false, \"\"\n\t} else {\n\t\treturn true, strings.TrimSpace(string(out))\n\t}\n}\n"
  },
  {
    "path": "extras/auth/http.go",
    "content": "package auth\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\nconst (\n\thttpAuthTimeout = 10 * time.Second\n)\n\nvar _ server.Authenticator = &HTTPAuthenticator{}\n\nvar errInvalidStatusCode = errors.New(\"invalid status code\")\n\ntype HTTPAuthenticator struct {\n\tClient *http.Client\n\tURL    string\n}\n\nfunc NewHTTPAuthenticator(url string, insecure bool) *HTTPAuthenticator {\n\ttr := http.DefaultTransport.(*http.Transport).Clone()\n\ttr.TLSClientConfig = &tls.Config{\n\t\tInsecureSkipVerify: insecure,\n\t}\n\treturn &HTTPAuthenticator{\n\t\tClient: &http.Client{\n\t\t\tTransport: tr,\n\t\t\tTimeout:   httpAuthTimeout,\n\t\t},\n\t\tURL: url,\n\t}\n}\n\ntype httpAuthRequest struct {\n\tAddr string `json:\"addr\"`\n\tAuth string `json:\"auth\"`\n\tTx   uint64 `json:\"tx\"`\n}\n\ntype httpAuthResponse struct {\n\tOK bool   `json:\"ok\"`\n\tID string `json:\"id\"`\n}\n\nfunc (a *HTTPAuthenticator) post(req *httpAuthRequest) (*httpAuthResponse, error) {\n\tbs, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := a.Client.Post(a.URL, \"application/json\", bytes.NewReader(bs))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errInvalidStatusCode\n\t}\n\trespData, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar authResp httpAuthResponse\n\terr = json.Unmarshal(respData, &authResp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &authResp, nil\n}\n\nfunc (a *HTTPAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {\n\treq := &httpAuthRequest{\n\t\tAddr: addr.String(),\n\t\tAuth: auth,\n\t\tTx:   tx,\n\t}\n\tresp, err := a.post(req)\n\tif err != nil {\n\t\treturn false, \"\"\n\t}\n\treturn resp.OK, resp.ID\n}\n"
  },
  {
    "path": "extras/auth/http_test.go",
    "content": "package auth\n\nimport (\n\t\"net\"\n\t\"os/exec\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHTTPAuthenticator(t *testing.T) {\n\t// Run the Python test auth server\n\tcmd := exec.Command(\"python\", \"http_test.py\")\n\terr := cmd.Start()\n\tassert.NoError(t, err)\n\tdefer cmd.Process.Kill()\n\n\ttime.Sleep(1 * time.Second) // Wait for the server to start\n\n\tauth := NewHTTPAuthenticator(\"http://127.0.0.1:5000/auth\", false)\n\n\tok, id := auth.Authenticate(&net.UDPAddr{\n\t\tIP:   net.ParseIP(\"1.2.3.4\"),\n\t\tPort: 34567,\n\t}, \"idk\", 123)\n\tassert.False(t, ok)\n\tassert.Equal(t, \"\", id)\n\n\tok, id = auth.Authenticate(&net.UDPAddr{\n\t\tIP:   net.ParseIP(\"123.123.123.123\"),\n\t\tPort: 5566,\n\t}, \"wahaha\", 12345)\n\tassert.True(t, ok)\n\tassert.Equal(t, \"some_unique_id\", id)\n}\n"
  },
  {
    "path": "extras/auth/http_test.py",
    "content": "from flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n\n@app.route(\"/auth\", methods=[\"POST\"])\ndef auth():\n    data = request.json\n\n    if data is None:\n        return jsonify({\"ok\": False, \"id\": \"\"}), 400\n\n    addr = data.get(\"addr\", \"\")\n    auth = data.get(\"auth\", \"\")\n    tx = data.get(\"tx\", 0)\n\n    if addr == \"123.123.123.123:5566\" and auth == \"wahaha\" and tx == 12345:\n        return jsonify({\"ok\": True, \"id\": \"some_unique_id\"})\n    else:\n        return jsonify({\"ok\": False, \"id\": \"\"})\n\n\nif __name__ == \"__main__\":\n    app.run()\n"
  },
  {
    "path": "extras/auth/password.go",
    "content": "package auth\n\nimport (\n\t\"net\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\nvar _ server.Authenticator = &PasswordAuthenticator{}\n\n// PasswordAuthenticator is a simple authenticator that checks the password against a single string.\ntype PasswordAuthenticator struct {\n\tPassword string\n}\n\nfunc (a *PasswordAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {\n\tif auth == a.Password {\n\t\treturn true, \"user\"\n\t} else {\n\t\treturn false, \"\"\n\t}\n}\n"
  },
  {
    "path": "extras/auth/password_test.go",
    "content": "package auth\n\nimport (\n\t\"net\"\n\t\"testing\"\n)\n\nfunc TestPasswordAuthenticator(t *testing.T) {\n\ttype fields struct {\n\t\tPassword string\n\t}\n\ttype args struct {\n\t\taddr net.Addr\n\t\tauth string\n\t\ttx   uint64\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twantOk bool\n\t\twantId string\n\t}{\n\t\t{\n\t\t\tname: \"correct\",\n\t\t\tfields: fields{\n\t\t\t\tPassword: \"yes,yes\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\taddr: nil,\n\t\t\t\tauth: \"yes,yes\",\n\t\t\t\ttx:   0,\n\t\t\t},\n\t\t\twantOk: true,\n\t\t\twantId: \"user\",\n\t\t},\n\t\t{\n\t\t\tname: \"incorrect\",\n\t\t\tfields: fields{\n\t\t\t\tPassword: \"something_somehow\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\taddr: nil,\n\t\t\t\tauth: \"random\",\n\t\t\t\ttx:   0,\n\t\t\t},\n\t\t\twantOk: false,\n\t\t\twantId: \"\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &PasswordAuthenticator{\n\t\t\t\tPassword: tt.fields.Password,\n\t\t\t}\n\t\t\tgotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx)\n\t\t\tif gotOk != tt.wantOk {\n\t\t\t\tt.Errorf(\"Authenticate() gotOk = %v, want %v\", gotOk, tt.wantOk)\n\t\t\t}\n\t\t\tif gotId != tt.wantId {\n\t\t\t\tt.Errorf(\"Authenticate() gotId = %v, want %v\", gotId, tt.wantId)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/auth/userpass.go",
    "content": "package auth\n\nimport (\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\nconst (\n\tuserPassSeparator = \":\"\n)\n\nvar _ server.Authenticator = &UserPassAuthenticator{}\n\n// UserPassAuthenticator checks the provided auth string against a map of username/password pairs.\n// The format of the auth string must be \"username:password\".\ntype UserPassAuthenticator struct {\n\tUsers map[string]string\n}\n\nfunc (a *UserPassAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {\n\tu, p, ok := splitUserPass(auth)\n\tif !ok {\n\t\treturn false, \"\"\n\t}\n\trp, ok := a.Users[u]\n\tif !ok || rp != p {\n\t\treturn false, \"\"\n\t}\n\treturn true, u\n}\n\nfunc splitUserPass(auth string) (user, pass string, ok bool) {\n\trs := strings.SplitN(auth, userPassSeparator, 2)\n\tif len(rs) != 2 {\n\t\treturn \"\", \"\", false\n\t}\n\treturn rs[0], rs[1], true\n}\n"
  },
  {
    "path": "extras/auth/userpass_test.go",
    "content": "package auth\n\nimport (\n\t\"net\"\n\t\"testing\"\n)\n\nfunc TestUserPassAuthenticator(t *testing.T) {\n\ttype fields struct {\n\t\tUsers map[string]string\n\t}\n\ttype args struct {\n\t\taddr net.Addr\n\t\tauth string\n\t\ttx   uint64\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twantOk bool\n\t\twantId string\n\t}{\n\t\t{\n\t\t\tname: \"correct 1\",\n\t\t\tfields: fields{\n\t\t\t\tUsers: map[string]string{\n\t\t\t\t\t\"saul\": \"goodman\",\n\t\t\t\t\t\"wang\": \"123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\taddr: nil,\n\t\t\t\tauth: \"wang:123\",\n\t\t\t\ttx:   0,\n\t\t\t},\n\t\t\twantOk: true,\n\t\t\twantId: \"wang\",\n\t\t},\n\t\t{\n\t\t\tname: \"correct 2\",\n\t\t\tfields: fields{\n\t\t\t\tUsers: map[string]string{\n\t\t\t\t\t\"gawr\":   \"gura\",\n\t\t\t\t\t\"fubuki\": \"shirakami\",\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\taddr: nil,\n\t\t\t\tauth: \"gawr:gura\",\n\t\t\t\ttx:   0,\n\t\t\t},\n\t\t\twantOk: true,\n\t\t\twantId: \"gawr\",\n\t\t},\n\t\t{\n\t\t\tname: \"incorrect 1\",\n\t\t\tfields: fields{\n\t\t\t\tUsers: map[string]string{\n\t\t\t\t\t\"gawr\":   \"gura\",\n\t\t\t\t\t\"fubuki\": \"shirakami\",\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\taddr: nil,\n\t\t\t\tauth: \"random:stranger\",\n\t\t\t\ttx:   0,\n\t\t\t},\n\t\t\twantOk: false,\n\t\t\twantId: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"incorrect 2\",\n\t\t\tfields: fields{\n\t\t\t\tUsers: map[string]string{\n\t\t\t\t\t\"gawr\":   \"gura\",\n\t\t\t\t\t\"fubuki\": \"shirakami\",\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\taddr: nil,\n\t\t\t\tauth: \"poop\",\n\t\t\t\ttx:   0,\n\t\t\t},\n\t\t\twantOk: false,\n\t\t\twantId: \"\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &UserPassAuthenticator{\n\t\t\t\tUsers: tt.fields.Users,\n\t\t\t}\n\t\t\tgotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx)\n\t\t\tif gotOk != tt.wantOk {\n\t\t\t\tt.Errorf(\"Authenticate() gotOk = %v, want %v\", gotOk, tt.wantOk)\n\t\t\t}\n\t\t\tif gotId != tt.wantId {\n\t\t\t\tt.Errorf(\"Authenticate() gotId = %v, want %v\", gotId, tt.wantId)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/auth/v2board.go",
    "content": "package auth\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\nvar _ server.Authenticator = &V2boardApiProvider{}\n\ntype V2boardApiProvider struct {\n\tClient *http.Client\n\tURL    string\n}\n\n// 用户列表\nvar (\n\tusersMap map[string]User\n\tlock     sync.Mutex\n)\n\ntype User struct {\n\tID         int     `json:\"id\"`\n\tUUID       string  `json:\"uuid\"`\n\tSpeedLimit *uint32 `json:\"speed_limit\"`\n}\ntype ResponseData struct {\n\tUsers []User `json:\"users\"`\n}\n\nfunc getUserList(url string) ([]User, error) {\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar responseData ResponseData\n\terr = json.NewDecoder(resp.Body).Decode(&responseData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn responseData.Users, nil\n}\n\nfunc UpdateUsers(url string, interval time.Duration, trafficlogger server.TrafficLogger) {\n\tfmt.Println(\"用户列表自动更新服务已激活，更新周期为\", interval)\n\n\t// 先立即执行一次更新\n\tuserList, err := getUserList(url)\n\tif err != nil {\n\t\tfmt.Println(\"Error:\", err)\n\t\treturn // 如果第一次获取失败，退出函数\n\t}\n\tprocessUserList(userList, trafficlogger)\n\n\t// 设置定时器\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tuserList, err := getUserList(url)\n\t\tif err != nil {\n\t\t\tfmt.Println(\"Error:\", err)\n\t\t\tcontinue\n\t\t}\n\t\tprocessUserList(userList, trafficlogger)\n\t}\n}\n\n// 处理用户列表的逻辑\nfunc processUserList(userList []User, trafficlogger server.TrafficLogger) {\n\tlock.Lock()\n\tdefer lock.Unlock()\n\n\tnewUsersMap := make(map[string]User)\n\tfor _, user := range userList {\n\t\tnewUsersMap[user.UUID] = user\n\t}\n\tif trafficlogger != nil {\n\t\tfor uuid := range usersMap {\n\t\t\tif _, exists := newUsersMap[uuid]; !exists {\n\t\t\t\tfmt.Println(usersMap[uuid].ID)\n\t\t\t\ttrafficlogger.NewKick(strconv.Itoa(usersMap[uuid].ID))\n\t\t\t}\n\t\t}\n\t}\n\n\tusersMap = newUsersMap\n}\n\n// 验证代码\nfunc (v *V2boardApiProvider) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {\n\n\t// 获取判断连接用户是否在用户列表内\n\tlock.Lock()\n\tdefer lock.Unlock()\n\n\tif user, exists := usersMap[auth]; exists {\n\t\treturn true, strconv.Itoa(user.ID)\n\t}\n\treturn false, \"\"\n}\n"
  },
  {
    "path": "extras/correctnet/correctnet.go",
    "content": "package correctnet\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc extractIPFamily(ip net.IP) (family string) {\n\tif len(ip) == 0 {\n\t\t// real family independent wildcard address, such as \":443\"\n\t\treturn \"\"\n\t}\n\tif p4 := ip.To4(); len(p4) == net.IPv4len {\n\t\treturn \"4\"\n\t}\n\treturn \"6\"\n}\n\nfunc tcpAddrNetwork(addr *net.TCPAddr) (network string) {\n\tif addr == nil {\n\t\treturn \"tcp\"\n\t}\n\treturn \"tcp\" + extractIPFamily(addr.IP)\n}\n\nfunc udpAddrNetwork(addr *net.UDPAddr) (network string) {\n\tif addr == nil {\n\t\treturn \"udp\"\n\t}\n\treturn \"udp\" + extractIPFamily(addr.IP)\n}\n\nfunc ipAddrNetwork(addr *net.IPAddr) (network string) {\n\tif addr == nil {\n\t\treturn \"ip\"\n\t}\n\treturn \"ip\" + extractIPFamily(addr.IP)\n}\n\nfunc Listen(network, address string) (net.Listener, error) {\n\tif network == \"tcp\" {\n\t\ttcpAddr, err := net.ResolveTCPAddr(network, address)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ListenTCP(network, tcpAddr)\n\t}\n\treturn net.Listen(network, address)\n}\n\nfunc ListenTCP(network string, laddr *net.TCPAddr) (*net.TCPListener, error) {\n\tif network == \"tcp\" {\n\t\treturn net.ListenTCP(tcpAddrNetwork(laddr), laddr)\n\t}\n\treturn net.ListenTCP(network, laddr)\n}\n\nfunc ListenPacket(network, address string) (listener net.PacketConn, err error) {\n\tif network == \"udp\" {\n\t\tudpAddr, err := net.ResolveUDPAddr(network, address)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ListenUDP(network, udpAddr)\n\t}\n\tif strings.HasPrefix(network, \"ip:\") {\n\t\tproto := network[3:]\n\t\tipAddr, err := net.ResolveIPAddr(proto, address)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn net.ListenIP(ipAddrNetwork(ipAddr)+\":\"+proto, ipAddr)\n\t}\n\treturn net.ListenPacket(network, address)\n}\n\nfunc ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error) {\n\tif network == \"udp\" {\n\t\treturn net.ListenUDP(udpAddrNetwork(laddr), laddr)\n\t}\n\treturn net.ListenUDP(network, laddr)\n}\n\nfunc HTTPListenAndServe(address string, handler http.Handler) error {\n\tlistener, err := Listen(\"tcp\", address)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer listener.Close()\n\treturn http.Serve(listener, handler)\n}\n"
  },
  {
    "path": "extras/go.mod",
    "content": "module github.com/apernet/hysteria/extras/v2\n\ngo 1.22\n\ntoolchain go1.23.2\n\nrequire (\n\tgithub.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000\n\tgithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d\n\tgithub.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.5\n\tgithub.com/miekg/dns v1.1.59\n\tgithub.com/refraction-networking/utls v1.6.6\n\tgithub.com/stretchr/testify v1.9.0\n\tgithub.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301\n\tgolang.org/x/crypto v0.26.0\n\tgolang.org/x/net v0.28.0\n\tgoogle.golang.org/protobuf v1.34.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.1.0 // indirect\n\tgithub.com/cloudflare/circl v1.3.9 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect\n\tgithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect\n\tgithub.com/klauspost/compress v1.17.9 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/onsi/ginkgo/v2 v2.9.5 // indirect\n\tgithub.com/patrickmn/go-cache v2.1.0+incompatible // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect\n\tgo.uber.org/mock v0.4.0 // indirect\n\tgolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect\n\tgolang.org/x/mod v0.17.0 // indirect\n\tgolang.org/x/sync v0.8.0 // indirect\n\tgolang.org/x/sys v0.23.0 // indirect\n\tgolang.org/x/text v0.17.0 // indirect\n\tgolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nreplace github.com/apernet/hysteria/core/v2 => ../core\n"
  },
  {
    "path": "extras/go.sum",
    "content": "github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=\ngithub.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=\ngithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d h1:KWRCWISqJOgY9/0hhH8Bevjw/k4tCQ7oJlXLyFv8u9s=\ngithub.com/apernet/quic-go v0.47.1-0.20241004180137-a80d14e2080d/go.mod h1:x0paLlmCzNOUDDQIgmgFWmnpWQIEuH1GNfA6NdgSTuM=\ngithub.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=\ngithub.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=\ngithub.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=\ngithub.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=\ngithub.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=\ngithub.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=\ngithub.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=\ngithub.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=\ngithub.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=\ngithub.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=\ngithub.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=\ngithub.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=\ngithub.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=\ngithub.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=\ngithub.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=\ngithub.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=\ngithub.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=\ngithub.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=\ngithub.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM=\ngithub.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=\ngo.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=\ngo.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=\ngo.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=\ngolang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=\ngolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=\ngolang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=\ngolang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=\ngolang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=\ngolang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=\ngolang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=\ngolang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=\ngolang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=\ngoogle.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "extras/masq/server.go",
    "content": "package masq\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/apernet/hysteria/extras/v2/correctnet\"\n)\n\n// MasqTCPServer covers the TCP parts of a standard web server (TCP based HTTP/HTTPS).\n// We provide this as an option for masquerading, as some may consider a server\n// \"suspicious\" if it only serves the QUIC protocol and not standard HTTP/HTTPS.\ntype MasqTCPServer struct {\n\tQUICPort   int\n\tHTTPSPort  int\n\tHandler    http.Handler\n\tTLSConfig  *tls.Config\n\tForceHTTPS bool // Always 301 redirect from HTTP to HTTPS\n}\n\nfunc (s *MasqTCPServer) ListenAndServeHTTP(addr string) error {\n\treturn correctnet.HTTPListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif s.ForceHTTPS {\n\t\t\tif s.HTTPSPort == 0 || s.HTTPSPort == 443 {\n\t\t\t\t// Omit port if it's the default\n\t\t\t\thttp.Redirect(w, r, \"https://\"+r.Host+r.RequestURI, http.StatusMovedPermanently)\n\t\t\t} else {\n\t\t\t\thttp.Redirect(w, r, fmt.Sprintf(\"https://%s:%d%s\", r.Host, s.HTTPSPort, r.RequestURI), http.StatusMovedPermanently)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\ts.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r)\n\t}))\n}\n\nfunc (s *MasqTCPServer) ListenAndServeHTTPS(addr string) error {\n\tserver := &http.Server{\n\t\tAddr: addr,\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\ts.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r)\n\t\t}),\n\t\tTLSConfig: s.TLSConfig,\n\t}\n\tlistener, err := correctnet.Listen(\"tcp\", addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer listener.Close()\n\treturn server.ServeTLS(listener, \"\", \"\")\n}\n\nvar _ http.ResponseWriter = (*altSvcHijackResponseWriter)(nil)\n\n// altSvcHijackResponseWriter makes sure that the Alt-Svc's port\n// is always set with our own value, no matter what the handler sets.\ntype altSvcHijackResponseWriter struct {\n\tPort int\n\thttp.ResponseWriter\n}\n\nfunc (w *altSvcHijackResponseWriter) WriteHeader(statusCode int) {\n\tw.Header().Set(\"Alt-Svc\", fmt.Sprintf(`h3=\":%d\"; ma=2592000`, w.Port))\n\tw.ResponseWriter.WriteHeader(statusCode)\n}\n\nvar _ http.Hijacker = (*altSvcHijackResponseWriterHijacker)(nil)\n\n// altSvcHijackResponseWriterHijacker is a wrapper around altSvcHijackResponseWriter\n// that also implements http.Hijacker. This is needed for WebSocket support.\ntype altSvcHijackResponseWriterHijacker struct {\n\taltSvcHijackResponseWriter\n}\n\nfunc (w *altSvcHijackResponseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\treturn w.ResponseWriter.(http.Hijacker).Hijack()\n}\n\nfunc newAltSvcHijackResponseWriter(w http.ResponseWriter, port int) http.ResponseWriter {\n\tif _, ok := w.(http.Hijacker); ok {\n\t\treturn &altSvcHijackResponseWriterHijacker{\n\t\t\taltSvcHijackResponseWriter: altSvcHijackResponseWriter{\n\t\t\t\tPort:           port,\n\t\t\t\tResponseWriter: w,\n\t\t\t},\n\t\t}\n\t}\n\treturn &altSvcHijackResponseWriter{\n\t\tPort:           port,\n\t\tResponseWriter: w,\n\t}\n}\n"
  },
  {
    "path": "extras/obfs/conn.go",
    "content": "package obfs\n\nimport (\n\t\"net\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n)\n\nconst udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough\n\n// Obfuscator is the interface that wraps the Obfuscate and Deobfuscate methods.\n// Both methods return the number of bytes written to out.\n// If a packet is not valid, the methods should return 0.\ntype Obfuscator interface {\n\tObfuscate(in, out []byte) int\n\tDeobfuscate(in, out []byte) int\n}\n\nvar _ net.PacketConn = (*obfsPacketConn)(nil)\n\ntype obfsPacketConn struct {\n\tConn net.PacketConn\n\tObfs Obfuscator\n\n\treadBuf    []byte\n\treadMutex  sync.Mutex\n\twriteBuf   []byte\n\twriteMutex sync.Mutex\n}\n\n// obfsPacketConnUDP is a special case of obfsPacketConn that uses a UDPConn\n// as the underlying connection. We pass additional methods to quic-go to\n// enable UDP-specific optimizations.\ntype obfsPacketConnUDP struct {\n\t*obfsPacketConn\n\tUDPConn *net.UDPConn\n}\n\n// WrapPacketConn enables obfuscation on a net.PacketConn.\n// The obfuscation is transparent to the caller - the n bytes returned by\n// ReadFrom and WriteTo are the number of original bytes, not after\n// obfuscation/deobfuscation.\nfunc WrapPacketConn(conn net.PacketConn, obfs Obfuscator) net.PacketConn {\n\topc := &obfsPacketConn{\n\t\tConn:     conn,\n\t\tObfs:     obfs,\n\t\treadBuf:  make([]byte, udpBufferSize),\n\t\twriteBuf: make([]byte, udpBufferSize),\n\t}\n\tif udpConn, ok := conn.(*net.UDPConn); ok {\n\t\treturn &obfsPacketConnUDP{\n\t\t\tobfsPacketConn: opc,\n\t\t\tUDPConn:        udpConn,\n\t\t}\n\t} else {\n\t\treturn opc\n\t}\n}\n\nfunc (c *obfsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {\n\tfor {\n\t\tc.readMutex.Lock()\n\t\tn, addr, err = c.Conn.ReadFrom(c.readBuf)\n\t\tif n <= 0 {\n\t\t\tc.readMutex.Unlock()\n\t\t\treturn\n\t\t}\n\t\tn = c.Obfs.Deobfuscate(c.readBuf[:n], p)\n\t\tc.readMutex.Unlock()\n\t\tif n > 0 || err != nil {\n\t\t\treturn\n\t\t}\n\t\t// Invalid packet, try again\n\t}\n}\n\nfunc (c *obfsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {\n\tc.writeMutex.Lock()\n\tnn := c.Obfs.Obfuscate(p, c.writeBuf)\n\t_, err = c.Conn.WriteTo(c.writeBuf[:nn], addr)\n\tc.writeMutex.Unlock()\n\tif err == nil {\n\t\tn = len(p)\n\t}\n\treturn\n}\n\nfunc (c *obfsPacketConn) Close() error {\n\treturn c.Conn.Close()\n}\n\nfunc (c *obfsPacketConn) LocalAddr() net.Addr {\n\treturn c.Conn.LocalAddr()\n}\n\nfunc (c *obfsPacketConn) SetDeadline(t time.Time) error {\n\treturn c.Conn.SetDeadline(t)\n}\n\nfunc (c *obfsPacketConn) SetReadDeadline(t time.Time) error {\n\treturn c.Conn.SetReadDeadline(t)\n}\n\nfunc (c *obfsPacketConn) SetWriteDeadline(t time.Time) error {\n\treturn c.Conn.SetWriteDeadline(t)\n}\n\n// UDP-specific methods below\n\nfunc (c *obfsPacketConnUDP) SetReadBuffer(bytes int) error {\n\treturn c.UDPConn.SetReadBuffer(bytes)\n}\n\nfunc (c *obfsPacketConnUDP) SetWriteBuffer(bytes int) error {\n\treturn c.UDPConn.SetWriteBuffer(bytes)\n}\n\nfunc (c *obfsPacketConnUDP) SyscallConn() (syscall.RawConn, error) {\n\treturn c.UDPConn.SyscallConn()\n}\n"
  },
  {
    "path": "extras/obfs/salamander.go",
    "content": "package obfs\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/blake2b\"\n)\n\nconst (\n\tsmPSKMinLen = 4\n\tsmSaltLen   = 8\n\tsmKeyLen    = blake2b.Size256\n)\n\nvar _ Obfuscator = (*SalamanderObfuscator)(nil)\n\nvar ErrPSKTooShort = fmt.Errorf(\"PSK must be at least %d bytes\", smPSKMinLen)\n\n// SalamanderObfuscator is an obfuscator that obfuscates each packet with\n// the BLAKE2b-256 hash of a pre-shared key combined with a random salt.\n// Packet format: [8-byte salt][payload]\ntype SalamanderObfuscator struct {\n\tPSK     []byte\n\tRandSrc *rand.Rand\n\n\tlk sync.Mutex\n}\n\nfunc NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) {\n\tif len(psk) < smPSKMinLen {\n\t\treturn nil, ErrPSKTooShort\n\t}\n\treturn &SalamanderObfuscator{\n\t\tPSK:     psk,\n\t\tRandSrc: rand.New(rand.NewSource(time.Now().UnixNano())),\n\t}, nil\n}\n\nfunc (o *SalamanderObfuscator) Obfuscate(in, out []byte) int {\n\toutLen := len(in) + smSaltLen\n\tif len(out) < outLen {\n\t\treturn 0\n\t}\n\to.lk.Lock()\n\t_, _ = o.RandSrc.Read(out[:smSaltLen])\n\to.lk.Unlock()\n\tkey := o.key(out[:smSaltLen])\n\tfor i, c := range in {\n\t\tout[i+smSaltLen] = c ^ key[i%smKeyLen]\n\t}\n\treturn outLen\n}\n\nfunc (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int {\n\toutLen := len(in) - smSaltLen\n\tif outLen <= 0 || len(out) < outLen {\n\t\treturn 0\n\t}\n\tkey := o.key(in[:smSaltLen])\n\tfor i, c := range in[smSaltLen:] {\n\t\tout[i] = c ^ key[i%smKeyLen]\n\t}\n\treturn outLen\n}\n\nfunc (o *SalamanderObfuscator) key(salt []byte) [smKeyLen]byte {\n\treturn blake2b.Sum256(append(o.PSK, salt...))\n}\n"
  },
  {
    "path": "extras/obfs/salamander_test.go",
    "content": "package obfs\n\nimport (\n\t\"crypto/rand\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) {\n\to, _ := NewSalamanderObfuscator([]byte(\"average_password\"))\n\tin := make([]byte, 1200)\n\t_, _ = rand.Read(in)\n\tout := make([]byte, 2048)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\to.Obfuscate(in, out)\n\t}\n}\n\nfunc BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) {\n\to, _ := NewSalamanderObfuscator([]byte(\"average_password\"))\n\tin := make([]byte, 1200)\n\t_, _ = rand.Read(in)\n\tout := make([]byte, 2048)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\to.Deobfuscate(in, out)\n\t}\n}\n\nfunc TestSalamanderObfuscator(t *testing.T) {\n\to, _ := NewSalamanderObfuscator([]byte(\"average_password\"))\n\tin := make([]byte, 1200)\n\toOut := make([]byte, 2048)\n\tdOut := make([]byte, 2048)\n\tfor i := 0; i < 1000; i++ {\n\t\t_, _ = rand.Read(in)\n\t\tn := o.Obfuscate(in, oOut)\n\t\tassert.Equal(t, len(in)+smSaltLen, n)\n\t\tn = o.Deobfuscate(oOut[:n], dOut)\n\t\tassert.Equal(t, len(in), n)\n\t\tassert.Equal(t, in, dOut[:n])\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/.mockery.yaml",
    "content": "with-expecter: true\ninpackage: true\ndir: .\npackages:\n  github.com/apernet/hysteria/extras/v2/outbounds:\n    interfaces:\n      PluggableOutbound:\n        config:\n          mockname: mockPluggableOutbound\n      UDPConn:\n        config:\n          mockname: mockUDPConn\n"
  },
  {
    "path": "extras/outbounds/acl/compile.go",
    "content": "package acl\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo\"\n\n\tlru \"github.com/hashicorp/golang-lru/v2\"\n)\n\ntype Protocol int\n\nconst (\n\tProtocolBoth Protocol = iota\n\tProtocolTCP\n\tProtocolUDP\n)\n\ntype Outbound interface {\n\tany\n}\n\ntype HostInfo struct {\n\tName string\n\tIPv4 net.IP\n\tIPv6 net.IP\n}\n\nfunc (h HostInfo) String() string {\n\treturn fmt.Sprintf(\"%s|%s|%s\", h.Name, h.IPv4, h.IPv6)\n}\n\ntype CompiledRuleSet[O Outbound] interface {\n\tMatch(host HostInfo, proto Protocol, port uint16) (O, net.IP)\n}\n\ntype compiledRule[O Outbound] struct {\n\tOutbound      O\n\tHostMatcher   hostMatcher\n\tProtocol      Protocol\n\tStartPort     uint16\n\tEndPort       uint16\n\tHijackAddress net.IP\n}\n\nfunc (r *compiledRule[O]) Match(host HostInfo, proto Protocol, port uint16) bool {\n\tif r.Protocol != ProtocolBoth && r.Protocol != proto {\n\t\treturn false\n\t}\n\tif r.StartPort != 0 && (port < r.StartPort || port > r.EndPort) {\n\t\treturn false\n\t}\n\treturn r.HostMatcher.Match(host)\n}\n\ntype matchResult[O Outbound] struct {\n\tOutbound      O\n\tHijackAddress net.IP\n}\n\ntype compiledRuleSetImpl[O Outbound] struct {\n\tRules []compiledRule[O]\n\tCache *lru.Cache[string, matchResult[O]] // key: HostInfo.String()\n}\n\nfunc (s *compiledRuleSetImpl[O]) Match(host HostInfo, proto Protocol, port uint16) (O, net.IP) {\n\thost.Name = strings.ToLower(host.Name) // Normalize host name to lower case\n\tkey := host.String()\n\tif result, ok := s.Cache.Get(key); ok {\n\t\treturn result.Outbound, result.HijackAddress\n\t}\n\tfor _, rule := range s.Rules {\n\t\tif rule.Match(host, proto, port) {\n\t\t\tresult := matchResult[O]{rule.Outbound, rule.HijackAddress}\n\t\t\ts.Cache.Add(key, result)\n\t\t\treturn result.Outbound, result.HijackAddress\n\t\t}\n\t}\n\t// No match should also be cached\n\tvar zero O\n\ts.Cache.Add(key, matchResult[O]{zero, nil})\n\treturn zero, nil\n}\n\ntype CompilationError struct {\n\tLineNum int\n\tMessage string\n}\n\nfunc (e *CompilationError) Error() string {\n\treturn fmt.Sprintf(\"error at line %d: %s\", e.LineNum, e.Message)\n}\n\ntype GeoLoader interface {\n\tLoadGeoIP() (map[string]*v2geo.GeoIP, error)\n\tLoadGeoSite() (map[string]*v2geo.GeoSite, error)\n}\n\n// Compile compiles TextRules into a CompiledRuleSet.\n// Names in the outbounds map MUST be in all lower case.\n// We want on-demand loading of GeoIP/GeoSite databases, so instead of passing the\n// databases directly, we use a GeoLoader interface to load them only when needed\n// by at least one rule.\nfunc Compile[O Outbound](rules []TextRule, outbounds map[string]O,\n\tcacheSize int, geoLoader GeoLoader,\n) (CompiledRuleSet[O], error) {\n\tcompiledRules := make([]compiledRule[O], len(rules))\n\tfor i, rule := range rules {\n\t\toutbound, ok := outbounds[strings.ToLower(rule.Outbound)]\n\t\tif !ok {\n\t\t\treturn nil, &CompilationError{rule.LineNum, fmt.Sprintf(\"outbound %s not found\", rule.Outbound)}\n\t\t}\n\t\thm, errStr := compileHostMatcher(rule.Address, geoLoader)\n\t\tif errStr != \"\" {\n\t\t\treturn nil, &CompilationError{rule.LineNum, errStr}\n\t\t}\n\t\tproto, startPort, endPort, ok := parseProtoPort(rule.ProtoPort)\n\t\tif !ok {\n\t\t\treturn nil, &CompilationError{rule.LineNum, fmt.Sprintf(\"invalid protocol/port: %s\", rule.ProtoPort)}\n\t\t}\n\t\tvar hijackAddress net.IP\n\t\tif rule.HijackAddress != \"\" {\n\t\t\thijackAddress = net.ParseIP(rule.HijackAddress)\n\t\t\tif hijackAddress == nil {\n\t\t\t\treturn nil, &CompilationError{rule.LineNum, fmt.Sprintf(\"invalid hijack address (must be an IP address): %s\", rule.HijackAddress)}\n\t\t\t}\n\t\t}\n\t\tcompiledRules[i] = compiledRule[O]{outbound, hm, proto, startPort, endPort, hijackAddress}\n\t}\n\tcache, err := lru.New[string, matchResult[O]](cacheSize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &compiledRuleSetImpl[O]{compiledRules, cache}, nil\n}\n\n// parseProtoPort parses the protocol and port from a protoPort string.\n// protoPort must be in one of the following formats:\n//\n//\tproto/port\n//\tproto/*\n//\tproto\n//\t*/port\n//\t*/*\n//\t*\n//\t[empty] (same as *)\n//\n// proto must be either \"tcp\" or \"udp\", case-insensitive.\nfunc parseProtoPort(protoPort string) (Protocol, uint16, uint16, bool) {\n\tprotoPort = strings.ToLower(protoPort)\n\tif protoPort == \"\" || protoPort == \"*\" || protoPort == \"*/*\" {\n\t\treturn ProtocolBoth, 0, 0, true\n\t}\n\tparts := strings.SplitN(protoPort, \"/\", 2)\n\tif len(parts) == 1 {\n\t\t// No port, only protocol\n\t\tswitch parts[0] {\n\t\tcase \"tcp\":\n\t\t\treturn ProtocolTCP, 0, 0, true\n\t\tcase \"udp\":\n\t\t\treturn ProtocolUDP, 0, 0, true\n\t\tdefault:\n\t\t\treturn ProtocolBoth, 0, 0, false\n\t\t}\n\t} else {\n\t\t// Both protocol and port\n\t\tvar proto Protocol\n\t\tvar startPort, endPort uint16\n\t\tswitch parts[0] {\n\t\tcase \"tcp\":\n\t\t\tproto = ProtocolTCP\n\t\tcase \"udp\":\n\t\t\tproto = ProtocolUDP\n\t\tcase \"*\":\n\t\t\tproto = ProtocolBoth\n\t\tdefault:\n\t\t\treturn ProtocolBoth, 0, 0, false\n\t\t}\n\t\tif parts[1] != \"*\" {\n\t\t\t// We allow either a single port or a range (e.g. \"1000-2000\")\n\t\t\tports := strings.SplitN(strings.TrimSpace(parts[1]), \"-\", 2)\n\t\t\tif len(ports) == 1 {\n\t\t\t\tp64, err := strconv.ParseUint(parts[1], 10, 16)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn ProtocolBoth, 0, 0, false\n\t\t\t\t}\n\t\t\t\tstartPort = uint16(p64)\n\t\t\t\tendPort = startPort\n\t\t\t} else {\n\t\t\t\tp64, err := strconv.ParseUint(ports[0], 10, 16)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn ProtocolBoth, 0, 0, false\n\t\t\t\t}\n\t\t\t\tstartPort = uint16(p64)\n\t\t\t\tp64, err = strconv.ParseUint(ports[1], 10, 16)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn ProtocolBoth, 0, 0, false\n\t\t\t\t}\n\t\t\t\tendPort = uint16(p64)\n\t\t\t\tif startPort > endPort {\n\t\t\t\t\treturn ProtocolBoth, 0, 0, false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn proto, startPort, endPort, true\n\t}\n}\n\nfunc compileHostMatcher(addr string, geoLoader GeoLoader) (hostMatcher, string) {\n\taddr = strings.ToLower(addr) // Normalize to lower case\n\tif addr == \"*\" || addr == \"all\" {\n\t\t// Match all hosts\n\t\treturn &allMatcher{}, \"\"\n\t}\n\tif strings.HasPrefix(addr, \"geoip:\") {\n\t\t// GeoIP matcher\n\t\tcountry := addr[6:]\n\t\tif len(country) == 0 {\n\t\t\treturn nil, \"empty GeoIP country code\"\n\t\t}\n\t\tgMap, err := geoLoader.LoadGeoIP()\n\t\tif err != nil {\n\t\t\treturn nil, err.Error()\n\t\t}\n\t\tlist, ok := gMap[country]\n\t\tif !ok || list == nil {\n\t\t\treturn nil, fmt.Sprintf(\"GeoIP country code %s not found\", country)\n\t\t}\n\t\tm, err := newGeoIPMatcher(list)\n\t\tif err != nil {\n\t\t\treturn nil, err.Error()\n\t\t}\n\t\treturn m, \"\"\n\t}\n\tif strings.HasPrefix(addr, \"geosite:\") {\n\t\t// GeoSite matcher\n\t\tname, attrs := parseGeoSiteName(addr[8:])\n\t\tif len(name) == 0 {\n\t\t\treturn nil, \"empty GeoSite name\"\n\t\t}\n\t\tgMap, err := geoLoader.LoadGeoSite()\n\t\tif err != nil {\n\t\t\treturn nil, err.Error()\n\t\t}\n\t\tlist, ok := gMap[name]\n\t\tif !ok || list == nil {\n\t\t\treturn nil, fmt.Sprintf(\"GeoSite name %s not found\", name)\n\t\t}\n\t\tm, err := newGeositeMatcher(list, attrs)\n\t\tif err != nil {\n\t\t\treturn nil, err.Error()\n\t\t}\n\t\treturn m, \"\"\n\t}\n\tif strings.HasPrefix(addr, \"suffix:\") {\n\t\t// Domain suffix matcher\n\t\tsuffix := addr[7:]\n\t\tif len(suffix) == 0 {\n\t\t\treturn nil, \"empty domain suffix\"\n\t\t}\n\t\treturn &domainMatcher{\n\t\t\tPattern: suffix,\n\t\t\tMode:    domainMatchSuffix,\n\t\t}, \"\"\n\t}\n\tif strings.Contains(addr, \"/\") {\n\t\t// CIDR matcher\n\t\t_, ipnet, err := net.ParseCIDR(addr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Sprintf(\"invalid CIDR address: %s\", addr)\n\t\t}\n\t\treturn &cidrMatcher{ipnet}, \"\"\n\t}\n\tif ip := net.ParseIP(addr); ip != nil {\n\t\t// Single IP matcher\n\t\treturn &ipMatcher{ip}, \"\"\n\t}\n\tif strings.Contains(addr, \"*\") {\n\t\t// Wildcard domain matcher\n\t\treturn &domainMatcher{\n\t\t\tPattern: addr,\n\t\t\tMode:    domainMatchWildcard,\n\t\t}, \"\"\n\t}\n\t// Nothing else matched, treat it as a non-wildcard domain\n\treturn &domainMatcher{\n\t\tPattern: addr,\n\t\tMode:    domainMatchExact,\n\t}, \"\"\n}\n\nfunc parseGeoSiteName(s string) (string, []string) {\n\tparts := strings.Split(s, \"@\")\n\tbase := strings.TrimSpace(parts[0])\n\tattrs := parts[1:]\n\tfor i := range attrs {\n\t\tattrs[i] = strings.TrimSpace(attrs[i])\n\t}\n\treturn base, attrs\n}\n"
  },
  {
    "path": "extras/outbounds/acl/compile_test.go",
    "content": "package acl\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar _ GeoLoader = (*testGeoLoader)(nil)\n\ntype testGeoLoader struct{}\n\nfunc (l *testGeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {\n\treturn v2geo.LoadGeoIP(\"v2geo/geoip.dat\")\n}\n\nfunc (l *testGeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {\n\treturn v2geo.LoadGeoSite(\"v2geo/geosite.dat\")\n}\n\nfunc TestCompile(t *testing.T) {\n\tob1, ob2, ob3, ob4, ob5, ob6 := 1, 2, 3, 4, 5, 6\n\trules := []TextRule{\n\t\t{\n\t\t\tOutbound:      \"ob1\",\n\t\t\tAddress:       \"1.2.3.4\",\n\t\t\tProtoPort:     \"\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob2\",\n\t\t\tAddress:       \"8.8.8.0/24\",\n\t\t\tProtoPort:     \"*\",\n\t\t\tHijackAddress: \"1.1.1.1\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob3\",\n\t\t\tAddress:       \"all\",\n\t\t\tProtoPort:     \"udp/443\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob1\",\n\t\t\tAddress:       \"2606:4700::6810:85e5\",\n\t\t\tProtoPort:     \"tcp\",\n\t\t\tHijackAddress: \"2606:4700::6810:85e6\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob2\",\n\t\t\tAddress:       \"2606:4700::/44\",\n\t\t\tProtoPort:     \"*/8888\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob3\",\n\t\t\tAddress:       \"*.v2ex.com\",\n\t\t\tProtoPort:     \"udp\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob1\",\n\t\t\tAddress:       \"crap.v2ex.com\",\n\t\t\tProtoPort:     \"tcp/80\",\n\t\t\tHijackAddress: \"2.2.2.2\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob2\",\n\t\t\tAddress:       \"geoip:JP\",\n\t\t\tProtoPort:     \"*/*\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob4\",\n\t\t\tAddress:       \"geosite:4chan\",\n\t\t\tProtoPort:     \"*/*\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob4\",\n\t\t\tAddress:       \"geosite:google @cn\",\n\t\t\tProtoPort:     \"*/*\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob5\",\n\t\t\tAddress:       \"suffix:microsoft.com\",\n\t\t\tProtoPort:     \"*/*\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t\t{\n\t\t\tOutbound:      \"ob6\",\n\t\t\tAddress:       \"all\",\n\t\t\tProtoPort:     \"tcp/6881-6889\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t}\n\tcomp, err := Compile[int](rules, map[string]int{\n\t\t\"ob1\": ob1,\n\t\t\"ob2\": ob2,\n\t\t\"ob3\": ob3,\n\t\t\"ob4\": ob4,\n\t\t\"ob5\": ob5,\n\t\t\"ob6\": ob6,\n\t}, 100, &testGeoLoader{})\n\tassert.NoError(t, err)\n\n\ttests := []struct {\n\t\thost         HostInfo\n\t\tproto        Protocol\n\t\tport         uint16\n\t\twantOutbound int\n\t\twantIP       net.IP\n\t}{\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"1.2.3.4\"),\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         1234,\n\t\t\twantOutbound: ob1,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"8.8.8.4\"),\n\t\t\t},\n\t\t\tproto:        ProtocolUDP,\n\t\t\tport:         5353,\n\t\t\twantOutbound: ob2,\n\t\t\twantIP:       net.ParseIP(\"1.1.1.1\"),\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"lean.delicious.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolUDP,\n\t\t\tport:         443,\n\t\t\twantOutbound: ob3,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tIPv6: net.ParseIP(\"2606:4700::6810:85e5\"),\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         80,\n\t\t\twantOutbound: ob1,\n\t\t\twantIP:       net.ParseIP(\"2606:4700::6810:85e6\"),\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tIPv6: net.ParseIP(\"2606:4700:0:0:0:0:0:1\"),\n\t\t\t},\n\t\t\tproto:        ProtocolUDP,\n\t\t\tport:         8888,\n\t\t\twantOutbound: ob2,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"www.v2ex.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolUDP,\n\t\t\tport:         1234,\n\t\t\twantOutbound: ob3,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"crap.v2ex.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         80,\n\t\t\twantOutbound: ob1,\n\t\t\twantIP:       net.ParseIP(\"2.2.2.2\"),\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"210.140.92.187\"),\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         25,\n\t\t\twantOutbound: ob2,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"175.45.176.73\"),\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         80,\n\t\t\twantOutbound: 0, // no match default\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"boards.4channel.org\",\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         443,\n\t\t\twantOutbound: ob4,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"gstatic-cn.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolUDP,\n\t\t\tport:         9999,\n\t\t\twantOutbound: ob4,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"hoho.waymo.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolUDP,\n\t\t\tport:         9999,\n\t\t\twantOutbound: 0, // no match default\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"microsoft.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         6000,\n\t\t\twantOutbound: ob5,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"real.microsoft.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolUDP,\n\t\t\tport:         5353,\n\t\t\twantOutbound: ob5,\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"fakemicrosoft.com\",\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         5000,\n\t\t\twantOutbound: 0, // no match default\n\t\t\twantIP:       nil,\n\t\t},\n\t\t{\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"223.1.1.1\"),\n\t\t\t},\n\t\t\tproto:        ProtocolTCP,\n\t\t\tport:         6883,\n\t\t\twantOutbound: ob6, // match range port rule 6881-6889\n\t\t\twantIP:       nil,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tgotOutbound, gotIP := comp.Match(test.host, test.proto, test.port)\n\t\tassert.Equal(t, test.wantOutbound, gotOutbound)\n\t\tassert.Equal(t, test.wantIP, gotIP)\n\t}\n\n\t// Test Invalid Port Range Rule\n\teb1 := 1\n\tinvalidRules := []TextRule{\n\t\t{\n\t\t\tOutbound:      \"eb1\",\n\t\t\tAddress:       \"1.1.2.0/24\",\n\t\t\tProtoPort:     \"*/3-1\",\n\t\t\tHijackAddress: \"\",\n\t\t},\n\t}\n\n\t_, err = Compile[int](invalidRules, map[string]int{\n\t\t\"eb1\": eb1,\n\t}, 100, &testGeoLoader{})\n\tassert.Error(t, err)\n}\n\nfunc Test_parseGeoSiteName(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\ts     string\n\t\twant  string\n\t\twant1 []string\n\t}{\n\t\t{\n\t\t\tname:  \"no attrs\",\n\t\t\ts:     \"pornhub\",\n\t\t\twant:  \"pornhub\",\n\t\t\twant1: []string{},\n\t\t},\n\t\t{\n\t\t\tname:  \"one attr 1\",\n\t\t\ts:     \"xiaomi@cn\",\n\t\t\twant:  \"xiaomi\",\n\t\t\twant1: []string{\"cn\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"one attr 2\",\n\t\t\ts:     \" google @jp \",\n\t\t\twant:  \"google\",\n\t\t\twant1: []string{\"jp\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"two attrs 1\",\n\t\t\ts:     \"netflix@jp@kr\",\n\t\t\twant:  \"netflix\",\n\t\t\twant1: []string{\"jp\", \"kr\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"two attrs 2\",\n\t\t\ts:     \"netflix @xixi    @haha \",\n\t\t\twant:  \"netflix\",\n\t\t\twant1: []string{\"xixi\", \"haha\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"empty\",\n\t\t\ts:     \"\",\n\t\t\twant:  \"\",\n\t\t\twant1: []string{},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1 := parseGeoSiteName(tt.s)\n\t\t\tassert.Equalf(t, tt.want, got, \"parseGeoSiteName(%v)\", tt.s)\n\t\t\tassert.Equalf(t, tt.want1, got1, \"parseGeoSiteName(%v)\", tt.s)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/acl/matchers.go",
    "content": "package acl\n\nimport (\n\t\"net\"\n\t\"strings\"\n\n\t\"golang.org/x/net/idna\"\n)\n\nconst (\n\tdomainMatchExact = uint8(iota)\n\tdomainMatchWildcard\n\tdomainMatchSuffix\n)\n\ntype hostMatcher interface {\n\tMatch(HostInfo) bool\n}\n\ntype ipMatcher struct {\n\tIP net.IP\n}\n\nfunc (m *ipMatcher) Match(host HostInfo) bool {\n\treturn m.IP.Equal(host.IPv4) || m.IP.Equal(host.IPv6)\n}\n\ntype cidrMatcher struct {\n\tIPNet *net.IPNet\n}\n\nfunc (m *cidrMatcher) Match(host HostInfo) bool {\n\treturn m.IPNet.Contains(host.IPv4) || m.IPNet.Contains(host.IPv6)\n}\n\ntype domainMatcher struct {\n\tPattern string\n\tMode    uint8\n}\n\nfunc (m *domainMatcher) Match(host HostInfo) bool {\n\tname, err := idna.ToUnicode(host.Name)\n\tif err != nil {\n\t\tname = host.Name\n\t}\n\tswitch m.Mode {\n\tcase domainMatchExact:\n\t\treturn name == m.Pattern\n\tcase domainMatchWildcard:\n\t\treturn deepMatchRune([]rune(name), []rune(m.Pattern))\n\tcase domainMatchSuffix:\n\t\treturn name == m.Pattern || strings.HasSuffix(name, \".\"+m.Pattern)\n\tdefault:\n\t\treturn false // Invalid mode\n\t}\n}\n\nfunc deepMatchRune(str, pattern []rune) bool {\n\tfor len(pattern) > 0 {\n\t\tswitch pattern[0] {\n\t\tdefault:\n\t\t\tif len(str) == 0 || str[0] != pattern[0] {\n\t\t\t\treturn false\n\t\t\t}\n\t\tcase '*':\n\t\t\treturn deepMatchRune(str, pattern[1:]) ||\n\t\t\t\t(len(str) > 0 && deepMatchRune(str[1:], pattern))\n\t\t}\n\t\tstr = str[1:]\n\t\tpattern = pattern[1:]\n\t}\n\treturn len(str) == 0 && len(pattern) == 0\n}\n\ntype allMatcher struct{}\n\nfunc (m *allMatcher) Match(host HostInfo) bool {\n\treturn true\n}\n"
  },
  {
    "path": "extras/outbounds/acl/matchers_test.go",
    "content": "package acl\n\nimport (\n\t\"net\"\n\t\"testing\"\n)\n\nfunc Test_ipMatcher_Match(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tIP   net.IP\n\t\thost HostInfo\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"ipv4 match\",\n\t\t\tIP:   net.IPv4(127, 0, 0, 1),\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.IPv4(127, 0, 0, 1),\n\t\t\t\tIPv6: nil,\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ipv6 match\",\n\t\t\tIP:   net.IPv6loopback,\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: nil,\n\t\t\t\tIPv6: net.IPv6loopback,\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"no match\",\n\t\t\tIP:   net.IPv4(127, 0, 0, 1),\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.IPv4(127, 0, 0, 2),\n\t\t\t\tIPv6: net.IPv6loopback,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"both nil\",\n\t\t\tIP:   net.IPv4(127, 0, 0, 1),\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: nil,\n\t\t\t\tIPv6: nil,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &ipMatcher{\n\t\t\t\tIP: tt.IP,\n\t\t\t}\n\t\t\tif got := m.Match(tt.host); got != tt.want {\n\t\t\t\tt.Errorf(\"Match() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_cidrMatcher_Match(t *testing.T) {\n\t_, cidr1, _ := net.ParseCIDR(\"192.168.1.0/24\")\n\t_, cidr2, _ := net.ParseCIDR(\"::1/128\")\n\t_, cidr3, _ := net.ParseCIDR(\"0.0.0.0/0\")\n\t_, cidr4, _ := net.ParseCIDR(\"::/0\")\n\n\ttests := []struct {\n\t\tname  string\n\t\tIPNet *net.IPNet\n\t\thost  HostInfo\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname:  \"ipv4 match\",\n\t\t\tIPNet: cidr1,\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"192.168.1.100\"),\n\t\t\t\tIPv6: net.ParseIP(\"::1\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"ipv6 match\",\n\t\t\tIPNet: cidr2,\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"10.0.0.1\"),\n\t\t\t\tIPv6: net.ParseIP(\"::1\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"no match\",\n\t\t\tIPNet: cidr1,\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"10.0.0.1\"),\n\t\t\t\tIPv6: net.ParseIP(\"2001:db8::2:1\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"ipv4 broad\",\n\t\t\tIPNet: cidr3,\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"10.0.0.1\"),\n\t\t\t\tIPv6: net.ParseIP(\"::1\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"ipv6 broad\",\n\t\t\tIPNet: cidr4,\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"10.0.0.1\"),\n\t\t\t\tIPv6: net.ParseIP(\"2001:db8::2:1\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"both nil\",\n\t\t\tIPNet: cidr1,\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: nil,\n\t\t\t\tIPv6: nil,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &cidrMatcher{\n\t\t\t\tIPNet: tt.IPNet,\n\t\t\t}\n\t\t\tif got := m.Match(tt.host); got != tt.want {\n\t\t\t\tt.Errorf(\"Match() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_domainMatcher_Match(t *testing.T) {\n\ttype fields struct {\n\t\tPattern string\n\t\tMode    uint8\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\thost   HostInfo\n\t\twant   bool\n\t}{\n\t\t{\n\t\t\tname: \"non-wildcard match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"example.com\",\n\t\t\t\tMode:    domainMatchExact,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"example.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"non-wildcard IDN match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"政府.中国\",\n\t\t\t\tMode:    domainMatchExact,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"xn--mxtq1m.xn--fiqs8s\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"non-wildcard no match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"example.com\",\n\t\t\t\tMode:    domainMatchExact,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"example.org\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"non-wildcard IDN no match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"政府.中国\",\n\t\t\t\tMode:    domainMatchExact,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"xn--mxtq1m.xn--yfro4i67o\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard match 1\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"*.example.com\",\n\t\t\t\tMode:    domainMatchWildcard,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"www.example.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard match 2\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"example*.com\",\n\t\t\t\tMode:    domainMatchWildcard,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"example2.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard IDN match 1\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"战狼*.com\",\n\t\t\t\tMode:    domainMatchWildcard,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"xn--2-x14by21c.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard IDN match 2\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"*大学*\",\n\t\t\t\tMode:    domainMatchWildcard,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"xn--xkry9kk1bz66a.xn--ses554g\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard no match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"*.example.com\",\n\t\t\t\tMode:    domainMatchWildcard,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"example.com\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard IDN no match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"*呵呵*\",\n\t\t\t\tMode:    domainMatchWildcard,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"xn--6qqt7juua.cn\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"suffix match 1\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"apple.com\",\n\t\t\t\tMode:    domainMatchSuffix,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"apple.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"suffix match 2\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"apple.com\",\n\t\t\t\tMode:    domainMatchSuffix,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"store.apple.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"suffix IDN match 1\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"中国\",\n\t\t\t\tMode:    domainMatchSuffix,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"中国\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"suffix IDN match 2\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"中国\",\n\t\t\t\tMode:    domainMatchSuffix,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"天安门.中国\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"suffix no match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"news.com\",\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"fakenews.com\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"suffix IDN no match\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"冲浪\",\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"666.网上冲浪\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tfields: fields{\n\t\t\t\tPattern: \"*.example.com\",\n\t\t\t\tMode:    domainMatchWildcard,\n\t\t\t},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &domainMatcher{\n\t\t\t\tPattern: tt.fields.Pattern,\n\t\t\t\tMode:    tt.fields.Mode,\n\t\t\t}\n\t\t\tif got := m.Match(tt.host); got != tt.want {\n\t\t\t\tt.Errorf(\"Match() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/acl/matchers_v2geo.go",
    "content": "package acl\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"net\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo\"\n)\n\nvar _ hostMatcher = (*geoipMatcher)(nil)\n\ntype geoipMatcher struct {\n\tN4      []*net.IPNet // sorted\n\tN6      []*net.IPNet // sorted\n\tInverse bool\n}\n\n// matchIP tries to match the given IP address with the corresponding IPNets.\n// Note that this function does NOT handle the Inverse flag.\nfunc (m *geoipMatcher) matchIP(ip net.IP) bool {\n\tvar n []*net.IPNet\n\tif ip4 := ip.To4(); ip4 != nil {\n\t\t// N4 stores IPv4 addresses in 4-byte form.\n\t\t// Make sure we use it here too, otherwise bytes.Compare will fail.\n\t\tip = ip4\n\t\tn = m.N4\n\t} else {\n\t\tn = m.N6\n\t}\n\tleft, right := 0, len(n)-1\n\tfor left <= right {\n\t\tmid := (left + right) / 2\n\t\tif n[mid].Contains(ip) {\n\t\t\treturn true\n\t\t} else if bytes.Compare(n[mid].IP, ip) < 0 {\n\t\t\tleft = mid + 1\n\t\t} else {\n\t\t\tright = mid - 1\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (m *geoipMatcher) Match(host HostInfo) bool {\n\tif host.IPv4 != nil {\n\t\tif m.matchIP(host.IPv4) {\n\t\t\treturn !m.Inverse\n\t\t}\n\t}\n\tif host.IPv6 != nil {\n\t\tif m.matchIP(host.IPv6) {\n\t\t\treturn !m.Inverse\n\t\t}\n\t}\n\treturn m.Inverse\n}\n\nfunc newGeoIPMatcher(list *v2geo.GeoIP) (*geoipMatcher, error) {\n\tn4 := make([]*net.IPNet, 0)\n\tn6 := make([]*net.IPNet, 0)\n\tfor _, cidr := range list.Cidr {\n\t\tif len(cidr.Ip) == 4 {\n\t\t\t// IPv4\n\t\t\tn4 = append(n4, &net.IPNet{\n\t\t\t\tIP:   cidr.Ip,\n\t\t\t\tMask: net.CIDRMask(int(cidr.Prefix), 32),\n\t\t\t})\n\t\t} else if len(cidr.Ip) == 16 {\n\t\t\t// IPv6\n\t\t\tn6 = append(n6, &net.IPNet{\n\t\t\t\tIP:   cidr.Ip,\n\t\t\t\tMask: net.CIDRMask(int(cidr.Prefix), 128),\n\t\t\t})\n\t\t} else {\n\t\t\treturn nil, errors.New(\"invalid IP length\")\n\t\t}\n\t}\n\t// Sort the IPNets, so we can do binary search later.\n\tsort.Slice(n4, func(i, j int) bool {\n\t\treturn bytes.Compare(n4[i].IP, n4[j].IP) < 0\n\t})\n\tsort.Slice(n6, func(i, j int) bool {\n\t\treturn bytes.Compare(n6[i].IP, n6[j].IP) < 0\n\t})\n\treturn &geoipMatcher{\n\t\tN4:      n4,\n\t\tN6:      n6,\n\t\tInverse: list.InverseMatch,\n\t}, nil\n}\n\nvar _ hostMatcher = (*geositeMatcher)(nil)\n\ntype geositeDomainType int\n\nconst (\n\tgeositeDomainPlain geositeDomainType = iota\n\tgeositeDomainRegex\n\tgeositeDomainRoot\n\tgeositeDomainFull\n)\n\ntype geositeDomain struct {\n\tType  geositeDomainType\n\tValue string\n\tRegex *regexp.Regexp\n\tAttrs map[string]bool\n}\n\ntype geositeMatcher struct {\n\tDomains []geositeDomain\n\t// Attributes are matched using \"and\" logic - if you have multiple attributes here,\n\t// a domain must have all of those attributes to be considered a match.\n\tAttrs []string\n}\n\nfunc (m *geositeMatcher) matchDomain(domain geositeDomain, host HostInfo) bool {\n\t// Match attributes first\n\tif len(m.Attrs) > 0 {\n\t\tif len(domain.Attrs) == 0 {\n\t\t\treturn false\n\t\t}\n\t\tfor _, attr := range m.Attrs {\n\t\t\tif !domain.Attrs[attr] {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch domain.Type {\n\tcase geositeDomainPlain:\n\t\treturn strings.Contains(host.Name, domain.Value)\n\tcase geositeDomainRegex:\n\t\tif domain.Regex != nil {\n\t\t\treturn domain.Regex.MatchString(host.Name)\n\t\t}\n\tcase geositeDomainFull:\n\t\treturn host.Name == domain.Value\n\tcase geositeDomainRoot:\n\t\tif host.Name == domain.Value {\n\t\t\treturn true\n\t\t}\n\t\treturn strings.HasSuffix(host.Name, \".\"+domain.Value)\n\tdefault:\n\t\treturn false\n\t}\n\treturn false\n}\n\nfunc (m *geositeMatcher) Match(host HostInfo) bool {\n\tfor _, domain := range m.Domains {\n\t\tif m.matchDomain(domain, host) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc newGeositeMatcher(list *v2geo.GeoSite, attrs []string) (*geositeMatcher, error) {\n\tdomains := make([]geositeDomain, len(list.Domain))\n\tfor i, domain := range list.Domain {\n\t\tswitch domain.Type {\n\t\tcase v2geo.Domain_Plain:\n\t\t\tdomains[i] = geositeDomain{\n\t\t\t\tType:  geositeDomainPlain,\n\t\t\t\tValue: domain.Value,\n\t\t\t\tAttrs: domainAttributeToMap(domain.Attribute),\n\t\t\t}\n\t\tcase v2geo.Domain_Regex:\n\t\t\tregex, err := regexp.Compile(domain.Value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tdomains[i] = geositeDomain{\n\t\t\t\tType:  geositeDomainRegex,\n\t\t\t\tRegex: regex,\n\t\t\t\tAttrs: domainAttributeToMap(domain.Attribute),\n\t\t\t}\n\t\tcase v2geo.Domain_Full:\n\t\t\tdomains[i] = geositeDomain{\n\t\t\t\tType:  geositeDomainFull,\n\t\t\t\tValue: domain.Value,\n\t\t\t\tAttrs: domainAttributeToMap(domain.Attribute),\n\t\t\t}\n\t\tcase v2geo.Domain_RootDomain:\n\t\t\tdomains[i] = geositeDomain{\n\t\t\t\tType:  geositeDomainRoot,\n\t\t\t\tValue: domain.Value,\n\t\t\t\tAttrs: domainAttributeToMap(domain.Attribute),\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"unsupported domain type\")\n\t\t}\n\t}\n\treturn &geositeMatcher{\n\t\tDomains: domains,\n\t\tAttrs:   attrs,\n\t}, nil\n}\n\nfunc domainAttributeToMap(attrs []*v2geo.Domain_Attribute) map[string]bool {\n\tm := make(map[string]bool)\n\tfor _, attr := range attrs {\n\t\t// Supposedly there are also int attributes,\n\t\t// but nobody seems to use them, so we treat everything as boolean for now.\n\t\tm[attr.Key] = true\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "extras/outbounds/acl/matchers_v2geo_test.go",
    "content": "package acl\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_geoipMatcher_Match(t *testing.T) {\n\tgeoipMap, err := v2geo.LoadGeoIP(\"v2geo/geoip.dat\")\n\tassert.NoError(t, err)\n\tm, err := newGeoIPMatcher(geoipMap[\"us\"])\n\tassert.NoError(t, err)\n\n\ttests := []struct {\n\t\tname string\n\t\thost HostInfo\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"IPv4 match\",\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"73.222.1.100\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"IPv4 no match\",\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: net.ParseIP(\"123.123.123.123\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"IPv6 match\",\n\t\t\thost: HostInfo{\n\t\t\t\tIPv6: net.ParseIP(\"2607:f8b0:4005:80c::2004\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"IPv6 no match\",\n\t\t\thost: HostInfo{\n\t\t\t\tIPv6: net.ParseIP(\"240e:947:6001::1f8\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"both nil\",\n\t\t\thost: HostInfo{\n\t\t\t\tIPv4: nil,\n\t\t\t\tIPv6: nil,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equalf(t, tt.want, m.Match(tt.host), \"Match(%v)\", tt.host)\n\t\t})\n\t}\n}\n\nfunc Test_geositeMatcher_Match(t *testing.T) {\n\tgeositeMap, err := v2geo.LoadGeoSite(\"v2geo/geosite.dat\")\n\tassert.NoError(t, err)\n\tm, err := newGeositeMatcher(geositeMap[\"apple\"], nil)\n\tassert.NoError(t, err)\n\n\ttests := []struct {\n\t\tname  string\n\t\tattrs []string\n\t\thost  HostInfo\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname:  \"subdomain\",\n\t\t\tattrs: nil,\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"poop.i-book.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"subdomain root\",\n\t\t\tattrs: nil,\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"applepaycash.net\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"full\",\n\t\t\tattrs: nil,\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"courier-push-apple.com.akadns.net\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"regexp\",\n\t\t\tattrs: nil,\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"cdn4.apple-mapkit.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"attr match\",\n\t\t\tattrs: []string{\"cn\"},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"bag.itunes.apple.com\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"attr multi no match\",\n\t\t\tattrs: []string{\"cn\", \"haha\"},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"bag.itunes.apple.com\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname:  \"attr no match\",\n\t\t\tattrs: []string{\"cn\"},\n\t\t\thost: HostInfo{\n\t\t\t\tName: \"mr-apple.com.tw\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm.Attrs = tt.attrs\n\t\t\tassert.Equalf(t, tt.want, m.Match(tt.host), \"Match(%v)\", tt.host)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/acl/parse.go",
    "content": "package acl\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar linePattern = regexp.MustCompile(`^(\\w+)\\s*\\(([^,]+)(?:,([^,]+))?(?:,([^,]+))?\\)$`)\n\ntype InvalidSyntaxError struct {\n\tLine    string\n\tLineNum int\n}\n\nfunc (e *InvalidSyntaxError) Error() string {\n\treturn fmt.Sprintf(\"invalid syntax at line %d: %s\", e.LineNum, e.Line)\n}\n\n// TextRule is the struct representation of a (non-comment) line parsed from an ACL file.\n// A line can be parsed into a TextRule as long as it matches one of the following patterns:\n//\n//\toutbound(address)\n//\toutbound(address,protoPort)\n//\toutbound(address,protoPort,hijackAddress)\n//\n// It does not check whether any of the fields is valid - it's up to the compiler to do so.\ntype TextRule struct {\n\tOutbound      string\n\tAddress       string\n\tProtoPort     string\n\tHijackAddress string\n\tLineNum       int\n}\n\nfunc parseLine(line string, num int) *TextRule {\n\tmatches := linePattern.FindStringSubmatch(line)\n\tif matches == nil {\n\t\treturn nil\n\t}\n\treturn &TextRule{\n\t\tOutbound:      matches[1],\n\t\tAddress:       strings.TrimSpace(matches[2]),\n\t\tProtoPort:     strings.TrimSpace(matches[3]),\n\t\tHijackAddress: strings.TrimSpace(matches[4]),\n\t\tLineNum:       num,\n\t}\n}\n\nfunc ParseTextRules(text string) ([]TextRule, error) {\n\trules := make([]TextRule, 0)\n\tlineNum := 0\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tlineNum++\n\t\t// Remove comments\n\t\tif i := strings.Index(line, \"#\"); i >= 0 {\n\t\t\tline = line[:i]\n\t\t}\n\t\tline = strings.TrimSpace(line)\n\t\t// Skip empty lines\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\t// Parse line\n\t\trule := parseLine(line, lineNum)\n\t\tif rule == nil {\n\t\t\treturn nil, &InvalidSyntaxError{line, lineNum}\n\t\t}\n\t\trules = append(rules, *rule)\n\t}\n\treturn rules, nil\n}\n"
  },
  {
    "path": "extras/outbounds/acl/parse_test.go",
    "content": "package acl\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestParseTextRules(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\ttext    string\n\t\twant    []TextRule\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"empty\",\n\t\t\ttext:    \"\",\n\t\t\twant:    []TextRule{},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\ttext: `\n# just a comment\n # another comment\ndirect(1.1.1.1)\ndirect(8.8.8.0/24)\nreject(all, udp/443) # inline comment\n reject(geoip:cn)\n  reject(*.v2ex.com)\nmy_custom_outbound1(9.9.9.9,*,   8.8.8.8) # bebop\nmy_custom_outbound2(all)\n`,\n\t\t\twant: []TextRule{\n\t\t\t\t{Outbound: \"direct\", Address: \"1.1.1.1\", LineNum: 4},\n\t\t\t\t{Outbound: \"direct\", Address: \"8.8.8.0/24\", LineNum: 5},\n\t\t\t\t{Outbound: \"reject\", Address: \"all\", ProtoPort: \"udp/443\", LineNum: 6},\n\t\t\t\t{Outbound: \"reject\", Address: \"geoip:cn\", LineNum: 7},\n\t\t\t\t{Outbound: \"reject\", Address: \"*.v2ex.com\", LineNum: 8},\n\t\t\t\t{Outbound: \"my_custom_outbound1\", Address: \"9.9.9.9\", ProtoPort: \"*\", HijackAddress: \"8.8.8.8\", LineNum: 9},\n\t\t\t\t{Outbound: \"my_custom_outbound2\", Address: \"all\", LineNum: 10},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"fail 1\",\n\t\t\ttext:    `boom()`,\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"fail 2\",\n\t\t\ttext:    `lol(1,1,1,1)`,\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := ParseTextRules(tt.text)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ParseTextRules() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"ParseTextRules() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/acl/v2geo/load.go",
    "content": "package v2geo\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// LoadGeoIP loads a GeoIP data file and converts it to a map.\n// The keys of the map (country codes) are all normalized to lowercase.\nfunc LoadGeoIP(filename string) (map[string]*GeoIP, error) {\n\tbs, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar list GeoIPList\n\tif err := proto.Unmarshal(bs, &list); err != nil {\n\t\treturn nil, err\n\t}\n\tm := make(map[string]*GeoIP)\n\tfor _, entry := range list.Entry {\n\t\tm[strings.ToLower(entry.CountryCode)] = entry\n\t}\n\treturn m, nil\n}\n\n// LoadGeoSite loads a GeoSite data file and converts it to a map.\n// The keys of the map (site keys) are all normalized to lowercase.\nfunc LoadGeoSite(filename string) (map[string]*GeoSite, error) {\n\tbs, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar list GeoSiteList\n\tif err := proto.Unmarshal(bs, &list); err != nil {\n\t\treturn nil, err\n\t}\n\tm := make(map[string]*GeoSite)\n\tfor _, entry := range list.Entry {\n\t\tm[strings.ToLower(entry.CountryCode)] = entry\n\t}\n\treturn m, nil\n}\n"
  },
  {
    "path": "extras/outbounds/acl/v2geo/load_test.go",
    "content": "package v2geo\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLoadGeoIP(t *testing.T) {\n\tm, err := LoadGeoIP(\"geoip.dat\")\n\tassert.NoError(t, err)\n\n\t// Exact checks since we know the data.\n\tassert.Len(t, m, 252)\n\tassert.Equal(t, m[\"cn\"].CountryCode, \"CN\")\n\tassert.Len(t, m[\"cn\"].Cidr, 10407)\n\tassert.Equal(t, m[\"us\"].CountryCode, \"US\")\n\tassert.Len(t, m[\"us\"].Cidr, 193171)\n\tassert.Equal(t, m[\"private\"].CountryCode, \"PRIVATE\")\n\tassert.Len(t, m[\"private\"].Cidr, 18)\n\tassert.Contains(t, m[\"private\"].Cidr, &CIDR{\n\t\tIp:     []byte(\"\\xc0\\xa8\\x00\\x00\"),\n\t\tPrefix: 16,\n\t})\n}\n\nfunc TestLoadGeoSite(t *testing.T) {\n\tm, err := LoadGeoSite(\"geosite.dat\")\n\tassert.NoError(t, err)\n\n\t// Exact checks since we know the data.\n\tassert.Len(t, m, 1204)\n\tassert.Equal(t, m[\"netflix\"].CountryCode, \"NETFLIX\")\n\tassert.Len(t, m[\"netflix\"].Domain, 25)\n\tassert.Contains(t, m[\"netflix\"].Domain, &Domain{\n\t\tType:  Domain_Full,\n\t\tValue: \"netflix.com.edgesuite.net\",\n\t})\n\tassert.Contains(t, m[\"netflix\"].Domain, &Domain{\n\t\tType:  Domain_RootDomain,\n\t\tValue: \"fast.com\",\n\t})\n\tassert.Len(t, m[\"google\"].Domain, 1066)\n\tassert.Contains(t, m[\"google\"].Domain, &Domain{\n\t\tType:  Domain_RootDomain,\n\t\tValue: \"ggpht.cn\",\n\t\tAttribute: []*Domain_Attribute{\n\t\t\t{\n\t\t\t\tKey:        \"cn\",\n\t\t\t\tTypedValue: &Domain_Attribute_BoolValue{BoolValue: true},\n\t\t\t},\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "extras/outbounds/acl/v2geo/v2geo.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.31.0\n// \tprotoc        v4.24.4\n// source: v2geo.proto\n\npackage v2geo\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// Type of domain value.\ntype Domain_Type int32\n\nconst (\n\t// The value is used as is.\n\tDomain_Plain Domain_Type = 0\n\t// The value is used as a regular expression.\n\tDomain_Regex Domain_Type = 1\n\t// The value is a root domain.\n\tDomain_RootDomain Domain_Type = 2\n\t// The value is a domain.\n\tDomain_Full Domain_Type = 3\n)\n\n// Enum value maps for Domain_Type.\nvar (\n\tDomain_Type_name = map[int32]string{\n\t\t0: \"Plain\",\n\t\t1: \"Regex\",\n\t\t2: \"RootDomain\",\n\t\t3: \"Full\",\n\t}\n\tDomain_Type_value = map[string]int32{\n\t\t\"Plain\":      0,\n\t\t\"Regex\":      1,\n\t\t\"RootDomain\": 2,\n\t\t\"Full\":       3,\n\t}\n)\n\nfunc (x Domain_Type) Enum() *Domain_Type {\n\tp := new(Domain_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x Domain_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Domain_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_v2geo_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Domain_Type) Type() protoreflect.EnumType {\n\treturn &file_v2geo_proto_enumTypes[0]\n}\n\nfunc (x Domain_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Domain_Type.Descriptor instead.\nfunc (Domain_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{0, 0}\n}\n\n// Domain for routing decision.\ntype Domain struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\t// Domain matching type.\n\tType Domain_Type `protobuf:\"varint,1,opt,name=type,proto3,enum=Domain_Type\" json:\"type,omitempty\"`\n\t// Domain value.\n\tValue string `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\t// Attributes of this domain. May be used for filtering.\n\tAttribute []*Domain_Attribute `protobuf:\"bytes,3,rep,name=attribute,proto3\" json:\"attribute,omitempty\"`\n}\n\nfunc (x *Domain) Reset() {\n\t*x = Domain{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_v2geo_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *Domain) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Domain) ProtoMessage() {}\n\nfunc (x *Domain) ProtoReflect() protoreflect.Message {\n\tmi := &file_v2geo_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Domain.ProtoReflect.Descriptor instead.\nfunc (*Domain) Descriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Domain) GetType() Domain_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn Domain_Plain\n}\n\nfunc (x *Domain) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\nfunc (x *Domain) GetAttribute() []*Domain_Attribute {\n\tif x != nil {\n\t\treturn x.Attribute\n\t}\n\treturn nil\n}\n\n// IP for routing decision, in CIDR form.\ntype CIDR struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\t// IP address, should be either 4 or 16 bytes.\n\tIp []byte `protobuf:\"bytes,1,opt,name=ip,proto3\" json:\"ip,omitempty\"`\n\t// Number of leading ones in the network mask.\n\tPrefix uint32 `protobuf:\"varint,2,opt,name=prefix,proto3\" json:\"prefix,omitempty\"`\n}\n\nfunc (x *CIDR) Reset() {\n\t*x = CIDR{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_v2geo_proto_msgTypes[1]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *CIDR) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CIDR) ProtoMessage() {}\n\nfunc (x *CIDR) ProtoReflect() protoreflect.Message {\n\tmi := &file_v2geo_proto_msgTypes[1]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CIDR.ProtoReflect.Descriptor instead.\nfunc (*CIDR) Descriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *CIDR) GetIp() []byte {\n\tif x != nil {\n\t\treturn x.Ip\n\t}\n\treturn nil\n}\n\nfunc (x *CIDR) GetPrefix() uint32 {\n\tif x != nil {\n\t\treturn x.Prefix\n\t}\n\treturn 0\n}\n\ntype GeoIP struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tCountryCode  string  `protobuf:\"bytes,1,opt,name=country_code,json=countryCode,proto3\" json:\"country_code,omitempty\"`\n\tCidr         []*CIDR `protobuf:\"bytes,2,rep,name=cidr,proto3\" json:\"cidr,omitempty\"`\n\tInverseMatch bool    `protobuf:\"varint,3,opt,name=inverse_match,json=inverseMatch,proto3\" json:\"inverse_match,omitempty\"`\n\t// resource_hash instruct simplified config converter to load domain from geo file.\n\tResourceHash []byte `protobuf:\"bytes,4,opt,name=resource_hash,json=resourceHash,proto3\" json:\"resource_hash,omitempty\"`\n\tCode         string `protobuf:\"bytes,5,opt,name=code,proto3\" json:\"code,omitempty\"`\n}\n\nfunc (x *GeoIP) Reset() {\n\t*x = GeoIP{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_v2geo_proto_msgTypes[2]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *GeoIP) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GeoIP) ProtoMessage() {}\n\nfunc (x *GeoIP) ProtoReflect() protoreflect.Message {\n\tmi := &file_v2geo_proto_msgTypes[2]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.\nfunc (*GeoIP) Descriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *GeoIP) GetCountryCode() string {\n\tif x != nil {\n\t\treturn x.CountryCode\n\t}\n\treturn \"\"\n}\n\nfunc (x *GeoIP) GetCidr() []*CIDR {\n\tif x != nil {\n\t\treturn x.Cidr\n\t}\n\treturn nil\n}\n\nfunc (x *GeoIP) GetInverseMatch() bool {\n\tif x != nil {\n\t\treturn x.InverseMatch\n\t}\n\treturn false\n}\n\nfunc (x *GeoIP) GetResourceHash() []byte {\n\tif x != nil {\n\t\treturn x.ResourceHash\n\t}\n\treturn nil\n}\n\nfunc (x *GeoIP) GetCode() string {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn \"\"\n}\n\ntype GeoIPList struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tEntry []*GeoIP `protobuf:\"bytes,1,rep,name=entry,proto3\" json:\"entry,omitempty\"`\n}\n\nfunc (x *GeoIPList) Reset() {\n\t*x = GeoIPList{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_v2geo_proto_msgTypes[3]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *GeoIPList) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GeoIPList) ProtoMessage() {}\n\nfunc (x *GeoIPList) ProtoReflect() protoreflect.Message {\n\tmi := &file_v2geo_proto_msgTypes[3]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.\nfunc (*GeoIPList) Descriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *GeoIPList) GetEntry() []*GeoIP {\n\tif x != nil {\n\t\treturn x.Entry\n\t}\n\treturn nil\n}\n\ntype GeoSite struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tCountryCode string    `protobuf:\"bytes,1,opt,name=country_code,json=countryCode,proto3\" json:\"country_code,omitempty\"`\n\tDomain      []*Domain `protobuf:\"bytes,2,rep,name=domain,proto3\" json:\"domain,omitempty\"`\n\t// resource_hash instruct simplified config converter to load domain from geo file.\n\tResourceHash []byte `protobuf:\"bytes,3,opt,name=resource_hash,json=resourceHash,proto3\" json:\"resource_hash,omitempty\"`\n\tCode         string `protobuf:\"bytes,4,opt,name=code,proto3\" json:\"code,omitempty\"`\n}\n\nfunc (x *GeoSite) Reset() {\n\t*x = GeoSite{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_v2geo_proto_msgTypes[4]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *GeoSite) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GeoSite) ProtoMessage() {}\n\nfunc (x *GeoSite) ProtoReflect() protoreflect.Message {\n\tmi := &file_v2geo_proto_msgTypes[4]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GeoSite.ProtoReflect.Descriptor instead.\nfunc (*GeoSite) Descriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *GeoSite) GetCountryCode() string {\n\tif x != nil {\n\t\treturn x.CountryCode\n\t}\n\treturn \"\"\n}\n\nfunc (x *GeoSite) GetDomain() []*Domain {\n\tif x != nil {\n\t\treturn x.Domain\n\t}\n\treturn nil\n}\n\nfunc (x *GeoSite) GetResourceHash() []byte {\n\tif x != nil {\n\t\treturn x.ResourceHash\n\t}\n\treturn nil\n}\n\nfunc (x *GeoSite) GetCode() string {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn \"\"\n}\n\ntype GeoSiteList struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tEntry []*GeoSite `protobuf:\"bytes,1,rep,name=entry,proto3\" json:\"entry,omitempty\"`\n}\n\nfunc (x *GeoSiteList) Reset() {\n\t*x = GeoSiteList{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_v2geo_proto_msgTypes[5]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *GeoSiteList) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GeoSiteList) ProtoMessage() {}\n\nfunc (x *GeoSiteList) ProtoReflect() protoreflect.Message {\n\tmi := &file_v2geo_proto_msgTypes[5]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead.\nfunc (*GeoSiteList) Descriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *GeoSiteList) GetEntry() []*GeoSite {\n\tif x != nil {\n\t\treturn x.Entry\n\t}\n\treturn nil\n}\n\ntype Domain_Attribute struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\tKey string `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`\n\t// Types that are assignable to TypedValue:\n\t//\n\t//\t*Domain_Attribute_BoolValue\n\t//\t*Domain_Attribute_IntValue\n\tTypedValue isDomain_Attribute_TypedValue `protobuf_oneof:\"typed_value\"`\n}\n\nfunc (x *Domain_Attribute) Reset() {\n\t*x = Domain_Attribute{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_v2geo_proto_msgTypes[6]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *Domain_Attribute) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Domain_Attribute) ProtoMessage() {}\n\nfunc (x *Domain_Attribute) ProtoReflect() protoreflect.Message {\n\tmi := &file_v2geo_proto_msgTypes[6]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead.\nfunc (*Domain_Attribute) Descriptor() ([]byte, []int) {\n\treturn file_v2geo_proto_rawDescGZIP(), []int{0, 0}\n}\n\nfunc (x *Domain_Attribute) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {\n\tif m != nil {\n\t\treturn m.TypedValue\n\t}\n\treturn nil\n}\n\nfunc (x *Domain_Attribute) GetBoolValue() bool {\n\tif x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok {\n\t\treturn x.BoolValue\n\t}\n\treturn false\n}\n\nfunc (x *Domain_Attribute) GetIntValue() int64 {\n\tif x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok {\n\t\treturn x.IntValue\n\t}\n\treturn 0\n}\n\ntype isDomain_Attribute_TypedValue interface {\n\tisDomain_Attribute_TypedValue()\n}\n\ntype Domain_Attribute_BoolValue struct {\n\tBoolValue bool `protobuf:\"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof\"`\n}\n\ntype Domain_Attribute_IntValue struct {\n\tIntValue int64 `protobuf:\"varint,3,opt,name=int_value,json=intValue,proto3,oneof\"`\n}\n\nfunc (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}\n\nfunc (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}\n\nvar File_v2geo_proto protoreflect.FileDescriptor\n\nvar file_v2geo_proto_rawDesc = []byte{\n\t0x0a, 0x0b, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x02,\n\t0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,\n\t0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e,\n\t0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,\n\t0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,\n\t0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20,\n\t0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74,\n\t0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,\n\t0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10,\n\t0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,\n\t0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,\n\t0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75,\n\t0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03,\n\t0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65,\n\t0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22,\n\t0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e,\n\t0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0e, 0x0a,\n\t0x0a, 0x52, 0x6f, 0x6f, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a,\n\t0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12,\n\t0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12,\n\t0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,\n\t0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xa3, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49,\n\t0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64,\n\t0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79,\n\t0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03,\n\t0x28, 0x0b, 0x32, 0x05, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12,\n\t0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68,\n\t0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d,\n\t0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,\n\t0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73,\n\t0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,\n\t0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a,\n\t0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x05, 0x65, 0x6e,\n\t0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x47, 0x65, 0x6f, 0x49,\n\t0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x86, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6f,\n\t0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f,\n\t0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e,\n\t0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,\n\t0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,\n\t0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f,\n\t0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,\n\t0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a,\n\t0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64,\n\t0x65, 0x22, 0x2d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74,\n\t0x12, 0x1e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,\n\t0x08, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79,\n\t0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,\n\t0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_v2geo_proto_rawDescOnce sync.Once\n\tfile_v2geo_proto_rawDescData = file_v2geo_proto_rawDesc\n)\n\nfunc file_v2geo_proto_rawDescGZIP() []byte {\n\tfile_v2geo_proto_rawDescOnce.Do(func() {\n\t\tfile_v2geo_proto_rawDescData = protoimpl.X.CompressGZIP(file_v2geo_proto_rawDescData)\n\t})\n\treturn file_v2geo_proto_rawDescData\n}\n\nvar file_v2geo_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_v2geo_proto_msgTypes = make([]protoimpl.MessageInfo, 7)\nvar file_v2geo_proto_goTypes = []interface{}{\n\t(Domain_Type)(0),         // 0: Domain.Type\n\t(*Domain)(nil),           // 1: Domain\n\t(*CIDR)(nil),             // 2: CIDR\n\t(*GeoIP)(nil),            // 3: GeoIP\n\t(*GeoIPList)(nil),        // 4: GeoIPList\n\t(*GeoSite)(nil),          // 5: GeoSite\n\t(*GeoSiteList)(nil),      // 6: GeoSiteList\n\t(*Domain_Attribute)(nil), // 7: Domain.Attribute\n}\nvar file_v2geo_proto_depIdxs = []int32{\n\t0, // 0: Domain.type:type_name -> Domain.Type\n\t7, // 1: Domain.attribute:type_name -> Domain.Attribute\n\t2, // 2: GeoIP.cidr:type_name -> CIDR\n\t3, // 3: GeoIPList.entry:type_name -> GeoIP\n\t1, // 4: GeoSite.domain:type_name -> Domain\n\t5, // 5: GeoSiteList.entry:type_name -> GeoSite\n\t6, // [6:6] is the sub-list for method output_type\n\t6, // [6:6] is the sub-list for method input_type\n\t6, // [6:6] is the sub-list for extension type_name\n\t6, // [6:6] is the sub-list for extension extendee\n\t0, // [0:6] is the sub-list for field type_name\n}\n\nfunc init() { file_v2geo_proto_init() }\nfunc file_v2geo_proto_init() {\n\tif File_v2geo_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_v2geo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*Domain); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_v2geo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*CIDR); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_v2geo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*GeoIP); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_v2geo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*GeoIPList); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_v2geo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*GeoSite); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_v2geo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*GeoSiteList); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tfile_v2geo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*Domain_Attribute); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\tfile_v2geo_proto_msgTypes[6].OneofWrappers = []interface{}{\n\t\t(*Domain_Attribute_BoolValue)(nil),\n\t\t(*Domain_Attribute_IntValue)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_v2geo_proto_rawDesc,\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   7,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_v2geo_proto_goTypes,\n\t\tDependencyIndexes: file_v2geo_proto_depIdxs,\n\t\tEnumInfos:         file_v2geo_proto_enumTypes,\n\t\tMessageInfos:      file_v2geo_proto_msgTypes,\n\t}.Build()\n\tFile_v2geo_proto = out.File\n\tfile_v2geo_proto_rawDesc = nil\n\tfile_v2geo_proto_goTypes = nil\n\tfile_v2geo_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "extras/outbounds/acl/v2geo/v2geo.proto",
    "content": "syntax = \"proto3\";\n\noption go_package = \"./v2geo\";\n\n// This file is copied from\n// https://github.com/v2fly/v2ray-core/blob/master/app/router/routercommon/common.proto\n// with some modifications.\n\n// Domain for routing decision.\nmessage Domain {\n  // Type of domain value.\n  enum Type {\n    // The value is used as is.\n    Plain = 0;\n    // The value is used as a regular expression.\n    Regex = 1;\n    // The value is a root domain.\n    RootDomain = 2;\n    // The value is a domain.\n    Full = 3;\n  }\n\n  // Domain matching type.\n  Type type = 1;\n\n  // Domain value.\n  string value = 2;\n\n  message Attribute {\n    string key = 1;\n\n    oneof typed_value {\n      bool bool_value = 2;\n      int64 int_value = 3;\n    }\n  }\n\n  // Attributes of this domain. May be used for filtering.\n  repeated Attribute attribute = 3;\n}\n\n// IP for routing decision, in CIDR form.\nmessage CIDR {\n  // IP address, should be either 4 or 16 bytes.\n  bytes ip = 1;\n\n  // Number of leading ones in the network mask.\n  uint32 prefix = 2;\n}\n\nmessage GeoIP {\n  string country_code = 1;\n  repeated CIDR cidr = 2;\n  bool inverse_match = 3;\n\n  // resource_hash instruct simplified config converter to load domain from geo file.\n  bytes resource_hash = 4;\n  string code = 5;\n}\n\nmessage GeoIPList {\n  repeated GeoIP entry = 1;\n}\n\nmessage GeoSite {\n  string country_code = 1;\n  repeated Domain domain = 2;\n\n  // resource_hash instruct simplified config converter to load domain from geo file.\n  bytes resource_hash = 3;\n  string code = 4;\n}\n\nmessage GeoSiteList {\n  repeated GeoSite entry = 1;\n}\n"
  },
  {
    "path": "extras/outbounds/acl.go",
    "content": "package outbounds\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/acl\"\n)\n\nconst (\n\taclCacheSize = 1024\n)\n\nvar errRejected = errors.New(\"rejected\")\n\n// aclEngine is a PluggableOutbound that dispatches connections to different\n// outbounds based on ACL rules.\n// There are 3 built-in outbounds:\n// - direct: directOutbound, auto mode\n// - reject: reject the connection\n// - default: first outbound in the list, or if the list is empty, equal to direct\n// If the user-defined outbounds contain any of the above names, they will\n// override the built-in outbounds.\ntype aclEngine struct {\n\tRuleSet acl.CompiledRuleSet[PluggableOutbound]\n\tDefault PluggableOutbound\n}\n\ntype OutboundEntry struct {\n\tName     string\n\tOutbound PluggableOutbound\n}\n\nfunc NewACLEngineFromString(rules string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) {\n\ttrs, err := acl.ParseTextRules(rules)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tobMap := outboundsToMap(outbounds)\n\trs, err := acl.Compile[PluggableOutbound](trs, obMap, aclCacheSize, geoLoader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &aclEngine{rs, obMap[\"default\"]}, nil\n}\n\nfunc NewACLEngineFromFile(filename string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) {\n\tbs, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn NewACLEngineFromString(string(bs), outbounds, geoLoader)\n}\n\nfunc outboundsToMap(outbounds []OutboundEntry) map[string]PluggableOutbound {\n\tobMap := make(map[string]PluggableOutbound)\n\tfor _, ob := range outbounds {\n\t\tobMap[strings.ToLower(ob.Name)] = ob.Outbound\n\t}\n\t// Add built-in outbounds if not overridden\n\tif _, ok := obMap[\"direct\"]; !ok {\n\t\tobMap[\"direct\"] = NewDirectOutboundSimple(DirectOutboundModeAuto)\n\t}\n\tif _, ok := obMap[\"reject\"]; !ok {\n\t\tobMap[\"reject\"] = &aclRejectOutbound{}\n\t}\n\tif _, ok := obMap[\"default\"]; !ok {\n\t\tif len(outbounds) > 0 {\n\t\t\tobMap[\"default\"] = outbounds[0].Outbound\n\t\t} else {\n\t\t\tobMap[\"default\"] = obMap[\"direct\"]\n\t\t}\n\t}\n\treturn obMap\n}\n\nfunc (a *aclEngine) handle(reqAddr *AddrEx, proto acl.Protocol) PluggableOutbound {\n\thostInfo := acl.HostInfo{Name: reqAddr.Host}\n\tif reqAddr.ResolveInfo != nil {\n\t\thostInfo.IPv4 = reqAddr.ResolveInfo.IPv4\n\t\thostInfo.IPv6 = reqAddr.ResolveInfo.IPv6\n\t}\n\tob, hijackIP := a.RuleSet.Match(hostInfo, proto, reqAddr.Port)\n\tif ob == nil {\n\t\t// No match, use default outbound\n\t\treturn a.Default\n\t}\n\tif hijackIP != nil {\n\t\t// We must rewrite both Host & ResolveInfo,\n\t\t// as some outbounds only care about Host.\n\t\treqAddr.Host = hijackIP.String()\n\t\tif ip4 := hijackIP.To4(); ip4 != nil {\n\t\t\treqAddr.ResolveInfo = &ResolveInfo{IPv4: ip4}\n\t\t} else {\n\t\t\treqAddr.ResolveInfo = &ResolveInfo{IPv6: hijackIP}\n\t\t}\n\t}\n\treturn ob\n}\n\nfunc (a *aclEngine) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tob := a.handle(reqAddr, acl.ProtocolTCP)\n\treturn ob.TCP(reqAddr)\n}\n\nfunc (a *aclEngine) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\tob := a.handle(reqAddr, acl.ProtocolUDP)\n\treturn ob.UDP(reqAddr)\n}\n\ntype aclRejectOutbound struct{}\n\nfunc (a *aclRejectOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\treturn nil, errRejected\n}\n\nfunc (a *aclRejectOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\treturn nil, errRejected\n}\n"
  },
  {
    "path": "extras/outbounds/acl_test.go",
    "content": "package outbounds\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestACLEngine(t *testing.T) {\n\tob1, ob2, ob3 := &mockPluggableOutbound{}, &mockPluggableOutbound{}, &mockPluggableOutbound{}\n\tobs := []OutboundEntry{\n\t\t{\"ob1\", ob1},\n\t\t{\"ob2\", ob2},\n\t\t{\"ob3\", ob3},\n\t\t{\"direct\", ob2},\n\t}\n\tacl, err := NewACLEngineFromString(`\nob2(google.com,tcp)\nob3(youtube.com,udp)\nob1 (1.1.1.1/24,*,8.8.8.8)\nDirect(cia.gov)\nreJect(nsa.gov)\n`, obs, nil)\n\tassert.NoError(t, err)\n\n\t// No match, default, should be the first (ob1)\n\tob1.EXPECT().TCP(&AddrEx{Host: \"example.com\"}).Return(nil, nil).Once()\n\tconn, err := acl.TCP(&AddrEx{Host: \"example.com\"})\n\tassert.NoError(t, err)\n\tassert.Nil(t, conn)\n\n\t// Match ob2\n\tob2.EXPECT().TCP(&AddrEx{Host: \"google.com\"}).Return(nil, nil).Once()\n\tconn, err = acl.TCP(&AddrEx{Host: \"google.com\"})\n\tassert.NoError(t, err)\n\tassert.Nil(t, conn)\n\n\t// Match ob3\n\tob3.EXPECT().UDP(&AddrEx{Host: \"youtube.com\"}).Return(nil, nil).Once()\n\tudpConn, err := acl.UDP(&AddrEx{Host: \"youtube.com\"})\n\tassert.NoError(t, err)\n\tassert.Nil(t, udpConn)\n\n\t// Match ob1 hijack IP\n\tob1.EXPECT().TCP(&AddrEx{Host: \"8.8.8.8\", ResolveInfo: &ResolveInfo{IPv4: net.ParseIP(\"8.8.8.8\").To4()}}).Return(nil, nil).Once()\n\tconn, err = acl.TCP(&AddrEx{ResolveInfo: &ResolveInfo{IPv4: net.ParseIP(\"1.1.1.22\")}})\n\tassert.NoError(t, err)\n\tassert.Nil(t, conn)\n\n\t// direct should be ob2 as we override it\n\tob2.EXPECT().TCP(&AddrEx{Host: \"cia.gov\"}).Return(nil, nil).Once()\n\tconn, err = acl.TCP(&AddrEx{Host: \"cia.gov\"})\n\tassert.NoError(t, err)\n\tassert.Nil(t, conn)\n\n\t// reject\n\tconn, err = acl.TCP(&AddrEx{Host: \"nsa.gov\"})\n\tassert.Error(t, err)\n\tassert.Nil(t, conn)\n}\n"
  },
  {
    "path": "extras/outbounds/dns_https.go",
    "content": "package outbounds\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/babolivier/go-doh-client\"\n)\n\n// dohResolver is a PluggableOutbound DNS resolver that resolves hostnames\n// using the user-provided DNS-over-HTTPS server.\ntype dohResolver struct {\n\tResolver *doh.Resolver\n\tNext     PluggableOutbound\n}\n\nfunc NewDoHResolver(host string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound {\n\ttr := http.DefaultTransport.(*http.Transport).Clone()\n\ttr.TLSClientConfig = &tls.Config{\n\t\tServerName:         sni,\n\t\tInsecureSkipVerify: insecure,\n\t}\n\treturn &dohResolver{\n\t\tResolver: &doh.Resolver{\n\t\t\tHost:  host,\n\t\t\tClass: doh.IN,\n\t\t\tHTTPClient: &http.Client{\n\t\t\t\tTransport: tr,\n\t\t\t\tTimeout:   timeoutOrDefault(timeout),\n\t\t\t},\n\t\t},\n\t\tNext: next,\n\t}\n}\n\nfunc (r *dohResolver) resolve(reqAddr *AddrEx) {\n\tif tryParseIP(reqAddr) {\n\t\t// The host is already an IP address, we don't need to resolve it.\n\t\treturn\n\t}\n\ttype lookupResult struct {\n\t\tip  net.IP\n\t\terr error\n\t}\n\tch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1)\n\tgo func() {\n\t\trecs, _, err := r.Resolver.LookupA(reqAddr.Host)\n\t\tvar ip net.IP\n\t\tif err == nil && len(recs) > 0 {\n\t\t\tip = net.ParseIP(recs[0].IP4).To4()\n\t\t}\n\t\tch4 <- lookupResult{ip, err}\n\t}()\n\tgo func() {\n\t\trecs, _, err := r.Resolver.LookupAAAA(reqAddr.Host)\n\t\tvar ip net.IP\n\t\tif err == nil && len(recs) > 0 {\n\t\t\tip = net.ParseIP(recs[0].IP6).To16()\n\t\t}\n\t\tch6 <- lookupResult{ip, err}\n\t}()\n\tresult4, result6 := <-ch4, <-ch6\n\treqAddr.ResolveInfo = &ResolveInfo{\n\t\tIPv4: result4.ip,\n\t\tIPv6: result6.ip,\n\t}\n\tif result4.err != nil {\n\t\treqAddr.ResolveInfo.Err = result4.err\n\t} else if result6.err != nil {\n\t\treqAddr.ResolveInfo.Err = result6.err\n\t}\n}\n\nfunc (r *dohResolver) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tr.resolve(reqAddr)\n\treturn r.Next.TCP(reqAddr)\n}\n\nfunc (r *dohResolver) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\tr.resolve(reqAddr)\n\treturn r.Next.UDP(reqAddr)\n}\n"
  },
  {
    "path": "extras/outbounds/dns_standard.go",
    "content": "package outbounds\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n)\n\nconst (\n\tresolverDefaultTimeout     = 2 * time.Second\n\tstandardResolverRetryTimes = 2\n)\n\n// standardResolver is a PluggableOutbound DNS resolver that resolves hostnames\n// using the user-provided DNS server.\n// Based on \"github.com/miekg/dns\", it supports UDP, TCP & DNS-over-TLS (TCP).\ntype standardResolver struct {\n\tAddr   string\n\tClient *dns.Client\n\tNext   PluggableOutbound\n}\n\nfunc NewStandardResolverUDP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound {\n\treturn &standardResolver{\n\t\tAddr: addDefaultPort(addr),\n\t\tClient: &dns.Client{\n\t\t\tTimeout: timeoutOrDefault(timeout),\n\t\t},\n\t\tNext: next,\n\t}\n}\n\nfunc NewStandardResolverTCP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound {\n\treturn &standardResolver{\n\t\tAddr: addDefaultPort(addr),\n\t\tClient: &dns.Client{\n\t\t\tNet:     \"tcp\",\n\t\t\tTimeout: timeoutOrDefault(timeout),\n\t\t},\n\t\tNext: next,\n\t}\n}\n\nfunc NewStandardResolverTLS(addr string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound {\n\treturn &standardResolver{\n\t\tAddr: addDefaultPortTLS(addr),\n\t\tClient: &dns.Client{\n\t\t\tNet:     \"tcp-tls\",\n\t\t\tTimeout: timeoutOrDefault(timeout),\n\t\t\tTLSConfig: &tls.Config{\n\t\t\t\tServerName:         sni,\n\t\t\t\tInsecureSkipVerify: insecure,\n\t\t\t},\n\t\t},\n\t\tNext: next,\n\t}\n}\n\n// addDefaultPort adds the default DNS port (53) to the address if not present.\nfunc addDefaultPort(addr string) string {\n\tif _, _, err := net.SplitHostPort(addr); err != nil {\n\t\treturn net.JoinHostPort(addr, \"53\")\n\t}\n\treturn addr\n}\n\n// addDefaultPortTLS adds the default DNS-over-TLS port (853) to the address if not present.\nfunc addDefaultPortTLS(addr string) string {\n\tif _, _, err := net.SplitHostPort(addr); err != nil {\n\t\treturn net.JoinHostPort(addr, \"853\")\n\t}\n\treturn addr\n}\n\nfunc timeoutOrDefault(timeout time.Duration) time.Duration {\n\tif timeout == 0 {\n\t\treturn resolverDefaultTimeout\n\t}\n\treturn timeout\n}\n\n// skipCNAMEChain skips the CNAME chain and returns the last CNAME target.\n// Sometimes the DNS server returns a CNAME chain like this, in one packet:\n// domain1.com. CNAME domain2.com.\n// domain2.com. CNAME domain3.com.\n// In this case, we should avoid sending a query for domain2.com and go\n// straight to domain3.com.\nfunc (r *standardResolver) skipCNAMEChain(answers []dns.RR) string {\n\tvar lastCNAME string\n\tfor _, a := range answers {\n\t\tif cname, ok := a.(*dns.CNAME); ok {\n\t\t\tif lastCNAME == \"\" {\n\t\t\t\t// First CNAME\n\t\t\t\tlastCNAME = cname.Target\n\t\t\t} else if cname.Hdr.Name == lastCNAME {\n\t\t\t\t// CNAME chain\n\t\t\t\tlastCNAME = cname.Target\n\t\t\t} else {\n\t\t\t\t// CNAME chain ends\n\t\t\t\treturn lastCNAME\n\t\t\t}\n\t\t}\n\t}\n\treturn lastCNAME\n}\n\n// lookup4 resolves a hostname to an IPv4 address.\n// If there's no IPv4 address, it returns (nil, nil), no error.\nfunc (r *standardResolver) lookup4(host string) (net.IP, error) {\n\tm := new(dns.Msg)\n\tm.SetQuestion(dns.Fqdn(host), dns.TypeA)\n\tm.RecursionDesired = true\n\tresp, _, err := r.Client.Exchange(m, r.Addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(resp.Answer) == 0 {\n\t\treturn nil, nil\n\t}\n\t// Sometimes the DNS server returns both CNAME and A records in one packet.\n\thasCNAME := false\n\tfor _, a := range resp.Answer {\n\t\tif aa, ok := a.(*dns.A); ok {\n\t\t\treturn aa.A.To4(), nil\n\t\t} else if _, ok := a.(*dns.CNAME); ok {\n\t\t\thasCNAME = true\n\t\t}\n\t}\n\tif hasCNAME {\n\t\treturn r.lookup4(r.skipCNAMEChain(resp.Answer))\n\t} else {\n\t\t// Should not happen\n\t\treturn nil, nil\n\t}\n}\n\n// lookup6 resolves a hostname to an IPv6 address.\n// If there's no IPv6 address, it returns (nil, nil), no error.\nfunc (r *standardResolver) lookup6(host string) (net.IP, error) {\n\tm := new(dns.Msg)\n\tm.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)\n\tm.RecursionDesired = true\n\tresp, _, err := r.Client.Exchange(m, r.Addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(resp.Answer) == 0 {\n\t\treturn nil, nil\n\t}\n\t// Sometimes the DNS server returns both CNAME and AAAA records in one packet.\n\thasCNAME := false\n\tfor _, a := range resp.Answer {\n\t\tif aa, ok := a.(*dns.AAAA); ok {\n\t\t\treturn aa.AAAA.To16(), nil\n\t\t} else if _, ok := a.(*dns.CNAME); ok {\n\t\t\thasCNAME = true\n\t\t}\n\t}\n\tif hasCNAME {\n\t\treturn r.lookup6(r.skipCNAMEChain(resp.Answer))\n\t} else {\n\t\t// Should not happen\n\t\treturn nil, nil\n\t}\n}\n\nfunc (r *standardResolver) resolve(reqAddr *AddrEx) {\n\tif tryParseIP(reqAddr) {\n\t\t// The host is already an IP address, we don't need to resolve it.\n\t\treturn\n\t}\n\ttype lookupResult struct {\n\t\tip  net.IP\n\t\terr error\n\t}\n\tch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1)\n\tgo func() {\n\t\tvar ip net.IP\n\t\tvar err error\n\t\tfor i := 0; i < standardResolverRetryTimes; i++ {\n\t\t\tip, err = r.lookup4(reqAddr.Host)\n\t\t\tif err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tch4 <- lookupResult{ip, err}\n\t}()\n\tgo func() {\n\t\tvar ip net.IP\n\t\tvar err error\n\t\tfor i := 0; i < standardResolverRetryTimes; i++ {\n\t\t\tip, err = r.lookup6(reqAddr.Host)\n\t\t\tif err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tch6 <- lookupResult{ip, err}\n\t}()\n\tresult4, result6 := <-ch4, <-ch6\n\treqAddr.ResolveInfo = &ResolveInfo{\n\t\tIPv4: result4.ip,\n\t\tIPv6: result6.ip,\n\t}\n\tif result4.err != nil {\n\t\treqAddr.ResolveInfo.Err = result4.err\n\t} else if result6.err != nil {\n\t\treqAddr.ResolveInfo.Err = result6.err\n\t}\n}\n\nfunc (r *standardResolver) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tr.resolve(reqAddr)\n\treturn r.Next.TCP(reqAddr)\n}\n\nfunc (r *standardResolver) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\tr.resolve(reqAddr)\n\treturn r.Next.UDP(reqAddr)\n}\n"
  },
  {
    "path": "extras/outbounds/dns_system.go",
    "content": "package outbounds\n\nimport (\n\t\"net\"\n)\n\n// systemResolver is a PluggableOutbound DNS resolver that resolves hostnames\n// using the default system DNS server.\n// Outbounds typically don't require a resolver, as they can do DNS resolution\n// themselves. However, when using ACL, it's necessary to place a resolver in\n// front of it in the pipeline (for IP rules to work on domain requests).\ntype systemResolver struct {\n\tNext PluggableOutbound\n}\n\nfunc NewSystemResolver(next PluggableOutbound) PluggableOutbound {\n\treturn &systemResolver{\n\t\tNext: next,\n\t}\n}\n\nfunc (r *systemResolver) resolve(reqAddr *AddrEx) {\n\tips, err := net.LookupIP(reqAddr.Host)\n\tif err != nil {\n\t\treqAddr.ResolveInfo = &ResolveInfo{Err: err}\n\t\treturn\n\t}\n\tinfo := &ResolveInfo{}\n\tinfo.IPv4, info.IPv6 = splitIPv4IPv6(ips)\n\treqAddr.ResolveInfo = info\n}\n\nfunc (r *systemResolver) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tr.resolve(reqAddr)\n\treturn r.Next.TCP(reqAddr)\n}\n\nfunc (r *systemResolver) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\tr.resolve(reqAddr)\n\treturn r.Next.UDP(reqAddr)\n}\n"
  },
  {
    "path": "extras/outbounds/interface.go",
    "content": "package outbounds\n\nimport (\n\t\"net\"\n\t\"strconv\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\n// The PluggableOutbound system is designed to function in a chain-like manner.\n// Not every outbound is an actual outbound; some are just wrappers around other\n// outbounds, such as custom resolvers, ACL engine, etc. It is a pipeline where\n// each stage can check (and optionally modify) the request before passing it\n// on to the next stage. The last stage in the pipeline is always a real outbound\n// that actually implements the logic of connecting to the remote server.\n// There can also be instances of branching, where requests can be sent to\n// different outbound sub-pipelines based on some criteria.\n\n// PluggableOutbound differs from the built-in Outbound interface from Hysteria core\n// in that it uses an AddrEx struct for addresses instead of a string. Because of this\n// difference, we need a special PluggableOutboundAdapter to convert between the two\n// for use in Hysteria core config.\ntype PluggableOutbound interface {\n\tTCP(reqAddr *AddrEx) (net.Conn, error)\n\tUDP(reqAddr *AddrEx) (UDPConn, error)\n}\n\ntype UDPConn interface {\n\tReadFrom(b []byte) (int, *AddrEx, error)\n\tWriteTo(b []byte, addr *AddrEx) (int, error)\n\tClose() error\n}\n\n// AddrEx keeps both the original string representation of the address and\n// the resolved IP addresses from the resolver, if any.\n// The actual outbound implementations can choose to use either the string\n// representation or the resolved IP addresses, depending on their capabilities.\n// A SOCKS5 outbound, for example, should prefer the string representation\n// because SOCKS5 protocol supports sending the hostname to the proxy server\n// and let the proxy server do the DNS resolution.\ntype AddrEx struct {\n\tHost        string // String representation of the host, can be an IP or a domain name\n\tPort        uint16\n\tResolveInfo *ResolveInfo // Only set if there's a resolver in the pipeline\n}\n\nfunc (a *AddrEx) String() string {\n\treturn net.JoinHostPort(a.Host, strconv.Itoa(int(a.Port)))\n}\n\n// ResolveInfo contains the resolved IP addresses from the resolver, and any\n// error that occurred during the resolution.\n// Note that there could be no error but also no resolved IP addresses,\n// or there could be an error but also some resolved IP addresses.\n// It's up to the actual outbound implementation to decide how to handle\n// these cases.\ntype ResolveInfo struct {\n\tIPv4 net.IP\n\tIPv6 net.IP\n\tErr  error\n}\n\nvar _ server.Outbound = (*PluggableOutboundAdapter)(nil)\n\ntype PluggableOutboundAdapter struct {\n\tPluggableOutbound\n}\n\nfunc (a *PluggableOutboundAdapter) TCP(reqAddr string) (net.Conn, error) {\n\thost, port, err := net.SplitHostPort(reqAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tportInt, err := strconv.Atoi(port)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn a.PluggableOutbound.TCP(&AddrEx{\n\t\tHost: host,\n\t\tPort: uint16(portInt),\n\t})\n}\n\nfunc (a *PluggableOutboundAdapter) UDP(reqAddr string) (server.UDPConn, error) {\n\thost, port, err := net.SplitHostPort(reqAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tportInt, err := strconv.Atoi(port)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconn, err := a.PluggableOutbound.UDP(&AddrEx{\n\t\tHost: host,\n\t\tPort: uint16(portInt),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &udpConnAdapter{conn}, nil\n}\n\ntype udpConnAdapter struct {\n\tUDPConn\n}\n\nfunc (u *udpConnAdapter) ReadFrom(b []byte) (int, string, error) {\n\tn, addr, err := u.UDPConn.ReadFrom(b)\n\tif addr != nil {\n\t\treturn n, addr.String(), err\n\t} else {\n\t\treturn n, \"\", err\n\t}\n}\n\nfunc (u *udpConnAdapter) WriteTo(b []byte, addr string) (int, error) {\n\thost, port, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tportInt, err := strconv.Atoi(port)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn u.UDPConn.WriteTo(b, &AddrEx{\n\t\tHost: host,\n\t\tPort: uint16(portInt),\n\t})\n}\n\nfunc (u *udpConnAdapter) Close() error {\n\treturn u.UDPConn.Close()\n}\n"
  },
  {
    "path": "extras/outbounds/interface_test.go",
    "content": "package outbounds\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\nfunc TestPluggableOutboundAdapter(t *testing.T) {\n\tob := newMockPluggableOutbound(t)\n\tadapter := &PluggableOutboundAdapter{ob}\n\n\tob.EXPECT().TCP(&AddrEx{\n\t\tHost: \"only.fans\",\n\t\tPort: 443,\n\t}).Return(nil, nil).Once()\n\tconn, err := adapter.TCP(\"only.fans:443\")\n\tassert.Nil(t, conn)\n\tassert.Nil(t, err)\n\n\tmc := newMockUDPConn(t)\n\tmc.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, *AddrEx, error) {\n\t\treturn copy(bs, \"gura\"), &AddrEx{\n\t\t\tHost: \"gura.com\",\n\t\t\tPort: 2333,\n\t\t}, nil\n\t}).Once()\n\tmc.EXPECT().WriteTo([]byte(\"gawr\"), &AddrEx{\n\t\tHost: \"another.hololive.tv\",\n\t\tPort: 1551,\n\t}).Return(4, nil).Once()\n\tob.EXPECT().UDP(&AddrEx{\n\t\tHost: \"hololive.tv\",\n\t\tPort: 8999,\n\t}).Return(mc, nil).Once()\n\n\tuConn, err := adapter.UDP(\"hololive.tv:8999\")\n\tassert.Nil(t, err)\n\tassert.NotNil(t, uConn)\n\tn, err := uConn.WriteTo([]byte(\"gawr\"), \"another.hololive.tv:1551\")\n\tassert.Nil(t, err)\n\tassert.Equal(t, 4, n)\n\tbs := make([]byte, 1024)\n\tn, addr, err := uConn.ReadFrom(bs)\n\tassert.Nil(t, err)\n\tassert.Equal(t, 4, n)\n\tassert.Equal(t, \"gura\", string(bs[:n]))\n\tassert.Equal(t, \"gura.com:2333\", addr)\n}\n"
  },
  {
    "path": "extras/outbounds/mock_PluggableOutbound.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage outbounds\n\nimport (\n\tnet \"net\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// mockPluggableOutbound is an autogenerated mock type for the PluggableOutbound type\ntype mockPluggableOutbound struct {\n\tmock.Mock\n}\n\ntype mockPluggableOutbound_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *mockPluggableOutbound) EXPECT() *mockPluggableOutbound_Expecter {\n\treturn &mockPluggableOutbound_Expecter{mock: &_m.Mock}\n}\n\n// TCP provides a mock function with given fields: reqAddr\nfunc (_m *mockPluggableOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tret := _m.Called(reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for TCP\")\n\t}\n\n\tvar r0 net.Conn\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(*AddrEx) (net.Conn, error)); ok {\n\t\treturn rf(reqAddr)\n\t}\n\tif rf, ok := ret.Get(0).(func(*AddrEx) net.Conn); ok {\n\t\tr0 = rf(reqAddr)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(net.Conn)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(*AddrEx) error); ok {\n\t\tr1 = rf(reqAddr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockPluggableOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP'\ntype mockPluggableOutbound_TCP_Call struct {\n\t*mock.Call\n}\n\n// TCP is a helper method to define mock.On call\n//   - reqAddr *AddrEx\nfunc (_e *mockPluggableOutbound_Expecter) TCP(reqAddr interface{}) *mockPluggableOutbound_TCP_Call {\n\treturn &mockPluggableOutbound_TCP_Call{Call: _e.mock.On(\"TCP\", reqAddr)}\n}\n\nfunc (_c *mockPluggableOutbound_TCP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_TCP_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(*AddrEx))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockPluggableOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *mockPluggableOutbound_TCP_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *mockPluggableOutbound_TCP_Call) RunAndReturn(run func(*AddrEx) (net.Conn, error)) *mockPluggableOutbound_TCP_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// UDP provides a mock function with given fields: reqAddr\nfunc (_m *mockPluggableOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\tret := _m.Called(reqAddr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for UDP\")\n\t}\n\n\tvar r0 UDPConn\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(*AddrEx) (UDPConn, error)); ok {\n\t\treturn rf(reqAddr)\n\t}\n\tif rf, ok := ret.Get(0).(func(*AddrEx) UDPConn); ok {\n\t\tr0 = rf(reqAddr)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(UDPConn)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(*AddrEx) error); ok {\n\t\tr1 = rf(reqAddr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockPluggableOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'\ntype mockPluggableOutbound_UDP_Call struct {\n\t*mock.Call\n}\n\n// UDP is a helper method to define mock.On call\n//   - reqAddr *AddrEx\nfunc (_e *mockPluggableOutbound_Expecter) UDP(reqAddr interface{}) *mockPluggableOutbound_UDP_Call {\n\treturn &mockPluggableOutbound_UDP_Call{Call: _e.mock.On(\"UDP\", reqAddr)}\n}\n\nfunc (_c *mockPluggableOutbound_UDP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_UDP_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(*AddrEx))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockPluggableOutbound_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockPluggableOutbound_UDP_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *mockPluggableOutbound_UDP_Call) RunAndReturn(run func(*AddrEx) (UDPConn, error)) *mockPluggableOutbound_UDP_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc newMockPluggableOutbound(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *mockPluggableOutbound {\n\tmock := &mockPluggableOutbound{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "extras/outbounds/mock_UDPConn.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage outbounds\n\nimport mock \"github.com/stretchr/testify/mock\"\n\n// mockUDPConn is an autogenerated mock type for the UDPConn type\ntype mockUDPConn struct {\n\tmock.Mock\n}\n\ntype mockUDPConn_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter {\n\treturn &mockUDPConn_Expecter{mock: &_m.Mock}\n}\n\n// Close provides a mock function with given fields:\nfunc (_m *mockUDPConn) Close() error {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Close\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype mockUDPConn_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\nfunc (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call {\n\treturn &mockUDPConn_Close_Call{Call: _e.mock.On(\"Close\")}\n}\n\nfunc (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// ReadFrom provides a mock function with given fields: b\nfunc (_m *mockUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {\n\tret := _m.Called(b)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for ReadFrom\")\n\t}\n\n\tvar r0 int\n\tvar r1 *AddrEx\n\tvar r2 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, *AddrEx, error)); ok {\n\t\treturn rf(b)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(b)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) *AddrEx); ok {\n\t\tr1 = rf(b)\n\t} else {\n\t\tif ret.Get(1) != nil {\n\t\t\tr1 = ret.Get(1).(*AddrEx)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(2).(func([]byte) error); ok {\n\t\tr2 = rf(b)\n\t} else {\n\t\tr2 = ret.Error(2)\n\t}\n\n\treturn r0, r1, r2\n}\n\n// mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom'\ntype mockUDPConn_ReadFrom_Call struct {\n\t*mock.Call\n}\n\n// ReadFrom is a helper method to define mock.On call\n//   - b []byte\nfunc (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call {\n\treturn &mockUDPConn_ReadFrom_Call{Call: _e.mock.On(\"ReadFrom\", b)}\n}\n\nfunc (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 *AddrEx, _a2 error) *mockUDPConn_ReadFrom_Call {\n\t_c.Call.Return(_a0, _a1, _a2)\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, *AddrEx, error)) *mockUDPConn_ReadFrom_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// WriteTo provides a mock function with given fields: b, addr\nfunc (_m *mockUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {\n\tret := _m.Called(b, addr)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for WriteTo\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte, *AddrEx) (int, error)); ok {\n\t\treturn rf(b, addr)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte, *AddrEx) int); ok {\n\t\tr0 = rf(b, addr)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte, *AddrEx) error); ok {\n\t\tr1 = rf(b, addr)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo'\ntype mockUDPConn_WriteTo_Call struct {\n\t*mock.Call\n}\n\n// WriteTo is a helper method to define mock.On call\n//   - b []byte\n//   - addr *AddrEx\nfunc (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call {\n\treturn &mockUDPConn_WriteTo_Call{Call: _e.mock.On(\"WriteTo\", b, addr)}\n}\n\nfunc (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr *AddrEx)) *mockUDPConn_WriteTo_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte), args[1].(*AddrEx))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call {\n\t_c.Call.Return(_a0, _a1)\n\treturn _c\n}\n\nfunc (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, *AddrEx) (int, error)) *mockUDPConn_WriteTo_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc newMockUDPConn(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *mockUDPConn {\n\tmock := &mockUDPConn{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "extras/outbounds/ob_direct.go",
    "content": "package outbounds\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype DirectOutboundMode int\n\ntype udpConnState int\n\nconst (\n\tDirectOutboundModeAuto DirectOutboundMode = iota // Dual-stack \"happy eyeballs\"-like mode\n\tDirectOutboundMode64                             // Use IPv6 address when available, otherwise IPv4\n\tDirectOutboundMode46                             // Use IPv4 address when available, otherwise IPv6\n\tDirectOutboundMode6                              // Use IPv6 only, fail if not available\n\tDirectOutboundMode4                              // Use IPv4 only, fail if not available\n\n\tdefaultDialerTimeout = 10 * time.Second\n)\n\nconst (\n\tudpConnStateDualStack udpConnState = iota\n\tudpConnStateIPv4\n\tudpConnStateIPv6\n)\n\n// directOutbound is a PluggableOutbound that connects directly to the target\n// using the local network (as opposed to using a proxy, for example).\n// It prefers to use ResolveInfo in AddrEx if available. But if it's nil,\n// it will fall back to resolving Host using Go's built-in DNS resolver.\ntype directOutbound struct {\n\tMode DirectOutboundMode\n\n\t// Dialer4 and Dialer6 are used for IPv4 and IPv6 TCP connections respectively.\n\tDialer4 *net.Dialer\n\tDialer6 *net.Dialer\n\n\t// DeviceName & BindIPs are for UDP connections. They don't use dialers, so we\n\t// need to bind them when creating the connection.\n\tDeviceName string\n\tBindIP4    net.IP\n\tBindIP6    net.IP\n}\n\ntype noAddressError struct {\n\tIPv4 bool\n\tIPv6 bool\n}\n\nfunc (e noAddressError) Error() string {\n\tif e.IPv4 && e.IPv6 {\n\t\treturn \"no IPv4 or IPv6 address available\"\n\t} else if e.IPv4 {\n\t\treturn \"no IPv4 address available\"\n\t} else if e.IPv6 {\n\t\treturn \"no IPv6 address available\"\n\t} else {\n\t\treturn \"no address available\"\n\t}\n}\n\ntype invalidOutboundModeError struct{}\n\nfunc (e invalidOutboundModeError) Error() string {\n\treturn \"invalid outbound mode\"\n}\n\ntype resolveError struct {\n\tErr error\n}\n\nfunc (e resolveError) Error() string {\n\tif e.Err == nil {\n\t\treturn \"resolve error\"\n\t} else {\n\t\treturn \"resolve error: \" + e.Err.Error()\n\t}\n}\n\nfunc (e resolveError) Unwrap() error {\n\treturn e.Err\n}\n\n// NewDirectOutboundSimple creates a new directOutbound with the given mode,\n// without binding to a specific device. Works on all platforms.\nfunc NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound {\n\td := &net.Dialer{\n\t\tTimeout: defaultDialerTimeout,\n\t}\n\treturn &directOutbound{\n\t\tMode:    mode,\n\t\tDialer4: d,\n\t\tDialer6: d,\n\t}\n}\n\n// NewDirectOutboundBindToIPs creates a new directOutbound with the given mode,\n// and binds to the given IPv4 and IPv6 addresses. Either or both of the addresses\n// can be nil, in which case the directOutbound will not bind to a specific address\n// for that family.\nfunc NewDirectOutboundBindToIPs(mode DirectOutboundMode, bindIP4, bindIP6 net.IP) (PluggableOutbound, error) {\n\tif bindIP4 != nil && bindIP4.To4() == nil {\n\t\treturn nil, errors.New(\"bindIP4 must be an IPv4 address\")\n\t}\n\tif bindIP6 != nil && bindIP6.To4() != nil {\n\t\treturn nil, errors.New(\"bindIP6 must be an IPv6 address\")\n\t}\n\tob := &directOutbound{\n\t\tMode: mode,\n\t\tDialer4: &net.Dialer{\n\t\t\tTimeout: defaultDialerTimeout,\n\t\t},\n\t\tDialer6: &net.Dialer{\n\t\t\tTimeout: defaultDialerTimeout,\n\t\t},\n\t\tBindIP4: bindIP4,\n\t\tBindIP6: bindIP6,\n\t}\n\tif bindIP4 != nil {\n\t\tob.Dialer4.LocalAddr = &net.TCPAddr{\n\t\t\tIP: bindIP4,\n\t\t}\n\t}\n\tif bindIP6 != nil {\n\t\tob.Dialer6.LocalAddr = &net.TCPAddr{\n\t\t\tIP: bindIP6,\n\t\t}\n\t}\n\treturn ob, nil\n}\n\n// resolve is our built-in DNS resolver for handling the case when\n// AddrEx.ResolveInfo is nil.\nfunc (d *directOutbound) resolve(reqAddr *AddrEx) {\n\tips, err := net.LookupIP(reqAddr.Host)\n\tif err != nil {\n\t\treqAddr.ResolveInfo = &ResolveInfo{Err: err}\n\t\treturn\n\t}\n\tr := &ResolveInfo{}\n\tr.IPv4, r.IPv6 = splitIPv4IPv6(ips)\n\tif r.IPv4 == nil && r.IPv6 == nil {\n\t\tr.Err = noAddressError{IPv4: true, IPv6: true}\n\t}\n\treqAddr.ResolveInfo = r\n}\n\nfunc (d *directOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tif reqAddr.ResolveInfo == nil {\n\t\t// AddrEx.ResolveInfo is nil (no resolver in the pipeline),\n\t\t// we need to resolve the address ourselves.\n\t\td.resolve(reqAddr)\n\t}\n\tr := reqAddr.ResolveInfo\n\tif r.IPv4 == nil && r.IPv6 == nil {\n\t\t// ResolveInfo not nil but no address available,\n\t\t// this can only mean that the resolver failed.\n\t\t// Return the error from the resolver.\n\t\treturn nil, resolveError{Err: r.Err}\n\t}\n\tswitch d.Mode {\n\tcase DirectOutboundModeAuto:\n\t\tif r.IPv4 != nil && r.IPv6 != nil {\n\t\t\treturn d.dualStackDialTCP(r.IPv4, r.IPv6, reqAddr.Port)\n\t\t} else if r.IPv4 != nil {\n\t\t\treturn d.dialTCP(r.IPv4, reqAddr.Port)\n\t\t} else {\n\t\t\treturn d.dialTCP(r.IPv6, reqAddr.Port)\n\t\t}\n\tcase DirectOutboundMode64:\n\t\tif r.IPv6 != nil {\n\t\t\treturn d.dialTCP(r.IPv6, reqAddr.Port)\n\t\t} else {\n\t\t\treturn d.dialTCP(r.IPv4, reqAddr.Port)\n\t\t}\n\tcase DirectOutboundMode46:\n\t\tif r.IPv4 != nil {\n\t\t\treturn d.dialTCP(r.IPv4, reqAddr.Port)\n\t\t} else {\n\t\t\treturn d.dialTCP(r.IPv6, reqAddr.Port)\n\t\t}\n\tcase DirectOutboundMode6:\n\t\tif r.IPv6 != nil {\n\t\t\treturn d.dialTCP(r.IPv6, reqAddr.Port)\n\t\t} else {\n\t\t\treturn nil, noAddressError{IPv6: true}\n\t\t}\n\tcase DirectOutboundMode4:\n\t\tif r.IPv4 != nil {\n\t\t\treturn d.dialTCP(r.IPv4, reqAddr.Port)\n\t\t} else {\n\t\t\treturn nil, noAddressError{IPv4: true}\n\t\t}\n\tdefault:\n\t\treturn nil, invalidOutboundModeError{}\n\t}\n}\n\nfunc (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) {\n\tif ip.To4() != nil {\n\t\treturn d.Dialer4.Dial(\"tcp4\", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))\n\t} else {\n\t\treturn d.Dialer6.Dial(\"tcp6\", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))\n\t}\n}\n\ntype dialResult struct {\n\tConn net.Conn\n\tErr  error\n}\n\n// dualStackDialTCP dials the target using both IPv4 and IPv6 addresses simultaneously.\n// It returns the first successful connection and drops the other one.\n// If both connections fail, it returns the last error.\nfunc (d *directOutbound) dualStackDialTCP(ipv4, ipv6 net.IP, port uint16) (net.Conn, error) {\n\tch := make(chan dialResult, 2)\n\tgo func() {\n\t\tconn, err := d.dialTCP(ipv4, port)\n\t\tch <- dialResult{Conn: conn, Err: err}\n\t}()\n\tgo func() {\n\t\tconn, err := d.dialTCP(ipv6, port)\n\t\tch <- dialResult{Conn: conn, Err: err}\n\t}()\n\t// Get the first result, check if it's successful\n\tif r := <-ch; r.Err == nil {\n\t\t// Yes. Return this and close the other connection when it's done\n\t\tgo func() {\n\t\t\tr2 := <-ch\n\t\t\tif r2.Conn != nil {\n\t\t\t\t_ = r2.Conn.Close()\n\t\t\t}\n\t\t}()\n\t\treturn r.Conn, nil\n\t} else {\n\t\t// No. Return the other result, which may or may not be successful\n\t\tr2 := <-ch\n\t\treturn r2.Conn, r2.Err\n\t}\n}\n\ntype directOutboundUDPConn struct {\n\t*directOutbound\n\t*net.UDPConn\n\tState udpConnState\n}\n\nfunc (u *directOutboundUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {\n\tn, addr, err := u.UDPConn.ReadFromUDP(b)\n\tif addr != nil {\n\t\treturn n, &AddrEx{\n\t\t\tHost: addr.IP.String(),\n\t\t\tPort: uint16(addr.Port),\n\t\t}, err\n\t} else {\n\t\treturn n, nil, err\n\t}\n}\n\nfunc (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {\n\tif addr.ResolveInfo == nil {\n\t\tu.directOutbound.resolve(addr)\n\t}\n\tr := addr.ResolveInfo\n\tif r.IPv4 == nil && r.IPv6 == nil {\n\t\treturn 0, resolveError{Err: r.Err}\n\t}\n\tif u.State == udpConnStateIPv4 {\n\t\tif r.IPv4 != nil {\n\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\tIP:   r.IPv4,\n\t\t\t\tPort: int(addr.Port),\n\t\t\t})\n\t\t} else {\n\t\t\treturn 0, noAddressError{IPv4: true}\n\t\t}\n\t} else if u.State == udpConnStateIPv6 {\n\t\tif r.IPv6 != nil {\n\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\tIP:   r.IPv6,\n\t\t\t\tPort: int(addr.Port),\n\t\t\t})\n\t\t} else {\n\t\t\treturn 0, noAddressError{IPv6: true}\n\t\t}\n\t} else {\n\t\t// Dual stack\n\t\tswitch u.directOutbound.Mode {\n\t\tcase DirectOutboundModeAuto:\n\t\t\t// This is a special case.\n\t\t\t// We must make a decision here, so we prefer IPv4 for maximum compatibility.\n\t\t\tif r.IPv4 != nil {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv4,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv6,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t}\n\t\tcase DirectOutboundMode64:\n\t\t\tif r.IPv6 != nil {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv6,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv4,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t}\n\t\tcase DirectOutboundMode46:\n\t\t\tif r.IPv4 != nil {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv4,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv6,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t}\n\t\tcase DirectOutboundMode6:\n\t\t\tif r.IPv6 != nil {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv6,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\treturn 0, noAddressError{IPv6: true}\n\t\t\t}\n\t\tcase DirectOutboundMode4:\n\t\t\tif r.IPv4 != nil {\n\t\t\t\treturn u.UDPConn.WriteToUDP(b, &net.UDPAddr{\n\t\t\t\t\tIP:   r.IPv4,\n\t\t\t\t\tPort: int(addr.Port),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\treturn 0, noAddressError{IPv4: true}\n\t\t\t}\n\t\tdefault:\n\t\t\treturn 0, invalidOutboundModeError{}\n\t\t}\n\t}\n}\n\nfunc (u *directOutboundUDPConn) Close() error {\n\treturn u.UDPConn.Close()\n}\n\nfunc (d *directOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\tif d.BindIP4 == nil && d.BindIP6 == nil {\n\t\t// No bind address specified, use default dual stack implementation\n\t\tc, err := net.ListenUDP(\"udp\", nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif d.DeviceName != \"\" {\n\t\t\tif err := udpConnBindToDevice(c, d.DeviceName); err != nil {\n\t\t\t\t// Don't forget to close the UDPConn if binding fails\n\t\t\t\t_ = c.Close()\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn &directOutboundUDPConn{\n\t\t\tdirectOutbound: d,\n\t\t\tUDPConn:        c,\n\t\t\tState:          udpConnStateDualStack,\n\t\t}, nil\n\t} else {\n\t\t// Bind address specified,\n\t\t// need to check what kind of address is in reqAddr\n\t\t// to determine which address family to bind to\n\t\tif reqAddr.ResolveInfo == nil {\n\t\t\td.resolve(reqAddr)\n\t\t}\n\t\tr := reqAddr.ResolveInfo\n\t\tif r.IPv4 == nil && r.IPv6 == nil {\n\t\t\treturn nil, resolveError{Err: r.Err}\n\t\t}\n\t\tvar bindIP net.IP      // can be nil, in which case we still lock the address family but don't bind to any address\n\t\tvar state udpConnState // either IPv4 or IPv6\n\t\tswitch d.Mode {\n\t\tcase DirectOutboundModeAuto:\n\t\t\t// This is a special case.\n\t\t\t// We must make a decision here, so we prefer IPv4 for maximum compatibility.\n\t\t\tif r.IPv4 != nil {\n\t\t\t\tbindIP = d.BindIP4\n\t\t\t\tstate = udpConnStateIPv4\n\t\t\t} else {\n\t\t\t\tbindIP = d.BindIP6\n\t\t\t\tstate = udpConnStateIPv6\n\t\t\t}\n\t\tcase DirectOutboundMode64:\n\t\t\tif r.IPv6 != nil {\n\t\t\t\tbindIP = d.BindIP6\n\t\t\t\tstate = udpConnStateIPv6\n\t\t\t} else {\n\t\t\t\tbindIP = d.BindIP4\n\t\t\t\tstate = udpConnStateIPv4\n\t\t\t}\n\t\tcase DirectOutboundMode46:\n\t\t\tif r.IPv4 != nil {\n\t\t\t\tbindIP = d.BindIP4\n\t\t\t\tstate = udpConnStateIPv4\n\t\t\t} else {\n\t\t\t\tbindIP = d.BindIP6\n\t\t\t\tstate = udpConnStateIPv6\n\t\t\t}\n\t\tcase DirectOutboundMode6:\n\t\t\tif r.IPv6 != nil {\n\t\t\t\tbindIP = d.BindIP6\n\t\t\t\tstate = udpConnStateIPv6\n\t\t\t} else {\n\t\t\t\treturn nil, noAddressError{IPv6: true}\n\t\t\t}\n\t\tcase DirectOutboundMode4:\n\t\t\tif r.IPv4 != nil {\n\t\t\t\tbindIP = d.BindIP4\n\t\t\t\tstate = udpConnStateIPv4\n\t\t\t} else {\n\t\t\t\treturn nil, noAddressError{IPv4: true}\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, invalidOutboundModeError{}\n\t\t}\n\t\tvar network string\n\t\tvar c *net.UDPConn\n\t\tvar err error\n\t\tif state == udpConnStateIPv4 {\n\t\t\tnetwork = \"udp4\"\n\t\t} else {\n\t\t\tnetwork = \"udp6\"\n\t\t}\n\t\tif bindIP != nil {\n\t\t\tc, err = net.ListenUDP(network, &net.UDPAddr{\n\t\t\t\tIP: bindIP,\n\t\t\t})\n\t\t} else {\n\t\t\tc, err = net.ListenUDP(network, nil)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// We don't support binding to both device & address at the same time,\n\t\t// so d.DeviceName is ignored in this case.\n\t\treturn &directOutboundUDPConn{\n\t\t\tdirectOutbound: d,\n\t\t\tUDPConn:        c,\n\t\t\tState:          state,\n\t\t}, nil\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/ob_direct_linux.go",
    "content": "package outbounds\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"syscall\"\n)\n\n// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode,\n// and binds to the given device. Only works on Linux.\nfunc NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) {\n\tif err := verifyDeviceName(deviceName); err != nil {\n\t\treturn nil, err\n\t}\n\td := &net.Dialer{\n\t\tTimeout: defaultDialerTimeout,\n\t\tControl: func(network, address string, c syscall.RawConn) error {\n\t\t\tvar errBind error\n\t\t\terr := c.Control(func(fd uintptr) {\n\t\t\t\terrBind = syscall.BindToDevice(int(fd), deviceName)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn errBind\n\t\t},\n\t}\n\treturn &directOutbound{\n\t\tMode:       mode,\n\t\tDialer4:    d,\n\t\tDialer6:    d,\n\t\tDeviceName: deviceName,\n\t}, nil\n}\n\nfunc verifyDeviceName(deviceName string) error {\n\tif deviceName == \"\" {\n\t\treturn errors.New(\"device name cannot be empty\")\n\t}\n\t_, err := net.InterfaceByName(deviceName)\n\treturn err\n}\n\nfunc udpConnBindToDevice(conn *net.UDPConn, deviceName string) error {\n\tsc, err := conn.SyscallConn()\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar errBind error\n\terr = sc.Control(func(fd uintptr) {\n\t\terrBind = syscall.BindToDevice(int(fd), deviceName)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn errBind\n}\n"
  },
  {
    "path": "extras/outbounds/ob_direct_others.go",
    "content": "//go:build !linux\n\npackage outbounds\n\nimport (\n\t\"errors\"\n\t\"net\"\n)\n\n// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode,\n// and binds to the given device. This doesn't work on non-Linux platforms, so this\n// is just a stub function that always returns an error.\nfunc NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) {\n\treturn nil, errors.New(\"binding to device is not supported on this platform\")\n}\n\nfunc udpConnBindToDevice(conn *net.UDPConn, deviceName string) error {\n\treturn errors.New(\"binding to device is not supported on this platform\")\n}\n"
  },
  {
    "path": "extras/outbounds/ob_http.go",
    "content": "package outbounds\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n)\n\nconst (\n\thttpRequestTimeout = 10 * time.Second\n)\n\nvar (\n\terrHTTPUDPNotSupported   = errors.New(\"UDP not supported by HTTP proxy\")\n\terrHTTPUnsupportedScheme = errors.New(\"unsupported scheme for HTTP proxy (use http:// or https://)\")\n)\n\ntype errHTTPRequestFailed struct {\n\tStatus int\n}\n\nfunc (e errHTTPRequestFailed) Error() string {\n\treturn fmt.Sprintf(\"HTTP request failed: %d\", e.Status)\n}\n\n// httpOutbound is a PluggableOutbound that connects to the target using\n// an HTTP/HTTPS proxy server (that supports the CONNECT method).\n// HTTP proxies don't support UDP by design, so this outbound will reject\n// any UDP request with errHTTPUDPNotSupported.\n// Since HTTP proxies support using either IP or domain name as the target\n// address, it will ignore ResolveInfo in AddrEx and always only use Host.\ntype httpOutbound struct {\n\tDialer     *net.Dialer\n\tAddr       string\n\tHTTPS      bool\n\tInsecure   bool\n\tServerName string\n\tBasicAuth  string // This is after Base64 encoding\n}\n\nfunc NewHTTPOutbound(proxyURL string, insecure bool) (PluggableOutbound, error) {\n\tu, err := url.Parse(proxyURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn nil, errHTTPUnsupportedScheme\n\t}\n\taddr := u.Host\n\tif u.Port() == \"\" {\n\t\tif u.Scheme == \"http\" {\n\t\t\taddr = net.JoinHostPort(u.Host, \"80\")\n\t\t} else {\n\t\t\taddr = net.JoinHostPort(u.Host, \"443\")\n\t\t}\n\t}\n\tvar basicAuth string\n\tif u.User != nil {\n\t\tusername := u.User.Username()\n\t\tpassword, _ := u.User.Password()\n\t\tbasicAuth = \"Basic \" + base64.StdEncoding.EncodeToString([]byte(username+\":\"+password))\n\t}\n\treturn &httpOutbound{\n\t\tDialer:     &net.Dialer{Timeout: defaultDialerTimeout},\n\t\tAddr:       addr,\n\t\tHTTPS:      u.Scheme == \"https\",\n\t\tInsecure:   insecure,\n\t\tServerName: u.Hostname(),\n\t\tBasicAuth:  basicAuth,\n\t}, nil\n}\n\nfunc (o *httpOutbound) dial() (net.Conn, error) {\n\tconn, err := o.Dialer.Dial(\"tcp\", o.Addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif o.HTTPS {\n\t\t// Wrap the connection with TLS if the proxy is HTTPS.\n\t\tconn = tls.Client(conn, &tls.Config{\n\t\t\tInsecureSkipVerify: o.Insecure,\n\t\t\tServerName:         o.Addr,\n\t\t})\n\t}\n\treturn conn, nil\n}\n\nfunc (o *httpOutbound) addrExToRequest(reqAddr *AddrEx) (*http.Request, error) {\n\treq := &http.Request{\n\t\tMethod: http.MethodConnect,\n\t\tURL: &url.URL{\n\t\t\tHost: net.JoinHostPort(reqAddr.Host, strconv.Itoa(int(reqAddr.Port))),\n\t\t},\n\t\tHeader: http.Header{\n\t\t\t\"Proxy-Connection\": []string{\"Keep-Alive\"},\n\t\t},\n\t}\n\tif o.BasicAuth != \"\" {\n\t\treq.Header.Add(\"Proxy-Authorization\", o.BasicAuth)\n\t}\n\treturn req, nil\n}\n\nfunc (o *httpOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\treq, err := o.addrExToRequest(reqAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconn, err := o.dial()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := req.Write(conn); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\tif err := conn.SetDeadline(time.Now().Add(httpRequestTimeout)); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\tbufReader := bufio.NewReader(conn)\n\tresp, err := http.ReadResponse(bufReader, req)\n\tif resp != nil {\n\t\t// Don't need response body here.\n\t\t_ = resp.Body.Close()\n\t}\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\t_ = conn.Close()\n\t\treturn nil, errHTTPRequestFailed{resp.StatusCode}\n\t}\n\tif err := conn.SetDeadline(time.Time{}); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\tif bufReader.Buffered() > 0 {\n\t\t// There is still data in the buffered reader.\n\t\t// We need to get it out and put it into a cachedConn,\n\t\t// so that handleConnect can read it.\n\t\tdata := make([]byte, bufReader.Buffered())\n\t\t_, err := io.ReadFull(bufReader, data)\n\t\tif err != nil {\n\t\t\t// Read from buffer failed, is this possible?\n\t\t\t_ = conn.Close()\n\t\t\treturn nil, err\n\t\t}\n\t\tcachedConn := &cachedConn{\n\t\t\tConn:   conn,\n\t\t\tBuffer: *bytes.NewBuffer(data),\n\t\t}\n\t\treturn cachedConn, nil\n\t} else {\n\t\treturn conn, nil\n\t}\n}\n\nfunc (o *httpOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\treturn nil, errHTTPUDPNotSupported\n}\n\n// cachedConn is a net.Conn wrapper that first Read()s from a buffer,\n// and then from the underlying net.Conn when the buffer is drained.\ntype cachedConn struct {\n\tnet.Conn\n\tBuffer bytes.Buffer\n}\n\nfunc (c *cachedConn) Read(b []byte) (int, error) {\n\tif c.Buffer.Len() > 0 {\n\t\tn, err := c.Buffer.Read(b)\n\t\tif err == io.EOF {\n\t\t\t// Buffer is drained, hide it from the caller\n\t\t\terr = nil\n\t\t}\n\t\treturn n, err\n\t}\n\treturn c.Conn.Read(b)\n}\n"
  },
  {
    "path": "extras/outbounds/ob_socks5.go",
    "content": "package outbounds\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/txthinking/socks5\"\n)\n\nconst (\n\tsocks5NegotiationTimeout = 10 * time.Second\n\tsocks5RequestTimeout     = 10 * time.Second\n)\n\nvar errSOCKS5AuthFailed = errors.New(\"SOCKS5 authentication failed\")\n\ntype errSOCKS5UnsupportedAuthMethod struct {\n\tMethod byte\n}\n\nfunc (e errSOCKS5UnsupportedAuthMethod) Error() string {\n\treturn fmt.Sprintf(\"unsupported SOCKS5 authentication method: %d\", e.Method)\n}\n\ntype errSOCKS5RequestFailed struct {\n\tRep byte\n}\n\nfunc (e errSOCKS5RequestFailed) Error() string {\n\tvar msg string\n\t// RFC 1928\n\tswitch e.Rep {\n\tcase 0x00:\n\t\tmsg = \"succeeded\"\n\tcase 0x01:\n\t\tmsg = \"general SOCKS server failure\"\n\tcase 0x02:\n\t\tmsg = \"connection not allowed by ruleset\"\n\tcase 0x03:\n\t\tmsg = \"Network unreachable\"\n\tcase 0x04:\n\t\tmsg = \"Host unreachable\"\n\tcase 0x05:\n\t\tmsg = \"Connection refused\"\n\tcase 0x06:\n\t\tmsg = \"TTL expired\"\n\tcase 0x07:\n\t\tmsg = \"Command not supported\"\n\tcase 0x08:\n\t\tmsg = \"Address type not supported\"\n\tdefault:\n\t\tmsg = \"undefined\"\n\t}\n\treturn fmt.Sprintf(\"SOCKS5 request failed: %s (%d)\", msg, e.Rep)\n}\n\n// socks5Outbound is a PluggableOutbound that connects to the target using\n// a SOCKS5 proxy server.\n// Since SOCKS5 supports using either IP or domain name as the target address,\n// it will ignore ResolveInfo in AddrEx and always only use Host.\ntype socks5Outbound struct {\n\tDialer   *net.Dialer\n\tAddr     string\n\tUsername string\n\tPassword string\n}\n\nfunc NewSOCKS5Outbound(addr, username, password string) PluggableOutbound {\n\treturn &socks5Outbound{\n\t\tDialer: &net.Dialer{\n\t\t\tTimeout: defaultDialerTimeout,\n\t\t},\n\t\tAddr:     addr,\n\t\tUsername: username,\n\t\tPassword: password,\n\t}\n}\n\n// dialAndNegotiate creates a new TCP connection to the SOCKS5 proxy server\n// and performs the negotiation. Returns an established connection ready to\n// handle requests, or an error if the process fails.\nfunc (o *socks5Outbound) dialAndNegotiate() (net.Conn, error) {\n\tconn, err := o.Dialer.Dial(\"tcp\", o.Addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := conn.SetDeadline(time.Now().Add(socks5NegotiationTimeout)); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\tauthMethods := []byte{socks5.MethodNone}\n\tif o.Username != \"\" && o.Password != \"\" {\n\t\tauthMethods = append(authMethods, socks5.MethodUsernamePassword)\n\t}\n\treq := socks5.NewNegotiationRequest(authMethods)\n\tif _, err := req.WriteTo(conn); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\tresp, err := socks5.NewNegotiationReplyFrom(conn)\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\tif resp.Method == socks5.MethodUsernamePassword {\n\t\tupReq := socks5.NewUserPassNegotiationRequest([]byte(o.Username), []byte(o.Password))\n\t\tif _, err := upReq.WriteTo(conn); err != nil {\n\t\t\t_ = conn.Close()\n\t\t\treturn nil, err\n\t\t}\n\t\tupResp, err := socks5.NewUserPassNegotiationReplyFrom(conn)\n\t\tif err != nil {\n\t\t\t_ = conn.Close()\n\t\t\treturn nil, err\n\t\t}\n\t\tif upResp.Status != socks5.UserPassStatusSuccess {\n\t\t\t_ = conn.Close()\n\t\t\treturn nil, errSOCKS5AuthFailed\n\t\t}\n\t} else if resp.Method != socks5.MethodNone {\n\t\t// We only support none & username/password authentication methods.\n\t\t_ = conn.Close()\n\t\treturn nil, errSOCKS5UnsupportedAuthMethod{resp.Method}\n\t}\n\t// Negotiation succeeded, reset the deadline.\n\tif err := conn.SetDeadline(time.Time{}); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\treturn conn, nil\n}\n\n// request sends a SOCKS5 request to the proxy server and returns the reply.\n// Note that it will return an error if the reply from the server indicates\n// a failure.\nfunc (o *socks5Outbound) request(conn net.Conn, req *socks5.Request) (*socks5.Reply, error) {\n\tif err := conn.SetDeadline(time.Now().Add(socks5RequestTimeout)); err != nil {\n\t\treturn nil, err\n\t}\n\tif _, err := req.WriteTo(conn); err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := socks5.NewReplyFrom(conn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif resp.Rep != socks5.RepSuccess {\n\t\treturn nil, errSOCKS5RequestFailed{resp.Rep}\n\t}\n\tif err := conn.SetDeadline(time.Time{}); err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (s *socks5Outbound) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tconn, err := s.dialAndNegotiate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tatyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr)\n\treq := socks5.NewRequest(socks5.CmdConnect, atyp, dstAddr, dstPort)\n\tif _, err := s.request(conn, req); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\treturn conn, nil\n}\n\nfunc (s *socks5Outbound) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\tconn, err := s.dialAndNegotiate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tatyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr)\n\treq := socks5.NewRequest(socks5.CmdUDP, atyp, dstAddr, dstPort)\n\tresp, err := s.request(conn, req)\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\treturn newSOCKS5UDPConn(conn, resp.Address())\n}\n\ntype socks5UDPConn struct {\n\ttcpConn net.Conn\n\tudpConn net.Conn\n}\n\nfunc newSOCKS5UDPConn(tcpConn net.Conn, udpAddr string) (*socks5UDPConn, error) {\n\tudpConn, err := net.Dial(\"udp\", udpAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsc := &socks5UDPConn{\n\t\ttcpConn: tcpConn,\n\t\tudpConn: udpConn,\n\t}\n\tgo sc.hold()\n\treturn sc, nil\n}\n\nfunc (c *socks5UDPConn) hold() {\n\t_, _ = io.Copy(io.Discard, c.tcpConn)\n\t_ = c.tcpConn.Close()\n\t_ = c.udpConn.Close()\n}\n\nfunc (c *socks5UDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {\n\tn, err := c.udpConn.Read(b)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\td, err := socks5.NewDatagramFromBytes(b[:n])\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\taddr := socks5AddrToAddrEx(d.Atyp, d.DstAddr, d.DstPort)\n\tn = copy(b, d.Data)\n\treturn n, addr, nil\n}\n\nfunc (c *socks5UDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {\n\tatyp, dstAddr, dstPort := addrExToSOCKS5Addr(addr)\n\td := socks5.NewDatagram(atyp, dstAddr, dstPort, b)\n\t_, err := c.udpConn.Write(d.Bytes())\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(b), nil\n}\n\nfunc (c *socks5UDPConn) Close() error {\n\t_ = c.tcpConn.Close()\n\t_ = c.udpConn.Close()\n\treturn nil\n}\n\nfunc addrExToSOCKS5Addr(addr *AddrEx) (atyp byte, dstAddr, dstPort []byte) {\n\t// Host\n\tip := net.ParseIP(addr.Host)\n\tif ip != nil {\n\t\tif ip.To4() != nil {\n\t\t\tatyp = socks5.ATYPIPv4\n\t\t\tdstAddr = ip.To4()\n\t\t} else {\n\t\t\tatyp = socks5.ATYPIPv6\n\t\t\tdstAddr = ip.To16()\n\t\t}\n\t} else {\n\t\tatyp = socks5.ATYPDomain\n\t\tdstAddr = []byte(addr.Host)\n\t}\n\t// Port\n\tdstPort = make([]byte, 2)\n\tbinary.BigEndian.PutUint16(dstPort, addr.Port)\n\treturn\n}\n\nfunc socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx {\n\t// Host\n\tvar host string\n\tif atyp == socks5.ATYPIPv4 {\n\t\thost = net.IP(dstAddr).To4().String()\n\t} else if atyp == socks5.ATYPIPv6 {\n\t\thost = net.IP(dstAddr).To16().String()\n\t} else if atyp == socks5.ATYPDomain {\n\t\t// Need to strip the first byte which is the domain length.\n\t\thost = string(dstAddr[1:])\n\t}\n\t// Port\n\tport := binary.BigEndian.Uint16(dstPort)\n\treturn &AddrEx{\n\t\tHost: host,\n\t\tPort: port,\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/speedtest/client.go",
    "content": "package speedtest\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\ntype Client struct {\n\tConn net.Conn\n}\n\n// Download requests the server to send l bytes of data.\n// The callback function cb is called every second with the time since the last call,\n// and the number of bytes received in that time.\nfunc (c *Client) Download(l uint32, cb func(time.Duration, uint32, bool)) error {\n\terr := writeDownloadRequest(c.Conn, l)\n\tif err != nil {\n\t\treturn err\n\t}\n\tok, msg, err := readDownloadResponse(c.Conn)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !ok {\n\t\treturn fmt.Errorf(\"server rejected download request: %s\", msg)\n\t}\n\tvar counter uint32\n\tstopChan := make(chan struct{})\n\tdefer close(stopChan)\n\t// Call the callback function every second,\n\t// with the time since the last call and the number of bytes received in that time.\n\tgo func() {\n\t\tticker := time.NewTicker(time.Second)\n\t\tdefer ticker.Stop()\n\t\tt := time.Now()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-stopChan:\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tcb(time.Since(t), atomic.SwapUint32(&counter, 0), false)\n\t\t\t\tt = time.Now()\n\t\t\t}\n\t\t}\n\t}()\n\tbuf := make([]byte, chunkSize)\n\tstartTime := time.Now()\n\tremaining := l\n\tfor remaining > 0 {\n\t\tn := remaining\n\t\tif n > chunkSize {\n\t\t\tn = chunkSize\n\t\t}\n\t\trn, err := c.Conn.Read(buf[:n])\n\t\tremaining -= uint32(rn)\n\t\tatomic.AddUint32(&counter, uint32(rn))\n\t\tif err != nil && !(remaining == 0 && err == io.EOF) {\n\t\t\treturn err\n\t\t}\n\t}\n\t// One last call to the callback function to report the total time and bytes received.\n\tcb(time.Since(startTime), l, true)\n\treturn nil\n}\n\n// Upload requests the server to receive l bytes of data.\n// The callback function cb is called every second with the time since the last call,\n// and the number of bytes sent in that time.\nfunc (c *Client) Upload(l uint32, cb func(time.Duration, uint32, bool)) error {\n\terr := writeUploadRequest(c.Conn, l)\n\tif err != nil {\n\t\treturn err\n\t}\n\tok, msg, err := readUploadResponse(c.Conn)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !ok {\n\t\treturn fmt.Errorf(\"server rejected upload request: %s\", msg)\n\t}\n\tvar counter uint32\n\tstopChan := make(chan struct{})\n\tdefer close(stopChan)\n\t// Call the callback function every second,\n\t// with the time since the last call and the number of bytes sent in that time.\n\tgo func() {\n\t\tticker := time.NewTicker(time.Second)\n\t\tdefer ticker.Stop()\n\t\tt := time.Now()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-stopChan:\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tcb(time.Since(t), atomic.SwapUint32(&counter, 0), false)\n\t\t\t\tt = time.Now()\n\t\t\t}\n\t\t}\n\t}()\n\tbuf := make([]byte, chunkSize)\n\tremaining := l\n\tfor remaining > 0 {\n\t\tn := remaining\n\t\tif n > chunkSize {\n\t\t\tn = chunkSize\n\t\t}\n\t\t_, err := c.Conn.Write(buf[:n])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tremaining -= n\n\t\tatomic.AddUint32(&counter, n)\n\t}\n\t// Now we should receive the upload summary from the server.\n\telapsed, received, err := readUploadSummary(c.Conn)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// One last call to the callback function to report the total time and bytes sent.\n\tcb(elapsed, received, true)\n\treturn nil\n}\n"
  },
  {
    "path": "extras/outbounds/speedtest/protocol.go",
    "content": "package speedtest\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\t\"time\"\n)\n\nconst (\n\ttypeDownload = 0x1\n\ttypeUpload   = 0x2\n)\n\n// DownloadRequest format:\n// 0x1 (byte)\n// Request data length (uint32 BE)\n\nfunc readDownloadRequest(r io.Reader) (uint32, error) {\n\tvar l uint32\n\terr := binary.Read(r, binary.BigEndian, &l)\n\treturn l, err\n}\n\nfunc writeDownloadRequest(w io.Writer, l uint32) error {\n\tbuf := make([]byte, 5)\n\tbuf[0] = typeDownload\n\tbinary.BigEndian.PutUint32(buf[1:], l)\n\t_, err := w.Write(buf)\n\treturn err\n}\n\n// DownloadResponse format:\n// Status (byte, 0=ok, 1=error)\n// Message length (uint16 BE)\n// Message (bytes)\n\nfunc readDownloadResponse(r io.Reader) (bool, string, error) {\n\tvar status [1]byte\n\tif _, err := io.ReadFull(r, status[:]); err != nil {\n\t\treturn false, \"\", err\n\t}\n\tvar msgLen uint16\n\tif err := binary.Read(r, binary.BigEndian, &msgLen); err != nil {\n\t\treturn false, \"\", err\n\t}\n\t// No message is fine\n\tif msgLen == 0 {\n\t\treturn status[0] == 0, \"\", nil\n\t}\n\tmsgBuf := make([]byte, msgLen)\n\t_, err := io.ReadFull(r, msgBuf)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\treturn status[0] == 0, string(msgBuf), nil\n}\n\nfunc writeDownloadResponse(w io.Writer, ok bool, msg string) error {\n\tsz := 1 + 2 + len(msg)\n\tbuf := make([]byte, sz)\n\tif ok {\n\t\tbuf[0] = 0\n\t} else {\n\t\tbuf[0] = 1\n\t}\n\tbinary.BigEndian.PutUint16(buf[1:], uint16(len(msg)))\n\tcopy(buf[3:], msg)\n\t_, err := w.Write(buf)\n\treturn err\n}\n\n// UploadRequest format:\n// 0x2 (byte)\n// Upload data length (uint32 BE)\n\nfunc readUploadRequest(r io.Reader) (uint32, error) {\n\tvar l uint32\n\terr := binary.Read(r, binary.BigEndian, &l)\n\treturn l, err\n}\n\nfunc writeUploadRequest(w io.Writer, l uint32) error {\n\tbuf := make([]byte, 5)\n\tbuf[0] = typeUpload\n\tbinary.BigEndian.PutUint32(buf[1:], l)\n\t_, err := w.Write(buf)\n\treturn err\n}\n\n// UploadResponse format:\n// Status (byte, 0=ok, 1=error)\n// Message length (uint16 BE)\n// Message (bytes)\n\nfunc readUploadResponse(r io.Reader) (bool, string, error) {\n\tvar status [1]byte\n\tif _, err := io.ReadFull(r, status[:]); err != nil {\n\t\treturn false, \"\", err\n\t}\n\tvar msgLen uint16\n\tif err := binary.Read(r, binary.BigEndian, &msgLen); err != nil {\n\t\treturn false, \"\", err\n\t}\n\t// No message is fine\n\tif msgLen == 0 {\n\t\treturn status[0] == 0, \"\", nil\n\t}\n\tmsgBuf := make([]byte, msgLen)\n\t_, err := io.ReadFull(r, msgBuf)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\treturn status[0] == 0, string(msgBuf), nil\n}\n\nfunc writeUploadResponse(w io.Writer, ok bool, msg string) error {\n\tsz := 1 + 2 + len(msg)\n\tbuf := make([]byte, sz)\n\tif ok {\n\t\tbuf[0] = 0\n\t} else {\n\t\tbuf[0] = 1\n\t}\n\tbinary.BigEndian.PutUint16(buf[1:], uint16(len(msg)))\n\tcopy(buf[3:], msg)\n\t_, err := w.Write(buf)\n\treturn err\n}\n\n// UploadSummary format:\n// Duration (in milliseconds, uint32 BE)\n// Received data length (uint32 BE)\n\nfunc readUploadSummary(r io.Reader) (time.Duration, uint32, error) {\n\tvar duration uint32\n\tif err := binary.Read(r, binary.BigEndian, &duration); err != nil {\n\t\treturn 0, 0, err\n\t}\n\tvar l uint32\n\tif err := binary.Read(r, binary.BigEndian, &l); err != nil {\n\t\treturn 0, 0, err\n\t}\n\treturn time.Duration(duration) * time.Millisecond, l, nil\n}\n\nfunc writeUploadSummary(w io.Writer, duration time.Duration, l uint32) error {\n\tbuf := make([]byte, 8)\n\tbinary.BigEndian.PutUint32(buf, uint32(duration/time.Millisecond))\n\tbinary.BigEndian.PutUint32(buf[4:], l)\n\t_, err := w.Write(buf)\n\treturn err\n}\n"
  },
  {
    "path": "extras/outbounds/speedtest/protocol_test.go",
    "content": "package speedtest\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestReadDownloadRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdata    []byte\n\t\twant    uint32\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal\",\n\t\t\tdata:    []byte{0x0, 0x1, 0xBD, 0xC2},\n\t\t\twant:    114114,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal zero\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x0, 0x0},\n\t\t\twant:    0,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete\",\n\t\t\tdata:    []byte{0x0, 0x1, 0x2},\n\t\t\twant:    0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := bytes.NewReader(tt.data)\n\t\t\tgot, err := readDownloadRequest(r)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"readDownloadRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"readDownloadRequest() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteDownloadRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tl       uint32\n\t\twantW   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal\",\n\t\t\tl:       78909912,\n\t\t\twantW:   \"\\x01\\x04\\xB4\\x11\\xD8\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal zero\",\n\t\t\tl:       0,\n\t\t\twantW:   \"\\x01\\x00\\x00\\x00\\x00\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &bytes.Buffer{}\n\t\t\terr := writeDownloadRequest(w, tt.l)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"writeDownloadRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotW := w.String(); gotW != tt.wantW {\n\t\t\t\tt.Errorf(\"writeDownloadRequest() gotW = %v, want %v\", gotW, tt.wantW)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadDownloadResponse(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdata    []byte\n\t\twant    bool\n\t\twant1   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal ok\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x2, 0x41, 0x42},\n\t\t\twant:    true,\n\t\t\twant1:   \"AB\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal ok no message\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x0, 0x0},\n\t\t\twant:    true,\n\t\t\twant1:   \"\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal error\",\n\t\t\tdata:    []byte{0x1, 0x0, 0x2, 0x43, 0x44},\n\t\t\twant:    false,\n\t\t\twant1:   \"CD\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete\",\n\t\t\tdata:    []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47},\n\t\t\twant:    false,\n\t\t\twant1:   \"\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := bytes.NewReader(tt.data)\n\t\t\tgot, got1, err := readDownloadResponse(r)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"readDownloadResponse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"readDownloadResponse() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"readDownloadResponse() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteDownloadResponse(t *testing.T) {\n\ttype args struct {\n\t\tok  bool\n\t\tmsg string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantW   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal ok\",\n\t\t\targs:    args{ok: true, msg: \"wahaha\"},\n\t\t\twantW:   \"\\x00\\x00\\x06wahaha\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal error\",\n\t\t\targs:    args{ok: false, msg: \"bullbull\"},\n\t\t\twantW:   \"\\x01\\x00\\x08bullbull\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty ok\",\n\t\t\targs:    args{ok: true, msg: \"\"},\n\t\t\twantW:   \"\\x00\\x00\\x00\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &bytes.Buffer{}\n\t\t\terr := writeDownloadResponse(w, tt.args.ok, tt.args.msg)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"writeDownloadResponse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotW := w.String(); gotW != tt.wantW {\n\t\t\t\tt.Errorf(\"writeDownloadResponse() gotW = %v, want %v\", gotW, tt.wantW)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadUploadRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdata    []byte\n\t\twant    uint32\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x26, 0xEE},\n\t\t\twant:    9966,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal zero\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x0, 0x0},\n\t\t\twant:    0,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete\",\n\t\t\tdata:    []byte{0x1},\n\t\t\twant:    0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := bytes.NewReader(tt.data)\n\t\t\tgot, err := readUploadRequest(r)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"readUploadRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"readUploadRequest() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteUploadRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tl       uint32\n\t\twantW   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal\",\n\t\t\tl:       2291758882,\n\t\t\twantW:   \"\\x02\\x88\\x99\\x77\\x22\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal zero\",\n\t\t\tl:       0,\n\t\t\twantW:   \"\\x02\\x00\\x00\\x00\\x00\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &bytes.Buffer{}\n\t\t\terr := writeUploadRequest(w, tt.l)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"writeUploadRequest() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotW := w.String(); gotW != tt.wantW {\n\t\t\t\tt.Errorf(\"writeUploadRequest() gotW = %v, want %v\", gotW, tt.wantW)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadUploadResponse(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdata    []byte\n\t\twant    bool\n\t\twant1   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal ok\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x2, 0x41, 0x42},\n\t\t\twant:    true,\n\t\t\twant1:   \"AB\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal ok no message\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x0},\n\t\t\twant:    true,\n\t\t\twant1:   \"\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal error\",\n\t\t\tdata:    []byte{0x1, 0x0, 0x2, 0x43, 0x44},\n\t\t\twant:    false,\n\t\t\twant1:   \"CD\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete\",\n\t\t\tdata:    []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47},\n\t\t\twant:    false,\n\t\t\twant1:   \"\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := bytes.NewReader(tt.data)\n\t\t\tgot, got1, err := readUploadResponse(r)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"readUploadResponse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"readUploadResponse() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"readUploadResponse() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteUploadResponse(t *testing.T) {\n\ttype args struct {\n\t\tok  bool\n\t\tmsg string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantW   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal ok\",\n\t\t\targs:    args{ok: true, msg: \"lul\"},\n\t\t\twantW:   \"\\x00\\x00\\x03lul\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"normal error\",\n\t\t\targs:    args{ok: false, msg: \"notforu\"},\n\t\t\twantW:   \"\\x01\\x00\\x07notforu\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"empty ok\",\n\t\t\targs:    args{ok: true, msg: \"\"},\n\t\t\twantW:   \"\\x00\\x00\\x00\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &bytes.Buffer{}\n\t\t\terr := writeUploadResponse(w, tt.args.ok, tt.args.msg)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"writeUploadResponse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotW := w.String(); gotW != tt.wantW {\n\t\t\t\tt.Errorf(\"writeUploadResponse() gotW = %v, want %v\", gotW, tt.wantW)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadUploadSummary(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tdata    []byte\n\t\twant    time.Duration\n\t\twant1   uint32\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x14, 0x6E, 0x0, 0x26, 0x25, 0xA0},\n\t\t\twant:    5230 * time.Millisecond,\n\t\t\twant1:   2500000,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"zero\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},\n\t\t\twant:    0,\n\t\t\twant1:   0,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"incomplete\",\n\t\t\tdata:    []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0},\n\t\t\twant:    0,\n\t\t\twant1:   0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := bytes.NewReader(tt.data)\n\t\t\tgot, got1, err := readUploadSummary(r)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"readUploadSummary() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"readUploadSummary() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"readUploadSummary() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteUploadSummary(t *testing.T) {\n\ttype args struct {\n\t\tduration time.Duration\n\t\tl        uint32\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantW   string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"normal\",\n\t\t\targs:    args{duration: 5230 * time.Millisecond, l: 2500000},\n\t\t\twantW:   \"\\x00\\x00\\x14\\x6E\\x00\\x26\\x25\\xA0\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"zero\",\n\t\t\targs:    args{duration: 0, l: 0},\n\t\t\twantW:   \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := &bytes.Buffer{}\n\t\t\terr := writeUploadSummary(w, tt.args.duration, tt.args.l)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"writeUploadSummary() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotW := w.String(); gotW != tt.wantW {\n\t\t\t\tt.Errorf(\"writeUploadSummary() gotW = %v, want %v\", gotW, tt.wantW)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/outbounds/speedtest/server.go",
    "content": "package speedtest\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n)\n\nconst (\n\tchunkSize = 64 * 1024\n)\n\n// NewServerConn creates a new \"pseudo\" connection that implements the speed test protocol.\n// It's called \"pseudo\" because it's not a real TCP connection - everything is done in memory.\nfunc NewServerConn() net.Conn {\n\trConn, iConn := net.Pipe() // return conn & internal conn\n\t// Start the server logic\n\tgo server(iConn)\n\treturn rConn\n}\n\nfunc server(conn net.Conn) error {\n\tdefer conn.Close()\n\t// First byte determines the request type\n\tvar typ [1]byte\n\tif _, err := io.ReadFull(conn, typ[:]); err != nil {\n\t\treturn err\n\t}\n\tswitch typ[0] {\n\tcase typeDownload:\n\t\treturn handleDownload(conn)\n\tcase typeUpload:\n\t\treturn handleUpload(conn)\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown request type: %d\", typ[0])\n\t}\n}\n\n// handleDownload reads the download request and sends the requested amount of data.\nfunc handleDownload(conn net.Conn) error {\n\tl, err := readDownloadRequest(conn)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = writeDownloadResponse(conn, true, \"OK\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tbuf := make([]byte, chunkSize)\n\t// Fill the buffer with random data.\n\t// For now, we only do it once and repeat the same data for performance reasons.\n\t_, err = rand.Read(buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tremaining := l\n\tfor remaining > 0 {\n\t\tn := remaining\n\t\tif n > chunkSize {\n\t\t\tn = chunkSize\n\t\t}\n\t\t_, err := conn.Write(buf[:n])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tremaining -= n\n\t}\n\treturn nil\n}\n\n// handleUpload reads the upload request, reads & discards the requested amount of data,\n// and sends the upload summary.\nfunc handleUpload(conn net.Conn) error {\n\tl, err := readUploadRequest(conn)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = writeUploadResponse(conn, true, \"OK\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tbuf := make([]byte, chunkSize)\n\tstartTime := time.Now()\n\tremaining := l\n\tfor remaining > 0 {\n\t\tn := remaining\n\t\tif n > chunkSize {\n\t\t\tn = chunkSize\n\t\t}\n\t\trn, err := conn.Read(buf[:n])\n\t\tremaining -= uint32(rn)\n\t\tif err != nil && !(remaining == 0 && err == io.EOF) {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn writeUploadSummary(conn, time.Since(startTime), l)\n}\n"
  },
  {
    "path": "extras/outbounds/speedtest.go",
    "content": "package outbounds\n\nimport (\n\t\"net\"\n\n\t\"github.com/apernet/hysteria/extras/v2/outbounds/speedtest\"\n)\n\nconst (\n\tSpeedtestDest = \"@SpeedTest\"\n)\n\n// speedtestHandler is a PluggableOutbound that handles speed test requests.\n// It's used to intercept speed test requests and return a pseudo connection that\n// implements the speed test protocol.\ntype speedtestHandler struct {\n\tNext PluggableOutbound\n}\n\nfunc NewSpeedtestHandler(next PluggableOutbound) PluggableOutbound {\n\treturn &speedtestHandler{\n\t\tNext: next,\n\t}\n}\n\nfunc (s *speedtestHandler) TCP(reqAddr *AddrEx) (net.Conn, error) {\n\tif reqAddr.Host == SpeedtestDest {\n\t\treturn speedtest.NewServerConn(), nil\n\t} else {\n\t\treturn s.Next.TCP(reqAddr)\n\t}\n}\n\nfunc (s *speedtestHandler) UDP(reqAddr *AddrEx) (UDPConn, error) {\n\treturn s.Next.UDP(reqAddr)\n}\n"
  },
  {
    "path": "extras/outbounds/utils.go",
    "content": "package outbounds\n\nimport \"net\"\n\n// splitIPv4IPv6 gets the first IPv4 and IPv6 address from a list of IP addresses.\n// Both of the return values can be nil when no IPv4 or IPv6 address is found.\nfunc splitIPv4IPv6(ips []net.IP) (ipv4, ipv6 net.IP) {\n\tfor _, ip := range ips {\n\t\tif ip.To4() != nil {\n\t\t\tif ipv4 == nil {\n\t\t\t\tipv4 = ip\n\t\t\t}\n\t\t} else {\n\t\t\tif ipv6 == nil {\n\t\t\t\tipv6 = ip\n\t\t\t}\n\t\t}\n\t\tif ipv4 != nil && ipv6 != nil {\n\t\t\t// We have everything we need.\n\t\t\tbreak\n\t\t}\n\t}\n\treturn\n}\n\n// tryParseIP tries to parse the host string in the AddrEx as an IP address.\n// If the host is indeed an IP address, it will fill the ResolveInfo with the\n// parsed IP address and return true. Otherwise, it will return false.\nfunc tryParseIP(addr *AddrEx) bool {\n\tif ip := net.ParseIP(addr.Host); ip != nil {\n\t\taddr.ResolveInfo = &ResolveInfo{}\n\t\tif ip.To4() != nil {\n\t\t\taddr.ResolveInfo.IPv4 = ip\n\t\t} else {\n\t\t\taddr.ResolveInfo.IPv6 = ip\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "extras/outbounds/utils_test.go",
    "content": "package outbounds\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSplitIPv4IPv6(t *testing.T) {\n\ttype args struct {\n\t\tips []net.IP\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\targs     args\n\t\twantIpv4 net.IP\n\t\twantIpv6 net.IP\n\t}{\n\t\t{\n\t\t\tname: \"IPv4 only\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"4.5.6.7\"),\n\t\t\t\t\tnet.ParseIP(\"9.9.9.9\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twantIpv4: net.ParseIP(\"4.5.6.7\"),\n\t\t\twantIpv6: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"IPv6 only\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"2001:db8::68\"),\n\t\t\t\t\tnet.ParseIP(\"2001:db8::69\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twantIpv4: nil,\n\t\t\twantIpv6: net.ParseIP(\"2001:db8::68\"),\n\t\t},\n\t\t{\n\t\t\tname: \"Both 1\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"2001:db8::68\"),\n\t\t\t\t\tnet.ParseIP(\"2001:db8::69\"),\n\t\t\t\t\tnet.ParseIP(\"4.5.6.7\"),\n\t\t\t\t\tnet.ParseIP(\"9.9.9.9\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twantIpv4: net.ParseIP(\"4.5.6.7\"),\n\t\t\twantIpv6: net.ParseIP(\"2001:db8::68\"),\n\t\t},\n\t\t{\n\t\t\tname: \"Both 2\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"2001:db8::69\"),\n\t\t\t\t\tnet.ParseIP(\"9.9.9.9\"),\n\t\t\t\t\tnet.ParseIP(\"2001:db8::68\"),\n\t\t\t\t\tnet.ParseIP(\"4.5.6.7\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twantIpv4: net.ParseIP(\"9.9.9.9\"),\n\t\t\twantIpv6: net.ParseIP(\"2001:db8::69\"),\n\t\t},\n\t\t{\n\t\t\tname: \"Empty\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{},\n\t\t\t},\n\t\t\twantIpv4: nil,\n\t\t\twantIpv6: nil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotIpv4, gotIpv6 := splitIPv4IPv6(tt.args.ips)\n\t\t\tassert.Equalf(t, tt.wantIpv4, gotIpv4, \"splitIPv4IPv6(%v)\", tt.args.ips)\n\t\t\tassert.Equalf(t, tt.wantIpv6, gotIpv6, \"splitIPv4IPv6(%v)\", tt.args.ips)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "extras/sniff/.mockery.yaml",
    "content": "with-expecter: true\ndir: .\noutpkg: sniff\npackages:\n  github.com/apernet/quic-go:\n    interfaces:\n      Stream:\n        config:\n          mockname: mockStream\n          replace-type: # internal package alias dirty fix\n            - github.com/apernet/quic-go/internal/protocol=github.com/apernet/quic-go\n            - github.com/apernet/quic-go/internal/qerr=github.com/apernet/quic-go\n"
  },
  {
    "path": "extras/sniff/internal/quic/LICENSE",
    "content": "Author:: Cuong Manh Le <cuong.manhle.vn@gmail.com>\nCopyright:: Copyright (c) 2023, Cuong Manh Le\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n\n    * Neither the name of the @organization@ nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR\nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE\nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN\nIF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
  },
  {
    "path": "extras/sniff/internal/quic/README.md",
    "content": "The code here is from https://github.com/cuonglm/quicsni with various modifications."
  },
  {
    "path": "extras/sniff/internal/quic/header.go",
    "content": "package quic\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/apernet/quic-go/quicvarint\"\n)\n\n// The Header represents a QUIC header.\ntype Header struct {\n\tType             uint8\n\tVersion          uint32\n\tSrcConnectionID  []byte\n\tDestConnectionID []byte\n\tLength           int64\n\tToken            []byte\n}\n\n// ParseInitialHeader parses the initial packet of a QUIC connection,\n// return the initial header and number of bytes read so far.\nfunc ParseInitialHeader(data []byte) (*Header, int64, error) {\n\tbr := bytes.NewReader(data)\n\thdr, err := parseLongHeader(br)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tn := int64(len(data) - br.Len())\n\treturn hdr, n, nil\n}\n\nfunc parseLongHeader(b *bytes.Reader) (*Header, error) {\n\ttypeByte, err := b.ReadByte()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th := &Header{}\n\tver, err := beUint32(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th.Version = ver\n\tif h.Version != 0 && typeByte&0x40 == 0 {\n\t\treturn nil, errors.New(\"not a QUIC packet\")\n\t}\n\tdestConnIDLen, err := b.ReadByte()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th.DestConnectionID = make([]byte, int(destConnIDLen))\n\tif err := readConnectionID(b, h.DestConnectionID); err != nil {\n\t\treturn nil, err\n\t}\n\tsrcConnIDLen, err := b.ReadByte()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th.SrcConnectionID = make([]byte, int(srcConnIDLen))\n\tif err := readConnectionID(b, h.SrcConnectionID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tinitialPacketType := byte(0b00)\n\tif h.Version == V2 {\n\t\tinitialPacketType = 0b01\n\t}\n\tif (typeByte >> 4 & 0b11) == initialPacketType {\n\t\ttokenLen, err := quicvarint.Read(b)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif tokenLen > uint64(b.Len()) {\n\t\t\treturn nil, io.EOF\n\t\t}\n\t\th.Token = make([]byte, tokenLen)\n\t\tif _, err := io.ReadFull(b, h.Token); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tpl, err := quicvarint.Read(b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th.Length = int64(pl)\n\treturn h, err\n}\n\nfunc readConnectionID(r io.Reader, cid []byte) error {\n\t_, err := io.ReadFull(r, cid)\n\tif err == io.ErrUnexpectedEOF {\n\t\treturn io.EOF\n\t}\n\treturn nil\n}\n\nfunc beUint32(r io.Reader) (uint32, error) {\n\tb := make([]byte, 4)\n\tif _, err := io.ReadFull(r, b); err != nil {\n\t\treturn 0, err\n\t}\n\treturn binary.BigEndian.Uint32(b), nil\n}\n"
  },
  {
    "path": "extras/sniff/internal/quic/packet_protector.go",
    "content": "package quic\n\nimport (\n\t\"crypto\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\n\t\"golang.org/x/crypto/chacha20\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/hkdf\"\n)\n\n// NewProtectionKey creates a new ProtectionKey.\nfunc NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {\n\treturn newProtectionKey(suite, secret, v)\n}\n\n// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key\n// is used for encrypt/decrypt Initial Packet only.\n//\n// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets\nfunc NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) {\n\treturn NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v)\n}\n\n// NewPacketProtector creates a new PacketProtector.\nfunc NewPacketProtector(key *ProtectionKey) *PacketProtector {\n\treturn &PacketProtector{key: key}\n}\n\n// PacketProtector is used for protecting a QUIC packet.\n//\n// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection\ntype PacketProtector struct {\n\tkey *ProtectionKey\n}\n\n// UnProtect decrypts a QUIC packet.\nfunc (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) {\n\tif isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 {\n\t\treturn nil, errors.New(\"packet with long header is too small\")\n\t}\n\n\t// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample\n\tsampleOffset := pnOffset + 4\n\tsample := packet[sampleOffset : sampleOffset+16]\n\n\t// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati\n\tmask := pp.key.headerProtection(sample)\n\tif isLongHeader(packet[0]) {\n\t\t// Long header: 4 bits masked\n\t\tpacket[0] ^= mask[0] & 0x0f\n\t} else {\n\t\t// Short header: 5 bits masked\n\t\tpacket[0] ^= mask[0] & 0x1f\n\t}\n\n\tpnLen := packet[0]&0x3 + 1\n\tpn := int64(0)\n\tfor i := uint8(0); i < pnLen; i++ {\n\t\tpacket[pnOffset:][i] ^= mask[1+i]\n\t\tpn = (pn << 8) | int64(packet[pnOffset:][i])\n\t}\n\tpn = decodePacketNumber(pnMax, pn, pnLen)\n\thdr := packet[:pnOffset+int64(pnLen)]\n\tpayload := packet[pnOffset:][pnLen:]\n\tdec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"decryption failed: %w\", err)\n\t}\n\treturn dec, nil\n}\n\n// ProtectionKey is the key used to protect a QUIC packet.\ntype ProtectionKey struct {\n\taead             cipher.AEAD\n\theaderProtection func(sample []byte) (mask []byte)\n\tiv               []byte\n}\n\n// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage\n//\n// \"The 62 bits of the reconstructed QUIC packet number in network byte order are\n// left-padded with zeros to the size of the IV. The exclusive OR of the padded\n// packet number and the IV forms the AEAD nonce.\"\nfunc (pk *ProtectionKey) nonce(pn int64) []byte {\n\tnonce := make([]byte, len(pk.iv))\n\tbinary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn))\n\tfor i := range pk.iv {\n\t\tnonce[i] ^= pk.iv[i]\n\t}\n\treturn nonce\n}\n\nfunc newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {\n\tswitch suite {\n\tcase tls.TLS_AES_128_GCM_SHA256:\n\t\tkey := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16)\n\t\tc, err := aes.NewCipher(key)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\taead, err := cipher.NewGCM(c)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tiv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())\n\t\thpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16)\n\t\thp, err := aes.NewCipher(hpKey)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tk := &ProtectionKey{}\n\t\tk.aead = aead\n\t\t// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection\n\t\tk.headerProtection = func(sample []byte) []byte {\n\t\t\tmask := make([]byte, hp.BlockSize())\n\t\t\thp.Encrypt(mask, sample)\n\t\t\treturn mask\n\t\t}\n\t\tk.iv = iv\n\t\treturn k, nil\n\tcase tls.TLS_CHACHA20_POLY1305_SHA256:\n\t\tkey := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize)\n\t\taead, err := chacha20poly1305.New(key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tiv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())\n\t\thpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize)\n\t\tk := &ProtectionKey{}\n\t\tk.aead = aead\n\t\t// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote\n\t\tk.headerProtection = func(sample []byte) []byte {\n\t\t\tnonce := sample[4:16]\n\t\t\tc, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tc.SetCounter(binary.LittleEndian.Uint32(sample[:4]))\n\t\t\tmask := make([]byte, 5)\n\t\t\tc.XORKeyStream(mask, mask)\n\t\t\treturn mask\n\t\t}\n\t\tk.iv = iv\n\t\treturn k, nil\n\t}\n\treturn nil, errors.New(\"not supported cipher suite\")\n}\n\n// decodePacketNumber decode the packet number after header protection removed.\n//\n// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a\nfunc decodePacketNumber(largest, truncated int64, nbits uint8) int64 {\n\texpected := largest + 1\n\twin := int64(1 << (nbits * 8))\n\thwin := win / 2\n\tmask := win - 1\n\tcandidate := (expected &^ mask) | truncated\n\tswitch {\n\tcase candidate <= expected-hwin && candidate < (1<<62)-win:\n\t\treturn candidate + win\n\tcase candidate > expected+hwin && candidate >= win:\n\t\treturn candidate - win\n\t}\n\treturn candidate\n}\n\n// Copied from crypto/tls/key_schedule.go.\nfunc hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte {\n\tvar hkdfLabel cryptobyte.Builder\n\thkdfLabel.AddUint16(uint16(length))\n\thkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {\n\t\tb.AddBytes([]byte(\"tls13 \"))\n\t\tb.AddBytes([]byte(label))\n\t})\n\thkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {\n\t\tb.AddBytes(context)\n\t})\n\tout := make([]byte, length)\n\tn, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out)\n\tif err != nil || n != length {\n\t\tpanic(\"quic: HKDF-Expand-Label invocation failed unexpectedly\")\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "extras/sniff/internal/quic/packet_protector_test.go",
    "content": "package quic\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"strings\"\n\t\"testing\"\n\t\"unicode\"\n\n\t\"golang.org/x/crypto/hkdf\"\n)\n\nfunc TestInitialPacketProtector_UnProtect(t *testing.T) {\n\t// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial\n\tprotect := mustHexDecodeString(`\n\t\t\tc7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7\n\t\t\t6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498\n\t\t\t44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5\n\t\t\tbe2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e\n\t\t\tfd8142fafc0f76\n\t\t`)\n\tunProtect := mustHexDecodeString(`\n\t\t\t02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739\n\t\t\t88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94\n\t\t\t0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00\n\t\t\t020304\n\t\t`)\n\n\tconnID := mustHexDecodeString(`8394c8f03e515708`)\n\n\tpacket := append([]byte{}, protect...)\n\thdr, offset, err := ParseInitialHeader(packet)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tinitialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version))\n\tserverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, \"server in\", []byte{}, crypto.SHA256.Size())\n\tkey, err := NewInitialProtectionKey(serverSecret, hdr.Version)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpp := NewPacketProtector(key)\n\tgot, err := pp.UnProtect(protect, offset, 1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !bytes.Equal(got, unProtect) {\n\t\tt.Error(\"UnProtect returns wrong result\")\n\t}\n}\n\nfunc TestPacketProtectorShortHeader_UnProtect(t *testing.T) {\n\t// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea\n\tprotect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`)\n\tunProtect := mustHexDecodeString(`01`)\n\thdr := mustHexDecodeString(`4200bff4`)\n\n\tsecret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`)\n\tk, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpnLen := int(hdr[0]&0x03) + 1\n\toffset := len(hdr) - pnLen\n\tpp := NewPacketProtector(k)\n\tgot, err := pp.UnProtect(protect, int64(offset), 654360564)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !bytes.Equal(got, unProtect) {\n\t\tt.Error(\"UnProtect returns wrong result\")\n\t}\n}\n\nfunc mustHexDecodeString(s string) []byte {\n\tb, err := hex.DecodeString(normalizeHex(s))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn b\n}\n\nfunc normalizeHex(s string) string {\n\treturn strings.Map(func(c rune) rune {\n\t\tif unicode.IsSpace(c) {\n\t\t\treturn -1\n\t\t}\n\t\treturn c\n\t}, s)\n}\n"
  },
  {
    "path": "extras/sniff/internal/quic/payload.go",
    "content": "package quic\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\n\t\"github.com/apernet/quic-go/quicvarint\"\n\t\"golang.org/x/crypto/hkdf\"\n)\n\nfunc ReadCryptoPayload(packet []byte) ([]byte, error) {\n\thdr, offset, err := ParseInitialHeader(packet)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Some sanity checks\n\tif hdr.Version != V1 && hdr.Version != V2 {\n\t\treturn nil, fmt.Errorf(\"unsupported version: %x\", hdr.Version)\n\t}\n\tif offset == 0 || hdr.Length == 0 {\n\t\treturn nil, errors.New(\"invalid packet\")\n\t}\n\n\tinitialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version))\n\tclientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, \"client in\", []byte{}, crypto.SHA256.Size())\n\tkey, err := NewInitialProtectionKey(clientSecret, hdr.Version)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"NewInitialProtectionKey: %w\", err)\n\t}\n\tpp := NewPacketProtector(key)\n\t// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial\n\t//\n\t// \"The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2\"\n\tif int64(len(packet)) < offset+hdr.Length {\n\t\treturn nil, fmt.Errorf(\"packet is too short: %d < %d\", len(packet), offset+hdr.Length)\n\t}\n\tunProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfrs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdata := assembleCryptoFrames(frs)\n\tif data == nil {\n\t\treturn nil, errors.New(\"unable to assemble crypto frames\")\n\t}\n\treturn data, nil\n}\n\nconst (\n\tpaddingFrameType = 0x00\n\tpingFrameType    = 0x01\n\tcryptoFrameType  = 0x06\n)\n\ntype cryptoFrame struct {\n\tOffset int64\n\tData   []byte\n}\n\nfunc extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {\n\tvar frames []cryptoFrame\n\tfor r.Len() > 0 {\n\t\ttyp, err := quicvarint.Read(r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif typ == paddingFrameType || typ == pingFrameType {\n\t\t\tcontinue\n\t\t}\n\t\tif typ != cryptoFrameType {\n\t\t\treturn nil, fmt.Errorf(\"encountered unexpected frame type: %d\", typ)\n\t\t}\n\t\tvar frame cryptoFrame\n\t\toffset, err := quicvarint.Read(r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tframe.Offset = int64(offset)\n\t\tdataLen, err := quicvarint.Read(r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tframe.Data = make([]byte, dataLen)\n\t\tif _, err := io.ReadFull(r, frame.Data); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tframes = append(frames, frame)\n\t}\n\treturn frames, nil\n}\n\n// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).\n// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.\nfunc assembleCryptoFrames(frames []cryptoFrame) []byte {\n\tif len(frames) == 0 {\n\t\treturn nil\n\t}\n\tif len(frames) == 1 {\n\t\treturn frames[0].Data\n\t}\n\t// sort the frames by offset\n\tsort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })\n\t// check if the frames are contiguous\n\tfor i := 1; i < len(frames); i++ {\n\t\tif frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) {\n\t\t\treturn nil\n\t\t}\n\t}\n\t// concatenate the frames\n\tdata := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data)))\n\tfor _, frame := range frames {\n\t\tcopy(data[frame.Offset:], frame.Data)\n\t}\n\treturn data\n}\n"
  },
  {
    "path": "extras/sniff/internal/quic/quic.go",
    "content": "package quic\n\nconst (\n\tV1 uint32 = 0x1\n\tV2 uint32 = 0x6b3343cf\n\n\thkdfLabelKeyV1 = \"quic key\"\n\thkdfLabelKeyV2 = \"quicv2 key\"\n\thkdfLabelIVV1  = \"quic iv\"\n\thkdfLabelIVV2  = \"quicv2 iv\"\n\thkdfLabelHPV1  = \"quic hp\"\n\thkdfLabelHPV2  = \"quicv2 hp\"\n)\n\nvar (\n\tquicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}\n\t// https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets\n\tquicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}\n\t// https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2\n\tquicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}\n)\n\n// isLongHeader reports whether b is the first byte of a long header packet.\nfunc isLongHeader(b byte) bool {\n\treturn b&0x80 > 0\n}\n\nfunc getSalt(v uint32) []byte {\n\tswitch v {\n\tcase V1:\n\t\treturn quicSaltV1\n\tcase V2:\n\t\treturn quicSaltV2\n\t}\n\treturn quicSaltOld\n}\n\nfunc keyLabel(v uint32) string {\n\tkl := hkdfLabelKeyV1\n\tif v == V2 {\n\t\tkl = hkdfLabelKeyV2\n\t}\n\treturn kl\n}\n\nfunc ivLabel(v uint32) string {\n\tivl := hkdfLabelIVV1\n\tif v == V2 {\n\t\tivl = hkdfLabelIVV2\n\t}\n\treturn ivl\n}\n\nfunc headerProtectionLabel(v uint32) string {\n\tif v == V2 {\n\t\treturn hkdfLabelHPV2\n\t}\n\treturn hkdfLabelHPV1\n}\n"
  },
  {
    "path": "extras/sniff/mock_Stream.go",
    "content": "// Code generated by mockery v2.43.0. DO NOT EDIT.\n\npackage sniff\n\nimport (\n\tcontext \"context\"\n\n\tqerr \"github.com/apernet/quic-go\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttime \"time\"\n)\n\n// mockStream is an autogenerated mock type for the Stream type\ntype mockStream struct {\n\tmock.Mock\n}\n\ntype mockStream_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *mockStream) EXPECT() *mockStream_Expecter {\n\treturn &mockStream_Expecter{mock: &_m.Mock}\n}\n\n// CancelRead provides a mock function with given fields: _a0\nfunc (_m *mockStream) CancelRead(_a0 qerr.StreamErrorCode) {\n\t_m.Called(_a0)\n}\n\n// mockStream_CancelRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelRead'\ntype mockStream_CancelRead_Call struct {\n\t*mock.Call\n}\n\n// CancelRead is a helper method to define mock.On call\n//   - _a0 qerr.StreamErrorCode\nfunc (_e *mockStream_Expecter) CancelRead(_a0 interface{}) *mockStream_CancelRead_Call {\n\treturn &mockStream_CancelRead_Call{Call: _e.mock.On(\"CancelRead\", _a0)}\n}\n\nfunc (_c *mockStream_CancelRead_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelRead_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(qerr.StreamErrorCode))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_CancelRead_Call) Return() *mockStream_CancelRead_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *mockStream_CancelRead_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelRead_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// CancelWrite provides a mock function with given fields: _a0\nfunc (_m *mockStream) CancelWrite(_a0 qerr.StreamErrorCode) {\n\t_m.Called(_a0)\n}\n\n// mockStream_CancelWrite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelWrite'\ntype mockStream_CancelWrite_Call struct {\n\t*mock.Call\n}\n\n// CancelWrite is a helper method to define mock.On call\n//   - _a0 qerr.StreamErrorCode\nfunc (_e *mockStream_Expecter) CancelWrite(_a0 interface{}) *mockStream_CancelWrite_Call {\n\treturn &mockStream_CancelWrite_Call{Call: _e.mock.On(\"CancelWrite\", _a0)}\n}\n\nfunc (_c *mockStream_CancelWrite_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelWrite_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(qerr.StreamErrorCode))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_CancelWrite_Call) Return() *mockStream_CancelWrite_Call {\n\t_c.Call.Return()\n\treturn _c\n}\n\nfunc (_c *mockStream_CancelWrite_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelWrite_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Close provides a mock function with given fields:\nfunc (_m *mockStream) Close() error {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Close\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockStream_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'\ntype mockStream_Close_Call struct {\n\t*mock.Call\n}\n\n// Close is a helper method to define mock.On call\nfunc (_e *mockStream_Expecter) Close() *mockStream_Close_Call {\n\treturn &mockStream_Close_Call{Call: _e.mock.On(\"Close\")}\n}\n\nfunc (_c *mockStream_Close_Call) Run(run func()) *mockStream_Close_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_Close_Call) Return(_a0 error) *mockStream_Close_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockStream_Close_Call) RunAndReturn(run func() error) *mockStream_Close_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Context provides a mock function with given fields:\nfunc (_m *mockStream) Context() context.Context {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Context\")\n\t}\n\n\tvar r0 context.Context\n\tif rf, ok := ret.Get(0).(func() context.Context); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(context.Context)\n\t\t}\n\t}\n\n\treturn r0\n}\n\n// mockStream_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context'\ntype mockStream_Context_Call struct {\n\t*mock.Call\n}\n\n// Context is a helper method to define mock.On call\nfunc (_e *mockStream_Expecter) Context() *mockStream_Context_Call {\n\treturn &mockStream_Context_Call{Call: _e.mock.On(\"Context\")}\n}\n\nfunc (_c *mockStream_Context_Call) Run(run func()) *mockStream_Context_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_Context_Call) Return(_a0 context.Context) *mockStream_Context_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockStream_Context_Call) RunAndReturn(run func() context.Context) *mockStream_Context_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Read provides a mock function with given fields: p\nfunc (_m *mockStream) Read(p []byte) (int, error) {\n\tret := _m.Called(p)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Read\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {\n\t\treturn rf(p)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(p)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) error); ok {\n\t\tr1 = rf(p)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockStream_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'\ntype mockStream_Read_Call struct {\n\t*mock.Call\n}\n\n// Read is a helper method to define mock.On call\n//   - p []byte\nfunc (_e *mockStream_Expecter) Read(p interface{}) *mockStream_Read_Call {\n\treturn &mockStream_Read_Call{Call: _e.mock.On(\"Read\", p)}\n}\n\nfunc (_c *mockStream_Read_Call) Run(run func(p []byte)) *mockStream_Read_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_Read_Call) Return(n int, err error) *mockStream_Read_Call {\n\t_c.Call.Return(n, err)\n\treturn _c\n}\n\nfunc (_c *mockStream_Read_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Read_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetDeadline provides a mock function with given fields: t\nfunc (_m *mockStream) SetDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockStream_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'\ntype mockStream_SetDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *mockStream_Expecter) SetDeadline(t interface{}) *mockStream_SetDeadline_Call {\n\treturn &mockStream_SetDeadline_Call{Call: _e.mock.On(\"SetDeadline\", t)}\n}\n\nfunc (_c *mockStream_SetDeadline_Call) Run(run func(t time.Time)) *mockStream_SetDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_SetDeadline_Call) Return(_a0 error) *mockStream_SetDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockStream_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetReadDeadline provides a mock function with given fields: t\nfunc (_m *mockStream) SetReadDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetReadDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockStream_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'\ntype mockStream_SetReadDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetReadDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *mockStream_Expecter) SetReadDeadline(t interface{}) *mockStream_SetReadDeadline_Call {\n\treturn &mockStream_SetReadDeadline_Call{Call: _e.mock.On(\"SetReadDeadline\", t)}\n}\n\nfunc (_c *mockStream_SetReadDeadline_Call) Run(run func(t time.Time)) *mockStream_SetReadDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_SetReadDeadline_Call) Return(_a0 error) *mockStream_SetReadDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockStream_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetReadDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// SetWriteDeadline provides a mock function with given fields: t\nfunc (_m *mockStream) SetWriteDeadline(t time.Time) error {\n\tret := _m.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for SetWriteDeadline\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(time.Time) error); ok {\n\t\tr0 = rf(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// mockStream_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'\ntype mockStream_SetWriteDeadline_Call struct {\n\t*mock.Call\n}\n\n// SetWriteDeadline is a helper method to define mock.On call\n//   - t time.Time\nfunc (_e *mockStream_Expecter) SetWriteDeadline(t interface{}) *mockStream_SetWriteDeadline_Call {\n\treturn &mockStream_SetWriteDeadline_Call{Call: _e.mock.On(\"SetWriteDeadline\", t)}\n}\n\nfunc (_c *mockStream_SetWriteDeadline_Call) Run(run func(t time.Time)) *mockStream_SetWriteDeadline_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(time.Time))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_SetWriteDeadline_Call) Return(_a0 error) *mockStream_SetWriteDeadline_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockStream_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetWriteDeadline_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// StreamID provides a mock function with given fields:\nfunc (_m *mockStream) StreamID() qerr.StreamID {\n\tret := _m.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for StreamID\")\n\t}\n\n\tvar r0 qerr.StreamID\n\tif rf, ok := ret.Get(0).(func() qerr.StreamID); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Get(0).(qerr.StreamID)\n\t}\n\n\treturn r0\n}\n\n// mockStream_StreamID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamID'\ntype mockStream_StreamID_Call struct {\n\t*mock.Call\n}\n\n// StreamID is a helper method to define mock.On call\nfunc (_e *mockStream_Expecter) StreamID() *mockStream_StreamID_Call {\n\treturn &mockStream_StreamID_Call{Call: _e.mock.On(\"StreamID\")}\n}\n\nfunc (_c *mockStream_StreamID_Call) Run(run func()) *mockStream_StreamID_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_StreamID_Call) Return(_a0 qerr.StreamID) *mockStream_StreamID_Call {\n\t_c.Call.Return(_a0)\n\treturn _c\n}\n\nfunc (_c *mockStream_StreamID_Call) RunAndReturn(run func() qerr.StreamID) *mockStream_StreamID_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Write provides a mock function with given fields: p\nfunc (_m *mockStream) Write(p []byte) (int, error) {\n\tret := _m.Called(p)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Write\")\n\t}\n\n\tvar r0 int\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {\n\t\treturn rf(p)\n\t}\n\tif rf, ok := ret.Get(0).(func([]byte) int); ok {\n\t\tr0 = rf(p)\n\t} else {\n\t\tr0 = ret.Get(0).(int)\n\t}\n\n\tif rf, ok := ret.Get(1).(func([]byte) error); ok {\n\t\tr1 = rf(p)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// mockStream_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'\ntype mockStream_Write_Call struct {\n\t*mock.Call\n}\n\n// Write is a helper method to define mock.On call\n//   - p []byte\nfunc (_e *mockStream_Expecter) Write(p interface{}) *mockStream_Write_Call {\n\treturn &mockStream_Write_Call{Call: _e.mock.On(\"Write\", p)}\n}\n\nfunc (_c *mockStream_Write_Call) Run(run func(p []byte)) *mockStream_Write_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].([]byte))\n\t})\n\treturn _c\n}\n\nfunc (_c *mockStream_Write_Call) Return(n int, err error) *mockStream_Write_Call {\n\t_c.Call.Return(n, err)\n\treturn _c\n}\n\nfunc (_c *mockStream_Write_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Write_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc newMockStream(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *mockStream {\n\tmock := &mockStream{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "extras/sniff/sniff.go",
    "content": "package sniff\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/apernet/quic-go\"\n\tutls \"github.com/refraction-networking/utls\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n\tquicInternal \"github.com/apernet/hysteria/extras/v2/sniff/internal/quic\"\n\t\"github.com/apernet/hysteria/extras/v2/utils\"\n)\n\nconst (\n\tsniffDefaultTimeout = 4 * time.Second\n)\n\nvar _ server.RequestHook = (*Sniffer)(nil)\n\n// Sniffer is a server core RequestHook that performs packet inspection and possibly\n// rewrites the request address based on what's in the protocol header.\n// This is mainly for inbounds that inherently cannot get domain information (e.g. TUN),\n// in which case sniffing can restore the domains and apply ACLs correctly.\n// Currently supports HTTP, HTTPS (TLS) and QUIC.\ntype Sniffer struct {\n\tTimeout       time.Duration\n\tRewriteDomain bool // Whether to rewrite the address even when it's already a domain\n\tTCPPorts      utils.PortUnion\n\tUDPPorts      utils.PortUnion\n}\n\nfunc (h *Sniffer) isDomain(addr string) bool {\n\thost, _, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn net.ParseIP(host) == nil\n}\n\nfunc (h *Sniffer) isHTTP(buf []byte) bool {\n\tif len(buf) < 3 {\n\t\treturn false\n\t}\n\t// First 3 bytes should be English letters (whatever HTTP method)\n\tfor _, b := range buf[:3] {\n\t\tif (b < 'A' || b > 'Z') && (b < 'a' || b > 'z') {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (h *Sniffer) isTLS(buf []byte) bool {\n\tif len(buf) < 3 {\n\t\treturn false\n\t}\n\treturn buf[0] >= 0x16 && buf[0] <= 0x17 &&\n\t\tbuf[1] == 0x03 && buf[2] <= 0x09\n}\n\nfunc (h *Sniffer) Check(isUDP bool, reqAddr string) bool {\n\t// @ means it's internal (e.g. speed test)\n\tif strings.HasPrefix(reqAddr, \"@\") {\n\t\treturn false\n\t}\n\thost, port, err := net.SplitHostPort(reqAddr)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif !h.RewriteDomain && net.ParseIP(host) == nil {\n\t\t// Is a domain and domain rewriting is disabled\n\t\treturn false\n\t}\n\tportNum, err := strconv.Atoi(port)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif isUDP {\n\t\treturn h.UDPPorts == nil || h.UDPPorts.Contains(uint16(portNum))\n\t} else {\n\t\treturn h.TCPPorts == nil || h.TCPPorts.Contains(uint16(portNum))\n\t}\n}\n\nfunc (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) {\n\tvar err error\n\tif h.Timeout == 0 {\n\t\terr = stream.SetReadDeadline(time.Now().Add(sniffDefaultTimeout))\n\t} else {\n\t\terr = stream.SetReadDeadline(time.Now().Add(h.Timeout))\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Make sure to reset the deadline after sniffing\n\tdefer stream.SetReadDeadline(time.Time{})\n\t// Read 3 bytes to determine the protocol\n\tpre := make([]byte, 3)\n\tn, err := io.ReadFull(stream, pre)\n\tif err != nil {\n\t\t// Not enough within the timeout, just return what we have\n\t\treturn pre[:n], nil\n\t}\n\tif h.isHTTP(pre) {\n\t\t// HTTP\n\t\ttr := &teeReader{Stream: stream, Pre: pre}\n\t\treq, _ := http.ReadRequest(bufio.NewReader(tr))\n\t\tif req != nil && req.Host != \"\" {\n\t\t\t// req.Host can be host:port, in which case we need to extract the host part\n\t\t\thost, _, err := net.SplitHostPort(req.Host)\n\t\t\tif err != nil {\n\t\t\t\t// No port, just use the whole string\n\t\t\t\thost = req.Host\n\t\t\t}\n\t\t\t_, port, err := net.SplitHostPort(*reqAddr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t*reqAddr = net.JoinHostPort(host, port)\n\t\t}\n\t\treturn tr.Buffer(), nil\n\t} else if h.isTLS(pre) {\n\t\t// TLS\n\t\t// Need to read 2 more bytes (content length)\n\t\tpre = append(pre, make([]byte, 2)...)\n\t\tn, err = io.ReadFull(stream, pre[3:])\n\t\tif err != nil {\n\t\t\t// Not enough within the timeout, just return what we have\n\t\t\treturn pre[:3+n], nil\n\t\t}\n\t\tcontentLength := int(pre[3])<<8 | int(pre[4])\n\t\tpre = append(pre, make([]byte, contentLength)...)\n\t\tn, err = io.ReadFull(stream, pre[5:])\n\t\tif err != nil {\n\t\t\t// Not enough within the timeout, just return what we have\n\t\t\treturn pre[:5+n], nil\n\t\t}\n\t\tclientHello := utls.UnmarshalClientHello(pre[5:])\n\t\tif clientHello != nil && clientHello.ServerName != \"\" {\n\t\t\t_, port, err := net.SplitHostPort(*reqAddr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\t*reqAddr = net.JoinHostPort(clientHello.ServerName, port)\n\t\t}\n\t\treturn pre, nil\n\t} else {\n\t\t// Unrecognized protocol, just return what we have\n\t\treturn pre, nil\n\t}\n}\n\nfunc (h *Sniffer) UDP(data []byte, reqAddr *string) error {\n\tpl, err := quicInternal.ReadCryptoPayload(data)\n\tif err != nil || len(pl) < 4 || pl[0] != 0x01 {\n\t\t// Unrecognized protocol, incomplete payload or not a client hello\n\t\treturn nil\n\t}\n\tclientHello := utls.UnmarshalClientHello(pl)\n\tif clientHello != nil && clientHello.ServerName != \"\" {\n\t\t_, port, err := net.SplitHostPort(*reqAddr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*reqAddr = net.JoinHostPort(clientHello.ServerName, port)\n\t}\n\treturn nil\n}\n\ntype teeReader struct {\n\tStream quic.Stream\n\tPre    []byte\n\n\tbuf []byte\n}\n\nfunc (c *teeReader) Read(b []byte) (n int, err error) {\n\tif len(c.Pre) > 0 {\n\t\tn = copy(b, c.Pre)\n\t\tc.Pre = c.Pre[n:]\n\t\tc.buf = append(c.buf, b[:n]...)\n\t\treturn n, nil\n\t}\n\tn, err = c.Stream.Read(b)\n\tif n > 0 {\n\t\tc.buf = append(c.buf, b[:n]...)\n\t}\n\treturn n, err\n}\n\nfunc (c *teeReader) Buffer() []byte {\n\treturn append(c.Pre, c.buf...)\n}\n"
  },
  {
    "path": "extras/sniff/sniff_test.go",
    "content": "package sniff\n\nimport (\n\t\"encoding/base64\"\n\t\"io\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/extras/v2/utils\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n)\n\nfunc TestSnifferCheck(t *testing.T) {\n\tsniffer := &Sniffer{\n\t\tTimeout:       1 * time.Second,\n\t\tRewriteDomain: false,\n\t\tTCPPorts:      nil, // nil = all\n\t\tUDPPorts:      nil, // nil = all\n\t}\n\n\tassert.True(t, sniffer.Check(false, \"1.1.1.1:80\"))\n\tassert.False(t, sniffer.Check(false, \"example.com:443\"))\n\n\tsniffer.RewriteDomain = true\n\tassert.True(t, sniffer.Check(false, \"example.com:443\"))\n\n\tsniffer.TCPPorts = []utils.PortRange{{80, 80}}\n\tassert.True(t, sniffer.Check(false, \"google.com:80\"))\n\tassert.False(t, sniffer.Check(false, \"google.com:443\"))\n\n\tsniffer.UDPPorts = []utils.PortRange{{443, 443}}\n\tassert.True(t, sniffer.Check(true, \"google.com:443\"))\n\tassert.False(t, sniffer.Check(true, \"google.com:80\"))\n}\n\nfunc TestSnifferTCP(t *testing.T) {\n\tsniffer := &Sniffer{\n\t\tTimeout:       1 * time.Second,\n\t\tRewriteDomain: false,\n\t}\n\n\tbuf := &[]byte{}\n\n\t// Test HTTP\n\t*buf = []byte(\"POST /hello HTTP/1.1\\r\\n\" +\n\t\t\"Host: example.com\\r\\n\" +\n\t\t\"User-Agent: mamamiya\\r\\n\" +\n\t\t\"Content-Length: 27\\r\\n\" +\n\t\t\"Connection: keep-alive\\r\\n\\r\\n\" +\n\t\t\"param1=value1&param2=value2\")\n\tindex := 0\n\tstream := &mockStream{}\n\tstream.EXPECT().SetReadDeadline(mock.Anything).Return(nil)\n\tstream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {\n\t\tif index < len(*buf) {\n\t\t\tn := copy(bs, (*buf)[index:])\n\t\t\tindex += n\n\t\t\treturn n, nil\n\t\t} else {\n\t\t\treturn 0, io.EOF\n\t\t}\n\t})\n\n\t// Rewrite IP to domain\n\treqAddr := \"111.111.111.111:80\"\n\tputback, err := sniffer.TCP(stream, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, *buf, putback)\n\tassert.Equal(t, \"example.com:80\", reqAddr)\n\n\t// Test HTTP with Host as host:port\n\t*buf = []byte(\"GET / HTTP/1.1\\r\\n\" +\n\t\t\"Host: example.com:8080\\r\\n\" +\n\t\t\"User-Agent: test-agent\\r\\n\" +\n\t\t\"Accept: */*\\r\\n\\r\\n\")\n\tindex = 0\n\treqAddr = \"222.222.222.222:10086\"\n\tputback, err = sniffer.TCP(stream, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, *buf, putback)\n\tassert.Equal(t, \"example.com:10086\", reqAddr)\n\n\t// Test TLS\n\t*buf, err = base64.StdEncoding.DecodeString(\"FgMBARcBAAETAwPJL2jlt1OAo+Rslkjv/aqKiTthKMaCKg2Gvd+uALDbDCDdY+UIk8ouadEB9fC3j52Y1i7SJZqGIgBRIS6kKieYrAAoEwITAcAswCvAMMAvwCTAI8AowCfACsAJwBTAEwCdAJwAPQA8ADUALwEAAKIAAAAOAAwAAAlpcGluZm8uaW8ABQAFAQAAAAAAKwAJCAMEAwMDAgMBAA0AGgAYCAQIBQgGBAEFAQIBBAMFAwIDAgIGAQYDACMAAAAKAAgABgAdABcAGAAQAAsACQhodHRwLzEuMQAzACYAJAAdACBguQbqNJNyamYxYcrBFpBP7pWv5TgZsP9gwGtMYNKVBQAxAAAAFwAA/wEAAQAALQACAQE=\")\n\tassert.NoError(t, err)\n\tindex = 0\n\treqAddr = \"222.222.222.222:443\"\n\tputback, err = sniffer.TCP(stream, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, *buf, putback)\n\tassert.Equal(t, \"ipinfo.io:443\", reqAddr)\n\n\t// Test unrecognized 1\n\t*buf = []byte(\"Wait It's All Ohio? Always Has Been.\")\n\tindex = 0\n\treqAddr = \"123.123.123.123:123\"\n\tputback, err = sniffer.TCP(stream, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, *buf, putback)\n\tassert.Equal(t, \"123.123.123.123:123\", reqAddr)\n\n\t// Test unrecognized 2\n\t*buf = []byte(\"\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\")\n\tindex = 0\n\treqAddr = \"45.45.45.45:45\"\n\tputback, err = sniffer.TCP(stream, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []byte(\"\\x01\\x02\\x03\"), putback)\n\tassert.Equal(t, \"45.45.45.45:45\", reqAddr)\n\n\t// Test timeout\n\tblockStream := &mockStream{}\n\tblockStream.EXPECT().SetReadDeadline(mock.Anything).Return(nil)\n\tblockStream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {\n\t\ttime.Sleep(2 * time.Second)\n\t\treturn 0, io.EOF\n\t})\n\treqAddr = \"66.66.66.66:66\"\n\tputback, err = sniffer.TCP(blockStream, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []byte{}, putback)\n\tassert.Equal(t, \"66.66.66.66:66\", reqAddr)\n}\n\nfunc TestSnifferUDP(t *testing.T) {\n\tsniffer := &Sniffer{\n\t\tTimeout:       1 * time.Second,\n\t\tRewriteDomain: false,\n\t}\n\n\t// Test QUIC\n\treqAddr := \"2.3.4.5:443\"\n\tpkt, 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\")\n\tassert.NoError(t, err)\n\terr = sniffer.UDP(pkt, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"www.notion.so:443\", reqAddr)\n\n\t// Test unrecognized\n\tpkt = []byte(\"oh my sweet summer child\")\n\treqAddr = \"90.90.90.90:90\"\n\terr = sniffer.UDP(pkt, &reqAddr)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"90.90.90.90:90\", reqAddr)\n}\n"
  },
  {
    "path": "extras/trafficlogger/http.go",
    "content": "package trafficlogger\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/apernet/hysteria/core/v2/server\"\n)\n\nconst (\n\tindexHTML = `<!DOCTYPE html><html lang=\"en\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <title>Hysteria Traffic Stats API Server</title> <style>body{font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; padding: 0; background-color: #f4f4f4;}.container{padding: 20px; background-color: #fff; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 5px;}</style></head><body> <div class=\"container\"> <p>This is a Hysteria Traffic Stats API server.</p><p>Check the documentation for usage.</p></div></body></html>`\n)\n\n// TrafficStatsServer implements both server.TrafficLogger and http.Handler\n// to provide a simple HTTP API to get the traffic stats per user.\ntype TrafficStatsServer interface {\n\tserver.TrafficLogger\n\thttp.Handler\n}\n\nfunc NewTrafficStatsServer(secret string) TrafficStatsServer {\n\treturn &trafficStatsServerImpl{\n\t\tStatsMap:  make(map[string]*trafficStatsEntry),\n\t\tKickMap:   make(map[string]struct{}),\n\t\tOnlineMap: make(map[string]int),\n\t\tSecret:    secret,\n\t}\n}\n\ntype TrafficPushRequest struct {\n\tData map[string][2]int64\n}\n\n// 定时提交用户流量情况\nfunc (s *trafficStatsServerImpl) PushTrafficToV2boardInterval(url string, interval time.Duration) {\n\tfmt.Println(\"用户流量情况监控已启动,提交周期为:\", interval)\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tif err := s.PushTrafficToV2board(url); err != nil {\n\t\t\tfmt.Println(\"用户流量信息提交失败:\", err)\n\t\t}\n\t}\n\n}\n\n// 向v2board 提交用户流量使用情况\nfunc (s *trafficStatsServerImpl) PushTrafficToV2board(url string) error {\n\ts.Mutex.Lock()         // 写锁，阻止其他操作 StatsMap 的并发访问\n\tdefer s.Mutex.Unlock() // 确保在函数退出时释放写锁\n\n\trequest := TrafficPushRequest{\n\t\tData: make(map[string][2]int64),\n\t}\n\tfor id, stats := range s.StatsMap {\n\t\trequest.Data[id] = [2]int64{int64(stats.Tx), int64(stats.Rx)}\n\t}\n\tif len(request.Data) == 0 {\n\t\treturn nil\n\t}\n\tjsonData, err := json.Marshal(request.Data)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp, err := http.Post(url, \"application/json\", bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\tfmt.Println(resp)\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"HTTP request failed with status code: \" + resp.Status)\n\t}\n\ts.StatsMap = make(map[string]*trafficStatsEntry)\n\n\treturn nil\n}\n\ntype trafficStatsServerImpl struct {\n\tMutex     sync.RWMutex\n\tStatsMap  map[string]*trafficStatsEntry\n\tOnlineMap map[string]int\n\tKickMap   map[string]struct{}\n\tSecret    string\n}\n\ntype trafficStatsEntry struct {\n\tTx uint64 `json:\"tx\"`\n\tRx uint64 `json:\"rx\"`\n}\n\nfunc (s *trafficStatsServerImpl) LogTraffic(id string, tx, rx uint64) (ok bool) {\n\ts.Mutex.Lock()\n\tdefer s.Mutex.Unlock()\n\n\t_, ok = s.KickMap[id]\n\tif ok {\n\t\tdelete(s.KickMap, id)\n\t\treturn false\n\t}\n\n\tentry, ok := s.StatsMap[id]\n\tif !ok {\n\t\tentry = &trafficStatsEntry{}\n\t\ts.StatsMap[id] = entry\n\t}\n\tentry.Tx += tx\n\tentry.Rx += rx\n\n\treturn true\n}\n\n// LogOnlineStateChanged updates the online state to the online map.\nfunc (s *trafficStatsServerImpl) LogOnlineState(id string, online bool) {\n\ts.Mutex.Lock()\n\tdefer s.Mutex.Unlock()\n\n\tif online {\n\t\ts.OnlineMap[id]++\n\t} else {\n\t\ts.OnlineMap[id]--\n\t\tif s.OnlineMap[id] <= 0 {\n\t\t\tdelete(s.OnlineMap, id)\n\t\t}\n\t}\n}\n\nfunc (s *trafficStatsServerImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif s.Secret != \"\" && r.Header.Get(\"Authorization\") != s.Secret {\n\t\thttp.Error(w, \"unauthorized\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\tif r.Method == http.MethodGet && r.URL.Path == \"/\" {\n\t\t_, _ = w.Write([]byte(indexHTML))\n\t\treturn\n\t}\n\tif r.Method == http.MethodGet && r.URL.Path == \"/traffic\" {\n\t\ts.getTraffic(w, r)\n\t\treturn\n\t}\n\tif r.Method == http.MethodPost && r.URL.Path == \"/kick\" {\n\t\ts.kick(w, r)\n\t\treturn\n\t}\n\tif r.Method == http.MethodGet && r.URL.Path == \"/online\" {\n\t\ts.getOnline(w, r)\n\t\treturn\n\t}\n\thttp.NotFound(w, r)\n}\n\nfunc (s *trafficStatsServerImpl) getTraffic(w http.ResponseWriter, r *http.Request) {\n\tbClear, _ := strconv.ParseBool(r.URL.Query().Get(\"clear\"))\n\tvar jb []byte\n\tvar err error\n\tif bClear {\n\t\ts.Mutex.Lock()\n\t\tjb, err = json.Marshal(s.StatsMap)\n\t\ts.StatsMap = make(map[string]*trafficStatsEntry)\n\t\ts.Mutex.Unlock()\n\t} else {\n\t\ts.Mutex.RLock()\n\t\tjb, err = json.Marshal(s.StatsMap)\n\t\ts.Mutex.RUnlock()\n\t}\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t_, _ = w.Write(jb)\n}\n\nfunc (s *trafficStatsServerImpl) getOnline(w http.ResponseWriter, r *http.Request) {\n\ts.Mutex.RLock()\n\tdefer s.Mutex.RUnlock()\n\n\tjb, err := json.Marshal(s.OnlineMap)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\t_, _ = w.Write(jb)\n}\n\nfunc (s *trafficStatsServerImpl) kick(w http.ResponseWriter, r *http.Request) {\n\tvar ids []string\n\terr := json.NewDecoder(r.Body).Decode(&ids)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\ts.Mutex.Lock()\n\tfor _, id := range ids {\n\t\ts.KickMap[id] = struct{}{}\n\t}\n\ts.Mutex.Unlock()\n\n\tw.WriteHeader(http.StatusOK)\n}\n\n// 踢出用户名单\nfunc (s *trafficStatsServerImpl) NewKick(id string) bool {\n\ts.Mutex.Lock()\n\ts.KickMap[id] = struct{}{}\n\ts.Mutex.Unlock()\n\treturn true\n}\n"
  },
  {
    "path": "extras/transport/udphop/addr.go",
    "content": "package udphop\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/apernet/hysteria/extras/v2/utils\"\n)\n\ntype InvalidPortError struct {\n\tPortStr string\n}\n\nfunc (e InvalidPortError) Error() string {\n\treturn fmt.Sprintf(\"%s is not a valid port number or range\", e.PortStr)\n}\n\n// UDPHopAddr contains an IP address and a list of ports.\ntype UDPHopAddr struct {\n\tIP      net.IP\n\tPorts   []uint16\n\tPortStr string\n}\n\nfunc (a *UDPHopAddr) Network() string {\n\treturn \"udphop\"\n}\n\nfunc (a *UDPHopAddr) String() string {\n\treturn net.JoinHostPort(a.IP.String(), a.PortStr)\n}\n\n// addrs returns a list of net.Addr's, one for each port.\nfunc (a *UDPHopAddr) addrs() ([]net.Addr, error) {\n\tvar addrs []net.Addr\n\tfor _, port := range a.Ports {\n\t\taddr := &net.UDPAddr{\n\t\t\tIP:   a.IP,\n\t\t\tPort: int(port),\n\t\t}\n\t\taddrs = append(addrs, addr)\n\t}\n\treturn addrs, nil\n}\n\nfunc ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) {\n\thost, portStr, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tip, err := net.ResolveIPAddr(\"ip\", host)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult := &UDPHopAddr{\n\t\tIP:      ip.IP,\n\t\tPortStr: portStr,\n\t}\n\n\tpu := utils.ParsePortUnion(portStr)\n\tif pu == nil {\n\t\treturn nil, InvalidPortError{portStr}\n\t}\n\tresult.Ports = pu.Ports()\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "extras/transport/udphop/conn.go",
    "content": "package udphop\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"net\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n)\n\nconst (\n\tpacketQueueSize = 1024\n\tudpBufferSize   = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough\n\n\tdefaultHopInterval = 30 * time.Second\n)\n\ntype udpHopPacketConn struct {\n\tAddr          net.Addr\n\tAddrs         []net.Addr\n\tHopInterval   time.Duration\n\tListenUDPFunc ListenUDPFunc\n\n\tconnMutex   sync.RWMutex\n\tprevConn    net.PacketConn\n\tcurrentConn net.PacketConn\n\taddrIndex   int\n\n\treadBufferSize  int\n\twriteBufferSize int\n\n\trecvQueue chan *udpPacket\n\tcloseChan chan struct{}\n\tclosed    bool\n\n\tbufPool sync.Pool\n}\n\ntype udpPacket struct {\n\tBuf  []byte\n\tN    int\n\tAddr net.Addr\n\tErr  error\n}\n\ntype ListenUDPFunc = func() (net.PacketConn, error)\n\nfunc NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc) (net.PacketConn, error) {\n\tif hopInterval == 0 {\n\t\thopInterval = defaultHopInterval\n\t} else if hopInterval < 5*time.Second {\n\t\treturn nil, errors.New(\"hop interval must be at least 5 seconds\")\n\t}\n\tif listenUDPFunc == nil {\n\t\tlistenUDPFunc = func() (net.PacketConn, error) {\n\t\t\treturn net.ListenUDP(\"udp\", nil)\n\t\t}\n\t}\n\taddrs, err := addr.addrs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcurConn, err := listenUDPFunc()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thConn := &udpHopPacketConn{\n\t\tAddr:          addr,\n\t\tAddrs:         addrs,\n\t\tHopInterval:   hopInterval,\n\t\tListenUDPFunc: listenUDPFunc,\n\t\tprevConn:      nil,\n\t\tcurrentConn:   curConn,\n\t\taddrIndex:     rand.Intn(len(addrs)),\n\t\trecvQueue:     make(chan *udpPacket, packetQueueSize),\n\t\tcloseChan:     make(chan struct{}),\n\t\tbufPool: sync.Pool{\n\t\t\tNew: func() interface{} {\n\t\t\t\treturn make([]byte, udpBufferSize)\n\t\t\t},\n\t\t},\n\t}\n\tgo hConn.recvLoop(curConn)\n\tgo hConn.hopLoop()\n\treturn hConn, nil\n}\n\nfunc (u *udpHopPacketConn) recvLoop(conn net.PacketConn) {\n\tfor {\n\t\tbuf := u.bufPool.Get().([]byte)\n\t\tn, addr, err := conn.ReadFrom(buf)\n\t\tif err != nil {\n\t\t\tu.bufPool.Put(buf)\n\t\t\tvar netErr net.Error\n\t\t\tif errors.As(err, &netErr) && netErr.Timeout() {\n\t\t\t\t// Only pass through timeout errors here, not permanent errors\n\t\t\t\t// like connection closed. Connection close is normal as we close\n\t\t\t\t// the old connection to exit this loop every time we hop.\n\t\t\t\tu.recvQueue <- &udpPacket{nil, 0, nil, netErr}\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tselect {\n\t\tcase u.recvQueue <- &udpPacket{buf, n, addr, nil}:\n\t\t\t// Packet successfully queued\n\t\tdefault:\n\t\t\t// Queue is full, drop the packet\n\t\t\tu.bufPool.Put(buf)\n\t\t}\n\t}\n}\n\nfunc (u *udpHopPacketConn) hopLoop() {\n\tticker := time.NewTicker(u.HopInterval)\n\tdefer ticker.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tu.hop()\n\t\tcase <-u.closeChan:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (u *udpHopPacketConn) hop() {\n\tu.connMutex.Lock()\n\tdefer u.connMutex.Unlock()\n\tif u.closed {\n\t\treturn\n\t}\n\tnewConn, err := u.ListenUDPFunc()\n\tif err != nil {\n\t\t// Could be temporary, just skip this hop\n\t\treturn\n\t}\n\t// We need to keep receiving packets from the previous connection,\n\t// because otherwise there will be packet loss due to the time gap\n\t// between we hop to a new port and the server acknowledges this change.\n\t// So we do the following:\n\t// Close prevConn,\n\t// move currentConn to prevConn,\n\t// set newConn as currentConn,\n\t// start recvLoop on newConn.\n\tif u.prevConn != nil {\n\t\t_ = u.prevConn.Close() // recvLoop for this conn will exit\n\t}\n\tu.prevConn = u.currentConn\n\tu.currentConn = newConn\n\t// Set buffer sizes if previously set\n\tif u.readBufferSize > 0 {\n\t\t_ = trySetReadBuffer(u.currentConn, u.readBufferSize)\n\t}\n\tif u.writeBufferSize > 0 {\n\t\t_ = trySetWriteBuffer(u.currentConn, u.writeBufferSize)\n\t}\n\tgo u.recvLoop(newConn)\n\t// Update addrIndex to a new random value\n\tu.addrIndex = rand.Intn(len(u.Addrs))\n}\n\nfunc (u *udpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {\n\tfor {\n\t\tselect {\n\t\tcase p := <-u.recvQueue:\n\t\t\tif p.Err != nil {\n\t\t\t\treturn 0, nil, p.Err\n\t\t\t}\n\t\t\t// Currently we do not check whether the packet is from\n\t\t\t// the server or not due to performance reasons.\n\t\t\tn := copy(b, p.Buf[:p.N])\n\t\t\tu.bufPool.Put(p.Buf)\n\t\t\treturn n, u.Addr, nil\n\t\tcase <-u.closeChan:\n\t\t\treturn 0, nil, net.ErrClosed\n\t\t}\n\t}\n}\n\nfunc (u *udpHopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {\n\tu.connMutex.RLock()\n\tdefer u.connMutex.RUnlock()\n\tif u.closed {\n\t\treturn 0, net.ErrClosed\n\t}\n\t// Skip the check for now, always write to the server,\n\t// for the same reason as in ReadFrom.\n\treturn u.currentConn.WriteTo(b, u.Addrs[u.addrIndex])\n}\n\nfunc (u *udpHopPacketConn) Close() error {\n\tu.connMutex.Lock()\n\tdefer u.connMutex.Unlock()\n\tif u.closed {\n\t\treturn nil\n\t}\n\t// Close prevConn and currentConn\n\t// Close closeChan to unblock ReadFrom & hopLoop\n\t// Set closed flag to true to prevent double close\n\tif u.prevConn != nil {\n\t\t_ = u.prevConn.Close()\n\t}\n\terr := u.currentConn.Close()\n\tclose(u.closeChan)\n\tu.closed = true\n\tu.Addrs = nil // For GC\n\treturn err\n}\n\nfunc (u *udpHopPacketConn) LocalAddr() net.Addr {\n\tu.connMutex.RLock()\n\tdefer u.connMutex.RUnlock()\n\treturn u.currentConn.LocalAddr()\n}\n\nfunc (u *udpHopPacketConn) SetDeadline(t time.Time) error {\n\tu.connMutex.RLock()\n\tdefer u.connMutex.RUnlock()\n\tif u.prevConn != nil {\n\t\t_ = u.prevConn.SetDeadline(t)\n\t}\n\treturn u.currentConn.SetDeadline(t)\n}\n\nfunc (u *udpHopPacketConn) SetReadDeadline(t time.Time) error {\n\tu.connMutex.RLock()\n\tdefer u.connMutex.RUnlock()\n\tif u.prevConn != nil {\n\t\t_ = u.prevConn.SetReadDeadline(t)\n\t}\n\treturn u.currentConn.SetReadDeadline(t)\n}\n\nfunc (u *udpHopPacketConn) SetWriteDeadline(t time.Time) error {\n\tu.connMutex.RLock()\n\tdefer u.connMutex.RUnlock()\n\tif u.prevConn != nil {\n\t\t_ = u.prevConn.SetWriteDeadline(t)\n\t}\n\treturn u.currentConn.SetWriteDeadline(t)\n}\n\n// UDP-specific methods below\n\nfunc (u *udpHopPacketConn) SetReadBuffer(bytes int) error {\n\tu.connMutex.Lock()\n\tdefer u.connMutex.Unlock()\n\tu.readBufferSize = bytes\n\tif u.prevConn != nil {\n\t\t_ = trySetReadBuffer(u.prevConn, bytes)\n\t}\n\treturn trySetReadBuffer(u.currentConn, bytes)\n}\n\nfunc (u *udpHopPacketConn) SetWriteBuffer(bytes int) error {\n\tu.connMutex.Lock()\n\tdefer u.connMutex.Unlock()\n\tu.writeBufferSize = bytes\n\tif u.prevConn != nil {\n\t\t_ = trySetWriteBuffer(u.prevConn, bytes)\n\t}\n\treturn trySetWriteBuffer(u.currentConn, bytes)\n}\n\nfunc (u *udpHopPacketConn) SyscallConn() (syscall.RawConn, error) {\n\tu.connMutex.RLock()\n\tdefer u.connMutex.RUnlock()\n\tsc, ok := u.currentConn.(syscall.Conn)\n\tif !ok {\n\t\treturn nil, errors.New(\"not supported\")\n\t}\n\treturn sc.SyscallConn()\n}\n\nfunc trySetReadBuffer(pc net.PacketConn, bytes int) error {\n\tsc, ok := pc.(interface {\n\t\tSetReadBuffer(bytes int) error\n\t})\n\tif ok {\n\t\treturn sc.SetReadBuffer(bytes)\n\t}\n\treturn nil\n}\n\nfunc trySetWriteBuffer(pc net.PacketConn, bytes int) error {\n\tsc, ok := pc.(interface {\n\t\tSetWriteBuffer(bytes int) error\n\t})\n\tif ok {\n\t\treturn sc.SetWriteBuffer(bytes)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "extras/utils/portunion.go",
    "content": "package utils\n\nimport (\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// PortUnion is a collection of multiple port ranges.\ntype PortUnion []PortRange\n\n// PortRange represents a range of ports.\n// Start and End are inclusive. [Start, End]\ntype PortRange struct {\n\tStart, End uint16\n}\n\n// ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion.\n// Returns nil if the input is invalid.\n// The returned PortUnion is guaranteed to be normalized.\nfunc ParsePortUnion(s string) PortUnion {\n\tif s == \"all\" || s == \"*\" {\n\t\t// Wildcard special case\n\t\treturn PortUnion{PortRange{0, 65535}}\n\t}\n\tvar result PortUnion\n\tportStrs := strings.Split(s, \",\")\n\tfor _, portStr := range portStrs {\n\t\tif strings.Contains(portStr, \"-\") {\n\t\t\t// Port range\n\t\t\tportRange := strings.Split(portStr, \"-\")\n\t\t\tif len(portRange) != 2 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tstart, err := strconv.ParseUint(portRange[0], 10, 16)\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tend, err := strconv.ParseUint(portRange[1], 10, 16)\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif start > end {\n\t\t\t\tstart, end = end, start\n\t\t\t}\n\t\t\tresult = append(result, PortRange{uint16(start), uint16(end)})\n\t\t} else {\n\t\t\t// Single port\n\t\t\tport, err := strconv.ParseUint(portStr, 10, 16)\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tresult = append(result, PortRange{uint16(port), uint16(port)})\n\t\t}\n\t}\n\tif result == nil {\n\t\treturn nil\n\t}\n\treturn result.Normalize()\n}\n\n// Normalize normalizes a PortUnion.\n// No overlapping ranges, ranges are sorted from low to high.\nfunc (u PortUnion) Normalize() PortUnion {\n\tif len(u) == 0 {\n\t\treturn u\n\t}\n\tsort.Slice(u, func(i, j int) bool {\n\t\tif u[i].Start == u[j].Start {\n\t\t\treturn u[i].End < u[j].End\n\t\t}\n\t\treturn u[i].Start < u[j].Start\n\t})\n\tnormalized := PortUnion{u[0]}\n\tfor _, current := range u[1:] {\n\t\tlast := &normalized[len(normalized)-1]\n\t\tif current.Start <= last.End+1 {\n\t\t\tif current.End > last.End {\n\t\t\t\tlast.End = current.End\n\t\t\t}\n\t\t} else {\n\t\t\tnormalized = append(normalized, current)\n\t\t}\n\t}\n\treturn normalized\n}\n\n// Ports returns all ports in the PortUnion as a slice.\nfunc (u PortUnion) Ports() []uint16 {\n\tvar ports []uint16\n\tfor _, r := range u {\n\t\tfor i := r.Start; i <= r.End; i++ {\n\t\t\tports = append(ports, i)\n\t\t}\n\t}\n\treturn ports\n}\n\n// Contains returns true if the PortUnion contains the given port.\nfunc (u PortUnion) Contains(port uint16) bool {\n\tfor _, r := range u {\n\t\tif port >= r.Start && port <= r.End {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "extras/utils/portunion_test.go",
    "content": "package utils\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestParsePortUnion(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\ts    string\n\t\twant PortUnion\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\ts:    \"\",\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"all 1\",\n\t\t\ts:    \"all\",\n\t\t\twant: PortUnion{{0, 65535}},\n\t\t},\n\t\t{\n\t\t\tname: \"all 2\",\n\t\t\ts:    \"*\",\n\t\t\twant: PortUnion{{0, 65535}},\n\t\t},\n\t\t{\n\t\t\tname: \"single port\",\n\t\t\ts:    \"1234\",\n\t\t\twant: PortUnion{{1234, 1234}},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple ports (unsorted)\",\n\t\t\ts:    \"5678,1234,9012\",\n\t\t\twant: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}},\n\t\t},\n\t\t{\n\t\t\tname: \"one range\",\n\t\t\ts:    \"1234-1240\",\n\t\t\twant: PortUnion{{1234, 1240}},\n\t\t},\n\t\t{\n\t\t\tname: \"one range (reversed)\",\n\t\t\ts:    \"1240-1234\",\n\t\t\twant: PortUnion{{1234, 1240}},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple ports and ranges (reversed, unsorted, overlapping)\",\n\t\t\ts:    \"5678,1200-1236,9100-9012,1234-1240\",\n\t\t\twant: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid 1\",\n\t\t\ts:    \"1234-\",\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid 2\",\n\t\t\ts:    \"1234-ggez\",\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid 3\",\n\t\t\ts:    \"233,\",\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid 4\",\n\t\t\ts:    \"1234-1240-1250\",\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid 5\",\n\t\t\ts:    \"-,,\",\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid 6\",\n\t\t\ts:    \"http\",\n\t\t\twant: nil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"ParsePortUnion() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "go.work.sum",
    "content": "cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=\ncloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=\ncloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=\ncloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=\ncloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=\ncloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=\ncloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=\ncloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=\ncloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=\ncloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=\ncloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=\ncloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=\ncloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=\ncloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=\ncloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=\ncloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=\ncloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU=\ndmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw=\ndmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=\ndmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0 h1:SPOUaucgtVls75mg+X7CXigS71EnsfVUK/2CgVrwqgw=\ndmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=\ndmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412 h1:GvWw74lx5noHocd+f6HBMXK6DuggBB1dhVkuGZbv7qM=\ndmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=\ndmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c h1:ivON6cwHK1OH26MZyWDCnbTRZZf0IhNsENoNAKFS1g4=\ndmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=\ngit.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:OR8VhtwhcAI3U48/rzBsVOuHi0zDPzYI1xASVcdSgR8=\ngit.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=\ngithub.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\ngithub.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=\ngithub.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=\ngithub.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=\ngithub.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=\ngithub.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q=\ngithub.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss=\ngithub.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=\ngithub.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=\ngithub.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=\ngithub.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=\ngithub.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=\ngithub.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M=\ngithub.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=\ngithub.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=\ngithub.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=\ngithub.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=\ngithub.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=\ngithub.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI=\ngithub.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad h1:EmNYJhPYy0pOFjCx2PrgtaBXmee0iUX9hLlxE1xHOJE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=\ngithub.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=\ngithub.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=\ngithub.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=\ngithub.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=\ngithub.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=\ngithub.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=\ngithub.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=\ngithub.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=\ngithub.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=\ngithub.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=\ngithub.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60=\ngithub.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=\ngithub.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=\ngithub.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=\ngithub.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=\ngithub.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=\ngithub.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=\ngithub.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI=\ngithub.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=\ngithub.com/hashicorp/consul/api v1.18.0 h1:R7PPNzTCeN6VuQNDwwhZWJvzCtGSrNpJqfb22h3yH9g=\ngithub.com/hashicorp/consul/api v1.18.0/go.mod h1:owRRGJ9M5xReDC5nfT8FTJrNAPbT4NM6p/k+d03q2v4=\ngithub.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=\ngithub.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=\ngithub.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=\ngithub.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8=\ngithub.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=\ngithub.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=\ngithub.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=\ngithub.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=\ngithub.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=\ngithub.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=\ngithub.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=\ngithub.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=\ngithub.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=\ngithub.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w=\ngithub.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=\ngithub.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=\ngithub.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=\ngithub.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=\ngithub.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=\ngithub.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk=\ngithub.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=\ngithub.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI=\ngithub.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=\ngithub.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw=\ngithub.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=\ngithub.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=\ngithub.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=\ngithub.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=\ngithub.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=\ngithub.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=\ngithub.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=\ngithub.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=\ngithub.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=\ngithub.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=\ngithub.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=\ngithub.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/sagikazarmark/crypt v0.9.0 h1:fipzMFW34hFUEc4D7fsLQFtE7yElkpgyS2zruedRdZk=\ngithub.com/sagikazarmark/crypt v0.9.0/go.mod h1:RnH7sEhxfdnPm1z+XMgSLjWTEIjyK4z2dw6+4vHTMuo=\ngithub.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=\ngithub.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=\ngithub.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI=\ngithub.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=\ngithub.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o=\ngithub.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=\ngithub.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64=\ngithub.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=\ngithub.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=\ngithub.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=\ngithub.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=\ngithub.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=\ngithub.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d h1:Yoy/IzG4lULT6qZg62sVC+qyBL8DQkmD2zv6i7OImrc=\ngithub.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=\ngithub.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c h1:UOk+nlt1BJtTcH15CT7iNO7YVWTfTv/DNwEAQHLIaDQ=\ngithub.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=\ngithub.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw=\ngithub.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=\ngithub.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20 h1:7pDq9pAMCQgRohFmd25X8hIH8VxmT3TaDm+r9LHxgBk=\ngithub.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=\ngithub.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9 h1:MPblCbqA5+z6XARjScMfz1TqtJC7TuTRj0U9VqIBs6k=\ngithub.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=\ngithub.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50 h1:crYRwvwjdVh1biHzzciFHe8DrZcYrVcZFlJtykhRctg=\ngithub.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=\ngithub.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc h1:eHRtZoIi6n9Wo1uR+RU44C247msLWwyA89hVKwRLkMk=\ngithub.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=\ngithub.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=\ngithub.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=\ngithub.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9 h1:fxoFD0in0/CBzXoyNhMTjvBZYW6ilSnTw7N7y/8vkmM=\ngithub.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=\ngithub.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191 h1:T4wuULTrzCKMFlg3HmKHgXAF8oStFb/+lOIupLV2v+o=\ngithub.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=\ngithub.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241 h1:Y+TeIabU8sJD10Qwd/zMty2/LEaT9GNDaA6nyZf+jgo=\ngithub.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=\ngithub.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122 h1:TQVQrsyNaimGwF7bIhzoVC9QkKm4KsWd8cECGzFx8gI=\ngithub.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=\ngithub.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2 h1:bu666BQci+y4S0tVRVjsHUeRon6vUXmsGBwdowgMrg4=\ngithub.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=\ngithub.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/AkPDU3AkqMxnMYL+imaqkpflHu73us8=\ngithub.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=\ngithub.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=\ngithub.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY=\ngithub.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=\ngithub.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ=\ngithub.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=\ngithub.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=\ngithub.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=\ngithub.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=\ngithub.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=\ngithub.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=\ngithub.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=\ngithub.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc=\ngithub.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=\ngithub.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k=\ngithub.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=\ngithub.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=\ngo.etcd.io/etcd/api/v3 v3.5.6 h1:Cy2qx3npLcYqTKqGJzMypnMv2tiRyifZJ17BlWIWA7A=\ngo.etcd.io/etcd/api/v3 v3.5.6/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.6 h1:TXQWYceBKqLp4sa87rcPs11SXxUA/mHwH975v+BDvLU=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.6/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=\ngo.etcd.io/etcd/client/v2 v2.305.6 h1:fIDR0p4KMjw01MJMfUIDWdQbjo06PD6CeYM5z4EHLi0=\ngo.etcd.io/etcd/client/v2 v2.305.6/go.mod h1:BHha8XJGe8vCIBfWBpbBLVZ4QjOIlfoouvOwydu63E0=\ngo.etcd.io/etcd/client/v3 v3.5.6 h1:coLs69PWCXE9G4FKquzNaSHrRyMCAXwF+IX1tAPVO8E=\ngo.etcd.io/etcd/client/v3 v3.5.6/go.mod h1:f6GRinRMCsFVv9Ht42EyY7nfsVGwrNO0WEoS2pRKzQk=\ngo.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM=\ngo4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=\ngolang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo=\ngolang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=\ngolang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=\ngolang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=\ngolang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=\ngolang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=\ngolang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=\ngolang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI=\ngolang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=\ngolang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=\ngolang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=\ngolang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=\ngolang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=\ngolang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=\ngolang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=\ngolang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=\ngolang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=\ngolang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=\ngolang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=\ngolang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=\ngolang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=\ngolang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngoogle.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=\ngoogle.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=\ngoogle.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=\ngoogle.golang.org/api v0.107.0 h1:I2SlFjD8ZWabaIFOfeEDg3pf0BHJDh6iYQ1ic3Yu/UU=\ngoogle.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=\ngoogle.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=\ngoogle.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY=\ngoogle.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=\ngoogle.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=\ngoogle.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=\ngoogle.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk=\ngoogle.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=\ngopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngrpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY=\ngrpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=\nhonnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=\nhonnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo=\nhonnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=\nrsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=\nrsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=\nrsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=\nsourcegraph.com/sourcegraph/go-diff v0.5.0 h1:eTiIR0CoWjGzJcnQ3OkhIl/b9GJovq4lSAVRt0ZFEG8=\nsourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=\nsourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=\nsourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=\n"
  },
  {
    "path": "hyperbole.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport argparse\nimport os\nimport re\nimport sys\nimport subprocess\nimport datetime\nimport shutil\n\n# Hyperbole is the official build script for Hysteria.\n# Available environment variables for controlling the build:\n#   - HY_APP_VERSION: App version\n#   - HY_APP_COMMIT: App commit hash\n#   - HY_APP_PLATFORMS: Platforms to build for (e.g. \"windows/amd64,linux/arm\")\n\n\nLOGO = \"\"\"\n░█░█░█░█░█▀█░█▀▀░█▀▄░█▀▄░█▀█░█░░░█▀▀\n░█▀█░░█░░█▀▀░█▀▀░█▀▄░█▀▄░█░█░█░░░█▀▀\n░▀░▀░░▀░░▀░░░▀▀▀░▀░▀░▀▀░░▀▀▀░▀▀▀░▀▀▀\n\"\"\"\n\nDESC = \"Hyperbole is the official build script for Hysteria.\"\n\nBUILD_DIR = \"build\"\n\nCORE_SRC_DIR = \"./core\"\nEXTRAS_SRC_DIR = \"./extras\"\nAPP_SRC_DIR = \"./app\"\nAPP_SRC_CMD_PKG = \"github.com/apernet/hysteria/app/v2/cmd\"\n\nMODULE_SRC_DIRS = [CORE_SRC_DIR, EXTRAS_SRC_DIR, APP_SRC_DIR]\n\nARCH_ALIASES = {\n    \"arm\": {\n        \"GOARCH\": \"arm\",\n        \"GOARM\": \"7\",\n    },\n    \"armv5\": {\n        \"GOARCH\": \"arm\",\n        \"GOARM\": \"5\",\n    },\n    \"armv6\": {\n        \"GOARCH\": \"arm\",\n        \"GOARM\": \"6\",\n    },\n    \"armv7\": {\n        \"GOARCH\": \"arm\",\n        \"GOARM\": \"7\",\n    },\n    \"mips\": {\n        \"GOARCH\": \"mips\",\n        \"GOMIPS\": \"\",\n    },\n    \"mipsle\": {\n        \"GOARCH\": \"mipsle\",\n        \"GOMIPS\": \"\",\n    },\n    \"mips-sf\": {\n        \"GOARCH\": \"mips\",\n        \"GOMIPS\": \"softfloat\",\n    },\n    \"mipsle-sf\": {\n        \"GOARCH\": \"mipsle\",\n        \"GOMIPS\": \"softfloat\",\n    },\n    \"amd64\": {\n        \"GOARCH\": \"amd64\",\n        \"GOAMD64\": \"\",\n    },\n    \"amd64-avx\": {\n        \"GOARCH\": \"amd64\",\n        \"GOAMD64\": \"v3\",\n    },\n}\n\n\ndef check_command(args):\n    try:\n        subprocess.check_call(\n            args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL\n        )\n        return True\n    except Exception:\n        return False\n\n\ndef check_build_env():\n    if not check_command([\"git\", \"--version\"]):\n        print(\"Git is not installed. Please install Git and try again.\")\n        return False\n    if not check_command([\"git\", \"rev-parse\", \"--is-inside-work-tree\"]):\n        print(\"Not in a Git repository. Please go to the project root and try again.\")\n        return False\n    if not check_command([\"go\", \"version\"]):\n        print(\"Go is not installed. Please install Go and try again.\")\n        return False\n    return True\n\n\ndef get_app_version():\n    app_version = os.environ.get(\"HY_APP_VERSION\")\n    if not app_version:\n        try:\n            output = (\n                subprocess.check_output(\n                    [\"git\", \"describe\", \"--tags\", \"--always\", \"--match\", \"app/v*\"]\n                )\n                .decode()\n                .strip()\n            )\n            app_version = output.split(\"/\")[-1]\n        except Exception:\n            app_version = \"Unknown\"\n    return app_version\n\n\ndef get_app_version_code(str=None):\n    if not str:\n        str = get_app_version()\n\n    match = re.search(r\"v(\\d+)\\.(\\d+)\\.(\\d+)\", str)\n\n    if match:\n        major, minor, patch = match.groups()\n        major = major.zfill(2)[:2]\n        minor = minor.zfill(2)[:2]\n        patch = patch.zfill(2)[:2]\n        return int(f\"{major}{minor}{patch[:2]}\")\n    else:\n        return 0\n\n\ndef get_app_commit():\n    app_commit = os.environ.get(\"HY_APP_COMMIT\")\n    if not app_commit:\n        try:\n            app_commit = (\n                subprocess.check_output([\"git\", \"rev-parse\", \"HEAD\"]).decode().strip()\n            )\n        except Exception:\n            app_commit = \"Unknown\"\n    return app_commit\n\n\ndef get_current_os_arch():\n    d_os = subprocess.check_output([\"go\", \"env\", \"GOOS\"]).decode().strip()\n    d_arch = subprocess.check_output([\"go\", \"env\", \"GOARCH\"]).decode().strip()\n    return (d_os, d_arch)\n\n\ndef get_app_platforms():\n    platforms = os.environ.get(\"HY_APP_PLATFORMS\")\n    if not platforms:\n        d_os, d_arch = get_current_os_arch()\n        return [(d_os, d_arch)]\n\n    result = []\n    for platform in platforms.split(\",\"):\n        platform = platform.strip()\n        if not platform:\n            continue\n        parts = platform.split(\"/\")\n        if len(parts) != 2:\n            continue\n        result.append((parts[0], parts[1]))\n    return result\n\n\ndef cmd_build(pprof=False, release=False, race=False):\n    if not check_build_env():\n        return\n\n    os.makedirs(BUILD_DIR, exist_ok=True)\n\n    app_version = get_app_version()\n    app_date = datetime.datetime.utcnow().strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n    app_commit = get_app_commit()\n\n    ldflags = [\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appVersion=\" + app_version,\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appDate=\" + app_date,\n        \"-X\",\n        APP_SRC_CMD_PKG\n        + \".appType=\"\n        + (\"release\" if release else \"dev\")\n        + (\"-pprof\" if pprof else \"\"),\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appCommit=\" + app_commit,\n    ]\n    if release:\n        ldflags.append(\"-s\")\n        ldflags.append(\"-w\")\n\n    for os_name, arch in get_app_platforms():\n        print(\"Building for %s/%s...\" % (os_name, arch))\n\n        out_name = \"hysteria-%s-%s\" % (os_name, arch)\n        if os_name == \"windows\":\n            out_name += \".exe\"\n\n        env = os.environ.copy()\n        env[\"GOOS\"] = os_name\n        if arch in ARCH_ALIASES:\n            for k, v in ARCH_ALIASES[arch].items():\n                env[k] = v\n        else:\n            env[\"GOARCH\"] = arch\n        if os_name == \"android\":\n            env[\"CGO_ENABLED\"] = \"1\"\n            ANDROID_NDK_HOME = (\n                os.environ.get(\"ANDROID_NDK_HOME\")\n                + \"/toolchains/llvm/prebuilt/linux-x86_64/bin\"\n            )\n            if arch == \"arm64\":\n                env[\"CC\"] = ANDROID_NDK_HOME + \"/aarch64-linux-android29-clang\"\n            elif arch == \"armv7\":\n                env[\"CC\"] = ANDROID_NDK_HOME + \"/armv7a-linux-androideabi29-clang\"\n            elif arch == \"386\":\n                env[\"CC\"] = ANDROID_NDK_HOME + \"/i686-linux-android29-clang\"\n            elif arch == \"amd64\":\n                env[\"CC\"] = ANDROID_NDK_HOME + \"/x86_64-linux-android29-clang\"\n            else:\n                print(\"Unsupported arch for android: %s\" % arch)\n                return\n        else:\n            env[\"CGO_ENABLED\"] = \"1\" if race else \"0\"  # Race detector requires cgo\n\n        plat_ldflags = ldflags.copy()\n        plat_ldflags.append(\"-X\")\n        plat_ldflags.append(APP_SRC_CMD_PKG + \".appPlatform=\" + os_name)\n        plat_ldflags.append(\"-X\")\n        plat_ldflags.append(APP_SRC_CMD_PKG + \".appArch=\" + arch)\n\n        cmd = [\n            \"go\",\n            \"build\",\n            \"-o\",\n            os.path.join(BUILD_DIR, out_name),\n            \"-ldflags\",\n            \" \".join(plat_ldflags),\n        ]\n        if pprof:\n            cmd.append(\"-tags\")\n            cmd.append(\"pprof\")\n        if race:\n            cmd.append(\"-race\")\n        if release:\n            cmd.append(\"-trimpath\")\n        cmd.append(APP_SRC_DIR)\n\n        try:\n            subprocess.check_call(cmd, env=env)\n        except Exception:\n            print(\"Failed to build for %s/%s\" % (os_name, arch))\n            sys.exit(1)\n\n        print(\"Built %s\" % out_name)\n\n\ndef cmd_run(args, pprof=False, race=False):\n    if not check_build_env():\n        return\n\n    app_version = get_app_version()\n    app_date = datetime.datetime.utcnow().strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n    app_commit = get_app_commit()\n\n    current_os, current_arch = get_current_os_arch()\n\n    ldflags = [\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appVersion=\" + app_version,\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appDate=\" + app_date,\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appType=dev-run\",\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appCommit=\" + app_commit,\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appPlatform=\" + current_os,\n        \"-X\",\n        APP_SRC_CMD_PKG + \".appArch=\" + current_arch,\n    ]\n\n    cmd = [\"go\", \"run\", \"-ldflags\", \" \".join(ldflags)]\n    if pprof:\n        cmd.append(\"-tags\")\n        cmd.append(\"pprof\")\n    if race:\n        cmd.append(\"-race\")\n    cmd.append(APP_SRC_DIR)\n    cmd.extend(args)\n\n    try:\n        subprocess.check_call(cmd)\n    except KeyboardInterrupt:\n        pass\n    except subprocess.CalledProcessError as e:\n        # Pass through the exit code\n        sys.exit(e.returncode)\n\n\ndef cmd_format():\n    if not check_command([\"gofumpt\", \"-version\"]):\n        print(\"gofumpt is not installed. Please install gofumpt and try again.\")\n        return\n\n    try:\n        subprocess.check_call([\"gofumpt\", \"-w\", \"-l\", \"-extra\", \".\"])\n    except Exception:\n        print(\"Failed to format code\")\n\n\ndef cmd_mockgen():\n    if not check_command([\"mockery\", \"--version\"]):\n        print(\"mockery is not installed. Please install mockery and try again.\")\n        return\n\n    for dirpath, dirnames, filenames in os.walk(\".\"):\n        dirnames[:] = [d for d in dirnames if not d.startswith(\".\")]\n        if \".mockery.yaml\" in filenames:\n            print(\"Generating mocks for %s...\" % dirpath)\n            try:\n                subprocess.check_call([\"mockery\"], cwd=dirpath)\n            except Exception:\n                print(\"Failed to generate mocks for %s\" % dirpath)\n\n\ndef cmd_protogen():\n    if not check_command([\"protoc\", \"--version\"]):\n        print(\"protoc is not installed. Please install protoc and try again.\")\n        return\n\n    for dirpath, dirnames, filenames in os.walk(\".\"):\n        dirnames[:] = [d for d in dirnames if not d.startswith(\".\")]\n        proto_files = [f for f in filenames if f.endswith(\".proto\")]\n\n        if len(proto_files) > 0:\n            for proto_file in proto_files:\n                print(\"Generating protobuf for %s...\" % proto_file)\n                try:\n                    subprocess.check_call(\n                        [\"protoc\", \"--go_out=paths=source_relative:.\", proto_file],\n                        cwd=dirpath,\n                    )\n                except Exception:\n                    print(\"Failed to generate protobuf for %s\" % proto_file)\n\n\ndef cmd_tidy():\n    if not check_build_env():\n        return\n\n    for dir in MODULE_SRC_DIRS:\n        print(\"Tidying %s...\" % dir)\n        try:\n            subprocess.check_call([\"go\", \"mod\", \"tidy\"], cwd=dir)\n        except Exception:\n            print(\"Failed to tidy %s\" % dir)\n\n    print(\"Syncing go work...\")\n    try:\n        subprocess.check_call([\"go\", \"work\", \"sync\"])\n    except Exception:\n        print(\"Failed to sync go work\")\n\n\ndef cmd_test(module=None):\n    if not check_build_env():\n        return\n\n    if module:\n        print(\"Testing %s...\" % module)\n        try:\n            subprocess.check_call([\"go\", \"test\", \"-v\", \"./...\"], cwd=module)\n        except Exception:\n            print(\"Failed to test %s\" % module)\n    else:\n        for dir in MODULE_SRC_DIRS:\n            print(\"Testing %s...\" % dir)\n            try:\n                subprocess.check_call([\"go\", \"test\", \"-v\", \"./...\"], cwd=dir)\n            except Exception:\n                print(\"Failed to test %s\" % dir)\n\n\ndef cmd_publish(urgent=False):\n    import requests\n\n    if not check_build_env():\n        return\n\n    app_version = get_app_version()\n    app_version_code = get_app_version_code(app_version)\n    if app_version_code == 0:\n        print(\"Invalid app version\")\n        return\n\n    payload = {\n        \"code\": app_version_code,\n        \"ver\": app_version,\n        \"chan\": \"release\",\n        \"url\": \"https://github.com/apernet/hysteria/releases\",\n        \"urgent\": urgent,\n    }\n    headers = {\n        \"Content-Type\": \"application/json\",\n        \"Authorization\": os.environ.get(\"HY_API_POST_KEY\"),\n    }\n    resp = requests.post(\"https://api.hy2.io/v1/update\", json=payload, headers=headers)\n\n    if resp.status_code == 200:\n        print(\"Published %s\" % app_version)\n    else:\n        print(\"Failed to publish %s, status code: %d\" % (app_version, resp.status_code))\n\n\ndef cmd_clean():\n    shutil.rmtree(BUILD_DIR, ignore_errors=True)\n\n\ndef cmd_about():\n    print(LOGO)\n    print(DESC)\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n\n    p_cmd = parser.add_subparsers(dest=\"command\")\n    p_cmd.required = True\n\n    # Run\n    p_run = p_cmd.add_parser(\"run\", help=\"Run the app\")\n    p_run.add_argument(\n        \"-p\", \"--pprof\", action=\"store_true\", help=\"Run with pprof enabled\"\n    )\n    p_run.add_argument(\n        \"-d\", \"--race\", action=\"store_true\", help=\"Build with data race detection\"\n    )\n    p_run.add_argument(\"args\", nargs=argparse.REMAINDER)\n\n    # Build\n    p_build = p_cmd.add_parser(\"build\", help=\"Build the app\")\n    p_build.add_argument(\n        \"-p\", \"--pprof\", action=\"store_true\", help=\"Build with pprof enabled\"\n    )\n    p_build.add_argument(\n        \"-r\", \"--release\", action=\"store_true\", help=\"Build a release version\"\n    )\n    p_build.add_argument(\n        \"-d\", \"--race\", action=\"store_true\", help=\"Build with data race detection\"\n    )\n\n    # Format\n    p_cmd.add_parser(\"format\", help=\"Format the code\")\n\n    # Mockgen\n    p_cmd.add_parser(\"mockgen\", help=\"Generate mock interfaces\")\n\n    # Protogen\n    p_cmd.add_parser(\"protogen\", help=\"Generate protobuf interfaces\")\n\n    # Tidy\n    p_cmd.add_parser(\"tidy\", help=\"Tidy the go modules\")\n\n    # Test\n    p_test = p_cmd.add_parser(\"test\", help=\"Test the code\")\n    p_test.add_argument(\"module\", nargs=\"?\", help=\"Module to test\")\n\n    # Publish\n    p_pub = p_cmd.add_parser(\"publish\", help=\"Publish the current version\")\n    p_pub.add_argument(\n        \"-u\", \"--urgent\", action=\"store_true\", help=\"Publish as an urgent update\"\n    )\n\n    # Clean\n    p_cmd.add_parser(\"clean\", help=\"Clean the build directory\")\n\n    # About\n    p_cmd.add_parser(\"about\", help=\"Print about information\")\n\n    args = parser.parse_args()\n\n    if args.command == \"run\":\n        cmd_run(args.args, args.pprof, args.race)\n    elif args.command == \"build\":\n        cmd_build(args.pprof, args.release, args.race)\n    elif args.command == \"format\":\n        cmd_format()\n    elif args.command == \"mockgen\":\n        cmd_mockgen()\n    elif args.command == \"protogen\":\n        cmd_protogen()\n    elif args.command == \"tidy\":\n        cmd_tidy()\n    elif args.command == \"test\":\n        cmd_test(args.module)\n    elif args.command == \"publish\":\n        cmd_publish(args.urgent)\n    elif args.command == \"clean\":\n        cmd_clean()\n    elif args.command == \"about\":\n        cmd_about()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "platforms.txt",
    "content": "# This file controls what platform/architecture combinations we build for a release.\n\n# Windows\nwindows/amd64\nwindows/amd64-avx\nwindows/386\nwindows/arm64\n\n# macOS\ndarwin/amd64\ndarwin/amd64-avx\ndarwin/arm64\n\n# Linux\nlinux/amd64\nlinux/amd64-avx\nlinux/386\nlinux/arm\nlinux/armv5\nlinux/arm64\nlinux/s390x\nlinux/mipsle\nlinux/mipsle-sf\nlinux/riscv64\n\n# Android\nandroid/386\nandroid/amd64\nandroid/armv7\nandroid/arm64\n\n# FreeBSD\nfreebsd/amd64\nfreebsd/amd64-avx\nfreebsd/386\nfreebsd/arm\nfreebsd/arm64\n"
  },
  {
    "path": "requirements.txt",
    "content": "blinker==1.8.2\ncffi==1.17.0\nclick==8.1.7\ncryptography==43.0.0\nFlask==3.0.3\nitsdangerous==2.2.0\nJinja2==3.1.4\nMarkupSafe==2.1.5\npycparser==2.22\nPySocks==1.7.1\nWerkzeug==3.0.4\n"
  },
  {
    "path": "scripts/_redirects",
    "content": "/ /install_server.sh 301\n"
  },
  {
    "path": "scripts/install_server.sh",
    "content": "#!/usr/bin/env bash\n#\n# install_server.sh - hysteria server install script\n# Try `install_server.sh --help` for usage.\n#\n# SPDX-License-Identifier: MIT\n# Copyright (c) 2023 Aperture Internet Laboratory\n#\n\nset -e\n\n\n###\n# SCRIPT CONFIGURATION\n###\n\n# Basename of this script\nSCRIPT_NAME=\"$(basename \"$0\")\"\n\n# Command line arguments of this script\nSCRIPT_ARGS=(\"$@\")\n\n# Path for installing executable\nEXECUTABLE_INSTALL_PATH=\"/usr/local/bin/hysteria\"\n\n# Paths to install systemd files\nSYSTEMD_SERVICES_DIR=\"/etc/systemd/system\"\n\n# Directory to store hysteria config file\nCONFIG_DIR=\"/etc/hysteria\"\n\n# URLs of GitHub\nREPO_URL=\"https://github.com/apernet/hysteria\"\n\n# URL of Hysteria 2 API\nHY2_API_BASE_URL=\"https://api.hy2.io/v1\"\n\n# curl command line flags.\n# To using a proxy, please specify ALL_PROXY in the environ variable, such like:\n# export ALL_PROXY=socks5h://192.0.2.1:1080\nCURL_FLAGS=(-L -f -q --retry 5 --retry-delay 10 --retry-max-time 60)\n\n\n###\n# AUTO DETECTED GLOBAL VARIABLE\n###\n\n# Package manager\nPACKAGE_MANAGEMENT_INSTALL=\"${PACKAGE_MANAGEMENT_INSTALL:-}\"\n\n# Operating System of current machine, supported: linux\nOPERATING_SYSTEM=\"${OPERATING_SYSTEM:-}\"\n\n# Architecture of current machine, supported: 386, amd64, arm, arm64, mipsle, s390x\nARCHITECTURE=\"${ARCHITECTURE:-}\"\n\n# User for running hysteria\nHYSTERIA_USER=\"${HYSTERIA_USER:-}\"\n\n# Directory for ACME certificates storage\nHYSTERIA_HOME_DIR=\"${HYSTERIA_HOME_DIR:-}\"\n\n# SELinux context of systemd unit files\nSECONTEXT_SYSTEMD_UNIT=\"${SECONTEXT_SYSTEMD_UNIT:-}\"\n\n\n###\n# ARGUMENTS\n###\n\n# Supported operation: install, remove, check_update\nOPERATION=\n\n# User specified version to install\nVERSION=\n\n# Force install even if installed\nFORCE=\n\n# User specified binary to install\nLOCAL_FILE=\n\n\n###\n# COMMAND REPLACEMENT & UTILITIES\n###\n\nhas_command() {\n  local _command=$1\n\n  type -P \"$_command\" > /dev/null 2>&1\n}\n\ncurl() {\n  command curl \"${CURL_FLAGS[@]}\" \"$@\"\n}\n\nmktemp() {\n  command mktemp \"$@\" \"/tmp/hyservinst.XXXXXXXXXX\"\n}\n\ntput() {\n  if has_command tput; then\n    command tput \"$@\"\n  fi\n}\n\ntred() {\n  tput setaf 1\n}\n\ntgreen() {\n  tput setaf 2\n}\n\ntyellow() {\n  tput setaf 3\n}\n\ntblue() {\n  tput setaf 4\n}\n\ntaoi() {\n  tput setaf 6\n}\n\ntbold() {\n  tput bold\n}\n\ntreset() {\n  tput sgr0\n}\n\nnote() {\n  local _msg=\"$1\"\n\n  echo -e \"$SCRIPT_NAME: $(tbold)note: $_msg$(treset)\"\n}\n\nwarning() {\n  local _msg=\"$1\"\n\n  echo -e \"$SCRIPT_NAME: $(tyellow)warning: $_msg$(treset)\"\n}\n\nerror() {\n  local _msg=\"$1\"\n\n  echo -e \"$SCRIPT_NAME: $(tred)error: $_msg$(treset)\"\n}\n\nhas_prefix() {\n    local _s=\"$1\"\n    local _prefix=\"$2\"\n\n    if [[ -z \"$_prefix\" ]]; then\n        return 0\n    fi\n\n    if [[ -z \"$_s\" ]]; then\n        return 1\n    fi\n\n    [[ \"x$_s\" != \"x${_s#\"$_prefix\"}\" ]]\n}\n\ngenerate_random_password() {\n  dd if=/dev/random bs=18 count=1 status=none | base64\n}\n\nsystemctl() {\n  if [[ \"x$FORCE_NO_SYSTEMD\" == \"x2\" ]] || ! has_command systemctl; then\n    warning \"Ignored systemd command: systemctl $@\"\n    return\n  fi\n\n  command systemctl \"$@\"\n}\n\nchcon() {\n  if ! has_command chcon || [[ \"x$FORCE_NO_SELINUX\" == \"x1\" ]]; then\n    return\n  fi\n\n  command chcon \"$@\"\n}\n\nget_systemd_version() {\n  if ! has_command systemctl; then\n    return\n  fi\n\n  command systemctl --version | head -1 | cut -d ' ' -f 2\n}\n\nsystemd_unit_working_directory() {\n  local _systemd_version=\"$(get_systemd_version || true)\"\n\n  # WorkingDirectory=~ requires systemd v227 or later.\n  # (released on Oct 2015, only CentOS 7 use an earlier version)\n  # ref: systemd/systemd@5f5d8eab1f2f5f5e088bc301533b3e4636de96c7\n  if [[ -n \"$_systemd_version\" && \"$_systemd_version\" -lt \"227\" ]]; then\n    echo \"$HYSTERIA_HOME_DIR\"\n    return\n  fi\n\n  echo \"~\"\n}\n\nget_selinux_context() {\n  local _file=\"$1\"\n\n  local _lsres=\"$(ls -dZ \"$_file\" | head -1)\"\n  local _sectx=''\n  case \"$(echo \"$_lsres\" | wc -w)\" in\n    2)\n      _sectx=\"$(echo \"$_lsres\" | cut -d ' ' -f 1)\"\n      ;;\n    5)\n      _sectx=\"$(echo \"$_lsres\" | cut -d ' ' -f 4)\"\n      ;;\n    *)\n      ;;\n  esac\n\n  if [[ \"x$_sectx\" == \"x?\" ]]; then\n    _sectx=\"\"\n  fi\n\n  echo \"$_sectx\"\n}\n\nshow_argument_error_and_exit() {\n  local _error_msg=\"$1\"\n\n  error \"$_error_msg\"\n  echo \"Try \\\"$0 --help\\\" for usage.\" >&2\n  exit 22\n}\n\ninstall_content() {\n  local _install_flags=\"$1\"\n  local _content=\"$2\"\n  local _destination=\"$3\"\n  local _overwrite=\"$4\"\n\n  local _tmpfile=\"$(mktemp)\"\n\n  echo -ne \"Install $_destination ... \"\n  echo \"$_content\" > \"$_tmpfile\"\n  if [[ -z \"$_overwrite\" && -e \"$_destination\" ]]; then\n    echo -e \"exists\"\n  elif install \"$_install_flags\" \"$_tmpfile\" \"$_destination\"; then\n    echo -e \"ok\"\n  fi\n\n  rm -f \"$_tmpfile\"\n}\n\nremove_file() {\n  local _target=\"$1\"\n\n  echo -ne \"Remove $_target ... \"\n  if rm \"$_target\"; then\n    echo -e \"ok\"\n  fi\n}\n\nexec_sudo() {\n  # exec sudo with configurable environ preserved.\n  local _saved_ifs=\"$IFS\"\n  IFS=$'\\n'\n  local _preserved_env=(\n    $(env | grep \"^PACKAGE_MANAGEMENT_INSTALL=\" || true)\n    $(env | grep \"^OPERATING_SYSTEM=\" || true)\n    $(env | grep \"^ARCHITECTURE=\" || true)\n    $(env | grep \"^HYSTERIA_\\w*=\" || true)\n    $(env | grep \"^SECONTEXT_SYSTEMD_UNIT=\" || true)\n    $(env | grep \"^FORCE_\\w*=\" || true)\n  )\n  IFS=\"$_saved_ifs\"\n\n  exec sudo env \\\n    \"${_preserved_env[@]}\" \\\n    \"$@\"\n}\n\ndetect_package_manager() {\n  if [[ -n \"$PACKAGE_MANAGEMENT_INSTALL\" ]]; then\n    return 0\n  fi\n\n  if has_command apt; then\n    apt update\n    PACKAGE_MANAGEMENT_INSTALL='apt -y --no-install-recommends install'\n    return 0\n  fi\n\n  if has_command dnf; then\n    PACKAGE_MANAGEMENT_INSTALL='dnf -y install'\n    return 0\n  fi\n\n  if has_command yum; then\n    PACKAGE_MANAGEMENT_INSTALL='yum -y install'\n    return 0\n  fi\n\n  if has_command zypper; then\n    PACKAGE_MANAGEMENT_INSTALL='zypper install -y --no-recommends'\n    return 0\n  fi\n\n  if has_command pacman; then\n    PACKAGE_MANAGEMENT_INSTALL='pacman -Syu --noconfirm'\n    return 0\n  fi\n\n  return 1\n}\n\ninstall_software() {\n  local _package_name=\"$1\"\n\n  if ! detect_package_manager; then\n    error \"Supported package manager is not detected, please install the following package manually:\"\n    echo\n    echo -e \"\\t* $_package_name\"\n    echo\n    exit 65\n  fi\n\n  echo \"Installing missing dependence '$_package_name' with '$PACKAGE_MANAGEMENT_INSTALL' ... \"\n  if $PACKAGE_MANAGEMENT_INSTALL \"$_package_name\"; then\n    echo \"ok\"\n  else\n    error \"Cannot install '$_package_name' with detected package manager, please install it manually.\"\n    exit 65\n  fi\n}\n\nis_user_exists() {\n  local _user=\"$1\"\n\n  id \"$_user\" > /dev/null 2>&1\n}\n\nrerun_with_sudo() {\n  if ! has_command sudo; then\n    return 13\n  fi\n\n  local _target_script\n\n  if has_prefix \"$0\" \"/dev/\" || has_prefix \"$0\" \"/proc/\"; then\n    local _tmp_script=\"$(mktemp)\"\n    chmod +x \"$_tmp_script\"\n\n    if has_command curl; then\n      curl -o \"$_tmp_script\" 'https://get.hy2.sh/'\n    elif has_command wget; then\n      wget -O \"$_tmp_script\" 'https://get.hy2.sh'\n    else\n      return 127\n    fi\n\n    _target_script=\"$_tmp_script\"\n  else\n    _target_script=\"$0\"\n  fi\n\n  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.\"\n  exec_sudo \"$_target_script\" \"${SCRIPT_ARGS[@]}\"\n}\n\ncheck_permission() {\n  if [[ \"$UID\" -eq '0' ]]; then\n    return\n  fi\n\n  note \"The user running this script is not root.\"\n\n  case \"$FORCE_NO_ROOT\" in\n    '1')\n      warning \"FORCE_NO_ROOT=1 detected, we will proceed without root, but you may get insufficient privileges errors.\"\n      ;;\n    *)\n      if ! rerun_with_sudo; then\n        error \"Please run this script with root or specify FORCE_NO_ROOT=1 to force this script to run as the current user.\"\n        exit 13\n      fi\n      ;;\n  esac\n}\n\ncheck_environment_operating_system() {\n  if [[ -n \"$OPERATING_SYSTEM\" ]]; then\n    warning \"OPERATING_SYSTEM=$OPERATING_SYSTEM detected, operating system detection will not be performed.\"\n    return\n  fi\n\n  if [[ \"x$(uname)\" == \"xLinux\" ]]; then\n    OPERATING_SYSTEM=linux\n    return\n  fi\n\n  error \"This script only supports Linux.\"\n  note \"Specify OPERATING_SYSTEM=[linux|darwin|freebsd|windows] to bypass this check and force this script to run on this $(uname).\"\n  exit 95\n}\n\ncheck_environment_architecture() {\n  if [[ -n \"$ARCHITECTURE\" ]]; then\n    warning \"ARCHITECTURE=$ARCHITECTURE detected, architecture detection will not be performed.\"\n    return\n  fi\n\n  case \"$(uname -m)\" in\n    'i386' | 'i686')\n      ARCHITECTURE='386'\n      ;;\n    'amd64' | 'x86_64')\n      ARCHITECTURE='amd64'\n      ;;\n    'armv5tel' | 'armv6l' | 'armv7' | 'armv7l')\n      ARCHITECTURE='arm'\n      ;;\n    'armv8' | 'aarch64')\n      ARCHITECTURE='arm64'\n      ;;\n    'mips' | 'mipsle' | 'mips64' | 'mips64le')\n      ARCHITECTURE='mipsle'\n      ;;\n    's390x')\n      ARCHITECTURE='s390x'\n      ;;\n    *)\n      error \"The architecture '$(uname -a)' is not supported.\"\n      note \"Specify ARCHITECTURE=<architecture> to bypass this check and force this script to run on this $(uname -m).\"\n      exit 8\n      ;;\n  esac\n}\n\ncheck_environment_systemd() {\n  if [[ -d \"/run/systemd/system\" ]] || grep -q systemd <(ls -l /sbin/init); then\n    return\n  fi\n\n  case \"$FORCE_NO_SYSTEMD\" in\n    '1')\n      warning \"FORCE_NO_SYSTEMD=1, we will proceed as normal even if systemd is not detected.\"\n      ;;\n    '2')\n      warning \"FORCE_NO_SYSTEMD=2, we will proceed but skip all systemd related commands.\"\n      ;;\n    *)\n      error \"This script only supports Linux distributions with systemd.\"\n      note \"Specify FORCE_NO_SYSTEMD=1 to disable this check and force this script to run as if systemd exists.\"\n      note \"Specify FORCE_NO_SYSTEMD=2 to disable this check and skip all systemd related commands.\"\n      ;;\n  esac\n}\n\ncheck_environment_selinux() {\n  if ! has_command getenforce; then\n    return\n  fi\n\n  note \"SELinux is detected\"\n\n  if [[ \"x$FORCE_NO_SELINUX\" == \"x1\" ]]; then\n    warning \"FORCE_NO_SELINUX=1, we will skip all SELinux related commands.\"\n    return\n  fi\n\n  if [[ -z \"$SECONTEXT_SYSTEMD_UNIT\" ]]; then\n    if [[ -z \"$FORCE_NO_SYSTEMD\" ]] && [[ -e \"$SYSTEMD_SERVICES_DIR\" ]]; then\n      local _sectx=\"$(get_selinux_context \"$SYSTEMD_SERVICES_DIR\")\"\n      if [[ -z \"$_sectx\" ]]; then\n        warning \"Failed to obtain SEContext of $SYSTEMD_SERVICES_DIR\"\n      else\n        SECONTEXT_SYSTEMD_UNIT=\"$_sectx\"\n      fi\n    fi\n  fi\n}\n\ncheck_environment_curl() {\n  if has_command curl; then\n    return\n  fi\n\n  install_software curl\n}\n\ncheck_environment_grep() {\n  if has_command grep; then\n    return\n  fi\n\n  install_software grep\n}\n\ncheck_environment() {\n  check_environment_operating_system\n  check_environment_architecture\n  check_environment_systemd\n  check_environment_selinux\n  check_environment_curl\n  check_environment_grep\n}\n\nvercmp_segment() {\n  local _lhs=\"$1\"\n  local _rhs=\"$2\"\n\n  if [[ \"x$_lhs\" == \"x$_rhs\" ]]; then\n    echo 0\n    return\n  fi\n  if [[ -z \"$_lhs\" ]]; then\n    echo -1\n    return\n  fi\n  if [[ -z \"$_rhs\" ]]; then\n    echo 1\n    return\n  fi\n\n  local _lhs_num=\"${_lhs//[A-Za-z]*/}\"\n  local _rhs_num=\"${_rhs//[A-Za-z]*/}\"\n\n  if [[ \"x$_lhs_num\" == \"x$_rhs_num\" ]]; then\n    echo 0\n    return\n  fi\n  if [[ -z \"$_lhs_num\" ]]; then\n    echo -1\n    return\n  fi\n  if [[ -z \"$_rhs_num\" ]]; then\n    echo 1\n    return\n  fi\n  local _numcmp=$(($_lhs_num - $_rhs_num))\n  if [[ \"$_numcmp\" -ne 0 ]]; then\n    echo \"$_numcmp\"\n    return\n  fi\n\n  local _lhs_suffix=\"${_lhs#\"$_lhs_num\"}\"\n  local _rhs_suffix=\"${_rhs#\"$_rhs_num\"}\"\n\n  if [[ \"x$_lhs_suffix\" == \"x$_rhs_suffix\" ]]; then\n    echo 0\n    return\n  fi\n  if [[ -z \"$_lhs_suffix\" ]]; then\n    echo 1\n    return\n  fi\n  if [[ -z \"$_rhs_suffix\" ]]; then\n    echo -1\n    return\n  fi\n  if [[ \"$_lhs_suffix\" < \"$_rhs_suffix\" ]]; then\n    echo -1\n    return\n  fi\n  echo 1\n}\n\nvercmp() {\n  local _lhs=${1#v}\n  local _rhs=${2#v}\n\n  while [[ -n \"$_lhs\" && -n \"$_rhs\" ]]; do\n    local _clhs=\"${_lhs/.*/}\"\n    local _crhs=\"${_rhs/.*/}\"\n\n    local _segcmp=\"$(vercmp_segment \"$_clhs\" \"$_crhs\")\"\n    if [[ \"$_segcmp\" -ne 0 ]]; then\n      echo \"$_segcmp\"\n      return\n    fi\n\n    _lhs=\"${_lhs#\"$_clhs\"}\"\n    _lhs=\"${_lhs#.}\"\n    _rhs=\"${_rhs#\"$_crhs\"}\"\n    _rhs=\"${_rhs#.}\"\n  done\n\n  if [[ \"x$_lhs\" == \"x$_rhs\" ]]; then\n    echo 0\n    return\n  fi\n\n  if [[ -z \"$_lhs\" ]]; then\n    echo -1\n    return\n  fi\n\n  if [[ -z \"$_rhs\" ]]; then\n    echo 1\n    return\n  fi\n\n  return\n}\n\ncheck_hysteria_user() {\n  local _default_hysteria_user=\"$1\"\n\n  if [[ -n \"$HYSTERIA_USER\" ]]; then\n    return\n  fi\n\n  if [[ ! -e \"$SYSTEMD_SERVICES_DIR/hysteria-server.service\" ]]; then\n    HYSTERIA_USER=\"$_default_hysteria_user\"\n    return\n  fi\n\n  HYSTERIA_USER=\"$(grep -o '^User=\\w*' \"$SYSTEMD_SERVICES_DIR/hysteria-server.service\" | tail -1 | cut -d '=' -f 2 || true)\"\n\n  if [[ -z \"$HYSTERIA_USER\" ]]; then\n    HYSTERIA_USER=\"$_default_hysteria_user\"\n  fi\n}\n\ncheck_hysteria_homedir() {\n  local _default_hysteria_homedir=\"$1\"\n\n  if [[ -n \"$HYSTERIA_HOME_DIR\" ]]; then\n    return\n  fi\n\n  if ! is_user_exists \"$HYSTERIA_USER\"; then\n    HYSTERIA_HOME_DIR=\"$_default_hysteria_homedir\"\n    return\n  fi\n\n  HYSTERIA_HOME_DIR=\"$(eval echo ~\"$HYSTERIA_USER\")\"\n}\n\n\n###\n# ARGUMENTS PARSER\n###\n\nshow_usage_and_exit() {\n  echo\n  echo -e \"\\t$(tbold)$SCRIPT_NAME$(treset) - hysteria server install script\"\n  echo\n  echo -e \"Usage:\"\n  echo\n  echo -e \"$(tbold)Install hysteria$(treset)\"\n  echo -e \"\\t$0 [ -f | -l <file> | --version <version> ]\"\n  echo -e \"Flags:\"\n  echo -e \"\\t-f, --force\\tForce re-install latest or specified version even if it has been installed.\"\n  echo -e \"\\t-l, --local <file>\\tInstall specified hysteria binary instead of download it.\"\n  echo -e \"\\t--version <version>\\tInstall specified version instead of the latest.\"\n  echo\n  echo -e \"$(tbold)Remove hysteria$(treset)\"\n  echo -e \"\\t$0 --remove\"\n  echo\n  echo -e \"$(tbold)Check for the update$(treset)\"\n  echo -e \"\\t$0 -c\"\n  echo -e \"\\t$0 --check\"\n  echo\n  echo -e \"$(tbold)Show this help$(treset)\"\n  echo -e \"\\t$0 -h\"\n  echo -e \"\\t$0 --help\"\n  exit 0\n}\n\nparse_arguments() {\n  while [[ \"$#\" -gt '0' ]]; do\n    case \"$1\" in\n      '--remove')\n        if [[ -n \"$OPERATION\" && \"$OPERATION\" != 'remove' ]]; then\n          show_argument_error_and_exit \"Option '--remove' is in conflict with other options.\"\n        fi\n        OPERATION='remove'\n        ;;\n      '--version')\n        VERSION=\"$2\"\n        if [[ -z \"$VERSION\" ]]; then\n          show_argument_error_and_exit \"Please specify the version for option '--version'.\"\n        fi\n        shift\n        if ! has_prefix \"$VERSION\" 'v'; then\n          show_argument_error_and_exit \"Version numbers should begin with 'v' (such as 'v2.0.0'), got '$VERSION'\"\n        fi\n        ;;\n      '-c' | '--check')\n        if [[ -n \"$OPERATION\" && \"$OPERATION\" != 'check' ]]; then\n          show_argument_error_and_exit \"Option '-c' or '--check' is in conflict with other options.\"\n        fi\n        OPERATION='check_update'\n        ;;\n      '-f' | '--force')\n        FORCE='1'\n        ;;\n      '-h' | '--help')\n        show_usage_and_exit\n        ;;\n      '-l' | '--local')\n        LOCAL_FILE=\"$2\"\n        if [[ -z \"$LOCAL_FILE\" ]]; then\n          show_argument_error_and_exit \"Please specify the local binary to install for option '-l' or '--local'.\"\n        fi\n        break\n        ;;\n      *)\n        show_argument_error_and_exit \"Unknown option '$1'\"\n        ;;\n    esac\n    shift\n  done\n\n  if [[ -z \"$OPERATION\" ]]; then\n    OPERATION='install'\n  fi\n\n  # validate arguments\n  case \"$OPERATION\" in\n    'install')\n      if [[ -n \"$VERSION\" && -n \"$LOCAL_FILE\" ]]; then\n        show_argument_error_and_exit '--version and --local cannot be used together.'\n      fi\n      ;;\n    *)\n      if [[ -n \"$VERSION\" ]]; then\n        show_argument_error_and_exit \"--version is only valid for install operation.\"\n      fi\n      if [[ -n \"$LOCAL_FILE\" ]]; then\n        show_argument_error_and_exit \"--local is only valid for install operation.\"\n      fi\n      ;;\n  esac\n}\n\n\n###\n# FILE TEMPLATES\n###\n\n# /etc/systemd/system/hysteria-server.service\ntpl_hysteria_server_service_base() {\n  local _config_name=\"$1\"\n\n  cat << EOF\n[Unit]\nDescription=Hysteria Server Service (${_config_name}.yaml)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=$EXECUTABLE_INSTALL_PATH server --config ${CONFIG_DIR}/${_config_name}.yaml\nWorkingDirectory=$(systemd_unit_working_directory)\nUser=$HYSTERIA_USER\nGroup=$HYSTERIA_USER\nEnvironment=HYSTERIA_LOG_LEVEL=info\nCapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW\nAmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW\nNoNewPrivileges=true\n\n[Install]\nWantedBy=multi-user.target\nEOF\n}\n\n# /etc/systemd/system/hysteria-server.service\ntpl_hysteria_server_service() {\n  tpl_hysteria_server_service_base 'config'\n}\n\n# /etc/systemd/system/hysteria-server@.service\ntpl_hysteria_server_x_service() {\n  tpl_hysteria_server_service_base '%i'\n}\n\n# /etc/hysteria/config.yaml\ntpl_etc_hysteria_config_yaml() {\n  cat << EOF\n# listen: :443\n\nacme:\n  domains:\n    - your.domain.net\n  email: your@email.com\n\nauth:\n  type: password\n  password: $(generate_random_password)\n\nmasquerade:\n  type: proxy\n  proxy:\n    url: https://news.ycombinator.com/\n    rewriteHost: true\nEOF\n}\n\n\n###\n# SYSTEMD\n###\n\nget_running_services() {\n  if [[ \"x$FORCE_NO_SYSTEMD\" == \"x2\" ]]; then\n    return\n  fi\n\n  systemctl list-units --state=active --plain --no-legend \\\n    | grep -o \"hysteria-server@*[^\\s]*.service\" || true\n}\n\nrestart_running_services() {\n  if [[ \"x$FORCE_NO_SYSTEMD\" == \"x2\" ]]; then\n    return\n  fi\n\n  echo \"Restarting running service ... \"\n\n  for service in $(get_running_services); do\n    echo -ne \"Restarting $service ... \"\n    systemctl restart \"$service\"\n    echo \"done\"\n  done\n}\n\nstop_running_services() {\n  if [[ \"x$FORCE_NO_SYSTEMD\" == \"x2\" ]]; then\n    return\n  fi\n\n  echo \"Stopping running service ... \"\n\n  for service in $(get_running_services); do\n    echo -ne \"Stopping $service ... \"\n    systemctl stop \"$service\"\n    echo \"done\"\n  done\n}\n\n\n###\n# HYSTERIA & GITHUB API\n###\n\nis_hysteria_installed() {\n  # RETURN VALUE\n  # 0: hysteria is installed\n  # 1: hysteria is not installed\n\n  if [[ -f \"$EXECUTABLE_INSTALL_PATH\" || -h \"$EXECUTABLE_INSTALL_PATH\" ]]; then\n    return 0\n  fi\n  return 1\n}\n\nis_hysteria1_version() {\n  local _version=\"$1\"\n\n  has_prefix \"$_version\" \"v1.\" || has_prefix \"$_version\" \"v0.\"\n}\n\nget_installed_version() {\n  if is_hysteria_installed; then\n    if \"$EXECUTABLE_INSTALL_PATH\" version > /dev/null 2>&1; then\n      \"$EXECUTABLE_INSTALL_PATH\" version | grep Version | grep -o 'v[.0-9]*'\n    elif \"$EXECUTABLE_INSTALL_PATH\" -v > /dev/null 2>&1; then\n      # hysteria 1\n      \"$EXECUTABLE_INSTALL_PATH\" -v | cut -d ' ' -f 3\n    fi\n  fi\n}\n\nget_latest_version() {\n  if [[ -n \"$VERSION\" ]]; then\n    echo \"$VERSION\"\n    return\n  fi\n\n  local _tmpfile=$(mktemp)\n  if ! curl -sS \"$HY2_API_BASE_URL/update?cver=installscript&plat=${OPERATING_SYSTEM}&arch=${ARCHITECTURE}&chan=release&side=server\" -o \"$_tmpfile\"; then\n    error \"Failed to get the latest version from Hysteria 2 API, please check your network and try again.\"\n    exit 11\n  fi\n\n  local _latest_version=$(grep -oP '\"lver\":\\s*\\K\"v.*?\"' \"$_tmpfile\" | head -1)\n  _latest_version=${_latest_version#'\"'}\n  _latest_version=${_latest_version%'\"'}\n\n  if [[ -n \"$_latest_version\" ]]; then\n    echo \"$_latest_version\"\n  fi\n\n  rm -f \"$_tmpfile\"\n}\n\ndownload_hysteria() {\n  local _version=\"$1\"\n  local _destination=\"$2\"\n\n  local _download_url=\"$REPO_URL/releases/download/app/$_version/hysteria-$OPERATING_SYSTEM-$ARCHITECTURE\"\n  echo \"Downloading hysteria binary: $_download_url ...\"\n  if ! curl -R -H 'Cache-Control: no-cache' \"$_download_url\" -o \"$_destination\"; then\n    error \"Download failed, please check your network and try again.\"\n    return 11\n  fi\n  return 0\n}\n\ncheck_update() {\n  # RETURN VALUE\n  # 0: update available\n  # 1: installed version is latest\n\n  echo -ne \"Checking for installed version ... \"\n  local _installed_version=\"$(get_installed_version)\"\n  if [[ -n \"$_installed_version\" ]]; then\n    echo \"$_installed_version\"\n  else\n    echo \"not installed\"\n  fi\n\n  echo -ne \"Checking for latest version ... \"\n  local _latest_version=\"$(get_latest_version)\"\n  if [[ -n \"$_latest_version\" ]]; then\n    echo \"$_latest_version\"\n    VERSION=\"$_latest_version\"\n  else\n    echo \"failed\"\n    return 1\n  fi\n\n  local _vercmp=\"$(vercmp \"$_installed_version\" \"$_latest_version\")\"\n  if [[ \"$_vercmp\" -lt 0 ]]; then\n    return 0\n  fi\n\n  return 1\n}\n\n\n###\n# ENTRY\n###\n\nperform_install_hysteria_binary() {\n  if [[ -n \"$LOCAL_FILE\" ]]; then\n    note \"Performing local install: $LOCAL_FILE\"\n\n    echo -ne \"Installing hysteria executable ... \"\n\n    if install -Dm755 \"$LOCAL_FILE\" \"$EXECUTABLE_INSTALL_PATH\"; then\n      echo \"ok\"\n    else\n      exit 2\n    fi\n\n    return\n  fi\n\n  local _tmpfile=$(mktemp)\n\n  if ! download_hysteria \"$VERSION\" \"$_tmpfile\"; then\n    rm -f \"$_tmpfile\"\n    exit 11\n  fi\n\n  echo -ne \"Installing hysteria executable ... \"\n\n  if install -Dm755 \"$_tmpfile\" \"$EXECUTABLE_INSTALL_PATH\"; then\n    echo \"ok\"\n  else\n    exit 13\n  fi\n\n  rm -f \"$_tmpfile\"\n}\n\nperform_remove_hysteria_binary() {\n  remove_file \"$EXECUTABLE_INSTALL_PATH\"\n}\n\nperform_install_hysteria_example_config() {\n  install_content -Dm644 \"$(tpl_etc_hysteria_config_yaml)\" \"$CONFIG_DIR/config.yaml\" \"\"\n}\n\nperform_install_hysteria_systemd() {\n  if [[ \"x$FORCE_NO_SYSTEMD\" == \"x2\" ]]; then\n    return\n  fi\n\n  install_content -Dm644 \"$(tpl_hysteria_server_service)\" \"$SYSTEMD_SERVICES_DIR/hysteria-server.service\" \"1\"\n  install_content -Dm644 \"$(tpl_hysteria_server_x_service)\" \"$SYSTEMD_SERVICES_DIR/hysteria-server@.service\" \"1\"\n  if [[ -n \"$SECONTEXT_SYSTEMD_UNIT\" ]]; then\n    chcon \"$SECONTEXT_SYSTEMD_UNIT\" \"$SYSTEMD_SERVICES_DIR/hysteria-server.service\"\n    chcon \"$SECONTEXT_SYSTEMD_UNIT\" \"$SYSTEMD_SERVICES_DIR/hysteria-server@.service\"\n  fi\n\n  systemctl daemon-reload\n}\n\nperform_remove_hysteria_systemd() {\n  remove_file \"$SYSTEMD_SERVICES_DIR/hysteria-server.service\"\n  remove_file \"$SYSTEMD_SERVICES_DIR/hysteria-server@.service\"\n\n  systemctl daemon-reload\n}\n\nperform_install_hysteria_home_legacy() {\n  if ! is_user_exists \"$HYSTERIA_USER\"; then\n    echo -ne \"Creating user $HYSTERIA_USER ... \"\n    useradd -r -d \"$HYSTERIA_HOME_DIR\" -m \"$HYSTERIA_USER\"\n    echo \"ok\"\n  fi\n}\n\nperform_install() {\n  local _is_frash_install\n  local _is_upgrade_from_hysteria1\n  if ! is_hysteria_installed; then\n    _is_frash_install=1\n  elif is_hysteria1_version \"$(get_installed_version)\"; then\n    _is_upgrade_from_hysteria1=1\n  fi\n\n  local _is_update_required\n\n  if [[ -n \"$LOCAL_FILE\" ]] || [[ -n \"$VERSION\" ]] || check_update; then\n    _is_update_required=1\n  fi\n\n  if [[ \"x$FORCE\" == \"x1\" ]]; then\n    if [[ -z \"$_is_update_required\" ]]; then\n      note \"Option '--force' detected, re-install even if installed version is the latest.\"\n    fi\n    _is_update_required=1\n  fi\n\n  if is_hysteria1_version \"$VERSION\"; then\n    error \"This script can only install Hysteria 2.\"\n    exit 95\n  fi\n\n  if [[ -n \"$_is_update_required\" ]]; then\n    perform_install_hysteria_binary\n  fi\n\n  # Always install additional files, regardless of $_is_update_required.\n  # This allows changes to be made with environment variables (e.g. change HYSTERIA_USER without --force).\n  perform_install_hysteria_example_config\n  perform_install_hysteria_home_legacy\n  perform_install_hysteria_systemd\n\n  if [[ -z \"$_is_update_required\" ]]; then\n    echo\n    echo \"$(tgreen)Installed version is up-to-date, there is nothing to do.$(treset)\"\n    echo\n  elif [[ -n \"$_is_frash_install\" ]]; then\n    echo\n    echo -e \"$(tbold)Congratulation! Hysteria 2 has been successfully installed on your server.$(treset)\"\n    echo\n    echo -e \"What's next?\"\n    echo\n    echo -e \"\\t+ Take a look at the differences between Hysteria 2 and Hysteria 1 at https://hysteria.network/docs/misc/2-vs-1/\"\n    echo -e \"\\t+ Check out the quick server config guide at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)\"\n    echo -e \"\\t+ Edit server config file at $(tred)$CONFIG_DIR/config.yaml$(treset)\"\n    echo -e \"\\t+ Start your hysteria server with $(tred)systemctl start hysteria-server.service$(treset)\"\n    echo -e \"\\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)\"\n    echo\n  elif [[ -n \"$_is_upgrade_from_hysteria1\" ]]; then\n    echo -e \"Skip automatic service restart due to $(tred)incompatible$(treset) upgrade.\"\n    echo\n    echo -e \"$(tbold)Hysteria has been successfully update to $VERSION from Hysteria 1.$(treset)\"\n    echo\n    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)\"\n    echo\n    echo -e \"\\t+ Take a look at the behavior changes in Hysteria 2 at $(tblue)https://hysteria.network/docs/misc/2-vs-1/$(treset)\"\n    echo -e \"\\t+ Check out the quick server configuration guide for Hysteria 2 at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)\"\n    echo -e \"\\t+ Migrate server config file to the Hysteria 2 at $(tred)$CONFIG_DIR/config.yaml$(treset)\"\n    echo -e \"\\t+ Start your hysteria server with $(tred)systemctl restart hysteria-server.service$(treset)\"\n    echo -e \"\\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)\"\n  else\n    restart_running_services\n\n    echo\n    echo -e \"$(tbold)Hysteria has been successfully update to $VERSION.$(treset)\"\n    echo\n    echo -e \"Check out the latest changelog $(tblue)https://github.com/apernet/hysteria/blob/master/CHANGELOG.md$(treset)\"\n    echo\n  fi\n}\n\nperform_remove() {\n  perform_remove_hysteria_binary\n  stop_running_services\n  perform_remove_hysteria_systemd\n\n  echo\n  echo -e \"$(tbold)Congratulation! Hysteria has been successfully removed from your server.$(treset)\"\n  echo\n  echo -e \"You still need to remove configuration files and ACME certificates manually with the following commands:\"\n  echo\n  echo -e \"\\t$(tred)rm -rf \"$CONFIG_DIR\"$(treset)\"\n  if [[ \"x$HYSTERIA_USER\" != \"xroot\" ]]; then\n    echo -e \"\\t$(tred)userdel -r \"$HYSTERIA_USER\"$(treset)\"\n  fi\n  if [[ \"x$FORCE_NO_SYSTEMD\" != \"x2\" ]]; then\n    echo\n    echo -e \"You still might need to disable all related systemd services with the following commands:\"\n    echo\n    echo -e \"\\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server.service$(treset)\"\n    echo -e \"\\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server@*.service$(treset)\"\n    echo -e \"\\t$(tred)systemctl daemon-reload$(treset)\"\n  fi\n  echo\n}\n\nperform_check_update() {\n  if check_update; then\n    echo\n    echo -e \"$(tbold)Update available: $VERSION$(treset)\"\n    echo\n    echo -e \"$(tgreen)You can download and install the latest version by execute this script without any arguments.$(treset)\"\n    echo\n  else\n    echo\n    echo \"$(tgreen)Installed version is up-to-date.$(treset)\"\n    echo\n  fi\n}\n\nmain() {\n  parse_arguments \"$@\"\n\n  check_permission\n  check_environment\n  check_hysteria_user \"hysteria\"\n  check_hysteria_homedir \"/var/lib/$HYSTERIA_USER\"\n\n  case \"$OPERATION\" in\n    \"install\")\n      perform_install\n      ;;\n    \"remove\")\n      perform_remove\n      ;;\n    \"check_update\")\n      perform_check_update\n      ;;\n    *)\n      error \"Unknown operation '$OPERATION'.\"\n      ;;\n  esac\n}\n\nmain \"$@\"\n\n# vim:set ft=bash ts=2 sw=2 sts=2 et:\n"
  }
]