Repository: apernet/hysteria
Branch: master
Commit: 3c9dd620ad27
Files: 220
Total size: 890.4 KB
Directory structure:
gitextract_00tfxijy/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── bug_report.zh.md
│ │ ├── feature_request.md
│ │ └── feature_request.zh.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── autotag.yaml
│ ├── build-common.yml
│ ├── docker.yml
│ ├── experimental.yml
│ ├── master.yml
│ ├── release.yml
│ └── scripts.yml
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE.md
├── PROTOCOL.md
├── README.md
├── app/
│ ├── LICENSE.md
│ ├── cmd/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── client_test.yaml
│ │ ├── errors.go
│ │ ├── ping.go
│ │ ├── root.go
│ │ ├── server.go
│ │ ├── server_test.go
│ │ ├── server_test.yaml
│ │ ├── share.go
│ │ ├── speedtest.go
│ │ ├── update.go
│ │ └── version.go
│ ├── go.mod
│ ├── go.sum
│ ├── internal/
│ │ ├── forwarding/
│ │ │ ├── tcp.go
│ │ │ ├── tcp_test.go
│ │ │ ├── udp.go
│ │ │ └── udp_test.go
│ │ ├── http/
│ │ │ ├── server.go
│ │ │ ├── server_test.go
│ │ │ ├── server_test.py
│ │ │ ├── test.crt
│ │ │ └── test.key
│ │ ├── proxymux/
│ │ │ ├── .mockery.yaml
│ │ │ ├── internal/
│ │ │ │ └── mocks/
│ │ │ │ ├── mock_Conn.go
│ │ │ │ └── mock_Listener.go
│ │ │ ├── manager.go
│ │ │ ├── manager_test.go
│ │ │ ├── mux.go
│ │ │ └── mux_test.go
│ │ ├── redirect/
│ │ │ ├── getsockopt_linux.go
│ │ │ ├── getsockopt_linux_386.go
│ │ │ ├── syscall_socketcall_linux_386.s
│ │ │ ├── tcp_linux.go
│ │ │ └── tcp_others.go
│ │ ├── sockopts/
│ │ │ ├── fd_control_unix_socket_test.py
│ │ │ ├── sockopts.go
│ │ │ ├── sockopts_linux.go
│ │ │ └── sockopts_linux_test.go
│ │ ├── socks5/
│ │ │ ├── server.go
│ │ │ ├── server_test.go
│ │ │ └── server_test.py
│ │ ├── tproxy/
│ │ │ ├── tcp_linux.go
│ │ │ ├── tcp_others.go
│ │ │ ├── udp_linux.go
│ │ │ └── udp_others.go
│ │ ├── tun/
│ │ │ ├── check_ipv6_others.go
│ │ │ ├── check_ipv6_unix.go
│ │ │ ├── check_ipv6_windows.go
│ │ │ ├── log.go
│ │ │ └── server.go
│ │ ├── url/
│ │ │ ├── url.go
│ │ │ └── url_test.go
│ │ ├── utils/
│ │ │ ├── bpsconv.go
│ │ │ ├── bpsconv_test.go
│ │ │ ├── certloader.go
│ │ │ ├── certloader_test.go
│ │ │ ├── certloader_test_gencert.py
│ │ │ ├── certloader_test_tlsclient.py
│ │ │ ├── geoloader.go
│ │ │ ├── qr.go
│ │ │ ├── testcerts/
│ │ │ │ └── .gitignore
│ │ │ └── update.go
│ │ └── utils_test/
│ │ └── mock.go
│ ├── main.go
│ ├── misc/
│ │ └── socks5_test.py
│ └── pprof.go
├── core/
│ ├── LICENSE.md
│ ├── client/
│ │ ├── .mockery.yaml
│ │ ├── client.go
│ │ ├── config.go
│ │ ├── mock_udpIO.go
│ │ ├── reconnect.go
│ │ ├── udp.go
│ │ └── udp_test.go
│ ├── errors/
│ │ └── errors.go
│ ├── go.mod
│ ├── go.sum
│ ├── internal/
│ │ ├── congestion/
│ │ │ ├── bbr/
│ │ │ │ ├── bandwidth.go
│ │ │ │ ├── bandwidth_sampler.go
│ │ │ │ ├── bbr_sender.go
│ │ │ │ ├── clock.go
│ │ │ │ ├── packet_number_indexed_queue.go
│ │ │ │ ├── ringbuffer.go
│ │ │ │ └── windowed_filter.go
│ │ │ ├── brutal/
│ │ │ │ └── brutal.go
│ │ │ ├── common/
│ │ │ │ └── pacer.go
│ │ │ └── utils.go
│ │ ├── frag/
│ │ │ ├── frag.go
│ │ │ └── frag_test.go
│ │ ├── integration_tests/
│ │ │ ├── .mockery.yaml
│ │ │ ├── close_test.go
│ │ │ ├── hook_test.go
│ │ │ ├── masq_test.go
│ │ │ ├── mocks/
│ │ │ │ ├── mock_Authenticator.go
│ │ │ │ ├── mock_Conn.go
│ │ │ │ ├── mock_EventLogger.go
│ │ │ │ ├── mock_Outbound.go
│ │ │ │ ├── mock_RequestHook.go
│ │ │ │ ├── mock_TrafficLogger.go
│ │ │ │ └── mock_UDPConn.go
│ │ │ ├── smoke_test.go
│ │ │ ├── stress_test.go
│ │ │ ├── test.crt
│ │ │ ├── test.key
│ │ │ ├── trafficlogger_test.go
│ │ │ └── utils_test.go
│ │ ├── pmtud/
│ │ │ ├── avail.go
│ │ │ └── unavail.go
│ │ ├── protocol/
│ │ │ ├── http.go
│ │ │ ├── padding.go
│ │ │ ├── proxy.go
│ │ │ └── proxy_test.go
│ │ └── utils/
│ │ ├── atomic.go
│ │ └── qstream.go
│ └── server/
│ ├── .mockery.yaml
│ ├── config.go
│ ├── copy.go
│ ├── mock_UDPConn.go
│ ├── mock_udpEventLogger.go
│ ├── mock_udpIO.go
│ ├── server.go
│ ├── udp.go
│ └── udp_test.go
├── extras/
│ ├── LICENSE.md
│ ├── auth/
│ │ ├── command.go
│ │ ├── http.go
│ │ ├── http_test.go
│ │ ├── http_test.py
│ │ ├── password.go
│ │ ├── password_test.go
│ │ ├── userpass.go
│ │ └── userpass_test.go
│ ├── correctnet/
│ │ └── correctnet.go
│ ├── go.mod
│ ├── go.sum
│ ├── masq/
│ │ └── server.go
│ ├── obfs/
│ │ ├── conn.go
│ │ ├── salamander.go
│ │ └── salamander_test.go
│ ├── outbounds/
│ │ ├── .mockery.yaml
│ │ ├── acl/
│ │ │ ├── compile.go
│ │ │ ├── compile_test.go
│ │ │ ├── matchers.go
│ │ │ ├── matchers_test.go
│ │ │ ├── matchers_v2geo.go
│ │ │ ├── matchers_v2geo_test.go
│ │ │ ├── parse.go
│ │ │ ├── parse_test.go
│ │ │ └── v2geo/
│ │ │ ├── load.go
│ │ │ ├── load_test.go
│ │ │ ├── v2geo.pb.go
│ │ │ └── v2geo.proto
│ │ ├── acl.go
│ │ ├── acl_test.go
│ │ ├── dns_https.go
│ │ ├── dns_standard.go
│ │ ├── dns_system.go
│ │ ├── fastopen.go
│ │ ├── interface.go
│ │ ├── interface_test.go
│ │ ├── mock_PluggableOutbound.go
│ │ ├── mock_UDPConn.go
│ │ ├── ob_direct.go
│ │ ├── ob_direct_linux.go
│ │ ├── ob_direct_others.go
│ │ ├── ob_http.go
│ │ ├── ob_socks5.go
│ │ ├── speedtest/
│ │ │ ├── client.go
│ │ │ ├── protocol.go
│ │ │ ├── protocol_test.go
│ │ │ └── server.go
│ │ ├── speedtest.go
│ │ ├── tinydoh/
│ │ │ ├── resolver.go
│ │ │ └── resolver_test.go
│ │ ├── utils.go
│ │ └── utils_test.go
│ ├── sniff/
│ │ ├── .mockery.yaml
│ │ ├── internal/
│ │ │ └── quic/
│ │ │ ├── LICENSE
│ │ │ ├── README.md
│ │ │ ├── header.go
│ │ │ ├── packet_protector.go
│ │ │ ├── packet_protector_test.go
│ │ │ ├── payload.go
│ │ │ └── quic.go
│ │ ├── mock_Stream.go
│ │ ├── sniff.go
│ │ └── sniff_test.go
│ ├── trafficlogger/
│ │ └── http.go
│ ├── transport/
│ │ └── udphop/
│ │ ├── addr.go
│ │ └── conn.go
│ └── utils/
│ ├── portunion.go
│ └── portunion_test.go
├── go.work.sum
├── hyperbole.py
├── platforms.txt
├── requirements.txt
└── scripts/
├── _redirects
└── install_server.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
custom: [ 'https://v2.hysteria.network/docs/Donation/' ]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Report anything you think is a bug and needs to be fixed.
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
Attach logs from the client/server when the error occurs.
**Device and Operating System**
What are you using it on.
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.zh.md
================================================
---
name: Bug 反馈
about: 反馈任何你认为是 bug 需要修复的问题。
title: ''
labels: bug
assignees: ''
---
**描述问题**
请尽量清晰精准地描述你遇到的问题。
**如何复现**
复现问题的步骤。
**预期行为**
你认为修复后的行为应该是怎样的。
**日志**
附上客户端/服务器端在错误发生前后的日志。
**设备和操作系统**
你在用什么设备和操作系统。
**额外信息**
其他你认为有助于解决问题的信息。
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project.
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.zh.md
================================================
---
name: 功能请求
about: 为这个项目提出改进意见。
title: ''
labels: enhancement
assignees: ''
---
**你的功能请求是否与某个问题有关?**
请尽量清晰精准地描述你遇到的问题。例如:我家运营商限制 UDP 协议速度,导致 Hysteria 很慢,希望增加 FakeTCP 支持。
**描述你希望的解决方案**
请尽量清晰精准地描述你希望的解决方案。
**有没有其他替代方案**
请尽量清晰精准地描述你认为可能的替代方案。
**额外信息**
其他你认为有助于开发者了解你需求的信息。
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
================================================
FILE: .github/workflows/autotag.yaml
================================================
name: "Create release tags for nested modules"
on:
push:
tags:
- app/v*.*.*
permissions:
contents: write
jobs:
tag:
name: "Create tags"
runs-on: ubuntu-latest
steps:
- name: "Extract tagbase"
id: extract_tagbase
uses: actions/github-script@v7
with:
script: |
const ref = context.ref;
core.info(`context.ref: ${ref}`);
const refPrefix = 'refs/tags/app/';
if (!ref.startsWith(refPrefix)) {
core.setFailed(`context.ref does not start with ${refPrefix}: ${ref}`);
return;
}
const tagbase = ref.slice(refPrefix.length);
core.info(`tagbase: ${tagbase}`);
core.setOutput('tagbase', tagbase);
- name: "Tagging core/*"
uses: actions/github-script@v7
env:
INPUT_TAGPREFIX: "core/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}
- name: "Tagging extras/*"
uses: actions/github-script@v7
env:
INPUT_TAGPREFIX: "extras/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}
================================================
FILE: .github/workflows/build-common.yml
================================================
name: "Build common"
on:
workflow_call:
jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
outputs:
files: ${{ steps.list-files.outputs.files }}
steps:
- name: Check out
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Setup Python # This is for the build script
uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r29
add-to-path: false
- name: Run build script
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
python hyperbole.py build -r
- name: Generate hashes
run: |
for file in build/*; do
sha256sum $file >> build/hashes.txt
done
- name: List build files
id: list-files
run: |
files=$(ls -1 build/ | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "files=$files" >> $GITHUB_OUTPUT
- name: Upload build directory
uses: actions/upload-artifact@v4
with:
name: build-temp-${{ github.sha }}
path: build/
upload:
name: Upload ${{ matrix.file }}
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
file: ${{ fromJson(needs.build.outputs.files) }}
steps:
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-temp-${{ github.sha }}
- name: Upload ${{ matrix.file }}
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.file }}
path: ${{ matrix.file }}
================================================
FILE: .github/workflows/docker.yml
================================================
name: "Build Docker Image"
on:
push:
tags:
- app/v*.*.*
jobs:
docker:
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:v2,tobyxdd/hysteria:${{ steps.get_version.outputs.version }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
================================================
FILE: .github/workflows/experimental.yml
================================================
name: "Build experimental branches"
on:
push:
branches:
- exp/**
jobs:
build:
uses: ./.github/workflows/build-common.yml
================================================
FILE: .github/workflows/master.yml
================================================
name: "Build master branch"
on:
push:
branches:
- master
jobs:
build:
uses: ./.github/workflows/build-common.yml
================================================
FILE: .github/workflows/release.yml
================================================
name: "Build release"
on:
push:
tags:
- app/v*.*.*
jobs:
build:
uses: ./.github/workflows/build-common.yml
publish:
name: Publish
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-temp-${{ github.sha }}
path: build
- name: Upload GitHub
uses: softprops/action-gh-release@v2
with:
files: build/*
- name: Upload CF bucket
uses: shallwefootball/upload-s3-action@v1.3.3
with:
aws_key_id: ${{ secrets.CF_KEY_ID }}
aws_secret_access_key: ${{ secrets.CF_KEY }}
aws_bucket: "hydownload"
endpoint: "https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/"
source_dir: "build"
destination_dir: "app/${{ steps.get_version.outputs.version }}"
- name: Publish to API
run: |
export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }}
pip install requests
python hyperbole.py publish
================================================
FILE: .github/workflows/scripts.yml
================================================
name: "Publish scripts"
on:
push:
branches:
- master
paths:
- scripts/**
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Publish scripts to Cloudflare Pages
steps:
- name: Check out
uses: actions/checkout@v4
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: hy2scripts
directory: scripts
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: main
================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### GoLand+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### GoLand+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### PyCharm+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos,python,pycharm+all
test-configs/
================================================
FILE: CHANGELOG.md
================================================
# Changelog
https://v2.hysteria.network/docs/Changelog/
================================================
FILE: Dockerfile
================================================
FROM golang:1-alpine AS builder
# GOPROXY is disabled by default, use:
# docker build --build-arg GOPROXY="https://goproxy.io" ...
# to enable GOPROXY.
ARG GOPROXY=""
ENV GOPROXY ${GOPROXY}
COPY . /go/src/github.com/apernet/hysteria
WORKDIR /go/src/github.com/apernet/hysteria
RUN set -ex \
&& apk add git build-base bash python3 \
&& python hyperbole.py build -r \
&& mv ./build/hysteria-* /go/bin/hysteria
# multi-stage builds to create the final image
FROM alpine AS dist
# set up nsswitch.conf for Go's "netgo" implementation
# - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275
# - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf
RUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch.conf; fi
# bash is used for debugging, tzdata is used to add timezone information.
# Install ca-certificates to ensure no CA certificate errors.
#
# Do not try to add the "--no-cache" option when there are multiple "apk"
# commands, this will cause the build process to become very slow.
RUN set -ex \
&& apk upgrade \
&& apk add bash tzdata ca-certificates \
&& rm -rf /var/cache/apk/*
COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria
ENTRYPOINT ["hysteria"]
================================================
FILE: LICENSE.md
================================================
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: PROTOCOL.md
================================================
# Hysteria 2 Protocol Specification
Hysteria is a TCP & UDP proxy based on QUIC, designed for speed, security and censorship resistance. This document describes the protocol used by Hysteria starting with version 2.0.0, sometimes internally referred to as the "v4" protocol. From here on, we will call it "the protocol" or "the Hysteria protocol".
## Requirements Language
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119).
## Underlying Protocol & Wire Format
The Hysteria protocol MUST be implemented on top of the standard QUIC transport protocol [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000) with [Unreliable Datagram Extension](https://datatracker.ietf.org/doc/rfc9221/).
All multibyte numbers use Big Endian format.
All variable-length integers ("varints") are encoded/decoded as defined in QUIC (RFC 9000).
## Authentication & HTTP/3 masquerading
One of the key features of the Hysteria protocol is that to a third party without proper authentication credentials (whether it's a middleman or an active prober), a Hysteria proxy server behaves just like a standard HTTP/3 web server. Additionally, the encrypted traffic between the client and the server appears indistinguishable from normal HTTP/3 traffic.
Therefore, a Hysteria server MUST implement an HTTP/3 server (as defined by [RFC 9114](https://datatracker.ietf.org/doc/rfc9114/)) and handle HTTP requests as any standard web server would. To prevent active probers from detecting common response patterns in Hysteria servers, implementations SHOULD advise users to either host actual content or set it up as a reverse proxy for other sites.
An actual Hysteria client, upon connection, MUST send the following HTTP/3 request to the server:
```
:method: POST
:path: /auth
:host: hysteria
Hysteria-Auth: [string]
Hysteria-CC-RX: [uint]
Hysteria-Padding: [string]
```
`Hysteria-Auth`: Authentication credentials.
`Hysteria-CC-RX`: Client's maximum receive rate in bytes per second. A value of 0 indicates unknown.
`Hysteria-Padding`: A random padding string of variable length.
The Hysteria server MUST identify this special request, and, instead of attempting to serve content or forwarding it to an upstream site, it MUST authenticate the client using the provided information. If authentication is successful, the server MUST send the following response (HTTP status code 233):
```
:status: 233 HyOK
Hysteria-UDP: [true/false]
Hysteria-CC-RX: [uint/"auto"]
Hysteria-Padding: [string]
```
`Hysteria-UDP`: Whether the server supports UDP relay.
`Hysteria-CC-RX`: Server's maximum receive rate in bytes per second. A value of 0 indicates unlimited; "auto" indicates the server refuses to provide a value and ask the client to use congestion control to determine the rate on its own.
`Hysteria-Padding`: A random padding string of variable length.
See the Congestion Control section for more information on how to use the `Hysteria-CC-RX` values.
`Hysteria-Padding` is optional and is only intended to obfuscate the request/response pattern. It SHOULD be ignored by both sides.
If authentication fails, the server MUST either act like a standard web server that does not understand the request, or in the case of being a reverse proxy, forward the request to the upstream site and return the response to the client.
The client MUST check the status code to determine if the authentication was successful. If the status code is anything other than 233, the client MUST consider authentication to have failed and disconnect from the server.
After (and only after) a client passes authentication, the server MUST consider this QUIC connection to be a Hysteria proxy connection. It MUST then start processing proxy requests from the client as described in the next section.
## Proxy Requests
### TCP
For each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message:
```
[varint] 0x401 (TCPRequest ID)
[varint] Address length
[bytes] Address string (host:port)
[varint] Padding length
[bytes] Random padding
```
The server MUST respond with a TCPResponse message:
```
[uint8] Status (0x00 = OK, 0x01 = Error)
[varint] Message length
[bytes] Message string
[varint] Padding length
[bytes] Random padding
```
If the status is OK, the server MUST then begin forwarding data between the client and the specified TCP address until either side closes the connection. If the status is Error, the server MUST close the QUIC stream.
### UDP
UDP packets MUST be encapsulated in the following UDPMessage format and sent over QUIC's unreliable datagram (for both client-to-server and server-to-client):
```
[uint32] Session ID
[uint16] Packet ID
[uint8] Fragment ID
[uint8] Fragment count
[varint] Address length
[bytes] Address string (host:port)
[bytes] Payload
```
The client MUST use a unique Session ID for each UDP session. The server SHOULD assign a unique UDP port to each Session ID, unless it has another mechanism to differentiate packets from different sessions (e.g., symmetric NAT, varying outbound IP addresses, etc.).
The protocol does not provide an explicit way to close a UDP session. While a client can retain and reuse a Session ID indefinitely, the server SHOULD release and reassign the port associated with the Session ID after a period of inactivity or some other criteria. If the client sends a UDP packet to a Session ID that is no longer recognized by the server, the server MUST treat it as a new session and assign a new port.
If a server does not support UDP relay, it SHOULD silently discard all UDP messages received from the client.
#### Fragmentation
Due to the limit imposed by QUIC's unreliable datagram channel, any UDP packet that exceeds QUIC's maximum datagram size MUST either be fragmented or discarded.
For fragmented packets, each fragment MUST carry the same unique Packet ID. The Fragment ID, starting from 0, indicates the index out of the total Fragment Count. Both the server and client MUST wait for all fragments of a fragmented packet to arrive before processing them. If one or more fragments of a packet are lost, the entire packet MUST be discarded.
For packets that are not fragmented, the Fragment Count MUST be set to 1. In this case, the values of Packet ID and Fragment ID are irrelevant.
## Congestion Control
A unique feature of Hysteria is the ability to set the tx/rx (upload/download) rate on the client side. During authentication, the client sends its rx rate to the server via the `Hysteria-CC-RX` header. The server can use this to determine its transmission rate to the client, and vice versa by returning its rx rate to the client through the same header.
Three special cases are:
- If the client sends 0, it doesn't know its own rx rate. The server MUST use a congestion control algorithm (e.g., BBR, Cubic) to adjust its transmission rate.
- If the server responds with 0, it has no bandwidth limit. The client MAY transmit at any rate it wants.
- If the server responds with "auto", it chooses not to specify a rate. The client MUST use a congestion control algorithm to adjust its transmission rate.
## "Salamander" Obfuscation
The Hysteria protocol supports an optional obfuscation layer codenamed "Salamander".
"Salamander" encapsulates all QUIC packets in the following format:
```
[8 bytes] Salt
[bytes] Payload
```
For each QUIC packet, the obfuscator MUST calculate the BLAKE2b-256 hash of a randomly generated 8-byte salt appended to a user-provided pre-shared key.
```
hash = BLAKE2b-256(key + salt)
```
The hash is then used to obfuscate the payload using the following algorithm:
```
for i in range(0, len(payload)):
payload[i] ^= hash[i % 32]
```
The deobfuscator MUST use the same algorithms to calculate the salted hash and deobfuscate the payload. Any invalid packet MUST be discarded.
================================================
FILE: README.md
================================================
# 
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8]
[1]: https://img.shields.io/badge/license-MIT-blue
[2]: LICENSE.md
[3]: https://img.shields.io/github/v/release/apernet/hysteria?style=flat-square
[4]: https://github.com/apernet/hysteria/releases
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
[6]: https://t.me/hysteria_github
[7]: https://img.shields.io/github/discussions/apernet/hysteria?style=flat-square
[8]: https://github.com/apernet/hysteria/discussions
Hysteria is a powerful, lightning fast and censorship resistant proxy.
### [Get Started](https://v2.hysteria.network/)
### [中文文档](https://v2.hysteria.network/zh/)
### [Hysteria 1.x (legacy)](https://v1.hysteria.network/)
---
🛠️ Jack of all trades
Wide range of modes including SOCKS5, HTTP Proxy, TCP/UDP Forwarding, Linux TProxy, TUN - with more features being added constantly.
⚡ Blazing fast
Powered by a customized QUIC protocol, Hysteria is designed to deliver unparalleled performance over unreliable and lossy networks.
✊ Censorship resistant
The protocol masquerades as standard HTTP/3 traffic, making it very difficult for censors to detect and block without widespread collateral damage.
💻 Cross-platform
We have builds for every major platform and architecture. Deploy anywhere & use everywhere. Not to mention the long list of 3rd party apps.
🔗 Easy integration
With built-in support for custom authentication, traffic statistics & access control, Hysteria is easy to integrate into your infrastructure.
🤗 Chill and supportive
We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.
---
**If you find Hysteria useful, consider giving it a ⭐️!**
[](https://star-history.com/#apernet/hysteria&Date)
================================================
FILE: app/LICENSE.md
================================================
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: app/cmd/client.go
================================================
package cmd
import (
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
"os"
"os/signal"
"runtime"
"slices"
"strconv"
"strings"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/app/v2/internal/forwarding"
"github.com/apernet/hysteria/app/v2/internal/http"
"github.com/apernet/hysteria/app/v2/internal/proxymux"
"github.com/apernet/hysteria/app/v2/internal/redirect"
"github.com/apernet/hysteria/app/v2/internal/sockopts"
"github.com/apernet/hysteria/app/v2/internal/socks5"
"github.com/apernet/hysteria/app/v2/internal/tproxy"
"github.com/apernet/hysteria/app/v2/internal/tun"
"github.com/apernet/hysteria/app/v2/internal/url"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/extras/v2/correctnet"
"github.com/apernet/hysteria/extras/v2/obfs"
"github.com/apernet/hysteria/extras/v2/transport/udphop"
)
// Client flags
var (
showQR bool
)
var clientCmd = &cobra.Command{
Use: "client",
Short: "Client mode",
Run: runClientCmd,
}
func init() {
initClientFlags()
rootCmd.AddCommand(clientCmd)
}
func initClientFlags() {
clientCmd.Flags().BoolVar(&showQR, "qr", false, "show QR code for server config sharing")
}
type clientConfig struct {
Server string `mapstructure:"server"`
Auth string `mapstructure:"auth"`
Transport clientConfigTransport `mapstructure:"transport"`
Obfs clientConfigObfs `mapstructure:"obfs"`
TLS clientConfigTLS `mapstructure:"tls"`
QUIC clientConfigQUIC `mapstructure:"quic"`
Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"`
FastOpen bool `mapstructure:"fastOpen"`
Lazy bool `mapstructure:"lazy"`
SOCKS5 *socks5Config `mapstructure:"socks5"`
HTTP *httpConfig `mapstructure:"http"`
TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"`
UDPForwarding []udpForwardingEntry `mapstructure:"udpForwarding"`
TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"`
UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"`
TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"`
TUN *tunConfig `mapstructure:"tun"`
}
type clientConfigTransportUDP struct {
HopInterval time.Duration `mapstructure:"hopInterval"`
}
type clientConfigTransport struct {
Type string `mapstructure:"type"`
UDP clientConfigTransportUDP `mapstructure:"udp"`
}
type clientConfigObfsSalamander struct {
Password string `mapstructure:"password"`
}
type clientConfigObfs struct {
Type string `mapstructure:"type"`
Salamander clientConfigObfsSalamander `mapstructure:"salamander"`
}
type clientConfigTLS struct {
SNI string `mapstructure:"sni"`
Insecure bool `mapstructure:"insecure"`
PinSHA256 string `mapstructure:"pinSHA256"`
CA string `mapstructure:"ca"`
ClientCertificate string `mapstructure:"clientCertificate"`
ClientKey string `mapstructure:"clientKey"`
}
type clientConfigQUIC struct {
InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"`
MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"`
MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"`
KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"`
DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"`
Sockopts clientConfigQUICSockopts `mapstructure:"sockopts"`
}
type clientConfigQUICSockopts struct {
BindInterface *string `mapstructure:"bindInterface"`
FirewallMark *uint32 `mapstructure:"fwmark"`
FdControlUnixSocket *string `mapstructure:"fdControlUnixSocket"`
}
type clientConfigBandwidth struct {
Up string `mapstructure:"up"`
Down string `mapstructure:"down"`
}
type socks5Config struct {
Listen string `mapstructure:"listen"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
DisableUDP bool `mapstructure:"disableUDP"`
}
type httpConfig struct {
Listen string `mapstructure:"listen"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Realm string `mapstructure:"realm"`
}
type tcpForwardingEntry struct {
Listen string `mapstructure:"listen"`
Remote string `mapstructure:"remote"`
}
type udpForwardingEntry struct {
Listen string `mapstructure:"listen"`
Remote string `mapstructure:"remote"`
Timeout time.Duration `mapstructure:"timeout"`
}
type tcpTProxyConfig struct {
Listen string `mapstructure:"listen"`
}
type udpTProxyConfig struct {
Listen string `mapstructure:"listen"`
Timeout time.Duration `mapstructure:"timeout"`
}
type tcpRedirectConfig struct {
Listen string `mapstructure:"listen"`
}
type tunConfig struct {
Name string `mapstructure:"name"`
MTU uint32 `mapstructure:"mtu"`
Timeout time.Duration `mapstructure:"timeout"`
Address struct {
IPv4 string `mapstructure:"ipv4"`
IPv6 string `mapstructure:"ipv6"`
} `mapstructure:"address"`
Route *struct {
Strict bool `mapstructure:"strict"`
IPv4 []string `mapstructure:"ipv4"`
IPv6 []string `mapstructure:"ipv6"`
IPv4Exclude []string `mapstructure:"ipv4Exclude"`
IPv6Exclude []string `mapstructure:"ipv6Exclude"`
} `mapstructure:"route"`
}
func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error {
if c.Server == "" {
return configError{Field: "server", Err: errors.New("server address is empty")}
}
var addr net.Addr
var err error
host, port, hostPort := parseServerAddrString(c.Server)
if !isPortHoppingPort(port) {
addr, err = net.ResolveUDPAddr("udp", hostPort)
} else {
addr, err = udphop.ResolveUDPHopAddr(hostPort)
}
if err != nil {
return configError{Field: "server", Err: err}
}
hyConfig.ServerAddr = addr
// Special handling for SNI
if c.TLS.SNI == "" {
// Use server hostname as SNI
hyConfig.TLSConfig.ServerName = host
}
return nil
}
// fillConnFactory must be called after fillServerAddr, as we have different logic
// for ConnFactory depending on whether we have a port hopping address.
func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
so := &sockopts.SocketOptions{
BindInterface: c.QUIC.Sockopts.BindInterface,
FirewallMark: c.QUIC.Sockopts.FirewallMark,
FdControlUnixSocket: c.QUIC.Sockopts.FdControlUnixSocket,
}
if err := so.CheckSupported(); err != nil {
var unsupportedErr *sockopts.UnsupportedError
if errors.As(err, &unsupportedErr) {
return configError{
Field: "quic.sockopts." + unsupportedErr.Field,
Err: errors.New("unsupported on this platform"),
}
}
return configError{Field: "quic.sockopts", Err: err}
}
// Inner PacketConn
var newFunc func(addr net.Addr) (net.PacketConn, error)
switch strings.ToLower(c.Transport.Type) {
case "", "udp":
if hyConfig.ServerAddr.Network() == "udphop" {
hopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr)
newFunc = func(addr net.Addr) (net.PacketConn, error) {
return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, so.ListenUDP)
}
} else {
newFunc = func(addr net.Addr) (net.PacketConn, error) {
return so.ListenUDP()
}
}
default:
return configError{Field: "transport.type", Err: errors.New("unsupported transport type")}
}
// Obfuscation
var ob obfs.Obfuscator
var err error
switch strings.ToLower(c.Obfs.Type) {
case "", "plain":
// Keep it nil
case "salamander":
ob, err = obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))
if err != nil {
return configError{Field: "obfs.salamander.password", Err: err}
}
default:
return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
}
hyConfig.ConnFactory = &adaptiveConnFactory{
NewFunc: newFunc,
Obfuscator: ob,
}
return nil
}
func (c *clientConfig) fillAuth(hyConfig *client.Config) error {
hyConfig.Auth = c.Auth
return nil
}
func (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error {
if c.TLS.SNI != "" {
hyConfig.TLSConfig.ServerName = c.TLS.SNI
}
hyConfig.TLSConfig.InsecureSkipVerify = c.TLS.Insecure
if c.TLS.PinSHA256 != "" {
nHash := normalizeCertHash(c.TLS.PinSHA256)
hyConfig.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
cert := rawCerts[0] // only check the end-entity cert hash in the chain of trust
hash := sha256.Sum256(cert)
hashHex := hex.EncodeToString(hash[:])
if hashHex == nHash {
return nil
}
// No match
return errors.New("no certificate matches the pinned hash")
}
}
if c.TLS.CA != "" {
ca, err := os.ReadFile(c.TLS.CA)
if err != nil {
return configError{Field: "tls.ca", Err: err}
}
cPool := x509.NewCertPool()
if !cPool.AppendCertsFromPEM(ca) {
return configError{Field: "tls.ca", Err: errors.New("failed to parse CA certificate")}
}
hyConfig.TLSConfig.RootCAs = cPool
}
if c.TLS.ClientCertificate != "" && c.TLS.ClientKey != "" {
certLoader := &utils.LocalCertificateLoader{
CertFile: c.TLS.ClientCertificate,
KeyFile: c.TLS.ClientKey,
}
// Try loading the cert-key pair here to catch errors early
err := certLoader.InitializeCache()
if err != nil {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
if pathErr.Path == c.TLS.ClientCertificate {
return configError{Field: "tls.clientCertificate", Err: pathErr}
}
if pathErr.Path == c.TLS.ClientKey {
return configError{Field: "tls.clientKey", Err: pathErr}
}
}
return configError{Field: "tls.clientCertificate", Err: err}
}
// Use GetClientCertificates so that users can update the cert without restarting the client.
hyConfig.TLSConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
// For simplicity, always respond with the configured client certs, regardless of server requests.
return certLoader.GetCertificate(nil)
}
}
return nil
}
func (c *clientConfig) fillQUICConfig(hyConfig *client.Config) error {
hyConfig.QUICConfig = client.QUICConfig{
InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow,
MaxConnectionReceiveWindow: c.QUIC.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.QUIC.MaxIdleTimeout,
KeepAlivePeriod: c.QUIC.KeepAlivePeriod,
DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery,
}
return nil
}
func (c *clientConfig) fillBandwidthConfig(hyConfig *client.Config) error {
// New core now allows users to omit bandwidth values and use built-in congestion control
var err error
if c.Bandwidth.Up != "" {
hyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up)
if err != nil {
return configError{Field: "bandwidth.up", Err: err}
}
}
if c.Bandwidth.Down != "" {
hyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down)
if err != nil {
return configError{Field: "bandwidth.down", Err: err}
}
}
return nil
}
func (c *clientConfig) fillFastOpen(hyConfig *client.Config) error {
hyConfig.FastOpen = c.FastOpen
return nil
}
// URI generates a URI for sharing the config with others.
// Note that only the bare minimum of information required to
// connect to the server is included in the URI, specifically:
// - server address
// - authentication
// - obfuscation type
// - obfuscation password
// - TLS SNI
// - TLS insecure
// - TLS pinned SHA256 hash (normalized)
func (c *clientConfig) URI() string {
q := url.Values{}
switch strings.ToLower(c.Obfs.Type) {
case "salamander":
q.Set("obfs", "salamander")
q.Set("obfs-password", c.Obfs.Salamander.Password)
}
if c.TLS.SNI != "" {
q.Set("sni", c.TLS.SNI)
}
if c.TLS.Insecure {
q.Set("insecure", "1")
}
if c.TLS.PinSHA256 != "" {
q.Set("pinSHA256", normalizeCertHash(c.TLS.PinSHA256))
}
var user *url.Userinfo
if c.Auth != "" {
// We need to handle the special case of user:pass pairs
rs := strings.SplitN(c.Auth, ":", 2)
if len(rs) == 2 {
user = url.UserPassword(rs[0], rs[1])
} else {
user = url.User(c.Auth)
}
}
u := url.URL{
Scheme: "hysteria2",
User: user,
Host: c.Server,
Path: "/",
RawQuery: q.Encode(),
}
return u.String()
}
// parseURI tries to parse the server address field as a URI,
// and fills the config with the information contained in the URI.
// Returns whether the server address field is a valid URI.
// This allows a user to use put a URI as the server address and
// omit the fields that are already contained in the URI.
func (c *clientConfig) parseURI() bool {
u, err := url.Parse(c.Server)
if err != nil {
return false
}
if u.Scheme != "hysteria2" && u.Scheme != "hy2" {
return false
}
if u.User != nil {
auth, err := url.QueryUnescape(u.User.String())
if err != nil {
return false
}
c.Auth = auth
}
c.Server = u.Host
q := u.Query()
if obfsType := q.Get("obfs"); obfsType != "" {
c.Obfs.Type = obfsType
switch strings.ToLower(obfsType) {
case "salamander":
c.Obfs.Salamander.Password = q.Get("obfs-password")
}
}
if sni := q.Get("sni"); sni != "" {
c.TLS.SNI = sni
}
if insecure, err := strconv.ParseBool(q.Get("insecure")); err == nil {
c.TLS.Insecure = insecure
}
if pinSHA256 := q.Get("pinSHA256"); pinSHA256 != "" {
c.TLS.PinSHA256 = pinSHA256
}
return true
}
// Config validates the fields and returns a ready-to-use Hysteria client config
func (c *clientConfig) Config() (*client.Config, error) {
c.parseURI()
hyConfig := &client.Config{}
fillers := []func(*client.Config) error{
c.fillServerAddr,
c.fillConnFactory,
c.fillAuth,
c.fillTLSConfig,
c.fillQUICConfig,
c.fillBandwidthConfig,
c.fillFastOpen,
}
for _, f := range fillers {
if err := f(hyConfig); err != nil {
return nil, err
}
}
return hyConfig, nil
}
func runClientCmd(cmd *cobra.Command, args []string) {
logger.Info("client mode")
runClient(defaultViper)
}
func runClient(v *viper.Viper) {
if err := v.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := v.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
c, err := client.NewReconnectableClient(
config.Config,
func(c client.Client, info *client.HandshakeInfo, count int) {
connectLog(info, count)
// On the client side, we start checking for updates after we successfully connect
// to the server, which, depending on whether lazy mode is enabled, may or may not
// be immediately after the client starts. We don't want the update check request
// to interfere with the lazy mode option.
if count == 1 && !disableUpdateCheck {
go runCheckUpdateClient(c)
}
}, config.Lazy)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
uri := config.URI()
if showQR {
logger.Warn("--qr flag is deprecated and will be removed in future release, " +
"please use `share` subcommand to generate share URI and QR code")
logger.Info("use this URI to share your server", zap.String("uri", uri))
utils.PrintQR(uri)
}
// Register modes
var runner clientModeRunner
if config.SOCKS5 != nil {
runner.Add("SOCKS5 server", func() error {
return clientSOCKS5(*config.SOCKS5, c)
})
}
if config.HTTP != nil {
runner.Add("HTTP proxy server", func() error {
return clientHTTP(*config.HTTP, c)
})
}
if len(config.TCPForwarding) > 0 {
runner.Add("TCP forwarding", func() error {
return clientTCPForwarding(config.TCPForwarding, c)
})
}
if len(config.UDPForwarding) > 0 {
runner.Add("UDP forwarding", func() error {
return clientUDPForwarding(config.UDPForwarding, c)
})
}
if config.TCPTProxy != nil {
runner.Add("TCP transparent proxy", func() error {
return clientTCPTProxy(*config.TCPTProxy, c)
})
}
if config.UDPTProxy != nil {
runner.Add("UDP transparent proxy", func() error {
return clientUDPTProxy(*config.UDPTProxy, c)
})
}
if config.TCPRedirect != nil {
runner.Add("TCP redirect", func() error {
return clientTCPRedirect(*config.TCPRedirect, c)
})
}
if config.TUN != nil {
runner.Add("TUN", func() error {
return clientTUN(*config.TUN, c)
})
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signalChan)
runnerChan := make(chan clientModeRunnerResult, 1)
go func() {
runnerChan <- runner.Run()
}()
select {
case <-signalChan:
logger.Info("received signal, shutting down gracefully")
case r := <-runnerChan:
if r.OK {
logger.Info(r.Msg)
} else {
_ = c.Close() // Close the client here as Fatal will exit the program without running defer
if r.Err != nil {
logger.Fatal(r.Msg, zap.Error(r.Err))
} else {
logger.Fatal(r.Msg)
}
}
}
}
type clientModeRunner struct {
ModeMap map[string]func() error
}
type clientModeRunnerResult struct {
OK bool
Msg string
Err error
}
func (r *clientModeRunner) Add(name string, f func() error) {
if r.ModeMap == nil {
r.ModeMap = make(map[string]func() error)
}
r.ModeMap[name] = f
}
func (r *clientModeRunner) Run() clientModeRunnerResult {
if len(r.ModeMap) == 0 {
return clientModeRunnerResult{OK: false, Msg: "no mode specified"}
}
type modeError struct {
Name string
Err error
}
errChan := make(chan modeError, len(r.ModeMap))
for name, f := range r.ModeMap {
go func(name string, f func() error) {
err := f()
errChan <- modeError{name, err}
}(name, f)
}
// Fatal if any one of the modes fails
for i := 0; i < len(r.ModeMap); i++ {
e := <-errChan
if e.Err != nil {
return clientModeRunnerResult{OK: false, Msg: "failed to run " + e.Name, Err: e.Err}
}
}
// We don't really have any such cases, as currently none of our modes would stop on themselves without error.
// But we leave the possibility here for future expansion.
return clientModeRunnerResult{OK: true, Msg: "finished without error"}
}
func clientSOCKS5(config socks5Config, c client.Client) error {
if config.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
l, err := proxymux.ListenSOCKS(config.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
var authFunc func(username, password string) bool
username, password := config.Username, config.Password
if username != "" && password != "" {
authFunc = func(u, p string) bool {
return u == username && p == password
}
}
s := socks5.Server{
HyClient: c,
AuthFunc: authFunc,
DisableUDP: config.DisableUDP,
EventLogger: &socks5Logger{},
}
logger.Info("SOCKS5 server listening", zap.String("addr", config.Listen))
return s.Serve(l)
}
func clientHTTP(config httpConfig, c client.Client) error {
if config.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
l, err := proxymux.ListenHTTP(config.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
var authFunc func(username, password string) bool
username, password := config.Username, config.Password
if username != "" && password != "" {
authFunc = func(u, p string) bool {
return u == username && p == password
}
}
if config.Realm == "" {
config.Realm = "Hysteria"
}
h := http.Server{
HyClient: c,
AuthFunc: authFunc,
AuthRealm: config.Realm,
EventLogger: &httpLogger{},
}
logger.Info("HTTP proxy server listening", zap.String("addr", config.Listen))
return h.Serve(l)
}
func clientTCPForwarding(entries []tcpForwardingEntry, c client.Client) error {
errChan := make(chan error, len(entries))
for _, e := range entries {
if e.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
if e.Remote == "" {
return configError{Field: "remote", Err: errors.New("remote address is empty")}
}
l, err := correctnet.Listen("tcp", e.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
logger.Info("TCP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote))
go func(remote string) {
t := &forwarding.TCPTunnel{
HyClient: c,
Remote: remote,
EventLogger: &tcpLogger{},
}
errChan <- t.Serve(l)
}(e.Remote)
}
// Return if any one of the forwarding fails
return <-errChan
}
func clientUDPForwarding(entries []udpForwardingEntry, c client.Client) error {
errChan := make(chan error, len(entries))
for _, e := range entries {
if e.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
if e.Remote == "" {
return configError{Field: "remote", Err: errors.New("remote address is empty")}
}
l, err := correctnet.ListenPacket("udp", e.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
logger.Info("UDP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote))
go func(remote string, timeout time.Duration) {
u := &forwarding.UDPTunnel{
HyClient: c,
Remote: remote,
Timeout: timeout,
EventLogger: &udpLogger{},
}
errChan <- u.Serve(l)
}(e.Remote, e.Timeout)
}
// Return if any one of the forwarding fails
return <-errChan
}
func clientTCPTProxy(config tcpTProxyConfig, c client.Client) error {
if config.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
laddr, err := net.ResolveTCPAddr("tcp", config.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
p := &tproxy.TCPTProxy{
HyClient: c,
EventLogger: &tcpTProxyLogger{},
}
logger.Info("TCP transparent proxy listening", zap.String("addr", config.Listen))
return p.ListenAndServe(laddr)
}
func clientUDPTProxy(config udpTProxyConfig, c client.Client) error {
if config.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
laddr, err := net.ResolveUDPAddr("udp", config.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
p := &tproxy.UDPTProxy{
HyClient: c,
Timeout: config.Timeout,
EventLogger: &udpTProxyLogger{},
}
logger.Info("UDP transparent proxy listening", zap.String("addr", config.Listen))
return p.ListenAndServe(laddr)
}
func clientTCPRedirect(config tcpRedirectConfig, c client.Client) error {
if config.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
laddr, err := net.ResolveTCPAddr("tcp", config.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
p := &redirect.TCPRedirect{
HyClient: c,
EventLogger: &tcpRedirectLogger{},
}
logger.Info("TCP redirect listening", zap.String("addr", config.Listen))
return p.ListenAndServe(laddr)
}
func clientTUN(config tunConfig, c client.Client) error {
supportedPlatforms := []string{"linux", "darwin", "windows", "android"}
if !slices.Contains(supportedPlatforms, runtime.GOOS) {
logger.Error("TUN is not supported on this platform", zap.String("platform", runtime.GOOS))
}
if config.Name == "" {
return configError{Field: "name", Err: errors.New("name is empty")}
}
if config.MTU == 0 {
config.MTU = 1500
}
timeout := int64(config.Timeout.Seconds())
if timeout == 0 {
timeout = 300
}
if config.Address.IPv4 == "" {
config.Address.IPv4 = "100.100.100.101/30"
}
prefix4, err := netip.ParsePrefix(config.Address.IPv4)
if err != nil {
return configError{Field: "address.ipv4", Err: err}
}
if config.Address.IPv6 == "" {
config.Address.IPv6 = "2001::ffff:ffff:ffff:fff1/126"
}
prefix6, err := netip.ParsePrefix(config.Address.IPv6)
if err != nil {
return configError{Field: "address.ipv6", Err: err}
}
server := &tun.Server{
HyClient: c,
EventLogger: &tunLogger{},
Logger: logger,
IfName: config.Name,
MTU: config.MTU,
Timeout: timeout,
Inet4Address: []netip.Prefix{prefix4},
Inet6Address: []netip.Prefix{prefix6},
}
if config.Route != nil {
server.AutoRoute = true
server.StructRoute = config.Route.Strict
parsePrefixes := func(field string, ss []string) ([]netip.Prefix, error) {
var prefixes []netip.Prefix
for i, s := range ss {
var p netip.Prefix
if strings.Contains(s, "/") {
var err error
p, err = netip.ParsePrefix(s)
if err != nil {
return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err}
}
} else {
pa, err := netip.ParseAddr(s)
if err != nil {
return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err}
}
p = netip.PrefixFrom(pa, pa.BitLen())
}
prefixes = append(prefixes, p)
}
return prefixes, nil
}
server.Inet4RouteAddress, err = parsePrefixes("route.ipv4", config.Route.IPv4)
if err != nil {
return err
}
server.Inet6RouteAddress, err = parsePrefixes("route.ipv6", config.Route.IPv6)
if err != nil {
return err
}
server.Inet4RouteExcludeAddress, err = parsePrefixes("route.ipv4Exclude", config.Route.IPv4Exclude)
if err != nil {
return err
}
server.Inet6RouteExcludeAddress, err = parsePrefixes("route.ipv6Exclude", config.Route.IPv6Exclude)
if err != nil {
return err
}
}
logger.Info("TUN listening", zap.String("interface", config.Name))
return server.Serve()
}
// parseServerAddrString parses server address string.
// Server address can be in either "host:port" or "host" format (in which case we assume port 443).
func parseServerAddrString(addrStr string) (host, port, hostPort string) {
h, p, err := net.SplitHostPort(addrStr)
if err != nil {
return addrStr, "443", net.JoinHostPort(addrStr, "443")
}
return h, p, addrStr
}
// isPortHoppingPort returns whether the port string is a port hopping port.
// We consider a port string to be a port hopping port if it contains "-" or ",".
func isPortHoppingPort(port string) bool {
return strings.Contains(port, "-") || strings.Contains(port, ",")
}
// normalizeCertHash normalizes a certificate hash string.
// It converts all characters to lowercase and removes possible separators such as ":" and "-".
func normalizeCertHash(hash string) string {
r := strings.ToLower(hash)
r = strings.ReplaceAll(r, ":", "")
r = strings.ReplaceAll(r, "-", "")
return r
}
type adaptiveConnFactory struct {
NewFunc func(addr net.Addr) (net.PacketConn, error)
Obfuscator obfs.Obfuscator // nil if no obfuscation
}
func (f *adaptiveConnFactory) New(addr net.Addr) (net.PacketConn, error) {
if f.Obfuscator == nil {
return f.NewFunc(addr)
} else {
conn, err := f.NewFunc(addr)
if err != nil {
return nil, err
}
return obfs.WrapPacketConn(conn, f.Obfuscator), nil
}
}
func connectLog(info *client.HandshakeInfo, count int) {
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx),
zap.Int("count", count))
}
type socks5Logger struct{}
func (l *socks5Logger) TCPRequest(addr net.Addr, reqAddr string) {
logger.Debug("SOCKS5 TCP request", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr))
}
func (l *socks5Logger) TCPError(addr net.Addr, reqAddr string, err error) {
if err == nil {
logger.Debug("SOCKS5 TCP closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("SOCKS5 TCP error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
func (l *socks5Logger) UDPRequest(addr net.Addr) {
logger.Debug("SOCKS5 UDP request", zap.String("addr", addr.String()))
}
func (l *socks5Logger) UDPError(addr net.Addr, err error) {
if err == nil {
logger.Debug("SOCKS5 UDP closed", zap.String("addr", addr.String()))
} else {
logger.Warn("SOCKS5 UDP error", zap.String("addr", addr.String()), zap.Error(err))
}
}
type httpLogger struct{}
func (l *httpLogger) ConnectRequest(addr net.Addr, reqAddr string) {
logger.Debug("HTTP CONNECT request", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr))
}
func (l *httpLogger) ConnectError(addr net.Addr, reqAddr string, err error) {
if err == nil {
logger.Debug("HTTP CONNECT closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("HTTP CONNECT error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
func (l *httpLogger) HTTPRequest(addr net.Addr, reqURL string) {
logger.Debug("HTTP request", zap.String("addr", addr.String()), zap.String("reqURL", reqURL))
}
func (l *httpLogger) HTTPError(addr net.Addr, reqURL string, err error) {
if err == nil {
logger.Debug("HTTP closed", zap.String("addr", addr.String()), zap.String("reqURL", reqURL))
} else {
logger.Warn("HTTP error", zap.String("addr", addr.String()), zap.String("reqURL", reqURL), zap.Error(err))
}
}
type tcpLogger struct{}
func (l *tcpLogger) Connect(addr net.Addr) {
logger.Debug("TCP forwarding connect", zap.String("addr", addr.String()))
}
func (l *tcpLogger) Error(addr net.Addr, err error) {
if err == nil {
logger.Debug("TCP forwarding closed", zap.String("addr", addr.String()))
} else {
logger.Warn("TCP forwarding error", zap.String("addr", addr.String()), zap.Error(err))
}
}
type udpLogger struct{}
func (l *udpLogger) Connect(addr net.Addr) {
logger.Debug("UDP forwarding connect", zap.String("addr", addr.String()))
}
func (l *udpLogger) Error(addr net.Addr, err error) {
if err == nil {
logger.Debug("UDP forwarding closed", zap.String("addr", addr.String()))
} else {
logger.Warn("UDP forwarding error", zap.String("addr", addr.String()), zap.Error(err))
}
}
type tcpTProxyLogger struct{}
func (l *tcpTProxyLogger) Connect(addr, reqAddr net.Addr) {
logger.Debug("TCP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
}
func (l *tcpTProxyLogger) Error(addr, reqAddr net.Addr, err error) {
if err == nil {
logger.Debug("TCP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
} else {
logger.Warn("TCP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
}
}
type udpTProxyLogger struct{}
func (l *udpTProxyLogger) Connect(addr, reqAddr net.Addr) {
logger.Debug("UDP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
}
func (l *udpTProxyLogger) Error(addr, reqAddr net.Addr, err error) {
if err == nil {
logger.Debug("UDP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
} else {
logger.Warn("UDP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
}
}
type tcpRedirectLogger struct{}
func (l *tcpRedirectLogger) Connect(addr, reqAddr net.Addr) {
logger.Debug("TCP redirect connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
}
func (l *tcpRedirectLogger) Error(addr, reqAddr net.Addr, err error) {
if err == nil {
logger.Debug("TCP redirect closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
} else {
logger.Warn("TCP redirect error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
}
}
type tunLogger struct{}
func (l *tunLogger) TCPRequest(addr, reqAddr string) {
logger.Debug("TUN TCP request", zap.String("addr", addr), zap.String("reqAddr", reqAddr))
}
func (l *tunLogger) TCPError(addr, reqAddr string, err error) {
if err == nil {
logger.Debug("TUN TCP closed", zap.String("addr", addr), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("TUN TCP error", zap.String("addr", addr), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
func (l *tunLogger) UDPRequest(addr string) {
logger.Debug("TUN UDP request", zap.String("addr", addr))
}
func (l *tunLogger) UDPError(addr string, err error) {
if err == nil {
logger.Debug("TUN UDP closed", zap.String("addr", addr))
} else {
logger.Warn("TUN UDP error", zap.String("addr", addr), zap.Error(err))
}
}
================================================
FILE: app/cmd/client_test.go
================================================
package cmd
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/spf13/viper"
)
// TestClientConfig tests the parsing of the client config
func TestClientConfig(t *testing.T) {
viper.SetConfigFile("client_test.yaml")
err := viper.ReadInConfig()
assert.NoError(t, err)
var config clientConfig
err = viper.Unmarshal(&config)
assert.NoError(t, err)
assert.Equal(t, config, clientConfig{
Server: "example.com",
Auth: "weak_ahh_password",
Transport: clientConfigTransport{
Type: "udp",
UDP: clientConfigTransportUDP{
HopInterval: 30 * time.Second,
},
},
Obfs: clientConfigObfs{
Type: "salamander",
Salamander: clientConfigObfsSalamander{
Password: "cry_me_a_r1ver",
},
},
TLS: clientConfigTLS{
SNI: "another.example.com",
Insecure: true,
PinSHA256: "114515DEADBEEF",
CA: "custom_ca.crt",
ClientCertificate: "client.crt",
ClientKey: "client.key",
},
QUIC: clientConfigQUIC{
InitStreamReceiveWindow: 1145141,
MaxStreamReceiveWindow: 1145142,
InitConnectionReceiveWindow: 1145143,
MaxConnectionReceiveWindow: 1145144,
MaxIdleTimeout: 10 * time.Second,
KeepAlivePeriod: 4 * time.Second,
DisablePathMTUDiscovery: true,
Sockopts: clientConfigQUICSockopts{
BindInterface: stringRef("eth0"),
FirewallMark: uint32Ref(1234),
FdControlUnixSocket: stringRef("test.sock"),
},
},
Bandwidth: clientConfigBandwidth{
Up: "200 mbps",
Down: "1 gbps",
},
FastOpen: true,
Lazy: true,
SOCKS5: &socks5Config{
Listen: "127.0.0.1:1080",
Username: "anon",
Password: "bro",
DisableUDP: true,
},
HTTP: &httpConfig{
Listen: "127.0.0.1:8080",
Username: "qqq",
Password: "bruh",
Realm: "martian",
},
TCPForwarding: []tcpForwardingEntry{
{
Listen: "127.0.0.1:8088",
Remote: "internal.example.com:80",
},
},
UDPForwarding: []udpForwardingEntry{
{
Listen: "127.0.0.1:5353",
Remote: "internal.example.com:53",
Timeout: 50 * time.Second,
},
},
TCPTProxy: &tcpTProxyConfig{
Listen: "127.0.0.1:2500",
},
UDPTProxy: &udpTProxyConfig{
Listen: "127.0.0.1:2501",
Timeout: 20 * time.Second,
},
TCPRedirect: &tcpRedirectConfig{
Listen: "127.0.0.1:3500",
},
TUN: &tunConfig{
Name: "hytun",
MTU: 1500,
Timeout: 60 * time.Second,
Address: struct {
IPv4 string `mapstructure:"ipv4"`
IPv6 string `mapstructure:"ipv6"`
}{IPv4: "100.100.100.101/30", IPv6: "2001::ffff:ffff:ffff:fff1/126"},
Route: &struct {
Strict bool `mapstructure:"strict"`
IPv4 []string `mapstructure:"ipv4"`
IPv6 []string `mapstructure:"ipv6"`
IPv4Exclude []string `mapstructure:"ipv4Exclude"`
IPv6Exclude []string `mapstructure:"ipv6Exclude"`
}{
Strict: true,
IPv4: []string{"0.0.0.0/0"},
IPv6: []string{"2000::/3"},
IPv4Exclude: []string{"192.0.2.1/32"},
IPv6Exclude: []string{"2001:db8::1/128"},
},
},
})
}
// TestClientConfigURI tests URI-related functions of clientConfig
func TestClientConfigURI(t *testing.T) {
tests := []struct {
uri string
uriOK bool
config *clientConfig
}{
{
uri: "hysteria2://god@zilla.jp/",
uriOK: true,
config: &clientConfig{
Server: "zilla.jp",
Auth: "god",
},
},
{
uri: "hysteria2://john:wick@continental.org:4443/",
uriOK: true,
config: &clientConfig{
Server: "continental.org:4443",
Auth: "john:wick",
},
},
{
uri: "hysteria2://saul@better.call:7000-10000,20000/",
uriOK: true,
config: &clientConfig{
Server: "better.call:7000-10000,20000",
Auth: "saul",
},
},
{
uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&pinSHA256=deadbeef&sni=crap.cc",
uriOK: true,
config: &clientConfig{
Server: "noauth.com",
Auth: "",
Obfs: clientConfigObfs{
Type: "salamander",
Salamander: clientConfigObfsSalamander{
Password: "66ccff",
},
},
TLS: clientConfigTLS{
SNI: "crap.cc",
Insecure: true,
PinSHA256: "deadbeef",
},
},
},
{
uri: "invalid.bs",
uriOK: false,
config: nil,
},
{
uri: "https://www.google.com/search?q=test",
uriOK: false,
config: nil,
},
}
for _, test := range tests {
t.Run(test.uri, func(t *testing.T) {
// Test parseURI
nc := &clientConfig{Server: test.uri}
assert.Equal(t, nc.parseURI(), test.uriOK)
if test.uriOK {
assert.Equal(t, nc, test.config)
}
// Test URI generation
if test.config != nil {
assert.Equal(t, test.config.URI(), test.uri)
}
})
}
}
func stringRef(s string) *string {
return &s
}
func uint32Ref(i uint32) *uint32 {
return &i
}
================================================
FILE: app/cmd/client_test.yaml
================================================
server: example.com
auth: weak_ahh_password
transport:
type: udp
udp:
hopInterval: 30s
obfs:
type: salamander
salamander:
password: cry_me_a_r1ver
tls:
sni: another.example.com
insecure: true
pinSHA256: 114515DEADBEEF
ca: custom_ca.crt
clientCertificate: client.crt
clientKey: client.key
quic:
initStreamReceiveWindow: 1145141
maxStreamReceiveWindow: 1145142
initConnReceiveWindow: 1145143
maxConnReceiveWindow: 1145144
maxIdleTimeout: 10s
keepAlivePeriod: 4s
disablePathMTUDiscovery: true
sockopts:
bindInterface: eth0
fwmark: 1234
fdControlUnixSocket: test.sock
bandwidth:
up: 200 mbps
down: 1 gbps
fastOpen: true
lazy: true
socks5:
listen: 127.0.0.1:1080
username: anon
password: bro
disableUDP: true
http:
listen: 127.0.0.1:8080
username: qqq
password: bruh
realm: martian
tcpForwarding:
- listen: 127.0.0.1:8088
remote: internal.example.com:80
udpForwarding:
- listen: 127.0.0.1:5353
remote: internal.example.com:53
timeout: 50s
tcpTProxy:
listen: 127.0.0.1:2500
udpTProxy:
listen: 127.0.0.1:2501
timeout: 20s
tcpRedirect:
listen: 127.0.0.1:3500
tun:
name: "hytun"
mtu: 1500
timeout: 1m
address:
ipv4: 100.100.100.101/30
ipv6: 2001::ffff:ffff:ffff:fff1/126
route:
strict: true
ipv4: [ 0.0.0.0/0 ]
ipv6: [ "2000::/3" ]
ipv4Exclude: [ 192.0.2.1/32 ]
ipv6Exclude: [ "2001:db8::1/128" ]
================================================
FILE: app/cmd/errors.go
================================================
package cmd
import (
"fmt"
)
type configError struct {
Field string
Err error
}
func (e configError) Error() string {
return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err)
}
func (e configError) Unwrap() error {
return e.Err
}
================================================
FILE: app/cmd/ping.go
================================================
package cmd
import (
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
)
// pingCmd represents the ping command
var pingCmd = &cobra.Command{
Use: "ping address",
Short: "Ping mode",
Long: "Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.",
Run: runPingCmd,
}
func init() {
rootCmd.AddCommand(pingCmd)
}
func runPingCmd(cmd *cobra.Command, args []string) {
logger.Info("ping mode")
if len(args) != 1 {
logger.Fatal("must specify one and only one address")
}
addr := args[0]
runPing(defaultViper, addr)
}
func runPing(v *viper.Viper, addr string) {
if err := v.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := v.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
hyConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
c, info, err := client.NewClient(hyConfig)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx))
logger.Info("connecting", zap.String("addr", addr))
start := time.Now()
conn, err := c.TCP(addr)
if err != nil {
logger.Fatal("failed to connect", zap.Error(err), zap.String("time", time.Since(start).String()))
}
defer conn.Close()
logger.Info("connected", zap.String("time", time.Since(start).String()))
}
================================================
FILE: app/cmd/root.go
================================================
package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
appLogo = `
░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█░░░▀▀▄
░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█░░░▄▀░
░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀▀▀
`
appDesc = "a powerful, lightning fast and censorship resistant proxy"
appAuthors = "Aperture Internet Laboratory "
appLogLevelEnv = "HYSTERIA_LOG_LEVEL"
appLogFormatEnv = "HYSTERIA_LOG_FORMAT"
appDisableUpdateCheckEnv = "HYSTERIA_DISABLE_UPDATE_CHECK"
appACMEDirEnv = "HYSTERIA_ACME_DIR"
)
var (
// These values will be injected by the build system
appVersion = "Unknown"
appDate = "Unknown"
appType = "Unknown" // aka channel
appToolchain = "Unknown"
appCommit = "Unknown"
appPlatform = "Unknown"
appArch = "Unknown"
libVersion = "Unknown"
appVersionLong = fmt.Sprintf("Version:\t%s\n"+
"BuildDate:\t%s\n"+
"BuildType:\t%s\n"+
"Toolchain:\t%s\n"+
"CommitHash:\t%s\n"+
"Platform:\t%s\n"+
"Architecture:\t%s\n"+
"Libraries:\tquic-go=%s",
appVersion, appDate, appType, appToolchain, appCommit, appPlatform, appArch, libVersion)
appAboutLong = fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong)
)
var (
logger *zap.Logger
defaultViper *viper.Viper
)
// Flags
var (
cfgFile string
logLevel string
logFormat string
disableUpdateCheck bool
)
var rootCmd = &cobra.Command{
Use: "hysteria",
Short: appDesc,
Long: appAboutLong,
Run: runClientCmd, // Default to client mode
}
var logLevelMap = map[string]zapcore.Level{
"debug": zapcore.DebugLevel,
"info": zapcore.InfoLevel,
"warn": zapcore.WarnLevel,
"error": zapcore.ErrorLevel,
}
var logFormatMap = map[string]zapcore.EncoderConfig{
"console": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.RFC3339TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
"json": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.EpochMillisTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
initFlags()
cobra.MousetrapHelpText = "" // Disable the mousetrap so Windows users can run the exe directly by double-clicking
cobra.OnInitialize(initConfig)
cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config
}
func initFlags() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", envOrDefaultString(appLogLevelEnv, "info"), "log level")
rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "f", envOrDefaultString(appLogFormatEnv, "console"), "log format")
rootCmd.PersistentFlags().BoolVar(&disableUpdateCheck, "disable-update-check", envOrDefaultBool(appDisableUpdateCheckEnv, false), "disable update check")
}
func initConfig() {
defaultViper = viper.New()
if cfgFile != "" {
defaultViper.SetConfigFile(cfgFile)
} else {
defaultViper.SetConfigName("config")
defaultViper.SetConfigType("yaml")
viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...)
defaultViper.AddConfigPath(".")
defaultViper.AddConfigPath("$HOME/.hysteria")
defaultViper.AddConfigPath("/etc/hysteria/")
}
}
func initLogger() {
level, ok := logLevelMap[strings.ToLower(logLevel)]
if !ok {
fmt.Printf("unsupported log level: %s\n", logLevel)
os.Exit(1)
}
enc, ok := logFormatMap[strings.ToLower(logFormat)]
if !ok {
fmt.Printf("unsupported log format: %s\n", logFormat)
os.Exit(1)
}
c := zap.Config{
Level: zap.NewAtomicLevelAt(level),
DisableCaller: true,
DisableStacktrace: true,
Encoding: strings.ToLower(logFormat),
EncoderConfig: enc,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
var err error
logger, err = c.Build()
if err != nil {
fmt.Printf("failed to initialize logger: %s\n", err)
os.Exit(1)
}
}
func envOrDefaultString(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envOrDefaultBool(key string, def bool) bool {
if v := os.Getenv(key); v != "" {
b, _ := strconv.ParseBool(v)
return b
}
return def
}
================================================
FILE: app/cmd/server.go
================================================
package cmd
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/caddyserver/certmagic"
"github.com/libdns/cloudflare"
"github.com/libdns/duckdns"
"github.com/libdns/gandi"
"github.com/libdns/godaddy"
"github.com/libdns/namedotcom"
"github.com/libdns/vultr"
"github.com/mholt/acmez/acme"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/hysteria/extras/v2/auth"
"github.com/apernet/hysteria/extras/v2/correctnet"
"github.com/apernet/hysteria/extras/v2/masq"
"github.com/apernet/hysteria/extras/v2/obfs"
"github.com/apernet/hysteria/extras/v2/outbounds"
"github.com/apernet/hysteria/extras/v2/sniff"
"github.com/apernet/hysteria/extras/v2/trafficlogger"
eUtils "github.com/apernet/hysteria/extras/v2/utils"
)
const (
defaultListenAddr = ":443"
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "Server mode",
Run: runServerCmd,
}
func init() {
rootCmd.AddCommand(serverCmd)
}
type serverConfig struct {
Listen string `mapstructure:"listen"`
Obfs serverConfigObfs `mapstructure:"obfs"`
TLS *serverConfigTLS `mapstructure:"tls"`
ACME *serverConfigACME `mapstructure:"acme"`
QUIC serverConfigQUIC `mapstructure:"quic"`
Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"`
IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"`
SpeedTest bool `mapstructure:"speedTest"`
DisableUDP bool `mapstructure:"disableUDP"`
UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"`
Auth serverConfigAuth `mapstructure:"auth"`
Resolver serverConfigResolver `mapstructure:"resolver"`
Sniff serverConfigSniff `mapstructure:"sniff"`
ACL serverConfigACL `mapstructure:"acl"`
Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"`
TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"`
Masquerade serverConfigMasquerade `mapstructure:"masquerade"`
}
type serverConfigObfsSalamander struct {
Password string `mapstructure:"password"`
}
type serverConfigObfs struct {
Type string `mapstructure:"type"`
Salamander serverConfigObfsSalamander `mapstructure:"salamander"`
}
type serverConfigTLS struct {
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict"
ClientCA string `mapstructure:"clientCA"`
}
type serverConfigACME struct {
// Common fields
Domains []string `mapstructure:"domains"`
Email string `mapstructure:"email"`
CA string `mapstructure:"ca"`
ListenHost string `mapstructure:"listenHost"`
Dir string `mapstructure:"dir"`
// Type selection
Type string `mapstructure:"type"`
HTTP serverConfigACMEHTTP `mapstructure:"http"`
TLS serverConfigACMETLS `mapstructure:"tls"`
DNS serverConfigACMEDNS `mapstructure:"dns"`
// Legacy fields for backwards compatibility
// Only applicable when Type is empty
DisableHTTP bool `mapstructure:"disableHTTP"`
DisableTLSALPN bool `mapstructure:"disableTLSALPN"`
AltHTTPPort int `mapstructure:"altHTTPPort"`
AltTLSALPNPort int `mapstructure:"altTLSALPNPort"`
}
type serverConfigACMEHTTP struct {
AltPort int `mapstructure:"altPort"`
}
type serverConfigACMETLS struct {
AltPort int `mapstructure:"altPort"`
}
type serverConfigACMEDNS struct {
Name string `mapstructure:"name"`
Config map[string]string `mapstructure:"config"`
}
type serverConfigQUIC struct {
InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"`
MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"`
MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"`
MaxIncomingStreams int64 `mapstructure:"maxIncomingStreams"`
DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"`
}
type serverConfigBandwidth struct {
Up string `mapstructure:"up"`
Down string `mapstructure:"down"`
}
type serverConfigAuthHTTP struct {
URL string `mapstructure:"url"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigAuth struct {
Type string `mapstructure:"type"`
Password string `mapstructure:"password"`
UserPass map[string]string `mapstructure:"userpass"`
HTTP serverConfigAuthHTTP `mapstructure:"http"`
Command string `mapstructure:"command"`
}
type serverConfigResolverTCP struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
}
type serverConfigResolverUDP struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
}
type serverConfigResolverTLS struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
SNI string `mapstructure:"sni"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigResolverHTTPS struct {
Addr string `mapstructure:"addr"`
Timeout time.Duration `mapstructure:"timeout"`
SNI string `mapstructure:"sni"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigResolver struct {
Type string `mapstructure:"type"`
TCP serverConfigResolverTCP `mapstructure:"tcp"`
UDP serverConfigResolverUDP `mapstructure:"udp"`
TLS serverConfigResolverTLS `mapstructure:"tls"`
HTTPS serverConfigResolverHTTPS `mapstructure:"https"`
}
type serverConfigSniff struct {
Enable bool `mapstructure:"enable"`
Timeout time.Duration `mapstructure:"timeout"`
RewriteDomain bool `mapstructure:"rewriteDomain"`
TCPPorts string `mapstructure:"tcpPorts"`
UDPPorts string `mapstructure:"udpPorts"`
}
type serverConfigACL struct {
File string `mapstructure:"file"`
Inline []string `mapstructure:"inline"`
GeoIP string `mapstructure:"geoip"`
GeoSite string `mapstructure:"geosite"`
GeoUpdateInterval time.Duration `mapstructure:"geoUpdateInterval"`
}
type serverConfigOutboundDirect struct {
Mode string `mapstructure:"mode"`
BindIPv4 string `mapstructure:"bindIPv4"`
BindIPv6 string `mapstructure:"bindIPv6"`
BindDevice string `mapstructure:"bindDevice"`
FastOpen bool `mapstructure:"fastOpen"`
}
type serverConfigOutboundSOCKS5 struct {
Addr string `mapstructure:"addr"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
}
type serverConfigOutboundHTTP struct {
URL string `mapstructure:"url"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigOutboundEntry struct {
Name string `mapstructure:"name"`
Type string `mapstructure:"type"`
Direct serverConfigOutboundDirect `mapstructure:"direct"`
SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"`
HTTP serverConfigOutboundHTTP `mapstructure:"http"`
}
type serverConfigTrafficStats struct {
Listen string `mapstructure:"listen"`
Secret string `mapstructure:"secret"`
}
type serverConfigMasqueradeFile struct {
Dir string `mapstructure:"dir"`
}
type serverConfigMasqueradeProxy struct {
URL string `mapstructure:"url"`
RewriteHost bool `mapstructure:"rewriteHost"`
XForwarded bool `mapstructure:"xForwarded"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigMasqueradeString struct {
Content string `mapstructure:"content"`
Headers map[string]string `mapstructure:"headers"`
StatusCode int `mapstructure:"statusCode"`
}
type serverConfigMasquerade struct {
Type string `mapstructure:"type"`
File serverConfigMasqueradeFile `mapstructure:"file"`
Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"`
String serverConfigMasqueradeString `mapstructure:"string"`
ListenHTTP string `mapstructure:"listenHTTP"`
ListenHTTPS string `mapstructure:"listenHTTPS"`
ForceHTTPS bool `mapstructure:"forceHTTPS"`
}
func (c *serverConfig) fillConn(hyConfig *server.Config) error {
listenAddr := c.Listen
if listenAddr == "" {
listenAddr = defaultListenAddr
}
uAddr, err := net.ResolveUDPAddr("udp", listenAddr)
if err != nil {
return configError{Field: "listen", Err: err}
}
conn, err := correctnet.ListenUDP("udp", uAddr)
if err != nil {
return configError{Field: "listen", Err: err}
}
switch strings.ToLower(c.Obfs.Type) {
case "", "plain":
hyConfig.Conn = conn
return nil
case "salamander":
ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))
if err != nil {
return configError{Field: "obfs.salamander.password", Err: err}
}
hyConfig.Conn = obfs.WrapPacketConn(conn, ob)
return nil
default:
return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
}
}
func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
if c.TLS == nil && c.ACME == nil {
return configError{Field: "tls", Err: errors.New("must set either tls or acme")}
}
if c.TLS != nil && c.ACME != nil {
return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")}
}
if c.TLS != nil {
// SNI guard
var sniGuard utils.SNIGuardFunc
switch strings.ToLower(c.TLS.SNIGuard) {
case "", "dns-san":
sniGuard = utils.SNIGuardDNSSAN
case "strict":
sniGuard = utils.SNIGuardStrict
case "disable":
sniGuard = nil
default:
return configError{Field: "tls.sniGuard", Err: errors.New("unsupported SNI guard")}
}
// Local TLS cert
if c.TLS.Cert == "" || c.TLS.Key == "" {
return configError{Field: "tls", Err: errors.New("empty cert or key path")}
}
certLoader := &utils.LocalCertificateLoader{
CertFile: c.TLS.Cert,
KeyFile: c.TLS.Key,
SNIGuard: sniGuard,
}
// Try loading the cert-key pair here to catch errors early
// (e.g. invalid files or insufficient permissions)
err := certLoader.InitializeCache()
if err != nil {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
if pathErr.Path == c.TLS.Cert {
return configError{Field: "tls.cert", Err: pathErr}
}
if pathErr.Path == c.TLS.Key {
return configError{Field: "tls.key", Err: pathErr}
}
}
return configError{Field: "tls", Err: err}
}
// Use GetCertificate instead of Certificates so that
// users can update the cert without restarting the server.
hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate
// Client CA
if c.TLS.ClientCA != "" {
ca, err := os.ReadFile(c.TLS.ClientCA)
if err != nil {
return configError{Field: "tls.clientCA", Err: err}
}
cPool := x509.NewCertPool()
if !cPool.AppendCertsFromPEM(ca) {
return configError{Field: "tls.clientCA", Err: errors.New("failed to parse client CA certificate")}
}
hyConfig.TLSConfig.ClientCAs = cPool
}
} else {
// ACME
dataDir := c.ACME.Dir
if dataDir == "" {
// If not specified in the config, check the environment variable
// before resorting to the default "acme" value. The main reason
// we have this is so that our setup script can set it to the
// user's home directory.
dataDir = envOrDefaultString(appACMEDirEnv, "acme")
}
cmCfg := &certmagic.Config{
RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio,
KeySource: certmagic.DefaultKeyGenerator,
Storage: &certmagic.FileStorage{Path: dataDir},
Logger: logger,
}
cmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{
Email: c.ACME.Email,
Agreed: true,
ListenHost: c.ACME.ListenHost,
Logger: logger,
})
switch strings.ToLower(c.ACME.CA) {
case "letsencrypt", "le", "":
// Default to Let's Encrypt
cmIssuer.CA = certmagic.LetsEncryptProductionCA
case "zerossl", "zero":
cmIssuer.CA = certmagic.ZeroSSLProductionCA
eab, err := genZeroSSLEAB(c.ACME.Email)
if err != nil {
return configError{Field: "acme.ca", Err: err}
}
cmIssuer.ExternalAccount = eab
default:
return configError{Field: "acme.ca", Err: errors.New("unsupported CA")}
}
switch strings.ToLower(c.ACME.Type) {
case "http":
cmIssuer.DisableHTTPChallenge = false
cmIssuer.DisableTLSALPNChallenge = true
cmIssuer.DNS01Solver = nil
cmIssuer.AltHTTPPort = c.ACME.HTTP.AltPort
case "tls":
cmIssuer.DisableHTTPChallenge = true
cmIssuer.DisableTLSALPNChallenge = false
cmIssuer.DNS01Solver = nil
cmIssuer.AltTLSALPNPort = c.ACME.TLS.AltPort
case "dns":
cmIssuer.DisableHTTPChallenge = true
cmIssuer.DisableTLSALPNChallenge = true
if c.ACME.DNS.Name == "" {
return configError{Field: "acme.dns.name", Err: errors.New("empty DNS provider name")}
}
if c.ACME.DNS.Config == nil {
return configError{Field: "acme.dns.config", Err: errors.New("empty DNS provider config")}
}
switch strings.ToLower(c.ACME.DNS.Name) {
case "cloudflare":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &cloudflare.Provider{
APIToken: c.ACME.DNS.Config["cloudflare_api_token"],
},
}
case "duckdns":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &duckdns.Provider{
APIToken: c.ACME.DNS.Config["duckdns_api_token"],
OverrideDomain: c.ACME.DNS.Config["duckdns_override_domain"],
},
}
case "gandi":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &gandi.Provider{
BearerToken: c.ACME.DNS.Config["gandi_api_token"],
},
}
case "godaddy":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &godaddy.Provider{
APIToken: c.ACME.DNS.Config["godaddy_api_token"],
},
}
case "namedotcom":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &namedotcom.Provider{
Token: c.ACME.DNS.Config["namedotcom_token"],
User: c.ACME.DNS.Config["namedotcom_user"],
Server: c.ACME.DNS.Config["namedotcom_server"],
},
}
case "vultr":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &vultr.Provider{
APIToken: c.ACME.DNS.Config["vultr_api_token"],
},
}
default:
return configError{Field: "acme.dns.name", Err: errors.New("unsupported DNS provider")}
}
case "":
// Legacy compatibility mode
cmIssuer.DisableHTTPChallenge = c.ACME.DisableHTTP
cmIssuer.DisableTLSALPNChallenge = c.ACME.DisableTLSALPN
cmIssuer.AltHTTPPort = c.ACME.AltHTTPPort
cmIssuer.AltTLSALPNPort = c.ACME.AltTLSALPNPort
default:
return configError{Field: "acme.type", Err: errors.New("unsupported ACME type")}
}
cmCfg.Issuers = []certmagic.Issuer{cmIssuer}
cmCache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil
},
Logger: logger,
})
cmCfg = certmagic.New(cmCache, *cmCfg)
if len(c.ACME.Domains) == 0 {
return configError{Field: "acme.domains", Err: errors.New("empty domains")}
}
err := cmCfg.ManageSync(context.Background(), c.ACME.Domains)
if err != nil {
return configError{Field: "acme.domains", Err: err}
}
hyConfig.TLSConfig.GetCertificate = cmCfg.GetCertificate
}
return nil
}
func genZeroSSLEAB(email string) (*acme.EAB, error) {
req, err := http.NewRequest(
http.MethodPost,
"https://api.zerossl.com/acme/eab-credentials-email",
strings.NewReader(url.Values{"email": []string{email}}.Encode()),
)
if err != nil {
return nil, fmt.Errorf("failed to creare ZeroSSL EAB request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", certmagic.UserAgent)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send ZeroSSL EAB request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
var result struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
} `json:"error"`
EABKID string `json:"eab_kid"`
EABHMACKey string `json:"eab_hmac_key"`
}
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed decoding ZeroSSL EAB API response: %w", err)
}
if result.Error.Code != 0 {
return nil, fmt.Errorf("failed getting ZeroSSL EAB credentials: HTTP %d: %s (code %d)", resp.StatusCode, result.Error.Type, result.Error.Code)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
}
return &acme.EAB{
KeyID: result.EABKID,
MACKey: result.EABHMACKey,
}, nil
}
func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error {
hyConfig.QUICConfig = server.QUICConfig{
InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow,
MaxConnectionReceiveWindow: c.QUIC.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.QUIC.MaxIdleTimeout,
MaxIncomingStreams: c.QUIC.MaxIncomingStreams,
DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery,
}
return nil
}
func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) {
opts := outbounds.DirectOutboundOptions{}
switch strings.ToLower(c.Mode) {
case "", "auto":
opts.Mode = outbounds.DirectOutboundModeAuto
case "64":
opts.Mode = outbounds.DirectOutboundMode64
case "46":
opts.Mode = outbounds.DirectOutboundMode46
case "6":
opts.Mode = outbounds.DirectOutboundMode6
case "4":
opts.Mode = outbounds.DirectOutboundMode4
default:
return nil, configError{Field: "outbounds.direct.mode", Err: errors.New("unsupported mode")}
}
bindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0
bindDevice := len(c.BindDevice) > 0
if bindIP && bindDevice {
return nil, configError{Field: "outbounds.direct", Err: errors.New("cannot bind both IP and device")}
}
if bindIP {
ip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6)
if len(c.BindIPv4) > 0 && ip4 == nil {
return nil, configError{Field: "outbounds.direct.bindIPv4", Err: errors.New("invalid IPv4 address")}
}
if len(c.BindIPv6) > 0 && ip6 == nil {
return nil, configError{Field: "outbounds.direct.bindIPv6", Err: errors.New("invalid IPv6 address")}
}
opts.BindIP4 = ip4
opts.BindIP6 = ip6
}
if bindDevice {
opts.DeviceName = c.BindDevice
}
opts.FastOpen = c.FastOpen
return outbounds.NewDirectOutboundWithOptions(opts)
}
func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) {
if c.Addr == "" {
return nil, configError{Field: "outbounds.socks5.addr", Err: errors.New("empty socks5 address")}
}
return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil
}
func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) {
if c.URL == "" {
return nil, configError{Field: "outbounds.http.url", Err: errors.New("empty http address")}
}
return outbounds.NewHTTPOutbound(c.URL, c.Insecure)
}
func (c *serverConfig) fillRequestHook(hyConfig *server.Config) error {
if c.Sniff.Enable {
s := &sniff.Sniffer{
Timeout: c.Sniff.Timeout,
RewriteDomain: c.Sniff.RewriteDomain,
}
if c.Sniff.TCPPorts != "" {
s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts)
if s.TCPPorts == nil {
return configError{Field: "sniff.tcpPorts", Err: errors.New("invalid port union")}
}
}
if c.Sniff.UDPPorts != "" {
s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts)
if s.UDPPorts == nil {
return configError{Field: "sniff.udpPorts", Err: errors.New("invalid port union")}
}
}
hyConfig.RequestHook = s
}
return nil
}
func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {
// Resolver, ACL, actual outbound are all implemented through the Outbound interface.
// Depending on the config, we build a chain like this:
// Resolver(ACL(Outbounds...))
// Outbounds
var obs []outbounds.OutboundEntry
if len(c.Outbounds) == 0 {
// Guarantee we have at least one outbound
obs = []outbounds.OutboundEntry{{
Name: "default",
Outbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto),
}}
} else {
obs = make([]outbounds.OutboundEntry, len(c.Outbounds))
for i, entry := range c.Outbounds {
if entry.Name == "" {
return configError{Field: "outbounds.name", Err: errors.New("empty outbound name")}
}
var ob outbounds.PluggableOutbound
var err error
switch strings.ToLower(entry.Type) {
case "direct":
ob, err = serverConfigOutboundDirectToOutbound(entry.Direct)
case "socks5":
ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5)
case "http":
ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP)
default:
err = configError{Field: "outbounds.type", Err: errors.New("unsupported outbound type")}
}
if err != nil {
return err
}
obs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob}
}
}
var uOb outbounds.PluggableOutbound // "unified" outbound
// ACL
hasACL := false
if c.ACL.File != "" && len(c.ACL.Inline) > 0 {
return configError{Field: "acl", Err: errors.New("cannot set both acl.file and acl.inline")}
}
gLoader := &utils.GeoLoader{
GeoIPFilename: c.ACL.GeoIP,
GeoSiteFilename: c.ACL.GeoSite,
UpdateInterval: c.ACL.GeoUpdateInterval,
DownloadFunc: geoDownloadFunc,
DownloadErrFunc: geoDownloadErrFunc,
}
if c.ACL.File != "" {
hasACL = true
acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader)
if err != nil {
return configError{Field: "acl.file", Err: err}
}
uOb = acl
} else if len(c.ACL.Inline) > 0 {
hasACL = true
acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader)
if err != nil {
return configError{Field: "acl.inline", Err: err}
}
uOb = acl
} else {
// No ACL, use the first outbound
uOb = obs[0].Outbound
}
// Resolver
switch strings.ToLower(c.Resolver.Type) {
case "", "system":
if hasACL {
// If the user uses ACL, we must put a resolver in front of it,
// for IP rules to work on domain requests.
uOb = outbounds.NewSystemResolver(uOb)
}
// Otherwise we can just rely on outbound handling on its own.
case "tcp":
if c.Resolver.TCP.Addr == "" {
return configError{Field: "resolver.tcp.addr", Err: errors.New("empty resolver address")}
}
uOb = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, uOb)
case "udp":
if c.Resolver.UDP.Addr == "" {
return configError{Field: "resolver.udp.addr", Err: errors.New("empty resolver address")}
}
uOb = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, uOb)
case "tls", "tcp-tls":
if c.Resolver.TLS.Addr == "" {
return configError{Field: "resolver.tls.addr", Err: errors.New("empty resolver address")}
}
uOb = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, uOb)
case "https", "http":
if c.Resolver.HTTPS.Addr == "" {
return configError{Field: "resolver.https.addr", Err: errors.New("empty resolver address")}
}
uOb = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, uOb)
default:
return configError{Field: "resolver.type", Err: errors.New("unsupported resolver type")}
}
// Speed test
if c.SpeedTest {
uOb = outbounds.NewSpeedtestHandler(uOb)
}
hyConfig.Outbound = &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb}
return nil
}
func (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error {
var err error
if c.Bandwidth.Up != "" {
hyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up)
if err != nil {
return configError{Field: "bandwidth.up", Err: err}
}
}
if c.Bandwidth.Down != "" {
hyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down)
if err != nil {
return configError{Field: "bandwidth.down", Err: err}
}
}
return nil
}
func (c *serverConfig) fillIgnoreClientBandwidth(hyConfig *server.Config) error {
hyConfig.IgnoreClientBandwidth = c.IgnoreClientBandwidth
return nil
}
func (c *serverConfig) fillDisableUDP(hyConfig *server.Config) error {
hyConfig.DisableUDP = c.DisableUDP
return nil
}
func (c *serverConfig) fillUDPIdleTimeout(hyConfig *server.Config) error {
hyConfig.UDPIdleTimeout = c.UDPIdleTimeout
return nil
}
func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error {
if c.Auth.Type == "" {
return configError{Field: "auth.type", Err: errors.New("empty auth type")}
}
switch strings.ToLower(c.Auth.Type) {
case "password":
if c.Auth.Password == "" {
return configError{Field: "auth.password", Err: errors.New("empty auth password")}
}
hyConfig.Authenticator = &auth.PasswordAuthenticator{Password: c.Auth.Password}
return nil
case "userpass":
if len(c.Auth.UserPass) == 0 {
return configError{Field: "auth.userpass", Err: errors.New("empty auth userpass")}
}
hyConfig.Authenticator = auth.NewUserPassAuthenticator(c.Auth.UserPass)
return nil
case "http", "https":
if c.Auth.HTTP.URL == "" {
return configError{Field: "auth.http.url", Err: errors.New("empty auth http url")}
}
hyConfig.Authenticator = auth.NewHTTPAuthenticator(c.Auth.HTTP.URL, c.Auth.HTTP.Insecure)
return nil
case "command", "cmd":
if c.Auth.Command == "" {
return configError{Field: "auth.command", Err: errors.New("empty auth command")}
}
hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command}
return nil
default:
return configError{Field: "auth.type", Err: errors.New("unsupported auth type")}
}
}
func (c *serverConfig) fillEventLogger(hyConfig *server.Config) error {
hyConfig.EventLogger = &serverLogger{}
return nil
}
func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error {
if c.TrafficStats.Listen != "" {
tss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret)
hyConfig.TrafficLogger = tss
go runTrafficStatsServer(c.TrafficStats.Listen, tss)
}
return nil
}
// fillMasqHandler must be called after fillConn, as we may need to extract the QUIC
// port number from Conn for MasqTCPServer.
func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error {
var handler http.Handler
switch strings.ToLower(c.Masquerade.Type) {
case "", "404":
handler = http.NotFoundHandler()
case "file":
if c.Masquerade.File.Dir == "" {
return configError{Field: "masquerade.file.dir", Err: errors.New("empty file directory")}
}
handler = http.FileServer(http.Dir(c.Masquerade.File.Dir))
case "proxy":
if c.Masquerade.Proxy.URL == "" {
return configError{Field: "masquerade.proxy.url", Err: errors.New("empty proxy url")}
}
u, err := url.Parse(c.Masquerade.Proxy.URL)
if err != nil {
return configError{Field: "masquerade.proxy.url", Err: err}
}
if u.Scheme != "http" && u.Scheme != "https" {
return configError{Field: "masquerade.proxy.url", Err: fmt.Errorf("unsupported protocol scheme \"%s\"", u.Scheme)}
}
transport := http.DefaultTransport
if c.Masquerade.Proxy.Insecure {
transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
// use default configs from http.DefaultTransport
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}
handler = &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(u)
// SetURL rewrites the Host header,
// but we don't want that if rewriteHost is false
if !c.Masquerade.Proxy.RewriteHost {
r.Out.Host = r.In.Host
}
if c.Masquerade.Proxy.XForwarded {
r.SetXForwarded()
}
},
Transport: transport,
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("HTTP reverse proxy error", zap.Error(err))
w.WriteHeader(http.StatusBadGateway)
},
}
case "string":
if c.Masquerade.String.Content == "" {
return configError{Field: "masquerade.string.content", Err: errors.New("empty string content")}
}
if c.Masquerade.String.StatusCode != 0 &&
(c.Masquerade.String.StatusCode < 200 ||
c.Masquerade.String.StatusCode > 599 ||
c.Masquerade.String.StatusCode == 233) {
// 233 is reserved for Hysteria authentication
return configError{Field: "masquerade.string.statusCode", Err: errors.New("invalid status code (must be 200-599, except 233)")}
}
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for k, v := range c.Masquerade.String.Headers {
w.Header().Set(k, v)
}
if c.Masquerade.String.StatusCode != 0 {
w.WriteHeader(c.Masquerade.String.StatusCode)
} else {
w.WriteHeader(http.StatusOK) // Use 200 OK by default
}
_, _ = w.Write([]byte(c.Masquerade.String.Content))
})
default:
return configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")}
}
hyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler, QUIC: true}
if c.Masquerade.ListenHTTP != "" || c.Masquerade.ListenHTTPS != "" {
if c.Masquerade.ListenHTTP != "" && c.Masquerade.ListenHTTPS == "" {
return configError{Field: "masquerade.listenHTTPS", Err: errors.New("having only HTTP server without HTTPS is not supported")}
}
s := masq.MasqTCPServer{
QUICPort: extractPortFromAddr(hyConfig.Conn.LocalAddr().String()),
HTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS),
Handler: &masqHandlerLogWrapper{H: handler, QUIC: false},
TLSConfig: &tls.Config{
Certificates: hyConfig.TLSConfig.Certificates,
GetCertificate: hyConfig.TLSConfig.GetCertificate,
},
ForceHTTPS: c.Masquerade.ForceHTTPS,
}
go runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS)
}
return nil
}
// Config validates the fields and returns a ready-to-use Hysteria server config
func (c *serverConfig) Config() (*server.Config, error) {
hyConfig := &server.Config{}
fillers := []func(*server.Config) error{
c.fillConn,
c.fillTLSConfig,
c.fillQUICConfig,
c.fillRequestHook,
c.fillOutboundConfig,
c.fillBandwidthConfig,
c.fillIgnoreClientBandwidth,
c.fillDisableUDP,
c.fillUDPIdleTimeout,
c.fillAuthenticator,
c.fillEventLogger,
c.fillTrafficLogger,
c.fillMasqHandler,
}
for _, f := range fillers {
if err := f(hyConfig); err != nil {
return nil, err
}
}
return hyConfig, nil
}
func runServerCmd(cmd *cobra.Command, args []string) {
logger.Info("server mode")
runServer(defaultViper)
}
func runServer(v *viper.Viper) {
if err := v.ReadInConfig(); err != nil {
logger.Fatal("failed to read server config", zap.Error(err))
}
var config serverConfig
if err := v.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse server config", zap.Error(err))
}
hyConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to load server config", zap.Error(err))
}
s, err := server.NewServer(hyConfig)
if err != nil {
logger.Fatal("failed to initialize server", zap.Error(err))
}
if config.Listen != "" {
logger.Info("server up and running", zap.String("listen", config.Listen))
} else {
logger.Info("server up and running", zap.String("listen", defaultListenAddr))
}
if !disableUpdateCheck {
go runCheckUpdateServer()
}
if err := s.Serve(); err != nil {
logger.Fatal("failed to serve", zap.Error(err))
}
}
func runTrafficStatsServer(listen string, handler http.Handler) {
logger.Info("traffic stats server up and running", zap.String("listen", listen))
if err := correctnet.HTTPListenAndServe(listen, handler); err != nil {
logger.Fatal("failed to serve traffic stats", zap.Error(err))
}
}
func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string) {
errChan := make(chan error, 2)
if httpAddr != "" {
go func() {
logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr))
errChan <- s.ListenAndServeHTTP(httpAddr)
}()
}
if httpsAddr != "" {
go func() {
logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr))
errChan <- s.ListenAndServeHTTPS(httpsAddr)
}()
}
err := <-errChan
if err != nil {
logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err))
}
}
func geoDownloadFunc(filename, url string) {
logger.Info("downloading database", zap.String("filename", filename), zap.String("url", url))
}
func geoDownloadErrFunc(err error) {
if err != nil {
logger.Error("failed to download database", zap.Error(err))
}
}
type serverLogger struct{}
func (l *serverLogger) Connect(addr net.Addr, id string, tx uint64) {
logger.Info("client connected", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint64("tx", tx))
}
func (l *serverLogger) Disconnect(addr net.Addr, id string, err error) {
logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("id", id), zap.Error(err))
}
func (l *serverLogger) TCPRequest(addr net.Addr, id, reqAddr string) {
logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr))
}
func (l *serverLogger) TCPError(addr net.Addr, id, reqAddr string, err error) {
if err == nil {
logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("TCP error", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
func (l *serverLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) {
logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.String("reqAddr", reqAddr))
}
func (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) {
if err == nil {
logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID))
} else {
logger.Warn("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err))
}
}
type masqHandlerLogWrapper struct {
H http.Handler
QUIC bool
}
func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger.Debug("masquerade request",
zap.String("addr", r.RemoteAddr),
zap.String("method", r.Method),
zap.String("host", r.Host),
zap.String("url", r.URL.String()),
zap.Bool("quic", m.QUIC))
m.H.ServeHTTP(w, r)
}
func extractPortFromAddr(addr string) int {
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
return 0
}
port, err := strconv.Atoi(portStr)
if err != nil {
return 0
}
return port
}
================================================
FILE: app/cmd/server_test.go
================================================
package cmd
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/spf13/viper"
)
// TestServerConfig tests the parsing of the server config
func TestServerConfig(t *testing.T) {
viper.SetConfigFile("server_test.yaml")
err := viper.ReadInConfig()
assert.NoError(t, err)
var config serverConfig
err = viper.Unmarshal(&config)
assert.NoError(t, err)
assert.Equal(t, config, serverConfig{
Listen: ":8443",
Obfs: serverConfigObfs{
Type: "salamander",
Salamander: serverConfigObfsSalamander{
Password: "cry_me_a_r1ver",
},
},
TLS: &serverConfigTLS{
Cert: "some.crt",
Key: "some.key",
SNIGuard: "strict",
ClientCA: "some_ca.crt",
},
ACME: &serverConfigACME{
Domains: []string{
"sub1.example.com",
"sub2.example.com",
},
Email: "haha@cringe.net",
CA: "zero",
ListenHost: "127.0.0.9",
Dir: "random_dir",
Type: "dns",
HTTP: serverConfigACMEHTTP{
AltPort: 8888,
},
TLS: serverConfigACMETLS{
AltPort: 44333,
},
DNS: serverConfigACMEDNS{
Name: "gomommy",
Config: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
DisableHTTP: true,
DisableTLSALPN: true,
AltHTTPPort: 8080,
AltTLSALPNPort: 4433,
},
QUIC: serverConfigQUIC{
InitStreamReceiveWindow: 77881,
MaxStreamReceiveWindow: 77882,
InitConnectionReceiveWindow: 77883,
MaxConnectionReceiveWindow: 77884,
MaxIdleTimeout: 999 * time.Second,
MaxIncomingStreams: 256,
DisablePathMTUDiscovery: true,
},
Bandwidth: serverConfigBandwidth{
Up: "500 mbps",
Down: "100 mbps",
},
IgnoreClientBandwidth: true,
SpeedTest: true,
DisableUDP: true,
UDPIdleTimeout: 120 * time.Second,
Auth: serverConfigAuth{
Type: "password",
Password: "goofy_ahh_password",
UserPass: map[string]string{
"yolo": "swag",
"lol": "kek",
"foo": "bar",
},
HTTP: serverConfigAuthHTTP{
URL: "http://127.0.0.1:5000/auth",
Insecure: true,
},
Command: "/etc/some_command",
},
Resolver: serverConfigResolver{
Type: "udp",
TCP: serverConfigResolverTCP{
Addr: "123.123.123.123:5353",
Timeout: 4 * time.Second,
},
UDP: serverConfigResolverUDP{
Addr: "4.6.8.0:53",
Timeout: 2 * time.Second,
},
TLS: serverConfigResolverTLS{
Addr: "dot.yolo.com:8853",
Timeout: 10 * time.Second,
SNI: "server1.yolo.net",
Insecure: true,
},
HTTPS: serverConfigResolverHTTPS{
Addr: "cringe.ahh.cc",
Timeout: 5 * time.Second,
SNI: "real.stuff.net",
Insecure: true,
},
},
Sniff: serverConfigSniff{
Enable: true,
Timeout: 1 * time.Second,
RewriteDomain: true,
TCPPorts: "80,443,1000-2000",
UDPPorts: "443",
},
ACL: serverConfigACL{
File: "chnroute.txt",
Inline: []string{
"lmao(ok)",
"kek(cringe,boba,tea)",
},
GeoIP: "some.dat",
GeoSite: "some_site.dat",
GeoUpdateInterval: 168 * time.Hour,
},
Outbounds: []serverConfigOutboundEntry{
{
Name: "goodstuff",
Type: "direct",
Direct: serverConfigOutboundDirect{
Mode: "64",
BindIPv4: "2.4.6.8",
BindIPv6: "0:0:0:0:0:ffff:0204:0608",
BindDevice: "eth233",
FastOpen: true,
},
},
{
Name: "badstuff",
Type: "socks5",
SOCKS5: serverConfigOutboundSOCKS5{
Addr: "shady.proxy.ru:1080",
Username: "hackerman",
Password: "Elliot Alderson",
},
},
{
Name: "weirdstuff",
Type: "http",
HTTP: serverConfigOutboundHTTP{
URL: "https://eyy.lmao:4443/goofy",
Insecure: true,
},
},
},
TrafficStats: serverConfigTrafficStats{
Listen: ":9999",
Secret: "its_me_mario",
},
Masquerade: serverConfigMasquerade{
Type: "proxy",
File: serverConfigMasqueradeFile{
Dir: "/www/masq",
},
Proxy: serverConfigMasqueradeProxy{
URL: "https://some.site.net",
RewriteHost: true,
XForwarded: true,
Insecure: true,
},
String: serverConfigMasqueradeString{
Content: "aint nothin here",
Headers: map[string]string{
"content-type": "text/plain",
"custom-haha": "lol",
},
StatusCode: 418,
},
ListenHTTP: ":80",
ListenHTTPS: ":443",
ForceHTTPS: true,
},
})
}
================================================
FILE: app/cmd/server_test.yaml
================================================
listen: :8443
obfs:
type: salamander
salamander:
password: cry_me_a_r1ver
tls:
cert: some.crt
key: some.key
sniGuard: strict
clientCA: some_ca.crt
acme:
domains:
- sub1.example.com
- sub2.example.com
email: haha@cringe.net
ca: zero
listenHost: 127.0.0.9
dir: random_dir
type: dns
http:
altPort: 8888
tls:
altPort: 44333
dns:
name: gomommy
config:
key1: value1
key2: value2
disableHTTP: true
disableTLSALPN: true
altHTTPPort: 8080
altTLSALPNPort: 4433
quic:
initStreamReceiveWindow: 77881
maxStreamReceiveWindow: 77882
initConnReceiveWindow: 77883
maxConnReceiveWindow: 77884
maxIdleTimeout: 999s
maxIncomingStreams: 256
disablePathMTUDiscovery: true
bandwidth:
up: 500 mbps
down: 100 mbps
ignoreClientBandwidth: true
speedTest: true
disableUDP: true
udpIdleTimeout: 120s
auth:
type: password
password: goofy_ahh_password
userpass:
yolo: swag
lol: kek
foo: bar
http:
url: http://127.0.0.1:5000/auth
insecure: true
command: /etc/some_command
resolver:
type: udp
tcp:
addr: 123.123.123.123:5353
timeout: 4s
udp:
addr: 4.6.8.0:53
timeout: 2s
tls:
addr: dot.yolo.com:8853
timeout: 10s
sni: server1.yolo.net
insecure: true
https:
addr: cringe.ahh.cc
timeout: 5s
sni: real.stuff.net
insecure: true
sniff:
enable: true
timeout: 1s
rewriteDomain: true
tcpPorts: 80,443,1000-2000
udpPorts: 443
acl:
file: chnroute.txt
inline:
- lmao(ok)
- kek(cringe,boba,tea)
geoip: some.dat
geosite: some_site.dat
geoUpdateInterval: 168h
outbounds:
- name: goodstuff
type: direct
direct:
mode: 64
bindIPv4: 2.4.6.8
bindIPv6: 0:0:0:0:0:ffff:0204:0608
bindDevice: eth233
fastOpen: true
- name: badstuff
type: socks5
socks5:
addr: shady.proxy.ru:1080
username: hackerman
password: Elliot Alderson
- name: weirdstuff
type: http
http:
url: https://eyy.lmao:4443/goofy
insecure: true
trafficStats:
listen: :9999
secret: its_me_mario
masquerade:
type: proxy
file:
dir: /www/masq
proxy:
url: https://some.site.net
rewriteHost: true
xForwarded: true
insecure: true
string:
content: aint nothin here
headers:
content-type: text/plain
custom-haha: lol
statusCode: 418
listenHTTP: :80
listenHTTPS: :443
forceHTTPS: true
================================================
FILE: app/cmd/share.go
================================================
package cmd
import (
"fmt"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
var (
noText bool
withQR bool
)
// shareCmd represents the share command
var shareCmd = &cobra.Command{
Use: "share",
Short: "Generate share URI",
Long: "Generate a hysteria2:// URI from a client config for sharing",
Run: runShareCmd,
}
func init() {
initShareFlags()
rootCmd.AddCommand(shareCmd)
}
func initShareFlags() {
shareCmd.Flags().BoolVar(&noText, "notext", false, "do not show URI as text")
shareCmd.Flags().BoolVar(&withQR, "qr", false, "show URI as QR code")
}
func runShareCmd(cmd *cobra.Command, args []string) {
runShare(defaultViper)
}
func runShare(v *viper.Viper) {
if err := v.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := v.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
if _, err := config.Config(); err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
u := config.URI()
if !noText {
fmt.Println(u)
}
if withQR {
utils.PrintQR(u)
}
}
================================================
FILE: app/cmd/speedtest.go
================================================
package cmd
import (
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
hyErrors "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/extras/v2/outbounds"
"github.com/apernet/hysteria/extras/v2/outbounds/speedtest"
)
var (
skipDownload bool
skipUpload bool
dataSize uint32
testDuration time.Duration
useBytes bool
speedtestAddr = fmt.Sprintf("%s:%d", outbounds.SpeedtestDest, 0)
)
// speedtestCmd represents the speedtest command
var speedtestCmd = &cobra.Command{
Use: "speedtest",
Short: "Speed test mode",
Long: "Perform a speed test through the proxy server. The server must have speed test support enabled.",
Run: runSpeedtestCmd,
}
func init() {
initSpeedtestFlags()
rootCmd.AddCommand(speedtestCmd)
}
func initSpeedtestFlags() {
speedtestCmd.Flags().BoolVar(&skipDownload, "skip-download", false, "Skip download test")
speedtestCmd.Flags().BoolVar(&skipUpload, "skip-upload", false, "Skip upload test")
speedtestCmd.Flags().DurationVar(&testDuration, "duration", 10*time.Second, "Duration for each direction in time-based mode")
speedtestCmd.Flags().Uint32Var(&dataSize, "data-size", 0, "Data size in bytes (switches to size-based mode when set)")
speedtestCmd.Flags().BoolVar(&useBytes, "use-bytes", false, "Use bytes per second instead of bits per second")
}
func runSpeedtestCmd(cmd *cobra.Command, args []string) {
logger.Info("speed test mode")
sizeBased := cmd.Flags().Changed("data-size")
runSpeedtest(defaultViper, sizeBased)
}
func runSpeedtest(v *viper.Viper, sizeBased bool) {
if err := v.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := v.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
hyConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
c, info, err := client.NewClient(hyConfig)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx))
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signalChan)
runChan := make(chan struct{}, 1)
go func() {
if !skipDownload {
runSingleTest(c, sizeBased, true)
}
if !skipUpload {
runSingleTest(c, sizeBased, false)
}
runChan <- struct{}{}
}()
select {
case <-signalChan:
logger.Info("received signal, shutting down gracefully")
case <-runChan:
logger.Info("speed test complete")
}
}
func runSingleTest(c client.Client, sizeBased, download bool) {
name := "upload"
if download {
name = "download"
}
logger.Info("performing " + name + " test")
conn, err := c.TCP(speedtestAddr)
if err != nil {
if errors.As(err, &hyErrors.DialError{}) {
logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err))
} else {
logger.Fatal("failed to connect", zap.Error(err))
}
}
defer conn.Close()
sc := &speedtest.Client{Conn: conn}
var currentTotal uint64
var elapsed time.Duration
dur := testDuration
if sizeBased {
dur = 0
}
cb := func(d time.Duration, b uint64, done bool) {
if !done {
currentTotal += b
elapsed += d
var progress float64
if sizeBased {
progress = float64(currentTotal) / float64(dataSize) * 100
} else {
progress = float64(elapsed) / float64(testDuration) * 100
}
logger.Info(name+"ing",
zap.Uint64("bytes", b),
zap.String("progress", fmt.Sprintf("%.2f%%", progress)),
zap.String("speed", formatSpeed(b, d, useBytes)))
} else {
logger.Info(name+" complete",
zap.Uint64("bytes", b),
zap.String("speed", formatSpeed(b, d, useBytes)))
}
}
if download {
err = sc.Download(dataSize, dur, cb)
} else {
err = sc.Upload(dataSize, dur, cb)
}
if err != nil {
logger.Fatal(name+" test failed", zap.Error(err))
}
logger.Info(name + " test complete")
}
func formatSpeed(bytes uint64, duration time.Duration, useBytes bool) string {
speed := float64(bytes) / duration.Seconds()
var units []string
if useBytes {
units = []string{"B/s", "KB/s", "MB/s", "GB/s"}
} else {
units = []string{"bps", "Kbps", "Mbps", "Gbps"}
speed *= 8
}
unitIndex := 0
for speed > 1000 && unitIndex < len(units)-1 {
speed /= 1000
unitIndex++
}
return fmt.Sprintf("%.2f %s", speed, units[unitIndex])
}
================================================
FILE: app/cmd/update.go
================================================
package cmd
import (
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/apernet/hysteria/core/v2/client"
)
const (
updateCheckInterval = 24 * time.Hour
)
// checkUpdateCmd represents the checkUpdate command
var checkUpdateCmd = &cobra.Command{
Use: "check-update",
Short: "Check for updates",
Long: "Check for updates.",
Run: runCheckUpdateCmd,
}
func init() {
rootCmd.AddCommand(checkUpdateCmd)
}
func runCheckUpdateCmd(cmd *cobra.Command, args []string) {
logger.Info("checking for updates",
zap.String("version", appVersion),
zap.String("platform", appPlatform),
zap.String("arch", appArch),
zap.String("channel", appType),
)
runCheckUpdate(defaultViper)
}
func runCheckUpdate(v *viper.Viper) {
checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)
resp, err := checker.Check()
if err != nil {
logger.Fatal("failed to check for updates", zap.Error(err))
}
if resp.HasUpdate {
logger.Info("update available",
zap.String("version", resp.LatestVersion),
zap.String("url", resp.URL),
zap.Bool("urgent", resp.Urgent),
)
} else {
logger.Info("no update available")
}
}
// runCheckUpdateServer is the background update checking routine for server mode
func runCheckUpdateServer() {
checker := utils.NewServerUpdateChecker(appVersion, appPlatform, appArch, appType)
checkUpdateRoutine(checker)
}
// runCheckUpdateClient is the background update checking routine for client mode
func runCheckUpdateClient(hyClient client.Client) {
checker := utils.NewClientUpdateChecker(appVersion, appPlatform, appArch, appType, hyClient)
checkUpdateRoutine(checker)
}
func checkUpdateRoutine(checker *utils.UpdateChecker) {
ticker := time.NewTicker(updateCheckInterval)
for {
logger.Debug("checking for updates",
zap.String("version", appVersion),
zap.String("platform", appPlatform),
zap.String("arch", appArch),
zap.String("channel", appType),
)
resp, err := checker.Check()
if err != nil {
logger.Debug("failed to check for updates", zap.Error(err))
} else if resp.HasUpdate {
logger.Info("update available",
zap.String("version", resp.LatestVersion),
zap.String("url", resp.URL),
zap.Bool("urgent", resp.Urgent),
)
} else {
logger.Debug("no update available")
}
<-ticker.C
}
}
================================================
FILE: app/cmd/version.go
================================================
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show version",
Long: "Show version.",
Run: runVersionCmd,
}
func init() {
rootCmd.AddCommand(versionCmd)
}
func runVersionCmd(cmd *cobra.Command, args []string) {
fmt.Println(appAboutLong)
}
================================================
FILE: app/go.mod
================================================
module github.com/apernet/hysteria/app/v2
go 1.24.0
toolchain go1.25.1
require (
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f
github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000
github.com/apernet/hysteria/extras/v2 v2.0.0-00010101000000-000000000000
github.com/apernet/sing-tun v0.2.6-0.20250920121535-299f04629986
github.com/caddyserver/certmagic v0.17.2
github.com/libdns/cloudflare v0.1.1
github.com/libdns/duckdns v0.2.0
github.com/libdns/gandi v1.0.3
github.com/libdns/godaddy v1.0.3
github.com/libdns/namedotcom v0.3.3
github.com/libdns/vultr v1.0.0
github.com/mdp/qrterminal/v3 v3.1.1
github.com/mholt/acmez v1.0.4
github.com/sagernet/sing v0.3.2
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/sys v0.41.0
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a // indirect
github.com/database64128/tfo-go/v2 v2.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.6 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/refraction-networking/utls v1.6.6 // indirect
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/vultr/govultr/v3 v3.6.4 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/qr v0.2.0 // indirect
)
replace github.com/apernet/hysteria/core/v2 => ../core
replace github.com/apernet/hysteria/extras/v2 => ../extras
================================================
FILE: app/go.sum
================================================
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY=
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/apernet/sing-tun v0.2.6-0.20250920121535-299f04629986 h1:w62V0oOO2Do0vXeZkx7mgZ2YFuUaRcO6rNZewI3xzuE=
github.com/apernet/sing-tun v0.2.6-0.20250920121535-299f04629986/go.mod h1:S5IydyLSN/QAfvY+r2GoomPJ6hidtXWm/Ad18sJVssk=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=
github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a h1:t4SDi0pmNkryzKdM4QF3o5vqSP4GRjeZD/6j3nyxNP0=
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a/go.mod h1:7K2NQKbabB5mBl41vF6YayYl5g7YpDwc4dQ5iMpP3Lg=
github.com/database64128/tfo-go/v2 v2.2.2 h1:BxynF4qGF5ct3DpPLEG62uyJZ3LQhqaf0Ken+kyy7PM=
github.com/database64128/tfo-go/v2 v2.2.2/go.mod h1:2IW8jppdBwdVMjA08uEyMNnqiAHKUlqAA+J8NrsfktY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0=
github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/duckdns v0.2.0 h1:vd3pE09G2qTx1Zh1o3LmrivWSByD3Z5FbL7csX5vDgE=
github.com/libdns/duckdns v0.2.0/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs=
github.com/libdns/gandi v1.0.3 h1:FIvipWOg/O4zi75fPRmtcolRKqI6MgrbpFy2p5KYdUk=
github.com/libdns/gandi v1.0.3/go.mod h1:G6dw58Xnji2xX+lb+uZxGbtmfxKllm1CGHE2bOPG3WA=
github.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44=
github.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE=
github.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s=
github.com/libdns/vultr v1.0.0 h1:W8B4+k2bm9ro3bZLSZV9hMOQI+uO6Svu+GmD+Olz7ZI=
github.com/libdns/vultr v1.0.0/go.mod h1:8K1HJExcbeHS4YPkFHRZpqpXZzZ+DZAA0m0VikJgEqk=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc=
github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU=
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo=
github.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM=
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vultr/govultr/v3 v3.6.4 h1:unvY9eXlBw667ECQZDbBDOIaWB8wkk6Bx+yB0IMKXJ4=
github.com/vultr/govultr/v3 v3.6.4/go.mod h1:rt9v2x114jZmmLAE/h5N5jnxTmsK9ewwS2oQZ0UBQzM=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
================================================
FILE: app/internal/forwarding/tcp.go
================================================
package forwarding
import (
"io"
"net"
"github.com/apernet/hysteria/core/v2/client"
)
type TCPTunnel struct {
HyClient client.Client
Remote string
EventLogger TCPEventLogger
}
type TCPEventLogger interface {
Connect(addr net.Addr)
Error(addr net.Addr, err error)
}
func (t *TCPTunnel) Serve(listener net.Listener) error {
for {
conn, err := listener.Accept()
if err != nil {
return err
}
go t.handle(conn)
}
}
func (t *TCPTunnel) handle(conn net.Conn) {
defer conn.Close()
if t.EventLogger != nil {
t.EventLogger.Connect(conn.RemoteAddr())
}
var closeErr error
defer func() {
if t.EventLogger != nil {
t.EventLogger.Error(conn.RemoteAddr(), closeErr)
}
}()
rc, err := t.HyClient.TCP(t.Remote)
if err != nil {
closeErr = err
return
}
defer rc.Close()
// Start forwarding
copyErrChan := make(chan error, 2)
go func() {
_, copyErr := io.Copy(rc, conn)
copyErrChan <- copyErr
}()
go func() {
_, copyErr := io.Copy(conn, rc)
copyErrChan <- copyErr
}()
closeErr = <-copyErrChan
}
================================================
FILE: app/internal/forwarding/tcp_test.go
================================================
package forwarding
import (
"crypto/rand"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/app/v2/internal/utils_test"
)
func TestTCPTunnel(t *testing.T) {
// Start the tunnel
l, err := net.Listen("tcp", "127.0.0.1:34567")
assert.NoError(t, err)
defer l.Close()
tunnel := &TCPTunnel{
HyClient: &utils_test.MockEchoHyClient{},
}
go tunnel.Serve(l)
for i := 0; i < 10; i++ {
conn, err := net.Dial("tcp", "127.0.0.1:34567")
assert.NoError(t, err)
data := make([]byte, 1024)
_, _ = rand.Read(data)
_, err = conn.Write(data)
assert.NoError(t, err)
recv := make([]byte, 1024)
_, err = conn.Read(recv)
assert.NoError(t, err)
assert.Equal(t, data, recv)
_ = conn.Close()
}
}
================================================
FILE: app/internal/forwarding/udp.go
================================================
package forwarding
import (
"net"
"sync"
"sync/atomic"
"time"
"github.com/apernet/hysteria/core/v2/client"
)
const (
udpBufferSize = 4096
defaultTimeout = 60 * time.Second
idleCleanupInterval = 1 * time.Second
)
type atomicTime struct {
v atomic.Value
}
func newAtomicTime(t time.Time) *atomicTime {
a := &atomicTime{}
a.Set(t)
return a
}
func (t *atomicTime) Set(new time.Time) {
t.v.Store(new)
}
func (t *atomicTime) Get() time.Time {
return t.v.Load().(time.Time)
}
type sessionEntry struct {
HyConn client.HyUDPConn
Last *atomicTime
Timeout bool // true if the session is closed due to timeout
}
func (e *sessionEntry) Feed(data []byte, addr string) error {
e.Last.Set(time.Now())
return e.HyConn.Send(data, addr)
}
func (e *sessionEntry) ReceiveLoop(pc net.PacketConn, addr net.Addr) error {
for {
data, _, err := e.HyConn.Receive()
if err != nil {
return err
}
_, err = pc.WriteTo(data, addr)
if err != nil {
return err
}
e.Last.Set(time.Now())
}
}
type UDPTunnel struct {
HyClient client.Client
Remote string
Timeout time.Duration
EventLogger UDPEventLogger
m map[string]*sessionEntry // addr -> HyConn
mutex sync.RWMutex
}
type UDPEventLogger interface {
Connect(addr net.Addr)
Error(addr net.Addr, err error)
}
func (t *UDPTunnel) Serve(pc net.PacketConn) error {
t.m = make(map[string]*sessionEntry)
stopCh := make(chan struct{})
go t.idleCleanupLoop(stopCh)
defer close(stopCh)
defer t.cleanup(false)
buf := make([]byte, udpBufferSize)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
return err
}
t.feed(pc, addr, buf[:n])
}
}
func (t *UDPTunnel) idleCleanupLoop(stopCh <-chan struct{}) {
ticker := time.NewTicker(idleCleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
t.cleanup(true)
case <-stopCh:
return
}
}
}
func (t *UDPTunnel) cleanup(idleOnly bool) {
// We use RLock here as we are only scanning the map, not deleting from it.
t.mutex.RLock()
defer t.mutex.RUnlock()
timeout := t.Timeout
if timeout == 0 {
timeout = defaultTimeout
}
now := time.Now()
for _, entry := range t.m {
if !idleOnly || now.Sub(entry.Last.Get()) > timeout {
entry.Timeout = true
_ = entry.HyConn.Close()
// Closing the connection here will cause the ReceiveLoop to exit,
// and the session will be removed from the map there.
}
}
}
func (t *UDPTunnel) feed(pc net.PacketConn, addr net.Addr, data []byte) {
t.mutex.RLock()
entry := t.m[addr.String()]
t.mutex.RUnlock()
// Create a new session if not exists
if entry == nil {
if t.EventLogger != nil {
t.EventLogger.Connect(addr)
}
hyConn, err := t.HyClient.UDP()
if err != nil {
if t.EventLogger != nil {
t.EventLogger.Error(addr, err)
}
return
}
entry = &sessionEntry{
HyConn: hyConn,
Last: newAtomicTime(time.Now()),
}
// Start the receive loop for this session
// Local <- Remote
go func() {
err := entry.ReceiveLoop(pc, addr)
if !entry.Timeout {
_ = hyConn.Close()
if t.EventLogger != nil {
t.EventLogger.Error(addr, err)
}
} else {
// Connection already closed by timeout cleanup,
// no need to close again here.
// Use nil error to indicate timeout.
if t.EventLogger != nil {
t.EventLogger.Error(addr, nil)
}
}
// Remove the session from the map
t.mutex.Lock()
delete(t.m, addr.String())
t.mutex.Unlock()
}()
// Insert the session into the map
t.mutex.Lock()
t.m[addr.String()] = entry
t.mutex.Unlock()
}
// Feed the message to the session
_ = entry.Feed(data, t.Remote)
}
================================================
FILE: app/internal/forwarding/udp_test.go
================================================
package forwarding
import (
"crypto/rand"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/app/v2/internal/utils_test"
)
func TestUDPTunnel(t *testing.T) {
// Start the tunnel
l, err := net.ListenPacket("udp", "127.0.0.1:34567")
assert.NoError(t, err)
defer l.Close()
tunnel := &UDPTunnel{
HyClient: &utils_test.MockEchoHyClient{},
}
go tunnel.Serve(l)
for i := 0; i < 10; i++ {
conn, err := net.Dial("udp", "127.0.0.1:34567")
assert.NoError(t, err)
data := make([]byte, 1024)
_, _ = rand.Read(data)
_, err = conn.Write(data)
assert.NoError(t, err)
recv := make([]byte, 1024)
_, err = conn.Read(recv)
assert.NoError(t, err)
assert.Equal(t, data, recv)
_ = conn.Close()
}
}
================================================
FILE: app/internal/http/server.go
================================================
package http
import (
"bufio"
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/apernet/hysteria/core/v2/client"
)
const (
httpClientTimeout = 10 * time.Second
)
// Server is an HTTP server using a Hysteria client as outbound.
type Server struct {
HyClient client.Client
AuthFunc func(username, password string) bool // nil = no authentication
AuthRealm string
EventLogger EventLogger
httpClient *http.Client
}
type EventLogger interface {
ConnectRequest(addr net.Addr, reqAddr string)
ConnectError(addr net.Addr, reqAddr string, err error)
HTTPRequest(addr net.Addr, reqURL string)
HTTPError(addr net.Addr, reqURL string, err error)
}
func (s *Server) Serve(listener net.Listener) error {
for {
conn, err := listener.Accept()
if err != nil {
return err
}
go s.dispatch(conn)
}
}
func (s *Server) dispatch(conn net.Conn) {
bufReader := bufio.NewReader(conn)
for {
req, err := http.ReadRequest(bufReader)
if err != nil {
// Connection error or invalid request
_ = conn.Close()
return
}
if s.AuthFunc != nil {
authOK := false
// Check the Proxy-Authorization header
pAuth := req.Header.Get("Proxy-Authorization")
if strings.HasPrefix(pAuth, "Basic ") {
userPass, err := base64.URLEncoding.DecodeString(pAuth[6:])
if err == nil {
userPassParts := strings.SplitN(string(userPass), ":", 2)
if len(userPassParts) == 2 {
authOK = s.AuthFunc(userPassParts[0], userPassParts[1])
}
}
}
if !authOK {
// Proxy authentication required
_ = sendProxyAuthRequired(conn, req, s.AuthRealm)
_ = conn.Close()
return
}
}
if req.Method == http.MethodConnect {
if bufReader.Buffered() > 0 {
// There is still data in the buffered reader.
// We need to get it out and put it into a cachedConn,
// so that handleConnect can read it.
data := make([]byte, bufReader.Buffered())
_, err := io.ReadFull(bufReader, data)
if err != nil {
// Read from buffer failed, is this possible?
_ = conn.Close()
return
}
cachedConn := &cachedConn{
Conn: conn,
Buffer: *bytes.NewBuffer(data),
}
s.handleConnect(cachedConn, req)
} else {
// No data in the buffered reader, we can just pass the original connection.
s.handleConnect(conn, req)
}
// handleConnect will take over the connection,
// i.e. it will not return until the connection is closed.
// When it returns, there will be no more requests from this connection,
// so we simply exit the loop.
return
} else {
// handleRequest on the other hand handles one request at a time,
// and returns when the request is done. It returns a bool indicating
// whether the connection should be kept alive, but itself never closes
// the connection.
keepAlive := s.handleRequest(conn, req)
if !keepAlive {
_ = conn.Close()
return
}
}
}
}
// cachedConn is a net.Conn wrapper that first Read()s from a buffer,
// and then from the underlying net.Conn when the buffer is drained.
type cachedConn struct {
net.Conn
Buffer bytes.Buffer
}
func (c *cachedConn) Read(b []byte) (int, error) {
if c.Buffer.Len() > 0 {
n, err := c.Buffer.Read(b)
if err == io.EOF {
// Buffer is drained, hide it from the caller
err = nil
}
return n, err
}
return c.Conn.Read(b)
}
func (s *Server) handleConnect(conn net.Conn, req *http.Request) {
defer conn.Close()
port := req.URL.Port()
if port == "" {
// HTTP defaults to port 80
port = "80"
}
reqAddr := net.JoinHostPort(req.URL.Hostname(), port)
// Connect request & error log
if s.EventLogger != nil {
s.EventLogger.ConnectRequest(conn.RemoteAddr(), reqAddr)
}
var closeErr error
defer func() {
if s.EventLogger != nil {
s.EventLogger.ConnectError(conn.RemoteAddr(), reqAddr, closeErr)
}
}()
// Dial
rConn, err := s.HyClient.TCP(reqAddr)
if err != nil {
_ = sendSimpleResponse(conn, req, http.StatusBadGateway)
closeErr = err
return
}
defer rConn.Close()
// Send 200 OK response and start relaying
_ = sendSimpleResponse(conn, req, http.StatusOK)
copyErrChan := make(chan error, 2)
go func() {
_, err := io.Copy(rConn, conn)
copyErrChan <- err
}()
go func() {
_, err := io.Copy(conn, rConn)
copyErrChan <- err
}()
closeErr = <-copyErrChan
}
func (s *Server) handleRequest(conn net.Conn, req *http.Request) bool {
// Some clients use Connection, some use Proxy-Connection
// https://www.oreilly.com/library/view/http-the-definitive/1565925092/re40.html
keepAlive := req.ProtoAtLeast(1, 1) &&
(strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" ||
strings.ToLower(req.Header.Get("Connection")) == "keep-alive")
req.RequestURI = "" // Outgoing request should not have RequestURI
removeHopByHopHeaders(req.Header)
removeExtraHTTPHostPort(req)
if req.URL.Scheme == "" || req.URL.Host == "" {
_ = sendSimpleResponse(conn, req, http.StatusBadRequest)
return false
}
// Request & error log
if s.EventLogger != nil {
s.EventLogger.HTTPRequest(conn.RemoteAddr(), req.URL.String())
}
var closeErr error
defer func() {
if s.EventLogger != nil {
s.EventLogger.HTTPError(conn.RemoteAddr(), req.URL.String(), closeErr)
}
}()
if s.httpClient == nil {
s.initHTTPClient()
}
// Do the request and send the response back
resp, err := s.httpClient.Do(req)
if err != nil {
closeErr = err
_ = sendSimpleResponse(conn, req, http.StatusBadGateway)
return false
}
removeHopByHopHeaders(resp.Header)
if keepAlive {
resp.Header.Set("Connection", "keep-alive")
resp.Header.Set("Proxy-Connection", "keep-alive")
resp.Header.Set("Keep-Alive", "timeout=60")
}
closeErr = resp.Write(conn)
return closeErr == nil && keepAlive
}
func (s *Server) initHTTPClient() {
s.httpClient = &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// HyClient doesn't support context for now
return s.HyClient.TCP(addr)
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: httpClientTimeout,
}
}
func removeHopByHopHeaders(header http.Header) {
header.Del("Proxy-Connection") // Not in RFC but common
// https://www.ietf.org/rfc/rfc2616.txt
header.Del("Connection")
header.Del("Keep-Alive")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailers")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
}
func removeExtraHTTPHostPort(req *http.Request) {
host := req.Host
if host == "" {
host = req.URL.Host
}
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
host = pHost
}
req.Host = host
req.URL.Host = host
}
// sendSimpleResponse sends a simple HTTP response with the given status code.
func sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error {
resp := &http.Response{
StatusCode: statusCode,
Status: http.StatusText(statusCode),
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: http.Header{},
}
// Remove the "Content-Length: 0" header, some clients (e.g. ffmpeg) may not like it.
resp.ContentLength = -1
// Also, prevent the "Connection: close" header.
resp.Close = false
resp.Uncompressed = true
return resp.Write(conn)
}
// sendProxyAuthRequired sends a 407 Proxy Authentication Required response.
func sendProxyAuthRequired(conn net.Conn, req *http.Request, realm string) error {
resp := &http.Response{
StatusCode: http.StatusProxyAuthRequired,
Status: http.StatusText(http.StatusProxyAuthRequired),
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: http.Header{},
}
resp.Header.Set("Proxy-Authenticate", fmt.Sprintf("Basic realm=%q", realm))
return resp.Write(conn)
}
================================================
FILE: app/internal/http/server_test.go
================================================
package http
import (
"errors"
"net"
"net/http"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/core/v2/client"
)
const (
testCertFile = "test.crt"
testKeyFile = "test.key"
)
type mockHyClient struct{}
func (c *mockHyClient) TCP(addr string) (net.Conn, error) {
return net.Dial("tcp", addr)
}
func (c *mockHyClient) UDP() (client.HyUDPConn, error) {
return nil, errors.New("not implemented")
}
func (c *mockHyClient) Close() error {
return nil
}
func TestServer(t *testing.T) {
// Start the server
l, err := net.Listen("tcp", "127.0.0.1:18080")
assert.NoError(t, err)
defer l.Close()
s := &Server{
HyClient: &mockHyClient{},
}
go s.Serve(l)
// Start a test HTTP & HTTPS server
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("control is an illusion"))
})
go http.ListenAndServe("127.0.0.1:18081", nil)
go http.ListenAndServeTLS("127.0.0.1:18082", testCertFile, testKeyFile, nil)
// Run the Python test script
cmd := exec.Command("python", "server_test.py")
// Suppress HTTPS warning text from Python
cmd.Env = append(cmd.Env, "PYTHONWARNINGS=ignore:Unverified HTTPS request")
out, err := cmd.CombinedOutput()
assert.NoError(t, err)
assert.Equal(t, "OK", strings.TrimSpace(string(out)))
}
================================================
FILE: app/internal/http/server_test.py
================================================
import requests
proxies = {
"http": "http://127.0.0.1:18080",
"https": "http://127.0.0.1:18080",
}
def test_http(it):
for i in range(it):
r = requests.get("http://127.0.0.1:18081", proxies=proxies)
assert r.status_code == 200 and r.text == "control is an illusion"
def test_https(it):
for i in range(it):
r = requests.get("https://127.0.0.1:18082", proxies=proxies, verify=False)
assert r.status_code == 200 and r.text == "control is an illusion"
if __name__ == "__main__":
test_http(10)
test_https(10)
print("OK")
================================================
FILE: app/internal/http/test.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL
BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM
EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3
DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy
MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE
CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI
hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4
ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W
DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW
CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf
jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N
o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v
M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf
1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc
luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4
FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD
UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD
OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7
ydYKuI8=
-----END CERTIFICATE-----
================================================
FILE: app/internal/http/test.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR
2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr
6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc
L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2
wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C
LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI
MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+
FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U
64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX
erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu
1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW
T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA
g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA
o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO
Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY
ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V
rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k
AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI
j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0
jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2
ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g
uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+
menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2
kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB
T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ
-----END RSA PRIVATE KEY-----
================================================
FILE: app/internal/proxymux/.mockery.yaml
================================================
with-expecter: true
dir: internal/mocks
outpkg: mocks
packages:
net:
interfaces:
Listener:
config:
mockname: MockListener
Conn:
config:
mockname: MockConn
================================================
FILE: app/internal/proxymux/internal/mocks/mock_Conn.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
time "time"
)
// MockConn is an autogenerated mock type for the Conn type
type MockConn struct {
mock.Mock
}
type MockConn_Expecter struct {
mock *mock.Mock
}
func (_m *MockConn) EXPECT() *MockConn_Expecter {
return &MockConn_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockConn_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockConn_Expecter) Close() *MockConn_Close_Call {
return &MockConn_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call {
_c.Call.Return(run)
return _c
}
// LocalAddr provides a mock function with no fields
func (_m *MockConn) LocalAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LocalAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr'
type MockConn_LocalAddr_Call struct {
*mock.Call
}
// LocalAddr is a helper method to define mock.On call
func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call {
return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")}
}
func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: b
func (_m *MockConn) Read(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(b)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type MockConn_Read_Call struct {
*mock.Call
}
// Read is a helper method to define mock.On call
// - b []byte
func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call {
return &MockConn_Read_Call{Call: _e.mock.On("Read", b)}
}
func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call {
_c.Call.Return(run)
return _c
}
// RemoteAddr provides a mock function with no fields
func (_m *MockConn) RemoteAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for RemoteAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr'
type MockConn_RemoteAddr_Call struct {
*mock.Call
}
// RemoteAddr is a helper method to define mock.On call
func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call {
return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")}
}
func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call {
_c.Call.Return(run)
return _c
}
// SetDeadline provides a mock function with given fields: t
func (_m *MockConn) SetDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'
type MockConn_SetDeadline_Call struct {
*mock.Call
}
// SetDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call {
return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)}
}
func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetReadDeadline provides a mock function with given fields: t
func (_m *MockConn) SetReadDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetReadDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'
type MockConn_SetReadDeadline_Call struct {
*mock.Call
}
// SetReadDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call {
return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)}
}
func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetWriteDeadline provides a mock function with given fields: t
func (_m *MockConn) SetWriteDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetWriteDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'
type MockConn_SetWriteDeadline_Call struct {
*mock.Call
}
// SetWriteDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call {
return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)}
}
func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: b
func (_m *MockConn) Write(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(b)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockConn_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - b []byte
func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call {
return &MockConn_Write_Call{Call: _e.mock.On("Write", b)}
}
func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call {
_c.Call.Return(run)
return _c
}
// NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockConn(t interface {
mock.TestingT
Cleanup(func())
}) *MockConn {
mock := &MockConn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: app/internal/proxymux/internal/mocks/mock_Listener.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
)
// MockListener is an autogenerated mock type for the Listener type
type MockListener struct {
mock.Mock
}
type MockListener_Expecter struct {
mock *mock.Mock
}
func (_m *MockListener) EXPECT() *MockListener_Expecter {
return &MockListener_Expecter{mock: &_m.Mock}
}
// Accept provides a mock function with no fields
func (_m *MockListener) Accept() (net.Conn, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Accept")
}
var r0 net.Conn
var r1 error
if rf, ok := ret.Get(0).(func() (net.Conn, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() net.Conn); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Conn)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'
type MockListener_Accept_Call struct {
*mock.Call
}
// Accept is a helper method to define mock.On call
func (_e *MockListener_Expecter) Accept() *MockListener_Accept_Call {
return &MockListener_Accept_Call{Call: _e.mock.On("Accept")}
}
func (_c *MockListener_Accept_Call) Run(run func()) *MockListener_Accept_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockListener_Accept_Call) Return(_a0 net.Conn, _a1 error) *MockListener_Accept_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockListener_Accept_Call) RunAndReturn(run func() (net.Conn, error)) *MockListener_Accept_Call {
_c.Call.Return(run)
return _c
}
// Addr provides a mock function with no fields
func (_m *MockListener) Addr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Addr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'
type MockListener_Addr_Call struct {
*mock.Call
}
// Addr is a helper method to define mock.On call
func (_e *MockListener_Expecter) Addr() *MockListener_Addr_Call {
return &MockListener_Addr_Call{Call: _e.mock.On("Addr")}
}
func (_c *MockListener_Addr_Call) Run(run func()) *MockListener_Addr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockListener_Addr_Call) Return(_a0 net.Addr) *MockListener_Addr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockListener_Addr_Call) RunAndReturn(run func() net.Addr) *MockListener_Addr_Call {
_c.Call.Return(run)
return _c
}
// Close provides a mock function with no fields
func (_m *MockListener) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockListener_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockListener_Expecter) Close() *MockListener_Close_Call {
return &MockListener_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockListener_Close_Call) Run(run func()) *MockListener_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockListener_Close_Call) Return(_a0 error) *MockListener_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockListener_Close_Call) RunAndReturn(run func() error) *MockListener_Close_Call {
_c.Call.Return(run)
return _c
}
// NewMockListener creates a new instance of MockListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockListener(t interface {
mock.TestingT
Cleanup(func())
}) *MockListener {
mock := &MockListener{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: app/internal/proxymux/manager.go
================================================
package proxymux
import (
"net"
"sync"
"github.com/apernet/hysteria/extras/v2/correctnet"
)
type muxManager struct {
listeners map[string]*muxListener
lock sync.Mutex
}
var globalMuxManager *muxManager
func init() {
globalMuxManager = &muxManager{
listeners: make(map[string]*muxListener),
}
}
func (m *muxManager) GetOrCreate(address string) (*muxListener, error) {
key, err := m.canonicalizeAddrPort(address)
if err != nil {
return nil, err
}
m.lock.Lock()
defer m.lock.Unlock()
if ml, ok := m.listeners[key]; ok {
return ml, nil
}
listener, err := correctnet.Listen("tcp", key)
if err != nil {
return nil, err
}
ml := newMuxListener(listener, func() {
m.lock.Lock()
defer m.lock.Unlock()
delete(m.listeners, key)
})
m.listeners[key] = ml
return ml, nil
}
func (m *muxManager) canonicalizeAddrPort(address string) (string, error) {
taddr, err := net.ResolveTCPAddr("tcp", address)
if err != nil {
return "", err
}
return taddr.String(), nil
}
func ListenHTTP(address string) (net.Listener, error) {
ml, err := globalMuxManager.GetOrCreate(address)
if err != nil {
return nil, err
}
return ml.ListenHTTP()
}
func ListenSOCKS(address string) (net.Listener, error) {
ml, err := globalMuxManager.GetOrCreate(address)
if err != nil {
return nil, err
}
return ml.ListenSOCKS()
}
================================================
FILE: app/internal/proxymux/manager_test.go
================================================
package proxymux
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestListenSOCKS(t *testing.T) {
address := "127.2.39.129:11081"
sl, err := ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
defer func() {
sl.Close()
}()
hl, err := ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
defer hl.Close()
_, err = ListenSOCKS(address)
if !assert.ErrorIs(t, err, ErrProtocolInUse) {
return
}
sl.Close()
sl, err = ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
}
func TestListenHTTP(t *testing.T) {
address := "127.2.39.129:11082"
hl, err := ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
defer func() {
hl.Close()
}()
sl, err := ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
defer sl.Close()
_, err = ListenHTTP(address)
if !assert.ErrorIs(t, err, ErrProtocolInUse) {
return
}
hl.Close()
hl, err = ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
}
func TestRelease(t *testing.T) {
address := "127.2.39.129:11083"
hl, err := ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
sl, err := ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
if !assert.True(t, globalMuxManager.testAddressExists(address)) {
return
}
_, err = net.Listen("tcp", address)
if !assert.Error(t, err) {
return
}
hl.Close()
sl.Close()
// Wait for muxListener released
time.Sleep(time.Second)
if !assert.False(t, globalMuxManager.testAddressExists(address)) {
return
}
lis, err := net.Listen("tcp", address)
if !assert.NoError(t, err) {
return
}
defer lis.Close()
}
func (m *muxManager) testAddressExists(address string) bool {
m.lock.Lock()
defer m.lock.Unlock()
_, ok := m.listeners[address]
return ok
}
================================================
FILE: app/internal/proxymux/mux.go
================================================
package proxymux
import (
"errors"
"fmt"
"io"
"net"
"sync"
)
func newMuxListener(listener net.Listener, deleteFunc func()) *muxListener {
l := &muxListener{
base: listener,
acceptChan: make(chan net.Conn),
closeChan: make(chan struct{}),
deleteFunc: deleteFunc,
}
go l.acceptLoop()
go l.mainLoop()
return l
}
type muxListener struct {
lock sync.Mutex
base net.Listener
acceptErr error
acceptChan chan net.Conn
closeChan chan struct{}
socksListener *subListener
httpListener *subListener
deleteFunc func()
}
func (l *muxListener) acceptLoop() {
defer close(l.acceptChan)
for {
conn, err := l.base.Accept()
if err != nil {
l.lock.Lock()
l.acceptErr = err
l.lock.Unlock()
return
}
select {
case <-l.closeChan:
return
case l.acceptChan <- conn:
}
}
}
func (l *muxListener) mainLoop() {
defer func() {
l.deleteFunc()
l.base.Close()
close(l.closeChan)
l.lock.Lock()
defer l.lock.Unlock()
if sl := l.httpListener; sl != nil {
close(sl.acceptChan)
l.httpListener = nil
}
if sl := l.socksListener; sl != nil {
close(sl.acceptChan)
l.socksListener = nil
}
}()
for {
var socksCloseChan, httpCloseChan chan struct{}
if l.httpListener != nil {
httpCloseChan = l.httpListener.closeChan
}
if l.socksListener != nil {
socksCloseChan = l.socksListener.closeChan
}
select {
case <-l.closeChan:
return
case conn, ok := <-l.acceptChan:
if !ok {
return
}
go l.dispatch(conn)
case <-socksCloseChan:
l.lock.Lock()
if socksCloseChan == l.socksListener.closeChan {
// not replaced by another ListenSOCKS()
l.socksListener = nil
}
l.lock.Unlock()
if l.checkIdle() {
return
}
case <-httpCloseChan:
l.lock.Lock()
if httpCloseChan == l.httpListener.closeChan {
// not replaced by another ListenHTTP()
l.httpListener = nil
}
l.lock.Unlock()
if l.checkIdle() {
return
}
}
}
}
func (l *muxListener) dispatch(conn net.Conn) {
var b [1]byte
if _, err := io.ReadFull(conn, b[:]); err != nil {
conn.Close()
return
}
l.lock.Lock()
var target *subListener
if b[0] == 5 {
target = l.socksListener
} else {
target = l.httpListener
}
l.lock.Unlock()
if target == nil {
conn.Close()
return
}
wconn := &connWithOneByte{Conn: conn, b: b[0]}
select {
case <-target.closeChan:
case target.acceptChan <- wconn:
}
}
func (l *muxListener) checkIdle() bool {
l.lock.Lock()
defer l.lock.Unlock()
return l.httpListener == nil && l.socksListener == nil
}
func (l *muxListener) getAndClearAcceptError() error {
l.lock.Lock()
defer l.lock.Unlock()
if l.acceptErr == nil {
return nil
}
err := l.acceptErr
l.acceptErr = nil
return err
}
func (l *muxListener) ListenHTTP() (net.Listener, error) {
l.lock.Lock()
defer l.lock.Unlock()
if l.httpListener != nil {
subListenerPendingClosed := false
select {
case <-l.httpListener.closeChan:
subListenerPendingClosed = true
default:
}
if !subListenerPendingClosed {
return nil, OpErr{
Addr: l.base.Addr(),
Protocol: "http",
Op: "bind-protocol",
Err: ErrProtocolInUse,
}
}
l.httpListener = nil
}
select {
case <-l.closeChan:
return nil, net.ErrClosed
default:
}
sl := newSubListener(l.getAndClearAcceptError, l.base.Addr)
l.httpListener = sl
return sl, nil
}
func (l *muxListener) ListenSOCKS() (net.Listener, error) {
l.lock.Lock()
defer l.lock.Unlock()
if l.socksListener != nil {
subListenerPendingClosed := false
select {
case <-l.socksListener.closeChan:
subListenerPendingClosed = true
default:
}
if !subListenerPendingClosed {
return nil, OpErr{
Addr: l.base.Addr(),
Protocol: "socks",
Op: "bind-protocol",
Err: ErrProtocolInUse,
}
}
l.socksListener = nil
}
select {
case <-l.closeChan:
return nil, net.ErrClosed
default:
}
sl := newSubListener(l.getAndClearAcceptError, l.base.Addr)
l.socksListener = sl
return sl, nil
}
func newSubListener(acceptErrorFunc func() error, addrFunc func() net.Addr) *subListener {
return &subListener{
acceptChan: make(chan net.Conn),
acceptErrorFunc: acceptErrorFunc,
closeChan: make(chan struct{}),
addrFunc: addrFunc,
}
}
type subListener struct {
// receive connections or closure from upstream
acceptChan chan net.Conn
// get an error of Accept() from upstream
acceptErrorFunc func() error
// notify upstream that we are closed
closeChan chan struct{}
// Listener.Addr() implementation of base listener
addrFunc func() net.Addr
}
func (l *subListener) Accept() (net.Conn, error) {
select {
case <-l.closeChan:
// closed by ourselves
return nil, net.ErrClosed
case conn, ok := <-l.acceptChan:
if !ok {
// closed by upstream
if acceptErr := l.acceptErrorFunc(); acceptErr != nil {
return nil, acceptErr
}
return nil, net.ErrClosed
}
return conn, nil
}
}
func (l *subListener) Addr() net.Addr {
return l.addrFunc()
}
// Close implements net.Listener.Close.
// Upstream should use close(l.acceptChan) instead.
func (l *subListener) Close() error {
select {
case <-l.closeChan:
return nil
default:
}
close(l.closeChan)
return nil
}
// connWithOneByte is a net.Conn that returns b for the first read
// request, then forwards everything else to Conn.
type connWithOneByte struct {
net.Conn
b byte
bRead bool
}
func (c *connWithOneByte) Read(bs []byte) (int, error) {
if c.bRead {
return c.Conn.Read(bs)
}
if len(bs) == 0 {
return 0, nil
}
c.bRead = true
bs[0] = c.b
return 1, nil
}
type OpErr struct {
Addr net.Addr
Protocol string
Op string
Err error
}
func (m OpErr) Error() string {
return fmt.Sprintf("mux-listen: %s[%s]: %s: %v", m.Addr, m.Protocol, m.Op, m.Err)
}
func (m OpErr) Unwrap() error {
return m.Err
}
var ErrProtocolInUse = errors.New("protocol already in use")
================================================
FILE: app/internal/proxymux/mux_test.go
================================================
package proxymux
import (
"bytes"
"net"
"sync"
"testing"
"time"
"github.com/apernet/hysteria/app/v2/internal/proxymux/internal/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
//go:generate mockery
func testMockListener(t *testing.T, connChan <-chan net.Conn) net.Listener {
closedChan := make(chan struct{})
mockListener := mocks.NewMockListener(t)
mockListener.EXPECT().Accept().RunAndReturn(func() (net.Conn, error) {
select {
case <-closedChan:
return nil, net.ErrClosed
case conn, ok := <-connChan:
if !ok {
panic("unexpected closed channel (connChan)")
}
return conn, nil
}
})
mockListener.EXPECT().Close().RunAndReturn(func() error {
select {
case <-closedChan:
default:
close(closedChan)
}
return nil
})
return mockListener
}
func testMockConn(t *testing.T, b []byte) net.Conn {
buf := bytes.NewReader(b)
isClosed := false
mockConn := mocks.NewMockConn(t)
mockConn.EXPECT().Read(mock.Anything).RunAndReturn(func(b []byte) (int, error) {
if isClosed {
return 0, net.ErrClosed
}
return buf.Read(b)
})
mockConn.EXPECT().Close().RunAndReturn(func() error {
isClosed = true
return nil
})
return mockConn
}
func TestMuxHTTP(t *testing.T) {
connChan := make(chan net.Conn)
mockListener := testMockListener(t, connChan)
mockConn := testMockConn(t, []byte("CONNECT example.com:443 HTTP/1.1\r\n\r\n"))
mux := newMuxListener(mockListener, func() {})
hl, err := mux.ListenHTTP()
if !assert.NoError(t, err) {
return
}
sl, err := mux.ListenSOCKS()
if !assert.NoError(t, err) {
return
}
connChan <- mockConn
var socksConn, httpConn net.Conn
var socksErr, httpErr error
var wg sync.WaitGroup
wg.Add(2)
go func() {
socksConn, socksErr = sl.Accept()
wg.Done()
}()
go func() {
httpConn, httpErr = hl.Accept()
wg.Done()
}()
time.Sleep(time.Second)
sl.Close()
hl.Close()
wg.Wait()
assert.Nil(t, socksConn)
assert.ErrorIs(t, socksErr, net.ErrClosed)
assert.NotNil(t, httpConn)
httpConn.Close()
assert.NoError(t, httpErr)
// Wait for muxListener released
<-mux.acceptChan
}
func TestMuxSOCKS(t *testing.T) {
connChan := make(chan net.Conn)
mockListener := testMockListener(t, connChan)
mockConn := testMockConn(t, []byte{0x05, 0x02, 0x00, 0x01}) // SOCKS5 Connect Request: NOAUTH+GSSAPI
mux := newMuxListener(mockListener, func() {})
hl, err := mux.ListenHTTP()
if !assert.NoError(t, err) {
return
}
sl, err := mux.ListenSOCKS()
if !assert.NoError(t, err) {
return
}
connChan <- mockConn
var socksConn, httpConn net.Conn
var socksErr, httpErr error
var wg sync.WaitGroup
wg.Add(2)
go func() {
socksConn, socksErr = sl.Accept()
wg.Done()
}()
go func() {
httpConn, httpErr = hl.Accept()
wg.Done()
}()
time.Sleep(time.Second)
sl.Close()
hl.Close()
wg.Wait()
assert.NotNil(t, socksConn)
socksConn.Close()
assert.NoError(t, socksErr)
assert.Nil(t, httpConn)
assert.ErrorIs(t, httpErr, net.ErrClosed)
// Wait for muxListener released
<-mux.acceptChan
}
================================================
FILE: app/internal/redirect/getsockopt_linux.go
================================================
//go:build !386
// +build !386
package redirect
import (
"syscall"
"unsafe"
)
func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) {
_, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)
if e != 0 {
err = e
}
return err
}
================================================
FILE: app/internal/redirect/getsockopt_linux_386.go
================================================
package redirect
import (
"syscall"
"unsafe"
)
const (
sysGetsockopt = 15
)
// On 386 we cannot call socketcall with syscall.Syscall6, as it always fails with EFAULT.
// Use our own syscall.socketcall hack instead.
func syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno)
func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) {
_, e := syscall_socketcall(sysGetsockopt, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)
if e != 0 {
err = e
}
return err
}
================================================
FILE: app/internal/redirect/syscall_socketcall_linux_386.s
================================================
//go:build gc
// +build gc
#include "textflag.h"
TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36
JMP syscall·socketcall(SB)
================================================
FILE: app/internal/redirect/tcp_linux.go
================================================
package redirect
import (
"encoding/binary"
"errors"
"io"
"net"
"syscall"
"unsafe"
"github.com/apernet/hysteria/core/v2/client"
)
const (
soOriginalDst = 80
soOriginalDstV6 = 80
)
type TCPRedirect struct {
HyClient client.Client
EventLogger TCPEventLogger
}
type TCPEventLogger interface {
Connect(addr, reqAddr net.Addr)
Error(addr, reqAddr net.Addr, err error)
}
func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error {
listener, err := net.ListenTCP("tcp", laddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.AcceptTCP()
if err != nil {
return err
}
go r.handle(c)
}
}
func (r *TCPRedirect) handle(conn *net.TCPConn) {
defer conn.Close()
dstAddr, err := getDstAddr(conn)
if err != nil {
// Fail silently if we can't get the original destination.
// Maybe we should print something to the log?
return
}
if r.EventLogger != nil {
r.EventLogger.Connect(conn.RemoteAddr(), dstAddr)
}
var closeErr error
defer func() {
if r.EventLogger != nil {
r.EventLogger.Error(conn.RemoteAddr(), dstAddr, closeErr)
}
}()
rc, err := r.HyClient.TCP(dstAddr.String())
if err != nil {
closeErr = err
return
}
defer rc.Close()
// Start forwarding
copyErrChan := make(chan error, 2)
go func() {
_, copyErr := io.Copy(rc, conn)
copyErrChan <- copyErr
}()
go func() {
_, copyErr := io.Copy(conn, rc)
copyErrChan <- copyErr
}()
closeErr = <-copyErrChan
}
type sockAddr struct {
family uint16
port [2]byte // always big endian regardless of platform
data [24]byte // sockaddr_in or sockaddr_in6
}
func getOriginalDst(fd uintptr) (*sockAddr, error) {
var addr sockAddr
addrSize := uint32(unsafe.Sizeof(addr))
// Try IPv6 first
err := getsockopt(fd, syscall.SOL_IPV6, soOriginalDstV6, unsafe.Pointer(&addr), &addrSize)
if err == nil {
return &addr, nil
}
// Then IPv4
err = getsockopt(fd, syscall.SOL_IP, soOriginalDst, unsafe.Pointer(&addr), &addrSize)
return &addr, err
}
// getDstAddr returns the original destination of a redirected TCP connection.
func getDstAddr(conn *net.TCPConn) (*net.TCPAddr, error) {
rc, err := conn.SyscallConn()
if err != nil {
return nil, err
}
var addr *sockAddr
var err2 error
err = rc.Control(func(fd uintptr) {
addr, err2 = getOriginalDst(fd)
})
if err != nil {
return nil, err
}
if err2 != nil {
return nil, err2
}
switch addr.family {
case syscall.AF_INET:
return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
case syscall.AF_INET6:
return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
default:
return nil, errors.New("address family not IPv4 or IPv6")
}
}
================================================
FILE: app/internal/redirect/tcp_others.go
================================================
//go:build !linux
package redirect
import (
"errors"
"net"
"github.com/apernet/hysteria/core/v2/client"
)
type TCPRedirect struct {
HyClient client.Client
EventLogger TCPEventLogger
}
type TCPEventLogger interface {
Connect(addr, reqAddr net.Addr)
Error(addr, reqAddr net.Addr, err error)
}
func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error {
return errors.New("not supported on this platform")
}
================================================
FILE: app/internal/sockopts/fd_control_unix_socket_test.py
================================================
import socket
import array
import os
import struct
import sys
def serve(path):
try:
os.unlink(path)
except OSError:
if os.path.exists(path):
raise
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(path)
server.listen()
print(f"Listening on {path}")
try:
while True:
connection, client_address = server.accept()
print(f"Client connected")
try:
# Receiving fd from client
fds = array.array("i")
msg, ancdata, flags, addr = connection.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i')))
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
fd = fds[0]
# We make a call to setsockopt(2) here, so client can verify we have received the fd
# In the real scenario, the server would set things like SO_MARK,
# we use SO_RCVBUF as it doesn't require any special capabilities.
nbytes = struct.pack("i", 2500)
fdsocket = fd_to_socket(fd)
fdsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, nbytes)
fdsocket.close()
# The only protocol-like thing specified in the client implementation.
connection.send(b'\x01')
finally:
connection.close()
print("Connection closed")
except KeyboardInterrupt:
print("Exit")
finally:
server.close()
os.unlink(path)
def fd_to_socket(fd):
return socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
if __name__ == "__main__":
if len(sys.argv) < 2:
raise ValueError("unix socket path is required")
serve(sys.argv[1])
================================================
FILE: app/internal/sockopts/sockopts.go
================================================
package sockopts
import (
"fmt"
"net"
)
type SocketOptions struct {
BindInterface *string
FirewallMark *uint32
FdControlUnixSocket *string
}
// implemented in platform-specific files
var (
bindInterfaceFunc func(c *net.UDPConn, device string) error
firewallMarkFunc func(c *net.UDPConn, fwmark uint32) error
fdControlUnixSocketFunc func(c *net.UDPConn, path string) error
)
func (o *SocketOptions) CheckSupported() (err error) {
if o.BindInterface != nil && bindInterfaceFunc == nil {
return &UnsupportedError{"bindInterface"}
}
if o.FirewallMark != nil && firewallMarkFunc == nil {
return &UnsupportedError{"fwmark"}
}
if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc == nil {
return &UnsupportedError{"fdControlUnixSocket"}
}
return nil
}
type UnsupportedError struct {
Field string
}
func (e *UnsupportedError) Error() string {
return fmt.Sprintf("%s is not supported on this platform", e.Field)
}
func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) {
uconn, err = net.ListenUDP("udp", nil)
if err != nil {
return uconn, err
}
err = o.applyToUDPConn(uconn.(*net.UDPConn))
if err != nil {
uconn.Close()
uconn = nil
return uconn, err
}
return uconn, err
}
func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error {
if o.BindInterface != nil && bindInterfaceFunc != nil {
err := bindInterfaceFunc(c, *o.BindInterface)
if err != nil {
return fmt.Errorf("failed to bind to interface: %w", err)
}
}
if o.FirewallMark != nil && firewallMarkFunc != nil {
err := firewallMarkFunc(c, *o.FirewallMark)
if err != nil {
return fmt.Errorf("failed to set fwmark: %w", err)
}
}
if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil {
err := fdControlUnixSocketFunc(c, *o.FdControlUnixSocket)
if err != nil {
return fmt.Errorf("failed to send fd to control unix socket: %w", err)
}
}
return nil
}
================================================
FILE: app/internal/sockopts/sockopts_linux.go
================================================
//go:build linux
package sockopts
import (
"fmt"
"net"
"time"
"golang.org/x/exp/constraints"
"golang.org/x/sys/unix"
)
const (
fdControlUnixTimeout = 3 * time.Second
)
func init() {
bindInterfaceFunc = bindInterfaceImpl
firewallMarkFunc = firewallMarkImpl
fdControlUnixSocketFunc = fdControlUnixSocketImpl
}
func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) {
rconn, err := c.SyscallConn()
if err != nil {
return err
}
cerr := rconn.Control(func(fd uintptr) {
err = cb(int(fd))
})
if err != nil {
return err
}
if cerr != nil {
err = fmt.Errorf("failed to control fd: %w", cerr)
return err
}
return err
}
func bindInterfaceImpl(c *net.UDPConn, device string) error {
return controlUDPConn(c, func(fd int) error {
return unix.BindToDevice(fd, device)
})
}
func firewallMarkImpl(c *net.UDPConn, fwmark uint32) error {
return controlUDPConn(c, func(fd int) error {
return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, int(fwmark))
})
}
func fdControlUnixSocketImpl(c *net.UDPConn, path string) error {
return controlUDPConn(c, func(fd int) error {
socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
return fmt.Errorf("failed to create unix socket: %w", err)
}
defer unix.Close(socketFd)
var timeout unix.Timeval
timeUsec := fdControlUnixTimeout.Microseconds()
castAssignInteger(timeUsec/1e6, &timeout.Sec)
// Specifying the type explicitly is not necessary here, but it makes GoLand happy.
castAssignInteger[int64](timeUsec%1e6, &timeout.Usec)
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout)
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout)
err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path})
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
err = unix.Sendmsg(socketFd, nil, unix.UnixRights(fd), nil, 0)
if err != nil {
return fmt.Errorf("failed to send: %w", err)
}
dummy := []byte{1}
n, err := unix.Read(socketFd, dummy)
if err != nil {
return fmt.Errorf("failed to receive: %w", err)
}
if n != 1 {
return fmt.Errorf("socket closed unexpectedly")
}
return nil
})
}
func castAssignInteger[F, T constraints.Integer](from F, to *T) {
*to = T(from)
}
================================================
FILE: app/internal/sockopts/sockopts_linux_test.go
================================================
//go:build linux
package sockopts
import (
"net"
"os"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"
)
func Test_fdControlUnixSocketImpl(t *testing.T) {
sockPath := "./fd_control_unix_socket_test.sock"
defer os.Remove(sockPath)
// Run test server
cmd := exec.Command("python", "fd_control_unix_socket_test.py", sockPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if !assert.NoError(t, err) {
return
}
defer cmd.Process.Kill()
// Wait for the server to start
time.Sleep(1 * time.Second)
so := SocketOptions{
FdControlUnixSocket: &sockPath,
}
conn, err := so.ListenUDP()
if !assert.NoError(t, err) {
return
}
defer conn.Close()
err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) {
rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF)
if err != nil {
return err
}
// The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500),
// and kernel will double this value for getsockopt().
assert.Equal(t, 5000, rcvbuf)
return err
})
assert.NoError(t, err)
}
================================================
FILE: app/internal/socks5/server.go
================================================
package socks5
import (
"encoding/binary"
"io"
"net"
"github.com/txthinking/socks5"
"github.com/apernet/hysteria/core/v2/client"
)
const udpBufferSize = 4096
// Server is a SOCKS5 server using a Hysteria client as outbound.
type Server struct {
HyClient client.Client
AuthFunc func(username, password string) bool // nil = no authentication
DisableUDP bool
EventLogger EventLogger
}
type EventLogger interface {
TCPRequest(addr net.Addr, reqAddr string)
TCPError(addr net.Addr, reqAddr string, err error)
UDPRequest(addr net.Addr)
UDPError(addr net.Addr, err error)
}
func (s *Server) Serve(listener net.Listener) error {
for {
conn, err := listener.Accept()
if err != nil {
return err
}
go s.dispatch(conn)
}
}
func (s *Server) dispatch(conn net.Conn) {
ok, _ := s.negotiate(conn)
if !ok {
_ = conn.Close()
return
}
// Negotiation ok, get and handle the request
req, err := socks5.NewRequestFrom(conn)
if err != nil {
_ = conn.Close()
return
}
switch req.Cmd {
case socks5.CmdConnect: // TCP
s.handleTCP(conn, req)
case socks5.CmdUDP: // UDP
if s.DisableUDP {
_ = sendSimpleReply(conn, socks5.RepCommandNotSupported)
_ = conn.Close()
return
}
s.handleUDP(conn, req)
default:
_ = sendSimpleReply(conn, socks5.RepCommandNotSupported)
_ = conn.Close()
}
}
func (s *Server) negotiate(conn net.Conn) (bool, error) {
req, err := socks5.NewNegotiationRequestFrom(conn)
if err != nil {
return false, err
}
var serverMethod byte
if s.AuthFunc != nil {
serverMethod = socks5.MethodUsernamePassword
} else {
serverMethod = socks5.MethodNone
}
// Look for the supported method in the client request
supported := false
for _, m := range req.Methods {
if m == serverMethod {
supported = true
break
}
}
if !supported {
// No supported method found, reject the client
rep := socks5.NewNegotiationReply(socks5.MethodUnsupportAll)
_, err := rep.WriteTo(conn)
return false, err
}
// OK, send the method we chose
rep := socks5.NewNegotiationReply(serverMethod)
_, err = rep.WriteTo(conn)
if err != nil {
return false, err
}
// If we chose the username/password method, authenticate the client
if serverMethod == socks5.MethodUsernamePassword {
req, err := socks5.NewUserPassNegotiationRequestFrom(conn)
if err != nil {
return false, err
}
ok := s.AuthFunc(string(req.Uname), string(req.Passwd))
if ok {
rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess)
_, err := rep.WriteTo(conn)
if err != nil {
return false, err
}
} else {
rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure)
_, err := rep.WriteTo(conn)
return false, err
}
}
return true, nil
}
func (s *Server) handleTCP(conn net.Conn, req *socks5.Request) {
defer conn.Close()
addr := req.Address()
// TCP request & error log
if s.EventLogger != nil {
s.EventLogger.TCPRequest(conn.RemoteAddr(), addr)
}
var closeErr error
defer func() {
if s.EventLogger != nil {
s.EventLogger.TCPError(conn.RemoteAddr(), addr, closeErr)
}
}()
// Dial
rConn, err := s.HyClient.TCP(addr)
if err != nil {
_ = sendSimpleReply(conn, socks5.RepHostUnreachable)
closeErr = err
return
}
defer rConn.Close()
// Send reply and start relaying
_ = sendSimpleReply(conn, socks5.RepSuccess)
copyErrChan := make(chan error, 2)
go func() {
_, err := io.Copy(rConn, conn)
copyErrChan <- err
}()
go func() {
_, err := io.Copy(conn, rConn)
copyErrChan <- err
}()
closeErr = <-copyErrChan
}
func (s *Server) handleUDP(conn net.Conn, req *socks5.Request) {
defer conn.Close()
// UDP request & error log
if s.EventLogger != nil {
s.EventLogger.UDPRequest(conn.RemoteAddr())
}
var closeErr error
defer func() {
if s.EventLogger != nil {
s.EventLogger.UDPError(conn.RemoteAddr(), closeErr)
}
}()
// Start UDP relay server
// SOCKS5 UDP requires the server to return the UDP bind address and port in the reply.
// We bind to the same address that our TCP server listens on (but a different port).
host, _, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
// Is this even possible?
_ = sendSimpleReply(conn, socks5.RepServerFailure)
closeErr = err
return
}
udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, "0"))
if err != nil {
_ = sendSimpleReply(conn, socks5.RepServerFailure)
closeErr = err
return
}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
_ = sendSimpleReply(conn, socks5.RepServerFailure)
closeErr = err
return
}
defer udpConn.Close()
// HyClient UDP session
hyUDP, err := s.HyClient.UDP()
if err != nil {
_ = sendSimpleReply(conn, socks5.RepServerFailure)
closeErr = err
return
}
defer hyUDP.Close()
// Send reply
_ = sendUDPReply(conn, udpConn.LocalAddr().(*net.UDPAddr))
// UDP relay & SOCKS5 connection holder
errChan := make(chan error, 2)
go func() {
err := s.udpServer(udpConn, hyUDP)
errChan <- err
}()
go func() {
_, err := io.Copy(io.Discard, conn)
errChan <- err
}()
closeErr = <-errChan
}
func (s *Server) udpServer(udpConn *net.UDPConn, hyUDP client.HyUDPConn) error {
var clientAddr *net.UDPAddr
buf := make([]byte, udpBufferSize)
// local -> remote
for {
n, cAddr, err := udpConn.ReadFromUDP(buf)
if err != nil {
return err
}
d, err := socks5.NewDatagramFromBytes(buf[:n])
if err != nil || d.Frag != 0 {
// Ignore bad packets
// Also we don't support SOCKS5 UDP fragmentation for now
continue
}
if clientAddr == nil {
// Before the first packet, we don't know what IP the client will use to send us packets,
// so we don't know what IP to return packets to.
// We treat whoever sends us the first packet as our client.
clientAddr = cAddr
// Now that we know the client's address, we can start the
// remote -> local direction.
go func() {
for {
bs, from, err := hyUDP.Receive()
if err != nil {
// Close the UDP conn so that the local -> remote direction will exit
_ = udpConn.Close()
return
}
atyp, addr, port, err := socks5.ParseAddress(from)
if err != nil {
continue
}
if atyp == socks5.ATYPDomain {
// socks5.ParseAddress adds a leading byte for domains,
// but socks5.NewDatagram will add it again as it expects a raw domain.
// So we must remove it here.
addr = addr[1:]
}
d := socks5.NewDatagram(atyp, addr, port, bs)
_, _ = udpConn.WriteToUDP(d.Bytes(), clientAddr)
}
}()
} else if !clientAddr.IP.Equal(cAddr.IP) || clientAddr.Port != cAddr.Port {
// Not our client, ignore
continue
}
// Send to remote
_ = hyUDP.Send(d.Data, d.Address())
}
}
// sendSimpleReply sends a SOCKS5 reply with the given reply code.
// It does not contain bind address or port, so it's not suitable for successful UDP requests.
func sendSimpleReply(conn net.Conn, rep byte) error {
p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00})
_, err := p.WriteTo(conn)
return err
}
// sendUDPReply sends a SOCKS5 reply with the given reply code and bind address/port.
func sendUDPReply(conn net.Conn, addr *net.UDPAddr) error {
var atyp byte
var bndAddr, bndPort []byte
if ip4 := addr.IP.To4(); ip4 != nil {
atyp = socks5.ATYPIPv4
bndAddr = ip4
} else {
atyp = socks5.ATYPIPv6
bndAddr = addr.IP
}
bndPort = make([]byte, 2)
binary.BigEndian.PutUint16(bndPort, uint16(addr.Port))
p := socks5.NewReply(socks5.RepSuccess, atyp, bndAddr, bndPort)
_, err := p.WriteTo(conn)
return err
}
================================================
FILE: app/internal/socks5/server_test.go
================================================
package socks5
import (
"net"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/app/v2/internal/utils_test"
)
func TestServer(t *testing.T) {
// Start the server
l, err := net.Listen("tcp", "127.0.0.1:11080")
assert.NoError(t, err)
defer l.Close()
s := &Server{
HyClient: &utils_test.MockEchoHyClient{},
}
go s.Serve(l)
// Run the Python test script
cmd := exec.Command("python", "server_test.py")
out, err := cmd.CombinedOutput()
assert.NoError(t, err)
assert.Equal(t, "OK", strings.TrimSpace(string(out)))
}
================================================
FILE: app/internal/socks5/server_test.py
================================================
import socket
import socks
import os
ADDR = "127.0.0.1"
PORT = 11080
def test_tcp(size, count, it, domain=False):
for i in range(it):
s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
s.set_proxy(socks.SOCKS5, ADDR, PORT)
if domain:
s.connect(("test.tcp.com", 12345))
else:
s.connect(("1.2.3.4", 12345))
for j in range(count):
payload = os.urandom(size)
s.send(payload)
rsp = s.recv(size)
assert rsp == payload
s.close()
def test_udp(size, count, it, domain=False):
for i in range(it):
s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)
s.set_proxy(socks.SOCKS5, ADDR, PORT)
for j in range(count):
payload = os.urandom(size)
if domain:
s.sendto(payload, ("test.udp.com", 12345))
else:
s.sendto(payload, ("1.2.3.4", 12345))
rsp, addr = s.recvfrom(size)
assert rsp == payload
if domain:
assert addr == (b"test.udp.com", 12345)
else:
assert addr == ("1.2.3.4", 12345)
s.close()
if __name__ == "__main__":
test_tcp(1024, 1024, 10, domain=False)
test_tcp(1024, 1024, 10, domain=True)
test_udp(1024, 1024, 10, domain=False)
test_udp(1024, 1024, 10, domain=True)
print("OK")
================================================
FILE: app/internal/tproxy/tcp_linux.go
================================================
package tproxy
import (
"io"
"net"
"github.com/apernet/go-tproxy"
"github.com/apernet/hysteria/core/v2/client"
)
type TCPTProxy struct {
HyClient client.Client
EventLogger TCPEventLogger
}
type TCPEventLogger interface {
Connect(addr, reqAddr net.Addr)
Error(addr, reqAddr net.Addr, err error)
}
func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error {
listener, err := tproxy.ListenTCP("tcp", laddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.Accept()
if err != nil {
return err
}
go r.handle(c)
}
}
func (r *TCPTProxy) handle(conn net.Conn) {
defer conn.Close()
// In TProxy mode, we are masquerading as the remote server.
// So LocalAddr is actually the target the user is trying to connect to,
// and RemoteAddr is the local address.
if r.EventLogger != nil {
r.EventLogger.Connect(conn.RemoteAddr(), conn.LocalAddr())
}
var closeErr error
defer func() {
if r.EventLogger != nil {
r.EventLogger.Error(conn.RemoteAddr(), conn.LocalAddr(), closeErr)
}
}()
rc, err := r.HyClient.TCP(conn.LocalAddr().String())
if err != nil {
closeErr = err
return
}
defer rc.Close()
// Start forwarding
copyErrChan := make(chan error, 2)
go func() {
_, copyErr := io.Copy(rc, conn)
copyErrChan <- copyErr
}()
go func() {
_, copyErr := io.Copy(conn, rc)
copyErrChan <- copyErr
}()
closeErr = <-copyErrChan
}
================================================
FILE: app/internal/tproxy/tcp_others.go
================================================
//go:build !linux
package tproxy
import (
"errors"
"net"
"github.com/apernet/hysteria/core/v2/client"
)
type TCPTProxy struct {
HyClient client.Client
EventLogger TCPEventLogger
}
type TCPEventLogger interface {
Connect(addr, reqAddr net.Addr)
Error(addr, reqAddr net.Addr, err error)
}
func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error {
return errors.New("not supported on this platform")
}
================================================
FILE: app/internal/tproxy/udp_linux.go
================================================
package tproxy
import (
"errors"
"net"
"time"
"github.com/apernet/go-tproxy"
"github.com/apernet/hysteria/core/v2/client"
)
const (
udpBufferSize = 4096
defaultTimeout = 60 * time.Second
)
type UDPTProxy struct {
HyClient client.Client
Timeout time.Duration
EventLogger UDPEventLogger
}
type UDPEventLogger interface {
Connect(addr, reqAddr net.Addr)
Error(addr, reqAddr net.Addr, err error)
}
func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error {
conn, err := tproxy.ListenUDP("udp", laddr)
if err != nil {
return err
}
defer conn.Close()
buf := make([]byte, udpBufferSize)
for {
// We will only get the first packet of each src/dst pair here,
// because newPair will create a TProxy connection and take over
// the src/dst pair. Later packets will be sent there instead of here.
n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf)
if err != nil {
return err
}
r.newPair(srcAddr, dstAddr, buf[:n])
}
}
func (r *UDPTProxy) newPair(srcAddr, dstAddr *net.UDPAddr, initPkt []byte) {
if r.EventLogger != nil {
r.EventLogger.Connect(srcAddr, dstAddr)
}
var closeErr error
defer func() {
// If closeErr is nil, it means we at least successfully sent the first packet
// and started forwarding, in which case we don't call the error logger.
if r.EventLogger != nil && closeErr != nil {
r.EventLogger.Error(srcAddr, dstAddr, closeErr)
}
}()
conn, err := tproxy.DialUDP("udp", dstAddr, srcAddr)
if err != nil {
closeErr = err
return
}
hyConn, err := r.HyClient.UDP()
if err != nil {
_ = conn.Close()
closeErr = err
return
}
// Send the first packet
err = hyConn.Send(initPkt, dstAddr.String())
if err != nil {
_ = conn.Close()
_ = hyConn.Close()
closeErr = err
return
}
// Start forwarding
go func() {
err := r.forwarding(conn, hyConn, dstAddr.String())
_ = conn.Close()
_ = hyConn.Close()
if r.EventLogger != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
// We don't consider deadline exceeded (timeout) an error
err = nil
}
r.EventLogger.Error(srcAddr, dstAddr, err)
}
}()
}
func (r *UDPTProxy) forwarding(conn *net.UDPConn, hyConn client.HyUDPConn, dst string) error {
errChan := make(chan error, 2)
// Local <- Remote
go func() {
for {
bs, _, err := hyConn.Receive()
if err != nil {
errChan <- err
return
}
_, err = conn.Write(bs)
if err != nil {
errChan <- err
return
}
_ = r.updateConnDeadline(conn)
}
}()
// Local -> Remote
go func() {
buf := make([]byte, udpBufferSize)
for {
_ = r.updateConnDeadline(conn)
n, err := conn.Read(buf)
if n > 0 {
err := hyConn.Send(buf[:n], dst)
if err != nil {
errChan <- err
return
}
}
if err != nil {
errChan <- err
return
}
}
}()
return <-errChan
}
func (r *UDPTProxy) updateConnDeadline(conn *net.UDPConn) error {
if r.Timeout == 0 {
return conn.SetReadDeadline(time.Now().Add(defaultTimeout))
} else {
return conn.SetReadDeadline(time.Now().Add(r.Timeout))
}
}
================================================
FILE: app/internal/tproxy/udp_others.go
================================================
//go:build !linux
package tproxy
import (
"errors"
"net"
"time"
"github.com/apernet/hysteria/core/v2/client"
)
type UDPTProxy struct {
HyClient client.Client
Timeout time.Duration
EventLogger UDPEventLogger
}
type UDPEventLogger interface {
Connect(addr, reqAddr net.Addr)
Error(addr, reqAddr net.Addr, err error)
}
func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error {
return errors.New("not supported on this platform")
}
================================================
FILE: app/internal/tun/check_ipv6_others.go
================================================
//go:build !unix && !windows
package tun
import "net"
func isIPv6Supported() bool {
lis, err := net.ListenPacket("udp6", "[::1]:0")
if err != nil {
return false
}
_ = lis.Close()
return true
}
================================================
FILE: app/internal/tun/check_ipv6_unix.go
================================================
//go:build unix
package tun
import (
"golang.org/x/sys/unix"
)
func isIPv6Supported() bool {
sock, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
if err != nil {
return false
}
_ = unix.Close(sock)
return true
}
================================================
FILE: app/internal/tun/check_ipv6_windows.go
================================================
//go:build windows
package tun
import (
"golang.org/x/sys/windows"
)
func isIPv6Supported() bool {
var wsaData windows.WSAData
err := windows.WSAStartup(uint32(0x202), &wsaData)
if err != nil {
// Failing silently: it is not our duty to report such errors
return true
}
defer windows.WSACleanup()
sock, err := windows.Socket(windows.AF_INET6, windows.SOCK_DGRAM, windows.IPPROTO_UDP)
if err != nil {
return false
}
_ = windows.Closesocket(sock)
return true
}
================================================
FILE: app/internal/tun/log.go
================================================
package tun
import (
"github.com/sagernet/sing/common/logger"
"go.uber.org/zap"
)
var _ logger.Logger = (*singLogger)(nil)
type singLogger struct {
tag string
zapLogger *zap.Logger
}
func extractSingExceptions(args []any) {
for i, arg := range args {
if err, ok := arg.(error); ok {
args[i] = err.Error()
}
}
}
func (l *singLogger) Trace(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Debug(l.tag, zap.Any("args", args))
}
func (l *singLogger) Debug(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Debug(l.tag, zap.Any("args", args))
}
func (l *singLogger) Info(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Info(l.tag, zap.Any("args", args))
}
func (l *singLogger) Warn(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Warn(l.tag, zap.Any("args", args))
}
func (l *singLogger) Error(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Error(l.tag, zap.Any("args", args))
}
func (l *singLogger) Fatal(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Fatal(l.tag, zap.Any("args", args))
}
func (l *singLogger) Panic(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Panic(l.tag, zap.Any("args", args))
}
================================================
FILE: app/internal/tun/server.go
================================================
package tun
import (
"context"
"fmt"
"io"
"net"
"net/netip"
tun "github.com/apernet/sing-tun"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
"github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/network"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
)
type Server struct {
HyClient client.Client
EventLogger EventLogger
// for debugging
Logger *zap.Logger
IfName string
MTU uint32
Timeout int64 // in seconds, also applied to TCP in system stack
// required by system stack
Inet4Address []netip.Prefix
Inet6Address []netip.Prefix
// auto route
AutoRoute bool
StructRoute bool
Inet4RouteAddress []netip.Prefix
Inet6RouteAddress []netip.Prefix
Inet4RouteExcludeAddress []netip.Prefix
Inet6RouteExcludeAddress []netip.Prefix
}
type EventLogger interface {
TCPRequest(addr, reqAddr string)
TCPError(addr, reqAddr string, err error)
UDPRequest(addr string)
UDPError(addr string, err error)
}
func (s *Server) Serve() error {
if !isIPv6Supported() {
s.Logger.Warn("tun-pre-check", zap.String("msg", "IPv6 is not supported or enabled on this system, TUN device is created without IPv6 support."))
s.Inet6Address = nil
}
tunOpts := tun.Options{
Name: s.IfName,
Inet4Address: s.Inet4Address,
Inet6Address: s.Inet6Address,
MTU: s.MTU,
GSO: true,
AutoRoute: s.AutoRoute,
StrictRoute: s.StructRoute,
Inet4RouteAddress: s.Inet4RouteAddress,
Inet6RouteAddress: s.Inet6RouteAddress,
Inet4RouteExcludeAddress: s.Inet4RouteExcludeAddress,
Inet6RouteExcludeAddress: s.Inet6RouteExcludeAddress,
Logger: &singLogger{
tag: "tun",
zapLogger: s.Logger,
},
}
tunIf, err := tun.New(tunOpts)
if err != nil {
return fmt.Errorf("failed to create tun interface: %w", err)
}
defer tunIf.Close()
tunStack, err := tun.NewSystem(tun.StackOptions{
Context: context.Background(),
Tun: tunIf,
TunOptions: tunOpts,
UDPTimeout: s.Timeout,
Handler: &tunHandler{s},
Logger: &singLogger{
tag: "tun-stack",
zapLogger: s.Logger,
},
ForwarderBindInterface: true,
InterfaceFinder: &interfaceFinder{},
})
if err != nil {
return fmt.Errorf("failed to create tun stack: %w", err)
}
defer tunStack.Close()
return tunStack.(tun.StackRunner).Run()
}
type tunHandler struct {
*Server
}
var _ tun.Handler = (*tunHandler)(nil)
func (t *tunHandler) NewConnection(ctx context.Context, conn net.Conn, m metadata.Metadata) error {
addr := m.Source.String()
reqAddr := m.Destination.String()
if t.EventLogger != nil {
t.EventLogger.TCPRequest(addr, reqAddr)
}
var closeErr error
defer func() {
if t.EventLogger != nil {
t.EventLogger.TCPError(addr, reqAddr, closeErr)
}
}()
rc, err := t.HyClient.TCP(reqAddr)
if err != nil {
closeErr = err
// the returned err is ignored by caller
return nil
}
defer rc.Close()
// start forwarding
copyErrChan := make(chan error, 2)
go func() {
_, copyErr := io.Copy(rc, conn)
copyErrChan <- copyErr
}()
go func() {
_, copyErr := io.Copy(conn, rc)
copyErrChan <- copyErr
}()
select {
case <-ctx.Done():
closeErr = ctx.Err()
case closeErr = <-copyErrChan:
}
return nil
}
func (t *tunHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, m metadata.Metadata) error {
addr := m.Source.String()
if t.EventLogger != nil {
t.EventLogger.UDPRequest(addr)
}
var closeErr error
defer func() {
if t.EventLogger != nil {
t.EventLogger.UDPError(addr, closeErr)
}
}()
rc, err := t.HyClient.UDP()
if err != nil {
closeErr = err
// the returned err is simply called into NewError again
return nil
}
defer rc.Close()
// start forwarding
copyErrChan := make(chan error, 2)
// local <- remote
go func() {
for {
bs, from, err := rc.Receive()
if err != nil {
copyErrChan <- err
return
}
var fromAddr metadata.Socksaddr
if ap, perr := netip.ParseAddrPort(from); perr == nil {
fromAddr = metadata.SocksaddrFromNetIP(ap)
} else {
fromAddr.Fqdn = from
}
err = conn.WritePacket(buf.As(bs), fromAddr)
if err != nil {
copyErrChan <- err
return
}
}
}()
// local -> remote
go func() {
buffer := buf.NewPacket()
defer buffer.Release()
for {
buffer.Reset()
addr, err := conn.ReadPacket(buffer)
if err != nil {
copyErrChan <- err
return
}
err = rc.Send(buffer.Bytes(), addr.String())
if err != nil {
copyErrChan <- err
return
}
}
}()
select {
case <-ctx.Done():
closeErr = ctx.Err()
case closeErr = <-copyErrChan:
}
return nil
}
func (t *tunHandler) NewError(ctx context.Context, err error) {
// unused
}
type interfaceFinder struct{}
var _ control.InterfaceFinder = (*interfaceFinder)(nil)
func (f *interfaceFinder) InterfaceIndexByName(name string) (int, error) {
ifce, err := net.InterfaceByName(name)
if err != nil {
return -1, err
}
return ifce.Index, nil
}
func (f *interfaceFinder) InterfaceNameByIndex(index int) (string, error) {
ifce, err := net.InterfaceByIndex(index)
if err != nil {
return "", err
}
return ifce.Name, nil
}
================================================
FILE: app/internal/url/url.go
================================================
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package url parses URLs and implements query escaping.
package url
// See RFC 3986. This package generally follows RFC 3986, except where
// it deviates for compatibility reasons. When sending changes, first
// search old issues for history on decisions. Unit tests should also
// contain references to issue numbers with details.
// Hysteria fork note: This file is grabbed from the standard Go library,
// but with a few modifications to make it support our special port format
// when using port hopping.
import (
"errors"
"fmt"
"path"
"sort"
"strconv"
"strings"
)
// Error reports an error and the operation and URL that caused it.
type Error struct {
Op string
URL string
Err error
}
func (e *Error) Unwrap() error { return e.Err }
func (e *Error) Error() string { return fmt.Sprintf("%s %q: %s", e.Op, e.URL, e.Err) }
func (e *Error) Timeout() bool {
t, ok := e.Err.(interface {
Timeout() bool
})
return ok && t.Timeout()
}
func (e *Error) Temporary() bool {
t, ok := e.Err.(interface {
Temporary() bool
})
return ok && t.Temporary()
}
const upperhex = "0123456789ABCDEF"
func ishex(c byte) bool {
switch {
case '0' <= c && c <= '9':
return true
case 'a' <= c && c <= 'f':
return true
case 'A' <= c && c <= 'F':
return true
}
return false
}
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
type encoding int
const (
encodePath encoding = 1 + iota
encodePathSegment
encodeHost
encodeZone
encodeUserPassword
encodeQueryComponent
encodeFragment
)
type EscapeError string
func (e EscapeError) Error() string {
return "invalid URL escape " + strconv.Quote(string(e))
}
type InvalidHostError string
func (e InvalidHostError) Error() string {
return "invalid character " + strconv.Quote(string(e)) + " in host name"
}
// Return true if the specified character should be escaped when
// appearing in a URL string, according to RFC 3986.
//
// Please be informed that for now shouldEscape does not check all
// reserved characters correctly. See golang.org/issue/5684.
func shouldEscape(c byte, mode encoding) bool {
// §2.3 Unreserved characters (alphanum)
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
return false
}
if mode == encodeHost || mode == encodeZone {
// §3.2.2 Host allows
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
// as part of reg-name.
// We add : because we include :port as part of host.
// We add [ ] because we include [ipv6]:port as part of host.
// We add < > because they're the only characters left that
// we could possibly allow, and Parse will reject them if we
// escape them (because hosts can't use %-encoding for
// ASCII bytes).
switch c {
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"':
return false
}
}
switch c {
case '-', '_', '.', '~': // §2.3 Unreserved characters (mark)
return false
case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)
// Different sections of the URL allow a few of
// the reserved characters to appear unescaped.
switch mode {
case encodePath: // §3.3
// The RFC allows : @ & = + $ but saves / ; , for assigning
// meaning to individual path segments. This package
// only manipulates the path as a whole, so we allow those
// last three as well. That leaves only ? to escape.
return c == '?'
case encodePathSegment: // §3.3
// The RFC allows : @ & = + $ but saves / ; , for assigning
// meaning to individual path segments.
return c == '/' || c == ';' || c == ',' || c == '?'
case encodeUserPassword: // §3.2.1
// The RFC allows ';', ':', '&', '=', '+', '$', and ',' in
// userinfo, so we must escape only '@', '/', and '?'.
// The parsing of userinfo treats ':' as special so we must escape
// that too.
return c == '@' || c == '/' || c == '?' || c == ':'
case encodeQueryComponent: // §3.4
// The RFC reserves (so we must escape) everything.
return true
case encodeFragment: // §4.1
// The RFC text is silent but the grammar allows
// everything, so escape nothing.
return false
}
}
if mode == encodeFragment {
// RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are
// included in reserved from RFC 2396 §2.2. The remaining sub-delims do not
// need to be escaped. To minimize potential breakage, we apply two restrictions:
// (1) we always escape sub-delims outside of the fragment, and (2) we always
// escape single quote to avoid breaking callers that had previously assumed that
// single quotes would be escaped. See issue #19917.
switch c {
case '!', '(', ')', '*':
return false
}
}
// Everything else must be escaped.
return true
}
// QueryUnescape does the inverse transformation of QueryEscape,
// converting each 3-byte encoded substring of the form "%AB" into the
// hex-decoded byte 0xAB.
// It returns an error if any % is not followed by two hexadecimal
// digits.
func QueryUnescape(s string) (string, error) {
return unescape(s, encodeQueryComponent)
}
// PathUnescape does the inverse transformation of PathEscape,
// converting each 3-byte encoded substring of the form "%AB" into the
// hex-decoded byte 0xAB. It returns an error if any % is not followed
// by two hexadecimal digits.
//
// PathUnescape is identical to QueryUnescape except that it does not
// unescape '+' to ' ' (space).
func PathUnescape(s string) (string, error) {
return unescape(s, encodePathSegment)
}
// unescape unescapes a string; the mode specifies
// which section of the URL string is being unescaped.
func unescape(s string, mode encoding) (string, error) {
// Count %, check that they're well-formed.
n := 0
hasPlus := false
for i := 0; i < len(s); {
switch s[i] {
case '%':
n++
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
s = s[i:]
if len(s) > 3 {
s = s[:3]
}
return "", EscapeError(s)
}
// Per https://tools.ietf.org/html/rfc3986#page-21
// in the host component %-encoding can only be used
// for non-ASCII bytes.
// But https://tools.ietf.org/html/rfc6874#section-2
// introduces %25 being allowed to escape a percent sign
// in IPv6 scoped-address literals. Yay.
if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" {
return "", EscapeError(s[i : i+3])
}
if mode == encodeZone {
// RFC 6874 says basically "anything goes" for zone identifiers
// and that even non-ASCII can be redundantly escaped,
// but it seems prudent to restrict %-escaped bytes here to those
// that are valid host name bytes in their unescaped form.
// That is, you can use escaping in the zone identifier but not
// to introduce bytes you couldn't just write directly.
// But Windows puts spaces here! Yay.
v := unhex(s[i+1])<<4 | unhex(s[i+2])
if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) {
return "", EscapeError(s[i : i+3])
}
}
i += 3
case '+':
hasPlus = mode == encodeQueryComponent
i++
default:
if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) {
return "", InvalidHostError(s[i : i+1])
}
i++
}
}
if n == 0 && !hasPlus {
return s, nil
}
var t strings.Builder
t.Grow(len(s) - 2*n)
for i := 0; i < len(s); i++ {
switch s[i] {
case '%':
t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2]))
i += 2
case '+':
if mode == encodeQueryComponent {
t.WriteByte(' ')
} else {
t.WriteByte('+')
}
default:
t.WriteByte(s[i])
}
}
return t.String(), nil
}
// QueryEscape escapes the string so it can be safely placed
// inside a URL query.
func QueryEscape(s string) string {
return escape(s, encodeQueryComponent)
}
// PathEscape escapes the string so it can be safely placed inside a URL path segment,
// replacing special characters (including /) with %XX sequences as needed.
func PathEscape(s string) string {
return escape(s, encodePathSegment)
}
func escape(s string, mode encoding) string {
spaceCount, hexCount := 0, 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c, mode) {
if c == ' ' && mode == encodeQueryComponent {
spaceCount++
} else {
hexCount++
}
}
}
if spaceCount == 0 && hexCount == 0 {
return s
}
var buf [64]byte
var t []byte
required := len(s) + 2*hexCount
if required <= len(buf) {
t = buf[:required]
} else {
t = make([]byte, required)
}
if hexCount == 0 {
copy(t, s)
for i := 0; i < len(s); i++ {
if s[i] == ' ' {
t[i] = '+'
}
}
return string(t)
}
j := 0
for i := 0; i < len(s); i++ {
switch c := s[i]; {
case c == ' ' && mode == encodeQueryComponent:
t[j] = '+'
j++
case shouldEscape(c, mode):
t[j] = '%'
t[j+1] = upperhex[c>>4]
t[j+2] = upperhex[c&15]
j += 3
default:
t[j] = s[i]
j++
}
}
return string(t)
}
// A URL represents a parsed URL (technically, a URI reference).
//
// The general form represented is:
//
// [scheme:][//[userinfo@]host][/]path[?query][#fragment]
//
// URLs that do not start with a slash after the scheme are interpreted as:
//
// scheme:opaque[?query][#fragment]
//
// Note that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.
// A consequence is that it is impossible to tell which slashes in the Path were
// slashes in the raw URL and which were %2f. This distinction is rarely important,
// but when it is, the code should use the EscapedPath method, which preserves
// the original encoding of Path.
//
// The RawPath field is an optional field which is only set when the default
// encoding of Path is different from the escaped path. See the EscapedPath method
// for more details.
//
// URL's String method uses the EscapedPath method to obtain the path.
type URL struct {
Scheme string
Opaque string // encoded opaque data
User *Userinfo // username and password information
Host string // host or host:port
Path string // path (relative paths may omit leading slash)
RawPath string // encoded path hint (see EscapedPath method)
OmitHost bool // do not emit empty host (authority)
ForceQuery bool // append a query ('?') even if RawQuery is empty
RawQuery string // encoded query values, without '?'
Fragment string // fragment for references, without '#'
RawFragment string // encoded fragment hint (see EscapedFragment method)
}
// User returns a Userinfo containing the provided username
// and no password set.
func User(username string) *Userinfo {
return &Userinfo{username, "", false}
}
// UserPassword returns a Userinfo containing the provided username
// and password.
//
// This functionality should only be used with legacy web sites.
// RFC 2396 warns that interpreting Userinfo this way
// “is NOT RECOMMENDED, because the passing of authentication
// information in clear text (such as URI) has proven to be a
// security risk in almost every case where it has been used.”
func UserPassword(username, password string) *Userinfo {
return &Userinfo{username, password, true}
}
// The Userinfo type is an immutable encapsulation of username and
// password details for a URL. An existing Userinfo value is guaranteed
// to have a username set (potentially empty, as allowed by RFC 2396),
// and optionally a password.
type Userinfo struct {
username string
password string
passwordSet bool
}
// Username returns the username.
func (u *Userinfo) Username() string {
if u == nil {
return ""
}
return u.username
}
// Password returns the password in case it is set, and whether it is set.
func (u *Userinfo) Password() (string, bool) {
if u == nil {
return "", false
}
return u.password, u.passwordSet
}
// String returns the encoded userinfo information in the standard form
// of "username[:password]".
func (u *Userinfo) String() string {
if u == nil {
return ""
}
s := escape(u.username, encodeUserPassword)
if u.passwordSet {
s += ":" + escape(u.password, encodeUserPassword)
}
return s
}
// Maybe rawURL is of the form scheme:path.
// (Scheme must be [a-zA-Z][a-zA-Z0-9+.-]*)
// If so, return scheme, path; else return "", rawURL.
func getScheme(rawURL string) (scheme, path string, err error) {
for i := 0; i < len(rawURL); i++ {
c := rawURL[i]
switch {
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
// do nothing
case '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.':
if i == 0 {
return "", rawURL, nil
}
case c == ':':
if i == 0 {
return "", "", errors.New("missing protocol scheme")
}
return rawURL[:i], rawURL[i+1:], nil
default:
// we have encountered an invalid character,
// so there is no valid scheme
return "", rawURL, nil
}
}
return "", rawURL, nil
}
// Parse parses a raw url into a URL structure.
//
// The url may be relative (a path, without a host) or absolute
// (starting with a scheme). Trying to parse a hostname and path
// without a scheme is invalid but may not necessarily return an
// error, due to parsing ambiguities.
func Parse(rawURL string) (*URL, error) {
// Cut off #frag
u, frag, _ := strings.Cut(rawURL, "#")
url, err := parse(u, false)
if err != nil {
return nil, &Error{"parse", u, err}
}
if frag == "" {
return url, nil
}
if err = url.setFragment(frag); err != nil {
return nil, &Error{"parse", rawURL, err}
}
return url, nil
}
// ParseRequestURI parses a raw url into a URL structure. It assumes that
// url was received in an HTTP request, so the url is interpreted
// only as an absolute URI or an absolute path.
// The string url is assumed not to have a #fragment suffix.
// (Web browsers strip #fragment before sending the URL to a web server.)
func ParseRequestURI(rawURL string) (*URL, error) {
url, err := parse(rawURL, true)
if err != nil {
return nil, &Error{"parse", rawURL, err}
}
return url, nil
}
// parse parses a URL from a string in one of two contexts. If
// viaRequest is true, the URL is assumed to have arrived via an HTTP request,
// in which case only absolute URLs or path-absolute relative URLs are allowed.
// If viaRequest is false, all forms of relative URLs are allowed.
func parse(rawURL string, viaRequest bool) (*URL, error) {
var rest string
var err error
if stringContainsCTLByte(rawURL) {
return nil, errors.New("net/url: invalid control character in URL")
}
if rawURL == "" && viaRequest {
return nil, errors.New("empty url")
}
url := new(URL)
if rawURL == "*" {
url.Path = "*"
return url, nil
}
// Split off possible leading "http:", "mailto:", etc.
// Cannot contain escaped characters.
if url.Scheme, rest, err = getScheme(rawURL); err != nil {
return nil, err
}
url.Scheme = strings.ToLower(url.Scheme)
if strings.HasSuffix(rest, "?") && strings.Count(rest, "?") == 1 {
url.ForceQuery = true
rest = rest[:len(rest)-1]
} else {
rest, url.RawQuery, _ = strings.Cut(rest, "?")
}
if !strings.HasPrefix(rest, "/") {
if url.Scheme != "" {
// We consider rootless paths per RFC 3986 as opaque.
url.Opaque = rest
return url, nil
}
if viaRequest {
return nil, errors.New("invalid URI for request")
}
// Avoid confusion with malformed schemes, like cache_object:foo/bar.
// See golang.org/issue/16822.
//
// RFC 3986, §3.3:
// In addition, a URI reference (Section 4.1) may be a relative-path reference,
// in which case the first path segment cannot contain a colon (":") character.
if segment, _, _ := strings.Cut(rest, "/"); strings.Contains(segment, ":") {
// First path segment has colon. Not allowed in relative URL.
return nil, errors.New("first path segment in URL cannot contain colon")
}
}
if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") {
var authority string
authority, rest = rest[2:], ""
if i := strings.Index(authority, "/"); i >= 0 {
authority, rest = authority[:i], authority[i:]
}
url.User, url.Host, err = parseAuthority(authority)
if err != nil {
return nil, err
}
} else if url.Scheme != "" && strings.HasPrefix(rest, "/") {
// OmitHost is set to true when rawURL has an empty host (authority).
// See golang.org/issue/46059.
url.OmitHost = true
}
// Set Path and, optionally, RawPath.
// RawPath is a hint of the encoding of Path. We don't want to set it if
// the default escaping of Path is equivalent, to help make sure that people
// don't rely on it in general.
if err := url.setPath(rest); err != nil {
return nil, err
}
return url, nil
}
func parseAuthority(authority string) (user *Userinfo, host string, err error) {
i := strings.LastIndex(authority, "@")
if i < 0 {
host, err = parseHost(authority)
} else {
host, err = parseHost(authority[i+1:])
}
if err != nil {
return nil, "", err
}
if i < 0 {
return nil, host, nil
}
userinfo := authority[:i]
if !validUserinfo(userinfo) {
return nil, "", errors.New("net/url: invalid userinfo")
}
if !strings.Contains(userinfo, ":") {
if userinfo, err = unescape(userinfo, encodeUserPassword); err != nil {
return nil, "", err
}
user = User(userinfo)
} else {
username, password, _ := strings.Cut(userinfo, ":")
if username, err = unescape(username, encodeUserPassword); err != nil {
return nil, "", err
}
if password, err = unescape(password, encodeUserPassword); err != nil {
return nil, "", err
}
user = UserPassword(username, password)
}
return user, host, nil
}
// parseHost parses host as an authority without user
// information. That is, as host[:port].
func parseHost(host string) (string, error) {
if strings.HasPrefix(host, "[") {
// Parse an IP-Literal in RFC 3986 and RFC 6874.
// E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80".
i := strings.LastIndex(host, "]")
if i < 0 {
return "", errors.New("missing ']' in host")
}
colonPort := host[i+1:]
if !validOptionalPort(colonPort) {
return "", fmt.Errorf("invalid port %q after host", colonPort)
}
// RFC 6874 defines that %25 (%-encoded percent) introduces
// the zone identifier, and the zone identifier can use basically
// any %-encoding it likes. That's different from the host, which
// can only %-encode non-ASCII bytes.
// We do impose some restrictions on the zone, to avoid stupidity
// like newlines.
zone := strings.Index(host[:i], "%25")
if zone >= 0 {
host1, err := unescape(host[:zone], encodeHost)
if err != nil {
return "", err
}
host2, err := unescape(host[zone:i], encodeZone)
if err != nil {
return "", err
}
host3, err := unescape(host[i:], encodeHost)
if err != nil {
return "", err
}
return host1 + host2 + host3, nil
}
} else if i := strings.LastIndex(host, ":"); i != -1 {
colonPort := host[i:]
if !validOptionalPort(colonPort) {
return "", fmt.Errorf("invalid port %q after host", colonPort)
}
}
var err error
if host, err = unescape(host, encodeHost); err != nil {
return "", err
}
return host, nil
}
// setPath sets the Path and RawPath fields of the URL based on the provided
// escaped path p. It maintains the invariant that RawPath is only specified
// when it differs from the default encoding of the path.
// For example:
// - setPath("/foo/bar") will set Path="/foo/bar" and RawPath=""
// - setPath("/foo%2fbar") will set Path="/foo/bar" and RawPath="/foo%2fbar"
// setPath will return an error only if the provided path contains an invalid
// escaping.
func (u *URL) setPath(p string) error {
path, err := unescape(p, encodePath)
if err != nil {
return err
}
u.Path = path
if escp := escape(path, encodePath); p == escp {
// Default encoding is fine.
u.RawPath = ""
} else {
u.RawPath = p
}
return nil
}
// EscapedPath returns the escaped form of u.Path.
// In general there are multiple possible escaped forms of any path.
// EscapedPath returns u.RawPath when it is a valid escaping of u.Path.
// Otherwise EscapedPath ignores u.RawPath and computes an escaped
// form on its own.
// The String and RequestURI methods use EscapedPath to construct
// their results.
// In general, code should call EscapedPath instead of
// reading u.RawPath directly.
func (u *URL) EscapedPath() string {
if u.RawPath != "" && validEncoded(u.RawPath, encodePath) {
p, err := unescape(u.RawPath, encodePath)
if err == nil && p == u.Path {
return u.RawPath
}
}
if u.Path == "*" {
return "*" // don't escape (Issue 11202)
}
return escape(u.Path, encodePath)
}
// validEncoded reports whether s is a valid encoded path or fragment,
// according to mode.
// It must not contain any bytes that require escaping during encoding.
func validEncoded(s string, mode encoding) bool {
for i := 0; i < len(s); i++ {
// RFC 3986, Appendix A.
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@".
// shouldEscape is not quite compliant with the RFC,
// so we check the sub-delims ourselves and let
// shouldEscape handle the others.
switch s[i] {
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@':
// ok
case '[', ']':
// ok - not specified in RFC 3986 but left alone by modern browsers
case '%':
// ok - percent encoded, will decode
default:
if shouldEscape(s[i], mode) {
return false
}
}
}
return true
}
// setFragment is like setPath but for Fragment/RawFragment.
func (u *URL) setFragment(f string) error {
frag, err := unescape(f, encodeFragment)
if err != nil {
return err
}
u.Fragment = frag
if escf := escape(frag, encodeFragment); f == escf {
// Default encoding is fine.
u.RawFragment = ""
} else {
u.RawFragment = f
}
return nil
}
// EscapedFragment returns the escaped form of u.Fragment.
// In general there are multiple possible escaped forms of any fragment.
// EscapedFragment returns u.RawFragment when it is a valid escaping of u.Fragment.
// Otherwise EscapedFragment ignores u.RawFragment and computes an escaped
// form on its own.
// The String method uses EscapedFragment to construct its result.
// In general, code should call EscapedFragment instead of
// reading u.RawFragment directly.
func (u *URL) EscapedFragment() string {
if u.RawFragment != "" && validEncoded(u.RawFragment, encodeFragment) {
f, err := unescape(u.RawFragment, encodeFragment)
if err == nil && f == u.Fragment {
return u.RawFragment
}
}
return escape(u.Fragment, encodeFragment)
}
// validOptionalPort reports whether port is either an empty string
// or matches /^:\d*$/
func validOptionalPort(port string) bool {
if port == "" {
return true
}
if port[0] != ':' {
return false
}
for _, b := range port[1:] {
if (b < '0' || b > '9') && (b != '-' && b != ',') {
// Neither a digit nor a valid separator character.
return false
}
}
return true
}
// String reassembles the URL into a valid URL string.
// The general form of the result is one of:
//
// scheme:opaque?query#fragment
// scheme://userinfo@host/path?query#fragment
//
// If u.Opaque is non-empty, String uses the first form;
// otherwise it uses the second form.
// Any non-ASCII characters in host are escaped.
// To obtain the path, String uses u.EscapedPath().
//
// In the second form, the following rules apply:
// - if u.Scheme is empty, scheme: is omitted.
// - if u.User is nil, userinfo@ is omitted.
// - if u.Host is empty, host/ is omitted.
// - if u.Scheme and u.Host are empty and u.User is nil,
// the entire scheme://userinfo@host/ is omitted.
// - if u.Host is non-empty and u.Path begins with a /,
// the form host/path does not add its own /.
// - if u.RawQuery is empty, ?query is omitted.
// - if u.Fragment is empty, #fragment is omitted.
func (u *URL) String() string {
var buf strings.Builder
if u.Scheme != "" {
buf.WriteString(u.Scheme)
buf.WriteByte(':')
}
if u.Opaque != "" {
buf.WriteString(u.Opaque)
} else {
if u.Scheme != "" || u.Host != "" || u.User != nil {
if u.OmitHost && u.Host == "" && u.User == nil {
// omit empty host
} else {
if u.Host != "" || u.Path != "" || u.User != nil {
buf.WriteString("//")
}
if ui := u.User; ui != nil {
buf.WriteString(ui.String())
buf.WriteByte('@')
}
if h := u.Host; h != "" {
buf.WriteString(escape(h, encodeHost))
}
}
}
path := u.EscapedPath()
if path != "" && path[0] != '/' && u.Host != "" {
buf.WriteByte('/')
}
if buf.Len() == 0 {
// RFC 3986 §4.2
// A path segment that contains a colon character (e.g., "this:that")
// cannot be used as the first segment of a relative-path reference, as
// it would be mistaken for a scheme name. Such a segment must be
// preceded by a dot-segment (e.g., "./this:that") to make a relative-
// path reference.
if segment, _, _ := strings.Cut(path, "/"); strings.Contains(segment, ":") {
buf.WriteString("./")
}
}
buf.WriteString(path)
}
if u.ForceQuery || u.RawQuery != "" {
buf.WriteByte('?')
buf.WriteString(u.RawQuery)
}
if u.Fragment != "" {
buf.WriteByte('#')
buf.WriteString(u.EscapedFragment())
}
return buf.String()
}
// Redacted is like String but replaces any password with "xxxxx".
// Only the password in u.User is redacted.
func (u *URL) Redacted() string {
if u == nil {
return ""
}
ru := *u
if _, has := ru.User.Password(); has {
ru.User = UserPassword(ru.User.Username(), "xxxxx")
}
return ru.String()
}
// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
type Values map[string][]string
// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns
// the empty string. To access multiple values, use the map
// directly.
func (v Values) Get(key string) string {
vs := v[key]
if len(vs) == 0 {
return ""
}
return vs[0]
}
// Set sets the key to value. It replaces any existing
// values.
func (v Values) Set(key, value string) {
v[key] = []string{value}
}
// Add adds the value to key. It appends to any existing
// values associated with key.
func (v Values) Add(key, value string) {
v[key] = append(v[key], value)
}
// Del deletes the values associated with key.
func (v Values) Del(key string) {
delete(v, key)
}
// Has checks whether a given key is set.
func (v Values) Has(key string) bool {
_, ok := v[key]
return ok
}
// ParseQuery parses the URL-encoded query string and returns
// a map listing the values specified for each key.
// ParseQuery always returns a non-nil map containing all the
// valid query parameters found; err describes the first decoding error
// encountered, if any.
//
// Query is expected to be a list of key=value settings separated by ampersands.
// A setting without an equals sign is interpreted as a key set to an empty
// value.
// Settings containing a non-URL-encoded semicolon are considered invalid.
func ParseQuery(query string) (Values, error) {
m := make(Values)
err := parseQuery(m, query)
return m, err
}
func parseQuery(m Values, query string) (err error) {
for query != "" {
var key string
key, query, _ = strings.Cut(query, "&")
if strings.Contains(key, ";") {
err = fmt.Errorf("invalid semicolon separator in query")
continue
}
if key == "" {
continue
}
key, value, _ := strings.Cut(key, "=")
key, err1 := QueryUnescape(key)
if err1 != nil {
if err == nil {
err = err1
}
continue
}
value, err1 = QueryUnescape(value)
if err1 != nil {
if err == nil {
err = err1
}
continue
}
m[key] = append(m[key], value)
}
return err
}
// Encode encodes the values into “URL encoded” form
// ("bar=baz&foo=quux") sorted by key.
func (v Values) Encode() string {
if v == nil {
return ""
}
var buf strings.Builder
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
vs := v[k]
keyEscaped := QueryEscape(k)
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(keyEscaped)
buf.WriteByte('=')
buf.WriteString(QueryEscape(v))
}
}
return buf.String()
}
// resolvePath applies special path segments from refs and applies
// them to base, per RFC 3986.
func resolvePath(base, ref string) string {
var full string
if ref == "" {
full = base
} else if ref[0] != '/' {
i := strings.LastIndex(base, "/")
full = base[:i+1] + ref
} else {
full = ref
}
if full == "" {
return ""
}
var (
elem string
dst strings.Builder
)
first := true
remaining := full
// We want to return a leading '/', so write it now.
dst.WriteByte('/')
found := true
for found {
elem, remaining, found = strings.Cut(remaining, "/")
if elem == "." {
first = false
// drop
continue
}
if elem == ".." {
// Ignore the leading '/' we already wrote.
str := dst.String()[1:]
index := strings.LastIndexByte(str, '/')
dst.Reset()
dst.WriteByte('/')
if index == -1 {
first = true
} else {
dst.WriteString(str[:index])
}
} else {
if !first {
dst.WriteByte('/')
}
dst.WriteString(elem)
first = false
}
}
if elem == "." || elem == ".." {
dst.WriteByte('/')
}
// We wrote an initial '/', but we don't want two.
r := dst.String()
if len(r) > 1 && r[1] == '/' {
r = r[1:]
}
return r
}
// IsAbs reports whether the URL is absolute.
// Absolute means that it has a non-empty scheme.
func (u *URL) IsAbs() bool {
return u.Scheme != ""
}
// Parse parses a URL in the context of the receiver. The provided URL
// may be relative or absolute. Parse returns nil, err on parse
// failure, otherwise its return value is the same as ResolveReference.
func (u *URL) Parse(ref string) (*URL, error) {
refURL, err := Parse(ref)
if err != nil {
return nil, err
}
return u.ResolveReference(refURL), nil
}
// ResolveReference resolves a URI reference to an absolute URI from
// an absolute base URI u, per RFC 3986 Section 5.2. The URI reference
// may be relative or absolute. ResolveReference always returns a new
// URL instance, even if the returned URL is identical to either the
// base or reference. If ref is an absolute URL, then ResolveReference
// ignores base and returns a copy of ref.
func (u *URL) ResolveReference(ref *URL) *URL {
url := *ref
if ref.Scheme == "" {
url.Scheme = u.Scheme
}
if ref.Scheme != "" || ref.Host != "" || ref.User != nil {
// The "absoluteURI" or "net_path" cases.
// We can ignore the error from setPath since we know we provided a
// validly-escaped path.
url.setPath(resolvePath(ref.EscapedPath(), ""))
return &url
}
if ref.Opaque != "" {
url.User = nil
url.Host = ""
url.Path = ""
return &url
}
if ref.Path == "" && !ref.ForceQuery && ref.RawQuery == "" {
url.RawQuery = u.RawQuery
if ref.Fragment == "" {
url.Fragment = u.Fragment
url.RawFragment = u.RawFragment
}
}
// The "abs_path" or "rel_path" cases.
url.Host = u.Host
url.User = u.User
url.setPath(resolvePath(u.EscapedPath(), ref.EscapedPath()))
return &url
}
// Query parses RawQuery and returns the corresponding values.
// It silently discards malformed value pairs.
// To check errors use ParseQuery.
func (u *URL) Query() Values {
v, _ := ParseQuery(u.RawQuery)
return v
}
// RequestURI returns the encoded path?query or opaque?query
// string that would be used in an HTTP request for u.
func (u *URL) RequestURI() string {
result := u.Opaque
if result == "" {
result = u.EscapedPath()
if result == "" {
result = "/"
}
} else {
if strings.HasPrefix(result, "//") {
result = u.Scheme + ":" + result
}
}
if u.ForceQuery || u.RawQuery != "" {
result += "?" + u.RawQuery
}
return result
}
// Hostname returns u.Host, stripping any valid port number if present.
//
// If the result is enclosed in square brackets, as literal IPv6 addresses are,
// the square brackets are removed from the result.
func (u *URL) Hostname() string {
host, _ := splitHostPort(u.Host)
return host
}
// Port returns the port part of u.Host, without the leading colon.
//
// If u.Host doesn't contain a valid numeric port, Port returns an empty string.
func (u *URL) Port() string {
_, port := splitHostPort(u.Host)
return port
}
// splitHostPort separates host and port. If the port is not valid, it returns
// the entire input as host, and it doesn't check the validity of the host.
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
func splitHostPort(hostPort string) (host, port string) {
host = hostPort
colon := strings.LastIndexByte(host, ':')
if colon != -1 && validOptionalPort(host[colon:]) {
host, port = host[:colon], host[colon+1:]
}
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = host[1 : len(host)-1]
}
return host, port
}
// Marshaling interface implementations.
// Would like to implement MarshalText/UnmarshalText but that will change the JSON representation of URLs.
func (u *URL) MarshalBinary() (text []byte, err error) {
return []byte(u.String()), nil
}
func (u *URL) UnmarshalBinary(text []byte) error {
u1, err := Parse(string(text))
if err != nil {
return err
}
*u = *u1
return nil
}
// JoinPath returns a new URL with the provided path elements joined to
// any existing path and the resulting path cleaned of any ./ or ../ elements.
// Any sequences of multiple / characters will be reduced to a single /.
func (u *URL) JoinPath(elem ...string) *URL {
elem = append([]string{u.EscapedPath()}, elem...)
var p string
if !strings.HasPrefix(elem[0], "/") {
// Return a relative path if u is relative,
// but ensure that it contains no ../ elements.
elem[0] = "/" + elem[0]
p = path.Join(elem...)[1:]
} else {
p = path.Join(elem...)
}
// path.Join will remove any trailing slashes.
// Preserve at least one.
if strings.HasSuffix(elem[len(elem)-1], "/") && !strings.HasSuffix(p, "/") {
p += "/"
}
url := *u
url.setPath(p)
return &url
}
// validUserinfo reports whether s is a valid userinfo string per RFC 3986
// Section 3.2.1:
//
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
// / "*" / "+" / "," / ";" / "="
//
// It doesn't validate pct-encoded. The caller does that via func unescape.
func validUserinfo(s string) bool {
for _, r := range s {
if 'A' <= r && r <= 'Z' {
continue
}
if 'a' <= r && r <= 'z' {
continue
}
if '0' <= r && r <= '9' {
continue
}
switch r {
case '-', '.', '_', ':', '~', '!', '$', '&', '\'',
'(', ')', '*', '+', ',', ';', '=', '%', '@':
continue
default:
return false
}
}
return true
}
// stringContainsCTLByte reports whether s contains any ASCII control character.
func stringContainsCTLByte(s string) bool {
for i := 0; i < len(s); i++ {
b := s[i]
if b < ' ' || b == 0x7f {
return true
}
}
return false
}
// JoinPath returns a URL string with the provided path elements joined to
// the existing path of base and the resulting path cleaned of any ./ or ../ elements.
func JoinPath(base string, elem ...string) (result string, err error) {
url, err := Parse(base)
if err != nil {
return result, err
}
result = url.JoinPath(elem...).String()
return result, err
}
================================================
FILE: app/internal/url/url_test.go
================================================
package url
import (
"reflect"
"testing"
)
func TestParse(t *testing.T) {
type args struct {
rawURL string
}
tests := []struct {
name string
args args
want *URL
wantErr bool
}{
{
name: "no port",
args: args{
rawURL: "hysteria2://ganggang@icecreamsogood/",
},
want: &URL{
Scheme: "hysteria2",
User: User("ganggang"),
Host: "icecreamsogood",
Path: "/",
},
},
{
name: "single port",
args: args{
rawURL: "hysteria2://yesyes@icecreamsogood:8888/",
},
want: &URL{
Scheme: "hysteria2",
User: User("yesyes"),
Host: "icecreamsogood:8888",
Path: "/",
},
},
{
name: "multi port",
args: args{
rawURL: "hysteria2://darkness@laplus.org:8888,9999,11111/",
},
want: &URL{
Scheme: "hysteria2",
User: User("darkness"),
Host: "laplus.org:8888,9999,11111",
Path: "/",
},
},
{
name: "range port",
args: args{
rawURL: "hysteria2://darkness@laplus.org:8888-9999/",
},
want: &URL{
Scheme: "hysteria2",
User: User("darkness"),
Host: "laplus.org:8888-9999",
Path: "/",
},
},
{
name: "both",
args: args{
rawURL: "hysteria2://gawr:gura@atlantis.moe:443,7788-8899,10010/",
},
want: &URL{
Scheme: "hysteria2",
User: UserPassword("gawr", "gura"),
Host: "atlantis.moe:443,7788-8899,10010",
Path: "/",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.args.rawURL)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() got = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: app/internal/utils/bpsconv.go
================================================
package utils
import (
"errors"
"fmt"
"strconv"
"strings"
)
const (
Byte = 1
Kilobyte = Byte * 1000
Megabyte = Kilobyte * 1000
Gigabyte = Megabyte * 1000
Terabyte = Gigabyte * 1000
)
// StringToBps converts a string to a bandwidth value in bytes per second.
// E.g. "100 Mbps", "512 kbps", "1g" are all valid.
func StringToBps(s string) (uint64, error) {
s = strings.ToLower(strings.TrimSpace(s))
spl := 0
for i, c := range s {
if c < '0' || c > '9' {
spl = i
break
}
}
if spl == 0 {
// No unit or no value
return 0, errors.New("invalid format")
}
v, err := strconv.ParseUint(s[:spl], 10, 64)
if err != nil {
return 0, err
}
unit := strings.TrimSpace(s[spl:])
switch strings.ToLower(unit) {
case "b", "bps":
return v * Byte / 8, nil
case "k", "kb", "kbps":
return v * Kilobyte / 8, nil
case "m", "mb", "mbps":
return v * Megabyte / 8, nil
case "g", "gb", "gbps":
return v * Gigabyte / 8, nil
case "t", "tb", "tbps":
return v * Terabyte / 8, nil
default:
return 0, errors.New("unsupported unit")
}
}
// ConvBandwidth handles both string and int types for bandwidth.
// When using string, it will be parsed as a bandwidth string with units.
// When using int, it will be parsed as a raw bandwidth in bytes per second.
// It does NOT support float types.
func ConvBandwidth(bw interface{}) (uint64, error) {
switch bwT := bw.(type) {
case string:
return StringToBps(bwT)
case int:
return uint64(bwT), nil
default:
return 0, fmt.Errorf("invalid type %T for bandwidth", bwT)
}
}
================================================
FILE: app/internal/utils/bpsconv_test.go
================================================
package utils
import "testing"
func TestStringToBps(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want uint64
wantErr bool
}{
{"bps", args{"800 bps"}, 100, false},
{"kbps", args{"800 kbps"}, 100_000, false},
{"mbps", args{"800 mbps"}, 100_000_000, false},
{"gbps", args{"800 gbps"}, 100_000_000_000, false},
{"tbps", args{"800 tbps"}, 100_000_000_000_000, false},
{"mbps simp", args{"100m"}, 12_500_000, false},
{"gbps simp upper", args{"2G"}, 250_000_000, false},
{"invalid 1", args{"damn"}, 0, true},
{"invalid 2", args{"6444"}, 0, true},
{"invalid 3", args{"5.4 mbps"}, 0, true},
{"invalid 4", args{"kbps"}, 0, true},
{"invalid 5", args{"1234 5678 gbps"}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := StringToBps(tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("StringToBps() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("StringToBps() got = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: app/internal/utils/certloader.go
================================================
package utils
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"strings"
"sync"
"sync/atomic"
"time"
)
type LocalCertificateLoader struct {
CertFile string
KeyFile string
SNIGuard SNIGuardFunc
lock sync.Mutex
cache atomic.Pointer[localCertificateCache]
}
type SNIGuardFunc func(info *tls.ClientHelloInfo, cert *tls.Certificate) error
// localCertificateCache holds the certificate and its mod times.
// this struct is designed to be read-only.
//
// to update the cache, use LocalCertificateLoader.makeCache and
// update the LocalCertificateLoader.cache field.
type localCertificateCache struct {
certificate *tls.Certificate
certModTime time.Time
keyModTime time.Time
}
func (l *LocalCertificateLoader) InitializeCache() error {
l.lock.Lock()
defer l.lock.Unlock()
cache, err := l.makeCache()
if err != nil {
return err
}
l.cache.Store(cache)
return nil
}
func (l *LocalCertificateLoader) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := l.getCertificateWithCache()
if err != nil {
return nil, err
}
if l.SNIGuard == nil {
return cert, nil
}
err = l.SNIGuard(info, cert)
if err != nil {
return nil, err
}
return cert, nil
}
func (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Time, err error) {
fi, err := os.Stat(l.CertFile)
if err != nil {
err = fmt.Errorf("failed to stat certificate file: %w", err)
return certModTime, keyModTime, err
}
certModTime = fi.ModTime()
fi, err = os.Stat(l.KeyFile)
if err != nil {
err = fmt.Errorf("failed to stat key file: %w", err)
return certModTime, keyModTime, err
}
keyModTime = fi.ModTime()
return certModTime, keyModTime, err
}
func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) {
c := &localCertificateCache{}
c.certModTime, c.keyModTime, err = l.checkModTime()
if err != nil {
return cache, err
}
cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile)
if err != nil {
return cache, err
}
c.certificate = &cert
if c.certificate.Leaf == nil {
// certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23
c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return cache, err
}
}
cache = c
return cache, err
}
func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) {
cache := l.cache.Load()
certModTime, keyModTime, terr := l.checkModTime()
if terr != nil {
if cache != nil {
// use cache when file is temporarily unavailable
return cache.certificate, nil
}
return nil, terr
}
if cache != nil && cache.certModTime.Equal(certModTime) && cache.keyModTime.Equal(keyModTime) {
// cache is up-to-date
return cache.certificate, nil
}
if cache != nil {
if !l.lock.TryLock() {
// another goroutine is updating the cache
return cache.certificate, nil
}
} else {
l.lock.Lock()
}
defer l.lock.Unlock()
if l.cache.Load() != cache {
// another goroutine updated the cache
return l.cache.Load().certificate, nil
}
newCache, err := l.makeCache()
if err != nil {
if cache != nil {
// use cache when loading failed
return cache.certificate, nil
}
return nil, err
}
l.cache.Store(newCache)
return newCache.certificate, nil
}
// getNameFromClientHello returns a normalized form of hello.ServerName.
// If hello.ServerName is empty (i.e. client did not use SNI), then the
// associated connection's local address is used to extract an IP address.
//
// ref: https://github.com/caddyserver/certmagic/blob/3bad5b6bb595b09c14bd86ff0b365d302faaf5e2/handshake.go#L838
func getNameFromClientHello(hello *tls.ClientHelloInfo) string {
normalizedName := func(serverName string) string {
return strings.ToLower(strings.TrimSpace(serverName))
}
localIPFromConn := func(c net.Conn) string {
if c == nil {
return ""
}
localAddr := c.LocalAddr().String()
ip, _, err := net.SplitHostPort(localAddr)
if err != nil {
ip = localAddr
}
if scopeIDStart := strings.Index(ip, "%"); scopeIDStart > -1 {
ip = ip[:scopeIDStart]
}
return ip
}
if name := normalizedName(hello.ServerName); name != "" {
return name
}
return localIPFromConn(hello.Conn)
}
func SNIGuardDNSSAN(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
if len(cert.Leaf.DNSNames) == 0 {
return nil
}
return SNIGuardStrict(info, cert)
}
func SNIGuardStrict(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
hostname := getNameFromClientHello(info)
err := cert.Leaf.VerifyHostname(hostname)
if err != nil {
return fmt.Errorf("sni guard: %w", err)
}
return nil
}
================================================
FILE: app/internal/utils/certloader_test.go
================================================
package utils
import (
"crypto/tls"
"log"
"net/http"
"os"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
testListen = "127.82.39.147:12947"
testCAFile = "./testcerts/ca"
testCertFile = "./testcerts/cert"
testKeyFile = "./testcerts/key"
)
func TestCertificateLoaderPathError(t *testing.T) {
assert.NoError(t, os.RemoveAll(testCertFile))
assert.NoError(t, os.RemoveAll(testKeyFile))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
err := loader.InitializeCache()
var pathErr *os.PathError
assert.ErrorAs(t, err, &pathErr)
}
func TestCertificateLoaderFullChain(t *testing.T) {
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.Error(t, runTestTLSClient("unmatched-sni.example.com"))
assert.Error(t, runTestTLSClient(""))
assert.NoError(t, runTestTLSClient("example.com"))
}
func TestCertificateLoaderNoSAN(t *testing.T) {
assert.NoError(t, generateTestCertificate(nil, "selfsign"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardDNSSAN,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.NoError(t, runTestTLSClient(""))
}
func TestCertificateLoaderReplaceCertificate(t *testing.T) {
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.NoError(t, runTestTLSClient("example.com"))
assert.Error(t, runTestTLSClient("2.example.com"))
assert.NoError(t, generateTestCertificate([]string{"2.example.com"}, "fullchain"))
assert.Error(t, runTestTLSClient("example.com"))
assert.NoError(t, runTestTLSClient("2.example.com"))
}
func generateTestCertificate(dnssan []string, certType string) error {
args := []string{
"certloader_test_gencert.py",
"--ca", testCAFile,
"--cert", testCertFile,
"--key", testKeyFile,
"--type", certType,
}
if len(dnssan) > 0 {
args = append(args, "--dnssan", strings.Join(dnssan, ","))
}
cmd := exec.Command("python", args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Failed to generate test certificate: %s", out)
return err
}
return nil
}
func runTestTLSClient(sni string) error {
args := []string{
"certloader_test_tlsclient.py",
"--server", testListen,
"--ca", testCAFile,
}
if sni != "" {
args = append(args, "--sni", sni)
}
cmd := exec.Command("python", args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Failed to run test TLS client: %s", out)
return err
}
return nil
}
================================================
FILE: app/internal/utils/certloader_test_gencert.py
================================================
import argparse
import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
def create_key():
return ec.generate_private_key(ec.SECP256R1())
def create_certificate(cert_type, subject, issuer, private_key, public_key, dns_san=None):
serial_number = x509.random_serial_number()
not_valid_before = datetime.datetime.now(datetime.UTC)
not_valid_after = not_valid_before + datetime.timedelta(days=365)
subject_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, subject.get('C', 'ZZ')),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject.get('O', 'No Organization')),
x509.NameAttribute(NameOID.COMMON_NAME, subject.get('CN', 'No CommonName')),
])
issuer_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, issuer.get('C', 'ZZ')),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, issuer.get('O', 'No Organization')),
x509.NameAttribute(NameOID.COMMON_NAME, issuer.get('CN', 'No CommonName')),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject_name)
builder = builder.issuer_name(issuer_name)
builder = builder.public_key(public_key)
builder = builder.serial_number(serial_number)
builder = builder.not_valid_before(not_valid_before)
builder = builder.not_valid_after(not_valid_after)
if cert_type == 'root':
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None), critical=True
)
elif cert_type == 'intermediate':
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=0), critical=True
)
elif cert_type == 'leaf':
builder = builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None), critical=True
)
else:
raise ValueError(f'Invalid cert_type: {cert_type}')
if dns_san:
builder = builder.add_extension(
x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_san.split(',')]),
critical=False
)
return builder.sign(private_key=private_key, algorithm=hashes.SHA256())
def main():
parser = argparse.ArgumentParser(description='Generate HTTPS server certificate.')
parser.add_argument('--ca', required=True,
help='Path to write the X509 CA certificate in PEM format')
parser.add_argument('--cert', required=True,
help='Path to write the X509 certificate in PEM format')
parser.add_argument('--key', required=True,
help='Path to write the private key in PEM format')
parser.add_argument('--dnssan', required=False, default=None,
help='Comma-separated list of DNS SANs')
parser.add_argument('--type', required=True, choices=['selfsign', 'fullchain'],
help='Type of certificate to generate')
args = parser.parse_args()
key = create_key()
public_key = key.public_key()
if args.type == 'selfsign':
subject = {"C": "ZZ", "O": "Certificate", "CN": "Certificate"}
cert = create_certificate(
cert_type='root',
subject=subject,
issuer=subject,
private_key=key,
public_key=public_key,
dns_san=args.dnssan)
with open(args.ca, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
with open(args.cert, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
with open(args.key, 'wb') as f:
f.write(
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
elif args.type == 'fullchain':
ca_key = create_key()
ca_public_key = ca_key.public_key()
ca_subject = {"C": "ZZ", "O": "Root CA", "CN": "Root CA"}
ca_cert = create_certificate(
cert_type='root',
subject=ca_subject,
issuer=ca_subject,
private_key=ca_key,
public_key=ca_public_key)
intermediate_key = create_key()
intermediate_public_key = intermediate_key.public_key()
intermediate_subject = {"C": "ZZ", "O": "Intermediate CA", "CN": "Intermediate CA"}
intermediate_cert = create_certificate(
cert_type='intermediate',
subject=intermediate_subject,
issuer=ca_subject,
private_key=ca_key,
public_key=intermediate_public_key)
leaf_subject = {"C": "ZZ", "O": "Leaf Certificate", "CN": "Leaf Certificate"}
cert = create_certificate(
cert_type='leaf',
subject=leaf_subject,
issuer=intermediate_subject,
private_key=intermediate_key,
public_key=public_key,
dns_san=args.dnssan)
with open(args.ca, 'wb') as f:
f.write(ca_cert.public_bytes(Encoding.PEM))
with open(args.cert, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
f.write(intermediate_cert.public_bytes(Encoding.PEM))
with open(args.key, 'wb') as f:
f.write(
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
if __name__ == "__main__":
main()
================================================
FILE: app/internal/utils/certloader_test_tlsclient.py
================================================
import argparse
import ssl
import socket
import sys
def check_tls(server, ca_cert, sni, alpn):
try:
host, port = server.split(":")
port = int(port)
if ca_cert:
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_cert)
context.check_hostname = sni is not None
context.verify_mode = ssl.CERT_REQUIRED
else:
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
if alpn:
context.set_alpn_protocols([p for p in alpn.split(",")])
with socket.create_connection((host, port)) as sock:
with context.wrap_socket(sock, server_hostname=sni) as ssock:
# Verify handshake and certificate
print(f'Connected to {ssock.version()} using {ssock.cipher()}')
print(f'Server certificate validated and details: {ssock.getpeercert()}')
print("OK")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
def main():
parser = argparse.ArgumentParser(description="Test TLS Server")
parser.add_argument("--server", required=True,
help="Server address to test (e.g., 127.1.2.3:8443)")
parser.add_argument("--ca", required=False, default=None,
help="CA certificate file used to validate the server certificate"
"Omit to use insecure connection")
parser.add_argument("--sni", required=False, default=None,
help="SNI to send in ClientHello")
parser.add_argument("--alpn", required=False, default='h2',
help="ALPN to send in ClientHello")
args = parser.parse_args()
exit_status = check_tls(
server=args.server,
ca_cert=args.ca,
sni=args.sni,
alpn=args.alpn)
sys.exit(exit_status)
if __name__ == "__main__":
main()
================================================
FILE: app/internal/utils/geoloader.go
================================================
package utils
import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/apernet/hysteria/extras/v2/outbounds/acl"
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
)
const (
geoipFilename = "geoip.dat"
geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
geositeFilename = "geosite.dat"
geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
geoDlTmpPattern = ".hysteria-geoloader.dlpart.*"
geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days
)
var _ acl.GeoLoader = (*GeoLoader)(nil)
// GeoLoader provides the on-demand GeoIP/GeoSite database
// loading functionality required by the ACL engine.
// Empty filenames = automatic download from built-in URLs.
type GeoLoader struct {
GeoIPFilename string
GeoSiteFilename string
UpdateInterval time.Duration
DownloadFunc func(filename, url string)
DownloadErrFunc func(err error)
geoipMap map[string]*v2geo.GeoIP
geositeMap map[string]*v2geo.GeoSite
}
func (l *GeoLoader) shouldDownload(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return true
}
if info.Size() == 0 {
// empty files are loadable by v2geo, but we consider it broken
return true
}
dt := time.Now().Sub(info.ModTime())
if l.UpdateInterval == 0 {
return dt > geoDefaultUpdateInterval
} else {
return dt > l.UpdateInterval
}
}
func (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error {
l.DownloadFunc(filename, url)
resp, err := http.Get(url)
if err != nil {
l.DownloadErrFunc(err)
return err
}
defer resp.Body.Close()
f, err := os.CreateTemp(".", geoDlTmpPattern)
if err != nil {
l.DownloadErrFunc(err)
return err
}
defer os.Remove(f.Name())
_, err = io.Copy(f, resp.Body)
if err != nil {
f.Close()
l.DownloadErrFunc(err)
return err
}
f.Close()
err = checkFunc(f.Name())
if err != nil {
l.DownloadErrFunc(fmt.Errorf("integrity check failed: %w", err))
return err
}
err = os.Rename(f.Name(), filename)
if err != nil {
l.DownloadErrFunc(fmt.Errorf("rename failed: %w", err))
return err
}
return nil
}
func (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
if l.geoipMap != nil {
return l.geoipMap, nil
}
autoDL := false
filename := l.GeoIPFilename
if filename == "" {
autoDL = true
filename = geoipFilename
}
if autoDL {
if !l.shouldDownload(filename) {
m, err := v2geo.LoadGeoIP(filename)
if err == nil {
l.geoipMap = m
return m, nil
}
// file is broken, download it again
}
err := l.downloadAndCheck(filename, geoipURL, func(filename string) error {
_, err := v2geo.LoadGeoIP(filename)
return err
})
if err != nil {
// as long as the previous download exists, fallback to it
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
return nil, err
}
}
}
m, err := v2geo.LoadGeoIP(filename)
if err != nil {
return nil, err
}
l.geoipMap = m
return m, nil
}
func (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
if l.geositeMap != nil {
return l.geositeMap, nil
}
autoDL := false
filename := l.GeoSiteFilename
if filename == "" {
autoDL = true
filename = geositeFilename
}
if autoDL {
if !l.shouldDownload(filename) {
m, err := v2geo.LoadGeoSite(filename)
if err == nil {
l.geositeMap = m
return m, nil
}
// file is broken, download it again
}
err := l.downloadAndCheck(filename, geositeURL, func(filename string) error {
_, err := v2geo.LoadGeoSite(filename)
return err
})
if err != nil {
// as long as the previous download exists, fallback to it
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
return nil, err
}
}
}
m, err := v2geo.LoadGeoSite(filename)
if err != nil {
return nil, err
}
l.geositeMap = m
return m, nil
}
================================================
FILE: app/internal/utils/qr.go
================================================
package utils
import (
"os"
"github.com/mdp/qrterminal/v3"
)
func PrintQR(str string) {
qrterminal.GenerateWithConfig(str, qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
BlackChar: qrterminal.BLACK,
WhiteChar: qrterminal.WHITE,
})
}
================================================
FILE: app/internal/utils/testcerts/.gitignore
================================================
# This directory is used for certificate generation in certloader_test.go
/*
!/.gitignore
================================================
FILE: app/internal/utils/update.go
================================================
package utils
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"time"
"github.com/apernet/hysteria/core/v2/client"
)
const (
updateCheckEndpoint = "https://api.hy2.io/v1/update"
updateCheckTimeout = 10 * time.Second
)
type UpdateChecker struct {
CurrentVersion string
Platform string
Architecture string
Channel string
Side string
Client *http.Client
}
func NewServerUpdateChecker(currentVersion, platform, architecture, channel string) *UpdateChecker {
return &UpdateChecker{
CurrentVersion: currentVersion,
Platform: platform,
Architecture: architecture,
Channel: channel,
Side: "server",
Client: &http.Client{
Timeout: updateCheckTimeout,
},
}
}
// NewClientUpdateChecker ensures that update checks are routed through a HyClient,
// not being sent directly. This safeguard is CRITICAL, especially in scenarios where
// users use Hysteria to bypass censorship. Making direct HTTPS requests to the API
// endpoint could be easily spotted by censors (through SNI, for example), and could
// serve as a signal to identify and penalize Hysteria users.
func NewClientUpdateChecker(currentVersion, platform, architecture, channel string, hyClient client.Client) *UpdateChecker {
return &UpdateChecker{
CurrentVersion: currentVersion,
Platform: platform,
Architecture: architecture,
Channel: channel,
Side: "client",
Client: &http.Client{
Timeout: updateCheckTimeout,
Transport: &http.Transport{
DialContext: func(_ context.Context, network, addr string) (net.Conn, error) {
// Unfortunately HyClient doesn't support context for now
return hyClient.TCP(addr)
},
},
},
}
}
type UpdateResponse struct {
HasUpdate bool `json:"update"`
LatestVersion string `json:"lver"`
URL string `json:"url"`
Urgent bool `json:"urgent"`
}
func (uc *UpdateChecker) Check() (*UpdateResponse, error) {
url := fmt.Sprintf("%s?cver=%s&plat=%s&arch=%s&chan=%s&side=%s",
updateCheckEndpoint,
uc.CurrentVersion,
uc.Platform,
uc.Architecture,
uc.Channel,
uc.Side,
)
resp, err := uc.Client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var uResp UpdateResponse
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&uResp); err != nil {
return nil, err
}
return &uResp, nil
}
================================================
FILE: app/internal/utils_test/mock.go
================================================
package utils_test
import (
"io"
"net"
"time"
"github.com/apernet/hysteria/core/v2/client"
)
type MockEchoHyClient struct{}
func (c *MockEchoHyClient) TCP(addr string) (net.Conn, error) {
return &mockEchoTCPConn{
BufChan: make(chan []byte, 10),
}, nil
}
func (c *MockEchoHyClient) UDP() (client.HyUDPConn, error) {
return &mockEchoUDPConn{
BufChan: make(chan mockEchoUDPPacket, 10),
}, nil
}
func (c *MockEchoHyClient) Close() error {
return nil
}
type mockEchoTCPConn struct {
BufChan chan []byte
}
func (c *mockEchoTCPConn) Read(b []byte) (n int, err error) {
buf := <-c.BufChan
if buf == nil {
// EOF
return 0, io.EOF
}
return copy(b, buf), nil
}
func (c *mockEchoTCPConn) Write(b []byte) (n int, err error) {
c.BufChan <- b
return len(b), nil
}
func (c *mockEchoTCPConn) Close() error {
close(c.BufChan)
return nil
}
func (c *mockEchoTCPConn) LocalAddr() net.Addr {
// Not implemented
return nil
}
func (c *mockEchoTCPConn) RemoteAddr() net.Addr {
// Not implemented
return nil
}
func (c *mockEchoTCPConn) SetDeadline(t time.Time) error {
// Not implemented
return nil
}
func (c *mockEchoTCPConn) SetReadDeadline(t time.Time) error {
// Not implemented
return nil
}
func (c *mockEchoTCPConn) SetWriteDeadline(t time.Time) error {
// Not implemented
return nil
}
type mockEchoUDPPacket struct {
Data []byte
Addr string
}
type mockEchoUDPConn struct {
BufChan chan mockEchoUDPPacket
}
func (c *mockEchoUDPConn) Receive() ([]byte, string, error) {
p := <-c.BufChan
if p.Data == nil {
// EOF
return nil, "", io.EOF
}
return p.Data, p.Addr, nil
}
func (c *mockEchoUDPConn) Send(bytes []byte, s string) error {
c.BufChan <- mockEchoUDPPacket{
Data: bytes,
Addr: s,
}
return nil
}
func (c *mockEchoUDPConn) Close() error {
close(c.BufChan)
return nil
}
================================================
FILE: app/main.go
================================================
package main
import "github.com/apernet/hysteria/app/v2/cmd"
func main() {
cmd.Execute()
}
================================================
FILE: app/misc/socks5_test.py
================================================
import socket
import socks
import time
TARGET = "1.1.1.1"
def test_tcp() -> None:
s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080)
print(f"TCP - Sending HTTP request to {TARGET}")
start = time.time()
s.connect((TARGET, 80))
s.send(b"GET / HTTP/1.1\r\nHost: " + TARGET.encode() + b"\r\n\r\n")
data = s.recv(1024)
if not data:
print("No data received")
elif not data.startswith(b"HTTP/1.1 "):
print("Invalid response received")
else:
print("TCP test passed")
end = time.time()
s.close()
print(f"Time: {round((end - start) * 1000, 2)} ms")
def test_udp() -> None:
s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)
s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080)
req = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05\x62\x61\x69\x64\x75\x03\x63\x6f\x6d\x00\x00\x01\x00\x01"
print(f"UDP - Sending DNS request to {TARGET}")
start = time.time()
s.sendto(req, (TARGET, 53))
(rsp, address) = s.recvfrom(4096)
if address[0] == TARGET and address[1] == 53 and rsp[0] == req[0] and rsp[1] == req[1]:
print("UDP test passed")
else:
print("Invalid response received")
end = time.time()
s.close()
print(f"Time: {round((end - start) * 1000, 2)} ms")
if __name__ == "__main__":
test_tcp()
test_udp()
================================================
FILE: app/pprof.go
================================================
//go:build pprof
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
const (
pprofListenAddr = ":6060"
)
func init() {
fmt.Printf("!!! pprof enabled, listening on %s\n", pprofListenAddr)
go func() {
if err := http.ListenAndServe(pprofListenAddr, nil); err != nil {
panic(err)
}
}()
}
================================================
FILE: core/LICENSE.md
================================================
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: core/client/.mockery.yaml
================================================
with-expecter: true
inpackage: true
dir: .
packages:
github.com/apernet/hysteria/core/v2/client:
interfaces:
udpIO:
config:
mockname: mockUDPIO
================================================
FILE: core/client/client.go
================================================
package client
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"net/url"
"time"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/congestion"
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/v2/internal/utils"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
)
const (
closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError
closeErrCodeProtocolError = 0x101 // HTTP3 ErrCodeGeneralProtocolError
)
type Client interface {
TCP(addr string) (net.Conn, error)
UDP() (HyUDPConn, error)
Close() error
}
type HyUDPConn interface {
Receive() ([]byte, string, error)
Send([]byte, string) error
Close() error
}
type HandshakeInfo struct {
UDPEnabled bool
Tx uint64 // 0 if using BBR
}
func NewClient(config *Config) (Client, *HandshakeInfo, error) {
if err := config.verifyAndFill(); err != nil {
return nil, nil, err
}
c := &clientImpl{
config: config,
}
info, err := c.connect()
if err != nil {
return nil, nil, err
}
return c, info, nil
}
type clientImpl struct {
config *Config
pktConn net.PacketConn
conn *quic.Conn
udpSM *udpSessionManager
}
func (c *clientImpl) connect() (*HandshakeInfo, error) {
pktConn, err := c.config.ConnFactory.New(c.config.ServerAddr)
if err != nil {
return nil, err
}
// Convert config to TLS config & QUIC config
tlsConfig := &tls.Config{
ServerName: c.config.TLSConfig.ServerName,
InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify,
VerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate,
RootCAs: c.config.TLSConfig.RootCAs,
GetClientCertificate: c.config.TLSConfig.GetClientCertificate,
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow,
MaxStreamReceiveWindow: c.config.QUICConfig.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: c.config.QUICConfig.InitialConnectionReceiveWindow,
MaxConnectionReceiveWindow: c.config.QUICConfig.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.config.QUICConfig.MaxIdleTimeout,
KeepAlivePeriod: c.config.QUICConfig.KeepAlivePeriod,
DisablePathMTUDiscovery: c.config.QUICConfig.DisablePathMTUDiscovery,
EnableDatagrams: true,
MaxDatagramFrameSize: protocol.MaxDatagramFrameSize,
DisablePathManager: true,
}
// Prepare RoundTripper
var conn *quic.Conn
rt := &http3.Transport{
TLSClientConfig: tlsConfig,
QUICConfig: quicConfig,
Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
qc, err := quic.DialEarly(ctx, pktConn, c.config.ServerAddr, tlsCfg, cfg)
if err != nil {
return nil, err
}
conn = qc
return qc, nil
},
}
// Send auth HTTP request
req := &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "https",
Host: protocol.URLHost,
Path: protocol.URLPath,
},
Header: make(http.Header),
}
protocol.AuthRequestToHeader(req.Header, protocol.AuthRequest{
Auth: c.config.Auth,
Rx: c.config.BandwidthConfig.MaxRx,
})
resp, err := rt.RoundTrip(req)
if err != nil {
if conn != nil {
_ = conn.CloseWithError(closeErrCodeProtocolError, "")
}
_ = pktConn.Close()
return nil, coreErrs.ConnectError{Err: err}
}
if resp.StatusCode != protocol.StatusAuthOK {
_ = conn.CloseWithError(closeErrCodeProtocolError, "")
_ = pktConn.Close()
return nil, coreErrs.AuthError{StatusCode: resp.StatusCode}
}
// Auth OK
authResp := protocol.AuthResponseFromHeader(resp.Header)
var actualTx uint64
if authResp.RxAuto {
// Server asks client to use bandwidth detection,
// ignore local bandwidth config and use BBR
congestion.UseBBR(conn)
} else {
// actualTx = min(serverRx, clientTx)
actualTx = authResp.Rx
if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx {
// Server doesn't have a limit, or our clientTx is smaller than serverRx
actualTx = c.config.BandwidthConfig.MaxTx
}
if actualTx > 0 {
congestion.UseBrutal(conn, actualTx)
} else {
// We don't know our own bandwidth either, use BBR
congestion.UseBBR(conn)
}
}
_ = resp.Body.Close()
c.pktConn = pktConn
c.conn = conn
if authResp.UDPEnabled {
c.udpSM = newUDPSessionManager(&udpIOImpl{Conn: conn})
}
return &HandshakeInfo{
UDPEnabled: authResp.UDPEnabled,
Tx: actualTx,
}, nil
}
// openStream wraps the stream with QStream, which handles Close() properly
func (c *clientImpl) openStream() (*utils.QStream, error) {
stream, err := c.conn.OpenStream()
if err != nil {
return nil, err
}
return &utils.QStream{Stream: stream}, nil
}
func (c *clientImpl) TCP(addr string) (net.Conn, error) {
stream, err := c.openStream()
if err != nil {
return nil, wrapIfConnectionClosed(err)
}
// Send request
err = protocol.WriteTCPRequest(stream, addr)
if err != nil {
_ = stream.Close()
return nil, wrapIfConnectionClosed(err)
}
if c.config.FastOpen {
// Don't wait for the response when fast open is enabled.
// Return the connection immediately, defer the response handling
// to the first Read() call.
return &tcpConn{
Orig: stream,
PseudoLocalAddr: c.conn.LocalAddr(),
PseudoRemoteAddr: c.conn.RemoteAddr(),
Established: false,
}, nil
}
// Read response
ok, msg, err := protocol.ReadTCPResponse(stream)
if err != nil {
_ = stream.Close()
return nil, wrapIfConnectionClosed(err)
}
if !ok {
_ = stream.Close()
return nil, coreErrs.DialError{Message: msg}
}
return &tcpConn{
Orig: stream,
PseudoLocalAddr: c.conn.LocalAddr(),
PseudoRemoteAddr: c.conn.RemoteAddr(),
Established: true,
}, nil
}
func (c *clientImpl) UDP() (HyUDPConn, error) {
if c.udpSM == nil {
return nil, coreErrs.DialError{Message: "UDP not enabled"}
}
return c.udpSM.NewUDP()
}
func (c *clientImpl) Close() error {
_ = c.conn.CloseWithError(closeErrCodeOK, "")
_ = c.pktConn.Close()
return nil
}
var nonPermanentErrors = []error{
quic.StreamLimitReachedError{},
}
// wrapIfConnectionClosed checks if the error returned by quic-go
// is recoverable (listed in nonPermanentErrors) or permanent.
// Recoverable errors are returned as-is,
// permanent ones are wrapped as ClosedError.
func wrapIfConnectionClosed(err error) error {
for _, e := range nonPermanentErrors {
if errors.Is(err, e) {
return err
}
}
return coreErrs.ClosedError{Err: err}
}
type tcpConn struct {
Orig *utils.QStream
PseudoLocalAddr net.Addr
PseudoRemoteAddr net.Addr
Established bool
}
func (c *tcpConn) Read(b []byte) (n int, err error) {
if !c.Established {
// Read response
ok, msg, err := protocol.ReadTCPResponse(c.Orig)
if err != nil {
return 0, err
}
if !ok {
return 0, coreErrs.DialError{Message: msg}
}
c.Established = true
}
return c.Orig.Read(b)
}
func (c *tcpConn) Write(b []byte) (n int, err error) {
return c.Orig.Write(b)
}
func (c *tcpConn) Close() error {
return c.Orig.Close()
}
func (c *tcpConn) LocalAddr() net.Addr {
return c.PseudoLocalAddr
}
func (c *tcpConn) RemoteAddr() net.Addr {
return c.PseudoRemoteAddr
}
func (c *tcpConn) SetDeadline(t time.Time) error {
return c.Orig.SetDeadline(t)
}
func (c *tcpConn) SetReadDeadline(t time.Time) error {
return c.Orig.SetReadDeadline(t)
}
func (c *tcpConn) SetWriteDeadline(t time.Time) error {
return c.Orig.SetWriteDeadline(t)
}
type udpIOImpl struct {
Conn *quic.Conn
}
func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {
for {
msg, err := io.Conn.ReceiveDatagram(context.Background())
if err != nil {
// Connection error, this will stop the session manager
return nil, err
}
udpMsg, err := protocol.ParseUDPMessage(msg)
if err != nil {
// Invalid message, this is fine - just wait for the next
continue
}
return udpMsg, nil
}
}
func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {
msgN := msg.Serialize(buf)
if msgN < 0 {
// Message larger than buffer, silent drop
return nil
}
return io.Conn.SendDatagram(buf[:msgN])
}
================================================
FILE: core/client/config.go
================================================
package client
import (
"crypto/tls"
"crypto/x509"
"net"
"time"
"github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/pmtud"
)
const (
defaultStreamReceiveWindow = 8388608 // 8MB
defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB
defaultMaxIdleTimeout = 30 * time.Second
defaultKeepAlivePeriod = 10 * time.Second
)
type Config struct {
ConnFactory ConnFactory
ServerAddr net.Addr
Auth string
TLSConfig TLSConfig
QUICConfig QUICConfig
BandwidthConfig BandwidthConfig
FastOpen bool
filled bool // whether the fields have been verified and filled
}
// verifyAndFill fills the fields that are not set by the user with default values when possible,
// and returns an error if the user has not set a required field or has set an invalid value.
func (c *Config) verifyAndFill() error {
if c.filled {
return nil
}
if c.ConnFactory == nil {
c.ConnFactory = &udpConnFactory{}
}
if c.ServerAddr == nil {
return errors.ConfigError{Field: "ServerAddr", Reason: "must be set"}
}
if c.QUICConfig.InitialStreamReceiveWindow == 0 {
c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow
} else if c.QUICConfig.InitialStreamReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.MaxStreamReceiveWindow == 0 {
c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow
} else if c.QUICConfig.MaxStreamReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.InitialConnectionReceiveWindow == 0 {
c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow
} else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.MaxConnectionReceiveWindow == 0 {
c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow
} else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.MaxIdleTimeout == 0 {
c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout
} else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second {
return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"}
}
if c.QUICConfig.KeepAlivePeriod == 0 {
c.QUICConfig.KeepAlivePeriod = defaultKeepAlivePeriod
} else if c.QUICConfig.KeepAlivePeriod < 2*time.Second || c.QUICConfig.KeepAlivePeriod > 60*time.Second {
return errors.ConfigError{Field: "QUICConfig.KeepAlivePeriod", Reason: "must be between 2s and 60s"}
}
c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery
c.filled = true
return nil
}
type ConnFactory interface {
New(net.Addr) (net.PacketConn, error)
}
type udpConnFactory struct{}
func (f *udpConnFactory) New(addr net.Addr) (net.PacketConn, error) {
return net.ListenUDP("udp", nil)
}
// TLSConfig contains the TLS configuration fields that we want to expose to the user.
type TLSConfig struct {
ServerName string
InsecureSkipVerify bool
VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
RootCAs *x509.CertPool
GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
}
// QUICConfig contains the QUIC configuration fields that we want to expose to the user.
type QUICConfig struct {
InitialStreamReceiveWindow uint64
MaxStreamReceiveWindow uint64
InitialConnectionReceiveWindow uint64
MaxConnectionReceiveWindow uint64
MaxIdleTimeout time.Duration
KeepAlivePeriod time.Duration
DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms.
}
// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second.
type BandwidthConfig struct {
MaxTx uint64
MaxRx uint64
}
================================================
FILE: core/client/mock_udpIO.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package client
import (
protocol "github.com/apernet/hysteria/core/v2/internal/protocol"
mock "github.com/stretchr/testify/mock"
)
// mockUDPIO is an autogenerated mock type for the udpIO type
type mockUDPIO struct {
mock.Mock
}
type mockUDPIO_Expecter struct {
mock *mock.Mock
}
func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter {
return &mockUDPIO_Expecter{mock: &_m.Mock}
}
// ReceiveMessage provides a mock function with no fields
func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ReceiveMessage")
}
var r0 *protocol.UDPMessage
var r1 error
if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*protocol.UDPMessage)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage'
type mockUDPIO_ReceiveMessage_Call struct {
*mock.Call
}
// ReceiveMessage is a helper method to define mock.On call
func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call {
return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")}
}
func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call {
_c.Call.Return(run)
return _c
}
// SendMessage provides a mock function with given fields: _a0, _a1
func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for SendMessage")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}
// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage'
type mockUDPIO_SendMessage_Call struct {
*mock.Call
}
// SendMessage is a helper method to define mock.On call
// - _a0 []byte
// - _a1 *protocol.UDPMessage
func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call {
return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)}
}
func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(*protocol.UDPMessage))
})
return _c
}
func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call {
_c.Call.Return(run)
return _c
}
// newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func newMockUDPIO(t interface {
mock.TestingT
Cleanup(func())
}) *mockUDPIO {
mock := &mockUDPIO{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/client/reconnect.go
================================================
package client
import (
"net"
"sync"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
)
// reconnectableClientImpl is a wrapper of Client, which can reconnect when the connection is closed,
// except when the caller explicitly calls Close() to permanently close this client.
type reconnectableClientImpl struct {
configFunc func() (*Config, error) // called before connecting
connectedFunc func(Client, *HandshakeInfo, int) // called when successfully connected
client Client
count int
m sync.Mutex
closed bool // permanent close
}
// NewReconnectableClient creates a reconnectable client.
// If lazy is true, the client will not connect until the first call to TCP() or UDP().
// We use a function for config mainly to delay config evaluation
// (which involves DNS resolution) until the actual connection attempt.
func NewReconnectableClient(configFunc func() (*Config, error), connectedFunc func(Client, *HandshakeInfo, int), lazy bool) (Client, error) {
rc := &reconnectableClientImpl{
configFunc: configFunc,
connectedFunc: connectedFunc,
}
if !lazy {
if err := rc.reconnect(); err != nil {
return nil, err
}
}
return rc, nil
}
func (rc *reconnectableClientImpl) reconnect() error {
if rc.client != nil {
_ = rc.client.Close()
}
var info *HandshakeInfo
config, err := rc.configFunc()
if err != nil {
return err
}
rc.client, info, err = NewClient(config)
if err != nil {
return err
} else {
rc.count++
if rc.connectedFunc != nil {
rc.connectedFunc(rc, info, rc.count)
}
return nil
}
}
// clientDo calls f with the current client.
// If the client is nil, it will first reconnect.
// It will also detect if the client is closed, and if so,
// set it to nil for reconnect next time.
func (rc *reconnectableClientImpl) clientDo(f func(Client) (interface{}, error)) (interface{}, error) {
rc.m.Lock()
if rc.closed {
rc.m.Unlock()
return nil, coreErrs.ClosedError{}
}
if rc.client == nil {
// No active connection, connect first
if err := rc.reconnect(); err != nil {
rc.m.Unlock()
return nil, err
}
}
client := rc.client
rc.m.Unlock()
ret, err := f(client)
if _, ok := err.(coreErrs.ClosedError); ok {
// Connection closed, set client to nil for reconnect next time
rc.m.Lock()
if rc.client == client {
// This check is in case the client is already changed by another goroutine
rc.client = nil
}
rc.m.Unlock()
}
return ret, err
}
func (rc *reconnectableClientImpl) TCP(addr string) (net.Conn, error) {
if c, err := rc.clientDo(func(client Client) (interface{}, error) {
return client.TCP(addr)
}); err != nil {
return nil, err
} else {
return c.(net.Conn), nil
}
}
func (rc *reconnectableClientImpl) UDP() (HyUDPConn, error) {
if c, err := rc.clientDo(func(client Client) (interface{}, error) {
return client.UDP()
}); err != nil {
return nil, err
} else {
return c.(HyUDPConn), nil
}
}
func (rc *reconnectableClientImpl) Close() error {
rc.m.Lock()
defer rc.m.Unlock()
rc.closed = true
if rc.client != nil {
return rc.client.Close()
}
return nil
}
================================================
FILE: core/client/udp.go
================================================
package client
import (
"errors"
"io"
"math/rand"
"sync"
"github.com/apernet/quic-go"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/frag"
"github.com/apernet/hysteria/core/v2/internal/protocol"
)
const (
udpMessageChanSize = 1024
)
type udpIO interface {
ReceiveMessage() (*protocol.UDPMessage, error)
SendMessage([]byte, *protocol.UDPMessage) error
}
type udpConn struct {
ID uint32
D *frag.Defragger
ReceiveCh chan *protocol.UDPMessage
SendBuf []byte
SendFunc func([]byte, *protocol.UDPMessage) error
CloseFunc func()
Closed bool
}
func (u *udpConn) Receive() ([]byte, string, error) {
for {
msg := <-u.ReceiveCh
if msg == nil {
// Closed
return nil, "", io.EOF
}
dfMsg := u.D.Feed(msg)
if dfMsg == nil {
// Incomplete message, wait for more
continue
}
return dfMsg.Data, dfMsg.Addr, nil
}
}
// Send is not thread-safe, as it uses a shared SendBuf.
func (u *udpConn) Send(data []byte, addr string) error {
// Try no frag first
msg := &protocol.UDPMessage{
SessionID: u.ID,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: addr,
Data: data,
}
err := u.SendFunc(u.SendBuf, msg)
var errTooLarge *quic.DatagramTooLargeError
if errors.As(err, &errTooLarge) {
// Message too large, try fragmentation
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
for _, fMsg := range fMsgs {
err := u.SendFunc(u.SendBuf, &fMsg)
if err != nil {
return err
}
}
return nil
} else {
return err
}
}
func (u *udpConn) Close() error {
u.CloseFunc()
return nil
}
type udpSessionManager struct {
io udpIO
mutex sync.RWMutex
m map[uint32]*udpConn
nextID uint32
closed bool
}
func newUDPSessionManager(io udpIO) *udpSessionManager {
m := &udpSessionManager{
io: io,
m: make(map[uint32]*udpConn),
nextID: 1,
}
go m.run()
return m
}
func (m *udpSessionManager) run() error {
defer m.closeCleanup()
for {
msg, err := m.io.ReceiveMessage()
if err != nil {
return err
}
m.feed(msg)
}
}
func (m *udpSessionManager) closeCleanup() {
m.mutex.Lock()
defer m.mutex.Unlock()
for _, conn := range m.m {
m.close(conn)
}
m.closed = true
}
func (m *udpSessionManager) feed(msg *protocol.UDPMessage) {
m.mutex.RLock()
defer m.mutex.RUnlock()
conn, ok := m.m[msg.SessionID]
if !ok {
// Ignore message from unknown session
return
}
select {
case conn.ReceiveCh <- msg:
// OK
default:
// Channel full, drop the message
}
}
// NewUDP creates a new UDP session.
func (m *udpSessionManager) NewUDP() (HyUDPConn, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
if m.closed {
return nil, coreErrs.ClosedError{}
}
id := m.nextID
m.nextID++
conn := &udpConn{
ID: id,
D: &frag.Defragger{},
ReceiveCh: make(chan *protocol.UDPMessage, udpMessageChanSize),
SendBuf: make([]byte, protocol.MaxUDPSize),
SendFunc: m.io.SendMessage,
}
conn.CloseFunc = func() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.close(conn)
}
m.m[id] = conn
return conn, nil
}
func (m *udpSessionManager) close(conn *udpConn) {
if !conn.Closed {
conn.Closed = true
close(conn.ReceiveCh)
delete(m.m, conn.ID)
}
}
func (m *udpSessionManager) Count() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.m)
}
================================================
FILE: core/client/udp_test.go
================================================
package client
import (
"errors"
io2 "io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.uber.org/goleak"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/protocol"
)
func TestUDPSessionManager(t *testing.T) {
io := newMockUDPIO(t)
receiveCh := make(chan *protocol.UDPMessage, 4)
io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) {
m := <-receiveCh
if m == nil {
return nil, errors.New("closed")
}
return m, nil
})
sm := newUDPSessionManager(io)
// Test UDP session IO
udpConn1, err := sm.NewUDP()
assert.NoError(t, err)
udpConn2, err := sm.NewUDP()
assert.NoError(t, err)
msg1 := &protocol.UDPMessage{
SessionID: 1,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: "random.site.com:9000",
Data: []byte("hello friend"),
}
io.EXPECT().SendMessage(mock.Anything, msg1).Return(nil).Once()
err = udpConn1.Send(msg1.Data, msg1.Addr)
assert.NoError(t, err)
msg2 := &protocol.UDPMessage{
SessionID: 2,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: "another.site.org:8000",
Data: []byte("mr robot"),
}
io.EXPECT().SendMessage(mock.Anything, msg2).Return(nil).Once()
err = udpConn2.Send(msg2.Data, msg2.Addr)
assert.NoError(t, err)
respMsg1 := &protocol.UDPMessage{
SessionID: 1,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: msg1.Addr,
Data: []byte("goodbye captain price"),
}
receiveCh <- respMsg1
data, addr, err := udpConn1.Receive()
assert.NoError(t, err)
assert.Equal(t, data, respMsg1.Data)
assert.Equal(t, addr, respMsg1.Addr)
respMsg2 := &protocol.UDPMessage{
SessionID: 2,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: msg2.Addr,
Data: []byte("white rose"),
}
receiveCh <- respMsg2
data, addr, err = udpConn2.Receive()
assert.NoError(t, err)
assert.Equal(t, data, respMsg2.Data)
assert.Equal(t, addr, respMsg2.Addr)
respMsg3 := &protocol.UDPMessage{
SessionID: 55, // Bogus session ID that doesn't exist
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: "burgerking.com:27017",
Data: []byte("impossible whopper"),
}
receiveCh <- respMsg3
// No test for this, just make sure it doesn't panic
// Test close UDP connection unblocks Receive()
errChan := make(chan error, 1)
go func() {
_, _, err := udpConn1.Receive()
errChan <- err
}()
assert.NoError(t, udpConn1.Close())
assert.Equal(t, <-errChan, io2.EOF)
// Test close IO unblocks Receive() and blocks new UDP creation
errChan = make(chan error, 1)
go func() {
_, _, err := udpConn2.Receive()
errChan <- err
}()
close(receiveCh)
assert.Equal(t, <-errChan, io2.EOF)
_, err = sm.NewUDP()
assert.Equal(t, err, coreErrs.ClosedError{})
// Leak checks
time.Sleep(1 * time.Second)
assert.Zero(t, sm.Count(), "session count should be 0")
goleak.VerifyNone(t)
}
================================================
FILE: core/errors/errors.go
================================================
package errors
import (
"fmt"
"strconv"
)
// ConfigError is returned when a configuration field is invalid.
type ConfigError struct {
Field string
Reason string
}
func (c ConfigError) Error() string {
return fmt.Sprintf("invalid config: %s: %s", c.Field, c.Reason)
}
// ConnectError is returned when the client fails to connect to the server.
type ConnectError struct {
Err error
}
func (c ConnectError) Error() string {
return "connect error: " + c.Err.Error()
}
func (c ConnectError) Unwrap() error {
return c.Err
}
// AuthError is returned when the client fails to authenticate with the server.
type AuthError struct {
StatusCode int
}
func (a AuthError) Error() string {
return "authentication error, HTTP status code: " + strconv.Itoa(a.StatusCode)
}
// DialError is returned when the server rejects the client's dial request.
// This applies to both TCP and UDP.
type DialError struct {
Message string
}
func (c DialError) Error() string {
return "dial error: " + c.Message
}
// ClosedError is returned when the client attempts to use a closed connection.
type ClosedError struct {
Err error // Can be nil
}
func (c ClosedError) Error() string {
if c.Err == nil {
return "connection closed"
} else {
return "connection closed: " + c.Err.Error()
}
}
func (c ClosedError) Unwrap() error {
return c.Err
}
// ProtocolError is returned when the server/client runs into an unexpected
// or malformed request/response/message.
type ProtocolError struct {
Message string
}
func (p ProtocolError) Error() string {
return "protocol error: " + p.Message
}
================================================
FILE: core/go.mod
================================================
module github.com/apernet/hysteria/core/v2
go 1.24.0
toolchain go1.25.1
require (
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22
github.com/stretchr/testify v1.11.1
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/time v0.12.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: core/go.sum
================================================
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: core/internal/congestion/bbr/bandwidth.go
================================================
package bbr
import (
"math"
"time"
"github.com/apernet/quic-go/congestion"
)
const (
infBandwidth = Bandwidth(math.MaxUint64)
)
// Bandwidth of a connection
type Bandwidth uint64
const (
// BitsPerSecond is 1 bit per second
BitsPerSecond Bandwidth = 1
// BytesPerSecond is 1 byte per second
BytesPerSecond = 8 * BitsPerSecond
)
// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta
func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth {
return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond
}
================================================
FILE: core/internal/congestion/bbr/bandwidth_sampler.go
================================================
package bbr
import (
"math"
"time"
"github.com/apernet/quic-go/congestion"
"github.com/apernet/quic-go/monotime"
)
const (
infRTT = time.Duration(math.MaxInt64)
defaultConnectionStateMapQueueSize = 256
defaultCandidatesBufferSize = 256
)
type roundTripCount uint64
// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned
// to the caller when the packet is acked or lost.
type sendTimeState struct {
// Whether other states in this object is valid.
isValid bool
// Whether the sender is app limited at the time the packet was sent.
// App limited bandwidth sample might be artificially low because the sender
// did not have enough data to send in order to saturate the link.
isAppLimited bool
// Total number of sent bytes at the time the packet was sent.
// Includes the packet itself.
totalBytesSent congestion.ByteCount
// Total number of acked bytes at the time the packet was sent.
totalBytesAcked congestion.ByteCount
// Total number of lost bytes at the time the packet was sent.
totalBytesLost congestion.ByteCount
// Total number of inflight bytes at the time the packet was sent.
// Includes the packet itself.
// It should be equal to |total_bytes_sent| minus the sum of
// |total_bytes_acked|, |total_bytes_lost| and total neutered bytes.
bytesInFlight congestion.ByteCount
}
func newSendTimeState(
isAppLimited bool,
totalBytesSent congestion.ByteCount,
totalBytesAcked congestion.ByteCount,
totalBytesLost congestion.ByteCount,
bytesInFlight congestion.ByteCount,
) *sendTimeState {
return &sendTimeState{
isValid: true,
isAppLimited: isAppLimited,
totalBytesSent: totalBytesSent,
totalBytesAcked: totalBytesAcked,
totalBytesLost: totalBytesLost,
bytesInFlight: bytesInFlight,
}
}
type extraAckedEvent struct {
// The excess bytes acknowlwedged in the time delta for this event.
extraAcked congestion.ByteCount
// The bytes acknowledged and time delta from the event.
bytesAcked congestion.ByteCount
timeDelta time.Duration
// The round trip of the event.
round roundTripCount
}
func maxExtraAckedEventFunc(a, b extraAckedEvent) int {
if a.extraAcked > b.extraAcked {
return 1
} else if a.extraAcked < b.extraAcked {
return -1
}
return 0
}
// BandwidthSample
type bandwidthSample struct {
// The bandwidth at that particular sample. Zero if no valid bandwidth sample
// is available.
bandwidth Bandwidth
// The RTT measurement at this particular sample. Zero if no RTT sample is
// available. Does not correct for delayed ack time.
rtt time.Duration
// |send_rate| is computed from the current packet being acked('P') and an
// earlier packet that is acked before P was sent.
sendRate Bandwidth
// States captured when the packet was sent.
stateAtSend sendTimeState
}
func newBandwidthSample() *bandwidthSample {
return &bandwidthSample{
sendRate: infBandwidth,
}
}
// MaxAckHeightTracker is part of the BandwidthSampler. It is called after every
// ack event to keep track the degree of ack aggregation(a.k.a "ack height").
type maxAckHeightTracker struct {
// Tracks the maximum number of bytes acked faster than the estimated
// bandwidth.
maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount]
// The time this aggregation started and the number of bytes acked during it.
aggregationEpochStartTime monotime.Time
aggregationEpochBytes congestion.ByteCount
// The last sent packet number before the current aggregation epoch started.
lastSentPacketNumberBeforeEpoch congestion.PacketNumber
// The number of ack aggregation epochs ever started, including the ongoing
// one. Stats only.
numAckAggregationEpochs uint64
ackAggregationBandwidthThreshold float64
startNewAggregationEpochAfterFullRound bool
reduceExtraAckedOnBandwidthIncrease bool
}
func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker {
return &maxAckHeightTracker{
maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc),
lastSentPacketNumberBeforeEpoch: invalidPacketNumber,
ackAggregationBandwidthThreshold: 1.0,
}
}
func (m *maxAckHeightTracker) Get() congestion.ByteCount {
return m.maxAckHeightFilter.GetBest().extraAcked
}
func (m *maxAckHeightTracker) Update(
bandwidthEstimate Bandwidth,
isNewMaxBandwidth bool,
roundTripCount roundTripCount,
lastSentPacketNumber congestion.PacketNumber,
lastAckedPacketNumber congestion.PacketNumber,
ackTime monotime.Time,
bytesAcked congestion.ByteCount,
) congestion.ByteCount {
forceNewEpoch := false
if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth {
// Save and clear existing entries.
best := m.maxAckHeightFilter.GetBest()
secondBest := m.maxAckHeightFilter.GetSecondBest()
thirdBest := m.maxAckHeightFilter.GetThirdBest()
m.maxAckHeightFilter.Clear()
// Reinsert the heights into the filter after recalculating.
expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta)
if expectedBytesAcked < best.bytesAcked {
best.extraAcked = best.bytesAcked - expectedBytesAcked
m.maxAckHeightFilter.Update(best, best.round)
}
expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta)
if expectedBytesAcked < secondBest.bytesAcked {
secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked
m.maxAckHeightFilter.Update(secondBest, secondBest.round)
}
expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta)
if expectedBytesAcked < thirdBest.bytesAcked {
thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked
m.maxAckHeightFilter.Update(thirdBest, thirdBest.round)
}
}
// If any packet sent after the start of the epoch has been acked, start a new
// epoch.
if m.startNewAggregationEpochAfterFullRound &&
m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber &&
lastAckedPacketNumber != invalidPacketNumber &&
lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch {
forceNewEpoch = true
}
if m.aggregationEpochStartTime.IsZero() || forceNewEpoch {
m.aggregationEpochBytes = bytesAcked
m.aggregationEpochStartTime = ackTime
m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber
m.numAckAggregationEpochs++
return 0
}
// Compute how many bytes are expected to be delivered, assuming max bandwidth
// is correct.
aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime)
expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta)
// Reset the current aggregation epoch as soon as the ack arrival rate is less
// than or equal to the max bandwidth.
if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) {
// Reset to start measuring a new aggregation epoch.
m.aggregationEpochBytes = bytesAcked
m.aggregationEpochStartTime = ackTime
m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber
m.numAckAggregationEpochs++
return 0
}
m.aggregationEpochBytes += bytesAcked
// Compute how many extra bytes were delivered vs max bandwidth.
extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked
newEvent := extraAckedEvent{
extraAcked: extraBytesAcked,
bytesAcked: m.aggregationEpochBytes,
timeDelta: aggregationDelta,
}
m.maxAckHeightFilter.Update(newEvent, roundTripCount)
return extraBytesAcked
}
func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) {
m.maxAckHeightFilter.SetWindowLength(length)
}
func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) {
newEvent := extraAckedEvent{
extraAcked: newHeight,
round: newTime,
}
m.maxAckHeightFilter.Reset(newEvent, newTime)
}
func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) {
m.ackAggregationBandwidthThreshold = threshold
}
func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) {
m.startNewAggregationEpochAfterFullRound = value
}
func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) {
m.reduceExtraAckedOnBandwidthIncrease = value
}
func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 {
return m.ackAggregationBandwidthThreshold
}
func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 {
return m.numAckAggregationEpochs
}
// AckPoint represents a point on the ack line.
type ackPoint struct {
ackTime monotime.Time
totalBytesAcked congestion.ByteCount
}
// RecentAckPoints maintains the most recent 2 ack points at distinct times.
type recentAckPoints struct {
ackPoints [2]ackPoint
}
func (r *recentAckPoints) Update(ackTime monotime.Time, totalBytesAcked congestion.ByteCount) {
if ackTime.Before(r.ackPoints[1].ackTime) {
r.ackPoints[1].ackTime = ackTime
} else if ackTime.After(r.ackPoints[1].ackTime) {
r.ackPoints[0] = r.ackPoints[1]
r.ackPoints[1].ackTime = ackTime
}
r.ackPoints[1].totalBytesAcked = totalBytesAcked
}
func (r *recentAckPoints) Clear() {
r.ackPoints[0] = ackPoint{}
r.ackPoints[1] = ackPoint{}
}
func (r *recentAckPoints) MostRecentPoint() *ackPoint {
return &r.ackPoints[1]
}
func (r *recentAckPoints) LessRecentPoint() *ackPoint {
if r.ackPoints[0].totalBytesAcked != 0 {
return &r.ackPoints[0]
}
return &r.ackPoints[1]
}
// ConnectionStateOnSentPacket represents the information about a sent packet
// and the state of the connection at the moment the packet was sent,
// specifically the information about the most recently acknowledged packet at
// that moment.
type connectionStateOnSentPacket struct {
// Time at which the packet is sent.
sentTime monotime.Time
// Size of the packet.
size congestion.ByteCount
// The value of |totalBytesSentAtLastAckedPacket| at the time the
// packet was sent.
totalBytesSentAtLastAckedPacket congestion.ByteCount
// The value of |lastAckedPacketSentTime| at the time the packet was
// sent.
lastAckedPacketSentTime monotime.Time
// The value of |lastAckedPacketAckTime| at the time the packet was
// sent.
lastAckedPacketAckTime monotime.Time
// Send time states that are returned to the congestion controller when the
// packet is acked or lost.
sendTimeState sendTimeState
}
// Snapshot constructor. Records the current state of the bandwidth
// sampler.
// |bytes_in_flight| is the bytes in flight right after the packet is sent.
func newConnectionStateOnSentPacket(
sentTime monotime.Time,
size congestion.ByteCount,
bytesInFlight congestion.ByteCount,
sampler *bandwidthSampler,
) *connectionStateOnSentPacket {
return &connectionStateOnSentPacket{
sentTime: sentTime,
size: size,
totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket,
lastAckedPacketSentTime: sampler.lastAckedPacketSentTime,
lastAckedPacketAckTime: sampler.lastAckedPacketAckTime,
sendTimeState: *newSendTimeState(
sampler.isAppLimited,
sampler.totalBytesSent,
sampler.totalBytesAcked,
sampler.totalBytesLost,
bytesInFlight,
),
}
}
// BandwidthSampler keeps track of sent and acknowledged packets and outputs a
// bandwidth sample for every packet acknowledged. The samples are taken for
// individual packets, and are not filtered; the consumer has to filter the
// bandwidth samples itself. In certain cases, the sampler will locally severely
// underestimate the bandwidth, hence a maximum filter with a size of at least
// one RTT is recommended.
//
// This class bases its samples on the slope of two curves: the number of bytes
// sent over time, and the number of bytes acknowledged as received over time.
// It produces a sample of both slopes for every packet that gets acknowledged,
// based on a slope between two points on each of the corresponding curves. Note
// that due to the packet loss, the number of bytes on each curve might get
// further and further away from each other, meaning that it is not feasible to
// compare byte values coming from different curves with each other.
//
// The obvious points for measuring slope sample are the ones corresponding to
// the packet that was just acknowledged. Let us denote them as S_1 (point at
// which the current packet was sent) and A_1 (point at which the current packet
// was acknowledged). However, taking a slope requires two points on each line,
// so estimating bandwidth requires picking a packet in the past with respect to
// which the slope is measured.
//
// For that purpose, BandwidthSampler always keeps track of the most recently
// acknowledged packet, and records it together with every outgoing packet.
// When a packet gets acknowledged (A_1), it has not only information about when
// it itself was sent (S_1), but also the information about the latest
// acknowledged packet right before it was sent (S_0 and A_0).
//
// Based on that data, send and ack rate are estimated as:
//
// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0))
// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0))
//
// Here, the ack rate is intuitively the rate we want to treat as bandwidth.
// However, in certain cases (e.g. ack compression) the ack rate at a point may
// end up higher than the rate at which the data was originally sent, which is
// not indicative of the real bandwidth. Hence, we use the send rate as an upper
// bound, and the sample value is
//
// rate_sample = min(send_rate, ack_rate)
//
// An important edge case handled by the sampler is tracking the app-limited
// samples. There are multiple meaning of "app-limited" used interchangeably,
// hence it is important to understand and to be able to distinguish between
// them.
//
// Meaning 1: connection state. The connection is said to be app-limited when
// there is no outstanding data to send. This means that certain bandwidth
// samples in the future would not be an accurate indication of the link
// capacity, and it is important to inform consumer about that. Whenever
// connection becomes app-limited, the sampler is notified via OnAppLimited()
// method.
//
// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth
// sampler becomes notified about the connection being app-limited, it enters
// app-limited phase. In that phase, all *sent* packets are marked as
// app-limited. Note that the connection itself does not have to be
// app-limited during the app-limited phase, and in fact it will not be
// (otherwise how would it send packets?). The boolean flag below indicates
// whether the sampler is in that phase.
//
// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is
// sent during the app-limited phase, the resulting sample related to the
// packet will be marked as app-limited.
//
// With the terminology issue out of the way, let us consider the question of
// what kind of situation it addresses.
//
// Consider a scenario where we first send packets 1 to 20 at a regular
// bandwidth, and then immediately run out of data. After a few seconds, we send
// packets 21 to 60, and only receive ack for 21 between sending packets 40 and
// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0
// we use to compute the slope is going to be packet 20, a few seconds apart
// from the current packet, hence the resulting estimate would be extremely low
// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21,
// meaning that the bandwidth sample would exclude the quiescence.
//
// Based on the analysis of that scenario, we implement the following rule: once
// OnAppLimited() is called, all sent packets will produce app-limited samples
// up until an ack for a packet that was sent after OnAppLimited() was called.
// Note that while the scenario above is not the only scenario when the
// connection is app-limited, the approach works in other cases too.
type congestionEventSample struct {
// The maximum bandwidth sample from all acked packets.
// QuicBandwidth::Zero() if no samples are available.
sampleMaxBandwidth Bandwidth
// Whether |sample_max_bandwidth| is from a app-limited sample.
sampleIsAppLimited bool
// The minimum rtt sample from all acked packets.
// QuicTime::Delta::Infinite() if no samples are available.
sampleRtt time.Duration
// For each packet p in acked packets, this is the max value of INFLIGHT(p),
// where INFLIGHT(p) is the number of bytes acked while p is inflight.
sampleMaxInflight congestion.ByteCount
// The send state of the largest packet in acked_packets, unless it is
// empty. If acked_packets is empty, it's the send state of the largest
// packet in lost_packets.
lastPacketSendState sendTimeState
// The number of extra bytes acked from this ack event, compared to what is
// expected from the flow's bandwidth. Larger value means more ack
// aggregation.
extraAcked congestion.ByteCount
}
func newCongestionEventSample() *congestionEventSample {
return &congestionEventSample{
sampleRtt: infRTT,
}
}
type bandwidthSampler struct {
// The total number of congestion controlled bytes sent during the connection.
totalBytesSent congestion.ByteCount
// The total number of congestion controlled bytes which were acknowledged.
totalBytesAcked congestion.ByteCount
// The total number of congestion controlled bytes which were lost.
totalBytesLost congestion.ByteCount
// The total number of congestion controlled bytes which have been neutered.
totalBytesNeutered congestion.ByteCount
// The value of |total_bytes_sent_| at the time the last acknowledged packet
// was sent. Valid only when |last_acked_packet_sent_time_| is valid.
totalBytesSentAtLastAckedPacket congestion.ByteCount
// The time at which the last acknowledged packet was sent. Set to
// QuicTime::Zero() if no valid timestamp is available.
lastAckedPacketSentTime monotime.Time
// The time at which the most recent packet was acknowledged.
lastAckedPacketAckTime monotime.Time
// The most recently sent packet.
lastSentPacket congestion.PacketNumber
// The most recently acked packet.
lastAckedPacket congestion.PacketNumber
// Indicates whether the bandwidth sampler is currently in an app-limited
// phase.
isAppLimited bool
// The packet that will be acknowledged after this one will cause the sampler
// to exit the app-limited phase.
endOfAppLimitedPhase congestion.PacketNumber
// Record of the connection state at the point where each packet in flight was
// sent, indexed by the packet number.
connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket]
recentAckPoints recentAckPoints
a0Candidates RingBuffer[ackPoint]
// Maximum number of tracked packets.
maxTrackedPackets congestion.ByteCount
maxAckHeightTracker *maxAckHeightTracker
totalBytesAckedAfterLastAckEvent congestion.ByteCount
// True if connection option 'BSAO' is set.
overestimateAvoidance bool
// True if connection option 'BBRB' is set.
limitMaxAckHeightTrackerBySendRate bool
}
func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler {
b := &bandwidthSampler{
maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength),
connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize),
lastSentPacket: invalidPacketNumber,
lastAckedPacket: invalidPacketNumber,
endOfAppLimitedPhase: invalidPacketNumber,
}
b.a0Candidates.Init(defaultCandidatesBufferSize)
return b
}
func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount {
return b.maxAckHeightTracker.Get()
}
func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 {
return b.maxAckHeightTracker.NumAckAggregationEpochs()
}
func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) {
b.maxAckHeightTracker.SetFilterWindowLength(length)
}
func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) {
b.maxAckHeightTracker.Reset(newHeight, newTime)
}
func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) {
b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value)
}
func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) {
b.limitMaxAckHeightTrackerBySendRate = value
}
func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) {
b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value)
}
func (b *bandwidthSampler) EnableOverestimateAvoidance() {
if b.overestimateAvoidance {
return
}
b.overestimateAvoidance = true
b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0)
}
func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool {
return b.overestimateAvoidance
}
func (b *bandwidthSampler) OnPacketSent(
sentTime monotime.Time,
packetNumber congestion.PacketNumber,
bytes congestion.ByteCount,
bytesInFlight congestion.ByteCount,
isRetransmittable bool,
) {
b.lastSentPacket = packetNumber
if !isRetransmittable {
return
}
b.totalBytesSent += bytes
// If there are no packets in flight, the time at which the new transmission
// opens can be treated as the A_0 point for the purpose of bandwidth
// sampling. This underestimates bandwidth to some extent, and produces some
// artificially low samples for most packets in flight, but it provides with
// samples at important points where we would not have them otherwise, most
// importantly at the beginning of the connection.
if bytesInFlight == 0 {
b.lastAckedPacketAckTime = sentTime
if b.overestimateAvoidance {
b.recentAckPoints.Clear()
b.recentAckPoints.Update(sentTime, b.totalBytesAcked)
b.a0Candidates.Clear()
b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint())
}
b.totalBytesSentAtLastAckedPacket = b.totalBytesSent
// In this situation ack compression is not a concern, set send rate to
// effectively infinite.
b.lastAckedPacketSentTime = sentTime
}
b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket(
sentTime,
bytes,
bytesInFlight+bytes,
b,
))
}
func (b *bandwidthSampler) OnCongestionEvent(
ackTime monotime.Time,
ackedPackets []congestion.AckedPacketInfo,
lostPackets []congestion.LostPacketInfo,
maxBandwidth Bandwidth,
estBandwidthUpperBound Bandwidth,
roundTripCount roundTripCount,
) congestionEventSample {
eventSample := newCongestionEventSample()
var lastLostPacketSendState sendTimeState
for _, p := range lostPackets {
sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost)
if sendState.isValid {
lastLostPacketSendState = sendState
}
}
if len(ackedPackets) == 0 {
// Only populate send state for a loss-only event.
eventSample.lastPacketSendState = lastLostPacketSendState
return *eventSample
}
var lastAckedPacketSendState sendTimeState
var maxSendRate Bandwidth
for _, p := range ackedPackets {
sample := b.onPacketAcknowledged(ackTime, p.PacketNumber)
if !sample.stateAtSend.isValid {
continue
}
lastAckedPacketSendState = sample.stateAtSend
if sample.rtt != 0 {
eventSample.sampleRtt = min(eventSample.sampleRtt, sample.rtt)
}
if sample.bandwidth > eventSample.sampleMaxBandwidth {
eventSample.sampleMaxBandwidth = sample.bandwidth
eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited
}
if sample.sendRate != infBandwidth {
maxSendRate = max(maxSendRate, sample.sendRate)
}
inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked
if inflightSample > eventSample.sampleMaxInflight {
eventSample.sampleMaxInflight = inflightSample
}
}
if !lastLostPacketSendState.isValid {
eventSample.lastPacketSendState = lastAckedPacketSendState
} else if !lastAckedPacketSendState.isValid {
eventSample.lastPacketSendState = lastLostPacketSendState
} else {
// If two packets are inflight and an alarm is armed to lose a packet and it
// wakes up late, then the first of two in flight packets could have been
// acknowledged before the wakeup, which re-evaluates loss detection, and
// could declare the later of the two lost.
if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber {
eventSample.lastPacketSendState = lastLostPacketSendState
} else {
eventSample.lastPacketSendState = lastAckedPacketSendState
}
}
isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth
maxBandwidth = max(maxBandwidth, eventSample.sampleMaxBandwidth)
if b.limitMaxAckHeightTrackerBySendRate {
maxBandwidth = max(maxBandwidth, maxSendRate)
}
eventSample.extraAcked = b.onAckEventEnd(min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount)
return *eventSample
}
func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) {
b.totalBytesLost += bytesLost
if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil {
sentPacketToSendTimeState(sentPacketPointer, &s)
}
return s
}
func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) {
b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) {
b.totalBytesNeutered += sentPacket.size
})
}
func (b *bandwidthSampler) OnAppLimited() {
b.isAppLimited = true
b.endOfAppLimitedPhase = b.lastSentPacket
}
func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) {
// A packet can become obsolete when it is removed from QuicUnackedPacketMap's
// view of inflight before it is acked or marked as lost. For example, when
// QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet,
// the packet is removed from QuicUnackedPacketMap's inflight, but is not
// marked as acked or lost in the BandwidthSampler.
b.connectionStateMap.RemoveUpTo(leastUnacked)
}
func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount {
return b.totalBytesSent
}
func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount {
return b.totalBytesLost
}
func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount {
return b.totalBytesAcked
}
func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount {
return b.totalBytesNeutered
}
func (b *bandwidthSampler) IsAppLimited() bool {
return b.isAppLimited
}
func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber {
return b.endOfAppLimitedPhase
}
func (b *bandwidthSampler) max_ack_height() congestion.ByteCount {
return b.maxAckHeightTracker.Get()
}
func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool {
if b.a0Candidates.Empty() {
return false
}
if b.a0Candidates.Len() == 1 {
*a0 = *b.a0Candidates.Front()
return true
}
for i := 1; i < b.a0Candidates.Len(); i++ {
if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked {
*a0 = *b.a0Candidates.Offset(i - 1)
if i > 1 {
for j := 0; j < i-1; j++ {
b.a0Candidates.PopFront()
}
}
return true
}
}
*a0 = *b.a0Candidates.Back()
for k := 0; k < b.a0Candidates.Len()-1; k++ {
b.a0Candidates.PopFront()
}
return true
}
func (b *bandwidthSampler) onPacketAcknowledged(ackTime monotime.Time, packetNumber congestion.PacketNumber) bandwidthSample {
sample := newBandwidthSample()
b.lastAckedPacket = packetNumber
sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber)
if sentPacketPointer == nil {
return *sample
}
// OnPacketAcknowledgedInner
b.totalBytesAcked += sentPacketPointer.size
b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent
b.lastAckedPacketSentTime = sentPacketPointer.sentTime
b.lastAckedPacketAckTime = ackTime
if b.overestimateAvoidance {
b.recentAckPoints.Update(ackTime, b.totalBytesAcked)
}
if b.isAppLimited {
// Exit app-limited phase in two cases:
// (1) end_of_app_limited_phase_ is not initialized, i.e., so far all
// packets are sent while there are buffered packets or pending data.
// (2) The current acked packet is after the sent packet marked as the end
// of the app limit phase.
if b.endOfAppLimitedPhase == invalidPacketNumber ||
packetNumber > b.endOfAppLimitedPhase {
b.isAppLimited = false
}
}
// There might have been no packets acknowledged at the moment when the
// current packet was sent. In that case, there is no bandwidth sample to
// make.
if sentPacketPointer.lastAckedPacketSentTime.IsZero() {
return *sample
}
// Infinite rate indicates that the sampler is supposed to discard the
// current send rate sample and use only the ack rate.
sendRate := infBandwidth
if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) {
sendRate = BandwidthFromDelta(
sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket,
sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime))
}
var a0 ackPoint
if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) {
} else {
a0.ackTime = sentPacketPointer.lastAckedPacketAckTime
a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked
}
// During the slope calculation, ensure that ack time of the current packet is
// always larger than the time of the previous packet, otherwise division by
// zero or integer underflow can occur.
if ackTime.Sub(a0.ackTime) <= 0 {
return *sample
}
ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime))
sample.bandwidth = min(sendRate, ackRate)
// Note: this sample does not account for delayed acknowledgement time. This
// means that the RTT measurements here can be artificially high, especially
// on low bandwidth connections.
sample.rtt = ackTime.Sub(sentPacketPointer.sentTime)
sample.sendRate = sendRate
sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend)
return *sample
}
func (b *bandwidthSampler) onAckEventEnd(
bandwidthEstimate Bandwidth,
isNewMaxBandwidth bool,
roundTripCount roundTripCount,
) congestion.ByteCount {
newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent
if newlyAckedBytes == 0 {
return 0
}
b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked
extraAcked := b.maxAckHeightTracker.Update(
bandwidthEstimate,
isNewMaxBandwidth,
roundTripCount,
b.lastSentPacket,
b.lastAckedPacket,
b.lastAckedPacketAckTime,
newlyAckedBytes)
// If |extra_acked| is zero, i.e. this ack event marks the start of a new ack
// aggregation epoch, save LessRecentPoint, which is the last ack point of the
// previous epoch, as a A0 candidate.
if b.overestimateAvoidance && extraAcked == 0 {
b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint())
}
return extraAcked
}
func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) {
*sendTimeState = sentPacket.sendTimeState
sendTimeState.isValid = true
}
// BytesFromBandwidthAndTimeDelta calculates the bytes
// from a bandwidth(bits per second) and a time delta
func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount {
return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) /
(congestion.ByteCount(time.Second) * 8)
}
func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration {
return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth)
}
================================================
FILE: core/internal/congestion/bbr/bbr_sender.go
================================================
package bbr
import (
"fmt"
"math/rand"
"net"
"os"
"strconv"
"time"
"github.com/apernet/quic-go/congestion"
"github.com/apernet/quic-go/monotime"
"github.com/apernet/hysteria/core/v2/internal/congestion/common"
)
// BbrSender implements BBR congestion control algorithm. BBR aims to estimate
// the current available Bottleneck Bandwidth and RTT (hence the name), and
// regulates the pacing rate and the size of the congestion window based on
// those signals.
//
// BBR relies on pacing in order to function properly. Do not use BBR when
// pacing is disabled.
//
const (
minBps = 65536 // 64 KB/s
invalidPacketNumber = -1
initialCongestionWindowPackets = 32
// Constants based on TCP defaults.
// The minimum CWND to ensure delayed acks don't reduce bandwidth measurements.
// Does not inflate the pacing rate.
defaultMinimumCongestionWindow = 4 * congestion.ByteCount(congestion.InitialPacketSize)
// The gain used for the STARTUP, equal to 2/ln(2).
defaultHighGain = 2.885
// The newly derived gain for STARTUP, equal to 4 * ln(2)
derivedHighGain = 2.773
// The newly derived CWND gain for STARTUP, 2.
derivedHighCWNDGain = 2.0
debugEnv = "HYSTERIA_BBR_DEBUG"
)
// The cycle of gains used during the PROBE_BW stage.
var pacingGain = [...]float64{1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}
const (
// The length of the gain cycle.
gainCycleLength = len(pacingGain)
// The size of the bandwidth filter window, in round-trips.
bandwidthWindowSize = gainCycleLength + 2
// The time after which the current min_rtt value expires.
minRttExpiry = 10 * time.Second
// The minimum time the connection can spend in PROBE_RTT mode.
probeRttTime = 200 * time.Millisecond
// If the bandwidth does not increase by the factor of |kStartupGrowthTarget|
// within |kRoundTripsWithoutGrowthBeforeExitingStartup| rounds, the connection
// will exit the STARTUP mode.
startupGrowthTarget = 1.25
roundTripsWithoutGrowthBeforeExitingStartup = int64(3)
// Flag.
defaultStartupFullLossCount = 8
quicBbr2DefaultLossThreshold = 0.02
maxBbrBurstPackets = 10
)
type bbrMode int
const (
// Startup phase of the connection.
bbrModeStartup = iota
// After achieving the highest possible bandwidth during the startup, lower
// the pacing rate in order to drain the queue.
bbrModeDrain
// Cruising mode.
bbrModeProbeBw
// Temporarily slow down sending in order to empty the buffer and measure
// the real minimum RTT.
bbrModeProbeRtt
)
// Indicates how the congestion control limits the amount of bytes in flight.
type bbrRecoveryState int
const (
// Do not limit.
bbrRecoveryStateNotInRecovery = iota
// Allow an extra outstanding byte for each byte acknowledged.
bbrRecoveryStateConservation
// Allow two extra outstanding bytes for each byte acknowledged (slow
// start).
bbrRecoveryStateGrowth
)
type bbrSender struct {
rttStats congestion.RTTStatsProvider
clock Clock
pacer *common.Pacer
mode bbrMode
// Bandwidth sampler provides BBR with the bandwidth measurements at
// individual points.
sampler *bandwidthSampler
// The number of the round trips that have occurred during the connection.
roundTripCount roundTripCount
// The packet number of the most recently sent packet.
lastSentPacket congestion.PacketNumber
// Acknowledgement of any packet after |current_round_trip_end_| will cause
// the round trip counter to advance.
currentRoundTripEnd congestion.PacketNumber
// Number of congestion events with some losses, in the current round.
numLossEventsInRound uint64
// Number of total bytes lost in the current round.
bytesLostInRound congestion.ByteCount
// The filter that tracks the maximum bandwidth over the multiple recent
// round-trips.
maxBandwidth *WindowedFilter[Bandwidth, roundTripCount]
// Minimum RTT estimate. Automatically expires within 10 seconds (and
// triggers PROBE_RTT mode) if no new value is sampled during that period.
minRtt time.Duration
// The time at which the current value of |min_rtt_| was assigned.
minRttTimestamp monotime.Time
// The maximum allowed number of bytes in flight.
congestionWindow congestion.ByteCount
// The initial value of the |congestion_window_|.
initialCongestionWindow congestion.ByteCount
// The largest value the |congestion_window_| can achieve.
maxCongestionWindow congestion.ByteCount
// The smallest value the |congestion_window_| can achieve.
minCongestionWindow congestion.ByteCount
// The pacing gain applied during the STARTUP phase.
highGain float64
// The CWND gain applied during the STARTUP phase.
highCwndGain float64
// The pacing gain applied during the DRAIN phase.
drainGain float64
// The current pacing rate of the connection.
pacingRate Bandwidth
// The gain currently applied to the pacing rate.
pacingGain float64
// The gain currently applied to the congestion window.
congestionWindowGain float64
// The gain used for the congestion window during PROBE_BW. Latched from
// quic_bbr_cwnd_gain flag.
congestionWindowGainConstant float64
// The number of RTTs to stay in STARTUP mode. Defaults to 3.
numStartupRtts int64
// Number of round-trips in PROBE_BW mode, used for determining the current
// pacing gain cycle.
cycleCurrentOffset int
// The time at which the last pacing gain cycle was started.
lastCycleStart monotime.Time
// Indicates whether the connection has reached the full bandwidth mode.
isAtFullBandwidth bool
// Number of rounds during which there was no significant bandwidth increase.
roundsWithoutBandwidthGain int64
// The bandwidth compared to which the increase is measured.
bandwidthAtLastRound Bandwidth
// Set to true upon exiting quiescence.
exitingQuiescence bool
// Time at which PROBE_RTT has to be exited. Setting it to zero indicates
// that the time is yet unknown as the number of packets in flight has not
// reached the required value.
exitProbeRttAt monotime.Time
// Indicates whether a round-trip has passed since PROBE_RTT became active.
probeRttRoundPassed bool
// Indicates whether the most recent bandwidth sample was marked as
// app-limited.
lastSampleIsAppLimited bool
// Indicates whether any non app-limited samples have been recorded.
hasNoAppLimitedSample bool
// Current state of recovery.
recoveryState bbrRecoveryState
// Receiving acknowledgement of a packet after |end_recovery_at_| will cause
// BBR to exit the recovery mode. A value above zero indicates at least one
// loss has been detected, so it must not be set back to zero.
endRecoveryAt congestion.PacketNumber
// A window used to limit the number of bytes in flight during loss recovery.
recoveryWindow congestion.ByteCount
// If true, consider all samples in recovery app-limited.
isAppLimitedRecovery bool // not used
// When true, pace at 1.5x and disable packet conservation in STARTUP.
slowerStartup bool // not used
// When true, disables packet conservation in STARTUP.
rateBasedStartup bool // not used
// When true, add the most recent ack aggregation measurement during STARTUP.
enableAckAggregationDuringStartup bool
// When true, expire the windowed ack aggregation values in STARTUP when
// bandwidth increases more than 25%.
expireAckAggregationInStartup bool
// If true, will not exit low gain mode until bytes_in_flight drops below BDP
// or it's time for high gain mode.
drainToTarget bool
// If true, slow down pacing rate in STARTUP when overshooting is detected.
detectOvershooting bool
// Bytes lost while detect_overshooting_ is true.
bytesLostWhileDetectingOvershooting congestion.ByteCount
// Slow down pacing rate if
// bytes_lost_while_detecting_overshooting_ *
// bytes_lost_multiplier_while_detecting_overshooting_ > IW.
bytesLostMultiplierWhileDetectingOvershooting uint8
// When overshooting is detected, do not drop pacing_rate_ below this value /
// min_rtt.
cwndToCalculateMinPacingRate congestion.ByteCount
// Max congestion window when adjusting network parameters.
maxCongestionWindowWithNetworkParametersAdjusted congestion.ByteCount // not used
// Params.
maxDatagramSize congestion.ByteCount
// Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()|
bytesInFlight congestion.ByteCount
debug bool
}
var _ congestion.CongestionControl = &bbrSender{}
func NewBbrSender(
clock Clock,
initialMaxDatagramSize congestion.ByteCount,
) *bbrSender {
return newBbrSender(
clock,
initialMaxDatagramSize,
initialCongestionWindowPackets*initialMaxDatagramSize,
congestion.MaxCongestionWindowPackets*initialMaxDatagramSize,
)
}
func newBbrSender(
clock Clock,
initialMaxDatagramSize,
initialCongestionWindow,
initialMaxCongestionWindow congestion.ByteCount,
) *bbrSender {
debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
b := &bbrSender{
clock: clock,
mode: bbrModeStartup,
sampler: newBandwidthSampler(roundTripCount(bandwidthWindowSize)),
lastSentPacket: invalidPacketNumber,
currentRoundTripEnd: invalidPacketNumber,
maxBandwidth: NewWindowedFilter(roundTripCount(bandwidthWindowSize), MaxFilter[Bandwidth]),
congestionWindow: initialCongestionWindow,
initialCongestionWindow: initialCongestionWindow,
maxCongestionWindow: initialMaxCongestionWindow,
minCongestionWindow: defaultMinimumCongestionWindow,
highGain: defaultHighGain,
highCwndGain: defaultHighGain,
drainGain: 1.0 / defaultHighGain,
pacingGain: 1.0,
congestionWindowGain: 1.0,
congestionWindowGainConstant: 2.0,
numStartupRtts: roundTripsWithoutGrowthBeforeExitingStartup,
recoveryState: bbrRecoveryStateNotInRecovery,
endRecoveryAt: invalidPacketNumber,
recoveryWindow: initialMaxCongestionWindow,
bytesLostMultiplierWhileDetectingOvershooting: 2,
cwndToCalculateMinPacingRate: initialCongestionWindow,
maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow,
maxDatagramSize: initialMaxDatagramSize,
debug: debug,
}
b.pacer = common.NewPacer(b.bandwidthForPacer)
/*
if b.tracer != nil {
b.lastState = logging.CongestionStateStartup
b.tracer.UpdatedCongestionState(logging.CongestionStateStartup)
}
*/
b.enterStartupMode(b.clock.Now())
b.setHighCwndGain(derivedHighCWNDGain)
return b
}
func (b *bbrSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) {
b.rttStats = provider
}
// TimeUntilSend implements the SendAlgorithm interface.
func (b *bbrSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time {
return b.pacer.TimeUntilSend()
}
// HasPacingBudget implements the SendAlgorithm interface.
func (b *bbrSender) HasPacingBudget(now monotime.Time) bool {
return b.pacer.Budget(now) >= b.maxDatagramSize
}
// OnPacketSent implements the SendAlgorithm interface.
func (b *bbrSender) OnPacketSent(
sentTime monotime.Time,
bytesInFlight congestion.ByteCount,
packetNumber congestion.PacketNumber,
bytes congestion.ByteCount,
isRetransmittable bool,
) {
b.pacer.SentPacket(sentTime, bytes)
b.lastSentPacket = packetNumber
b.bytesInFlight = bytesInFlight
if bytesInFlight == 0 {
b.exitingQuiescence = true
}
b.sampler.OnPacketSent(sentTime, packetNumber, bytes, bytesInFlight, isRetransmittable)
}
// CanSend implements the SendAlgorithm interface.
func (b *bbrSender) CanSend(bytesInFlight congestion.ByteCount) bool {
return bytesInFlight < b.GetCongestionWindow()
}
// MaybeExitSlowStart implements the SendAlgorithm interface.
func (b *bbrSender) MaybeExitSlowStart() {
// Do nothing
}
// OnPacketAcked implements the SendAlgorithm interface.
func (b *bbrSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes, priorInFlight congestion.ByteCount, eventTime monotime.Time) {
// Do nothing.
}
// OnPacketLost implements the SendAlgorithm interface.
func (b *bbrSender) OnPacketLost(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) {
// Do nothing.
}
// OnRetransmissionTimeout implements the SendAlgorithm interface.
func (b *bbrSender) OnRetransmissionTimeout(packetsRetransmitted bool) {
// Do nothing.
}
// SetMaxDatagramSize implements the SendAlgorithm interface.
func (b *bbrSender) SetMaxDatagramSize(s congestion.ByteCount) {
if s < b.maxDatagramSize {
panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", b.maxDatagramSize, s))
}
cwndIsMinCwnd := b.congestionWindow == b.minCongestionWindow
b.maxDatagramSize = s
if cwndIsMinCwnd {
b.congestionWindow = b.minCongestionWindow
}
b.pacer.SetMaxDatagramSize(s)
}
// InSlowStart implements the SendAlgorithmWithDebugInfos interface.
func (b *bbrSender) InSlowStart() bool {
return b.mode == bbrModeStartup
}
// InRecovery implements the SendAlgorithmWithDebugInfos interface.
func (b *bbrSender) InRecovery() bool {
return b.recoveryState != bbrRecoveryStateNotInRecovery
}
// GetCongestionWindow implements the SendAlgorithmWithDebugInfos interface.
func (b *bbrSender) GetCongestionWindow() congestion.ByteCount {
if b.mode == bbrModeProbeRtt {
return b.probeRttCongestionWindow()
}
if b.InRecovery() {
return min(b.congestionWindow, b.recoveryWindow)
}
return b.congestionWindow
}
func (b *bbrSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) {
// Do nothing.
}
func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {
totalBytesAckedBefore := b.sampler.TotalBytesAcked()
totalBytesLostBefore := b.sampler.TotalBytesLost()
var isRoundStart, minRttExpired bool
var excessAcked, bytesLost congestion.ByteCount
// The send state of the largest packet in acked_packets, unless it is
// empty. If acked_packets is empty, it's the send state of the largest
// packet in lost_packets.
var lastPacketSendState sendTimeState
b.maybeAppLimited(priorInFlight)
// Update bytesInFlight
b.bytesInFlight = priorInFlight
for _, p := range ackedPackets {
b.bytesInFlight -= p.BytesAcked
}
for _, p := range lostPackets {
b.bytesInFlight -= p.BytesLost
}
if len(ackedPackets) != 0 {
lastAckedPacket := ackedPackets[len(ackedPackets)-1].PacketNumber
isRoundStart = b.updateRoundTripCounter(lastAckedPacket)
b.updateRecoveryState(lastAckedPacket, len(lostPackets) != 0, isRoundStart)
}
sample := b.sampler.OnCongestionEvent(eventTime,
ackedPackets, lostPackets, b.maxBandwidth.GetBest(), infBandwidth, b.roundTripCount)
if sample.lastPacketSendState.isValid {
b.lastSampleIsAppLimited = sample.lastPacketSendState.isAppLimited
b.hasNoAppLimitedSample = b.hasNoAppLimitedSample || !b.lastSampleIsAppLimited
}
// Avoid updating |max_bandwidth_| if a) this is a loss-only event, or b) all
// packets in |acked_packets| did not generate valid samples. (e.g. ack of
// ack-only packets). In both cases, sampler_.total_bytes_acked() will not
// change.
if totalBytesAckedBefore != b.sampler.TotalBytesAcked() {
if !sample.sampleIsAppLimited || sample.sampleMaxBandwidth > b.maxBandwidth.GetBest() {
b.maxBandwidth.Update(sample.sampleMaxBandwidth, b.roundTripCount)
}
}
if sample.sampleRtt != infRTT {
minRttExpired = b.maybeUpdateMinRtt(eventTime, sample.sampleRtt)
}
bytesLost = b.sampler.TotalBytesLost() - totalBytesLostBefore
excessAcked = sample.extraAcked
lastPacketSendState = sample.lastPacketSendState
if len(lostPackets) != 0 {
b.numLossEventsInRound++
b.bytesLostInRound += bytesLost
}
// Handle logic specific to PROBE_BW mode.
if b.mode == bbrModeProbeBw {
b.updateGainCyclePhase(eventTime, priorInFlight, len(lostPackets) != 0)
}
// Handle logic specific to STARTUP and DRAIN modes.
if isRoundStart && !b.isAtFullBandwidth {
b.checkIfFullBandwidthReached(&lastPacketSendState)
}
b.maybeExitStartupOrDrain(eventTime)
// Handle logic specific to PROBE_RTT.
b.maybeEnterOrExitProbeRtt(eventTime, isRoundStart, minRttExpired)
// Calculate number of packets acked and lost.
bytesAcked := b.sampler.TotalBytesAcked() - totalBytesAckedBefore
// After the model is updated, recalculate the pacing rate and congestion
// window.
b.calculatePacingRate(bytesLost)
b.calculateCongestionWindow(bytesAcked, excessAcked)
b.calculateRecoveryWindow(bytesAcked, bytesLost)
// Cleanup internal state.
// This is where we clean up obsolete (acked or lost) packets from the bandwidth sampler.
// The "least unacked" should actually be FirstOutstanding, but since we are not passing
// that through OnCongestionEventEx, we will only do an estimate using acked/lost packets
// for now. Because of fast retransmission, they should differ by no more than 2 packets.
// (this is controlled by packetThreshold in quic-go's sentPacketHandler)
var leastUnacked congestion.PacketNumber
if len(ackedPackets) != 0 {
leastUnacked = ackedPackets[len(ackedPackets)-1].PacketNumber - 2
} else {
leastUnacked = lostPackets[len(lostPackets)-1].PacketNumber + 1
}
b.sampler.RemoveObsoletePackets(leastUnacked)
if isRoundStart {
b.numLossEventsInRound = 0
b.bytesLostInRound = 0
}
}
func (b *bbrSender) PacingRate() Bandwidth {
if b.pacingRate == 0 {
return Bandwidth(b.highGain * float64(
BandwidthFromDelta(b.initialCongestionWindow, b.getMinRtt())))
}
return b.pacingRate
}
func (b *bbrSender) hasGoodBandwidthEstimateForResumption() bool {
return b.hasNonAppLimitedSample()
}
func (b *bbrSender) hasNonAppLimitedSample() bool {
return b.hasNoAppLimitedSample
}
// Sets the pacing gain used in STARTUP. Must be greater than 1.
func (b *bbrSender) setHighGain(highGain float64) {
b.highGain = highGain
if b.mode == bbrModeStartup {
b.pacingGain = highGain
}
}
// Sets the CWND gain used in STARTUP. Must be greater than 1.
func (b *bbrSender) setHighCwndGain(highCwndGain float64) {
b.highCwndGain = highCwndGain
if b.mode == bbrModeStartup {
b.congestionWindowGain = highCwndGain
}
}
// Sets the gain used in DRAIN. Must be less than 1.
func (b *bbrSender) setDrainGain(drainGain float64) {
b.drainGain = drainGain
}
// Get the current bandwidth estimate. Note that Bandwidth is in bits per second.
func (b *bbrSender) bandwidthEstimate() Bandwidth {
return b.maxBandwidth.GetBest()
}
func (b *bbrSender) bandwidthForPacer() congestion.ByteCount {
bps := congestion.ByteCount(float64(b.PacingRate()) / float64(BytesPerSecond))
if bps < minBps {
// We need to make sure that the bandwidth value for pacer is never zero,
// otherwise it will go into an edge case where HasPacingBudget = false
// but TimeUntilSend is before, causing the quic-go send loop to go crazy and get stuck.
return minBps
}
return bps
}
// Returns the current estimate of the RTT of the connection. Outside of the
// edge cases, this is minimum RTT.
func (b *bbrSender) getMinRtt() time.Duration {
if b.minRtt != 0 {
return b.minRtt
}
// min_rtt could be available if the handshake packet gets neutered then
// gets acknowledged. This could only happen for QUIC crypto where we do not
// drop keys.
minRtt := b.rttStats.MinRTT()
if minRtt == 0 {
return 100 * time.Millisecond
} else {
return minRtt
}
}
// Computes the target congestion window using the specified gain.
func (b *bbrSender) getTargetCongestionWindow(gain float64) congestion.ByteCount {
bdp := bdpFromRttAndBandwidth(b.getMinRtt(), b.bandwidthEstimate())
congestionWindow := congestion.ByteCount(gain * float64(bdp))
// BDP estimate will be zero if no bandwidth samples are available yet.
if congestionWindow == 0 {
congestionWindow = congestion.ByteCount(gain * float64(b.initialCongestionWindow))
}
return max(congestionWindow, b.minCongestionWindow)
}
// The target congestion window during PROBE_RTT.
func (b *bbrSender) probeRttCongestionWindow() congestion.ByteCount {
return b.minCongestionWindow
}
func (b *bbrSender) maybeUpdateMinRtt(now monotime.Time, sampleMinRtt time.Duration) bool {
// Do not expire min_rtt if none was ever available.
minRttExpired := b.minRtt != 0 && now.After(b.minRttTimestamp.Add(minRttExpiry))
if minRttExpired || sampleMinRtt < b.minRtt || b.minRtt == 0 {
b.minRtt = sampleMinRtt
b.minRttTimestamp = now
}
return minRttExpired
}
// Enters the STARTUP mode.
func (b *bbrSender) enterStartupMode(now monotime.Time) {
b.mode = bbrModeStartup
// b.maybeTraceStateChange(logging.CongestionStateStartup)
b.pacingGain = b.highGain
b.congestionWindowGain = b.highCwndGain
if b.debug {
b.debugPrint("Phase: STARTUP")
}
}
// Enters the PROBE_BW mode.
func (b *bbrSender) enterProbeBandwidthMode(now monotime.Time) {
b.mode = bbrModeProbeBw
// b.maybeTraceStateChange(logging.CongestionStateProbeBw)
b.congestionWindowGain = b.congestionWindowGainConstant
// Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is
// excluded because in that case increased gain and decreased gain would not
// follow each other.
b.cycleCurrentOffset = int(rand.Int31n(congestion.PacketsPerConnectionID)) % (gainCycleLength - 1)
if b.cycleCurrentOffset >= 1 {
b.cycleCurrentOffset += 1
}
b.lastCycleStart = now
b.pacingGain = pacingGain[b.cycleCurrentOffset]
if b.debug {
b.debugPrint("Phase: PROBE_BW")
}
}
// Updates the round-trip counter if a round-trip has passed. Returns true if
// the counter has been advanced.
func (b *bbrSender) updateRoundTripCounter(lastAckedPacket congestion.PacketNumber) bool {
if b.currentRoundTripEnd == invalidPacketNumber || lastAckedPacket > b.currentRoundTripEnd {
b.roundTripCount++
b.currentRoundTripEnd = b.lastSentPacket
return true
}
return false
}
// Updates the current gain used in PROBE_BW mode.
func (b *bbrSender) updateGainCyclePhase(now monotime.Time, priorInFlight congestion.ByteCount, hasLosses bool) {
// In most cases, the cycle is advanced after an RTT passes.
shouldAdvanceGainCycling := now.After(b.lastCycleStart.Add(b.getMinRtt()))
// If the pacing gain is above 1.0, the connection is trying to probe the
// bandwidth by increasing the number of bytes in flight to at least
// pacing_gain * BDP. Make sure that it actually reaches the target, as long
// as there are no losses suggesting that the buffers are not able to hold
// that much.
if b.pacingGain > 1.0 && !hasLosses && priorInFlight < b.getTargetCongestionWindow(b.pacingGain) {
shouldAdvanceGainCycling = false
}
// If pacing gain is below 1.0, the connection is trying to drain the extra
// queue which could have been incurred by probing prior to it. If the number
// of bytes in flight falls down to the estimated BDP value earlier, conclude
// that the queue has been successfully drained and exit this cycle early.
if b.pacingGain < 1.0 && b.bytesInFlight <= b.getTargetCongestionWindow(1) {
shouldAdvanceGainCycling = true
}
if shouldAdvanceGainCycling {
b.cycleCurrentOffset = (b.cycleCurrentOffset + 1) % gainCycleLength
b.lastCycleStart = now
// Stay in low gain mode until the target BDP is hit.
// Low gain mode will be exited immediately when the target BDP is achieved.
if b.drainToTarget && b.pacingGain < 1 &&
pacingGain[b.cycleCurrentOffset] == 1 &&
b.bytesInFlight > b.getTargetCongestionWindow(1) {
return
}
b.pacingGain = pacingGain[b.cycleCurrentOffset]
}
}
// Tracks for how many round-trips the bandwidth has not increased
// significantly.
func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeState) {
if b.lastSampleIsAppLimited {
return
}
target := Bandwidth(float64(b.bandwidthAtLastRound) * startupGrowthTarget)
if b.bandwidthEstimate() >= target {
b.bandwidthAtLastRound = b.bandwidthEstimate()
b.roundsWithoutBandwidthGain = 0
if b.expireAckAggregationInStartup {
// Expire old excess delivery measurements now that bandwidth increased.
b.sampler.ResetMaxAckHeightTracker(0, b.roundTripCount)
}
return
}
b.roundsWithoutBandwidthGain++
if b.roundsWithoutBandwidthGain >= b.numStartupRtts ||
b.shouldExitStartupDueToLoss(lastPacketSendState) {
b.isAtFullBandwidth = true
}
}
func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) {
if bytesInFlight < b.getTargetCongestionWindow(1) {
b.sampler.OnAppLimited()
}
}
// Transitions from STARTUP to DRAIN and from DRAIN to PROBE_BW if
// appropriate.
func (b *bbrSender) maybeExitStartupOrDrain(now monotime.Time) {
if b.mode == bbrModeStartup && b.isAtFullBandwidth {
b.mode = bbrModeDrain
// b.maybeTraceStateChange(logging.CongestionStateDrain)
b.pacingGain = b.drainGain
b.congestionWindowGain = b.highCwndGain
if b.debug {
b.debugPrint("Phase: DRAIN")
}
}
if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) {
b.enterProbeBandwidthMode(now)
}
}
// Decides whether to enter or exit PROBE_RTT.
func (b *bbrSender) maybeEnterOrExitProbeRtt(now monotime.Time, isRoundStart, minRttExpired bool) {
if minRttExpired && !b.exitingQuiescence && b.mode != bbrModeProbeRtt {
b.mode = bbrModeProbeRtt
// b.maybeTraceStateChange(logging.CongestionStateProbRtt)
b.pacingGain = 1.0
// Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight|
// is at the target small value.
b.exitProbeRttAt = 0
if b.debug {
b.debugPrint("BandwidthEstimate: %s, CongestionWindowGain: %.2f, PacingGain: %.2f, PacingRate: %s",
formatSpeed(b.bandwidthEstimate()), b.congestionWindowGain, b.pacingGain, formatSpeed(b.PacingRate()))
b.debugPrint("Phase: PROBE_RTT")
}
}
if b.mode == bbrModeProbeRtt {
b.sampler.OnAppLimited()
// b.maybeTraceStateChange(logging.CongestionStateApplicationLimited)
if b.exitProbeRttAt.IsZero() {
// If the window has reached the appropriate size, schedule exiting
// PROBE_RTT. The CWND during PROBE_RTT is kMinimumCongestionWindow, but
// we allow an extra packet since QUIC checks CWND before sending a
// packet.
if b.bytesInFlight < b.probeRttCongestionWindow()+congestion.MaxPacketBufferSize {
b.exitProbeRttAt = now.Add(probeRttTime)
b.probeRttRoundPassed = false
}
} else {
if isRoundStart {
b.probeRttRoundPassed = true
}
if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed {
b.minRttTimestamp = now
if b.debug {
b.debugPrint("MinRTT: %s", b.getMinRtt())
}
if !b.isAtFullBandwidth {
b.enterStartupMode(now)
} else {
b.enterProbeBandwidthMode(now)
}
}
}
}
b.exitingQuiescence = false
}
// Determines whether BBR needs to enter, exit or advance state of the
// recovery.
func (b *bbrSender) updateRecoveryState(lastAckedPacket congestion.PacketNumber, hasLosses, isRoundStart bool) {
// Disable recovery in startup, if loss-based exit is enabled.
if !b.isAtFullBandwidth {
return
}
// Exit recovery when there are no losses for a round.
if hasLosses {
b.endRecoveryAt = b.lastSentPacket
}
switch b.recoveryState {
case bbrRecoveryStateNotInRecovery:
if hasLosses {
b.recoveryState = bbrRecoveryStateConservation
// This will cause the |recovery_window_| to be set to the correct
// value in CalculateRecoveryWindow().
b.recoveryWindow = 0
// Since the conservation phase is meant to be lasting for a whole
// round, extend the current round as if it were started right now.
b.currentRoundTripEnd = b.lastSentPacket
}
case bbrRecoveryStateConservation:
if isRoundStart {
b.recoveryState = bbrRecoveryStateGrowth
}
fallthrough
case bbrRecoveryStateGrowth:
// Exit recovery if appropriate.
if !hasLosses && lastAckedPacket > b.endRecoveryAt {
b.recoveryState = bbrRecoveryStateNotInRecovery
}
}
}
// Determines the appropriate pacing rate for the connection.
func (b *bbrSender) calculatePacingRate(bytesLost congestion.ByteCount) {
if b.bandwidthEstimate() == 0 {
return
}
targetRate := Bandwidth(b.pacingGain * float64(b.bandwidthEstimate()))
if b.isAtFullBandwidth {
b.pacingRate = targetRate
return
}
// Pace at the rate of initial_window / RTT as soon as RTT measurements are
// available.
if b.pacingRate == 0 && b.rttStats.MinRTT() != 0 {
b.pacingRate = BandwidthFromDelta(b.initialCongestionWindow, b.rttStats.MinRTT())
return
}
if b.detectOvershooting {
b.bytesLostWhileDetectingOvershooting += bytesLost
// Check for overshooting with network parameters adjusted when pacing rate
// > target_rate and loss has been detected.
if b.pacingRate > targetRate && b.bytesLostWhileDetectingOvershooting > 0 {
if b.hasNoAppLimitedSample ||
b.bytesLostWhileDetectingOvershooting*congestion.ByteCount(b.bytesLostMultiplierWhileDetectingOvershooting) > b.initialCongestionWindow {
// We are fairly sure overshoot happens if 1) there is at least one
// non app-limited bw sample or 2) half of IW gets lost. Slow pacing
// rate.
b.pacingRate = max(targetRate, BandwidthFromDelta(b.cwndToCalculateMinPacingRate, b.rttStats.MinRTT()))
b.bytesLostWhileDetectingOvershooting = 0
b.detectOvershooting = false
}
}
}
// Do not decrease the pacing rate during startup.
b.pacingRate = max(b.pacingRate, targetRate)
}
// Determines the appropriate congestion window for the connection.
func (b *bbrSender) calculateCongestionWindow(bytesAcked, excessAcked congestion.ByteCount) {
if b.mode == bbrModeProbeRtt {
return
}
targetWindow := b.getTargetCongestionWindow(b.congestionWindowGain)
if b.isAtFullBandwidth {
// Add the max recently measured ack aggregation to CWND.
targetWindow += b.sampler.MaxAckHeight()
} else if b.enableAckAggregationDuringStartup {
// Add the most recent excess acked. Because CWND never decreases in
// STARTUP, this will automatically create a very localized max filter.
targetWindow += excessAcked
}
// Instead of immediately setting the target CWND as the new one, BBR grows
// the CWND towards |target_window| by only increasing it |bytes_acked| at a
// time.
if b.isAtFullBandwidth {
b.congestionWindow = min(targetWindow, b.congestionWindow+bytesAcked)
} else if b.congestionWindow < targetWindow ||
b.sampler.TotalBytesAcked() < b.initialCongestionWindow {
// If the connection is not yet out of startup phase, do not decrease the
// window.
b.congestionWindow += bytesAcked
}
// Enforce the limits on the congestion window.
b.congestionWindow = max(b.congestionWindow, b.minCongestionWindow)
b.congestionWindow = min(b.congestionWindow, b.maxCongestionWindow)
}
// Determines the appropriate window that constrains the in-flight during recovery.
func (b *bbrSender) calculateRecoveryWindow(bytesAcked, bytesLost congestion.ByteCount) {
if b.recoveryState == bbrRecoveryStateNotInRecovery {
return
}
// Set up the initial recovery window.
if b.recoveryWindow == 0 {
b.recoveryWindow = b.bytesInFlight + bytesAcked
b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow)
return
}
// Remove losses from the recovery window, while accounting for a potential
// integer underflow.
if b.recoveryWindow >= bytesLost {
b.recoveryWindow = b.recoveryWindow - bytesLost
} else {
b.recoveryWindow = b.maxDatagramSize
}
// In CONSERVATION mode, just subtracting losses is sufficient. In GROWTH,
// release additional |bytes_acked| to achieve a slow-start-like behavior.
if b.recoveryState == bbrRecoveryStateGrowth {
b.recoveryWindow += bytesAcked
}
// Always allow sending at least |bytes_acked| in response.
b.recoveryWindow = max(b.recoveryWindow, b.bytesInFlight+bytesAcked)
b.recoveryWindow = max(b.minCongestionWindow, b.recoveryWindow)
}
// Return whether we should exit STARTUP due to excessive loss.
func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeState) bool {
if b.numLossEventsInRound < defaultStartupFullLossCount || !lastPacketSendState.isValid {
return false
}
inflightAtSend := lastPacketSendState.bytesInFlight
if inflightAtSend > 0 && b.bytesLostInRound > 0 {
if b.bytesLostInRound > congestion.ByteCount(float64(inflightAtSend)*quicBbr2DefaultLossThreshold) {
return true
}
return false
}
return false
}
func (b *bbrSender) debugPrint(format string, a ...any) {
fmt.Printf("[BBRSender] [%s] %s\n",
time.Now().Format("15:04:05"),
fmt.Sprintf(format, a...))
}
func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount {
return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second)
}
func GetInitialPacketSize(addr net.Addr) congestion.ByteCount {
// If this is not a UDP address, we don't know anything about the MTU.
// Use the minimum size of an Initial packet as the max packet size.
if _, ok := addr.(*net.UDPAddr); ok {
return congestion.InitialPacketSize
} else {
return congestion.MinInitialPacketSize
}
}
func formatSpeed(bw Bandwidth) string {
bwf := float64(bw)
units := []string{"bps", "Kbps", "Mbps", "Gbps"}
unitIndex := 0
for bwf > 1000 && unitIndex < len(units)-1 {
bwf /= 1000
unitIndex++
}
return fmt.Sprintf("%.2f %s", bwf, units[unitIndex])
}
================================================
FILE: core/internal/congestion/bbr/clock.go
================================================
package bbr
import "github.com/apernet/quic-go/monotime"
// A Clock returns the current time
type Clock interface {
Now() monotime.Time
}
// DefaultClock implements the Clock interface using the Go stdlib clock.
type DefaultClock struct{}
var _ Clock = DefaultClock{}
// Now gets the current time
func (DefaultClock) Now() monotime.Time {
return monotime.Now()
}
================================================
FILE: core/internal/congestion/bbr/packet_number_indexed_queue.go
================================================
package bbr
import (
"github.com/apernet/quic-go/congestion"
)
// packetNumberIndexedQueue is a queue of mostly continuous numbered entries
// which supports the following operations:
// - adding elements to the end of the queue, or at some point past the end
// - removing elements in any order
// - retrieving elements
// If all elements are inserted in order, all of the operations above are
// amortized O(1) time.
//
// Internally, the data structure is a deque where each element is marked as
// present or not. The deque starts at the lowest present index. Whenever an
// element is removed, it's marked as not present, and the front of the deque is
// cleared of elements that are not present.
//
// The tail of the queue is not cleared due to the assumption of entries being
// inserted in order, though removing all elements of the queue will return it
// to its initial state.
//
// Note that this data structure is inherently hazardous, since an addition of
// just two entries will cause it to consume all of the memory available.
// Because of that, it is not a general-purpose container and should not be used
// as one.
type entryWrapper[T any] struct {
present bool
entry T
}
type packetNumberIndexedQueue[T any] struct {
entries RingBuffer[entryWrapper[T]]
numberOfPresentEntries int
firstPacket congestion.PacketNumber
}
func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] {
q := &packetNumberIndexedQueue[T]{
firstPacket: invalidPacketNumber,
}
q.entries.Init(size)
return q
}
// Emplace inserts data associated |packet_number| into (or past) the end of the
// queue, filling up the missing intermediate entries as necessary. Returns
// true if the element has been inserted successfully, false if it was already
// in the queue or inserted out of order.
func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool {
if packetNumber == invalidPacketNumber || entry == nil {
return false
}
if p.IsEmpty() {
p.entries.PushBack(entryWrapper[T]{
present: true,
entry: *entry,
})
p.numberOfPresentEntries = 1
p.firstPacket = packetNumber
return true
}
// Do not allow insertion out-of-order.
if packetNumber <= p.LastPacket() {
return false
}
// Handle potentially missing elements.
offset := int(packetNumber - p.FirstPacket())
if gap := offset - p.entries.Len(); gap > 0 {
for i := 0; i < gap; i++ {
p.entries.PushBack(entryWrapper[T]{})
}
}
p.entries.PushBack(entryWrapper[T]{
present: true,
entry: *entry,
})
p.numberOfPresentEntries++
return true
}
// GetEntry Retrieve the entry associated with the packet number. Returns the pointer
// to the entry in case of success, or nullptr if the entry does not exist.
func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T {
ew := p.getEntryWraper(packetNumber)
if ew == nil {
return nil
}
return &ew.entry
}
// Remove, Same as above, but if an entry is present in the queue, also call f(entry)
// before removing it.
func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool {
ew := p.getEntryWraper(packetNumber)
if ew == nil {
return false
}
if f != nil {
f(ew.entry)
}
ew.present = false
p.numberOfPresentEntries--
if packetNumber == p.FirstPacket() {
p.clearup()
}
return true
}
// RemoveUpTo, but not including |packet_number|.
// Unused slots in the front are also removed, which means when the function
// returns, |first_packet()| can be larger than |packet_number|.
func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) {
for !p.entries.Empty() &&
p.firstPacket != invalidPacketNumber &&
p.firstPacket < packetNumber {
if p.entries.Front().present {
p.numberOfPresentEntries--
}
p.entries.PopFront()
p.firstPacket++
}
p.clearup()
return
}
// IsEmpty return if queue is empty.
func (p *packetNumberIndexedQueue[T]) IsEmpty() bool {
return p.numberOfPresentEntries == 0
}
// NumberOfPresentEntries returns the number of entries in the queue.
func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int {
return p.numberOfPresentEntries
}
// EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is
// proportional to the memory usage of the queue.
func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int {
return p.entries.Len()
}
// FirstPacket returns packet number of the first entry in the queue.
func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) {
return p.firstPacket
}
// LastPacket returns packet number of the last entry ever inserted in the queue. Note that the
// entry in question may have already been removed. Zero if the queue is
// empty.
func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) {
if p.IsEmpty() {
return invalidPacketNumber
}
return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1)
}
func (p *packetNumberIndexedQueue[T]) clearup() {
for !p.entries.Empty() && !p.entries.Front().present {
p.entries.PopFront()
p.firstPacket++
}
if p.entries.Empty() {
p.firstPacket = invalidPacketNumber
}
}
func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] {
if packetNumber == invalidPacketNumber ||
p.IsEmpty() ||
packetNumber < p.firstPacket {
return nil
}
offset := int(packetNumber - p.firstPacket)
if offset >= p.entries.Len() {
return nil
}
ew := p.entries.Offset(offset)
if ew == nil || !ew.present {
return nil
}
return ew
}
================================================
FILE: core/internal/congestion/bbr/ringbuffer.go
================================================
package bbr
// A RingBuffer is a ring buffer.
// It acts as a heap that doesn't cause any allocations.
type RingBuffer[T any] struct {
ring []T
headPos, tailPos int
full bool
}
// Init preallocs a buffer with a certain size.
func (r *RingBuffer[T]) Init(size int) {
r.ring = make([]T, size)
}
// Len returns the number of elements in the ring buffer.
func (r *RingBuffer[T]) Len() int {
if r.full {
return len(r.ring)
}
if r.tailPos >= r.headPos {
return r.tailPos - r.headPos
}
return r.tailPos - r.headPos + len(r.ring)
}
// Empty says if the ring buffer is empty.
func (r *RingBuffer[T]) Empty() bool {
return !r.full && r.headPos == r.tailPos
}
// PushBack adds a new element.
// If the ring buffer is full, its capacity is increased first.
func (r *RingBuffer[T]) PushBack(t T) {
if r.full || len(r.ring) == 0 {
r.grow()
}
r.ring[r.tailPos] = t
r.tailPos++
if r.tailPos == len(r.ring) {
r.tailPos = 0
}
if r.tailPos == r.headPos {
r.full = true
}
}
// PopFront returns the next element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first.
func (r *RingBuffer[T]) PopFront() T {
if r.Empty() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue")
}
r.full = false
t := r.ring[r.headPos]
r.ring[r.headPos] = *new(T)
r.headPos++
if r.headPos == len(r.ring) {
r.headPos = 0
}
return t
}
// Offset returns the offset element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first
// and check if the index larger than buffer length.
func (r *RingBuffer[T]) Offset(index int) *T {
if r.Empty() || index >= r.Len() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index")
}
offset := (r.headPos + index) % len(r.ring)
return &r.ring[offset]
}
// Front returns the front element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first.
func (r *RingBuffer[T]) Front() *T {
if r.Empty() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue")
}
return &r.ring[r.headPos]
}
// Back returns the back element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first.
func (r *RingBuffer[T]) Back() *T {
if r.Empty() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue")
}
return r.Offset(r.Len() - 1)
}
// Grow the maximum size of the queue.
// This method assume the queue is full.
func (r *RingBuffer[T]) grow() {
oldRing := r.ring
newSize := len(oldRing) * 2
if newSize == 0 {
newSize = 1
}
r.ring = make([]T, newSize)
headLen := copy(r.ring, oldRing[r.headPos:])
copy(r.ring[headLen:], oldRing[:r.headPos])
r.headPos, r.tailPos, r.full = 0, len(oldRing), false
}
// Clear removes all elements.
func (r *RingBuffer[T]) Clear() {
var zeroValue T
for i := range r.ring {
r.ring[i] = zeroValue
}
r.headPos, r.tailPos, r.full = 0, 0, false
}
================================================
FILE: core/internal/congestion/bbr/windowed_filter.go
================================================
package bbr
import (
"golang.org/x/exp/constraints"
)
// Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum)
// estimate of a stream of samples over some fixed time interval. (E.g.,
// the minimum RTT over the past five minutes.) The algorithm keeps track of
// the best, second best, and third best min (or max) estimates, maintaining an
// invariant that the measurement time of the n'th best >= n-1'th best.
// The algorithm works as follows. On a reset, all three estimates are set to
// the same sample. The second best estimate is then recorded in the second
// quarter of the window, and a third best estimate is recorded in the second
// half of the window, bounding the worst case error when the true min is
// monotonically increasing (or true max is monotonically decreasing) over the
// window.
//
// A new best sample replaces all three estimates, since the new best is lower
// (or higher) than everything else in the window and it is the most recent.
// The window thus effectively gets reset on every new min. The same property
// holds true for second best and third best estimates. Specifically, when a
// sample arrives that is better than the second best but not better than the
// best, it replaces the second and third best estimates but not the best
// estimate. Similarly, a sample that is better than the third best estimate
// but not the other estimates replaces only the third best estimate.
//
// Finally, when the best expires, it is replaced by the second best, which in
// turn is replaced by the third best. The newest sample replaces the third
// best.
type WindowedFilterValue interface {
any
}
type WindowedFilterTime interface {
constraints.Integer | constraints.Float
}
type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct {
// Time length of window.
windowLength T
estimates []entry[V, T]
comparator func(V, V) int
}
type entry[V WindowedFilterValue, T WindowedFilterTime] struct {
sample V
time T
}
// Compares two values and returns true if the first is greater than or equal
// to the second.
func MaxFilter[O constraints.Ordered](a, b O) int {
if a > b {
return 1
} else if a < b {
return -1
}
return 0
}
// Compares two values and returns true if the first is less than or equal
// to the second.
func MinFilter[O constraints.Ordered](a, b O) int {
if a < b {
return 1
} else if a > b {
return -1
}
return 0
}
func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] {
return &WindowedFilter[V, T]{
windowLength: windowLength,
estimates: make([]entry[V, T], 3, 3),
comparator: comparator,
}
}
// Changes the window length. Does not update any current samples.
func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) {
f.windowLength = windowLength
}
func (f *WindowedFilter[V, T]) GetBest() V {
return f.estimates[0].sample
}
func (f *WindowedFilter[V, T]) GetSecondBest() V {
return f.estimates[1].sample
}
func (f *WindowedFilter[V, T]) GetThirdBest() V {
return f.estimates[2].sample
}
// Updates best estimates with |sample|, and expires and updates best
// estimates as necessary.
func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) {
// Reset all estimates if they have not yet been initialized, if new sample
// is a new best, or if the newest recorded estimate is too old.
if f.comparator(f.estimates[0].sample, *new(V)) == 0 ||
f.comparator(newSample, f.estimates[0].sample) >= 0 ||
newTime-f.estimates[2].time > f.windowLength {
f.Reset(newSample, newTime)
return
}
if f.comparator(newSample, f.estimates[1].sample) >= 0 {
f.estimates[1] = entry[V, T]{newSample, newTime}
f.estimates[2] = f.estimates[1]
} else if f.comparator(newSample, f.estimates[2].sample) >= 0 {
f.estimates[2] = entry[V, T]{newSample, newTime}
}
// Expire and update estimates as necessary.
if newTime-f.estimates[0].time > f.windowLength {
// The best estimate hasn't been updated for an entire window, so promote
// second and third best estimates.
f.estimates[0] = f.estimates[1]
f.estimates[1] = f.estimates[2]
f.estimates[2] = entry[V, T]{newSample, newTime}
// Need to iterate one more time. Check if the new best estimate is
// outside the window as well, since it may also have been recorded a
// long time ago. Don't need to iterate once more since we cover that
// case at the beginning of the method.
if newTime-f.estimates[0].time > f.windowLength {
f.estimates[0] = f.estimates[1]
f.estimates[1] = f.estimates[2]
}
return
}
if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 &&
newTime-f.estimates[1].time > f.windowLength/4 {
// A quarter of the window has passed without a better sample, so the
// second-best estimate is taken from the second quarter of the window.
f.estimates[1] = entry[V, T]{newSample, newTime}
f.estimates[2] = f.estimates[1]
return
}
if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 &&
newTime-f.estimates[2].time > f.windowLength/2 {
// We've passed a half of the window without a better estimate, so take
// a third-best estimate from the second half of the window.
f.estimates[2] = entry[V, T]{newSample, newTime}
}
}
// Resets all estimates to new sample.
func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) {
f.estimates[2] = entry[V, T]{newSample, newTime}
f.estimates[1] = f.estimates[2]
f.estimates[0] = f.estimates[1]
}
func (f *WindowedFilter[V, T]) Clear() {
f.estimates = make([]entry[V, T], 3, 3)
}
================================================
FILE: core/internal/congestion/brutal/brutal.go
================================================
package brutal
import (
"fmt"
"os"
"strconv"
"time"
"github.com/apernet/hysteria/core/v2/internal/congestion/common"
"github.com/apernet/quic-go/congestion"
"github.com/apernet/quic-go/monotime"
)
const (
pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample
minSampleCount = 50
minAckRate = 0.8
congestionWindowMultiplier = 2
debugEnv = "HYSTERIA_BRUTAL_DEBUG"
debugPrintInterval = 2
)
var _ congestion.CongestionControl = &BrutalSender{}
type BrutalSender struct {
rttStats congestion.RTTStatsProvider
bps congestion.ByteCount
maxDatagramSize congestion.ByteCount
pacer *common.Pacer
pktInfoSlots [pktInfoSlotCount]pktInfo
ackRate float64
debug bool
lastAckPrintTimestamp int64
}
type pktInfo struct {
Timestamp int64
AckCount uint64
LossCount uint64
}
func NewBrutalSender(bps uint64) *BrutalSender {
debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
bs := &BrutalSender{
bps: congestion.ByteCount(bps),
maxDatagramSize: congestion.InitialPacketSize,
ackRate: 1,
debug: debug,
}
bs.pacer = common.NewPacer(func() congestion.ByteCount {
return congestion.ByteCount(float64(bs.bps) / bs.ackRate)
})
return bs
}
func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) {
b.rttStats = rttStats
}
func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time {
return b.pacer.TimeUntilSend()
}
func (b *BrutalSender) HasPacingBudget(now monotime.Time) bool {
return b.pacer.Budget(now) >= b.maxDatagramSize
}
func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {
return bytesInFlight <= b.GetCongestionWindow()
}
func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {
rtt := b.rttStats.SmoothedRTT()
if rtt <= 0 {
return 10240
}
cwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate)
if cwnd < b.maxDatagramSize {
cwnd = b.maxDatagramSize
}
return cwnd
}
func (b *BrutalSender) OnPacketSent(sentTime monotime.Time, bytesInFlight congestion.ByteCount,
packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool,
) {
b.pacer.SentPacket(sentTime, bytes)
}
func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount,
priorInFlight congestion.ByteCount, eventTime monotime.Time,
) {
// Stub
}
func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount,
priorInFlight congestion.ByteCount,
) {
// Stub
}
func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {
currentTimestamp := int64(time.Duration(eventTime) / time.Second)
slot := currentTimestamp % pktInfoSlotCount
if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets))
b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets))
} else {
// uninitialized slot or too old, reset
b.pktInfoSlots[slot].Timestamp = currentTimestamp
b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets))
b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets))
}
b.updateAckRate(currentTimestamp)
}
func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {
b.maxDatagramSize = size
b.pacer.SetMaxDatagramSize(size)
if b.debug {
b.debugPrint("SetMaxDatagramSize: %d", size)
}
}
func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
minTimestamp := currentTimestamp - pktInfoSlotCount
var ackCount, lossCount uint64
for _, info := range b.pktInfoSlots {
if info.Timestamp < minTimestamp {
continue
}
ackCount += info.AckCount
lossCount += info.LossCount
}
if ackCount+lossCount < minSampleCount {
b.ackRate = 1
if b.canPrintAckRate(currentTimestamp) {
b.lastAckPrintTimestamp = currentTimestamp
b.debugPrint("Not enough samples (total=%d, ack=%d, loss=%d, rtt=%d)",
ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())
}
return
}
rate := float64(ackCount) / float64(ackCount+lossCount)
if rate < minAckRate {
b.ackRate = minAckRate
if b.canPrintAckRate(currentTimestamp) {
b.lastAckPrintTimestamp = currentTimestamp
b.debugPrint("ACK rate too low: %.2f, clamped to %.2f (total=%d, ack=%d, loss=%d, rtt=%d)",
rate, minAckRate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())
}
return
}
b.ackRate = rate
if b.canPrintAckRate(currentTimestamp) {
b.lastAckPrintTimestamp = currentTimestamp
b.debugPrint("ACK rate: %.2f (total=%d, ack=%d, loss=%d, rtt=%d)",
rate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())
}
}
func (b *BrutalSender) InSlowStart() bool {
return false
}
func (b *BrutalSender) InRecovery() bool {
return false
}
func (b *BrutalSender) MaybeExitSlowStart() {}
func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {}
func (b *BrutalSender) canPrintAckRate(currentTimestamp int64) bool {
return b.debug && currentTimestamp-b.lastAckPrintTimestamp >= debugPrintInterval
}
func (b *BrutalSender) debugPrint(format string, a ...any) {
fmt.Printf("[BrutalSender] [%s] %s\n",
time.Now().Format("15:04:05"),
fmt.Sprintf(format, a...))
}
================================================
FILE: core/internal/congestion/common/pacer.go
================================================
package common
import (
"time"
"github.com/apernet/quic-go/congestion"
"github.com/apernet/quic-go/monotime"
)
const (
maxBurstPackets = 10
maxBurstPacingDelayMultiplier = 4
)
// Pacer implements a token bucket pacing algorithm.
type Pacer struct {
budgetAtLastSent congestion.ByteCount
maxDatagramSize congestion.ByteCount
lastSentTime monotime.Time
getBandwidth func() congestion.ByteCount // in bytes/s
}
func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer {
p := &Pacer{
budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSize,
maxDatagramSize: congestion.InitialPacketSize,
getBandwidth: getBandwidth,
}
return p
}
func (p *Pacer) SentPacket(sendTime monotime.Time, size congestion.ByteCount) {
budget := p.Budget(sendTime)
if size > budget {
p.budgetAtLastSent = 0
} else {
p.budgetAtLastSent = budget - size
}
p.lastSentTime = sendTime
}
func (p *Pacer) Budget(now monotime.Time) congestion.ByteCount {
if p.lastSentTime.IsZero() {
return p.maxBurstSize()
}
budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9
if budget < 0 { // protect against overflows
budget = congestion.ByteCount(1<<62 - 1)
}
return min(p.maxBurstSize(), budget)
}
func (p *Pacer) maxBurstSize() congestion.ByteCount {
return max(
congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9,
maxBurstPackets*p.maxDatagramSize,
)
}
// TimeUntilSend returns when the next packet should be sent.
// It returns the zero value if a packet can be sent immediately.
func (p *Pacer) TimeUntilSend() monotime.Time {
if p.budgetAtLastSent >= p.maxDatagramSize {
return 0
}
diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent)
bw := uint64(p.getBandwidth())
// We might need to round up this value.
// Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires.
d := diff / bw
// this is effectively a math.Ceil, but using only integer math
if diff%bw > 0 {
d++
}
return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond))
}
func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) {
p.maxDatagramSize = s
}
================================================
FILE: core/internal/congestion/utils.go
================================================
package congestion
import (
"github.com/apernet/hysteria/core/v2/internal/congestion/bbr"
"github.com/apernet/hysteria/core/v2/internal/congestion/brutal"
"github.com/apernet/quic-go"
)
func UseBBR(conn *quic.Conn) {
conn.SetCongestionControl(bbr.NewBbrSender(
bbr.DefaultClock{},
bbr.GetInitialPacketSize(conn.RemoteAddr()),
))
}
func UseBrutal(conn *quic.Conn, tx uint64) {
conn.SetCongestionControl(brutal.NewBrutalSender(tx))
}
================================================
FILE: core/internal/frag/frag.go
================================================
package frag
import (
"github.com/apernet/hysteria/core/v2/internal/protocol"
)
func FragUDPMessage(m *protocol.UDPMessage, maxSize int) []protocol.UDPMessage {
if m.Size() <= maxSize {
return []protocol.UDPMessage{*m}
}
fullPayload := m.Data
maxPayloadSize := maxSize - m.HeaderSize()
off := 0
fragID := uint8(0)
fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up
frags := make([]protocol.UDPMessage, fragCount)
for off < len(fullPayload) {
payloadSize := len(fullPayload) - off
if payloadSize > maxPayloadSize {
payloadSize = maxPayloadSize
}
frag := *m
frag.FragID = fragID
frag.FragCount = fragCount
frag.Data = fullPayload[off : off+payloadSize]
frags[fragID] = frag
off += payloadSize
fragID++
}
return frags
}
// Defragger handles the defragmentation of UDP messages.
// The current implementation can only handle one packet ID at a time.
// If another packet arrives before a packet has received all fragments
// in their entirety, any previous state is discarded.
type Defragger struct {
pktID uint16
frags []*protocol.UDPMessage
count uint8
size int // data size
}
func (d *Defragger) Feed(m *protocol.UDPMessage) *protocol.UDPMessage {
if m.FragCount <= 1 {
return m
}
if m.FragID >= m.FragCount {
// wtf is this?
return nil
}
if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {
// new message, clear previous state
d.pktID = m.PacketID
d.frags = make([]*protocol.UDPMessage, m.FragCount)
d.frags[m.FragID] = m
d.count = 1
d.size = len(m.Data)
} else if d.frags[m.FragID] == nil {
d.frags[m.FragID] = m
d.count++
d.size += len(m.Data)
if int(d.count) == len(d.frags) {
// all fragments received, assemble
data := make([]byte, d.size)
off := 0
for _, frag := range d.frags {
off += copy(data[off:], frag.Data)
}
m.Data = data
m.FragID = 0
m.FragCount = 1
return m
}
}
return nil
}
================================================
FILE: core/internal/frag/frag_test.go
================================================
package frag
import (
"reflect"
"testing"
"github.com/apernet/hysteria/core/v2/internal/protocol"
)
func TestFragUDPMessage(t *testing.T) {
type args struct {
m *protocol.UDPMessage
maxSize int
}
tests := []struct {
name string
args args
want []protocol.UDPMessage
}{
{
"no frag",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 123,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("hello"),
},
100,
},
[]protocol.UDPMessage{
{
SessionID: 123,
PacketID: 123,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("hello"),
},
},
},
{
"2 frags",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 123,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("hello"),
},
20,
},
[]protocol.UDPMessage{
{
SessionID: 123,
PacketID: 123,
FragID: 0,
FragCount: 2,
Addr: "test:123",
Data: []byte("hel"),
},
{
SessionID: 123,
PacketID: 123,
FragID: 1,
FragCount: 2,
Addr: "test:123",
Data: []byte("lo"),
},
},
},
{
"4 frags",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 123,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("abcdefgh"),
},
19,
},
[]protocol.UDPMessage{
{
SessionID: 123,
PacketID: 123,
FragID: 0,
FragCount: 4,
Addr: "test:123",
Data: []byte("ab"),
},
{
SessionID: 123,
PacketID: 123,
FragID: 1,
FragCount: 4,
Addr: "test:123",
Data: []byte("cd"),
},
{
SessionID: 123,
PacketID: 123,
FragID: 2,
FragCount: 4,
Addr: "test:123",
Data: []byte("ef"),
},
{
SessionID: 123,
PacketID: 123,
FragID: 3,
FragCount: 4,
Addr: "test:123",
Data: []byte("gh"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := FragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) {
t.Errorf("FragUDPMessage() = %v, want %v", got, tt.want)
}
})
}
}
func TestDefragger(t *testing.T) {
type args struct {
m *protocol.UDPMessage
}
tests := []struct {
name string
args args
want *protocol.UDPMessage
}{
{
"no frag",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("hello"),
},
},
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("hello"),
},
},
{
"frag 0 - 1/2",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 0,
FragCount: 2,
Addr: "test:123",
Data: []byte("hello "),
},
},
nil,
},
{
"frag 0 - 2/2",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 1,
FragCount: 2,
Addr: "test:123",
Data: []byte("moto"),
},
},
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("hello moto"),
},
},
{
"frag 1 - 1/3",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 0,
FragCount: 3,
Addr: "test:123",
Data: []byte("deco"),
},
},
nil,
},
{
"frag 1 - 2/3",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 1,
FragCount: 3,
Addr: "test:123",
Data: []byte("*"),
},
},
nil,
},
{
"frag 1 - 3/3",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 2,
FragCount: 3,
Addr: "test:123",
Data: []byte("27"),
},
},
&protocol.UDPMessage{
SessionID: 123,
PacketID: 987,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("deco*27"),
},
},
{
"frag 2 - 1/2",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 233,
FragID: 1,
FragCount: 2,
Addr: "test:123",
Data: []byte("shinsekai"),
},
},
nil,
},
{
"frag 3 - 2/2",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 244,
FragID: 1,
FragCount: 2,
Addr: "test:123",
Data: []byte("what???"),
},
},
nil,
},
{
"frag 2 - 2/2",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 233,
FragID: 1,
FragCount: 2,
Addr: "test:123",
Data: []byte(" annaijo"),
},
},
nil,
},
{
"invalid id",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 233,
FragID: 88,
FragCount: 2,
Addr: "test:123",
Data: []byte("shinsekai"),
},
},
nil,
},
{
"frag 2 - 1/2 re",
args{
&protocol.UDPMessage{
SessionID: 123,
PacketID: 233,
FragID: 0,
FragCount: 2,
Addr: "test:123",
Data: []byte("shinsekai"),
},
},
&protocol.UDPMessage{
SessionID: 123,
PacketID: 233,
FragID: 0,
FragCount: 1,
Addr: "test:123",
Data: []byte("shinsekai annaijo"),
},
},
}
d := &Defragger{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Feed() = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: core/internal/integration_tests/.mockery.yaml
================================================
with-expecter: true
dir: mocks
outpkg: mocks
packages:
net:
interfaces:
Conn:
config:
mockname: MockConn
github.com/apernet/hysteria/core/v2/server:
interfaces:
Outbound:
config:
mockname: MockOutbound
UDPConn:
config:
mockname: MockUDPConn
Authenticator:
config:
mockname: MockAuthenticator
EventLogger:
config:
mockname: MockEventLogger
TrafficLogger:
config:
mockname: MockTrafficLogger
RequestHook:
config:
mockname: MockRequestHook
================================================
FILE: core/internal/integration_tests/close_test.go
================================================
package integration_tests
import (
"io"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
)
// TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly.
// Closing one side of the connection should close the other side as well.
func TestClientServerTCPClose(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
serverOb := mocks.NewMockOutbound(t)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Outbound: serverOb,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
addr := "hi-and-goodbye:2333"
// Test close from client side:
// Client creates a connection, writes something, then closes it.
// Server outbound connection should write the same thing, then close.
sobConn := mocks.NewMockConn(t)
sobConnCh := make(chan struct{}) // For close signal only
sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })
sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
<-sobConnCh
return 0, io.EOF
})
sobConn.EXPECT().Write([]byte("happy")).Return(5, nil)
sobConn.EXPECT().Close().RunAndReturn(func() error {
sobConnChCloseFunc()
return nil
})
serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()
conn, err := c.TCP(addr)
assert.NoError(t, err)
_, err = conn.Write([]byte("happy"))
assert.NoError(t, err)
err = conn.Close()
assert.NoError(t, err)
time.Sleep(1 * time.Second)
mock.AssertExpectationsForObjects(t, sobConn, serverOb)
// Test close from server side:
// Client creates a connection.
// Server outbound connection reads something, then closes.
// Client connection should read the same thing, then close.
sobConn = mocks.NewMockConn(t)
sobConnCh2 := make(chan []byte, 1)
sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
d := <-sobConnCh2
if d == nil {
return 0, io.EOF
} else {
return copy(bs, d), nil
}
})
sobConn.EXPECT().Close().Return(nil)
serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()
conn, err = c.TCP(addr)
assert.NoError(t, err)
sobConnCh2 <- []byte("happy")
close(sobConnCh2)
bs, err := io.ReadAll(conn)
assert.NoError(t, err)
assert.Equal(t, "happy", string(bs))
}
// TestClientServerUDPIdleTimeout tests whether the server's UDP idle timeout works correctly.
func TestClientServerUDPIdleTimeout(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
serverOb := mocks.NewMockOutbound(t)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
eventLogger := mocks.NewMockEventLogger(t)
eventLogger.EXPECT().Connect(mock.Anything, "nobody", mock.Anything).Once()
eventLogger.EXPECT().Disconnect(mock.Anything, "nobody", mock.Anything).Maybe() // Depends on the timing, don't care
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Outbound: serverOb,
UDPIdleTimeout: 2 * time.Second,
Authenticator: auth,
EventLogger: eventLogger,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
addr := "spy.x.family:2023"
// On the client side, create a UDP session and send a packet every 1 second,
// 4 packets in total. The server should have one UDP session and receive all
// 4 packets. Then the UDP connection on the server side will receive a packet
// every 1 second, 4 packets in total. The client session should receive all
// 4 packets. Then the session will be idle for 3 seconds - should be enough
// to trigger the server's UDP idle timeout.
sobConn := mocks.NewMockUDPConn(t)
sobConnCh := make(chan []byte, 1)
sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })
sobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) {
d := <-sobConnCh
if d == nil {
return 0, "", io.EOF
} else {
return copy(bs, d), addr, nil
}
})
sobConn.EXPECT().WriteTo([]byte("happy"), addr).Return(5, nil).Times(4)
serverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once()
eventLogger.EXPECT().UDPRequest(mock.Anything, mock.Anything, uint32(1), addr).Once()
cu, err := c.UDP()
assert.NoError(t, err)
// Client sends 4 packets
for i := 0; i < 4; i++ {
err = cu.Send([]byte("happy"), addr)
assert.NoError(t, err)
time.Sleep(1 * time.Second)
}
// Client receives 4 packets
go func() {
for i := 0; i < 4; i++ {
sobConnCh <- []byte("sad")
time.Sleep(1 * time.Second)
}
}()
for i := 0; i < 4; i++ {
bs, rAddr, err := cu.Receive()
assert.NoError(t, err)
assert.Equal(t, "sad", string(bs))
assert.Equal(t, addr, rAddr)
}
// Now we wait for 3 seconds, the server should close the UDP session.
sobConn.EXPECT().Close().RunAndReturn(func() error {
sobConnChCloseFunc()
return nil
})
eventLogger.EXPECT().UDPError(mock.Anything, mock.Anything, uint32(1), nil).Once()
time.Sleep(3 * time.Second)
}
// TestClientServerClientShutdown tests whether the server can handle the client's shutdown correctly.
func TestClientServerClientShutdown(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
eventLogger := mocks.NewMockEventLogger(t)
eventLogger.EXPECT().Connect(mock.Anything, "nobody", mock.Anything).Once()
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
EventLogger: eventLogger,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
// Close the client - expect disconnect event on the server side.
// Since client.Close() sends HTTP3 ErrCodeNoError, the error should be nil.
eventLogger.EXPECT().Disconnect(mock.Anything, "nobody", nil).Once()
_ = c.Close()
time.Sleep(1 * time.Second)
}
// TestClientServerServerShutdown tests whether the client can handle the server's shutdown correctly.
func TestClientServerServerShutdown(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
QUICConfig: client.QUICConfig{
MaxIdleTimeout: 4 * time.Second,
},
})
assert.NoError(t, err)
// Close the server - expect the client to return ClosedError for both TCP & UDP calls.
_ = s.Close()
_, err = c.TCP("whatever")
_, ok := err.(errors.ClosedError)
assert.True(t, ok)
time.Sleep(1 * time.Second) // Allow some time for the error to be propagated to the UDP session manager
_, err = c.UDP()
_, ok = err.(errors.ClosedError)
assert.True(t, ok)
assert.NoError(t, c.Close())
}
================================================
FILE: core/internal/integration_tests/hook_test.go
================================================
package integration_tests
import (
"io"
"net"
"testing"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestClientServerHookTCP(t *testing.T) {
fakeEchoAddr := "hahanope:6666"
realEchoAddr := "127.0.0.1:22333"
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
hook := mocks.NewMockRequestHook(t)
hook.EXPECT().Check(false, fakeEchoAddr).Return(true).Once()
hook.EXPECT().TCP(mock.Anything, mock.Anything).RunAndReturn(func(stream server.HyStream, s *string) ([]byte, error) {
assert.Equal(t, fakeEchoAddr, *s)
// Change the address
*s = realEchoAddr
// Read the first 5 bytes and replace them with "byeee"
data := make([]byte, 5)
_, err := io.ReadFull(stream, data)
if err != nil {
return nil, err
}
assert.Equal(t, []byte("hello"), data)
return []byte("byeee"), nil
}).Once()
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
RequestHook: hook,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create TCP echo server
echoListener, err := net.Listen("tcp", realEchoAddr)
assert.NoError(t, err)
echoServer := &tcpEchoServer{Listener: echoListener}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
// Dial TCP
conn, err := c.TCP(fakeEchoAddr)
assert.NoError(t, err)
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
_, err = conn.Write(sData)
assert.NoError(t, err)
rData := make([]byte, len(sData))
_, err = io.ReadFull(conn, rData)
assert.NoError(t, err)
assert.Equal(t, []byte("byeee world"), rData)
}
func TestClientServerHookUDP(t *testing.T) {
fakeEchoAddr := "hahanope:6666"
realEchoAddr := "127.0.0.1:22333"
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
hook := mocks.NewMockRequestHook(t)
hook.EXPECT().Check(true, fakeEchoAddr).Return(true).Once()
hook.EXPECT().UDP(mock.Anything, mock.Anything).RunAndReturn(func(bytes []byte, s *string) error {
assert.Equal(t, fakeEchoAddr, *s)
assert.Equal(t, []byte("hello world"), bytes)
// Change the address
*s = realEchoAddr
return nil
}).Once()
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
RequestHook: hook,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create UDP echo server
echoConn, err := net.ListenPacket("udp", realEchoAddr)
assert.NoError(t, err)
echoServer := &udpEchoServer{Conn: echoConn}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
// Listen UDP
conn, err := c.UDP()
assert.NoError(t, err)
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
err = conn.Send(sData, fakeEchoAddr)
assert.NoError(t, err)
rData, rAddr, err := conn.Receive()
assert.NoError(t, err)
assert.Equal(t, sData, rData)
// Hook address change is transparent,
// the client should still see the fake echo address it sent packets to
assert.Equal(t, fakeEchoAddr, rAddr)
// Subsequent packets should also be sent to the real echo server
sData = []byte("never stop fighting")
err = conn.Send(sData, fakeEchoAddr)
assert.NoError(t, err)
rData, rAddr, err = conn.Receive()
assert.NoError(t, err)
assert.Equal(t, sData, rData)
assert.Equal(t, fakeEchoAddr, rAddr)
}
================================================
FILE: core/internal/integration_tests/masq_test.go
================================================
package integration_tests
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
)
// TestServerMasquerade is a test to ensure that the server behaves as a normal
// HTTP/3 server when dealing with an unauthenticated client. This is mainly to
// confirm that the server does not expose itself to active probing.
func TestServerMasquerade(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, "", uint64(0)).Return(false, "").Once()
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// QUIC connection & RoundTripper
var conn *quic.Conn
rt := &http3.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
qc, err := quic.DialAddrEarly(ctx, udpAddr.String(), tlsCfg, cfg)
if err != nil {
return nil, err
}
conn = qc
return qc, nil
},
}
defer rt.Close() // This will close the QUIC connection
// Send the bogus request
// We expect 404 (from the default handler)
req := &http.Request{
Method: http.MethodPost,
URL: &url.URL{
Scheme: "https",
Host: protocol.URLHost,
Path: protocol.URLPath,
},
Header: make(http.Header),
}
resp, err := rt.RoundTrip(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
for k := range resp.Header {
// Make sure no strange headers are sent by the server
assert.NotContains(t, k, "Hysteria")
}
buf := make([]byte, 1024)
// We send a TCP request anyway, see if we get a response
tcpStream, err := conn.OpenStream()
assert.NoError(t, err)
defer tcpStream.Close()
err = protocol.WriteTCPRequest(tcpStream, "www.google.com:443")
assert.NoError(t, err)
// We should receive nothing
_ = tcpStream.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err := tcpStream.Read(buf)
assert.Equal(t, 0, n)
nErr, ok := err.(net.Error)
assert.True(t, ok)
assert.True(t, nErr.Timeout())
}
================================================
FILE: core/internal/integration_tests/mocks/mock_Authenticator.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
)
// MockAuthenticator is an autogenerated mock type for the Authenticator type
type MockAuthenticator struct {
mock.Mock
}
type MockAuthenticator_Expecter struct {
mock *mock.Mock
}
func (_m *MockAuthenticator) EXPECT() *MockAuthenticator_Expecter {
return &MockAuthenticator_Expecter{mock: &_m.Mock}
}
// Authenticate provides a mock function with given fields: addr, auth, tx
func (_m *MockAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (bool, string) {
ret := _m.Called(addr, auth, tx)
if len(ret) == 0 {
panic("no return value specified for Authenticate")
}
var r0 bool
var r1 string
if rf, ok := ret.Get(0).(func(net.Addr, string, uint64) (bool, string)); ok {
return rf(addr, auth, tx)
}
if rf, ok := ret.Get(0).(func(net.Addr, string, uint64) bool); ok {
r0 = rf(addr, auth, tx)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(net.Addr, string, uint64) string); ok {
r1 = rf(addr, auth, tx)
} else {
r1 = ret.Get(1).(string)
}
return r0, r1
}
// MockAuthenticator_Authenticate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authenticate'
type MockAuthenticator_Authenticate_Call struct {
*mock.Call
}
// Authenticate is a helper method to define mock.On call
// - addr net.Addr
// - auth string
// - tx uint64
func (_e *MockAuthenticator_Expecter) Authenticate(addr interface{}, auth interface{}, tx interface{}) *MockAuthenticator_Authenticate_Call {
return &MockAuthenticator_Authenticate_Call{Call: _e.mock.On("Authenticate", addr, auth, tx)}
}
func (_c *MockAuthenticator_Authenticate_Call) Run(run func(addr net.Addr, auth string, tx uint64)) *MockAuthenticator_Authenticate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(net.Addr), args[1].(string), args[2].(uint64))
})
return _c
}
func (_c *MockAuthenticator_Authenticate_Call) Return(ok bool, id string) *MockAuthenticator_Authenticate_Call {
_c.Call.Return(ok, id)
return _c
}
func (_c *MockAuthenticator_Authenticate_Call) RunAndReturn(run func(net.Addr, string, uint64) (bool, string)) *MockAuthenticator_Authenticate_Call {
_c.Call.Return(run)
return _c
}
// NewMockAuthenticator creates a new instance of MockAuthenticator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockAuthenticator(t interface {
mock.TestingT
Cleanup(func())
}) *MockAuthenticator {
mock := &MockAuthenticator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/internal/integration_tests/mocks/mock_Conn.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
time "time"
)
// MockConn is an autogenerated mock type for the Conn type
type MockConn struct {
mock.Mock
}
type MockConn_Expecter struct {
mock *mock.Mock
}
func (_m *MockConn) EXPECT() *MockConn_Expecter {
return &MockConn_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockConn_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockConn_Expecter) Close() *MockConn_Close_Call {
return &MockConn_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call {
_c.Call.Return(run)
return _c
}
// LocalAddr provides a mock function with no fields
func (_m *MockConn) LocalAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LocalAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr'
type MockConn_LocalAddr_Call struct {
*mock.Call
}
// LocalAddr is a helper method to define mock.On call
func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call {
return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")}
}
func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: b
func (_m *MockConn) Read(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(b)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type MockConn_Read_Call struct {
*mock.Call
}
// Read is a helper method to define mock.On call
// - b []byte
func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call {
return &MockConn_Read_Call{Call: _e.mock.On("Read", b)}
}
func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call {
_c.Call.Return(run)
return _c
}
// RemoteAddr provides a mock function with no fields
func (_m *MockConn) RemoteAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for RemoteAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr'
type MockConn_RemoteAddr_Call struct {
*mock.Call
}
// RemoteAddr is a helper method to define mock.On call
func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call {
return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")}
}
func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call {
_c.Call.Return(run)
return _c
}
// SetDeadline provides a mock function with given fields: t
func (_m *MockConn) SetDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'
type MockConn_SetDeadline_Call struct {
*mock.Call
}
// SetDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call {
return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)}
}
func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetReadDeadline provides a mock function with given fields: t
func (_m *MockConn) SetReadDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetReadDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'
type MockConn_SetReadDeadline_Call struct {
*mock.Call
}
// SetReadDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call {
return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)}
}
func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetWriteDeadline provides a mock function with given fields: t
func (_m *MockConn) SetWriteDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetWriteDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'
type MockConn_SetWriteDeadline_Call struct {
*mock.Call
}
// SetWriteDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call {
return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)}
}
func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: b
func (_m *MockConn) Write(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(b)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockConn_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - b []byte
func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call {
return &MockConn_Write_Call{Call: _e.mock.On("Write", b)}
}
func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call {
_c.Call.Return(run)
return _c
}
// NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockConn(t interface {
mock.TestingT
Cleanup(func())
}) *MockConn {
mock := &MockConn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/internal/integration_tests/mocks/mock_EventLogger.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
)
// MockEventLogger is an autogenerated mock type for the EventLogger type
type MockEventLogger struct {
mock.Mock
}
type MockEventLogger_Expecter struct {
mock *mock.Mock
}
func (_m *MockEventLogger) EXPECT() *MockEventLogger_Expecter {
return &MockEventLogger_Expecter{mock: &_m.Mock}
}
// Connect provides a mock function with given fields: addr, id, tx
func (_m *MockEventLogger) Connect(addr net.Addr, id string, tx uint64) {
_m.Called(addr, id, tx)
}
// MockEventLogger_Connect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Connect'
type MockEventLogger_Connect_Call struct {
*mock.Call
}
// Connect is a helper method to define mock.On call
// - addr net.Addr
// - id string
// - tx uint64
func (_e *MockEventLogger_Expecter) Connect(addr interface{}, id interface{}, tx interface{}) *MockEventLogger_Connect_Call {
return &MockEventLogger_Connect_Call{Call: _e.mock.On("Connect", addr, id, tx)}
}
func (_c *MockEventLogger_Connect_Call) Run(run func(addr net.Addr, id string, tx uint64)) *MockEventLogger_Connect_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(net.Addr), args[1].(string), args[2].(uint64))
})
return _c
}
func (_c *MockEventLogger_Connect_Call) Return() *MockEventLogger_Connect_Call {
_c.Call.Return()
return _c
}
func (_c *MockEventLogger_Connect_Call) RunAndReturn(run func(net.Addr, string, uint64)) *MockEventLogger_Connect_Call {
_c.Run(run)
return _c
}
// Disconnect provides a mock function with given fields: addr, id, err
func (_m *MockEventLogger) Disconnect(addr net.Addr, id string, err error) {
_m.Called(addr, id, err)
}
// MockEventLogger_Disconnect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Disconnect'
type MockEventLogger_Disconnect_Call struct {
*mock.Call
}
// Disconnect is a helper method to define mock.On call
// - addr net.Addr
// - id string
// - err error
func (_e *MockEventLogger_Expecter) Disconnect(addr interface{}, id interface{}, err interface{}) *MockEventLogger_Disconnect_Call {
return &MockEventLogger_Disconnect_Call{Call: _e.mock.On("Disconnect", addr, id, err)}
}
func (_c *MockEventLogger_Disconnect_Call) Run(run func(addr net.Addr, id string, err error)) *MockEventLogger_Disconnect_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(net.Addr), args[1].(string), args[2].(error))
})
return _c
}
func (_c *MockEventLogger_Disconnect_Call) Return() *MockEventLogger_Disconnect_Call {
_c.Call.Return()
return _c
}
func (_c *MockEventLogger_Disconnect_Call) RunAndReturn(run func(net.Addr, string, error)) *MockEventLogger_Disconnect_Call {
_c.Run(run)
return _c
}
// TCPError provides a mock function with given fields: addr, id, reqAddr, err
func (_m *MockEventLogger) TCPError(addr net.Addr, id string, reqAddr string, err error) {
_m.Called(addr, id, reqAddr, err)
}
// MockEventLogger_TCPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPError'
type MockEventLogger_TCPError_Call struct {
*mock.Call
}
// TCPError is a helper method to define mock.On call
// - addr net.Addr
// - id string
// - reqAddr string
// - err error
func (_e *MockEventLogger_Expecter) TCPError(addr interface{}, id interface{}, reqAddr interface{}, err interface{}) *MockEventLogger_TCPError_Call {
return &MockEventLogger_TCPError_Call{Call: _e.mock.On("TCPError", addr, id, reqAddr, err)}
}
func (_c *MockEventLogger_TCPError_Call) Run(run func(addr net.Addr, id string, reqAddr string, err error)) *MockEventLogger_TCPError_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(net.Addr), args[1].(string), args[2].(string), args[3].(error))
})
return _c
}
func (_c *MockEventLogger_TCPError_Call) Return() *MockEventLogger_TCPError_Call {
_c.Call.Return()
return _c
}
func (_c *MockEventLogger_TCPError_Call) RunAndReturn(run func(net.Addr, string, string, error)) *MockEventLogger_TCPError_Call {
_c.Run(run)
return _c
}
// TCPRequest provides a mock function with given fields: addr, id, reqAddr
func (_m *MockEventLogger) TCPRequest(addr net.Addr, id string, reqAddr string) {
_m.Called(addr, id, reqAddr)
}
// MockEventLogger_TCPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCPRequest'
type MockEventLogger_TCPRequest_Call struct {
*mock.Call
}
// TCPRequest is a helper method to define mock.On call
// - addr net.Addr
// - id string
// - reqAddr string
func (_e *MockEventLogger_Expecter) TCPRequest(addr interface{}, id interface{}, reqAddr interface{}) *MockEventLogger_TCPRequest_Call {
return &MockEventLogger_TCPRequest_Call{Call: _e.mock.On("TCPRequest", addr, id, reqAddr)}
}
func (_c *MockEventLogger_TCPRequest_Call) Run(run func(addr net.Addr, id string, reqAddr string)) *MockEventLogger_TCPRequest_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(net.Addr), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockEventLogger_TCPRequest_Call) Return() *MockEventLogger_TCPRequest_Call {
_c.Call.Return()
return _c
}
func (_c *MockEventLogger_TCPRequest_Call) RunAndReturn(run func(net.Addr, string, string)) *MockEventLogger_TCPRequest_Call {
_c.Run(run)
return _c
}
// UDPError provides a mock function with given fields: addr, id, sessionID, err
func (_m *MockEventLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) {
_m.Called(addr, id, sessionID, err)
}
// MockEventLogger_UDPError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPError'
type MockEventLogger_UDPError_Call struct {
*mock.Call
}
// UDPError is a helper method to define mock.On call
// - addr net.Addr
// - id string
// - sessionID uint32
// - err error
func (_e *MockEventLogger_Expecter) UDPError(addr interface{}, id interface{}, sessionID interface{}, err interface{}) *MockEventLogger_UDPError_Call {
return &MockEventLogger_UDPError_Call{Call: _e.mock.On("UDPError", addr, id, sessionID, err)}
}
func (_c *MockEventLogger_UDPError_Call) Run(run func(addr net.Addr, id string, sessionID uint32, err error)) *MockEventLogger_UDPError_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(error))
})
return _c
}
func (_c *MockEventLogger_UDPError_Call) Return() *MockEventLogger_UDPError_Call {
_c.Call.Return()
return _c
}
func (_c *MockEventLogger_UDPError_Call) RunAndReturn(run func(net.Addr, string, uint32, error)) *MockEventLogger_UDPError_Call {
_c.Run(run)
return _c
}
// UDPRequest provides a mock function with given fields: addr, id, sessionID, reqAddr
func (_m *MockEventLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) {
_m.Called(addr, id, sessionID, reqAddr)
}
// MockEventLogger_UDPRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDPRequest'
type MockEventLogger_UDPRequest_Call struct {
*mock.Call
}
// UDPRequest is a helper method to define mock.On call
// - addr net.Addr
// - id string
// - sessionID uint32
// - reqAddr string
func (_e *MockEventLogger_Expecter) UDPRequest(addr interface{}, id interface{}, sessionID interface{}, reqAddr interface{}) *MockEventLogger_UDPRequest_Call {
return &MockEventLogger_UDPRequest_Call{Call: _e.mock.On("UDPRequest", addr, id, sessionID, reqAddr)}
}
func (_c *MockEventLogger_UDPRequest_Call) Run(run func(addr net.Addr, id string, sessionID uint32, reqAddr string)) *MockEventLogger_UDPRequest_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(net.Addr), args[1].(string), args[2].(uint32), args[3].(string))
})
return _c
}
func (_c *MockEventLogger_UDPRequest_Call) Return() *MockEventLogger_UDPRequest_Call {
_c.Call.Return()
return _c
}
func (_c *MockEventLogger_UDPRequest_Call) RunAndReturn(run func(net.Addr, string, uint32, string)) *MockEventLogger_UDPRequest_Call {
_c.Run(run)
return _c
}
// NewMockEventLogger creates a new instance of MockEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockEventLogger(t interface {
mock.TestingT
Cleanup(func())
}) *MockEventLogger {
mock := &MockEventLogger{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/internal/integration_tests/mocks/mock_Outbound.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
server "github.com/apernet/hysteria/core/v2/server"
)
// MockOutbound is an autogenerated mock type for the Outbound type
type MockOutbound struct {
mock.Mock
}
type MockOutbound_Expecter struct {
mock *mock.Mock
}
func (_m *MockOutbound) EXPECT() *MockOutbound_Expecter {
return &MockOutbound_Expecter{mock: &_m.Mock}
}
// TCP provides a mock function with given fields: reqAddr
func (_m *MockOutbound) TCP(reqAddr string) (net.Conn, error) {
ret := _m.Called(reqAddr)
if len(ret) == 0 {
panic("no return value specified for TCP")
}
var r0 net.Conn
var r1 error
if rf, ok := ret.Get(0).(func(string) (net.Conn, error)); ok {
return rf(reqAddr)
}
if rf, ok := ret.Get(0).(func(string) net.Conn); ok {
r0 = rf(reqAddr)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Conn)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(reqAddr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP'
type MockOutbound_TCP_Call struct {
*mock.Call
}
// TCP is a helper method to define mock.On call
// - reqAddr string
func (_e *MockOutbound_Expecter) TCP(reqAddr interface{}) *MockOutbound_TCP_Call {
return &MockOutbound_TCP_Call{Call: _e.mock.On("TCP", reqAddr)}
}
func (_c *MockOutbound_TCP_Call) Run(run func(reqAddr string)) *MockOutbound_TCP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *MockOutbound_TCP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockOutbound_TCP_Call) RunAndReturn(run func(string) (net.Conn, error)) *MockOutbound_TCP_Call {
_c.Call.Return(run)
return _c
}
// UDP provides a mock function with given fields: reqAddr
func (_m *MockOutbound) UDP(reqAddr string) (server.UDPConn, error) {
ret := _m.Called(reqAddr)
if len(ret) == 0 {
panic("no return value specified for UDP")
}
var r0 server.UDPConn
var r1 error
if rf, ok := ret.Get(0).(func(string) (server.UDPConn, error)); ok {
return rf(reqAddr)
}
if rf, ok := ret.Get(0).(func(string) server.UDPConn); ok {
r0 = rf(reqAddr)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(server.UDPConn)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(reqAddr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'
type MockOutbound_UDP_Call struct {
*mock.Call
}
// UDP is a helper method to define mock.On call
// - reqAddr string
func (_e *MockOutbound_Expecter) UDP(reqAddr interface{}) *MockOutbound_UDP_Call {
return &MockOutbound_UDP_Call{Call: _e.mock.On("UDP", reqAddr)}
}
func (_c *MockOutbound_UDP_Call) Run(run func(reqAddr string)) *MockOutbound_UDP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockOutbound_UDP_Call) Return(_a0 server.UDPConn, _a1 error) *MockOutbound_UDP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockOutbound_UDP_Call) RunAndReturn(run func(string) (server.UDPConn, error)) *MockOutbound_UDP_Call {
_c.Call.Return(run)
return _c
}
// NewMockOutbound creates a new instance of MockOutbound. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockOutbound(t interface {
mock.TestingT
Cleanup(func())
}) *MockOutbound {
mock := &MockOutbound{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/internal/integration_tests/mocks/mock_RequestHook.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
server "github.com/apernet/hysteria/core/v2/server"
mock "github.com/stretchr/testify/mock"
)
// MockRequestHook is an autogenerated mock type for the RequestHook type
type MockRequestHook struct {
mock.Mock
}
type MockRequestHook_Expecter struct {
mock *mock.Mock
}
func (_m *MockRequestHook) EXPECT() *MockRequestHook_Expecter {
return &MockRequestHook_Expecter{mock: &_m.Mock}
}
// Check provides a mock function with given fields: isUDP, reqAddr
func (_m *MockRequestHook) Check(isUDP bool, reqAddr string) bool {
ret := _m.Called(isUDP, reqAddr)
if len(ret) == 0 {
panic("no return value specified for Check")
}
var r0 bool
if rf, ok := ret.Get(0).(func(bool, string) bool); ok {
r0 = rf(isUDP, reqAddr)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockRequestHook_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check'
type MockRequestHook_Check_Call struct {
*mock.Call
}
// Check is a helper method to define mock.On call
// - isUDP bool
// - reqAddr string
func (_e *MockRequestHook_Expecter) Check(isUDP interface{}, reqAddr interface{}) *MockRequestHook_Check_Call {
return &MockRequestHook_Check_Call{Call: _e.mock.On("Check", isUDP, reqAddr)}
}
func (_c *MockRequestHook_Check_Call) Run(run func(isUDP bool, reqAddr string)) *MockRequestHook_Check_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(bool), args[1].(string))
})
return _c
}
func (_c *MockRequestHook_Check_Call) Return(_a0 bool) *MockRequestHook_Check_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRequestHook_Check_Call) RunAndReturn(run func(bool, string) bool) *MockRequestHook_Check_Call {
_c.Call.Return(run)
return _c
}
// TCP provides a mock function with given fields: stream, reqAddr
func (_m *MockRequestHook) TCP(stream server.HyStream, reqAddr *string) ([]byte, error) {
ret := _m.Called(stream, reqAddr)
if len(ret) == 0 {
panic("no return value specified for TCP")
}
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func(server.HyStream, *string) ([]byte, error)); ok {
return rf(stream, reqAddr)
}
if rf, ok := ret.Get(0).(func(server.HyStream, *string) []byte); ok {
r0 = rf(stream, reqAddr)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func(server.HyStream, *string) error); ok {
r1 = rf(stream, reqAddr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockRequestHook_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP'
type MockRequestHook_TCP_Call struct {
*mock.Call
}
// TCP is a helper method to define mock.On call
// - stream server.HyStream
// - reqAddr *string
func (_e *MockRequestHook_Expecter) TCP(stream interface{}, reqAddr interface{}) *MockRequestHook_TCP_Call {
return &MockRequestHook_TCP_Call{Call: _e.mock.On("TCP", stream, reqAddr)}
}
func (_c *MockRequestHook_TCP_Call) Run(run func(stream server.HyStream, reqAddr *string)) *MockRequestHook_TCP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(server.HyStream), args[1].(*string))
})
return _c
}
func (_c *MockRequestHook_TCP_Call) Return(_a0 []byte, _a1 error) *MockRequestHook_TCP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockRequestHook_TCP_Call) RunAndReturn(run func(server.HyStream, *string) ([]byte, error)) *MockRequestHook_TCP_Call {
_c.Call.Return(run)
return _c
}
// UDP provides a mock function with given fields: data, reqAddr
func (_m *MockRequestHook) UDP(data []byte, reqAddr *string) error {
ret := _m.Called(data, reqAddr)
if len(ret) == 0 {
panic("no return value specified for UDP")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, *string) error); ok {
r0 = rf(data, reqAddr)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockRequestHook_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'
type MockRequestHook_UDP_Call struct {
*mock.Call
}
// UDP is a helper method to define mock.On call
// - data []byte
// - reqAddr *string
func (_e *MockRequestHook_Expecter) UDP(data interface{}, reqAddr interface{}) *MockRequestHook_UDP_Call {
return &MockRequestHook_UDP_Call{Call: _e.mock.On("UDP", data, reqAddr)}
}
func (_c *MockRequestHook_UDP_Call) Run(run func(data []byte, reqAddr *string)) *MockRequestHook_UDP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(*string))
})
return _c
}
func (_c *MockRequestHook_UDP_Call) Return(_a0 error) *MockRequestHook_UDP_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRequestHook_UDP_Call) RunAndReturn(run func([]byte, *string) error) *MockRequestHook_UDP_Call {
_c.Call.Return(run)
return _c
}
// NewMockRequestHook creates a new instance of MockRequestHook. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockRequestHook(t interface {
mock.TestingT
Cleanup(func())
}) *MockRequestHook {
mock := &MockRequestHook{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/internal/integration_tests/mocks/mock_TrafficLogger.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import (
server "github.com/apernet/hysteria/core/v2/server"
mock "github.com/stretchr/testify/mock"
)
// MockTrafficLogger is an autogenerated mock type for the TrafficLogger type
type MockTrafficLogger struct {
mock.Mock
}
type MockTrafficLogger_Expecter struct {
mock *mock.Mock
}
func (_m *MockTrafficLogger) EXPECT() *MockTrafficLogger_Expecter {
return &MockTrafficLogger_Expecter{mock: &_m.Mock}
}
// LogOnlineState provides a mock function with given fields: id, online
func (_m *MockTrafficLogger) LogOnlineState(id string, online bool) {
_m.Called(id, online)
}
// MockTrafficLogger_LogOnlineState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogOnlineState'
type MockTrafficLogger_LogOnlineState_Call struct {
*mock.Call
}
// LogOnlineState is a helper method to define mock.On call
// - id string
// - online bool
func (_e *MockTrafficLogger_Expecter) LogOnlineState(id interface{}, online interface{}) *MockTrafficLogger_LogOnlineState_Call {
return &MockTrafficLogger_LogOnlineState_Call{Call: _e.mock.On("LogOnlineState", id, online)}
}
func (_c *MockTrafficLogger_LogOnlineState_Call) Run(run func(id string, online bool)) *MockTrafficLogger_LogOnlineState_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockTrafficLogger_LogOnlineState_Call) Return() *MockTrafficLogger_LogOnlineState_Call {
_c.Call.Return()
return _c
}
func (_c *MockTrafficLogger_LogOnlineState_Call) RunAndReturn(run func(string, bool)) *MockTrafficLogger_LogOnlineState_Call {
_c.Run(run)
return _c
}
// LogTraffic provides a mock function with given fields: id, tx, rx
func (_m *MockTrafficLogger) LogTraffic(id string, tx uint64, rx uint64) bool {
ret := _m.Called(id, tx, rx)
if len(ret) == 0 {
panic("no return value specified for LogTraffic")
}
var r0 bool
if rf, ok := ret.Get(0).(func(string, uint64, uint64) bool); ok {
r0 = rf(id, tx, rx)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockTrafficLogger_LogTraffic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogTraffic'
type MockTrafficLogger_LogTraffic_Call struct {
*mock.Call
}
// LogTraffic is a helper method to define mock.On call
// - id string
// - tx uint64
// - rx uint64
func (_e *MockTrafficLogger_Expecter) LogTraffic(id interface{}, tx interface{}, rx interface{}) *MockTrafficLogger_LogTraffic_Call {
return &MockTrafficLogger_LogTraffic_Call{Call: _e.mock.On("LogTraffic", id, tx, rx)}
}
func (_c *MockTrafficLogger_LogTraffic_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_LogTraffic_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(uint64), args[2].(uint64))
})
return _c
}
func (_c *MockTrafficLogger_LogTraffic_Call) Return(ok bool) *MockTrafficLogger_LogTraffic_Call {
_c.Call.Return(ok)
return _c
}
func (_c *MockTrafficLogger_LogTraffic_Call) RunAndReturn(run func(string, uint64, uint64) bool) *MockTrafficLogger_LogTraffic_Call {
_c.Call.Return(run)
return _c
}
// TraceStream provides a mock function with given fields: stream, stats
func (_m *MockTrafficLogger) TraceStream(stream server.HyStream, stats *server.StreamStats) {
_m.Called(stream, stats)
}
// MockTrafficLogger_TraceStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TraceStream'
type MockTrafficLogger_TraceStream_Call struct {
*mock.Call
}
// TraceStream is a helper method to define mock.On call
// - stream server.HyStream
// - stats *server.StreamStats
func (_e *MockTrafficLogger_Expecter) TraceStream(stream interface{}, stats interface{}) *MockTrafficLogger_TraceStream_Call {
return &MockTrafficLogger_TraceStream_Call{Call: _e.mock.On("TraceStream", stream, stats)}
}
func (_c *MockTrafficLogger_TraceStream_Call) Run(run func(stream server.HyStream, stats *server.StreamStats)) *MockTrafficLogger_TraceStream_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(server.HyStream), args[1].(*server.StreamStats))
})
return _c
}
func (_c *MockTrafficLogger_TraceStream_Call) Return() *MockTrafficLogger_TraceStream_Call {
_c.Call.Return()
return _c
}
func (_c *MockTrafficLogger_TraceStream_Call) RunAndReturn(run func(server.HyStream, *server.StreamStats)) *MockTrafficLogger_TraceStream_Call {
_c.Run(run)
return _c
}
// UntraceStream provides a mock function with given fields: stream
func (_m *MockTrafficLogger) UntraceStream(stream server.HyStream) {
_m.Called(stream)
}
// MockTrafficLogger_UntraceStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UntraceStream'
type MockTrafficLogger_UntraceStream_Call struct {
*mock.Call
}
// UntraceStream is a helper method to define mock.On call
// - stream server.HyStream
func (_e *MockTrafficLogger_Expecter) UntraceStream(stream interface{}) *MockTrafficLogger_UntraceStream_Call {
return &MockTrafficLogger_UntraceStream_Call{Call: _e.mock.On("UntraceStream", stream)}
}
func (_c *MockTrafficLogger_UntraceStream_Call) Run(run func(stream server.HyStream)) *MockTrafficLogger_UntraceStream_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(server.HyStream))
})
return _c
}
func (_c *MockTrafficLogger_UntraceStream_Call) Return() *MockTrafficLogger_UntraceStream_Call {
_c.Call.Return()
return _c
}
func (_c *MockTrafficLogger_UntraceStream_Call) RunAndReturn(run func(server.HyStream)) *MockTrafficLogger_UntraceStream_Call {
_c.Run(run)
return _c
}
// NewMockTrafficLogger creates a new instance of MockTrafficLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockTrafficLogger(t interface {
mock.TestingT
Cleanup(func())
}) *MockTrafficLogger {
mock := &MockTrafficLogger{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/internal/integration_tests/mocks/mock_UDPConn.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// MockUDPConn is an autogenerated mock type for the UDPConn type
type MockUDPConn struct {
mock.Mock
}
type MockUDPConn_Expecter struct {
mock *mock.Mock
}
func (_m *MockUDPConn) EXPECT() *MockUDPConn_Expecter {
return &MockUDPConn_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockUDPConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockUDPConn_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockUDPConn_Expecter) Close() *MockUDPConn_Close_Call {
return &MockUDPConn_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockUDPConn_Close_Call) Run(run func()) *MockUDPConn_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockUDPConn_Close_Call) Return(_a0 error) *MockUDPConn_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockUDPConn_Close_Call) RunAndReturn(run func() error) *MockUDPConn_Close_Call {
_c.Call.Return(run)
return _c
}
// ReadFrom provides a mock function with given fields: b
func (_m *MockUDPConn) ReadFrom(b []byte) (int, string, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for ReadFrom")
}
var r0 int
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) string); ok {
r1 = rf(b)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func([]byte) error); ok {
r2 = rf(b)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom'
type MockUDPConn_ReadFrom_Call struct {
*mock.Call
}
// ReadFrom is a helper method to define mock.On call
// - b []byte
func (_e *MockUDPConn_Expecter) ReadFrom(b interface{}) *MockUDPConn_ReadFrom_Call {
return &MockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)}
}
func (_c *MockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *MockUDPConn_ReadFrom_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *MockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *MockUDPConn_ReadFrom_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *MockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *MockUDPConn_ReadFrom_Call {
_c.Call.Return(run)
return _c
}
// WriteTo provides a mock function with given fields: b, addr
func (_m *MockUDPConn) WriteTo(b []byte, addr string) (int, error) {
ret := _m.Called(b, addr)
if len(ret) == 0 {
panic("no return value specified for WriteTo")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok {
return rf(b, addr)
}
if rf, ok := ret.Get(0).(func([]byte, string) int); ok {
r0 = rf(b, addr)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte, string) error); ok {
r1 = rf(b, addr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo'
type MockUDPConn_WriteTo_Call struct {
*mock.Call
}
// WriteTo is a helper method to define mock.On call
// - b []byte
// - addr string
func (_e *MockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *MockUDPConn_WriteTo_Call {
return &MockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)}
}
func (_c *MockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *MockUDPConn_WriteTo_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(string))
})
return _c
}
func (_c *MockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *MockUDPConn_WriteTo_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *MockUDPConn_WriteTo_Call {
_c.Call.Return(run)
return _c
}
// NewMockUDPConn creates a new instance of MockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockUDPConn(t interface {
mock.TestingT
Cleanup(func())
}) *MockUDPConn {
mock := &MockUDPConn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/internal/integration_tests/smoke_test.go
================================================
package integration_tests
import (
"io"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/apernet/hysteria/core/v2/client"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
)
// Smoke tests that act as a sanity check for client & server to ensure they can talk to each other correctly.
// TestClientNoServer tests how the client handles a server address it cannot connect to.
// NewClient should return a ConnectError.
func TestClientNoServer(t *testing.T) {
c, _, err := client.NewClient(&client.Config{
ServerAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 55666},
})
assert.Nil(t, c)
_, ok := err.(coreErrs.ConnectError)
assert.True(t, ok)
}
// TestClientServerBadAuth tests two things:
// - The server uses Authenticator when a client connects.
// - How the client handles failed authentication.
func TestClientServerBadAuth(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, "badpassword", uint64(0)).Return(false, "").Once()
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
Auth: "badpassword",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.Nil(t, c)
_, ok := err.(coreErrs.AuthError)
assert.True(t, ok)
}
// TestClientServerUDPDisabled tests how the client handles a server that does not support UDP.
// UDP should return a DialError.
func TestClientServerUDPDisabled(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
DisableUDP: true,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
conn, err := c.UDP()
assert.Nil(t, conn)
_, ok := err.(coreErrs.DialError)
assert.True(t, ok)
}
// TestClientServerTCPEcho tests TCP forwarding using a TCP echo server.
func TestClientServerTCPEcho(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create TCP echo server
echoAddr := "127.0.0.1:22333"
echoListener, err := net.Listen("tcp", echoAddr)
assert.NoError(t, err)
echoServer := &tcpEchoServer{Listener: echoListener}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
// Dial TCP
conn, err := c.TCP(echoAddr)
assert.NoError(t, err)
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
_, err = conn.Write(sData)
assert.NoError(t, err)
rData := make([]byte, len(sData))
_, err = io.ReadFull(conn, rData)
assert.NoError(t, err)
assert.Equal(t, sData, rData)
}
// TestClientServerUDPEcho tests UDP forwarding using a UDP echo server.
func TestClientServerUDPEcho(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create UDP echo server
echoAddr := "127.0.0.1:22333"
echoConn, err := net.ListenPacket("udp", echoAddr)
assert.NoError(t, err)
echoServer := &udpEchoServer{Conn: echoConn}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
// Listen UDP
conn, err := c.UDP()
assert.NoError(t, err)
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
err = conn.Send(sData, echoAddr)
assert.NoError(t, err)
rData, rAddr, err := conn.Receive()
assert.NoError(t, err)
assert.Equal(t, sData, rData)
assert.Equal(t, echoAddr, rAddr)
}
// TestClientServerHandshakeInfo tests that the client returns the correct handshake info.
func TestClientServerHandshakeInfo(t *testing.T) {
// Create server 1, UDP enabled, unlimited bandwidth
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
go s.Serve()
// Create client 1, with specified tx bandwidth
c, info, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
BandwidthConfig: client.BandwidthConfig{
MaxTx: 123456,
},
})
assert.NoError(t, err)
assert.Equal(t, &client.HandshakeInfo{
UDPEnabled: true,
Tx: 123456,
}, info)
// Close server 1 and client 1
_ = s.Close()
_ = c.Close()
// Create server 2, UDP disabled, limited rx bandwidth
udpConn, udpAddr, err = serverConn()
assert.NoError(t, err)
s, err = server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
BandwidthConfig: server.BandwidthConfig{
MaxRx: 100000,
},
DisableUDP: true,
Authenticator: auth,
})
assert.NoError(t, err)
go s.Serve()
// Create client 2, with specified tx bandwidth
c, info, err = client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
BandwidthConfig: client.BandwidthConfig{
MaxTx: 123456,
},
})
assert.NoError(t, err)
assert.Equal(t, &client.HandshakeInfo{
UDPEnabled: false,
Tx: 100000,
}, info)
// Close server 2 and client 2
_ = s.Close()
_ = c.Close()
// Create server 3, UDP enabled, ignore client bandwidth
udpConn, udpAddr, err = serverConn()
assert.NoError(t, err)
s, err = server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
IgnoreClientBandwidth: true,
Authenticator: auth,
})
assert.NoError(t, err)
go s.Serve()
// Create client 3, with specified tx bandwidth
c, info, err = client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
BandwidthConfig: client.BandwidthConfig{
MaxTx: 123456,
},
})
assert.NoError(t, err)
assert.Equal(t, &client.HandshakeInfo{
UDPEnabled: true,
Tx: 0,
}, info)
// Close server 3 and client 3
_ = s.Close()
_ = c.Close()
}
================================================
FILE: core/internal/integration_tests/stress_test.go
================================================
package integration_tests
import (
"context"
"crypto/rand"
"fmt"
"io"
"net"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/time/rate"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
)
type tcpStressor struct {
DialFunc func() (net.Conn, error)
Size int
Parallel int
Iterations int
}
func (s *tcpStressor) Run(t *testing.T) {
// Make some random data
sData := make([]byte, s.Size)
_, err := rand.Read(sData)
assert.NoError(t, err)
// Run iterations
for i := 0; i < s.Iterations; i++ {
var wg sync.WaitGroup
errChan := make(chan error, s.Parallel)
for j := 0; j < s.Parallel; j++ {
wg.Add(1)
go func() {
defer wg.Done()
conn, err := s.DialFunc()
if err != nil {
errChan <- err
return
}
defer conn.Close()
go conn.Write(sData)
rData := make([]byte, len(sData))
_, err = io.ReadFull(conn, rData)
if err != nil {
errChan <- err
return
}
}()
}
wg.Wait()
assert.Empty(t, errChan)
}
}
type udpStressor struct {
ListenFunc func() (client.HyUDPConn, error)
ServerAddr string
Size int
Count int
Parallel int
Iterations int
}
func (s *udpStressor) Run(t *testing.T) {
// Make some random data
sData := make([]byte, s.Size)
_, err := rand.Read(sData)
assert.NoError(t, err)
// Due to UDP's unreliability, we need to limit the rate of sending
// to reduce packet loss. This is hardcoded to 1 MiB/s for now.
limiter := rate.NewLimiter(1048576, 1048576)
// Run iterations
for i := 0; i < s.Iterations; i++ {
var wg sync.WaitGroup
errChan := make(chan error, s.Parallel)
for j := 0; j < s.Parallel; j++ {
wg.Add(1)
go func() {
defer wg.Done()
conn, err := s.ListenFunc()
if err != nil {
errChan <- err
return
}
defer conn.Close()
go func() {
// Sending routine
for i := 0; i < s.Count; i++ {
_ = limiter.WaitN(context.Background(), len(sData))
_ = conn.Send(sData, s.ServerAddr)
}
}()
minCount := s.Count * 8 / 10 // Tolerate 20% packet loss
for i := 0; i < minCount; i++ {
rData, _, err := conn.Receive()
if err != nil {
errChan <- err
return
}
if len(rData) != len(sData) {
errChan <- fmt.Errorf("incomplete data received: %d/%d bytes", len(rData), len(sData))
return
}
}
}()
}
wg.Wait()
assert.Empty(t, errChan)
}
}
func TestClientServerTCPStress(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create TCP echo server
echoAddr := "127.0.0.1:22333"
echoListener, err := net.Listen("tcp", echoAddr)
assert.NoError(t, err)
echoServer := &tcpEchoServer{Listener: echoListener}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
dialFunc := func() (net.Conn, error) {
return c.TCP(echoAddr)
}
t.Run("Single 500m", (&tcpStressor{DialFunc: dialFunc, Size: 524288000, Parallel: 1, Iterations: 1}).Run)
t.Run("Sequential 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1, Iterations: 1000}).Run)
t.Run("Sequential 10000x100k", (&tcpStressor{DialFunc: dialFunc, Size: 102400, Parallel: 1, Iterations: 10000}).Run)
t.Run("Parallel 100x10m", (&tcpStressor{DialFunc: dialFunc, Size: 10485760, Parallel: 100, Iterations: 1}).Run)
t.Run("Parallel 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1000, Iterations: 1}).Run)
}
func TestClientServerUDPStress(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create UDP echo server
echoAddr := "127.0.0.1:22333"
echoConn, err := net.ListenPacket("udp", echoAddr)
assert.NoError(t, err)
echoServer := &udpEchoServer{Conn: echoConn}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
t.Run("Single 1000x100b", (&udpStressor{
ListenFunc: c.UDP,
ServerAddr: echoAddr,
Size: 100,
Count: 1000,
Parallel: 1,
Iterations: 1,
}).Run)
t.Run("Single 1000x3k", (&udpStressor{
ListenFunc: c.UDP,
ServerAddr: echoAddr,
Size: 3000,
Count: 1000,
Parallel: 1,
Iterations: 1,
}).Run)
t.Run("5 Sequential 1000x100b", (&udpStressor{
ListenFunc: c.UDP,
ServerAddr: echoAddr,
Size: 100,
Count: 1000,
Parallel: 1,
Iterations: 5,
}).Run)
t.Run("5 Sequential 200x3k", (&udpStressor{
ListenFunc: c.UDP,
ServerAddr: echoAddr,
Size: 3000,
Count: 200,
Parallel: 1,
Iterations: 5,
}).Run)
t.Run("2 Sequential 5 Parallel 1000x100b", (&udpStressor{
ListenFunc: c.UDP,
ServerAddr: echoAddr,
Size: 100,
Count: 1000,
Parallel: 5,
Iterations: 2,
}).Run)
t.Run("2 Sequential 5 Parallel 200x3k", (&udpStressor{
ListenFunc: c.UDP,
ServerAddr: echoAddr,
Size: 3000,
Count: 200,
Parallel: 5,
Iterations: 2,
}).Run)
}
================================================
FILE: core/internal/integration_tests/test.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL
BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM
EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3
DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy
MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE
CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI
hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4
ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W
DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW
CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf
jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N
o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v
M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf
1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc
luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4
FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD
UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD
OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7
ydYKuI8=
-----END CERTIFICATE-----
================================================
FILE: core/internal/integration_tests/test.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR
2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr
6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc
L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2
wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C
LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI
MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+
FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U
64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX
erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu
1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW
T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA
g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA
o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO
Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY
ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V
rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k
AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI
j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0
jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2
ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g
uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+
menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2
kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB
T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ
-----END RSA PRIVATE KEY-----
================================================
FILE: core/internal/integration_tests/trafficlogger_test.go
================================================
package integration_tests
import (
"io"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
)
// TestClientServerTrafficLoggerTCP tests that the traffic logger is correctly called for TCP connections,
// and that the client is disconnected when the traffic logger returns false.
func TestClientServerTrafficLoggerTCP(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
serverOb := mocks.NewMockOutbound(t)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
trafficLogger := mocks.NewMockTrafficLogger(t)
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Outbound: serverOb,
Authenticator: auth,
TrafficLogger: trafficLogger,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create client
trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once()
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
addr := "dontcare.cc:4455"
sobConn := mocks.NewMockConn(t)
sobConnCh := make(chan []byte, 1)
sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })
sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
b := <-sobConnCh
if b == nil {
return 0, io.EOF
} else {
return copy(bs, b), nil
}
})
sobConn.EXPECT().Close().RunAndReturn(func() error {
sobConnChCloseFunc()
return nil
})
serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()
trafficLogger.EXPECT().TraceStream(mock.Anything, mock.Anything).Return().Once()
conn, err := c.TCP(addr)
assert.NoError(t, err)
// Client reads from server
trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(11)).Return(true).Once()
sobConnCh <- []byte("knock knock")
buf := make([]byte, 100)
n, err := conn.Read(buf)
assert.NoError(t, err)
assert.Equal(t, 11, n)
assert.Equal(t, "knock knock", string(buf[:n]))
// Client writes to server
trafficLogger.EXPECT().LogTraffic("nobody", uint64(12), uint64(0)).Return(true).Once()
sobConn.EXPECT().Write([]byte("who is there")).Return(12, nil).Once()
n, err = conn.Write([]byte("who is there"))
assert.NoError(t, err)
assert.Equal(t, 12, n)
time.Sleep(1 * time.Second) // Need some time for the server to receive the data
// Client reads from server again but blocked
trafficLogger.EXPECT().UntraceStream(mock.Anything).Return().Once()
trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once()
trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once()
sobConnCh <- []byte("nope")
n, err = conn.Read(buf)
assert.Zero(t, n)
assert.Error(t, err)
// The client should be disconnected
_, err = c.TCP("whatever")
assert.Error(t, err)
}
// TestClientServerTrafficLoggerUDP tests that the traffic logger is correctly called for UDP sessions,
// and that the client is disconnected when the traffic logger returns false.
func TestClientServerTrafficLoggerUDP(t *testing.T) {
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
serverOb := mocks.NewMockOutbound(t)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
trafficLogger := mocks.NewMockTrafficLogger(t)
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Outbound: serverOb,
Authenticator: auth,
TrafficLogger: trafficLogger,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create client
trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once()
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
addr := "shady.org:43211"
sobConn := mocks.NewMockUDPConn(t)
sobConnCh := make(chan []byte, 1)
sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })
sobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) {
b := <-sobConnCh
if b == nil {
return 0, "", io.EOF
} else {
return copy(bs, b), addr, nil
}
})
sobConn.EXPECT().Close().RunAndReturn(func() error {
sobConnChCloseFunc()
return nil
})
serverOb.EXPECT().UDP(addr).Return(sobConn, nil).Once()
conn, err := c.UDP()
assert.NoError(t, err)
// Client writes to server
trafficLogger.EXPECT().LogTraffic("nobody", uint64(9), uint64(0)).Return(true).Once()
sobConn.EXPECT().WriteTo([]byte("small sad"), addr).Return(9, nil).Once()
err = conn.Send([]byte("small sad"), addr)
assert.NoError(t, err)
time.Sleep(1 * time.Second) // Need some time for the server to receive the data
// Client reads from server
trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(7)).Return(true).Once()
sobConnCh <- []byte("big mad")
bs, rAddr, err := conn.Receive()
assert.NoError(t, err)
assert.Equal(t, rAddr, addr)
assert.Equal(t, "big mad", string(bs))
// Client reads from server again but blocked
trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once()
trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once()
sobConnCh <- []byte("nope")
bs, rAddr, err = conn.Receive()
assert.Equal(t, err, io.EOF)
assert.Empty(t, rAddr)
assert.Empty(t, bs)
// The client should be disconnected
_, err = c.UDP()
assert.Error(t, err)
}
================================================
FILE: core/internal/integration_tests/utils_test.go
================================================
package integration_tests
import (
"crypto/tls"
"io"
"net"
"github.com/apernet/hysteria/core/v2/server"
)
// This file provides utilities for the integration tests.
const (
testCertFile = "test.crt"
testKeyFile = "test.key"
)
func serverTLSConfig() server.TLSConfig {
cert, err := tls.LoadX509KeyPair(testCertFile, testKeyFile)
if err != nil {
panic(err)
}
return server.TLSConfig{
Certificates: []tls.Certificate{cert},
}
}
func serverConn() (net.PacketConn, net.Addr, error) {
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return nil, nil, err
}
return udpConn, udpAddr, nil
}
// tcpEchoServer is a TCP server that echoes what it reads from the connection.
// It will never actively close the connection.
type tcpEchoServer struct {
Listener net.Listener
}
func (s *tcpEchoServer) Serve() error {
for {
conn, err := s.Listener.Accept()
if err != nil {
return err
}
go func() {
_, _ = io.Copy(conn, conn)
_ = conn.Close()
}()
}
}
func (s *tcpEchoServer) Close() error {
return s.Listener.Close()
}
// udpEchoServer is a UDP server that echoes what it reads from the connection.
// It will never actively close the connection.
type udpEchoServer struct {
Conn net.PacketConn
}
func (s *udpEchoServer) Serve() error {
buf := make([]byte, 65536)
for {
n, addr, err := s.Conn.ReadFrom(buf)
if err != nil {
return err
}
_, err = s.Conn.WriteTo(buf[:n], addr)
if err != nil {
return err
}
}
}
func (s *udpEchoServer) Close() error {
return s.Conn.Close()
}
================================================
FILE: core/internal/pmtud/avail.go
================================================
//go:build linux || windows || darwin
package pmtud
const (
DisablePathMTUDiscovery = false
)
================================================
FILE: core/internal/pmtud/unavail.go
================================================
//go:build !linux && !windows && !darwin
package pmtud
// quic-go's MTU detection is enabled by default on all platforms.
// However, it only actually sets the DF bit on 3 supported platforms (Windows, macOS, Linux).
// As a result, on other platforms, probe packets that should never be fragmented will still
// be fragmented and transmitted. So we're only enabling it for platforms where we've verified
// its functionality for now.
const (
DisablePathMTUDiscovery = true
)
================================================
FILE: core/internal/protocol/http.go
================================================
package protocol
import (
"net/http"
"strconv"
)
const (
URLHost = "hysteria"
URLPath = "/auth"
RequestHeaderAuth = "Hysteria-Auth"
ResponseHeaderUDPEnabled = "Hysteria-UDP"
CommonHeaderCCRX = "Hysteria-CC-RX"
CommonHeaderPadding = "Hysteria-Padding"
StatusAuthOK = 233
)
// AuthRequest is what client sends to server for authentication.
type AuthRequest struct {
Auth string
Rx uint64 // 0 = unknown, client asks server to use bandwidth detection
}
// AuthResponse is what server sends to client when authentication is passed.
type AuthResponse struct {
UDPEnabled bool
Rx uint64 // 0 = unlimited
RxAuto bool // true = server asks client to use bandwidth detection
}
func AuthRequestFromHeader(h http.Header) AuthRequest {
rx, _ := strconv.ParseUint(h.Get(CommonHeaderCCRX), 10, 64)
return AuthRequest{
Auth: h.Get(RequestHeaderAuth),
Rx: rx,
}
}
func AuthRequestToHeader(h http.Header, req AuthRequest) {
h.Set(RequestHeaderAuth, req.Auth)
h.Set(CommonHeaderCCRX, strconv.FormatUint(req.Rx, 10))
h.Set(CommonHeaderPadding, authRequestPadding.String())
}
func AuthResponseFromHeader(h http.Header) AuthResponse {
resp := AuthResponse{}
resp.UDPEnabled, _ = strconv.ParseBool(h.Get(ResponseHeaderUDPEnabled))
rxStr := h.Get(CommonHeaderCCRX)
if rxStr == "auto" {
// Special case for server requesting client to use bandwidth detection
resp.RxAuto = true
} else {
resp.Rx, _ = strconv.ParseUint(rxStr, 10, 64)
}
return resp
}
func AuthResponseToHeader(h http.Header, resp AuthResponse) {
h.Set(ResponseHeaderUDPEnabled, strconv.FormatBool(resp.UDPEnabled))
if resp.RxAuto {
h.Set(CommonHeaderCCRX, "auto")
} else {
h.Set(CommonHeaderCCRX, strconv.FormatUint(resp.Rx, 10))
}
h.Set(CommonHeaderPadding, authResponsePadding.String())
}
================================================
FILE: core/internal/protocol/padding.go
================================================
package protocol
import (
"math/rand"
)
const (
paddingChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
// padding specifies a half-open range [Min, Max).
type padding struct {
Min int
Max int
}
func (p padding) String() string {
n := p.Min + rand.Intn(p.Max-p.Min)
bs := make([]byte, n)
for i := range bs {
bs[i] = paddingChars[rand.Intn(len(paddingChars))]
}
return string(bs)
}
var (
authRequestPadding = padding{Min: 256, Max: 2048}
authResponsePadding = padding{Min: 256, Max: 2048}
tcpRequestPadding = padding{Min: 64, Max: 512}
tcpResponsePadding = padding{Min: 128, Max: 1024}
)
================================================
FILE: core/internal/protocol/proxy.go
================================================
package protocol
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/quic-go/quicvarint"
)
const (
FrameTypeTCPRequest = 0x401
// Max length values are for preventing DoS attacks
MaxAddressLength = 2048
MaxMessageLength = 2048
MaxPaddingLength = 4096
MaxDatagramFrameSize = 1200
MaxUDPSize = 4096
maxVarInt1 = 63
maxVarInt2 = 16383
maxVarInt4 = 1073741823
maxVarInt8 = 4611686018427387903
)
// TCPRequest format:
// 0x401 (QUIC varint)
// Address length (QUIC varint)
// Address (bytes)
// Padding length (QUIC varint)
// Padding (bytes)
func ReadTCPRequest(r io.Reader) (string, error) {
bReader := quicvarint.NewReader(r)
addrLen, err := quicvarint.Read(bReader)
if err != nil {
return "", err
}
if addrLen == 0 || addrLen > MaxAddressLength {
return "", errors.ProtocolError{Message: "invalid address length"}
}
addrBuf := make([]byte, addrLen)
_, err = io.ReadFull(r, addrBuf)
if err != nil {
return "", err
}
paddingLen, err := quicvarint.Read(bReader)
if err != nil {
return "", err
}
if paddingLen > MaxPaddingLength {
return "", errors.ProtocolError{Message: "invalid padding length"}
}
if paddingLen > 0 {
_, err = io.CopyN(io.Discard, r, int64(paddingLen))
if err != nil {
return "", err
}
}
return string(addrBuf), nil
}
func WriteTCPRequest(w io.Writer, addr string) error {
padding := tcpRequestPadding.String()
paddingLen := len(padding)
addrLen := len(addr)
sz := int(quicvarint.Len(FrameTypeTCPRequest)) +
int(quicvarint.Len(uint64(addrLen))) + addrLen +
int(quicvarint.Len(uint64(paddingLen))) + paddingLen
buf := make([]byte, sz)
i := varintPut(buf, FrameTypeTCPRequest)
i += varintPut(buf[i:], uint64(addrLen))
i += copy(buf[i:], addr)
i += varintPut(buf[i:], uint64(paddingLen))
copy(buf[i:], padding)
_, err := w.Write(buf)
return err
}
// TCPResponse format:
// Status (byte, 0=ok, 1=error)
// Message length (QUIC varint)
// Message (bytes)
// Padding length (QUIC varint)
// Padding (bytes)
func ReadTCPResponse(r io.Reader) (bool, string, error) {
var status [1]byte
if _, err := io.ReadFull(r, status[:]); err != nil {
return false, "", err
}
bReader := quicvarint.NewReader(r)
msgLen, err := quicvarint.Read(bReader)
if err != nil {
return false, "", err
}
if msgLen > MaxMessageLength {
return false, "", errors.ProtocolError{Message: "invalid message length"}
}
var msgBuf []byte
// No message is fine
if msgLen > 0 {
msgBuf = make([]byte, msgLen)
_, err = io.ReadFull(r, msgBuf)
if err != nil {
return false, "", err
}
}
paddingLen, err := quicvarint.Read(bReader)
if err != nil {
return false, "", err
}
if paddingLen > MaxPaddingLength {
return false, "", errors.ProtocolError{Message: "invalid padding length"}
}
if paddingLen > 0 {
_, err = io.CopyN(io.Discard, r, int64(paddingLen))
if err != nil {
return false, "", err
}
}
return status[0] == 0, string(msgBuf), nil
}
func WriteTCPResponse(w io.Writer, ok bool, msg string) error {
padding := tcpResponsePadding.String()
paddingLen := len(padding)
msgLen := len(msg)
sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen +
int(quicvarint.Len(uint64(paddingLen))) + paddingLen
buf := make([]byte, sz)
if ok {
buf[0] = 0
} else {
buf[0] = 1
}
i := varintPut(buf[1:], uint64(msgLen))
i += copy(buf[1+i:], msg)
i += varintPut(buf[1+i:], uint64(paddingLen))
copy(buf[1+i:], padding)
_, err := w.Write(buf)
return err
}
// UDPMessage format:
// Session ID (uint32 BE)
// Packet ID (uint16 BE)
// Fragment ID (uint8)
// Fragment count (uint8)
// Address length (QUIC varint)
// Address (bytes)
// Data...
type UDPMessage struct {
SessionID uint32 // 4
PacketID uint16 // 2
FragID uint8 // 1
FragCount uint8 // 1
Addr string // varint + bytes
Data []byte
}
func (m *UDPMessage) HeaderSize() int {
lAddr := len(m.Addr)
return 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr
}
func (m *UDPMessage) Size() int {
return m.HeaderSize() + len(m.Data)
}
func (m *UDPMessage) Serialize(buf []byte) int {
// Make sure the buffer is big enough
if len(buf) < m.Size() {
return -1
}
binary.BigEndian.PutUint32(buf, m.SessionID)
binary.BigEndian.PutUint16(buf[4:], m.PacketID)
buf[6] = m.FragID
buf[7] = m.FragCount
i := varintPut(buf[8:], uint64(len(m.Addr)))
i += copy(buf[8+i:], m.Addr)
i += copy(buf[8+i:], m.Data)
return 8 + i
}
func ParseUDPMessage(msg []byte) (*UDPMessage, error) {
m := &UDPMessage{}
buf := bytes.NewBuffer(msg)
if err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil {
return nil, err
}
if err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil {
return nil, err
}
if err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil {
return nil, err
}
if err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil {
return nil, err
}
lAddr, err := quicvarint.Read(buf)
if err != nil {
return nil, err
}
if lAddr == 0 || lAddr > MaxMessageLength {
return nil, errors.ProtocolError{Message: "invalid address length"}
}
bs := buf.Bytes()
if len(bs) <= int(lAddr) {
// We use <= instead of < here as we expect at least one byte of data after the address
return nil, errors.ProtocolError{Message: "invalid message length"}
}
m.Addr = string(bs[:lAddr])
m.Data = bs[lAddr:]
return m, nil
}
// varintPut is like quicvarint.Append, but instead of appending to a slice,
// it writes to a fixed-size buffer. Returns the number of bytes written.
func varintPut(b []byte, i uint64) int {
if i <= maxVarInt1 {
b[0] = uint8(i)
return 1
}
if i <= maxVarInt2 {
b[0] = uint8(i>>8) | 0x40
b[1] = uint8(i)
return 2
}
if i <= maxVarInt4 {
b[0] = uint8(i>>24) | 0x80
b[1] = uint8(i >> 16)
b[2] = uint8(i >> 8)
b[3] = uint8(i)
return 4
}
if i <= maxVarInt8 {
b[0] = uint8(i>>56) | 0xc0
b[1] = uint8(i >> 48)
b[2] = uint8(i >> 40)
b[3] = uint8(i >> 32)
b[4] = uint8(i >> 24)
b[5] = uint8(i >> 16)
b[6] = uint8(i >> 8)
b[7] = uint8(i)
return 8
}
panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i))
}
================================================
FILE: core/internal/protocol/proxy_test.go
================================================
package protocol
import (
"bytes"
"reflect"
"strings"
"testing"
)
func TestUDPMessage(t *testing.T) {
t.Run("buffer too small", func(t *testing.T) {
// Make sure Serialize returns -1 when the buffer is too small.
tBuf := make([]byte, 20)
if (&UDPMessage{
SessionID: 66,
PacketID: 77,
FragID: 2,
FragCount: 5,
Addr: "random_addr",
Data: []byte("random_data"),
}).Serialize(tBuf) != -1 {
t.Error("Serialize() did not return -1 when the buffer was too small")
}
})
type fields struct {
SessionID uint32
PacketID uint16
FragID uint8
FragCount uint8
Addr string
Data []byte
}
tests := []struct {
name string
fields fields
want []byte
}{
{
name: "test 1",
fields: fields{
SessionID: 1,
PacketID: 1,
FragID: 0,
FragCount: 1,
Addr: "example.com:80",
Data: []byte("GET /nothing HTTP/1.1\r\n"),
},
want: []byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0xe, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x38, 0x30, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31, 0xd, 0xa},
},
{
name: "test 2",
fields: fields{
SessionID: 1329655244,
Addr: "some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long:9000",
PacketID: 62233,
FragID: 8,
FragCount: 19,
Data: []byte("God is great, beer is good, and people are crazy."),
},
want: []byte{0x4f, 0x40, 0xed, 0xcc, 0xf3, 0x19, 0x8, 0x13, 0x41, 0xee, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x3a, 0x39, 0x30, 0x30, 0x30, 0x47, 0x6f, 0x64, 0x20, 0x69, 0x73, 0x20, 0x67, 0x72, 0x65, 0x61, 0x74, 0x2c, 0x20, 0x62, 0x65, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x67, 0x6f, 0x6f, 0x64, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x20, 0x61, 0x72, 0x65, 0x20, 0x63, 0x72, 0x61, 0x7a, 0x79, 0x2e},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &UDPMessage{
SessionID: tt.fields.SessionID,
Addr: tt.fields.Addr,
PacketID: tt.fields.PacketID,
FragID: tt.fields.FragID,
FragCount: tt.fields.FragCount,
Data: tt.fields.Data,
}
// Serialize
buf := make([]byte, MaxUDPSize)
n := m.Serialize(buf)
if got := buf[:n]; !reflect.DeepEqual(got, tt.want) {
t.Errorf("Serialize() = %v, want %v", got, tt.want)
}
// Parse back
if m2, err := ParseUDPMessage(tt.want); err != nil {
t.Errorf("ParseUDPMessage() error = %v", err)
} else {
if !reflect.DeepEqual(m2, m) {
t.Errorf("ParseUDPMessage() = %v, want %v", m2, m)
}
}
})
}
}
// TestUDPMessageMalformed is to make sure ParseUDPMessage() fails (but not panic) on malformed data.
func TestUDPMessageMalformed(t *testing.T) {
tests := []struct {
name string
data []byte
}{
{
name: "empty",
data: []byte{},
},
{
name: "zeroes 1",
data: []byte{0, 0, 0, 0},
},
{
name: "zeroes 2",
data: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
{
name: "incomplete 1",
data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55},
},
{
name: "incomplete 2",
data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x90, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := ParseUDPMessage(tt.data); err == nil {
t.Errorf("ParseUDPMessage() should fail")
}
})
}
}
func TestReadTCPRequest(t *testing.T) {
tests := []struct {
name string
data []byte
want string
wantErr bool
}{
{
name: "normal no padding",
data: []byte("\x0egoogle.com:443\x00"),
want: "google.com:443",
wantErr: false,
},
{
name: "normal with padding",
data: []byte("\x0bholy.cc:443\x02gg"),
want: "holy.cc:443",
wantErr: false,
},
{
name: "incomplete 1",
data: []byte("\x0bhoho"),
want: "",
wantErr: true,
},
{
name: "incomplete 2",
data: []byte("\x0bholy.cc:443\x05x"),
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
got, err := ReadTCPRequest(r)
if (err != nil) != tt.wantErr {
t.Errorf("ReadTCPRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ReadTCPRequest() got = %v, want %v", got, tt.want)
}
})
}
}
func TestWriteTCPRequest(t *testing.T) {
tests := []struct {
name string
addr string
wantW string // Just a prefix, we don't care about the padding
wantErr bool
}{
{
name: "normal 1",
addr: "google.com:443",
wantW: "\x44\x01\x0egoogle.com:443",
wantErr: false,
},
{
name: "normal 2",
addr: "client-api.arkoselabs.com:8080",
wantW: "\x44\x01\x1eclient-api.arkoselabs.com:8080",
wantErr: false,
},
{
name: "empty",
addr: "",
wantW: "\x44\x01\x00",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := WriteTCPRequest(w, tt.addr)
if (err != nil) != tt.wantErr {
t.Errorf("WriteTCPRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) {
t.Errorf("WriteTCPRequest() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
func TestReadTCPResponse(t *testing.T) {
tests := []struct {
name string
data []byte
want bool
want1 string
wantErr bool
}{
{
name: "normal ok no padding",
data: []byte("\x00\x0bhello world\x00"),
want: true,
want1: "hello world",
wantErr: false,
},
{
name: "normal error with padding",
data: []byte("\x01\x06stop!!\x05xxxxx"),
want1: "stop!!",
wantErr: false,
},
{
name: "normal ok no message with padding",
data: []byte("\x01\x00\x05xxxxx"),
want1: "",
wantErr: false,
},
{
name: "incomplete 1",
data: []byte("\x00\x0bhoho"),
want1: "",
wantErr: true,
},
{
name: "incomplete 2",
data: []byte("\x01\x05jesus\x05x"),
want1: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
got, got1, err := ReadTCPResponse(r)
if (err != nil) != tt.wantErr {
t.Errorf("ReadTCPResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ReadTCPResponse() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("ReadTCPResponse() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func TestWriteTCPResponse(t *testing.T) {
type args struct {
ok bool
msg string
}
tests := []struct {
name string
args args
wantW string // Just a prefix, we don't care about the padding
wantErr bool
}{
{
name: "normal ok",
args: args{ok: true, msg: "hello world"},
wantW: "\x00\x0bhello world",
wantErr: false,
},
{
name: "normal error",
args: args{ok: false, msg: "stop!!"},
wantW: "\x01\x06stop!!",
wantErr: false,
},
{
name: "empty",
args: args{ok: true, msg: ""},
wantW: "\x00\x00",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := WriteTCPResponse(w, tt.args.ok, tt.args.msg)
if (err != nil) != tt.wantErr {
t.Errorf("WriteTCPResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); !(strings.HasPrefix(gotW, tt.wantW) && len(gotW) > len(tt.wantW)) {
t.Errorf("WriteTCPResponse() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
================================================
FILE: core/internal/utils/atomic.go
================================================
package utils
import (
"sync/atomic"
"time"
)
type AtomicTime struct {
v atomic.Value
}
func NewAtomicTime(t time.Time) *AtomicTime {
a := &AtomicTime{}
a.Set(t)
return a
}
func (t *AtomicTime) Set(new time.Time) {
t.v.Store(new)
}
func (t *AtomicTime) Get() time.Time {
return t.v.Load().(time.Time)
}
type Atomic[T any] struct {
v atomic.Value
}
func (a *Atomic[T]) Load() T {
value := a.v.Load()
if value == nil {
var zero T
return zero
}
return value.(T)
}
func (a *Atomic[T]) Store(value T) {
a.v.Store(value)
}
func (a *Atomic[T]) Swap(new T) T {
old := a.v.Swap(new)
if old == nil {
var zero T
return zero
}
return old.(T)
}
func (a *Atomic[T]) CompareAndSwap(old, new T) bool {
return a.v.CompareAndSwap(old, new)
}
================================================
FILE: core/internal/utils/qstream.go
================================================
package utils
import (
"context"
"time"
"github.com/apernet/quic-go"
)
// QStream is a wrapper of quic.Stream that handles Close() in a way that
// makes more sense to us. By default, quic.Stream's Close() only closes
// the write side of the stream, not the read side. And if there is unread
// data, the stream is not really considered closed until either the data
// is drained or CancelRead() is called.
// References:
// - https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/stream.go
// - https://github.com/quic-go/quic-go/issues/3558
// - https://github.com/quic-go/quic-go/issues/1599
type QStream struct {
Stream *quic.Stream
}
func (s *QStream) StreamID() quic.StreamID {
return s.Stream.StreamID()
}
func (s *QStream) Read(p []byte) (n int, err error) {
return s.Stream.Read(p)
}
func (s *QStream) CancelRead(code quic.StreamErrorCode) {
s.Stream.CancelRead(code)
}
func (s *QStream) SetReadDeadline(t time.Time) error {
return s.Stream.SetReadDeadline(t)
}
func (s *QStream) Write(p []byte) (n int, err error) {
return s.Stream.Write(p)
}
func (s *QStream) Close() error {
s.Stream.CancelRead(0)
return s.Stream.Close()
}
func (s *QStream) CancelWrite(code quic.StreamErrorCode) {
s.Stream.CancelWrite(code)
}
func (s *QStream) Context() context.Context {
return s.Stream.Context()
}
func (s *QStream) SetWriteDeadline(t time.Time) error {
return s.Stream.SetWriteDeadline(t)
}
func (s *QStream) SetDeadline(t time.Time) error {
return s.Stream.SetDeadline(t)
}
================================================
FILE: core/server/.mockery.yaml
================================================
with-expecter: true
inpackage: true
dir: .
packages:
github.com/apernet/hysteria/core/v2/server:
interfaces:
udpIO:
config:
mockname: mockUDPIO
udpEventLogger:
config:
mockname: mockUDPEventLogger
UDPConn:
config:
mockname: mockUDPConn
================================================
FILE: core/server/config.go
================================================
package server
import (
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"sync/atomic"
"time"
"github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/pmtud"
"github.com/apernet/hysteria/core/v2/internal/utils"
"github.com/apernet/quic-go"
)
const (
defaultStreamReceiveWindow = 8388608 // 8MB
defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB
defaultMaxIdleTimeout = 30 * time.Second
defaultMaxIncomingStreams = 1024
defaultUDPIdleTimeout = 60 * time.Second
)
type Config struct {
TLSConfig TLSConfig
QUICConfig QUICConfig
Conn net.PacketConn
RequestHook RequestHook
Outbound Outbound
BandwidthConfig BandwidthConfig
IgnoreClientBandwidth bool
DisableUDP bool
UDPIdleTimeout time.Duration
Authenticator Authenticator
EventLogger EventLogger
TrafficLogger TrafficLogger
MasqHandler http.Handler
}
// fill fills the fields that are not set by the user with default values when possible,
// and returns an error if the user has not set a required field, or if a field is invalid.
func (c *Config) fill() error {
if len(c.TLSConfig.Certificates) == 0 && c.TLSConfig.GetCertificate == nil {
return errors.ConfigError{Field: "TLSConfig", Reason: "must set at least one of Certificates or GetCertificate"}
}
if c.QUICConfig.InitialStreamReceiveWindow == 0 {
c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow
} else if c.QUICConfig.InitialStreamReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.MaxStreamReceiveWindow == 0 {
c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow
} else if c.QUICConfig.MaxStreamReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.InitialConnectionReceiveWindow == 0 {
c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow
} else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.MaxConnectionReceiveWindow == 0 {
c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow
} else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 {
return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"}
}
if c.QUICConfig.MaxIdleTimeout == 0 {
c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout
} else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second {
return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"}
}
if c.QUICConfig.MaxIncomingStreams == 0 {
c.QUICConfig.MaxIncomingStreams = defaultMaxIncomingStreams
} else if c.QUICConfig.MaxIncomingStreams < 8 {
return errors.ConfigError{Field: "QUICConfig.MaxIncomingStreams", Reason: "must be at least 8"}
}
c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery
if c.Conn == nil {
return errors.ConfigError{Field: "Conn", Reason: "must be set"}
}
if c.Outbound == nil {
c.Outbound = &defaultOutbound{}
}
if c.BandwidthConfig.MaxTx != 0 && c.BandwidthConfig.MaxTx < 65536 {
return errors.ConfigError{Field: "BandwidthConfig.MaxTx", Reason: "must be at least 65536"}
}
if c.BandwidthConfig.MaxRx != 0 && c.BandwidthConfig.MaxRx < 65536 {
return errors.ConfigError{Field: "BandwidthConfig.MaxRx", Reason: "must be at least 65536"}
}
if c.UDPIdleTimeout == 0 {
c.UDPIdleTimeout = defaultUDPIdleTimeout
} else if c.UDPIdleTimeout < 2*time.Second || c.UDPIdleTimeout > 600*time.Second {
return errors.ConfigError{Field: "UDPIdleTimeout", Reason: "must be between 2s and 600s"}
}
if c.Authenticator == nil {
return errors.ConfigError{Field: "Authenticator", Reason: "must be set"}
}
return nil
}
// TLSConfig contains the TLS configuration fields that we want to expose to the user.
type TLSConfig struct {
Certificates []tls.Certificate
GetCertificate func(info *tls.ClientHelloInfo) (*tls.Certificate, error)
ClientCAs *x509.CertPool
}
// QUICConfig contains the QUIC configuration fields that we want to expose to the user.
type QUICConfig struct {
InitialStreamReceiveWindow uint64
MaxStreamReceiveWindow uint64
InitialConnectionReceiveWindow uint64
MaxConnectionReceiveWindow uint64
MaxIdleTimeout time.Duration
MaxIncomingStreams int64
DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms.
}
// RequestHook allows filtering and modifying requests before the server connects to the remote.
// A request will only be hooked if Check returns true.
// The returned byte slice, if not empty, will be sent to the remote before proxying - this is
// mainly for "putting back" the content read from the client for sniffing, etc.
// Return a non-nil error to abort the connection.
// Note that due to the current architectural limitations, it can only inspect the first packet
// of a UDP connection. It also cannot put back any data as the first packet is always sent as-is.
type RequestHook interface {
Check(isUDP bool, reqAddr string) bool
TCP(stream HyStream, reqAddr *string) ([]byte, error)
UDP(data []byte, reqAddr *string) error
}
// Outbound provides the implementation of how the server should connect to remote servers.
// Although UDP includes a reqAddr, the implementation does not necessarily have to use it
// to make a "connected" UDP connection that does not accept packets from other addresses.
// In fact, the default implementation simply uses net.ListenUDP for a "full-cone" behavior.
type Outbound interface {
TCP(reqAddr string) (net.Conn, error)
UDP(reqAddr string) (UDPConn, error)
}
// UDPConn is like net.PacketConn, but uses string for addresses.
type UDPConn interface {
ReadFrom(b []byte) (int, string, error)
WriteTo(b []byte, addr string) (int, error)
Close() error
}
type defaultOutbound struct{}
var defaultOutboundDialer = net.Dialer{
Timeout: 10 * time.Second,
}
func (o *defaultOutbound) TCP(reqAddr string) (net.Conn, error) {
return defaultOutboundDialer.Dial("tcp", reqAddr)
}
func (o *defaultOutbound) UDP(reqAddr string) (UDPConn, error) {
conn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
return &defaultUDPConn{conn}, nil
}
type defaultUDPConn struct {
*net.UDPConn
}
func (c *defaultUDPConn) ReadFrom(b []byte) (int, string, error) {
n, addr, err := c.UDPConn.ReadFrom(b)
if addr != nil {
return n, addr.String(), err
} else {
return n, "", err
}
}
func (c *defaultUDPConn) WriteTo(b []byte, addr string) (int, error) {
uAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return 0, err
}
return c.UDPConn.WriteTo(b, uAddr)
}
// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second.
type BandwidthConfig struct {
MaxTx uint64
MaxRx uint64
}
// Authenticator is an interface that provides authentication logic.
type Authenticator interface {
Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string)
}
// EventLogger is an interface that provides logging logic.
type EventLogger interface {
Connect(addr net.Addr, id string, tx uint64)
Disconnect(addr net.Addr, id string, err error)
TCPRequest(addr net.Addr, id, reqAddr string)
TCPError(addr net.Addr, id, reqAddr string, err error)
UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string)
UDPError(addr net.Addr, id string, sessionID uint32, err error)
}
type HyStream interface {
StreamID() quic.StreamID
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
Close() error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
SetDeadline(t time.Time) error
}
// TrafficLogger is an interface that provides traffic logging logic.
// Tx/Rx in this context refers to the server-remote (proxy target) perspective.
// Tx is the bytes sent from the server to the remote.
// Rx is the bytes received by the server from the remote.
// Apart from logging, the Log function can also return false to signal
// that the client should be disconnected. This can be used to implement
// bandwidth limits or post-connection authentication, for example.
// The implementation of this interface must be thread-safe.
type TrafficLogger interface {
LogTraffic(id string, tx, rx uint64) (ok bool)
LogOnlineState(id string, online bool)
TraceStream(stream HyStream, stats *StreamStats)
UntraceStream(stream HyStream)
}
type StreamState int
const (
// StreamStateInitial indicates the initial state of a stream.
// Client has opened the stream, but we have not received the proxy request yet.
StreamStateInitial StreamState = iota
// StreamStateHooking indicates that the hook (usually sniff) is processing.
// Client has sent the proxy request, but sniff requires more data to complete.
StreamStateHooking
// StreamStateConnecting indicates that we are connecting to the proxy target.
StreamStateConnecting
// StreamStateEstablished indicates the proxy is established.
StreamStateEstablished
// StreamStateClosed indicates the stream is closed.
StreamStateClosed
)
func (s StreamState) String() string {
switch s {
case StreamStateInitial:
return "init"
case StreamStateHooking:
return "hook"
case StreamStateConnecting:
return "connect"
case StreamStateEstablished:
return "estab"
case StreamStateClosed:
return "closed"
default:
return "unknown"
}
}
type StreamStats struct {
State utils.Atomic[StreamState]
AuthID string
ConnID uint32
InitialTime time.Time
ReqAddr utils.Atomic[string]
HookedReqAddr utils.Atomic[string]
Tx atomic.Uint64
Rx atomic.Uint64
LastActiveTime utils.Atomic[time.Time]
}
func (s *StreamStats) setHookedReqAddr(addr string) {
if addr != s.ReqAddr.Load() {
s.HookedReqAddr.Store(addr)
}
}
================================================
FILE: core/server/copy.go
================================================
package server
import (
"errors"
"io"
"time"
)
var errDisconnect = errors.New("traffic logger requested disconnect")
func copyBufferLog(dst io.Writer, src io.Reader, log func(n uint64) bool) error {
buf := make([]byte, 32*1024)
for {
nr, er := src.Read(buf)
if nr > 0 {
if !log(uint64(nr)) {
// Log returns false, which means that the client should be disconnected
return errDisconnect
}
_, ew := dst.Write(buf[0:nr])
if ew != nil {
return ew
}
}
if er != nil {
if er == io.EOF {
// EOF should not be considered as an error
return nil
}
return er
}
}
}
func copyTwoWayEx(id string, serverRw, remoteRw io.ReadWriter, l TrafficLogger, stats *StreamStats) error {
errChan := make(chan error, 2)
go func() {
errChan <- copyBufferLog(serverRw, remoteRw, func(n uint64) bool {
stats.LastActiveTime.Store(time.Now())
stats.Rx.Add(n)
return l.LogTraffic(id, 0, n)
})
}()
go func() {
errChan <- copyBufferLog(remoteRw, serverRw, func(n uint64) bool {
stats.LastActiveTime.Store(time.Now())
stats.Tx.Add(n)
return l.LogTraffic(id, n, 0)
})
}()
// Block until one of the two goroutines returns
return <-errChan
}
// copyTwoWay is the "fast-path" version of copyTwoWayEx that does not log traffic or update stream stats.
// It uses the built-in io.Copy instead of our own copyBufferLog.
func copyTwoWay(serverRw, remoteRw io.ReadWriter) error {
errChan := make(chan error, 2)
go func() {
_, err := io.Copy(serverRw, remoteRw)
errChan <- err
}()
go func() {
_, err := io.Copy(remoteRw, serverRw)
errChan <- err
}()
// Block until one of the two goroutines returns
return <-errChan
}
================================================
FILE: core/server/mock_UDPConn.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package server
import mock "github.com/stretchr/testify/mock"
// mockUDPConn is an autogenerated mock type for the UDPConn type
type mockUDPConn struct {
mock.Mock
}
type mockUDPConn_Expecter struct {
mock *mock.Mock
}
func (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter {
return &mockUDPConn_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *mockUDPConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type mockUDPConn_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call {
return &mockUDPConn_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call {
_c.Call.Return(run)
return _c
}
// ReadFrom provides a mock function with given fields: b
func (_m *mockUDPConn) ReadFrom(b []byte) (int, string, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for ReadFrom")
}
var r0 int
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func([]byte) (int, string, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) string); ok {
r1 = rf(b)
} else {
r1 = ret.Get(1).(string)
}
if rf, ok := ret.Get(2).(func([]byte) error); ok {
r2 = rf(b)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom'
type mockUDPConn_ReadFrom_Call struct {
*mock.Call
}
// ReadFrom is a helper method to define mock.On call
// - b []byte
func (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call {
return &mockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)}
}
func (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 string, _a2 error) *mockUDPConn_ReadFrom_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string, error)) *mockUDPConn_ReadFrom_Call {
_c.Call.Return(run)
return _c
}
// WriteTo provides a mock function with given fields: b, addr
func (_m *mockUDPConn) WriteTo(b []byte, addr string) (int, error) {
ret := _m.Called(b, addr)
if len(ret) == 0 {
panic("no return value specified for WriteTo")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok {
return rf(b, addr)
}
if rf, ok := ret.Get(0).(func([]byte, string) int); ok {
r0 = rf(b, addr)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte, string) error); ok {
r1 = rf(b, addr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo'
type mockUDPConn_WriteTo_Call struct {
*mock.Call
}
// WriteTo is a helper method to define mock.On call
// - b []byte
// - addr string
func (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call {
return &mockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)}
}
func (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr string)) *mockUDPConn_WriteTo_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(string))
})
return _c
}
func (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, string) (int, error)) *mockUDPConn_WriteTo_Call {
_c.Call.Return(run)
return _c
}
// newMockUDPConn creates a new instance of mockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func newMockUDPConn(t interface {
mock.TestingT
Cleanup(func())
}) *mockUDPConn {
mock := &mockUDPConn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/server/mock_udpEventLogger.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package server
import mock "github.com/stretchr/testify/mock"
// mockUDPEventLogger is an autogenerated mock type for the udpEventLogger type
type mockUDPEventLogger struct {
mock.Mock
}
type mockUDPEventLogger_Expecter struct {
mock *mock.Mock
}
func (_m *mockUDPEventLogger) EXPECT() *mockUDPEventLogger_Expecter {
return &mockUDPEventLogger_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with given fields: sessionID, err
func (_m *mockUDPEventLogger) Close(sessionID uint32, err error) {
_m.Called(sessionID, err)
}
// mockUDPEventLogger_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type mockUDPEventLogger_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
// - sessionID uint32
// - err error
func (_e *mockUDPEventLogger_Expecter) Close(sessionID interface{}, err interface{}) *mockUDPEventLogger_Close_Call {
return &mockUDPEventLogger_Close_Call{Call: _e.mock.On("Close", sessionID, err)}
}
func (_c *mockUDPEventLogger_Close_Call) Run(run func(sessionID uint32, err error)) *mockUDPEventLogger_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(uint32), args[1].(error))
})
return _c
}
func (_c *mockUDPEventLogger_Close_Call) Return() *mockUDPEventLogger_Close_Call {
_c.Call.Return()
return _c
}
func (_c *mockUDPEventLogger_Close_Call) RunAndReturn(run func(uint32, error)) *mockUDPEventLogger_Close_Call {
_c.Run(run)
return _c
}
// New provides a mock function with given fields: sessionID, reqAddr
func (_m *mockUDPEventLogger) New(sessionID uint32, reqAddr string) {
_m.Called(sessionID, reqAddr)
}
// mockUDPEventLogger_New_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'New'
type mockUDPEventLogger_New_Call struct {
*mock.Call
}
// New is a helper method to define mock.On call
// - sessionID uint32
// - reqAddr string
func (_e *mockUDPEventLogger_Expecter) New(sessionID interface{}, reqAddr interface{}) *mockUDPEventLogger_New_Call {
return &mockUDPEventLogger_New_Call{Call: _e.mock.On("New", sessionID, reqAddr)}
}
func (_c *mockUDPEventLogger_New_Call) Run(run func(sessionID uint32, reqAddr string)) *mockUDPEventLogger_New_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(uint32), args[1].(string))
})
return _c
}
func (_c *mockUDPEventLogger_New_Call) Return() *mockUDPEventLogger_New_Call {
_c.Call.Return()
return _c
}
func (_c *mockUDPEventLogger_New_Call) RunAndReturn(run func(uint32, string)) *mockUDPEventLogger_New_Call {
_c.Run(run)
return _c
}
// newMockUDPEventLogger creates a new instance of mockUDPEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func newMockUDPEventLogger(t interface {
mock.TestingT
Cleanup(func())
}) *mockUDPEventLogger {
mock := &mockUDPEventLogger{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/server/mock_udpIO.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package server
import (
protocol "github.com/apernet/hysteria/core/v2/internal/protocol"
mock "github.com/stretchr/testify/mock"
)
// mockUDPIO is an autogenerated mock type for the udpIO type
type mockUDPIO struct {
mock.Mock
}
type mockUDPIO_Expecter struct {
mock *mock.Mock
}
func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter {
return &mockUDPIO_Expecter{mock: &_m.Mock}
}
// Hook provides a mock function with given fields: data, reqAddr
func (_m *mockUDPIO) Hook(data []byte, reqAddr *string) error {
ret := _m.Called(data, reqAddr)
if len(ret) == 0 {
panic("no return value specified for Hook")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, *string) error); ok {
r0 = rf(data, reqAddr)
} else {
r0 = ret.Error(0)
}
return r0
}
// mockUDPIO_Hook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Hook'
type mockUDPIO_Hook_Call struct {
*mock.Call
}
// Hook is a helper method to define mock.On call
// - data []byte
// - reqAddr *string
func (_e *mockUDPIO_Expecter) Hook(data interface{}, reqAddr interface{}) *mockUDPIO_Hook_Call {
return &mockUDPIO_Hook_Call{Call: _e.mock.On("Hook", data, reqAddr)}
}
func (_c *mockUDPIO_Hook_Call) Run(run func(data []byte, reqAddr *string)) *mockUDPIO_Hook_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(*string))
})
return _c
}
func (_c *mockUDPIO_Hook_Call) Return(_a0 error) *mockUDPIO_Hook_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockUDPIO_Hook_Call) RunAndReturn(run func([]byte, *string) error) *mockUDPIO_Hook_Call {
_c.Call.Return(run)
return _c
}
// ReceiveMessage provides a mock function with no fields
func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ReceiveMessage")
}
var r0 *protocol.UDPMessage
var r1 error
if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *protocol.UDPMessage); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*protocol.UDPMessage)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockUDPIO_ReceiveMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveMessage'
type mockUDPIO_ReceiveMessage_Call struct {
*mock.Call
}
// ReceiveMessage is a helper method to define mock.On call
func (_e *mockUDPIO_Expecter) ReceiveMessage() *mockUDPIO_ReceiveMessage_Call {
return &mockUDPIO_ReceiveMessage_Call{Call: _e.mock.On("ReceiveMessage")}
}
func (_c *mockUDPIO_ReceiveMessage_Call) Run(run func()) *mockUDPIO_ReceiveMessage_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *mockUDPIO_ReceiveMessage_Call) Return(_a0 *protocol.UDPMessage, _a1 error) *mockUDPIO_ReceiveMessage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPMessage, error)) *mockUDPIO_ReceiveMessage_Call {
_c.Call.Return(run)
return _c
}
// SendMessage provides a mock function with given fields: _a0, _a1
func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for SendMessage")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}
// mockUDPIO_SendMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessage'
type mockUDPIO_SendMessage_Call struct {
*mock.Call
}
// SendMessage is a helper method to define mock.On call
// - _a0 []byte
// - _a1 *protocol.UDPMessage
func (_e *mockUDPIO_Expecter) SendMessage(_a0 interface{}, _a1 interface{}) *mockUDPIO_SendMessage_Call {
return &mockUDPIO_SendMessage_Call{Call: _e.mock.On("SendMessage", _a0, _a1)}
}
func (_c *mockUDPIO_SendMessage_Call) Run(run func(_a0 []byte, _a1 *protocol.UDPMessage)) *mockUDPIO_SendMessage_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(*protocol.UDPMessage))
})
return _c
}
func (_c *mockUDPIO_SendMessage_Call) Return(_a0 error) *mockUDPIO_SendMessage_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UDPMessage) error) *mockUDPIO_SendMessage_Call {
_c.Call.Return(run)
return _c
}
// UDP provides a mock function with given fields: reqAddr
func (_m *mockUDPIO) UDP(reqAddr string) (UDPConn, error) {
ret := _m.Called(reqAddr)
if len(ret) == 0 {
panic("no return value specified for UDP")
}
var r0 UDPConn
var r1 error
if rf, ok := ret.Get(0).(func(string) (UDPConn, error)); ok {
return rf(reqAddr)
}
if rf, ok := ret.Get(0).(func(string) UDPConn); ok {
r0 = rf(reqAddr)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(UDPConn)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(reqAddr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockUDPIO_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'
type mockUDPIO_UDP_Call struct {
*mock.Call
}
// UDP is a helper method to define mock.On call
// - reqAddr string
func (_e *mockUDPIO_Expecter) UDP(reqAddr interface{}) *mockUDPIO_UDP_Call {
return &mockUDPIO_UDP_Call{Call: _e.mock.On("UDP", reqAddr)}
}
func (_c *mockUDPIO_UDP_Call) Run(run func(reqAddr string)) *mockUDPIO_UDP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *mockUDPIO_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockUDPIO_UDP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *mockUDPIO_UDP_Call) RunAndReturn(run func(string) (UDPConn, error)) *mockUDPIO_UDP_Call {
_c.Call.Return(run)
return _c
}
// newMockUDPIO creates a new instance of mockUDPIO. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func newMockUDPIO(t interface {
mock.TestingT
Cleanup(func())
}) *mockUDPIO {
mock := &mockUDPIO{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: core/server/server.go
================================================
package server
import (
"context"
"crypto/tls"
"math/rand"
"net/http"
"sync"
"time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
"github.com/apernet/quic-go/quicvarint"
"github.com/apernet/hysteria/core/v2/internal/congestion"
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/v2/internal/utils"
)
const (
closeErrCodeOK = 0x100 // HTTP3 ErrCodeNoError
closeErrCodeTrafficLimitReached = 0x107 // HTTP3 ErrCodeExcessiveLoad
)
type Server interface {
Serve() error
Close() error
}
func convertToStdTLSConfig(config *Config) *tls.Config {
var clientAuth tls.ClientAuthType
if config.TLSConfig.ClientCAs != nil {
clientAuth = tls.RequireAndVerifyClientCert
} else {
clientAuth = tls.NoClientCert
}
return http3.ConfigureTLSConfig(&tls.Config{
Certificates: config.TLSConfig.Certificates,
GetCertificate: config.TLSConfig.GetCertificate,
ClientCAs: config.TLSConfig.ClientCAs,
ClientAuth: clientAuth,
})
}
func NewServer(config *Config) (Server, error) {
if err := config.fill(); err != nil {
return nil, err
}
tlsConfig := convertToStdTLSConfig(config)
quicConfig := &quic.Config{
InitialStreamReceiveWindow: config.QUICConfig.InitialStreamReceiveWindow,
MaxStreamReceiveWindow: config.QUICConfig.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: config.QUICConfig.InitialConnectionReceiveWindow,
MaxConnectionReceiveWindow: config.QUICConfig.MaxConnectionReceiveWindow,
MaxIdleTimeout: config.QUICConfig.MaxIdleTimeout,
MaxIncomingStreams: config.QUICConfig.MaxIncomingStreams,
DisablePathMTUDiscovery: config.QUICConfig.DisablePathMTUDiscovery,
EnableDatagrams: true,
MaxDatagramFrameSize: protocol.MaxDatagramFrameSize,
DisablePathManager: true,
}
listener, err := quic.Listen(config.Conn, tlsConfig, quicConfig)
if err != nil {
_ = config.Conn.Close()
return nil, err
}
return &serverImpl{
config: config,
listener: listener,
}, nil
}
type serverImpl struct {
config *Config
listener *quic.Listener
}
func (s *serverImpl) Serve() error {
for {
conn, err := s.listener.Accept(context.Background())
if err != nil {
return err
}
go s.handleClient(conn)
}
}
func (s *serverImpl) Close() error {
err := s.listener.Close()
_ = s.config.Conn.Close()
return err
}
func (s *serverImpl) handleClient(conn *quic.Conn) {
handler := newH3sHandler(s.config, conn)
h3s := http3.Server{
Handler: handler,
StreamDispatcher: handler.ProxyStreamHijacker,
}
err := h3s.ServeQUICConn(conn)
// If the client is authenticated, we need to log the disconnect event
if handler.authenticated {
if tl := s.config.TrafficLogger; tl != nil {
tl.LogOnlineState(handler.authID, false)
}
if el := s.config.EventLogger; el != nil {
el.Disconnect(conn.RemoteAddr(), handler.authID, err)
}
}
_ = conn.CloseWithError(closeErrCodeOK, "")
}
type h3sHandler struct {
config *Config
conn *quic.Conn
authenticated bool
authMutex sync.Mutex
authID string
connID uint32 // a random id for dump streams
udpSM *udpSessionManager // Only set after authentication
}
func newH3sHandler(config *Config, conn *quic.Conn) *h3sHandler {
return &h3sHandler{
config: config,
conn: conn,
connID: rand.Uint32(),
}
}
func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.Host == protocol.URLHost && r.URL.Path == protocol.URLPath {
h.authMutex.Lock()
defer h.authMutex.Unlock()
if h.authenticated {
// Already authenticated
protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{
UDPEnabled: !h.config.DisableUDP,
Rx: h.config.BandwidthConfig.MaxRx,
RxAuto: h.config.IgnoreClientBandwidth,
})
w.WriteHeader(protocol.StatusAuthOK)
return
}
authReq := protocol.AuthRequestFromHeader(r.Header)
actualTx := authReq.Rx
ok, id := h.config.Authenticator.Authenticate(h.conn.RemoteAddr(), authReq.Auth, actualTx)
if ok {
// Set authenticated flag
h.authenticated = true
h.authID = id
if h.config.IgnoreClientBandwidth {
// Ignore client bandwidth, always use BBR
congestion.UseBBR(h.conn)
actualTx = 0
} else {
// actualTx = min(serverTx, clientRx)
if h.config.BandwidthConfig.MaxTx > 0 && actualTx > h.config.BandwidthConfig.MaxTx {
// We have a maxTx limit and the client is asking for more than that,
// return and use the limit instead
actualTx = h.config.BandwidthConfig.MaxTx
}
if actualTx > 0 {
congestion.UseBrutal(h.conn, actualTx)
} else {
// Client doesn't know its own bandwidth, use BBR
congestion.UseBBR(h.conn)
}
}
// Auth OK, send response
protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{
UDPEnabled: !h.config.DisableUDP,
Rx: h.config.BandwidthConfig.MaxRx,
RxAuto: h.config.IgnoreClientBandwidth,
})
w.WriteHeader(protocol.StatusAuthOK)
// Call event logger
if tl := h.config.TrafficLogger; tl != nil {
tl.LogOnlineState(id, true)
}
if el := h.config.EventLogger; el != nil {
el.Connect(h.conn.RemoteAddr(), id, actualTx)
}
// Initialize UDP session manager (if UDP is enabled)
// We use sync.Once to make sure that only one goroutine is started,
// as ServeHTTP may be called by multiple goroutines simultaneously
if !h.config.DisableUDP {
go func() {
sm := newUDPSessionManager(
&udpIOImpl{h.conn, id, h.config.TrafficLogger, h.config.RequestHook, h.config.Outbound},
&udpEventLoggerImpl{h.conn, id, h.config.EventLogger},
h.config.UDPIdleTimeout)
h.udpSM = sm
go sm.Run()
}()
}
} else {
// Auth failed, pretend to be a normal HTTP server
h.masqHandler(w, r)
}
} else {
// Not an auth request, pretend to be a normal HTTP server
h.masqHandler(w, r)
}
}
func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, stream *quic.Stream, err error) (bool, error) {
if err != nil || !h.authenticated {
return false, nil
}
switch ft {
case protocol.FrameTypeTCPRequest:
// StreamDispatcher only peeks the frame type. Consume it so ReadTCPRequest
// starts at address length, matching pre-upgrade StreamHijacker behavior.
if _, err := quicvarint.Read(quicvarint.NewReader(stream)); err != nil {
return false, err
}
// Wraps the stream with QStream, which handles Close() properly
qStream := &utils.QStream{Stream: stream}
go h.handleTCPRequest(qStream)
return true, nil
default:
return false, nil
}
}
func (h *h3sHandler) handleTCPRequest(stream *utils.QStream) {
trafficLogger := h.config.TrafficLogger
streamStats := &StreamStats{
AuthID: h.authID,
ConnID: h.connID,
InitialTime: time.Now(),
}
streamStats.State.Store(StreamStateInitial)
streamStats.LastActiveTime.Store(time.Now())
defer func() {
streamStats.State.Store(StreamStateClosed)
}()
if trafficLogger != nil {
trafficLogger.TraceStream(stream, streamStats)
defer trafficLogger.UntraceStream(stream)
}
// Read request
reqAddr, err := protocol.ReadTCPRequest(stream)
if err != nil {
_ = stream.Close()
return
}
streamStats.ReqAddr.Store(reqAddr)
// Call the hook if set
var putback []byte
var hooked bool
if h.config.RequestHook != nil {
hooked = h.config.RequestHook.Check(false, reqAddr)
// When the hook is enabled, the server should always accept a connection
// so that the client will send whatever request the hook wants to see.
// This is essentially a server-side fast-open.
if hooked {
streamStats.State.Store(StreamStateHooking)
_ = protocol.WriteTCPResponse(stream, true, "RequestHook enabled")
putback, err = h.config.RequestHook.TCP(stream, &reqAddr)
if err != nil {
_ = stream.Close()
return
}
streamStats.setHookedReqAddr(reqAddr)
}
}
// Log the event
if h.config.EventLogger != nil {
h.config.EventLogger.TCPRequest(h.conn.RemoteAddr(), h.authID, reqAddr)
}
// Dial target
streamStats.State.Store(StreamStateConnecting)
tConn, err := h.config.Outbound.TCP(reqAddr)
if err != nil {
if !hooked {
_ = protocol.WriteTCPResponse(stream, false, err.Error())
}
_ = stream.Close()
// Log the error
if h.config.EventLogger != nil {
h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err)
}
return
}
if !hooked {
_ = protocol.WriteTCPResponse(stream, true, "Connected")
}
streamStats.State.Store(StreamStateEstablished)
// Put back the data if the hook requested
if len(putback) > 0 {
n, _ := tConn.Write(putback)
streamStats.Tx.Add(uint64(n))
}
// Start proxying
if trafficLogger != nil {
err = copyTwoWayEx(h.authID, stream, tConn, trafficLogger, streamStats)
} else {
// Use the fast path if no traffic logger is set
err = copyTwoWay(stream, tConn)
}
if h.config.EventLogger != nil {
h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err)
}
// Cleanup
_ = tConn.Close()
_ = stream.Close()
// Disconnect the client if TrafficLogger requested
if err == errDisconnect {
_ = h.conn.CloseWithError(closeErrCodeTrafficLimitReached, "")
}
}
func (h *h3sHandler) masqHandler(w http.ResponseWriter, r *http.Request) {
if h.config.MasqHandler != nil {
h.config.MasqHandler.ServeHTTP(w, r)
} else {
// Return 404 for everything
http.NotFound(w, r)
}
}
// udpIOImpl is the IO implementation for udpSessionManager with TrafficLogger support
type udpIOImpl struct {
Conn *quic.Conn
AuthID string
TrafficLogger TrafficLogger
RequestHook RequestHook
Outbound Outbound
}
func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {
for {
msg, err := io.Conn.ReceiveDatagram(context.Background())
if err != nil {
// Connection error, this will stop the session manager
return nil, err
}
udpMsg, err := protocol.ParseUDPMessage(msg)
if err != nil {
// Invalid message, this is fine - just wait for the next
continue
}
if io.TrafficLogger != nil {
ok := io.TrafficLogger.LogTraffic(io.AuthID, uint64(len(udpMsg.Data)), 0)
if !ok {
// TrafficLogger requested to disconnect the client
_ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "")
return nil, errDisconnect
}
}
return udpMsg, nil
}
}
func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {
if io.TrafficLogger != nil {
ok := io.TrafficLogger.LogTraffic(io.AuthID, 0, uint64(len(msg.Data)))
if !ok {
// TrafficLogger requested to disconnect the client
_ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "")
return errDisconnect
}
}
msgN := msg.Serialize(buf)
if msgN < 0 {
// Message larger than buffer, silent drop
return nil
}
return io.Conn.SendDatagram(buf[:msgN])
}
func (io *udpIOImpl) Hook(data []byte, reqAddr *string) error {
if io.RequestHook != nil && io.RequestHook.Check(true, *reqAddr) {
return io.RequestHook.UDP(data, reqAddr)
} else {
return nil
}
}
func (io *udpIOImpl) UDP(reqAddr string) (UDPConn, error) {
return io.Outbound.UDP(reqAddr)
}
type udpEventLoggerImpl struct {
Conn *quic.Conn
AuthID string
EventLogger EventLogger
}
func (l *udpEventLoggerImpl) New(sessionID uint32, reqAddr string) {
if l.EventLogger != nil {
l.EventLogger.UDPRequest(l.Conn.RemoteAddr(), l.AuthID, sessionID, reqAddr)
}
}
func (l *udpEventLoggerImpl) Close(sessionID uint32, err error) {
if l.EventLogger != nil {
l.EventLogger.UDPError(l.Conn.RemoteAddr(), l.AuthID, sessionID, err)
}
}
================================================
FILE: core/server/udp.go
================================================
package server
import (
"errors"
"math/rand"
"sync"
"time"
"github.com/apernet/quic-go"
"github.com/apernet/hysteria/core/v2/internal/frag"
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/v2/internal/utils"
)
const (
idleCleanupInterval = 1 * time.Second
)
type udpIO interface {
ReceiveMessage() (*protocol.UDPMessage, error)
SendMessage([]byte, *protocol.UDPMessage) error
Hook(data []byte, reqAddr *string) error
UDP(reqAddr string) (UDPConn, error)
}
type udpEventLogger interface {
New(sessionID uint32, reqAddr string)
Close(sessionID uint32, err error)
}
type udpSessionEntry struct {
ID uint32
OverrideAddr string // Ignore the address in the UDP message, always use this if not empty
OriginalAddr string // The original address in the UDP message
D *frag.Defragger
Last *utils.AtomicTime
IO udpIO
DialFunc func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error)
ExitFunc func(err error)
conn UDPConn
connLock sync.Mutex
closed bool
}
func newUDPSessionEntry(
id uint32, io udpIO,
dialFunc func(string, []byte) (UDPConn, string, error),
exitFunc func(error),
) (e *udpSessionEntry) {
e = &udpSessionEntry{
ID: id,
D: &frag.Defragger{},
Last: utils.NewAtomicTime(time.Now()),
IO: io,
DialFunc: dialFunc,
ExitFunc: exitFunc,
}
return e
}
// CloseWithErr closes the session and calls ExitFunc with the given error.
// A nil error indicates the session is cleaned up due to timeout.
func (e *udpSessionEntry) CloseWithErr(err error) {
// We need this lock to ensure not to create conn after session exit
e.connLock.Lock()
if e.closed {
// Already closed
e.connLock.Unlock()
return
}
e.closed = true
if e.conn != nil {
_ = e.conn.Close()
}
e.connLock.Unlock()
e.ExitFunc(err)
}
// Feed feeds a UDP message to the session.
// If the message itself is a complete message, or it completes a fragmented message,
// the message is written to the session's UDP connection, and the number of bytes
// written is returned.
// Otherwise, 0 and nil are returned.
func (e *udpSessionEntry) Feed(msg *protocol.UDPMessage) (int, error) {
e.Last.Set(time.Now())
dfMsg := e.D.Feed(msg)
if dfMsg == nil {
return 0, nil
}
if e.conn == nil {
err := e.initConn(dfMsg)
if err != nil {
return 0, err
}
}
addr := dfMsg.Addr
if e.OverrideAddr != "" {
addr = e.OverrideAddr
}
return e.conn.WriteTo(dfMsg.Data, addr)
}
// initConn initializes the UDP connection of the session.
// If no error is returned, the e.conn is set to the new connection.
func (e *udpSessionEntry) initConn(firstMsg *protocol.UDPMessage) error {
// We need this lock to ensure not to create conn after session exit
e.connLock.Lock()
if e.closed {
e.connLock.Unlock()
return errors.New("session is closed")
}
conn, actualAddr, err := e.DialFunc(firstMsg.Addr, firstMsg.Data)
if err != nil {
// Fail fast if DialFunc failed
// (usually indicates the connection has been rejected by the ACL)
e.connLock.Unlock()
// CloseWithErr acquires the connLock again
e.CloseWithErr(err)
return err
}
e.conn = conn
if firstMsg.Addr != actualAddr {
// Hook changed the address, enable address override
e.OverrideAddr = actualAddr
e.OriginalAddr = firstMsg.Addr
}
go e.receiveLoop()
e.connLock.Unlock()
return nil
}
// receiveLoop receives incoming UDP packets, packs them into UDP messages,
// and sends using the IO.
// Exit when either the underlying UDP connection returns error (e.g. closed),
// or the IO returns error when sending.
func (e *udpSessionEntry) receiveLoop() {
udpBuf := make([]byte, protocol.MaxUDPSize)
msgBuf := make([]byte, protocol.MaxUDPSize)
for {
udpN, rAddr, err := e.conn.ReadFrom(udpBuf)
if err != nil {
e.CloseWithErr(err)
return
}
e.Last.Set(time.Now())
if e.OriginalAddr != "" {
// Use the original address in the opposite direction,
// otherwise the QUIC clients or NAT on the client side
// may not treat it as the same UDP session.
rAddr = e.OriginalAddr
}
msg := &protocol.UDPMessage{
SessionID: e.ID,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: rAddr,
Data: udpBuf[:udpN],
}
err = sendMessageAutoFrag(e.IO, msgBuf, msg)
if err != nil {
e.CloseWithErr(err)
return
}
}
}
// sendMessageAutoFrag tries to send a UDP message as a whole first,
// but if it fails due to quic.ErrMessageTooLarge, it tries again by
// fragmenting the message.
func sendMessageAutoFrag(io udpIO, buf []byte, msg *protocol.UDPMessage) error {
err := io.SendMessage(buf, msg)
var errTooLarge *quic.DatagramTooLargeError
if errors.As(err, &errTooLarge) {
// Message too large, try fragmentation
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
for _, fMsg := range fMsgs {
err := io.SendMessage(buf, &fMsg)
if err != nil {
return err
}
}
return nil
} else {
return err
}
}
// udpSessionManager manages the lifecycle of UDP sessions.
// Each UDP session is identified by a SessionID, and corresponds to a UDP connection.
// A UDP session is created when a UDP message with a new SessionID is received.
// Similar to standard NAT, a UDP session is destroyed when no UDP message is received
// for a certain period of time (specified by idleTimeout).
type udpSessionManager struct {
io udpIO
eventLogger udpEventLogger
idleTimeout time.Duration
mutex sync.RWMutex
m map[uint32]*udpSessionEntry
}
func newUDPSessionManager(io udpIO, eventLogger udpEventLogger, idleTimeout time.Duration) *udpSessionManager {
return &udpSessionManager{
io: io,
eventLogger: eventLogger,
idleTimeout: idleTimeout,
m: make(map[uint32]*udpSessionEntry),
}
}
// Run runs the session manager main loop.
// Exit and returns error when the underlying io returns error (e.g. closed).
func (m *udpSessionManager) Run() error {
stopCh := make(chan struct{})
go m.idleCleanupLoop(stopCh)
defer close(stopCh)
defer m.cleanup(false)
for {
msg, err := m.io.ReceiveMessage()
if err != nil {
return err
}
m.feed(msg)
}
}
func (m *udpSessionManager) idleCleanupLoop(stopCh <-chan struct{}) {
ticker := time.NewTicker(idleCleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.cleanup(true)
case <-stopCh:
return
}
}
}
func (m *udpSessionManager) cleanup(idleOnly bool) {
// We use RLock here as we are only scanning the map, not deleting from it.
m.mutex.RLock()
timeoutEntry := make([]*udpSessionEntry, 0, len(m.m))
now := time.Now()
for _, entry := range m.m {
if !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout {
timeoutEntry = append(timeoutEntry, entry)
}
}
m.mutex.RUnlock()
for _, entry := range timeoutEntry {
// This eventually calls entry.ExitFunc,
// where the m.mutex will be locked again to remove the entry from the map.
entry.CloseWithErr(nil)
}
}
func (m *udpSessionManager) feed(msg *protocol.UDPMessage) {
m.mutex.RLock()
entry := m.m[msg.SessionID]
m.mutex.RUnlock()
// Create a new session if not exists
if entry == nil {
dialFunc := func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error) {
// Call the hook
err = m.io.Hook(firstMsgData, &addr)
if err != nil {
return conn, actualAddr, err
}
actualAddr = addr
// Log the event
m.eventLogger.New(msg.SessionID, addr)
// Dial target
conn, err = m.io.UDP(addr)
return conn, actualAddr, err
}
exitFunc := func(err error) {
// Log the event
m.eventLogger.Close(entry.ID, err)
// Remove the session from the map
m.mutex.Lock()
delete(m.m, entry.ID)
m.mutex.Unlock()
}
entry = newUDPSessionEntry(msg.SessionID, m.io, dialFunc, exitFunc)
// Insert the session into the map
m.mutex.Lock()
m.m[msg.SessionID] = entry
m.mutex.Unlock()
}
// Feed the message to the session
// Feed (send) errors are ignored for now,
// as some are temporary (e.g. invalid address)
_, _ = entry.Feed(msg)
}
func (m *udpSessionManager) Count() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.m)
}
================================================
FILE: core/server/udp_test.go
================================================
package server
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.uber.org/goleak"
"github.com/apernet/hysteria/core/v2/internal/protocol"
)
func TestUDPSessionManager(t *testing.T) {
io := newMockUDPIO(t)
eventLogger := newMockUDPEventLogger(t)
sm := newUDPSessionManager(io, eventLogger, 2*time.Second)
msgCh := make(chan *protocol.UDPMessage, 4)
io.EXPECT().ReceiveMessage().RunAndReturn(func() (*protocol.UDPMessage, error) {
m := <-msgCh
if m == nil {
return nil, errors.New("closed")
}
return m, nil
})
go sm.Run()
udpReadFunc := func(addr string, ch chan []byte, b []byte) (int, string, error) {
bs := <-ch
if bs == nil {
return 0, "", errors.New("closed")
}
n := copy(b, bs)
return n, addr, nil
}
// Test normal session creation & timeout
msg1 := &protocol.UDPMessage{
SessionID: 1234,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: "address1.com:9000",
Data: []byte("hello"),
}
eventLogger.EXPECT().New(msg1.SessionID, msg1.Addr).Return().Once()
udpConn1 := newMockUDPConn(t)
udpConn1Ch := make(chan []byte, 1)
io.EXPECT().Hook(msg1.Data, &msg1.Addr).Return(nil).Once()
io.EXPECT().UDP(msg1.Addr).Return(udpConn1, nil).Once()
udpConn1.EXPECT().WriteTo(msg1.Data, msg1.Addr).Return(5, nil).Once()
udpConn1.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) {
return udpReadFunc(msg1.Addr, udpConn1Ch, b)
})
io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{
SessionID: msg1.SessionID,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: msg1.Addr,
Data: []byte("hi back"),
}).Return(nil).Once()
msgCh <- msg1
udpConn1Ch <- []byte("hi back")
msg2data := []byte("how are you doing?")
msg2_1 := &protocol.UDPMessage{
SessionID: 5678,
PacketID: 0,
FragID: 0,
FragCount: 2,
Addr: "address2.net:12450",
Data: msg2data[:6],
}
msg2_2 := &protocol.UDPMessage{
SessionID: 5678,
PacketID: 0,
FragID: 1,
FragCount: 2,
Addr: "address2.net:12450",
Data: msg2data[6:],
}
eventLogger.EXPECT().New(msg2_1.SessionID, msg2_1.Addr).Return().Once()
udpConn2 := newMockUDPConn(t)
udpConn2Ch := make(chan []byte, 1)
// On fragmentation, make sure hook gets the whole message
io.EXPECT().Hook(msg2data, &msg2_1.Addr).Return(nil).Once()
io.EXPECT().UDP(msg2_1.Addr).Return(udpConn2, nil).Once()
udpConn2.EXPECT().WriteTo(msg2data, msg2_1.Addr).Return(11, nil).Once()
udpConn2.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) {
return udpReadFunc(msg2_1.Addr, udpConn2Ch, b)
})
io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{
SessionID: msg2_1.SessionID,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: msg2_1.Addr,
Data: []byte("im fine"),
}).Return(nil).Once()
msgCh <- msg2_1
msgCh <- msg2_2
udpConn2Ch <- []byte("im fine")
msg3 := &protocol.UDPMessage{
SessionID: 1234,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: "address1.com:9000",
Data: []byte("who are you?"),
}
udpConn1.EXPECT().WriteTo(msg3.Data, msg3.Addr).Return(12, nil).Once()
io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{
SessionID: msg3.SessionID,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: msg3.Addr,
Data: []byte("im your father"),
}).Return(nil).Once()
msgCh <- msg3
udpConn1Ch <- []byte("im your father")
// Make sure timeout works (connections closed & close events emitted)
udpConn1.EXPECT().Close().RunAndReturn(func() error {
close(udpConn1Ch)
return nil
}).Once()
udpConn2.EXPECT().Close().RunAndReturn(func() error {
close(udpConn2Ch)
return nil
}).Once()
eventLogger.EXPECT().Close(msg1.SessionID, nil).Once()
eventLogger.EXPECT().Close(msg2_1.SessionID, nil).Once()
time.Sleep(3 * time.Second) // Wait for timeout
mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn1, udpConn2)
// Test UDP connection close error propagation
errUDPClosed := errors.New("UDP connection closed")
msg4 := &protocol.UDPMessage{
SessionID: 666,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: "oh-no.com:27015",
Data: []byte("dont say bye"),
}
eventLogger.EXPECT().New(msg4.SessionID, msg4.Addr).Return().Once()
udpConn4 := newMockUDPConn(t)
io.EXPECT().Hook(msg4.Data, &msg4.Addr).Return(nil).Once()
io.EXPECT().UDP(msg4.Addr).Return(udpConn4, nil).Once()
udpConn4.EXPECT().WriteTo(msg4.Data, msg4.Addr).Return(12, nil).Once()
udpConn4.EXPECT().ReadFrom(mock.Anything).Return(0, "", errUDPClosed).Once()
udpConn4.EXPECT().Close().Return(nil).Once()
eventLogger.EXPECT().Close(msg4.SessionID, errUDPClosed).Once()
msgCh <- msg4
time.Sleep(1 * time.Second)
mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn4)
// Test UDP connection creation error propagation
errUDPIO := errors.New("UDP IO error")
msg5 := &protocol.UDPMessage{
SessionID: 777,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: "callmemaybe.com:15353",
Data: []byte("babe i miss you"),
}
eventLogger.EXPECT().New(msg5.SessionID, msg5.Addr).Return().Once()
io.EXPECT().Hook(msg5.Data, &msg5.Addr).Return(nil).Once()
io.EXPECT().UDP(msg5.Addr).Return(nil, errUDPIO).Once()
eventLogger.EXPECT().Close(msg5.SessionID, errUDPIO).Once()
msgCh <- msg5
time.Sleep(1 * time.Second)
mock.AssertExpectationsForObjects(t, io, eventLogger)
// Leak checks
close(msgCh) // This will return error from ReceiveMessage(), should stop the session manager
time.Sleep(1 * time.Second) // Wait one more second just to be sure
assert.Zero(t, sm.Count(), "session count should be 0")
goleak.VerifyNone(t)
}
================================================
FILE: extras/LICENSE.md
================================================
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: extras/auth/command.go
================================================
package auth
import (
"net"
"os/exec"
"strconv"
"strings"
"github.com/apernet/hysteria/core/v2/server"
)
var _ server.Authenticator = &CommandAuthenticator{}
type CommandAuthenticator struct {
Cmd string
}
func (a *CommandAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
cmd := exec.Command(a.Cmd, addr.String(), auth, strconv.Itoa(int(tx)))
out, err := cmd.Output()
if err != nil {
// This includes failing to execute the command,
// or the command exiting with a non-zero exit code.
return false, ""
} else {
return true, strings.TrimSpace(string(out))
}
}
================================================
FILE: extras/auth/http.go
================================================
package auth
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"time"
"github.com/apernet/hysteria/core/v2/server"
)
const (
httpAuthTimeout = 10 * time.Second
)
var _ server.Authenticator = &HTTPAuthenticator{}
var errInvalidStatusCode = errors.New("invalid status code")
type HTTPAuthenticator struct {
Client *http.Client
URL string
}
func NewHTTPAuthenticator(url string, insecure bool) *HTTPAuthenticator {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = &tls.Config{
InsecureSkipVerify: insecure,
}
return &HTTPAuthenticator{
Client: &http.Client{
Transport: tr,
Timeout: httpAuthTimeout,
},
URL: url,
}
}
type httpAuthRequest struct {
Addr string `json:"addr"`
Auth string `json:"auth"`
Tx uint64 `json:"tx"`
}
type httpAuthResponse struct {
OK bool `json:"ok"`
ID string `json:"id"`
}
func (a *HTTPAuthenticator) post(req *httpAuthRequest) (*httpAuthResponse, error) {
bs, err := json.Marshal(req)
if err != nil {
return nil, err
}
resp, err := a.Client.Post(a.URL, "application/json", bytes.NewReader(bs))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errInvalidStatusCode
}
respData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var authResp httpAuthResponse
err = json.Unmarshal(respData, &authResp)
if err != nil {
return nil, err
}
return &authResp, nil
}
func (a *HTTPAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
req := &httpAuthRequest{
Addr: addr.String(),
Auth: auth,
Tx: tx,
}
resp, err := a.post(req)
if err != nil {
return false, ""
}
return resp.OK, resp.ID
}
================================================
FILE: extras/auth/http_test.go
================================================
package auth
import (
"net"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestHTTPAuthenticator(t *testing.T) {
// Run the Python test auth server
cmd := exec.Command("python", "http_test.py")
err := cmd.Start()
assert.NoError(t, err)
defer cmd.Process.Kill()
time.Sleep(1 * time.Second) // Wait for the server to start
auth := NewHTTPAuthenticator("http://127.0.0.1:5000/auth", false)
ok, id := auth.Authenticate(&net.UDPAddr{
IP: net.ParseIP("1.2.3.4"),
Port: 34567,
}, "idk", 123)
assert.False(t, ok)
assert.Equal(t, "", id)
ok, id = auth.Authenticate(&net.UDPAddr{
IP: net.ParseIP("123.123.123.123"),
Port: 5566,
}, "wahaha", 12345)
assert.True(t, ok)
assert.Equal(t, "some_unique_id", id)
}
================================================
FILE: extras/auth/http_test.py
================================================
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/auth", methods=["POST"])
def auth():
data = request.json
if data is None:
return jsonify({"ok": False, "id": ""}), 400
addr = data.get("addr", "")
auth = data.get("auth", "")
tx = data.get("tx", 0)
if addr == "123.123.123.123:5566" and auth == "wahaha" and tx == 12345:
return jsonify({"ok": True, "id": "some_unique_id"})
else:
return jsonify({"ok": False, "id": ""})
if __name__ == "__main__":
app.run()
================================================
FILE: extras/auth/password.go
================================================
package auth
import (
"net"
"github.com/apernet/hysteria/core/v2/server"
)
var _ server.Authenticator = &PasswordAuthenticator{}
// PasswordAuthenticator is a simple authenticator that checks the password against a single string.
type PasswordAuthenticator struct {
Password string
}
func (a *PasswordAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
if auth == a.Password {
return true, "user"
} else {
return false, ""
}
}
================================================
FILE: extras/auth/password_test.go
================================================
package auth
import (
"net"
"testing"
)
func TestPasswordAuthenticator(t *testing.T) {
type fields struct {
Password string
}
type args struct {
addr net.Addr
auth string
tx uint64
}
tests := []struct {
name string
fields fields
args args
wantOk bool
wantId string
}{
{
name: "correct",
fields: fields{
Password: "yes,yes",
},
args: args{
addr: nil,
auth: "yes,yes",
tx: 0,
},
wantOk: true,
wantId: "user",
},
{
name: "incorrect",
fields: fields{
Password: "something_somehow",
},
args: args{
addr: nil,
auth: "random",
tx: 0,
},
wantOk: false,
wantId: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &PasswordAuthenticator{
Password: tt.fields.Password,
}
gotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx)
if gotOk != tt.wantOk {
t.Errorf("Authenticate() gotOk = %v, want %v", gotOk, tt.wantOk)
}
if gotId != tt.wantId {
t.Errorf("Authenticate() gotId = %v, want %v", gotId, tt.wantId)
}
})
}
}
================================================
FILE: extras/auth/userpass.go
================================================
package auth
import (
"net"
"strings"
"github.com/apernet/hysteria/core/v2/server"
)
const (
userPassSeparator = ":"
)
var _ server.Authenticator = &UserPassAuthenticator{}
// UserPassAuthenticator checks the provided auth string against a map of username/password pairs.
// The format of the auth string must be "username:password".
type UserPassAuthenticator struct {
users map[string]string
}
func NewUserPassAuthenticator(users map[string]string) *UserPassAuthenticator {
// Usernames are case-insensitive, as they are already lowercased by viper.
// Lowercase it again on our own to make it explicit.
lcUsers := make(map[string]string, len(users))
for user, pass := range users {
lcUsers[strings.ToLower(user)] = pass
}
return &UserPassAuthenticator{users: lcUsers}
}
func (a *UserPassAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
u, p, ok := splitUserPass(auth)
if !ok {
return false, ""
}
rp, ok := a.users[u]
if !ok || rp != p {
return false, ""
}
return true, u
}
func splitUserPass(auth string) (user, pass string, ok bool) {
rs := strings.SplitN(auth, userPassSeparator, 2)
if len(rs) != 2 {
return "", "", false
}
// Usernames are case-insensitive
return strings.ToLower(rs[0]), rs[1], true
}
================================================
FILE: extras/auth/userpass_test.go
================================================
package auth
import (
"net"
"testing"
)
func TestUserPassAuthenticator(t *testing.T) {
type fields struct {
Users map[string]string
}
type args struct {
addr net.Addr
auth string
tx uint64
}
tests := []struct {
name string
fields fields
args args
wantOk bool
wantId string
}{
{
name: "correct 1",
fields: fields{
Users: map[string]string{
"saul": "goodman",
"wang": "123",
},
},
args: args{
addr: nil,
auth: "wang:123",
tx: 0,
},
wantOk: true,
wantId: "wang",
},
{
name: "correct 2",
fields: fields{
Users: map[string]string{
"gawr": "gura",
"fubuki": "shirakami",
},
},
args: args{
addr: nil,
auth: "gawr:gura",
tx: 0,
},
wantOk: true,
wantId: "gawr",
},
{
name: "incorrect 1",
fields: fields{
Users: map[string]string{
"gawr": "gura",
"fubuki": "shirakami",
},
},
args: args{
addr: nil,
auth: "random:stranger",
tx: 0,
},
wantOk: false,
wantId: "",
},
{
name: "incorrect 2",
fields: fields{
Users: map[string]string{
"gawr": "gura",
"fubuki": "shirakami",
},
},
args: args{
addr: nil,
auth: "poop",
tx: 0,
},
wantOk: false,
wantId: "",
},
{
name: "case insensitive username",
fields: fields{
Users: map[string]string{
"gawR": "gura",
"fubuki": "shirakami",
},
},
args: args{
addr: nil,
auth: "Gawr:gura",
tx: 0,
},
wantOk: true,
wantId: "gawr",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := NewUserPassAuthenticator(tt.fields.Users)
gotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx)
if gotOk != tt.wantOk {
t.Errorf("Authenticate() gotOk = %v, want %v", gotOk, tt.wantOk)
}
if gotId != tt.wantId {
t.Errorf("Authenticate() gotId = %v, want %v", gotId, tt.wantId)
}
})
}
}
================================================
FILE: extras/correctnet/correctnet.go
================================================
package correctnet
import (
"net"
"net/http"
"strings"
)
func extractIPFamily(ip net.IP) (family string) {
if len(ip) == 0 {
// real family independent wildcard address, such as ":443"
return ""
}
if p4 := ip.To4(); len(p4) == net.IPv4len {
return "4"
}
return "6"
}
func tcpAddrNetwork(addr *net.TCPAddr) (network string) {
if addr == nil {
return "tcp"
}
return "tcp" + extractIPFamily(addr.IP)
}
func udpAddrNetwork(addr *net.UDPAddr) (network string) {
if addr == nil {
return "udp"
}
return "udp" + extractIPFamily(addr.IP)
}
func ipAddrNetwork(addr *net.IPAddr) (network string) {
if addr == nil {
return "ip"
}
return "ip" + extractIPFamily(addr.IP)
}
func Listen(network, address string) (net.Listener, error) {
if network == "tcp" {
tcpAddr, err := net.ResolveTCPAddr(network, address)
if err != nil {
return nil, err
}
return ListenTCP(network, tcpAddr)
}
return net.Listen(network, address)
}
func ListenTCP(network string, laddr *net.TCPAddr) (*net.TCPListener, error) {
if network == "tcp" {
return net.ListenTCP(tcpAddrNetwork(laddr), laddr)
}
return net.ListenTCP(network, laddr)
}
func ListenPacket(network, address string) (listener net.PacketConn, err error) {
if network == "udp" {
udpAddr, err := net.ResolveUDPAddr(network, address)
if err != nil {
return nil, err
}
return ListenUDP(network, udpAddr)
}
if strings.HasPrefix(network, "ip:") {
proto := network[3:]
ipAddr, err := net.ResolveIPAddr(proto, address)
if err != nil {
return nil, err
}
return net.ListenIP(ipAddrNetwork(ipAddr)+":"+proto, ipAddr)
}
return net.ListenPacket(network, address)
}
func ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error) {
if network == "udp" {
return net.ListenUDP(udpAddrNetwork(laddr), laddr)
}
return net.ListenUDP(network, laddr)
}
func HTTPListenAndServe(address string, handler http.Handler) error {
listener, err := Listen("tcp", address)
if err != nil {
return err
}
defer listener.Close()
return http.Serve(listener, handler)
}
================================================
FILE: extras/go.mod
================================================
module github.com/apernet/hysteria/extras/v2
go 1.24.0
toolchain go1.25.1
require (
github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22
github.com/database64128/tfo-go/v2 v2.2.2
github.com/hashicorp/golang-lru/v2 v2.0.5
github.com/miekg/dns v1.1.59
github.com/refraction-networking/utls v1.6.6
github.com/stretchr/testify v1.11.1
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
golang.org/x/crypto v0.47.0
golang.org/x/net v0.49.0
google.golang.org/protobuf v1.34.1
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/apernet/hysteria/core/v2 => ../core
================================================
FILE: extras/go.sum
================================================
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a h1:t4SDi0pmNkryzKdM4QF3o5vqSP4GRjeZD/6j3nyxNP0=
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a/go.mod h1:7K2NQKbabB5mBl41vF6YayYl5g7YpDwc4dQ5iMpP3Lg=
github.com/database64128/tfo-go/v2 v2.2.2 h1:BxynF4qGF5ct3DpPLEG62uyJZ3LQhqaf0Ken+kyy7PM=
github.com/database64128/tfo-go/v2 v2.2.2/go.mod h1:2IW8jppdBwdVMjA08uEyMNnqiAHKUlqAA+J8NrsfktY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM=
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: extras/masq/server.go
================================================
package masq
import (
"bufio"
"crypto/tls"
"fmt"
"net"
"net/http"
"github.com/apernet/hysteria/extras/v2/correctnet"
)
// MasqTCPServer covers the TCP parts of a standard web server (TCP based HTTP/HTTPS).
// We provide this as an option for masquerading, as some may consider a server
// "suspicious" if it only serves the QUIC protocol and not standard HTTP/HTTPS.
type MasqTCPServer struct {
QUICPort int
HTTPSPort int
Handler http.Handler
TLSConfig *tls.Config
ForceHTTPS bool // Always 301 redirect from HTTP to HTTPS
}
func (s *MasqTCPServer) ListenAndServeHTTP(addr string) error {
return correctnet.HTTPListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.ForceHTTPS {
if s.HTTPSPort == 0 || s.HTTPSPort == 443 {
// Omit port if it's the default
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently)
} else {
http.Redirect(w, r, fmt.Sprintf("https://%s:%d%s", r.Host, s.HTTPSPort, r.RequestURI), http.StatusMovedPermanently)
}
return
}
s.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r)
}))
}
func (s *MasqTCPServer) ListenAndServeHTTPS(addr string) error {
server := &http.Server{
Addr: addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.Handler.ServeHTTP(newAltSvcHijackResponseWriter(w, s.QUICPort), r)
}),
TLSConfig: s.TLSConfig,
}
listener, err := correctnet.Listen("tcp", addr)
if err != nil {
return err
}
defer listener.Close()
return server.ServeTLS(listener, "", "")
}
var _ http.ResponseWriter = (*altSvcHijackResponseWriter)(nil)
// altSvcHijackResponseWriter makes sure that the Alt-Svc's port
// is always set with our own value, no matter what the handler sets.
type altSvcHijackResponseWriter struct {
Port int
http.ResponseWriter
}
func (w *altSvcHijackResponseWriter) WriteHeader(statusCode int) {
w.Header().Set("Alt-Svc", fmt.Sprintf(`h3=":%d"; ma=2592000`, w.Port))
w.ResponseWriter.WriteHeader(statusCode)
}
var _ http.Hijacker = (*altSvcHijackResponseWriterHijacker)(nil)
// altSvcHijackResponseWriterHijacker is a wrapper around altSvcHijackResponseWriter
// that also implements http.Hijacker. This is needed for WebSocket support.
type altSvcHijackResponseWriterHijacker struct {
altSvcHijackResponseWriter
}
func (w *altSvcHijackResponseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.ResponseWriter.(http.Hijacker).Hijack()
}
func newAltSvcHijackResponseWriter(w http.ResponseWriter, port int) http.ResponseWriter {
if _, ok := w.(http.Hijacker); ok {
return &altSvcHijackResponseWriterHijacker{
altSvcHijackResponseWriter: altSvcHijackResponseWriter{
Port: port,
ResponseWriter: w,
},
}
}
return &altSvcHijackResponseWriter{
Port: port,
ResponseWriter: w,
}
}
================================================
FILE: extras/obfs/conn.go
================================================
package obfs
import (
"net"
"sync"
"syscall"
"time"
)
const udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough
// Obfuscator is the interface that wraps the Obfuscate and Deobfuscate methods.
// Both methods return the number of bytes written to out.
// If a packet is not valid, the methods should return 0.
type Obfuscator interface {
Obfuscate(in, out []byte) int
Deobfuscate(in, out []byte) int
}
var _ net.PacketConn = (*obfsPacketConn)(nil)
type obfsPacketConn struct {
Conn net.PacketConn
Obfs Obfuscator
readBuf []byte
readMutex sync.Mutex
writeBuf []byte
writeMutex sync.Mutex
}
// obfsPacketConnUDP is a special case of obfsPacketConn that uses a UDPConn
// as the underlying connection. We pass additional methods to quic-go to
// enable UDP-specific optimizations.
type obfsPacketConnUDP struct {
*obfsPacketConn
UDPConn *net.UDPConn
}
// WrapPacketConn enables obfuscation on a net.PacketConn.
// The obfuscation is transparent to the caller - the n bytes returned by
// ReadFrom and WriteTo are the number of original bytes, not after
// obfuscation/deobfuscation.
func WrapPacketConn(conn net.PacketConn, obfs Obfuscator) net.PacketConn {
opc := &obfsPacketConn{
Conn: conn,
Obfs: obfs,
readBuf: make([]byte, udpBufferSize),
writeBuf: make([]byte, udpBufferSize),
}
if udpConn, ok := conn.(*net.UDPConn); ok {
return &obfsPacketConnUDP{
obfsPacketConn: opc,
UDPConn: udpConn,
}
} else {
return opc
}
}
func (c *obfsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
for {
c.readMutex.Lock()
n, addr, err = c.Conn.ReadFrom(c.readBuf)
if n <= 0 {
c.readMutex.Unlock()
return n, addr, err
}
n = c.Obfs.Deobfuscate(c.readBuf[:n], p)
c.readMutex.Unlock()
if n > 0 || err != nil {
return n, addr, err
}
// Invalid packet, try again
}
}
func (c *obfsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
c.writeMutex.Lock()
nn := c.Obfs.Obfuscate(p, c.writeBuf)
_, err = c.Conn.WriteTo(c.writeBuf[:nn], addr)
c.writeMutex.Unlock()
if err == nil {
n = len(p)
}
return n, err
}
func (c *obfsPacketConn) Close() error {
return c.Conn.Close()
}
func (c *obfsPacketConn) LocalAddr() net.Addr {
return c.Conn.LocalAddr()
}
func (c *obfsPacketConn) SetDeadline(t time.Time) error {
return c.Conn.SetDeadline(t)
}
func (c *obfsPacketConn) SetReadDeadline(t time.Time) error {
return c.Conn.SetReadDeadline(t)
}
func (c *obfsPacketConn) SetWriteDeadline(t time.Time) error {
return c.Conn.SetWriteDeadline(t)
}
// UDP-specific methods below
func (c *obfsPacketConnUDP) SetReadBuffer(bytes int) error {
return c.UDPConn.SetReadBuffer(bytes)
}
func (c *obfsPacketConnUDP) SetWriteBuffer(bytes int) error {
return c.UDPConn.SetWriteBuffer(bytes)
}
func (c *obfsPacketConnUDP) SyscallConn() (syscall.RawConn, error) {
return c.UDPConn.SyscallConn()
}
================================================
FILE: extras/obfs/salamander.go
================================================
package obfs
import (
"fmt"
"math/rand"
"sync"
"time"
"golang.org/x/crypto/blake2b"
)
const (
smPSKMinLen = 4
smSaltLen = 8
smKeyLen = blake2b.Size256
)
var _ Obfuscator = (*SalamanderObfuscator)(nil)
var ErrPSKTooShort = fmt.Errorf("PSK must be at least %d bytes", smPSKMinLen)
// SalamanderObfuscator is an obfuscator that obfuscates each packet with
// the BLAKE2b-256 hash of a pre-shared key combined with a random salt.
// Packet format: [8-byte salt][payload]
type SalamanderObfuscator struct {
PSK []byte
RandSrc *rand.Rand
lk sync.Mutex
}
func NewSalamanderObfuscator(psk []byte) (*SalamanderObfuscator, error) {
if len(psk) < smPSKMinLen {
return nil, ErrPSKTooShort
}
return &SalamanderObfuscator{
PSK: psk,
RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())),
}, nil
}
func (o *SalamanderObfuscator) Obfuscate(in, out []byte) int {
outLen := len(in) + smSaltLen
if len(out) < outLen {
return 0
}
o.lk.Lock()
_, _ = o.RandSrc.Read(out[:smSaltLen])
o.lk.Unlock()
key := o.key(out[:smSaltLen])
for i, c := range in {
out[i+smSaltLen] = c ^ key[i%smKeyLen]
}
return outLen
}
func (o *SalamanderObfuscator) Deobfuscate(in, out []byte) int {
outLen := len(in) - smSaltLen
if outLen <= 0 || len(out) < outLen {
return 0
}
key := o.key(in[:smSaltLen])
for i, c := range in[smSaltLen:] {
out[i] = c ^ key[i%smKeyLen]
}
return outLen
}
func (o *SalamanderObfuscator) key(salt []byte) [smKeyLen]byte {
return blake2b.Sum256(append(o.PSK, salt...))
}
================================================
FILE: extras/obfs/salamander_test.go
================================================
package obfs
import (
"crypto/rand"
"testing"
"github.com/stretchr/testify/assert"
)
func BenchmarkSalamanderObfuscator_Obfuscate(b *testing.B) {
o, _ := NewSalamanderObfuscator([]byte("average_password"))
in := make([]byte, 1200)
_, _ = rand.Read(in)
out := make([]byte, 2048)
b.ResetTimer()
for i := 0; i < b.N; i++ {
o.Obfuscate(in, out)
}
}
func BenchmarkSalamanderObfuscator_Deobfuscate(b *testing.B) {
o, _ := NewSalamanderObfuscator([]byte("average_password"))
in := make([]byte, 1200)
_, _ = rand.Read(in)
out := make([]byte, 2048)
b.ResetTimer()
for i := 0; i < b.N; i++ {
o.Deobfuscate(in, out)
}
}
func TestSalamanderObfuscator(t *testing.T) {
o, _ := NewSalamanderObfuscator([]byte("average_password"))
in := make([]byte, 1200)
oOut := make([]byte, 2048)
dOut := make([]byte, 2048)
for i := 0; i < 1000; i++ {
_, _ = rand.Read(in)
n := o.Obfuscate(in, oOut)
assert.Equal(t, len(in)+smSaltLen, n)
n = o.Deobfuscate(oOut[:n], dOut)
assert.Equal(t, len(in), n)
assert.Equal(t, in, dOut[:n])
}
}
================================================
FILE: extras/outbounds/.mockery.yaml
================================================
with-expecter: true
inpackage: true
dir: .
packages:
github.com/apernet/hysteria/extras/v2/outbounds:
interfaces:
PluggableOutbound:
config:
mockname: mockPluggableOutbound
UDPConn:
config:
mockname: mockUDPConn
================================================
FILE: extras/outbounds/acl/compile.go
================================================
package acl
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
lru "github.com/hashicorp/golang-lru/v2"
)
type Protocol int
const (
ProtocolBoth Protocol = iota
ProtocolTCP
ProtocolUDP
)
func (p Protocol) String() string {
switch p {
case ProtocolBoth:
return "tcp+udp"
case ProtocolTCP:
return "tcp"
case ProtocolUDP:
return "udp"
default:
return fmt.Sprintf("Protocol(%d)", int(p))
}
}
type Outbound interface {
any
}
type HostInfo struct {
Name string
IPv4 net.IP
IPv6 net.IP
}
func (h HostInfo) String() string {
return fmt.Sprintf("%s|%s|%s", h.Name, h.IPv4, h.IPv6)
}
type CompiledRuleSet[O Outbound] interface {
Match(host HostInfo, proto Protocol, port uint16) (O, net.IP)
}
type compiledRule[O Outbound] struct {
Outbound O
HostMatcher hostMatcher
Protocol Protocol
StartPort uint16
EndPort uint16
HijackAddress net.IP
}
func (r *compiledRule[O]) Match(host HostInfo, proto Protocol, port uint16) bool {
if r.Protocol != ProtocolBoth && r.Protocol != proto {
return false
}
if r.StartPort != 0 && (port < r.StartPort || port > r.EndPort) {
return false
}
return r.HostMatcher.Match(host)
}
type matchResult[O Outbound] struct {
Outbound O
HijackAddress net.IP
}
type compiledRuleSetImpl[O Outbound] struct {
Rules []compiledRule[O]
Cache *lru.Cache[matchResultCacheKey, matchResult[O]] // key: HostInfo.String()
}
type matchResultCacheKey struct {
Host string
Proto Protocol
Port uint16
}
func (s *compiledRuleSetImpl[O]) Match(host HostInfo, proto Protocol, port uint16) (O, net.IP) {
host.Name = strings.ToLower(host.Name) // Normalize host name to lower case
key := matchResultCacheKey{
Host: host.String(),
Proto: proto,
Port: port,
}
if result, ok := s.Cache.Get(key); ok {
return result.Outbound, result.HijackAddress
}
for _, rule := range s.Rules {
if rule.Match(host, proto, port) {
result := matchResult[O]{rule.Outbound, rule.HijackAddress}
s.Cache.Add(key, result)
return result.Outbound, result.HijackAddress
}
}
// No match should also be cached
var zero O
s.Cache.Add(key, matchResult[O]{zero, nil})
return zero, nil
}
type CompilationError struct {
LineNum int
Message string
}
func (e *CompilationError) Error() string {
return fmt.Sprintf("error at line %d: %s", e.LineNum, e.Message)
}
type GeoLoader interface {
LoadGeoIP() (map[string]*v2geo.GeoIP, error)
LoadGeoSite() (map[string]*v2geo.GeoSite, error)
}
// Compile compiles TextRules into a CompiledRuleSet.
// Names in the outbounds map MUST be in all lower case.
// We want on-demand loading of GeoIP/GeoSite databases, so instead of passing the
// databases directly, we use a GeoLoader interface to load them only when needed
// by at least one rule.
func Compile[O Outbound](rules []TextRule, outbounds map[string]O,
cacheSize int, geoLoader GeoLoader,
) (CompiledRuleSet[O], error) {
compiledRules := make([]compiledRule[O], len(rules))
for i, rule := range rules {
outbound, ok := outbounds[strings.ToLower(rule.Outbound)]
if !ok {
return nil, &CompilationError{rule.LineNum, fmt.Sprintf("outbound %s not found", rule.Outbound)}
}
hm, errStr := compileHostMatcher(rule.Address, geoLoader)
if errStr != "" {
return nil, &CompilationError{rule.LineNum, errStr}
}
proto, startPort, endPort, ok := parseProtoPort(rule.ProtoPort)
if !ok {
return nil, &CompilationError{rule.LineNum, fmt.Sprintf("invalid protocol/port: %s", rule.ProtoPort)}
}
var hijackAddress net.IP
if rule.HijackAddress != "" {
hijackAddress = net.ParseIP(rule.HijackAddress)
if hijackAddress == nil {
return nil, &CompilationError{rule.LineNum, fmt.Sprintf("invalid hijack address (must be an IP address): %s", rule.HijackAddress)}
}
}
compiledRules[i] = compiledRule[O]{outbound, hm, proto, startPort, endPort, hijackAddress}
}
cache, err := lru.New[matchResultCacheKey, matchResult[O]](cacheSize)
if err != nil {
return nil, err
}
return &compiledRuleSetImpl[O]{compiledRules, cache}, nil
}
// parseProtoPort parses the protocol and port from a protoPort string.
// protoPort must be in one of the following formats:
//
// proto/port
// proto/*
// proto
// */port
// */*
// *
// [empty] (same as *)
//
// proto must be either "tcp" or "udp", case-insensitive.
func parseProtoPort(protoPort string) (Protocol, uint16, uint16, bool) {
protoPort = strings.ToLower(protoPort)
if protoPort == "" || protoPort == "*" || protoPort == "*/*" {
return ProtocolBoth, 0, 0, true
}
parts := strings.SplitN(protoPort, "/", 2)
if len(parts) == 1 {
// No port, only protocol
switch parts[0] {
case "tcp":
return ProtocolTCP, 0, 0, true
case "udp":
return ProtocolUDP, 0, 0, true
default:
return ProtocolBoth, 0, 0, false
}
} else {
// Both protocol and port
var proto Protocol
var startPort, endPort uint16
switch parts[0] {
case "tcp":
proto = ProtocolTCP
case "udp":
proto = ProtocolUDP
case "*":
proto = ProtocolBoth
default:
return ProtocolBoth, 0, 0, false
}
if parts[1] != "*" {
// We allow either a single port or a range (e.g. "1000-2000")
ports := strings.SplitN(strings.TrimSpace(parts[1]), "-", 2)
if len(ports) == 1 {
p64, err := strconv.ParseUint(parts[1], 10, 16)
if err != nil {
return ProtocolBoth, 0, 0, false
}
startPort = uint16(p64)
endPort = startPort
} else {
p64, err := strconv.ParseUint(ports[0], 10, 16)
if err != nil {
return ProtocolBoth, 0, 0, false
}
startPort = uint16(p64)
p64, err = strconv.ParseUint(ports[1], 10, 16)
if err != nil {
return ProtocolBoth, 0, 0, false
}
endPort = uint16(p64)
if startPort > endPort {
return ProtocolBoth, 0, 0, false
}
}
}
return proto, startPort, endPort, true
}
}
func compileHostMatcher(addr string, geoLoader GeoLoader) (hostMatcher, string) {
addr = strings.ToLower(addr) // Normalize to lower case
if addr == "*" || addr == "all" {
// Match all hosts
return &allMatcher{}, ""
}
if strings.HasPrefix(addr, "geoip:") {
// GeoIP matcher
country := addr[6:]
if len(country) == 0 {
return nil, "empty GeoIP country code"
}
gMap, err := geoLoader.LoadGeoIP()
if err != nil {
return nil, err.Error()
}
list, ok := gMap[country]
if !ok || list == nil {
return nil, fmt.Sprintf("GeoIP country code %s not found", country)
}
m, err := newGeoIPMatcher(list)
if err != nil {
return nil, err.Error()
}
return m, ""
}
if strings.HasPrefix(addr, "geosite:") {
// GeoSite matcher
name, attrs := parseGeoSiteName(addr[8:])
if len(name) == 0 {
return nil, "empty GeoSite name"
}
gMap, err := geoLoader.LoadGeoSite()
if err != nil {
return nil, err.Error()
}
list, ok := gMap[name]
if !ok || list == nil {
return nil, fmt.Sprintf("GeoSite name %s not found", name)
}
m, err := newGeositeMatcher(list, attrs)
if err != nil {
return nil, err.Error()
}
return m, ""
}
if strings.HasPrefix(addr, "suffix:") {
// Domain suffix matcher
suffix := addr[7:]
if len(suffix) == 0 {
return nil, "empty domain suffix"
}
return &domainMatcher{
Pattern: suffix,
Mode: domainMatchSuffix,
}, ""
}
if strings.Contains(addr, "/") {
// CIDR matcher
_, ipnet, err := net.ParseCIDR(addr)
if err != nil {
return nil, fmt.Sprintf("invalid CIDR address: %s", addr)
}
return &cidrMatcher{ipnet}, ""
}
if ip := net.ParseIP(addr); ip != nil {
// Single IP matcher
return &ipMatcher{ip}, ""
}
if strings.Contains(addr, "*") {
// Wildcard domain matcher
return &domainMatcher{
Pattern: addr,
Mode: domainMatchWildcard,
}, ""
}
// Nothing else matched, treat it as a non-wildcard domain
return &domainMatcher{
Pattern: addr,
Mode: domainMatchExact,
}, ""
}
func parseGeoSiteName(s string) (string, []string) {
parts := strings.Split(s, "@")
base := strings.TrimSpace(parts[0])
attrs := parts[1:]
for i := range attrs {
attrs[i] = strings.TrimSpace(attrs[i])
}
return base, attrs
}
================================================
FILE: extras/outbounds/acl/compile_test.go
================================================
package acl
import (
"fmt"
"net"
"testing"
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
"github.com/stretchr/testify/assert"
)
var _ GeoLoader = (*testGeoLoader)(nil)
type testGeoLoader struct{}
func (l *testGeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
return v2geo.LoadGeoIP("v2geo/geoip.dat")
}
func (l *testGeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
return v2geo.LoadGeoSite("v2geo/geosite.dat")
}
func TestCompile(t *testing.T) {
ob1, ob2, ob3, ob4, ob5, ob6 := 1, 2, 3, 4, 5, 6
rules := []TextRule{
{
Outbound: "ob1",
Address: "1.2.3.4",
ProtoPort: "",
HijackAddress: "",
},
{
Outbound: "ob2",
Address: "8.8.8.0/24",
ProtoPort: "*",
HijackAddress: "1.1.1.1",
},
{
Outbound: "ob3",
Address: "all",
ProtoPort: "udp/443",
HijackAddress: "",
},
{
Outbound: "ob1",
Address: "2606:4700::6810:85e5",
ProtoPort: "tcp",
HijackAddress: "2606:4700::6810:85e6",
},
{
Outbound: "ob2",
Address: "2606:4700::/44",
ProtoPort: "*/8888",
HijackAddress: "",
},
{
Outbound: "ob3",
Address: "*.v2ex.com",
ProtoPort: "udp",
HijackAddress: "",
},
{
Outbound: "ob1",
Address: "crap.v2ex.com",
ProtoPort: "tcp/80",
HijackAddress: "2.2.2.2",
},
{
Outbound: "ob2",
Address: "geoip:JP",
ProtoPort: "*/*",
HijackAddress: "",
},
{
Outbound: "ob4",
Address: "geosite:4chan",
ProtoPort: "*/*",
HijackAddress: "",
},
{
Outbound: "ob4",
Address: "geosite:google @cn",
ProtoPort: "*/*",
HijackAddress: "",
},
{
Outbound: "ob5",
Address: "suffix:microsoft.com",
ProtoPort: "*/*",
HijackAddress: "",
},
{
Outbound: "ob6",
Address: "all",
ProtoPort: "tcp/6881-6889",
HijackAddress: "",
},
}
comp, err := Compile[int](rules, map[string]int{
"ob1": ob1,
"ob2": ob2,
"ob3": ob3,
"ob4": ob4,
"ob5": ob5,
"ob6": ob6,
}, 100, &testGeoLoader{})
assert.NoError(t, err)
tests := []struct {
host HostInfo
proto Protocol
port uint16
wantOutbound int
wantIP net.IP
}{
{
host: HostInfo{
IPv4: net.ParseIP("1.2.3.4"),
},
proto: ProtocolTCP,
port: 1234,
wantOutbound: ob1,
wantIP: nil,
},
{
host: HostInfo{
IPv4: net.ParseIP("8.8.8.4"),
},
proto: ProtocolUDP,
port: 5353,
wantOutbound: ob2,
wantIP: net.ParseIP("1.1.1.1"),
},
{
host: HostInfo{
Name: "lean.delicious.com",
},
proto: ProtocolUDP,
port: 443,
wantOutbound: ob3,
wantIP: nil,
},
{
host: HostInfo{
IPv6: net.ParseIP("2606:4700::6810:85e5"),
},
proto: ProtocolTCP,
port: 80,
wantOutbound: ob1,
wantIP: net.ParseIP("2606:4700::6810:85e6"),
},
{
host: HostInfo{
IPv6: net.ParseIP("2606:4700:0:0:0:0:0:1"),
},
proto: ProtocolUDP,
port: 8888,
wantOutbound: ob2,
wantIP: nil,
},
{
host: HostInfo{
Name: "www.v2ex.com",
},
proto: ProtocolUDP,
port: 1234,
wantOutbound: ob3,
wantIP: nil,
},
{
host: HostInfo{
Name: "crap.v2ex.com",
},
proto: ProtocolTCP,
port: 80,
wantOutbound: ob1,
wantIP: net.ParseIP("2.2.2.2"),
},
{
host: HostInfo{
Name: "crap.v2ex.com",
},
proto: ProtocolTCP,
port: 81,
wantOutbound: 0,
},
{
host: HostInfo{
Name: "crap.v2ex.com",
},
proto: ProtocolUDP,
port: 80,
wantOutbound: ob3,
},
{
host: HostInfo{
Name: "crap.v2ex.com",
},
proto: ProtocolUDP,
port: 81,
wantOutbound: ob3,
},
{
host: HostInfo{
IPv4: net.ParseIP("210.140.92.187"),
},
proto: ProtocolTCP,
port: 25,
wantOutbound: ob2,
wantIP: nil,
},
{
host: HostInfo{
IPv4: net.ParseIP("175.45.176.73"),
},
proto: ProtocolTCP,
port: 80,
wantOutbound: 0, // no match default
wantIP: nil,
},
{
host: HostInfo{
Name: "boards.4channel.org",
},
proto: ProtocolTCP,
port: 443,
wantOutbound: ob4,
wantIP: nil,
},
{
host: HostInfo{
Name: "gstatic-cn.com",
},
proto: ProtocolUDP,
port: 9999,
wantOutbound: ob4,
wantIP: nil,
},
{
host: HostInfo{
Name: "hoho.waymo.com",
},
proto: ProtocolUDP,
port: 9999,
wantOutbound: 0, // no match default
wantIP: nil,
},
{
host: HostInfo{
Name: "microsoft.com",
},
proto: ProtocolTCP,
port: 6000,
wantOutbound: ob5,
wantIP: nil,
},
{
host: HostInfo{
Name: "real.microsoft.com",
},
proto: ProtocolUDP,
port: 5353,
wantOutbound: ob5,
wantIP: nil,
},
{
host: HostInfo{
Name: "fakemicrosoft.com",
},
proto: ProtocolTCP,
port: 5000,
wantOutbound: 0, // no match default
wantIP: nil,
},
{
host: HostInfo{
IPv4: net.ParseIP("223.1.1.1"),
},
proto: ProtocolTCP,
port: 6883,
wantOutbound: ob6, // match range port rule 6881-6889
wantIP: nil,
},
}
for _, test := range tests {
testName := fmt.Sprintf("%s#%s#%d", test.host, test.proto, test.port)
t.Run(testName, func(t *testing.T) {
gotOutbound, gotIP := comp.Match(test.host, test.proto, test.port)
assert.Equal(t, test.wantOutbound, gotOutbound)
assert.Equal(t, test.wantIP, gotIP)
})
}
// Test Invalid Port Range Rule
eb1 := 1
invalidRules := []TextRule{
{
Outbound: "eb1",
Address: "1.1.2.0/24",
ProtoPort: "*/3-1",
HijackAddress: "",
},
}
_, err = Compile[int](invalidRules, map[string]int{
"eb1": eb1,
}, 100, &testGeoLoader{})
assert.Error(t, err)
}
func Test_parseGeoSiteName(t *testing.T) {
tests := []struct {
name string
s string
want string
want1 []string
}{
{
name: "no attrs",
s: "pornhub",
want: "pornhub",
want1: []string{},
},
{
name: "one attr 1",
s: "xiaomi@cn",
want: "xiaomi",
want1: []string{"cn"},
},
{
name: "one attr 2",
s: " google @jp ",
want: "google",
want1: []string{"jp"},
},
{
name: "two attrs 1",
s: "netflix@jp@kr",
want: "netflix",
want1: []string{"jp", "kr"},
},
{
name: "two attrs 2",
s: "netflix @xixi @haha ",
want: "netflix",
want1: []string{"xixi", "haha"},
},
{
name: "empty",
s: "",
want: "",
want1: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := parseGeoSiteName(tt.s)
assert.Equalf(t, tt.want, got, "parseGeoSiteName(%v)", tt.s)
assert.Equalf(t, tt.want1, got1, "parseGeoSiteName(%v)", tt.s)
})
}
}
================================================
FILE: extras/outbounds/acl/matchers.go
================================================
package acl
import (
"net"
"strings"
"golang.org/x/net/idna"
)
const (
domainMatchExact = uint8(iota)
domainMatchWildcard
domainMatchSuffix
)
type hostMatcher interface {
Match(HostInfo) bool
}
type ipMatcher struct {
IP net.IP
}
func (m *ipMatcher) Match(host HostInfo) bool {
return m.IP.Equal(host.IPv4) || m.IP.Equal(host.IPv6)
}
type cidrMatcher struct {
IPNet *net.IPNet
}
func (m *cidrMatcher) Match(host HostInfo) bool {
return m.IPNet.Contains(host.IPv4) || m.IPNet.Contains(host.IPv6)
}
type domainMatcher struct {
Pattern string
Mode uint8
}
func (m *domainMatcher) Match(host HostInfo) bool {
name, err := idna.ToUnicode(host.Name)
if err != nil {
name = host.Name
}
switch m.Mode {
case domainMatchExact:
return name == m.Pattern
case domainMatchWildcard:
return deepMatchRune([]rune(name), []rune(m.Pattern))
case domainMatchSuffix:
return name == m.Pattern || strings.HasSuffix(name, "."+m.Pattern)
default:
return false // Invalid mode
}
}
func deepMatchRune(str, pattern []rune) bool {
for len(pattern) > 0 {
switch pattern[0] {
default:
if len(str) == 0 || str[0] != pattern[0] {
return false
}
case '*':
return deepMatchRune(str, pattern[1:]) ||
(len(str) > 0 && deepMatchRune(str[1:], pattern))
}
str = str[1:]
pattern = pattern[1:]
}
return len(str) == 0 && len(pattern) == 0
}
type allMatcher struct{}
func (m *allMatcher) Match(host HostInfo) bool {
return true
}
================================================
FILE: extras/outbounds/acl/matchers_test.go
================================================
package acl
import (
"net"
"testing"
)
func Test_ipMatcher_Match(t *testing.T) {
tests := []struct {
name string
IP net.IP
host HostInfo
want bool
}{
{
name: "ipv4 match",
IP: net.IPv4(127, 0, 0, 1),
host: HostInfo{
IPv4: net.IPv4(127, 0, 0, 1),
IPv6: nil,
},
want: true,
},
{
name: "ipv6 match",
IP: net.IPv6loopback,
host: HostInfo{
IPv4: nil,
IPv6: net.IPv6loopback,
},
want: true,
},
{
name: "no match",
IP: net.IPv4(127, 0, 0, 1),
host: HostInfo{
IPv4: net.IPv4(127, 0, 0, 2),
IPv6: net.IPv6loopback,
},
want: false,
},
{
name: "both nil",
IP: net.IPv4(127, 0, 0, 1),
host: HostInfo{
IPv4: nil,
IPv6: nil,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &ipMatcher{
IP: tt.IP,
}
if got := m.Match(tt.host); got != tt.want {
t.Errorf("Match() = %v, want %v", got, tt.want)
}
})
}
}
func Test_cidrMatcher_Match(t *testing.T) {
_, cidr1, _ := net.ParseCIDR("192.168.1.0/24")
_, cidr2, _ := net.ParseCIDR("::1/128")
_, cidr3, _ := net.ParseCIDR("0.0.0.0/0")
_, cidr4, _ := net.ParseCIDR("::/0")
tests := []struct {
name string
IPNet *net.IPNet
host HostInfo
want bool
}{
{
name: "ipv4 match",
IPNet: cidr1,
host: HostInfo{
IPv4: net.ParseIP("192.168.1.100"),
IPv6: net.ParseIP("::1"),
},
want: true,
},
{
name: "ipv6 match",
IPNet: cidr2,
host: HostInfo{
IPv4: net.ParseIP("10.0.0.1"),
IPv6: net.ParseIP("::1"),
},
want: true,
},
{
name: "no match",
IPNet: cidr1,
host: HostInfo{
IPv4: net.ParseIP("10.0.0.1"),
IPv6: net.ParseIP("2001:db8::2:1"),
},
want: false,
},
{
name: "ipv4 broad",
IPNet: cidr3,
host: HostInfo{
IPv4: net.ParseIP("10.0.0.1"),
IPv6: net.ParseIP("::1"),
},
want: true,
},
{
name: "ipv6 broad",
IPNet: cidr4,
host: HostInfo{
IPv4: net.ParseIP("10.0.0.1"),
IPv6: net.ParseIP("2001:db8::2:1"),
},
want: true,
},
{
name: "both nil",
IPNet: cidr1,
host: HostInfo{
IPv4: nil,
IPv6: nil,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &cidrMatcher{
IPNet: tt.IPNet,
}
if got := m.Match(tt.host); got != tt.want {
t.Errorf("Match() = %v, want %v", got, tt.want)
}
})
}
}
func Test_domainMatcher_Match(t *testing.T) {
type fields struct {
Pattern string
Mode uint8
}
tests := []struct {
name string
fields fields
host HostInfo
want bool
}{
{
name: "non-wildcard match",
fields: fields{
Pattern: "example.com",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "example.com",
},
want: true,
},
{
name: "non-wildcard IDN match",
fields: fields{
Pattern: "政府.中国",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "xn--mxtq1m.xn--fiqs8s",
},
want: true,
},
{
name: "non-wildcard no match",
fields: fields{
Pattern: "example.com",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "example.org",
},
want: false,
},
{
name: "non-wildcard IDN no match",
fields: fields{
Pattern: "政府.中国",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "xn--mxtq1m.xn--yfro4i67o",
},
want: false,
},
{
name: "wildcard match 1",
fields: fields{
Pattern: "*.example.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "www.example.com",
},
want: true,
},
{
name: "wildcard match 2",
fields: fields{
Pattern: "example*.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "example2.com",
},
want: true,
},
{
name: "wildcard IDN match 1",
fields: fields{
Pattern: "战狼*.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "xn--2-x14by21c.com",
},
want: true,
},
{
name: "wildcard IDN match 2",
fields: fields{
Pattern: "*大学*",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "xn--xkry9kk1bz66a.xn--ses554g",
},
want: true,
},
{
name: "wildcard no match",
fields: fields{
Pattern: "*.example.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "example.com",
},
want: false,
},
{
name: "wildcard IDN no match",
fields: fields{
Pattern: "*呵呵*",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "xn--6qqt7juua.cn",
},
want: false,
},
{
name: "suffix match 1",
fields: fields{
Pattern: "apple.com",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "apple.com",
},
want: true,
},
{
name: "suffix match 2",
fields: fields{
Pattern: "apple.com",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "store.apple.com",
},
want: true,
},
{
name: "suffix IDN match 1",
fields: fields{
Pattern: "中国",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "中国",
},
want: true,
},
{
name: "suffix IDN match 2",
fields: fields{
Pattern: "中国",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "天安门.中国",
},
want: true,
},
{
name: "suffix no match",
fields: fields{
Pattern: "news.com",
},
host: HostInfo{
Name: "fakenews.com",
},
want: false,
},
{
name: "suffix IDN no match",
fields: fields{
Pattern: "冲浪",
},
host: HostInfo{
Name: "666.网上冲浪",
},
want: false,
},
{
name: "empty",
fields: fields{
Pattern: "*.example.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &domainMatcher{
Pattern: tt.fields.Pattern,
Mode: tt.fields.Mode,
}
if got := m.Match(tt.host); got != tt.want {
t.Errorf("Match() = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: extras/outbounds/acl/matchers_v2geo.go
================================================
package acl
import (
"bytes"
"errors"
"net"
"regexp"
"sort"
"strings"
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
)
var _ hostMatcher = (*geoipMatcher)(nil)
type geoipMatcher struct {
N4 []*net.IPNet // sorted
N6 []*net.IPNet // sorted
Inverse bool
}
// matchIP tries to match the given IP address with the corresponding IPNets.
// Note that this function does NOT handle the Inverse flag.
func (m *geoipMatcher) matchIP(ip net.IP) bool {
var n []*net.IPNet
if ip4 := ip.To4(); ip4 != nil {
// N4 stores IPv4 addresses in 4-byte form.
// Make sure we use it here too, otherwise bytes.Compare will fail.
ip = ip4
n = m.N4
} else {
n = m.N6
}
left, right := 0, len(n)-1
for left <= right {
mid := (left + right) / 2
if n[mid].Contains(ip) {
return true
} else if bytes.Compare(n[mid].IP, ip) < 0 {
left = mid + 1
} else {
right = mid - 1
}
}
return false
}
func (m *geoipMatcher) Match(host HostInfo) bool {
if host.IPv4 != nil {
if m.matchIP(host.IPv4) {
return !m.Inverse
}
}
if host.IPv6 != nil {
if m.matchIP(host.IPv6) {
return !m.Inverse
}
}
return m.Inverse
}
func newGeoIPMatcher(list *v2geo.GeoIP) (*geoipMatcher, error) {
n4 := make([]*net.IPNet, 0)
n6 := make([]*net.IPNet, 0)
for _, cidr := range list.Cidr {
if len(cidr.Ip) == 4 {
// IPv4
n4 = append(n4, &net.IPNet{
IP: cidr.Ip,
Mask: net.CIDRMask(int(cidr.Prefix), 32),
})
} else if len(cidr.Ip) == 16 {
// IPv6
n6 = append(n6, &net.IPNet{
IP: cidr.Ip,
Mask: net.CIDRMask(int(cidr.Prefix), 128),
})
} else {
return nil, errors.New("invalid IP length")
}
}
// Sort the IPNets, so we can do binary search later.
sort.Slice(n4, func(i, j int) bool {
return bytes.Compare(n4[i].IP, n4[j].IP) < 0
})
sort.Slice(n6, func(i, j int) bool {
return bytes.Compare(n6[i].IP, n6[j].IP) < 0
})
return &geoipMatcher{
N4: n4,
N6: n6,
Inverse: list.InverseMatch,
}, nil
}
var _ hostMatcher = (*geositeMatcher)(nil)
type geositeDomainType int
const (
geositeDomainPlain geositeDomainType = iota
geositeDomainRegex
geositeDomainRoot
geositeDomainFull
)
type geositeDomain struct {
Type geositeDomainType
Value string
Regex *regexp.Regexp
Attrs map[string]bool
}
type geositeMatcher struct {
Domains []geositeDomain
// Attributes are matched using "and" logic - if you have multiple attributes here,
// a domain must have all of those attributes to be considered a match.
Attrs []string
}
func (m *geositeMatcher) matchDomain(domain geositeDomain, host HostInfo) bool {
// Match attributes first
if len(m.Attrs) > 0 {
if len(domain.Attrs) == 0 {
return false
}
for _, attr := range m.Attrs {
if !domain.Attrs[attr] {
return false
}
}
}
switch domain.Type {
case geositeDomainPlain:
return strings.Contains(host.Name, domain.Value)
case geositeDomainRegex:
if domain.Regex != nil {
return domain.Regex.MatchString(host.Name)
}
case geositeDomainFull:
return host.Name == domain.Value
case geositeDomainRoot:
if host.Name == domain.Value {
return true
}
return strings.HasSuffix(host.Name, "."+domain.Value)
default:
return false
}
return false
}
func (m *geositeMatcher) Match(host HostInfo) bool {
for _, domain := range m.Domains {
if m.matchDomain(domain, host) {
return true
}
}
return false
}
func newGeositeMatcher(list *v2geo.GeoSite, attrs []string) (*geositeMatcher, error) {
domains := make([]geositeDomain, len(list.Domain))
for i, domain := range list.Domain {
switch domain.Type {
case v2geo.Domain_Plain:
domains[i] = geositeDomain{
Type: geositeDomainPlain,
Value: domain.Value,
Attrs: domainAttributeToMap(domain.Attribute),
}
case v2geo.Domain_Regex:
regex, err := regexp.Compile(domain.Value)
if err != nil {
return nil, err
}
domains[i] = geositeDomain{
Type: geositeDomainRegex,
Regex: regex,
Attrs: domainAttributeToMap(domain.Attribute),
}
case v2geo.Domain_Full:
domains[i] = geositeDomain{
Type: geositeDomainFull,
Value: domain.Value,
Attrs: domainAttributeToMap(domain.Attribute),
}
case v2geo.Domain_RootDomain:
domains[i] = geositeDomain{
Type: geositeDomainRoot,
Value: domain.Value,
Attrs: domainAttributeToMap(domain.Attribute),
}
default:
return nil, errors.New("unsupported domain type")
}
}
return &geositeMatcher{
Domains: domains,
Attrs: attrs,
}, nil
}
func domainAttributeToMap(attrs []*v2geo.Domain_Attribute) map[string]bool {
m := make(map[string]bool)
for _, attr := range attrs {
// Supposedly there are also int attributes,
// but nobody seems to use them, so we treat everything as boolean for now.
m[attr.Key] = true
}
return m
}
================================================
FILE: extras/outbounds/acl/matchers_v2geo_test.go
================================================
package acl
import (
"net"
"testing"
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
"github.com/stretchr/testify/assert"
)
func Test_geoipMatcher_Match(t *testing.T) {
geoipMap, err := v2geo.LoadGeoIP("v2geo/geoip.dat")
assert.NoError(t, err)
m, err := newGeoIPMatcher(geoipMap["us"])
assert.NoError(t, err)
tests := []struct {
name string
host HostInfo
want bool
}{
{
name: "IPv4 match",
host: HostInfo{
IPv4: net.ParseIP("73.222.1.100"),
},
want: true,
},
{
name: "IPv4 no match",
host: HostInfo{
IPv4: net.ParseIP("123.123.123.123"),
},
want: false,
},
{
name: "IPv6 match",
host: HostInfo{
IPv6: net.ParseIP("2607:f8b0:4005:80c::2004"),
},
want: true,
},
{
name: "IPv6 no match",
host: HostInfo{
IPv6: net.ParseIP("240e:947:6001::1f8"),
},
want: false,
},
{
name: "both nil",
host: HostInfo{
IPv4: nil,
IPv6: nil,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, m.Match(tt.host), "Match(%v)", tt.host)
})
}
}
func Test_geositeMatcher_Match(t *testing.T) {
geositeMap, err := v2geo.LoadGeoSite("v2geo/geosite.dat")
assert.NoError(t, err)
m, err := newGeositeMatcher(geositeMap["apple"], nil)
assert.NoError(t, err)
tests := []struct {
name string
attrs []string
host HostInfo
want bool
}{
{
name: "subdomain",
attrs: nil,
host: HostInfo{
Name: "poop.i-book.com",
},
want: true,
},
{
name: "subdomain root",
attrs: nil,
host: HostInfo{
Name: "applepaycash.net",
},
want: true,
},
{
name: "full",
attrs: nil,
host: HostInfo{
Name: "courier-push-apple.com.akadns.net",
},
want: true,
},
{
name: "regexp",
attrs: nil,
host: HostInfo{
Name: "cdn4.apple-mapkit.com",
},
want: true,
},
{
name: "attr match",
attrs: []string{"cn"},
host: HostInfo{
Name: "bag.itunes.apple.com",
},
want: true,
},
{
name: "attr multi no match",
attrs: []string{"cn", "haha"},
host: HostInfo{
Name: "bag.itunes.apple.com",
},
want: false,
},
{
name: "attr no match",
attrs: []string{"cn"},
host: HostInfo{
Name: "mr-apple.com.tw",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m.Attrs = tt.attrs
assert.Equalf(t, tt.want, m.Match(tt.host), "Match(%v)", tt.host)
})
}
}
================================================
FILE: extras/outbounds/acl/parse.go
================================================
package acl
import (
"fmt"
"regexp"
"strings"
)
var linePattern = regexp.MustCompile(`^(\w+)\s*\(([^,]+)(?:,([^,]+))?(?:,([^,]+))?\)$`)
type InvalidSyntaxError struct {
Line string
LineNum int
}
func (e *InvalidSyntaxError) Error() string {
return fmt.Sprintf("invalid syntax at line %d: %s", e.LineNum, e.Line)
}
// TextRule is the struct representation of a (non-comment) line parsed from an ACL file.
// A line can be parsed into a TextRule as long as it matches one of the following patterns:
//
// outbound(address)
// outbound(address,protoPort)
// outbound(address,protoPort,hijackAddress)
//
// It does not check whether any of the fields is valid - it's up to the compiler to do so.
type TextRule struct {
Outbound string
Address string
ProtoPort string
HijackAddress string
LineNum int
}
func parseLine(line string, num int) *TextRule {
matches := linePattern.FindStringSubmatch(line)
if matches == nil {
return nil
}
return &TextRule{
Outbound: matches[1],
Address: strings.TrimSpace(matches[2]),
ProtoPort: strings.TrimSpace(matches[3]),
HijackAddress: strings.TrimSpace(matches[4]),
LineNum: num,
}
}
func ParseTextRules(text string) ([]TextRule, error) {
rules := make([]TextRule, 0)
lineNum := 0
for _, line := range strings.Split(text, "\n") {
lineNum++
// Remove comments
if i := strings.Index(line, "#"); i >= 0 {
line = line[:i]
}
line = strings.TrimSpace(line)
// Skip empty lines
if len(line) == 0 {
continue
}
// Parse line
rule := parseLine(line, lineNum)
if rule == nil {
return nil, &InvalidSyntaxError{line, lineNum}
}
rules = append(rules, *rule)
}
return rules, nil
}
================================================
FILE: extras/outbounds/acl/parse_test.go
================================================
package acl
import (
"reflect"
"testing"
)
func TestParseTextRules(t *testing.T) {
tests := []struct {
name string
text string
want []TextRule
wantErr bool
}{
{
name: "empty",
text: "",
want: []TextRule{},
wantErr: false,
},
{
name: "ok",
text: `
# just a comment
# another comment
direct(1.1.1.1)
direct(8.8.8.0/24)
reject(all, udp/443) # inline comment
reject(geoip:cn)
reject(*.v2ex.com)
my_custom_outbound1(9.9.9.9,*, 8.8.8.8) # bebop
my_custom_outbound2(all)
`,
want: []TextRule{
{Outbound: "direct", Address: "1.1.1.1", LineNum: 4},
{Outbound: "direct", Address: "8.8.8.0/24", LineNum: 5},
{Outbound: "reject", Address: "all", ProtoPort: "udp/443", LineNum: 6},
{Outbound: "reject", Address: "geoip:cn", LineNum: 7},
{Outbound: "reject", Address: "*.v2ex.com", LineNum: 8},
{Outbound: "my_custom_outbound1", Address: "9.9.9.9", ProtoPort: "*", HijackAddress: "8.8.8.8", LineNum: 9},
{Outbound: "my_custom_outbound2", Address: "all", LineNum: 10},
},
wantErr: false,
},
{
name: "fail 1",
text: `boom()`,
want: nil,
wantErr: true,
},
{
name: "fail 2",
text: `lol(1,1,1,1)`,
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseTextRules(tt.text)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTextRules() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseTextRules() got = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: extras/outbounds/acl/v2geo/load.go
================================================
package v2geo
import (
"os"
"strings"
"google.golang.org/protobuf/proto"
)
// LoadGeoIP loads a GeoIP data file and converts it to a map.
// The keys of the map (country codes) are all normalized to lowercase.
func LoadGeoIP(filename string) (map[string]*GeoIP, error) {
bs, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var list GeoIPList
if err := proto.Unmarshal(bs, &list); err != nil {
return nil, err
}
m := make(map[string]*GeoIP)
for _, entry := range list.Entry {
m[strings.ToLower(entry.CountryCode)] = entry
}
return m, nil
}
// LoadGeoSite loads a GeoSite data file and converts it to a map.
// The keys of the map (site keys) are all normalized to lowercase.
func LoadGeoSite(filename string) (map[string]*GeoSite, error) {
bs, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var list GeoSiteList
if err := proto.Unmarshal(bs, &list); err != nil {
return nil, err
}
m := make(map[string]*GeoSite)
for _, entry := range list.Entry {
m[strings.ToLower(entry.CountryCode)] = entry
}
return m, nil
}
================================================
FILE: extras/outbounds/acl/v2geo/load_test.go
================================================
package v2geo
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadGeoIP(t *testing.T) {
m, err := LoadGeoIP("geoip.dat")
assert.NoError(t, err)
// Exact checks since we know the data.
assert.Len(t, m, 252)
assert.Equal(t, m["cn"].CountryCode, "CN")
assert.Len(t, m["cn"].Cidr, 10407)
assert.Equal(t, m["us"].CountryCode, "US")
assert.Len(t, m["us"].Cidr, 193171)
assert.Equal(t, m["private"].CountryCode, "PRIVATE")
assert.Len(t, m["private"].Cidr, 18)
assert.Contains(t, m["private"].Cidr, &CIDR{
Ip: []byte("\xc0\xa8\x00\x00"),
Prefix: 16,
})
}
func TestLoadGeoSite(t *testing.T) {
m, err := LoadGeoSite("geosite.dat")
assert.NoError(t, err)
// Exact checks since we know the data.
assert.Len(t, m, 1204)
assert.Equal(t, m["netflix"].CountryCode, "NETFLIX")
assert.Len(t, m["netflix"].Domain, 25)
assert.Contains(t, m["netflix"].Domain, &Domain{
Type: Domain_Full,
Value: "netflix.com.edgesuite.net",
})
assert.Contains(t, m["netflix"].Domain, &Domain{
Type: Domain_RootDomain,
Value: "fast.com",
})
assert.Len(t, m["google"].Domain, 1066)
assert.Contains(t, m["google"].Domain, &Domain{
Type: Domain_RootDomain,
Value: "ggpht.cn",
Attribute: []*Domain_Attribute{
{
Key: "cn",
TypedValue: &Domain_Attribute_BoolValue{BoolValue: true},
},
},
})
}
================================================
FILE: extras/outbounds/acl/v2geo/v2geo.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v4.24.4
// source: v2geo.proto
package v2geo
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Type of domain value.
type Domain_Type int32
const (
// The value is used as is.
Domain_Plain Domain_Type = 0
// The value is used as a regular expression.
Domain_Regex Domain_Type = 1
// The value is a root domain.
Domain_RootDomain Domain_Type = 2
// The value is a domain.
Domain_Full Domain_Type = 3
)
// Enum value maps for Domain_Type.
var (
Domain_Type_name = map[int32]string{
0: "Plain",
1: "Regex",
2: "RootDomain",
3: "Full",
}
Domain_Type_value = map[string]int32{
"Plain": 0,
"Regex": 1,
"RootDomain": 2,
"Full": 3,
}
)
func (x Domain_Type) Enum() *Domain_Type {
p := new(Domain_Type)
*p = x
return p
}
func (x Domain_Type) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Domain_Type) Descriptor() protoreflect.EnumDescriptor {
return file_v2geo_proto_enumTypes[0].Descriptor()
}
func (Domain_Type) Type() protoreflect.EnumType {
return &file_v2geo_proto_enumTypes[0]
}
func (x Domain_Type) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Domain_Type.Descriptor instead.
func (Domain_Type) EnumDescriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
}
// Domain for routing decision.
type Domain struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Domain matching type.
Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Domain_Type" json:"type,omitempty"`
// Domain value.
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
// Attributes of this domain. May be used for filtering.
Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"`
}
func (x *Domain) Reset() {
*x = Domain{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Domain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{0}
}
func (x *Domain) GetType() Domain_Type {
if x != nil {
return x.Type
}
return Domain_Plain
}
func (x *Domain) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Domain) GetAttribute() []*Domain_Attribute {
if x != nil {
return x.Attribute
}
return nil
}
// IP for routing decision, in CIDR form.
type CIDR struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// IP address, should be either 4 or 16 bytes.
Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
// Number of leading ones in the network mask.
Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
}
func (x *CIDR) Reset() {
*x = CIDR{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CIDR) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDR) ProtoMessage() {}
func (x *CIDR) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CIDR.ProtoReflect.Descriptor instead.
func (*CIDR) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{1}
}
func (x *CIDR) GetIp() []byte {
if x != nil {
return x.Ip
}
return nil
}
func (x *CIDR) GetPrefix() uint32 {
if x != nil {
return x.Prefix
}
return 0
}
type GeoIP struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"`
InverseMatch bool `protobuf:"varint,3,opt,name=inverse_match,json=inverseMatch,proto3" json:"inverse_match,omitempty"`
// resource_hash instruct simplified config converter to load domain from geo file.
ResourceHash []byte `protobuf:"bytes,4,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
Code string `protobuf:"bytes,5,opt,name=code,proto3" json:"code,omitempty"`
}
func (x *GeoIP) Reset() {
*x = GeoIP{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoIP) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{2}
}
func (x *GeoIP) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
func (x *GeoIP) GetCidr() []*CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *GeoIP) GetInverseMatch() bool {
if x != nil {
return x.InverseMatch
}
return false
}
func (x *GeoIP) GetResourceHash() []byte {
if x != nil {
return x.ResourceHash
}
return nil
}
func (x *GeoIP) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
type GeoIPList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
}
func (x *GeoIPList) Reset() {
*x = GeoIPList{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoIPList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{3}
}
func (x *GeoIPList) GetEntry() []*GeoIP {
if x != nil {
return x.Entry
}
return nil
}
type GeoSite struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
// resource_hash instruct simplified config converter to load domain from geo file.
ResourceHash []byte `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
Code string `protobuf:"bytes,4,opt,name=code,proto3" json:"code,omitempty"`
}
func (x *GeoSite) Reset() {
*x = GeoSite{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoSite) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSite) ProtoMessage() {}
func (x *GeoSite) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSite.ProtoReflect.Descriptor instead.
func (*GeoSite) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{4}
}
func (x *GeoSite) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
func (x *GeoSite) GetDomain() []*Domain {
if x != nil {
return x.Domain
}
return nil
}
func (x *GeoSite) GetResourceHash() []byte {
if x != nil {
return x.ResourceHash
}
return nil
}
func (x *GeoSite) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
type GeoSiteList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
}
func (x *GeoSiteList) Reset() {
*x = GeoSiteList{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoSiteList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSiteList) ProtoMessage() {}
func (x *GeoSiteList) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead.
func (*GeoSiteList) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{5}
}
func (x *GeoSiteList) GetEntry() []*GeoSite {
if x != nil {
return x.Entry
}
return nil
}
type Domain_Attribute struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Types that are assignable to TypedValue:
//
// *Domain_Attribute_BoolValue
// *Domain_Attribute_IntValue
TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"`
}
func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Domain_Attribute) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead.
func (*Domain_Attribute) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Domain_Attribute) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {
if m != nil {
return m.TypedValue
}
return nil
}
func (x *Domain_Attribute) GetBoolValue() bool {
if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok {
return x.BoolValue
}
return false
}
func (x *Domain_Attribute) GetIntValue() int64 {
if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok {
return x.IntValue
}
return 0
}
type isDomain_Attribute_TypedValue interface {
isDomain_Attribute_TypedValue()
}
type Domain_Attribute_BoolValue struct {
BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"`
}
type Domain_Attribute_IntValue struct {
IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"`
}
func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}
func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}
var File_v2geo_proto protoreflect.FileDescriptor
var file_v2geo_proto_rawDesc = []byte{
0x0a, 0x0b, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x02,
0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e,
0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74,
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10,
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22,
0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e,
0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0e, 0x0a,
0x0a, 0x52, 0x6f, 0x6f, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a,
0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12,
0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xa3, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49,
0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79,
0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x05, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12,
0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68,
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d,
0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a,
0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x05, 0x65, 0x6e,
0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x47, 0x65, 0x6f, 0x49,
0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x86, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6f,
0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,
0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a,
0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x22, 0x2d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74,
0x12, 0x1e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x08, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79,
0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
file_v2geo_proto_rawDescOnce sync.Once
file_v2geo_proto_rawDescData = file_v2geo_proto_rawDesc
)
func file_v2geo_proto_rawDescGZIP() []byte {
file_v2geo_proto_rawDescOnce.Do(func() {
file_v2geo_proto_rawDescData = protoimpl.X.CompressGZIP(file_v2geo_proto_rawDescData)
})
return file_v2geo_proto_rawDescData
}
var file_v2geo_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_v2geo_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_v2geo_proto_goTypes = []interface{}{
(Domain_Type)(0), // 0: Domain.Type
(*Domain)(nil), // 1: Domain
(*CIDR)(nil), // 2: CIDR
(*GeoIP)(nil), // 3: GeoIP
(*GeoIPList)(nil), // 4: GeoIPList
(*GeoSite)(nil), // 5: GeoSite
(*GeoSiteList)(nil), // 6: GeoSiteList
(*Domain_Attribute)(nil), // 7: Domain.Attribute
}
var file_v2geo_proto_depIdxs = []int32{
0, // 0: Domain.type:type_name -> Domain.Type
7, // 1: Domain.attribute:type_name -> Domain.Attribute
2, // 2: GeoIP.cidr:type_name -> CIDR
3, // 3: GeoIPList.entry:type_name -> GeoIP
1, // 4: GeoSite.domain:type_name -> Domain
5, // 5: GeoSiteList.entry:type_name -> GeoSite
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_v2geo_proto_init() }
func file_v2geo_proto_init() {
if File_v2geo_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_v2geo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Domain); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CIDR); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoIP); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoIPList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoSite); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoSiteList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Domain_Attribute); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_v2geo_proto_msgTypes[6].OneofWrappers = []interface{}{
(*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_v2geo_proto_rawDesc,
NumEnums: 1,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_v2geo_proto_goTypes,
DependencyIndexes: file_v2geo_proto_depIdxs,
EnumInfos: file_v2geo_proto_enumTypes,
MessageInfos: file_v2geo_proto_msgTypes,
}.Build()
File_v2geo_proto = out.File
file_v2geo_proto_rawDesc = nil
file_v2geo_proto_goTypes = nil
file_v2geo_proto_depIdxs = nil
}
================================================
FILE: extras/outbounds/acl/v2geo/v2geo.proto
================================================
syntax = "proto3";
option go_package = "./v2geo";
// This file is copied from
// https://github.com/v2fly/v2ray-core/blob/master/app/router/routercommon/common.proto
// with some modifications.
// Domain for routing decision.
message Domain {
// Type of domain value.
enum Type {
// The value is used as is.
Plain = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a root domain.
RootDomain = 2;
// The value is a domain.
Full = 3;
}
// Domain matching type.
Type type = 1;
// Domain value.
string value = 2;
message Attribute {
string key = 1;
oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}
// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}
// IP for routing decision, in CIDR form.
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;
// Number of leading ones in the network mask.
uint32 prefix = 2;
}
message GeoIP {
string country_code = 1;
repeated CIDR cidr = 2;
bool inverse_match = 3;
// resource_hash instruct simplified config converter to load domain from geo file.
bytes resource_hash = 4;
string code = 5;
}
message GeoIPList {
repeated GeoIP entry = 1;
}
message GeoSite {
string country_code = 1;
repeated Domain domain = 2;
// resource_hash instruct simplified config converter to load domain from geo file.
bytes resource_hash = 3;
string code = 4;
}
message GeoSiteList {
repeated GeoSite entry = 1;
}
================================================
FILE: extras/outbounds/acl.go
================================================
package outbounds
import (
"errors"
"net"
"os"
"strings"
"github.com/apernet/hysteria/extras/v2/outbounds/acl"
)
const (
aclCacheSize = 1024
)
var errRejected = errors.New("rejected")
// aclEngine is a PluggableOutbound that dispatches connections to different
// outbounds based on ACL rules.
// There are 3 built-in outbounds:
// - direct: directOutbound, auto mode
// - reject: reject the connection
// - default: first outbound in the list, or if the list is empty, equal to direct
// If the user-defined outbounds contain any of the above names, they will
// override the built-in outbounds.
type aclEngine struct {
RuleSet acl.CompiledRuleSet[PluggableOutbound]
Default PluggableOutbound
}
type OutboundEntry struct {
Name string
Outbound PluggableOutbound
}
func NewACLEngineFromString(rules string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) {
trs, err := acl.ParseTextRules(rules)
if err != nil {
return nil, err
}
obMap := outboundsToMap(outbounds)
rs, err := acl.Compile[PluggableOutbound](trs, obMap, aclCacheSize, geoLoader)
if err != nil {
return nil, err
}
return &aclEngine{rs, obMap["default"]}, nil
}
func NewACLEngineFromFile(filename string, outbounds []OutboundEntry, geoLoader acl.GeoLoader) (PluggableOutbound, error) {
bs, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return NewACLEngineFromString(string(bs), outbounds, geoLoader)
}
func outboundsToMap(outbounds []OutboundEntry) map[string]PluggableOutbound {
obMap := make(map[string]PluggableOutbound)
for _, ob := range outbounds {
obMap[strings.ToLower(ob.Name)] = ob.Outbound
}
// Add built-in outbounds if not overridden
if _, ok := obMap["direct"]; !ok {
obMap["direct"] = NewDirectOutboundSimple(DirectOutboundModeAuto)
}
if _, ok := obMap["reject"]; !ok {
obMap["reject"] = &aclRejectOutbound{}
}
if _, ok := obMap["default"]; !ok {
if len(outbounds) > 0 {
obMap["default"] = outbounds[0].Outbound
} else {
obMap["default"] = obMap["direct"]
}
}
return obMap
}
func (a *aclEngine) handle(reqAddr *AddrEx, proto acl.Protocol) PluggableOutbound {
hostInfo := acl.HostInfo{Name: reqAddr.Host}
if reqAddr.ResolveInfo != nil {
hostInfo.IPv4 = reqAddr.ResolveInfo.IPv4
hostInfo.IPv6 = reqAddr.ResolveInfo.IPv6
}
ob, hijackIP := a.RuleSet.Match(hostInfo, proto, reqAddr.Port)
if ob == nil {
// No match, use default outbound
return a.Default
}
if hijackIP != nil {
// We must rewrite both Host & ResolveInfo,
// as some outbounds only care about Host.
reqAddr.Host = hijackIP.String()
if ip4 := hijackIP.To4(); ip4 != nil {
reqAddr.ResolveInfo = &ResolveInfo{IPv4: ip4}
} else {
reqAddr.ResolveInfo = &ResolveInfo{IPv6: hijackIP}
}
}
return ob
}
func (a *aclEngine) TCP(reqAddr *AddrEx) (net.Conn, error) {
ob := a.handle(reqAddr, acl.ProtocolTCP)
return ob.TCP(reqAddr)
}
func (a *aclEngine) UDP(reqAddr *AddrEx) (UDPConn, error) {
ob := a.handle(reqAddr, acl.ProtocolUDP)
return ob.UDP(reqAddr)
}
type aclRejectOutbound struct{}
func (a *aclRejectOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
return nil, errRejected
}
func (a *aclRejectOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
return nil, errRejected
}
================================================
FILE: extras/outbounds/acl_test.go
================================================
package outbounds
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestACLEngine(t *testing.T) {
ob1, ob2, ob3 := &mockPluggableOutbound{}, &mockPluggableOutbound{}, &mockPluggableOutbound{}
obs := []OutboundEntry{
{"ob1", ob1},
{"ob2", ob2},
{"ob3", ob3},
{"direct", ob2},
}
acl, err := NewACLEngineFromString(`
ob2(google.com,tcp)
ob3(youtube.com,udp)
ob1 (1.1.1.1/24,*,8.8.8.8)
Direct(cia.gov)
reJect(nsa.gov)
`, obs, nil)
assert.NoError(t, err)
// No match, default, should be the first (ob1)
ob1.EXPECT().TCP(&AddrEx{Host: "example.com"}).Return(nil, nil).Once()
conn, err := acl.TCP(&AddrEx{Host: "example.com"})
assert.NoError(t, err)
assert.Nil(t, conn)
// Match ob2
ob2.EXPECT().TCP(&AddrEx{Host: "google.com"}).Return(nil, nil).Once()
conn, err = acl.TCP(&AddrEx{Host: "google.com"})
assert.NoError(t, err)
assert.Nil(t, conn)
// Match ob3
ob3.EXPECT().UDP(&AddrEx{Host: "youtube.com"}).Return(nil, nil).Once()
udpConn, err := acl.UDP(&AddrEx{Host: "youtube.com"})
assert.NoError(t, err)
assert.Nil(t, udpConn)
// Match ob1 hijack IP
ob1.EXPECT().TCP(&AddrEx{Host: "8.8.8.8", ResolveInfo: &ResolveInfo{IPv4: net.ParseIP("8.8.8.8").To4()}}).Return(nil, nil).Once()
conn, err = acl.TCP(&AddrEx{ResolveInfo: &ResolveInfo{IPv4: net.ParseIP("1.1.1.22")}})
assert.NoError(t, err)
assert.Nil(t, conn)
// direct should be ob2 as we override it
ob2.EXPECT().TCP(&AddrEx{Host: "cia.gov"}).Return(nil, nil).Once()
conn, err = acl.TCP(&AddrEx{Host: "cia.gov"})
assert.NoError(t, err)
assert.Nil(t, conn)
// reject
conn, err = acl.TCP(&AddrEx{Host: "nsa.gov"})
assert.Error(t, err)
assert.Nil(t, conn)
}
================================================
FILE: extras/outbounds/dns_https.go
================================================
package outbounds
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/apernet/hysteria/extras/v2/outbounds/tinydoh"
)
// dohResolver is a PluggableOutbound DNS resolver that resolves hostnames
// using the user-provided DNS-over-HTTPS server.
type dohResolver struct {
Resolver *tinydoh.Resolver
Next PluggableOutbound
}
func NewDoHResolver(addr string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound {
// User may provide just the IP address or full URL
if !strings.HasSuffix(addr, "https://") {
addr = fmt.Sprintf("https://%s/dns-query", addr)
}
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = &tls.Config{
ServerName: sni,
InsecureSkipVerify: insecure,
}
return &dohResolver{
Resolver: &tinydoh.Resolver{
URL: addr,
HTTPClient: &http.Client{
Transport: tr,
Timeout: timeoutOrDefault(timeout),
},
},
Next: next,
}
}
func (r *dohResolver) resolve(reqAddr *AddrEx) {
if tryParseIP(reqAddr) {
// The host is already an IP address, we don't need to resolve it.
return
}
type lookupResult struct {
ip net.IP
err error
}
ch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1)
go func() {
ips, err := r.Resolver.LookupA(reqAddr.Host)
var ip net.IP
if err == nil && len(ips) > 0 {
ip = ips[0]
}
ch4 <- lookupResult{ip, err}
}()
go func() {
ips, err := r.Resolver.LookupAAAA(reqAddr.Host)
var ip net.IP
if err == nil && len(ips) > 0 {
ip = ips[0]
}
ch6 <- lookupResult{ip, err}
}()
result4, result6 := <-ch4, <-ch6
reqAddr.ResolveInfo = &ResolveInfo{
IPv4: result4.ip,
IPv6: result6.ip,
}
if result4.err != nil {
reqAddr.ResolveInfo.Err = result4.err
} else if result6.err != nil {
reqAddr.ResolveInfo.Err = result6.err
}
}
func (r *dohResolver) TCP(reqAddr *AddrEx) (net.Conn, error) {
r.resolve(reqAddr)
return r.Next.TCP(reqAddr)
}
func (r *dohResolver) UDP(reqAddr *AddrEx) (UDPConn, error) {
r.resolve(reqAddr)
return r.Next.UDP(reqAddr)
}
================================================
FILE: extras/outbounds/dns_standard.go
================================================
package outbounds
import (
"crypto/tls"
"net"
"time"
"github.com/miekg/dns"
)
const (
resolverDefaultTimeout = 2 * time.Second
standardResolverRetryTimes = 2
)
// standardResolver is a PluggableOutbound DNS resolver that resolves hostnames
// using the user-provided DNS server.
// Based on "github.com/miekg/dns", it supports UDP, TCP & DNS-over-TLS (TCP).
type standardResolver struct {
Addr string
Client *dns.Client
Next PluggableOutbound
}
func NewStandardResolverUDP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound {
return &standardResolver{
Addr: addDefaultPort(addr),
Client: &dns.Client{
Timeout: timeoutOrDefault(timeout),
},
Next: next,
}
}
func NewStandardResolverTCP(addr string, timeout time.Duration, next PluggableOutbound) PluggableOutbound {
return &standardResolver{
Addr: addDefaultPort(addr),
Client: &dns.Client{
Net: "tcp",
Timeout: timeoutOrDefault(timeout),
},
Next: next,
}
}
func NewStandardResolverTLS(addr string, timeout time.Duration, sni string, insecure bool, next PluggableOutbound) PluggableOutbound {
return &standardResolver{
Addr: addDefaultPortTLS(addr),
Client: &dns.Client{
Net: "tcp-tls",
Timeout: timeoutOrDefault(timeout),
TLSConfig: &tls.Config{
ServerName: sni,
InsecureSkipVerify: insecure,
},
},
Next: next,
}
}
// addDefaultPort adds the default DNS port (53) to the address if not present.
func addDefaultPort(addr string) string {
if _, _, err := net.SplitHostPort(addr); err != nil {
return net.JoinHostPort(addr, "53")
}
return addr
}
// addDefaultPortTLS adds the default DNS-over-TLS port (853) to the address if not present.
func addDefaultPortTLS(addr string) string {
if _, _, err := net.SplitHostPort(addr); err != nil {
return net.JoinHostPort(addr, "853")
}
return addr
}
func timeoutOrDefault(timeout time.Duration) time.Duration {
if timeout == 0 {
return resolverDefaultTimeout
}
return timeout
}
// skipCNAMEChain skips the CNAME chain and returns the last CNAME target.
// Sometimes the DNS server returns a CNAME chain like this, in one packet:
// domain1.com. CNAME domain2.com.
// domain2.com. CNAME domain3.com.
// In this case, we should avoid sending a query for domain2.com and go
// straight to domain3.com.
func (r *standardResolver) skipCNAMEChain(answers []dns.RR) string {
var lastCNAME string
for _, a := range answers {
if cname, ok := a.(*dns.CNAME); ok {
if lastCNAME == "" {
// First CNAME
lastCNAME = cname.Target
} else if cname.Hdr.Name == lastCNAME {
// CNAME chain
lastCNAME = cname.Target
} else {
// CNAME chain ends
return lastCNAME
}
}
}
return lastCNAME
}
// lookup4 resolves a hostname to an IPv4 address.
// If there's no IPv4 address, it returns (nil, nil), no error.
func (r *standardResolver) lookup4(host string) (net.IP, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(host), dns.TypeA)
m.RecursionDesired = true
resp, _, err := r.Client.Exchange(m, r.Addr)
if err != nil {
return nil, err
}
if len(resp.Answer) == 0 {
return nil, nil
}
// Sometimes the DNS server returns both CNAME and A records in one packet.
hasCNAME := false
for _, a := range resp.Answer {
if aa, ok := a.(*dns.A); ok {
return aa.A.To4(), nil
} else if _, ok := a.(*dns.CNAME); ok {
hasCNAME = true
}
}
if hasCNAME {
return r.lookup4(r.skipCNAMEChain(resp.Answer))
} else {
// Should not happen
return nil, nil
}
}
// lookup6 resolves a hostname to an IPv6 address.
// If there's no IPv6 address, it returns (nil, nil), no error.
func (r *standardResolver) lookup6(host string) (net.IP, error) {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)
m.RecursionDesired = true
resp, _, err := r.Client.Exchange(m, r.Addr)
if err != nil {
return nil, err
}
if len(resp.Answer) == 0 {
return nil, nil
}
// Sometimes the DNS server returns both CNAME and AAAA records in one packet.
hasCNAME := false
for _, a := range resp.Answer {
if aa, ok := a.(*dns.AAAA); ok {
return aa.AAAA.To16(), nil
} else if _, ok := a.(*dns.CNAME); ok {
hasCNAME = true
}
}
if hasCNAME {
return r.lookup6(r.skipCNAMEChain(resp.Answer))
} else {
// Should not happen
return nil, nil
}
}
func (r *standardResolver) resolve(reqAddr *AddrEx) {
if tryParseIP(reqAddr) {
// The host is already an IP address, we don't need to resolve it.
return
}
type lookupResult struct {
ip net.IP
err error
}
ch4, ch6 := make(chan lookupResult, 1), make(chan lookupResult, 1)
go func() {
var ip net.IP
var err error
for i := 0; i < standardResolverRetryTimes; i++ {
ip, err = r.lookup4(reqAddr.Host)
if err == nil {
break
}
}
ch4 <- lookupResult{ip, err}
}()
go func() {
var ip net.IP
var err error
for i := 0; i < standardResolverRetryTimes; i++ {
ip, err = r.lookup6(reqAddr.Host)
if err == nil {
break
}
}
ch6 <- lookupResult{ip, err}
}()
result4, result6 := <-ch4, <-ch6
reqAddr.ResolveInfo = &ResolveInfo{
IPv4: result4.ip,
IPv6: result6.ip,
}
if result4.err != nil {
reqAddr.ResolveInfo.Err = result4.err
} else if result6.err != nil {
reqAddr.ResolveInfo.Err = result6.err
}
}
func (r *standardResolver) TCP(reqAddr *AddrEx) (net.Conn, error) {
r.resolve(reqAddr)
return r.Next.TCP(reqAddr)
}
func (r *standardResolver) UDP(reqAddr *AddrEx) (UDPConn, error) {
r.resolve(reqAddr)
return r.Next.UDP(reqAddr)
}
================================================
FILE: extras/outbounds/dns_system.go
================================================
package outbounds
import (
"net"
)
// systemResolver is a PluggableOutbound DNS resolver that resolves hostnames
// using the default system DNS server.
// Outbounds typically don't require a resolver, as they can do DNS resolution
// themselves. However, when using ACL, it's necessary to place a resolver in
// front of it in the pipeline (for IP rules to work on domain requests).
type systemResolver struct {
Next PluggableOutbound
}
func NewSystemResolver(next PluggableOutbound) PluggableOutbound {
return &systemResolver{
Next: next,
}
}
func (r *systemResolver) resolve(reqAddr *AddrEx) {
ips, err := net.LookupIP(reqAddr.Host)
if err != nil {
reqAddr.ResolveInfo = &ResolveInfo{Err: err}
return
}
info := &ResolveInfo{}
info.IPv4, info.IPv6 = splitIPv4IPv6(ips)
reqAddr.ResolveInfo = info
}
func (r *systemResolver) TCP(reqAddr *AddrEx) (net.Conn, error) {
r.resolve(reqAddr)
return r.Next.TCP(reqAddr)
}
func (r *systemResolver) UDP(reqAddr *AddrEx) (UDPConn, error) {
r.resolve(reqAddr)
return r.Next.UDP(reqAddr)
}
================================================
FILE: extras/outbounds/fastopen.go
================================================
package outbounds
import (
"net"
"sync"
"time"
"github.com/database64128/tfo-go/v2"
)
type fastOpenDialer struct {
dialer *tfo.Dialer
}
func newFastOpenDialer(netDialer *net.Dialer) *fastOpenDialer {
return &fastOpenDialer{
dialer: &tfo.Dialer{
Dialer: *netDialer,
},
}
}
// Dial returns immediately without actually establishing a connection.
// The connection will be established by the first Write() call.
func (d *fastOpenDialer) Dial(network, address string) (net.Conn, error) {
return &fastOpenConn{
dialer: d.dialer,
network: network,
address: address,
readyChan: make(chan struct{}),
}, nil
}
type fastOpenConn struct {
dialer *tfo.Dialer
network string
address string
conn net.Conn
connLock sync.RWMutex
readyChan chan struct{}
// States before connection ready
deadline *time.Time
readDeadline *time.Time
writeDeadline *time.Time
}
func (c *fastOpenConn) Read(b []byte) (n int, err error) {
c.connLock.RLock()
conn := c.conn
c.connLock.RUnlock()
if conn != nil {
return conn.Read(b)
}
// Wait until the connection is ready or closed
<-c.readyChan
if c.conn == nil {
// This is equivalent to isClosedBeforeReady() == true
return 0, net.ErrClosed
}
return c.conn.Read(b)
}
func (c *fastOpenConn) Write(b []byte) (n int, err error) {
c.connLock.RLock()
conn := c.conn
c.connLock.RUnlock()
if conn != nil {
return conn.Write(b)
}
c.connLock.RLock()
closed := c.isClosedBeforeReady()
c.connLock.RUnlock()
if closed {
return 0, net.ErrClosed
}
c.connLock.Lock()
defer c.connLock.Unlock()
if c.isClosedBeforeReady() {
// Closed by other goroutine
return 0, net.ErrClosed
}
conn = c.conn
if conn != nil {
// Established by other goroutine
return conn.Write(b)
}
conn, err = c.dialer.Dial(c.network, c.address, b)
if err != nil {
close(c.readyChan)
return 0, err
}
// Apply pre-set states
if c.deadline != nil {
_ = conn.SetDeadline(*c.deadline)
}
if c.readDeadline != nil {
_ = conn.SetReadDeadline(*c.readDeadline)
}
if c.writeDeadline != nil {
_ = conn.SetWriteDeadline(*c.writeDeadline)
}
c.conn = conn
close(c.readyChan)
return len(b), nil
}
func (c *fastOpenConn) Close() error {
c.connLock.RLock()
defer c.connLock.RUnlock()
if c.isClosedBeforeReady() {
return net.ErrClosed
}
if c.conn != nil {
return c.conn.Close()
}
close(c.readyChan)
return nil
}
// isClosedBeforeReady returns true if the connection is closed before the real connection is established.
// This function should be called with connLock.RLock().
func (c *fastOpenConn) isClosedBeforeReady() bool {
select {
case <-c.readyChan:
if c.conn == nil {
return true
}
default:
}
return false
}
func (c *fastOpenConn) LocalAddr() net.Addr {
c.connLock.RLock()
defer c.connLock.RUnlock()
if c.conn != nil {
return c.conn.LocalAddr()
}
return nil
}
func (c *fastOpenConn) RemoteAddr() net.Addr {
c.connLock.RLock()
conn := c.conn
c.connLock.RUnlock()
if conn != nil {
return conn.RemoteAddr()
}
addr, err := net.ResolveTCPAddr(c.network, c.address)
if err != nil {
return nil
}
return addr
}
func (c *fastOpenConn) SetDeadline(t time.Time) error {
c.connLock.RLock()
defer c.connLock.RUnlock()
c.deadline = &t
if c.conn != nil {
return c.conn.SetDeadline(t)
}
if c.isClosedBeforeReady() {
return net.ErrClosed
}
return nil
}
func (c *fastOpenConn) SetReadDeadline(t time.Time) error {
c.connLock.RLock()
defer c.connLock.RUnlock()
c.readDeadline = &t
if c.conn != nil {
return c.conn.SetReadDeadline(t)
}
if c.isClosedBeforeReady() {
return net.ErrClosed
}
return nil
}
func (c *fastOpenConn) SetWriteDeadline(t time.Time) error {
c.connLock.RLock()
defer c.connLock.RUnlock()
c.writeDeadline = &t
if c.conn != nil {
return c.conn.SetWriteDeadline(t)
}
if c.isClosedBeforeReady() {
return net.ErrClosed
}
return nil
}
var _ net.Conn = (*fastOpenConn)(nil)
================================================
FILE: extras/outbounds/interface.go
================================================
package outbounds
import (
"net"
"strconv"
"github.com/apernet/hysteria/core/v2/server"
)
// The PluggableOutbound system is designed to function in a chain-like manner.
// Not every outbound is an actual outbound; some are just wrappers around other
// outbounds, such as custom resolvers, ACL engine, etc. It is a pipeline where
// each stage can check (and optionally modify) the request before passing it
// on to the next stage. The last stage in the pipeline is always a real outbound
// that actually implements the logic of connecting to the remote server.
// There can also be instances of branching, where requests can be sent to
// different outbound sub-pipelines based on some criteria.
// PluggableOutbound differs from the built-in Outbound interface from Hysteria core
// in that it uses an AddrEx struct for addresses instead of a string. Because of this
// difference, we need a special PluggableOutboundAdapter to convert between the two
// for use in Hysteria core config.
type PluggableOutbound interface {
TCP(reqAddr *AddrEx) (net.Conn, error)
UDP(reqAddr *AddrEx) (UDPConn, error)
}
type UDPConn interface {
ReadFrom(b []byte) (int, *AddrEx, error)
WriteTo(b []byte, addr *AddrEx) (int, error)
Close() error
}
// AddrEx keeps both the original string representation of the address and
// the resolved IP addresses from the resolver, if any.
// The actual outbound implementations can choose to use either the string
// representation or the resolved IP addresses, depending on their capabilities.
// A SOCKS5 outbound, for example, should prefer the string representation
// because SOCKS5 protocol supports sending the hostname to the proxy server
// and let the proxy server do the DNS resolution.
type AddrEx struct {
Host string // String representation of the host, can be an IP or a domain name
Port uint16
ResolveInfo *ResolveInfo // Only set if there's a resolver in the pipeline
}
func (a *AddrEx) String() string {
return net.JoinHostPort(a.Host, strconv.Itoa(int(a.Port)))
}
// ResolveInfo contains the resolved IP addresses from the resolver, and any
// error that occurred during the resolution.
// Note that there could be no error but also no resolved IP addresses,
// or there could be an error but also some resolved IP addresses.
// It's up to the actual outbound implementation to decide how to handle
// these cases.
type ResolveInfo struct {
IPv4 net.IP
IPv6 net.IP
Err error
}
var _ server.Outbound = (*PluggableOutboundAdapter)(nil)
type PluggableOutboundAdapter struct {
PluggableOutbound
}
func (a *PluggableOutboundAdapter) TCP(reqAddr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(reqAddr)
if err != nil {
return nil, err
}
portInt, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
return a.PluggableOutbound.TCP(&AddrEx{
Host: host,
Port: uint16(portInt),
})
}
func (a *PluggableOutboundAdapter) UDP(reqAddr string) (server.UDPConn, error) {
host, port, err := net.SplitHostPort(reqAddr)
if err != nil {
return nil, err
}
portInt, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
conn, err := a.PluggableOutbound.UDP(&AddrEx{
Host: host,
Port: uint16(portInt),
})
if err != nil {
return nil, err
}
return &udpConnAdapter{conn}, nil
}
type udpConnAdapter struct {
UDPConn
}
func (u *udpConnAdapter) ReadFrom(b []byte) (int, string, error) {
n, addr, err := u.UDPConn.ReadFrom(b)
if addr != nil {
return n, addr.String(), err
} else {
return n, "", err
}
}
func (u *udpConnAdapter) WriteTo(b []byte, addr string) (int, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return 0, err
}
portInt, err := strconv.Atoi(port)
if err != nil {
return 0, err
}
return u.UDPConn.WriteTo(b, &AddrEx{
Host: host,
Port: uint16(portInt),
})
}
func (u *udpConnAdapter) Close() error {
return u.UDPConn.Close()
}
================================================
FILE: extras/outbounds/interface_test.go
================================================
package outbounds
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestPluggableOutboundAdapter(t *testing.T) {
ob := newMockPluggableOutbound(t)
adapter := &PluggableOutboundAdapter{ob}
ob.EXPECT().TCP(&AddrEx{
Host: "only.fans",
Port: 443,
}).Return(nil, nil).Once()
conn, err := adapter.TCP("only.fans:443")
assert.Nil(t, conn)
assert.Nil(t, err)
mc := newMockUDPConn(t)
mc.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, *AddrEx, error) {
return copy(bs, "gura"), &AddrEx{
Host: "gura.com",
Port: 2333,
}, nil
}).Once()
mc.EXPECT().WriteTo([]byte("gawr"), &AddrEx{
Host: "another.hololive.tv",
Port: 1551,
}).Return(4, nil).Once()
ob.EXPECT().UDP(&AddrEx{
Host: "hololive.tv",
Port: 8999,
}).Return(mc, nil).Once()
uConn, err := adapter.UDP("hololive.tv:8999")
assert.Nil(t, err)
assert.NotNil(t, uConn)
n, err := uConn.WriteTo([]byte("gawr"), "another.hololive.tv:1551")
assert.Nil(t, err)
assert.Equal(t, 4, n)
bs := make([]byte, 1024)
n, addr, err := uConn.ReadFrom(bs)
assert.Nil(t, err)
assert.Equal(t, 4, n)
assert.Equal(t, "gura", string(bs[:n]))
assert.Equal(t, "gura.com:2333", addr)
}
================================================
FILE: extras/outbounds/mock_PluggableOutbound.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package outbounds
import (
net "net"
mock "github.com/stretchr/testify/mock"
)
// mockPluggableOutbound is an autogenerated mock type for the PluggableOutbound type
type mockPluggableOutbound struct {
mock.Mock
}
type mockPluggableOutbound_Expecter struct {
mock *mock.Mock
}
func (_m *mockPluggableOutbound) EXPECT() *mockPluggableOutbound_Expecter {
return &mockPluggableOutbound_Expecter{mock: &_m.Mock}
}
// TCP provides a mock function with given fields: reqAddr
func (_m *mockPluggableOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
ret := _m.Called(reqAddr)
if len(ret) == 0 {
panic("no return value specified for TCP")
}
var r0 net.Conn
var r1 error
if rf, ok := ret.Get(0).(func(*AddrEx) (net.Conn, error)); ok {
return rf(reqAddr)
}
if rf, ok := ret.Get(0).(func(*AddrEx) net.Conn); ok {
r0 = rf(reqAddr)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Conn)
}
}
if rf, ok := ret.Get(1).(func(*AddrEx) error); ok {
r1 = rf(reqAddr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockPluggableOutbound_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP'
type mockPluggableOutbound_TCP_Call struct {
*mock.Call
}
// TCP is a helper method to define mock.On call
// - reqAddr *AddrEx
func (_e *mockPluggableOutbound_Expecter) TCP(reqAddr interface{}) *mockPluggableOutbound_TCP_Call {
return &mockPluggableOutbound_TCP_Call{Call: _e.mock.On("TCP", reqAddr)}
}
func (_c *mockPluggableOutbound_TCP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_TCP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*AddrEx))
})
return _c
}
func (_c *mockPluggableOutbound_TCP_Call) Return(_a0 net.Conn, _a1 error) *mockPluggableOutbound_TCP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *mockPluggableOutbound_TCP_Call) RunAndReturn(run func(*AddrEx) (net.Conn, error)) *mockPluggableOutbound_TCP_Call {
_c.Call.Return(run)
return _c
}
// UDP provides a mock function with given fields: reqAddr
func (_m *mockPluggableOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
ret := _m.Called(reqAddr)
if len(ret) == 0 {
panic("no return value specified for UDP")
}
var r0 UDPConn
var r1 error
if rf, ok := ret.Get(0).(func(*AddrEx) (UDPConn, error)); ok {
return rf(reqAddr)
}
if rf, ok := ret.Get(0).(func(*AddrEx) UDPConn); ok {
r0 = rf(reqAddr)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(UDPConn)
}
}
if rf, ok := ret.Get(1).(func(*AddrEx) error); ok {
r1 = rf(reqAddr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockPluggableOutbound_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'
type mockPluggableOutbound_UDP_Call struct {
*mock.Call
}
// UDP is a helper method to define mock.On call
// - reqAddr *AddrEx
func (_e *mockPluggableOutbound_Expecter) UDP(reqAddr interface{}) *mockPluggableOutbound_UDP_Call {
return &mockPluggableOutbound_UDP_Call{Call: _e.mock.On("UDP", reqAddr)}
}
func (_c *mockPluggableOutbound_UDP_Call) Run(run func(reqAddr *AddrEx)) *mockPluggableOutbound_UDP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*AddrEx))
})
return _c
}
func (_c *mockPluggableOutbound_UDP_Call) Return(_a0 UDPConn, _a1 error) *mockPluggableOutbound_UDP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *mockPluggableOutbound_UDP_Call) RunAndReturn(run func(*AddrEx) (UDPConn, error)) *mockPluggableOutbound_UDP_Call {
_c.Call.Return(run)
return _c
}
// newMockPluggableOutbound creates a new instance of mockPluggableOutbound. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func newMockPluggableOutbound(t interface {
mock.TestingT
Cleanup(func())
}) *mockPluggableOutbound {
mock := &mockPluggableOutbound{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: extras/outbounds/mock_UDPConn.go
================================================
// Code generated by mockery v2.53.5. DO NOT EDIT.
package outbounds
import mock "github.com/stretchr/testify/mock"
// mockUDPConn is an autogenerated mock type for the UDPConn type
type mockUDPConn struct {
mock.Mock
}
type mockUDPConn_Expecter struct {
mock *mock.Mock
}
func (_m *mockUDPConn) EXPECT() *mockUDPConn_Expecter {
return &mockUDPConn_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *mockUDPConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// mockUDPConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type mockUDPConn_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *mockUDPConn_Expecter) Close() *mockUDPConn_Close_Call {
return &mockUDPConn_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *mockUDPConn_Close_Call) Run(run func()) *mockUDPConn_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *mockUDPConn_Close_Call) Return(_a0 error) *mockUDPConn_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Close_Call {
_c.Call.Return(run)
return _c
}
// ReadFrom provides a mock function with given fields: b
func (_m *mockUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for ReadFrom")
}
var r0 int
var r1 *AddrEx
var r2 error
if rf, ok := ret.Get(0).(func([]byte) (int, *AddrEx, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) *AddrEx); ok {
r1 = rf(b)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*AddrEx)
}
}
if rf, ok := ret.Get(2).(func([]byte) error); ok {
r2 = rf(b)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// mockUDPConn_ReadFrom_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadFrom'
type mockUDPConn_ReadFrom_Call struct {
*mock.Call
}
// ReadFrom is a helper method to define mock.On call
// - b []byte
func (_e *mockUDPConn_Expecter) ReadFrom(b interface{}) *mockUDPConn_ReadFrom_Call {
return &mockUDPConn_ReadFrom_Call{Call: _e.mock.On("ReadFrom", b)}
}
func (_c *mockUDPConn_ReadFrom_Call) Run(run func(b []byte)) *mockUDPConn_ReadFrom_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *mockUDPConn_ReadFrom_Call) Return(_a0 int, _a1 *AddrEx, _a2 error) *mockUDPConn_ReadFrom_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, *AddrEx, error)) *mockUDPConn_ReadFrom_Call {
_c.Call.Return(run)
return _c
}
// WriteTo provides a mock function with given fields: b, addr
func (_m *mockUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
ret := _m.Called(b, addr)
if len(ret) == 0 {
panic("no return value specified for WriteTo")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte, *AddrEx) (int, error)); ok {
return rf(b, addr)
}
if rf, ok := ret.Get(0).(func([]byte, *AddrEx) int); ok {
r0 = rf(b, addr)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte, *AddrEx) error); ok {
r1 = rf(b, addr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockUDPConn_WriteTo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteTo'
type mockUDPConn_WriteTo_Call struct {
*mock.Call
}
// WriteTo is a helper method to define mock.On call
// - b []byte
// - addr *AddrEx
func (_e *mockUDPConn_Expecter) WriteTo(b interface{}, addr interface{}) *mockUDPConn_WriteTo_Call {
return &mockUDPConn_WriteTo_Call{Call: _e.mock.On("WriteTo", b, addr)}
}
func (_c *mockUDPConn_WriteTo_Call) Run(run func(b []byte, addr *AddrEx)) *mockUDPConn_WriteTo_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(*AddrEx))
})
return _c
}
func (_c *mockUDPConn_WriteTo_Call) Return(_a0 int, _a1 error) *mockUDPConn_WriteTo_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *mockUDPConn_WriteTo_Call) RunAndReturn(run func([]byte, *AddrEx) (int, error)) *mockUDPConn_WriteTo_Call {
_c.Call.Return(run)
return _c
}
// newMockUDPConn creates a new instance of mockUDPConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func newMockUDPConn(t interface {
mock.TestingT
Cleanup(func())
}) *mockUDPConn {
mock := &mockUDPConn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: extras/outbounds/ob_direct.go
================================================
package outbounds
import (
"errors"
"net"
"strconv"
"time"
)
type DirectOutboundMode int
type udpConnState int
const (
DirectOutboundModeAuto DirectOutboundMode = iota // Dual-stack "happy eyeballs"-like mode
DirectOutboundMode64 // Use IPv6 address when available, otherwise IPv4
DirectOutboundMode46 // Use IPv4 address when available, otherwise IPv6
DirectOutboundMode6 // Use IPv6 only, fail if not available
DirectOutboundMode4 // Use IPv4 only, fail if not available
defaultDialerTimeout = 10 * time.Second
)
const (
udpConnStateDualStack udpConnState = iota
udpConnStateIPv4
udpConnStateIPv6
)
// directOutbound is a PluggableOutbound that connects directly to the target
// using the local network (as opposed to using a proxy, for example).
// It prefers to use ResolveInfo in AddrEx if available. But if it's nil,
// it will fall back to resolving Host using Go's built-in DNS resolver.
type directOutbound struct {
Mode DirectOutboundMode
// Dialer4 and Dialer6 are used for IPv4 and IPv6 TCP connections respectively.
DialFunc4 func(network, address string) (net.Conn, error)
DialFunc6 func(network, address string) (net.Conn, error)
// DeviceName & BindIPs are for UDP connections. They don't use dialers, so we
// need to bind them when creating the connection.
DeviceName string
BindIP4 net.IP
BindIP6 net.IP
}
type DirectOutboundOptions struct {
Mode DirectOutboundMode
DeviceName string
BindIP4 net.IP
BindIP6 net.IP
FastOpen bool
}
type noAddressError struct {
IPv4 bool
IPv6 bool
}
func (e noAddressError) Error() string {
if e.IPv4 && e.IPv6 {
return "no IPv4 or IPv6 address available"
} else if e.IPv4 {
return "no IPv4 address available"
} else if e.IPv6 {
return "no IPv6 address available"
} else {
return "no address available"
}
}
type invalidOutboundModeError struct{}
func (e invalidOutboundModeError) Error() string {
return "invalid outbound mode"
}
type resolveError struct {
Err error
}
func (e resolveError) Error() string {
if e.Err == nil {
return "resolve error"
} else {
return "resolve error: " + e.Err.Error()
}
}
func (e resolveError) Unwrap() error {
return e.Err
}
func NewDirectOutboundWithOptions(opts DirectOutboundOptions) (PluggableOutbound, error) {
dialer4 := &net.Dialer{
Timeout: defaultDialerTimeout,
}
if opts.BindIP4 != nil {
if opts.BindIP4.To4() == nil {
return nil, errors.New("BindIP4 must be an IPv4 address")
}
dialer4.LocalAddr = &net.TCPAddr{
IP: opts.BindIP4,
}
}
dialer6 := &net.Dialer{
Timeout: defaultDialerTimeout,
}
if opts.BindIP6 != nil {
if opts.BindIP6.To4() != nil {
return nil, errors.New("BindIP6 must be an IPv6 address")
}
dialer6.LocalAddr = &net.TCPAddr{
IP: opts.BindIP6,
}
}
if opts.DeviceName != "" {
err := dialerBindToDevice(dialer4, opts.DeviceName)
if err != nil {
return nil, err
}
err = dialerBindToDevice(dialer6, opts.DeviceName)
if err != nil {
return nil, err
}
}
dialFunc4 := dialer4.Dial
dialFunc6 := dialer6.Dial
if opts.FastOpen {
dialFunc4 = newFastOpenDialer(dialer4).Dial
dialFunc6 = newFastOpenDialer(dialer6).Dial
}
return &directOutbound{
Mode: opts.Mode,
DialFunc4: dialFunc4,
DialFunc6: dialFunc6,
DeviceName: opts.DeviceName,
BindIP4: opts.BindIP4,
BindIP6: opts.BindIP6,
}, nil
}
// NewDirectOutboundSimple creates a new directOutbound with the given mode,
// without binding to a specific device. Works on all platforms.
func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound {
d := &net.Dialer{
Timeout: defaultDialerTimeout,
}
return &directOutbound{
Mode: mode,
DialFunc4: d.Dial,
DialFunc6: d.Dial,
}
}
// NewDirectOutboundBindToIPs creates a new directOutbound with the given mode,
// and binds to the given IPv4 and IPv6 addresses. Either or both of the addresses
// can be nil, in which case the directOutbound will not bind to a specific address
// for that family.
func NewDirectOutboundBindToIPs(mode DirectOutboundMode, bindIP4, bindIP6 net.IP) (PluggableOutbound, error) {
return NewDirectOutboundWithOptions(DirectOutboundOptions{
Mode: mode,
BindIP4: bindIP4,
BindIP6: bindIP6,
})
}
// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode,
// and binds to the given device. Only works on Linux.
func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) {
return NewDirectOutboundWithOptions(DirectOutboundOptions{
Mode: mode,
DeviceName: deviceName,
})
}
// resolve is our built-in DNS resolver for handling the case when
// AddrEx.ResolveInfo is nil.
func (d *directOutbound) resolve(reqAddr *AddrEx) {
ips, err := net.LookupIP(reqAddr.Host)
if err != nil {
reqAddr.ResolveInfo = &ResolveInfo{Err: err}
return
}
r := &ResolveInfo{}
r.IPv4, r.IPv6 = splitIPv4IPv6(ips)
if r.IPv4 == nil && r.IPv6 == nil {
r.Err = noAddressError{IPv4: true, IPv6: true}
}
reqAddr.ResolveInfo = r
}
func (d *directOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
if reqAddr.ResolveInfo == nil {
// AddrEx.ResolveInfo is nil (no resolver in the pipeline),
// we need to resolve the address ourselves.
d.resolve(reqAddr)
}
r := reqAddr.ResolveInfo
if r.IPv4 == nil && r.IPv6 == nil {
// ResolveInfo not nil but no address available,
// this can only mean that the resolver failed.
// Return the error from the resolver.
return nil, resolveError{Err: r.Err}
}
switch d.Mode {
case DirectOutboundModeAuto:
if r.IPv4 != nil && r.IPv6 != nil {
return d.dualStackDialTCP(r.IPv4, r.IPv6, reqAddr.Port)
} else if r.IPv4 != nil {
return d.dialTCP(r.IPv4, reqAddr.Port)
} else {
return d.dialTCP(r.IPv6, reqAddr.Port)
}
case DirectOutboundMode64:
if r.IPv6 != nil {
return d.dialTCP(r.IPv6, reqAddr.Port)
} else {
return d.dialTCP(r.IPv4, reqAddr.Port)
}
case DirectOutboundMode46:
if r.IPv4 != nil {
return d.dialTCP(r.IPv4, reqAddr.Port)
} else {
return d.dialTCP(r.IPv6, reqAddr.Port)
}
case DirectOutboundMode6:
if r.IPv6 != nil {
return d.dialTCP(r.IPv6, reqAddr.Port)
} else {
return nil, noAddressError{IPv6: true}
}
case DirectOutboundMode4:
if r.IPv4 != nil {
return d.dialTCP(r.IPv4, reqAddr.Port)
} else {
return nil, noAddressError{IPv4: true}
}
default:
return nil, invalidOutboundModeError{}
}
}
func (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) {
if ip.To4() != nil {
return d.DialFunc4("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
} else {
return d.DialFunc6("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
}
}
type dialResult struct {
Conn net.Conn
Err error
}
// dualStackDialTCP dials the target using both IPv4 and IPv6 addresses simultaneously.
// It returns the first successful connection and drops the other one.
// If both connections fail, it returns the last error.
func (d *directOutbound) dualStackDialTCP(ipv4, ipv6 net.IP, port uint16) (net.Conn, error) {
ch := make(chan dialResult, 2)
go func() {
conn, err := d.dialTCP(ipv4, port)
ch <- dialResult{Conn: conn, Err: err}
}()
go func() {
conn, err := d.dialTCP(ipv6, port)
ch <- dialResult{Conn: conn, Err: err}
}()
// Get the first result, check if it's successful
if r := <-ch; r.Err == nil {
// Yes. Return this and close the other connection when it's done
go func() {
r2 := <-ch
if r2.Conn != nil {
_ = r2.Conn.Close()
}
}()
return r.Conn, nil
} else {
// No. Return the other result, which may or may not be successful
r2 := <-ch
return r2.Conn, r2.Err
}
}
type directOutboundUDPConn struct {
*directOutbound
*net.UDPConn
State udpConnState
}
func (u *directOutboundUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
n, addr, err := u.UDPConn.ReadFromUDP(b)
if addr != nil {
return n, &AddrEx{
Host: addr.IP.String(),
Port: uint16(addr.Port),
}, err
} else {
return n, nil, err
}
}
func (u *directOutboundUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
if addr.ResolveInfo == nil {
u.directOutbound.resolve(addr)
}
r := addr.ResolveInfo
if r.IPv4 == nil && r.IPv6 == nil {
return 0, resolveError{Err: r.Err}
}
if u.State == udpConnStateIPv4 {
if r.IPv4 != nil {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv4,
Port: int(addr.Port),
})
} else {
return 0, noAddressError{IPv4: true}
}
} else if u.State == udpConnStateIPv6 {
if r.IPv6 != nil {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv6,
Port: int(addr.Port),
})
} else {
return 0, noAddressError{IPv6: true}
}
} else {
// Dual stack
switch u.directOutbound.Mode {
case DirectOutboundModeAuto:
// This is a special case.
// We must make a decision here, so we prefer IPv4 for maximum compatibility.
if r.IPv4 != nil {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv4,
Port: int(addr.Port),
})
} else {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv6,
Port: int(addr.Port),
})
}
case DirectOutboundMode64:
if r.IPv6 != nil {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv6,
Port: int(addr.Port),
})
} else {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv4,
Port: int(addr.Port),
})
}
case DirectOutboundMode46:
if r.IPv4 != nil {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv4,
Port: int(addr.Port),
})
} else {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv6,
Port: int(addr.Port),
})
}
case DirectOutboundMode6:
if r.IPv6 != nil {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv6,
Port: int(addr.Port),
})
} else {
return 0, noAddressError{IPv6: true}
}
case DirectOutboundMode4:
if r.IPv4 != nil {
return u.UDPConn.WriteToUDP(b, &net.UDPAddr{
IP: r.IPv4,
Port: int(addr.Port),
})
} else {
return 0, noAddressError{IPv4: true}
}
default:
return 0, invalidOutboundModeError{}
}
}
}
func (u *directOutboundUDPConn) Close() error {
return u.UDPConn.Close()
}
func (d *directOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
if d.BindIP4 == nil && d.BindIP6 == nil {
// No bind address specified, use default dual stack implementation
c, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
if d.DeviceName != "" {
if err := udpConnBindToDevice(c, d.DeviceName); err != nil {
// Don't forget to close the UDPConn if binding fails
_ = c.Close()
return nil, err
}
}
return &directOutboundUDPConn{
directOutbound: d,
UDPConn: c,
State: udpConnStateDualStack,
}, nil
} else {
// Bind address specified,
// need to check what kind of address is in reqAddr
// to determine which address family to bind to
if reqAddr.ResolveInfo == nil {
d.resolve(reqAddr)
}
r := reqAddr.ResolveInfo
if r.IPv4 == nil && r.IPv6 == nil {
return nil, resolveError{Err: r.Err}
}
var bindIP net.IP // can be nil, in which case we still lock the address family but don't bind to any address
var state udpConnState // either IPv4 or IPv6
switch d.Mode {
case DirectOutboundModeAuto:
// This is a special case.
// We must make a decision here, so we prefer IPv4 for maximum compatibility.
if r.IPv4 != nil {
bindIP = d.BindIP4
state = udpConnStateIPv4
} else {
bindIP = d.BindIP6
state = udpConnStateIPv6
}
case DirectOutboundMode64:
if r.IPv6 != nil {
bindIP = d.BindIP6
state = udpConnStateIPv6
} else {
bindIP = d.BindIP4
state = udpConnStateIPv4
}
case DirectOutboundMode46:
if r.IPv4 != nil {
bindIP = d.BindIP4
state = udpConnStateIPv4
} else {
bindIP = d.BindIP6
state = udpConnStateIPv6
}
case DirectOutboundMode6:
if r.IPv6 != nil {
bindIP = d.BindIP6
state = udpConnStateIPv6
} else {
return nil, noAddressError{IPv6: true}
}
case DirectOutboundMode4:
if r.IPv4 != nil {
bindIP = d.BindIP4
state = udpConnStateIPv4
} else {
return nil, noAddressError{IPv4: true}
}
default:
return nil, invalidOutboundModeError{}
}
var network string
var c *net.UDPConn
var err error
if state == udpConnStateIPv4 {
network = "udp4"
} else {
network = "udp6"
}
if bindIP != nil {
c, err = net.ListenUDP(network, &net.UDPAddr{
IP: bindIP,
})
} else {
c, err = net.ListenUDP(network, nil)
}
if err != nil {
return nil, err
}
// We don't support binding to both device & address at the same time,
// so d.DeviceName is ignored in this case.
return &directOutboundUDPConn{
directOutbound: d,
UDPConn: c,
State: state,
}, nil
}
}
================================================
FILE: extras/outbounds/ob_direct_linux.go
================================================
package outbounds
import (
"errors"
"net"
"syscall"
)
func dialerBindToDevice(dialer *net.Dialer, deviceName string) error {
if err := verifyDeviceName(deviceName); err != nil {
return err
}
originControl := dialer.Control
dialer.Control = func(network, address string, c syscall.RawConn) error {
if originControl != nil {
// Chaining other control function
err := originControl(network, address, c)
if err != nil {
return err
}
}
var errBind error
err := c.Control(func(fd uintptr) {
errBind = syscall.BindToDevice(int(fd), deviceName)
})
if err != nil {
return err
}
return errBind
}
return nil
}
func verifyDeviceName(deviceName string) error {
if deviceName == "" {
return errors.New("device name cannot be empty")
}
_, err := net.InterfaceByName(deviceName)
return err
}
func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error {
sc, err := conn.SyscallConn()
if err != nil {
return err
}
var errBind error
err = sc.Control(func(fd uintptr) {
errBind = syscall.BindToDevice(int(fd), deviceName)
})
if err != nil {
return err
}
return errBind
}
================================================
FILE: extras/outbounds/ob_direct_others.go
================================================
//go:build !linux
package outbounds
import (
"errors"
"net"
)
func dialerBindToDevice(dialer *net.Dialer, deviceName string) error {
return errors.New("binding to device is not supported on this platform")
}
func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error {
return errors.New("binding to device is not supported on this platform")
}
================================================
FILE: extras/outbounds/ob_http.go
================================================
package outbounds
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"time"
)
const (
httpRequestTimeout = 10 * time.Second
)
var (
errHTTPUDPNotSupported = errors.New("UDP not supported by HTTP proxy")
errHTTPUnsupportedScheme = errors.New("unsupported scheme for HTTP proxy (use http:// or https://)")
)
type errHTTPRequestFailed struct {
Status int
}
func (e errHTTPRequestFailed) Error() string {
return fmt.Sprintf("HTTP request failed: %d", e.Status)
}
// httpOutbound is a PluggableOutbound that connects to the target using
// an HTTP/HTTPS proxy server (that supports the CONNECT method).
// HTTP proxies don't support UDP by design, so this outbound will reject
// any UDP request with errHTTPUDPNotSupported.
// Since HTTP proxies support using either IP or domain name as the target
// address, it will ignore ResolveInfo in AddrEx and always only use Host.
type httpOutbound struct {
Dialer *net.Dialer
Addr string
HTTPS bool
Insecure bool
ServerName string
BasicAuth string // This is after Base64 encoding
}
func NewHTTPOutbound(proxyURL string, insecure bool) (PluggableOutbound, error) {
u, err := url.Parse(proxyURL)
if err != nil {
return nil, err
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, errHTTPUnsupportedScheme
}
addr := u.Host
if u.Port() == "" {
if u.Scheme == "http" {
addr = net.JoinHostPort(u.Host, "80")
} else {
addr = net.JoinHostPort(u.Host, "443")
}
}
var basicAuth string
if u.User != nil {
username := u.User.Username()
password, _ := u.User.Password()
basicAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
}
return &httpOutbound{
Dialer: &net.Dialer{Timeout: defaultDialerTimeout},
Addr: addr,
HTTPS: u.Scheme == "https",
Insecure: insecure,
ServerName: u.Hostname(),
BasicAuth: basicAuth,
}, nil
}
func (o *httpOutbound) dial() (net.Conn, error) {
conn, err := o.Dialer.Dial("tcp", o.Addr)
if err != nil {
return nil, err
}
if o.HTTPS {
// Wrap the connection with TLS if the proxy is HTTPS.
conn = tls.Client(conn, &tls.Config{
InsecureSkipVerify: o.Insecure,
ServerName: o.Addr,
})
}
return conn, nil
}
func (o *httpOutbound) addrExToRequest(reqAddr *AddrEx) (*http.Request, error) {
req := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{
Host: net.JoinHostPort(reqAddr.Host, strconv.Itoa(int(reqAddr.Port))),
},
Header: http.Header{
"Proxy-Connection": []string{"Keep-Alive"},
},
}
if o.BasicAuth != "" {
req.Header.Add("Proxy-Authorization", o.BasicAuth)
}
return req, nil
}
func (o *httpOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
req, err := o.addrExToRequest(reqAddr)
if err != nil {
return nil, err
}
conn, err := o.dial()
if err != nil {
return nil, err
}
if err := req.Write(conn); err != nil {
_ = conn.Close()
return nil, err
}
if err := conn.SetDeadline(time.Now().Add(httpRequestTimeout)); err != nil {
_ = conn.Close()
return nil, err
}
bufReader := bufio.NewReader(conn)
resp, err := http.ReadResponse(bufReader, req)
if resp != nil {
// Don't need response body here.
_ = resp.Body.Close()
}
if err != nil {
_ = conn.Close()
return nil, err
}
if resp.StatusCode != http.StatusOK {
_ = conn.Close()
return nil, errHTTPRequestFailed{resp.StatusCode}
}
if err := conn.SetDeadline(time.Time{}); err != nil {
_ = conn.Close()
return nil, err
}
if bufReader.Buffered() > 0 {
// There is still data in the buffered reader.
// We need to get it out and put it into a cachedConn,
// so that handleConnect can read it.
data := make([]byte, bufReader.Buffered())
_, err := io.ReadFull(bufReader, data)
if err != nil {
// Read from buffer failed, is this possible?
_ = conn.Close()
return nil, err
}
cachedConn := &cachedConn{
Conn: conn,
Buffer: *bytes.NewBuffer(data),
}
return cachedConn, nil
} else {
return conn, nil
}
}
func (o *httpOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
return nil, errHTTPUDPNotSupported
}
// cachedConn is a net.Conn wrapper that first Read()s from a buffer,
// and then from the underlying net.Conn when the buffer is drained.
type cachedConn struct {
net.Conn
Buffer bytes.Buffer
}
func (c *cachedConn) Read(b []byte) (int, error) {
if c.Buffer.Len() > 0 {
n, err := c.Buffer.Read(b)
if err == io.EOF {
// Buffer is drained, hide it from the caller
err = nil
}
return n, err
}
return c.Conn.Read(b)
}
================================================
FILE: extras/outbounds/ob_socks5.go
================================================
package outbounds
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"
"github.com/txthinking/socks5"
)
const (
socks5NegotiationTimeout = 10 * time.Second
socks5RequestTimeout = 10 * time.Second
)
var errSOCKS5AuthFailed = errors.New("SOCKS5 authentication failed")
type errSOCKS5UnsupportedAuthMethod struct {
Method byte
}
func (e errSOCKS5UnsupportedAuthMethod) Error() string {
return fmt.Sprintf("unsupported SOCKS5 authentication method: %d", e.Method)
}
type errSOCKS5RequestFailed struct {
Rep byte
}
func (e errSOCKS5RequestFailed) Error() string {
var msg string
// RFC 1928
switch e.Rep {
case 0x00:
msg = "succeeded"
case 0x01:
msg = "general SOCKS server failure"
case 0x02:
msg = "connection not allowed by ruleset"
case 0x03:
msg = "Network unreachable"
case 0x04:
msg = "Host unreachable"
case 0x05:
msg = "Connection refused"
case 0x06:
msg = "TTL expired"
case 0x07:
msg = "Command not supported"
case 0x08:
msg = "Address type not supported"
default:
msg = "undefined"
}
return fmt.Sprintf("SOCKS5 request failed: %s (%d)", msg, e.Rep)
}
// socks5Outbound is a PluggableOutbound that connects to the target using
// a SOCKS5 proxy server.
// Since SOCKS5 supports using either IP or domain name as the target address,
// it will ignore ResolveInfo in AddrEx and always only use Host.
type socks5Outbound struct {
Dialer *net.Dialer
Addr string
Username string
Password string
}
func NewSOCKS5Outbound(addr, username, password string) PluggableOutbound {
return &socks5Outbound{
Dialer: &net.Dialer{
Timeout: defaultDialerTimeout,
},
Addr: addr,
Username: username,
Password: password,
}
}
// dialAndNegotiate creates a new TCP connection to the SOCKS5 proxy server
// and performs the negotiation. Returns an established connection ready to
// handle requests, or an error if the process fails.
func (o *socks5Outbound) dialAndNegotiate() (net.Conn, error) {
conn, err := o.Dialer.Dial("tcp", o.Addr)
if err != nil {
return nil, err
}
if err := conn.SetDeadline(time.Now().Add(socks5NegotiationTimeout)); err != nil {
_ = conn.Close()
return nil, err
}
authMethods := []byte{socks5.MethodNone}
if o.Username != "" && o.Password != "" {
authMethods = append(authMethods, socks5.MethodUsernamePassword)
}
req := socks5.NewNegotiationRequest(authMethods)
if _, err := req.WriteTo(conn); err != nil {
_ = conn.Close()
return nil, err
}
resp, err := socks5.NewNegotiationReplyFrom(conn)
if err != nil {
_ = conn.Close()
return nil, err
}
if resp.Method == socks5.MethodUsernamePassword {
upReq := socks5.NewUserPassNegotiationRequest([]byte(o.Username), []byte(o.Password))
if _, err := upReq.WriteTo(conn); err != nil {
_ = conn.Close()
return nil, err
}
upResp, err := socks5.NewUserPassNegotiationReplyFrom(conn)
if err != nil {
_ = conn.Close()
return nil, err
}
if upResp.Status != socks5.UserPassStatusSuccess {
_ = conn.Close()
return nil, errSOCKS5AuthFailed
}
} else if resp.Method != socks5.MethodNone {
// We only support none & username/password authentication methods.
_ = conn.Close()
return nil, errSOCKS5UnsupportedAuthMethod{resp.Method}
}
// Negotiation succeeded, reset the deadline.
if err := conn.SetDeadline(time.Time{}); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
// request sends a SOCKS5 request to the proxy server and returns the reply.
// Note that it will return an error if the reply from the server indicates
// a failure.
func (o *socks5Outbound) request(conn net.Conn, req *socks5.Request) (*socks5.Reply, error) {
if err := conn.SetDeadline(time.Now().Add(socks5RequestTimeout)); err != nil {
return nil, err
}
if _, err := req.WriteTo(conn); err != nil {
return nil, err
}
resp, err := socks5.NewReplyFrom(conn)
if err != nil {
return nil, err
}
if resp.Rep != socks5.RepSuccess {
return nil, errSOCKS5RequestFailed{resp.Rep}
}
if err := conn.SetDeadline(time.Time{}); err != nil {
return nil, err
}
return resp, nil
}
func (s *socks5Outbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
conn, err := s.dialAndNegotiate()
if err != nil {
return nil, err
}
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr)
req := socks5.NewRequest(socks5.CmdConnect, atyp, dstAddr, dstPort)
if _, err := s.request(conn, req); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
func (s *socks5Outbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
conn, err := s.dialAndNegotiate()
if err != nil {
return nil, err
}
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr)
req := socks5.NewRequest(socks5.CmdUDP, atyp, dstAddr, dstPort)
resp, err := s.request(conn, req)
if err != nil {
_ = conn.Close()
return nil, err
}
return newSOCKS5UDPConn(conn, resp.Address())
}
type socks5UDPConn struct {
tcpConn net.Conn
udpConn net.Conn
}
func newSOCKS5UDPConn(tcpConn net.Conn, udpAddr string) (*socks5UDPConn, error) {
udpConn, err := net.Dial("udp", udpAddr)
if err != nil {
return nil, err
}
sc := &socks5UDPConn{
tcpConn: tcpConn,
udpConn: udpConn,
}
go sc.hold()
return sc, nil
}
func (c *socks5UDPConn) hold() {
_, _ = io.Copy(io.Discard, c.tcpConn)
_ = c.tcpConn.Close()
_ = c.udpConn.Close()
}
func (c *socks5UDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
n, err := c.udpConn.Read(b)
if err != nil {
return 0, nil, err
}
d, err := socks5.NewDatagramFromBytes(b[:n])
if err != nil {
return 0, nil, err
}
addr := socks5AddrToAddrEx(d.Atyp, d.DstAddr, d.DstPort)
n = copy(b, d.Data)
return n, addr, nil
}
func (c *socks5UDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(addr)
d := socks5.NewDatagram(atyp, dstAddr, dstPort, b)
_, err := c.udpConn.Write(d.Bytes())
if err != nil {
return 0, err
}
return len(b), nil
}
func (c *socks5UDPConn) Close() error {
_ = c.tcpConn.Close()
_ = c.udpConn.Close()
return nil
}
func addrExToSOCKS5Addr(addr *AddrEx) (atyp byte, dstAddr, dstPort []byte) {
// Host
ip := net.ParseIP(addr.Host)
if ip != nil {
if ip.To4() != nil {
atyp = socks5.ATYPIPv4
dstAddr = ip.To4()
} else {
atyp = socks5.ATYPIPv6
dstAddr = ip.To16()
}
} else {
atyp = socks5.ATYPDomain
dstAddr = []byte(addr.Host)
}
// Port
dstPort = make([]byte, 2)
binary.BigEndian.PutUint16(dstPort, addr.Port)
return atyp, dstAddr, dstPort
}
func socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx {
// Host
var host string
if atyp == socks5.ATYPIPv4 {
host = net.IP(dstAddr).To4().String()
} else if atyp == socks5.ATYPIPv6 {
host = net.IP(dstAddr).To16().String()
} else if atyp == socks5.ATYPDomain {
// Need to strip the first byte which is the domain length.
host = string(dstAddr[1:])
}
// Port
port := binary.BigEndian.Uint16(dstPort)
return &AddrEx{
Host: host,
Port: port,
}
}
================================================
FILE: extras/outbounds/speedtest/client.go
================================================
package speedtest
import (
"fmt"
"io"
"math"
"net"
"sync/atomic"
"time"
)
type Client struct {
Conn net.Conn
}
// Download performs a download speed test.
// If duration > 0, runs for the specified duration (time-based mode).
// Otherwise, downloads exactly dataSize bytes (size-based mode).
// The callback cb is called every second with interval stats, and once
// at the end with done=true reporting totals.
func (c *Client) Download(dataSize uint32, duration time.Duration, cb func(time.Duration, uint64, bool)) error {
reqSize := dataSize
if duration > 0 {
reqSize = math.MaxUint32
}
if err := writeDownloadRequest(c.Conn, reqSize); err != nil {
return err
}
ok, msg, err := readDownloadResponse(c.Conn)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("server rejected download request: %s", msg)
}
addBytes, stop := startProgressReporter(cb)
defer stop()
if duration > 0 {
c.Conn.SetReadDeadline(time.Now().Add(duration))
}
buf := make([]byte, chunkSize)
startTime := time.Now()
var totalBytes uint64
remaining := dataSize
for duration > 0 || remaining > 0 {
readSize := uint32(chunkSize)
if duration == 0 && remaining < readSize {
readSize = remaining
}
n, err := c.Conn.Read(buf[:readSize])
totalBytes += uint64(n)
addBytes(uint64(n))
if duration == 0 {
remaining -= uint32(n)
}
if err != nil {
if duration > 0 {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
break
}
} else if remaining == 0 && err == io.EOF {
break
}
return err
}
}
cb(time.Since(startTime), totalBytes, true)
return nil
}
// Upload performs an upload speed test.
// If duration > 0, runs for the specified duration (time-based mode).
// Otherwise, uploads exactly dataSize bytes (size-based mode).
// The callback cb is called every second with interval stats, and once
// at the end with done=true reporting totals. In size-based mode the
// final callback uses server-reported elapsed time and byte count.
func (c *Client) Upload(dataSize uint32, duration time.Duration, cb func(time.Duration, uint64, bool)) error {
reqSize := dataSize
if duration > 0 {
reqSize = math.MaxUint32
}
if err := writeUploadRequest(c.Conn, reqSize); err != nil {
return err
}
ok, msg, err := readUploadResponse(c.Conn)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("server rejected upload request: %s", msg)
}
addBytes, stop := startProgressReporter(cb)
defer stop()
if duration > 0 {
c.Conn.SetWriteDeadline(time.Now().Add(duration))
}
buf := make([]byte, chunkSize)
startTime := time.Now()
var totalBytes uint64
remaining := dataSize
for duration > 0 || remaining > 0 {
writeSize := uint32(chunkSize)
if duration == 0 && remaining < writeSize {
writeSize = remaining
}
n, err := c.Conn.Write(buf[:writeSize])
totalBytes += uint64(n)
addBytes(uint64(n))
if duration == 0 {
remaining -= uint32(n)
}
if err != nil {
if duration > 0 {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
break
}
}
return err
}
}
if duration == 0 {
elapsed, received, err := readUploadSummary(c.Conn)
if err != nil {
return err
}
cb(elapsed, uint64(received), true)
} else {
cb(time.Since(startTime), totalBytes, true)
}
return nil
}
func startProgressReporter(cb func(time.Duration, uint64, bool)) (addBytes func(uint64), stop func()) {
var counter uint64
stopChan := make(chan struct{})
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
t := time.Now()
for {
select {
case <-stopChan:
return
case <-ticker.C:
cb(time.Since(t), atomic.SwapUint64(&counter, 0), false)
t = time.Now()
}
}
}()
return func(n uint64) { atomic.AddUint64(&counter, n) }, func() { close(stopChan) }
}
================================================
FILE: extras/outbounds/speedtest/protocol.go
================================================
package speedtest
import (
"encoding/binary"
"io"
"time"
)
const (
typeDownload = 0x1
typeUpload = 0x2
)
// DownloadRequest format:
// 0x1 (byte)
// Request data length (uint32 BE)
func readDownloadRequest(r io.Reader) (uint32, error) {
var l uint32
err := binary.Read(r, binary.BigEndian, &l)
return l, err
}
func writeDownloadRequest(w io.Writer, l uint32) error {
buf := make([]byte, 5)
buf[0] = typeDownload
binary.BigEndian.PutUint32(buf[1:], l)
_, err := w.Write(buf)
return err
}
// DownloadResponse format:
// Status (byte, 0=ok, 1=error)
// Message length (uint16 BE)
// Message (bytes)
func readDownloadResponse(r io.Reader) (bool, string, error) {
var status [1]byte
if _, err := io.ReadFull(r, status[:]); err != nil {
return false, "", err
}
var msgLen uint16
if err := binary.Read(r, binary.BigEndian, &msgLen); err != nil {
return false, "", err
}
// No message is fine
if msgLen == 0 {
return status[0] == 0, "", nil
}
msgBuf := make([]byte, msgLen)
_, err := io.ReadFull(r, msgBuf)
if err != nil {
return false, "", err
}
return status[0] == 0, string(msgBuf), nil
}
func writeDownloadResponse(w io.Writer, ok bool, msg string) error {
sz := 1 + 2 + len(msg)
buf := make([]byte, sz)
if ok {
buf[0] = 0
} else {
buf[0] = 1
}
binary.BigEndian.PutUint16(buf[1:], uint16(len(msg)))
copy(buf[3:], msg)
_, err := w.Write(buf)
return err
}
// UploadRequest format:
// 0x2 (byte)
// Upload data length (uint32 BE)
func readUploadRequest(r io.Reader) (uint32, error) {
var l uint32
err := binary.Read(r, binary.BigEndian, &l)
return l, err
}
func writeUploadRequest(w io.Writer, l uint32) error {
buf := make([]byte, 5)
buf[0] = typeUpload
binary.BigEndian.PutUint32(buf[1:], l)
_, err := w.Write(buf)
return err
}
// UploadResponse format:
// Status (byte, 0=ok, 1=error)
// Message length (uint16 BE)
// Message (bytes)
func readUploadResponse(r io.Reader) (bool, string, error) {
var status [1]byte
if _, err := io.ReadFull(r, status[:]); err != nil {
return false, "", err
}
var msgLen uint16
if err := binary.Read(r, binary.BigEndian, &msgLen); err != nil {
return false, "", err
}
// No message is fine
if msgLen == 0 {
return status[0] == 0, "", nil
}
msgBuf := make([]byte, msgLen)
_, err := io.ReadFull(r, msgBuf)
if err != nil {
return false, "", err
}
return status[0] == 0, string(msgBuf), nil
}
func writeUploadResponse(w io.Writer, ok bool, msg string) error {
sz := 1 + 2 + len(msg)
buf := make([]byte, sz)
if ok {
buf[0] = 0
} else {
buf[0] = 1
}
binary.BigEndian.PutUint16(buf[1:], uint16(len(msg)))
copy(buf[3:], msg)
_, err := w.Write(buf)
return err
}
// UploadSummary format:
// Duration (in milliseconds, uint32 BE)
// Received data length (uint32 BE)
func readUploadSummary(r io.Reader) (time.Duration, uint32, error) {
var duration uint32
if err := binary.Read(r, binary.BigEndian, &duration); err != nil {
return 0, 0, err
}
var l uint32
if err := binary.Read(r, binary.BigEndian, &l); err != nil {
return 0, 0, err
}
return time.Duration(duration) * time.Millisecond, l, nil
}
func writeUploadSummary(w io.Writer, duration time.Duration, l uint32) error {
buf := make([]byte, 8)
binary.BigEndian.PutUint32(buf, uint32(duration/time.Millisecond))
binary.BigEndian.PutUint32(buf[4:], l)
_, err := w.Write(buf)
return err
}
================================================
FILE: extras/outbounds/speedtest/protocol_test.go
================================================
package speedtest
import (
"bytes"
"testing"
"time"
)
func TestReadDownloadRequest(t *testing.T) {
tests := []struct {
name string
data []byte
want uint32
wantErr bool
}{
{
name: "normal",
data: []byte{0x0, 0x1, 0xBD, 0xC2},
want: 114114,
wantErr: false,
},
{
name: "normal zero",
data: []byte{0x0, 0x0, 0x0, 0x0},
want: 0,
wantErr: false,
},
{
name: "incomplete",
data: []byte{0x0, 0x1, 0x2},
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
got, err := readDownloadRequest(r)
if (err != nil) != tt.wantErr {
t.Errorf("readDownloadRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("readDownloadRequest() got = %v, want %v", got, tt.want)
}
})
}
}
func TestWriteDownloadRequest(t *testing.T) {
tests := []struct {
name string
l uint32
wantW string
wantErr bool
}{
{
name: "normal",
l: 78909912,
wantW: "\x01\x04\xB4\x11\xD8",
wantErr: false,
},
{
name: "normal zero",
l: 0,
wantW: "\x01\x00\x00\x00\x00",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := writeDownloadRequest(w, tt.l)
if (err != nil) != tt.wantErr {
t.Errorf("writeDownloadRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("writeDownloadRequest() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
func TestReadDownloadResponse(t *testing.T) {
tests := []struct {
name string
data []byte
want bool
want1 string
wantErr bool
}{
{
name: "normal ok",
data: []byte{0x0, 0x0, 0x2, 0x41, 0x42},
want: true,
want1: "AB",
wantErr: false,
},
{
name: "normal ok no message",
data: []byte{0x0, 0x0, 0x0, 0x0},
want: true,
want1: "",
wantErr: false,
},
{
name: "normal error",
data: []byte{0x1, 0x0, 0x2, 0x43, 0x44},
want: false,
want1: "CD",
wantErr: false,
},
{
name: "incomplete",
data: []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47},
want: false,
want1: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
got, got1, err := readDownloadResponse(r)
if (err != nil) != tt.wantErr {
t.Errorf("readDownloadResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("readDownloadResponse() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("readDownloadResponse() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func TestWriteDownloadResponse(t *testing.T) {
type args struct {
ok bool
msg string
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "normal ok",
args: args{ok: true, msg: "wahaha"},
wantW: "\x00\x00\x06wahaha",
wantErr: false,
},
{
name: "normal error",
args: args{ok: false, msg: "bullbull"},
wantW: "\x01\x00\x08bullbull",
wantErr: false,
},
{
name: "empty ok",
args: args{ok: true, msg: ""},
wantW: "\x00\x00\x00",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := writeDownloadResponse(w, tt.args.ok, tt.args.msg)
if (err != nil) != tt.wantErr {
t.Errorf("writeDownloadResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("writeDownloadResponse() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
func TestReadUploadRequest(t *testing.T) {
tests := []struct {
name string
data []byte
want uint32
wantErr bool
}{
{
name: "normal",
data: []byte{0x0, 0x0, 0x26, 0xEE},
want: 9966,
wantErr: false,
},
{
name: "normal zero",
data: []byte{0x0, 0x0, 0x0, 0x0},
want: 0,
wantErr: false,
},
{
name: "incomplete",
data: []byte{0x1},
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
got, err := readUploadRequest(r)
if (err != nil) != tt.wantErr {
t.Errorf("readUploadRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("readUploadRequest() got = %v, want %v", got, tt.want)
}
})
}
}
func TestWriteUploadRequest(t *testing.T) {
tests := []struct {
name string
l uint32
wantW string
wantErr bool
}{
{
name: "normal",
l: 2291758882,
wantW: "\x02\x88\x99\x77\x22",
wantErr: false,
},
{
name: "normal zero",
l: 0,
wantW: "\x02\x00\x00\x00\x00",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := writeUploadRequest(w, tt.l)
if (err != nil) != tt.wantErr {
t.Errorf("writeUploadRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("writeUploadRequest() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
func TestReadUploadResponse(t *testing.T) {
tests := []struct {
name string
data []byte
want bool
want1 string
wantErr bool
}{
{
name: "normal ok",
data: []byte{0x0, 0x0, 0x2, 0x41, 0x42},
want: true,
want1: "AB",
wantErr: false,
},
{
name: "normal ok no message",
data: []byte{0x0, 0x0, 0x0},
want: true,
want1: "",
wantErr: false,
},
{
name: "normal error",
data: []byte{0x1, 0x0, 0x2, 0x43, 0x44},
want: false,
want1: "CD",
wantErr: false,
},
{
name: "incomplete",
data: []byte{0x0, 0x99, 0x99, 0x45, 0x46, 0x47},
want: false,
want1: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
got, got1, err := readUploadResponse(r)
if (err != nil) != tt.wantErr {
t.Errorf("readUploadResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("readUploadResponse() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("readUploadResponse() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func TestWriteUploadResponse(t *testing.T) {
type args struct {
ok bool
msg string
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "normal ok",
args: args{ok: true, msg: "lul"},
wantW: "\x00\x00\x03lul",
wantErr: false,
},
{
name: "normal error",
args: args{ok: false, msg: "notforu"},
wantW: "\x01\x00\x07notforu",
wantErr: false,
},
{
name: "empty ok",
args: args{ok: true, msg: ""},
wantW: "\x00\x00\x00",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := writeUploadResponse(w, tt.args.ok, tt.args.msg)
if (err != nil) != tt.wantErr {
t.Errorf("writeUploadResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("writeUploadResponse() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
func TestReadUploadSummary(t *testing.T) {
tests := []struct {
name string
data []byte
want time.Duration
want1 uint32
wantErr bool
}{
{
name: "normal",
data: []byte{0x0, 0x0, 0x14, 0x6E, 0x0, 0x26, 0x25, 0xA0},
want: 5230 * time.Millisecond,
want1: 2500000,
wantErr: false,
},
{
name: "zero",
data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
want: 0,
want1: 0,
wantErr: false,
},
{
name: "incomplete",
data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
want: 0,
want1: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.data)
got, got1, err := readUploadSummary(r)
if (err != nil) != tt.wantErr {
t.Errorf("readUploadSummary() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("readUploadSummary() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("readUploadSummary() got1 = %v, want %v", got1, tt.want1)
}
})
}
}
func TestWriteUploadSummary(t *testing.T) {
type args struct {
duration time.Duration
l uint32
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "normal",
args: args{duration: 5230 * time.Millisecond, l: 2500000},
wantW: "\x00\x00\x14\x6E\x00\x26\x25\xA0",
wantErr: false,
},
{
name: "zero",
args: args{duration: 0, l: 0},
wantW: "\x00\x00\x00\x00\x00\x00\x00\x00",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := writeUploadSummary(w, tt.args.duration, tt.args.l)
if (err != nil) != tt.wantErr {
t.Errorf("writeUploadSummary() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("writeUploadSummary() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
================================================
FILE: extras/outbounds/speedtest/server.go
================================================
package speedtest
import (
"crypto/rand"
"fmt"
"io"
"net"
"time"
)
const (
chunkSize = 64 * 1024
)
// NewServerConn creates a new "pseudo" connection that implements the speed test protocol.
// It's called "pseudo" because it's not a real TCP connection - everything is done in memory.
func NewServerConn() net.Conn {
rConn, iConn := net.Pipe() // return conn & internal conn
// Start the server logic
go server(iConn)
return rConn
}
func server(conn net.Conn) error {
defer conn.Close()
// First byte determines the request type
var typ [1]byte
if _, err := io.ReadFull(conn, typ[:]); err != nil {
return err
}
switch typ[0] {
case typeDownload:
return handleDownload(conn)
case typeUpload:
return handleUpload(conn)
default:
return fmt.Errorf("unknown request type: %d", typ[0])
}
}
// handleDownload reads the download request and sends the requested amount of data.
func handleDownload(conn net.Conn) error {
l, err := readDownloadRequest(conn)
if err != nil {
return err
}
err = writeDownloadResponse(conn, true, "OK")
if err != nil {
return err
}
buf := make([]byte, chunkSize)
// Fill the buffer with random data.
// For now, we only do it once and repeat the same data for performance reasons.
_, err = rand.Read(buf)
if err != nil {
return err
}
remaining := l
for remaining > 0 {
n := remaining
if n > chunkSize {
n = chunkSize
}
_, err := conn.Write(buf[:n])
if err != nil {
return err
}
remaining -= n
}
return nil
}
// handleUpload reads the upload request, reads & discards the requested amount of data,
// and sends the upload summary.
func handleUpload(conn net.Conn) error {
l, err := readUploadRequest(conn)
if err != nil {
return err
}
err = writeUploadResponse(conn, true, "OK")
if err != nil {
return err
}
buf := make([]byte, chunkSize)
startTime := time.Now()
remaining := l
for remaining > 0 {
n := remaining
if n > chunkSize {
n = chunkSize
}
rn, err := conn.Read(buf[:n])
remaining -= uint32(rn)
if err != nil && !(remaining == 0 && err == io.EOF) {
return err
}
}
return writeUploadSummary(conn, time.Since(startTime), l)
}
================================================
FILE: extras/outbounds/speedtest.go
================================================
package outbounds
import (
"net"
"github.com/apernet/hysteria/extras/v2/outbounds/speedtest"
)
const (
SpeedtestDest = "@SpeedTest"
)
// speedtestHandler is a PluggableOutbound that handles speed test requests.
// It's used to intercept speed test requests and return a pseudo connection that
// implements the speed test protocol.
type speedtestHandler struct {
Next PluggableOutbound
}
func NewSpeedtestHandler(next PluggableOutbound) PluggableOutbound {
return &speedtestHandler{
Next: next,
}
}
func (s *speedtestHandler) TCP(reqAddr *AddrEx) (net.Conn, error) {
if reqAddr.Host == SpeedtestDest {
return speedtest.NewServerConn(), nil
} else {
return s.Next.TCP(reqAddr)
}
}
func (s *speedtestHandler) UDP(reqAddr *AddrEx) (UDPConn, error) {
return s.Next.UDP(reqAddr)
}
================================================
FILE: extras/outbounds/tinydoh/resolver.go
================================================
package tinydoh
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"golang.org/x/net/dns/dnsmessage"
)
type Resolver struct {
URL string
HTTPClient *http.Client
}
func (r *Resolver) lookup(dnsType dnsmessage.Type, host string) ([]dnsmessage.Resource, error) {
url := r.URL
if url == "" {
return nil, errors.New("no DoH URL provided")
}
client := r.HTTPClient
if client == nil {
client = http.DefaultClient
}
if !strings.HasSuffix(host, ".") {
host += "."
}
name, err := dnsmessage.NewName(host)
if err != nil {
return nil, fmt.Errorf("failed to parse host %s: %w", host, err)
}
reqBuilder := dnsmessage.NewBuilder(nil, dnsmessage.Header{
RecursionDesired: true,
})
reqBuilder.EnableCompression()
err = reqBuilder.StartQuestions()
if err != nil {
return nil, fmt.Errorf("failed to start dns questions for host %s: %w", host, err)
}
err = reqBuilder.Question(dnsmessage.Question{
Name: name,
Type: dnsType,
Class: dnsmessage.ClassINET,
})
if err != nil {
return nil, fmt.Errorf("failed to build dns question for host %s: %w", host, err)
}
reqMsg, err := reqBuilder.Finish()
if err != nil {
return nil, fmt.Errorf("failed to finish dns message for host %s: %w", host, err)
}
httpReq, err := http.NewRequest("POST", url, strings.NewReader(string(reqMsg)))
if err != nil {
return nil, fmt.Errorf("failed to create http request for host %s: %w", host, err)
}
httpReq.Header.Set("Content-Type", "application/dns-message")
httpResp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to perform http request for host %s: %w", host, err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("non-200 status-code=%d for host %s", httpResp.StatusCode, host)
}
if httpResp.Header.Get("Content-Type") != "application/dns-message" {
return nil, fmt.Errorf("unexpected content-type=%s for host %s", httpResp.Header.Get("Content-Type"), host)
}
// 64KB should be enough for all DNS response
limitedBody := io.LimitReader(httpResp.Body, 65536)
respMsg, err := io.ReadAll(limitedBody)
if err != nil {
return nil, fmt.Errorf("failed to read http response body for host %s: %w", host, err)
}
parser := dnsmessage.Parser{}
header, err := parser.Start(respMsg)
if err != nil {
return nil, fmt.Errorf("failed to parse dns message header for host %s: %w", host, err)
}
if header.RCode != dnsmessage.RCodeSuccess {
return nil, fmt.Errorf("dns query failed with %s for host %s", header.RCode, host)
}
err = parser.SkipAllQuestions()
if err != nil {
return nil, fmt.Errorf("failed to skip dns questions for host %s: %w", host, err)
}
answers, err := parser.AllAnswers()
if err != nil {
return nil, fmt.Errorf("failed to parse dns answers for host %s: %w", host, err)
}
return answers, nil
}
func (r *Resolver) LookupA(host string) ([]net.IP, error) {
answers, err := r.lookup(dnsmessage.TypeA, host)
if err != nil {
return nil, err
}
var results []net.IP
for _, rr := range answers {
if rr.Header.Type == dnsmessage.TypeA {
a := rr.Body.(*dnsmessage.AResource)
results = append(results, a.A[:])
}
}
return results, nil
}
func (r *Resolver) LookupAAAA(host string) ([]net.IP, error) {
answers, err := r.lookup(dnsmessage.TypeAAAA, host)
if err != nil {
return nil, err
}
var results []net.IP
for _, rr := range answers {
if rr.Header.Type == dnsmessage.TypeAAAA {
aaaa := rr.Body.(*dnsmessage.AAAAResource)
results = append(results, aaaa.AAAA[:])
}
}
return results, nil
}
================================================
FILE: extras/outbounds/tinydoh/resolver_test.go
================================================
package tinydoh
import (
"fmt"
"testing"
)
func TestResolver(t *testing.T) {
r := &Resolver{
URL: "https://1.1.1.1/dns-query",
}
ipv4, err := r.LookupA("www.wikipedia.org")
if err != nil {
t.Error(err)
}
fmt.Println(ipv4)
ipv6, err := r.LookupAAAA("www.wikipedia.org")
if err != nil {
t.Error(err)
}
fmt.Println(ipv6)
}
================================================
FILE: extras/outbounds/utils.go
================================================
package outbounds
import "net"
// splitIPv4IPv6 gets the first IPv4 and IPv6 address from a list of IP addresses.
// Both of the return values can be nil when no IPv4 or IPv6 address is found.
func splitIPv4IPv6(ips []net.IP) (ipv4, ipv6 net.IP) {
for _, ip := range ips {
if ip.To4() != nil {
if ipv4 == nil {
ipv4 = ip
}
} else {
if ipv6 == nil {
ipv6 = ip
}
}
if ipv4 != nil && ipv6 != nil {
// We have everything we need.
break
}
}
return ipv4, ipv6
}
// tryParseIP tries to parse the host string in the AddrEx as an IP address.
// If the host is indeed an IP address, it will fill the ResolveInfo with the
// parsed IP address and return true. Otherwise, it will return false.
func tryParseIP(addr *AddrEx) bool {
if ip := net.ParseIP(addr.Host); ip != nil {
addr.ResolveInfo = &ResolveInfo{}
if ip.To4() != nil {
addr.ResolveInfo.IPv4 = ip
} else {
addr.ResolveInfo.IPv6 = ip
}
return true
}
return false
}
================================================
FILE: extras/outbounds/utils_test.go
================================================
package outbounds
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSplitIPv4IPv6(t *testing.T) {
type args struct {
ips []net.IP
}
tests := []struct {
name string
args args
wantIpv4 net.IP
wantIpv6 net.IP
}{
{
name: "IPv4 only",
args: args{
ips: []net.IP{
net.ParseIP("4.5.6.7"),
net.ParseIP("9.9.9.9"),
},
},
wantIpv4: net.ParseIP("4.5.6.7"),
wantIpv6: nil,
},
{
name: "IPv6 only",
args: args{
ips: []net.IP{
net.ParseIP("2001:db8::68"),
net.ParseIP("2001:db8::69"),
},
},
wantIpv4: nil,
wantIpv6: net.ParseIP("2001:db8::68"),
},
{
name: "Both 1",
args: args{
ips: []net.IP{
net.ParseIP("2001:db8::68"),
net.ParseIP("2001:db8::69"),
net.ParseIP("4.5.6.7"),
net.ParseIP("9.9.9.9"),
},
},
wantIpv4: net.ParseIP("4.5.6.7"),
wantIpv6: net.ParseIP("2001:db8::68"),
},
{
name: "Both 2",
args: args{
ips: []net.IP{
net.ParseIP("2001:db8::69"),
net.ParseIP("9.9.9.9"),
net.ParseIP("2001:db8::68"),
net.ParseIP("4.5.6.7"),
},
},
wantIpv4: net.ParseIP("9.9.9.9"),
wantIpv6: net.ParseIP("2001:db8::69"),
},
{
name: "Empty",
args: args{
ips: []net.IP{},
},
wantIpv4: nil,
wantIpv6: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotIpv4, gotIpv6 := splitIPv4IPv6(tt.args.ips)
assert.Equalf(t, tt.wantIpv4, gotIpv4, "splitIPv4IPv6(%v)", tt.args.ips)
assert.Equalf(t, tt.wantIpv6, gotIpv6, "splitIPv4IPv6(%v)", tt.args.ips)
})
}
}
================================================
FILE: extras/sniff/.mockery.yaml
================================================
with-expecter: true
dir: .
outpkg: sniff
packages:
github.com/apernet/quic-go:
interfaces:
Stream:
config:
mockname: mockStream
replace-type: # internal package alias dirty fix
- github.com/apernet/quic-go/internal/protocol=github.com/apernet/quic-go
- github.com/apernet/quic-go/internal/qerr=github.com/apernet/quic-go
================================================
FILE: extras/sniff/internal/quic/LICENSE
================================================
Author:: Cuong Manh Le
Copyright:: Copyright (c) 2023, Cuong Manh Le
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of the @organization@ nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: extras/sniff/internal/quic/README.md
================================================
The code here is from https://github.com/cuonglm/quicsni with various modifications.
================================================
FILE: extras/sniff/internal/quic/header.go
================================================
package quic
import (
"bytes"
"encoding/binary"
"errors"
"io"
"github.com/apernet/quic-go/quicvarint"
)
// The Header represents a QUIC header.
type Header struct {
Type uint8
Version uint32
SrcConnectionID []byte
DestConnectionID []byte
Length int64
Token []byte
}
// ParseInitialHeader parses the initial packet of a QUIC connection,
// return the initial header and number of bytes read so far.
func ParseInitialHeader(data []byte) (*Header, int64, error) {
br := bytes.NewReader(data)
hdr, err := parseLongHeader(br)
if err != nil {
return nil, 0, err
}
n := int64(len(data) - br.Len())
return hdr, n, nil
}
func parseLongHeader(b *bytes.Reader) (*Header, error) {
typeByte, err := b.ReadByte()
if err != nil {
return nil, err
}
h := &Header{}
ver, err := beUint32(b)
if err != nil {
return nil, err
}
h.Version = ver
if h.Version != 0 && typeByte&0x40 == 0 {
return nil, errors.New("not a QUIC packet")
}
destConnIDLen, err := b.ReadByte()
if err != nil {
return nil, err
}
h.DestConnectionID = make([]byte, int(destConnIDLen))
if err := readConnectionID(b, h.DestConnectionID); err != nil {
return nil, err
}
srcConnIDLen, err := b.ReadByte()
if err != nil {
return nil, err
}
h.SrcConnectionID = make([]byte, int(srcConnIDLen))
if err := readConnectionID(b, h.SrcConnectionID); err != nil {
return nil, err
}
initialPacketType := byte(0b00)
if h.Version == V2 {
initialPacketType = 0b01
}
if (typeByte >> 4 & 0b11) == initialPacketType {
tokenLen, err := quicvarint.Read(b)
if err != nil {
return nil, err
}
if tokenLen > uint64(b.Len()) {
return nil, io.EOF
}
h.Token = make([]byte, tokenLen)
if _, err := io.ReadFull(b, h.Token); err != nil {
return nil, err
}
}
pl, err := quicvarint.Read(b)
if err != nil {
return nil, err
}
h.Length = int64(pl)
return h, err
}
func readConnectionID(r io.Reader, cid []byte) error {
_, err := io.ReadFull(r, cid)
if err == io.ErrUnexpectedEOF {
return io.EOF
}
return nil
}
func beUint32(r io.Reader) (uint32, error) {
b := make([]byte, 4)
if _, err := io.ReadFull(r, b); err != nil {
return 0, err
}
return binary.BigEndian.Uint32(b), nil
}
================================================
FILE: extras/sniff/internal/quic/packet_protector.go
================================================
package quic
import (
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"hash"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/hkdf"
)
// NewProtectionKey creates a new ProtectionKey.
func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
return newProtectionKey(suite, secret, v)
}
// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key
// is used for encrypt/decrypt Initial Packet only.
//
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets
func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) {
return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v)
}
// NewPacketProtector creates a new PacketProtector.
func NewPacketProtector(key *ProtectionKey) *PacketProtector {
return &PacketProtector{key: key}
}
// PacketProtector is used for protecting a QUIC packet.
//
// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection
type PacketProtector struct {
key *ProtectionKey
}
// UnProtect decrypts a QUIC packet.
func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) {
if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 {
return nil, errors.New("packet with long header is too small")
}
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample
sampleOffset := pnOffset + 4
sample := packet[sampleOffset : sampleOffset+16]
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati
mask := pp.key.headerProtection(sample)
if isLongHeader(packet[0]) {
// Long header: 4 bits masked
packet[0] ^= mask[0] & 0x0f
} else {
// Short header: 5 bits masked
packet[0] ^= mask[0] & 0x1f
}
pnLen := packet[0]&0x3 + 1
pn := int64(0)
for i := uint8(0); i < pnLen; i++ {
packet[pnOffset:][i] ^= mask[1+i]
pn = (pn << 8) | int64(packet[pnOffset:][i])
}
pn = decodePacketNumber(pnMax, pn, pnLen)
hdr := packet[:pnOffset+int64(pnLen)]
payload := packet[pnOffset:][pnLen:]
dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr)
if err != nil {
return nil, fmt.Errorf("decryption failed: %w", err)
}
return dec, nil
}
// ProtectionKey is the key used to protect a QUIC packet.
type ProtectionKey struct {
aead cipher.AEAD
headerProtection func(sample []byte) (mask []byte)
iv []byte
}
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage
//
// "The 62 bits of the reconstructed QUIC packet number in network byte order are
// left-padded with zeros to the size of the IV. The exclusive OR of the padded
// packet number and the IV forms the AEAD nonce."
func (pk *ProtectionKey) nonce(pn int64) []byte {
nonce := make([]byte, len(pk.iv))
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn))
for i := range pk.iv {
nonce[i] ^= pk.iv[i]
}
return nonce
}
func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
switch suite {
case tls.TLS_AES_128_GCM_SHA256:
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16)
c, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
aead, err := cipher.NewGCM(c)
if err != nil {
panic(err)
}
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16)
hp, err := aes.NewCipher(hpKey)
if err != nil {
panic(err)
}
k := &ProtectionKey{}
k.aead = aead
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection
k.headerProtection = func(sample []byte) []byte {
mask := make([]byte, hp.BlockSize())
hp.Encrypt(mask, sample)
return mask
}
k.iv = iv
return k, nil
case tls.TLS_CHACHA20_POLY1305_SHA256:
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize)
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize)
k := &ProtectionKey{}
k.aead = aead
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote
k.headerProtection = func(sample []byte) []byte {
nonce := sample[4:16]
c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce)
if err != nil {
panic(err)
}
c.SetCounter(binary.LittleEndian.Uint32(sample[:4]))
mask := make([]byte, 5)
c.XORKeyStream(mask, mask)
return mask
}
k.iv = iv
return k, nil
}
return nil, errors.New("not supported cipher suite")
}
// decodePacketNumber decode the packet number after header protection removed.
//
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a
func decodePacketNumber(largest, truncated int64, nbits uint8) int64 {
expected := largest + 1
win := int64(1 << (nbits * 8))
hwin := win / 2
mask := win - 1
candidate := (expected &^ mask) | truncated
switch {
case candidate <= expected-hwin && candidate < (1<<62)-win:
return candidate + win
case candidate > expected+hwin && candidate >= win:
return candidate - win
}
return candidate
}
// Copied from crypto/tls/key_schedule.go.
func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte {
var hkdfLabel cryptobyte.Builder
hkdfLabel.AddUint16(uint16(length))
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes([]byte("tls13 "))
b.AddBytes([]byte(label))
})
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(context)
})
out := make([]byte, length)
n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out)
if err != nil || n != length {
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
}
return out
}
================================================
FILE: extras/sniff/internal/quic/packet_protector_test.go
================================================
package quic
import (
"bytes"
"crypto"
"crypto/tls"
"encoding/hex"
"strings"
"testing"
"unicode"
"golang.org/x/crypto/hkdf"
)
func TestInitialPacketProtector_UnProtect(t *testing.T) {
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial
protect := mustHexDecodeString(`
c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7
6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498
44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5
be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e
fd8142fafc0f76
`)
unProtect := mustHexDecodeString(`
02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739
88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94
0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00
020304
`)
connID := mustHexDecodeString(`8394c8f03e515708`)
packet := append([]byte{}, protect...)
hdr, offset, err := ParseInitialHeader(packet)
if err != nil {
t.Fatal(err)
}
initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version))
serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size())
key, err := NewInitialProtectionKey(serverSecret, hdr.Version)
if err != nil {
t.Fatal(err)
}
pp := NewPacketProtector(key)
got, err := pp.UnProtect(protect, offset, 1)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, unProtect) {
t.Error("UnProtect returns wrong result")
}
}
func TestPacketProtectorShortHeader_UnProtect(t *testing.T) {
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea
protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`)
unProtect := mustHexDecodeString(`01`)
hdr := mustHexDecodeString(`4200bff4`)
secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`)
k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1)
if err != nil {
t.Fatal(err)
}
pnLen := int(hdr[0]&0x03) + 1
offset := len(hdr) - pnLen
pp := NewPacketProtector(k)
got, err := pp.UnProtect(protect, int64(offset), 654360564)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, unProtect) {
t.Error("UnProtect returns wrong result")
}
}
func mustHexDecodeString(s string) []byte {
b, err := hex.DecodeString(normalizeHex(s))
if err != nil {
panic(err)
}
return b
}
func normalizeHex(s string) string {
return strings.Map(func(c rune) rune {
if unicode.IsSpace(c) {
return -1
}
return c
}, s)
}
================================================
FILE: extras/sniff/internal/quic/payload.go
================================================
package quic
import (
"bytes"
"crypto"
"errors"
"fmt"
"io"
"sort"
"github.com/apernet/quic-go/quicvarint"
"golang.org/x/crypto/hkdf"
)
func ReadCryptoPayload(packet []byte) ([]byte, error) {
hdr, offset, err := ParseInitialHeader(packet)
if err != nil {
return nil, err
}
// Some sanity checks
if hdr.Version != V1 && hdr.Version != V2 {
return nil, fmt.Errorf("unsupported version: %x", hdr.Version)
}
if offset == 0 || hdr.Length == 0 {
return nil, errors.New("invalid packet")
}
initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version))
clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size())
key, err := NewInitialProtectionKey(clientSecret, hdr.Version)
if err != nil {
return nil, fmt.Errorf("NewInitialProtectionKey: %w", err)
}
pp := NewPacketProtector(key)
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial
//
// "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2"
if int64(len(packet)) < offset+hdr.Length {
return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length)
}
unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2)
if err != nil {
return nil, err
}
frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload))
if err != nil {
return nil, err
}
data := assembleCryptoFrames(frs)
if data == nil {
return nil, errors.New("unable to assemble crypto frames")
}
return data, nil
}
const (
paddingFrameType = 0x00
pingFrameType = 0x01
cryptoFrameType = 0x06
)
type cryptoFrame struct {
Offset int64
Data []byte
}
func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {
var frames []cryptoFrame
for r.Len() > 0 {
typ, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
if typ == paddingFrameType || typ == pingFrameType {
continue
}
if typ != cryptoFrameType {
return nil, fmt.Errorf("encountered unexpected frame type: %d", typ)
}
var frame cryptoFrame
offset, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Offset = int64(offset)
dataLen, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Data = make([]byte, dataLen)
if _, err := io.ReadFull(r, frame.Data); err != nil {
return nil, err
}
frames = append(frames, frame)
}
return frames, nil
}
// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).
// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.
func assembleCryptoFrames(frames []cryptoFrame) []byte {
if len(frames) == 0 {
return nil
}
if len(frames) == 1 {
return frames[0].Data
}
// sort the frames by offset
sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })
// check if the frames are contiguous
for i := 1; i < len(frames); i++ {
if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) {
return nil
}
}
// concatenate the frames
data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data)))
for _, frame := range frames {
copy(data[frame.Offset:], frame.Data)
}
return data
}
================================================
FILE: extras/sniff/internal/quic/quic.go
================================================
package quic
const (
V1 uint32 = 0x1
V2 uint32 = 0x6b3343cf
hkdfLabelKeyV1 = "quic key"
hkdfLabelKeyV2 = "quicv2 key"
hkdfLabelIVV1 = "quic iv"
hkdfLabelIVV2 = "quicv2 iv"
hkdfLabelHPV1 = "quic hp"
hkdfLabelHPV2 = "quicv2 hp"
)
var (
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
// https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets
quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
// https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2
quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}
)
// isLongHeader reports whether b is the first byte of a long header packet.
func isLongHeader(b byte) bool {
return b&0x80 > 0
}
func getSalt(v uint32) []byte {
switch v {
case V1:
return quicSaltV1
case V2:
return quicSaltV2
}
return quicSaltOld
}
func keyLabel(v uint32) string {
kl := hkdfLabelKeyV1
if v == V2 {
kl = hkdfLabelKeyV2
}
return kl
}
func ivLabel(v uint32) string {
ivl := hkdfLabelIVV1
if v == V2 {
ivl = hkdfLabelIVV2
}
return ivl
}
func headerProtectionLabel(v uint32) string {
if v == V2 {
return hkdfLabelHPV2
}
return hkdfLabelHPV1
}
================================================
FILE: extras/sniff/mock_Stream.go
================================================
// Code generated by mockery v2.43.0. DO NOT EDIT.
package sniff
import (
context "context"
qerr "github.com/apernet/quic-go"
mock "github.com/stretchr/testify/mock"
time "time"
)
// mockStream is an autogenerated mock type for the Stream type
type mockStream struct {
mock.Mock
}
type mockStream_Expecter struct {
mock *mock.Mock
}
func (_m *mockStream) EXPECT() *mockStream_Expecter {
return &mockStream_Expecter{mock: &_m.Mock}
}
// CancelRead provides a mock function with given fields: _a0
func (_m *mockStream) CancelRead(_a0 qerr.StreamErrorCode) {
_m.Called(_a0)
}
// mockStream_CancelRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelRead'
type mockStream_CancelRead_Call struct {
*mock.Call
}
// CancelRead is a helper method to define mock.On call
// - _a0 qerr.StreamErrorCode
func (_e *mockStream_Expecter) CancelRead(_a0 interface{}) *mockStream_CancelRead_Call {
return &mockStream_CancelRead_Call{Call: _e.mock.On("CancelRead", _a0)}
}
func (_c *mockStream_CancelRead_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelRead_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(qerr.StreamErrorCode))
})
return _c
}
func (_c *mockStream_CancelRead_Call) Return() *mockStream_CancelRead_Call {
_c.Call.Return()
return _c
}
func (_c *mockStream_CancelRead_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelRead_Call {
_c.Call.Return(run)
return _c
}
// CancelWrite provides a mock function with given fields: _a0
func (_m *mockStream) CancelWrite(_a0 qerr.StreamErrorCode) {
_m.Called(_a0)
}
// mockStream_CancelWrite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelWrite'
type mockStream_CancelWrite_Call struct {
*mock.Call
}
// CancelWrite is a helper method to define mock.On call
// - _a0 qerr.StreamErrorCode
func (_e *mockStream_Expecter) CancelWrite(_a0 interface{}) *mockStream_CancelWrite_Call {
return &mockStream_CancelWrite_Call{Call: _e.mock.On("CancelWrite", _a0)}
}
func (_c *mockStream_CancelWrite_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelWrite_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(qerr.StreamErrorCode))
})
return _c
}
func (_c *mockStream_CancelWrite_Call) Return() *mockStream_CancelWrite_Call {
_c.Call.Return()
return _c
}
func (_c *mockStream_CancelWrite_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelWrite_Call {
_c.Call.Return(run)
return _c
}
// Close provides a mock function with given fields:
func (_m *mockStream) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// mockStream_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type mockStream_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *mockStream_Expecter) Close() *mockStream_Close_Call {
return &mockStream_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *mockStream_Close_Call) Run(run func()) *mockStream_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *mockStream_Close_Call) Return(_a0 error) *mockStream_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockStream_Close_Call) RunAndReturn(run func() error) *mockStream_Close_Call {
_c.Call.Return(run)
return _c
}
// Context provides a mock function with given fields:
func (_m *mockStream) Context() context.Context {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Context")
}
var r0 context.Context
if rf, ok := ret.Get(0).(func() context.Context); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(context.Context)
}
}
return r0
}
// mockStream_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context'
type mockStream_Context_Call struct {
*mock.Call
}
// Context is a helper method to define mock.On call
func (_e *mockStream_Expecter) Context() *mockStream_Context_Call {
return &mockStream_Context_Call{Call: _e.mock.On("Context")}
}
func (_c *mockStream_Context_Call) Run(run func()) *mockStream_Context_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *mockStream_Context_Call) Return(_a0 context.Context) *mockStream_Context_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockStream_Context_Call) RunAndReturn(run func() context.Context) *mockStream_Context_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: p
func (_m *mockStream) Read(p []byte) (int, error) {
ret := _m.Called(p)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(p)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(p)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(p)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockStream_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type mockStream_Read_Call struct {
*mock.Call
}
// Read is a helper method to define mock.On call
// - p []byte
func (_e *mockStream_Expecter) Read(p interface{}) *mockStream_Read_Call {
return &mockStream_Read_Call{Call: _e.mock.On("Read", p)}
}
func (_c *mockStream_Read_Call) Run(run func(p []byte)) *mockStream_Read_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *mockStream_Read_Call) Return(n int, err error) *mockStream_Read_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *mockStream_Read_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Read_Call {
_c.Call.Return(run)
return _c
}
// SetDeadline provides a mock function with given fields: t
func (_m *mockStream) SetDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// mockStream_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'
type mockStream_SetDeadline_Call struct {
*mock.Call
}
// SetDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *mockStream_Expecter) SetDeadline(t interface{}) *mockStream_SetDeadline_Call {
return &mockStream_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)}
}
func (_c *mockStream_SetDeadline_Call) Run(run func(t time.Time)) *mockStream_SetDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *mockStream_SetDeadline_Call) Return(_a0 error) *mockStream_SetDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockStream_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetReadDeadline provides a mock function with given fields: t
func (_m *mockStream) SetReadDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetReadDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// mockStream_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'
type mockStream_SetReadDeadline_Call struct {
*mock.Call
}
// SetReadDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *mockStream_Expecter) SetReadDeadline(t interface{}) *mockStream_SetReadDeadline_Call {
return &mockStream_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)}
}
func (_c *mockStream_SetReadDeadline_Call) Run(run func(t time.Time)) *mockStream_SetReadDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *mockStream_SetReadDeadline_Call) Return(_a0 error) *mockStream_SetReadDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockStream_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetReadDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetWriteDeadline provides a mock function with given fields: t
func (_m *mockStream) SetWriteDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetWriteDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// mockStream_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'
type mockStream_SetWriteDeadline_Call struct {
*mock.Call
}
// SetWriteDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *mockStream_Expecter) SetWriteDeadline(t interface{}) *mockStream_SetWriteDeadline_Call {
return &mockStream_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)}
}
func (_c *mockStream_SetWriteDeadline_Call) Run(run func(t time.Time)) *mockStream_SetWriteDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *mockStream_SetWriteDeadline_Call) Return(_a0 error) *mockStream_SetWriteDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockStream_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetWriteDeadline_Call {
_c.Call.Return(run)
return _c
}
// StreamID provides a mock function with given fields:
func (_m *mockStream) StreamID() qerr.StreamID {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for StreamID")
}
var r0 qerr.StreamID
if rf, ok := ret.Get(0).(func() qerr.StreamID); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(qerr.StreamID)
}
return r0
}
// mockStream_StreamID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamID'
type mockStream_StreamID_Call struct {
*mock.Call
}
// StreamID is a helper method to define mock.On call
func (_e *mockStream_Expecter) StreamID() *mockStream_StreamID_Call {
return &mockStream_StreamID_Call{Call: _e.mock.On("StreamID")}
}
func (_c *mockStream_StreamID_Call) Run(run func()) *mockStream_StreamID_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *mockStream_StreamID_Call) Return(_a0 qerr.StreamID) *mockStream_StreamID_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *mockStream_StreamID_Call) RunAndReturn(run func() qerr.StreamID) *mockStream_StreamID_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: p
func (_m *mockStream) Write(p []byte) (int, error) {
ret := _m.Called(p)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(p)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(p)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(p)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// mockStream_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type mockStream_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - p []byte
func (_e *mockStream_Expecter) Write(p interface{}) *mockStream_Write_Call {
return &mockStream_Write_Call{Call: _e.mock.On("Write", p)}
}
func (_c *mockStream_Write_Call) Run(run func(p []byte)) *mockStream_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *mockStream_Write_Call) Return(n int, err error) *mockStream_Write_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *mockStream_Write_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Write_Call {
_c.Call.Return(run)
return _c
}
// newMockStream creates a new instance of mockStream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func newMockStream(t interface {
mock.TestingT
Cleanup(func())
}) *mockStream {
mock := &mockStream{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: extras/sniff/sniff.go
================================================
package sniff
import (
"bufio"
"io"
"net"
"net/http"
"strconv"
"strings"
"time"
utls "github.com/refraction-networking/utls"
"github.com/apernet/hysteria/core/v2/server"
quicInternal "github.com/apernet/hysteria/extras/v2/sniff/internal/quic"
"github.com/apernet/hysteria/extras/v2/utils"
)
const (
sniffDefaultTimeout = 4 * time.Second
)
var _ server.RequestHook = (*Sniffer)(nil)
// Sniffer is a server core RequestHook that performs packet inspection and possibly
// rewrites the request address based on what's in the protocol header.
// This is mainly for inbounds that inherently cannot get domain information (e.g. TUN),
// in which case sniffing can restore the domains and apply ACLs correctly.
// Currently supports HTTP, HTTPS (TLS) and QUIC.
type Sniffer struct {
Timeout time.Duration
RewriteDomain bool // Whether to rewrite the address even when it's already a domain
TCPPorts utils.PortUnion
UDPPorts utils.PortUnion
}
func (h *Sniffer) isDomain(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return false
}
return net.ParseIP(host) == nil
}
func (h *Sniffer) isHTTP(buf []byte) bool {
if len(buf) < 3 {
return false
}
// First 3 bytes should be English letters (whatever HTTP method)
for _, b := range buf[:3] {
if (b < 'A' || b > 'Z') && (b < 'a' || b > 'z') {
return false
}
}
return true
}
func (h *Sniffer) isTLS(buf []byte) bool {
if len(buf) < 3 {
return false
}
return buf[0] >= 0x16 && buf[0] <= 0x17 &&
buf[1] == 0x03 && buf[2] <= 0x09
}
func (h *Sniffer) Check(isUDP bool, reqAddr string) bool {
// @ means it's internal (e.g. speed test)
if strings.HasPrefix(reqAddr, "@") {
return false
}
host, port, err := net.SplitHostPort(reqAddr)
if err != nil {
return false
}
if !h.RewriteDomain && net.ParseIP(host) == nil {
// Is a domain and domain rewriting is disabled
return false
}
portNum, err := strconv.Atoi(port)
if err != nil {
return false
}
if isUDP {
return h.UDPPorts == nil || h.UDPPorts.Contains(uint16(portNum))
} else {
return h.TCPPorts == nil || h.TCPPorts.Contains(uint16(portNum))
}
}
func (h *Sniffer) TCP(stream server.HyStream, reqAddr *string) ([]byte, error) {
var err error
if h.Timeout == 0 {
err = stream.SetReadDeadline(time.Now().Add(sniffDefaultTimeout))
} else {
err = stream.SetReadDeadline(time.Now().Add(h.Timeout))
}
if err != nil {
return nil, err
}
// Make sure to reset the deadline after sniffing
defer stream.SetReadDeadline(time.Time{})
// Read 3 bytes to determine the protocol
pre := make([]byte, 3)
n, err := io.ReadFull(stream, pre)
if err != nil {
// Not enough within the timeout, just return what we have
return pre[:n], nil
}
if h.isHTTP(pre) {
// HTTP
tr := &teeReader{Stream: stream, Pre: pre}
req, _ := http.ReadRequest(bufio.NewReader(tr))
if req != nil && req.Host != "" {
// req.Host can be host:port, in which case we need to extract the host part
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
// No port, just use the whole string
host = req.Host
}
_, port, err := net.SplitHostPort(*reqAddr)
if err != nil {
return nil, err
}
*reqAddr = net.JoinHostPort(host, port)
}
return tr.Buffer(), nil
} else if h.isTLS(pre) {
// TLS
// Need to read 2 more bytes (content length)
pre = append(pre, make([]byte, 2)...)
n, err = io.ReadFull(stream, pre[3:])
if err != nil {
// Not enough within the timeout, just return what we have
return pre[:3+n], nil
}
contentLength := int(pre[3])<<8 | int(pre[4])
pre = append(pre, make([]byte, contentLength)...)
n, err = io.ReadFull(stream, pre[5:])
if err != nil {
// Not enough within the timeout, just return what we have
return pre[:5+n], nil
}
clientHello := utls.UnmarshalClientHello(pre[5:])
if clientHello != nil && clientHello.ServerName != "" {
_, port, err := net.SplitHostPort(*reqAddr)
if err != nil {
return nil, err
}
*reqAddr = net.JoinHostPort(clientHello.ServerName, port)
}
return pre, nil
} else {
// Unrecognized protocol, just return what we have
return pre, nil
}
}
func (h *Sniffer) UDP(data []byte, reqAddr *string) error {
pl, err := quicInternal.ReadCryptoPayload(data)
if err != nil || len(pl) < 4 || pl[0] != 0x01 {
// Unrecognized protocol, incomplete payload or not a client hello
return nil
}
clientHello := utls.UnmarshalClientHello(pl)
if clientHello != nil && clientHello.ServerName != "" {
_, port, err := net.SplitHostPort(*reqAddr)
if err != nil {
return err
}
*reqAddr = net.JoinHostPort(clientHello.ServerName, port)
}
return nil
}
type teeReader struct {
Stream server.HyStream
Pre []byte
buf []byte
}
func (c *teeReader) Read(b []byte) (n int, err error) {
if len(c.Pre) > 0 {
n = copy(b, c.Pre)
c.Pre = c.Pre[n:]
c.buf = append(c.buf, b[:n]...)
return n, nil
}
n, err = c.Stream.Read(b)
if n > 0 {
c.buf = append(c.buf, b[:n]...)
}
return n, err
}
func (c *teeReader) Buffer() []byte {
return append(c.Pre, c.buf...)
}
================================================
FILE: extras/sniff/sniff_test.go
================================================
package sniff
import (
"encoding/base64"
"io"
"testing"
"time"
"github.com/apernet/hysteria/extras/v2/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestSnifferCheck(t *testing.T) {
sniffer := &Sniffer{
Timeout: 1 * time.Second,
RewriteDomain: false,
TCPPorts: nil, // nil = all
UDPPorts: nil, // nil = all
}
assert.True(t, sniffer.Check(false, "1.1.1.1:80"))
assert.False(t, sniffer.Check(false, "example.com:443"))
sniffer.RewriteDomain = true
assert.True(t, sniffer.Check(false, "example.com:443"))
sniffer.TCPPorts = []utils.PortRange{{80, 80}}
assert.True(t, sniffer.Check(false, "google.com:80"))
assert.False(t, sniffer.Check(false, "google.com:443"))
sniffer.UDPPorts = []utils.PortRange{{443, 443}}
assert.True(t, sniffer.Check(true, "google.com:443"))
assert.False(t, sniffer.Check(true, "google.com:80"))
}
func TestSnifferTCP(t *testing.T) {
sniffer := &Sniffer{
Timeout: 1 * time.Second,
RewriteDomain: false,
}
buf := &[]byte{}
// Test HTTP
*buf = []byte("POST /hello HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"User-Agent: mamamiya\r\n" +
"Content-Length: 27\r\n" +
"Connection: keep-alive\r\n\r\n" +
"param1=value1¶m2=value2")
index := 0
stream := &mockStream{}
stream.EXPECT().SetReadDeadline(mock.Anything).Return(nil)
stream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
if index < len(*buf) {
n := copy(bs, (*buf)[index:])
index += n
return n, nil
} else {
return 0, io.EOF
}
})
// Rewrite IP to domain
reqAddr := "111.111.111.111:80"
putback, err := sniffer.TCP(stream, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, *buf, putback)
assert.Equal(t, "example.com:80", reqAddr)
// Test HTTP with Host as host:port
*buf = []byte("GET / HTTP/1.1\r\n" +
"Host: example.com:8080\r\n" +
"User-Agent: test-agent\r\n" +
"Accept: */*\r\n\r\n")
index = 0
reqAddr = "222.222.222.222:10086"
putback, err = sniffer.TCP(stream, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, *buf, putback)
assert.Equal(t, "example.com:10086", reqAddr)
// Test TLS
*buf, err = base64.StdEncoding.DecodeString("FgMBARcBAAETAwPJL2jlt1OAo+Rslkjv/aqKiTthKMaCKg2Gvd+uALDbDCDdY+UIk8ouadEB9fC3j52Y1i7SJZqGIgBRIS6kKieYrAAoEwITAcAswCvAMMAvwCTAI8AowCfACsAJwBTAEwCdAJwAPQA8ADUALwEAAKIAAAAOAAwAAAlpcGluZm8uaW8ABQAFAQAAAAAAKwAJCAMEAwMDAgMBAA0AGgAYCAQIBQgGBAEFAQIBBAMFAwIDAgIGAQYDACMAAAAKAAgABgAdABcAGAAQAAsACQhodHRwLzEuMQAzACYAJAAdACBguQbqNJNyamYxYcrBFpBP7pWv5TgZsP9gwGtMYNKVBQAxAAAAFwAA/wEAAQAALQACAQE=")
assert.NoError(t, err)
index = 0
reqAddr = "222.222.222.222:443"
putback, err = sniffer.TCP(stream, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, *buf, putback)
assert.Equal(t, "ipinfo.io:443", reqAddr)
// Test unrecognized 1
*buf = []byte("Wait It's All Ohio? Always Has Been.")
index = 0
reqAddr = "123.123.123.123:123"
putback, err = sniffer.TCP(stream, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, *buf, putback)
assert.Equal(t, "123.123.123.123:123", reqAddr)
// Test unrecognized 2
*buf = []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a")
index = 0
reqAddr = "45.45.45.45:45"
putback, err = sniffer.TCP(stream, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, []byte("\x01\x02\x03"), putback)
assert.Equal(t, "45.45.45.45:45", reqAddr)
// Test timeout
blockStream := &mockStream{}
blockStream.EXPECT().SetReadDeadline(mock.Anything).Return(nil)
blockStream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
time.Sleep(2 * time.Second)
return 0, io.EOF
})
reqAddr = "66.66.66.66:66"
putback, err = sniffer.TCP(blockStream, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, []byte{}, putback)
assert.Equal(t, "66.66.66.66:66", reqAddr)
}
func TestSnifferUDP(t *testing.T) {
sniffer := &Sniffer{
Timeout: 1 * time.Second,
RewriteDomain: false,
}
// Test QUIC
reqAddr := "2.3.4.5:443"
pkt, err := base64.StdEncoding.DecodeString("ygAAAAEIwugWgPS7ulYAAES8hY891uwgGE9GG4CPOLd+nsDe28raso24lCSFmlFwYQG1uF39ikbL13/R9ZTghYmTl+jEbr6F9TxxRiOgpTmKRmh6aKZiIiVfy5pVRckovaI8lq0WRoW9xoFNTyYtQP8TVJ3bLCK+zUqpquEQSyWf7CE43ywayyMpE9UlIoPXFWCoopXLM1SvzdQ+17P51N9KR7m4emti4DWWTBLMQOvrwd2HEEkbiZdRO1wf6ZXJlIat5dN0R/6uod60OFPO+u+awvq67MoMReC7+5I/xWI+xx6o4JpnZNn6YPG8Gqi8hS6doNcAAdtD8h5eMLuHCCgkpX3QVjjfWtcOhtw9xKjU43HhUPwzUTv+JDLgwuTQCTmlfYlb3B+pk4b2I9si0tJ0SBuYaZ2VQPtZbj2hpGXw3gn11pbN8xsbKkQL50+Scd4dGJxWQlGaJHeaU5WOCkxLXc635z8m5XO/CBHVYPGp4pfwfwNUgbe5WF+3MaUIlDB8dMfsnrO0BmZPo379jVx0SFLTAiS8wAdHib1WNEY8qKYnTWuiyxYg1GZEhJt0nXmI+8f0eJq42DgHBWC+Rf5rRBr/Sf25o3mFAmTUaul0Woo9/CIrpT73B63N91xd9A77i4ru995YG8l9Hen+eLtpDU9Q9376nwMDYBzeYG9U/Rn0Urbm6q4hmAgV/xlNJ2rAyDS+yLnwqD6I0PRy8bZJEttcidb/SkOyrpgMiAzWeT+SO+c/k+Y8H0UTRa05faZUrhuUaym9wAcaIVRA6nFI+fejfjVp+7afFv+kWn3vCqQEij+CRHuxkltrixZMD2rfYj6NUW7TTYBtPRtuV/V0ZIDjRR26vr4K+0D84+l3c0mA/l6nmpP5kkco3nmpdjtQN6sGXL7+5o0nnsftX5d6/n5mLyEpP+AEDl1zk3iqkS62RsITwql6DMMoGbSDdUpMclCIeM0vlo3CkxGMO7QA9ruVeNddkL3EWMivl+uxO43sXEEqYQHVl4N75y63t05GOf7/gm9Kb/BJ8MpG9ViEkVYaskQCzi3D8bVpzo8FfTj8te8B6c3ikc/cm7r8k0ZcZpr+YiLGDYq+0ilHxpqJfmq8dPkSvxdzLcUSvy7+LMQ/TTobRSF7L4JhtDKck0+00vl9H35Tkh9N+MsVtpKdWyoqZ4XaK2Nx1M6AieczXpdFc0y7lYPoUfF4IeW8WzeVUclol5ElYjkyFz/lDOGAe1bF2g5AYaGWCPiGleVZknNdD5ihB8W8Mfkt1pEwq2S97AHrppqkf/VoIfZzeqH8wUFw8fDDrZIpnoa0rW7HfwIQaqJhPCyB9Z6TVbV4x9UWmaHfVAcinCK/7o10dtaj3rvEqcUC/iPceGq3Tqv/p9GGNJ+Ci2JBjXqNxYr893Llk75VdPD9pM6y1SM0P80oXNy32VMtafkFFST8GpvvqWcxUJ93kzaY8RmU1g3XFOImSU2utU6+FUQ2Pn5uLwcfT2cTYfTpPGh+WXjSbZ6trqdEMEsLHybuPo2UN4WpVLXVQma3kSaHQggcLlEip8GhEUAy/xCb2eKqhI4HkDpDjwDnDVKufWlnRaOHf58cc8Woi+WT8JTOkHC+nBEG6fKRPHDG08U5yayIQIjI")
assert.NoError(t, err)
err = sniffer.UDP(pkt, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, "www.notion.so:443", reqAddr)
// Test unrecognized
pkt = []byte("oh my sweet summer child")
reqAddr = "90.90.90.90:90"
err = sniffer.UDP(pkt, &reqAddr)
assert.NoError(t, err)
assert.Equal(t, "90.90.90.90:90", reqAddr)
}
================================================
FILE: extras/trafficlogger/http.go
================================================
package trafficlogger
import (
"cmp"
"encoding/json"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/apernet/hysteria/core/v2/server"
)
const (
indexHTML = ` Hysteria Traffic Stats API Server This is a Hysteria Traffic Stats API server.
Check the documentation for usage.
`
)
// TrafficStatsServer implements both server.TrafficLogger and http.Handler
// to provide a simple HTTP API to get the traffic stats per user.
type TrafficStatsServer interface {
server.TrafficLogger
http.Handler
}
func NewTrafficStatsServer(secret string) TrafficStatsServer {
return &trafficStatsServerImpl{
StatsMap: make(map[string]*trafficStatsEntry),
KickMap: make(map[string]struct{}),
OnlineMap: make(map[string]int),
StreamMap: make(map[server.HyStream]*server.StreamStats),
Secret: secret,
}
}
type trafficStatsServerImpl struct {
Mutex sync.RWMutex
StatsMap map[string]*trafficStatsEntry
OnlineMap map[string]int
StreamMap map[server.HyStream]*server.StreamStats
KickMap map[string]struct{}
Secret string
}
type trafficStatsEntry struct {
Tx uint64 `json:"tx"`
Rx uint64 `json:"rx"`
}
func (s *trafficStatsServerImpl) LogTraffic(id string, tx, rx uint64) (ok bool) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
_, ok = s.KickMap[id]
if ok {
delete(s.KickMap, id)
return false
}
entry, ok := s.StatsMap[id]
if !ok {
entry = &trafficStatsEntry{}
s.StatsMap[id] = entry
}
entry.Tx += tx
entry.Rx += rx
return true
}
// LogOnlineState updates the online state to the online map.
func (s *trafficStatsServerImpl) LogOnlineState(id string, online bool) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
if online {
s.OnlineMap[id]++
} else {
s.OnlineMap[id]--
if s.OnlineMap[id] <= 0 {
delete(s.OnlineMap, id)
}
}
}
func (s *trafficStatsServerImpl) TraceStream(stream server.HyStream, stats *server.StreamStats) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
s.StreamMap[stream] = stats
}
func (s *trafficStatsServerImpl) UntraceStream(stream server.HyStream) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
delete(s.StreamMap, stream)
}
func (s *trafficStatsServerImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.Secret != "" && r.Header.Get("Authorization") != s.Secret {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/" {
_, _ = w.Write([]byte(indexHTML))
return
}
if r.Method == http.MethodGet && r.URL.Path == "/traffic" {
s.getTraffic(w, r)
return
}
if r.Method == http.MethodPost && r.URL.Path == "/kick" {
s.kick(w, r)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/online" {
s.getOnline(w, r)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/dump/streams" {
s.getDumpStreams(w, r)
return
}
http.NotFound(w, r)
}
func (s *trafficStatsServerImpl) getTraffic(w http.ResponseWriter, r *http.Request) {
bClear, _ := strconv.ParseBool(r.URL.Query().Get("clear"))
var jb []byte
var err error
if bClear {
s.Mutex.Lock()
jb, err = json.Marshal(s.StatsMap)
s.StatsMap = make(map[string]*trafficStatsEntry)
s.Mutex.Unlock()
} else {
s.Mutex.RLock()
jb, err = json.Marshal(s.StatsMap)
s.Mutex.RUnlock()
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(jb)
}
func (s *trafficStatsServerImpl) getOnline(w http.ResponseWriter, r *http.Request) {
s.Mutex.RLock()
defer s.Mutex.RUnlock()
jb, err := json.Marshal(s.OnlineMap)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(jb)
}
type dumpStreamEntry struct {
State string `json:"state"`
Auth string `json:"auth"`
Connection uint32 `json:"connection"`
Stream uint64 `json:"stream"`
ReqAddr string `json:"req_addr"`
HookedReqAddr string `json:"hooked_req_addr"`
Tx uint64 `json:"tx"`
Rx uint64 `json:"rx"`
InitialAt string `json:"initial_at"`
LastActiveAt string `json:"last_active_at"`
// for text/plain output
initialTime time.Time
lastActiveTime time.Time
}
func (e *dumpStreamEntry) fromStreamStats(stream server.HyStream, s *server.StreamStats) {
e.State = s.State.Load().String()
e.Auth = s.AuthID
e.Connection = s.ConnID
e.Stream = uint64(stream.StreamID())
e.ReqAddr = s.ReqAddr.Load()
e.HookedReqAddr = s.HookedReqAddr.Load()
e.Tx = s.Tx.Load()
e.Rx = s.Rx.Load()
e.initialTime = s.InitialTime
e.lastActiveTime = s.LastActiveTime.Load()
e.InitialAt = e.initialTime.Format(time.RFC3339Nano)
e.LastActiveAt = e.lastActiveTime.Format(time.RFC3339Nano)
}
func formatDumpStreamLine(state, auth, connection, stream, reqAddr, hookedReqAddr, tx, rx, lifetime, lastActive string) string {
return fmt.Sprintf("%-8s %-12s %12s %8s %12s %12s %12s %12s %-16s %s", state, auth, connection, stream, tx, rx, lifetime, lastActive, reqAddr, hookedReqAddr)
}
func (e *dumpStreamEntry) String() string {
stateText := strings.ToUpper(e.State)
connectionText := fmt.Sprintf("%08X", e.Connection)
streamText := strconv.FormatUint(e.Stream, 10)
reqAddrText := e.ReqAddr
if reqAddrText == "" {
reqAddrText = "-"
}
hookedReqAddrText := e.HookedReqAddr
if hookedReqAddrText == "" {
hookedReqAddrText = "-"
}
txText := strconv.FormatUint(e.Tx, 10)
rxText := strconv.FormatUint(e.Rx, 10)
lifetime := time.Since(e.initialTime)
if lifetime < 10*time.Minute {
lifetime = lifetime.Round(time.Millisecond)
} else {
lifetime = lifetime.Round(time.Second)
}
lastActive := time.Since(e.lastActiveTime)
if lastActive < 10*time.Minute {
lastActive = lastActive.Round(time.Millisecond)
} else {
lastActive = lastActive.Round(time.Second)
}
return formatDumpStreamLine(stateText, e.Auth, connectionText, streamText, reqAddrText, hookedReqAddrText, txText, rxText, lifetime.String(), lastActive.String())
}
func (s *trafficStatsServerImpl) getDumpStreams(w http.ResponseWriter, r *http.Request) {
var entries []dumpStreamEntry
s.Mutex.RLock()
entries = make([]dumpStreamEntry, len(s.StreamMap))
index := 0
for stream, stats := range s.StreamMap {
entries[index].fromStreamStats(stream, stats)
index++
}
s.Mutex.RUnlock()
slices.SortFunc(entries, func(lhs, rhs dumpStreamEntry) int {
if ret := cmp.Compare(lhs.Auth, rhs.Auth); ret != 0 {
return ret
}
if ret := cmp.Compare(lhs.Connection, rhs.Connection); ret != 0 {
return ret
}
if ret := cmp.Compare(lhs.Stream, rhs.Stream); ret != 0 {
return ret
}
return 0
})
accept := r.Header.Get("Accept")
if strings.Contains(accept, "text/plain") {
// Generate netstat-like output for humans
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// Print table header
_, _ = fmt.Fprintln(w, formatDumpStreamLine("State", "Auth", "Connection", "Stream", "Req-Addr", "Hooked-Req-Addr", "TX-Bytes", "RX-Bytes", "Lifetime", "Last-Active"))
for _, entry := range entries {
_, _ = fmt.Fprintln(w, entry.String())
}
return
}
// Response with json by default
wrapper := struct {
Streams []dumpStreamEntry `json:"streams"`
}{entries}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
err := json.NewEncoder(w).Encode(&wrapper)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *trafficStatsServerImpl) kick(w http.ResponseWriter, r *http.Request) {
var ids []string
err := json.NewDecoder(r.Body).Decode(&ids)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.Mutex.Lock()
for _, id := range ids {
s.KickMap[id] = struct{}{}
}
s.Mutex.Unlock()
w.WriteHeader(http.StatusOK)
}
================================================
FILE: extras/transport/udphop/addr.go
================================================
package udphop
import (
"fmt"
"net"
"github.com/apernet/hysteria/extras/v2/utils"
)
type InvalidPortError struct {
PortStr string
}
func (e InvalidPortError) Error() string {
return fmt.Sprintf("%s is not a valid port number or range", e.PortStr)
}
// UDPHopAddr contains an IP address and a list of ports.
type UDPHopAddr struct {
IP net.IP
Ports []uint16
PortStr string
}
func (a *UDPHopAddr) Network() string {
return "udphop"
}
func (a *UDPHopAddr) String() string {
return net.JoinHostPort(a.IP.String(), a.PortStr)
}
// addrs returns a list of net.Addr's, one for each port.
func (a *UDPHopAddr) addrs() ([]net.Addr, error) {
var addrs []net.Addr
for _, port := range a.Ports {
addr := &net.UDPAddr{
IP: a.IP,
Port: int(port),
}
addrs = append(addrs, addr)
}
return addrs, nil
}
func ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
ip, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, err
}
result := &UDPHopAddr{
IP: ip.IP,
PortStr: portStr,
}
pu := utils.ParsePortUnion(portStr)
if pu == nil {
return nil, InvalidPortError{portStr}
}
result.Ports = pu.Ports()
return result, nil
}
================================================
FILE: extras/transport/udphop/conn.go
================================================
package udphop
import (
"errors"
"math/rand"
"net"
"sync"
"syscall"
"time"
)
const (
packetQueueSize = 1024
udpBufferSize = 2048 // QUIC packets are at most 1500 bytes long, so 2k should be more than enough
defaultHopInterval = 30 * time.Second
)
type udpHopPacketConn struct {
Addr net.Addr
Addrs []net.Addr
HopInterval time.Duration
ListenUDPFunc ListenUDPFunc
connMutex sync.RWMutex
prevConn net.PacketConn
currentConn net.PacketConn
addrIndex int
readBufferSize int
writeBufferSize int
recvQueue chan *udpPacket
closeChan chan struct{}
closed bool
bufPool sync.Pool
}
type udpPacket struct {
Buf []byte
N int
Addr net.Addr
Err error
}
type ListenUDPFunc = func() (net.PacketConn, error)
func NewUDPHopPacketConn(addr *UDPHopAddr, hopInterval time.Duration, listenUDPFunc ListenUDPFunc) (net.PacketConn, error) {
if hopInterval == 0 {
hopInterval = defaultHopInterval
} else if hopInterval < 5*time.Second {
return nil, errors.New("hop interval must be at least 5 seconds")
}
if listenUDPFunc == nil {
listenUDPFunc = func() (net.PacketConn, error) {
return net.ListenUDP("udp", nil)
}
}
addrs, err := addr.addrs()
if err != nil {
return nil, err
}
curConn, err := listenUDPFunc()
if err != nil {
return nil, err
}
hConn := &udpHopPacketConn{
Addr: addr,
Addrs: addrs,
HopInterval: hopInterval,
ListenUDPFunc: listenUDPFunc,
prevConn: nil,
currentConn: curConn,
addrIndex: rand.Intn(len(addrs)),
recvQueue: make(chan *udpPacket, packetQueueSize),
closeChan: make(chan struct{}),
bufPool: sync.Pool{
New: func() interface{} {
return make([]byte, udpBufferSize)
},
},
}
go hConn.recvLoop(curConn)
go hConn.hopLoop()
return hConn, nil
}
func (u *udpHopPacketConn) recvLoop(conn net.PacketConn) {
for {
buf := u.bufPool.Get().([]byte)
n, addr, err := conn.ReadFrom(buf)
if err != nil {
u.bufPool.Put(buf)
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
// Only pass through timeout errors here, not permanent errors
// like connection closed. Connection close is normal as we close
// the old connection to exit this loop every time we hop.
u.recvQueue <- &udpPacket{nil, 0, nil, netErr}
}
return
}
select {
case u.recvQueue <- &udpPacket{buf, n, addr, nil}:
// Packet successfully queued
default:
// Queue is full, drop the packet
u.bufPool.Put(buf)
}
}
}
func (u *udpHopPacketConn) hopLoop() {
ticker := time.NewTicker(u.HopInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
u.hop()
case <-u.closeChan:
return
}
}
}
func (u *udpHopPacketConn) hop() {
u.connMutex.Lock()
defer u.connMutex.Unlock()
if u.closed {
return
}
newConn, err := u.ListenUDPFunc()
if err != nil {
// Could be temporary, just skip this hop
return
}
// We need to keep receiving packets from the previous connection,
// because otherwise there will be packet loss due to the time gap
// between we hop to a new port and the server acknowledges this change.
// So we do the following:
// Close prevConn,
// move currentConn to prevConn,
// set newConn as currentConn,
// start recvLoop on newConn.
if u.prevConn != nil {
_ = u.prevConn.Close() // recvLoop for this conn will exit
}
u.prevConn = u.currentConn
u.currentConn = newConn
// Set buffer sizes if previously set
if u.readBufferSize > 0 {
_ = trySetReadBuffer(u.currentConn, u.readBufferSize)
}
if u.writeBufferSize > 0 {
_ = trySetWriteBuffer(u.currentConn, u.writeBufferSize)
}
go u.recvLoop(newConn)
// Update addrIndex to a new random value
u.addrIndex = rand.Intn(len(u.Addrs))
}
func (u *udpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
for {
select {
case p := <-u.recvQueue:
if p.Err != nil {
return 0, nil, p.Err
}
// Currently we do not check whether the packet is from
// the server or not due to performance reasons.
n := copy(b, p.Buf[:p.N])
u.bufPool.Put(p.Buf)
return n, u.Addr, nil
case <-u.closeChan:
return 0, nil, net.ErrClosed
}
}
}
func (u *udpHopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
u.connMutex.RLock()
defer u.connMutex.RUnlock()
if u.closed {
return 0, net.ErrClosed
}
// Skip the check for now, always write to the server,
// for the same reason as in ReadFrom.
return u.currentConn.WriteTo(b, u.Addrs[u.addrIndex])
}
func (u *udpHopPacketConn) Close() error {
u.connMutex.Lock()
defer u.connMutex.Unlock()
if u.closed {
return nil
}
// Close prevConn and currentConn
// Close closeChan to unblock ReadFrom & hopLoop
// Set closed flag to true to prevent double close
if u.prevConn != nil {
_ = u.prevConn.Close()
}
err := u.currentConn.Close()
close(u.closeChan)
u.closed = true
u.Addrs = nil // For GC
return err
}
func (u *udpHopPacketConn) LocalAddr() net.Addr {
u.connMutex.RLock()
defer u.connMutex.RUnlock()
return u.currentConn.LocalAddr()
}
func (u *udpHopPacketConn) SetDeadline(t time.Time) error {
u.connMutex.RLock()
defer u.connMutex.RUnlock()
if u.prevConn != nil {
_ = u.prevConn.SetDeadline(t)
}
return u.currentConn.SetDeadline(t)
}
func (u *udpHopPacketConn) SetReadDeadline(t time.Time) error {
u.connMutex.RLock()
defer u.connMutex.RUnlock()
if u.prevConn != nil {
_ = u.prevConn.SetReadDeadline(t)
}
return u.currentConn.SetReadDeadline(t)
}
func (u *udpHopPacketConn) SetWriteDeadline(t time.Time) error {
u.connMutex.RLock()
defer u.connMutex.RUnlock()
if u.prevConn != nil {
_ = u.prevConn.SetWriteDeadline(t)
}
return u.currentConn.SetWriteDeadline(t)
}
// UDP-specific methods below
func (u *udpHopPacketConn) SetReadBuffer(bytes int) error {
u.connMutex.Lock()
defer u.connMutex.Unlock()
u.readBufferSize = bytes
if u.prevConn != nil {
_ = trySetReadBuffer(u.prevConn, bytes)
}
return trySetReadBuffer(u.currentConn, bytes)
}
func (u *udpHopPacketConn) SetWriteBuffer(bytes int) error {
u.connMutex.Lock()
defer u.connMutex.Unlock()
u.writeBufferSize = bytes
if u.prevConn != nil {
_ = trySetWriteBuffer(u.prevConn, bytes)
}
return trySetWriteBuffer(u.currentConn, bytes)
}
func (u *udpHopPacketConn) SyscallConn() (syscall.RawConn, error) {
u.connMutex.RLock()
defer u.connMutex.RUnlock()
sc, ok := u.currentConn.(syscall.Conn)
if !ok {
return nil, errors.New("not supported")
}
return sc.SyscallConn()
}
func trySetReadBuffer(pc net.PacketConn, bytes int) error {
sc, ok := pc.(interface {
SetReadBuffer(bytes int) error
})
if ok {
return sc.SetReadBuffer(bytes)
}
return nil
}
func trySetWriteBuffer(pc net.PacketConn, bytes int) error {
sc, ok := pc.(interface {
SetWriteBuffer(bytes int) error
})
if ok {
return sc.SetWriteBuffer(bytes)
}
return nil
}
================================================
FILE: extras/utils/portunion.go
================================================
package utils
import (
"sort"
"strconv"
"strings"
)
// PortUnion is a collection of multiple port ranges.
type PortUnion []PortRange
// PortRange represents a range of ports.
// Start and End are inclusive. [Start, End]
type PortRange struct {
Start, End uint16
}
// ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion.
// Returns nil if the input is invalid.
// The returned PortUnion is guaranteed to be normalized.
func ParsePortUnion(s string) PortUnion {
if s == "all" || s == "*" {
// Wildcard special case
return PortUnion{PortRange{0, 65535}}
}
var result PortUnion
portStrs := strings.Split(s, ",")
for _, portStr := range portStrs {
if strings.Contains(portStr, "-") {
// Port range
portRange := strings.Split(portStr, "-")
if len(portRange) != 2 {
return nil
}
start, err := strconv.ParseUint(portRange[0], 10, 16)
if err != nil {
return nil
}
end, err := strconv.ParseUint(portRange[1], 10, 16)
if err != nil {
return nil
}
if start > end {
start, end = end, start
}
result = append(result, PortRange{uint16(start), uint16(end)})
} else {
// Single port
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return nil
}
result = append(result, PortRange{uint16(port), uint16(port)})
}
}
if result == nil {
return nil
}
return result.Normalize()
}
// Normalize normalizes a PortUnion.
// No overlapping ranges, ranges are sorted from low to high.
func (u PortUnion) Normalize() PortUnion {
if len(u) == 0 {
return u
}
sort.Slice(u, func(i, j int) bool {
if u[i].Start == u[j].Start {
return u[i].End < u[j].End
}
return u[i].Start < u[j].Start
})
normalized := PortUnion{u[0]}
for _, current := range u[1:] {
last := &normalized[len(normalized)-1]
if uint32(current.Start) <= uint32(last.End)+1 {
if current.End > last.End {
last.End = current.End
}
} else {
normalized = append(normalized, current)
}
}
return normalized
}
// Ports returns all ports in the PortUnion as a slice.
func (u PortUnion) Ports() []uint16 {
var ports []uint16
for _, r := range u {
for i := uint32(r.Start); i <= uint32(r.End); i++ {
ports = append(ports, uint16(i))
}
}
return ports
}
// Contains returns true if the PortUnion contains the given port.
func (u PortUnion) Contains(port uint16) bool {
for _, r := range u {
if port >= r.Start && port <= r.End {
return true
}
}
return false
}
================================================
FILE: extras/utils/portunion_test.go
================================================
package utils
import (
"reflect"
"slices"
"testing"
)
func TestParsePortUnion(t *testing.T) {
tests := []struct {
name string
s string
want PortUnion
}{
{
name: "empty",
s: "",
want: nil,
},
{
name: "all 1",
s: "all",
want: PortUnion{{0, 65535}},
},
{
name: "all 2",
s: "*",
want: PortUnion{{0, 65535}},
},
{
name: "single port",
s: "1234",
want: PortUnion{{1234, 1234}},
},
{
name: "multiple ports (unsorted)",
s: "5678,1234,9012",
want: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}},
},
{
name: "one range",
s: "1234-1240",
want: PortUnion{{1234, 1240}},
},
{
name: "one range (reversed)",
s: "1240-1234",
want: PortUnion{{1234, 1240}},
},
{
name: "multiple ports and ranges (reversed, unsorted, overlapping)",
s: "5678,1200-1236,9100-9012,1234-1240",
want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}},
},
{
name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping)",
s: "5678,1200-1236,65531-65535,65532-65534,9100-9012,1234-1240",
want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}},
},
{
name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping) 2",
s: "5678,1200-1236,65532-65535,65531-65534,9100-9012,1234-1240",
want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}},
},
{
name: "invalid 1",
s: "1234-",
want: nil,
},
{
name: "invalid 2",
s: "1234-ggez",
want: nil,
},
{
name: "invalid 3",
s: "233,",
want: nil,
},
{
name: "invalid 4",
s: "1234-1240-1250",
want: nil,
},
{
name: "invalid 5",
s: "-,,",
want: nil,
},
{
name: "invalid 6",
s: "http",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParsePortUnion() = %v, want %v", got, tt.want)
}
})
}
}
func TestPortUnion_Ports(t *testing.T) {
tests := []struct {
name string
pu PortUnion
want []uint16
}{
{
name: "single port",
pu: PortUnion{{1234, 1234}},
want: []uint16{1234},
},
{
name: "multiple ports",
pu: PortUnion{{1234, 1236}},
want: []uint16{1234, 1235, 1236},
},
{
name: "multiple ports and ranges",
pu: PortUnion{{1234, 1236}, {5678, 5680}, {9000, 9002}},
want: []uint16{1234, 1235, 1236, 5678, 5679, 5680, 9000, 9001, 9002},
},
{
name: "single port 65535",
pu: PortUnion{{65535, 65535}},
want: []uint16{65535},
},
{
name: "port range with 65535",
pu: PortUnion{{65530, 65535}},
want: []uint16{65530, 65531, 65532, 65533, 65534, 65535},
},
{
name: "multiple ports and ranges with 65535",
pu: PortUnion{{65530, 65535}, {1234, 1236}},
want: []uint16{65530, 65531, 65532, 65533, 65534, 65535, 1234, 1235, 1236},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.pu.Ports(); !slices.Equal(got, tt.want) {
t.Errorf("PortUnion.Ports() = %v, want %v", got, tt.want)
}
})
}
}
================================================
FILE: go.work.sum
================================================
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0 h1:SPOUaucgtVls75mg+X7CXigS71EnsfVUK/2CgVrwqgw=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412 h1:GvWw74lx5noHocd+f6HBMXK6DuggBB1dhVkuGZbv7qM=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c h1:ivON6cwHK1OH26MZyWDCnbTRZZf0IhNsENoNAKFS1g4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:OR8VhtwhcAI3U48/rzBsVOuHi0zDPzYI1xASVcdSgR8=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/apernet/quic-go v0.54.1-0.20260110201338-839e2640e302/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU=
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q=
github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad h1:EmNYJhPYy0pOFjCx2PrgtaBXmee0iUX9hLlxE1xHOJE=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg=
github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/consul/api v1.18.0 h1:R7PPNzTCeN6VuQNDwwhZWJvzCtGSrNpJqfb22h3yH9g=
github.com/hashicorp/consul/api v1.18.0/go.mod h1:owRRGJ9M5xReDC5nfT8FTJrNAPbT4NM6p/k+d03q2v4=
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/sagikazarmark/crypt v0.9.0 h1:fipzMFW34hFUEc4D7fsLQFtE7yElkpgyS2zruedRdZk=
github.com/sagikazarmark/crypt v0.9.0/go.mod h1:RnH7sEhxfdnPm1z+XMgSLjWTEIjyK4z2dw6+4vHTMuo=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d h1:Yoy/IzG4lULT6qZg62sVC+qyBL8DQkmD2zv6i7OImrc=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c h1:UOk+nlt1BJtTcH15CT7iNO7YVWTfTv/DNwEAQHLIaDQ=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20 h1:7pDq9pAMCQgRohFmd25X8hIH8VxmT3TaDm+r9LHxgBk=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9 h1:MPblCbqA5+z6XARjScMfz1TqtJC7TuTRj0U9VqIBs6k=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50 h1:crYRwvwjdVh1biHzzciFHe8DrZcYrVcZFlJtykhRctg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc h1:eHRtZoIi6n9Wo1uR+RU44C247msLWwyA89hVKwRLkMk=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9 h1:fxoFD0in0/CBzXoyNhMTjvBZYW6ilSnTw7N7y/8vkmM=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191 h1:T4wuULTrzCKMFlg3HmKHgXAF8oStFb/+lOIupLV2v+o=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241 h1:Y+TeIabU8sJD10Qwd/zMty2/LEaT9GNDaA6nyZf+jgo=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122 h1:TQVQrsyNaimGwF7bIhzoVC9QkKm4KsWd8cECGzFx8gI=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2 h1:bu666BQci+y4S0tVRVjsHUeRon6vUXmsGBwdowgMrg4=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/AkPDU3AkqMxnMYL+imaqkpflHu73us8=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
go.etcd.io/etcd/api/v3 v3.5.6 h1:Cy2qx3npLcYqTKqGJzMypnMv2tiRyifZJ17BlWIWA7A=
go.etcd.io/etcd/api/v3 v3.5.6/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
go.etcd.io/etcd/client/pkg/v3 v3.5.6 h1:TXQWYceBKqLp4sa87rcPs11SXxUA/mHwH975v+BDvLU=
go.etcd.io/etcd/client/pkg/v3 v3.5.6/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
go.etcd.io/etcd/client/v2 v2.305.6 h1:fIDR0p4KMjw01MJMfUIDWdQbjo06PD6CeYM5z4EHLi0=
go.etcd.io/etcd/client/v2 v2.305.6/go.mod h1:BHha8XJGe8vCIBfWBpbBLVZ4QjOIlfoouvOwydu63E0=
go.etcd.io/etcd/client/v3 v3.5.6 h1:coLs69PWCXE9G4FKquzNaSHrRyMCAXwF+IX1tAPVO8E=
go.etcd.io/etcd/client/v3 v3.5.6/go.mod h1:f6GRinRMCsFVv9Ht42EyY7nfsVGwrNO0WEoS2pRKzQk=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.107.0 h1:I2SlFjD8ZWabaIFOfeEDg3pf0BHJDh6iYQ1ic3Yu/UU=
google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY=
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk=
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo=
honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
sourcegraph.com/sourcegraph/go-diff v0.5.0 h1:eTiIR0CoWjGzJcnQ3OkhIl/b9GJovq4lSAVRt0ZFEG8=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
================================================
FILE: hyperbole.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import os
import re
import sys
import subprocess
import datetime
import shutil
# Hyperbole is the official build script for Hysteria.
# Available environment variables for controlling the build:
# - HY_APP_VERSION: App version
# - HY_APP_COMMIT: App commit hash
# - HY_APP_PLATFORMS: Platforms to build for (e.g. "windows/amd64,linux/arm")
LOGO = """
░█░█░█░█░█▀█░█▀▀░█▀▄░█▀▄░█▀█░█░░░█▀▀
░█▀█░░█░░█▀▀░█▀▀░█▀▄░█▀▄░█░█░█░░░█▀▀
░▀░▀░░▀░░▀░░░▀▀▀░▀░▀░▀▀░░▀▀▀░▀▀▀░▀▀▀
"""
DESC = "Hyperbole is the official build script for Hysteria."
BUILD_DIR = "build"
CORE_SRC_DIR = "./core"
EXTRAS_SRC_DIR = "./extras"
APP_SRC_DIR = "./app"
APP_SRC_CMD_PKG = "github.com/apernet/hysteria/app/v2/cmd"
MODULE_SRC_DIRS = [CORE_SRC_DIR, EXTRAS_SRC_DIR, APP_SRC_DIR]
ARCH_ALIASES = {
"arm": {
"GOARCH": "arm",
"GOARM": "7",
},
"armv5": {
"GOARCH": "arm",
"GOARM": "5",
},
"armv6": {
"GOARCH": "arm",
"GOARM": "6",
},
"armv7": {
"GOARCH": "arm",
"GOARM": "7",
},
"mips": {
"GOARCH": "mips",
"GOMIPS": "",
},
"mipsle": {
"GOARCH": "mipsle",
"GOMIPS": "",
},
"mips-sf": {
"GOARCH": "mips",
"GOMIPS": "softfloat",
},
"mipsle-sf": {
"GOARCH": "mipsle",
"GOMIPS": "softfloat",
},
"amd64": {
"GOARCH": "amd64",
"GOAMD64": "",
},
"amd64-avx": {
"GOARCH": "amd64",
"GOAMD64": "v3",
},
"loong64": {
"GOARCH": "loong64",
},
}
def check_command(args):
try:
subprocess.check_call(
args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
return True
except Exception:
return False
def check_build_env():
if not check_command(["git", "--version"]):
print("Git is not installed. Please install Git and try again.")
return False
if not check_command(["git", "rev-parse", "--is-inside-work-tree"]):
print("Not in a Git repository. Please go to the project root and try again.")
return False
if not check_command(["go", "version"]):
print("Go is not installed. Please install Go and try again.")
return False
return True
def get_app_version():
app_version = os.environ.get("HY_APP_VERSION")
if not app_version:
try:
output = (
subprocess.check_output(
["git", "describe", "--tags", "--always", "--match", "app/v*"]
)
.decode()
.strip()
)
app_version = output.split("/")[-1]
except Exception:
app_version = "Unknown"
return app_version
def get_app_version_code(str=None):
if not str:
str = get_app_version()
match = re.search(r"v(\d+)\.(\d+)\.(\d+)", str)
if match:
major, minor, patch = match.groups()
major = major.zfill(2)[:2]
minor = minor.zfill(2)[:2]
patch = patch.zfill(2)[:2]
return int(f"{major}{minor}{patch[:2]}")
else:
return 0
def get_app_commit():
app_commit = os.environ.get("HY_APP_COMMIT")
if not app_commit:
try:
app_commit = (
subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
)
except Exception:
app_commit = "Unknown"
return app_commit
def get_toolchain():
try:
output = subprocess.check_output(["go", "version"]).decode().strip()
if output.startswith("go version "):
output = output[11:]
return output
except Exception:
return "Unknown"
def get_current_os_arch():
d_os = subprocess.check_output(["go", "env", "GOOS"]).decode().strip()
d_arch = subprocess.check_output(["go", "env", "GOARCH"]).decode().strip()
return (d_os, d_arch)
def get_lib_version():
try:
with open(CORE_SRC_DIR + "/go.mod") as f:
for line in f:
line = line.strip()
if line.startswith("github.com/apernet/quic-go"):
return line.split(" ")[1].strip()
except Exception:
return "Unknown"
def get_app_platforms():
platforms = os.environ.get("HY_APP_PLATFORMS")
if not platforms:
d_os, d_arch = get_current_os_arch()
return [(d_os, d_arch)]
result = []
for platform in platforms.split(","):
platform = platform.strip()
if not platform:
continue
parts = platform.split("/")
if len(parts) != 2:
continue
result.append((parts[0], parts[1]))
return result
def cmd_build(pprof=False, release=False, race=False):
if not check_build_env():
return
os.makedirs(BUILD_DIR, exist_ok=True)
app_version = get_app_version()
app_date = datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
app_toolchain = get_toolchain()
app_commit = get_app_commit()
lib_version = get_lib_version()
ldflags = [
"-X",
APP_SRC_CMD_PKG + ".appVersion=" + app_version,
"-X",
APP_SRC_CMD_PKG + ".appDate=" + app_date,
"-X",
APP_SRC_CMD_PKG
+ ".appType="
+ ("release" if release else "dev")
+ ("-pprof" if pprof else ""),
"-X",
'"' + APP_SRC_CMD_PKG + ".appToolchain=" + app_toolchain + '"',
"-X",
APP_SRC_CMD_PKG + ".appCommit=" + app_commit,
"-X",
APP_SRC_CMD_PKG + ".libVersion=" + lib_version,
]
if release:
ldflags.append("-s")
ldflags.append("-w")
for os_name, arch in get_app_platforms():
print("Building for %s/%s..." % (os_name, arch))
out_name = "hysteria-%s-%s" % (os_name, arch)
if os_name == "windows":
out_name += ".exe"
env = os.environ.copy()
env["GOOS"] = os_name
if arch in ARCH_ALIASES:
for k, v in ARCH_ALIASES[arch].items():
env[k] = v
else:
env["GOARCH"] = arch
if os_name == "android":
env["CGO_ENABLED"] = "1"
ANDROID_NDK_HOME = (
os.environ.get("ANDROID_NDK_HOME")
+ "/toolchains/llvm/prebuilt/linux-x86_64/bin"
)
if arch == "arm64":
env["CC"] = ANDROID_NDK_HOME + "/aarch64-linux-android29-clang"
elif arch == "armv7":
env["CC"] = ANDROID_NDK_HOME + "/armv7a-linux-androideabi29-clang"
elif arch == "386":
env["CC"] = ANDROID_NDK_HOME + "/i686-linux-android29-clang"
elif arch == "amd64":
env["CC"] = ANDROID_NDK_HOME + "/x86_64-linux-android29-clang"
else:
print("Unsupported arch for android: %s" % arch)
return
else:
env["CGO_ENABLED"] = "1" if race else "0" # Race detector requires cgo
plat_ldflags = ldflags.copy()
plat_ldflags.append("-X")
plat_ldflags.append(APP_SRC_CMD_PKG + ".appPlatform=" + os_name)
plat_ldflags.append("-X")
plat_ldflags.append(APP_SRC_CMD_PKG + ".appArch=" + arch)
cmd = [
"go",
"build",
"-o",
os.path.join(BUILD_DIR, out_name),
"-ldflags",
" ".join(plat_ldflags),
]
if pprof:
cmd.append("-tags")
cmd.append("pprof")
if race:
cmd.append("-race")
if release:
cmd.append("-trimpath")
cmd.append(APP_SRC_DIR)
try:
subprocess.check_call(cmd, env=env)
except Exception:
print("Failed to build for %s/%s" % (os_name, arch))
sys.exit(1)
print("Built %s" % out_name)
def cmd_run(args, pprof=False, race=False):
if not check_build_env():
return
app_version = get_app_version()
app_date = datetime.datetime.now(datetime.timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
app_toolchain = get_toolchain()
app_commit = get_app_commit()
lib_version = get_lib_version()
current_os, current_arch = get_current_os_arch()
ldflags = [
"-X",
APP_SRC_CMD_PKG + ".appVersion=" + app_version,
"-X",
APP_SRC_CMD_PKG + ".appDate=" + app_date,
"-X",
APP_SRC_CMD_PKG + ".appType=dev-run",
"-X",
'"' + APP_SRC_CMD_PKG + ".appToolchain=" + app_toolchain + '"',
"-X",
APP_SRC_CMD_PKG + ".appCommit=" + app_commit,
"-X",
APP_SRC_CMD_PKG + ".appPlatform=" + current_os,
"-X",
APP_SRC_CMD_PKG + ".appArch=" + current_arch,
"-X",
APP_SRC_CMD_PKG + ".libVersion=" + lib_version,
]
cmd = ["go", "run", "-ldflags", " ".join(ldflags)]
if pprof:
cmd.append("-tags")
cmd.append("pprof")
if race:
cmd.append("-race")
cmd.append(APP_SRC_DIR)
cmd.extend(args)
try:
subprocess.check_call(cmd)
except KeyboardInterrupt:
pass
except subprocess.CalledProcessError as e:
# Pass through the exit code
sys.exit(e.returncode)
def cmd_format():
if not check_command(["gofumpt", "-version"]):
print("gofumpt is not installed. Please install gofumpt and try again.")
return
try:
subprocess.check_call(["gofumpt", "-w", "-l", "-extra", "."])
except Exception:
print("Failed to format code")
def cmd_mockgen():
if not check_command(["mockery", "--version"]):
print("mockery is not installed. Please install mockery and try again.")
return
for dirpath, dirnames, filenames in os.walk("."):
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
if ".mockery.yaml" in filenames:
print("Generating mocks for %s..." % dirpath)
try:
subprocess.check_call(["mockery"], cwd=dirpath)
except Exception:
print("Failed to generate mocks for %s" % dirpath)
def cmd_protogen():
if not check_command(["protoc", "--version"]):
print("protoc is not installed. Please install protoc and try again.")
return
for dirpath, dirnames, filenames in os.walk("."):
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
proto_files = [f for f in filenames if f.endswith(".proto")]
if len(proto_files) > 0:
for proto_file in proto_files:
print("Generating protobuf for %s..." % proto_file)
try:
subprocess.check_call(
["protoc", "--go_out=paths=source_relative:.", proto_file],
cwd=dirpath,
)
except Exception:
print("Failed to generate protobuf for %s" % proto_file)
def cmd_tidy():
if not check_build_env():
return
for dir in MODULE_SRC_DIRS:
print("Tidying %s..." % dir)
try:
subprocess.check_call(["go", "mod", "tidy"], cwd=dir)
except Exception:
print("Failed to tidy %s" % dir)
print("Syncing go work...")
try:
subprocess.check_call(["go", "work", "sync"])
except Exception:
print("Failed to sync go work")
def cmd_test(module=None):
if not check_build_env():
return
if module:
print("Testing %s..." % module)
try:
subprocess.check_call(["go", "test", "-v", "./..."], cwd=module)
except Exception:
print("Failed to test %s" % module)
else:
for dir in MODULE_SRC_DIRS:
print("Testing %s..." % dir)
try:
subprocess.check_call(["go", "test", "-v", "./..."], cwd=dir)
except Exception:
print("Failed to test %s" % dir)
def cmd_publish(urgent=False):
import requests
if not check_build_env():
return
app_version = get_app_version()
app_version_code = get_app_version_code(app_version)
if app_version_code == 0:
print("Invalid app version")
return
payload = {
"code": app_version_code,
"ver": app_version,
"chan": "release",
"url": "https://github.com/apernet/hysteria/releases",
"urgent": urgent,
}
headers = {
"Content-Type": "application/json",
"Authorization": os.environ.get("HY_API_POST_KEY"),
}
resp = requests.post("https://api.hy2.io/v1/update", json=payload, headers=headers)
if resp.status_code == 200:
print("Published %s" % app_version)
else:
print("Failed to publish %s, status code: %d" % (app_version, resp.status_code))
def cmd_clean():
shutil.rmtree(BUILD_DIR, ignore_errors=True)
def cmd_about():
print(LOGO)
print(DESC)
def main():
parser = argparse.ArgumentParser()
p_cmd = parser.add_subparsers(dest="command")
p_cmd.required = True
# Run
p_run = p_cmd.add_parser("run", help="Run the app")
p_run.add_argument(
"-p", "--pprof", action="store_true", help="Run with pprof enabled"
)
p_run.add_argument(
"-d", "--race", action="store_true", help="Build with data race detection"
)
p_run.add_argument("args", nargs=argparse.REMAINDER)
# Build
p_build = p_cmd.add_parser("build", help="Build the app")
p_build.add_argument(
"-p", "--pprof", action="store_true", help="Build with pprof enabled"
)
p_build.add_argument(
"-r", "--release", action="store_true", help="Build a release version"
)
p_build.add_argument(
"-d", "--race", action="store_true", help="Build with data race detection"
)
# Format
p_cmd.add_parser("format", help="Format the code")
# Mockgen
p_cmd.add_parser("mockgen", help="Generate mock interfaces")
# Protogen
p_cmd.add_parser("protogen", help="Generate protobuf interfaces")
# Tidy
p_cmd.add_parser("tidy", help="Tidy the go modules")
# Test
p_test = p_cmd.add_parser("test", help="Test the code")
p_test.add_argument("module", nargs="?", help="Module to test")
# Publish
p_pub = p_cmd.add_parser("publish", help="Publish the current version")
p_pub.add_argument(
"-u", "--urgent", action="store_true", help="Publish as an urgent update"
)
# Clean
p_cmd.add_parser("clean", help="Clean the build directory")
# About
p_cmd.add_parser("about", help="Print about information")
args = parser.parse_args()
if args.command == "run":
cmd_run(args.args, args.pprof, args.race)
elif args.command == "build":
cmd_build(args.pprof, args.release, args.race)
elif args.command == "format":
cmd_format()
elif args.command == "mockgen":
cmd_mockgen()
elif args.command == "protogen":
cmd_protogen()
elif args.command == "tidy":
cmd_tidy()
elif args.command == "test":
cmd_test(args.module)
elif args.command == "publish":
cmd_publish(args.urgent)
elif args.command == "clean":
cmd_clean()
elif args.command == "about":
cmd_about()
if __name__ == "__main__":
main()
================================================
FILE: platforms.txt
================================================
# This file controls what platform/architecture combinations we build for a release.
# Windows
windows/amd64
windows/amd64-avx
windows/386
windows/arm64
# macOS
darwin/amd64
darwin/amd64-avx
darwin/arm64
# Linux
linux/amd64
linux/amd64-avx
linux/386
linux/arm
linux/armv5
linux/arm64
linux/s390x
linux/mipsle
linux/mipsle-sf
linux/riscv64
linux/loong64
# Android
android/386
android/amd64
android/armv7
android/arm64
# FreeBSD
freebsd/amd64
freebsd/amd64-avx
freebsd/386
freebsd/arm
freebsd/arm64
================================================
FILE: requirements.txt
================================================
blinker==1.8.2
cffi==1.17.0
click==8.1.7
cryptography==43.0.0
Flask==3.0.3
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
pycparser==2.22
PySocks==1.7.1
Werkzeug==3.0.4
================================================
FILE: scripts/_redirects
================================================
/ /install_server.sh 301
================================================
FILE: scripts/install_server.sh
================================================
#!/usr/bin/env bash
#
# install_server.sh - hysteria server install script
# Try `install_server.sh --help` for usage.
#
# SPDX-License-Identifier: MIT
# Copyright (c) 2023 Aperture Internet Laboratory
#
set -e
###
# SCRIPT CONFIGURATION
###
# Basename of this script
SCRIPT_NAME="$(basename "$0")"
# Command line arguments of this script
SCRIPT_ARGS=("$@")
# Path for installing executable
EXECUTABLE_INSTALL_PATH="/usr/local/bin/hysteria"
# Paths to install systemd files
SYSTEMD_SERVICES_DIR="/etc/systemd/system"
# Directory to store hysteria config file
CONFIG_DIR="/etc/hysteria"
# URLs of GitHub
REPO_URL="https://github.com/apernet/hysteria"
# URL of Hysteria 2 API
HY2_API_BASE_URL="https://api.hy2.io/v1"
# curl command line flags.
# To using a proxy, please specify ALL_PROXY in the environ variable, such like:
# export ALL_PROXY=socks5h://192.0.2.1:1080
CURL_FLAGS=(-L -f -q --retry 5 --retry-delay 10 --retry-max-time 60)
###
# AUTO DETECTED GLOBAL VARIABLE
###
# Package manager
PACKAGE_MANAGEMENT_INSTALL="${PACKAGE_MANAGEMENT_INSTALL:-}"
# Operating System of current machine, supported: linux
OPERATING_SYSTEM="${OPERATING_SYSTEM:-}"
# Architecture of current machine, supported: 386, amd64, arm, arm64, mipsle, s390x
ARCHITECTURE="${ARCHITECTURE:-}"
# User for running hysteria
HYSTERIA_USER="${HYSTERIA_USER:-}"
# Directory for ACME certificates storage
HYSTERIA_HOME_DIR="${HYSTERIA_HOME_DIR:-}"
# SELinux context of systemd unit files
SECONTEXT_SYSTEMD_UNIT="${SECONTEXT_SYSTEMD_UNIT:-}"
###
# ARGUMENTS
###
# Supported operation: install, remove, check_update
OPERATION=
# User specified version to install
VERSION=
# Force install even if installed
FORCE=
# User specified binary to install
LOCAL_FILE=
###
# COMMAND REPLACEMENT & UTILITIES
###
has_command() {
local _command=$1
type -P "$_command" > /dev/null 2>&1
}
curl() {
command curl "${CURL_FLAGS[@]}" "$@"
}
mktemp() {
command mktemp "$@" "/tmp/hyservinst.XXXXXXXXXX"
}
tput() {
if has_command tput; then
command tput "$@"
fi
}
tred() {
tput setaf 1
}
tgreen() {
tput setaf 2
}
tyellow() {
tput setaf 3
}
tblue() {
tput setaf 4
}
taoi() {
tput setaf 6
}
tbold() {
tput bold
}
treset() {
tput sgr0
}
note() {
local _msg="$1"
echo -e "$SCRIPT_NAME: $(tbold)note: $_msg$(treset)"
}
warning() {
local _msg="$1"
echo -e "$SCRIPT_NAME: $(tyellow)warning: $_msg$(treset)"
}
error() {
local _msg="$1"
echo -e "$SCRIPT_NAME: $(tred)error: $_msg$(treset)"
}
has_prefix() {
local _s="$1"
local _prefix="$2"
if [[ -z "$_prefix" ]]; then
return 0
fi
if [[ -z "$_s" ]]; then
return 1
fi
[[ "x$_s" != "x${_s#"$_prefix"}" ]]
}
generate_random_password() {
dd if=/dev/random bs=18 count=1 status=none | base64
}
systemctl() {
if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]] || ! has_command systemctl; then
warning "Ignored systemd command: systemctl $@"
return
fi
command systemctl "$@"
}
chcon() {
if ! has_command chcon || [[ "x$FORCE_NO_SELINUX" == "x1" ]]; then
return
fi
command chcon "$@"
}
get_systemd_version() {
if ! has_command systemctl; then
return
fi
command systemctl --version | head -1 | cut -d ' ' -f 2
}
systemd_unit_working_directory() {
local _systemd_version="$(get_systemd_version || true)"
# WorkingDirectory=~ requires systemd v227 or later.
# (released on Oct 2015, only CentOS 7 use an earlier version)
# ref: systemd/systemd@5f5d8eab1f2f5f5e088bc301533b3e4636de96c7
if [[ -n "$_systemd_version" && "$_systemd_version" -lt "227" ]]; then
echo "$HYSTERIA_HOME_DIR"
return
fi
echo "~"
}
get_selinux_context() {
local _file="$1"
local _lsres="$(ls -dZ "$_file" | head -1)"
local _sectx=''
case "$(echo "$_lsres" | wc -w)" in
2)
_sectx="$(echo "$_lsres" | cut -d ' ' -f 1)"
;;
5)
_sectx="$(echo "$_lsres" | cut -d ' ' -f 4)"
;;
*)
;;
esac
if [[ "x$_sectx" == "x?" ]]; then
_sectx=""
fi
echo "$_sectx"
}
show_argument_error_and_exit() {
local _error_msg="$1"
error "$_error_msg"
echo "Try \"$0 --help\" for usage." >&2
exit 22
}
install_content() {
local _install_flags="$1"
local _content="$2"
local _destination="$3"
local _overwrite="$4"
local _tmpfile="$(mktemp)"
echo -ne "Install $_destination ... "
echo "$_content" > "$_tmpfile"
if [[ -z "$_overwrite" && -e "$_destination" ]]; then
echo -e "exists"
elif install "$_install_flags" "$_tmpfile" "$_destination"; then
echo -e "ok"
fi
rm -f "$_tmpfile"
}
remove_file() {
local _target="$1"
echo -ne "Remove $_target ... "
if rm "$_target"; then
echo -e "ok"
fi
}
exec_sudo() {
# exec sudo with configurable environ preserved.
local _saved_ifs="$IFS"
IFS=$'\n'
local _preserved_env=(
$(env | grep "^PACKAGE_MANAGEMENT_INSTALL=" || true)
$(env | grep "^OPERATING_SYSTEM=" || true)
$(env | grep "^ARCHITECTURE=" || true)
$(env | grep "^HYSTERIA_\w*=" || true)
$(env | grep "^SECONTEXT_SYSTEMD_UNIT=" || true)
$(env | grep "^FORCE_\w*=" || true)
)
IFS="$_saved_ifs"
exec sudo env \
"${_preserved_env[@]}" \
"$@"
}
detect_package_manager() {
if [[ -n "$PACKAGE_MANAGEMENT_INSTALL" ]]; then
return 0
fi
if has_command apt; then
apt update
PACKAGE_MANAGEMENT_INSTALL='apt -y --no-install-recommends install'
return 0
fi
if has_command dnf; then
PACKAGE_MANAGEMENT_INSTALL='dnf -y install'
return 0
fi
if has_command yum; then
PACKAGE_MANAGEMENT_INSTALL='yum -y install'
return 0
fi
if has_command zypper; then
PACKAGE_MANAGEMENT_INSTALL='zypper install -y --no-recommends'
return 0
fi
if has_command pacman; then
PACKAGE_MANAGEMENT_INSTALL='pacman -Syu --noconfirm'
return 0
fi
return 1
}
install_software() {
local _package_name="$1"
if ! detect_package_manager; then
error "Supported package manager is not detected, please install the following package manually:"
echo
echo -e "\t* $_package_name"
echo
exit 65
fi
echo "Installing missing dependence '$_package_name' with '$PACKAGE_MANAGEMENT_INSTALL' ... "
if $PACKAGE_MANAGEMENT_INSTALL "$_package_name"; then
echo "ok"
else
error "Cannot install '$_package_name' with detected package manager, please install it manually."
exit 65
fi
}
is_user_exists() {
local _user="$1"
id "$_user" > /dev/null 2>&1
}
rerun_with_sudo() {
if ! has_command sudo; then
return 13
fi
local _target_script
if has_prefix "$0" "/dev/" || has_prefix "$0" "/proc/"; then
local _tmp_script="$(mktemp)"
chmod +x "$_tmp_script"
if has_command curl; then
curl -o "$_tmp_script" 'https://get.hy2.sh/'
elif has_command wget; then
wget -O "$_tmp_script" 'https://get.hy2.sh'
else
return 127
fi
_target_script="$_tmp_script"
else
_target_script="$0"
fi
note "Re-running this script with sudo. You can also specify FORCE_NO_ROOT=1 to force this script to run as the current user."
exec_sudo "$_target_script" "${SCRIPT_ARGS[@]}"
}
check_permission() {
if [[ "$UID" -eq '0' ]]; then
return
fi
note "The user running this script is not root."
case "$FORCE_NO_ROOT" in
'1')
warning "FORCE_NO_ROOT=1 detected, we will proceed without root, but you may get insufficient privileges errors."
;;
*)
if ! rerun_with_sudo; then
error "Please run this script with root or specify FORCE_NO_ROOT=1 to force this script to run as the current user."
exit 13
fi
;;
esac
}
check_environment_operating_system() {
if [[ -n "$OPERATING_SYSTEM" ]]; then
warning "OPERATING_SYSTEM=$OPERATING_SYSTEM detected, operating system detection will not be performed."
return
fi
if [[ "x$(uname)" == "xLinux" ]]; then
OPERATING_SYSTEM=linux
return
fi
error "This script only supports Linux."
note "Specify OPERATING_SYSTEM=[linux|darwin|freebsd|windows] to bypass this check and force this script to run on this $(uname)."
exit 95
}
check_environment_architecture() {
if [[ -n "$ARCHITECTURE" ]]; then
warning "ARCHITECTURE=$ARCHITECTURE detected, architecture detection will not be performed."
return
fi
case "$(uname -m)" in
'i386' | 'i686')
ARCHITECTURE='386'
;;
'amd64' | 'x86_64')
ARCHITECTURE='amd64'
;;
'armv5tel' | 'armv6l' | 'armv7' | 'armv7l')
ARCHITECTURE='arm'
;;
'armv8' | 'aarch64')
ARCHITECTURE='arm64'
;;
'mips' | 'mipsle' | 'mips64' | 'mips64le')
ARCHITECTURE='mipsle'
;;
's390x')
ARCHITECTURE='s390x'
;;
'loongarch64')
ARCHITECTURE='loong64'
;;
*)
error "The architecture '$(uname -a)' is not supported."
note "Specify ARCHITECTURE= to bypass this check and force this script to run on this $(uname -m)."
exit 8
;;
esac
}
check_environment_systemd() {
if [[ -d "/run/systemd/system" ]] || grep -q systemd <(ls -l /sbin/init); then
return
fi
case "$FORCE_NO_SYSTEMD" in
'1')
warning "FORCE_NO_SYSTEMD=1, we will proceed as normal even if systemd is not detected."
;;
'2')
warning "FORCE_NO_SYSTEMD=2, we will proceed but skip all systemd related commands."
;;
*)
error "This script only supports Linux distributions with systemd."
note "Specify FORCE_NO_SYSTEMD=1 to disable this check and force this script to run as if systemd exists."
note "Specify FORCE_NO_SYSTEMD=2 to disable this check and skip all systemd related commands."
;;
esac
}
check_environment_selinux() {
if ! has_command getenforce; then
return
fi
note "SELinux is detected"
if [[ "x$FORCE_NO_SELINUX" == "x1" ]]; then
warning "FORCE_NO_SELINUX=1, we will skip all SELinux related commands."
return
fi
if [[ -z "$SECONTEXT_SYSTEMD_UNIT" ]]; then
if [[ -z "$FORCE_NO_SYSTEMD" ]] && [[ -e "$SYSTEMD_SERVICES_DIR" ]]; then
local _sectx="$(get_selinux_context "$SYSTEMD_SERVICES_DIR")"
if [[ -z "$_sectx" ]]; then
warning "Failed to obtain SEContext of $SYSTEMD_SERVICES_DIR"
else
SECONTEXT_SYSTEMD_UNIT="$_sectx"
fi
fi
fi
}
check_environment_curl() {
if has_command curl; then
return
fi
install_software curl
}
check_environment_grep() {
if has_command grep; then
return
fi
install_software grep
}
check_environment() {
check_environment_operating_system
check_environment_architecture
check_environment_systemd
check_environment_selinux
check_environment_curl
check_environment_grep
}
vercmp_segment() {
local _lhs="$1"
local _rhs="$2"
if [[ "x$_lhs" == "x$_rhs" ]]; then
echo 0
return
fi
if [[ -z "$_lhs" ]]; then
echo -1
return
fi
if [[ -z "$_rhs" ]]; then
echo 1
return
fi
local _lhs_num="${_lhs//[A-Za-z]*/}"
local _rhs_num="${_rhs//[A-Za-z]*/}"
if [[ "x$_lhs_num" == "x$_rhs_num" ]]; then
echo 0
return
fi
if [[ -z "$_lhs_num" ]]; then
echo -1
return
fi
if [[ -z "$_rhs_num" ]]; then
echo 1
return
fi
local _numcmp=$(($_lhs_num - $_rhs_num))
if [[ "$_numcmp" -ne 0 ]]; then
echo "$_numcmp"
return
fi
local _lhs_suffix="${_lhs#"$_lhs_num"}"
local _rhs_suffix="${_rhs#"$_rhs_num"}"
if [[ "x$_lhs_suffix" == "x$_rhs_suffix" ]]; then
echo 0
return
fi
if [[ -z "$_lhs_suffix" ]]; then
echo 1
return
fi
if [[ -z "$_rhs_suffix" ]]; then
echo -1
return
fi
if [[ "$_lhs_suffix" < "$_rhs_suffix" ]]; then
echo -1
return
fi
echo 1
}
vercmp() {
local _lhs=${1#v}
local _rhs=${2#v}
while [[ -n "$_lhs" && -n "$_rhs" ]]; do
local _clhs="${_lhs/.*/}"
local _crhs="${_rhs/.*/}"
local _segcmp="$(vercmp_segment "$_clhs" "$_crhs")"
if [[ "$_segcmp" -ne 0 ]]; then
echo "$_segcmp"
return
fi
_lhs="${_lhs#"$_clhs"}"
_lhs="${_lhs#.}"
_rhs="${_rhs#"$_crhs"}"
_rhs="${_rhs#.}"
done
if [[ "x$_lhs" == "x$_rhs" ]]; then
echo 0
return
fi
if [[ -z "$_lhs" ]]; then
echo -1
return
fi
if [[ -z "$_rhs" ]]; then
echo 1
return
fi
return
}
check_hysteria_user() {
local _default_hysteria_user="$1"
if [[ -n "$HYSTERIA_USER" ]]; then
return
fi
if [[ ! -e "$SYSTEMD_SERVICES_DIR/hysteria-server.service" ]]; then
HYSTERIA_USER="$_default_hysteria_user"
return
fi
HYSTERIA_USER="$(grep -o '^User=\w*' "$SYSTEMD_SERVICES_DIR/hysteria-server.service" | tail -1 | cut -d '=' -f 2 || true)"
if [[ -z "$HYSTERIA_USER" ]]; then
HYSTERIA_USER="$_default_hysteria_user"
fi
}
check_hysteria_homedir() {
local _default_hysteria_homedir="$1"
if [[ -n "$HYSTERIA_HOME_DIR" ]]; then
return
fi
if ! is_user_exists "$HYSTERIA_USER"; then
HYSTERIA_HOME_DIR="$_default_hysteria_homedir"
return
fi
HYSTERIA_HOME_DIR="$(eval echo ~"$HYSTERIA_USER")"
}
###
# ARGUMENTS PARSER
###
show_usage_and_exit() {
echo
echo -e "\t$(tbold)$SCRIPT_NAME$(treset) - hysteria server install script"
echo
echo -e "Usage:"
echo
echo -e "$(tbold)Install hysteria$(treset)"
echo -e "\t$0 [ -f | -l | --version ]"
echo -e "Flags:"
echo -e "\t-f, --force\tForce re-install latest or specified version even if it has been installed."
echo -e "\t-l, --local \tInstall specified hysteria binary instead of download it."
echo -e "\t--version \tInstall specified version instead of the latest."
echo
echo -e "$(tbold)Remove hysteria$(treset)"
echo -e "\t$0 --remove"
echo
echo -e "$(tbold)Check for the update$(treset)"
echo -e "\t$0 -c"
echo -e "\t$0 --check"
echo
echo -e "$(tbold)Show this help$(treset)"
echo -e "\t$0 -h"
echo -e "\t$0 --help"
exit 0
}
parse_arguments() {
while [[ "$#" -gt '0' ]]; do
case "$1" in
'--remove')
if [[ -n "$OPERATION" && "$OPERATION" != 'remove' ]]; then
show_argument_error_and_exit "Option '--remove' is in conflict with other options."
fi
OPERATION='remove'
;;
'--version')
VERSION="$2"
if [[ -z "$VERSION" ]]; then
show_argument_error_and_exit "Please specify the version for option '--version'."
fi
shift
if ! has_prefix "$VERSION" 'v'; then
show_argument_error_and_exit "Version numbers should begin with 'v' (such as 'v2.0.0'), got '$VERSION'"
fi
;;
'-c' | '--check')
if [[ -n "$OPERATION" && "$OPERATION" != 'check' ]]; then
show_argument_error_and_exit "Option '-c' or '--check' is in conflict with other options."
fi
OPERATION='check_update'
;;
'-f' | '--force')
FORCE='1'
;;
'-h' | '--help')
show_usage_and_exit
;;
'-l' | '--local')
LOCAL_FILE="$2"
if [[ -z "$LOCAL_FILE" ]]; then
show_argument_error_and_exit "Please specify the local binary to install for option '-l' or '--local'."
fi
break
;;
*)
show_argument_error_and_exit "Unknown option '$1'"
;;
esac
shift
done
if [[ -z "$OPERATION" ]]; then
OPERATION='install'
fi
# validate arguments
case "$OPERATION" in
'install')
if [[ -n "$VERSION" && -n "$LOCAL_FILE" ]]; then
show_argument_error_and_exit '--version and --local cannot be used together.'
fi
;;
*)
if [[ -n "$VERSION" ]]; then
show_argument_error_and_exit "--version is only valid for install operation."
fi
if [[ -n "$LOCAL_FILE" ]]; then
show_argument_error_and_exit "--local is only valid for install operation."
fi
;;
esac
}
###
# FILE TEMPLATES
###
# /etc/systemd/system/hysteria-server.service
tpl_hysteria_server_service_base() {
local _config_name="$1"
cat << EOF
[Unit]
Description=Hysteria Server Service (${_config_name}.yaml)
After=network.target
[Service]
Type=simple
ExecStart=$EXECUTABLE_INSTALL_PATH server --config ${CONFIG_DIR}/${_config_name}.yaml
WorkingDirectory=$(systemd_unit_working_directory)
User=$HYSTERIA_USER
Group=$HYSTERIA_USER
Environment=HYSTERIA_LOG_LEVEL=info
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
EOF
}
# /etc/systemd/system/hysteria-server.service
tpl_hysteria_server_service() {
tpl_hysteria_server_service_base 'config'
}
# /etc/systemd/system/hysteria-server@.service
tpl_hysteria_server_x_service() {
tpl_hysteria_server_service_base '%i'
}
# /etc/hysteria/config.yaml
tpl_etc_hysteria_config_yaml() {
cat << EOF
# listen: :443
acme:
domains:
- your.domain.net
email: your@email.com
auth:
type: password
password: $(generate_random_password)
masquerade:
type: proxy
proxy:
url: https://news.ycombinator.com/
rewriteHost: true
EOF
}
###
# SYSTEMD
###
get_running_services() {
if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then
return
fi
systemctl list-units --state=active --plain --no-legend \
| grep -o "hysteria-server@*[^\s]*.service" || true
}
restart_running_services() {
if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then
return
fi
echo "Restarting running service ... "
for service in $(get_running_services); do
echo -ne "Restarting $service ... "
systemctl restart "$service"
echo "done"
done
}
stop_running_services() {
if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then
return
fi
echo "Stopping running service ... "
for service in $(get_running_services); do
echo -ne "Stopping $service ... "
systemctl stop "$service"
echo "done"
done
}
###
# HYSTERIA & GITHUB API
###
is_hysteria_installed() {
# RETURN VALUE
# 0: hysteria is installed
# 1: hysteria is not installed
if [[ -f "$EXECUTABLE_INSTALL_PATH" || -h "$EXECUTABLE_INSTALL_PATH" ]]; then
return 0
fi
return 1
}
is_hysteria1_version() {
local _version="$1"
has_prefix "$_version" "v1." || has_prefix "$_version" "v0."
}
get_installed_version() {
if is_hysteria_installed; then
if "$EXECUTABLE_INSTALL_PATH" version > /dev/null 2>&1; then
"$EXECUTABLE_INSTALL_PATH" version | grep '^Version' | grep -o 'v[.0-9]*'
elif "$EXECUTABLE_INSTALL_PATH" -v > /dev/null 2>&1; then
# hysteria 1
"$EXECUTABLE_INSTALL_PATH" -v | cut -d ' ' -f 3
fi
fi
}
get_latest_version() {
if [[ -n "$VERSION" ]]; then
echo "$VERSION"
return
fi
local _tmpfile=$(mktemp)
if ! curl -sS "$HY2_API_BASE_URL/update?cver=installscript&plat=${OPERATING_SYSTEM}&arch=${ARCHITECTURE}&chan=release&side=server" -o "$_tmpfile"; then
error "Failed to get the latest version from Hysteria 2 API, please check your network and try again."
exit 11
fi
local _latest_version=$(grep -oP '"lver":\s*\K"v.*?"' "$_tmpfile" | head -1)
_latest_version=${_latest_version#'"'}
_latest_version=${_latest_version%'"'}
if [[ -n "$_latest_version" ]]; then
echo "$_latest_version"
fi
rm -f "$_tmpfile"
}
download_hysteria() {
local _version="$1"
local _destination="$2"
local _download_url="$REPO_URL/releases/download/app/$_version/hysteria-$OPERATING_SYSTEM-$ARCHITECTURE"
echo "Downloading hysteria binary: $_download_url ..."
if ! curl -R -H 'Cache-Control: no-cache' "$_download_url" -o "$_destination"; then
error "Download failed, please check your network and try again."
return 11
fi
return 0
}
check_update() {
# RETURN VALUE
# 0: update available
# 1: installed version is latest
echo -ne "Checking for installed version ... "
local _installed_version="$(get_installed_version)"
if [[ -n "$_installed_version" ]]; then
echo "$_installed_version"
else
echo "not installed"
fi
echo -ne "Checking for latest version ... "
local _latest_version="$(get_latest_version)"
if [[ -n "$_latest_version" ]]; then
echo "$_latest_version"
VERSION="$_latest_version"
else
echo "failed"
return 1
fi
local _vercmp="$(vercmp "$_installed_version" "$_latest_version")"
if [[ "$_vercmp" -lt 0 ]]; then
return 0
fi
return 1
}
###
# ENTRY
###
perform_install_hysteria_binary() {
if [[ -n "$LOCAL_FILE" ]]; then
note "Performing local install: $LOCAL_FILE"
echo -ne "Installing hysteria executable ... "
if install -Dm755 "$LOCAL_FILE" "$EXECUTABLE_INSTALL_PATH"; then
echo "ok"
else
exit 2
fi
return
fi
local _tmpfile=$(mktemp)
if ! download_hysteria "$VERSION" "$_tmpfile"; then
rm -f "$_tmpfile"
exit 11
fi
echo -ne "Installing hysteria executable ... "
if install -Dm755 "$_tmpfile" "$EXECUTABLE_INSTALL_PATH"; then
echo "ok"
else
exit 13
fi
rm -f "$_tmpfile"
}
perform_remove_hysteria_binary() {
remove_file "$EXECUTABLE_INSTALL_PATH"
}
perform_install_hysteria_example_config() {
install_content -Dm644 "$(tpl_etc_hysteria_config_yaml)" "$CONFIG_DIR/config.yaml" ""
}
perform_install_hysteria_systemd() {
if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then
return
fi
install_content -Dm644 "$(tpl_hysteria_server_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server.service" "1"
install_content -Dm644 "$(tpl_hysteria_server_x_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" "1"
if [[ -n "$SECONTEXT_SYSTEMD_UNIT" ]]; then
chcon "$SECONTEXT_SYSTEMD_UNIT" "$SYSTEMD_SERVICES_DIR/hysteria-server.service"
chcon "$SECONTEXT_SYSTEMD_UNIT" "$SYSTEMD_SERVICES_DIR/hysteria-server@.service"
fi
systemctl daemon-reload
}
perform_remove_hysteria_systemd() {
remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server.service"
remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server@.service"
systemctl daemon-reload
}
perform_install_hysteria_home_legacy() {
if ! is_user_exists "$HYSTERIA_USER"; then
echo -ne "Creating user $HYSTERIA_USER ... "
useradd -r -d "$HYSTERIA_HOME_DIR" -m "$HYSTERIA_USER"
echo "ok"
fi
}
perform_install() {
local _is_frash_install
local _is_upgrade_from_hysteria1
if ! is_hysteria_installed; then
_is_frash_install=1
elif is_hysteria1_version "$(get_installed_version)"; then
_is_upgrade_from_hysteria1=1
fi
local _is_update_required
if [[ -n "$LOCAL_FILE" ]] || [[ -n "$VERSION" ]] || check_update; then
_is_update_required=1
fi
if [[ "x$FORCE" == "x1" ]]; then
if [[ -z "$_is_update_required" ]]; then
note "Option '--force' detected, re-install even if installed version is the latest."
fi
_is_update_required=1
fi
if is_hysteria1_version "$VERSION"; then
error "This script can only install Hysteria 2."
exit 95
fi
if [[ -n "$_is_update_required" ]]; then
perform_install_hysteria_binary
fi
# Always install additional files, regardless of $_is_update_required.
# This allows changes to be made with environment variables (e.g. change HYSTERIA_USER without --force).
perform_install_hysteria_example_config
perform_install_hysteria_home_legacy
perform_install_hysteria_systemd
if [[ -z "$_is_update_required" ]]; then
echo
echo "$(tgreen)Installed version is up-to-date, there is nothing to do.$(treset)"
echo
elif [[ -n "$_is_frash_install" ]]; then
echo
echo -e "$(tbold)Congratulation! Hysteria 2 has been successfully installed on your server.$(treset)"
echo
echo -e "What's next?"
echo
echo -e "\t+ Take a look at the differences between Hysteria 2 and Hysteria 1 at https://hysteria.network/docs/misc/2-vs-1/"
echo -e "\t+ Check out the quick server config guide at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)"
echo -e "\t+ Edit server config file at $(tred)$CONFIG_DIR/config.yaml$(treset)"
echo -e "\t+ Start your hysteria server with $(tred)systemctl start hysteria-server.service$(treset)"
echo -e "\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)"
echo
elif [[ -n "$_is_upgrade_from_hysteria1" ]]; then
echo -e "Skip automatic service restart due to $(tred)incompatible$(treset) upgrade."
echo
echo -e "$(tbold)Hysteria has been successfully update to $VERSION from Hysteria 1.$(treset)"
echo
echo -e "$(tred)Hysteria 2 uses a completely redesigned protocol & config, which is NOT compatible with the version 1.x.x in any way.$(treset)"
echo
echo -e "\t+ Take a look at the behavior changes in Hysteria 2 at $(tblue)https://hysteria.network/docs/misc/2-vs-1/$(treset)"
echo -e "\t+ Check out the quick server configuration guide for Hysteria 2 at $(tblue)https://hysteria.network/docs/getting-started/Server/$(treset)"
echo -e "\t+ Migrate server config file to the Hysteria 2 at $(tred)$CONFIG_DIR/config.yaml$(treset)"
echo -e "\t+ Start your hysteria server with $(tred)systemctl restart hysteria-server.service$(treset)"
echo -e "\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)"
else
restart_running_services
echo
echo -e "$(tbold)Hysteria has been successfully update to $VERSION.$(treset)"
echo
echo -e "Check out the latest changelog $(tblue)https://github.com/apernet/hysteria/blob/master/CHANGELOG.md$(treset)"
echo
fi
}
perform_remove() {
perform_remove_hysteria_binary
stop_running_services
perform_remove_hysteria_systemd
echo
echo -e "$(tbold)Congratulation! Hysteria has been successfully removed from your server.$(treset)"
echo
echo -e "You still need to remove configuration files and ACME certificates manually with the following commands:"
echo
echo -e "\t$(tred)rm -rf "$CONFIG_DIR"$(treset)"
if [[ "x$HYSTERIA_USER" != "xroot" ]]; then
echo -e "\t$(tred)userdel -r "$HYSTERIA_USER"$(treset)"
fi
if [[ "x$FORCE_NO_SYSTEMD" != "x2" ]]; then
echo
echo -e "You still might need to disable all related systemd services with the following commands:"
echo
echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server.service$(treset)"
echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server@*.service$(treset)"
echo -e "\t$(tred)systemctl daemon-reload$(treset)"
fi
echo
}
perform_check_update() {
if check_update; then
echo
echo -e "$(tbold)Update available: $VERSION$(treset)"
echo
echo -e "$(tgreen)You can download and install the latest version by execute this script without any arguments.$(treset)"
echo
else
echo
echo "$(tgreen)Installed version is up-to-date.$(treset)"
echo
fi
}
main() {
parse_arguments "$@"
check_permission
check_environment
check_hysteria_user "hysteria"
check_hysteria_homedir "/var/lib/$HYSTERIA_USER"
case "$OPERATION" in
"install")
perform_install
;;
"remove")
perform_remove
;;
"check_update")
perform_check_update
;;
*)
error "Unknown operation '$OPERATION'."
;;
esac
}
main "$@"
# vim:set ft=bash ts=2 sw=2 sts=2 et: