Merge pull request #36 from tobyxdd/wip-ng

Code Refactoring & Implementing SOCKS5 UDP with QUIC Unreliable Datagram
This commit is contained in:
Toby
2021-03-28 01:07:53 -07:00
committed by GitHub
40 changed files with 1921 additions and 2498 deletions

View File

@@ -27,7 +27,7 @@ jobs:
id: current-time
- name: Build
uses: tobyxdd/go-cross-build@e269fe09a60b9a6a7325cb06075e32ab1b7c2d12
uses: thatisuday/go-cross-build@v1.1.0
env:
TIME: "${{ steps.current-time.outputs.time }}"
CGO_ENABLED: "0"

1
.gitignore vendored
View File

@@ -180,3 +180,4 @@ $RECYCLE.BIN/
# End of https://www.gitignore.io/api/go,linux,macos,windows,intellij+all
cmd/relay/*.json
hy_linux

15
ACL.md
View File

@@ -1,12 +1,14 @@
# ACL File Format
ACL files describe how to process incoming requests. Both the server and the client support ACL and follow the identical syntax.
ACL files describe how to process incoming requests. Both the server and the client support ACL and follow the identical
syntax.
```
action condition_type condition argument
```
Example:
```
direct domain evil.corp
proxy domain-suffix google.com
@@ -16,9 +18,12 @@ hijack cidr 192.168.1.1/24 127.0.0.1
direct all
```
A real-life ACL example of directly connecting to all China IPs (and its generator Python script) [can be found here](docs/acl).
A real-life ACL example of directly connecting to all China IPs (and its generator Python
script) [can be found here](docs/acl).
Hysteria acts according to the first matching rule in the file for each request. When there is no match, the default behavior is to proxy all connections. You can override this by adding a rule at the end of the file with the condition "all".
Hysteria acts according to the first matching rule in the file for each request. When there is no match, the default
behavior is to proxy all connections. You can override this by adding a rule at the end of the file with the condition
`all`.
4 actions:
@@ -42,4 +47,6 @@ Hysteria acts according to the first matching rule in the file for each request.
`all` - match anything (usually placed at the end of the file as a default rule)
For domain requests, Hysteria will try to resolve the domains and match both domain & IP rules. In other words, an IP rule covers all connections that would end up connecting to this IP, regardless of whether the client requests with IP or domain.
For domain requests, Hysteria will try to resolve the domains and match both domain & IP rules. In other words, an IP
rule covers all connections that would end up connecting to this IP, regardless of whether the client requests with IP
or domain.

View File

@@ -7,6 +7,7 @@ ACL 文件描述如何处理传入请求。服务器和客户端都支持 ACL
```
例子:
```
direct domain evil.corp
proxy domain-suffix google.com

View File

@@ -1,21 +1,16 @@
The MIT License (MIT)
Copyright (c) 2020 Toby
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:
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 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.
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.

272
README.md
View File

@@ -3,169 +3,187 @@
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6]
[1]: https://img.shields.io/github/license/tobyxdd/hysteria?style=flat-square
[2]: LICENSE.md
[3]: https://img.shields.io/github/v/release/tobyxdd/hysteria?style=flat-square
[4]: https://github.com/tobyxdd/hysteria/releases
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
[6]: https://t.me/hysteria_github
[中文 README](README.zh.md)
Hysteria is a set of relay & proxy utilities that are specifically optimized for harsh network environments (commonly seen in connecting to overseas servers from China). It's based on a modified version of the QUIC protocol, and can be considered a sequel to my previous (abandoned) project https://github.com/dragonite-network/dragonite-java
Hysteria is a TCP relay & SOCKS5/HTTP proxy tool optimized for poor network environments (satellite networks,
connections from China to foreign servers, etc.) powered by a custom version of QUIC protocol.
It is essentially a spiritual successor of my previous (now abandoned)
project https://github.com/dragonite-network/dragonite-java
## Quick Start
(See the [advanced usage section](#advanced-usage) for the exact meaning of each argument)
Note: The configs provided in this section are only for people to get started quickly and may not meet your needs.
Please go to [Advanced usage](#advanced-usage) to see all the available options and their meanings.
### Proxy
### Server
Server:
```
./cmd_linux_amd64 proxy server -listen :36712 -cert example.crt -key example.key -obfs BlueberryFaygo
```
A TLS certificate (not necessarily issued by a trusted CA) is required on the server side. If you are using a self-issued certificate, use `-ca` to specify your own CA file on clients, or `-insecure` to ignore all certificate errors (not recommended)
Create a `config.json` under the root directory of the program:
Client:
```
./cmd_linux_amd64 proxy client -server example.com:36712 -socks5-addr localhost:1080 -up-mbps 10 -down-mbps 50 -obfs BlueberryFaygo
```
This will start a SOCKS5 proxy server on the client's localhost TCP 1080 available for use by other programs.
In addition to SOCKS5, it also supports HTTP proxy (`-http-addr` & `-http-timeout`). Both modes can be turned on simultaneously on different ports.
`-up-mbps 10 -down-mbps 50` tells the server that your bandwidth is 50 Mbps down, 10 Mbps up. Properly setting the client's upload and download speeds based on your network conditions is essential for it to work at optimal performance!
### Relay
Suppose you have a TCP program on your server at `localhost:8080` that you want to forward.
Server:
```
./cmd_linux_amd64 relay server -listen :36712 -remote localhost:8080 -cert example.crt -key example.key
```json
{
"listen": ":36712",
"cert": "/home/ubuntu/my_cert.crt",
"key": "/home/ubuntu/my_key.crt",
"obfs": "AMOGUS",
"up_mbps": 100,
"down_mbps": 100
}
```
Client:
```
./cmd_linux_amd64 relay client -server example.com:36712 -listen localhost:8080 -up-mbps 10 -down-mbps 50
```
All connections to client's localhost TCP 8080 will pass through the relay and connect to the server's `localhost:8080`
A TLS certificate (not necessarily issued by a trusted CA) is required on the server side.
Some users may attempt to forward other encrypted proxy protocols such as Shadowsocks with relay. While this totally works, it's not optimal from a performance standpoint - our protocol itself uses TLS, considering that the proxy protocols being forwarded are also encrypted, and the fact that users mainly use them for HTTPS connections nowadays, you are essentially doing triple encryption. If you need a proxy, use our proxy mode.
The (optional) `obfs` option obfuscates the protocol using the provided password, so that it is not apparent that this
is Hysteria/QUIC, which could be useful for bypassing DPI blocking or QoS. If the passwords of the server and client do
not match, no connection can be established. Therefore, this can also serve as a simple password authentication. For
more advanced authentication schemes, see `auth` below.
`up_mbps` and `down_mbps` limit the maximum upload and download speed of the server for each client. These are also
optional and can be removed if not needed.
To launch the server, simply run
```
./cmd_linux_amd64 server
```
If your config file is not named `config.json` or is in a different path, specify it with `-config`:
```
./cmd_linux_amd64 server -config blah.json
```
### Client
Same as the server side, create a `config.json` under the root directory of the program:
```json
{
"server": "example.com:36712",
"obfs": "AMOGUS",
"up_mbps": 10,
"down_mbps": 50,
"socks5": {
"listen": "127.0.0.1:1080"
},
"http": {
"listen": "127.0.0.1:8080"
},
"relay": {
"listen": "127.0.0.1:2222",
"remote": "123.123.123.123:22"
}
}
```
This config enables a SOCKS5 proxy (with both TCP & UDP support), an HTTP proxy, and a TCP relay to `123.123.123.123:22`
at the same time. Please modify or remove these entries depending on your actual needs.
If your server certificate is not issued by a trusted CA, you need to specify the CA used
with `"ca": "/path/to/file.ca"` on the client or use `"insecure": true` to ignore all certificate errors (not
recommended).
`up_mbps` and `down_mbps` are mandatory on the client side. Please try to fill in these values as accurately as possible
according to your network conditions. They are crucial for Hysteria to work in an optimal state.
Some users may attempt to forward other encrypted proxy protocols such as Shadowsocks with relay. While this technically
works, it's not optimal from a performance standpoint - Hysteria itself uses TLS, considering that the proxy protocol
being forwarded is also encrypted, and the fact that almost all sites are now using HTTPS, it essentially becomes triple
encryption. If you need a proxy, just use our proxy modes.
## Comparison
Proxy Client: Guangzhou, China Mobile Broadband 100 Mbps
Proxy Server: AWS US West Oregon (us-west-2)
![Bench1](docs/bench/bench1.png)
## Advanced usage
The command line program supports loading configurations from both JSON files and arguments. Use `-config` to specify a JSON file. Config loaded from it can also be overwritten or extended with command line arguments.
### Server
### Proxy server
```json5
{
"listen": ":36712", // Listen address
"cert": "/home/ubuntu/my_cert.crt", // Cert file
"key": "/home/ubuntu/my_key.crt", // Key file
"up_mbps": 100, // Max upload Mbps per client
"down_mbps": 100, // Max download Mbps per client
"disable_udp": false, // Disable UDP support
"acl": "my_list.acl", // See ACL below
"obfs": "AMOGUS", // Obfuscation password
"auth": { // Authentication
"mode": "password", // Mode, only supports "password" and "none" for now
"config": {
"password": "yubiyubi"
}
},
"recv_window_conn": 33554432, // QUIC stream receive window
"recv_window_client": 67108864, // QUIC connection receive window
"max_conn_client": 4096 // Max concurrent connections per client
}
```
| Description | JSON config field | CLI argument |
| --- | --- | --- |
| Server listen address | listen | -listen |
| Disable UDP support | disable_udp | -disable-udp |
| Access control list | acl | -acl |
| TLS certificate file | cert | -cert |
| TLS key file | key | -key |
| Authentication file | auth | -auth |
| Max upload speed per client in Mbps | up_mbps | -up-mbps |
| Max download speed per client in Mbps | down_mbps | -down-mbps |
| Max receive window size per connection | recv_window_conn | -recv-window-conn |
| Max receive window size per client | recv_window_client | -recv-window-client |
| Max simultaneous connections allowed per client | max_conn_client | -max-conn-client |
| Obfuscation key | obfs | -obfs |
### Client
### Proxy client
```json5
{
"server": "example.com:36712", // Server address
"up_mbps": 10, // Max upload Mbps
"down_mbps": 50, // Max download Mbps
"socks5": {
"listen": "127.0.0.1:1080", // SOCKS5 listen address
"timeout": 300, // TCP timeout in seconds
"disable_udp": false, // Disable UDP support
"user": "me", // SOCKS5 authentication username
"password": "lmaolmao" // SOCKS5 authentication password
},
"http": {
"listen": "127.0.0.1:8080", // HTTP listen address
"timeout": 300, // TCP timeout in seconds
"user": "me", // HTTP authentication username
"password": "lmaolmao", // HTTP authentication password
"cert": "/home/ubuntu/my_cert.crt", // Cert file (HTTPS proxy)
"key": "/home/ubuntu/my_key.crt" // Key file (HTTPS proxy)
},
"relay": {
"listen": "127.0.0.1:2222", // Relay listen address
"remote": "123.123.123.123:22", // Relay remote address
"timeout": 300 // TCP timeout in seconds
},
"acl": "my_list.acl", // See ACL below
"obfs": "AMOGUS", // Obfuscation password
"auth": "[BASE64]", // Authentication payload in Base64
"auth_str": "yubiyubi", // Authentication payload in string, mutually exclusive with the option above
"insecure": false, // Ignore all certificate errors
"ca": "my.ca", // Custom CA file
"recv_window_conn": 33554432, // QUIC stream receive window
"recv_window": 67108864 // QUIC connection receive window
}
```
| Description | JSON config field | CLI argument |
| --- | --- | --- |
| SOCKS5 listen address | socks5_addr | -socks5-addr |
| SOCKS5 connection timeout in seconds | socks5_timeout | -socks5-timeout |
| Disable SOCKS5 UDP support | socks5_disable_udp | -socks5-disable-udp |
| SOCKS5 auth username | socks5_user | -socks5-user |
| SOCKS5 auth password | socks5_password | -socks5-password |
| HTTP listen address | http_addr | -http-addr |
| HTTP connection timeout in seconds | http_timeout | -http-timeout |
| HTTP basic auth username | http_user | -http-user |
| HTTP basic auth password | http_password | -http-password |
| HTTPS certificate file | https_cert | -http-cert |
| HTTPS key file | https_key | -http-key |
| Access control list | acl | -acl |
| Server address | server | -server |
| Authentication username | username | -username |
| Authentication password | password | -password |
| Ignore TLS certificate errors | insecure | -insecure |
| Specify a trusted CA file | ca | -ca |
| Upload speed in Mbps | up_mbps | -up-mbps |
| Download speed in Mbps | down_mbps | -down-mbps |
| Max receive window size per connection | recv_window_conn | -recv-window-conn |
| Max receive window size | recv_window | -recv-window |
| Obfuscation key | obfs | -obfs |
#### About SOCKS5
Supports TCP (CONNECT) and UDP (ASSOCIATE) commands. BIND is not supported and is not planned to be supported.
#### About ACL
## ACL
[ACL File Format](ACL.md)
#### About proxy authentication
## Logging
Proxy supports username and password authentication (sent encrypted with TLS). If the server starts with an authentication file, it will check for the existence of the corresponding username and password in this file when each user connects. A valid authentication file is a text file with a pair of username and password per line (separated by a space). Example:
```
admin K2MfcwyZNJy3
shady_hacker smokeweed420
The program outputs `DEBUG` level, text format logs via stdout by default.
This line is invalid and will be ignored
```
Changes to the file take effect immediately while the server is running.
#### About obfuscation
To prevent firewalls from potentially detecting & blocking the protocol, a simple XOR-based packet obfuscation mechanism has been built in. Note that clients and servers with different obfuscation settings are not be able to communicate at all.
### Relay server
| Description | JSON config field | CLI argument |
| --- | --- | --- |
| Server listen address | listen | -listen |
| Remote relay address | remote | -remote |
| TLS certificate file | cert | -cert |
| TLS key file | key | -key |
| Max upload speed per client in Mbps | up_mbps | -up-mbps |
| Max download speed per client in Mbps | down_mbps | -down-mbps |
| Max receive window size per connection | recv_window_conn | -recv-window-conn |
| Max receive window size per client | recv_window_client | -recv-window-client |
| Max simultaneous connections allowed per client | max_conn_client | -max-conn-client |
| Obfuscation key | obfs | -obfs |
### Relay client
| Description | JSON config field | CLI argument |
| --- | --- | --- |
| TCP listen address | listen | -listen |
| Server address | server | -server |
| Client name presented to the server | name | -name |
| Ignore TLS certificate errors | insecure | -insecure |
| Specify a trusted CA file | ca | -ca |
| Upload speed in Mbps | up_mbps | -up-mbps |
| Download speed in Mbps | down_mbps | -down-mbps |
| Max receive window size per connection | recv_window_conn | -recv-window-conn |
| Max receive window size | recv_window | -recv-window |
| Obfuscation key | obfs | -obfs |
## Logs
By default, the program outputs DEBUG level, text format logs via stdout.
To change the logging level, set `LOGGING_LEVEL` environment variable, which supports `panic`, `fatal`, `error`, `warn`, `info`, ` debug`, `trace`
To change the logging level, use `LOGGING_LEVEL` environment variable. The available levels are `panic`, `fatal`
, `error`, `warn`, `info`, ` debug`, `trace`
To print JSON instead, set `LOGGING_FORMATTER` to `json`

View File

@@ -3,162 +3,169 @@
[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6]
[1]: https://img.shields.io/github/license/tobyxdd/hysteria?style=flat-square
[2]: LICENSE.md
[3]: https://img.shields.io/github/v/release/tobyxdd/hysteria?style=flat-square
[4]: https://github.com/tobyxdd/hysteria/releases
[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square
[6]: https://t.me/hysteria_github
Hysteria 是专门针对恶劣网络环境(常见于在中国访问海外服务器)进行优化的连接转发和代理工具(即所谓的双边加速)。其基于修改版的 QUIC 协议,可以理解为是我此前弃坑的项目 https://github.com/dragonite-network/dragonite-java 的续作。
Hysteria 是专门针对恶劣网络环境(常见于卫星网络、在中国连接国外服务器)进行优化的 TCP 连接转发和代理工具(即所谓的双边加速)
基于修改版的 QUIC 协议。
可以理解为是我此前弃坑的项目 https://github.com/dragonite-network/dragonite-java 的续作。
## 快速入门
关于每个参数具体的含义请见 [高级用法](#高级用法)
注意:本节提供的配置只是为了快速上手,可能无法满足你的需求。请到 [高级用法](#高级用法) 中查看所有可用选项及其含义。
### 代理
### 服务器
服务端
```
./cmd_linux_amd64 proxy server -listen :36712 -cert example.crt -key example.key -obfs BlueberryFaygo
```
服务端需要一个 TLS 证书(不一定是由可信 CA 签发的有效证书)。如果你使用自签证书,请在客户端使用 `-ca` 指定你自己的 CA 文件,或者用 `-insecure` 忽略所有证书错误(不推荐)
在目录下建立一个 `config.json`
客户端
```
./cmd_linux_amd64 proxy client -server example.com:36712 -socks5-addr localhost:1080 -up-mbps 10 -down-mbps 50 -obfs BlueberryFaygo
```
在客户端的本地 TCP 1080 上启动一个 SOCKS5 代理服务器供其他程序使用。
除了 SOCKS5 还支持 HTTP 代理 (`-http-addr` & `-http-timeout`)。两个模式可以同时开在不同端口。
`-up-mbps 10 -down-mbps 50` 是告诉服务端你的下行速度为 50 Mbps, 上行 10 Mbps。根据实际网络条件正确设置客户端的上传和下载速度十分重要
### 转发
假设你想转发服务端上 `localhost:8080` 的一个 TCP 协议程序。
服务端
```
./cmd_linux_amd64 relay server -listen :36712 -remote localhost:8080 -cert example.crt -key example.key
```json
{
"listen": ":36712",
"cert": "/home/ubuntu/my_cert.crt",
"key": "/home/ubuntu/my_key.crt",
"obfs": "AMOGUS",
"up_mbps": 100,
"down_mbps": 100
}
```
客户端
```
./cmd_linux_amd64 relay client -server example.com:36712 -listen localhost:8080 -up-mbps 10 -down-mbps 50
```
所有到客户端本地 TCP 8080 的 TCP 连接都将通过转发,到服务器连接那里的 `localhost:8080`
服务端必须要一个 TLS 证书(但并非一定要受信 CA 签发的)。
有些用户可能会尝试用这个功能转发其他加密代理协议比如Shadowsocks。虽然这完全可行但从性能的角度并不是最佳选择 - 我们的协议本身就有 TLS转发的代理协议也是加密的再加上用户用来访问 HTTPS 网站,等于做了三重加密。如果需要代理就用我们的代理模式
`obfs` 选项使用提供的密码对协议进行混淆,这样协议就不容易被检测出是 Hysteria/QUIC可以用来绕过针对性的 DPI 屏蔽或者 QoS
如果服务端和客户端的密码不匹配就不能建立连接,因此这也可以作为一个简单的密码验证。对于更高级的验证方案请见下文 `auth`
`up_mbps``down_mbps` 限制服务器对每个客户端的最大上传和下载速度。这些也是可选的,如果不需要可以移除。
要启动服务端,只需运行
```
./cmd_linux_amd64 server
```
如果你的配置文件没有命名为 `config.json` 或在别的路径,请用 `-config` 指定路径:
```
./cmd_linux_amd64 server -config blah.json
```
### 客户端
和服务器端一样,在程序根目录下建立一个`config.json`
```json
{
"server": "example.com:36712",
"obfs": "AMOGUS",
"up_mbps": 10,
"down_mbps": 50,
"socks5": {
"listen": "127.0.0.1:1080"
},
"http": {
"listen": "127.0.0.1:8080"
},
"relay": {
"listen": "127.0.0.1:2222",
"remote": "123.123.123.123:22"
}
}
```
这个配置同时开了 SOCK5 (支持 TCP & UDP) 代理HTTP 代理和到 `123.123.123.123:22` 的 TCP 转发。请根据自己实际需要修改和删减。
如果你的服务端证书不是由受信任的 CA 签发的,需要用 `"ca": "/path/to/file.ca"` 指定使用的 CA 或者用 `"insecure": true` 忽略所有
证书错误(不推荐)。
`up_mbps``down_mbps` 在客户端是必填选项,请根据实际网络情况尽量准确地填写,否则将影响 Hysteria 的使用体验。
有些用户可能会尝试用这个功能转发其他加密代理协议,比如 Shadowsocks。这样虽然可行但从性能的角度不推荐 - Hysteria 本身就用 TLS
转发的代理协议也是加密的,再加上如今几乎所有网站都是 HTTPS 了,等于做了三重加密。如果需要代理就用我们的代理模式。
## 对比
代理客户端:广州移动宽带 100M
代理服务端AWS 美西 Oregon (us-west-2) (最差线路之一)
![Bench1](docs/bench/bench1.png)
## 高级用法
命令行程序支持从 JSON 文件和参数加载配置。使用 `-config` 指定一个JSON文件。从文件加载的配置也可以被命令行参数覆盖或进一步扩展。
### Server
### 代理 服务端
```json5
{
"listen": ":36712", // 监听地址
"cert": "/home/ubuntu/my_cert.crt", // 证书
"key": "/home/ubuntu/my_key.crt", // 证书密钥
"up_mbps": 100, // 单客户端最大上传速度
"down_mbps": 100, // 单客户端最大下载速度
"disable_udp": false, // 禁用 UDP 支持
"acl": "my_list.acl", // 见下文 ACL
"obfs": "AMOGUS", // 混淆密码
"auth": { // 验证
"mode": "password", // 验证模式,暂时只支持 "password" 与 "none"
"config": {
"password": "yubiyubi"
}
},
"recv_window_conn": 33554432, // QUIC stream receive window
"recv_window_client": 67108864, // QUIC connection receive window
"max_conn_client": 4096 // 单客户端最大活跃连接数
}
```
| 描述 | JSON 字段 | 命令行参数 |
| --- | --- | --- |
| 服务端监听地址 | listen | -listen |
| 禁用 UDP 支持 | disable_udp | -disable-udp |
| ACL 规则文件 | acl | -acl |
| TLS 证书文件 | cert | -cert |
| TLS 密钥文件 | key | -key |
| 用户名密码验证文件 | auth | -auth |
| 单客户端最大上传速度 Mbps | up_mbps | -up-mbps |
| 单客户端最大下载速度 Mbps | down_mbps | -down-mbps |
| 单连接最大接收窗口大小 | recv_window_conn | -recv-window-conn |
| 单客户端最大接收窗口大小 | recv_window_client | -recv-window-client |
| 单客户端最大连接数 | max_conn_client | -max-conn-client |
| 混淆密钥 | obfs | -obfs |
### Client
### 代理 客户端
```json5
{
"server": "example.com:36712", // 服务器地址
"up_mbps": 10, // 最大上传速度
"down_mbps": 50, // 最大下载速度
"socks5": {
"listen": "127.0.0.1:1080", // SOCKS5 监听地址
"timeout": 300, // TCP 超时秒数
"disable_udp": false, // 禁用 UDP 支持
"user": "me", // SOCKS5 验证用户名
"password": "lmaolmao" // SOCKS5 验证密码
},
"http": {
"listen": "127.0.0.1:8080", // HTTP 监听地址
"timeout": 300, // TCP 超时秒数
"user": "me", // HTTP 验证用户名
"password": "lmaolmao", // HTTP 验证密码
"cert": "/home/ubuntu/my_cert.crt", // 证书 (变为 HTTPS 代理)
"key": "/home/ubuntu/my_key.crt" // 证书密钥 (变为 HTTPS 代理)
},
"relay": {
"listen": "127.0.0.1:2222", // 转发监听地址
"remote": "123.123.123.123:22", // 转发目标地址
"timeout": 300 // TCP 超时秒数
},
"acl": "my_list.acl", // 见下文 ACL
"obfs": "AMOGUS", // 混淆密码
"auth": "[BASE64]", // Base64 验证密钥
"auth_str": "yubiyubi", // 字符串验证密钥,和上面的选项二选一
"insecure": false, // 忽略一切证书错误
"ca": "my.ca", // 自定义 CA
"recv_window_conn": 33554432, // QUIC stream receive window
"recv_window": 67108864 // QUIC connection receive window
}
```
| 描述 | JSON 字段 | 命令行参数 |
| --- | --- | --- |
| SOCKS5 监听地址 | socks5_addr | -socks5-addr |
| SOCKS5 超时时间(秒) | socks5_timeout | -socks5-timeout |
| 禁用 SOCKS5 UDP 支持 | socks5_disable_udp | -socks5-disable-udp |
| SOCKS5 验证用户名 | socks5_user | -socks5-user |
| SOCKS5 验证密码 | socks5_password | -socks5-password |
| HTTP 监听地址 | http_addr | -http-addr |
| HTTP 超时时间(秒) | http_timeout | -http-timeout |
| HTTP 验证用户名 | http_user | -http-user |
| HTTP 验证密码 | http_password | -http-password |
| HTTPS 证书文件 | https_cert | -http-cert |
| HTTPS 密钥文件 | https_key | -http-key |
| ACL 规则文件 | acl | -acl |
| 服务端地址 | server | -server |
| 验证用户名 | username | -username |
| 验证密码 | password | -password |
| 忽略证书错误 | insecure | -insecure |
| 指定可信 CA 文件 | ca | -ca |
| 上传速度 Mbps | up_mbps | -up-mbps |
| 下载速度 Mbps | down_mbps | -down-mbps |
| 单连接最大接收窗口大小 | recv_window_conn | -recv-window-conn |
| 总最大接收窗口大小 | recv_window | -recv-window |
| 混淆密钥 | obfs | -obfs |
#### 关于 SOCKS5
支持 TCP (CONNECT) 和 UDP (ASSOCIATE),不支持 BIND 也无计划支持。
#### 关于 ACL
## 关于 ACL
[ACL 文件格式](ACL.zh.md)
#### 关于用户名密码验证
代理支持用户名和密码认证(经过 TLS 加密发送)。如果服务器启动时指定了一个验证文件,当每个用户连接时,服务器会检查该文件中是否存在相应的用户名和密码。验证文件是一个文本文件,每行有一对用户名和密码(用空格分割)。比如:
```
admin K2MfcwyZNJy3
shady_hacker smokeweed420
这行无效会被忽略
```
对文件的更改立即生效,即使服务端正在运行。
#### 关于混淆
为了防止各类防火墙今后可能检测并阻止协议,程序内置了简单的基于 XOR 的数据包混淆机制。注意客户端和服务器的混淆设置如果不同则完全无法通信。
### 转发 服务端
| 描述 | JSON 字段 | 命令行参数 |
| --- | --- | --- |
| 服务端监听地址 | listen | -listen |
| 转发目标地址 | remote | -remote |
| TLS 证书文件 | cert | -cert |
| TLS 密钥文件 | key | -key |
| 单客户端最大上传速度 Mbps | up_mbps | -up-mbps |
| 单客户端最大下载速度 Mbps | down_mbps | -down-mbps |
| 单连接最大接收窗口大小 | recv_window_conn | -recv-window-conn |
| 单客户端最大接收窗口大小 | recv_window_client | -recv-window-client |
| 单客户端最大连接数 | max_conn_client | -max-conn-client |
| 混淆密钥 | obfs | -obfs |
### 转发 客户端
| 描述 | JSON 字段 | 命令行参数 |
| --- | --- | --- |
| TCP 监听地址 | listen | -listen |
| 服务端地址 | server | -server |
| 客户端名称 | name | -name |
| 忽略证书错误 | insecure | -insecure |
| 指定可信 CA 文件 | ca | -ca |
| 上传速度 Mbps | up_mbps | -up-mbps |
| 下载速度 Mbps | down_mbps | -down-mbps |
| 单连接最大接收窗口大小 | recv_window_conn | -recv-window-conn |
| 总最大接收窗口大小 | recv_window | -recv-window |
| 混淆密钥 | obfs | -obfs |
## 日志
程序默认在 stdout 输出 DEBUG 级别,文字格式的日志。

5
build.ps1 Normal file
View File

@@ -0,0 +1,5 @@
$env:GOOS = "windows"
go build -ldflags="-w -s" -o "hy_windows.exe" ./cmd
$env:GOOS = "linux"
go build -ldflags="-w -s" -o "hy_linux" ./cmd

View File

@@ -1 +0,0 @@
go build -ldflags="-w -s" ./cmd

216
cmd/client.go Normal file
View File

@@ -0,0 +1,216 @@
package main
import (
"crypto/tls"
"crypto/x509"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/sirupsen/logrus"
"github.com/tobyxdd/hysteria/pkg/acl"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
hyHTTP "github.com/tobyxdd/hysteria/pkg/http"
"github.com/tobyxdd/hysteria/pkg/obfs"
"github.com/tobyxdd/hysteria/pkg/relay"
"github.com/tobyxdd/hysteria/pkg/socks5"
"io"
"io/ioutil"
"net"
"net/http"
"time"
)
func client(config *clientConfig) {
logrus.WithField("config", config.String()).Info("Client configuration loaded")
// TLS
tlsConfig := &tls.Config{
InsecureSkipVerify: config.Insecure,
NextProtos: []string{tlsProtocolName},
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{
MaxStreamReceiveWindow: config.ReceiveWindowConn,
MaxConnectionReceiveWindow: config.ReceiveWindow,
KeepAlive: true,
EnableDatagrams: true,
}
if quicConfig.MaxStreamReceiveWindow == 0 {
quicConfig.MaxStreamReceiveWindow = DefaultMaxReceiveStreamFlowControlWindow
}
if quicConfig.MaxConnectionReceiveWindow == 0 {
quicConfig.MaxConnectionReceiveWindow = DefaultMaxReceiveConnectionFlowControlWindow
}
// Auth
var auth []byte
if len(config.Auth) > 0 {
auth = config.Auth
} else {
auth = []byte(config.AuthString)
}
// Obfuscator
var obfuscator core.Obfuscator
if len(config.Obfs) > 0 {
obfuscator = obfs.XORObfuscator(config.Obfs)
}
// ACL
var aclEngine *acl.Engine
if len(config.ACL) > 0 {
var err error
aclEngine, err = acl.LoadFromFile(config.ACL)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACL,
}).Fatal("Failed to parse ACL")
}
}
// Client
client, err := core.NewClient(config.Server, auth, tlsConfig, quicConfig,
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator)
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize client")
}
defer client.Close()
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, 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": addr.String(),
"dst": reqAddr,
}).Debug("SOCKS5 TCP request")
},
func(addr net.Addr, reqAddr string, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": addr.String(),
"dst": reqAddr,
}).Info("SOCKS5 TCP error")
} else {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
"dst": reqAddr,
}).Debug("SOCKS5 TCP EOF")
}
},
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
}).Debug("SOCKS5 UDP associate")
},
func(addr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": addr.String(),
}).Info("SOCKS5 UDP error")
} else {
logrus.WithFields(logrus.Fields{
"src": 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, time.Duration(config.HTTP.Timeout)*time.Second, aclEngine,
func(reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"dst": reqAddr,
}).Debug("HTTP request")
},
authFunc)
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.Relay.Listen) > 0 {
go func() {
rl, err := relay.NewRelay(client, config.Relay.Listen, config.Relay.Remote,
time.Duration(config.Relay.Timeout)*time.Second,
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
}).Debug("TCP relay request")
},
func(addr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": addr.String(),
}).Info("TCP relay error")
} else {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
}).Debug("TCP relay EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP relay")
}
logrus.WithField("addr", config.Relay.Listen).Info("TCP relay up and running")
errChan <- rl.ListenAndServe()
}()
}
err = <-errChan
logrus.WithField("error", err).Fatal("Client shutdown")
}

View File

@@ -1,86 +1,128 @@
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"os"
"reflect"
"strings"
"time"
"errors"
"fmt"
"github.com/yosuke-furukawa/json5/encoding/json5"
)
const (
mbpsToBps = 125000
dialTimeout = 10 * time.Second
mbpsToBps = 125000
DefaultMaxReceiveStreamFlowControlWindow = 33554432
DefaultMaxReceiveConnectionFlowControlWindow = 67108864
DefaultMaxIncomingStreams = 200
DefaultMaxIncomingStreams = 1024
tlsProtocolName = "hysteria"
)
func loadConfig(cfg interface{}, args []string) error {
cfgVal := reflect.ValueOf(cfg).Elem()
fs := flag.NewFlagSet("", flag.ContinueOnError)
fsValMap := make(map[reflect.Value]interface{}, cfgVal.NumField())
for i := 0; i < cfgVal.NumField(); i++ {
structField := cfgVal.Type().Field(i)
tag := structField.Tag
switch structField.Type.Kind() {
case reflect.String:
fsValMap[cfgVal.Field(i)] =
fs.String(jsonTagToFlagName(tag.Get("json")), "", tag.Get("desc"))
case reflect.Int:
fsValMap[cfgVal.Field(i)] =
fs.Int(jsonTagToFlagName(tag.Get("json")), 0, tag.Get("desc"))
case reflect.Uint64:
fsValMap[cfgVal.Field(i)] =
fs.Uint64(jsonTagToFlagName(tag.Get("json")), 0, tag.Get("desc"))
case reflect.Bool:
var bf optionalBoolFlag
fs.Var(&bf, jsonTagToFlagName(tag.Get("json")), tag.Get("desc"))
fsValMap[cfgVal.Field(i)] = &bf
}
type serverConfig struct {
Listen string `json:"listen"`
CertFile string `json:"cert"`
KeyFile string `json:"key"`
// Optional below
UpMbps int `json:"up_mbps"`
DownMbps int `json:"down_mbps"`
DisableUDP bool `json:"disable_udp"`
ACL string `json:"acl"`
Obfs string `json:"obfs"`
Auth struct {
Mode string `json:"mode"`
Config json5.RawMessage `json:"config"`
} `json:"auth"`
ReceiveWindowConn uint64 `json:"recv_window_conn"`
ReceiveWindowClient uint64 `json:"recv_window_client"`
MaxConnClient int `json:"max_conn_client"`
}
func (c *serverConfig) Check() error {
if len(c.Listen) == 0 {
return errors.New("no listen address")
}
configFile := fs.String("config", "", "Configuration file")
// Parse
if err := fs.Parse(args); err != nil {
os.Exit(1)
if len(c.CertFile) == 0 || len(c.KeyFile) == 0 {
return errors.New("TLS cert or key not provided")
}
// Put together the config
if len(*configFile) > 0 {
cb, err := ioutil.ReadFile(*configFile)
if err != nil {
return err
}
if err := json.Unmarshal(cb, cfg); err != nil {
return err
}
if c.UpMbps < 0 || c.DownMbps < 0 {
return errors.New("invalid speed")
}
// Flags override config from file
for field, val := range fsValMap {
switch v := val.(type) {
case *string:
if len(*v) > 0 {
field.SetString(*v)
}
case *int:
if *v != 0 {
field.SetInt(int64(*v))
}
case *uint64:
if *v != 0 {
field.SetUint(*v)
}
case *optionalBoolFlag:
if v.Exists {
field.SetBool(v.Value)
}
}
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 jsonTagToFlagName(tag string) string {
return strings.ReplaceAll(tag, "_", "-")
func (c *serverConfig) String() string {
return fmt.Sprintf("%+v", *c)
}
type clientConfig struct {
Server string `json:"server"`
UpMbps int `json:"up_mbps"`
DownMbps int `json:"down_mbps"`
// Optional below
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"`
Relay struct {
Listen string `json:"listen"`
Remote string `json:"remote"`
Timeout int `json:"timeout"`
} `json:"relay"`
ACL string `json:"acl"`
Obfs string `json:"obfs"`
Auth []byte `json:"auth"`
AuthString string `json:"auth_str"`
Insecure bool `json:"insecure"`
CustomCA string `json:"ca"`
ReceiveWindowConn uint64 `json:"recv_window_conn"`
ReceiveWindow uint64 `json:"recv_window"`
}
func (c *clientConfig) Check() error {
if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.Relay.Listen) == 0 {
return errors.New("no SOCKS5, HTTP or relay listen address")
}
if len(c.Relay.Listen) > 0 && len(c.Relay.Remote) == 0 {
return errors.New("no relay remote address")
}
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.Relay.Timeout != 0 && c.Relay.Timeout <= 4 {
return errors.New("invalid relay timeout")
}
if len(c.Server) == 0 {
return errors.New("no server address")
}
if c.UpMbps <= 0 || c.DownMbps <= 0 {
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")
}
return nil
}
func (c *clientConfig) String() string {
return fmt.Sprintf("%+v", *c)
}

View File

@@ -1,11 +1,13 @@
package main
import (
"flag"
"fmt"
"github.com/sirupsen/logrus"
"github.com/yosuke-furukawa/json5/encoding/json5"
"io/ioutil"
"os"
"strings"
"github.com/sirupsen/logrus"
)
// Injected when compiling
@@ -15,12 +17,10 @@ var (
appDate = "Unknown"
)
var modeMap = map[string]func(args []string){
"relay server": relayServer,
"relay client": relayClient,
"proxy server": proxyServer,
"proxy client": proxyClient,
}
var (
configPath = flag.String("config", "config.json", "Config file")
showVersion = flag.Bool("version", false, "Show version")
)
func init() {
logrus.SetOutput(os.Stdout)
@@ -50,37 +50,68 @@ func init() {
TimestampFormat: tsFormat,
})
}
flag.Parse()
}
func main() {
if len(os.Args) == 2 && strings.ToLower(strings.TrimSpace(os.Args[1])) == "version" {
if *showVersion {
// Print version and quit
fmt.Printf("%-10s%s\n", "Version:", appVersion)
fmt.Printf("%-10s%s\n", "Commit:", appCommit)
fmt.Printf("%-10s%s\n", "Date:", appDate)
return
}
if len(os.Args) < 3 {
fmt.Println()
fmt.Printf("Usage: %s MODE SUBMODE [OPTIONS]\n\n"+
"Available mode/submode combinations: "+getModes()+"\n"+
"Use -h to see the available options for a mode.\n\n", os.Args[0])
return
cb, err := ioutil.ReadFile(*configPath)
if err != nil {
logrus.WithFields(logrus.Fields{
"file": *configPath,
"error": err,
}).Fatal("Failed to read configuration")
}
modeStr := fmt.Sprintf("%s %s", strings.ToLower(strings.TrimSpace(os.Args[1])),
strings.ToLower(strings.TrimSpace(os.Args[2])))
f := modeMap[modeStr]
if f != nil {
f(os.Args[3:])
mode := flag.Arg(0)
if strings.EqualFold(mode, "server") {
// server mode
c, err := parseServerConfig(cb)
if err != nil {
logrus.WithFields(logrus.Fields{
"file": *configPath,
"error": err,
}).Fatal("Failed to parse server configuration")
}
server(c)
} else if len(mode) == 0 || strings.EqualFold(mode, "client") {
// client mode
c, err := parseClientConfig(cb)
if err != nil {
logrus.WithFields(logrus.Fields{
"file": *configPath,
"error": err,
}).Fatal("Failed to parse client configuration")
}
client(c)
} else {
fmt.Println("Invalid mode:", modeStr)
// invalid
fmt.Println()
fmt.Printf("Usage: %s MODE [OPTIONS]\n\n"+
"Available modes: server, client\n\n", os.Args[0])
}
}
func getModes() string {
modes := make([]string, 0, len(modeMap))
for mode := range modeMap {
modes = append(modes, mode)
func parseServerConfig(cb []byte) (*serverConfig, error) {
var c serverConfig
err := json5.Unmarshal(cb, &c)
if err != nil {
return nil, err
}
return strings.Join(modes, ", ")
return &c, c.Check()
}
func parseClientConfig(cb []byte) (*clientConfig, error) {
var c clientConfig
err := json5.Unmarshal(cb, &c)
if err != nil {
return nil, err
}
return &c, c.Check()
}

View File

@@ -1,200 +0,0 @@
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net"
"net/http"
"time"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/sirupsen/logrus"
"github.com/tobyxdd/hysteria/pkg/acl"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
hyHTTP "github.com/tobyxdd/hysteria/pkg/http"
"github.com/tobyxdd/hysteria/pkg/obfs"
"github.com/tobyxdd/hysteria/pkg/socks5"
)
func proxyClient(args []string) {
var config proxyClientConfig
err := loadConfig(&config, args)
if err != nil {
logrus.WithField("error", err).Fatal("Unable to load configuration")
}
if err := config.Check(); err != nil {
logrus.WithField("error", err).Fatal("Configuration error")
}
logrus.WithField("config", config.String()).Info("Configuration loaded")
tlsConfig := &tls.Config{
InsecureSkipVerify: config.Insecure,
NextProtos: []string{proxyTLSProtocol},
MinVersion: tls.VersionTLS13,
}
// Load CA
if len(config.CustomCAFile) > 0 {
bs, err := ioutil.ReadFile(config.CustomCAFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.CustomCAFile,
}).Fatal("Unable to load CA file")
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(bs) {
logrus.WithFields(logrus.Fields{
"file": config.CustomCAFile,
}).Fatal("Unable to parse CA file")
}
tlsConfig.RootCAs = cp
}
quicConfig := &quic.Config{
MaxReceiveStreamFlowControlWindow: config.ReceiveWindowConn,
MaxReceiveConnectionFlowControlWindow: config.ReceiveWindow,
KeepAlive: true,
}
if quicConfig.MaxReceiveStreamFlowControlWindow == 0 {
quicConfig.MaxReceiveStreamFlowControlWindow = DefaultMaxReceiveStreamFlowControlWindow
}
if quicConfig.MaxReceiveConnectionFlowControlWindow == 0 {
quicConfig.MaxReceiveConnectionFlowControlWindow = DefaultMaxReceiveConnectionFlowControlWindow
}
var obfuscator core.Obfuscator
if len(config.Obfs) > 0 {
obfuscator = obfs.XORObfuscator(config.Obfs)
}
var aclEngine *acl.Engine
if len(config.ACLFile) > 0 {
aclEngine, err = acl.LoadFromFile(config.ACLFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACLFile,
}).Fatal("Unable to parse ACL")
}
}
client, err := core.NewClient(config.ServerAddr, config.Username, config.Password, tlsConfig, quicConfig,
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
func(refBPS uint64) congestion.ExternalSendAlgorithm {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator)
if err != nil {
logrus.WithField("error", err).Fatal("Client initialization failed")
}
defer client.Close()
logrus.WithField("addr", config.ServerAddr).Info("Connected")
errChan := make(chan error)
if len(config.SOCKS5Addr) > 0 {
go func() {
var authFunc func(user, password string) bool
if config.SOCKS5User != "" && config.SOCKS5Password != "" {
authFunc = func(user, password string) bool {
return config.SOCKS5User == user && config.SOCKS5Password == password
}
}
socks5server, err := socks5.NewServer(client, config.SOCKS5Addr, authFunc, config.SOCKS5Timeout, aclEngine,
config.SOCKS5DisableUDP,
func(addr net.Addr, reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"src": addr.String(),
"dst": reqAddr,
}).Debug("New SOCKS5 TCP request")
},
func(addr net.Addr, reqAddr string, err error) {
logrus.WithFields(logrus.Fields{
"error": err,
"src": addr.String(),
"dst": reqAddr,
}).Debug("SOCKS5 TCP request closed")
},
func(addr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
}).Debug("New SOCKS5 UDP associate request")
},
func(addr net.Addr, err error) {
logrus.WithFields(logrus.Fields{
"error": err,
"src": addr.String(),
}).Debug("SOCKS5 UDP associate request closed")
},
func(addr net.Addr, reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"src": addr.String(),
"dst": reqAddr,
}).Debug("New SOCKS5 UDP tunnel")
},
func(addr net.Addr, reqAddr string, err error) {
logrus.WithFields(logrus.Fields{
"error": err,
"src": addr.String(),
"dst": reqAddr,
}).Debug("SOCKS5 UDP tunnel closed")
})
if err != nil {
logrus.WithField("error", err).Fatal("SOCKS5 server initialization failed")
}
logrus.WithField("addr", config.SOCKS5Addr).Info("SOCKS5 server up and running")
errChan <- socks5server.ListenAndServe()
}()
}
if len(config.HTTPAddr) > 0 {
go func() {
var authFunc func(user, password string) bool
if config.HTTPUser != "" && config.HTTPPassword != "" {
authFunc = func(user, password string) bool {
return config.HTTPUser == user && config.HTTPPassword == password
}
}
proxy, err := hyHTTP.NewProxyHTTPServer(client, time.Duration(config.HTTPTimeout)*time.Second, aclEngine,
func(reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"action": actionToString(action, arg),
"dst": reqAddr,
}).Debug("New HTTP request")
},
authFunc)
if err != nil {
logrus.WithField("error", err).Fatal("HTTP server initialization failed")
}
if config.HTTPSCert != "" && config.HTTPSKey != "" {
logrus.WithField("addr", config.HTTPAddr).Info("HTTPS server up and running")
errChan <- http.ListenAndServeTLS(config.HTTPAddr, config.HTTPSCert, config.HTTPSKey, proxy)
} else {
logrus.WithField("addr", config.HTTPAddr).Info("HTTP server up and running")
errChan <- http.ListenAndServe(config.HTTPAddr, proxy)
}
}()
}
err = <-errChan
logrus.WithField("error", err).Fatal("Client shutdown")
}
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"
}
}

View File

@@ -1,99 +0,0 @@
package main
import (
"errors"
"fmt"
)
const proxyTLSProtocol = "hysteria-proxy"
type proxyClientConfig struct {
SOCKS5Addr string `json:"socks5_addr" desc:"SOCKS5 listen address"`
SOCKS5Timeout int `json:"socks5_timeout" desc:"SOCKS5 connection timeout in seconds"`
SOCKS5DisableUDP bool `json:"socks5_disable_udp" desc:"Disable SOCKS5 UDP support"`
SOCKS5User string `json:"socks5_user" desc:"SOCKS5 auth username"`
SOCKS5Password string `json:"socks5_password" desc:"SOCKS5 auth password"`
HTTPAddr string `json:"http_addr" desc:"HTTP listen address"`
HTTPTimeout int `json:"http_timeout" desc:"HTTP connection timeout in seconds"`
HTTPUser string `json:"http_user" desc:"HTTP basic auth username"`
HTTPPassword string `json:"http_password" desc:"HTTP basic auth password"`
HTTPSCert string `json:"https_cert" desc:"HTTPS certificate file"`
HTTPSKey string `json:"https_key" desc:"HTTPS key file"`
ACLFile string `json:"acl" desc:"Access control list"`
ServerAddr string `json:"server" desc:"Server address"`
Username string `json:"username" desc:"Authentication username"`
Password string `json:"password" desc:"Authentication password"`
Insecure bool `json:"insecure" desc:"Ignore TLS certificate errors"`
CustomCAFile string `json:"ca" desc:"Specify a trusted CA file"`
UpMbps int `json:"up_mbps" desc:"Upload speed in Mbps"`
DownMbps int `json:"down_mbps" desc:"Download speed in Mbps"`
ReceiveWindowConn uint64 `json:"recv_window_conn" desc:"Max receive window size per connection"`
ReceiveWindow uint64 `json:"recv_window" desc:"Max receive window size"`
Obfs string `json:"obfs" desc:"Obfuscation key"`
}
func (c *proxyClientConfig) Check() error {
if len(c.SOCKS5Addr) == 0 && len(c.HTTPAddr) == 0 {
return errors.New("no SOCKS5 or HTTP listen address")
}
if c.SOCKS5Timeout != 0 && c.SOCKS5Timeout <= 4 {
return errors.New("invalid SOCKS5 timeout")
}
if c.HTTPTimeout != 0 && c.HTTPTimeout <= 4 {
return errors.New("invalid HTTP timeout")
}
if len(c.ServerAddr) == 0 {
return errors.New("no server address")
}
if c.UpMbps <= 0 || c.DownMbps <= 0 {
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")
}
return nil
}
func (c *proxyClientConfig) String() string {
return fmt.Sprintf("%+v", *c)
}
type proxyServerConfig struct {
ListenAddr string `json:"listen" desc:"Server listen address"`
DisableUDP bool `json:"disable_udp" desc:"Disable UDP support"`
ACLFile string `json:"acl" desc:"Access control list"`
CertFile string `json:"cert" desc:"TLS certificate file"`
KeyFile string `json:"key" desc:"TLS key file"`
AuthFile string `json:"auth" desc:"Authentication file"`
UpMbps int `json:"up_mbps" desc:"Max upload speed per client in Mbps"`
DownMbps int `json:"down_mbps" desc:"Max download speed per client in Mbps"`
ReceiveWindowConn uint64 `json:"recv_window_conn" desc:"Max receive window size per connection"`
ReceiveWindowClient uint64 `json:"recv_window_client" desc:"Max receive window size per client"`
MaxConnClient int `json:"max_conn_client" desc:"Max simultaneous connections allowed per client"`
Obfs string `json:"obfs" desc:"Obfuscation key"`
}
func (c *proxyServerConfig) Check() error {
if len(c.ListenAddr) == 0 {
return errors.New("no listen address")
}
if len(c.CertFile) == 0 || len(c.KeyFile) == 0 {
return errors.New("TLS cert or key not provided")
}
if c.UpMbps < 0 || c.DownMbps < 0 {
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 *proxyServerConfig) String() string {
return fmt.Sprintf("%+v", *c)
}

View File

@@ -1,298 +0,0 @@
package main
import (
"bufio"
"crypto/tls"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/sirupsen/logrus"
"github.com/tobyxdd/hysteria/pkg/acl"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/obfs"
"io"
"net"
"os"
"strings"
)
func proxyServer(args []string) {
var config proxyServerConfig
err := loadConfig(&config, args)
if err != nil {
logrus.WithField("error", err).Fatal("Unable to load configuration")
}
if err := config.Check(); err != nil {
logrus.WithField("error", err).Fatal("Configuration error")
}
logrus.WithField("config", config.String()).Info("Configuration loaded")
// Load cert
cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"cert": config.CertFile,
"key": config.KeyFile,
}).Fatal("Unable to load the certificate")
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{proxyTLSProtocol},
MinVersion: tls.VersionTLS13,
}
quicConfig := &quic.Config{
MaxReceiveStreamFlowControlWindow: config.ReceiveWindowConn,
MaxReceiveConnectionFlowControlWindow: config.ReceiveWindowClient,
MaxIncomingStreams: int64(config.MaxConnClient),
KeepAlive: true,
}
if quicConfig.MaxReceiveStreamFlowControlWindow == 0 {
quicConfig.MaxReceiveStreamFlowControlWindow = DefaultMaxReceiveStreamFlowControlWindow
}
if quicConfig.MaxReceiveConnectionFlowControlWindow == 0 {
quicConfig.MaxReceiveConnectionFlowControlWindow = DefaultMaxReceiveConnectionFlowControlWindow
}
if quicConfig.MaxIncomingStreams == 0 {
quicConfig.MaxIncomingStreams = DefaultMaxIncomingStreams
}
if len(config.AuthFile) == 0 {
logrus.Warn("No authentication configured, this server can be used by anyone")
}
var obfuscator core.Obfuscator
if len(config.Obfs) > 0 {
obfuscator = obfs.XORObfuscator(config.Obfs)
}
var aclEngine *acl.Engine
if len(config.ACLFile) > 0 {
aclEngine, err = acl.LoadFromFile(config.ACLFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACLFile,
}).Fatal("Unable to parse ACL")
}
aclEngine.DefaultAction = acl.ActionDirect
}
server, err := core.NewServer(config.ListenAddr, tlsConfig, quicConfig,
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
func(refBPS uint64) congestion.ExternalSendAlgorithm {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
},
obfuscator,
func(addr net.Addr, username string, password string, sSend uint64, sRecv uint64) (core.AuthResult, string) {
if len(config.AuthFile) == 0 {
logrus.WithFields(logrus.Fields{
"addr": addr.String(),
"username": username,
"up": sSend / mbpsToBps,
"down": sRecv / mbpsToBps,
}).Info("Client connected")
return core.AuthResultSuccess, ""
} else {
// Need auth
ok, err := checkAuth(config.AuthFile, username, password)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err.Error(),
"addr": addr.String(),
"username": username,
}).Error("Client authentication error")
return core.AuthResultInternalError, "Server auth error"
}
if ok {
logrus.WithFields(logrus.Fields{
"addr": addr.String(),
"username": username,
"up": sSend / mbpsToBps,
"down": sRecv / mbpsToBps,
}).Info("Client authenticated")
return core.AuthResultSuccess, ""
} else {
logrus.WithFields(logrus.Fields{
"addr": addr.String(),
"username": username,
"up": sSend / mbpsToBps,
"down": sRecv / mbpsToBps,
}).Info("Client rejected due to invalid credential")
return core.AuthResultInvalidCred, "Invalid credential"
}
}
},
func(addr net.Addr, username string, err error) {
logrus.WithFields(logrus.Fields{
"error": err.Error(),
"addr": addr.String(),
"username": username,
}).Info("Client disconnected")
},
func(addr net.Addr, username string, id int, reqType core.ConnectionType, reqAddr string) (core.ConnectResult, string, io.ReadWriteCloser) {
packet := reqType == core.ConnectionTypePacket
if packet && config.DisableUDP {
return core.ConnectResultBlocked, "UDP disabled", nil
}
host, port, err := net.SplitHostPort(reqAddr)
if err != nil {
return core.ConnectResultFailed, err.Error(), nil
}
ip := net.ParseIP(host)
if ip != nil {
// IP request, clear host for ACL engine
host = ""
}
action, arg := acl.ActionDirect, ""
if aclEngine != nil {
action, arg = aclEngine.Lookup(host, ip)
}
switch action {
case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side
if !packet {
// TCP
logrus.WithFields(logrus.Fields{
"action": "direct",
"username": username,
"src": addr.String(),
"dst": reqAddr,
}).Debug("New TCP request")
conn, err := net.DialTimeout("tcp", reqAddr, dialTimeout)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"dst": reqAddr,
}).Error("TCP error")
return core.ConnectResultFailed, err.Error(), nil
}
return core.ConnectResultSuccess, "", conn
} else {
// UDP
logrus.WithFields(logrus.Fields{
"action": "direct",
"username": username,
"src": addr.String(),
"dst": reqAddr,
}).Debug("New UDP request")
conn, err := net.Dial("udp", reqAddr)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"dst": reqAddr,
}).Error("UDP error")
return core.ConnectResultFailed, err.Error(), nil
}
return core.ConnectResultSuccess, "", conn
}
case acl.ActionBlock:
if !packet {
// TCP
logrus.WithFields(logrus.Fields{
"action": "block",
"username": username,
"src": addr.String(),
"dst": reqAddr,
}).Debug("New TCP request")
return core.ConnectResultBlocked, "blocked by ACL", nil
} else {
// UDP
logrus.WithFields(logrus.Fields{
"action": "block",
"username": username,
"src": addr.String(),
"dst": reqAddr,
}).Debug("New UDP request")
return core.ConnectResultBlocked, "blocked by ACL", nil
}
case acl.ActionHijack:
hijackAddr := net.JoinHostPort(arg, port)
if !packet {
// TCP
logrus.WithFields(logrus.Fields{
"action": "hijack",
"username": username,
"src": addr.String(),
"dst": reqAddr,
"rdst": arg,
}).Debug("New TCP request")
conn, err := net.DialTimeout("tcp", hijackAddr, dialTimeout)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"dst": hijackAddr,
}).Error("TCP error")
return core.ConnectResultFailed, err.Error(), nil
}
return core.ConnectResultSuccess, "", conn
} else {
// UDP
logrus.WithFields(logrus.Fields{
"action": "hijack",
"username": username,
"src": addr.String(),
"dst": reqAddr,
"rdst": arg,
}).Debug("New UDP request")
conn, err := net.Dial("udp", hijackAddr)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"dst": hijackAddr,
}).Error("UDP error")
return core.ConnectResultFailed, err.Error(), nil
}
return core.ConnectResultSuccess, "", conn
}
default:
return core.ConnectResultFailed, "server ACL error", nil
}
},
func(addr net.Addr, username string, id int, reqType core.ConnectionType, reqAddr string, err error) {
packet := reqType == core.ConnectionTypePacket
if !packet {
logrus.WithFields(logrus.Fields{
"error": err,
"username": username,
"src": addr.String(),
"dst": reqAddr,
}).Debug("TCP request closed")
} else {
logrus.WithFields(logrus.Fields{
"error": err,
"username": username,
"src": addr.String(),
"dst": reqAddr,
}).Debug("UDP request closed")
}
},
)
if err != nil {
logrus.WithField("error", err).Fatal("Server initialization failed")
}
defer server.Close()
logrus.WithField("addr", config.ListenAddr).Info("Server up and running")
err = server.Serve()
logrus.WithField("error", err).Fatal("Server shutdown")
}
func checkAuth(authFile, username, password string) (bool, error) {
f, err := os.Open(authFile)
if err != nil {
return false, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
pair := strings.Fields(scanner.Text())
if len(pair) != 2 {
// Invalid format
continue
}
if username == pair[0] && password == pair[1] {
return true, nil
}
}
return false, nil
}

View File

@@ -1,119 +0,0 @@
package main
import (
"crypto/tls"
"crypto/x509"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/sirupsen/logrus"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/obfs"
"github.com/tobyxdd/hysteria/pkg/utils"
"io/ioutil"
"net"
"os/user"
)
func relayClient(args []string) {
var config relayClientConfig
err := loadConfig(&config, args)
if err != nil {
logrus.WithField("error", err).Fatal("Unable to load configuration")
}
if err := config.Check(); err != nil {
logrus.WithField("error", err).Fatal("Configuration error")
}
if len(config.Name) == 0 {
usr, err := user.Current()
if err == nil {
config.Name = usr.Name
}
}
logrus.WithField("config", config.String()).Info("Configuration loaded")
tlsConfig := &tls.Config{
InsecureSkipVerify: config.Insecure,
NextProtos: []string{relayTLSProtocol},
MinVersion: tls.VersionTLS13,
}
// Load CA
if len(config.CustomCAFile) > 0 {
bs, err := ioutil.ReadFile(config.CustomCAFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.CustomCAFile,
}).Fatal("Unable to load CA file")
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(bs) {
logrus.WithFields(logrus.Fields{
"file": config.CustomCAFile,
}).Fatal("Unable to parse CA file")
}
tlsConfig.RootCAs = cp
}
quicConfig := &quic.Config{
MaxReceiveStreamFlowControlWindow: config.ReceiveWindowConn,
MaxReceiveConnectionFlowControlWindow: config.ReceiveWindow,
KeepAlive: true,
}
if quicConfig.MaxReceiveStreamFlowControlWindow == 0 {
quicConfig.MaxReceiveStreamFlowControlWindow = DefaultMaxReceiveStreamFlowControlWindow
}
if quicConfig.MaxReceiveConnectionFlowControlWindow == 0 {
quicConfig.MaxReceiveConnectionFlowControlWindow = DefaultMaxReceiveConnectionFlowControlWindow
}
var obfuscator core.Obfuscator
if len(config.Obfs) > 0 {
obfuscator = obfs.XORObfuscator(config.Obfs)
}
client, err := core.NewClient(config.ServerAddr, config.Name, "", tlsConfig, quicConfig,
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
func(refBPS uint64) congestion.ExternalSendAlgorithm {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator)
if err != nil {
logrus.WithField("error", err).Fatal("Client initialization failed")
}
defer client.Close()
logrus.WithField("addr", config.ServerAddr).Info("Connected")
listener, err := net.Listen("tcp", config.ListenAddr)
if err != nil {
logrus.WithField("error", err).Fatal("TCP listen failed")
}
defer listener.Close()
logrus.WithField("addr", listener.Addr().String()).Info("TCP server listening")
for {
conn, err := listener.Accept()
if err != nil {
logrus.WithField("error", err).Fatal("TCP accept failed")
}
go relayClientHandleConn(conn, client)
}
}
func relayClientHandleConn(conn net.Conn, client *core.Client) {
logrus.WithField("src", conn.RemoteAddr().String()).Debug("New connection")
var closeErr error
defer func() {
_ = conn.Close()
logrus.WithFields(logrus.Fields{
"error": closeErr,
"src": conn.RemoteAddr().String(),
}).Debug("Connection closed")
}()
rwc, err := client.Dial(false, "")
if err != nil {
closeErr = err
return
}
defer rwc.Close()
closeErr = utils.PipePair(conn, rwc, nil, nil)
}

View File

@@ -1,82 +0,0 @@
package main
import (
"errors"
"fmt"
)
const relayTLSProtocol = "hysteria-relay"
type relayClientConfig struct {
ListenAddr string `json:"listen" desc:"TCP listen address"`
ServerAddr string `json:"server" desc:"Server address"`
Name string `json:"name" desc:"Client name presented to the server"`
Insecure bool `json:"insecure" desc:"Ignore TLS certificate errors"`
CustomCAFile string `json:"ca" desc:"Specify a trusted CA file"`
UpMbps int `json:"up_mbps" desc:"Upload speed in Mbps"`
DownMbps int `json:"down_mbps" desc:"Download speed in Mbps"`
ReceiveWindowConn uint64 `json:"recv_window_conn" desc:"Max receive window size per connection"`
ReceiveWindow uint64 `json:"recv_window" desc:"Max receive window size"`
Obfs string `json:"obfs" desc:"Obfuscation key"`
}
func (c *relayClientConfig) Check() error {
if len(c.ListenAddr) == 0 {
return errors.New("no listen address")
}
if len(c.ServerAddr) == 0 {
return errors.New("no server address")
}
if c.UpMbps <= 0 || c.DownMbps <= 0 {
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")
}
return nil
}
func (c *relayClientConfig) String() string {
return fmt.Sprintf("%+v", *c)
}
type relayServerConfig struct {
ListenAddr string `json:"listen" desc:"Server listen address"`
RemoteAddr string `json:"remote" desc:"Remote relay address"`
CertFile string `json:"cert" desc:"TLS certificate file"`
KeyFile string `json:"key" desc:"TLS key file"`
UpMbps int `json:"up_mbps" desc:"Max upload speed per client in Mbps"`
DownMbps int `json:"down_mbps" desc:"Max download speed per client in Mbps"`
ReceiveWindowConn uint64 `json:"recv_window_conn" desc:"Max receive window size per connection"`
ReceiveWindowClient uint64 `json:"recv_window_client" desc:"Max receive window size per client"`
MaxConnClient int `json:"max_conn_client" desc:"Max simultaneous connections allowed per client"`
Obfs string `json:"obfs" desc:"Obfuscation key"`
}
func (c *relayServerConfig) Check() error {
if len(c.ListenAddr) == 0 {
return errors.New("no listen address")
}
if len(c.RemoteAddr) == 0 {
return errors.New("no remote address")
}
if len(c.CertFile) == 0 || len(c.KeyFile) == 0 {
return errors.New("TLS cert or key not provided")
}
if c.UpMbps < 0 || c.DownMbps < 0 {
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 *relayServerConfig) String() string {
return fmt.Sprintf("%+v", *c)
}

View File

@@ -1,121 +0,0 @@
package main
import (
"crypto/tls"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/sirupsen/logrus"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/obfs"
"io"
"net"
)
func relayServer(args []string) {
var config relayServerConfig
err := loadConfig(&config, args)
if err != nil {
logrus.WithField("error", err).Fatal("Unable to load configuration")
}
if err := config.Check(); err != nil {
logrus.WithField("error", err).Fatal("Configuration error")
}
logrus.WithField("config", config.String()).Info("Configuration loaded")
// Load cert
cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"cert": config.CertFile,
"key": config.KeyFile,
}).Fatal("Unable to load the certificate")
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{relayTLSProtocol},
MinVersion: tls.VersionTLS13,
}
quicConfig := &quic.Config{
MaxReceiveStreamFlowControlWindow: config.ReceiveWindowConn,
MaxReceiveConnectionFlowControlWindow: config.ReceiveWindowClient,
MaxIncomingStreams: int64(config.MaxConnClient),
KeepAlive: true,
}
if quicConfig.MaxReceiveStreamFlowControlWindow == 0 {
quicConfig.MaxReceiveStreamFlowControlWindow = DefaultMaxReceiveStreamFlowControlWindow
}
if quicConfig.MaxReceiveConnectionFlowControlWindow == 0 {
quicConfig.MaxReceiveConnectionFlowControlWindow = DefaultMaxReceiveConnectionFlowControlWindow
}
if quicConfig.MaxIncomingStreams == 0 {
quicConfig.MaxIncomingStreams = DefaultMaxIncomingStreams
}
var obfuscator core.Obfuscator
if len(config.Obfs) > 0 {
obfuscator = obfs.XORObfuscator(config.Obfs)
}
server, err := core.NewServer(config.ListenAddr, tlsConfig, quicConfig,
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
func(refBPS uint64) congestion.ExternalSendAlgorithm {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
},
obfuscator,
func(addr net.Addr, username string, password string, sSend uint64, sRecv uint64) (core.AuthResult, string) {
// No authentication logic in relay, just log username and speed
logrus.WithFields(logrus.Fields{
"addr": addr.String(),
"username": username,
"up": sSend / mbpsToBps,
"down": sRecv / mbpsToBps,
}).Info("Client connected")
return core.AuthResultSuccess, ""
},
func(addr net.Addr, username string, err error) {
logrus.WithFields(logrus.Fields{
"error": err.Error(),
"addr": addr.String(),
"username": username,
}).Info("Client disconnected")
},
func(addr net.Addr, username string, id int, reqType core.ConnectionType, reqAddr string) (core.ConnectResult, string, io.ReadWriteCloser) {
packet := reqType == core.ConnectionTypePacket
logrus.WithFields(logrus.Fields{
"username": username,
"src": addr.String(),
"id": id,
}).Debug("New stream")
if packet {
return core.ConnectResultBlocked, "unsupported", nil
}
conn, err := net.DialTimeout("tcp", config.RemoteAddr, dialTimeout)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"dst": config.RemoteAddr,
}).Error("TCP error")
return core.ConnectResultFailed, err.Error(), nil
}
return core.ConnectResultSuccess, "", conn
},
func(addr net.Addr, username string, id int, reqType core.ConnectionType, reqAddr string, err error) {
logrus.WithFields(logrus.Fields{
"error": err,
"username": username,
"src": addr.String(),
"id": id,
}).Debug("Stream closed")
},
)
if err != nil {
logrus.WithField("error", err).Fatal("Server initialization failed")
}
defer server.Close()
logrus.WithField("addr", config.ListenAddr).Info("Server up and running")
err = server.Serve()
logrus.WithField("error", err).Fatal("Server shutdown")
}

170
cmd/server.go Normal file
View File

@@ -0,0 +1,170 @@
package main
import (
"crypto/tls"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/sirupsen/logrus"
"github.com/tobyxdd/hysteria/pkg/acl"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/obfs"
"github.com/yosuke-furukawa/json5/encoding/json5"
"io"
"net"
)
func server(config *serverConfig) {
logrus.WithField("config", config.String()).Info("Server configuration loaded")
// Load cert
cert, err := tls.LoadX509KeyPair(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{
Certificates: []tls.Certificate{cert},
NextProtos: []string{tlsProtocolName},
MinVersion: tls.VersionTLS13,
}
// QUIC config
quicConfig := &quic.Config{
MaxStreamReceiveWindow: config.ReceiveWindowConn,
MaxConnectionReceiveWindow: config.ReceiveWindowClient,
MaxIncomingStreams: int64(config.MaxConnClient),
KeepAlive: true,
EnableDatagrams: true,
}
if quicConfig.MaxStreamReceiveWindow == 0 {
quicConfig.MaxStreamReceiveWindow = DefaultMaxReceiveStreamFlowControlWindow
}
if quicConfig.MaxConnectionReceiveWindow == 0 {
quicConfig.MaxConnectionReceiveWindow = DefaultMaxReceiveConnectionFlowControlWindow
}
if quicConfig.MaxIncomingStreams == 0 {
quicConfig.MaxIncomingStreams = DefaultMaxIncomingStreams
}
// Auth
var authFunc func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string)
switch authMode := config.Auth.Mode; authMode {
case "", "none":
logrus.Warn("No authentication configured")
authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
return true, "Welcome"
}
case "password":
logrus.Info("Password authentication enabled")
var pwdConfig map[string]string
err = json5.Unmarshal(config.Auth.Config, &pwdConfig)
if err != nil || len(pwdConfig["password"]) == 0 {
logrus.WithFields(logrus.Fields{
"error": err,
}).Fatal("Invalid password authentication config")
}
pwd := pwdConfig["password"]
authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) {
if string(auth) == pwd {
return true, "Welcome"
} else {
return false, "Wrong password"
}
}
default:
logrus.WithField("mode", config.Auth.Mode).Fatal("Unsupported authentication mode")
}
// Obfuscator
var obfuscator core.Obfuscator
if len(config.Obfs) > 0 {
obfuscator = obfs.XORObfuscator(config.Obfs)
}
// ACL
var aclEngine *acl.Engine
if len(config.ACL) > 0 {
aclEngine, err = acl.LoadFromFile(config.ACL)
if err != nil {
logrus.WithFields(logrus.Fields{
"error": err,
"file": config.ACL,
}).Fatal("Failed to parse ACL")
}
aclEngine.DefaultAction = acl.ActionDirect
}
// Server
server, err := core.NewServer(config.Listen, tlsConfig, quicConfig,
uint64(config.UpMbps)*mbpsToBps, uint64(config.DownMbps)*mbpsToBps,
func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, config.DisableUDP, aclEngine, obfuscator, authFunc,
tcpRequestFunc, tcpErrorFunc, udpRequestFunc, udpErrorFunc)
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize server")
}
defer server.Close()
logrus.WithField("addr", config.Listen).Info("Server up and running")
err = server.Serve()
logrus.WithField("error", err).Fatal("Server shutdown")
}
func tcpRequestFunc(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
"dst": 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": addr.String(),
"dst": reqAddr,
"error": err,
}).Info("TCP error")
} else {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
"dst": reqAddr,
}).Debug("TCP EOF")
}
}
func udpRequestFunc(addr net.Addr, auth []byte, sessionID uint32) {
logrus.WithFields(logrus.Fields{
"src": 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": addr.String(),
"session": sessionID,
"error": err,
}).Info("UDP error")
} else {
logrus.WithFields(logrus.Fields{
"src": 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"
}
}

View File

@@ -1,28 +0,0 @@
package main
import (
"strconv"
)
type optionalBoolFlag struct {
Exists bool
Value bool
}
func (flag *optionalBoolFlag) String() string {
return strconv.FormatBool(flag.Value)
}
func (flag *optionalBoolFlag) Set(s string) error {
v, err := strconv.ParseBool(s)
if err != nil {
return err
}
flag.Exists = true
flag.Value = v
return nil
}
func (flag *optionalBoolFlag) IsBoolFlag() bool {
return true
}

19
docs/socks5/udpchk.py Normal file
View File

@@ -0,0 +1,19 @@
import socks
import socket
def main():
s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)
s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080)
# Raw DNS request
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"
s.sendto(req, ("8.8.8.8", 53))
(rsp, address) = s.recvfrom(4096)
if rsp[0] == req[0] and rsp[1] == req[1]:
print("UDP check passed")
else:
print("Invalid response")
if __name__ == "__main__":
main()

7
go.mod
View File

@@ -5,14 +5,15 @@ go 1.14
require (
github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
github.com/golang/protobuf v1.4.2
github.com/hashicorp/golang-lru v0.5.4
github.com/lucas-clemente/quic-go v0.16.1
github.com/lucas-clemente/quic-go v0.20.0
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/sirupsen/logrus v1.6.0
github.com/txthinking/runnergroup v0.0.0-20200327135940-540a793bb997 // indirect
github.com/txthinking/socks5 v0.0.0-20200327133705-caf148ab5e9d
github.com/txthinking/x v0.0.0-20200330144832-5ad2416896a9 // indirect
github.com/yosuke-furukawa/json5 v0.1.1
)
replace github.com/lucas-clemente/quic-go => github.com/tobyxdd/quic-go v0.3.0-tquic-3
replace github.com/lucas-clemente/quic-go => github.com/tobyxdd/quic-go v0.20.1-0.20210320001547-eb20d732ffed

44
go.sum
View File

@@ -16,7 +16,6 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -36,15 +35,13 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
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-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -87,13 +84,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc=
github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs=
github.com/marten-seemann/qtls-go1-15 v0.1.0 h1:i/YPXVxz8q9umso/5y474CNcHmTpA+5DH+mFPjx6PZg=
github.com/marten-seemann/qtls-go1-15 v0.1.0/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -148,14 +147,11 @@ github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tobyxdd/quic-go v0.3.0-tquic-3 h1:DjvbOSRydm1P+CYqg/Kb+awXELRpk01IhNhKiJTFCaQ=
github.com/tobyxdd/quic-go v0.3.0-tquic-3/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg=
github.com/tobyxdd/quic-go v0.20.1-0.20210320001547-eb20d732ffed h1:bhRIrbeMeMHYFqjNPkQaLWH+kd4gBjkdeKgZUJtokYE=
github.com/tobyxdd/quic-go v0.20.1-0.20210320001547-eb20d732ffed/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk=
github.com/txthinking/runnergroup v0.0.0-20200327135940-540a793bb997 h1:vlDgnShahmE2XLslpr0hnzxfAmSj3JLX2CYi8Xct7G4=
github.com/txthinking/runnergroup v0.0.0-20200327135940-540a793bb997/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM=
github.com/txthinking/socks5 v0.0.0-20200327133705-caf148ab5e9d h1:V+Wyj2AqtLwLG7KnniV8QG+gEkENPsudZbivvLyX4kw=
@@ -164,13 +160,15 @@ github.com/txthinking/x v0.0.0-20200330144832-5ad2416896a9 h1:ngJOce33YJJT1PFTfC
github.com/txthinking/x v0.0.0-20200330144832-5ad2416896a9/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/yosuke-furukawa/json5 v0.1.1 h1:0F9mNwTvOuDNH243hoPqvf+dxa5QsKnZzU20uNsh3ZI=
github.com/yosuke-furukawa/json5 v0.1.1/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
@@ -179,7 +177,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -213,7 +211,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -222,7 +219,8 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 h1:joucsQqXmyBVxViHCPFjG3hx8JzIFSaym3l3MM/Jsdg=
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -235,8 +233,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
@@ -251,12 +251,10 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -283,7 +281,5 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
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=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View File

@@ -6,17 +6,16 @@ import (
)
const (
maxDatagramSize = 1252
ackRateMinSampleInterval = 4 * time.Second
ackRateMaxSampleInterval = 20 * time.Second
ackRateMinACKSampleCount = 100
)
type BrutalSender struct {
rttStats congestion.RTTStats
bps congestion.ByteCount
pacer *pacer
rttStats congestion.RTTStatsProvider
bps congestion.ByteCount
maxDatagramSize congestion.ByteCount
pacer *pacer
ackCount, lossCount uint64
ackRate float64
@@ -28,13 +27,13 @@ func NewBrutalSender(bps congestion.ByteCount) *BrutalSender {
bs := &BrutalSender{
bps: bps,
}
bs.pacer = newPacer(func() uint64 {
return uint64(float64(bs.bps)/bs.getAckRate()) * 5 / 4
bs.pacer = newPacer(func() congestion.ByteCount {
return congestion.ByteCount(float64(bs.bps)/bs.getAckRate()) * 5 / 4
})
return bs
}
func (b *BrutalSender) SetRTTStats(rttStats congestion.RTTStats) {
func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) {
b.rttStats = rttStats
}
@@ -43,7 +42,7 @@ func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Ti
}
func (b *BrutalSender) HasPacingBudget() bool {
return b.pacer.Budget(time.Now()) >= maxDatagramSize
return b.pacer.Budget(time.Now()) >= b.maxDatagramSize
}
func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {
@@ -60,7 +59,7 @@ func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {
func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount,
packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool) {
b.pacer.SentPacket(sentTime, uint64(bytes))
b.pacer.SentPacket(sentTime, bytes)
}
func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount,
@@ -75,6 +74,11 @@ func (b *BrutalSender) OnPacketLost(number congestion.PacketNumber, lostBytes co
b.maybeUpdateACKRate()
}
func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {
b.maxDatagramSize = size
b.pacer.SetMaxDatagramSize(size)
}
func (b *BrutalSender) maybeUpdateACKRate() {
now := time.Now()
if !now.After(b.ackRateNextUpdateMin) {
@@ -82,6 +86,7 @@ func (b *BrutalSender) maybeUpdateACKRate() {
}
// Min interval reached
if b.ackCount >= ackRateMinACKSampleCount {
// And enough samples, update ackRate now
b.ackRate = float64(b.ackCount) / float64(b.ackCount+b.lossCount)
b.ackCount, b.lossCount = 0, 0
b.ackRateNextUpdateMin = now.Add(ackRateMinSampleInterval)

View File

@@ -1,27 +1,35 @@
package congestion
import (
"github.com/lucas-clemente/quic-go/congestion"
"math"
"time"
)
const maxBurstSize = 10 * maxDatagramSize
const (
initMaxDatagramSize = 1252
maxBurstPackets = 10
minPacingDelay = time.Millisecond
)
// The pacer implements a token bucket pacing algorithm.
type pacer struct {
bandwidthFunc func() uint64
budgetAtLastSent uint64
budgetAtLastSent congestion.ByteCount
maxDatagramSize congestion.ByteCount
lastSentTime time.Time
getBandwidth func() congestion.ByteCount // in bytes/s
}
func newPacer(bandwidthFunc func() uint64) *pacer {
func newPacer(getBandwidth func() congestion.ByteCount) *pacer {
p := &pacer{
bandwidthFunc: bandwidthFunc,
budgetAtLastSent: maxBurstPackets * initMaxDatagramSize,
maxDatagramSize: initMaxDatagramSize,
getBandwidth: getBandwidth,
}
p.budgetAtLastSent = maxBurstSize
return p
}
func (p *pacer) SentPacket(sendTime time.Time, size uint64) {
func (p *pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) {
budget := p.Budget(sendTime)
if size > budget {
p.budgetAtLastSent = 0
@@ -31,26 +39,47 @@ func (p *pacer) SentPacket(sendTime time.Time, size uint64) {
p.lastSentTime = sendTime
}
func (p *pacer) Budget(now time.Time) uint64 {
func (p *pacer) Budget(now time.Time) congestion.ByteCount {
if p.lastSentTime.IsZero() {
return maxBurstSize
}
budget := p.budgetAtLastSent + p.bandwidthFunc()*uint64(now.Sub(p.lastSentTime).Nanoseconds())/1e9
if budget > maxBurstSize {
return maxBurstSize
} else {
return budget
return p.maxBurstSize()
}
budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9
return minByteCount(p.maxBurstSize(), budget)
}
func (p *pacer) maxBurstSize() congestion.ByteCount {
return maxByteCount(
congestion.ByteCount((minPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9,
maxBurstPackets*p.maxDatagramSize,
)
}
// TimeUntilSend returns when the next packet should be sent.
// It returns the zero value of time.Time if a packet can be sent immediately.
func (p *pacer) TimeUntilSend() time.Time {
if p.budgetAtLastSent >= maxDatagramSize {
if p.budgetAtLastSent >= p.maxDatagramSize {
return time.Time{}
}
d := time.Duration(math.Ceil(float64(maxDatagramSize-p.budgetAtLastSent)*1e9/float64(p.bandwidthFunc()))) * time.Nanosecond
if d < time.Millisecond {
return p.lastSentTime.Add(time.Millisecond)
} else {
return p.lastSentTime.Add(d)
}
return p.lastSentTime.Add(maxDuration(
minPacingDelay,
time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/float64(p.getBandwidth())))*time.Nanosecond,
))
}
func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) {
p.maxDatagramSize = s
}
func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount {
if a < b {
return b
}
return a
}
func minByteCount(a, b congestion.ByteCount) congestion.ByteCount {
if a < b {
return a
}
return b
}

View File

@@ -1,49 +1,54 @@
package core
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/lucas-clemente/quic-go"
"github.com/tobyxdd/hysteria/pkg/core/pb"
"github.com/tobyxdd/hysteria/pkg/utils"
"github.com/lucas-clemente/quic-go/congestion"
"github.com/lunixbochs/struc"
"net"
"sync"
"sync/atomic"
"time"
)
var (
ErrClosed = errors.New("client closed")
ErrClosed = errors.New("closed")
)
type Client struct {
inboundBytes, outboundBytes uint64 // atomic
type CongestionFactory func(refBPS uint64) congestion.CongestionControl
reconnectMutex sync.Mutex
closed bool
quicSession quic.Session
serverAddr string
username, password string
tlsConfig *tls.Config
quicConfig *quic.Config
sendBPS, recvBPS uint64
congestionFactory CongestionFactory
obfuscator Obfuscator
type Client struct {
serverAddr string
sendBPS, recvBPS uint64
auth []byte
congestionFactory CongestionFactory
obfuscator Obfuscator
tlsConfig *tls.Config
quicConfig *quic.Config
quicSession quic.Session
reconnectMutex sync.Mutex
closed bool
udpSessionMutex sync.RWMutex
udpSessionMap map[uint32]chan *udpMessage
}
func NewClient(serverAddr string, username string, password string, tlsConfig *tls.Config, quicConfig *quic.Config,
func NewClient(serverAddr string, auth []byte, tlsConfig *tls.Config, quicConfig *quic.Config,
sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory, obfuscator Obfuscator) (*Client, error) {
c := &Client{
serverAddr: serverAddr,
username: username,
password: password,
tlsConfig: tlsConfig,
quicConfig: quicConfig,
sendBPS: sendBPS,
recvBPS: recvBPS,
auth: auth,
congestionFactory: congestionFactory,
obfuscator: obfuscator,
tlsConfig: tlsConfig,
quicConfig: quicConfig,
}
if err := c.connectToServer(); err != nil {
return nil, err
@@ -51,58 +56,6 @@ func NewClient(serverAddr string, username string, password string, tlsConfig *t
return c, nil
}
func (c *Client) Dial(packet bool, addr string) (net.Conn, error) {
stream, localAddr, remoteAddr, err := c.openStreamWithReconnect()
if err != nil {
return nil, err
}
// Send request
req := &pb.ClientConnectRequest{Address: addr}
if packet {
req.Type = pb.ConnectionType_Packet
} else {
req.Type = pb.ConnectionType_Stream
}
err = writeClientConnectRequest(stream, req)
if err != nil {
_ = stream.Close()
return nil, err
}
// Read response
resp, err := readServerConnectResponse(stream)
if err != nil {
_ = stream.Close()
return nil, err
}
if resp.Result != pb.ConnectResult_CONN_SUCCESS {
_ = stream.Close()
return nil, fmt.Errorf("server rejected the connection %s (msg: %s)",
resp.Result.String(), resp.Message)
}
connWrap := &utils.QUICStreamWrapperConn{
Orig: stream,
PseudoLocalAddr: localAddr,
PseudoRemoteAddr: remoteAddr,
}
if packet {
return &utils.PacketWrapperConn{Orig: connWrap}, nil
} else {
return connWrap, nil
}
}
func (c *Client) Stats() (uint64, uint64) {
return atomic.LoadUint64(&c.inboundBytes), atomic.LoadUint64(&c.outboundBytes)
}
func (c *Client) Close() error {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
err := c.quicSession.CloseWithError(closeErrorCodeGeneric, "generic")
c.closed = true
return err
}
func (c *Client) connectToServer() error {
serverUDPAddr, err := net.ResolveUDPAddr("udp", c.serverAddr)
if err != nil {
@@ -124,75 +77,282 @@ func (c *Client) connectToServer() error {
return err
}
// Control stream
ctx, ctxCancel := context.WithTimeout(context.Background(), controlStreamTimeout)
ctlStream, err := qs.OpenStreamSync(ctx)
ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout)
stream, err := qs.OpenStreamSync(ctx)
ctxCancel()
if err != nil {
_ = qs.CloseWithError(closeErrorCodeProtocolFailure, "control stream error")
_ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error")
return err
}
result, msg, err := c.handleControlStream(qs, ctlStream)
ok, msg, err := c.handleControlStream(qs, stream)
if err != nil {
_ = qs.CloseWithError(closeErrorCodeProtocolFailure, "control stream handling error")
_ = qs.CloseWithError(closeErrorCodeProtocol, "protocol error")
return err
}
if result != pb.AuthResult_AUTH_SUCCESS {
_ = qs.CloseWithError(closeErrorCodeProtocolFailure, "authentication failure")
return fmt.Errorf("authentication failure %s (msg: %s)", result.String(), msg)
if !ok {
_ = qs.CloseWithError(closeErrorCodeAuth, "auth error")
return fmt.Errorf("auth error: %s", msg)
}
// All good
c.udpSessionMap = make(map[uint32]chan *udpMessage)
go c.handleMessage(qs)
c.quicSession = qs
return nil
}
func (c *Client) handleControlStream(qs quic.Session, stream quic.Stream) (pb.AuthResult, string, error) {
err := writeClientAuthRequest(stream, &pb.ClientAuthRequest{
Credential: &pb.Credential{
Username: c.username,
Password: c.password,
},
Speed: &pb.Speed{
SendBps: c.sendBPS,
ReceiveBps: c.recvBPS,
func (c *Client) handleControlStream(qs quic.Session, stream quic.Stream) (bool, string, error) {
// Send client hello
err := struc.Pack(stream, &clientHello{
Rate: transmissionRate{
SendBPS: c.sendBPS,
RecvBPS: c.recvBPS,
},
Auth: c.auth,
})
if err != nil {
return 0, "", err
return false, "", err
}
// Response
resp, err := readServerAuthResponse(stream)
// Receive server hello
var sh serverHello
err = struc.Unpack(stream, &sh)
if err != nil {
return 0, "", err
return false, "", err
}
// Set the congestion accordingly
if resp.Result == pb.AuthResult_AUTH_SUCCESS && c.congestionFactory != nil {
qs.SetCongestion(c.congestionFactory(resp.Speed.ReceiveBps))
if sh.OK && c.congestionFactory != nil {
qs.SetCongestionControl(c.congestionFactory(sh.Rate.RecvBPS))
}
return resp.Result, resp.Message, nil
return true, sh.Message, nil
}
func (c *Client) openStreamWithReconnect() (quic.Stream, net.Addr, net.Addr, error) {
func (c *Client) handleMessage(qs quic.Session) {
for {
msg, err := qs.ReceiveMessage()
if err != nil {
break
}
var udpMsg udpMessage
err = struc.Unpack(bytes.NewBuffer(msg), &udpMsg)
if err != nil {
continue
}
c.udpSessionMutex.RLock()
ch, ok := c.udpSessionMap[udpMsg.SessionID]
if ok {
select {
case ch <- &udpMsg:
// OK
default:
// Silently drop the message when the channel is full
}
}
c.udpSessionMutex.RUnlock()
}
}
func (c *Client) openStreamWithReconnect() (quic.Session, quic.Stream, error) {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
if c.closed {
return nil, nil, nil, ErrClosed
return nil, nil, ErrClosed
}
stream, err := c.quicSession.OpenStream()
if err == nil {
// All good
return stream, c.quicSession.LocalAddr(), c.quicSession.RemoteAddr(), nil
return c.quicSession, stream, nil
}
// Something is wrong
if nErr, ok := err.(net.Error); ok && nErr.Temporary() {
// Temporary error, just return
return nil, nil, nil, err
return nil, nil, err
}
// Permanent error, need to reconnect
if err := c.connectToServer(); err != nil {
// Still error, oops
return nil, nil, nil, err
return nil, nil, err
}
// We are not going to try again even if it still fails the second time
stream, err = c.quicSession.OpenStream()
return stream, c.quicSession.LocalAddr(), c.quicSession.RemoteAddr(), err
return c.quicSession, stream, nil
}
func (c *Client) DialTCP(addr string) (net.Conn, error) {
session, stream, err := c.openStreamWithReconnect()
if err != nil {
return nil, err
}
// Send request
err = struc.Pack(stream, &clientRequest{
UDP: false,
Address: addr,
})
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)
}
return &quicConn{
Orig: stream,
PseudoLocalAddr: session.LocalAddr(),
PseudoRemoteAddr: session.RemoteAddr(),
}, nil
}
func (c *Client) DialUDP() (UDPConn, 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 ensures 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 := &quicPktConn{
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 := c.quicSession.CloseWithError(closeErrorCodeGeneric, "")
c.closed = true
return err
}
type quicConn struct {
Orig quic.Stream
PseudoLocalAddr net.Addr
PseudoRemoteAddr net.Addr
}
func (w *quicConn) Read(b []byte) (n int, err error) {
return w.Orig.Read(b)
}
func (w *quicConn) Write(b []byte) (n int, err error) {
return w.Orig.Write(b)
}
func (w *quicConn) Close() error {
return w.Orig.Close()
}
func (w *quicConn) LocalAddr() net.Addr {
return w.PseudoLocalAddr
}
func (w *quicConn) RemoteAddr() net.Addr {
return w.PseudoRemoteAddr
}
func (w *quicConn) SetDeadline(t time.Time) error {
return w.Orig.SetDeadline(t)
}
func (w *quicConn) SetReadDeadline(t time.Time) error {
return w.Orig.SetReadDeadline(t)
}
func (w *quicConn) SetWriteDeadline(t time.Time) error {
return w.Orig.SetWriteDeadline(t)
}
type UDPConn interface {
ReadFrom() ([]byte, string, error)
WriteTo([]byte, string) error
Close() error
}
type quicPktConn struct {
Session quic.Session
Stream quic.Stream
CloseFunc func()
UDPSessionID uint32
MsgCh <-chan *udpMessage
}
func (c *quicPktConn) 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 *quicPktConn) ReadFrom() ([]byte, string, error) {
msg := <-c.MsgCh
if msg == nil {
// Closed
return nil, "", ErrClosed
}
return msg.Data, msg.Address, nil
}
func (c *quicPktConn) WriteTo(p []byte, addr string) error {
var msgBuf bytes.Buffer
_ = struc.Pack(&msgBuf, &udpMessage{
SessionID: c.UDPSessionID,
Address: addr,
Data: p,
})
return c.Session.SendMessage(msgBuf.Bytes())
}
func (c *quicPktConn) Close() error {
c.CloseFunc()
return c.Stream.Close()
}

View File

@@ -0,0 +1,58 @@
package core
import (
"crypto/tls"
"github.com/lucas-clemente/quic-go"
"testing"
)
func TestClientUDP(t *testing.T) {
client, err := NewClient("toby.moe:36713", nil, &tls.Config{
NextProtos: []string{"hysteria"},
MinVersion: tls.VersionTLS13,
}, &quic.Config{
EnableDatagrams: true,
}, 125000, 125000, nil, nil)
if err != nil {
t.Fatal(err)
}
conn, err := client.DialUDP()
if err != nil {
t.Fatal("conn DialUDP", err)
}
t.Run("8.8.8.8", func(t *testing.T) {
dnsReq := []byte{0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62, 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01}
err := conn.WriteTo(dnsReq, "8.8.8.8:53")
if err != nil {
t.Error("WriteTo", err)
}
buf, _, err := conn.ReadFrom()
if err != nil {
t.Error("ReadFrom", err)
}
if buf[0] != dnsReq[0] || buf[1] != dnsReq[1] {
t.Error("invalid response")
}
})
t.Run("1.1.1.1", func(t *testing.T) {
dnsReq := []byte{0x66, 0x77, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62, 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01}
err := conn.WriteTo(dnsReq, "1.1.1.1:53")
if err != nil {
t.Error("WriteTo", err)
}
buf, _, err := conn.ReadFrom()
if err != nil {
t.Error("ReadFrom", err)
}
if buf[0] != dnsReq[0] || buf[1] != dnsReq[1] {
t.Error("invalid response")
}
})
t.Run("Close", func(t *testing.T) {
_ = conn.Close()
_, _, err := conn.ReadFrom()
if err != ErrClosed {
t.Error("closed conn not returning the correct error")
}
})
}

View File

@@ -1,104 +0,0 @@
package core
import (
"encoding/binary"
"github.com/golang/protobuf/proto"
"github.com/tobyxdd/hysteria/pkg/core/pb"
"io"
)
const (
closeErrorCodeGeneric = 0
closeErrorCodeProtocolFailure = 1
)
func readDataBlock(r io.Reader) ([]byte, error) {
var sz uint32
if err := binary.Read(r, controlProtocolEndian, &sz); err != nil {
return nil, err
}
buf := make([]byte, sz)
_, err := io.ReadFull(r, buf)
return buf, err
}
func writeDataBlock(w io.Writer, data []byte) error {
sz := uint32(len(data))
if err := binary.Write(w, controlProtocolEndian, &sz); err != nil {
return err
}
_, err := w.Write(data)
return err
}
func readClientAuthRequest(r io.Reader) (*pb.ClientAuthRequest, error) {
bs, err := readDataBlock(r)
if err != nil {
return nil, err
}
var req pb.ClientAuthRequest
err = proto.Unmarshal(bs, &req)
return &req, err
}
func writeClientAuthRequest(w io.Writer, req *pb.ClientAuthRequest) error {
bs, err := proto.Marshal(req)
if err != nil {
return err
}
return writeDataBlock(w, bs)
}
func readServerAuthResponse(r io.Reader) (*pb.ServerAuthResponse, error) {
bs, err := readDataBlock(r)
if err != nil {
return nil, err
}
var resp pb.ServerAuthResponse
err = proto.Unmarshal(bs, &resp)
return &resp, err
}
func writeServerAuthResponse(w io.Writer, resp *pb.ServerAuthResponse) error {
bs, err := proto.Marshal(resp)
if err != nil {
return err
}
return writeDataBlock(w, bs)
}
func readClientConnectRequest(r io.Reader) (*pb.ClientConnectRequest, error) {
bs, err := readDataBlock(r)
if err != nil {
return nil, err
}
var req pb.ClientConnectRequest
err = proto.Unmarshal(bs, &req)
return &req, err
}
func writeClientConnectRequest(w io.Writer, req *pb.ClientConnectRequest) error {
bs, err := proto.Marshal(req)
if err != nil {
return err
}
return writeDataBlock(w, bs)
}
func readServerConnectResponse(r io.Reader) (*pb.ServerConnectResponse, error) {
bs, err := readDataBlock(r)
if err != nil {
return nil, err
}
var resp pb.ServerConnectResponse
err = proto.Unmarshal(bs, &resp)
return &resp, err
}
func writeServerConnectResponse(w io.Writer, resp *pb.ServerConnectResponse) error {
bs, err := proto.Marshal(resp)
if err != nil {
return err
}
return writeDataBlock(w, bs)
}

View File

@@ -1,440 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: control.proto
package pb
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type AuthResult int32
const (
AuthResult_AUTH_SUCCESS AuthResult = 0
AuthResult_AUTH_INVALID_CRED AuthResult = 1
AuthResult_AUTH_INTERNAL_ERROR AuthResult = 2
)
var AuthResult_name = map[int32]string{
0: "AUTH_SUCCESS",
1: "AUTH_INVALID_CRED",
2: "AUTH_INTERNAL_ERROR",
}
var AuthResult_value = map[string]int32{
"AUTH_SUCCESS": 0,
"AUTH_INVALID_CRED": 1,
"AUTH_INTERNAL_ERROR": 2,
}
func (x AuthResult) String() string {
return proto.EnumName(AuthResult_name, int32(x))
}
func (AuthResult) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{0}
}
type ConnectionType int32
const (
ConnectionType_Stream ConnectionType = 0
ConnectionType_Packet ConnectionType = 1
)
var ConnectionType_name = map[int32]string{
0: "Stream",
1: "Packet",
}
var ConnectionType_value = map[string]int32{
"Stream": 0,
"Packet": 1,
}
func (x ConnectionType) String() string {
return proto.EnumName(ConnectionType_name, int32(x))
}
func (ConnectionType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{1}
}
type ConnectResult int32
const (
ConnectResult_CONN_SUCCESS ConnectResult = 0
ConnectResult_CONN_FAILED ConnectResult = 1
ConnectResult_CONN_BLOCKED ConnectResult = 2
)
var ConnectResult_name = map[int32]string{
0: "CONN_SUCCESS",
1: "CONN_FAILED",
2: "CONN_BLOCKED",
}
var ConnectResult_value = map[string]int32{
"CONN_SUCCESS": 0,
"CONN_FAILED": 1,
"CONN_BLOCKED": 2,
}
func (x ConnectResult) String() string {
return proto.EnumName(ConnectResult_name, int32(x))
}
func (ConnectResult) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{2}
}
type Speed struct {
SendBps uint64 `protobuf:"varint,1,opt,name=send_bps,json=sendBps,proto3" json:"send_bps,omitempty"`
ReceiveBps uint64 `protobuf:"varint,2,opt,name=receive_bps,json=receiveBps,proto3" json:"receive_bps,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Speed) Reset() { *m = Speed{} }
func (m *Speed) String() string { return proto.CompactTextString(m) }
func (*Speed) ProtoMessage() {}
func (*Speed) Descriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{0}
}
func (m *Speed) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Speed.Unmarshal(m, b)
}
func (m *Speed) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Speed.Marshal(b, m, deterministic)
}
func (m *Speed) XXX_Merge(src proto.Message) {
xxx_messageInfo_Speed.Merge(m, src)
}
func (m *Speed) XXX_Size() int {
return xxx_messageInfo_Speed.Size(m)
}
func (m *Speed) XXX_DiscardUnknown() {
xxx_messageInfo_Speed.DiscardUnknown(m)
}
var xxx_messageInfo_Speed proto.InternalMessageInfo
func (m *Speed) GetSendBps() uint64 {
if m != nil {
return m.SendBps
}
return 0
}
func (m *Speed) GetReceiveBps() uint64 {
if m != nil {
return m.ReceiveBps
}
return 0
}
type Credential struct {
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Credential) Reset() { *m = Credential{} }
func (m *Credential) String() string { return proto.CompactTextString(m) }
func (*Credential) ProtoMessage() {}
func (*Credential) Descriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{1}
}
func (m *Credential) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Credential.Unmarshal(m, b)
}
func (m *Credential) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Credential.Marshal(b, m, deterministic)
}
func (m *Credential) XXX_Merge(src proto.Message) {
xxx_messageInfo_Credential.Merge(m, src)
}
func (m *Credential) XXX_Size() int {
return xxx_messageInfo_Credential.Size(m)
}
func (m *Credential) XXX_DiscardUnknown() {
xxx_messageInfo_Credential.DiscardUnknown(m)
}
var xxx_messageInfo_Credential proto.InternalMessageInfo
func (m *Credential) GetUsername() string {
if m != nil {
return m.Username
}
return ""
}
func (m *Credential) GetPassword() string {
if m != nil {
return m.Password
}
return ""
}
type ClientAuthRequest struct {
Credential *Credential `protobuf:"bytes,1,opt,name=credential,proto3" json:"credential,omitempty"`
Speed *Speed `protobuf:"bytes,2,opt,name=speed,proto3" json:"speed,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ClientAuthRequest) Reset() { *m = ClientAuthRequest{} }
func (m *ClientAuthRequest) String() string { return proto.CompactTextString(m) }
func (*ClientAuthRequest) ProtoMessage() {}
func (*ClientAuthRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{2}
}
func (m *ClientAuthRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ClientAuthRequest.Unmarshal(m, b)
}
func (m *ClientAuthRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ClientAuthRequest.Marshal(b, m, deterministic)
}
func (m *ClientAuthRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ClientAuthRequest.Merge(m, src)
}
func (m *ClientAuthRequest) XXX_Size() int {
return xxx_messageInfo_ClientAuthRequest.Size(m)
}
func (m *ClientAuthRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ClientAuthRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ClientAuthRequest proto.InternalMessageInfo
func (m *ClientAuthRequest) GetCredential() *Credential {
if m != nil {
return m.Credential
}
return nil
}
func (m *ClientAuthRequest) GetSpeed() *Speed {
if m != nil {
return m.Speed
}
return nil
}
type ServerAuthResponse struct {
Result AuthResult `protobuf:"varint,1,opt,name=result,proto3,enum=core.AuthResult" json:"result,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
Speed *Speed `protobuf:"bytes,3,opt,name=speed,proto3" json:"speed,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerAuthResponse) Reset() { *m = ServerAuthResponse{} }
func (m *ServerAuthResponse) String() string { return proto.CompactTextString(m) }
func (*ServerAuthResponse) ProtoMessage() {}
func (*ServerAuthResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{3}
}
func (m *ServerAuthResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerAuthResponse.Unmarshal(m, b)
}
func (m *ServerAuthResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerAuthResponse.Marshal(b, m, deterministic)
}
func (m *ServerAuthResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerAuthResponse.Merge(m, src)
}
func (m *ServerAuthResponse) XXX_Size() int {
return xxx_messageInfo_ServerAuthResponse.Size(m)
}
func (m *ServerAuthResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ServerAuthResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ServerAuthResponse proto.InternalMessageInfo
func (m *ServerAuthResponse) GetResult() AuthResult {
if m != nil {
return m.Result
}
return AuthResult_AUTH_SUCCESS
}
func (m *ServerAuthResponse) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func (m *ServerAuthResponse) GetSpeed() *Speed {
if m != nil {
return m.Speed
}
return nil
}
type ClientConnectRequest struct {
Type ConnectionType `protobuf:"varint,1,opt,name=type,proto3,enum=core.ConnectionType" json:"type,omitempty"`
Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ClientConnectRequest) Reset() { *m = ClientConnectRequest{} }
func (m *ClientConnectRequest) String() string { return proto.CompactTextString(m) }
func (*ClientConnectRequest) ProtoMessage() {}
func (*ClientConnectRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{4}
}
func (m *ClientConnectRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ClientConnectRequest.Unmarshal(m, b)
}
func (m *ClientConnectRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ClientConnectRequest.Marshal(b, m, deterministic)
}
func (m *ClientConnectRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ClientConnectRequest.Merge(m, src)
}
func (m *ClientConnectRequest) XXX_Size() int {
return xxx_messageInfo_ClientConnectRequest.Size(m)
}
func (m *ClientConnectRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ClientConnectRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ClientConnectRequest proto.InternalMessageInfo
func (m *ClientConnectRequest) GetType() ConnectionType {
if m != nil {
return m.Type
}
return ConnectionType_Stream
}
func (m *ClientConnectRequest) GetAddress() string {
if m != nil {
return m.Address
}
return ""
}
type ServerConnectResponse struct {
Result ConnectResult `protobuf:"varint,1,opt,name=result,proto3,enum=core.ConnectResult" json:"result,omitempty"`
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ServerConnectResponse) Reset() { *m = ServerConnectResponse{} }
func (m *ServerConnectResponse) String() string { return proto.CompactTextString(m) }
func (*ServerConnectResponse) ProtoMessage() {}
func (*ServerConnectResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_0c5120591600887d, []int{5}
}
func (m *ServerConnectResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ServerConnectResponse.Unmarshal(m, b)
}
func (m *ServerConnectResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ServerConnectResponse.Marshal(b, m, deterministic)
}
func (m *ServerConnectResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ServerConnectResponse.Merge(m, src)
}
func (m *ServerConnectResponse) XXX_Size() int {
return xxx_messageInfo_ServerConnectResponse.Size(m)
}
func (m *ServerConnectResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ServerConnectResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ServerConnectResponse proto.InternalMessageInfo
func (m *ServerConnectResponse) GetResult() ConnectResult {
if m != nil {
return m.Result
}
return ConnectResult_CONN_SUCCESS
}
func (m *ServerConnectResponse) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func init() {
proto.RegisterEnum("core.AuthResult", AuthResult_name, AuthResult_value)
proto.RegisterEnum("core.ConnectionType", ConnectionType_name, ConnectionType_value)
proto.RegisterEnum("core.ConnectResult", ConnectResult_name, ConnectResult_value)
proto.RegisterType((*Speed)(nil), "core.Speed")
proto.RegisterType((*Credential)(nil), "core.Credential")
proto.RegisterType((*ClientAuthRequest)(nil), "core.ClientAuthRequest")
proto.RegisterType((*ServerAuthResponse)(nil), "core.ServerAuthResponse")
proto.RegisterType((*ClientConnectRequest)(nil), "core.ClientConnectRequest")
proto.RegisterType((*ServerConnectResponse)(nil), "core.ServerConnectResponse")
}
func init() {
proto.RegisterFile("control.proto", fileDescriptor_0c5120591600887d)
}
var fileDescriptor_0c5120591600887d = []byte{
// 434 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0xd1, 0x6e, 0xd3, 0x30,
0x14, 0x86, 0xd7, 0xd2, 0x75, 0xdb, 0x09, 0x1b, 0x99, 0xb7, 0x89, 0xc1, 0x0d, 0x90, 0xab, 0xaa,
0x48, 0x15, 0x1a, 0x4f, 0x90, 0x3a, 0x41, 0x54, 0x54, 0x29, 0x72, 0x3a, 0x2e, 0xb8, 0xa0, 0xca,
0x92, 0x23, 0x56, 0x91, 0xda, 0xc6, 0x76, 0x86, 0x26, 0x5e, 0x1e, 0xc5, 0x71, 0xd2, 0x16, 0x09,
0x89, 0xbb, 0x9e, 0x73, 0x7e, 0xfd, 0x9f, 0xbf, 0x2a, 0x70, 0x9a, 0x0b, 0x6e, 0x94, 0x28, 0x27,
0x52, 0x09, 0x23, 0xc8, 0x20, 0x17, 0x0a, 0x03, 0x0a, 0x87, 0xa9, 0x44, 0x2c, 0xc8, 0x0b, 0x38,
0xd6, 0xc8, 0x8b, 0xd5, 0x9d, 0xd4, 0xd7, 0xbd, 0xd7, 0xbd, 0xd1, 0x80, 0x1d, 0xd5, 0xf3, 0x54,
0x6a, 0xf2, 0x0a, 0x3c, 0x85, 0x39, 0xae, 0x1f, 0xd0, 0x5e, 0xfb, 0xf6, 0x0a, 0x6e, 0x35, 0x95,
0x3a, 0x88, 0x00, 0xa8, 0xc2, 0x02, 0xb9, 0x59, 0x67, 0x25, 0x79, 0x09, 0xc7, 0x95, 0x46, 0xc5,
0xb3, 0x0d, 0xda, 0xa6, 0x13, 0xd6, 0xcd, 0xf5, 0x4d, 0x66, 0x5a, 0xff, 0x12, 0xaa, 0xb0, 0x3d,
0x27, 0xac, 0x9b, 0x83, 0x7b, 0x38, 0xa7, 0xe5, 0x1a, 0xb9, 0x09, 0x2b, 0x73, 0xcf, 0xf0, 0x67,
0x85, 0xda, 0x90, 0x77, 0x00, 0x79, 0x57, 0x6d, 0xeb, 0xbc, 0x1b, 0x7f, 0x52, 0x3f, 0x7d, 0xb2,
0x45, 0xb2, 0x9d, 0x0c, 0x79, 0x03, 0x87, 0xba, 0x36, 0xb2, 0xfd, 0xde, 0x8d, 0xd7, 0x84, 0xad,
0x24, 0x6b, 0x2e, 0xc1, 0x6f, 0x20, 0x29, 0xaa, 0x07, 0x54, 0x0d, 0x49, 0x4b, 0xc1, 0x35, 0x92,
0x11, 0x0c, 0x15, 0xea, 0xaa, 0x34, 0x16, 0x73, 0xd6, 0x62, 0x5c, 0xa6, 0x2a, 0x0d, 0x73, 0x77,
0x72, 0x0d, 0x47, 0x1b, 0xd4, 0x3a, 0xfb, 0x8e, 0x4e, 0xa2, 0x1d, 0xb7, 0xf0, 0x27, 0xff, 0x84,
0x7f, 0x85, 0xcb, 0x46, 0x93, 0x0a, 0xce, 0x31, 0x37, 0xad, 0xe9, 0x08, 0x06, 0xe6, 0x51, 0xa2,
0x83, 0x5f, 0x3a, 0xc7, 0x26, 0xb3, 0x16, 0x7c, 0xf9, 0x28, 0x91, 0xd9, 0x44, 0x8d, 0xcf, 0x8a,
0x42, 0xa1, 0xd6, 0x2d, 0xde, 0x8d, 0xc1, 0x37, 0xb8, 0x6a, 0xc4, 0xba, 0x6e, 0xe7, 0xf6, 0xf6,
0x2f, 0xb7, 0x8b, 0xbd, 0xfa, 0xff, 0xd5, 0x1b, 0x27, 0x00, 0xdb, 0xbf, 0x83, 0xf8, 0xf0, 0x34,
0xbc, 0x5d, 0x7e, 0x5c, 0xa5, 0xb7, 0x94, 0xc6, 0x69, 0xea, 0x1f, 0x90, 0x2b, 0x38, 0xb7, 0x9b,
0x59, 0xf2, 0x25, 0x9c, 0xcf, 0xa2, 0x15, 0x65, 0x71, 0xe4, 0xf7, 0xc8, 0x73, 0xb8, 0x70, 0xeb,
0x65, 0xcc, 0x92, 0x70, 0xbe, 0x8a, 0x19, 0x5b, 0x30, 0xbf, 0x3f, 0x1e, 0xc1, 0xd9, 0xbe, 0x21,
0x01, 0x18, 0xa6, 0x46, 0x61, 0xb6, 0xf1, 0x0f, 0xea, 0xdf, 0x9f, 0xb3, 0xfc, 0x07, 0x1a, 0xbf,
0x37, 0x8e, 0xe0, 0x74, 0xef, 0xb1, 0x35, 0x9c, 0x2e, 0x92, 0x64, 0x07, 0xfe, 0x0c, 0x3c, 0xbb,
0xf9, 0x10, 0xce, 0xe6, 0x16, 0xdb, 0x46, 0xa6, 0xf3, 0x05, 0xfd, 0x14, 0x47, 0x7e, 0xff, 0x6e,
0x68, 0x3f, 0xfd, 0xf7, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xd2, 0x2f, 0x8d, 0x6f, 0x0b, 0x03,
0x00, 0x00,
}

View File

@@ -1,50 +0,0 @@
syntax = "proto3";
package core;
message Speed {
uint64 send_bps = 1;
uint64 receive_bps = 2;
}
message Credential {
string username = 1;
string password = 2;
}
enum AuthResult {
AUTH_SUCCESS = 0;
AUTH_INVALID_CRED = 1;
AUTH_INTERNAL_ERROR = 2;
}
message ClientAuthRequest {
Credential credential = 1;
Speed speed = 2;
}
message ServerAuthResponse {
AuthResult result = 1;
string message = 2;
Speed speed = 3;
}
enum ConnectionType {
Stream = 0;
Packet = 1;
}
enum ConnectResult {
CONN_SUCCESS = 0;
CONN_FAILED = 1;
CONN_BLOCKED = 2;
}
message ClientConnectRequest {
ConnectionType type = 1;
string address = 2;
}
message ServerConnectResponse {
ConnectResult result = 1;
string message = 2;
}

View File

@@ -1,3 +0,0 @@
package pb
//go:generate protoc --go_out=. control.proto

53
pkg/core/protocol.go Normal file
View File

@@ -0,0 +1,53 @@
package core
import (
"time"
)
const (
protocolVersion = uint8(1)
protocolTimeout = 10 * time.Second
closeErrorCodeGeneric = 0
closeErrorCodeProtocol = 1
closeErrorCodeAuth = 2
)
type transmissionRate struct {
SendBPS uint64
RecvBPS uint64
}
type clientHello struct {
Rate transmissionRate
AuthLen uint16 `struc:"sizeof=Auth"`
Auth []byte
}
type serverHello struct {
OK bool
Rate transmissionRate
MessageLen uint16 `struc:"sizeof=Message"`
Message string
}
type clientRequest struct {
UDP bool
AddressLen uint16 `struc:"sizeof=Address"`
Address string
}
type serverResponse struct {
OK bool
UDPSessionID uint32
MessageLen uint16 `struc:"sizeof=Message"`
Message string
}
type udpMessage struct {
SessionID uint32
AddressLen uint16 `struc:"sizeof=Address"`
Address string
DataLen uint16 `struc:"sizeof=Data"`
Data []byte
}

View File

@@ -4,61 +4,40 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/lucas-clemente/quic-go"
"github.com/tobyxdd/hysteria/pkg/core/pb"
"github.com/tobyxdd/hysteria/pkg/utils"
"io"
"github.com/lunixbochs/struc"
"github.com/tobyxdd/hysteria/pkg/acl"
"net"
"sync/atomic"
"time"
)
type AuthResult int32
type ConnectionType int32
type ConnectResult int32
const dialTimeout = 10 * time.Second
const (
AuthResultSuccess AuthResult = iota
AuthResultInvalidCred
AuthResultInternalError
)
const (
ConnectionTypeStream ConnectionType = iota
ConnectionTypePacket
)
const (
ConnectResultSuccess ConnectResult = iota
ConnectResultFailed
ConnectResultBlocked
)
type ClientAuthFunc func(addr net.Addr, username string, password string, sSend uint64, sRecv uint64) (AuthResult, string)
type ClientDisconnectedFunc func(addr net.Addr, username string, err error)
type HandleRequestFunc func(addr net.Addr, username string, id int, reqType ConnectionType, reqAddr string) (ConnectResult, string, io.ReadWriteCloser)
type RequestClosedFunc func(addr net.Addr, username string, id int, reqType ConnectionType, reqAddr string, err error)
type AuthFunc func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string)
type TCPRequestFunc func(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string)
type TCPErrorFunc func(addr net.Addr, auth []byte, reqAddr string, err error)
type UDPRequestFunc func(addr net.Addr, auth []byte, sessionID uint32)
type UDPErrorFunc func(addr net.Addr, auth []byte, sessionID uint32, err error)
type Server struct {
inboundBytes, outboundBytes uint64 // atomic
sendBPS, recvBPS uint64
congestionFactory CongestionFactory
disableUDP bool
aclEngine *acl.Engine
listener quic.Listener
sendBPS, recvBPS uint64
authFunc AuthFunc
tcpRequestFunc TCPRequestFunc
tcpErrorFunc TCPErrorFunc
udpRequestFunc UDPRequestFunc
udpErrorFunc UDPErrorFunc
congestionFactory CongestionFactory
clientAuthFunc ClientAuthFunc
clientDisconnectedFunc ClientDisconnectedFunc
handleRequestFunc HandleRequestFunc
requestClosedFunc RequestClosedFunc
listener quic.Listener
}
func NewServer(addr string, tlsConfig *tls.Config, quicConfig *quic.Config,
sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory,
obfuscator Obfuscator,
clientAuthFunc ClientAuthFunc,
clientDisconnectedFunc ClientDisconnectedFunc,
handleRequestFunc HandleRequestFunc,
requestClosedFunc RequestClosedFunc) (*Server, error) {
sendBPS uint64, recvBPS uint64, congestionFactory CongestionFactory, disableUDP bool, aclEngine *acl.Engine,
obfuscator Obfuscator, authFunc AuthFunc, tcpRequestFunc TCPRequestFunc, tcpErrorFunc TCPErrorFunc,
udpRequestFunc UDPRequestFunc, udpErrorFunc UDPErrorFunc) (*Server, error) {
packetConn, err := net.ListenPacket("udp", addr)
if err != nil {
return nil, err
@@ -75,14 +54,17 @@ func NewServer(addr string, tlsConfig *tls.Config, quicConfig *quic.Config,
return nil, err
}
s := &Server{
listener: listener,
sendBPS: sendBPS,
recvBPS: recvBPS,
congestionFactory: congestionFactory,
clientAuthFunc: clientAuthFunc,
clientDisconnectedFunc: clientDisconnectedFunc,
handleRequestFunc: handleRequestFunc,
requestClosedFunc: requestClosedFunc,
listener: listener,
sendBPS: sendBPS,
recvBPS: recvBPS,
congestionFactory: congestionFactory,
disableUDP: disableUDP,
aclEngine: aclEngine,
authFunc: authFunc,
tcpRequestFunc: tcpRequestFunc,
tcpErrorFunc: tcpErrorFunc,
udpRequestFunc: udpRequestFunc,
udpErrorFunc: udpErrorFunc,
}
return s, nil
}
@@ -97,128 +79,71 @@ func (s *Server) Serve() error {
}
}
func (s *Server) Stats() (uint64, uint64) {
return atomic.LoadUint64(&s.inboundBytes), atomic.LoadUint64(&s.outboundBytes)
}
func (s *Server) Close() error {
return s.listener.Close()
}
func (s *Server) handleClient(cs quic.Session) {
// Expect the client to create a control stream to send its own information
ctx, ctxCancel := context.WithTimeout(context.Background(), controlStreamTimeout)
ctlStream, err := cs.AcceptStream(ctx)
ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout)
stream, err := cs.AcceptStream(ctx)
ctxCancel()
if err != nil {
_ = cs.CloseWithError(closeErrorCodeProtocolFailure, "control stream error")
_ = cs.CloseWithError(closeErrorCodeProtocol, "protocol error")
return
}
// Handle the control stream
username, ok, err := s.handleControlStream(cs, ctlStream)
auth, ok, err := s.handleControlStream(cs, stream)
if err != nil {
_ = cs.CloseWithError(closeErrorCodeProtocolFailure, "control stream handling error")
_ = cs.CloseWithError(closeErrorCodeProtocol, "protocol error")
return
}
if !ok {
_ = cs.CloseWithError(closeErrorCodeGeneric, "authentication failure")
_ = cs.CloseWithError(closeErrorCodeAuth, "auth error")
return
}
// Start accepting streams
var closeErr error
for {
stream, err := cs.AcceptStream(context.Background())
if err != nil {
closeErr = err
break
}
go s.handleStream(cs.LocalAddr(), cs.RemoteAddr(), username, stream)
}
s.clientDisconnectedFunc(cs.RemoteAddr(), username, closeErr)
_ = cs.CloseWithError(closeErrorCodeGeneric, "generic")
// Start accepting streams and messages
sc := newServerClient(cs, auth, s.disableUDP, s.aclEngine,
s.tcpRequestFunc, s.tcpErrorFunc, s.udpRequestFunc, s.udpErrorFunc)
sc.Run()
_ = cs.CloseWithError(closeErrorCodeGeneric, "")
}
// Auth & negotiate speed
func (s *Server) handleControlStream(cs quic.Session, stream quic.Stream) (string, bool, error) {
req, err := readClientAuthRequest(stream)
func (s *Server) handleControlStream(cs quic.Session, stream quic.Stream) ([]byte, bool, error) {
var ch clientHello
err := struc.Unpack(stream, &ch)
if err != nil {
return "", false, err
return nil, false, err
}
// Speed
if req.Speed == nil || req.Speed.SendBps == 0 || req.Speed.ReceiveBps == 0 {
return "", false, errors.New("incorrect speed provided by the client")
if ch.Rate.SendBPS == 0 || ch.Rate.RecvBPS == 0 {
return nil, false, errors.New("invalid rate from client")
}
serverSendBPS, serverReceiveBPS := req.Speed.ReceiveBps, req.Speed.SendBps
serverSendBPS, serverRecvBPS := ch.Rate.RecvBPS, ch.Rate.SendBPS
if s.sendBPS > 0 && serverSendBPS > s.sendBPS {
serverSendBPS = s.sendBPS
}
if s.recvBPS > 0 && serverReceiveBPS > s.recvBPS {
serverReceiveBPS = s.recvBPS
if s.recvBPS > 0 && serverRecvBPS > s.recvBPS {
serverRecvBPS = s.recvBPS
}
// Auth
if req.Credential == nil {
return "", false, errors.New("incorrect credential provided by the client")
}
authResult, msg := s.clientAuthFunc(cs.RemoteAddr(), req.Credential.Username, req.Credential.Password,
serverSendBPS, serverReceiveBPS)
ok, msg := s.authFunc(cs.RemoteAddr(), ch.Auth, serverSendBPS, serverRecvBPS)
// Response
err = writeServerAuthResponse(stream, &pb.ServerAuthResponse{
Result: pb.AuthResult(authResult),
Message: msg,
Speed: &pb.Speed{
SendBps: serverSendBPS,
ReceiveBps: serverReceiveBPS,
err = struc.Pack(stream, &serverHello{
OK: ok,
Rate: transmissionRate{
SendBPS: serverSendBPS,
RecvBPS: serverRecvBPS,
},
Message: msg,
})
if err != nil {
return "", false, err
return nil, false, err
}
// Set the congestion accordingly
if authResult == AuthResultSuccess && s.congestionFactory != nil {
cs.SetCongestion(s.congestionFactory(serverSendBPS))
if ok && s.congestionFactory != nil {
cs.SetCongestionControl(s.congestionFactory(serverSendBPS))
}
return req.Credential.Username, authResult == AuthResultSuccess, nil
}
func (s *Server) handleStream(localAddr net.Addr, remoteAddr net.Addr, username string, stream quic.Stream) {
defer stream.Close()
// Read request
req, err := readClientConnectRequest(stream)
if err != nil {
return
}
// Create connection with the handler
result, msg, conn := s.handleRequestFunc(remoteAddr, username, int(stream.StreamID()), ConnectionType(req.Type), req.Address)
defer func() {
if conn != nil {
_ = conn.Close()
}
}()
// Send response
err = writeServerConnectResponse(stream, &pb.ServerConnectResponse{
Result: pb.ConnectResult(result),
Message: msg,
})
if err != nil {
s.requestClosedFunc(remoteAddr, username, int(stream.StreamID()), ConnectionType(req.Type), req.Address, err)
return
}
if result != ConnectResultSuccess {
s.requestClosedFunc(remoteAddr, username, int(stream.StreamID()), ConnectionType(req.Type), req.Address,
fmt.Errorf("handler returned an unsuccessful state %d (msg: %s)", result, msg))
return
}
switch req.Type {
case pb.ConnectionType_Stream:
err = utils.PipePair(stream, conn, &s.outboundBytes, &s.inboundBytes)
case pb.ConnectionType_Packet:
err = utils.PipePair(&utils.PacketWrapperConn{Orig: &utils.QUICStreamWrapperConn{
Orig: stream,
PseudoLocalAddr: localAddr,
PseudoRemoteAddr: remoteAddr,
}}, conn, &s.outboundBytes, &s.inboundBytes)
default:
err = fmt.Errorf("unsupported connection type %s", req.Type.String())
}
s.requestClosedFunc(remoteAddr, username, int(stream.StreamID()), ConnectionType(req.Type), req.Address, err)
return ch.Auth, ok, nil
}

269
pkg/core/server_client.go Normal file
View File

@@ -0,0 +1,269 @@
package core
import (
"bytes"
"context"
"github.com/lucas-clemente/quic-go"
"github.com/lunixbochs/struc"
"github.com/tobyxdd/hysteria/pkg/acl"
"github.com/tobyxdd/hysteria/pkg/utils"
"net"
"sync"
)
const udpBufferSize = 65535
type serverClient struct {
CS quic.Session
Auth []byte
ClientAddr net.Addr
DisableUDP bool
ACLEngine *acl.Engine
CTCPRequestFunc TCPRequestFunc
CTCPErrorFunc TCPErrorFunc
CUDPRequestFunc UDPRequestFunc
CUDPErrorFunc UDPErrorFunc
udpSessionMutex sync.RWMutex
udpSessionMap map[uint32]*net.UDPConn
nextUDPSessionID uint32
}
func newServerClient(cs quic.Session, auth []byte, disableUDP bool, ACLEngine *acl.Engine,
CTCPRequestFunc TCPRequestFunc, CTCPErrorFunc TCPErrorFunc,
CUDPRequestFunc UDPRequestFunc, CUDPErrorFunc UDPErrorFunc) *serverClient {
return &serverClient{
CS: cs,
Auth: auth,
ClientAddr: cs.RemoteAddr(),
DisableUDP: disableUDP,
ACLEngine: ACLEngine,
CTCPRequestFunc: CTCPRequestFunc,
CTCPErrorFunc: CTCPErrorFunc,
CUDPRequestFunc: CUDPRequestFunc,
CUDPErrorFunc: CUDPErrorFunc,
udpSessionMap: make(map[uint32]*net.UDPConn),
}
}
func (c *serverClient) Run() {
if !c.DisableUDP {
go func() {
for {
msg, err := c.CS.ReceiveMessage()
if err != nil {
break
}
c.handleMessage(msg)
}
}()
}
for {
stream, err := c.CS.AcceptStream(context.Background())
if err != nil {
break
}
go c.handleStream(stream)
}
}
func (c *serverClient) handleStream(stream quic.Stream) {
defer stream.Close()
// Read request
var req clientRequest
err := struc.Unpack(stream, &req)
if err != nil {
return
}
if !req.UDP {
// TCP connection
c.handleTCP(stream, req.Address)
} 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
}
c.udpSessionMutex.RLock()
conn, ok := c.udpSessionMap[udpMsg.SessionID]
c.udpSessionMutex.RUnlock()
if ok {
// Session found, send the message
host, port, err := net.SplitHostPort(udpMsg.Address)
if err != nil {
return
}
action, arg := acl.ActionDirect, ""
if c.ACLEngine != nil {
ip := net.ParseIP(host)
if ip != nil {
// IP request, clear host for ACL engine
host = ""
}
action, arg = c.ACLEngine.Lookup(host, ip)
}
switch action {
case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side
addr, err := net.ResolveUDPAddr("udp", udpMsg.Address)
if err == nil {
_, _ = conn.WriteToUDP(udpMsg.Data, addr)
}
case acl.ActionBlock:
// Do nothing
case acl.ActionHijack:
hijackAddr := net.JoinHostPort(arg, port)
addr, err := net.ResolveUDPAddr("udp", hijackAddr)
if err == nil {
_, _ = conn.WriteToUDP(udpMsg.Data, addr)
}
default:
// Do nothing
}
}
}
func (c *serverClient) handleTCP(stream quic.Stream, reqAddr string) {
host, port, err := net.SplitHostPort(reqAddr)
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: "invalid address",
})
c.CTCPErrorFunc(c.ClientAddr, c.Auth, reqAddr, err)
return
}
action, arg := acl.ActionDirect, ""
if c.ACLEngine != nil {
ip := net.ParseIP(host)
if ip != nil {
// IP request, clear host for ACL engine
host = ""
}
action, arg = c.ACLEngine.Lookup(host, ip)
}
c.CTCPRequestFunc(c.ClientAddr, c.Auth, reqAddr, action, arg)
var conn net.Conn // Connection to be piped
switch action {
case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side
conn, err = net.DialTimeout("tcp", reqAddr, dialTimeout)
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: err.Error(),
})
c.CTCPErrorFunc(c.ClientAddr, c.Auth, reqAddr, err)
return
}
case acl.ActionBlock:
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: "blocked by ACL",
})
return
case acl.ActionHijack:
hijackAddr := net.JoinHostPort(arg, port)
conn, err = net.DialTimeout("tcp", hijackAddr, dialTimeout)
if err != nil {
_ = struc.Pack(stream, &serverResponse{
OK: false,
Message: err.Error(),
})
c.CTCPErrorFunc(c.ClientAddr, c.Auth, reqAddr, 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
}
err = utils.Pipe2Way(stream, conn)
c.CTCPErrorFunc(c.ClientAddr, c.Auth, reqAddr, 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 := net.ListenUDP("udp", nil)
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.ReadFromUDP(buf)
if n > 0 {
var msgBuf bytes.Buffer
_ = struc.Pack(&msgBuf, &udpMessage{
SessionID: id,
Address: rAddr.String(),
Data: buf[:n],
})
_ = c.CS.SendMessage(msgBuf.Bytes())
}
if err != nil {
break
}
}
}()
// 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,13 +0,0 @@
package core
import (
"encoding/binary"
"github.com/lucas-clemente/quic-go/congestion"
"time"
)
const controlStreamTimeout = 10 * time.Second
var controlProtocolEndian = binary.BigEndian
type CongestionFactory func(refBPS uint64) congestion.ExternalSendAlgorithm

View File

@@ -27,13 +27,13 @@ func NewProxyHTTPServer(hyClient *core.Client, idleTimeout time.Duration, aclEng
if err != nil {
return nil, err
}
ip := net.ParseIP(host)
if ip != nil {
host = ""
}
// ACL
action, arg := acl.ActionProxy, ""
if aclEngine != nil {
ip := net.ParseIP(host)
if ip != nil {
host = ""
}
action, arg = aclEngine.Lookup(host, ip)
}
newDialFunc(addr, action, arg)
@@ -42,9 +42,9 @@ func NewProxyHTTPServer(hyClient *core.Client, idleTimeout time.Duration, aclEng
case acl.ActionDirect:
return net.Dial(network, addr)
case acl.ActionProxy:
return hyClient.Dial(false, addr)
return hyClient.DialTCP(addr)
case acl.ActionBlock:
return nil, errors.New("blocked in ACL")
return nil, errors.New("blocked by ACL")
case acl.ActionHijack:
return net.Dial(network, net.JoinHostPort(arg, port))
default:
@@ -52,8 +52,7 @@ func NewProxyHTTPServer(hyClient *core.Client, idleTimeout time.Duration, aclEng
}
},
IdleConnTimeout: idleTimeout,
// TODO: Disable HTTP2 support? ref: https://github.com/elazarl/goproxy/issues/361
//TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
// Disable HTTP2 support? ref: https://github.com/elazarl/goproxy/issues/361
}
proxy.ConnectDial = nil
if basicAuthFunc != nil {

64
pkg/relay/relay.go Normal file
View File

@@ -0,0 +1,64 @@
package relay
import (
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/utils"
"net"
"time"
)
type Relay struct {
HyClient *core.Client
ListenAddr *net.TCPAddr
Remote string
Timeout time.Duration
ConnFunc func(addr net.Addr)
ErrorFunc func(addr net.Addr, err error)
tcpListener *net.TCPListener
}
func NewRelay(hyClient *core.Client, listen, remote string, timeout time.Duration,
connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error)) (*Relay, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &Relay{
HyClient: hyClient,
ListenAddr: tAddr,
Remote: remote,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *Relay) ListenAndServe() error {
var err error
r.tcpListener, err = net.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer r.tcpListener.Close()
for {
c, err := r.tcpListener.AcceptTCP()
if err != nil {
return err
}
go func(c *net.TCPConn) {
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)
}(c)
}
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/tobyxdd/hysteria/pkg/acl"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/utils"
"io"
"strconv"
)
@@ -17,40 +16,36 @@ import (
"time"
)
const udpBufferSize = 65535
var (
ErrUnsupportedCmd = errors.New("unsupported command")
ErrUserPassAuth = errors.New("invalid username or password")
)
type Server struct {
HyClient *core.Client
AuthFunc func(username, password string) bool
Method byte
TCPAddr *net.TCPAddr
TCPDeadline int
ACLEngine *acl.Engine
DisableUDP bool
HyClient *core.Client
AuthFunc func(username, password string) bool
Method byte
TCPAddr *net.TCPAddr
TCPTimeout time.Duration
ACLEngine *acl.Engine
DisableUDP bool
NewRequestFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string)
RequestClosedFunc func(addr net.Addr, reqAddr string, err error)
NewUDPAssociateFunc func(addr net.Addr)
UDPAssociateClosedFunc func(addr net.Addr, err error)
NewUDPTunnelFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string)
UDPTunnelClosedFunc func(addr net.Addr, reqAddr string, err error)
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 *core.Client, addr string, authFunc func(username, password string) bool, tcpDeadline int,
func NewServer(hyClient *core.Client, addr string, authFunc func(username, password string) bool, tcpTimeout time.Duration,
aclEngine *acl.Engine, disableUDP bool,
newReqFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string),
reqClosedFunc func(addr net.Addr, reqAddr string, err error),
newUDPAssociateFunc func(addr net.Addr),
udpAssociateClosedFunc func(addr net.Addr, err error),
newUDPTunnelFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string),
udpTunnelClosedFunc func(addr net.Addr, reqAddr string, err error)) (*Server, error) {
taddr, err := net.ResolveTCPAddr("tcp", addr)
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
}
@@ -59,19 +54,17 @@ func NewServer(hyClient *core.Client, addr string, authFunc func(username, passw
m = socks5.MethodUsernamePassword
}
s := &Server{
HyClient: hyClient,
AuthFunc: authFunc,
Method: m,
TCPAddr: taddr,
TCPDeadline: tcpDeadline,
ACLEngine: aclEngine,
DisableUDP: disableUDP,
NewRequestFunc: newReqFunc,
RequestClosedFunc: reqClosedFunc,
NewUDPAssociateFunc: newUDPAssociateFunc,
UDPAssociateClosedFunc: udpAssociateClosedFunc,
NewUDPTunnelFunc: newUDPTunnelFunc,
UDPTunnelClosedFunc: udpTunnelClosedFunc,
HyClient: hyClient,
AuthFunc: authFunc,
Method: m,
TCPAddr: tAddr,
TCPTimeout: tcpTimeout,
ACLEngine: aclEngine,
DisableUDP: disableUDP,
TCPRequestFunc: tcpReqFunc,
TCPErrorFunc: tcpErrorFunc,
UDPAssociateFunc: udpAssocFunc,
UDPErrorFunc: udpErrorFunc,
}
return s, nil
}
@@ -133,8 +126,8 @@ func (s *Server) ListenAndServe() error {
}
go func(c *net.TCPConn) {
defer c.Close()
if s.TCPDeadline != 0 {
if err := c.SetDeadline(time.Now().Add(time.Duration(s.TCPDeadline) * time.Second)); err != nil {
if s.TCPTimeout != 0 {
if err := c.SetDeadline(time.Now().Add(s.TCPTimeout)); err != nil {
return
}
}
@@ -174,10 +167,10 @@ func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
if s.ACLEngine != nil {
action, arg = s.ACLEngine.Lookup(domain, ip)
}
s.NewRequestFunc(c.RemoteAddr(), addr, action, arg)
s.TCPRequestFunc(c.RemoteAddr(), addr, action, arg)
var closeErr error
defer func() {
s.RequestClosedFunc(c.RemoteAddr(), addr, closeErr)
s.TCPErrorFunc(c.RemoteAddr(), addr, closeErr)
}()
// Handle according to the action
switch action {
@@ -190,10 +183,10 @@ func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = pipePair(c, rc, s.TCPDeadline)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
case acl.ActionProxy:
rc, err := s.HyClient.Dial(false, addr)
rc, err := s.HyClient.DialTCP(addr)
if err != nil {
_ = sendReply(c, socks5.RepHostUnreachable)
closeErr = err
@@ -201,7 +194,7 @@ func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = pipePair(c, rc, s.TCPDeadline)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
case acl.ActionBlock:
_ = sendReply(c, socks5.RepHostUnreachable)
@@ -216,7 +209,7 @@ func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
}
defer rc.Close()
_ = sendReply(c, socks5.RepSuccess)
closeErr = pipePair(c, rc, s.TCPDeadline)
closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout)
return nil
default:
_ = sendReply(c, socks5.RepServerFailure)
@@ -226,11 +219,12 @@ func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error {
}
func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error {
s.NewUDPAssociateFunc(c.RemoteAddr())
s.UDPAssociateFunc(c.RemoteAddr())
var closeErr error
defer func() {
s.UDPAssociateClosedFunc(c.RemoteAddr(), closeErr)
s.UDPErrorFunc(c.RemoteAddr(), closeErr)
}()
// Start local UDP server
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: s.TCPAddr.IP,
Zone: s.TCPAddr.Zone,
@@ -241,6 +235,25 @@ func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error {
return err
}
defer udpConn.Close()
// Local UDP relay conn for ACL Direct
var localRelayConn *net.UDPConn
if s.ACLEngine != nil {
localRelayConn, err = net.ListenUDP("udp", nil)
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
atyp, addr, port, err := socks5.ParseAddress(udpConn.LocalAddr().String())
if err != nil {
@@ -250,11 +263,11 @@ func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error {
}
_, _ = 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)
go s.udpServer(udpConn, localRelayConn, hyUDP)
buf := make([]byte, 1024)
for {
if s.TCPDeadline != 0 {
_ = c.SetDeadline(time.Now().Add(time.Duration(s.TCPDeadline) * time.Second))
if s.TCPTimeout != 0 {
_ = c.SetDeadline(time.Now().Add(s.TCPTimeout))
}
_, err := c.Read(buf)
if err != nil {
@@ -262,20 +275,17 @@ func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error {
break
}
}
// As the TCP connection closes, so does the UDP listener
// As the TCP connection closes, so does the UDP server & HyClient session
return nil
}
func (s *Server) udpServer(c *net.UDPConn) {
func (s *Server) udpServer(clientConn *net.UDPConn, localRelayConn *net.UDPConn, hyUDP core.UDPConn) {
var clientAddr *net.UDPAddr
remoteMap := make(map[string]io.ReadWriteCloser) // Remote addr <-> Remote conn
buf := make([]byte, utils.PipeBufferSize)
var closeErr error
buf := make([]byte, udpBufferSize)
// Local to remote
for {
n, caddr, err := c.ReadFromUDP(buf)
n, cAddr, err := clientConn.ReadFromUDP(buf)
if err != nil {
closeErr = err
break
}
d, err := socks5.NewDatagramFromBytes(buf[:n])
@@ -284,71 +294,67 @@ func (s *Server) udpServer(c *net.UDPConn) {
continue
}
if clientAddr == nil {
// Whoever sends the first valid packet is our client :P
clientAddr = caddr
} else if caddr.String() != clientAddr.String() {
// We already have a client and you're not it!
// Whoever sends the first valid packet is our client
clientAddr = cAddr
// Start remote to local
go func() {
for {
bs, _, err := hyUDP.ReadFrom()
if err != nil {
break
}
// RFC 1928 is very ambiguous on how to properly use DST.ADDR and DST.PORT in reply packets
// So we just fill in zeros for now. Works fine for all the SOCKS5 clients I tested
d := socks5.NewDatagram(socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00}, bs)
_, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr)
}
}()
if localRelayConn != nil {
go func() {
buf := make([]byte, udpBufferSize)
for {
n, _, err := localRelayConn.ReadFrom(buf)
if n > 0 {
d := socks5.NewDatagram(socks5.ATYPIPv4,
[]byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00}, buf[:n])
_, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr)
}
if err != nil {
break
}
}
}()
}
} else if cAddr.String() != clientAddr.String() {
// Not our client, bye
continue
}
domain, ip, port, addr := parseDatagramRequestAddress(d)
rc := remoteMap[addr]
if rc == nil {
// Need a new entry
action, arg := acl.ActionProxy, ""
if s.ACLEngine != nil {
action, arg = s.ACLEngine.Lookup(domain, ip)
}
s.NewUDPTunnelFunc(clientAddr, addr, action, arg)
// Handle according to the action
switch action {
case acl.ActionDirect:
rc, err = net.Dial("udp", addr)
if err != nil {
s.UDPTunnelClosedFunc(clientAddr, addr, err)
continue
}
// The other direction
go udpReversePipe(clientAddr, c, rc)
remoteMap[addr] = rc
case acl.ActionProxy:
rc, err = s.HyClient.Dial(true, addr)
if err != nil {
s.UDPTunnelClosedFunc(clientAddr, addr, err)
continue
}
// The other direction
go udpReversePipe(clientAddr, c, rc)
remoteMap[addr] = rc
case acl.ActionBlock:
s.UDPTunnelClosedFunc(clientAddr, addr, errors.New("blocked in ACL"))
continue
case acl.ActionHijack:
rc, err = net.Dial("udp", net.JoinHostPort(arg, port))
if err != nil {
s.UDPTunnelClosedFunc(clientAddr, addr, err)
continue
}
// The other direction
go udpReversePipe(clientAddr, c, rc)
remoteMap[addr] = rc
default:
s.UDPTunnelClosedFunc(clientAddr, addr, fmt.Errorf("unknown action %d", action))
continue
}
action, arg := acl.ActionProxy, ""
if s.ACLEngine != nil && localRelayConn != nil {
action, arg = s.ACLEngine.Lookup(domain, ip)
}
_, err = rc.Write(d.Data)
if err != nil {
// The connection is no longer valid, close & remove from map
_ = rc.Close()
delete(remoteMap, addr)
s.UDPTunnelClosedFunc(clientAddr, addr, err)
// Handle according to the action
switch action {
case acl.ActionDirect:
rAddr, err := net.ResolveUDPAddr("udp", addr)
if err == nil {
_, _ = localRelayConn.WriteToUDP(d.Data, rAddr)
}
case acl.ActionProxy:
_ = hyUDP.WriteTo(d.Data, addr)
case acl.ActionBlock:
// Do nothing
case acl.ActionHijack:
hijackAddr := net.JoinHostPort(arg, port)
rAddr, err := net.ResolveUDPAddr("udp", hijackAddr)
if err == nil {
_, _ = localRelayConn.WriteToUDP(d.Data, rAddr)
}
default:
// Do nothing
}
}
// Close all remote connections
for raddr, rc := range remoteMap {
_ = rc.Close()
s.UDPTunnelClosedFunc(clientAddr, raddr, closeErr)
}
}
func sendReply(conn *net.TCPConn, rep byte) error {
@@ -376,48 +382,3 @@ func parseDatagramRequestAddress(r *socks5.Datagram) (domain string, ip net.IP,
return "", r.DstAddr, p, net.JoinHostPort(net.IP(r.DstAddr).String(), p)
}
}
func pipePair(conn *net.TCPConn, stream io.ReadWriteCloser, deadline int) error {
errChan := make(chan error, 2)
// TCP to stream
go func() {
buf := make([]byte, utils.PipeBufferSize)
for {
if deadline != 0 {
_ = conn.SetDeadline(time.Now().Add(time.Duration(deadline) * time.Second))
}
rn, err := conn.Read(buf)
if rn > 0 {
_, err := stream.Write(buf[:rn])
if err != nil {
errChan <- err
return
}
}
if err != nil {
errChan <- err
return
}
}
}()
// Stream to TCP
go func() {
errChan <- utils.Pipe(stream, conn, nil)
}()
return <-errChan
}
func udpReversePipe(clientAddr *net.UDPAddr, c *net.UDPConn, rc io.ReadWriteCloser) {
buf := make([]byte, utils.PipeBufferSize)
for {
n, err := rc.Read(buf)
if err != nil {
break
}
d := socks5.NewDatagram(socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00}, buf[:n])
_, err = c.WriteTo(d.Bytes(), clientAddr)
if err != nil {
break
}
}
}

View File

@@ -1,96 +0,0 @@
package utils
import (
"encoding/binary"
"fmt"
"github.com/lucas-clemente/quic-go"
"io"
"net"
"time"
)
type PacketWrapperConn struct {
Orig net.Conn
}
func (w *PacketWrapperConn) Read(b []byte) (n int, err error) {
var sz uint32
if err := binary.Read(w.Orig, binary.BigEndian, &sz); err != nil {
return 0, err
}
if int(sz) <= len(b) {
return io.ReadFull(w.Orig, b[:sz])
} else {
return 0, fmt.Errorf("the buffer is too small to hold %d bytes of packet data", sz)
}
}
func (w *PacketWrapperConn) Write(b []byte) (n int, err error) {
sz := uint32(len(b))
if err := binary.Write(w.Orig, binary.BigEndian, &sz); err != nil {
return 0, err
}
return w.Orig.Write(b)
}
func (w *PacketWrapperConn) Close() error {
return w.Orig.Close()
}
func (w *PacketWrapperConn) LocalAddr() net.Addr {
return w.Orig.LocalAddr()
}
func (w *PacketWrapperConn) RemoteAddr() net.Addr {
return w.Orig.RemoteAddr()
}
func (w *PacketWrapperConn) SetDeadline(t time.Time) error {
return w.Orig.SetDeadline(t)
}
func (w *PacketWrapperConn) SetReadDeadline(t time.Time) error {
return w.Orig.SetReadDeadline(t)
}
func (w *PacketWrapperConn) SetWriteDeadline(t time.Time) error {
return w.Orig.SetWriteDeadline(t)
}
type QUICStreamWrapperConn struct {
Orig quic.Stream
PseudoLocalAddr net.Addr
PseudoRemoteAddr net.Addr
}
func (w *QUICStreamWrapperConn) Read(b []byte) (n int, err error) {
return w.Orig.Read(b)
}
func (w *QUICStreamWrapperConn) Write(b []byte) (n int, err error) {
return w.Orig.Write(b)
}
func (w *QUICStreamWrapperConn) Close() error {
return w.Orig.Close()
}
func (w *QUICStreamWrapperConn) LocalAddr() net.Addr {
return w.PseudoLocalAddr
}
func (w *QUICStreamWrapperConn) RemoteAddr() net.Addr {
return w.PseudoRemoteAddr
}
func (w *QUICStreamWrapperConn) SetDeadline(t time.Time) error {
return w.Orig.SetDeadline(t)
}
func (w *QUICStreamWrapperConn) SetReadDeadline(t time.Time) error {
return w.Orig.SetReadDeadline(t)
}
func (w *QUICStreamWrapperConn) SetWriteDeadline(t time.Time) error {
return w.Orig.SetWriteDeadline(t)
}

View File

@@ -2,20 +2,18 @@ package utils
import (
"io"
"sync/atomic"
"net"
"time"
)
const PipeBufferSize = 65536
func Pipe(src, dst io.ReadWriter, atomicCounter *uint64) error {
func Pipe(src, dst io.ReadWriter) error {
buf := make([]byte, PipeBufferSize)
for {
rn, err := src.Read(buf)
if rn > 0 {
wn, err := dst.Write(buf[:rn])
if atomicCounter != nil {
atomic.AddUint64(atomicCounter, uint64(wn))
}
_, err := dst.Write(buf[:rn])
if err != nil {
return err
}
@@ -26,14 +24,61 @@ func Pipe(src, dst io.ReadWriter, atomicCounter *uint64) error {
}
}
func PipePair(rw1, rw2 io.ReadWriter, rw1WriteCounter, rw2WriteCounter *uint64) error {
func Pipe2Way(rw1, rw2 io.ReadWriter) error {
errChan := make(chan error, 2)
go func() {
errChan <- Pipe(rw2, rw1, rw1WriteCounter)
errChan <- Pipe(rw2, rw1)
}()
go func() {
errChan <- Pipe(rw1, rw2, rw2WriteCounter)
errChan <- Pipe(rw1, rw2)
}()
// We only need the first error
return <-errChan
}
func PipePairWithTimeout(conn *net.TCPConn, stream io.ReadWriteCloser, timeout time.Duration) error {
errChan := make(chan error, 2)
// TCP to stream
go func() {
buf := make([]byte, PipeBufferSize)
for {
if timeout != 0 {
_ = conn.SetDeadline(time.Now().Add(timeout))
}
rn, err := conn.Read(buf)
if rn > 0 {
_, err := stream.Write(buf[:rn])
if err != nil {
errChan <- err
return
}
}
if err != nil {
errChan <- err
return
}
}
}()
// Stream to TCP
go func() {
buf := make([]byte, PipeBufferSize)
for {
rn, err := stream.Read(buf)
if rn > 0 {
_, err := conn.Write(buf[:rn])
if err != nil {
errChan <- err
return
}
if timeout != 0 {
_ = conn.SetDeadline(time.Now().Add(timeout))
}
}
if err != nil {
errChan <- err
return
}
}
}()
return <-errChan
}