hysteria 2 prototype first public release

This commit is contained in:
tobyxdd 2023-05-25 20:24:24 -07:00
parent c0ab06e961
commit 9f54aade8f
139 changed files with 5146 additions and 11657 deletions

1
.github/FUNDING.yml vendored
View file

@ -1 +0,0 @@
custom: ['https://hysteria.network/docs/donations/']

View file

@ -1,42 +0,0 @@
name: "[en] Feature Request"
description: "Request to add a new feature, or improvement to an existing feature."
title: "[Feature Request] "
body:
- type: markdown
id: header
attributes:
value: |
Before creating an issue, please take a look at [Advanced Usage](https://hysteria.network/docs/advanced-usage/) & existing issues to make sure it does not exist or has already been proposed.
You can also join our Telegram group or use Discussion to share your ideas with the community.
If you have the skills to implement the features you want, Pull Requests are more than welcomed :)
- type: textarea
id: detail
attributes:
label: "Details"
description: |
Describe what you want to add or change.
validations:
required: true
- type: textarea
id: necessary
attributes:
label: "Value"
description: |
What is the value added?
validations:
required: true
- type: textarea
id: alternative
attributes:
label: "Available alternatives"
description: |
Are there other projects that have implemented this feature that we can refer to?
- type: textarea
id: other-info
attributes:
label: "Additional information"
description: |
Links to any relevant issues, pull requests, or discussions.

View file

@ -1,136 +0,0 @@
name: "[en] Help me!"
description: "Unable to connect? Server/client crashed? Choose this to get help."
title: "[Help me] "
body:
- type: markdown
id: header
attributes:
value: |
Before creating an issue, please take a look at [Quick Start Guide](https://hysteria.network/docs/quick-start/) and [Advanced Usage](https://hysteria.network/docs/advanced-usage/).
You can find solutions to common problems in [Common Problems](https://hysteria.network/docs/common-problems/). Anything already covered there will be closed without reply.
You can [join our Telegram group](https://t.me/hysteria_github) our use Discussion for community support.
Try searching existing issues to see if it has been already answered.
If your problem still can't be solved, fill out the form as detailed as you can to help us reproduce it.
- type: textarea
id: detail
attributes:
label: "Details"
description: |
Describe the problem you encountered in detail.
If you are using hysteria in an unusual way, describe your setup and what you are trying to achieve.
validations:
required: true
- type: input
id: server-install-info
attributes:
label: "Hysteria server information"
description: |
Paste the version of hysteria server here (output of `hysteria --version`).
If you used a script to install and config hysteria on your server, please paste the command that you executed here (such as `curl https://xxx | sh -`)
If you are using a VPN provider, please ask the VPN provider for help first.
placeholder: |
hysteria version v1.x.x 2006-01-02t08:04:05z 0123456789abcdef0123456789abcdef01234567
validations:
required: true
- type: textarea
id: server-provider-info
attributes:
label: "VPS information"
description: |
Fill in the provider and specs of the VPS you are using to run the hysteria server here.
If you are using a VPN provider, please fill in the website of the VPN provider.
placeholder: |
TurtleShell, Chuncheon, ARM, 1 Core, 512MB RAM
validations:
required: true
- type: textarea
id: server-config-info
attributes:
label: "Server config"
description: |
Paste the server config.json you are using here.
If you are using a script that doesn't require any configuration, please specify `N/A`.
placeholder: |
{
"listen": ":36712",
"acme": {
"domains": [
"your.domain.com"
],
"email": "hacker@gmail.com"
},
"obfs": "fuck me till the daylight",
"up_mbps": 100,
"down_mbps": 100
}
validations:
required: true
- type: textarea
id: server-log
attributes:
label: "Server logs"
description: |
Paste the hysteria server output here. Screenshots are acceptable but plaintext would be much better.
validations:
required: true
- type: input
id: client-install-info
attributes:
label: "Hysteria client information"
description: |
Paste the version of hysteria client here (output of `hysteria --version`).
If you are using any third-party clients (e.g. Clash, Passwall, or SagerNet), paste their version instead. You can also find help in their communities.
placeholder: |
hysteria version v1.x.x 2006-01-02T08:04:05Z 0123456789abcdef0123456789abcdef01234567
validations:
required: true
- type: textarea
id: client-config-info
attributes:
label: "Client config"
description: |
Paste the client config.json you are using here.
Make sure to remove sensitive information (e.g. server address, password).
If you are using a third-party client, you can paste or upload a screenshot of their configuration instead.
placeholder: |
{
"server": "example.com:36712",
"obfs": "fuck me till the daylight",
"up_mbps": 10,
"down_mbps": 50,
"socks5": {
"listen": "127.0.0.1:1080"
},
"http": {
"listen": "127.0.0.1:8080"
}
}
validations:
required: true
- type: input
id: client-environment
attributes:
label: "Client environment (operating system)"
description: |
The OS you are using to run hysteria client.
If you are running hysteria client on OpenWRT, provide the version of OpenWRT (and any plugins you are using, e.g. Passwall).
placeholder: |
Windows 11
validations:
required: true
- type: textarea
id: client-log
attributes:
label: "Client logs"
description: |
Paste the hysteria client output here. Screenshots are acceptable but plaintext would be much better.
validations:
required: true

View file

@ -1,43 +0,0 @@
name: "[zh] 功能请求"
description: "希望 Hysteria 添加新功能?或者希望 Hysteria 作出什么改变? 请选这个。"
title: "[功能请求] "
body:
- type: markdown
id: header
attributes:
value: |
在创建 Issue 之前, 请花几分钟阅读一下我们 Wiki 上的 [高级用法](https://hysteria.network/zh/docs/advanced-usage/)。 确认你想要的功能是否已经被实现。
如果你有什么好的想法, 欢迎 [加入 Hysteria 的 Telegram 群组](https://t.me/hysteria_github) 参与功能上的讨论。
也请搜索一下已有 Issue, 检查一下你所需的功能有没有人曾经提出过。
如果你有能力实现这个功能, 欢迎为 Hysteria 提交 Pull Request。
- type: textarea
id: detail
attributes:
label: "功能描述"
description: |
请描述你希望 Hysteria 增加的功能或者希望 Hysteria 能作出的变更。
validations:
required: true
- type: textarea
id: necessary
attributes:
label: "这个功能的必要性"
description: |
为什么这个功能对 Hysteria 来说是必须的? 或者为什么你认为这个功能需要内置在 Hysteria 中?
validations:
required: true
- type: textarea
id: alternative
attributes:
label: "当前可用的替代方案"
description: |
在当前没有这个功能的前提下, 你使用什么方案来达到类似的效果?
- type: textarea
id: other-info
attributes:
label: "补充"
description: |
如果有任何涉及到这个功能请求的 Issue、 Pull Request、 博客文章等, 请把链接贴在下面。

View file

@ -1,137 +0,0 @@
name: "[zh] 请求帮助"
description: "不会用?连不上?请选这个。"
title: "[请求帮助] "
body:
- type: markdown
id: header
attributes:
value: |
在创建 Issue 之前, 请花几分钟阅读一下我们 Wiki 上的 [配置指南](https://hysteria.network/zh/docs/quick-start/),
最新的配置参数在 [高级用法](https://hysteria.network/zh/docs/advanced-usage/) 里有详细的说明。
您可能遇到的绝大部分问题都能在 [常见问题](https://hysteria.network/zh/docs/common-problems/) 中找到解决方案。
任何已有解决方案的 Issue 将会被直接关闭, 感谢理解。
请考虑 [加入 Hysteria 的 Telegram 群组](https://t.me/hysteria_github) 来寻求即时的社区帮助。
也请搜索一下已有 Issue, 看看是否能找到现成的解决方案。
请尽可能详细地填写下面这个表单来帮助我们检查并复现您遇到的问题, 请记住我们不会预测魔法, 只有复现了您遇到的问题, 我们才知道该如何帮助你解决它。
- type: textarea
id: detail
attributes:
label: "问题详情"
description: |
请描述你遇到的问题。
如果你的需求和通常的用法有所不同, 也请在这里说明。
validations:
required: true
- type: input
id: server-install-info
attributes:
label: "服务端安装信息或者一键脚本信息"
description: |
请填写你使用的服务端版本(在 VPS 上执行 `hysteria --version`, 把输出贴在这里)。
如果你使用一键脚本, 请把一键脚本让你复制和执行的命令贴在这里。
如果你使用机场, 请优先联系机场售后以获取使用帮助。
placeholder: |
hysteria version v1.x.x 2006-01-02t08:04:05z 0123456789abcdef0123456789abcdef01234567
validations:
required: true
- type: textarea
id: server-provider-info
attributes:
label: "VPS 信息"
description: |
请填写你搭建服务端所使用的 VPS 服务商以及 VPS 配置。
如果你使用机场, 请填写机场网址。
placeholder: |
TurtleShell 春川机房 ARM 单核 512MB内存
validations:
required: true
- type: textarea
id: server-config-info
attributes:
label: "服务端配置"
description: |
请把你的服务端配置 JSON 粘贴在这里。
如果你使用的是一键脚本并且不需要任何配置, 请填写「无」。
placeholder: |
{
"listen": ":36712",
"acme": {
"domains": [
"your.domain.com"
],
"email": "hacker@gmail.com"
},
"obfs": "fuck me till the daylight",
"up_mbps": 100,
"down_mbps": 100
}
validations:
required: true
- type: textarea
id: server-log
attributes:
label: "服务端日志"
description: |
请把你的服务端日志贴在这里, 可以是截图但是请尽可能提供纯文本。
validations:
required: true
- type: input
id: client-install-info
attributes:
label: "客户端安装信息"
description: |
请填写你使用的客户端版本(在客户端执行 `hysteria --version`, 并把输出贴在这里)。
如果你使用第三方客户端(包括但不限于 Clash、 Passwall、 SagerNet), 请贴上它们的版本, 或者到这些第三方客户端的社群寻求帮助。
placeholder: |
hysteria version v1.x.x 2006-01-02T08:04:05Z 0123456789abcdef0123456789abcdef01234567
validations:
required: true
- type: textarea
id: client-config-info
attributes:
label: "客户端配置"
description: |
请把你的客户端配置 JSON 粘贴在这里。
你可以移除客户端配置里的敏感信息(像服务器地址、 混淆密码、 认证密码), 但是这也意味着你必须自己检查这些配置是否填写正确。
如果你使用第三方客户端, 你可以贴上第三方客户端的配置或者配置截图。
placeholder: |
{
"server": "example.com:36712",
"obfs": "fuck me till the daylight",
"up_mbps": 10,
"down_mbps": 50,
"socks5": {
"listen": "127.0.0.1:1080"
},
"http": {
"listen": "127.0.0.1:8080"
}
}
validations:
required: true
- type: input
id: client-environment
attributes:
label: "客户端运行环境(操作系统)"
description: |
请填写客户端使用的操作系统的名称和版本。
如果你在 OpenWRT 上运行 hysteria 客户端, 请填写 OpenWRT (以及你使用的插件, 如 Passwall)的版本。
placeholder: |
Windows 11
validations:
required: true
- type: textarea
id: client-log
attributes:
label: "客户端日志"
description: |
请把你的客户端日志贴在这里, 可以是截图但是请尽可能提供纯文本。
validations:
required: true

View file

@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View file

@ -1,60 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '17 14 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild
# uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
- name: Run build script
run: ./build.sh
shell: bash
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View file

@ -1,40 +0,0 @@
name: "Build master"
on:
push:
branches:
- 'master'
tags-ignore:
- 'v*'
- 'core/v*'
- 'app/v*'
jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
- name: Run build script
env:
HY_APP_PLATFORMS: 'darwin/amd64,darwin/amd64-avx,darwin/arm64,windows/amd64,windows/amd64-avx,windows/386,windows/arm64,linux/amd64,linux/amd64-avx,linux/386,linux/arm,linux/armv5,linux/arm64,linux/s390x,linux/mipsle,linux/mipsle-sf,freebsd/amd64,freebsd/amd64-avx,freebsd/386,freebsd/arm,freebsd/arm64'
run: ./build.sh
shell: bash
- name: Archive
uses: actions/upload-artifact@v3
with:
name: hysteria-binaries-${{ github.sha }}
path: ./build

View file

@ -1,44 +0,0 @@
name: Build Docker Image
on:
push:
tags:
- 'v*'
jobs:
docker:
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v3
- name: Get tag
id: get_tag
run: echo "TAG=$(git describe --tags --always --match 'v*')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v4.0.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:${{ steps.get_tag.outputs.TAG }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,65 +0,0 @@
name: Build and release
on:
push:
tags:
- 'v*'
jobs:
build:
name: Build and release
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
steps:
- name: Check out
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
- name: Run build script
env:
HY_APP_PLATFORMS: 'darwin/amd64,darwin/amd64-avx,darwin/arm64,windows/amd64,windows/amd64-avx,windows/386,windows/arm64,linux/amd64,linux/amd64-avx,linux/386,linux/arm,linux/armv5,linux/arm64,linux/s390x,linux/mipsle,linux/mipsle-sf,freebsd/amd64,freebsd/amd64-avx,freebsd/386,freebsd/arm,freebsd/arm64'
run: ./build.sh
shell: bash
- name: Generate hashes
run: |
cd build
for f in $(find . -type f); do
sha256sum $f | sudo tee -a hashes.txt
done
- name: Upload
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
./build/hysteria-darwin-amd64
./build/hysteria-darwin-amd64-avx
./build/hysteria-darwin-arm64
./build/hysteria-windows-amd64.exe
./build/hysteria-windows-amd64-avx.exe
./build/hysteria-windows-386.exe
./build/hysteria-windows-arm64.exe
./build/hysteria-linux-amd64
./build/hysteria-linux-amd64-avx
./build/hysteria-linux-386
./build/hysteria-linux-arm
./build/hysteria-linux-armv5
./build/hysteria-linux-arm64
./build/hysteria-linux-s390x
./build/hysteria-linux-mipsle
./build/hysteria-linux-mipsle-sf
./build/hysteria-freebsd-amd64
./build/hysteria-freebsd-amd64-avx
./build/hysteria-freebsd-386
./build/hysteria-freebsd-arm
./build/hysteria-freebsd-arm64
./build/hashes.txt

113
.gitignore vendored
View file

@ -1,7 +1,10 @@
# Created by https://www.gitignore.io/api/go,linux,macos,windows,intellij+all
# Edit at https://www.gitignore.io/?templates=go,linux,macos,windows,intellij+all
# Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos
# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos
### 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~
@ -18,12 +21,11 @@
# Dependency directories (remove the comment below to include it)
# vendor/
### Go Patch ###
/vendor/
/Godeps/
# Go workspace file
go.work
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
### 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
@ -33,6 +35,9 @@
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
@ -53,6 +58,9 @@
# 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
@ -80,6 +88,9 @@ 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
@ -92,21 +103,69 @@ fabric.properties
# 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 ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Linux ###
*~
@ -132,6 +191,7 @@ modules.xml
# Icon must end with two \r
Icon
# Thumbnails
._*
@ -151,6 +211,10 @@ Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Windows ###
# Windows thumbnail cache files
Thumbs.db
@ -177,13 +241,4 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
# End of https://www.gitignore.io/api/go,linux,macos,windows,intellij+all
cmd/relay/*.json
hy_linux
.vscode
/build/
/dist/
config*.json
# End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos

View file

@ -1,148 +0,0 @@
# Changelog
## 1.3.4
- Eliminate unnecessary DNS lookups when using SOCKS5 outbound with ACL disabled
- Add a `lazy_start` option to let the client connect to the server only when there is an incoming connection
- Fix a bug where TCP redirect didn't work on x86 (32-bit) machines
- Fix memory leak when using UDP port hopping
- Updated quic-go to v0.33.0
## 1.3.3
- Fix a bug that made UDP unusable when using `socks5_outbound`
- Set the default value of `retry_interval` to 1 to prevent the client from retrying too often when errors occur
- Prompt error if both acme and local cert file are specified in client config
- Updated quic-go to v0.32.0, performance improvements
## 1.3.2
- Fix a bug where some malformed UDP packets would cause the server to crash
- Fix a bug where the server did not have a timeout for SOCKS5 outbound connections
- Add build variants: amd64-avx, armv5, mipsle-sf, windows/arm64
## 1.3.1
- New `fast_open` option for client to reduce RTT when dialing TCP connections
- Fix a bug where the HTTP proxy would not close connections properly
- Minor performance improvements here and there
## 1.3.0
- Connection migration: clients can now seamlessly switch between networks without losing their connection to the server
- Dynamic port hopping: see https://hysteria.network/docs/port-hopping/ for more information
## 1.2.2
- Fix a bug where the client would crash for IPv6 UDP requests in TProxy mode.
- Fix a bug where the client did not release old UDP sockets when reconnecting.
- Fix a bug where using DoT (DNS over TLS) as resolver would cause the client/server to crash.
- Add `quit_on_disconnect`, `handshake_timeout`, `idle_timeout` options to client config.
- Drop server's legacy protocol (v2) support.
- Updated quic-go to v0.30.0, small performance improvements.
## 1.2.1
- Fix a bug that caused DNS failure when using domain names in the "resolver" option
- Fix a bug where errors in HTTP proxy mode were not logged
- Fix a bug where WeChat protocol was not working properly when obfuscation was not enabled
- New TCP buffer options for tun mode (`tcp_sndbuf`, `tcp_rcvbuf`, `tcp_autotuning`)
## 1.2.0
- Reworked TUN mode
- DoT/DoH/DoQ support for resolver
- IP masking (anonymization)
- FreeBSD builds
## 1.1.0
- Super major CPU performance improvements (~30% to several times faster, depending on the circumstances) by optimizing several data structures in quic-go (changes upstreamed)
## 1.0.5
- `bind_outbound` server option for binding outbound connections to a specific address or interface
- TCP Redirect mode (for Linux)
## 1.0.4
- ~10% CPU usage reduction
- Improve performance when packet loss is high
- New ACL syntax to support protocol/port
## 1.0.3
- New string-based speed (up/down) options
- Server SOCKS5 outbound domain pass-through
- Linux s390x build
- Updated quic-go to v0.27.0
## 1.0.2
- Added an option for DNS resolution preference `resolve_preference`
## 1.0.1
- Fix server SOCKS5 outbound bug
- Fix incorrect UDP fragmentation handling
## 1.0.0
- Protocol v3: UDP fragmentation support
- Fix SOCKS5 UDP timeout issue
- SOCKS5 outbound support
## 0.9.7
- CLI improvements (cobra)
- Fix broken UDP TProxy mode
- Re-enable PMTUD on Windows & Linux
## 0.9.6
- Disable quic-go PMTUD due to broken implementation
- Fix zero initMaxDatagramSize in brutal CC
- Client retry
## 0.9.5
- Client connect & disconnect log
- Warning when no auth or obfs is set
- Multi-password & cmd auth support
## 0.9.4
- fsnotify-based auto keypair reloading
- ACL country code support
## 0.9.3
- CC optimizations
- Set buffer correctly for faketcp mode
- "wechat-video" protocol
## 0.9.2
- Updated quic-go to v0.24.0
- Reduced obfs overhead by reusing buffers
## 0.9.1
- faketcp implementation
- DNS `resolver` option in config
## 0.9.0
- Auto keypair reloading
- SOCKS5 listen address no longer needs a specific IP
- Multi-relay support
- IPv6 only mode for server
## 0.8.6
- Added an option for customizing ALPN `alpn`
- Removed ACL support from TPROXY & TUN modes
## 0.8.5
- Added an option to disable MTU discovery `disable_mtu_discovery`

View file

@ -1,43 +0,0 @@
FROM golang:1-alpine AS builder
LABEL maintainer="mritd <mritd@linux.com>"
# 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 \
&& ./build.sh \
&& mv ./build/hysteria-* /go/bin/hysteria
# multi-stage builds to create the final image
FROM alpine AS dist
LABEL maintainer="mritd <mritd@linux.com>"
# 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"]

View file

@ -1,30 +0,0 @@
License
==================
Hysteria itself, including all codes under this directory, is licensed under the MIT License.
```
The MIT License (MIT)
Copyright (c) 2021 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.
```
However, when building with `-tags gpl`, the produced executable shall be distributed under GPLv3.

100
README.md
View file

@ -1,87 +1,37 @@
# ![Logo](docs/logos/readme.png)
# Hysteria 2 Prototype
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8]
> **Warning**
> The code on this branch is a work-in-progress prototype of what will become Hysteria 2.0. It is currently very unfinished, and unless you know what you are doing, you should stick with the stable 1.x releases for now. **The protocol is also subject to change, so we do not recommend third-party developers use this as a reference for the Hysteria 2 protocol at this time.**
[1]: https://img.shields.io/badge/license-MIT-blue
> **警告**
> 此分支的代码是 Hysteria 2.0 的原型,目前仍在开发中,完成度十分有限。除非你十分确定自己在做什么,否则请继续使用稳定的 1.x 版本。**协议也可能会发生变化,因此我们不建议第三方开发者在目前使用此分支作为 Hysteria 2 协议的参考。**
[2]: LICENSE.md
## Build (编译)
[3]: https://img.shields.io/github/v/release/apernet/hysteria?style=flat-square
```bash
go build ./app
```
[4]: https://github.com/apernet/hysteria/releases
## Usage (使用)
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
### Server
```bash
./app server -c config.yaml
```
[6]: https://t.me/hysteria_github
[Example sever config (示例服务器配置)](app/server.example.yaml)
[7]: https://img.shields.io/github/discussions/apernet/hysteria?style=flat-square
### Client
```bash
./app client -c config.yaml
```
[8]: https://github.com/apernet/hysteria/discussions
[Example client config (示例客户端配置)](app/client.example.yaml)
![AperNet](docs/logos/AperNetLogo.png)
## Test HTTP/3 masquerading (测试 HTTP/3 伪装)
An [Aperture Internet Laboratory](https://apernet.io/) project
```bash
chrome --origin-to-force-quic-on=example.com:443
```
----------
Hysteria is a feature-packed proxy & relay tool optimized for lossy, unstable connections (e.g. satellite networks,
congested public Wi-Fi, connecting to foreign servers from China) powered by a customized protocol based on QUIC.
## Use cases
- Censorship circumvention
- Boosting slow connections
- Bypassing commercial/academic/corporate firewalls
- Bypassing ISP throttling
- ...
## Modes
- SOCKS5 proxy (TCP & UDP)
- HTTP/HTTPS proxy
- TCP/UDP relay
- TCP/UDP TPROXY (Linux)
- TCP REDIRECT (Linux)
- TUN (TAP on Windows)
- Still growing...
## **[Documentation](https://hysteria.network/)**
----------
Hysteria 是一个功能丰富的,专为恶劣网络环境(如卫星网络、拥挤的公共 Wi-Fi、从中国连接境外服务器等)进行优化的双边加速工具,基于修改版的 QUIC 协议。
## 常见用例
- 绕过网络审查
- 提升传输速度
- 绕过商业/学校/企业防火墙
- 绕过运营商 QoS 限速
## 模式
- SOCKS5 代理 (TCP & UDP)
- HTTP/HTTPS 代理
- TCP/UDP 转发
- TCP/UDP TPROXY 透明代理 (Linux)
- TCP REDIRECT 透明代理 (Linux)
- TUN (Windows 下为 TAP)
- 仍在增加中...
## **[中文文档](https://hysteria.network/zh/)**
----------
## Benchmarks
![Bench](docs/bench/bench.png)
----------
**Donations are greatly appreciated!** Contact me if you would like your name listed as a sponsor.
**欢迎大佬捐赠!** 如希望挂名请在捐赠后联系我。
<a href="https://nowpayments.io/donation?api_key=EJH83FM-FDC40ZW-QGDZRR4-A7SC67S" target="_blank">
<img src="https://nowpayments.io/images/embeds/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
</a>
Then visit `https://example.com:443` in Chrome.

View file

@ -1,362 +0,0 @@
##############################################################################
# #
# go-task: https://taskfile.dev/installation/ #
# #
# For the role of 'amd64-v*', please refer to #
# https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels. #
# #
##############################################################################
version: '3'
vars:
BUILD_VERSION:
sh: git describe --tags --always --match 'v*'
BUILD_COMMIT:
sh: git rev-parse HEAD
BUILD_DATE:
sh: date -u '+%F %T'
tasks:
clean:
cmds:
- rm -rf dist
- mkdir -p dist
hash:
dir: ./dist
cmds:
- sha256sum hysteria-* > hashes.txt
build-hysteria:
label: build-{{.TASK}}
dir: ./app/cmd
cmds:
- |
GOOS={{.GOOS}} GOARCH={{.GOARCH}} GOARM={{.GOARM}} GOAMD64={{.GOAMD64}} GOMIPS={{.GOMIPS}} \
go build -trimpath -o ../../dist/hysteria-{{.TASK}}{{.BINEXT}} -ldflags \
"-w -s -X 'main.appVersion={{.BUILD_VERSION}}' -X 'main.appCommit={{.BUILD_COMMIT}}' -X 'main.appDate={{.BUILD_DATE}}'"
linux-386:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: 386
}
linux-amd64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: amd64
}
linux-amd64-v2:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: amd64,
GOAMD64: v2
}
linux-amd64-v3:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: amd64,
GOAMD64: v3
}
linux-amd64-v4:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: amd64,
GOAMD64: v4
}
linux-armv5:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: arm,
GOARM: 5
}
linux-armv6:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: arm,
GOARM: 6
}
linux-armv7:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: arm,
GOARM: 7
}
linux-armv8:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: arm64
}
linux-s390x:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: s390x
}
linux-mips-hardfloat:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: mips,
GOMIPS: hardfloat
}
linux-mipsle-softfloat:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: mipsle,
GOMIPS: softfloat
}
linux-mipsle-hardfloat:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: mipsle,
GOMIPS: hardfloat
}
linux-mips64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: mips64
}
linux-mips64le:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: linux,
GOARCH: mips64le
}
darwin-amd64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: darwin,
GOARCH: amd64
}
darwin-amd64-v2:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: darwin,
GOARCH: amd64,
GOAMD64: v2
}
darwin-amd64-v3:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: darwin,
GOARCH: amd64,
GOAMD64: v3
}
darwin-amd64-v4:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: darwin,
GOARCH: amd64,
GOAMD64: v4
}
darwin-arm64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: darwin,
GOARCH: arm64
}
freebsd-386:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: freebsd,
GOARCH: 386
}
freebsd-amd64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: freebsd,
GOARCH: amd64
}
freebsd-amd64-v2:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: freebsd,
GOARCH: amd64,
GOAMD64: v2
}
freebsd-amd64-v3:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: freebsd,
GOARCH: amd64,
GOAMD64: v3
}
freebsd-amd64-v4:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: freebsd,
GOARCH: amd64,
GOAMD64: v4
}
freebsd-arm:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: freebsd,
GOARCH: arm
}
freebsd-arm64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
GOOS: freebsd,
GOARCH: arm64
}
windows-386:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
BINEXT: ".exe",
GOOS: windows,
GOARCH: 386
}
windows-amd64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
BINEXT: ".exe",
GOOS: windows,
GOARCH: amd64
}
windows-amd64-v2:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
BINEXT: ".exe",
GOOS: windows,
GOARCH: amd64,
GOAMD64: v2
}
windows-amd64-v3:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
BINEXT: ".exe",
GOOS: windows,
GOARCH: amd64,
GOAMD64: v3
}
windows-amd64-v4:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
BINEXT: ".exe",
GOOS: windows,
GOARCH: amd64,
GOAMD64: v4
}
windows-arm64:
cmds:
- task: build-hysteria
vars: {
TASK: "{{.TASK}}",
BINEXT: ".exe",
GOOS: windows,
GOARCH: arm64
}
default:
cmds:
- task: clean
- task: linux-386
- task: linux-amd64
- task: linux-amd64-v2
- task: linux-amd64-v3
- task: linux-amd64-v4
- task: linux-armv5
- task: linux-armv6
- task: linux-armv7
- task: linux-armv8
- task: linux-s390x
- task: linux-mips-hardfloat
- task: linux-mipsle-softfloat
- task: linux-mipsle-hardfloat
- task: linux-mips64
- task: linux-mips64le
- task: darwin-amd64
- task: darwin-amd64-v2
- task: darwin-amd64-v3
- task: darwin-amd64-v4
- task: darwin-arm64
- task: freebsd-386
- task: freebsd-amd64
- task: freebsd-amd64-v2
- task: freebsd-amd64-v3
- task: freebsd-amd64-v4
- task: freebsd-arm
- task: freebsd-arm64
- task: windows-386
- task: windows-amd64
- task: windows-amd64-v2
- task: windows-amd64-v3
- task: windows-amd64-v4
- task: windows-arm64
- task: hash

View file

@ -1,97 +0,0 @@
package auth
import (
"bytes"
"encoding/json"
"io/ioutil"
"net"
"net/http"
"os/exec"
"strconv"
"strings"
"github.com/sirupsen/logrus"
)
type CmdAuthProvider struct {
Cmd string
}
func (p *CmdAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
cmd := exec.Command(p.Cmd, addr.String(), string(auth), strconv.Itoa(int(sSend)), strconv.Itoa(int(sRecv)))
out, err := cmd.Output()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
return false, strings.TrimSpace(string(out))
} else {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to execute auth command")
return false, "internal error"
}
} else {
return true, strings.TrimSpace(string(out))
}
}
type HTTPAuthProvider struct {
Client *http.Client
URL string
}
type authReq struct {
Addr string `json:"addr"`
Payload []byte `json:"payload"`
Send uint64 `json:"send"`
Recv uint64 `json:"recv"`
}
type authResp struct {
OK bool `json:"ok"`
Msg string `json:"msg"`
}
func (p *HTTPAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
jbs, err := json.Marshal(&authReq{
Addr: addr.String(),
Payload: auth,
Send: sSend,
Recv: sRecv,
})
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to marshal auth request")
return false, "internal error"
}
resp, err := p.Client.Post(p.URL, "application/json", bytes.NewBuffer(jbs))
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to send auth request")
return false, "internal error"
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logrus.WithFields(logrus.Fields{
"code": resp.StatusCode,
}).Error("Invalid status code from auth server")
return false, "internal error"
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to read auth response")
return false, "internal error"
}
var ar authResp
err = json.Unmarshal(data, &ar)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to unmarshal auth response")
return false, "internal error"
}
return ar.OK, ar.Msg
}

View file

@ -1,59 +0,0 @@
package auth
import (
"errors"
"net"
"net/http"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/yosuke-furukawa/json5/encoding/json5"
)
func PasswordAuthFunc(rawMsg json5.RawMessage) (cs.ConnectFunc, error) {
var pwds []string
err := json5.Unmarshal(rawMsg, &pwds)
if err != nil {
// not a string list, legacy format?
var pwdConfig map[string]string
err = json5.Unmarshal(rawMsg, &pwdConfig)
if err != nil || len(pwdConfig["password"]) == 0 {
// still no, invalid config
return nil, errors.New("invalid config")
}
// yes it is
pwds = []string{pwdConfig["password"]}
}
return func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
for _, pwd := range pwds {
if string(auth) == pwd {
return true, "Welcome"
}
}
return false, "Wrong password"
}, nil
}
func ExternalAuthFunc(rawMsg json5.RawMessage) (cs.ConnectFunc, error) {
var extConfig map[string]string
err := json5.Unmarshal(rawMsg, &extConfig)
if err != nil {
return nil, errors.New("invalid config")
}
if len(extConfig["http"]) != 0 {
hp := &HTTPAuthProvider{
Client: &http.Client{
Timeout: 10 * time.Second,
},
URL: extConfig["http"],
}
return hp.Auth, nil
} else if len(extConfig["cmd"]) != 0 {
cp := &CmdAuthProvider{
Cmd: extConfig["cmd"],
}
return cp.Auth, nil
} else {
return nil, errors.New("invalid config")
}
}

29
app/client.example.yaml Normal file
View file

@ -0,0 +1,29 @@
server: example.com
# sni: other.example.com
auth: "hello world"
# tls:
# insecure: false
# ca: "custom.ca"
# quic:
# initStreamReceiveWindow: 8388608
# maxStreamReceiveWindow: 8388608
# initConnReceiveWindow: 20971520
# maxConnReceiveWindow: 20971520
# maxIdleTimeout: 30s
# keepAlivePeriod: 10s
# disablePathMTUDiscovery: false
bandwidth:
up: "100 mbps"
down: "100 mbps"
# fastOpen: true
socks5:
listen: 127.0.0.1:1080
# username: "user"
# password: "haha233"
# disableUDP: true

View file

@ -1,74 +0,0 @@
package main
import (
"context"
"crypto/tls"
"os"
"path/filepath"
"runtime"
"go.uber.org/zap"
"github.com/caddyserver/certmagic"
)
func acmeTLSConfig(domains []string, email string, disableHTTP bool, disableTLSALPN bool,
altHTTPPort int, altTLSALPNPort int,
) (*tls.Config, error) {
cfg := &certmagic.Config{
RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio,
KeySource: certmagic.DefaultKeyGenerator,
Storage: &certmagic.FileStorage{Path: dataDir()},
Logger: zap.NewNop(),
}
issuer := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{
CA: certmagic.LetsEncryptProductionCA,
TestCA: certmagic.LetsEncryptStagingCA,
Email: email,
Agreed: true,
DisableHTTPChallenge: disableHTTP,
DisableTLSALPNChallenge: disableTLSALPN,
AltHTTPPort: altHTTPPort,
AltTLSALPNPort: altTLSALPNPort,
Logger: zap.NewNop(),
})
cfg.Issuers = []certmagic.Issuer{issuer}
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return cfg, nil
},
Logger: zap.NewNop(),
})
cfg = certmagic.New(cache, *cfg)
err := cfg.ManageSync(context.Background(), domains)
if err != nil {
return nil, err
}
return cfg.TLSConfig(), nil
}
func homeDir() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" {
home = "."
}
return home
}
func dataDir() string {
baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
baseDir = xdgData
}
return filepath.Join(baseDir, "certmagic")
}

View file

@ -1,462 +1,216 @@
package main
package cmd
import (
"crypto/tls"
"crypto/x509"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"time"
"sync"
hyHTTP "github.com/apernet/hysteria/app/http"
"github.com/apernet/hysteria/app/redirect"
"github.com/apernet/hysteria/app/relay"
"github.com/apernet/hysteria/app/socks5"
"github.com/apernet/hysteria/app/tproxy"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/pktconns"
"github.com/apernet/hysteria/core/pmtud"
"github.com/oschwald/geoip2-golang"
"github.com/yosuke-furukawa/json5/encoding/json5"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/transport"
"github.com/quic-go/quic-go"
"github.com/sirupsen/logrus"
"github.com/apernet/hysteria/app/internal/socks5"
"github.com/apernet/hysteria/core/client"
)
var clientPacketConnFuncFactoryMap = map[string]pktconns.ClientPacketConnFuncFactory{
"": pktconns.NewClientUDPConnFunc,
"udp": pktconns.NewClientUDPConnFunc,
"wechat": pktconns.NewClientWeChatConnFunc,
"wechat-video": pktconns.NewClientWeChatConnFunc,
"faketcp": pktconns.NewClientFakeTCPConnFunc,
var clientCmd = &cobra.Command{
Use: "client",
Short: "Client mode",
Run: runClient,
}
func client(config *clientConfig) {
logrus.WithField("config", config.String()).Info("Client configuration loaded")
config.Fill() // Fill default values
// Resolver
if len(config.Resolver) > 0 {
err := setResolver(config.Resolver)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to set resolver")
var modeMap = map[string]func(*viper.Viper, client.Client) error{
"socks5": clientSOCKS5,
}
func init() {
rootCmd.AddCommand(clientCmd)
}
func runClient(cmd *cobra.Command, args []string) {
logger.Info("client mode")
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
config, err := viperToClientConfig()
if err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
c, err := client.NewClient(config)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
var wg sync.WaitGroup
hasMode := false
for mode, f := range modeMap {
v := viper.Sub(mode)
if v != nil {
hasMode = true
wg.Add(1)
go func() {
defer wg.Done()
if err := f(v, c); err != nil {
logger.Fatal("failed to run mode", zap.String("mode", mode), zap.Error(err))
}
}()
}
}
if !hasMode {
logger.Fatal("no mode specified")
}
wg.Wait()
}
func viperToClientConfig() (*client.Config, error) {
// Conn and address
addrStr := viper.GetString("server")
if addrStr == "" {
return nil, configError{Field: "server", Err: errors.New("server address is empty")}
}
addrStr = completeServerAddrString(addrStr)
addr, err := net.ResolveUDPAddr("udp", addrStr)
if err != nil {
return nil, configError{Field: "server", Err: err}
}
sni := viper.GetString("sni")
if sni == "" {
sni = addrStr
}
// TLS
tlsConfig := &tls.Config{
NextProtos: []string{config.ALPN},
ServerName: config.ServerName,
InsecureSkipVerify: config.Insecure,
MinVersion: tls.VersionTLS13,
}
// Load CA
if len(config.CustomCA) > 0 {
bs, err := ioutil.ReadFile(config.CustomCA)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.CustomCA,
}).Fatal("Failed to load CA")
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(bs) {
logrus.WithFields(logrus.Fields{
"file": config.CustomCA,
}).Fatal("Failed to parse CA")
}
tlsConfig.RootCAs = cp
}
// QUIC config
quicConfig := &quic.Config{
InitialStreamReceiveWindow: config.ReceiveWindowConn,
MaxStreamReceiveWindow: config.ReceiveWindowConn,
InitialConnectionReceiveWindow: config.ReceiveWindow,
MaxConnectionReceiveWindow: config.ReceiveWindow,
HandshakeIdleTimeout: time.Duration(config.HandshakeTimeout) * time.Second,
MaxIdleTimeout: time.Duration(config.IdleTimeout) * time.Second,
KeepAlivePeriod: time.Duration(config.IdleTimeout) * time.Second * 2 / 5,
DisablePathMTUDiscovery: config.DisableMTUDiscovery,
EnableDatagrams: true,
}
if !quicConfig.DisablePathMTUDiscovery && pmtud.DisablePathMTUDiscovery {
logrus.Info("Path MTU Discovery is not yet supported on this platform")
}
// Auth
var auth []byte
if len(config.Auth) > 0 {
auth = config.Auth
} else {
auth = []byte(config.AuthString)
}
// Packet conn
pktConnFuncFactory := clientPacketConnFuncFactoryMap[config.Protocol]
if pktConnFuncFactory == nil {
logrus.WithFields(logrus.Fields{
"protocol": config.Protocol,
}).Fatal("Unsupported protocol")
}
pktConnFunc := pktConnFuncFactory(config.Obfs, time.Duration(config.HopInterval)*time.Second)
// Resolve preference
if len(config.ResolvePreference) > 0 {
pref, err := transport.ResolvePreferenceFromString(config.ResolvePreference)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to parse the resolve preference")
}
transport.DefaultClientTransport.ResolvePreference = pref
}
// ACL
var aclEngine *acl.Engine
if len(config.ACL) > 0 {
var err error
aclEngine, err = acl.LoadFromFile(config.ACL, transport.DefaultClientTransport.ResolveIPAddr,
func() (*geoip2.Reader, error) {
return loadMMDBReader(config.MMDB)
})
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACL,
}).Fatal("Failed to parse ACL")
}
}
// Client
var client *cs.Client
try := 0
up, down, _ := config.Speed()
for {
try += 1
c, err := cs.NewClient(config.Server, auth, tlsConfig, quicConfig, pktConnFunc, up, down, config.FastOpen,
config.LazyStart,
func(err error) {
if config.QuitOnDisconnect {
logrus.WithFields(logrus.Fields{
"addr": config.Server,
"error": err,
}).Fatal("Connection to server lost, exiting...")
} else {
logrus.WithFields(logrus.Fields{
"addr": config.Server,
"error": err,
}).Error("Connection to server lost, reconnecting...")
}
})
if err != nil {
logrus.WithField("error", err).Error("Failed to initialize client")
if try <= config.Retry || config.Retry < 0 {
retryInterval := 1
if config.RetryInterval != nil {
retryInterval = *config.RetryInterval
}
logrus.WithFields(logrus.Fields{
"retry": try,
"interval": retryInterval,
}).Info("Retrying...")
time.Sleep(time.Duration(retryInterval) * time.Second)
} else {
logrus.Fatal("Out of retries, exiting...")
}
} else {
client = c
break
}
}
defer client.Close()
if config.LazyStart {
logrus.WithField("addr", config.Server).Info("Lazy start enabled, waiting for first connection")
} else {
logrus.WithField("addr", config.Server).Info("Connected")
}
// Local
errChan := make(chan error)
if len(config.SOCKS5.Listen) > 0 {
go func() {
var authFunc func(user, password string) bool
if config.SOCKS5.User != "" && config.SOCKS5.Password != "" {
authFunc = func(user, password string) bool {
return config.SOCKS5.User == user && config.SOCKS5.Password == password
}
}
socks5server, err := socks5.NewServer(client, transport.DefaultClientTransport, config.SOCKS5.Listen,
authFunc, time.Duration(config.SOCKS5.Timeout)*time.Second, aclEngine, config.SOCKS5.DisableUDP,
func(addr net.Addr, reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("SOCKS5 TCP request")
},
func(addr net.Addr, reqAddr string, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Info("SOCKS5 TCP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("SOCKS5 TCP EOF")
}
},
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("SOCKS5 UDP associate")
},
func(addr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
}).Info("SOCKS5 UDP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("SOCKS5 UDP EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize SOCKS5 server")
}
logrus.WithField("addr", config.SOCKS5.Listen).Info("SOCKS5 server up and running")
errChan <- socks5server.ListenAndServe()
}()
}
if len(config.HTTP.Listen) > 0 {
go func() {
var authFunc func(user, password string) bool
if config.HTTP.User != "" && config.HTTP.Password != "" {
authFunc = func(user, password string) bool {
return config.HTTP.User == user && config.HTTP.Password == password
}
}
proxy, err := hyHTTP.NewProxyHTTPServer(client, transport.DefaultClientTransport,
time.Duration(config.HTTP.Timeout)*time.Second, aclEngine, authFunc,
func(reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("HTTP request")
},
func(reqAddr string, err error) {
logrus.WithFields(logrus.Fields{
"error": err,
"dst": defaultIPMasker.Mask(reqAddr),
}).Info("HTTP error")
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize HTTP server")
}
if config.HTTP.Cert != "" && config.HTTP.Key != "" {
logrus.WithField("addr", config.HTTP.Listen).Info("HTTPS server up and running")
errChan <- http.ListenAndServeTLS(config.HTTP.Listen, config.HTTP.Cert, config.HTTP.Key, proxy)
} else {
logrus.WithField("addr", config.HTTP.Listen).Info("HTTP server up and running")
errChan <- http.ListenAndServe(config.HTTP.Listen, proxy)
}
}()
}
if len(config.TUN.Name) != 0 {
go startTUN(config, client, errChan)
}
if len(config.TCPRelay.Listen) > 0 {
config.TCPRelays = append(config.TCPRelays, Relay{
Listen: config.TCPRelay.Listen,
Remote: config.TCPRelay.Remote,
Timeout: config.TCPRelay.Timeout,
})
}
if len(config.TCPRelays) > 0 {
for _, tcpr := range config.TCPRelays {
go func(tcpr Relay) {
rl, err := relay.NewTCPRelay(client, tcpr.Listen, tcpr.Remote,
time.Duration(tcpr.Timeout)*time.Second,
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("TCP relay request")
},
func(addr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
}).Info("TCP relay error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("TCP relay EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP relay")
}
logrus.WithField("addr", tcpr.Listen).Info("TCP relay up and running")
errChan <- rl.ListenAndServe()
}(tcpr)
}
}
if len(config.UDPRelay.Listen) > 0 {
config.UDPRelays = append(config.UDPRelays, Relay{
Listen: config.UDPRelay.Listen,
Remote: config.UDPRelay.Remote,
Timeout: config.UDPRelay.Timeout,
})
}
if len(config.UDPRelays) > 0 {
for _, udpr := range config.UDPRelays {
go func(udpr Relay) {
rl, err := relay.NewUDPRelay(client, udpr.Listen, udpr.Remote,
time.Duration(udpr.Timeout)*time.Second,
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("UDP relay request")
},
func(addr net.Addr, err error) {
if err != relay.ErrTimeout {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
}).Info("UDP relay error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Debug("UDP relay session closed")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize UDP relay")
}
logrus.WithField("addr", udpr.Listen).Info("UDP relay up and running")
errChan <- rl.ListenAndServe()
}(udpr)
}
}
if len(config.TCPTProxy.Listen) > 0 {
go func() {
rl, err := tproxy.NewTCPTProxy(client, config.TCPTProxy.Listen,
time.Duration(config.TCPTProxy.Timeout)*time.Second,
func(addr, reqAddr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP TProxy request")
},
func(addr, reqAddr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Info("TCP TProxy error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP TProxy EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP TProxy")
}
logrus.WithField("addr", config.TCPTProxy.Listen).Info("TCP TProxy up and running")
errChan <- rl.ListenAndServe()
}()
}
if len(config.UDPTProxy.Listen) > 0 {
go func() {
rl, err := tproxy.NewUDPTProxy(client, config.UDPTProxy.Listen,
time.Duration(config.UDPTProxy.Timeout)*time.Second,
func(addr, reqAddr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("UDP TProxy request")
},
func(addr, reqAddr net.Addr, err error) {
if !errors.Is(err, os.ErrDeadlineExceeded) {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Info("UDP TProxy error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("UDP TProxy session closed")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize UDP TProxy")
}
logrus.WithField("addr", config.UDPTProxy.Listen).Info("UDP TProxy up and running")
errChan <- rl.ListenAndServe()
}()
}
if len(config.TCPRedirect.Listen) > 0 {
go func() {
rl, err := redirect.NewTCPRedirect(client, config.TCPRedirect.Listen,
time.Duration(config.TCPRedirect.Timeout)*time.Second,
func(addr, reqAddr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP Redirect request")
},
func(addr, reqAddr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Info("TCP Redirect error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr.String()),
}).Debug("TCP Redirect EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP Redirect")
}
logrus.WithField("addr", config.TCPRedirect.Listen).Info("TCP Redirect up and running")
errChan <- rl.ListenAndServe()
}()
}
err := <-errChan
logrus.WithField("error", err).Fatal("Client shutdown")
}
func parseClientConfig(cb []byte) (*clientConfig, error) {
var c clientConfig
err := json5.Unmarshal(cb, &c)
tlsConfig, err := viperToClientTLSConfig()
if err != nil {
return nil, err
}
return &c, c.Check()
// QUIC
quicConfig := viperToClientQUICConfig()
// Bandwidth
bwConfig, err := viperToClientBandwidthConfig()
if err != nil {
return nil, err
}
return &client.Config{
ConnFactory: nil, // TODO
ServerAddr: addr,
ServerName: sni,
Auth: viper.GetString("auth"),
TLSConfig: tlsConfig,
QUICConfig: quicConfig,
BandwidthConfig: bwConfig,
FastOpen: viper.GetBool("fastOpen"),
}, nil
}
func viperToClientTLSConfig() (client.TLSConfig, error) {
config := client.TLSConfig{
InsecureSkipVerify: viper.GetBool("tls.insecure"),
}
caPath := viper.GetString("tls.ca")
if caPath != "" {
ca, err := os.ReadFile(caPath)
if err != nil {
return client.TLSConfig{}, configError{Field: "tls.ca", Err: err}
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(ca) {
return client.TLSConfig{}, configError{Field: "tls.ca", Err: errors.New("failed to parse CA certificate")}
}
config.RootCAs = pool
}
return config, nil
}
func viperToClientQUICConfig() client.QUICConfig {
return client.QUICConfig{
InitialStreamReceiveWindow: viper.GetUint64("quic.initStreamReceiveWindow"),
MaxStreamReceiveWindow: viper.GetUint64("quic.maxStreamReceiveWindow"),
InitialConnectionReceiveWindow: viper.GetUint64("quic.initConnReceiveWindow"),
MaxConnectionReceiveWindow: viper.GetUint64("quic.maxConnReceiveWindow"),
MaxIdleTimeout: viper.GetDuration("quic.maxIdleTimeout"),
KeepAlivePeriod: viper.GetDuration("quic.keepAlivePeriod"),
DisablePathMTUDiscovery: viper.GetBool("quic.disablePathMTUDiscovery"),
}
}
func viperToClientBandwidthConfig() (client.BandwidthConfig, error) {
bw := client.BandwidthConfig{}
upStr, downStr := viper.GetString("bandwidth.up"), viper.GetString("bandwidth.down")
if upStr == "" || downStr == "" {
return client.BandwidthConfig{}, configError{Field: "bandwidth", Err: errors.New("bandwidth.up and bandwidth.down must be set")}
}
up, err := convBandwidth(upStr)
if err != nil {
return client.BandwidthConfig{}, configError{Field: "bandwidth.up", Err: err}
}
down, err := convBandwidth(downStr)
if err != nil {
return client.BandwidthConfig{}, configError{Field: "bandwidth.down", Err: err}
}
bw.MaxTx, bw.MaxRx = up, down
return bw, nil
}
func clientSOCKS5(v *viper.Viper, c client.Client) error {
listenAddr := v.GetString("listen")
if listenAddr == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
l, err := net.Listen("tcp", listenAddr)
if err != nil {
return configError{Field: "listen", Err: err}
}
var authFunc func(username, password string) bool
username, password := v.GetString("username"), v.GetString("password")
if username != "" && password != "" {
authFunc = func(username, password string) bool {
return username == username && password == password
}
}
s := socks5.Server{
HyClient: c,
AuthFunc: authFunc,
DisableUDP: viper.GetBool("disableUDP"),
EventLogger: &socks5Logger{},
}
logger.Info("SOCKS5 server listening", zap.String("addr", listenAddr))
return s.Serve(l)
}
func completeServerAddrString(addrStr string) string {
if _, _, err := net.SplitHostPort(addrStr); err != nil {
// No port provided, use default HTTPS port
return net.JoinHostPort(addrStr, "443")
}
return addrStr
}
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.Error("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.Error("SOCKS5 UDP error", zap.String("addr", addr.String()), zap.Error(err))
}
}

View file

@ -1,117 +0,0 @@
//go:build gpl
// +build gpl
package main
import (
"io"
"net"
"strings"
"time"
"github.com/apernet/hysteria/app/tun"
"github.com/docker/go-units"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"github.com/apernet/hysteria/core/cs"
"github.com/sirupsen/logrus"
)
const license = `Hysteria is a feature-packed proxy & relay utility optimized for lossy, unstable connections.
Copyright (C) 2022 Toby
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
`
func startTUN(config *clientConfig, client *cs.Client, errChan chan error) {
timeout := time.Duration(config.TUN.Timeout) * time.Second
if timeout == 0 {
timeout = 300 * time.Second
}
var err error
var tcpSendBufferSize, tcpReceiveBufferSize int64
if config.TUN.TCPSendBufferSize != "" {
tcpSendBufferSize, err = units.RAMInBytes(config.TUN.TCPSendBufferSize)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"tcp-sndbuf": config.TUN.TCPSendBufferSize,
}).Fatal("Failed to parse tcp-sndbuf in the TUN config")
}
if (tcpSendBufferSize != 0 && tcpSendBufferSize < tcp.MinBufferSize) || tcpSendBufferSize > tcp.MaxBufferSize {
logrus.WithFields(logrus.Fields{
"tcp-sndbuf": config.TUN.TCPSendBufferSize,
}).Fatal("Invalid tcp-sndbuf in the TUN config")
}
}
if config.TUN.TCPReceiveBufferSize != "" {
tcpReceiveBufferSize, err = units.RAMInBytes(config.TUN.TCPReceiveBufferSize)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"tcp-rcvbuf": config.TUN.TCPReceiveBufferSize,
}).Fatal("Failed to parse tcp-rcvbuf in the TUN config")
}
if (tcpReceiveBufferSize != 0 && tcpReceiveBufferSize < tcp.MinBufferSize) || tcpReceiveBufferSize > tcp.MaxBufferSize {
logrus.WithFields(logrus.Fields{
"error": err,
"tcp-rcvbuf": config.TUN.TCPReceiveBufferSize,
}).Fatal("Invalid tcp-rcvbuf in the TUN config")
}
}
tunServer, err := tun.NewServer(client, timeout,
config.TUN.Name, config.TUN.MTU,
int(tcpSendBufferSize), int(tcpReceiveBufferSize), config.TUN.TCPModerateReceiveBuffer)
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TUN server")
}
tunServer.RequestFunc = func(addr net.Addr, reqAddr string) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s request", strings.ToUpper(addr.Network()))
}
tunServer.ErrorFunc = func(addr net.Addr, reqAddr string, err error) {
if err != nil {
if err == io.EOF {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s EOF", strings.ToUpper(addr.Network()))
} else if err == cs.ErrClosed && strings.HasPrefix(addr.Network(), "udp") {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network()))
} else if nErr, ok := err.(net.Error); ok && nErr.Timeout() && strings.HasPrefix(addr.Network(), "tcp") {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network()))
} else {
logrus.WithFields(logrus.Fields{
"error": err,
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Infof("TUN %s error", strings.ToUpper(addr.Network()))
}
}
}
logrus.WithField("interface", config.TUN.Name).Info("TUN up and running")
errChan <- tunServer.ListenAndServe()
}

View file

@ -1,36 +0,0 @@
//go:build !gpl
// +build !gpl
package main
import (
"github.com/apernet/hysteria/core/cs"
"github.com/sirupsen/logrus"
)
const license = `The MIT License (MIT)
Copyright (c) 2021 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.
`
func startTUN(config *clientConfig, client *cs.Client, errChan chan error) {
logrus.Fatalln("TUN mode is only available in GPL builds. Please rebuild hysteria with -tags gpl")
}

View file

@ -1,67 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
$ source <(%[1]s completion bash)
# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect.
fish:
$ %[1]s completion fish | source
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, rootCmd.Name()),
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
_ = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
_ = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
_ = cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}

View file

@ -1,385 +0,0 @@
package main
import (
"errors"
"fmt"
"regexp"
"strconv"
"github.com/sirupsen/logrus"
"github.com/yosuke-furukawa/json5/encoding/json5"
)
const (
mbpsToBps = 125000
minSpeedBPS = 16384
DefaultALPN = "hysteria"
DefaultStreamReceiveWindow = 16777216 // 16 MB
DefaultConnectionReceiveWindow = DefaultStreamReceiveWindow * 5 / 2 // 40 MB
DefaultMaxIncomingStreams = 1024
DefaultMMDBFilename = "GeoLite2-Country.mmdb"
ServerMaxIdleTimeoutSec = 60
DefaultClientIdleTimeoutSec = 20
DefaultClientHopIntervalSec = 10
)
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type serverConfig struct {
Listen string `json:"listen"`
Protocol string `json:"protocol"`
ACME struct {
Domains []string `json:"domains"`
Email string `json:"email"`
DisableHTTPChallenge bool `json:"disable_http"`
DisableTLSALPNChallenge bool `json:"disable_tlsalpn"`
AltHTTPPort int `json:"alt_http_port"`
AltTLSALPNPort int `json:"alt_tlsalpn_port"`
} `json:"acme"`
CertFile string `json:"cert"`
KeyFile string `json:"key"`
// Optional below
Up string `json:"up"`
UpMbps int `json:"up_mbps"`
Down string `json:"down"`
DownMbps int `json:"down_mbps"`
DisableUDP bool `json:"disable_udp"`
ACL string `json:"acl"`
MMDB string `json:"mmdb"`
Obfs string `json:"obfs"`
Auth struct {
Mode string `json:"mode"`
Config json5.RawMessage `json:"config"`
} `json:"auth"`
ALPN string `json:"alpn"`
PrometheusListen string `json:"prometheus_listen"`
ReceiveWindowConn uint64 `json:"recv_window_conn"`
ReceiveWindowClient uint64 `json:"recv_window_client"`
MaxConnClient int `json:"max_conn_client"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
Resolver string `json:"resolver"`
ResolvePreference string `json:"resolve_preference"`
SOCKS5Outbound struct {
Server string `json:"server"`
User string `json:"user"`
Password string `json:"password"`
} `json:"socks5_outbound"`
BindOutbound struct {
Address string `json:"address"`
Device string `json:"device"`
} `json:"bind_outbound"`
}
func (c *serverConfig) Speed() (uint64, uint64, error) {
var up, down uint64
if len(c.Up) > 0 {
up = stringToBps(c.Up)
if up == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
up = uint64(c.UpMbps) * mbpsToBps
}
if len(c.Down) > 0 {
down = stringToBps(c.Down)
if down == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
down = uint64(c.DownMbps) * mbpsToBps
}
return up, down, nil
}
func (c *serverConfig) Check() error {
if len(c.Listen) == 0 {
return errors.New("missing listen address")
}
if len(c.ACME.Domains) == 0 && (len(c.CertFile) == 0 || len(c.KeyFile) == 0) {
return errors.New("need either ACME info or cert/key files")
}
if len(c.ACME.Domains) > 0 && (len(c.CertFile) > 0 || len(c.KeyFile) > 0) {
return errors.New("cannot use both ACME and cert/key files, they are mutually exclusive")
}
if up, down, err := c.Speed(); err != nil || (up != 0 && up < minSpeedBPS) || (down != 0 && down < minSpeedBPS) {
return errors.New("invalid speed")
}
if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) ||
(c.ReceiveWindowClient != 0 && c.ReceiveWindowClient < 65536) {
return errors.New("invalid receive window size")
}
if c.MaxConnClient < 0 {
return errors.New("invalid max connections per client")
}
return nil
}
func (c *serverConfig) Fill() {
if len(c.ALPN) == 0 {
c.ALPN = DefaultALPN
}
if c.ReceiveWindowConn == 0 {
c.ReceiveWindowConn = DefaultStreamReceiveWindow
}
if c.ReceiveWindowClient == 0 {
c.ReceiveWindowClient = DefaultConnectionReceiveWindow
}
if c.MaxConnClient == 0 {
c.MaxConnClient = DefaultMaxIncomingStreams
}
if len(c.MMDB) == 0 {
c.MMDB = DefaultMMDBFilename
}
}
func (c *serverConfig) String() string {
return fmt.Sprintf("%+v", *c)
}
type Relay struct {
Listen string `json:"listen"`
Remote string `json:"remote"`
Timeout int `json:"timeout"`
}
func (r *Relay) Check() error {
if len(r.Listen) == 0 {
return errors.New("missing relay listen address")
}
if len(r.Remote) == 0 {
return errors.New("missing relay remote address")
}
if r.Timeout != 0 && r.Timeout < 4 {
return errors.New("invalid relay timeout")
}
return nil
}
type clientConfig struct {
Server string `json:"server"`
Protocol string `json:"protocol"`
Up string `json:"up"`
UpMbps int `json:"up_mbps"`
Down string `json:"down"`
DownMbps int `json:"down_mbps"`
// Optional below
Retry int `json:"retry"`
RetryInterval *int `json:"retry_interval"`
QuitOnDisconnect bool `json:"quit_on_disconnect"`
HandshakeTimeout int `json:"handshake_timeout"`
IdleTimeout int `json:"idle_timeout"`
HopInterval int `json:"hop_interval"`
SOCKS5 struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
DisableUDP bool `json:"disable_udp"`
User string `json:"user"`
Password string `json:"password"`
} `json:"socks5"`
HTTP struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
User string `json:"user"`
Password string `json:"password"`
Cert string `json:"cert"`
Key string `json:"key"`
} `json:"http"`
TUN struct {
Name string `json:"name"`
Timeout int `json:"timeout"`
MTU uint32 `json:"mtu"`
TCPSendBufferSize string `json:"tcp_sndbuf"`
TCPReceiveBufferSize string `json:"tcp_rcvbuf"`
TCPModerateReceiveBuffer bool `json:"tcp_autotuning"`
} `json:"tun"`
TCPRelays []Relay `json:"relay_tcps"`
TCPRelay Relay `json:"relay_tcp"` // deprecated, but we still support it for backward compatibility
UDPRelays []Relay `json:"relay_udps"`
UDPRelay Relay `json:"relay_udp"` // deprecated, but we still support it for backward compatibility
TCPTProxy struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"tproxy_tcp"`
UDPTProxy struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"tproxy_udp"`
TCPRedirect struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"redirect_tcp"`
ACL string `json:"acl"`
MMDB string `json:"mmdb"`
Obfs string `json:"obfs"`
Auth []byte `json:"auth"`
AuthString string `json:"auth_str"`
ALPN string `json:"alpn"`
ServerName string `json:"server_name"`
Insecure bool `json:"insecure"`
CustomCA string `json:"ca"`
ReceiveWindowConn uint64 `json:"recv_window_conn"`
ReceiveWindow uint64 `json:"recv_window"`
DisableMTUDiscovery bool `json:"disable_mtu_discovery"`
FastOpen bool `json:"fast_open"`
LazyStart bool `json:"lazy_start"`
Resolver string `json:"resolver"`
ResolvePreference string `json:"resolve_preference"`
}
func (c *clientConfig) Speed() (uint64, uint64, error) {
var up, down uint64
if len(c.Up) > 0 {
up = stringToBps(c.Up)
if up == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
up = uint64(c.UpMbps) * mbpsToBps
}
if len(c.Down) > 0 {
down = stringToBps(c.Down)
if down == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
down = uint64(c.DownMbps) * mbpsToBps
}
return up, down, nil
}
func (c *clientConfig) Check() error {
if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.TUN.Name) == 0 &&
len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 &&
len(c.TCPRelays) == 0 && len(c.UDPRelays) == 0 &&
len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 &&
len(c.TCPRedirect.Listen) == 0 {
return errors.New("please enable at least one mode")
}
if c.HandshakeTimeout != 0 && c.HandshakeTimeout < 2 {
return errors.New("invalid handshake timeout")
}
if c.IdleTimeout != 0 && c.IdleTimeout < 4 {
return errors.New("invalid idle timeout")
}
if c.HopInterval != 0 && c.HopInterval < 8 {
return errors.New("invalid hop interval")
}
if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout < 4 {
return errors.New("invalid SOCKS5 timeout")
}
if c.HTTP.Timeout != 0 && c.HTTP.Timeout < 4 {
return errors.New("invalid HTTP timeout")
}
if c.TUN.Timeout != 0 && c.TUN.Timeout < 4 {
return errors.New("invalid TUN timeout")
}
if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 {
return errors.New("missing TCP relay remote address")
}
if len(c.UDPRelay.Listen) > 0 && len(c.UDPRelay.Remote) == 0 {
return errors.New("missing UDP relay remote address")
}
if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout < 4 {
return errors.New("invalid TCP relay timeout")
}
if c.UDPRelay.Timeout != 0 && c.UDPRelay.Timeout < 4 {
return errors.New("invalid UDP relay timeout")
}
for _, r := range c.TCPRelays {
if err := r.Check(); err != nil {
return err
}
}
for _, r := range c.UDPRelays {
if err := r.Check(); err != nil {
return err
}
}
if c.TCPTProxy.Timeout != 0 && c.TCPTProxy.Timeout < 4 {
return errors.New("invalid TCP TProxy timeout")
}
if c.UDPTProxy.Timeout != 0 && c.UDPTProxy.Timeout < 4 {
return errors.New("invalid UDP TProxy timeout")
}
if c.TCPRedirect.Timeout != 0 && c.TCPRedirect.Timeout < 4 {
return errors.New("invalid TCP Redirect timeout")
}
if len(c.Server) == 0 {
return errors.New("missing server address")
}
if up, down, err := c.Speed(); err != nil || up < minSpeedBPS || down < minSpeedBPS {
return errors.New("invalid speed")
}
if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) ||
(c.ReceiveWindow != 0 && c.ReceiveWindow < 65536) {
return errors.New("invalid receive window size")
}
if len(c.TCPRelay.Listen) > 0 {
logrus.Warn("'relay_tcp' is deprecated, consider using 'relay_tcps' instead")
}
if len(c.UDPRelay.Listen) > 0 {
logrus.Warn("'relay_udp' is deprecated, consider using 'relay_udps' instead")
}
return nil
}
func (c *clientConfig) Fill() {
if len(c.ALPN) == 0 {
c.ALPN = DefaultALPN
}
if c.ReceiveWindowConn == 0 {
c.ReceiveWindowConn = DefaultStreamReceiveWindow
}
if c.ReceiveWindow == 0 {
c.ReceiveWindow = DefaultConnectionReceiveWindow
}
if len(c.MMDB) == 0 {
c.MMDB = DefaultMMDBFilename
}
if c.IdleTimeout == 0 {
c.IdleTimeout = DefaultClientIdleTimeoutSec
}
if c.HopInterval == 0 {
c.HopInterval = DefaultClientHopIntervalSec
}
}
func (c *clientConfig) String() string {
return fmt.Sprintf("%+v", *c)
}
func stringToBps(s string) uint64 {
if s == "" {
return 0
}
m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
switch m[2] {
case "K":
n = 1 << 10
case "M":
n = 1 << 20
case "G":
n = 1 << 30
case "T":
n = 1 << 40
default:
n = 1
}
v, _ := strconv.ParseUint(m[1], 10, 64)
n = v * n
if m[3] == "b" {
// Bits, need to convert to bytes
n = n >> 3
}
return n
}

View file

@ -1,34 +0,0 @@
package main
import "testing"
func Test_stringToBps(t *testing.T) {
tests := []struct {
name string
s string
want uint64
}{
{name: "bps 1", s: "8 bps", want: 1},
{name: "bps 2", s: "3 bps", want: 0},
{name: "Bps", s: "9991Bps", want: 9991},
{name: "KBps", s: "10 KBps", want: 10240},
{name: "Kbps", s: "10 Kbps", want: 1280},
{name: "MBps", s: "10 MBps", want: 10485760},
{name: "Mbps", s: "10 Mbps", want: 1310720},
{name: "GBps", s: "10 GBps", want: 10737418240},
{name: "Gbps", s: "10 Gbps", want: 1342177280},
{name: "TBps", s: "10 TBps", want: 10995116277760},
{name: "Tbps", s: "10 Tbps", want: 1374389534720},
{name: "invalid 1", s: "6699E Kbps", want: 0},
{name: "invalid 2", s: "400 Bsp", want: 0},
{name: "invalid 3", s: "9 GBbps", want: 0},
{name: "invalid 4", s: "Mbps", want: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := stringToBps(tt.s); got != tt.want {
t.Errorf("stringToBps() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,43 +0,0 @@
package main
import (
"net"
)
type ipMasker struct {
IPv4Mask net.IPMask
IPv6Mask net.IPMask
}
// Mask masks an address with the configured CIDR.
// addr can be "host:port" or just host.
func (m *ipMasker) Mask(addr string) string {
if m.IPv4Mask == nil && m.IPv6Mask == nil {
return addr
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
// just host
host, port = addr, ""
}
ip := net.ParseIP(host)
if ip == nil {
// not an IP address, return as is
return addr
}
if ip4 := ip.To4(); ip4 != nil && m.IPv4Mask != nil {
// IPv4
host = ip4.Mask(m.IPv4Mask).String()
} else if ip6 := ip.To16(); ip6 != nil && m.IPv6Mask != nil {
// IPv6
host = ip6.Mask(m.IPv6Mask).String()
}
if port != "" {
return net.JoinHostPort(host, port)
} else {
return host
}
}
var defaultIPMasker = &ipMasker{}

View file

@ -1,95 +0,0 @@
package main
import (
"crypto/tls"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
)
type keypairLoader struct {
certMu sync.RWMutex
cert *tls.Certificate
certPath string
keyPath string
}
func newKeypairLoader(certPath, keyPath string) (*keypairLoader, error) {
loader := &keypairLoader{
certPath: certPath,
keyPath: keyPath,
}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
loader.cert = &cert
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
switch event.Op {
case fsnotify.Create, fsnotify.Write, fsnotify.Rename, fsnotify.Chmod:
logrus.WithFields(logrus.Fields{
"file": event.Name,
}).Info("Keypair change detected, reloading...")
if err := loader.load(); err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to reload keypair")
} else {
logrus.Info("Keypair successfully reloaded")
}
case fsnotify.Remove:
_ = watcher.Add(event.Name) // Workaround for vim
// https://github.com/fsnotify/fsnotify/issues/92
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
logrus.WithFields(logrus.Fields{
"error": err,
}).Error("Failed to watch keypair files for changes")
}
}
}()
err = watcher.Add(certPath)
if err != nil {
_ = watcher.Close()
return nil, err
}
err = watcher.Add(keyPath)
if err != nil {
_ = watcher.Close()
return nil, err
}
return loader, nil
}
func (kpr *keypairLoader) load() error {
cert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
if err != nil {
return err
}
kpr.certMu.Lock()
kpr.cert = &cert
kpr.certMu.Unlock()
return nil
}
func (kpr *keypairLoader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
kpr.certMu.RLock()
defer kpr.certMu.RUnlock()
return kpr.cert, nil
}
}

View file

@ -1,208 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"math/rand"
"net"
"os"
"regexp"
"strings"
"time"
nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
logo = `
`
desc = "A TCP/UDP relay & SOCKS5/HTTP proxy tool optimized for poor network environments"
authors = "Aperture Internet Laboratory <https://github.com/apernet>"
)
var (
appVersion = "Unknown"
appCommit = "Unknown"
appDate = "Unknown"
)
var rootCmd = &cobra.Command{
Use: "hysteria",
Long: fmt.Sprintf("%s%s\n\nVersion:\t%s\nBuildDate:\t%s\nCommitHash:\t%s\nAuthors:\t%s", logo, desc, appVersion, appDate, appCommit, authors),
Example: "./hysteria server --config /etc/hysteria.json",
Version: fmt.Sprintf("%s %s %s", appVersion, appDate, appCommit),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
rand.Seed(time.Now().UnixNano())
// log config
logrus.SetOutput(os.Stdout)
if lvl, err := logrus.ParseLevel(viper.GetString("log-level")); err == nil {
logrus.SetLevel(lvl)
} else {
logrus.SetLevel(logrus.DebugLevel)
}
if strings.ToLower(viper.GetString("log-format")) == "json" {
logrus.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: viper.GetString("log-timestamp"),
})
} else {
logrus.SetFormatter(&nested.Formatter{
FieldsOrder: []string{
"version", "url",
"config", "file", "mode", "protocol",
"cert", "key",
"addr", "src", "dst", "session", "action", "interface",
"tcp-sndbuf", "tcp-rcvbuf",
"retry", "interval",
"code", "msg", "error",
},
TimestampFormat: viper.GetString("log-timestamp"),
})
}
// license
if viper.GetBool("license") {
fmt.Printf("%s\n", license)
os.Exit(0)
}
// ip mask config
v4m := viper.GetUint("log-ipv4-mask")
if v4m > 0 && v4m < 32 {
defaultIPMasker.IPv4Mask = net.CIDRMask(int(v4m), 32)
}
v6m := viper.GetUint("log-ipv6-mask")
if v6m > 0 && v6m < 128 {
defaultIPMasker.IPv6Mask = net.CIDRMask(int(v6m), 128)
}
// check update
if !viper.GetBool("no-check") {
go checkUpdate()
}
},
Run: func(cmd *cobra.Command, args []string) {
clientCmd.Run(cmd, args)
},
}
var clientCmd = &cobra.Command{
Use: "client",
Short: "Run as client mode",
Example: "./hysteria client --config /etc/hysteria/client.json",
Run: func(cmd *cobra.Command, args []string) {
cbs, err := ioutil.ReadFile(viper.GetString("config"))
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to read configuration")
}
// client mode
cc, err := parseClientConfig(cbs)
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to parse client configuration")
}
client(cc)
},
}
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run as server mode",
Example: "./hysteria server --config /etc/hysteria/server.json",
Run: func(cmd *cobra.Command, args []string) {
cbs, err := ioutil.ReadFile(viper.GetString("config"))
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to read configuration")
}
// server mode
sc, err := parseServerConfig(cbs)
if err != nil {
logrus.WithFields(logrus.Fields{
"file": viper.GetString("config"),
"error": err,
}).Fatal("Failed to parse server configuration")
}
server(sc)
},
}
// fakeFlags replace the old flag format with the new format(eg: `-config` ->> `--config`)
func fakeFlags() {
var args []string
fr, _ := regexp.Compile(`^-[a-zA-Z]{2,}`)
for _, arg := range os.Args {
if fr.MatchString(arg) {
args = append(args, "-"+arg)
} else {
args = append(args, arg)
}
}
os.Args = args
}
func init() {
// compatible with old flag format
fakeFlags()
// compatible windows double click
cobra.MousetrapHelpText = ""
// disable cmd sorting
cobra.EnableCommandSorting = false
// add global flags
rootCmd.PersistentFlags().StringP("config", "c", "./config.json", "config file")
rootCmd.PersistentFlags().String("mmdb-url", "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb", "mmdb download url")
rootCmd.PersistentFlags().String("log-level", "debug", "log level")
rootCmd.PersistentFlags().String("log-timestamp", time.RFC3339, "log timestamp format")
rootCmd.PersistentFlags().String("log-format", "txt", "log output format (txt/json)")
rootCmd.PersistentFlags().Uint("log-ipv4-mask", 0, "mask IPv4 addresses in log using a CIDR mask")
rootCmd.PersistentFlags().Uint("log-ipv6-mask", 0, "mask IPv6 addresses in log using a CIDR mask")
rootCmd.PersistentFlags().Bool("no-check", false, "disable update check")
rootCmd.PersistentFlags().Bool("license", false, "show license and exit")
// add to root cmd
rootCmd.AddCommand(clientCmd, serverCmd, completionCmd)
// bind flag
_ = viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
_ = viper.BindPFlag("mmdb-url", rootCmd.PersistentFlags().Lookup("mmdb-url"))
_ = viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level"))
_ = viper.BindPFlag("log-timestamp", rootCmd.PersistentFlags().Lookup("log-timestamp"))
_ = viper.BindPFlag("log-format", rootCmd.PersistentFlags().Lookup("log-format"))
_ = viper.BindPFlag("log-ipv4-mask", rootCmd.PersistentFlags().Lookup("log-ipv4-mask"))
_ = viper.BindPFlag("log-ipv6-mask", rootCmd.PersistentFlags().Lookup("log-ipv6-mask"))
_ = viper.BindPFlag("no-check", rootCmd.PersistentFlags().Lookup("no-check"))
_ = viper.BindPFlag("license", rootCmd.PersistentFlags().Lookup("license"))
// bind env
_ = viper.BindEnv("config", "HYSTERIA_CONFIG")
_ = viper.BindEnv("mmdb-url", "HYSTERIA_MMDB_URL")
_ = viper.BindEnv("log-level", "HYSTERIA_LOG_LEVEL", "LOGGING_LEVEL")
_ = viper.BindEnv("log-timestamp", "HYSTERIA_LOG_TIMESTAMP", "LOGGING_TIMESTAMP_FORMAT")
_ = viper.BindEnv("log-format", "HYSTERIA_LOG_FORMAT", "LOGGING_FORMATTER")
_ = viper.BindEnv("log-ipv4-mask", "HYSTERIA_LOG_IPV4_MASK", "LOGGING_IPV4_MASK")
_ = viper.BindEnv("log-ipv6-mask", "HYSTERIA_LOG_IPV6_MASK", "LOGGING_IPV6_MASK")
_ = viper.BindEnv("no-check", "HYSTERIA_NO_CHECK", "HYSTERIA_NO_CHECK_UPDATE")
viper.AutomaticEnv()
}
func main() {
cobra.CheckErr(rootCmd.Execute())
}

View file

@ -1,49 +0,0 @@
package main
import (
"io"
"net/http"
"os"
"github.com/oschwald/geoip2-golang"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
func downloadMMDB(filename string) error {
resp, err := http.Get(viper.GetString("mmdb-url"))
if err != nil {
return err
}
defer resp.Body.Close()
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
return err
}
func loadMMDBReader(filename string) (*geoip2.Reader, error) {
if _, err := os.Stat(filename); err != nil {
if os.IsNotExist(err) {
logrus.Info("GeoLite2 database not found, downloading...")
if err := downloadMMDB(filename); err != nil {
return nil, err
}
logrus.WithFields(logrus.Fields{
"file": filename,
}).Info("GeoLite2 database downloaded")
return geoip2.Open(filename)
} else {
// some other error
return nil, err
}
} else {
// file exists, just open it
return geoip2.Open(filename)
}
}

56
app/cmd/ping.go Normal file
View file

@ -0,0 +1,56 @@
package cmd
import (
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/client"
)
// pingCmd represents the ping command
var pingCmd = &cobra.Command{
Use: "ping address",
Short: "Ping mode",
Long: "Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.",
Run: runPing,
}
func init() {
rootCmd.AddCommand(pingCmd)
}
func runPing(cmd *cobra.Command, args []string) {
logger.Info("ping mode")
if len(args) != 1 {
logger.Fatal("no address specified")
}
addr := args[0]
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
config, err := viperToClientConfig()
if err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
c, err := client.NewClient(config)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
logger.Info("connecting", zap.String("address", addr))
start := time.Now()
conn, err := c.DialTCP(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()))
}

View file

@ -1,71 +0,0 @@
package main
import (
"github.com/apernet/hysteria/core/cs"
"github.com/prometheus/client_golang/prometheus"
)
type prometheusTrafficCounter struct {
reg *prometheus.Registry
upCounterVec *prometheus.CounterVec
downCounterVec *prometheus.CounterVec
connGaugeVec *prometheus.GaugeVec
counterMap map[string]counters
}
type counters struct {
UpCounter prometheus.Counter
DownCounter prometheus.Counter
ConnGauge prometheus.Gauge
}
func NewPrometheusTrafficCounter(reg *prometheus.Registry) cs.TrafficCounter {
c := &prometheusTrafficCounter{
reg: reg,
upCounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "hysteria_traffic_uplink_bytes_total",
}, []string{"auth"}),
downCounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "hysteria_traffic_downlink_bytes_total",
}, []string{"auth"}),
connGaugeVec: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "hysteria_active_conn",
}, []string{"auth"}),
counterMap: make(map[string]counters),
}
reg.MustRegister(c.upCounterVec, c.downCounterVec, c.connGaugeVec)
return c
}
func (c *prometheusTrafficCounter) getCounters(auth string) counters {
cts, ok := c.counterMap[auth]
if !ok {
cts = counters{
UpCounter: c.upCounterVec.WithLabelValues(auth),
DownCounter: c.downCounterVec.WithLabelValues(auth),
ConnGauge: c.connGaugeVec.WithLabelValues(auth),
}
c.counterMap[auth] = cts
}
return cts
}
func (c *prometheusTrafficCounter) Rx(auth string, n int) {
cts := c.getCounters(auth)
cts.DownCounter.Add(float64(n))
}
func (c *prometheusTrafficCounter) Tx(auth string, n int) {
cts := c.getCounters(auth)
cts.UpCounter.Add(float64(n))
}
func (c *prometheusTrafficCounter) IncConn(auth string) {
cts := c.getCounters(auth)
cts.ConnGauge.Inc()
}
func (c *prometheusTrafficCounter) DecConn(auth string) {
cts := c.getCounters(auth)
cts.ConnGauge.Dec()
}

View file

@ -1,123 +0,0 @@
package main
import (
"crypto/tls"
"errors"
"net"
"net/url"
"strings"
"github.com/apernet/hysteria/core/utils"
rdns "github.com/folbricht/routedns"
)
var errInvalidSyntax = errors.New("invalid syntax")
func setResolver(dns string) error {
if net.ParseIP(dns) != nil {
// Just an IP address, treat as UDP 53
dns = "udp://" + net.JoinHostPort(dns, "53")
}
var r rdns.Resolver
if strings.HasPrefix(dns, "udp://") {
// Standard UDP DNS resolver
dns = strings.TrimPrefix(dns, "udp://")
if dns == "" {
return errInvalidSyntax
}
if _, _, err := utils.SplitHostPort(dns); err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "53")
}
client, err := rdns.NewDNSClient("dns-udp", dns, "udp", rdns.DNSClientOptions{})
if err != nil {
return err
}
r = client
} else if strings.HasPrefix(dns, "tcp://") {
// Standard TCP DNS resolver
dns = strings.TrimPrefix(dns, "tcp://")
if dns == "" {
return errInvalidSyntax
}
if _, _, err := utils.SplitHostPort(dns); err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "53")
}
client, err := rdns.NewDNSClient("dns-tcp", dns, "tcp", rdns.DNSClientOptions{})
if err != nil {
return err
}
r = client
} else if strings.HasPrefix(dns, "https://") {
// DoH resolver
if dohURL, err := url.Parse(dns); err != nil {
return err
} else {
// Need to set bootstrap address to avoid loopback DNS lookup
dohIPAddr, err := net.ResolveIPAddr("ip", dohURL.Hostname())
if err != nil {
return err
}
client, err := rdns.NewDoHClient("doh", dns, rdns.DoHClientOptions{
BootstrapAddr: dohIPAddr.String(),
})
if err != nil {
return err
}
r = client
}
} else if strings.HasPrefix(dns, "tls://") {
// DoT resolver
dns = strings.TrimPrefix(dns, "tls://")
if dns == "" {
return errInvalidSyntax
}
dotHost, _, err := utils.SplitHostPort(dns)
if err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "853")
}
// Need to set bootstrap address to avoid loopback DNS lookup
dotIPAddr, err := net.ResolveIPAddr("ip", dotHost)
if err != nil {
return err
}
client, err := rdns.NewDoTClient("dot", dns, rdns.DoTClientOptions{
BootstrapAddr: dotIPAddr.String(),
TLSConfig: new(tls.Config),
})
if err != nil {
return err
}
r = client
} else if strings.HasPrefix(dns, "quic://") {
// DoQ resolver
dns = strings.TrimPrefix(dns, "quic://")
if dns == "" {
return errInvalidSyntax
}
doqHost, _, err := utils.SplitHostPort(dns)
if err != nil {
// Append the default DNS port
dns = net.JoinHostPort(dns, "853")
}
// Need to set bootstrap address to avoid loopback DNS lookup
doqIPAddr, err := net.ResolveIPAddr("ip", doqHost)
if err != nil {
return err
}
client, err := rdns.NewDoQClient("doq", dns, rdns.DoQClientOptions{
BootstrapAddr: doqIPAddr.String(),
})
if err != nil {
return err
}
r = client
} else {
return errInvalidSyntax
}
cache := rdns.NewCache("cache", r, rdns.CacheOptions{})
net.DefaultResolver = rdns.NewNetResolver(cache)
return nil
}

79
app/cmd/root.go Normal file
View file

@ -0,0 +1,79 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
const (
appLogo = `
`
appDesc = "a powerful, censorship-resistant proxy tool optimized for lossy networks"
appAuthors = "Aperture Internet Laboratory <https://github.com/apernet>"
)
var (
// These values will be injected by the build system
appVersion = "Unknown"
appDate = "Unknown"
appCommit = "Unknown"
appVersionLong = fmt.Sprintf("Version:\t%s\nBuildDate:\t%s\nCommitHash:\t%s", appVersion, appDate, appCommit)
)
var logger *zap.Logger
// Flags
var cfgFile string
var rootCmd = &cobra.Command{
Use: "hysteria",
Short: appDesc,
Long: fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong),
Run: runClient, // Default to client mode
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
initLogger()
initFlags()
cobra.OnInitialize(initConfig)
}
func initLogger() {
// TODO: Configurable logging
l, err := zap.NewDevelopment()
if err != nil {
panic(err)
}
logger = l
}
func initFlags() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/hysteria/")
viper.AddConfigPath("$HOME/.hysteria")
viper.AddConfigPath(".")
}
}

View file

@ -1,307 +1,275 @@
package main
package cmd
import (
"context"
"crypto/tls"
"io"
"errors"
"net"
"net/http"
"time"
"strings"
"github.com/apernet/hysteria/app/auth"
"github.com/apernet/hysteria/core/server"
"github.com/apernet/hysteria/extras/auth"
"github.com/apernet/hysteria/core/pktconns"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/pmtud"
"github.com/apernet/hysteria/core/sockopt"
"github.com/apernet/hysteria/core/transport"
"github.com/oschwald/geoip2-golang"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/quic-go/quic-go"
"github.com/sirupsen/logrus"
"github.com/yosuke-furukawa/json5/encoding/json5"
"github.com/caddyserver/certmagic"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
var serverPacketConnFuncFactoryMap = map[string]pktconns.ServerPacketConnFuncFactory{
"": pktconns.NewServerUDPConnFunc,
"udp": pktconns.NewServerUDPConnFunc,
"wechat": pktconns.NewServerWeChatConnFunc,
"wechat-video": pktconns.NewServerWeChatConnFunc,
"faketcp": pktconns.NewServerFakeTCPConnFunc,
var serverCmd = &cobra.Command{
Use: "server",
Short: "Server mode",
Run: runServer,
}
func server(config *serverConfig) {
logrus.WithField("config", config.String()).Info("Server configuration loaded")
config.Fill() // Fill default values
// Resolver
if len(config.Resolver) > 0 {
err := setResolver(config.Resolver)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to set resolver")
}
func init() {
rootCmd.AddCommand(serverCmd)
initServerConfigDefaults()
}
func initServerConfigDefaults() {
viper.SetDefault("listen", ":443")
}
func runServer(cmd *cobra.Command, args []string) {
logger.Info("server mode")
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read server config", zap.Error(err))
}
// Load TLS config
var tlsConfig *tls.Config
if len(config.ACME.Domains) > 0 {
// ACME mode
tc, err := acmeTLSConfig(config.ACME.Domains, config.ACME.Email,
config.ACME.DisableHTTPChallenge, config.ACME.DisableTLSALPNChallenge,
config.ACME.AltHTTPPort, config.ACME.AltTLSALPNPort)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to get a certificate with ACME")
}
tc.NextProtos = []string{config.ALPN}
tc.MinVersion = tls.VersionTLS13
tlsConfig = tc
} else {
// Local cert mode
kpl, err := newKeypairLoader(config.CertFile, config.KeyFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"cert": config.CertFile,
"key": config.KeyFile,
}).Fatal("Failed to load the certificate")
}
tlsConfig = &tls.Config{
GetCertificate: kpl.GetCertificateFunc(),
NextProtos: []string{config.ALPN},
MinVersion: tls.VersionTLS13,
}
}
// QUIC config
quicConfig := &quic.Config{
InitialStreamReceiveWindow: config.ReceiveWindowConn,
MaxStreamReceiveWindow: config.ReceiveWindowConn,
InitialConnectionReceiveWindow: config.ReceiveWindowClient,
MaxConnectionReceiveWindow: config.ReceiveWindowClient,
MaxIncomingStreams: int64(config.MaxConnClient),
MaxIdleTimeout: ServerMaxIdleTimeoutSec * time.Second,
KeepAlivePeriod: 0, // Keep alive should solely be client's responsibility
DisablePathMTUDiscovery: config.DisableMTUDiscovery,
EnableDatagrams: true,
}
if !quicConfig.DisablePathMTUDiscovery && pmtud.DisablePathMTUDiscovery {
logrus.Info("Path MTU Discovery is not yet supported on this platform")
}
// Auth
var authFunc cs.ConnectFunc
var err error
switch authMode := config.Auth.Mode; authMode {
case "", "none":
if len(config.Obfs) == 0 {
logrus.Warn("Neither authentication nor obfuscation is turned on. " +
"Your server could be used by anyone! Are you sure this is what you want?")
}
authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
return true, "Welcome"
}
case "password", "passwords":
authFunc, err = auth.PasswordAuthFunc(config.Auth.Config)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to enable password authentication")
} else {
logrus.Info("Password authentication enabled")
}
case "external":
authFunc, err = auth.ExternalAuthFunc(config.Auth.Config)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to enable external authentication")
} else {
logrus.Info("External authentication enabled")
}
default:
logrus.WithField("mode", config.Auth.Mode).Fatal("Unsupported authentication mode")
}
connectFunc := func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
ok, msg := authFunc(addr, auth, sSend, sRecv)
if !ok {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"msg": msg,
}).Info("Authentication failed, client rejected")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
}).Info("Client connected")
}
return ok, msg
}
// Resolve preference
if len(config.ResolvePreference) > 0 {
pref, err := transport.ResolvePreferenceFromString(config.ResolvePreference)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to parse the resolve preference")
}
transport.DefaultServerTransport.ResolvePreference = pref
}
// SOCKS5 outbound
if config.SOCKS5Outbound.Server != "" {
transport.DefaultServerTransport.SOCKS5Client = transport.NewSOCKS5Client(config.SOCKS5Outbound.Server,
config.SOCKS5Outbound.User, config.SOCKS5Outbound.Password)
}
// Bind outbound
if config.BindOutbound.Device != "" {
iface, err := net.InterfaceByName(config.BindOutbound.Device)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to find the interface")
}
transport.DefaultServerTransport.LocalUDPIntf = iface
sockopt.BindDialer(transport.DefaultServerTransport.Dialer, iface)
}
if config.BindOutbound.Address != "" {
ip := net.ParseIP(config.BindOutbound.Address)
if ip == nil {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Failed to parse the address")
}
transport.DefaultServerTransport.Dialer.LocalAddr = &net.TCPAddr{IP: ip}
transport.DefaultServerTransport.LocalUDPAddr = &net.UDPAddr{IP: ip}
}
// ACL
var aclEngine *acl.Engine
if len(config.ACL) > 0 {
aclEngine, err = acl.LoadFromFile(config.ACL, func(addr string) (*net.IPAddr, error) {
ipAddr, _, err := transport.DefaultServerTransport.ResolveIPAddr(addr)
return ipAddr, err
},
func() (*geoip2.Reader, error) {
return loadMMDBReader(config.MMDB)
})
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACL,
}).Fatal("Failed to parse ACL")
}
aclEngine.DefaultAction = acl.ActionDirect
}
// Prometheus
var trafficCounter cs.TrafficCounter
if len(config.PrometheusListen) > 0 {
promReg := prometheus.NewRegistry()
trafficCounter = NewPrometheusTrafficCounter(promReg)
go func() {
http.Handle("/metrics", promhttp.HandlerFor(promReg, promhttp.HandlerOpts{}))
err := http.ListenAndServe(config.PrometheusListen, nil)
logrus.WithField("error", err).Fatal("Prometheus HTTP server error")
}()
}
// Packet conn
pktConnFuncFactory := serverPacketConnFuncFactoryMap[config.Protocol]
if pktConnFuncFactory == nil {
logrus.WithField("protocol", config.Protocol).Fatal("Unsupported protocol")
}
pktConnFunc := pktConnFuncFactory(config.Obfs)
pktConn, err := pktConnFunc(config.Listen)
config, err := viperToServerConfig()
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"addr": config.Listen,
}).Fatal("Failed to listen on the UDP address")
logger.Fatal("failed to parse server config", zap.Error(err))
}
// Server
up, down, _ := config.Speed()
server, err := cs.NewServer(tlsConfig, quicConfig, pktConn,
transport.DefaultServerTransport, up, down, config.DisableUDP, aclEngine,
connectFunc, disconnectFunc, tcpRequestFunc, tcpErrorFunc, udpRequestFunc, udpErrorFunc, trafficCounter)
s, err := server.NewServer(config)
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize server")
logger.Fatal("failed to initialize server", zap.Error(err))
}
defer server.Close()
logrus.WithField("addr", config.Listen).Info("Server up and running")
logger.Info("server up and running")
err = server.Serve()
logrus.WithField("error", err).Fatal("Server shutdown")
}
func disconnectFunc(addr net.Addr, auth []byte, err error) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"error": err,
}).Info("Client disconnected")
}
func tcpRequestFunc(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
"action": actionToString(action, arg),
}).Debug("TCP request")
}
func tcpErrorFunc(addr net.Addr, auth []byte, reqAddr string, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
"error": err,
}).Info("TCP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"dst": defaultIPMasker.Mask(reqAddr),
}).Debug("TCP EOF")
if err := s.Serve(); err != nil {
logger.Fatal("failed to serve", zap.Error(err))
}
}
func udpRequestFunc(addr net.Addr, auth []byte, sessionID uint32) {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"session": sessionID,
}).Debug("UDP request")
}
func udpErrorFunc(addr net.Addr, auth []byte, sessionID uint32, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"session": sessionID,
"error": err,
}).Info("UDP error")
} else {
logrus.WithFields(logrus.Fields{
"src": defaultIPMasker.Mask(addr.String()),
"session": sessionID,
}).Debug("UDP EOF")
}
}
func actionToString(action acl.Action, arg string) string {
switch action {
case acl.ActionDirect:
return "Direct"
case acl.ActionProxy:
return "Proxy"
case acl.ActionBlock:
return "Block"
case acl.ActionHijack:
return "Hijack to " + arg
default:
return "Unknown"
}
}
func parseServerConfig(cb []byte) (*serverConfig, error) {
var c serverConfig
err := json5.Unmarshal(cb, &c)
func viperToServerConfig() (*server.Config, error) {
// Conn
conn, err := viperToServerConn()
if err != nil {
return nil, err
}
return &c, c.Check()
// TLS
tlsConfig, err := viperToServerTLSConfig()
if err != nil {
return nil, err
}
// QUIC
quicConfig := viperToServerQUICConfig()
// Bandwidth
bwConfig, err := viperToServerBandwidthConfig()
if err != nil {
return nil, err
}
// Disable UDP
disableUDP := viper.GetBool("disableUDP")
// Authenticator
authenticator, err := viperToAuthenticator()
if err != nil {
return nil, err
}
// Config
config := &server.Config{
TLSConfig: tlsConfig,
QUICConfig: quicConfig,
Conn: conn,
Outbound: nil, // TODO
BandwidthConfig: bwConfig,
DisableUDP: disableUDP,
Authenticator: authenticator,
EventLogger: &serverLogger{},
MasqHandler: nil, // TODO
}
return config, nil
}
func viperToServerConn() (net.PacketConn, error) {
listen := viper.GetString("listen")
if listen == "" {
return nil, configError{Field: "listen", Err: errors.New("empty listen address")}
}
uAddr, err := net.ResolveUDPAddr("udp", listen)
if err != nil {
return nil, configError{Field: "listen", Err: err}
}
conn, err := net.ListenUDP("udp", uAddr)
if err != nil {
return nil, configError{Field: "listen", Err: err}
}
return conn, nil
}
func viperToServerTLSConfig() (server.TLSConfig, error) {
vTLS, vACME := viper.Sub("tls"), viper.Sub("acme")
if vTLS == nil && vACME == nil {
return server.TLSConfig{}, configError{Field: "tls", Err: errors.New("must set either tls or acme")}
}
if vTLS != nil && vACME != nil {
return server.TLSConfig{}, configError{Field: "tls", Err: errors.New("cannot set both tls and acme")}
}
if vTLS != nil {
return viperToServerTLSConfigLocal(vTLS)
} else {
return viperToServerTLSConfigACME(vACME)
}
}
func viperToServerTLSConfigLocal(v *viper.Viper) (server.TLSConfig, error) {
certPath, keyPath := v.GetString("cert"), v.GetString("key")
if certPath == "" || keyPath == "" {
return server.TLSConfig{}, configError{Field: "tls", Err: errors.New("empty cert or key path")}
}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return server.TLSConfig{}, configError{Field: "tls", Err: err}
}
return server.TLSConfig{
Certificates: []tls.Certificate{cert},
}, nil
}
func viperToServerTLSConfigACME(v *viper.Viper) (server.TLSConfig, error) {
dataDir := v.GetString("dir")
if dataDir == "" {
dataDir = "acme"
}
cfg := &certmagic.Config{
RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio,
KeySource: certmagic.DefaultKeyGenerator,
Storage: &certmagic.FileStorage{Path: dataDir},
Logger: logger,
}
issuer := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{
Email: v.GetString("email"),
Agreed: true,
DisableHTTPChallenge: v.GetBool("disableHTTP"),
DisableTLSALPNChallenge: v.GetBool("disableTLSALPN"),
AltHTTPPort: v.GetInt("altHTTPPort"),
AltTLSALPNPort: v.GetInt("altTLSALPNPort"),
Logger: logger,
})
switch strings.ToLower(v.GetString("ca")) {
case "letsencrypt", "le", "":
// Default to Let's Encrypt
issuer.CA = certmagic.LetsEncryptProductionCA
case "zerossl", "zero":
issuer.CA = certmagic.ZeroSSLProductionCA
default:
return server.TLSConfig{}, configError{Field: "acme.ca", Err: errors.New("unknown CA")}
}
cfg.Issuers = []certmagic.Issuer{issuer}
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return cfg, nil
},
Logger: logger,
})
cfg = certmagic.New(cache, *cfg)
domains := v.GetStringSlice("domains")
if len(domains) == 0 {
return server.TLSConfig{}, configError{Field: "acme.domains", Err: errors.New("empty domains")}
}
err := cfg.ManageSync(context.Background(), domains)
if err != nil {
return server.TLSConfig{}, configError{Field: "acme", Err: err}
}
return server.TLSConfig{
GetCertificate: cfg.GetCertificate,
}, nil
}
func viperToServerQUICConfig() server.QUICConfig {
return server.QUICConfig{
InitialStreamReceiveWindow: viper.GetUint64("quic.initStreamReceiveWindow"),
MaxStreamReceiveWindow: viper.GetUint64("quic.maxStreamReceiveWindow"),
InitialConnectionReceiveWindow: viper.GetUint64("quic.initConnReceiveWindow"),
MaxConnectionReceiveWindow: viper.GetUint64("quic.maxConnReceiveWindow"),
MaxIdleTimeout: viper.GetDuration("quic.maxIdleTimeout"),
MaxIncomingStreams: viper.GetInt64("quic.maxIncomingStreams"),
DisablePathMTUDiscovery: viper.GetBool("quic.disablePathMTUDiscovery"),
}
}
func viperToServerBandwidthConfig() (server.BandwidthConfig, error) {
bw := server.BandwidthConfig{}
upStr, downStr := viper.GetString("bandwidth.up"), viper.GetString("bandwidth.down")
if upStr != "" {
up, err := convBandwidth(upStr)
if err != nil {
return server.BandwidthConfig{}, configError{Field: "bandwidth.up", Err: err}
}
bw.MaxTx = up
}
if downStr != "" {
down, err := convBandwidth(downStr)
if err != nil {
return server.BandwidthConfig{}, configError{Field: "bandwidth.down", Err: err}
}
bw.MaxRx = down
}
return bw, nil
}
func viperToAuthenticator() (server.Authenticator, error) {
authType := viper.GetString("auth.type")
if authType == "" {
return nil, configError{Field: "auth.type", Err: errors.New("empty auth type")}
}
switch authType {
case "password":
pw := viper.GetString("auth.password")
if pw == "" {
return nil, configError{Field: "auth.password", Err: errors.New("empty auth password")}
}
return &auth.PasswordAuthenticator{Password: pw}, nil
default:
return nil, configError{Field: "auth.type", Err: errors.New("unsupported auth type")}
}
}
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.Error("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) {
logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID))
}
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.Error("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err))
}
}

View file

@ -1,49 +0,0 @@
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/sirupsen/logrus"
)
const githubAPIURL = "https://api.github.com/repos/apernet/hysteria/releases/latest"
type releaseInfo struct {
URL string `json:"html_url"`
TagName string `json:"tag_name"`
CreatedAt string `json:"created_at"`
PublishedAt string `json:"published_at"`
}
func checkUpdate() {
sv := strings.Split(appVersion, "-")[0]
info, err := fetchLatestRelease()
if err == nil && info.TagName != sv {
logrus.WithFields(logrus.Fields{
"version": info.TagName,
"url": info.URL,
}).Info("New version available")
}
}
func fetchLatestRelease() (*releaseInfo, error) {
hc := &http.Client{
Timeout: time.Second * 20,
}
resp, err := hc.Get(githubAPIURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var info releaseInfo
err = json.Unmarshal(body, &info)
return &info, err
}

35
app/cmd/utils.go Normal file
View file

@ -0,0 +1,35 @@
package cmd
import (
"fmt"
"github.com/apernet/hysteria/extras/utils"
)
// 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 utils.StringToBps(bwT)
case int:
return uint64(bwT), nil
default:
return 0, fmt.Errorf("invalid type %T for bandwidth", bwT)
}
}
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
}

View file

@ -3,95 +3,56 @@ module github.com/apernet/hysteria/app
go 1.20
require (
github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000
github.com/apernet/hysteria/extras v0.0.0-00010101000000-000000000000
github.com/caddyserver/certmagic v0.17.2
github.com/docker/go-units v0.5.0
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819
github.com/elazarl/goproxy/ext v0.0.0-20221015165544-a0805db90819
github.com/folbricht/routedns v0.1.21-0.20230220022436-4ae86ce30d53
github.com/fsnotify/fsnotify v1.6.0
github.com/oschwald/geoip2-golang v1.8.0
github.com/prometheus/client_golang v1.14.0
github.com/quic-go/quic-go v0.34.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a
github.com/xjasonlyu/tun2socks/v2 v2.5.0
github.com/yosuke-furukawa/json5 v0.1.1
go.uber.org/zap v1.23.0
gvisor.dev/gvisor v0.0.0-20230401011607-0333bf067633
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
go.uber.org/zap v1.24.0
)
require (
github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-iptables v0.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f // indirect
github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jtacoma/uritemplates v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mholt/acmez v1.0.4 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/miekg/dns v1.1.51 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo/v2 v2.8.0 // indirect
github.com/oschwald/maxminddb-golang v1.10.0 // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pion/dtls/v2 v2.2.4 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport/v2 v2.0.0 // indirect
github.com/pion/udp v0.1.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/quic-go/quic-go v0.0.0-00010101000000-000000000000 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.3.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/apernet/hysteria/core => ../core/
replace github.com/quic-go/quic-go => github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473
replace github.com/LiamHaworth/go-tproxy => github.com/apernet/go-tproxy v0.0.0-20221025153553-ed04a2935f88
replace github.com/apernet/hysteria/core => ../core
replace github.com/elazarl/goproxy => github.com/apernet/goproxy v0.0.0-20221124043924-155acfaf278f
replace github.com/apernet/hysteria/extras => ../extras

View file

@ -38,33 +38,13 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91 h1:vX+gnvBc56EbWYrmlhYbFYRaeikAke1GL84N4BEYOFE=
github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91/go.mod h1:cDLGBht23g0XQdLjzn6xOGXDkLK182YfINAaZEQLCHQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/apernet/go-tproxy v0.0.0-20221025153553-ed04a2935f88 h1:YNsl7PMiU9x/0CleMHJ7GUdS8y1aRTFwTxdSmLLEijQ=
github.com/apernet/go-tproxy v0.0.0-20221025153553-ed04a2935f88/go.mod h1:uxH+nFzlJug5OHjPYmzKwvVVb9wOToeGuLNVeerwWtc=
github.com/apernet/goproxy v0.0.0-20221124043924-155acfaf278f h1:v3Bn97M5KWzdVajNphf3PxoHdsRF/RzBVovIsH/DEvY=
github.com/apernet/goproxy v0.0.0-20221124043924-155acfaf278f/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 h1:3KFetJ/lUFn0m9xTFg+rMmz2nyHg+D2boJX0Rp4OF6c=
github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
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/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=
github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -72,44 +52,24 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/elazarl/goproxy/ext v0.0.0-20221015165544-a0805db90819 h1:PBc3oUutXxwCibSLQCmpunGvruDnoS6kdnaL7a0xwKY=
github.com/elazarl/goproxy/ext v0.0.0-20221015165544-a0805db90819/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/folbricht/routedns v0.1.21-0.20230220022436-4ae86ce30d53 h1:jbMwVtjBl/cQM+l+xjGtV2C3jdpOTCZW2U3kIUf7Czg=
github.com/folbricht/routedns v0.1.21-0.20230220022436-4ae86ce30d53/go.mod h1:Kig320CyqKR4G/JQ9G5QLJaUkkGoySJjgUXRjGnnQ0I=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -137,13 +97,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -153,11 +109,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -171,8 +123,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f h1:gl1DCiSk+mrXXBGPm6CEeS2MkJuMVzAOrXg34oVj1QI=
github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@ -180,133 +132,62 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4=
github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtacoma/uritemplates v1.0.0 h1:xwx5sBF7pPAb0Uj8lDC1Q/aBPpOFyQza7OC705ZlLCo=
github.com/jtacoma/uritemplates v1.0.0/go.mod h1:IhIICdE9OcvgUnGwTtJxgBQ+VrTrti5PcbLVSJianO8=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
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.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
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/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
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/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.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI=
github.com/onsi/ginkgo/v2 v2.8.0/go.mod h1:6JsQiECmxCa3V5st74AL/AmsV482EDdVrGaVW6z3oYU=
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -314,7 +195,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@ -330,14 +210,8 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0=
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a h1:BOqgJ4jku0LHPDoR51RD8Mxmo0LHxCzJT/M9MemYdHo=
github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a/go.mod h1:7NloQcrxaZYKURWph5HLxVDlIwMHJXCPkeWPtpftsIg=
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe h1:gMWxZxBFRAXqoGkwkYlPX2zvyyKNWJpxOxCrjqJkm5A=
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4=
github.com/xjasonlyu/tun2socks/v2 v2.5.0 h1:eV/8PUZnRBd7CA3x6eE8xnHOHg072MzWMv+en+c5pPg=
github.com/xjasonlyu/tun2socks/v2 v2.5.0/go.mod h1:iDkMNAYEUDN651NClepI2eRqwUnk3qVKyex/NzubffQ=
github.com/yosuke-furukawa/json5 v0.1.1 h1:0F9mNwTvOuDNH243hoPqvf+dxa5QsKnZzU20uNsh3ZI=
github.com/yosuke-furukawa/json5 v0.1.1/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU=
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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -351,17 +225,16 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
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 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
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.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -370,9 +243,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -383,8 +255,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 h1:BEABXpNXLEz0WxtA+6CQIz2xkg80e+1zrhWyMcq8VzE=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -410,11 +282,10 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -422,7 +293,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -446,16 +316,11 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
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.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -465,8 +330,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -480,13 +343,11 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -495,7 +356,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -508,8 +368,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -517,48 +375,34 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -608,18 +452,13 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
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=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo=
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -708,31 +547,19 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.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.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ=
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=
gvisor.dev/gvisor v0.0.0-20230401011607-0333bf067633 h1:trSjCkJT04PkOY8WayMOlk9MV6SK2LqvMBRnoi9jy5E=
gvisor.dev/gvisor v0.0.0-20230401011607-0333bf067633/go.mod h1:pzr6sy8gDLfVmDAg8OYrlKvGEHw5C3PGTiBXBTCx76Q=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -1,91 +0,0 @@
package http
import (
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/apernet/hysteria/core/transport"
"github.com/apernet/hysteria/core/utils"
"github.com/elazarl/goproxy/ext/auth"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/elazarl/goproxy"
)
func NewProxyHTTPServer(hyClient *cs.Client, transport *transport.ClientTransport, idleTimeout time.Duration,
aclEngine *acl.Engine,
basicAuthFunc func(user, password string) bool,
newDialFunc func(reqAddr string, action acl.Action, arg string),
proxyErrorFunc func(reqAddr string, err error),
) (*goproxy.ProxyHttpServer, error) {
proxy := goproxy.NewProxyHttpServer()
proxy.Logger = &nopLogger{}
proxy.NonproxyHandler = http.NotFoundHandler()
proxy.Tr = &http.Transport{
Dial: func(network, addr string) (conn net.Conn, err error) {
defer func() {
if err != nil {
proxyErrorFunc(addr, err)
}
}()
// Parse addr string
host, port, err := utils.SplitHostPort(addr)
if err != nil {
return nil, err
}
// ACL
action, arg := acl.ActionProxy, ""
var ipAddr *net.IPAddr
var resErr error
if aclEngine != nil {
action, arg, _, ipAddr, resErr = aclEngine.ResolveAndMatch(host, port, false)
// Doesn't always matter if the resolution fails, as we may send it through HyClient
}
newDialFunc(addr, action, arg)
// Handle according to the action
switch action {
case acl.ActionDirect:
if resErr != nil {
return nil, resErr
}
return transport.DialTCP(&net.TCPAddr{
IP: ipAddr.IP,
Port: int(port),
Zone: ipAddr.Zone,
})
case acl.ActionProxy:
return hyClient.DialTCP(addr)
case acl.ActionBlock:
return nil, errors.New("blocked by ACL")
case acl.ActionHijack:
hijackIPAddr, err := transport.ResolveIPAddr(arg)
if err != nil {
return nil, err
}
return transport.DialTCP(&net.TCPAddr{
IP: hijackIPAddr.IP,
Port: int(port),
Zone: hijackIPAddr.Zone,
})
default:
return nil, fmt.Errorf("unknown action %d", action)
}
},
IdleConnTimeout: idleTimeout,
// Disable HTTP2 support? ref: https://github.com/elazarl/goproxy/issues/361
}
proxy.ConnectDial = nil
if basicAuthFunc != nil {
auth.ProxyBasic(proxy, "hysteria", basicAuthFunc)
}
return proxy, nil
}
type nopLogger struct{}
func (n *nopLogger) Printf(format string, v ...interface{}) {}

View file

@ -0,0 +1,294 @@
package socks5
import (
"encoding/binary"
"io"
"net"
"github.com/txthinking/socks5"
"github.com/apernet/hysteria/core/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.DialTCP(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.ListenUDP()
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
}
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
}

7
app/main.go Normal file
View file

@ -0,0 +1,7 @@
package main
import "github.com/apernet/hysteria/app/cmd"
func main() {
cmd.Execute()
}

View file

@ -1,16 +1,15 @@
import socket
import time
import socks
import time
TARGET = "1.1.1.1"
def check_tcp() -> None:
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"Sending HTTP request to {TARGET}")
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")
@ -20,26 +19,26 @@ def check_tcp() -> None:
elif not data.startswith(b"HTTP/1.1 "):
print("Invalid response received")
else:
print("Response received")
print("TCP test passed")
end = time.time()
s.close()
print(f"Time: {round((end - start) * 1000, 2)} ms")
def check_udp() -> None:
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"Sending DNS request to {TARGET}")
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 check passed")
print("UDP test passed")
else:
print("Invalid response")
print("Invalid response received")
end = time.time()
s.close()
@ -47,5 +46,5 @@ def check_udp() -> None:
if __name__ == "__main__":
check_tcp()
check_udp()
test_tcp()
test_udp()

View file

@ -1,17 +0,0 @@
//go:build !386
// +build !386
package redirect
import (
"syscall"
"unsafe"
)
func getsockopt(s uintptr, level uintptr, 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
}

View file

@ -1,22 +0,0 @@
package redirect
import (
"syscall"
"unsafe"
)
const (
SYS_GETSOCKOPT = 15
)
// we cannot call socketcall with syscall.Syscall6, it always fails with EFAULT.
// we have to call syscall.socketcall with this trick.
func syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno)
func getsockopt(s uintptr, level uintptr, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) {
_, e := syscall_socketcall(SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0)
if e != 0 {
err = e
}
return
}

View file

@ -1,33 +0,0 @@
package redirect
import (
"syscall"
"unsafe"
)
const (
SO_ORIGINAL_DST = 80
IP6T_SO_ORIGINAL_DST = 80
)
type sockAddr struct {
family uint16
port [2]byte // big endian regardless of host byte order
data [24]byte // check sockaddr_in or sockaddr_in6 for more information
}
func getOrigDst(fd uintptr) (*sockAddr, error) {
var addr sockAddr
addrSize := uint32(unsafe.Sizeof(addr))
// try IPv6 first
err := getsockopt(fd, syscall.SOL_IPV6, IP6T_SO_ORIGINAL_DST, unsafe.Pointer(&addr), &addrSize)
if err != nil {
// try IPv4
err = getsockopt(fd, syscall.SOL_IP, SO_ORIGINAL_DST, unsafe.Pointer(&addr), &addrSize)
if err != nil {
// failed
return nil, err
}
}
return &addr, nil
}

View file

@ -1,7 +0,0 @@
//go:build gc
// +build gc
#include "textflag.h"
TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36
JMP syscall·socketcall(SB)

View file

@ -1,97 +0,0 @@
package redirect
import (
"encoding/binary"
"errors"
"net"
"syscall"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/utils"
)
type TCPRedirect struct {
HyClient *cs.Client
ListenAddr *net.TCPAddr
Timeout time.Duration
ConnFunc func(addr, reqAddr net.Addr)
ErrorFunc func(addr, reqAddr net.Addr, err error)
}
func NewTCPRedirect(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPRedirect, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &TCPRedirect{
HyClient: hyClient,
ListenAddr: tAddr,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *TCPRedirect) ListenAndServe() error {
listener, err := net.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.Accept()
if err != nil {
return err
}
go func() {
defer c.Close()
dest, err := getDestAddr(c.(*net.TCPConn))
if err != nil || dest.IP.IsLoopback() {
// Silently drop the connection if we failed to get the destination address,
// or if it's a loopback address (not a redirected connection).
return
}
r.ConnFunc(c.RemoteAddr(), dest)
rc, err := r.HyClient.DialTCP(dest.String())
if err != nil {
r.ErrorFunc(c.RemoteAddr(), dest, err)
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(c, rc, r.Timeout)
r.ErrorFunc(c.RemoteAddr(), dest, err)
}()
}
}
func getDestAddr(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 = getOrigDst(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("unknown address family")
}
}

View file

@ -1,25 +0,0 @@
//go:build !linux
// +build !linux
package redirect
import (
"errors"
"net"
"time"
"github.com/apernet/hysteria/core/cs"
)
type TCPRedirect struct{}
func NewTCPRedirect(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPRedirect, error) {
return nil, errors.New("not supported on the current system")
}
func (r *TCPRedirect) ListenAndServe() error {
return nil
}

View file

@ -1,63 +0,0 @@
package relay
import (
"net"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/utils"
)
type TCPRelay struct {
HyClient *cs.Client
ListenAddr *net.TCPAddr
Remote string
Timeout time.Duration
ConnFunc func(addr net.Addr)
ErrorFunc func(addr net.Addr, err error)
}
func NewTCPRelay(hyClient *cs.Client, listen, remote string, timeout time.Duration,
connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error),
) (*TCPRelay, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &TCPRelay{
HyClient: hyClient,
ListenAddr: tAddr,
Remote: remote,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *TCPRelay) ListenAndServe() error {
listener, err := net.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.AcceptTCP()
if err != nil {
return err
}
go func() {
defer c.Close()
r.ConnFunc(c.RemoteAddr())
rc, err := r.HyClient.DialTCP(r.Remote)
if err != nil {
r.ErrorFunc(c.RemoteAddr(), err)
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(c, rc, r.Timeout)
r.ErrorFunc(c.RemoteAddr(), err)
}()
}
}

View file

@ -1,124 +0,0 @@
package relay
import (
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/apernet/hysteria/core/cs"
)
const udpBufferSize = 4096
var ErrTimeout = errors.New("inactivity timeout")
type UDPRelay struct {
HyClient *cs.Client
ListenAddr *net.UDPAddr
Remote string
Timeout time.Duration
ConnFunc func(addr net.Addr)
ErrorFunc func(addr net.Addr, err error)
}
func NewUDPRelay(hyClient *cs.Client, listen, remote string, timeout time.Duration,
connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error),
) (*UDPRelay, error) {
uAddr, err := net.ResolveUDPAddr("udp", listen)
if err != nil {
return nil, err
}
r := &UDPRelay{
HyClient: hyClient,
ListenAddr: uAddr,
Remote: remote,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
if timeout == 0 {
r.Timeout = 1 * time.Minute
}
return r, nil
}
type connEntry struct {
HyConn cs.HyUDPConn
Deadline atomic.Value
}
func (r *UDPRelay) ListenAndServe() error {
conn, err := net.ListenUDP("udp", r.ListenAddr)
if err != nil {
return err
}
defer conn.Close()
// src <-> HyClient HyUDPConn
connMap := make(map[string]*connEntry)
var connMapMutex sync.RWMutex
// Read loop
buf := make([]byte, udpBufferSize)
for {
n, rAddr, err := conn.ReadFromUDP(buf)
if n > 0 {
connMapMutex.RLock()
entry := connMap[rAddr.String()]
connMapMutex.RUnlock()
if entry != nil {
// Existing conn
entry.Deadline.Store(time.Now().Add(r.Timeout))
_ = entry.HyConn.WriteTo(buf[:n], r.Remote)
} else {
// New
r.ConnFunc(rAddr)
hyConn, err := r.HyClient.DialUDP()
if err != nil {
r.ErrorFunc(rAddr, err)
} else {
// Add it to the map
entry := &connEntry{HyConn: hyConn}
entry.Deadline.Store(time.Now().Add(r.Timeout))
connMapMutex.Lock()
connMap[rAddr.String()] = entry
connMapMutex.Unlock()
// Start remote to local
go func() {
for {
bs, _, err := hyConn.ReadFrom()
if err != nil {
break
}
entry.Deadline.Store(time.Now().Add(r.Timeout))
_, _ = conn.WriteToUDP(bs, rAddr)
}
}()
// Timeout cleanup routine
go func() {
for {
ttl := entry.Deadline.Load().(time.Time).Sub(time.Now())
if ttl <= 0 {
// Time to die
connMapMutex.Lock()
_ = hyConn.Close()
delete(connMap, rAddr.String())
connMapMutex.Unlock()
r.ErrorFunc(rAddr, ErrTimeout)
return
} else {
time.Sleep(ttl)
}
}
}()
// Send the packet
_ = hyConn.WriteTo(buf[:n], r.Remote)
}
}
}
if err != nil {
return err
}
}
}

35
app/server.example.yaml Normal file
View file

@ -0,0 +1,35 @@
listen: :443
# tls:
# cert: my.crt
# key: my.key
acme:
domains:
- my.example.com
email: hackerman@abcd.com
# ca: LetsEncrypt
# disableHTTP: false
# disableTLSALPN: false
# altHTTPPort: 80
# altTLSALPNPort: 443
# dir: "custom_dir"
# quic:
# initStreamReceiveWindow: 8388608
# maxStreamReceiveWindow: 8388608
# initConnReceiveWindow: 20971520
# maxConnReceiveWindow: 20971520
# maxIdleTimeout: 130s
# maxIncomingStreams: 1024
# disablePathMTUDiscovery: false
# bandwidth:
# up: "100 mbps"
# down: "100 mbps"
#
# disableUDP: false
auth:
type: "password"
password: "hello world"

View file

@ -1,442 +0,0 @@
package socks5
import (
"encoding/binary"
"errors"
"fmt"
"strconv"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/transport"
"github.com/apernet/hysteria/core/utils"
)
import (
"net"
"time"
"github.com/txthinking/socks5"
)
const udpBufferSize = 4096
var (
ErrUnsupportedCmd = errors.New("unsupported command")
ErrUserPassAuth = errors.New("invalid username or password")
)
type Server struct {
HyClient *cs.Client
Transport *transport.ClientTransport
AuthFunc func(username, password string) bool
Method byte
TCPAddr *net.TCPAddr
TCPTimeout time.Duration
ACLEngine *acl.Engine
DisableUDP bool
TCPRequestFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string)
TCPErrorFunc func(addr net.Addr, reqAddr string, err error)
UDPAssociateFunc func(addr net.Addr)
UDPErrorFunc func(addr net.Addr, err error)
tcpListener *net.TCPListener
}
func NewServer(hyClient *cs.Client, transport *transport.ClientTransport, addr string,
authFunc func(username, password string) bool, tcpTimeout time.Duration,
aclEngine *acl.Engine, disableUDP bool,
tcpReqFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string),
tcpErrorFunc func(addr net.Addr, reqAddr string, err error),
udpAssocFunc func(addr net.Addr), udpErrorFunc func(addr net.Addr, err error),
) (*Server, error) {
tAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
}
m := socks5.MethodNone
if authFunc != nil {
m = socks5.MethodUsernamePassword
}
s := &Server{
HyClient: hyClient,
Transport: transport,
AuthFunc: authFunc,
Method: m,
TCPAddr: tAddr,
TCPTimeout: tcpTimeout,
ACLEngine: aclEngine,
DisableUDP: disableUDP,
TCPRequestFunc: tcpReqFunc,
TCPErrorFunc: tcpErrorFunc,
UDPAssociateFunc: udpAssocFunc,
UDPErrorFunc: udpErrorFunc,
}
return s, nil
}
func (s *Server) negotiate(c *net.TCPConn) error {
rq, err := socks5.NewNegotiationRequestFrom(c)
if err != nil {
return err
}
var got bool
var m byte
for _, m = range rq.Methods {
if m == s.Method {
got = true
}
}
if !got {
rp := socks5.NewNegotiationReply(socks5.MethodUnsupportAll)
if _, err := rp.WriteTo(c); err != nil {
return err
}
}
rp := socks5.NewNegotiationReply(s.Method)
if _, err := rp.WriteTo(c); err != nil {
return err
}
if s.Method == socks5.MethodUsernamePassword {
urq, err := socks5.NewUserPassNegotiationRequestFrom(c)
if err != nil {
return err
}
if !s.AuthFunc(string(urq.Uname), string(urq.Passwd)) {
urp := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure)
if _, err := urp.WriteTo(c); err != nil {
return err
}
return ErrUserPassAuth
}
urp := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess)
if _, err := urp.WriteTo(c); err != nil {
return err
}
}
return nil
}
func (s *Server) ListenAndServe() error {
var err error
s.tcpListener, err = net.ListenTCP("tcp", s.TCPAddr)
if err != nil {
return err
}
defer s.tcpListener.Close()
for {
c, err := s.tcpListener.AcceptTCP()
if err != nil {
return err
}
go func() {
defer c.Close()
if s.TCPTimeout != 0 {
if err := c.SetDeadline(time.Now().Add(s.TCPTimeout)); err != nil {
return
}
}
if err := s.negotiate(c); err != nil {
return
}
r, err := socks5.NewRequestFrom(c)
if err != nil {
return
}
_ = s.handle(c, r)
}()
}
}
func (s *Server) handle(c *net.TCPConn, r *socks5.Request) error {
if r.Cmd == socks5.CmdConnect {
// TCP
return s.handleTCP(c, r)
} else if r.Cmd == socks5.CmdUDP {
// UDP
if !s.DisableUDP {
return s.handleUDP(c, r)
} else {
_ = sendReply(c, socks5.RepCommandNotSupported)
return ErrUnsupportedCmd
}
} else {
_ = sendReply(c, socks5.RepCommandNotSupported)
return ErrUnsupportedCmd
}
}
func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
host, port, addr := parseRequestAddress(r)
action, arg := acl.ActionProxy, ""
var ipAddr *net.IPAddr
var resErr error
if s.ACLEngine != nil {
action, arg, _, ipAddr, resErr = s.ACLEngine.ResolveAndMatch(host, port, false)
// Doesn't always matter if the resolution fails, as we may send it through HyClient
}
s.TCPRequestFunc(c.RemoteAddr(), addr, action, arg)
var closeErr error
defer func() {
s.TCPErrorFunc(c.RemoteAddr(), addr, closeErr)
}()
// Handle according to the action
switch action {
case acl.ActionDirect:
if resErr != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = resErr
return resErr
}
rc, err := s.Transport.DialTCP(&net.TCPAddr{
IP: ipAddr.IP,
Port: int(port),
Zone: ipAddr.Zone,
})
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
case acl.ActionProxy:
rc, err := s.HyClient.DialTCP(addr)
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
case acl.ActionBlock:
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = errors.New("blocked in ACL")
return nil
case acl.ActionHijack:
hijackIPAddr, err := s.Transport.ResolveIPAddr(arg)
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
rc, err := s.Transport.DialTCP(&net.TCPAddr{
IP: hijackIPAddr.IP,
Port: int(port),
Zone: hijackIPAddr.Zone,
})
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
return err
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
default:
_ = sendReply(c, socks5.RepServerFailure)
closeErr = fmt.Errorf("unknown action %d", action)
return nil
}
}
func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error {
s.UDPAssociateFunc(c.RemoteAddr())
var closeErr error
defer func() {
s.UDPErrorFunc(c.RemoteAddr(), closeErr)
}()
// Start local UDP server
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: s.TCPAddr.IP,
Zone: s.TCPAddr.Zone,
})
if err != nil {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = err
return err
}
defer udpConn.Close()
// Local UDP relay conn for ACL Direct
var localRelayConn *net.UDPConn
if s.ACLEngine != nil {
localRelayConn, err = s.Transport.ListenUDP()
if err != nil {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = err
return err
}
defer localRelayConn.Close()
}
// HyClient UDP session
hyUDP, err := s.HyClient.DialUDP()
if err != nil {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = err
return err
}
defer hyUDP.Close()
// Send UDP server addr to the client
// Same IP as TCP but a different port
tcpLocalAddr := c.LocalAddr().(*net.TCPAddr)
var atyp byte
var addr, port []byte
if ip4 := tcpLocalAddr.IP.To4(); ip4 != nil {
atyp = socks5.ATYPIPv4
addr = ip4
} else if ip6 := tcpLocalAddr.IP.To16(); ip6 != nil {
atyp = socks5.ATYPIPv6
addr = ip6
} else {
_ = sendReply(c, socks5.RepServerFailure)
closeErr = errors.New("invalid local addr")
return closeErr
}
port = make([]byte, 2)
binary.BigEndian.PutUint16(port, uint16(udpConn.LocalAddr().(*net.UDPAddr).Port))
_, _ = socks5.NewReply(socks5.RepSuccess, atyp, addr, port).WriteTo(c)
// Let UDP server do its job, we hold the TCP connection here
go s.udpServer(udpConn, localRelayConn, hyUDP)
if s.TCPTimeout != 0 {
// Disable TCP timeout for UDP holder
_ = c.SetDeadline(time.Time{})
}
buf := make([]byte, 1024)
for {
_, err := c.Read(buf)
if err != nil {
closeErr = err
break
}
}
// As the TCP connection closes, so does the UDP server & HyClient session
return nil
}
func (s *Server) udpServer(clientConn *net.UDPConn, localRelayConn *net.UDPConn, hyUDP cs.HyUDPConn) {
var clientAddr *net.UDPAddr
buf := make([]byte, udpBufferSize)
// Local to remote
for {
n, cAddr, err := clientConn.ReadFromUDP(buf)
if err != nil {
break
}
d, err := socks5.NewDatagramFromBytes(buf[:n])
if err != nil || d.Frag != 0 {
// Ignore bad packets
continue
}
if clientAddr == nil {
// Whoever sends the first valid packet is our client
clientAddr = cAddr
// Start remote to local
go func() {
for {
bs, from, err := hyUDP.ReadFrom()
if err != nil {
break
}
atyp, addr, port, err := socks5.ParseAddress(from)
if err != nil {
continue
}
d := socks5.NewDatagram(atyp, addr, port, bs)
_, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr)
}
}()
if localRelayConn != nil {
go func() {
buf := make([]byte, udpBufferSize)
for {
n, from, err := localRelayConn.ReadFrom(buf)
if n > 0 {
atyp, addr, port, err := socks5.ParseAddress(from.String())
if err != nil {
continue
}
d := socks5.NewDatagram(atyp, addr, port, buf[:n])
_, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr)
}
if err != nil {
break
}
}
}()
}
} else if cAddr.String() != clientAddr.String() {
// Not our client, bye
continue
}
host, port, addr := parseDatagramRequestAddress(d)
action, arg := acl.ActionProxy, ""
var ipAddr *net.IPAddr
var resErr error
if s.ACLEngine != nil && localRelayConn != nil {
action, arg, _, ipAddr, resErr = s.ACLEngine.ResolveAndMatch(host, port, true)
// Doesn't always matter if the resolution fails, as we may send it through HyClient
}
// Handle according to the action
switch action {
case acl.ActionDirect:
if resErr != nil {
return
}
_, _ = localRelayConn.WriteToUDP(d.Data, &net.UDPAddr{
IP: ipAddr.IP,
Port: int(port),
Zone: ipAddr.Zone,
})
case acl.ActionProxy:
_ = hyUDP.WriteTo(d.Data, addr)
case acl.ActionBlock:
// Do nothing
case acl.ActionHijack:
hijackIPAddr, err := s.Transport.ResolveIPAddr(arg)
if err == nil {
_, _ = localRelayConn.WriteToUDP(d.Data, &net.UDPAddr{
IP: hijackIPAddr.IP,
Port: int(port),
Zone: hijackIPAddr.Zone,
})
}
default:
// Do nothing
}
}
}
func sendReply(conn *net.TCPConn, rep byte) error {
p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00})
_, err := p.WriteTo(conn)
return err
}
func parseRequestAddress(r *socks5.Request) (host string, port uint16, addr string) {
p := binary.BigEndian.Uint16(r.DstPort)
if r.Atyp == socks5.ATYPDomain {
d := string(r.DstAddr[1:])
return d, p, net.JoinHostPort(d, strconv.Itoa(int(p)))
} else {
ipStr := net.IP(r.DstAddr).String()
return ipStr, p, net.JoinHostPort(ipStr, strconv.Itoa(int(p)))
}
}
func parseDatagramRequestAddress(r *socks5.Datagram) (host string, port uint16, addr string) {
p := binary.BigEndian.Uint16(r.DstPort)
if r.Atyp == socks5.ATYPDomain {
d := string(r.DstAddr[1:])
return d, p, net.JoinHostPort(d, strconv.Itoa(int(p)))
} else {
ipStr := net.IP(r.DstAddr).String()
return ipStr, p, net.JoinHostPort(ipStr, strconv.Itoa(int(p)))
}
}

View file

@ -1,66 +0,0 @@
package tproxy
import (
"net"
"time"
"github.com/LiamHaworth/go-tproxy"
"github.com/apernet/hysteria/core/cs"
"github.com/apernet/hysteria/core/utils"
)
type TCPTProxy struct {
HyClient *cs.Client
ListenAddr *net.TCPAddr
Timeout time.Duration
ConnFunc func(addr, reqAddr net.Addr)
ErrorFunc func(addr, reqAddr net.Addr, err error)
}
func NewTCPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPTProxy, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &TCPTProxy{
HyClient: hyClient,
ListenAddr: tAddr,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *TCPTProxy) ListenAndServe() error {
listener, err := tproxy.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.Accept()
if err != nil {
return err
}
go func() {
defer c.Close()
// Under TPROXY mode, we are effectively acting as the remote server
// So our LocalAddr is actually the target to which the user is trying to connect
// and our RemoteAddr is the local address where the user initiates the connection
r.ConnFunc(c.RemoteAddr(), c.LocalAddr())
rc, err := r.HyClient.DialTCP(c.LocalAddr().String())
if err != nil {
r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err)
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(c, rc, r.Timeout)
r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err)
}()
}
}

View file

@ -1,25 +0,0 @@
//go:build !linux
// +build !linux
package tproxy
import (
"errors"
"net"
"time"
"github.com/apernet/hysteria/core/cs"
)
type TCPTProxy struct{}
func NewTCPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*TCPTProxy, error) {
return nil, errors.New("not supported on the current system")
}
func (r *TCPTProxy) ListenAndServe() error {
return nil
}

View file

@ -1,115 +0,0 @@
package tproxy
import (
"net"
"time"
"github.com/LiamHaworth/go-tproxy"
"github.com/apernet/hysteria/core/cs"
)
const udpBufferSize = 4096
type UDPTProxy struct {
HyClient *cs.Client
ListenAddr *net.UDPAddr
Timeout time.Duration
ConnFunc func(addr, reqAddr net.Addr)
ErrorFunc func(addr, reqAddr net.Addr, err error)
}
func NewUDPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error),
) (*UDPTProxy, error) {
uAddr, err := net.ResolveUDPAddr("udp", listen)
if err != nil {
return nil, err
}
r := &UDPTProxy{
HyClient: hyClient,
ListenAddr: uAddr,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
if timeout == 0 {
r.Timeout = 1 * time.Minute
}
return r, nil
}
func (r *UDPTProxy) ListenAndServe() error {
conn, err := tproxy.ListenUDP("udp", r.ListenAddr)
if err != nil {
return err
}
defer conn.Close()
// Read loop
buf := make([]byte, udpBufferSize)
for {
n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf) // Huge Caveat!! This essentially works as TCP's Accept here - won't repeat for the same srcAddr/dstAddr pair - because and only because we have tproxy.DialUDP("udp", dstAddr, srcAddr) to take over the connection below
if n > 0 {
r.ConnFunc(srcAddr, dstAddr)
localConn, err := tproxy.DialUDP("udp", dstAddr, srcAddr)
if err != nil {
r.ErrorFunc(srcAddr, dstAddr, err)
continue
}
hyConn, err := r.HyClient.DialUDP()
if err != nil {
r.ErrorFunc(srcAddr, dstAddr, err)
_ = localConn.Close()
continue
}
_ = hyConn.WriteTo(buf[:n], dstAddr.String())
errChan := make(chan error, 2)
// Start remote to local
go func() {
for {
bs, _, err := hyConn.ReadFrom()
if err != nil {
errChan <- err
return
}
_, err = localConn.Write(bs)
if err != nil {
errChan <- err
return
}
_ = localConn.SetDeadline(time.Now().Add(r.Timeout))
}
}()
// Start local to remote
go func() {
for {
_ = localConn.SetDeadline(time.Now().Add(r.Timeout))
n, err := localConn.Read(buf)
if n > 0 {
err := hyConn.WriteTo(buf[:n], dstAddr.String())
if err != nil {
errChan <- err
return
}
}
if err != nil {
errChan <- err
return
}
}
}()
// Error cleanup routine
go func() {
err := <-errChan
_ = localConn.Close()
_ = hyConn.Close()
r.ErrorFunc(srcAddr, dstAddr, err)
}()
}
if err != nil {
return err
}
}
}

View file

@ -1,26 +0,0 @@
//go:build !linux
// +build !linux
package tproxy
import (
"errors"
"net"
"time"
"github.com/apernet/hysteria/core/cs"
)
var ErrTimeout = errors.New("inactivity timeout")
type UDPTProxy struct{}
func NewUDPTProxy(hyClient *cs.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr), errorFunc func(addr, reqAddr net.Addr, err error),
) (*UDPTProxy, error) {
return nil, errors.New("not supported on the current system")
}
func (r *UDPTProxy) ListenAndServe() error {
return nil
}

View file

@ -1,156 +0,0 @@
//go:build gpl
// +build gpl
package tun
import (
"fmt"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/xjasonlyu/tun2socks/v2/core/option"
"github.com/apernet/hysteria/core/cs"
t2score "github.com/xjasonlyu/tun2socks/v2/core"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
"github.com/xjasonlyu/tun2socks/v2/core/device"
"github.com/xjasonlyu/tun2socks/v2/core/device/fdbased"
"github.com/xjasonlyu/tun2socks/v2/core/device/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
var _ adapter.TransportHandler = (*Server)(nil)
type Server struct {
HyClient *cs.Client
Timeout time.Duration
DeviceInfo DeviceInfo
RequestFunc func(addr net.Addr, reqAddr string)
ErrorFunc func(addr net.Addr, reqAddr string, err error)
}
const (
MTU = 1500
)
const (
DeviceTypeFd = iota
DeviceTypeName
)
type DeviceInfo struct {
Type int
Fd int
Name string
MTU uint32
TCPSendBufferSize int
TCPReceiveBufferSize int
TCPModerateReceiveBuffer bool
}
func (d *DeviceInfo) Open() (dev device.Device, err error) {
switch d.Type {
case DeviceTypeFd:
dev, err = fdbased.Open(strconv.Itoa(d.Fd), d.MTU)
case DeviceTypeName:
dev, err = tun.Open(d.Name, d.MTU)
default:
err = fmt.Errorf("unknown device type: %d", d.Type)
}
return
}
func NewServerWithTunFd(hyClient *cs.Client, timeout time.Duration, tunFd int, mtu uint32,
tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool,
) (*Server, error) {
if mtu == 0 {
mtu = MTU
}
s := &Server{
HyClient: hyClient,
Timeout: timeout,
DeviceInfo: DeviceInfo{
Type: DeviceTypeFd,
Fd: tunFd,
MTU: mtu,
TCPSendBufferSize: tcpSendBufferSize,
TCPReceiveBufferSize: tcpReceiveBufferSize,
TCPModerateReceiveBuffer: tcpModerateReceiveBuffer,
},
}
return s, nil
}
func NewServer(hyClient *cs.Client, timeout time.Duration, name string, mtu uint32,
tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool,
) (*Server, error) {
if mtu == 0 {
mtu = MTU
}
s := &Server{
HyClient: hyClient,
Timeout: timeout,
DeviceInfo: DeviceInfo{
Type: DeviceTypeName,
Name: name,
MTU: mtu,
TCPSendBufferSize: tcpSendBufferSize,
TCPReceiveBufferSize: tcpReceiveBufferSize,
TCPModerateReceiveBuffer: tcpModerateReceiveBuffer,
},
}
return s, nil
}
func (s *Server) ListenAndServe() error {
var dev device.Device
var st *stack.Stack
defer func() {
if dev != nil {
_ = dev.Close()
}
if st != nil {
st.Close()
st.Wait()
}
}()
dev, err := s.DeviceInfo.Open()
if err != nil {
return err
}
var opts []option.Option
if s.DeviceInfo.TCPSendBufferSize > 0 {
opts = append(opts, option.WithTCPSendBufferSize(s.DeviceInfo.TCPSendBufferSize))
}
if s.DeviceInfo.TCPReceiveBufferSize > 0 {
opts = append(opts, option.WithTCPReceiveBufferSize(s.DeviceInfo.TCPReceiveBufferSize))
}
if s.DeviceInfo.TCPModerateReceiveBuffer {
opts = append(opts, option.WithTCPModerateReceiveBuffer(s.DeviceInfo.TCPModerateReceiveBuffer))
}
t2sconf := t2score.Config{
LinkEndpoint: dev,
TransportHandler: s,
Options: opts,
}
st, err = t2score.CreateStack(&t2sconf)
if err != nil {
return err
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
return nil
}

View file

@ -1,48 +0,0 @@
//go:build gpl
// +build gpl
package tun
import (
"net"
"github.com/apernet/hysteria/core/utils"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
)
func (s *Server) HandleTCP(localConn adapter.TCPConn) {
go s.handleTCPConn(localConn)
}
func (s *Server) handleTCPConn(localConn adapter.TCPConn) {
defer localConn.Close()
id := localConn.ID()
remoteAddr := net.TCPAddr{
IP: net.IP(id.LocalAddress),
Port: int(id.LocalPort),
}
localAddr := net.TCPAddr{
IP: net.IP(id.RemoteAddress),
Port: int(id.RemotePort),
}
if s.RequestFunc != nil {
s.RequestFunc(&localAddr, remoteAddr.String())
}
var err error
defer func() {
if s.ErrorFunc != nil && err != nil {
s.ErrorFunc(&localAddr, remoteAddr.String(), err)
}
}()
rc, err := s.HyClient.DialTCP(remoteAddr.String())
if err != nil {
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(localConn, rc, s.Timeout)
}

View file

@ -1,114 +0,0 @@
//go:build gpl
// +build gpl
package tun
import (
"fmt"
"net"
"strconv"
"time"
"github.com/apernet/hysteria/core/cs"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
)
const udpBufferSize = 4096
func (s *Server) HandleUDP(conn adapter.UDPConn) {
go s.handleUDPConn(conn)
}
func (s *Server) handleUDPConn(conn adapter.UDPConn) {
defer conn.Close()
id := conn.ID()
remoteAddr := net.UDPAddr{
IP: net.IP(id.LocalAddress),
Port: int(id.LocalPort),
}
localAddr := net.UDPAddr{
IP: net.IP(id.RemoteAddress),
Port: int(id.RemotePort),
}
if s.RequestFunc != nil {
s.RequestFunc(&localAddr, remoteAddr.String())
}
var err error
defer func() {
if s.ErrorFunc != nil && err != nil {
s.ErrorFunc(&localAddr, remoteAddr.String(), err)
}
}()
rc, err := s.HyClient.DialUDP()
if err != nil {
return
}
defer rc.Close()
err = s.relayUDP(conn, rc, &remoteAddr, s.Timeout)
}
func (s *Server) relayUDP(lc adapter.UDPConn, rc cs.HyUDPConn, to *net.UDPAddr, timeout time.Duration) (err error) {
errChan := make(chan error, 2)
// local => remote
go func() {
buf := make([]byte, udpBufferSize)
for {
if timeout != 0 {
_ = lc.SetDeadline(time.Now().Add(timeout))
n, err := lc.Read(buf)
if n > 0 {
err = rc.WriteTo(buf[:n], to.String())
if err != nil {
errChan <- err
return
}
}
if err != nil {
errChan <- err
return
}
}
}
}()
// remote => local
go func() {
for {
pkt, addr, err := rc.ReadFrom()
if err != nil {
errChan <- err
return
}
if pkt != nil {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
errChan <- err
return
}
port, err := strconv.Atoi(portStr)
if err != nil {
errChan <- fmt.Errorf("cannot parse as port: %s", portStr)
return
}
// adapter.UDPConn doesn't support WriteFrom() yet,
// so we check the src address and behavior like a symmetric NAT
if !to.IP.Equal(net.ParseIP(host)) || to.Port != port {
// drop the packet silently
continue
}
_, err = lc.Write(pkt)
if err != nil {
errChan <- err
return
}
}
}
}()
return <-errChan
}

113
build.ps1
View file

@ -1,113 +0,0 @@
# Hysteria build script for Windows (PowerShell)
# Environment variable options:
# - HY_APP_VERSION: App version
# - HY_APP_COMMIT: App commit hash
# - HY_APP_PLATFORMS: Platforms to build for (e.g. "windows/amd64,linux/amd64,darwin/amd64")
function PlatformToEnv($os, $arch) {
$env:CGO_ENABLED = 0
$env:GOOS = $os
$env:GOARCH = $arch
switch -Regex ($arch) {
"arm" {
$env:GOARM = "7"
}
"armv5" {
$env:GOARM = "5"
$env:GOARCH = "arm"
}
"armv6" {
$env:GOARM = "6"
$env:GOARCH = "arm"
}
"armv7" {
$env:GOARM = "7"
$env:GOARCH = "arm"
}
"mips(le)?" {
$env:GOMIPS = ""
}
"mips-sf" {
$env:GOMIPS = "softfloat"
$env:GOARCH = "mips"
}
"mipsle-sf" {
$env:GOMIPS = "softfloat"
$env:GOARCH = "mipsle"
}
"amd64" {
$env:GOAMD64 = ""
$env:GOARCH = "amd64"
}
"amd64-avx" {
$env:GOAMD64 = "v3"
$env:GOARCH = "amd64"
}
}
}
if (!(Get-Command go -ErrorAction SilentlyContinue)) {
Write-Host "Error: go is not installed." -ForegroundColor Red
exit 1
}
if (!(Get-Command git -ErrorAction SilentlyContinue)) {
Write-Host "Error: git is not installed." -ForegroundColor Red
exit 1
}
if (!(git rev-parse --is-inside-work-tree 2>$null)) {
Write-Host "Error: not in a git repository." -ForegroundColor Red
exit 1
}
$ldflags = "-s -w -X 'main.appDate=$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")'"
if ($env:HY_APP_VERSION) {
$ldflags += " -X 'main.appVersion=$($env:HY_APP_VERSION)'"
}
else {
$ldflags += " -X 'main.appVersion=$(git describe --tags --always --match "v*")'"
}
if ($env:HY_APP_COMMIT) {
$ldflags += " -X 'main.appCommit=$($env:HY_APP_COMMIT)'"
}
else {
$ldflags += " -X 'main.appCommit=$(git rev-parse HEAD)'"
}
if ($env:HY_APP_PLATFORMS) {
$platforms = $env:HY_APP_PLATFORMS.Split(",")
}
else {
$goos = go env GOOS
$goarch = go env GOARCH
$platforms = @("$goos/$goarch")
}
if (Test-Path build) {
Remove-Item -Recurse -Force build
}
New-Item -ItemType Directory -Force -Path build
Write-Host "Starting build..." -ForegroundColor Green
foreach ($platform in $platforms) {
$os = $platform.Split("/")[0]
$arch = $platform.Split("/")[1]
PlatformToEnv $os $arch
Write-Host "Building $os/$arch" -ForegroundColor Green
$output = "build/hysteria-$os-$arch"
if ($os -eq "windows") {
$output = "$output.exe"
}
go build -o $output -tags=gpl -ldflags $ldflags -trimpath ./app/cmd/
if ($LastExitCode -ne 0) {
Write-Host "Error: failed to build $os/$arch" -ForegroundColor Red
exit 1
}
}
Write-Host "Build complete." -ForegroundColor Green
Get-ChildItem -Path build | Format-Table -AutoSize

130
build.sh
View file

@ -1,130 +0,0 @@
#!/usr/bin/env bash
set -e
# Hysteria build script for Linux
# Environment variable options:
# - HY_APP_VERSION: App version
# - HY_APP_COMMIT: App commit hash
# - HY_APP_PLATFORMS: Platforms to build for (e.g. "windows/amd64,linux/amd64,darwin/amd64")
export LC_ALL=C
export LC_DATE=C
has_command() {
local cmd="$1"
type -P "$cmd" > /dev/null 2>&1
}
if ! has_command go; then
echo 'Error: go is not installed.' >&2
exit 1
fi
if ! has_command git; then
echo 'Error: git is not installed.' >&2
exit 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo 'Error: not in a git repository.' >&2
exit 1
fi
platform_to_env() {
local os="$1"
local arch="$2"
local env="GOOS=$os GOARCH=$arch CGO_ENABLED=0"
case "$arch" in
arm)
env+=" GOARM=7 GOARCH=arm"
;;
armv5)
env+=" GOARM=5 GOARCH=arm"
;;
armv6)
env+=" GOARM=6 GOARCH=arm"
;;
armv7)
env+=" GOARM=7 GOARCH=arm"
;;
mips | mipsle)
env+=" GOMIPS="
;;
mips-sf)
env+=" GOMIPS=softfloat GOARCH=mips"
;;
mipsle-sf)
env+=" GOMIPS=softfloat GOARCH=mipsle"
;;
amd64)
env+=" GOAMD64= GOARCH=amd64"
;;
amd64-avx)
env+=" GOAMD64=v3 GOARCH=amd64"
;;
esac
echo "$env"
}
make_ldflags() {
local ldflags="-s -w -X 'main.appDate=$(date -u '+%F %T')'"
if [ -n "$HY_APP_VERSION" ]; then
ldflags="$ldflags -X 'main.appVersion=$HY_APP_VERSION'"
else
ldflags="$ldflags -X 'main.appVersion=$(git describe --tags --always --match 'v*')'"
fi
if [ -n "$HY_APP_COMMIT" ]; then
ldflags="$ldflags -X 'main.appCommit=$HY_APP_COMMIT'"
else
ldflags="$ldflags -X 'main.appCommit=$(git rev-parse HEAD)'"
fi
echo "$ldflags"
}
build_for_platform() {
local platform="$1"
local ldflags="$2"
local GOOS="${platform%/*}"
local GOARCH="${platform#*/}"
if [[ -z "$GOOS" || -z "$GOARCH" ]]; then
echo "Invalid platform $platform" >&2
return 1
fi
echo "Building $GOOS/$GOARCH"
local output="build/hysteria-$GOOS-$GOARCH"
if [[ "$GOOS" = "windows" ]]; then
output="$output.exe"
fi
local envs="$(platform_to_env "$GOOS" "$GOARCH")"
local exit_val=0
env $envs go build -o "$output" -tags=gpl -ldflags "$ldflags" -trimpath ./app/cmd/ || exit_val=$?
if [[ "$exit_val" -ne 0 ]]; then
echo "Error: failed to build $GOOS/$GOARCH" >&2
return $exit_val
fi
}
if [ -z "$HY_APP_PLATFORMS" ]; then
HY_APP_PLATFORMS="$(go env GOOS)/$(go env GOARCH)"
fi
platforms=(${HY_APP_PLATFORMS//,/ })
ldflags="$(make_ldflags)"
mkdir -p build
rm -rf build/*
echo "Starting build..."
for platform in "${platforms[@]}"; do
build_for_platform "$platform" "$ldflags"
done
echo "Build complete."
ls -lh build/ | awk '{print $9, $5}'

View file

@ -1,143 +0,0 @@
package acl
import (
"bufio"
"net"
"os"
"strings"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/apernet/hysteria/core/utils"
"github.com/oschwald/geoip2-golang"
)
const entryCacheSize = 1024
type Engine struct {
DefaultAction Action
Entries []Entry
Cache *lru.ARCCache[cacheKey, cacheValue]
ResolveIPAddr func(string) (*net.IPAddr, error)
GeoIPReader *geoip2.Reader
}
type cacheKey struct {
Host string
Port uint16
IsUDP bool
}
type cacheValue struct {
Action Action
Arg string
}
func LoadFromFile(filename string, resolveIPAddr func(string) (*net.IPAddr, error), geoIPLoadFunc func() (*geoip2.Reader, error)) (*Engine, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
entries := make([]Entry, 0, 1024)
var geoIPReader *geoip2.Reader
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) == 0 || strings.HasPrefix(line, "#") {
// Ignore empty lines & comments
continue
}
entry, err := ParseEntry(line)
if err != nil {
return nil, err
}
if _, ok := entry.Matcher.(*countryMatcher); ok && geoIPReader == nil {
geoIPReader, err = geoIPLoadFunc() // lazy load GeoIP reader only when needed
if err != nil {
return nil, err
}
}
entries = append(entries, entry)
}
cache, err := lru.NewARC[cacheKey, cacheValue](entryCacheSize)
if err != nil {
return nil, err
}
return &Engine{
DefaultAction: ActionProxy,
Entries: entries,
Cache: cache,
ResolveIPAddr: resolveIPAddr,
GeoIPReader: geoIPReader,
}, nil
}
// action, arg, isDomain, resolvedIP, error
func (e *Engine) ResolveAndMatch(host string, port uint16, isUDP bool) (Action, string, bool, *net.IPAddr, error) {
ip, zone := utils.ParseIPZone(host)
if ip == nil {
// Domain
ipAddr, err := e.ResolveIPAddr(host)
if ce, ok := e.Cache.Get(cacheKey{host, port, isUDP}); ok {
// Cache hit
return ce.Action, ce.Arg, true, ipAddr, err
}
for _, entry := range e.Entries {
mReq := MatchRequest{
Domain: host,
Port: port,
DB: e.GeoIPReader,
}
if ipAddr != nil {
mReq.IP = ipAddr.IP
}
if isUDP {
mReq.Protocol = ProtocolUDP
} else {
mReq.Protocol = ProtocolTCP
}
if entry.Match(mReq) {
e.Cache.Add(cacheKey{host, port, isUDP},
cacheValue{entry.Action, entry.ActionArg})
return entry.Action, entry.ActionArg, true, ipAddr, err
}
}
e.Cache.Add(cacheKey{host, port, isUDP}, cacheValue{e.DefaultAction, ""})
return e.DefaultAction, "", true, ipAddr, err
} else {
// IP
if ce, ok := e.Cache.Get(cacheKey{ip.String(), port, isUDP}); ok {
// Cache hit
return ce.Action, ce.Arg, false, &net.IPAddr{
IP: ip,
Zone: zone,
}, nil
}
for _, entry := range e.Entries {
mReq := MatchRequest{
IP: ip,
Port: port,
DB: e.GeoIPReader,
}
if isUDP {
mReq.Protocol = ProtocolUDP
} else {
mReq.Protocol = ProtocolTCP
}
if entry.Match(mReq) {
e.Cache.Add(cacheKey{ip.String(), port, isUDP},
cacheValue{entry.Action, entry.ActionArg})
return entry.Action, entry.ActionArg, false, &net.IPAddr{
IP: ip,
Zone: zone,
}, nil
}
}
e.Cache.Add(cacheKey{ip.String(), port, isUDP}, cacheValue{e.DefaultAction, ""})
return e.DefaultAction, "", false, &net.IPAddr{
IP: ip,
Zone: zone,
}, nil
}
}

View file

@ -1,155 +0,0 @@
package acl
import (
"errors"
"net"
"strings"
"testing"
lru "github.com/hashicorp/golang-lru/v2"
)
func TestEngine_ResolveAndMatch(t *testing.T) {
cache, _ := lru.NewARC[cacheKey, cacheValue](entryCacheSize)
e := &Engine{
DefaultAction: ActionDirect,
Entries: []Entry{
{
Action: ActionProxy,
ActionArg: "",
Matcher: &domainMatcher{
matcherBase: matcherBase{
Protocol: ProtocolTCP,
Port: 443,
},
Domain: "google.com",
Suffix: false,
},
},
{
Action: ActionHijack,
ActionArg: "good.org",
Matcher: &domainMatcher{
matcherBase: matcherBase{},
Domain: "evil.corp",
Suffix: true,
},
},
{
Action: ActionProxy,
ActionArg: "",
Matcher: &netMatcher{
matcherBase: matcherBase{},
Net: &net.IPNet{
IP: net.ParseIP("10.0.0.0"),
Mask: net.CIDRMask(8, 32),
},
},
},
{
Action: ActionBlock,
ActionArg: "",
Matcher: &allMatcher{},
},
},
Cache: cache,
ResolveIPAddr: func(s string) (*net.IPAddr, error) {
if strings.Contains(s, "evil.corp") {
return nil, errors.New("resolve error")
}
return net.ResolveIPAddr("ip", s)
},
}
tests := []struct {
name string
host string
port uint16
isUDP bool
wantAction Action
wantArg string
wantErr bool
}{
{
name: "domain proxy",
host: "google.com",
port: 443,
isUDP: false,
wantAction: ActionProxy,
wantArg: "",
},
{
name: "domain block",
host: "google.com",
port: 80,
isUDP: false,
wantAction: ActionBlock,
wantArg: "",
},
{
name: "domain suffix 1",
host: "evil.corp",
port: 8899,
isUDP: true,
wantAction: ActionHijack,
wantArg: "good.org",
wantErr: true,
},
{
name: "domain suffix 2",
host: "notevil.corp",
port: 22,
isUDP: false,
wantAction: ActionBlock,
wantArg: "",
wantErr: true,
},
{
name: "domain suffix 3",
host: "im.real.evil.corp",
port: 443,
isUDP: true,
wantAction: ActionHijack,
wantArg: "good.org",
wantErr: true,
},
{
name: "ip match",
host: "10.2.3.4",
port: 80,
isUDP: false,
wantAction: ActionProxy,
wantArg: "",
},
{
name: "ip mismatch",
host: "100.5.6.0",
port: 1234,
isUDP: false,
wantAction: ActionBlock,
wantArg: "",
},
{
name: "domain proxy cache",
host: "google.com",
port: 443,
isUDP: false,
wantAction: ActionProxy,
wantArg: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotAction, gotArg, _, _, err := e.ResolveAndMatch(tt.host, tt.port, tt.isUDP)
if (err != nil) != tt.wantErr {
t.Errorf("ResolveAndMatch() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotAction != tt.wantAction {
t.Errorf("ResolveAndMatch() gotAction = %v, wantAction %v", gotAction, tt.wantAction)
}
if gotArg != tt.wantArg {
t.Errorf("ResolveAndMatch() gotArg = %v, wantAction %v", gotArg, tt.wantArg)
}
})
}
}

View file

@ -1,334 +0,0 @@
package acl
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/oschwald/geoip2-golang"
)
type (
Action byte
Protocol byte
)
const (
ActionDirect = Action(iota)
ActionProxy
ActionBlock
ActionHijack
)
const (
ProtocolAll = Protocol(iota)
ProtocolTCP
ProtocolUDP
)
var protocolPortAliases = map[string]string{
"echo": "*/7",
"ftp-data": "*/20",
"ftp": "*/21",
"ssh": "*/22",
"telnet": "*/23",
"domain": "*/53",
"dns": "*/53",
"http": "*/80",
"sftp": "*/115",
"ntp": "*/123",
"https": "*/443",
"quic": "udp/443",
"socks": "*/1080",
}
type Entry struct {
Action Action
ActionArg string
Matcher Matcher
}
type MatchRequest struct {
IP net.IP
Domain string
Protocol Protocol
Port uint16
DB *geoip2.Reader
}
type Matcher interface {
Match(MatchRequest) bool
}
type matcherBase struct {
Protocol Protocol
Port uint16 // 0 for all ports
}
func (m *matcherBase) MatchProtocolPort(p Protocol, port uint16) bool {
return (m.Protocol == ProtocolAll || m.Protocol == p) && (m.Port == 0 || m.Port == port)
}
func parseProtocolPort(s string) (Protocol, uint16, error) {
if protocolPortAliases[s] != "" {
s = protocolPortAliases[s]
}
if len(s) == 0 || s == "*" {
return ProtocolAll, 0, nil
}
parts := strings.Split(s, "/")
if len(parts) != 2 {
return ProtocolAll, 0, errors.New("invalid protocol/port syntax")
}
protocol := ProtocolAll
switch parts[0] {
case "tcp":
protocol = ProtocolTCP
case "udp":
protocol = ProtocolUDP
case "*":
protocol = ProtocolAll
default:
return ProtocolAll, 0, errors.New("invalid protocol")
}
if parts[1] == "*" {
return protocol, 0, nil
}
port, err := strconv.ParseUint(parts[1], 10, 16)
if err != nil {
return ProtocolAll, 0, errors.New("invalid port")
}
return protocol, uint16(port), nil
}
type netMatcher struct {
matcherBase
Net *net.IPNet
}
func (m *netMatcher) Match(r MatchRequest) bool {
if r.IP == nil {
return false
}
return m.Net.Contains(r.IP) && m.MatchProtocolPort(r.Protocol, r.Port)
}
type domainMatcher struct {
matcherBase
Domain string
Suffix bool
}
func (m *domainMatcher) Match(r MatchRequest) bool {
if len(r.Domain) == 0 {
return false
}
domain := strings.ToLower(r.Domain)
return (m.Domain == domain || (m.Suffix && strings.HasSuffix(domain, "."+m.Domain))) &&
m.MatchProtocolPort(r.Protocol, r.Port)
}
type countryMatcher struct {
matcherBase
Country string // ISO 3166-1 alpha-2 country code, upper case
}
func (m *countryMatcher) Match(r MatchRequest) bool {
if r.IP == nil || r.DB == nil {
return false
}
c, err := r.DB.Country(r.IP)
if err != nil {
return false
}
return c.Country.IsoCode == m.Country && m.MatchProtocolPort(r.Protocol, r.Port)
}
type allMatcher struct {
matcherBase
}
func (m *allMatcher) Match(r MatchRequest) bool {
return m.MatchProtocolPort(r.Protocol, r.Port)
}
func (e Entry) Match(r MatchRequest) bool {
return e.Matcher.Match(r)
}
func ParseEntry(s string) (Entry, error) {
fields := strings.Fields(s)
if len(fields) < 2 {
return Entry{}, fmt.Errorf("expected at least 2 fields, got %d", len(fields))
}
e := Entry{}
action := fields[0]
conds := fields[1:]
switch strings.ToLower(action) {
case "direct":
e.Action = ActionDirect
case "proxy":
e.Action = ActionProxy
case "block":
e.Action = ActionBlock
case "hijack":
if len(conds) < 2 {
return Entry{}, fmt.Errorf("hijack requires at least 3 fields, got %d", len(fields))
}
e.Action = ActionHijack
e.ActionArg = conds[len(conds)-1]
conds = conds[:len(conds)-1]
default:
return Entry{}, fmt.Errorf("invalid action %s", fields[0])
}
m, err := condsToMatcher(conds)
if err != nil {
return Entry{}, err
}
e.Matcher = m
return e, nil
}
func condsToMatcher(conds []string) (Matcher, error) {
if len(conds) < 1 {
return nil, errors.New("no condition specified")
}
typ, args := conds[0], conds[1:]
switch strings.ToLower(typ) {
case "domain":
// domain <domain> <optional: protocol/port>
if len(args) == 0 || len(args) > 2 {
return nil, fmt.Errorf("invalid number of arguments for domain: %d, expected 1 or 2", len(args))
}
mb := matcherBase{}
if len(args) == 2 {
protocol, port, err := parseProtocolPort(args[1])
if err != nil {
return nil, err
}
mb.Protocol = protocol
mb.Port = port
}
return &domainMatcher{
matcherBase: mb,
Domain: args[0],
Suffix: false,
}, nil
case "domain-suffix":
// domain-suffix <domain> <optional: protocol/port>
if len(args) == 0 || len(args) > 2 {
return nil, fmt.Errorf("invalid number of arguments for domain-suffix: %d, expected 1 or 2", len(args))
}
mb := matcherBase{}
if len(args) == 2 {
protocol, port, err := parseProtocolPort(args[1])
if err != nil {
return nil, err
}
mb.Protocol = protocol
mb.Port = port
}
return &domainMatcher{
matcherBase: mb,
Domain: args[0],
Suffix: true,
}, nil
case "cidr":
// cidr <cidr> <optional: protocol/port>
if len(args) == 0 || len(args) > 2 {
return nil, fmt.Errorf("invalid number of arguments for cidr: %d, expected 1 or 2", len(args))
}
mb := matcherBase{}
if len(args) == 2 {
protocol, port, err := parseProtocolPort(args[1])
if err != nil {
return nil, err
}
mb.Protocol = protocol
mb.Port = port
}
_, ipNet, err := net.ParseCIDR(args[0])
if err != nil {
return nil, err
}
return &netMatcher{
matcherBase: mb,
Net: ipNet,
}, nil
case "ip":
// ip <ip> <optional: protocol/port>
if len(args) == 0 || len(args) > 2 {
return nil, fmt.Errorf("invalid number of arguments for ip: %d, expected 1 or 2", len(args))
}
mb := matcherBase{}
if len(args) == 2 {
protocol, port, err := parseProtocolPort(args[1])
if err != nil {
return nil, err
}
mb.Protocol = protocol
mb.Port = port
}
ip := net.ParseIP(args[0])
if ip == nil {
return nil, fmt.Errorf("invalid ip: %s", args[0])
}
var ipNet *net.IPNet
if ip.To4() != nil {
ipNet = &net.IPNet{
IP: ip,
Mask: net.CIDRMask(32, 32),
}
} else {
ipNet = &net.IPNet{
IP: ip,
Mask: net.CIDRMask(128, 128),
}
}
return &netMatcher{
matcherBase: mb,
Net: ipNet,
}, nil
case "country":
// country <country> <optional: protocol/port>
if len(args) == 0 || len(args) > 2 {
return nil, fmt.Errorf("invalid number of arguments for country: %d, expected 1 or 2", len(args))
}
mb := matcherBase{}
if len(args) == 2 {
protocol, port, err := parseProtocolPort(args[1])
if err != nil {
return nil, err
}
mb.Protocol = protocol
mb.Port = port
}
return &countryMatcher{
matcherBase: mb,
Country: strings.ToUpper(args[0]),
}, nil
case "all":
// all <optional: protocol/port>
if len(args) > 1 {
return nil, fmt.Errorf("invalid number of arguments for all: %d, expected 0 or 1", len(args))
}
mb := matcherBase{}
if len(args) == 1 {
protocol, port, err := parseProtocolPort(args[0])
if err != nil {
return nil, err
}
mb.Protocol = protocol
mb.Port = port
}
return &allMatcher{
matcherBase: mb,
}, nil
default:
return nil, fmt.Errorf("invalid condition type: %s", typ)
}
}

View file

@ -1,93 +0,0 @@
package acl
import (
"net"
"reflect"
"testing"
)
func TestParseEntry(t *testing.T) {
_, ok3net, _ := net.ParseCIDR("8.8.8.0/24")
type args struct {
s string
}
tests := []struct {
name string
args args
want Entry
wantErr bool
}{
{name: "empty", args: args{""}, want: Entry{}, wantErr: true},
{
name: "ok 1", args: args{"direct domain-suffix google.com"},
want: Entry{ActionDirect, "", &domainMatcher{
matcherBase: matcherBase{},
Domain: "google.com",
Suffix: true,
}},
wantErr: false,
},
{
name: "ok 2", args: args{"proxy domain shithole"},
want: Entry{ActionProxy, "", &domainMatcher{
matcherBase: matcherBase{},
Domain: "shithole",
Suffix: false,
}},
wantErr: false,
},
{
name: "ok 3", args: args{"block cidr 8.8.8.0/24 */53"},
want: Entry{ActionBlock, "", &netMatcher{
matcherBase: matcherBase{ProtocolAll, 53},
Net: ok3net,
}},
wantErr: false,
},
{
name: "ok 4", args: args{"hijack all udp/* udpblackhole.net"},
want: Entry{ActionHijack, "udpblackhole.net", &allMatcher{
matcherBase: matcherBase{ProtocolUDP, 0},
}},
wantErr: false,
},
{
name: "err 1", args: args{"what the heck"},
want: Entry{},
wantErr: true,
},
{
name: "err 2", args: args{"proxy sucks ass"},
want: Entry{},
wantErr: true,
},
{
name: "err 3", args: args{"block ip 999.999.999.999"},
want: Entry{},
wantErr: true,
},
{
name: "err 4", args: args{"hijack domain google.com"},
want: Entry{},
wantErr: true,
},
{
name: "err 5", args: args{"hijack domain google.com bing.com 123"},
want: Entry{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseEntry(tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("ParseEntry() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseEntry() got = %v, wantAction %v", got, tt.want)
}
})
}
}

439
core/client/client.go Normal file
View file

@ -0,0 +1,439 @@
package client
import (
"context"
"crypto/tls"
"errors"
"io"
"math/rand"
"net"
"net/http"
"net/url"
"sync"
"time"
coreErrs "github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/internal/congestion"
"github.com/apernet/hysteria/core/internal/frag"
"github.com/apernet/hysteria/core/internal/protocol"
"github.com/apernet/hysteria/core/internal/utils"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
)
const (
udpMessageChanSize = 1024
)
type Client interface {
DialTCP(addr string) (net.Conn, error)
ListenUDP() (HyUDPConn, error)
Close() error
}
type HyUDPConn interface {
Receive() ([]byte, string, error)
Send([]byte, string) error
Close() error
}
func NewClient(config *Config) (Client, error) {
if err := config.fill(); err != nil {
return nil, err
}
c := &clientImpl{
config: config,
}
c.conn = &autoReconnectConn{
Connect: c.connect,
}
return c, nil
}
type clientImpl struct {
config *Config
conn *autoReconnectConn
udpSM udpSessionManager
}
type udpSessionEntry struct {
Ch chan *protocol.UDPMessage
D *frag.Defragger
Closed bool
}
type udpSessionManager struct {
mutex sync.RWMutex
m map[uint32]*udpSessionEntry
}
func (m *udpSessionManager) Init() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.m = make(map[uint32]*udpSessionEntry)
}
// Add returns both a channel for receiving messages and a function to close the channel & delete the session.
func (m *udpSessionManager) Add(id uint32) (<-chan *protocol.UDPMessage, func()) {
m.mutex.Lock()
defer m.mutex.Unlock()
// Important: make sure we add and delete the channel in the same map,
// as the map may be replaced by Init() at any time.
currentM := m.m
entry := &udpSessionEntry{
Ch: make(chan *protocol.UDPMessage, udpMessageChanSize),
D: &frag.Defragger{},
Closed: false,
}
currentM[id] = entry
return entry.Ch, func() {
m.mutex.Lock()
defer m.mutex.Unlock()
if entry.Closed {
// Double close a channel will panic,
// so we need a flag to make sure we only close it once.
return
}
entry.Closed = true
close(entry.Ch)
delete(currentM, id)
}
}
func (m *udpSessionManager) Feed(msg *protocol.UDPMessage) {
m.mutex.RLock()
defer m.mutex.RUnlock()
entry, ok := m.m[msg.SessionID]
if !ok {
// No such session, drop the message
return
}
dfMsg := entry.D.Feed(msg)
if dfMsg == nil {
// Not a complete message yet
return
}
select {
case entry.Ch <- dfMsg:
// OK
default:
// Channel is full, drop the message
}
}
func (c *clientImpl) connect() (quic.Connection, func(), error) {
// Use a new packet conn for each connection,
// remember to close it after the QUIC connection is closed.
pktConn, err := c.config.ConnFactory.New(c.config.ServerAddr)
if err != nil {
return nil, nil, err
}
// Convert config to TLS config & QUIC config
tlsConfig := &tls.Config{
InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify,
RootCAs: c.config.TLSConfig.RootCAs,
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow,
MaxStreamReceiveWindow: c.config.QUICConfig.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: c.config.QUICConfig.InitialConnectionReceiveWindow,
MaxConnectionReceiveWindow: c.config.QUICConfig.MaxConnectionReceiveWindow,
MaxIdleTimeout: c.config.QUICConfig.MaxIdleTimeout,
KeepAlivePeriod: c.config.QUICConfig.KeepAlivePeriod,
DisablePathMTUDiscovery: c.config.QUICConfig.DisablePathMTUDiscovery,
EnableDatagrams: true,
}
// Prepare RoundTripper
var conn quic.EarlyConnection
rt := &http3.RoundTripper{
EnableDatagrams: true,
TLSClientConfig: tlsConfig,
QuicConfig: quicConfig,
Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
qc, err := quic.DialEarlyContext(ctx, pktConn, c.config.ServerAddr, c.config.ServerName, 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.AuthRequestDataToHeader(req.Header, c.config.Auth, c.config.BandwidthConfig.MaxRx)
resp, err := rt.RoundTrip(req)
if err != nil {
if conn != nil {
_ = conn.CloseWithError(0, "")
}
_ = pktConn.Close()
return nil, nil, &coreErrs.ConnectError{Err: err}
}
if resp.StatusCode != protocol.StatusAuthOK {
_ = conn.CloseWithError(0, "")
_ = pktConn.Close()
return nil, nil, &coreErrs.AuthError{StatusCode: resp.StatusCode}
}
// Auth OK
serverRx := protocol.AuthResponseDataFromHeader(resp.Header)
// actualTx = min(serverRx, clientTx)
actualTx := serverRx
if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx {
actualTx = c.config.BandwidthConfig.MaxTx
}
// Set congestion control when applicable
if actualTx > 0 {
conn.SetCongestionControl(congestion.NewBrutalSender(actualTx))
}
_ = resp.Body.Close()
c.udpSM.Init()
go c.udpLoop(conn)
return conn, func() {
_ = conn.CloseWithError(0, "")
_ = pktConn.Close()
}, nil
}
func (c *clientImpl) udpLoop(conn quic.Connection) {
for {
msg, err := conn.ReceiveMessage()
if err != nil {
return
}
c.handleUDPMessage(msg)
}
}
// client <- remote direction
func (c *clientImpl) handleUDPMessage(msg []byte) {
udpMsg, err := protocol.ParseUDPMessage(msg)
if err != nil {
return
}
c.udpSM.Feed(udpMsg)
}
// openStream wraps the stream with QStream, which handles Close() properly
func (c *clientImpl) openStream() (quic.Connection, quic.Stream, error) {
qc, stream, err := c.conn.OpenStream()
if err != nil {
return nil, nil, err
}
return qc, &utils.QStream{Stream: stream}, nil
}
func (c *clientImpl) DialTCP(addr string) (net.Conn, error) {
qc, stream, err := c.openStream()
if err != nil {
return nil, err
}
// Send request
err = protocol.WriteTCPRequest(stream, addr)
if err != nil {
_ = stream.Close()
return nil, 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: qc.LocalAddr(),
PseudoRemoteAddr: qc.RemoteAddr(),
Established: false,
}, nil
}
// Read response
ok, msg, err := protocol.ReadTCPResponse(stream)
if err != nil {
_ = stream.Close()
return nil, err
}
if !ok {
_ = stream.Close()
return nil, coreErrs.DialError{Message: msg}
}
return &tcpConn{
Orig: stream,
PseudoLocalAddr: qc.LocalAddr(),
PseudoRemoteAddr: qc.RemoteAddr(),
Established: true,
}, nil
}
func (c *clientImpl) ListenUDP() (HyUDPConn, error) {
qc, stream, err := c.openStream()
if err != nil {
return nil, err
}
// Send request
err = protocol.WriteUDPRequest(stream)
if err != nil {
_ = stream.Close()
return nil, err
}
// Read response
ok, sessionID, msg, err := protocol.ReadUDPResponse(stream)
if err != nil {
_ = stream.Close()
return nil, err
}
if !ok {
_ = stream.Close()
return nil, coreErrs.DialError{Message: msg}
}
ch, closeFunc := c.udpSM.Add(sessionID)
uc := &udpConn{
QC: qc,
Stream: stream,
SessionID: sessionID,
Ch: ch,
CloseFunc: closeFunc,
SendBuf: make([]byte, protocol.MaxUDPSize),
}
go uc.Hold()
return uc, nil
}
func (c *clientImpl) Close() error {
return c.conn.Close()
}
type tcpConn struct {
Orig quic.Stream
PseudoLocalAddr net.Addr
PseudoRemoteAddr net.Addr
Established bool
}
func (c *tcpConn) Read(b []byte) (n int, err error) {
if !c.Established {
// Read response
ok, msg, err := protocol.ReadTCPResponse(c.Orig)
if err != nil {
return 0, err
}
if !ok {
return 0, coreErrs.DialError{Message: msg}
}
c.Established = true
}
return c.Orig.Read(b)
}
func (c *tcpConn) Write(b []byte) (n int, err error) {
return c.Orig.Write(b)
}
func (c *tcpConn) Close() error {
return c.Orig.Close()
}
func (c *tcpConn) LocalAddr() net.Addr {
return c.PseudoLocalAddr
}
func (c *tcpConn) RemoteAddr() net.Addr {
return c.PseudoRemoteAddr
}
func (c *tcpConn) SetDeadline(t time.Time) error {
return c.Orig.SetDeadline(t)
}
func (c *tcpConn) SetReadDeadline(t time.Time) error {
return c.Orig.SetReadDeadline(t)
}
func (c *tcpConn) SetWriteDeadline(t time.Time) error {
return c.Orig.SetWriteDeadline(t)
}
type udpConn struct {
QC quic.Connection
Stream quic.Stream
SessionID uint32
Ch <-chan *protocol.UDPMessage
CloseFunc func()
SendBuf []byte
}
func (c *udpConn) Hold() {
// Hold (drain) the stream until someone closes it.
// Closing the stream is the signal to stop the UDP session.
_, _ = io.Copy(io.Discard, c.Stream)
_ = c.Close()
}
func (c *udpConn) Receive() ([]byte, string, error) {
msg := <-c.Ch
if msg == nil {
// Closed
return nil, "", io.EOF
}
return msg.Data, msg.Addr, nil
}
// Send is not thread-safe as it uses a shared send buffer for now.
func (c *udpConn) Send(data []byte, addr string) error {
// Try no frag first
msg := protocol.UDPMessage{
SessionID: c.SessionID,
PacketID: 0,
FragID: 0,
FragCount: 1,
Addr: addr,
Data: data,
}
n := msg.Serialize(c.SendBuf)
if n < 0 {
// Message even larger than MaxUDPSize, drop it
// Maybe we should return an error in the future?
return nil
}
sendErr := c.QC.SendMessage(c.SendBuf[:n])
if sendErr == nil {
// All good
return nil
}
var errTooLarge quic.ErrMessageTooLarge
if errors.As(sendErr, &errTooLarge) {
// Message too large, try fragmentation
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := frag.FragUDPMessage(msg, int(errTooLarge))
for _, fMsg := range fMsgs {
n = fMsg.Serialize(c.SendBuf)
err := c.QC.SendMessage(c.SendBuf[:n])
if err != nil {
return err
}
}
return nil
}
// Other error
return sendErr
}
func (c *udpConn) Close() error {
c.CloseFunc()
return c.Stream.Close()
}

107
core/client/config.go Normal file
View file

@ -0,0 +1,107 @@
package client
import (
"crypto/x509"
"net"
"time"
"github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/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
ServerName string // host or host:port
Auth string
TLSConfig TLSConfig
QUICConfig QUICConfig
BandwidthConfig BandwidthConfig
FastOpen bool
}
// 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.
func (c *Config) fill() error {
if c.ConnFactory == nil {
c.ConnFactory = &udpConnFactory{}
}
if c.ServerAddr == nil {
return errors.ConfigError{Field: "ServerAddr", Reason: "must be set"}
}
if c.ServerName == "" {
return errors.ConfigError{Field: "ServerName", 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
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 {
InsecureSkipVerify bool
RootCAs *x509.CertPool
}
// QUICConfig contains the QUIC configuration fields that we want to expose to the user.
type QUICConfig struct {
InitialStreamReceiveWindow uint64
MaxStreamReceiveWindow uint64
InitialConnectionReceiveWindow uint64
MaxConnectionReceiveWindow uint64
MaxIdleTimeout time.Duration
KeepAlivePeriod time.Duration
DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms.
}
// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second.
type BandwidthConfig struct {
MaxTx uint64
MaxRx uint64
}

68
core/client/reconnect.go Normal file
View file

@ -0,0 +1,68 @@
package client
import (
"net"
"sync"
"github.com/quic-go/quic-go"
)
// autoReconnectConn is a wrapper of quic.Connection that automatically reconnects
// when a non-temporary error (usually a timeout) occurs.
type autoReconnectConn struct {
// Connect is called whenever a new QUIC connection is needed.
// It should return a new QUIC connection, a function to close the connection
// (and potentially other underlying resources), and an error if one occurred.
Connect func() (quic.Connection, func(), error)
conn quic.Connection
closeFunc func()
connMutex sync.RWMutex
}
func (c *autoReconnectConn) OpenStream() (quic.Connection, quic.Stream, error) {
c.connMutex.Lock()
defer c.connMutex.Unlock()
// First time?
if c.conn == nil {
conn, closeFunc, err := c.Connect()
if err != nil {
return nil, nil, err
}
c.conn = conn
c.closeFunc = closeFunc
}
stream, err := c.conn.OpenStream()
if err == nil {
// All is good
return c.conn, stream, nil
} else if nErr, ok := err.(net.Error); ok && nErr.Temporary() {
// Temporary error, just pass the error to the caller
return nil, nil, err
} else {
// Permanent error
// Close the previous connection,
// reconnect and try again (only once)
c.closeFunc()
conn, closeFunc, err := c.Connect()
if err != nil {
return nil, nil, err
}
c.conn = conn
c.closeFunc = closeFunc
stream, err = c.conn.OpenStream()
return c.conn, stream, err
}
}
func (c *autoReconnectConn) Close() error {
c.connMutex.Lock()
defer c.connMutex.Unlock()
if c.conn == nil {
return nil
}
c.closeFunc()
c.conn = nil
c.closeFunc = nil
return nil
}

View file

@ -1,447 +0,0 @@
package cs
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"math/rand"
"net"
"strconv"
"sync"
"time"
"github.com/apernet/hysteria/core/pktconns"
"github.com/apernet/hysteria/core/congestion"
"github.com/apernet/hysteria/core/pmtud"
"github.com/apernet/hysteria/core/utils"
"github.com/lunixbochs/struc"
"github.com/quic-go/quic-go"
)
var ErrClosed = errors.New("closed")
type Client struct {
serverAddr string
sendBPS, recvBPS uint64
auth []byte
fastOpen bool
tlsConfig *tls.Config
quicConfig *quic.Config
pktConnFunc pktconns.ClientPacketConnFunc
reconnectMutex sync.Mutex
pktConn net.PacketConn
quicConn quic.Connection
closed bool
udpSessionMutex sync.RWMutex
udpSessionMap map[uint32]chan *udpMessage
udpDefragger defragger
quicReconnectFunc func(err error)
}
func NewClient(serverAddr string, auth []byte, tlsConfig *tls.Config, quicConfig *quic.Config,
pktConnFunc pktconns.ClientPacketConnFunc, sendBPS uint64, recvBPS uint64, fastOpen bool, lazyStart bool,
quicReconnectFunc func(err error),
) (*Client, error) {
quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery
c := &Client{
serverAddr: serverAddr,
sendBPS: sendBPS,
recvBPS: recvBPS,
auth: auth,
fastOpen: fastOpen,
tlsConfig: tlsConfig,
quicConfig: quicConfig,
pktConnFunc: pktConnFunc,
quicReconnectFunc: quicReconnectFunc,
}
if lazyStart {
return c, nil
}
if err := c.connect(); err != nil {
return nil, err
}
return c, nil
}
func (c *Client) connect() error {
// Clear previous connection
if c.quicConn != nil {
_ = c.quicConn.CloseWithError(0, "")
}
if c.pktConn != nil {
_ = c.pktConn.Close()
}
// New connection
pktConn, sAddr, err := c.pktConnFunc(c.serverAddr)
if err != nil {
return err
}
// Dial QUIC
quicConn, err := quic.Dial(pktConn, sAddr, c.serverAddr, c.tlsConfig, c.quicConfig)
if err != nil {
_ = pktConn.Close()
return err
}
// Control stream
ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout)
stream, err := quicConn.OpenStreamSync(ctx)
ctxCancel()
if err != nil {
_ = qErrorProtocol.Send(quicConn)
_ = pktConn.Close()
return err
}
ok, msg, err := c.handleControlStream(quicConn, stream)
if err != nil {
_ = qErrorProtocol.Send(quicConn)
_ = pktConn.Close()
return err
}
if !ok {
_ = qErrorAuth.Send(quicConn)
_ = pktConn.Close()
return fmt.Errorf("auth error: %s", msg)
}
// All good
c.udpSessionMap = make(map[uint32]chan *udpMessage)
go c.handleMessage(quicConn)
c.pktConn = pktConn
c.quicConn = quicConn
return nil
}
func (c *Client) handleControlStream(qc quic.Connection, stream quic.Stream) (bool, string, error) {
// Send protocol version
_, err := stream.Write([]byte{protocolVersion})
if err != nil {
return false, "", err
}
// Send client hello
err = struc.Pack(stream, &clientHello{
Rate: maxRate{
SendBPS: c.sendBPS,
RecvBPS: c.recvBPS,
},
Auth: c.auth,
})
if err != nil {
return false, "", err
}
// Receive server hello
var sh serverHello
err = struc.Unpack(stream, &sh)
if err != nil {
return false, "", err
}
// Set the congestion accordingly
if sh.OK {
qc.SetCongestionControl(congestion.NewBrutalSender(sh.Rate.RecvBPS))
}
return sh.OK, sh.Message, nil
}
func (c *Client) handleMessage(qc quic.Connection) {
for {
msg, err := qc.ReceiveMessage()
if err != nil {
break
}
var udpMsg udpMessage
err = struc.Unpack(bytes.NewBuffer(msg), &udpMsg)
if err != nil {
continue
}
dfMsg := c.udpDefragger.Feed(udpMsg)
if dfMsg == nil {
continue
}
c.udpSessionMutex.RLock()
ch, ok := c.udpSessionMap[dfMsg.SessionID]
if ok {
select {
case ch <- dfMsg:
// OK
default:
// Silently drop the message when the channel is full
}
}
c.udpSessionMutex.RUnlock()
}
}
func (c *Client) openStreamWithReconnect() (quic.Connection, quic.Stream, error) {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
if c.closed {
return nil, nil, ErrClosed
}
if c.quicConn != nil {
stream, err := c.quicConn.OpenStream()
if err == nil {
// All good
return c.quicConn, &qStream{stream}, nil
}
// Something is wrong
if nErr, ok := err.(net.Error); ok && nErr.Temporary() {
// Temporary error, just return
return nil, nil, err
}
if c.quicReconnectFunc != nil {
c.quicReconnectFunc(err)
}
}
// Permanent error, need to reconnect
if err := c.connect(); err != nil {
// Still error, oops
return nil, nil, err
}
// We are not going to try again even if it still fails the second time
stream, err := c.quicConn.OpenStream()
return c.quicConn, &qStream{stream}, err
}
func (c *Client) DialTCP(addr string) (net.Conn, error) {
host, port, err := utils.SplitHostPort(addr)
if err != nil {
return nil, err
}
session, stream, err := c.openStreamWithReconnect()
if err != nil {
return nil, err
}
// Send request
err = struc.Pack(stream, &clientRequest{
UDP: false,
Host: host,
Port: port,
})
if err != nil {
_ = stream.Close()
return nil, err
}
// If fast open is enabled, we return the stream immediately
// and defer the response handling to the first Read() call
if !c.fastOpen {
// Read response
var sr serverResponse
err = struc.Unpack(stream, &sr)
if err != nil {
_ = stream.Close()
return nil, err
}
if !sr.OK {
_ = stream.Close()
return nil, fmt.Errorf("connection rejected: %s", sr.Message)
}
}
return &hyTCPConn{
Orig: stream,
PseudoLocalAddr: session.LocalAddr(),
PseudoRemoteAddr: session.RemoteAddr(),
Established: !c.fastOpen,
}, nil
}
func (c *Client) DialUDP() (HyUDPConn, error) {
session, stream, err := c.openStreamWithReconnect()
if err != nil {
return nil, err
}
// Send request
err = struc.Pack(stream, &clientRequest{
UDP: true,
})
if err != nil {
_ = stream.Close()
return nil, err
}
// Read response
var sr serverResponse
err = struc.Unpack(stream, &sr)
if err != nil {
_ = stream.Close()
return nil, err
}
if !sr.OK {
_ = stream.Close()
return nil, fmt.Errorf("connection rejected: %s", sr.Message)
}
// Create a session in the map
c.udpSessionMutex.Lock()
nCh := make(chan *udpMessage, 1024)
// Store the current session map for CloseFunc below
// to ensure that we are adding and removing sessions on the same map,
// as reconnecting will reassign the map
sessionMap := c.udpSessionMap
sessionMap[sr.UDPSessionID] = nCh
c.udpSessionMutex.Unlock()
pktConn := &hyUDPConn{
Session: session,
Stream: stream,
CloseFunc: func() {
c.udpSessionMutex.Lock()
if ch, ok := sessionMap[sr.UDPSessionID]; ok {
close(ch)
delete(sessionMap, sr.UDPSessionID)
}
c.udpSessionMutex.Unlock()
},
UDPSessionID: sr.UDPSessionID,
MsgCh: nCh,
}
go pktConn.Hold()
return pktConn, nil
}
func (c *Client) Close() error {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
err := qErrorGeneric.Send(c.quicConn)
_ = c.pktConn.Close()
c.closed = true
return err
}
// hyTCPConn wraps a QUIC stream and implements net.Conn returned by Client.DialTCP
type hyTCPConn struct {
Orig quic.Stream
PseudoLocalAddr net.Addr
PseudoRemoteAddr net.Addr
Established bool
}
func (w *hyTCPConn) Read(b []byte) (n int, err error) {
if !w.Established {
var sr serverResponse
err := struc.Unpack(w.Orig, &sr)
if err != nil {
_ = w.Close()
return 0, err
}
if !sr.OK {
_ = w.Close()
return 0, fmt.Errorf("connection rejected: %s", sr.Message)
}
w.Established = true
}
return w.Orig.Read(b)
}
func (w *hyTCPConn) Write(b []byte) (n int, err error) {
return w.Orig.Write(b)
}
func (w *hyTCPConn) Close() error {
return w.Orig.Close()
}
func (w *hyTCPConn) LocalAddr() net.Addr {
return w.PseudoLocalAddr
}
func (w *hyTCPConn) RemoteAddr() net.Addr {
return w.PseudoRemoteAddr
}
func (w *hyTCPConn) SetDeadline(t time.Time) error {
return w.Orig.SetDeadline(t)
}
func (w *hyTCPConn) SetReadDeadline(t time.Time) error {
return w.Orig.SetReadDeadline(t)
}
func (w *hyTCPConn) SetWriteDeadline(t time.Time) error {
return w.Orig.SetWriteDeadline(t)
}
type HyUDPConn interface {
ReadFrom() ([]byte, string, error)
WriteTo([]byte, string) error
Close() error
}
type hyUDPConn struct {
Session quic.Connection
Stream quic.Stream
CloseFunc func()
UDPSessionID uint32
MsgCh <-chan *udpMessage
}
func (c *hyUDPConn) Hold() {
// Hold the stream until it's closed
buf := make([]byte, 1024)
for {
_, err := c.Stream.Read(buf)
if err != nil {
break
}
}
_ = c.Close()
}
func (c *hyUDPConn) ReadFrom() ([]byte, string, error) {
msg := <-c.MsgCh
if msg == nil {
// Closed
return nil, "", ErrClosed
}
return msg.Data, net.JoinHostPort(msg.Host, strconv.Itoa(int(msg.Port))), nil
}
func (c *hyUDPConn) WriteTo(p []byte, addr string) error {
host, port, err := utils.SplitHostPort(addr)
if err != nil {
return err
}
msg := udpMessage{
SessionID: c.UDPSessionID,
Host: host,
Port: port,
FragCount: 1,
Data: p,
}
// try no frag first
var msgBuf bytes.Buffer
_ = struc.Pack(&msgBuf, &msg)
err = c.Session.SendMessage(msgBuf.Bytes())
if err != nil {
if errSize, ok := err.(quic.ErrMessageTooLarge); ok {
// need to frag
msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1
fragMsgs := fragUDPMessage(msg, int(errSize))
for _, fragMsg := range fragMsgs {
msgBuf.Reset()
_ = struc.Pack(&msgBuf, &fragMsg)
err = c.Session.SendMessage(msgBuf.Bytes())
if err != nil {
return err
}
}
return nil
} else {
// some other error
return err
}
} else {
return nil
}
}
func (c *hyUDPConn) Close() error {
c.CloseFunc()
return c.Stream.Close()
}

View file

@ -1,67 +0,0 @@
package cs
func fragUDPMessage(m udpMessage, maxSize int) []udpMessage {
if m.Size() <= maxSize {
return []udpMessage{m}
}
fullPayload := m.Data
maxPayloadSize := maxSize - m.HeaderSize()
off := 0
fragID := uint8(0)
fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up
var frags []udpMessage
for off < len(fullPayload) {
payloadSize := len(fullPayload) - off
if payloadSize > maxPayloadSize {
payloadSize = maxPayloadSize
}
frag := m
frag.FragID = fragID
frag.FragCount = fragCount
frag.DataLen = uint16(payloadSize)
frag.Data = fullPayload[off : off+payloadSize]
frags = append(frags, frag)
off += payloadSize
fragID++
}
return frags
}
type defragger struct {
msgID uint16
frags []*udpMessage
count uint8
}
func (d *defragger) Feed(m udpMessage) *udpMessage {
if m.FragCount <= 1 {
return &m
}
if m.FragID >= m.FragCount {
// wtf is this?
return nil
}
if m.MsgID != d.msgID || m.FragCount != uint8(len(d.frags)) {
// new message, clear previous state
d.msgID = m.MsgID
d.frags = make([]*udpMessage, m.FragCount)
d.count = 1
d.frags[m.FragID] = &m
} else if d.frags[m.FragID] == nil {
d.frags[m.FragID] = &m
d.count++
if int(d.count) == len(d.frags) {
// all fragments received, assemble
var data []byte
for _, frag := range d.frags {
data = append(data, frag.Data...)
}
m.DataLen = uint16(len(data))
m.Data = data
m.FragID = 0
m.FragCount = 1
return &m
}
}
return nil
}

View file

@ -1,390 +0,0 @@
package cs
import (
"reflect"
"testing"
)
func Test_fragUDPMessage(t *testing.T) {
type args struct {
m udpMessage
maxSize int
}
tests := []struct {
name string
args args
want []udpMessage
}{
{
"no frag",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 1,
DataLen: 5,
Data: []byte("hello"),
},
100,
},
[]udpMessage{
{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 1,
DataLen: 5,
Data: []byte("hello"),
},
},
},
{
"2 frags",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 1,
DataLen: 5,
Data: []byte("hello"),
},
22,
},
[]udpMessage{
{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 2,
DataLen: 4,
Data: []byte("hell"),
},
{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 1,
FragCount: 2,
DataLen: 1,
Data: []byte("o"),
},
},
},
{
"4 frags",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 1,
DataLen: 20,
Data: []byte("wow wow wow lol lmao"),
},
23,
},
[]udpMessage{
{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 4,
DataLen: 5,
Data: []byte("wow w"),
},
{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 1,
FragCount: 4,
DataLen: 5,
Data: []byte("ow wo"),
},
{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 2,
FragCount: 4,
DataLen: 5,
Data: []byte("w lol"),
},
{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 3,
FragCount: 4,
DataLen: 5,
Data: []byte(" lmao"),
},
},
},
}
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 Test_defragger_Feed(t *testing.T) {
d := &defragger{}
type args struct {
m udpMessage
}
tests := []struct {
name string
args args
want *udpMessage
}{
{
"no frag",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 1,
DataLen: 5,
Data: []byte("hello"),
},
},
&udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 123,
FragID: 0,
FragCount: 1,
DataLen: 5,
Data: []byte("hello"),
},
},
{
"frag 0 - 1/2",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 0,
FragID: 0,
FragCount: 2,
DataLen: 5,
Data: []byte("ilove"),
},
},
nil,
},
{
"frag 0 - 2/2",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 0,
FragID: 1,
FragCount: 2,
DataLen: 6,
Data: []byte("nobody"),
},
},
&udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 0,
FragID: 0,
FragCount: 1,
DataLen: 11,
Data: []byte("ilovenobody"),
},
},
{
"frag 1 - 1/3",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 666,
FragID: 0,
FragCount: 3,
DataLen: 5,
Data: []byte("hello"),
},
},
nil,
},
{
"frag 1 - 2/3",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 666,
FragID: 1,
FragCount: 3,
DataLen: 8,
Data: []byte(" shitty "),
},
},
nil,
},
{
"frag 1 - 3/3",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 666,
FragID: 2,
FragCount: 3,
DataLen: 7,
Data: []byte("world!!"),
},
},
&udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 666,
FragID: 0,
FragCount: 1,
DataLen: 20,
Data: []byte("hello shitty world!!"),
},
},
{
"frag 2 - 1/2",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 777,
FragID: 0,
FragCount: 2,
DataLen: 5,
Data: []byte("hello"),
},
},
nil,
},
{
"frag 3 - 2/2",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 778,
FragID: 1,
FragCount: 2,
DataLen: 5,
Data: []byte(" moto"),
},
},
nil,
},
{
"frag 2 - 2/2",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 777,
FragID: 1,
FragCount: 2,
DataLen: 5,
Data: []byte(" moto"),
},
},
nil,
},
{
"frag 2 - 1/2 re",
args{
udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 777,
FragID: 0,
FragCount: 2,
DataLen: 5,
Data: []byte("hello"),
},
},
&udpMessage{
SessionID: 123,
HostLen: 4,
Host: "test",
Port: 123,
MsgID: 777,
FragID: 0,
FragCount: 1,
DataLen: 10,
Data: []byte("hello moto"),
},
},
}
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)
}
})
}
}

View file

@ -1,79 +0,0 @@
package cs
import (
"time"
"github.com/quic-go/quic-go"
)
const (
protocolVersion = uint8(3)
protocolTimeout = 10 * time.Second
)
type qError struct {
Code quic.ApplicationErrorCode
Msg string
}
func (e qError) Send(c quic.Connection) error {
return c.CloseWithError(e.Code, e.Msg)
}
var (
qErrorGeneric = qError{0, ""}
qErrorProtocol = qError{1, "protocol error"}
qErrorAuth = qError{2, "auth error"}
)
type maxRate struct {
SendBPS uint64
RecvBPS uint64
}
type clientHello struct {
Rate maxRate
AuthLen uint16 `struc:"sizeof=Auth"`
Auth []byte
}
type serverHello struct {
OK bool
Rate maxRate
MessageLen uint16 `struc:"sizeof=Message"`
Message string
}
type clientRequest struct {
UDP bool
HostLen uint16 `struc:"sizeof=Host"`
Host string
Port uint16
}
type serverResponse struct {
OK bool
UDPSessionID uint32
MessageLen uint16 `struc:"sizeof=Message"`
Message string
}
type udpMessage struct {
SessionID uint32
HostLen uint16 `struc:"sizeof=Host"`
Host string
Port uint16
MsgID uint16 // doesn't matter when not fragmented, but must not be 0 when fragmented
FragID uint8 // doesn't matter when not fragmented, starts at 0 when fragmented
FragCount uint8 // must be 1 when not fragmented
DataLen uint16 `struc:"sizeof=Data"`
Data []byte
}
func (m udpMessage) HeaderSize() int {
return 4 + 2 + len(m.Host) + 2 + 2 + 1 + 1 + 2
}
func (m udpMessage) Size() int {
return m.HeaderSize() + len(m.Data)
}

View file

@ -1,178 +0,0 @@
package cs
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"github.com/apernet/hysteria/core/congestion"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/pmtud"
"github.com/apernet/hysteria/core/transport"
"github.com/lunixbochs/struc"
"github.com/quic-go/quic-go"
)
type (
ConnectFunc func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string)
DisconnectFunc func(addr net.Addr, auth []byte, err error)
TCPRequestFunc func(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string)
TCPErrorFunc func(addr net.Addr, auth []byte, reqAddr string, err error)
UDPRequestFunc func(addr net.Addr, auth []byte, sessionID uint32)
UDPErrorFunc func(addr net.Addr, auth []byte, sessionID uint32, err error)
)
type TrafficCounter interface {
Rx(auth string, n int)
Tx(auth string, n int)
IncConn(auth string) // increase connection count
DecConn(auth string) // decrease connection count
}
type Server struct {
transport *transport.ServerTransport
sendBPS, recvBPS uint64
disableUDP bool
aclEngine *acl.Engine
connectFunc ConnectFunc
disconnectFunc DisconnectFunc
tcpRequestFunc TCPRequestFunc
tcpErrorFunc TCPErrorFunc
udpRequestFunc UDPRequestFunc
udpErrorFunc UDPErrorFunc
trafficCounter TrafficCounter
pktConn net.PacketConn
listener quic.Listener
}
func NewServer(tlsConfig *tls.Config, quicConfig *quic.Config,
pktConn net.PacketConn, transport *transport.ServerTransport,
sendBPS uint64, recvBPS uint64, disableUDP bool, aclEngine *acl.Engine,
connectFunc ConnectFunc, disconnectFunc DisconnectFunc,
tcpRequestFunc TCPRequestFunc, tcpErrorFunc TCPErrorFunc,
udpRequestFunc UDPRequestFunc, udpErrorFunc UDPErrorFunc,
trafficCounter TrafficCounter,
) (*Server, error) {
quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery
listener, err := quic.Listen(pktConn, tlsConfig, quicConfig)
if err != nil {
_ = pktConn.Close()
return nil, err
}
s := &Server{
pktConn: pktConn,
listener: listener,
transport: transport,
sendBPS: sendBPS,
recvBPS: recvBPS,
disableUDP: disableUDP,
aclEngine: aclEngine,
connectFunc: connectFunc,
disconnectFunc: disconnectFunc,
tcpRequestFunc: tcpRequestFunc,
tcpErrorFunc: tcpErrorFunc,
udpRequestFunc: udpRequestFunc,
udpErrorFunc: udpErrorFunc,
trafficCounter: trafficCounter,
}
return s, nil
}
func (s *Server) Serve() error {
for {
cc, err := s.listener.Accept(context.Background())
if err != nil {
return err
}
go s.handleClient(cc)
}
}
func (s *Server) Close() error {
err := s.listener.Close()
_ = s.pktConn.Close()
return err
}
func (s *Server) handleClient(cc quic.Connection) {
// Expect the client to create a control stream to send its own information
ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout)
stream, err := cc.AcceptStream(ctx)
ctxCancel()
if err != nil {
_ = qErrorProtocol.Send(cc)
return
}
// Handle the control stream
auth, ok, err := s.handleControlStream(cc, stream)
if err != nil {
_ = qErrorProtocol.Send(cc)
return
}
if !ok {
_ = qErrorAuth.Send(cc)
return
}
// Start accepting streams and messages
sc := newServerClient(cc, s.transport, auth, s.disableUDP, s.aclEngine,
s.tcpRequestFunc, s.tcpErrorFunc, s.udpRequestFunc, s.udpErrorFunc,
s.trafficCounter)
err = sc.Run()
_ = qErrorGeneric.Send(cc)
s.disconnectFunc(cc.RemoteAddr(), auth, err)
}
// Auth & negotiate speed
func (s *Server) handleControlStream(cc quic.Connection, stream quic.Stream) ([]byte, bool, error) {
// Check version
vb := make([]byte, 1)
_, err := stream.Read(vb)
if err != nil {
return nil, false, err
}
if vb[0] != protocolVersion {
return nil, false, fmt.Errorf("unsupported protocol version %d, expecting %d", vb[0], protocolVersion)
}
// Parse client hello
var ch clientHello
err = struc.Unpack(stream, &ch)
if err != nil {
return nil, false, err
}
// Speed
if ch.Rate.SendBPS == 0 || ch.Rate.RecvBPS == 0 {
return nil, false, errors.New("invalid rate from client")
}
serverSendBPS, serverRecvBPS := ch.Rate.RecvBPS, ch.Rate.SendBPS
if s.sendBPS > 0 && serverSendBPS > s.sendBPS {
serverSendBPS = s.sendBPS
}
if s.recvBPS > 0 && serverRecvBPS > s.recvBPS {
serverRecvBPS = s.recvBPS
}
// Auth
ok, msg := s.connectFunc(cc.RemoteAddr(), ch.Auth, serverSendBPS, serverRecvBPS)
// Response
err = struc.Pack(stream, &serverHello{
OK: ok,
Rate: maxRate{
SendBPS: serverSendBPS,
RecvBPS: serverRecvBPS,
},
Message: msg,
})
if err != nil {
return nil, false, err
}
// Set the congestion accordingly
if ok {
cc.SetCongestionControl(congestion.NewBrutalSender(serverSendBPS))
}
return ch.Auth, ok, nil
}

View file

@ -1,391 +0,0 @@
package cs
import (
"bytes"
"context"
"encoding/base64"
"math/rand"
"net"
"strconv"
"sync"
"github.com/apernet/hysteria/core/acl"
"github.com/apernet/hysteria/core/transport"
"github.com/apernet/hysteria/core/utils"
"github.com/lunixbochs/struc"
"github.com/quic-go/quic-go"
)
const udpBufferSize = 4096
type serverClient struct {
CC quic.Connection
Transport *transport.ServerTransport
Auth []byte
AuthLabel string // Base64 encoded auth
DisableUDP bool
ACLEngine *acl.Engine
CTCPRequestFunc TCPRequestFunc
CTCPErrorFunc TCPErrorFunc
CUDPRequestFunc UDPRequestFunc
CUDPErrorFunc UDPErrorFunc
TrafficCounter TrafficCounter
udpSessionMutex sync.RWMutex
udpSessionMap map[uint32]transport.STPacketConn
nextUDPSessionID uint32
udpDefragger defragger
}
func newServerClient(cc quic.Connection, tr *transport.ServerTransport, auth []byte, disableUDP bool, ACLEngine *acl.Engine,
CTCPRequestFunc TCPRequestFunc, CTCPErrorFunc TCPErrorFunc,
CUDPRequestFunc UDPRequestFunc, CUDPErrorFunc UDPErrorFunc,
TrafficCounter TrafficCounter,
) *serverClient {
sc := &serverClient{
CC: cc,
Transport: tr,
Auth: auth,
AuthLabel: base64.StdEncoding.EncodeToString(auth),
DisableUDP: disableUDP,
ACLEngine: ACLEngine,
CTCPRequestFunc: CTCPRequestFunc,
CTCPErrorFunc: CTCPErrorFunc,
CUDPRequestFunc: CUDPRequestFunc,
CUDPErrorFunc: CUDPErrorFunc,
TrafficCounter: TrafficCounter,
udpSessionMap: make(map[uint32]transport.STPacketConn),
}
return sc
}
func (c *serverClient) ClientAddr() net.Addr {
// quic.Connection's remote address may change since we have connection migration now,
// so logs need to dynamically get the remote address every time.
return c.CC.RemoteAddr()
}
func (c *serverClient) Run() error {
if !c.DisableUDP {
go func() {
for {
msg, err := c.CC.ReceiveMessage()
if err != nil {
break
}
c.handleMessage(msg)
}
}()
}
for {
stream, err := c.CC.AcceptStream(context.Background())
if err != nil {
return err
}
if c.TrafficCounter != nil {
c.TrafficCounter.IncConn(c.AuthLabel)
}
go func() {
stream := &qStream{stream}
c.handleStream(stream)
_ = stream.Close()
if c.TrafficCounter != nil {
c.TrafficCounter.DecConn(c.AuthLabel)
}
}()
}
}
func (c *serverClient) handleStream(stream quic.Stream) {
// Read request
var req clientRequest
err := struc.Unpack(stream, &req)
if err != nil {
return
}
if !req.UDP {
// TCP connection
c.handleTCP(stream, req.Host, req.Port)
} else if !c.DisableUDP {
// UDP connection
c.handleUDP(stream)
} else {
// UDP disabled
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: "UDP disabled",
})
}
}
func (c *serverClient) handleMessage(msg []byte) {
var udpMsg udpMessage
err := struc.Unpack(bytes.NewBuffer(msg), &udpMsg)
if err != nil {
return
}
dfMsg := c.udpDefragger.Feed(udpMsg)
if dfMsg == nil {
return
}
c.udpSessionMutex.RLock()
conn, ok := c.udpSessionMap[dfMsg.SessionID]
c.udpSessionMutex.RUnlock()
if ok {
// Session found, send the message
action, arg := acl.ActionDirect, ""
var isDomain bool
var ipAddr *net.IPAddr
var err error
if c.ACLEngine != nil {
action, arg, isDomain, ipAddr, err = c.ACLEngine.ResolveAndMatch(dfMsg.Host, dfMsg.Port, true)
} else if c.Transport.ProxyEnabled() { // Case for SOCKS5 outbound
ipAddr, isDomain = c.Transport.ParseIPAddr(dfMsg.Host) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain
err = nil
} else {
ipAddr, isDomain, err = c.Transport.ResolveIPAddr(dfMsg.Host)
}
if err != nil {
return
}
switch action {
case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side
addrEx := &transport.AddrEx{
IPAddr: ipAddr,
Port: int(dfMsg.Port),
}
if isDomain {
addrEx.Domain = dfMsg.Host
}
_, _ = conn.WriteTo(dfMsg.Data, addrEx)
if c.TrafficCounter != nil {
c.TrafficCounter.Tx(c.AuthLabel, len(dfMsg.Data))
}
case acl.ActionBlock:
// Do nothing
case acl.ActionHijack:
var isDomain bool
var hijackIPAddr *net.IPAddr
var err error
if c.Transport.ProxyEnabled() { // Case for domain requests + SOCKS5 outbound
hijackIPAddr, isDomain = c.Transport.ParseIPAddr(arg) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain
err = nil
} else {
hijackIPAddr, isDomain, err = c.Transport.ResolveIPAddr(arg)
}
if err == nil {
addrEx := &transport.AddrEx{
IPAddr: hijackIPAddr,
Port: int(dfMsg.Port),
}
if isDomain {
addrEx.Domain = arg
}
_, _ = conn.WriteTo(dfMsg.Data, addrEx)
if c.TrafficCounter != nil {
c.TrafficCounter.Tx(c.AuthLabel, len(dfMsg.Data))
}
}
default:
// Do nothing
}
}
}
func (c *serverClient) handleTCP(stream quic.Stream, host string, port uint16) {
addrStr := net.JoinHostPort(host, strconv.Itoa(int(port)))
action, arg := acl.ActionDirect, ""
var isDomain bool
var ipAddr *net.IPAddr
var err error
if c.ACLEngine != nil {
action, arg, isDomain, ipAddr, err = c.ACLEngine.ResolveAndMatch(host, port, false)
} else if c.Transport.ProxyEnabled() { // Case for domain requests + SOCKS5 outbound
ipAddr, isDomain = c.Transport.ParseIPAddr(host) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain
err = nil
} else {
ipAddr, isDomain, err = c.Transport.ResolveIPAddr(host)
}
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: "host resolution failure",
})
c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err)
return
}
c.CTCPRequestFunc(c.ClientAddr(), c.Auth, addrStr, action, arg)
var conn net.Conn // Connection to be piped
switch action {
case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side
addrEx := &transport.AddrEx{
IPAddr: ipAddr,
Port: int(port),
}
if isDomain {
addrEx.Domain = host
}
conn, err = c.Transport.DialTCP(addrEx)
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: err.Error(),
})
c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err)
return
}
case acl.ActionBlock:
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: "blocked by ACL",
})
return
case acl.ActionHijack:
var isDomain bool
var hijackIPAddr *net.IPAddr
var err error
if c.Transport.ProxyEnabled() { // Case for domain requests + SOCKS5 outbound
hijackIPAddr, isDomain = c.Transport.ParseIPAddr(arg) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain
err = nil
} else {
hijackIPAddr, isDomain, err = c.Transport.ResolveIPAddr(arg)
}
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: err.Error(),
})
c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err)
return
}
addrEx := &transport.AddrEx{
IPAddr: hijackIPAddr,
Port: int(port),
}
if isDomain {
addrEx.Domain = arg
}
conn, err = c.Transport.DialTCP(addrEx)
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: err.Error(),
})
c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err)
return
}
default:
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: "ACL error",
})
return
}
// So far so good if we reach here
defer conn.Close()
err = struc.Pack(stream, &serverResponse{
OK: true,
})
if err != nil {
return
}
if c.TrafficCounter != nil {
err = utils.Pipe2Way(stream, conn, func(i int) {
if i > 0 {
c.TrafficCounter.Tx(c.AuthLabel, i)
} else {
c.TrafficCounter.Rx(c.AuthLabel, -i)
}
})
} else {
err = utils.Pipe2Way(stream, conn, nil)
}
c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err)
}
func (c *serverClient) handleUDP(stream quic.Stream) {
// Like in SOCKS5, the stream here is only used to maintain the UDP session. No need to read anything from it
conn, err := c.Transport.ListenUDP()
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: "UDP initialization failed",
})
c.CUDPErrorFunc(c.ClientAddr(), c.Auth, 0, err)
return
}
defer conn.Close()
var id uint32
c.udpSessionMutex.Lock()
id = c.nextUDPSessionID
c.udpSessionMap[id] = conn
c.nextUDPSessionID += 1
c.udpSessionMutex.Unlock()
err = struc.Pack(stream, &serverResponse{
OK: true,
UDPSessionID: id,
})
if err != nil {
return
}
c.CUDPRequestFunc(c.ClientAddr(), c.Auth, id)
// Receive UDP packets, send them to the client
go func() {
buf := make([]byte, udpBufferSize)
for {
n, rAddr, err := conn.ReadFrom(buf)
if n > 0 {
var msgBuf bytes.Buffer
msg := udpMessage{
SessionID: id,
Host: rAddr.IP.String(),
Port: uint16(rAddr.Port),
FragCount: 1,
Data: buf[:n],
}
// try no frag first
_ = struc.Pack(&msgBuf, &msg)
sendErr := c.CC.SendMessage(msgBuf.Bytes())
if sendErr != nil {
if errSize, ok := sendErr.(quic.ErrMessageTooLarge); ok {
// need to frag
msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1
fragMsgs := fragUDPMessage(msg, int(errSize))
for _, fragMsg := range fragMsgs {
msgBuf.Reset()
_ = struc.Pack(&msgBuf, &fragMsg)
_ = c.CC.SendMessage(msgBuf.Bytes())
}
}
}
if c.TrafficCounter != nil {
c.TrafficCounter.Rx(c.AuthLabel, n)
}
}
if err != nil {
break
}
}
_ = stream.Close()
}()
// Hold the stream until it's closed by the client
buf := make([]byte, 1024)
for {
_, err = stream.Read(buf)
if err != nil {
break
}
}
c.CUDPErrorFunc(c.ClientAddr(), c.Auth, id, err)
// Remove the session
c.udpSessionMutex.Lock()
delete(c.udpSessionMap, id)
c.udpSessionMutex.Unlock()
}

View file

@ -1,58 +0,0 @@
package cs
import (
"context"
"time"
"github.com/quic-go/quic-go"
)
// qStream is a wrapper of quic.Stream that handles Close() correctly.
// quic-go's quic.Stream.Close() only closes the write side of the stream,
// NOT the read side. This would cause the pipe(s) to hang at Read() even
// after the stream is supposedly "closed".
// Ref: https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/stream.go
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)
}

58
core/errors/errors.go Normal file
View file

@ -0,0 +1,58 @@
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
}
// 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
}

View file

@ -3,34 +3,28 @@ module github.com/apernet/hysteria/core
go 1.20
require (
github.com/coreos/go-iptables v0.6.0
github.com/google/gopacket v1.1.19
github.com/hashicorp/golang-lru/v2 v2.0.1
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/oschwald/geoip2-golang v1.8.0
github.com/quic-go/quic-go v0.34.0
github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a
golang.org/x/sys v0.7.0
github.com/quic-go/quic-go v0.0.0-00010101000000-000000000000
golang.org/x/time v0.3.0
)
require (
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f // indirect
github.com/onsi/ginkgo/v2 v2.8.0 // indirect
github.com/oschwald/maxminddb-golang v1.10.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.3.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)
replace github.com/quic-go/quic-go => github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473

View file

@ -1,11 +1,11 @@
github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 h1:3KFetJ/lUFn0m9xTFg+rMmz2nyHg+D2boJX0Rp4OF6c=
github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
@ -14,25 +14,17 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f h1:gl1DCiSk+mrXXBGPm6CEeS2MkJuMVzAOrXg34oVj1QI=
github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4=
github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI=
github.com/onsi/ginkgo/v2 v2.8.0/go.mod h1:6JsQiECmxCa3V5st74AL/AmsV482EDdVrGaVW6z3oYU=
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
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/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
@ -45,55 +37,50 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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-20220212043548-414499347d4a h1:BOqgJ4jku0LHPDoR51RD8Mxmo0LHxCzJT/M9MemYdHo=
github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a/go.mod h1:7NloQcrxaZYKURWph5HLxVDlIwMHJXCPkeWPtpftsIg=
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe h1:gMWxZxBFRAXqoGkwkYlPX2zvyyKNWJpxOxCrjqJkm5A=
github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 h1:BEABXpNXLEz0WxtA+6CQIz2xkg80e+1zrhWyMcq8VzE=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ=
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,77 @@
package frag
import (
"github.com/apernet/hysteria/core/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
}

View file

@ -0,0 +1,336 @@
package frag
import (
"reflect"
"testing"
"github.com/apernet/hysteria/core/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)
}
})
}
}

View file

@ -0,0 +1,191 @@
package integration_tests
import (
"bytes"
"crypto/rand"
"io"
"net"
"testing"
"github.com/apernet/hysteria/core/client"
"github.com/apernet/hysteria/core/server"
)
// TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly.
// In other words, closing one of the client/remote connections should cause the other to close as well.
func TestClientServerTCPClose(t *testing.T) {
// Create server
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "password",
ID: "nobody",
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
ServerName: udpAddr.String(),
Auth: "password",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
t.Run("Close local", func(t *testing.T) {
// TCP sink server
sinkAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 33344}
sinkListener, err := net.ListenTCP("tcp", sinkAddr)
if err != nil {
t.Fatal("error creating sink server:", err)
}
sinkCh := make(chan sinkEvent, 1)
sinkServer := &tcpSinkServer{
Listener: sinkListener,
Ch: sinkCh,
}
defer sinkServer.Close()
go sinkServer.Serve()
// Generate some random data
sData := make([]byte, 1024000)
_, err = rand.Read(sData)
if err != nil {
t.Fatal("error generating random data:", err)
}
// Dial and send data to TCP sink server
conn, err := c.DialTCP(sinkAddr.String())
if err != nil {
t.Fatal("error dialing TCP:", err)
}
defer conn.Close()
_, err = conn.Write(sData)
if err != nil {
t.Fatal("error writing to TCP:", err)
}
// Close the connection
// This should cause the sink server to send an event to the channel
_ = conn.Close()
event := <-sinkCh
if event.Err != nil {
t.Fatal("non-nil error received from sink server:", event.Err)
}
if !bytes.Equal(event.Data, sData) {
t.Fatal("data mismatch")
}
})
t.Run("Close remote", func(t *testing.T) {
// Generate some random data
sData := make([]byte, 1024000)
_, err = rand.Read(sData)
if err != nil {
t.Fatal("error generating random data:", err)
}
// TCP sender server
senderAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 33345}
senderListener, err := net.ListenTCP("tcp", senderAddr)
if err != nil {
t.Fatal("error creating sender server:", err)
}
senderServer := &tcpSenderServer{
Listener: senderListener,
Data: sData,
}
defer senderServer.Close()
go senderServer.Serve()
// Dial and read data from TCP sender server
conn, err := c.DialTCP(senderAddr.String())
if err != nil {
t.Fatal("error dialing TCP:", err)
}
defer conn.Close()
rData, err := io.ReadAll(conn)
if err != nil {
t.Fatal("error reading from TCP:", err)
}
if !bytes.Equal(rData, sData) {
t.Fatal("data mismatch")
}
})
}
// TestClientServerUDPClose is the same as TestClientServerTCPClose, but for UDP.
// Checking for UDP close is a bit tricky, so we will rely on the server event for now.
func TestClientServerUDPClose(t *testing.T) {
urCh := make(chan udpRequestEvent, 1)
ueCh := make(chan udpErrorEvent, 1)
// Create server
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "password",
ID: "nobody",
},
EventLogger: &channelEventLogger{
UDPRequestEventCh: urCh,
UDPErrorEventCh: ueCh,
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
ServerName: udpAddr.String(),
Auth: "password",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
// Listen UDP and close it, then check the server events
conn, err := c.ListenUDP()
if err != nil {
t.Fatal("error listening UDP:", err)
}
_ = conn.Close()
reqEvent := <-urCh
if reqEvent.ID != "nobody" {
t.Fatal("incorrect ID in request event")
}
errEvent := <-ueCh
if errEvent.ID != "nobody" {
t.Fatal("incorrect ID in error event")
}
if errEvent.Err != nil {
t.Fatal("non-nil error received from server:", errEvent.Err)
}
}

View file

@ -0,0 +1,130 @@
package integration_tests
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/apernet/hysteria/core/internal/protocol"
"github.com/apernet/hysteria/core/server"
"github.com/quic-go/quic-go"
"github.com/quic-go/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 probers.
func TestServerMasquerade(t *testing.T) {
// Create server
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "password",
ID: "nobody",
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// QUIC connection & RoundTripper
var conn quic.EarlyConnection
rt := &http3.RoundTripper{
EnableDatagrams: true,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
qc, err := quic.DialAddrEarlyContext(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)
if err != nil {
t.Fatal("error sending request:", err)
}
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected status %d, got %d", http.StatusNotFound, resp.StatusCode)
}
for k := range resp.Header {
// Make sure no strange headers are sent
if strings.Contains(k, "Hysteria") {
t.Fatal("expected no Hysteria headers, got", k)
}
}
buf := make([]byte, 1024)
// We send a TCP request anyway, see if we get a response
tcpStream, err := conn.OpenStream()
if err != nil {
t.Fatal("error opening stream:", err)
}
defer tcpStream.Close()
err = protocol.WriteTCPRequest(tcpStream, "www.google.com:443")
if err != nil {
t.Fatal("error sending request:", err)
}
// We should receive nothing
_ = tcpStream.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err := tcpStream.Read(buf)
if n != 0 {
t.Fatal("expected no response, got", n)
}
if nErr, ok := err.(net.Error); !ok || !nErr.Timeout() {
t.Fatal("expected timeout, got", err)
}
// Try UDP request
udpStream, err := conn.OpenStream()
if err != nil {
t.Fatal("error opening stream:", err)
}
defer udpStream.Close()
err = protocol.WriteUDPRequest(udpStream)
if err != nil {
t.Fatal("error sending request:", err)
}
// We should receive nothing
_ = udpStream.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err = udpStream.Read(buf)
if n != 0 {
t.Fatal("expected no response, got", n)
}
if nErr, ok := err.(net.Error); !ok || !nErr.Timeout() {
t.Fatal("expected timeout, got", err)
}
}

View file

@ -0,0 +1,229 @@
package integration_tests
import (
"errors"
"io"
"net"
"testing"
"github.com/apernet/hysteria/core/client"
coreErrs "github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/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 that doesn't exist.
// The client should still be able to be created, but TCP & UDP requests should fail.
func TestClientNoServer(t *testing.T) {
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514},
ServerName: "not_a_real_server",
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
var cErr *coreErrs.ConnectError
// Try TCP
_, err = c.DialTCP("google.com:443")
if !errors.As(err, &cErr) {
t.Fatal("expected connect error from DialTCP")
}
// Try UDP
_, err = c.ListenUDP()
if !errors.As(err, &cErr) {
t.Fatal("expected connect error from ListenUDP")
}
}
// 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
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "correct password",
ID: "nobody",
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
ServerName: udpAddr.String(),
Auth: "wrong password",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
var aErr *coreErrs.AuthError
// Try TCP
_, err = c.DialTCP("google.com:443")
if !errors.As(err, &aErr) {
t.Fatal("expected auth error from DialTCP")
}
// Try UDP
_, err = c.ListenUDP()
if !errors.As(err, &aErr) {
t.Fatal("expected auth error from ListenUDP")
}
}
// TestClientServerTCPEcho tests TCP forwarding using a TCP echo server.
func TestClientServerTCPEcho(t *testing.T) {
// Create server
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "password",
ID: "nobody",
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// Create TCP echo server
echoTCPAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14515}
echoListener, err := net.ListenTCP("tcp", echoTCPAddr)
if err != nil {
t.Fatal("error creating TCP echo server:", err)
}
echoServer := &tcpEchoServer{Listener: echoListener}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
ServerName: udpAddr.String(),
Auth: "password",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
// Dial TCP
conn, err := c.DialTCP(echoTCPAddr.String())
if err != nil {
t.Fatal("error dialing TCP:", err)
}
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
_, err = conn.Write(sData)
if err != nil {
t.Fatal("error writing to TCP:", err)
}
rData := make([]byte, len(sData))
_, err = io.ReadFull(conn, rData)
if err != nil {
t.Fatal("error reading from TCP:", err)
}
if string(rData) != string(sData) {
t.Fatalf("expected %q, got %q", sData, rData)
}
}
// TestClientServerUDPEcho tests UDP forwarding using a UDP echo server.
func TestClientServerUDPEcho(t *testing.T) {
// Create server
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "password",
ID: "nobody",
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// Create UDP echo server
echoUDPAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 55555}
echoConn, err := net.ListenUDP("udp", echoUDPAddr)
if err != nil {
t.Fatal("error creating UDP echo server:", err)
}
echoServer := &udpEchoServer{Conn: echoConn}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
ServerName: udpAddr.String(),
Auth: "password",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
// Listen UDP
conn, err := c.ListenUDP()
if err != nil {
t.Fatal("error listening UDP:", err)
}
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
err = conn.Send(sData, echoUDPAddr.String())
if err != nil {
t.Fatal("error sending UDP:", err)
}
rData, rAddr, err := conn.Receive()
if err != nil {
t.Fatal("error receiving UDP:", err)
}
if string(rData) != string(sData) {
t.Fatalf("expected %q, got %q", sData, rData)
}
if rAddr != echoUDPAddr.String() {
t.Fatalf("expected %q, got %q", echoUDPAddr.String(), rAddr)
}
}

View file

@ -0,0 +1,291 @@
package integration_tests
import (
"context"
"crypto/rand"
"fmt"
"io"
"net"
"sync"
"testing"
"golang.org/x/time/rate"
"github.com/apernet/hysteria/core/client"
"github.com/apernet/hysteria/core/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)
if err != nil {
t.Fatal("error generating random data:", 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()
if len(errChan) > 0 {
t.Fatal("error reading from TCP:", <-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)
if err != nil {
t.Fatal("error generating random data:", 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()
if len(errChan) > 0 {
t.Fatal("error reading from UDP:", <-errChan)
}
}
}
func TestClientServerTCPStress(t *testing.T) {
// Create server
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "password",
ID: "nobody",
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// Create TCP echo server
echoTCPAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14515}
echoListener, err := net.ListenTCP("tcp", echoTCPAddr)
if err != nil {
t.Fatal("error creating TCP echo server:", err)
}
echoServer := &tcpEchoServer{Listener: echoListener}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
ServerName: udpAddr.String(),
Auth: "password",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
dialFunc := func() (net.Conn, error) {
return c.DialTCP(echoTCPAddr.String())
}
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
udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
t.Fatal("error creating server:", err)
}
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: &pwAuthenticator{
Password: "password",
ID: "nobody",
},
})
if err != nil {
t.Fatal("error creating server:", err)
}
defer s.Close()
go s.Serve()
// Create UDP echo server
echoUDPAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14515}
echoListener, err := net.ListenUDP("udp", echoUDPAddr)
if err != nil {
t.Fatal("error creating UDP echo server:", err)
}
echoServer := &udpEchoServer{Conn: echoListener}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
ServerName: udpAddr.String(),
Auth: "password",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
if err != nil {
t.Fatal("error creating client:", err)
}
defer c.Close()
t.Run("Single 1000x100b", (&udpStressor{
ListenFunc: c.ListenUDP,
ServerAddr: echoUDPAddr.String(),
Size: 100,
Count: 1000,
Parallel: 1,
Iterations: 1,
}).Run)
t.Run("Single 1000x3k", (&udpStressor{
ListenFunc: c.ListenUDP,
ServerAddr: echoUDPAddr.String(),
Size: 3000,
Count: 1000,
Parallel: 1,
Iterations: 1,
}).Run)
t.Run("5 Sequential 1000x100b", (&udpStressor{
ListenFunc: c.ListenUDP,
ServerAddr: echoUDPAddr.String(),
Size: 100,
Count: 1000,
Parallel: 1,
Iterations: 5,
}).Run)
t.Run("5 Sequential 200x3k", (&udpStressor{
ListenFunc: c.ListenUDP,
ServerAddr: echoUDPAddr.String(),
Size: 3000,
Count: 200,
Parallel: 1,
Iterations: 5,
}).Run)
t.Run("2 Sequential 5 Parallel 1000x100b", (&udpStressor{
ListenFunc: c.ListenUDP,
ServerAddr: echoUDPAddr.String(),
Size: 100,
Count: 1000,
Parallel: 5,
Iterations: 2,
}).Run)
t.Run("2 Sequential 5 Parallel 200x3k", (&udpStressor{
ListenFunc: c.ListenUDP,
ServerAddr: echoUDPAddr.String(),
Size: 3000,
Count: 200,
Parallel: 5,
Iterations: 2,
}).Run)
t.Run("10 Sequential 5 Parallel 200x3k", (&udpStressor{
ListenFunc: c.ListenUDP,
ServerAddr: echoUDPAddr.String(),
Size: 3000,
Count: 200,
Parallel: 5,
Iterations: 10,
}).Run)
}

View file

@ -0,0 +1,23 @@
-----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-----

View file

@ -0,0 +1,27 @@
-----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-----

View file

@ -0,0 +1,250 @@
package integration_tests
import (
"bytes"
"crypto/tls"
"io"
"net"
"github.com/apernet/hysteria/core/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},
}
}
type pwAuthenticator struct {
Password string
ID string
}
func (a *pwAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
if auth != a.Password {
return false, ""
}
return true, a.ID
}
// 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()
}
type sinkEvent struct {
Data []byte
Err error
}
// tcpSinkServer is a TCP server that reads data from the connection,
// and sends what it read to the channel when the connection is closed.
type tcpSinkServer struct {
Listener net.Listener
Ch chan<- sinkEvent
}
func (s *tcpSinkServer) Serve() error {
for {
conn, err := s.Listener.Accept()
if err != nil {
return err
}
go func() {
var buf bytes.Buffer
_, err := io.Copy(&buf, conn)
_ = conn.Close()
s.Ch <- sinkEvent{Data: buf.Bytes(), Err: err}
}()
}
}
func (s *tcpSinkServer) Close() error {
return s.Listener.Close()
}
// tcpSenderServer is a TCP server that sends data to the connection,
// and closes the connection when all data has been sent.
type tcpSenderServer struct {
Listener net.Listener
Data []byte
}
func (s *tcpSenderServer) Serve() error {
for {
conn, err := s.Listener.Accept()
if err != nil {
return err
}
go func() {
_, _ = conn.Write(s.Data)
_ = conn.Close()
}()
}
}
func (s *tcpSenderServer) 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()
}
type connectEvent struct {
Addr net.Addr
ID string
TX uint64
}
type disconnectEvent struct {
Addr net.Addr
ID string
Err error
}
type tcpRequestEvent struct {
Addr net.Addr
ID string
ReqAddr string
}
type tcpErrorEvent struct {
Addr net.Addr
ID string
ReqAddr string
Err error
}
type udpRequestEvent struct {
Addr net.Addr
ID string
SessionID uint32
}
type udpErrorEvent struct {
Addr net.Addr
ID string
SessionID uint32
Err error
}
type channelEventLogger struct {
ConnectEventCh chan connectEvent
DisconnectEventCh chan disconnectEvent
TCPRequestEventCh chan tcpRequestEvent
TCPErrorEventCh chan tcpErrorEvent
UDPRequestEventCh chan udpRequestEvent
UDPErrorEventCh chan udpErrorEvent
}
func (l *channelEventLogger) Connect(addr net.Addr, id string, tx uint64) {
if l.ConnectEventCh != nil {
l.ConnectEventCh <- connectEvent{
Addr: addr,
ID: id,
TX: tx,
}
}
}
func (l *channelEventLogger) Disconnect(addr net.Addr, id string, err error) {
if l.DisconnectEventCh != nil {
l.DisconnectEventCh <- disconnectEvent{
Addr: addr,
ID: id,
Err: err,
}
}
}
func (l *channelEventLogger) TCPRequest(addr net.Addr, id, reqAddr string) {
if l.TCPRequestEventCh != nil {
l.TCPRequestEventCh <- tcpRequestEvent{
Addr: addr,
ID: id,
ReqAddr: reqAddr,
}
}
}
func (l *channelEventLogger) TCPError(addr net.Addr, id, reqAddr string, err error) {
if l.TCPErrorEventCh != nil {
l.TCPErrorEventCh <- tcpErrorEvent{
Addr: addr,
ID: id,
ReqAddr: reqAddr,
Err: err,
}
}
}
func (l *channelEventLogger) UDPRequest(addr net.Addr, id string, sessionID uint32) {
if l.UDPRequestEventCh != nil {
l.UDPRequestEventCh <- udpRequestEvent{
Addr: addr,
ID: id,
SessionID: sessionID,
}
}
}
func (l *channelEventLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) {
if l.UDPErrorEventCh != nil {
l.UDPErrorEventCh <- udpErrorEvent{
Addr: addr,
ID: id,
SessionID: sessionID,
Err: err,
}
}
}

View file

@ -0,0 +1,7 @@
//go:build linux || windows
package pmtud
const (
DisablePathMTUDiscovery = false
)

View file

@ -0,0 +1,12 @@
//go:build !linux && !windows
package pmtud
// quic-go's MTU discovery is enabled by default across all platforms. However, our testing has found that on certain
// platforms (e.g. macOS) the DF bit is not set. As a result, probe packets that should never be fragmented are still
// fragmented and transmitted. So we have decided to enable MTU discovery only on Linux and Windows for now, as we have
// verified its functionality on these platforms.
const (
DisablePathMTUDiscovery = true
)

View file

@ -0,0 +1,36 @@
package protocol
import (
"net/http"
"strconv"
)
const (
URLHost = "hysteria"
URLPath = "/auth"
HeaderAuth = "Hysteria-Auth"
HeaderCCRX = "Hysteria-CC-RX"
StatusAuthOK = 233
)
func AuthRequestDataFromHeader(h http.Header) (auth string, rx uint64) {
auth = h.Get(HeaderAuth)
rx, _ = strconv.ParseUint(h.Get(HeaderCCRX), 10, 64)
return
}
func AuthRequestDataToHeader(h http.Header, auth string, rx uint64) {
h.Set(HeaderAuth, auth)
h.Set(HeaderCCRX, strconv.FormatUint(rx, 10))
}
func AuthResponseDataFromHeader(h http.Header) (rx uint64) {
rx, _ = strconv.ParseUint(h.Get(HeaderCCRX), 10, 64)
return
}
func AuthResponseDataToHeader(h http.Header, rx uint64) {
h.Set(HeaderCCRX, strconv.FormatUint(rx, 10))
}

View file

@ -0,0 +1,261 @@
package protocol
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"github.com/apernet/hysteria/core/errors"
"github.com/quic-go/quic-go/quicvarint"
)
const (
FrameTypeTCPRequest = 0x401
FrameTypeUDPRequest = 0x402
MaxAddressLength = 2048 // for preventing DoS attack by sending a very large address length
MaxMessageLength = 2048 // for preventing DoS attack by sending a very large message length
MaxUDPSize = 4096
maxVarInt1 = 63
maxVarInt2 = 16383
maxVarInt4 = 1073741823
maxVarInt8 = 4611686018427387903
)
// TCPRequest format:
// 0x401 (QUIC varint)
// Address length (QUIC varint)
// Address (bytes)
func ReadTCPRequest(r io.Reader) (string, error) {
bReader := quicvarint.NewReader(r)
l, err := quicvarint.Read(bReader)
if err != nil {
return "", err
}
if l == 0 || l > MaxAddressLength {
return "", errors.ProtocolError{Message: "invalid address length"}
}
buf := make([]byte, l)
_, err = io.ReadFull(r, buf)
return string(buf), err
}
func WriteTCPRequest(w io.Writer, addr string) error {
l := len(addr)
sz := int(quicvarint.Len(FrameTypeTCPRequest)) + int(quicvarint.Len(uint64(l))) + l
buf := make([]byte, sz)
i := varintPut(buf, FrameTypeTCPRequest)
i += varintPut(buf[i:], uint64(l))
copy(buf[i:], addr)
_, err := w.Write(buf)
return err
}
// TCPResponse format:
// Status (byte, 0=ok, 1=error)
// Message length (QUIC varint)
// Message (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)
l, err := quicvarint.Read(bReader)
if err != nil {
return false, "", err
}
if l == 0 {
// No message is ok
return status[0] == 0, "", nil
}
if l > MaxMessageLength {
return false, "", errors.ProtocolError{Message: "invalid message length"}
}
buf := make([]byte, l)
_, err = io.ReadFull(r, buf)
return status[0] == 0, string(buf), err
}
func WriteTCPResponse(w io.Writer, ok bool, msg string) error {
l := len(msg)
sz := 1 + int(quicvarint.Len(uint64(l))) + l
buf := make([]byte, sz)
if ok {
buf[0] = 0
} else {
buf[0] = 1
}
i := varintPut(buf[1:], uint64(l))
copy(buf[1+i:], msg)
_, err := w.Write(buf)
return err
}
// UDPRequest format:
// 0x402 (QUIC varint)
// Nothing to read
func WriteUDPRequest(w io.Writer) error {
buf := make([]byte, quicvarint.Len(FrameTypeUDPRequest))
varintPut(buf, FrameTypeUDPRequest)
_, err := w.Write(buf)
return err
}
// UDPResponse format:
// Status (byte, 0=ok, 1=error)
// Session ID (uint32 BE)
// Message length (QUIC varint)
// Message (bytes)
func ReadUDPResponse(r io.Reader) (bool, uint32, string, error) {
var status [1]byte
if _, err := io.ReadFull(r, status[:]); err != nil {
return false, 0, "", err
}
var sessionID uint32
if err := binary.Read(r, binary.BigEndian, &sessionID); err != nil {
return false, 0, "", err
}
bReader := quicvarint.NewReader(r)
l, err := quicvarint.Read(bReader)
if err != nil {
return false, 0, "", err
}
if l == 0 {
// No message is ok
return status[0] == 0, sessionID, "", nil
}
if l > MaxMessageLength {
return false, 0, "", errors.ProtocolError{Message: "invalid message length"}
}
buf := make([]byte, l)
_, err = io.ReadFull(r, buf)
return status[0] == 0, sessionID, string(buf), err
}
func WriteUDPResponse(w io.Writer, ok bool, sessionID uint32, msg string) error {
l := len(msg)
buf := make([]byte, 5+int(quicvarint.Len(uint64(l)))+l)
if ok {
buf[0] = 0
} else {
buf[0] = 1
}
binary.BigEndian.PutUint32(buf[1:], sessionID)
i := varintPut(buf[5:], uint64(l))
copy(buf[5+i:], msg)
_, 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()
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))
}

View file

@ -0,0 +1,557 @@
package protocol
import (
"bytes"
"io"
"reflect"
"testing"
)
func TestReadTCPRequest(t *testing.T) {
type args struct {
r io.Reader
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "normal 1",
args: args{
r: bytes.NewReader([]byte("\x05hello")),
},
want: "hello",
wantErr: false,
},
{
name: "normal 2",
args: args{
r: bytes.NewReader([]byte("\x41\x25We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People")),
},
want: "We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People",
wantErr: false,
},
{
name: "empty",
args: args{
r: bytes.NewReader([]byte("\x00")),
},
want: "",
wantErr: true,
},
{
name: "incomplete",
args: args{
r: bytes.NewReader([]byte("\x06oh no")),
},
want: "oh no\x00",
wantErr: true,
},
{
name: "too long",
args: args{
r: bytes.NewReader([]byte("\x66\x77\x88Whatever")),
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ReadTCPRequest(tt.args.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) {
type args struct {
addr string
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "normal 1",
args: args{
addr: "hello",
},
wantW: "\x44\x01\x05hello",
wantErr: false,
},
{
name: "normal 2",
args: args{
addr: "We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People",
},
wantW: "\x44\x01\x41\x25We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People",
wantErr: false,
},
{
name: "empty",
args: args{
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.args.addr)
if (err != nil) != tt.wantErr {
t.Errorf("WriteTCPRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("WriteTCPRequest() gotW = %v, want %v", []byte(gotW), tt.wantW)
}
})
}
}
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 TestReadTCPResponse(t *testing.T) {
type args struct {
r io.Reader
}
tests := []struct {
name string
args args
want bool
want1 string
wantErr bool
}{
{
name: "success 1",
args: args{
r: bytes.NewReader([]byte("\x00\x00")),
},
want: true,
want1: "",
wantErr: false,
},
{
name: "success 2",
args: args{
r: bytes.NewReader([]byte("\x00\x12are ya winning son")),
},
want: true,
want1: "are ya winning son",
wantErr: false,
},
{
name: "failure 1",
args: args{
r: bytes.NewReader([]byte("\x01\x00")),
},
want: false,
want1: "",
wantErr: false,
},
{
name: "failure 2",
args: args{
r: bytes.NewReader([]byte("\x01\x15you ain't winning son")),
},
want: false,
want1: "you ain't winning son",
wantErr: false,
},
{
name: "incomplete",
args: args{
r: bytes.NewReader([]byte("\x01\x25princess peach is in another castle")),
},
want: false,
want1: "princess peach is in another castle\x00\x00",
wantErr: true,
},
{
name: "too long",
args: args{
r: bytes.NewReader([]byte("\xAA\xBB\xCCrandom stuff")),
},
want: false,
want1: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := ReadTCPResponse(tt.args.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
wantErr bool
}{
{
name: "success 1",
args: args{
ok: true,
msg: "",
},
wantW: "\x00\x00",
},
{
name: "success 2",
args: args{
ok: true,
msg: "Welcome XDXDXD",
},
wantW: "\x00\x0EWelcome XDXDXD",
},
{
name: "failure 1",
args: args{
ok: false,
msg: "",
},
wantW: "\x01\x00",
},
{
name: "failure 2",
args: args{
ok: false,
msg: "me trying to find who u are: ...",
},
wantW: "\x01\x20me trying to find who u are: ...",
},
}
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(); gotW != tt.wantW {
t.Errorf("WriteTCPResponse() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
func TestWriteUDPRequest(t *testing.T) {
tests := []struct {
name string
wantW string
wantErr bool
}{
{
name: "normal",
wantW: "\x44\x02",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := WriteUDPRequest(w)
if (err != nil) != tt.wantErr {
t.Errorf("WriteUDPRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("WriteUDPRequest() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}
func TestReadUDPResponse(t *testing.T) {
type args struct {
r io.Reader
}
tests := []struct {
name string
args args
want bool
want1 uint32
want2 string
wantErr bool
}{
{
name: "success 1",
args: args{
r: bytes.NewReader([]byte("\x00\x00\x00\x00\x02\x00")),
},
want: true,
want1: 2,
want2: "",
wantErr: false,
},
{
name: "success 2",
args: args{
r: bytes.NewReader([]byte("\x00\x00\x00\x00\x03\x0EWelcome XDXDXD")),
},
want: true,
want1: 3,
want2: "Welcome XDXDXD",
wantErr: false,
},
{
name: "failure",
args: args{
r: bytes.NewReader([]byte("\x01\x00\x00\x00\x01\x20me trying to find who u are: ...")),
},
want: false,
want1: 1,
want2: "me trying to find who u are: ...",
wantErr: false,
},
{
name: "incomplete",
args: args{
r: bytes.NewReader([]byte("\x00\x00\x00\x00\x02")),
},
want: false,
want1: 0,
want2: "",
wantErr: true,
},
{
name: "too long",
args: args{
r: bytes.NewReader([]byte("\x00\x00\x00\x00\x02\xCC\xFF\x66no cap")),
},
want: false,
want1: 0,
want2: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, got2, err := ReadUDPResponse(tt.args.r)
if (err != nil) != tt.wantErr {
t.Errorf("ReadUDPResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ReadUDPResponse() got = %v, want %v", got, tt.want)
}
if got1 != tt.want1 {
t.Errorf("ReadUDPResponse() got1 = %v, want %v", got1, tt.want1)
}
if got2 != tt.want2 {
t.Errorf("ReadUDPResponse() got2 = %v, want %v", got2, tt.want2)
}
})
}
}
func TestWriteUDPResponse(t *testing.T) {
type args struct {
ok bool
sessionID uint32
msg string
}
tests := []struct {
name string
args args
wantW string
wantErr bool
}{
{
name: "success 1",
args: args{
ok: true,
sessionID: 88,
msg: "",
},
wantW: "\x00\x00\x00\x00\x58\x00",
},
{
name: "success 2",
args: args{
ok: true,
sessionID: 233,
msg: "together forever",
},
wantW: "\x00\x00\x00\x00\xE9\x10together forever",
},
{
name: "failure 1",
args: args{
ok: false,
sessionID: 1,
msg: "",
},
wantW: "\x01\x00\x00\x00\x01\x00",
},
{
name: "failure 2",
args: args{
ok: false,
sessionID: 696969,
msg: "run away run away",
},
wantW: "\x01\x00\x0A\xA2\x89\x11run away run away",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
err := WriteUDPResponse(w, tt.args.ok, tt.args.sessionID, tt.args.msg)
if (err != nil) != tt.wantErr {
t.Errorf("WriteUDPResponse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("WriteUDPResponse() gotW = %v, want %v", gotW, tt.wantW)
}
})
}
}

View file

@ -0,0 +1,62 @@
package utils
import (
"context"
"time"
"github.com/quic-go/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)
}

View file

@ -1 +0,0 @@
Grabbed from https://github.com/xtaci/tcpraw with modifications

View file

@ -1,95 +0,0 @@
package faketcp
import (
"net"
"sync"
"syscall"
"time"
"github.com/apernet/hysteria/core/pktconns/obfs"
)
const udpBufferSize = 4096
type ObfsFakeTCPPacketConn struct {
orig *TCPConn
obfs obfs.Obfuscator
readBuf []byte
readMutex sync.Mutex
writeBuf []byte
writeMutex sync.Mutex
}
func NewObfsFakeTCPConn(orig *TCPConn, obfs obfs.Obfuscator) *ObfsFakeTCPPacketConn {
return &ObfsFakeTCPPacketConn{
orig: orig,
obfs: obfs,
readBuf: make([]byte, udpBufferSize),
writeBuf: make([]byte, udpBufferSize),
}
}
func (c *ObfsFakeTCPPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
for {
c.readMutex.Lock()
n, addr, err := c.orig.ReadFrom(c.readBuf)
if n <= 0 {
c.readMutex.Unlock()
return 0, addr, err
}
newN := c.obfs.Deobfuscate(c.readBuf[:n], p)
c.readMutex.Unlock()
if newN > 0 {
// Valid packet
return newN, addr, err
} else if err != nil {
// Not valid and orig.ReadFrom had some error
return 0, addr, err
}
}
}
func (c *ObfsFakeTCPPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
c.writeMutex.Lock()
bn := c.obfs.Obfuscate(p, c.writeBuf)
_, err = c.orig.WriteTo(c.writeBuf[:bn], addr)
c.writeMutex.Unlock()
if err != nil {
return 0, err
} else {
return len(p), nil
}
}
func (c *ObfsFakeTCPPacketConn) Close() error {
return c.orig.Close()
}
func (c *ObfsFakeTCPPacketConn) LocalAddr() net.Addr {
return c.orig.LocalAddr()
}
func (c *ObfsFakeTCPPacketConn) SetDeadline(t time.Time) error {
return c.orig.SetDeadline(t)
}
func (c *ObfsFakeTCPPacketConn) SetReadDeadline(t time.Time) error {
return c.orig.SetReadDeadline(t)
}
func (c *ObfsFakeTCPPacketConn) SetWriteDeadline(t time.Time) error {
return c.orig.SetWriteDeadline(t)
}
func (c *ObfsFakeTCPPacketConn) SetReadBuffer(bytes int) error {
return c.orig.SetReadBuffer(bytes)
}
func (c *ObfsFakeTCPPacketConn) SetWriteBuffer(bytes int) error {
return c.orig.SetWriteBuffer(bytes)
}
func (c *ObfsFakeTCPPacketConn) SyscallConn() (syscall.RawConn, error) {
return c.orig.SyscallConn()
}

View file

@ -1,616 +0,0 @@
//go:build linux
// +build linux
package faketcp
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/coreos/go-iptables/iptables"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
var (
errOpNotImplemented = errors.New("operation not implemented")
errTimeout = errors.New("timeout")
expire = time.Minute
)
// a message from NIC
type message struct {
bts []byte
addr net.Addr
}
// a tcp flow information of a connection pair
type tcpFlow struct {
conn *net.TCPConn // the related system TCP connection of this flow
handle *net.IPConn // the handle to send packets
seq uint32 // TCP sequence number
ack uint32 // TCP acknowledge number
networkLayer gopacket.SerializableLayer // network layer header for tx
ts time.Time // last packet incoming time
buf gopacket.SerializeBuffer // a buffer for write
tcpHeader layers.TCP
}
// TCPConn defines a TCP-packet oriented connection
type TCPConn struct {
die chan struct{}
dieOnce sync.Once
// the main golang sockets
tcpconn *net.TCPConn // from net.Dial
listener *net.TCPListener // from net.Listen
// handles
handles []*net.IPConn
// packets captured from all related NICs will be delivered to this channel
chMessage chan message
// all TCP flows
flowTable map[string]*tcpFlow
flowsLock sync.Mutex
// iptables
iptables *iptables.IPTables
iprule []string
ip6tables *iptables.IPTables
ip6rule []string
// deadlines
readDeadline atomic.Value
writeDeadline atomic.Value
// serialization
opts gopacket.SerializeOptions
}
// lockflow locks the flow table and apply function `f` to the entry, and create one if not exist
func (conn *TCPConn) lockflow(addr net.Addr, f func(e *tcpFlow)) {
key := addr.String()
conn.flowsLock.Lock()
e := conn.flowTable[key]
if e == nil { // entry first visit
e = new(tcpFlow)
e.ts = time.Now()
e.buf = gopacket.NewSerializeBuffer()
}
f(e)
conn.flowTable[key] = e
conn.flowsLock.Unlock()
}
// clean expired flows
func (conn *TCPConn) cleaner() {
ticker := time.NewTicker(time.Minute)
select {
case <-conn.die:
return
case <-ticker.C:
conn.flowsLock.Lock()
for k, v := range conn.flowTable {
if time.Now().Sub(v.ts) > expire {
if v.conn != nil {
setTTL(v.conn, 64)
v.conn.Close()
}
delete(conn.flowTable, k)
}
}
conn.flowsLock.Unlock()
}
}
// captureFlow capture every inbound packets based on rules of BPF
func (conn *TCPConn) captureFlow(handle *net.IPConn, port int) {
buf := make([]byte, 2048)
opt := gopacket.DecodeOptions{NoCopy: true, Lazy: true}
for {
n, addr, err := handle.ReadFromIP(buf)
if err != nil {
return
}
// try decoding TCP frame from buf[:n]
packet := gopacket.NewPacket(buf[:n], layers.LayerTypeTCP, opt)
transport := packet.TransportLayer()
tcp, ok := transport.(*layers.TCP)
if !ok {
continue
}
// port filtering
if int(tcp.DstPort) != port {
continue
}
// address building
var src net.TCPAddr
src.IP = addr.IP
src.Port = int(tcp.SrcPort)
var orphan bool
// flow maintaince
conn.lockflow(&src, func(e *tcpFlow) {
if e.conn == nil { // make sure it's related to net.TCPConn
orphan = true // mark as orphan if it's not related net.TCPConn
}
// to keep track of TCP header related to this source
e.ts = time.Now()
if tcp.ACK {
e.seq = tcp.Ack
}
if tcp.SYN {
e.ack = tcp.Seq + 1
}
if tcp.PSH {
if e.ack == tcp.Seq {
e.ack = tcp.Seq + uint32(len(tcp.Payload))
}
}
e.handle = handle
})
// push data if it's not orphan
if !orphan && tcp.PSH {
payload := make([]byte, len(tcp.Payload))
copy(payload, tcp.Payload)
select {
case conn.chMessage <- message{payload, &src}:
case <-conn.die:
return
}
}
}
}
// ReadFrom implements the PacketConn ReadFrom method.
func (conn *TCPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
var timer *time.Timer
var deadline <-chan time.Time
if d, ok := conn.readDeadline.Load().(time.Time); ok && !d.IsZero() {
timer = time.NewTimer(time.Until(d))
defer timer.Stop()
deadline = timer.C
}
select {
case <-deadline:
return 0, nil, errTimeout
case <-conn.die:
return 0, nil, io.EOF
case packet := <-conn.chMessage:
n = copy(p, packet.bts)
return n, packet.addr, nil
}
}
// WriteTo implements the PacketConn WriteTo method.
func (conn *TCPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
var deadline <-chan time.Time
if d, ok := conn.writeDeadline.Load().(time.Time); ok && !d.IsZero() {
timer := time.NewTimer(time.Until(d))
defer timer.Stop()
deadline = timer.C
}
select {
case <-deadline:
return 0, errTimeout
case <-conn.die:
return 0, io.EOF
default:
raddr, err := net.ResolveTCPAddr("tcp", addr.String())
if err != nil {
return 0, err
}
var lport int
if conn.tcpconn != nil {
lport = conn.tcpconn.LocalAddr().(*net.TCPAddr).Port
} else {
lport = conn.listener.Addr().(*net.TCPAddr).Port
}
conn.lockflow(addr, func(e *tcpFlow) {
// if the flow doesn't have handle , assume this packet has lost, without notification
if e.handle == nil {
n = len(p)
return
}
// build tcp header with local and remote port
e.tcpHeader.SrcPort = layers.TCPPort(lport)
e.tcpHeader.DstPort = layers.TCPPort(raddr.Port)
binary.Read(rand.Reader, binary.LittleEndian, &e.tcpHeader.Window)
e.tcpHeader.Window |= 0x8000 // make sure it's larger than 32768
e.tcpHeader.Ack = e.ack
e.tcpHeader.Seq = e.seq
e.tcpHeader.PSH = true
e.tcpHeader.ACK = true
// build IP header with src & dst ip for TCP checksum
if raddr.IP.To4() != nil {
ip := &layers.IPv4{
Protocol: layers.IPProtocolTCP,
SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To4(),
DstIP: raddr.IP.To4(),
}
e.tcpHeader.SetNetworkLayerForChecksum(ip)
} else {
ip := &layers.IPv6{
NextHeader: layers.IPProtocolTCP,
SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To16(),
DstIP: raddr.IP.To16(),
}
e.tcpHeader.SetNetworkLayerForChecksum(ip)
}
e.buf.Clear()
gopacket.SerializeLayers(e.buf, conn.opts, &e.tcpHeader, gopacket.Payload(p))
if conn.tcpconn != nil {
_, err = e.handle.Write(e.buf.Bytes())
} else {
_, err = e.handle.WriteToIP(e.buf.Bytes(), &net.IPAddr{IP: raddr.IP})
}
// increase seq in flow
e.seq += uint32(len(p))
n = len(p)
})
}
return
}
// Close closes the connection.
func (conn *TCPConn) Close() error {
var err error
conn.dieOnce.Do(func() {
// signal closing
close(conn.die)
// close all established tcp connections
if conn.tcpconn != nil { // client
setTTL(conn.tcpconn, 64)
err = conn.tcpconn.Close()
} else if conn.listener != nil {
err = conn.listener.Close() // server
conn.flowsLock.Lock()
for k, v := range conn.flowTable {
if v.conn != nil {
setTTL(v.conn, 64)
v.conn.Close()
}
delete(conn.flowTable, k)
}
conn.flowsLock.Unlock()
}
// close handles
for k := range conn.handles {
conn.handles[k].Close()
}
// delete iptable
if conn.iptables != nil {
conn.iptables.Delete("filter", "OUTPUT", conn.iprule...)
}
if conn.ip6tables != nil {
conn.ip6tables.Delete("filter", "OUTPUT", conn.ip6rule...)
}
})
return err
}
// LocalAddr returns the local network address.
func (conn *TCPConn) LocalAddr() net.Addr {
if conn.tcpconn != nil {
return conn.tcpconn.LocalAddr()
} else if conn.listener != nil {
return conn.listener.Addr()
}
return nil
}
// SetDeadline implements the Conn SetDeadline method.
func (conn *TCPConn) SetDeadline(t time.Time) error {
if err := conn.SetReadDeadline(t); err != nil {
return err
}
if err := conn.SetWriteDeadline(t); err != nil {
return err
}
return nil
}
// SetReadDeadline implements the Conn SetReadDeadline method.
func (conn *TCPConn) SetReadDeadline(t time.Time) error {
conn.readDeadline.Store(t)
return nil
}
// SetWriteDeadline implements the Conn SetWriteDeadline method.
func (conn *TCPConn) SetWriteDeadline(t time.Time) error {
conn.writeDeadline.Store(t)
return nil
}
// SetDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header.
func (conn *TCPConn) SetDSCP(dscp int) error {
for k := range conn.handles {
if err := setDSCP(conn.handles[k], dscp); err != nil {
return err
}
}
return nil
}
// SetReadBuffer sets the size of the operating system's receive buffer associated with the connection.
func (conn *TCPConn) SetReadBuffer(bytes int) error {
var err error
for k := range conn.handles {
if err := conn.handles[k].SetReadBuffer(bytes); err != nil {
return err
}
}
return err
}
// SetWriteBuffer sets the size of the operating system's transmit buffer associated with the connection.
func (conn *TCPConn) SetWriteBuffer(bytes int) error {
var err error
for k := range conn.handles {
if err := conn.handles[k].SetWriteBuffer(bytes); err != nil {
return err
}
}
return err
}
func (conn *TCPConn) SyscallConn() (syscall.RawConn, error) {
if len(conn.handles) == 0 {
return nil, errors.New("no handles")
// How is it possible?
}
return conn.handles[0].SyscallConn()
}
// Dial connects to the remote TCP port,
// and returns a single packet-oriented connection
func Dial(network, address string) (*TCPConn, error) {
// remote address resolve
raddr, err := net.ResolveTCPAddr(network, address)
if err != nil {
return nil, err
}
// AF_INET
handle, err := net.DialIP("ip:tcp", nil, &net.IPAddr{IP: raddr.IP})
if err != nil {
return nil, err
}
// create an established tcp connection
// will hack this tcp connection for packet transmission
tcpconn, err := net.DialTCP(network, nil, raddr)
if err != nil {
return nil, err
}
// fields
conn := new(TCPConn)
conn.die = make(chan struct{})
conn.flowTable = make(map[string]*tcpFlow)
conn.tcpconn = tcpconn
conn.chMessage = make(chan message)
conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn })
conn.handles = append(conn.handles, handle)
conn.opts = gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
go conn.captureFlow(handle, tcpconn.LocalAddr().(*net.TCPAddr).Port)
go conn.cleaner()
// iptables
err = setTTL(tcpconn, 1)
if err != nil {
return nil, err
}
if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil {
rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"}
if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil {
if !exists {
if err = ipt.Append("filter", "OUTPUT", rule...); err == nil {
conn.iprule = rule
conn.iptables = ipt
}
}
}
}
if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil {
rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"}
if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil {
if !exists {
if err = ipt.Append("filter", "OUTPUT", rule...); err == nil {
conn.ip6rule = rule
conn.ip6tables = ipt
}
}
}
}
// discard everything
go io.Copy(ioutil.Discard, tcpconn)
return conn, nil
}
// Listen acts like net.ListenTCP,
// and returns a single packet-oriented connection
func Listen(network, address string) (*TCPConn, error) {
// fields
conn := new(TCPConn)
conn.flowTable = make(map[string]*tcpFlow)
conn.die = make(chan struct{})
conn.chMessage = make(chan message)
conn.opts = gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
// resolve address
laddr, err := net.ResolveTCPAddr(network, address)
if err != nil {
return nil, err
}
// AF_INET
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
if laddr.IP == nil || laddr.IP.IsUnspecified() { // if address is not specified, capture on all ifaces
var lasterr error
for _, iface := range ifaces {
if addrs, err := iface.Addrs(); err == nil {
for _, addr := range addrs {
if ipaddr, ok := addr.(*net.IPNet); ok {
if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: ipaddr.IP}); err == nil {
conn.handles = append(conn.handles, handle)
go conn.captureFlow(handle, laddr.Port)
} else {
lasterr = err
}
}
}
}
}
if len(conn.handles) == 0 {
return nil, lasterr
}
} else {
if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: laddr.IP}); err == nil {
conn.handles = append(conn.handles, handle)
go conn.captureFlow(handle, laddr.Port)
} else {
return nil, err
}
}
// start listening
l, err := net.ListenTCP(network, laddr)
if err != nil {
return nil, err
}
conn.listener = l
// start cleaner
go conn.cleaner()
// iptables drop packets marked with TTL = 1
// TODO: what if iptables is not available, the next hop will send back ICMP Time Exceeded,
// is this still an acceptable behavior?
if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil {
rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"}
if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil {
if !exists {
if err = ipt.Append("filter", "OUTPUT", rule...); err == nil {
conn.iprule = rule
conn.iptables = ipt
}
}
}
}
if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil {
rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"}
if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil {
if !exists {
if err = ipt.Append("filter", "OUTPUT", rule...); err == nil {
conn.ip6rule = rule
conn.ip6tables = ipt
}
}
}
}
// discard everything in original connection
go func() {
for {
tcpconn, err := l.AcceptTCP()
if err != nil {
return
}
// if we cannot set TTL = 1, the only thing reasonable is panic
if err := setTTL(tcpconn, 1); err != nil {
panic(err)
}
// record net.Conn
conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn })
// discard everything
go io.Copy(ioutil.Discard, tcpconn)
}
}()
return conn, nil
}
// setTTL sets the Time-To-Live field on a given connection
func setTTL(c *net.TCPConn, ttl int) error {
raw, err := c.SyscallConn()
if err != nil {
return err
}
addr := c.LocalAddr().(*net.TCPAddr)
if addr.IP.To4() == nil {
raw.Control(func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_UNICAST_HOPS, ttl)
})
} else {
raw.Control(func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TTL, ttl)
})
}
return err
}
// setDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header.
func setDSCP(c *net.IPConn, dscp int) error {
raw, err := c.SyscallConn()
if err != nil {
return err
}
addr := c.LocalAddr().(*net.IPAddr)
if addr.IP.To4() == nil {
raw.Control(func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_TCLASS, dscp)
})
} else {
raw.Control(func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, dscp<<2)
})
}
return err
}

View file

@ -1,21 +0,0 @@
//go:build !linux
// +build !linux
package faketcp
import (
"errors"
"net"
)
type TCPConn struct{ *net.UDPConn }
// Dial connects to the remote TCP port,
// and returns a single packet-oriented connection
func Dial(network, address string) (*TCPConn, error) {
return nil, errors.New("faketcp is not supported on this platform")
}
func Listen(network, address string) (*TCPConn, error) {
return nil, errors.New("faketcp is not supported on this platform")
}

Some files were not shown because too many files have changed in this diff Show more