From 9f54aade8fddd3bfdfe2a8b0071996e200440ac1 Mon Sep 17 00:00:00 2001 From: tobyxdd Date: Thu, 25 May 2023 20:24:24 -0700 Subject: [PATCH] hysteria 2 prototype first public release --- .github/FUNDING.yml | 1 - .../ISSUE_TEMPLATE/en_feature_request.yaml | 42 - .github/ISSUE_TEMPLATE/en_newbie.yml | 136 --- .../ISSUE_TEMPLATE/zh_feature_request.yaml | 43 - .github/ISSUE_TEMPLATE/zh_newbie.yml | 137 --- .github/dependabot.yml | 10 - .github/workflows/codeql-analysis.yml | 60 - .github/workflows/dev-build-master.yml | 40 - .github/workflows/release-docker.yaml | 44 - .github/workflows/release.yml | 65 -- .gitignore | 113 +- CHANGELOG.md | 148 --- Dockerfile | 43 - LICENSE.md | 30 - README.md | 100 +- Taskfile.yaml | 362 ------ app/auth/external.go | 97 -- app/auth/funcs.go | 59 - app/client.example.yaml | 29 + app/cmd/acme.go | 74 -- app/cmd/client.go | 640 ++++------- app/cmd/client_gpl.go | 117 -- app/cmd/client_nongpl.go | 36 - app/cmd/completion.go | 67 -- app/cmd/config.go | 385 ------- app/cmd/config_test.go | 34 - app/cmd/ipmasker.go | 43 - app/cmd/kploader.go | 95 -- app/cmd/main.go | 208 ---- app/cmd/mmdb.go | 49 - app/cmd/ping.go | 56 + app/cmd/prom.go | 71 -- app/cmd/resolver.go | 123 -- app/cmd/root.go | 79 ++ app/cmd/server.go | 536 +++++---- app/cmd/update.go | 49 - app/cmd/utils.go | 35 + app/go.mod | 79 +- app/go.sum | 249 +--- app/http/server.go | 91 -- app/internal/socks5/server.go | 294 +++++ app/main.go | 7 + .../check.py => app/misc/socks5_test.py | 21 +- app/redirect/getsockopt_linux.go | 17 - app/redirect/getsockopt_linux_386.go | 22 - app/redirect/origdst_linux.go | 33 - app/redirect/syscall_socketcall_linux_386.s | 7 - app/redirect/tcp_linux.go | 97 -- app/redirect/tcp_stub.go | 25 - app/relay/tcp.go | 63 -- app/relay/udp.go | 124 -- app/server.example.yaml | 35 + app/socks5/server.go | 442 -------- app/tproxy/tcp_linux.go | 66 -- app/tproxy/tcp_stub.go | 25 - app/tproxy/udp_linux.go | 115 -- app/tproxy/udp_stub.go | 26 - app/tun/server.go | 156 --- app/tun/tcp.go | 48 - app/tun/udp.go | 114 -- build.ps1 | 113 -- build.sh | 130 --- core/acl/engine.go | 143 --- core/acl/engine_test.go | 155 --- core/acl/entry.go | 334 ------ core/acl/entry_test.go | 93 -- core/client/client.go | 439 +++++++ core/client/config.go | 107 ++ core/client/reconnect.go | 68 ++ core/cs/client.go | 447 -------- core/cs/frag.go | 67 -- core/cs/frag_test.go | 390 ------- core/cs/protocol.go | 79 -- core/cs/server.go | 178 --- core/cs/server_client.go | 391 ------- core/cs/stream.go | 58 - core/errors/errors.go | 58 + core/go.mod | 34 +- core/go.sum | 75 +- core/{ => internal}/congestion/brutal.go | 0 core/{ => internal}/congestion/pacer.go | 0 core/internal/frag/frag.go | 77 ++ core/internal/frag/frag_test.go | 336 ++++++ core/internal/integration_tests/close_test.go | 191 ++++ core/internal/integration_tests/masq_test.go | 130 +++ core/internal/integration_tests/smoke_test.go | 229 ++++ .../internal/integration_tests/stress_test.go | 291 +++++ core/internal/integration_tests/test.crt | 23 + core/internal/integration_tests/test.key | 27 + core/internal/integration_tests/utils_test.go | 250 ++++ core/internal/pmtud/avail.go | 7 + core/internal/pmtud/unavail.go | 12 + core/internal/protocol/http.go | 36 + core/internal/protocol/proxy.go | 261 +++++ core/internal/protocol/proxy_test.go | 557 +++++++++ core/internal/utils/qstream.go | 62 + core/pktconns/faketcp/LICENSE | 1 - core/pktconns/faketcp/obfs.go | 95 -- core/pktconns/faketcp/tcp_linux.go | 616 ---------- core/pktconns/faketcp/tcp_stub.go | 21 - core/pktconns/faketcp/tcp_test.go | 198 ---- core/pktconns/funcs.go | 189 ---- core/pktconns/obfs/obfs.go | 58 - core/pktconns/obfs/obfs_test.go | 31 - core/pktconns/udp/hop.go | 353 ------ core/pktconns/udp/hop_test.go | 102 -- core/pktconns/udp/obfs.go | 100 -- core/pktconns/wechat/obfs.go | 127 --- core/pmtud/avail.go | 14 - core/pmtud/unavail.go | 8 - core/server/config.go | 172 +++ core/server/server.go | 386 +++++++ core/sockopt/sockopt.go | 23 - core/sockopt/sockopt_linux.go | 22 - core/sockopt/sockopt_others.go | 13 - core/transport/client.go | 34 - core/transport/resolve.go | 98 -- core/transport/server.go | 128 --- core/transport/socks5.go | 279 ----- core/utils/misc.go | 42 - core/utils/pipe.go | 94 -- docker-compose.yaml | 10 - docs/bench/bench.png | Bin 12521 -> 0 bytes docs/logos/AperNetLogo.png | Bin 5177 -> 0 bytes docs/logos/readme.png | Bin 11979 -> 0 bytes docs/logos/transparent_black.png | Bin 106300 -> 0 bytes docs/logos/transparent_pink.png | Bin 115442 -> 0 bytes docs/logos/whitebg_black.png | Bin 110656 -> 0 bytes docs/logos/whitebg_pink.png | Bin 109492 -> 0 bytes extras/auth/password.go | 22 + extras/go.mod | 27 + extras/go.sum | 73 ++ extras/utils/bpsconv.go | 52 + extras/utils/bpsconv_test.go | 40 + go.work | 1 + go.work.sum | 33 +- install_server.sh | 1006 ----------------- tag.ps1 | 35 - tag.sh | 40 - 139 files changed, 5146 insertions(+), 11657 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/ISSUE_TEMPLATE/en_feature_request.yaml delete mode 100644 .github/ISSUE_TEMPLATE/en_newbie.yml delete mode 100644 .github/ISSUE_TEMPLATE/zh_feature_request.yaml delete mode 100644 .github/ISSUE_TEMPLATE/zh_newbie.yml delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/dev-build-master.yml delete mode 100644 .github/workflows/release-docker.yaml delete mode 100644 .github/workflows/release.yml delete mode 100644 CHANGELOG.md delete mode 100644 Dockerfile delete mode 100644 LICENSE.md delete mode 100644 Taskfile.yaml delete mode 100644 app/auth/external.go delete mode 100644 app/auth/funcs.go create mode 100644 app/client.example.yaml delete mode 100644 app/cmd/acme.go delete mode 100644 app/cmd/client_gpl.go delete mode 100644 app/cmd/client_nongpl.go delete mode 100644 app/cmd/completion.go delete mode 100644 app/cmd/config.go delete mode 100644 app/cmd/config_test.go delete mode 100644 app/cmd/ipmasker.go delete mode 100644 app/cmd/kploader.go delete mode 100644 app/cmd/main.go delete mode 100644 app/cmd/mmdb.go create mode 100644 app/cmd/ping.go delete mode 100644 app/cmd/prom.go delete mode 100644 app/cmd/resolver.go create mode 100644 app/cmd/root.go delete mode 100644 app/cmd/update.go create mode 100644 app/cmd/utils.go delete mode 100644 app/http/server.go create mode 100644 app/internal/socks5/server.go create mode 100644 app/main.go rename docs/socks5/check.py => app/misc/socks5_test.py (79%) delete mode 100644 app/redirect/getsockopt_linux.go delete mode 100644 app/redirect/getsockopt_linux_386.go delete mode 100644 app/redirect/origdst_linux.go delete mode 100644 app/redirect/syscall_socketcall_linux_386.s delete mode 100644 app/redirect/tcp_linux.go delete mode 100644 app/redirect/tcp_stub.go delete mode 100644 app/relay/tcp.go delete mode 100644 app/relay/udp.go create mode 100644 app/server.example.yaml delete mode 100644 app/socks5/server.go delete mode 100644 app/tproxy/tcp_linux.go delete mode 100644 app/tproxy/tcp_stub.go delete mode 100644 app/tproxy/udp_linux.go delete mode 100644 app/tproxy/udp_stub.go delete mode 100644 app/tun/server.go delete mode 100644 app/tun/tcp.go delete mode 100644 app/tun/udp.go delete mode 100644 build.ps1 delete mode 100755 build.sh delete mode 100644 core/acl/engine.go delete mode 100644 core/acl/engine_test.go delete mode 100644 core/acl/entry.go delete mode 100644 core/acl/entry_test.go create mode 100644 core/client/client.go create mode 100644 core/client/config.go create mode 100644 core/client/reconnect.go delete mode 100644 core/cs/client.go delete mode 100644 core/cs/frag.go delete mode 100644 core/cs/frag_test.go delete mode 100644 core/cs/protocol.go delete mode 100644 core/cs/server.go delete mode 100644 core/cs/server_client.go delete mode 100644 core/cs/stream.go create mode 100644 core/errors/errors.go rename core/{ => internal}/congestion/brutal.go (100%) rename core/{ => internal}/congestion/pacer.go (100%) create mode 100644 core/internal/frag/frag.go create mode 100644 core/internal/frag/frag_test.go create mode 100644 core/internal/integration_tests/close_test.go create mode 100644 core/internal/integration_tests/masq_test.go create mode 100644 core/internal/integration_tests/smoke_test.go create mode 100644 core/internal/integration_tests/stress_test.go create mode 100644 core/internal/integration_tests/test.crt create mode 100644 core/internal/integration_tests/test.key create mode 100644 core/internal/integration_tests/utils_test.go create mode 100644 core/internal/pmtud/avail.go create mode 100644 core/internal/pmtud/unavail.go create mode 100644 core/internal/protocol/http.go create mode 100644 core/internal/protocol/proxy.go create mode 100644 core/internal/protocol/proxy_test.go create mode 100644 core/internal/utils/qstream.go delete mode 100644 core/pktconns/faketcp/LICENSE delete mode 100644 core/pktconns/faketcp/obfs.go delete mode 100644 core/pktconns/faketcp/tcp_linux.go delete mode 100644 core/pktconns/faketcp/tcp_stub.go delete mode 100644 core/pktconns/faketcp/tcp_test.go delete mode 100644 core/pktconns/funcs.go delete mode 100644 core/pktconns/obfs/obfs.go delete mode 100644 core/pktconns/obfs/obfs_test.go delete mode 100644 core/pktconns/udp/hop.go delete mode 100644 core/pktconns/udp/hop_test.go delete mode 100644 core/pktconns/udp/obfs.go delete mode 100644 core/pktconns/wechat/obfs.go delete mode 100644 core/pmtud/avail.go delete mode 100644 core/pmtud/unavail.go create mode 100644 core/server/config.go create mode 100644 core/server/server.go delete mode 100644 core/sockopt/sockopt.go delete mode 100644 core/sockopt/sockopt_linux.go delete mode 100644 core/sockopt/sockopt_others.go delete mode 100644 core/transport/client.go delete mode 100644 core/transport/resolve.go delete mode 100644 core/transport/server.go delete mode 100644 core/transport/socks5.go delete mode 100644 core/utils/misc.go delete mode 100644 core/utils/pipe.go delete mode 100644 docker-compose.yaml delete mode 100644 docs/bench/bench.png delete mode 100644 docs/logos/AperNetLogo.png delete mode 100644 docs/logos/readme.png delete mode 100644 docs/logos/transparent_black.png delete mode 100644 docs/logos/transparent_pink.png delete mode 100644 docs/logos/whitebg_black.png delete mode 100644 docs/logos/whitebg_pink.png create mode 100644 extras/auth/password.go create mode 100644 extras/go.mod create mode 100644 extras/go.sum create mode 100644 extras/utils/bpsconv.go create mode 100644 extras/utils/bpsconv_test.go delete mode 100755 install_server.sh delete mode 100644 tag.ps1 delete mode 100644 tag.sh diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index a0c79dd..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -custom: ['https://hysteria.network/docs/donations/'] diff --git a/.github/ISSUE_TEMPLATE/en_feature_request.yaml b/.github/ISSUE_TEMPLATE/en_feature_request.yaml deleted file mode 100644 index eed08ac..0000000 --- a/.github/ISSUE_TEMPLATE/en_feature_request.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: "[en] Feature Request" -description: "Request to add a new feature, or improvement to an existing feature." -title: "[Feature Request] " -body: - - type: markdown - id: header - attributes: - value: | - Before creating an issue, please take a look at [Advanced Usage](https://hysteria.network/docs/advanced-usage/) & existing issues to make sure it does not exist or has already been proposed. - - You can also join our Telegram group or use Discussion to share your ideas with the community. - - If you have the skills to implement the features you want, Pull Requests are more than welcomed :) - - type: textarea - id: detail - attributes: - label: "Details" - description: | - Describe what you want to add or change. - validations: - required: true - - type: textarea - id: necessary - attributes: - label: "Value" - description: | - What is the value added? - validations: - required: true - - type: textarea - id: alternative - attributes: - label: "Available alternatives" - description: | - Are there other projects that have implemented this feature that we can refer to? - - type: textarea - id: other-info - attributes: - label: "Additional information" - description: | - Links to any relevant issues, pull requests, or discussions. - diff --git a/.github/ISSUE_TEMPLATE/en_newbie.yml b/.github/ISSUE_TEMPLATE/en_newbie.yml deleted file mode 100644 index ca41537..0000000 --- a/.github/ISSUE_TEMPLATE/en_newbie.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: "[en] Help me!" -description: "Unable to connect? Server/client crashed? Choose this to get help." -title: "[Help me] " -body: - - type: markdown - id: header - attributes: - value: | - Before creating an issue, please take a look at [Quick Start Guide](https://hysteria.network/docs/quick-start/) and [Advanced Usage](https://hysteria.network/docs/advanced-usage/). - - You can find solutions to common problems in [Common Problems](https://hysteria.network/docs/common-problems/). Anything already covered there will be closed without reply. - - You can [join our Telegram group](https://t.me/hysteria_github) our use Discussion for community support. - - Try searching existing issues to see if it has been already answered. - - If your problem still can't be solved, fill out the form as detailed as you can to help us reproduce it. - - type: textarea - id: detail - attributes: - label: "Details" - description: | - Describe the problem you encountered in detail. - - If you are using hysteria in an unusual way, describe your setup and what you are trying to achieve. - validations: - required: true - - type: input - id: server-install-info - attributes: - label: "Hysteria server information" - description: | - Paste the version of hysteria server here (output of `hysteria --version`). - If you used a script to install and config hysteria on your server, please paste the command that you executed here (such as `curl https://xxx | sh -`) - - If you are using a VPN provider, please ask the VPN provider for help first. - placeholder: | - hysteria version v1.x.x 2006-01-02t08:04:05z 0123456789abcdef0123456789abcdef01234567 - validations: - required: true - - type: textarea - id: server-provider-info - attributes: - label: "VPS information" - description: | - Fill in the provider and specs of the VPS you are using to run the hysteria server here. - - If you are using a VPN provider, please fill in the website of the VPN provider. - placeholder: | - TurtleShell, Chuncheon, ARM, 1 Core, 512MB RAM - validations: - required: true - - type: textarea - id: server-config-info - attributes: - label: "Server config" - description: | - Paste the server config.json you are using here. - If you are using a script that doesn't require any configuration, please specify `N/A`. - placeholder: | - { - "listen": ":36712", - "acme": { - "domains": [ - "your.domain.com" - ], - "email": "hacker@gmail.com" - }, - "obfs": "fuck me till the daylight", - "up_mbps": 100, - "down_mbps": 100 - } - validations: - required: true - - type: textarea - id: server-log - attributes: - label: "Server logs" - description: | - Paste the hysteria server output here. Screenshots are acceptable but plaintext would be much better. - validations: - required: true - - type: input - id: client-install-info - attributes: - label: "Hysteria client information" - description: | - Paste the version of hysteria client here (output of `hysteria --version`). - If you are using any third-party clients (e.g. Clash, Passwall, or SagerNet), paste their version instead. You can also find help in their communities. - placeholder: | - hysteria version v1.x.x 2006-01-02T08:04:05Z 0123456789abcdef0123456789abcdef01234567 - validations: - required: true - - type: textarea - id: client-config-info - attributes: - label: "Client config" - description: | - Paste the client config.json you are using here. - Make sure to remove sensitive information (e.g. server address, password). - If you are using a third-party client, you can paste or upload a screenshot of their configuration instead. - placeholder: | - { - "server": "example.com:36712", - "obfs": "fuck me till the daylight", - "up_mbps": 10, - "down_mbps": 50, - "socks5": { - "listen": "127.0.0.1:1080" - }, - "http": { - "listen": "127.0.0.1:8080" - } - } - validations: - required: true - - type: input - id: client-environment - attributes: - label: "Client environment (operating system)" - description: | - The OS you are using to run hysteria client. - If you are running hysteria client on OpenWRT, provide the version of OpenWRT (and any plugins you are using, e.g. Passwall). - placeholder: | - Windows 11 - validations: - required: true - - type: textarea - id: client-log - attributes: - label: "Client logs" - description: | - Paste the hysteria client output here. Screenshots are acceptable but plaintext would be much better. - validations: - required: true - diff --git a/.github/ISSUE_TEMPLATE/zh_feature_request.yaml b/.github/ISSUE_TEMPLATE/zh_feature_request.yaml deleted file mode 100644 index b5fc1ef..0000000 --- a/.github/ISSUE_TEMPLATE/zh_feature_request.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: "[zh] 功能请求" -description: "希望 Hysteria 添加新功能?或者希望 Hysteria 作出什么改变? 请选这个。" -title: "[功能请求] " -body: - - type: markdown - id: header - attributes: - value: | - 在创建 Issue 之前, 请花几分钟阅读一下我们 Wiki 上的 [高级用法](https://hysteria.network/zh/docs/advanced-usage/)。 确认你想要的功能是否已经被实现。 - - 如果你有什么好的想法, 欢迎 [加入 Hysteria 的 Telegram 群组](https://t.me/hysteria_github) 参与功能上的讨论。 - - 也请搜索一下已有 Issue, 检查一下你所需的功能有没有人曾经提出过。 - - 如果你有能力实现这个功能, 欢迎为 Hysteria 提交 Pull Request。 - - type: textarea - id: detail - attributes: - label: "功能描述" - description: | - 请描述你希望 Hysteria 增加的功能或者希望 Hysteria 能作出的变更。 - validations: - required: true - - type: textarea - id: necessary - attributes: - label: "这个功能的必要性" - description: | - 为什么这个功能对 Hysteria 来说是必须的? 或者为什么你认为这个功能需要内置在 Hysteria 中? - validations: - required: true - - type: textarea - id: alternative - attributes: - label: "当前可用的替代方案" - description: | - 在当前没有这个功能的前提下, 你使用什么方案来达到类似的效果? - - type: textarea - id: other-info - attributes: - label: "补充" - description: | - 如果有任何涉及到这个功能请求的 Issue、 Pull Request、 博客文章等, 请把链接贴在下面。 diff --git a/.github/ISSUE_TEMPLATE/zh_newbie.yml b/.github/ISSUE_TEMPLATE/zh_newbie.yml deleted file mode 100644 index 274cb8d..0000000 --- a/.github/ISSUE_TEMPLATE/zh_newbie.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: "[zh] 请求帮助" -description: "不会用?连不上?请选这个。" -title: "[请求帮助] " -body: - - type: markdown - id: header - attributes: - value: | - 在创建 Issue 之前, 请花几分钟阅读一下我们 Wiki 上的 [配置指南](https://hysteria.network/zh/docs/quick-start/), - - 最新的配置参数在 [高级用法](https://hysteria.network/zh/docs/advanced-usage/) 里有详细的说明。 - - 您可能遇到的绝大部分问题都能在 [常见问题](https://hysteria.network/zh/docs/common-problems/) 中找到解决方案。 - - 任何已有解决方案的 Issue 将会被直接关闭, 感谢理解。 - - 请考虑 [加入 Hysteria 的 Telegram 群组](https://t.me/hysteria_github) 来寻求即时的社区帮助。 - - 也请搜索一下已有 Issue, 看看是否能找到现成的解决方案。 - - 请尽可能详细地填写下面这个表单来帮助我们检查并复现您遇到的问题, 请记住我们不会预测魔法, 只有复现了您遇到的问题, 我们才知道该如何帮助你解决它。 - - type: textarea - id: detail - attributes: - label: "问题详情" - description: | - 请描述你遇到的问题。 - 如果你的需求和通常的用法有所不同, 也请在这里说明。 - validations: - required: true - - type: input - id: server-install-info - attributes: - label: "服务端安装信息或者一键脚本信息" - description: | - 请填写你使用的服务端版本(在 VPS 上执行 `hysteria --version`, 把输出贴在这里)。 - 如果你使用一键脚本, 请把一键脚本让你复制和执行的命令贴在这里。 - 如果你使用机场, 请优先联系机场售后以获取使用帮助。 - placeholder: | - hysteria version v1.x.x 2006-01-02t08:04:05z 0123456789abcdef0123456789abcdef01234567 - validations: - required: true - - type: textarea - id: server-provider-info - attributes: - label: "VPS 信息" - description: | - 请填写你搭建服务端所使用的 VPS 服务商以及 VPS 配置。 - 如果你使用机场, 请填写机场网址。 - placeholder: | - TurtleShell 春川机房 ARM 单核 512MB内存 - validations: - required: true - - type: textarea - id: server-config-info - attributes: - label: "服务端配置" - description: | - 请把你的服务端配置 JSON 粘贴在这里。 - 如果你使用的是一键脚本并且不需要任何配置, 请填写「无」。 - placeholder: | - { - "listen": ":36712", - "acme": { - "domains": [ - "your.domain.com" - ], - "email": "hacker@gmail.com" - }, - "obfs": "fuck me till the daylight", - "up_mbps": 100, - "down_mbps": 100 - } - validations: - required: true - - type: textarea - id: server-log - attributes: - label: "服务端日志" - description: | - 请把你的服务端日志贴在这里, 可以是截图但是请尽可能提供纯文本。 - validations: - required: true - - type: input - id: client-install-info - attributes: - label: "客户端安装信息" - description: | - 请填写你使用的客户端版本(在客户端执行 `hysteria --version`, 并把输出贴在这里)。 - 如果你使用第三方客户端(包括但不限于 Clash、 Passwall、 SagerNet), 请贴上它们的版本, 或者到这些第三方客户端的社群寻求帮助。 - placeholder: | - hysteria version v1.x.x 2006-01-02T08:04:05Z 0123456789abcdef0123456789abcdef01234567 - validations: - required: true - - type: textarea - id: client-config-info - attributes: - label: "客户端配置" - description: | - 请把你的客户端配置 JSON 粘贴在这里。 - 你可以移除客户端配置里的敏感信息(像服务器地址、 混淆密码、 认证密码), 但是这也意味着你必须自己检查这些配置是否填写正确。 - 如果你使用第三方客户端, 你可以贴上第三方客户端的配置或者配置截图。 - placeholder: | - { - "server": "example.com:36712", - "obfs": "fuck me till the daylight", - "up_mbps": 10, - "down_mbps": 50, - "socks5": { - "listen": "127.0.0.1:1080" - }, - "http": { - "listen": "127.0.0.1:8080" - } - } - validations: - required: true - - type: input - id: client-environment - attributes: - label: "客户端运行环境(操作系统)" - description: | - 请填写客户端使用的操作系统的名称和版本。 - 如果你在 OpenWRT 上运行 hysteria 客户端, 请填写 OpenWRT (以及你使用的插件, 如 Passwall)的版本。 - placeholder: | - Windows 11 - validations: - required: true - - type: textarea - id: client-log - attributes: - label: "客户端日志" - description: | - 请把你的客户端日志贴在这里, 可以是截图但是请尽可能提供纯文本。 - validations: - required: true - diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 5445036..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 2842fd7..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '17 14 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - # - name: Autobuild - # uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - - name: Run build script - run: ./build.sh - shell: bash - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/dev-build-master.yml b/.github/workflows/dev-build-master.yml deleted file mode 100644 index a1171eb..0000000 --- a/.github/workflows/dev-build-master.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: "Build master" - -on: - push: - branches: - - 'master' - tags-ignore: - - 'v*' - - 'core/v*' - - 'app/v*' - -jobs: - - build: - name: Build - runs-on: ubuntu-latest - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: true - - steps: - - - name: Check out - uses: actions/checkout@v3 - - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: "1.20" - - - name: Run build script - env: - HY_APP_PLATFORMS: 'darwin/amd64,darwin/amd64-avx,darwin/arm64,windows/amd64,windows/amd64-avx,windows/386,windows/arm64,linux/amd64,linux/amd64-avx,linux/386,linux/arm,linux/armv5,linux/arm64,linux/s390x,linux/mipsle,linux/mipsle-sf,freebsd/amd64,freebsd/amd64-avx,freebsd/386,freebsd/arm,freebsd/arm64' - run: ./build.sh - shell: bash - - - name: Archive - uses: actions/upload-artifact@v3 - with: - name: hysteria-binaries-${{ github.sha }} - path: ./build diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml deleted file mode 100644 index 854d6e5..0000000 --- a/.github/workflows/release-docker.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: Build Docker Image - -on: - push: - tags: - - 'v*' - -jobs: - docker: - runs-on: ubuntu-latest - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: true - - steps: - - name: Check out - uses: actions/checkout@v3 - - - name: Get tag - id: get_tag - run: echo "TAG=$(git describe --tags --always --match 'v*')" >> $GITHUB_OUTPUT - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - id: docker_build - uses: docker/build-push-action@v4.0.0 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: tobyxdd/hysteria:latest,tobyxdd/hysteria:${{ steps.get_tag.outputs.TAG }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 4a8cb30..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Build and release - -on: - push: - tags: - - 'v*' - -jobs: - - build: - name: Build and release - runs-on: ubuntu-latest - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: true - - steps: - - - name: Check out - uses: actions/checkout@v3 - - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: "1.20" - - - name: Run build script - env: - HY_APP_PLATFORMS: 'darwin/amd64,darwin/amd64-avx,darwin/arm64,windows/amd64,windows/amd64-avx,windows/386,windows/arm64,linux/amd64,linux/amd64-avx,linux/386,linux/arm,linux/armv5,linux/arm64,linux/s390x,linux/mipsle,linux/mipsle-sf,freebsd/amd64,freebsd/amd64-avx,freebsd/386,freebsd/arm,freebsd/arm64' - run: ./build.sh - shell: bash - - - name: Generate hashes - run: | - cd build - for f in $(find . -type f); do - sha256sum $f | sudo tee -a hashes.txt - done - - - name: Upload - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - ./build/hysteria-darwin-amd64 - ./build/hysteria-darwin-amd64-avx - ./build/hysteria-darwin-arm64 - ./build/hysteria-windows-amd64.exe - ./build/hysteria-windows-amd64-avx.exe - ./build/hysteria-windows-386.exe - ./build/hysteria-windows-arm64.exe - ./build/hysteria-linux-amd64 - ./build/hysteria-linux-amd64-avx - ./build/hysteria-linux-386 - ./build/hysteria-linux-arm - ./build/hysteria-linux-armv5 - ./build/hysteria-linux-arm64 - ./build/hysteria-linux-s390x - ./build/hysteria-linux-mipsle - ./build/hysteria-linux-mipsle-sf - ./build/hysteria-freebsd-amd64 - ./build/hysteria-freebsd-amd64-avx - ./build/hysteria-freebsd-386 - ./build/hysteria-freebsd-arm - ./build/hysteria-freebsd-arm64 - ./build/hashes.txt diff --git a/.gitignore b/.gitignore index e226098..d868acb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ -# Created by https://www.gitignore.io/api/go,linux,macos,windows,intellij+all -# Edit at https://www.gitignore.io/?templates=go,linux,macos,windows,intellij+all +# Created by https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,intellij+all,go,windows,linux,macos ### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# # Binaries for programs and plugins *.exe *.exe~ @@ -18,12 +21,11 @@ # Dependency directories (remove the comment below to include it) # vendor/ -### Go Patch ### -/vendor/ -/Godeps/ +# Go workspace file +go.work -### Intellij+all ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +### GoLand+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff @@ -33,6 +35,9 @@ .idea/**/dictionaries .idea/**/shelf +# AWS User-specific +.idea/**/aws.xml + # Generated files .idea/**/contentModel.xml @@ -53,6 +58,9 @@ # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules @@ -80,6 +88,9 @@ atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml +# SonarLint plugin +.idea/sonarlint/ + # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties @@ -92,21 +103,69 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser +### GoLand+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + ### Intellij+all Patch ### -# Ignores the whole .idea folder and all .iml files -# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. -.idea/ -# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 - -*.iml -modules.xml -.idea/misc.xml -*.ipr - -# Sonarlint plugin -.idea/sonarlint ### Linux ### *~ @@ -132,6 +191,7 @@ modules.xml # Icon must end with two \r Icon + # Thumbnails ._* @@ -151,6 +211,10 @@ Network Trash Folder Temporary Items .apdisk +### macOS Patch ### +# iCloud generated files +*.icloud + ### Windows ### # Windows thumbnail cache files Thumbs.db @@ -177,13 +241,4 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.gitignore.io/api/go,linux,macos,windows,intellij+all - -cmd/relay/*.json -hy_linux -.vscode - -/build/ -/dist/ - -config*.json +# End of https://www.toptal.com/developers/gitignore/api/goland+all,intellij+all,go,windows,linux,macos \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 4032cfa..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,148 +0,0 @@ -# Changelog - -## 1.3.4 - -- Eliminate unnecessary DNS lookups when using SOCKS5 outbound with ACL disabled -- Add a `lazy_start` option to let the client connect to the server only when there is an incoming connection -- Fix a bug where TCP redirect didn't work on x86 (32-bit) machines -- Fix memory leak when using UDP port hopping -- Updated quic-go to v0.33.0 - -## 1.3.3 - -- Fix a bug that made UDP unusable when using `socks5_outbound` -- Set the default value of `retry_interval` to 1 to prevent the client from retrying too often when errors occur -- Prompt error if both acme and local cert file are specified in client config -- Updated quic-go to v0.32.0, performance improvements - -## 1.3.2 - -- Fix a bug where some malformed UDP packets would cause the server to crash -- Fix a bug where the server did not have a timeout for SOCKS5 outbound connections -- Add build variants: amd64-avx, armv5, mipsle-sf, windows/arm64 - -## 1.3.1 - -- New `fast_open` option for client to reduce RTT when dialing TCP connections -- Fix a bug where the HTTP proxy would not close connections properly -- Minor performance improvements here and there - -## 1.3.0 - -- Connection migration: clients can now seamlessly switch between networks without losing their connection to the server -- Dynamic port hopping: see https://hysteria.network/docs/port-hopping/ for more information - -## 1.2.2 - -- Fix a bug where the client would crash for IPv6 UDP requests in TProxy mode. -- Fix a bug where the client did not release old UDP sockets when reconnecting. -- Fix a bug where using DoT (DNS over TLS) as resolver would cause the client/server to crash. -- Add `quit_on_disconnect`, `handshake_timeout`, `idle_timeout` options to client config. -- Drop server's legacy protocol (v2) support. -- Updated quic-go to v0.30.0, small performance improvements. - -## 1.2.1 - -- Fix a bug that caused DNS failure when using domain names in the "resolver" option -- Fix a bug where errors in HTTP proxy mode were not logged -- Fix a bug where WeChat protocol was not working properly when obfuscation was not enabled -- New TCP buffer options for tun mode (`tcp_sndbuf`, `tcp_rcvbuf`, `tcp_autotuning`) - -## 1.2.0 - -- Reworked TUN mode -- DoT/DoH/DoQ support for resolver -- IP masking (anonymization) -- FreeBSD builds - -## 1.1.0 - -- Super major CPU performance improvements (~30% to several times faster, depending on the circumstances) by optimizing several data structures in quic-go (changes upstreamed) - -## 1.0.5 - -- `bind_outbound` server option for binding outbound connections to a specific address or interface -- TCP Redirect mode (for Linux) - -## 1.0.4 - -- ~10% CPU usage reduction -- Improve performance when packet loss is high -- New ACL syntax to support protocol/port - -## 1.0.3 - -- New string-based speed (up/down) options -- Server SOCKS5 outbound domain pass-through -- Linux s390x build -- Updated quic-go to v0.27.0 - -## 1.0.2 - -- Added an option for DNS resolution preference `resolve_preference` - -## 1.0.1 - -- Fix server SOCKS5 outbound bug -- Fix incorrect UDP fragmentation handling - -## 1.0.0 - -- Protocol v3: UDP fragmentation support -- Fix SOCKS5 UDP timeout issue -- SOCKS5 outbound support - -## 0.9.7 - -- CLI improvements (cobra) -- Fix broken UDP TProxy mode -- Re-enable PMTUD on Windows & Linux - -## 0.9.6 - -- Disable quic-go PMTUD due to broken implementation -- Fix zero initMaxDatagramSize in brutal CC -- Client retry - -## 0.9.5 - -- Client connect & disconnect log -- Warning when no auth or obfs is set -- Multi-password & cmd auth support - -## 0.9.4 - -- fsnotify-based auto keypair reloading -- ACL country code support - -## 0.9.3 - -- CC optimizations -- Set buffer correctly for faketcp mode -- "wechat-video" protocol - -## 0.9.2 - -- Updated quic-go to v0.24.0 -- Reduced obfs overhead by reusing buffers - -## 0.9.1 - -- faketcp implementation -- DNS `resolver` option in config - -## 0.9.0 - -- Auto keypair reloading -- SOCKS5 listen address no longer needs a specific IP -- Multi-relay support -- IPv6 only mode for server - -## 0.8.6 - -- Added an option for customizing ALPN `alpn` -- Removed ACL support from TPROXY & TUN modes - -## 0.8.5 - -- Added an option to disable MTU discovery `disable_mtu_discovery` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 34d0b35..0000000 --- a/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM golang:1-alpine AS builder - -LABEL maintainer="mritd " - -# GOPROXY is disabled by default, use: -# docker build --build-arg GOPROXY="https://goproxy.io" ... -# to enable GOPROXY. -ARG GOPROXY="" - -ENV GOPROXY ${GOPROXY} - -COPY . /go/src/github.com/apernet/hysteria - -WORKDIR /go/src/github.com/apernet/hysteria - -RUN set -ex \ - && apk add git build-base bash \ - && ./build.sh \ - && mv ./build/hysteria-* /go/bin/hysteria - -# multi-stage builds to create the final image -FROM alpine AS dist - -LABEL maintainer="mritd " - -# set up nsswitch.conf for Go's "netgo" implementation -# - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275 -# - docker run --rm debian:stretch grep '^hosts:' /etc/nsswitch.conf -RUN if [ ! -e /etc/nsswitch.conf ]; then echo 'hosts: files dns' > /etc/nsswitch.conf; fi - -# bash is used for debugging, tzdata is used to add timezone information. -# Install ca-certificates to ensure no CA certificate errors. -# -# Do not try to add the "--no-cache" option when there are multiple "apk" -# commands, this will cause the build process to become very slow. -RUN set -ex \ - && apk upgrade \ - && apk add bash tzdata ca-certificates \ - && rm -rf /var/cache/apk/* - -COPY --from=builder /go/bin/hysteria /usr/local/bin/hysteria - -ENTRYPOINT ["hysteria"] diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 78bd535..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,30 +0,0 @@ -License -================== - -Hysteria itself, including all codes under this directory, is licensed under the MIT License. - -``` -The MIT License (MIT) - -Copyright (c) 2021 Toby - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -However, when building with `-tags gpl`, the produced executable shall be distributed under GPLv3. diff --git a/README.md b/README.md index e577669..b66514b 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,37 @@ -# ![Logo](docs/logos/readme.png) +# Hysteria 2 Prototype -[![License][1]][2] [![Release][3]][4] [![Telegram][5]][6] [![Discussions][7]][8] +> **Warning** +> The code on this branch is a work-in-progress prototype of what will become Hysteria 2.0. It is currently very unfinished, and unless you know what you are doing, you should stick with the stable 1.x releases for now. **The protocol is also subject to change, so we do not recommend third-party developers use this as a reference for the Hysteria 2 protocol at this time.** -[1]: https://img.shields.io/badge/license-MIT-blue +> **警告** +> 此分支的代码是 Hysteria 2.0 的原型,目前仍在开发中,完成度十分有限。除非你十分确定自己在做什么,否则请继续使用稳定的 1.x 版本。**协议也可能会发生变化,因此我们不建议第三方开发者在目前使用此分支作为 Hysteria 2 协议的参考。** -[2]: LICENSE.md +## Build (编译) -[3]: https://img.shields.io/github/v/release/apernet/hysteria?style=flat-square +```bash +go build ./app +``` -[4]: https://github.com/apernet/hysteria/releases +## Usage (使用) -[5]: https://img.shields.io/badge/chat-Telegram-blue?style=flat-square +### Server +```bash +./app server -c config.yaml +``` -[6]: https://t.me/hysteria_github +[Example sever config (示例服务器配置)](app/server.example.yaml) -[7]: https://img.shields.io/github/discussions/apernet/hysteria?style=flat-square +### Client +```bash +./app client -c config.yaml +``` -[8]: https://github.com/apernet/hysteria/discussions +[Example client config (示例客户端配置)](app/client.example.yaml) -![AperNet](docs/logos/AperNetLogo.png) +## Test HTTP/3 masquerading (测试 HTTP/3 伪装) -An [Aperture Internet Laboratory](https://apernet.io/) project +```bash +chrome --origin-to-force-quic-on=example.com:443 +``` ----------- - -Hysteria is a feature-packed proxy & relay tool optimized for lossy, unstable connections (e.g. satellite networks, -congested public Wi-Fi, connecting to foreign servers from China) powered by a customized protocol based on QUIC. - -## Use cases - -- Censorship circumvention -- Boosting slow connections -- Bypassing commercial/academic/corporate firewalls -- Bypassing ISP throttling -- ... - -## Modes - -- SOCKS5 proxy (TCP & UDP) -- HTTP/HTTPS proxy -- TCP/UDP relay -- TCP/UDP TPROXY (Linux) -- TCP REDIRECT (Linux) -- TUN (TAP on Windows) -- Still growing... - -## **[Documentation](https://hysteria.network/)** - ----------- - -Hysteria 是一个功能丰富的,专为恶劣网络环境(如卫星网络、拥挤的公共 Wi-Fi、从中国连接境外服务器等)进行优化的双边加速工具,基于修改版的 QUIC 协议。 - -## 常见用例 - -- 绕过网络审查 -- 提升传输速度 -- 绕过商业/学校/企业防火墙 -- 绕过运营商 QoS 限速 - -## 模式 - -- SOCKS5 代理 (TCP & UDP) -- HTTP/HTTPS 代理 -- TCP/UDP 转发 -- TCP/UDP TPROXY 透明代理 (Linux) -- TCP REDIRECT 透明代理 (Linux) -- TUN (Windows 下为 TAP) -- 仍在增加中... - -## **[中文文档](https://hysteria.network/zh/)** - ----------- - -## Benchmarks - -![Bench](docs/bench/bench.png) - ----------- - -**Donations are greatly appreciated!** Contact me if you would like your name listed as a sponsor. - -**欢迎大佬捐赠!** 如希望挂名请在捐赠后联系我。 - - - Crypto donation button by NOWPayments - +Then visit `https://example.com:443` in Chrome. \ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml deleted file mode 100644 index 2ebdb30..0000000 --- a/Taskfile.yaml +++ /dev/null @@ -1,362 +0,0 @@ -############################################################################## -# # -# go-task: https://taskfile.dev/installation/ # -# # -# For the role of 'amd64-v*', please refer to # -# https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels. # -# # -############################################################################## - -version: '3' - -vars: - BUILD_VERSION: - sh: git describe --tags --always --match 'v*' - BUILD_COMMIT: - sh: git rev-parse HEAD - BUILD_DATE: - sh: date -u '+%F %T' - -tasks: - clean: - cmds: - - rm -rf dist - - mkdir -p dist - hash: - dir: ./dist - cmds: - - sha256sum hysteria-* > hashes.txt - build-hysteria: - label: build-{{.TASK}} - dir: ./app/cmd - cmds: - - | - GOOS={{.GOOS}} GOARCH={{.GOARCH}} GOARM={{.GOARM}} GOAMD64={{.GOAMD64}} GOMIPS={{.GOMIPS}} \ - go build -trimpath -o ../../dist/hysteria-{{.TASK}}{{.BINEXT}} -ldflags \ - "-w -s -X 'main.appVersion={{.BUILD_VERSION}}' -X 'main.appCommit={{.BUILD_COMMIT}}' -X 'main.appDate={{.BUILD_DATE}}'" - linux-386: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: 386 - } - linux-amd64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: amd64 - } - linux-amd64-v2: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: amd64, - GOAMD64: v2 - } - linux-amd64-v3: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: amd64, - GOAMD64: v3 - } - linux-amd64-v4: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: amd64, - GOAMD64: v4 - } - linux-armv5: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: arm, - GOARM: 5 - } - linux-armv6: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: arm, - GOARM: 6 - } - linux-armv7: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: arm, - GOARM: 7 - } - linux-armv8: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: arm64 - } - linux-s390x: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: s390x - } - linux-mips-hardfloat: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: mips, - GOMIPS: hardfloat - } - linux-mipsle-softfloat: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: mipsle, - GOMIPS: softfloat - } - linux-mipsle-hardfloat: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: mipsle, - GOMIPS: hardfloat - } - linux-mips64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: mips64 - } - linux-mips64le: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: linux, - GOARCH: mips64le - } - darwin-amd64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: darwin, - GOARCH: amd64 - } - darwin-amd64-v2: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: darwin, - GOARCH: amd64, - GOAMD64: v2 - } - darwin-amd64-v3: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: darwin, - GOARCH: amd64, - GOAMD64: v3 - } - darwin-amd64-v4: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: darwin, - GOARCH: amd64, - GOAMD64: v4 - } - darwin-arm64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: darwin, - GOARCH: arm64 - } - freebsd-386: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: freebsd, - GOARCH: 386 - } - freebsd-amd64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: freebsd, - GOARCH: amd64 - } - freebsd-amd64-v2: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: freebsd, - GOARCH: amd64, - GOAMD64: v2 - } - freebsd-amd64-v3: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: freebsd, - GOARCH: amd64, - GOAMD64: v3 - } - freebsd-amd64-v4: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: freebsd, - GOARCH: amd64, - GOAMD64: v4 - } - freebsd-arm: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: freebsd, - GOARCH: arm - } - freebsd-arm64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - GOOS: freebsd, - GOARCH: arm64 - } - windows-386: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - BINEXT: ".exe", - GOOS: windows, - GOARCH: 386 - } - windows-amd64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - BINEXT: ".exe", - GOOS: windows, - GOARCH: amd64 - } - windows-amd64-v2: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - BINEXT: ".exe", - GOOS: windows, - GOARCH: amd64, - GOAMD64: v2 - } - windows-amd64-v3: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - BINEXT: ".exe", - GOOS: windows, - GOARCH: amd64, - GOAMD64: v3 - } - windows-amd64-v4: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - BINEXT: ".exe", - GOOS: windows, - GOARCH: amd64, - GOAMD64: v4 - } - windows-arm64: - cmds: - - task: build-hysteria - vars: { - TASK: "{{.TASK}}", - BINEXT: ".exe", - GOOS: windows, - GOARCH: arm64 - } - default: - cmds: - - task: clean - - task: linux-386 - - task: linux-amd64 - - task: linux-amd64-v2 - - task: linux-amd64-v3 - - task: linux-amd64-v4 - - task: linux-armv5 - - task: linux-armv6 - - task: linux-armv7 - - task: linux-armv8 - - task: linux-s390x - - task: linux-mips-hardfloat - - task: linux-mipsle-softfloat - - task: linux-mipsle-hardfloat - - task: linux-mips64 - - task: linux-mips64le - - task: darwin-amd64 - - task: darwin-amd64-v2 - - task: darwin-amd64-v3 - - task: darwin-amd64-v4 - - task: darwin-arm64 - - task: freebsd-386 - - task: freebsd-amd64 - - task: freebsd-amd64-v2 - - task: freebsd-amd64-v3 - - task: freebsd-amd64-v4 - - task: freebsd-arm - - task: freebsd-arm64 - - task: windows-386 - - task: windows-amd64 - - task: windows-amd64-v2 - - task: windows-amd64-v3 - - task: windows-amd64-v4 - - task: windows-arm64 - - task: hash - diff --git a/app/auth/external.go b/app/auth/external.go deleted file mode 100644 index 768e331..0000000 --- a/app/auth/external.go +++ /dev/null @@ -1,97 +0,0 @@ -package auth - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net" - "net/http" - "os/exec" - "strconv" - "strings" - - "github.com/sirupsen/logrus" -) - -type CmdAuthProvider struct { - Cmd string -} - -func (p *CmdAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { - cmd := exec.Command(p.Cmd, addr.String(), string(auth), strconv.Itoa(int(sSend)), strconv.Itoa(int(sRecv))) - out, err := cmd.Output() - if err != nil { - if _, ok := err.(*exec.ExitError); ok { - return false, strings.TrimSpace(string(out)) - } else { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Error("Failed to execute auth command") - return false, "internal error" - } - } else { - return true, strings.TrimSpace(string(out)) - } -} - -type HTTPAuthProvider struct { - Client *http.Client - URL string -} - -type authReq struct { - Addr string `json:"addr"` - Payload []byte `json:"payload"` - Send uint64 `json:"send"` - Recv uint64 `json:"recv"` -} - -type authResp struct { - OK bool `json:"ok"` - Msg string `json:"msg"` -} - -func (p *HTTPAuthProvider) Auth(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { - jbs, err := json.Marshal(&authReq{ - Addr: addr.String(), - Payload: auth, - Send: sSend, - Recv: sRecv, - }) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Error("Failed to marshal auth request") - return false, "internal error" - } - resp, err := p.Client.Post(p.URL, "application/json", bytes.NewBuffer(jbs)) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Error("Failed to send auth request") - return false, "internal error" - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - logrus.WithFields(logrus.Fields{ - "code": resp.StatusCode, - }).Error("Invalid status code from auth server") - return false, "internal error" - } - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Error("Failed to read auth response") - return false, "internal error" - } - var ar authResp - err = json.Unmarshal(data, &ar) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Error("Failed to unmarshal auth response") - return false, "internal error" - } - return ar.OK, ar.Msg -} diff --git a/app/auth/funcs.go b/app/auth/funcs.go deleted file mode 100644 index 63d3f50..0000000 --- a/app/auth/funcs.go +++ /dev/null @@ -1,59 +0,0 @@ -package auth - -import ( - "errors" - "net" - "net/http" - "time" - - "github.com/apernet/hysteria/core/cs" - "github.com/yosuke-furukawa/json5/encoding/json5" -) - -func PasswordAuthFunc(rawMsg json5.RawMessage) (cs.ConnectFunc, error) { - var pwds []string - err := json5.Unmarshal(rawMsg, &pwds) - if err != nil { - // not a string list, legacy format? - var pwdConfig map[string]string - err = json5.Unmarshal(rawMsg, &pwdConfig) - if err != nil || len(pwdConfig["password"]) == 0 { - // still no, invalid config - return nil, errors.New("invalid config") - } - // yes it is - pwds = []string{pwdConfig["password"]} - } - return func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { - for _, pwd := range pwds { - if string(auth) == pwd { - return true, "Welcome" - } - } - return false, "Wrong password" - }, nil -} - -func ExternalAuthFunc(rawMsg json5.RawMessage) (cs.ConnectFunc, error) { - var extConfig map[string]string - err := json5.Unmarshal(rawMsg, &extConfig) - if err != nil { - return nil, errors.New("invalid config") - } - if len(extConfig["http"]) != 0 { - hp := &HTTPAuthProvider{ - Client: &http.Client{ - Timeout: 10 * time.Second, - }, - URL: extConfig["http"], - } - return hp.Auth, nil - } else if len(extConfig["cmd"]) != 0 { - cp := &CmdAuthProvider{ - Cmd: extConfig["cmd"], - } - return cp.Auth, nil - } else { - return nil, errors.New("invalid config") - } -} diff --git a/app/client.example.yaml b/app/client.example.yaml new file mode 100644 index 0000000..73638e1 --- /dev/null +++ b/app/client.example.yaml @@ -0,0 +1,29 @@ +server: example.com +# sni: other.example.com + +auth: "hello world" + +# tls: +# insecure: false +# ca: "custom.ca" + +# quic: +# initStreamReceiveWindow: 8388608 +# maxStreamReceiveWindow: 8388608 +# initConnReceiveWindow: 20971520 +# maxConnReceiveWindow: 20971520 +# maxIdleTimeout: 30s +# keepAlivePeriod: 10s +# disablePathMTUDiscovery: false + +bandwidth: + up: "100 mbps" + down: "100 mbps" + +# fastOpen: true + +socks5: + listen: 127.0.0.1:1080 + # username: "user" + # password: "haha233" + # disableUDP: true \ No newline at end of file diff --git a/app/cmd/acme.go b/app/cmd/acme.go deleted file mode 100644 index 9b2a26a..0000000 --- a/app/cmd/acme.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "os" - "path/filepath" - "runtime" - - "go.uber.org/zap" - - "github.com/caddyserver/certmagic" -) - -func acmeTLSConfig(domains []string, email string, disableHTTP bool, disableTLSALPN bool, - altHTTPPort int, altTLSALPNPort int, -) (*tls.Config, error) { - cfg := &certmagic.Config{ - RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio, - KeySource: certmagic.DefaultKeyGenerator, - Storage: &certmagic.FileStorage{Path: dataDir()}, - Logger: zap.NewNop(), - } - issuer := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{ - CA: certmagic.LetsEncryptProductionCA, - TestCA: certmagic.LetsEncryptStagingCA, - Email: email, - Agreed: true, - DisableHTTPChallenge: disableHTTP, - DisableTLSALPNChallenge: disableTLSALPN, - AltHTTPPort: altHTTPPort, - AltTLSALPNPort: altTLSALPNPort, - Logger: zap.NewNop(), - }) - cfg.Issuers = []certmagic.Issuer{issuer} - - cache := certmagic.NewCache(certmagic.CacheOptions{ - GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { - return cfg, nil - }, - Logger: zap.NewNop(), - }) - cfg = certmagic.New(cache, *cfg) - - err := cfg.ManageSync(context.Background(), domains) - if err != nil { - return nil, err - } - return cfg.TLSConfig(), nil -} - -func homeDir() string { - home := os.Getenv("HOME") - if home == "" && runtime.GOOS == "windows" { - drive := os.Getenv("HOMEDRIVE") - path := os.Getenv("HOMEPATH") - home = drive + path - if drive == "" || path == "" { - home = os.Getenv("USERPROFILE") - } - } - if home == "" { - home = "." - } - return home -} - -func dataDir() string { - baseDir := filepath.Join(homeDir(), ".local", "share") - if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { - baseDir = xdgData - } - return filepath.Join(baseDir, "certmagic") -} diff --git a/app/cmd/client.go b/app/cmd/client.go index 3f51e86..c917d24 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -1,462 +1,216 @@ -package main +package cmd import ( - "crypto/tls" "crypto/x509" "errors" - "io" - "io/ioutil" "net" - "net/http" "os" - "time" + "sync" - hyHTTP "github.com/apernet/hysteria/app/http" - "github.com/apernet/hysteria/app/redirect" - "github.com/apernet/hysteria/app/relay" - "github.com/apernet/hysteria/app/socks5" - "github.com/apernet/hysteria/app/tproxy" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" - "github.com/apernet/hysteria/core/pktconns" - - "github.com/apernet/hysteria/core/pmtud" - "github.com/oschwald/geoip2-golang" - "github.com/yosuke-furukawa/json5/encoding/json5" - - "github.com/apernet/hysteria/core/acl" - "github.com/apernet/hysteria/core/cs" - "github.com/apernet/hysteria/core/transport" - "github.com/quic-go/quic-go" - "github.com/sirupsen/logrus" + "github.com/apernet/hysteria/app/internal/socks5" + "github.com/apernet/hysteria/core/client" ) -var clientPacketConnFuncFactoryMap = map[string]pktconns.ClientPacketConnFuncFactory{ - "": pktconns.NewClientUDPConnFunc, - "udp": pktconns.NewClientUDPConnFunc, - "wechat": pktconns.NewClientWeChatConnFunc, - "wechat-video": pktconns.NewClientWeChatConnFunc, - "faketcp": pktconns.NewClientFakeTCPConnFunc, +var clientCmd = &cobra.Command{ + Use: "client", + Short: "Client mode", + Run: runClient, } -func client(config *clientConfig) { - logrus.WithField("config", config.String()).Info("Client configuration loaded") - config.Fill() // Fill default values - // Resolver - if len(config.Resolver) > 0 { - err := setResolver(config.Resolver) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to set resolver") +var modeMap = map[string]func(*viper.Viper, client.Client) error{ + "socks5": clientSOCKS5, +} + +func init() { + rootCmd.AddCommand(clientCmd) +} + +func runClient(cmd *cobra.Command, args []string) { + logger.Info("client mode") + + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read client config", zap.Error(err)) + } + config, err := viperToClientConfig() + if err != nil { + logger.Fatal("failed to parse client config", zap.Error(err)) + } + + c, err := client.NewClient(config) + if err != nil { + logger.Fatal("failed to initialize client", zap.Error(err)) + } + defer c.Close() + + var wg sync.WaitGroup + hasMode := false + for mode, f := range modeMap { + v := viper.Sub(mode) + if v != nil { + hasMode = true + wg.Add(1) + go func() { + defer wg.Done() + if err := f(v, c); err != nil { + logger.Fatal("failed to run mode", zap.String("mode", mode), zap.Error(err)) + } + }() } } + if !hasMode { + logger.Fatal("no mode specified") + } + wg.Wait() +} + +func viperToClientConfig() (*client.Config, error) { + // Conn and address + addrStr := viper.GetString("server") + if addrStr == "" { + return nil, configError{Field: "server", Err: errors.New("server address is empty")} + } + addrStr = completeServerAddrString(addrStr) + addr, err := net.ResolveUDPAddr("udp", addrStr) + if err != nil { + return nil, configError{Field: "server", Err: err} + } + sni := viper.GetString("sni") + if sni == "" { + sni = addrStr + } // TLS - tlsConfig := &tls.Config{ - NextProtos: []string{config.ALPN}, - ServerName: config.ServerName, - InsecureSkipVerify: config.Insecure, - MinVersion: tls.VersionTLS13, - } - // Load CA - if len(config.CustomCA) > 0 { - bs, err := ioutil.ReadFile(config.CustomCA) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - "file": config.CustomCA, - }).Fatal("Failed to load CA") - } - cp := x509.NewCertPool() - if !cp.AppendCertsFromPEM(bs) { - logrus.WithFields(logrus.Fields{ - "file": config.CustomCA, - }).Fatal("Failed to parse CA") - } - tlsConfig.RootCAs = cp - } - // QUIC config - quicConfig := &quic.Config{ - InitialStreamReceiveWindow: config.ReceiveWindowConn, - MaxStreamReceiveWindow: config.ReceiveWindowConn, - InitialConnectionReceiveWindow: config.ReceiveWindow, - MaxConnectionReceiveWindow: config.ReceiveWindow, - HandshakeIdleTimeout: time.Duration(config.HandshakeTimeout) * time.Second, - MaxIdleTimeout: time.Duration(config.IdleTimeout) * time.Second, - KeepAlivePeriod: time.Duration(config.IdleTimeout) * time.Second * 2 / 5, - DisablePathMTUDiscovery: config.DisableMTUDiscovery, - EnableDatagrams: true, - } - if !quicConfig.DisablePathMTUDiscovery && pmtud.DisablePathMTUDiscovery { - logrus.Info("Path MTU Discovery is not yet supported on this platform") - } - // Auth - var auth []byte - if len(config.Auth) > 0 { - auth = config.Auth - } else { - auth = []byte(config.AuthString) - } - // Packet conn - pktConnFuncFactory := clientPacketConnFuncFactoryMap[config.Protocol] - if pktConnFuncFactory == nil { - logrus.WithFields(logrus.Fields{ - "protocol": config.Protocol, - }).Fatal("Unsupported protocol") - } - pktConnFunc := pktConnFuncFactory(config.Obfs, time.Duration(config.HopInterval)*time.Second) - // Resolve preference - if len(config.ResolvePreference) > 0 { - pref, err := transport.ResolvePreferenceFromString(config.ResolvePreference) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to parse the resolve preference") - } - transport.DefaultClientTransport.ResolvePreference = pref - } - // ACL - var aclEngine *acl.Engine - if len(config.ACL) > 0 { - var err error - aclEngine, err = acl.LoadFromFile(config.ACL, transport.DefaultClientTransport.ResolveIPAddr, - func() (*geoip2.Reader, error) { - return loadMMDBReader(config.MMDB) - }) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - "file": config.ACL, - }).Fatal("Failed to parse ACL") - } - } - // Client - var client *cs.Client - try := 0 - up, down, _ := config.Speed() - for { - try += 1 - c, err := cs.NewClient(config.Server, auth, tlsConfig, quicConfig, pktConnFunc, up, down, config.FastOpen, - config.LazyStart, - func(err error) { - if config.QuitOnDisconnect { - logrus.WithFields(logrus.Fields{ - "addr": config.Server, - "error": err, - }).Fatal("Connection to server lost, exiting...") - } else { - logrus.WithFields(logrus.Fields{ - "addr": config.Server, - "error": err, - }).Error("Connection to server lost, reconnecting...") - } - }) - if err != nil { - logrus.WithField("error", err).Error("Failed to initialize client") - if try <= config.Retry || config.Retry < 0 { - retryInterval := 1 - if config.RetryInterval != nil { - retryInterval = *config.RetryInterval - } - logrus.WithFields(logrus.Fields{ - "retry": try, - "interval": retryInterval, - }).Info("Retrying...") - time.Sleep(time.Duration(retryInterval) * time.Second) - } else { - logrus.Fatal("Out of retries, exiting...") - } - } else { - client = c - break - } - } - defer client.Close() - if config.LazyStart { - logrus.WithField("addr", config.Server).Info("Lazy start enabled, waiting for first connection") - } else { - logrus.WithField("addr", config.Server).Info("Connected") - } - - // Local - errChan := make(chan error) - if len(config.SOCKS5.Listen) > 0 { - go func() { - var authFunc func(user, password string) bool - if config.SOCKS5.User != "" && config.SOCKS5.Password != "" { - authFunc = func(user, password string) bool { - return config.SOCKS5.User == user && config.SOCKS5.Password == password - } - } - socks5server, err := socks5.NewServer(client, transport.DefaultClientTransport, config.SOCKS5.Listen, - authFunc, time.Duration(config.SOCKS5.Timeout)*time.Second, aclEngine, config.SOCKS5.DisableUDP, - func(addr net.Addr, reqAddr string, action acl.Action, arg string) { - logrus.WithFields(logrus.Fields{ - "action": actionToString(action, arg), - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debug("SOCKS5 TCP request") - }, - func(addr net.Addr, reqAddr string, err error) { - if err != io.EOF { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Info("SOCKS5 TCP error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debug("SOCKS5 TCP EOF") - } - }, - func(addr net.Addr) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - }).Debug("SOCKS5 UDP associate") - }, - func(addr net.Addr, err error) { - if err != io.EOF { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - }).Info("SOCKS5 UDP error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - }).Debug("SOCKS5 UDP EOF") - } - }) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize SOCKS5 server") - } - logrus.WithField("addr", config.SOCKS5.Listen).Info("SOCKS5 server up and running") - errChan <- socks5server.ListenAndServe() - }() - } - - if len(config.HTTP.Listen) > 0 { - go func() { - var authFunc func(user, password string) bool - if config.HTTP.User != "" && config.HTTP.Password != "" { - authFunc = func(user, password string) bool { - return config.HTTP.User == user && config.HTTP.Password == password - } - } - proxy, err := hyHTTP.NewProxyHTTPServer(client, transport.DefaultClientTransport, - time.Duration(config.HTTP.Timeout)*time.Second, aclEngine, authFunc, - func(reqAddr string, action acl.Action, arg string) { - logrus.WithFields(logrus.Fields{ - "action": actionToString(action, arg), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debug("HTTP request") - }, - func(reqAddr string, err error) { - logrus.WithFields(logrus.Fields{ - "error": err, - "dst": defaultIPMasker.Mask(reqAddr), - }).Info("HTTP error") - }) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize HTTP server") - } - if config.HTTP.Cert != "" && config.HTTP.Key != "" { - logrus.WithField("addr", config.HTTP.Listen).Info("HTTPS server up and running") - errChan <- http.ListenAndServeTLS(config.HTTP.Listen, config.HTTP.Cert, config.HTTP.Key, proxy) - } else { - logrus.WithField("addr", config.HTTP.Listen).Info("HTTP server up and running") - errChan <- http.ListenAndServe(config.HTTP.Listen, proxy) - } - }() - } - - if len(config.TUN.Name) != 0 { - go startTUN(config, client, errChan) - } - - if len(config.TCPRelay.Listen) > 0 { - config.TCPRelays = append(config.TCPRelays, Relay{ - Listen: config.TCPRelay.Listen, - Remote: config.TCPRelay.Remote, - Timeout: config.TCPRelay.Timeout, - }) - } - - if len(config.TCPRelays) > 0 { - for _, tcpr := range config.TCPRelays { - go func(tcpr Relay) { - rl, err := relay.NewTCPRelay(client, tcpr.Listen, tcpr.Remote, - time.Duration(tcpr.Timeout)*time.Second, - func(addr net.Addr) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - }).Debug("TCP relay request") - }, - func(addr net.Addr, err error) { - if err != io.EOF { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - }).Info("TCP relay error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - }).Debug("TCP relay EOF") - } - }) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize TCP relay") - } - logrus.WithField("addr", tcpr.Listen).Info("TCP relay up and running") - errChan <- rl.ListenAndServe() - }(tcpr) - } - } - - if len(config.UDPRelay.Listen) > 0 { - config.UDPRelays = append(config.UDPRelays, Relay{ - Listen: config.UDPRelay.Listen, - Remote: config.UDPRelay.Remote, - Timeout: config.UDPRelay.Timeout, - }) - } - - if len(config.UDPRelays) > 0 { - for _, udpr := range config.UDPRelays { - go func(udpr Relay) { - rl, err := relay.NewUDPRelay(client, udpr.Listen, udpr.Remote, - time.Duration(udpr.Timeout)*time.Second, - func(addr net.Addr) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - }).Debug("UDP relay request") - }, - func(addr net.Addr, err error) { - if err != relay.ErrTimeout { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - }).Info("UDP relay error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - }).Debug("UDP relay session closed") - } - }) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize UDP relay") - } - logrus.WithField("addr", udpr.Listen).Info("UDP relay up and running") - errChan <- rl.ListenAndServe() - }(udpr) - } - } - - if len(config.TCPTProxy.Listen) > 0 { - go func() { - rl, err := tproxy.NewTCPTProxy(client, config.TCPTProxy.Listen, - time.Duration(config.TCPTProxy.Timeout)*time.Second, - func(addr, reqAddr net.Addr) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Debug("TCP TProxy request") - }, - func(addr, reqAddr net.Addr, err error) { - if err != io.EOF { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Info("TCP TProxy error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Debug("TCP TProxy EOF") - } - }) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize TCP TProxy") - } - logrus.WithField("addr", config.TCPTProxy.Listen).Info("TCP TProxy up and running") - errChan <- rl.ListenAndServe() - }() - } - - if len(config.UDPTProxy.Listen) > 0 { - go func() { - rl, err := tproxy.NewUDPTProxy(client, config.UDPTProxy.Listen, - time.Duration(config.UDPTProxy.Timeout)*time.Second, - func(addr, reqAddr net.Addr) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Debug("UDP TProxy request") - }, - func(addr, reqAddr net.Addr, err error) { - if !errors.Is(err, os.ErrDeadlineExceeded) { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Info("UDP TProxy error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Debug("UDP TProxy session closed") - } - }) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize UDP TProxy") - } - logrus.WithField("addr", config.UDPTProxy.Listen).Info("UDP TProxy up and running") - errChan <- rl.ListenAndServe() - }() - } - - if len(config.TCPRedirect.Listen) > 0 { - go func() { - rl, err := redirect.NewTCPRedirect(client, config.TCPRedirect.Listen, - time.Duration(config.TCPRedirect.Timeout)*time.Second, - func(addr, reqAddr net.Addr) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Debug("TCP Redirect request") - }, - func(addr, reqAddr net.Addr, err error) { - if err != io.EOF { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Info("TCP Redirect error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr.String()), - }).Debug("TCP Redirect EOF") - } - }) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize TCP Redirect") - } - logrus.WithField("addr", config.TCPRedirect.Listen).Info("TCP Redirect up and running") - errChan <- rl.ListenAndServe() - }() - } - - err := <-errChan - logrus.WithField("error", err).Fatal("Client shutdown") -} - -func parseClientConfig(cb []byte) (*clientConfig, error) { - var c clientConfig - err := json5.Unmarshal(cb, &c) + tlsConfig, err := viperToClientTLSConfig() if err != nil { return nil, err } - return &c, c.Check() + // QUIC + quicConfig := viperToClientQUICConfig() + // Bandwidth + bwConfig, err := viperToClientBandwidthConfig() + if err != nil { + return nil, err + } + return &client.Config{ + ConnFactory: nil, // TODO + ServerAddr: addr, + ServerName: sni, + Auth: viper.GetString("auth"), + TLSConfig: tlsConfig, + QUICConfig: quicConfig, + BandwidthConfig: bwConfig, + FastOpen: viper.GetBool("fastOpen"), + }, nil +} + +func viperToClientTLSConfig() (client.TLSConfig, error) { + config := client.TLSConfig{ + InsecureSkipVerify: viper.GetBool("tls.insecure"), + } + caPath := viper.GetString("tls.ca") + if caPath != "" { + ca, err := os.ReadFile(caPath) + if err != nil { + return client.TLSConfig{}, configError{Field: "tls.ca", Err: err} + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(ca) { + return client.TLSConfig{}, configError{Field: "tls.ca", Err: errors.New("failed to parse CA certificate")} + } + config.RootCAs = pool + } + return config, nil +} + +func viperToClientQUICConfig() client.QUICConfig { + return client.QUICConfig{ + InitialStreamReceiveWindow: viper.GetUint64("quic.initStreamReceiveWindow"), + MaxStreamReceiveWindow: viper.GetUint64("quic.maxStreamReceiveWindow"), + InitialConnectionReceiveWindow: viper.GetUint64("quic.initConnReceiveWindow"), + MaxConnectionReceiveWindow: viper.GetUint64("quic.maxConnReceiveWindow"), + MaxIdleTimeout: viper.GetDuration("quic.maxIdleTimeout"), + KeepAlivePeriod: viper.GetDuration("quic.keepAlivePeriod"), + DisablePathMTUDiscovery: viper.GetBool("quic.disablePathMTUDiscovery"), + } +} + +func viperToClientBandwidthConfig() (client.BandwidthConfig, error) { + bw := client.BandwidthConfig{} + upStr, downStr := viper.GetString("bandwidth.up"), viper.GetString("bandwidth.down") + if upStr == "" || downStr == "" { + return client.BandwidthConfig{}, configError{Field: "bandwidth", Err: errors.New("bandwidth.up and bandwidth.down must be set")} + } + up, err := convBandwidth(upStr) + if err != nil { + return client.BandwidthConfig{}, configError{Field: "bandwidth.up", Err: err} + } + down, err := convBandwidth(downStr) + if err != nil { + return client.BandwidthConfig{}, configError{Field: "bandwidth.down", Err: err} + } + bw.MaxTx, bw.MaxRx = up, down + return bw, nil +} + +func clientSOCKS5(v *viper.Viper, c client.Client) error { + listenAddr := v.GetString("listen") + if listenAddr == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + l, err := net.Listen("tcp", listenAddr) + if err != nil { + return configError{Field: "listen", Err: err} + } + var authFunc func(username, password string) bool + username, password := v.GetString("username"), v.GetString("password") + if username != "" && password != "" { + authFunc = func(username, password string) bool { + return username == username && password == password + } + } + s := socks5.Server{ + HyClient: c, + AuthFunc: authFunc, + DisableUDP: viper.GetBool("disableUDP"), + EventLogger: &socks5Logger{}, + } + logger.Info("SOCKS5 server listening", zap.String("addr", listenAddr)) + return s.Serve(l) +} + +func completeServerAddrString(addrStr string) string { + if _, _, err := net.SplitHostPort(addrStr); err != nil { + // No port provided, use default HTTPS port + return net.JoinHostPort(addrStr, "443") + } + return addrStr +} + +type socks5Logger struct{} + +func (l *socks5Logger) TCPRequest(addr net.Addr, reqAddr string) { + logger.Debug("SOCKS5 TCP request", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) +} + +func (l *socks5Logger) TCPError(addr net.Addr, reqAddr string, err error) { + if err == nil { + logger.Debug("SOCKS5 TCP closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr)) + } else { + logger.Error("SOCKS5 TCP error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err)) + } +} + +func (l *socks5Logger) UDPRequest(addr net.Addr) { + logger.Debug("SOCKS5 UDP request", zap.String("addr", addr.String())) +} + +func (l *socks5Logger) UDPError(addr net.Addr, err error) { + if err == nil { + logger.Debug("SOCKS5 UDP closed", zap.String("addr", addr.String())) + } else { + logger.Error("SOCKS5 UDP error", zap.String("addr", addr.String()), zap.Error(err)) + } } diff --git a/app/cmd/client_gpl.go b/app/cmd/client_gpl.go deleted file mode 100644 index dd5dad2..0000000 --- a/app/cmd/client_gpl.go +++ /dev/null @@ -1,117 +0,0 @@ -//go:build gpl -// +build gpl - -package main - -import ( - "io" - "net" - "strings" - "time" - - "github.com/apernet/hysteria/app/tun" - - "github.com/docker/go-units" - "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" - - "github.com/apernet/hysteria/core/cs" - "github.com/sirupsen/logrus" -) - -const license = `Hysteria is a feature-packed proxy & relay utility optimized for lossy, unstable connections. -Copyright (C) 2022 Toby - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -` - -func startTUN(config *clientConfig, client *cs.Client, errChan chan error) { - timeout := time.Duration(config.TUN.Timeout) * time.Second - if timeout == 0 { - timeout = 300 * time.Second - } - - var err error - var tcpSendBufferSize, tcpReceiveBufferSize int64 - - if config.TUN.TCPSendBufferSize != "" { - tcpSendBufferSize, err = units.RAMInBytes(config.TUN.TCPSendBufferSize) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - "tcp-sndbuf": config.TUN.TCPSendBufferSize, - }).Fatal("Failed to parse tcp-sndbuf in the TUN config") - } - if (tcpSendBufferSize != 0 && tcpSendBufferSize < tcp.MinBufferSize) || tcpSendBufferSize > tcp.MaxBufferSize { - logrus.WithFields(logrus.Fields{ - "tcp-sndbuf": config.TUN.TCPSendBufferSize, - }).Fatal("Invalid tcp-sndbuf in the TUN config") - } - } - if config.TUN.TCPReceiveBufferSize != "" { - tcpReceiveBufferSize, err = units.RAMInBytes(config.TUN.TCPReceiveBufferSize) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - "tcp-rcvbuf": config.TUN.TCPReceiveBufferSize, - }).Fatal("Failed to parse tcp-rcvbuf in the TUN config") - } - if (tcpReceiveBufferSize != 0 && tcpReceiveBufferSize < tcp.MinBufferSize) || tcpReceiveBufferSize > tcp.MaxBufferSize { - logrus.WithFields(logrus.Fields{ - "error": err, - "tcp-rcvbuf": config.TUN.TCPReceiveBufferSize, - }).Fatal("Invalid tcp-rcvbuf in the TUN config") - } - } - - tunServer, err := tun.NewServer(client, timeout, - config.TUN.Name, config.TUN.MTU, - int(tcpSendBufferSize), int(tcpReceiveBufferSize), config.TUN.TCPModerateReceiveBuffer) - if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize TUN server") - } - tunServer.RequestFunc = func(addr net.Addr, reqAddr string) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debugf("TUN %s request", strings.ToUpper(addr.Network())) - } - tunServer.ErrorFunc = func(addr net.Addr, reqAddr string, err error) { - if err != nil { - if err == io.EOF { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debugf("TUN %s EOF", strings.ToUpper(addr.Network())) - } else if err == cs.ErrClosed && strings.HasPrefix(addr.Network(), "udp") { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network())) - } else if nErr, ok := err.(net.Error); ok && nErr.Timeout() && strings.HasPrefix(addr.Network(), "tcp") { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debugf("TUN %s closed for timeout", strings.ToUpper(addr.Network())) - } else { - logrus.WithFields(logrus.Fields{ - "error": err, - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Infof("TUN %s error", strings.ToUpper(addr.Network())) - } - } - } - logrus.WithField("interface", config.TUN.Name).Info("TUN up and running") - errChan <- tunServer.ListenAndServe() -} diff --git a/app/cmd/client_nongpl.go b/app/cmd/client_nongpl.go deleted file mode 100644 index 417159b..0000000 --- a/app/cmd/client_nongpl.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build !gpl -// +build !gpl - -package main - -import ( - "github.com/apernet/hysteria/core/cs" - "github.com/sirupsen/logrus" -) - -const license = `The MIT License (MIT) - -Copyright (c) 2021 Toby - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -` - -func startTUN(config *clientConfig, client *cs.Client, errChan chan error) { - logrus.Fatalln("TUN mode is only available in GPL builds. Please rebuild hysteria with -tags gpl") -} diff --git a/app/cmd/completion.go b/app/cmd/completion.go deleted file mode 100644 index ea6bbed..0000000 --- a/app/cmd/completion.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: fmt.Sprintf(`To load completions: - -Bash: - - $ source <(%[1]s completion bash) - - # To load completions for each session, execute once: - # Linux: - $ %[1]s completion bash > /etc/bash_completion.d/%[1]s - # macOS: - $ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s - -Zsh: - - # If shell completion is not already enabled in your environment, - # you will need to enable it. You can execute the following once: - - $ echo "autoload -U compinit; compinit" >> ~/.zshrc - - # To load completions for each session, execute once: - $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" - - # You will need to start a new shell for this setup to take effect. - -fish: - - $ %[1]s completion fish | source - - # To load completions for each session, execute once: - $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish - -PowerShell: - - PS> %[1]s completion powershell | Out-String | Invoke-Expression - - # To load completions for every new session, run: - PS> %[1]s completion powershell > %[1]s.ps1 - # and source this file from your PowerShell profile. -`, rootCmd.Name()), - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.ExactValidArgs(1), - Run: func(cmd *cobra.Command, args []string) { - switch args[0] { - case "bash": - _ = cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - _ = cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - _ = cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - _ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - } - }, -} diff --git a/app/cmd/config.go b/app/cmd/config.go deleted file mode 100644 index 67b30f0..0000000 --- a/app/cmd/config.go +++ /dev/null @@ -1,385 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "regexp" - "strconv" - - "github.com/sirupsen/logrus" - "github.com/yosuke-furukawa/json5/encoding/json5" -) - -const ( - mbpsToBps = 125000 - minSpeedBPS = 16384 - - DefaultALPN = "hysteria" - - DefaultStreamReceiveWindow = 16777216 // 16 MB - DefaultConnectionReceiveWindow = DefaultStreamReceiveWindow * 5 / 2 // 40 MB - - DefaultMaxIncomingStreams = 1024 - - DefaultMMDBFilename = "GeoLite2-Country.mmdb" - - ServerMaxIdleTimeoutSec = 60 - DefaultClientIdleTimeoutSec = 20 - - DefaultClientHopIntervalSec = 10 -) - -var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`) - -type serverConfig struct { - Listen string `json:"listen"` - Protocol string `json:"protocol"` - ACME struct { - Domains []string `json:"domains"` - Email string `json:"email"` - DisableHTTPChallenge bool `json:"disable_http"` - DisableTLSALPNChallenge bool `json:"disable_tlsalpn"` - AltHTTPPort int `json:"alt_http_port"` - AltTLSALPNPort int `json:"alt_tlsalpn_port"` - } `json:"acme"` - CertFile string `json:"cert"` - KeyFile string `json:"key"` - // Optional below - Up string `json:"up"` - UpMbps int `json:"up_mbps"` - Down string `json:"down"` - DownMbps int `json:"down_mbps"` - DisableUDP bool `json:"disable_udp"` - ACL string `json:"acl"` - MMDB string `json:"mmdb"` - Obfs string `json:"obfs"` - Auth struct { - Mode string `json:"mode"` - Config json5.RawMessage `json:"config"` - } `json:"auth"` - ALPN string `json:"alpn"` - PrometheusListen string `json:"prometheus_listen"` - ReceiveWindowConn uint64 `json:"recv_window_conn"` - ReceiveWindowClient uint64 `json:"recv_window_client"` - MaxConnClient int `json:"max_conn_client"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery"` - Resolver string `json:"resolver"` - ResolvePreference string `json:"resolve_preference"` - SOCKS5Outbound struct { - Server string `json:"server"` - User string `json:"user"` - Password string `json:"password"` - } `json:"socks5_outbound"` - BindOutbound struct { - Address string `json:"address"` - Device string `json:"device"` - } `json:"bind_outbound"` -} - -func (c *serverConfig) Speed() (uint64, uint64, error) { - var up, down uint64 - if len(c.Up) > 0 { - up = stringToBps(c.Up) - if up == 0 { - return 0, 0, errors.New("invalid speed format") - } - } else { - up = uint64(c.UpMbps) * mbpsToBps - } - if len(c.Down) > 0 { - down = stringToBps(c.Down) - if down == 0 { - return 0, 0, errors.New("invalid speed format") - } - } else { - down = uint64(c.DownMbps) * mbpsToBps - } - return up, down, nil -} - -func (c *serverConfig) Check() error { - if len(c.Listen) == 0 { - return errors.New("missing listen address") - } - if len(c.ACME.Domains) == 0 && (len(c.CertFile) == 0 || len(c.KeyFile) == 0) { - return errors.New("need either ACME info or cert/key files") - } - if len(c.ACME.Domains) > 0 && (len(c.CertFile) > 0 || len(c.KeyFile) > 0) { - return errors.New("cannot use both ACME and cert/key files, they are mutually exclusive") - } - if up, down, err := c.Speed(); err != nil || (up != 0 && up < minSpeedBPS) || (down != 0 && down < minSpeedBPS) { - return errors.New("invalid speed") - } - if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) || - (c.ReceiveWindowClient != 0 && c.ReceiveWindowClient < 65536) { - return errors.New("invalid receive window size") - } - if c.MaxConnClient < 0 { - return errors.New("invalid max connections per client") - } - return nil -} - -func (c *serverConfig) Fill() { - if len(c.ALPN) == 0 { - c.ALPN = DefaultALPN - } - if c.ReceiveWindowConn == 0 { - c.ReceiveWindowConn = DefaultStreamReceiveWindow - } - if c.ReceiveWindowClient == 0 { - c.ReceiveWindowClient = DefaultConnectionReceiveWindow - } - if c.MaxConnClient == 0 { - c.MaxConnClient = DefaultMaxIncomingStreams - } - if len(c.MMDB) == 0 { - c.MMDB = DefaultMMDBFilename - } -} - -func (c *serverConfig) String() string { - return fmt.Sprintf("%+v", *c) -} - -type Relay struct { - Listen string `json:"listen"` - Remote string `json:"remote"` - Timeout int `json:"timeout"` -} - -func (r *Relay) Check() error { - if len(r.Listen) == 0 { - return errors.New("missing relay listen address") - } - if len(r.Remote) == 0 { - return errors.New("missing relay remote address") - } - if r.Timeout != 0 && r.Timeout < 4 { - return errors.New("invalid relay timeout") - } - return nil -} - -type clientConfig struct { - Server string `json:"server"` - Protocol string `json:"protocol"` - Up string `json:"up"` - UpMbps int `json:"up_mbps"` - Down string `json:"down"` - DownMbps int `json:"down_mbps"` - // Optional below - Retry int `json:"retry"` - RetryInterval *int `json:"retry_interval"` - QuitOnDisconnect bool `json:"quit_on_disconnect"` - HandshakeTimeout int `json:"handshake_timeout"` - IdleTimeout int `json:"idle_timeout"` - HopInterval int `json:"hop_interval"` - SOCKS5 struct { - Listen string `json:"listen"` - Timeout int `json:"timeout"` - DisableUDP bool `json:"disable_udp"` - User string `json:"user"` - Password string `json:"password"` - } `json:"socks5"` - HTTP struct { - Listen string `json:"listen"` - Timeout int `json:"timeout"` - User string `json:"user"` - Password string `json:"password"` - Cert string `json:"cert"` - Key string `json:"key"` - } `json:"http"` - TUN struct { - Name string `json:"name"` - Timeout int `json:"timeout"` - MTU uint32 `json:"mtu"` - TCPSendBufferSize string `json:"tcp_sndbuf"` - TCPReceiveBufferSize string `json:"tcp_rcvbuf"` - TCPModerateReceiveBuffer bool `json:"tcp_autotuning"` - } `json:"tun"` - TCPRelays []Relay `json:"relay_tcps"` - TCPRelay Relay `json:"relay_tcp"` // deprecated, but we still support it for backward compatibility - UDPRelays []Relay `json:"relay_udps"` - UDPRelay Relay `json:"relay_udp"` // deprecated, but we still support it for backward compatibility - TCPTProxy struct { - Listen string `json:"listen"` - Timeout int `json:"timeout"` - } `json:"tproxy_tcp"` - UDPTProxy struct { - Listen string `json:"listen"` - Timeout int `json:"timeout"` - } `json:"tproxy_udp"` - TCPRedirect struct { - Listen string `json:"listen"` - Timeout int `json:"timeout"` - } `json:"redirect_tcp"` - ACL string `json:"acl"` - MMDB string `json:"mmdb"` - Obfs string `json:"obfs"` - Auth []byte `json:"auth"` - AuthString string `json:"auth_str"` - ALPN string `json:"alpn"` - ServerName string `json:"server_name"` - Insecure bool `json:"insecure"` - CustomCA string `json:"ca"` - ReceiveWindowConn uint64 `json:"recv_window_conn"` - ReceiveWindow uint64 `json:"recv_window"` - DisableMTUDiscovery bool `json:"disable_mtu_discovery"` - FastOpen bool `json:"fast_open"` - LazyStart bool `json:"lazy_start"` - Resolver string `json:"resolver"` - ResolvePreference string `json:"resolve_preference"` -} - -func (c *clientConfig) Speed() (uint64, uint64, error) { - var up, down uint64 - if len(c.Up) > 0 { - up = stringToBps(c.Up) - if up == 0 { - return 0, 0, errors.New("invalid speed format") - } - } else { - up = uint64(c.UpMbps) * mbpsToBps - } - if len(c.Down) > 0 { - down = stringToBps(c.Down) - if down == 0 { - return 0, 0, errors.New("invalid speed format") - } - } else { - down = uint64(c.DownMbps) * mbpsToBps - } - return up, down, nil -} - -func (c *clientConfig) Check() error { - if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.TUN.Name) == 0 && - len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 && - len(c.TCPRelays) == 0 && len(c.UDPRelays) == 0 && - len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 && - len(c.TCPRedirect.Listen) == 0 { - return errors.New("please enable at least one mode") - } - if c.HandshakeTimeout != 0 && c.HandshakeTimeout < 2 { - return errors.New("invalid handshake timeout") - } - if c.IdleTimeout != 0 && c.IdleTimeout < 4 { - return errors.New("invalid idle timeout") - } - if c.HopInterval != 0 && c.HopInterval < 8 { - return errors.New("invalid hop interval") - } - if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout < 4 { - return errors.New("invalid SOCKS5 timeout") - } - if c.HTTP.Timeout != 0 && c.HTTP.Timeout < 4 { - return errors.New("invalid HTTP timeout") - } - if c.TUN.Timeout != 0 && c.TUN.Timeout < 4 { - return errors.New("invalid TUN timeout") - } - if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 { - return errors.New("missing TCP relay remote address") - } - if len(c.UDPRelay.Listen) > 0 && len(c.UDPRelay.Remote) == 0 { - return errors.New("missing UDP relay remote address") - } - if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout < 4 { - return errors.New("invalid TCP relay timeout") - } - if c.UDPRelay.Timeout != 0 && c.UDPRelay.Timeout < 4 { - return errors.New("invalid UDP relay timeout") - } - for _, r := range c.TCPRelays { - if err := r.Check(); err != nil { - return err - } - } - for _, r := range c.UDPRelays { - if err := r.Check(); err != nil { - return err - } - } - if c.TCPTProxy.Timeout != 0 && c.TCPTProxy.Timeout < 4 { - return errors.New("invalid TCP TProxy timeout") - } - if c.UDPTProxy.Timeout != 0 && c.UDPTProxy.Timeout < 4 { - return errors.New("invalid UDP TProxy timeout") - } - if c.TCPRedirect.Timeout != 0 && c.TCPRedirect.Timeout < 4 { - return errors.New("invalid TCP Redirect timeout") - } - if len(c.Server) == 0 { - return errors.New("missing server address") - } - if up, down, err := c.Speed(); err != nil || up < minSpeedBPS || down < minSpeedBPS { - return errors.New("invalid speed") - } - if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) || - (c.ReceiveWindow != 0 && c.ReceiveWindow < 65536) { - return errors.New("invalid receive window size") - } - if len(c.TCPRelay.Listen) > 0 { - logrus.Warn("'relay_tcp' is deprecated, consider using 'relay_tcps' instead") - } - if len(c.UDPRelay.Listen) > 0 { - logrus.Warn("'relay_udp' is deprecated, consider using 'relay_udps' instead") - } - return nil -} - -func (c *clientConfig) Fill() { - if len(c.ALPN) == 0 { - c.ALPN = DefaultALPN - } - if c.ReceiveWindowConn == 0 { - c.ReceiveWindowConn = DefaultStreamReceiveWindow - } - if c.ReceiveWindow == 0 { - c.ReceiveWindow = DefaultConnectionReceiveWindow - } - if len(c.MMDB) == 0 { - c.MMDB = DefaultMMDBFilename - } - if c.IdleTimeout == 0 { - c.IdleTimeout = DefaultClientIdleTimeoutSec - } - if c.HopInterval == 0 { - c.HopInterval = DefaultClientHopIntervalSec - } -} - -func (c *clientConfig) String() string { - return fmt.Sprintf("%+v", *c) -} - -func stringToBps(s string) uint64 { - if s == "" { - return 0 - } - m := rateStringRegexp.FindStringSubmatch(s) - if m == nil { - return 0 - } - var n uint64 - switch m[2] { - case "K": - n = 1 << 10 - case "M": - n = 1 << 20 - case "G": - n = 1 << 30 - case "T": - n = 1 << 40 - default: - n = 1 - } - v, _ := strconv.ParseUint(m[1], 10, 64) - n = v * n - if m[3] == "b" { - // Bits, need to convert to bytes - n = n >> 3 - } - return n -} diff --git a/app/cmd/config_test.go b/app/cmd/config_test.go deleted file mode 100644 index 11006ba..0000000 --- a/app/cmd/config_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import "testing" - -func Test_stringToBps(t *testing.T) { - tests := []struct { - name string - s string - want uint64 - }{ - {name: "bps 1", s: "8 bps", want: 1}, - {name: "bps 2", s: "3 bps", want: 0}, - {name: "Bps", s: "9991Bps", want: 9991}, - {name: "KBps", s: "10 KBps", want: 10240}, - {name: "Kbps", s: "10 Kbps", want: 1280}, - {name: "MBps", s: "10 MBps", want: 10485760}, - {name: "Mbps", s: "10 Mbps", want: 1310720}, - {name: "GBps", s: "10 GBps", want: 10737418240}, - {name: "Gbps", s: "10 Gbps", want: 1342177280}, - {name: "TBps", s: "10 TBps", want: 10995116277760}, - {name: "Tbps", s: "10 Tbps", want: 1374389534720}, - {name: "invalid 1", s: "6699E Kbps", want: 0}, - {name: "invalid 2", s: "400 Bsp", want: 0}, - {name: "invalid 3", s: "9 GBbps", want: 0}, - {name: "invalid 4", s: "Mbps", want: 0}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := stringToBps(tt.s); got != tt.want { - t.Errorf("stringToBps() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/app/cmd/ipmasker.go b/app/cmd/ipmasker.go deleted file mode 100644 index 97a1d13..0000000 --- a/app/cmd/ipmasker.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "net" -) - -type ipMasker struct { - IPv4Mask net.IPMask - IPv6Mask net.IPMask -} - -// Mask masks an address with the configured CIDR. -// addr can be "host:port" or just host. -func (m *ipMasker) Mask(addr string) string { - if m.IPv4Mask == nil && m.IPv6Mask == nil { - return addr - } - - host, port, err := net.SplitHostPort(addr) - if err != nil { - // just host - host, port = addr, "" - } - ip := net.ParseIP(host) - if ip == nil { - // not an IP address, return as is - return addr - } - if ip4 := ip.To4(); ip4 != nil && m.IPv4Mask != nil { - // IPv4 - host = ip4.Mask(m.IPv4Mask).String() - } else if ip6 := ip.To16(); ip6 != nil && m.IPv6Mask != nil { - // IPv6 - host = ip6.Mask(m.IPv6Mask).String() - } - if port != "" { - return net.JoinHostPort(host, port) - } else { - return host - } -} - -var defaultIPMasker = &ipMasker{} diff --git a/app/cmd/kploader.go b/app/cmd/kploader.go deleted file mode 100644 index 5d6f60e..0000000 --- a/app/cmd/kploader.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "crypto/tls" - "sync" - - "github.com/fsnotify/fsnotify" - "github.com/sirupsen/logrus" -) - -type keypairLoader struct { - certMu sync.RWMutex - cert *tls.Certificate - certPath string - keyPath string -} - -func newKeypairLoader(certPath, keyPath string) (*keypairLoader, error) { - loader := &keypairLoader{ - certPath: certPath, - keyPath: keyPath, - } - cert, err := tls.LoadX509KeyPair(certPath, keyPath) - if err != nil { - return nil, err - } - loader.cert = &cert - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - go func() { - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - switch event.Op { - case fsnotify.Create, fsnotify.Write, fsnotify.Rename, fsnotify.Chmod: - logrus.WithFields(logrus.Fields{ - "file": event.Name, - }).Info("Keypair change detected, reloading...") - if err := loader.load(); err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Error("Failed to reload keypair") - } else { - logrus.Info("Keypair successfully reloaded") - } - case fsnotify.Remove: - _ = watcher.Add(event.Name) // Workaround for vim - // https://github.com/fsnotify/fsnotify/issues/92 - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - logrus.WithFields(logrus.Fields{ - "error": err, - }).Error("Failed to watch keypair files for changes") - } - } - }() - err = watcher.Add(certPath) - if err != nil { - _ = watcher.Close() - return nil, err - } - err = watcher.Add(keyPath) - if err != nil { - _ = watcher.Close() - return nil, err - } - return loader, nil -} - -func (kpr *keypairLoader) load() error { - cert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath) - if err != nil { - return err - } - kpr.certMu.Lock() - kpr.cert = &cert - kpr.certMu.Unlock() - return nil -} - -func (kpr *keypairLoader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { - return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - kpr.certMu.RLock() - defer kpr.certMu.RUnlock() - return kpr.cert, nil - } -} diff --git a/app/cmd/main.go b/app/cmd/main.go deleted file mode 100644 index 36ef82c..0000000 --- a/app/cmd/main.go +++ /dev/null @@ -1,208 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "math/rand" - "net" - "os" - "regexp" - "strings" - "time" - - nested "github.com/antonfisher/nested-logrus-formatter" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -const ( - logo = ` -░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█ -░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█ -░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀ - -` - desc = "A TCP/UDP relay & SOCKS5/HTTP proxy tool optimized for poor network environments" - authors = "Aperture Internet Laboratory " -) - -var ( - appVersion = "Unknown" - appCommit = "Unknown" - appDate = "Unknown" -) - -var rootCmd = &cobra.Command{ - Use: "hysteria", - Long: fmt.Sprintf("%s%s\n\nVersion:\t%s\nBuildDate:\t%s\nCommitHash:\t%s\nAuthors:\t%s", logo, desc, appVersion, appDate, appCommit, authors), - Example: "./hysteria server --config /etc/hysteria.json", - Version: fmt.Sprintf("%s %s %s", appVersion, appDate, appCommit), - PersistentPreRun: func(cmd *cobra.Command, args []string) { - rand.Seed(time.Now().UnixNano()) - - // log config - logrus.SetOutput(os.Stdout) - if lvl, err := logrus.ParseLevel(viper.GetString("log-level")); err == nil { - logrus.SetLevel(lvl) - } else { - logrus.SetLevel(logrus.DebugLevel) - } - - if strings.ToLower(viper.GetString("log-format")) == "json" { - logrus.SetFormatter(&logrus.JSONFormatter{ - TimestampFormat: viper.GetString("log-timestamp"), - }) - } else { - logrus.SetFormatter(&nested.Formatter{ - FieldsOrder: []string{ - "version", "url", - "config", "file", "mode", "protocol", - "cert", "key", - "addr", "src", "dst", "session", "action", "interface", - "tcp-sndbuf", "tcp-rcvbuf", - "retry", "interval", - "code", "msg", "error", - }, - TimestampFormat: viper.GetString("log-timestamp"), - }) - } - - // license - if viper.GetBool("license") { - fmt.Printf("%s\n", license) - os.Exit(0) - } - - // ip mask config - v4m := viper.GetUint("log-ipv4-mask") - if v4m > 0 && v4m < 32 { - defaultIPMasker.IPv4Mask = net.CIDRMask(int(v4m), 32) - } - v6m := viper.GetUint("log-ipv6-mask") - if v6m > 0 && v6m < 128 { - defaultIPMasker.IPv6Mask = net.CIDRMask(int(v6m), 128) - } - - // check update - if !viper.GetBool("no-check") { - go checkUpdate() - } - }, - Run: func(cmd *cobra.Command, args []string) { - clientCmd.Run(cmd, args) - }, -} - -var clientCmd = &cobra.Command{ - Use: "client", - Short: "Run as client mode", - Example: "./hysteria client --config /etc/hysteria/client.json", - Run: func(cmd *cobra.Command, args []string) { - cbs, err := ioutil.ReadFile(viper.GetString("config")) - if err != nil { - logrus.WithFields(logrus.Fields{ - "file": viper.GetString("config"), - "error": err, - }).Fatal("Failed to read configuration") - } - // client mode - cc, err := parseClientConfig(cbs) - if err != nil { - logrus.WithFields(logrus.Fields{ - "file": viper.GetString("config"), - "error": err, - }).Fatal("Failed to parse client configuration") - } - client(cc) - }, -} - -var serverCmd = &cobra.Command{ - Use: "server", - Short: "Run as server mode", - Example: "./hysteria server --config /etc/hysteria/server.json", - Run: func(cmd *cobra.Command, args []string) { - cbs, err := ioutil.ReadFile(viper.GetString("config")) - if err != nil { - logrus.WithFields(logrus.Fields{ - "file": viper.GetString("config"), - "error": err, - }).Fatal("Failed to read configuration") - } - // server mode - sc, err := parseServerConfig(cbs) - if err != nil { - logrus.WithFields(logrus.Fields{ - "file": viper.GetString("config"), - "error": err, - }).Fatal("Failed to parse server configuration") - } - server(sc) - }, -} - -// fakeFlags replace the old flag format with the new format(eg: `-config` ->> `--config`) -func fakeFlags() { - var args []string - fr, _ := regexp.Compile(`^-[a-zA-Z]{2,}`) - for _, arg := range os.Args { - if fr.MatchString(arg) { - args = append(args, "-"+arg) - } else { - args = append(args, arg) - } - } - os.Args = args -} - -func init() { - // compatible with old flag format - fakeFlags() - - // compatible windows double click - cobra.MousetrapHelpText = "" - - // disable cmd sorting - cobra.EnableCommandSorting = false - - // add global flags - rootCmd.PersistentFlags().StringP("config", "c", "./config.json", "config file") - rootCmd.PersistentFlags().String("mmdb-url", "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb", "mmdb download url") - rootCmd.PersistentFlags().String("log-level", "debug", "log level") - rootCmd.PersistentFlags().String("log-timestamp", time.RFC3339, "log timestamp format") - rootCmd.PersistentFlags().String("log-format", "txt", "log output format (txt/json)") - rootCmd.PersistentFlags().Uint("log-ipv4-mask", 0, "mask IPv4 addresses in log using a CIDR mask") - rootCmd.PersistentFlags().Uint("log-ipv6-mask", 0, "mask IPv6 addresses in log using a CIDR mask") - rootCmd.PersistentFlags().Bool("no-check", false, "disable update check") - rootCmd.PersistentFlags().Bool("license", false, "show license and exit") - - // add to root cmd - rootCmd.AddCommand(clientCmd, serverCmd, completionCmd) - - // bind flag - _ = viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) - _ = viper.BindPFlag("mmdb-url", rootCmd.PersistentFlags().Lookup("mmdb-url")) - _ = viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level")) - _ = viper.BindPFlag("log-timestamp", rootCmd.PersistentFlags().Lookup("log-timestamp")) - _ = viper.BindPFlag("log-format", rootCmd.PersistentFlags().Lookup("log-format")) - _ = viper.BindPFlag("log-ipv4-mask", rootCmd.PersistentFlags().Lookup("log-ipv4-mask")) - _ = viper.BindPFlag("log-ipv6-mask", rootCmd.PersistentFlags().Lookup("log-ipv6-mask")) - _ = viper.BindPFlag("no-check", rootCmd.PersistentFlags().Lookup("no-check")) - _ = viper.BindPFlag("license", rootCmd.PersistentFlags().Lookup("license")) - - // bind env - _ = viper.BindEnv("config", "HYSTERIA_CONFIG") - _ = viper.BindEnv("mmdb-url", "HYSTERIA_MMDB_URL") - _ = viper.BindEnv("log-level", "HYSTERIA_LOG_LEVEL", "LOGGING_LEVEL") - _ = viper.BindEnv("log-timestamp", "HYSTERIA_LOG_TIMESTAMP", "LOGGING_TIMESTAMP_FORMAT") - _ = viper.BindEnv("log-format", "HYSTERIA_LOG_FORMAT", "LOGGING_FORMATTER") - _ = viper.BindEnv("log-ipv4-mask", "HYSTERIA_LOG_IPV4_MASK", "LOGGING_IPV4_MASK") - _ = viper.BindEnv("log-ipv6-mask", "HYSTERIA_LOG_IPV6_MASK", "LOGGING_IPV6_MASK") - _ = viper.BindEnv("no-check", "HYSTERIA_NO_CHECK", "HYSTERIA_NO_CHECK_UPDATE") - viper.AutomaticEnv() -} - -func main() { - cobra.CheckErr(rootCmd.Execute()) -} diff --git a/app/cmd/mmdb.go b/app/cmd/mmdb.go deleted file mode 100644 index f35185d..0000000 --- a/app/cmd/mmdb.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "io" - "net/http" - "os" - - "github.com/oschwald/geoip2-golang" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" -) - -func downloadMMDB(filename string) error { - resp, err := http.Get(viper.GetString("mmdb-url")) - if err != nil { - return err - } - defer resp.Body.Close() - - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - - _, err = io.Copy(file, resp.Body) - return err -} - -func loadMMDBReader(filename string) (*geoip2.Reader, error) { - if _, err := os.Stat(filename); err != nil { - if os.IsNotExist(err) { - logrus.Info("GeoLite2 database not found, downloading...") - if err := downloadMMDB(filename); err != nil { - return nil, err - } - logrus.WithFields(logrus.Fields{ - "file": filename, - }).Info("GeoLite2 database downloaded") - return geoip2.Open(filename) - } else { - // some other error - return nil, err - } - } else { - // file exists, just open it - return geoip2.Open(filename) - } -} diff --git a/app/cmd/ping.go b/app/cmd/ping.go new file mode 100644 index 0000000..0719ee0 --- /dev/null +++ b/app/cmd/ping.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + + "github.com/apernet/hysteria/core/client" +) + +// pingCmd represents the ping command +var pingCmd = &cobra.Command{ + Use: "ping address", + Short: "Ping mode", + Long: "Perform a TCP ping to a specified remote address through the proxy server. Can be used as a simple connectivity test.", + Run: runPing, +} + +func init() { + rootCmd.AddCommand(pingCmd) +} + +func runPing(cmd *cobra.Command, args []string) { + logger.Info("ping mode") + + if len(args) != 1 { + logger.Fatal("no address specified") + } + addr := args[0] + + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read client config", zap.Error(err)) + } + config, err := viperToClientConfig() + if err != nil { + logger.Fatal("failed to parse client config", zap.Error(err)) + } + + c, err := client.NewClient(config) + if err != nil { + logger.Fatal("failed to initialize client", zap.Error(err)) + } + defer c.Close() + + logger.Info("connecting", zap.String("address", addr)) + start := time.Now() + conn, err := c.DialTCP(addr) + if err != nil { + logger.Fatal("failed to connect", zap.Error(err), zap.String("time", time.Since(start).String())) + } + defer conn.Close() + + logger.Info("connected", zap.String("time", time.Since(start).String())) +} diff --git a/app/cmd/prom.go b/app/cmd/prom.go deleted file mode 100644 index f29926b..0000000 --- a/app/cmd/prom.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "github.com/apernet/hysteria/core/cs" - "github.com/prometheus/client_golang/prometheus" -) - -type prometheusTrafficCounter struct { - reg *prometheus.Registry - upCounterVec *prometheus.CounterVec - downCounterVec *prometheus.CounterVec - connGaugeVec *prometheus.GaugeVec - counterMap map[string]counters -} - -type counters struct { - UpCounter prometheus.Counter - DownCounter prometheus.Counter - ConnGauge prometheus.Gauge -} - -func NewPrometheusTrafficCounter(reg *prometheus.Registry) cs.TrafficCounter { - c := &prometheusTrafficCounter{ - reg: reg, - upCounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "hysteria_traffic_uplink_bytes_total", - }, []string{"auth"}), - downCounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "hysteria_traffic_downlink_bytes_total", - }, []string{"auth"}), - connGaugeVec: prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "hysteria_active_conn", - }, []string{"auth"}), - counterMap: make(map[string]counters), - } - reg.MustRegister(c.upCounterVec, c.downCounterVec, c.connGaugeVec) - return c -} - -func (c *prometheusTrafficCounter) getCounters(auth string) counters { - cts, ok := c.counterMap[auth] - if !ok { - cts = counters{ - UpCounter: c.upCounterVec.WithLabelValues(auth), - DownCounter: c.downCounterVec.WithLabelValues(auth), - ConnGauge: c.connGaugeVec.WithLabelValues(auth), - } - c.counterMap[auth] = cts - } - return cts -} - -func (c *prometheusTrafficCounter) Rx(auth string, n int) { - cts := c.getCounters(auth) - cts.DownCounter.Add(float64(n)) -} - -func (c *prometheusTrafficCounter) Tx(auth string, n int) { - cts := c.getCounters(auth) - cts.UpCounter.Add(float64(n)) -} - -func (c *prometheusTrafficCounter) IncConn(auth string) { - cts := c.getCounters(auth) - cts.ConnGauge.Inc() -} - -func (c *prometheusTrafficCounter) DecConn(auth string) { - cts := c.getCounters(auth) - cts.ConnGauge.Dec() -} diff --git a/app/cmd/resolver.go b/app/cmd/resolver.go deleted file mode 100644 index 473712c..0000000 --- a/app/cmd/resolver.go +++ /dev/null @@ -1,123 +0,0 @@ -package main - -import ( - "crypto/tls" - "errors" - "net" - "net/url" - "strings" - - "github.com/apernet/hysteria/core/utils" - rdns "github.com/folbricht/routedns" -) - -var errInvalidSyntax = errors.New("invalid syntax") - -func setResolver(dns string) error { - if net.ParseIP(dns) != nil { - // Just an IP address, treat as UDP 53 - dns = "udp://" + net.JoinHostPort(dns, "53") - } - var r rdns.Resolver - if strings.HasPrefix(dns, "udp://") { - // Standard UDP DNS resolver - dns = strings.TrimPrefix(dns, "udp://") - if dns == "" { - return errInvalidSyntax - } - if _, _, err := utils.SplitHostPort(dns); err != nil { - // Append the default DNS port - dns = net.JoinHostPort(dns, "53") - } - client, err := rdns.NewDNSClient("dns-udp", dns, "udp", rdns.DNSClientOptions{}) - if err != nil { - return err - } - r = client - } else if strings.HasPrefix(dns, "tcp://") { - // Standard TCP DNS resolver - dns = strings.TrimPrefix(dns, "tcp://") - if dns == "" { - return errInvalidSyntax - } - if _, _, err := utils.SplitHostPort(dns); err != nil { - // Append the default DNS port - dns = net.JoinHostPort(dns, "53") - } - client, err := rdns.NewDNSClient("dns-tcp", dns, "tcp", rdns.DNSClientOptions{}) - if err != nil { - return err - } - r = client - } else if strings.HasPrefix(dns, "https://") { - // DoH resolver - if dohURL, err := url.Parse(dns); err != nil { - return err - } else { - // Need to set bootstrap address to avoid loopback DNS lookup - dohIPAddr, err := net.ResolveIPAddr("ip", dohURL.Hostname()) - if err != nil { - return err - } - client, err := rdns.NewDoHClient("doh", dns, rdns.DoHClientOptions{ - BootstrapAddr: dohIPAddr.String(), - }) - if err != nil { - return err - } - r = client - } - } else if strings.HasPrefix(dns, "tls://") { - // DoT resolver - dns = strings.TrimPrefix(dns, "tls://") - if dns == "" { - return errInvalidSyntax - } - dotHost, _, err := utils.SplitHostPort(dns) - if err != nil { - // Append the default DNS port - dns = net.JoinHostPort(dns, "853") - } - // Need to set bootstrap address to avoid loopback DNS lookup - dotIPAddr, err := net.ResolveIPAddr("ip", dotHost) - if err != nil { - return err - } - client, err := rdns.NewDoTClient("dot", dns, rdns.DoTClientOptions{ - BootstrapAddr: dotIPAddr.String(), - TLSConfig: new(tls.Config), - }) - if err != nil { - return err - } - r = client - } else if strings.HasPrefix(dns, "quic://") { - // DoQ resolver - dns = strings.TrimPrefix(dns, "quic://") - if dns == "" { - return errInvalidSyntax - } - doqHost, _, err := utils.SplitHostPort(dns) - if err != nil { - // Append the default DNS port - dns = net.JoinHostPort(dns, "853") - } - // Need to set bootstrap address to avoid loopback DNS lookup - doqIPAddr, err := net.ResolveIPAddr("ip", doqHost) - if err != nil { - return err - } - client, err := rdns.NewDoQClient("doq", dns, rdns.DoQClientOptions{ - BootstrapAddr: doqIPAddr.String(), - }) - if err != nil { - return err - } - r = client - } else { - return errInvalidSyntax - } - cache := rdns.NewCache("cache", r, rdns.CacheOptions{}) - net.DefaultResolver = rdns.NewNetResolver(cache) - return nil -} diff --git a/app/cmd/root.go b/app/cmd/root.go new file mode 100644 index 0000000..4666f7e --- /dev/null +++ b/app/cmd/root.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +const ( + appLogo = ` +░█░█░█░█░█▀▀░▀█▀░█▀▀░█▀▄░▀█▀░█▀█░░░▀▀▄ +░█▀█░░█░░▀▀█░░█░░█▀▀░█▀▄░░█░░█▀█░░░▄▀░ +░▀░▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀▀▀ +` + appDesc = "a powerful, censorship-resistant proxy tool optimized for lossy networks" + appAuthors = "Aperture Internet Laboratory " +) + +var ( + // These values will be injected by the build system + appVersion = "Unknown" + appDate = "Unknown" + appCommit = "Unknown" + + appVersionLong = fmt.Sprintf("Version:\t%s\nBuildDate:\t%s\nCommitHash:\t%s", appVersion, appDate, appCommit) +) + +var logger *zap.Logger + +// Flags +var cfgFile string + +var rootCmd = &cobra.Command{ + Use: "hysteria", + Short: appDesc, + Long: fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong), + Run: runClient, // Default to client mode +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + initLogger() + initFlags() + cobra.OnInitialize(initConfig) +} + +func initLogger() { + // TODO: Configurable logging + l, err := zap.NewDevelopment() + if err != nil { + panic(err) + } + logger = l +} + +func initFlags() { + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file") +} + +func initConfig() { + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath("/etc/hysteria/") + viper.AddConfigPath("$HOME/.hysteria") + viper.AddConfigPath(".") + } +} diff --git a/app/cmd/server.go b/app/cmd/server.go index c6909d3..593be87 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -1,307 +1,275 @@ -package main +package cmd import ( + "context" "crypto/tls" - "io" + "errors" "net" - "net/http" - "time" + "strings" - "github.com/apernet/hysteria/app/auth" + "github.com/apernet/hysteria/core/server" + "github.com/apernet/hysteria/extras/auth" - "github.com/apernet/hysteria/core/pktconns" - - "github.com/apernet/hysteria/core/acl" - "github.com/apernet/hysteria/core/cs" - "github.com/apernet/hysteria/core/pmtud" - "github.com/apernet/hysteria/core/sockopt" - "github.com/apernet/hysteria/core/transport" - "github.com/oschwald/geoip2-golang" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/quic-go/quic-go" - "github.com/sirupsen/logrus" - "github.com/yosuke-furukawa/json5/encoding/json5" + "github.com/caddyserver/certmagic" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" ) -var serverPacketConnFuncFactoryMap = map[string]pktconns.ServerPacketConnFuncFactory{ - "": pktconns.NewServerUDPConnFunc, - "udp": pktconns.NewServerUDPConnFunc, - "wechat": pktconns.NewServerWeChatConnFunc, - "wechat-video": pktconns.NewServerWeChatConnFunc, - "faketcp": pktconns.NewServerFakeTCPConnFunc, +var serverCmd = &cobra.Command{ + Use: "server", + Short: "Server mode", + Run: runServer, } -func server(config *serverConfig) { - logrus.WithField("config", config.String()).Info("Server configuration loaded") - config.Fill() // Fill default values - // Resolver - if len(config.Resolver) > 0 { - err := setResolver(config.Resolver) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to set resolver") - } +func init() { + rootCmd.AddCommand(serverCmd) + initServerConfigDefaults() +} + +func initServerConfigDefaults() { + viper.SetDefault("listen", ":443") +} + +func runServer(cmd *cobra.Command, args []string) { + logger.Info("server mode") + + if err := viper.ReadInConfig(); err != nil { + logger.Fatal("failed to read server config", zap.Error(err)) } - // Load TLS config - var tlsConfig *tls.Config - if len(config.ACME.Domains) > 0 { - // ACME mode - tc, err := acmeTLSConfig(config.ACME.Domains, config.ACME.Email, - config.ACME.DisableHTTPChallenge, config.ACME.DisableTLSALPNChallenge, - config.ACME.AltHTTPPort, config.ACME.AltTLSALPNPort) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to get a certificate with ACME") - } - tc.NextProtos = []string{config.ALPN} - tc.MinVersion = tls.VersionTLS13 - tlsConfig = tc - } else { - // Local cert mode - kpl, err := newKeypairLoader(config.CertFile, config.KeyFile) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - "cert": config.CertFile, - "key": config.KeyFile, - }).Fatal("Failed to load the certificate") - } - tlsConfig = &tls.Config{ - GetCertificate: kpl.GetCertificateFunc(), - NextProtos: []string{config.ALPN}, - MinVersion: tls.VersionTLS13, - } - } - // QUIC config - quicConfig := &quic.Config{ - InitialStreamReceiveWindow: config.ReceiveWindowConn, - MaxStreamReceiveWindow: config.ReceiveWindowConn, - InitialConnectionReceiveWindow: config.ReceiveWindowClient, - MaxConnectionReceiveWindow: config.ReceiveWindowClient, - MaxIncomingStreams: int64(config.MaxConnClient), - MaxIdleTimeout: ServerMaxIdleTimeoutSec * time.Second, - KeepAlivePeriod: 0, // Keep alive should solely be client's responsibility - DisablePathMTUDiscovery: config.DisableMTUDiscovery, - EnableDatagrams: true, - } - if !quicConfig.DisablePathMTUDiscovery && pmtud.DisablePathMTUDiscovery { - logrus.Info("Path MTU Discovery is not yet supported on this platform") - } - // Auth - var authFunc cs.ConnectFunc - var err error - switch authMode := config.Auth.Mode; authMode { - case "", "none": - if len(config.Obfs) == 0 { - logrus.Warn("Neither authentication nor obfuscation is turned on. " + - "Your server could be used by anyone! Are you sure this is what you want?") - } - authFunc = func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { - return true, "Welcome" - } - case "password", "passwords": - authFunc, err = auth.PasswordAuthFunc(config.Auth.Config) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to enable password authentication") - } else { - logrus.Info("Password authentication enabled") - } - case "external": - authFunc, err = auth.ExternalAuthFunc(config.Auth.Config) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to enable external authentication") - } else { - logrus.Info("External authentication enabled") - } - default: - logrus.WithField("mode", config.Auth.Mode).Fatal("Unsupported authentication mode") - } - connectFunc := func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) { - ok, msg := authFunc(addr, auth, sSend, sRecv) - if !ok { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "msg": msg, - }).Info("Authentication failed, client rejected") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - }).Info("Client connected") - } - return ok, msg - } - // Resolve preference - if len(config.ResolvePreference) > 0 { - pref, err := transport.ResolvePreferenceFromString(config.ResolvePreference) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to parse the resolve preference") - } - transport.DefaultServerTransport.ResolvePreference = pref - } - // SOCKS5 outbound - if config.SOCKS5Outbound.Server != "" { - transport.DefaultServerTransport.SOCKS5Client = transport.NewSOCKS5Client(config.SOCKS5Outbound.Server, - config.SOCKS5Outbound.User, config.SOCKS5Outbound.Password) - } - // Bind outbound - if config.BindOutbound.Device != "" { - iface, err := net.InterfaceByName(config.BindOutbound.Device) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to find the interface") - } - transport.DefaultServerTransport.LocalUDPIntf = iface - sockopt.BindDialer(transport.DefaultServerTransport.Dialer, iface) - } - if config.BindOutbound.Address != "" { - ip := net.ParseIP(config.BindOutbound.Address) - if ip == nil { - logrus.WithFields(logrus.Fields{ - "error": err, - }).Fatal("Failed to parse the address") - } - transport.DefaultServerTransport.Dialer.LocalAddr = &net.TCPAddr{IP: ip} - transport.DefaultServerTransport.LocalUDPAddr = &net.UDPAddr{IP: ip} - } - // ACL - var aclEngine *acl.Engine - if len(config.ACL) > 0 { - aclEngine, err = acl.LoadFromFile(config.ACL, func(addr string) (*net.IPAddr, error) { - ipAddr, _, err := transport.DefaultServerTransport.ResolveIPAddr(addr) - return ipAddr, err - }, - func() (*geoip2.Reader, error) { - return loadMMDBReader(config.MMDB) - }) - if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - "file": config.ACL, - }).Fatal("Failed to parse ACL") - } - aclEngine.DefaultAction = acl.ActionDirect - } - // Prometheus - var trafficCounter cs.TrafficCounter - if len(config.PrometheusListen) > 0 { - promReg := prometheus.NewRegistry() - trafficCounter = NewPrometheusTrafficCounter(promReg) - go func() { - http.Handle("/metrics", promhttp.HandlerFor(promReg, promhttp.HandlerOpts{})) - err := http.ListenAndServe(config.PrometheusListen, nil) - logrus.WithField("error", err).Fatal("Prometheus HTTP server error") - }() - } - // Packet conn - pktConnFuncFactory := serverPacketConnFuncFactoryMap[config.Protocol] - if pktConnFuncFactory == nil { - logrus.WithField("protocol", config.Protocol).Fatal("Unsupported protocol") - } - pktConnFunc := pktConnFuncFactory(config.Obfs) - pktConn, err := pktConnFunc(config.Listen) + config, err := viperToServerConfig() if err != nil { - logrus.WithFields(logrus.Fields{ - "error": err, - "addr": config.Listen, - }).Fatal("Failed to listen on the UDP address") + logger.Fatal("failed to parse server config", zap.Error(err)) } - // Server - up, down, _ := config.Speed() - server, err := cs.NewServer(tlsConfig, quicConfig, pktConn, - transport.DefaultServerTransport, up, down, config.DisableUDP, aclEngine, - connectFunc, disconnectFunc, tcpRequestFunc, tcpErrorFunc, udpRequestFunc, udpErrorFunc, trafficCounter) + + s, err := server.NewServer(config) if err != nil { - logrus.WithField("error", err).Fatal("Failed to initialize server") + logger.Fatal("failed to initialize server", zap.Error(err)) } - defer server.Close() - logrus.WithField("addr", config.Listen).Info("Server up and running") + logger.Info("server up and running") - err = server.Serve() - logrus.WithField("error", err).Fatal("Server shutdown") -} - -func disconnectFunc(addr net.Addr, auth []byte, err error) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "error": err, - }).Info("Client disconnected") -} - -func tcpRequestFunc(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - "action": actionToString(action, arg), - }).Debug("TCP request") -} - -func tcpErrorFunc(addr net.Addr, auth []byte, reqAddr string, err error) { - if err != io.EOF { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - "error": err, - }).Info("TCP error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "dst": defaultIPMasker.Mask(reqAddr), - }).Debug("TCP EOF") + if err := s.Serve(); err != nil { + logger.Fatal("failed to serve", zap.Error(err)) } } -func udpRequestFunc(addr net.Addr, auth []byte, sessionID uint32) { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "session": sessionID, - }).Debug("UDP request") -} - -func udpErrorFunc(addr net.Addr, auth []byte, sessionID uint32, err error) { - if err != io.EOF { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "session": sessionID, - "error": err, - }).Info("UDP error") - } else { - logrus.WithFields(logrus.Fields{ - "src": defaultIPMasker.Mask(addr.String()), - "session": sessionID, - }).Debug("UDP EOF") - } -} - -func actionToString(action acl.Action, arg string) string { - switch action { - case acl.ActionDirect: - return "Direct" - case acl.ActionProxy: - return "Proxy" - case acl.ActionBlock: - return "Block" - case acl.ActionHijack: - return "Hijack to " + arg - default: - return "Unknown" - } -} - -func parseServerConfig(cb []byte) (*serverConfig, error) { - var c serverConfig - err := json5.Unmarshal(cb, &c) +func viperToServerConfig() (*server.Config, error) { + // Conn + conn, err := viperToServerConn() if err != nil { return nil, err } - return &c, c.Check() + // TLS + tlsConfig, err := viperToServerTLSConfig() + if err != nil { + return nil, err + } + // QUIC + quicConfig := viperToServerQUICConfig() + // Bandwidth + bwConfig, err := viperToServerBandwidthConfig() + if err != nil { + return nil, err + } + // Disable UDP + disableUDP := viper.GetBool("disableUDP") + // Authenticator + authenticator, err := viperToAuthenticator() + if err != nil { + return nil, err + } + // Config + config := &server.Config{ + TLSConfig: tlsConfig, + QUICConfig: quicConfig, + Conn: conn, + Outbound: nil, // TODO + BandwidthConfig: bwConfig, + DisableUDP: disableUDP, + Authenticator: authenticator, + EventLogger: &serverLogger{}, + MasqHandler: nil, // TODO + } + return config, nil +} + +func viperToServerConn() (net.PacketConn, error) { + listen := viper.GetString("listen") + if listen == "" { + return nil, configError{Field: "listen", Err: errors.New("empty listen address")} + } + uAddr, err := net.ResolveUDPAddr("udp", listen) + if err != nil { + return nil, configError{Field: "listen", Err: err} + } + conn, err := net.ListenUDP("udp", uAddr) + if err != nil { + return nil, configError{Field: "listen", Err: err} + } + return conn, nil +} + +func viperToServerTLSConfig() (server.TLSConfig, error) { + vTLS, vACME := viper.Sub("tls"), viper.Sub("acme") + if vTLS == nil && vACME == nil { + return server.TLSConfig{}, configError{Field: "tls", Err: errors.New("must set either tls or acme")} + } + if vTLS != nil && vACME != nil { + return server.TLSConfig{}, configError{Field: "tls", Err: errors.New("cannot set both tls and acme")} + } + if vTLS != nil { + return viperToServerTLSConfigLocal(vTLS) + } else { + return viperToServerTLSConfigACME(vACME) + } +} + +func viperToServerTLSConfigLocal(v *viper.Viper) (server.TLSConfig, error) { + certPath, keyPath := v.GetString("cert"), v.GetString("key") + if certPath == "" || keyPath == "" { + return server.TLSConfig{}, configError{Field: "tls", Err: errors.New("empty cert or key path")} + } + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return server.TLSConfig{}, configError{Field: "tls", Err: err} + } + return server.TLSConfig{ + Certificates: []tls.Certificate{cert}, + }, nil +} + +func viperToServerTLSConfigACME(v *viper.Viper) (server.TLSConfig, error) { + dataDir := v.GetString("dir") + if dataDir == "" { + dataDir = "acme" + } + + cfg := &certmagic.Config{ + RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio, + KeySource: certmagic.DefaultKeyGenerator, + Storage: &certmagic.FileStorage{Path: dataDir}, + Logger: logger, + } + issuer := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{ + Email: v.GetString("email"), + Agreed: true, + DisableHTTPChallenge: v.GetBool("disableHTTP"), + DisableTLSALPNChallenge: v.GetBool("disableTLSALPN"), + AltHTTPPort: v.GetInt("altHTTPPort"), + AltTLSALPNPort: v.GetInt("altTLSALPNPort"), + Logger: logger, + }) + switch strings.ToLower(v.GetString("ca")) { + case "letsencrypt", "le", "": + // Default to Let's Encrypt + issuer.CA = certmagic.LetsEncryptProductionCA + case "zerossl", "zero": + issuer.CA = certmagic.ZeroSSLProductionCA + default: + return server.TLSConfig{}, configError{Field: "acme.ca", Err: errors.New("unknown CA")} + } + cfg.Issuers = []certmagic.Issuer{issuer} + + cache := certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { + return cfg, nil + }, + Logger: logger, + }) + cfg = certmagic.New(cache, *cfg) + + domains := v.GetStringSlice("domains") + if len(domains) == 0 { + return server.TLSConfig{}, configError{Field: "acme.domains", Err: errors.New("empty domains")} + } + err := cfg.ManageSync(context.Background(), domains) + if err != nil { + return server.TLSConfig{}, configError{Field: "acme", Err: err} + } + return server.TLSConfig{ + GetCertificate: cfg.GetCertificate, + }, nil +} + +func viperToServerQUICConfig() server.QUICConfig { + return server.QUICConfig{ + InitialStreamReceiveWindow: viper.GetUint64("quic.initStreamReceiveWindow"), + MaxStreamReceiveWindow: viper.GetUint64("quic.maxStreamReceiveWindow"), + InitialConnectionReceiveWindow: viper.GetUint64("quic.initConnReceiveWindow"), + MaxConnectionReceiveWindow: viper.GetUint64("quic.maxConnReceiveWindow"), + MaxIdleTimeout: viper.GetDuration("quic.maxIdleTimeout"), + MaxIncomingStreams: viper.GetInt64("quic.maxIncomingStreams"), + DisablePathMTUDiscovery: viper.GetBool("quic.disablePathMTUDiscovery"), + } +} + +func viperToServerBandwidthConfig() (server.BandwidthConfig, error) { + bw := server.BandwidthConfig{} + upStr, downStr := viper.GetString("bandwidth.up"), viper.GetString("bandwidth.down") + if upStr != "" { + up, err := convBandwidth(upStr) + if err != nil { + return server.BandwidthConfig{}, configError{Field: "bandwidth.up", Err: err} + } + bw.MaxTx = up + } + if downStr != "" { + down, err := convBandwidth(downStr) + if err != nil { + return server.BandwidthConfig{}, configError{Field: "bandwidth.down", Err: err} + } + bw.MaxRx = down + } + return bw, nil +} + +func viperToAuthenticator() (server.Authenticator, error) { + authType := viper.GetString("auth.type") + if authType == "" { + return nil, configError{Field: "auth.type", Err: errors.New("empty auth type")} + } + switch authType { + case "password": + pw := viper.GetString("auth.password") + if pw == "" { + return nil, configError{Field: "auth.password", Err: errors.New("empty auth password")} + } + return &auth.PasswordAuthenticator{Password: pw}, nil + default: + return nil, configError{Field: "auth.type", Err: errors.New("unsupported auth type")} + } +} + +type serverLogger struct{} + +func (l *serverLogger) Connect(addr net.Addr, id string, tx uint64) { + logger.Info("client connected", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint64("tx", tx)) +} + +func (l *serverLogger) Disconnect(addr net.Addr, id string, err error) { + logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("id", id), zap.Error(err)) +} + +func (l *serverLogger) TCPRequest(addr net.Addr, id, reqAddr string) { + logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) +} + +func (l *serverLogger) TCPError(addr net.Addr, id, reqAddr string, err error) { + if err == nil { + logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) + } else { + logger.Error("TCP error", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr), zap.Error(err)) + } +} + +func (l *serverLogger) UDPRequest(addr net.Addr, id string, sessionID uint32) { + logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID)) +} + +func (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) { + if err == nil { + logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID)) + } else { + logger.Error("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err)) + } } diff --git a/app/cmd/update.go b/app/cmd/update.go deleted file mode 100644 index cd29595..0000000 --- a/app/cmd/update.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "strings" - "time" - - "github.com/sirupsen/logrus" -) - -const githubAPIURL = "https://api.github.com/repos/apernet/hysteria/releases/latest" - -type releaseInfo struct { - URL string `json:"html_url"` - TagName string `json:"tag_name"` - CreatedAt string `json:"created_at"` - PublishedAt string `json:"published_at"` -} - -func checkUpdate() { - sv := strings.Split(appVersion, "-")[0] - info, err := fetchLatestRelease() - if err == nil && info.TagName != sv { - logrus.WithFields(logrus.Fields{ - "version": info.TagName, - "url": info.URL, - }).Info("New version available") - } -} - -func fetchLatestRelease() (*releaseInfo, error) { - hc := &http.Client{ - Timeout: time.Second * 20, - } - resp, err := hc.Get(githubAPIURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var info releaseInfo - err = json.Unmarshal(body, &info) - return &info, err -} diff --git a/app/cmd/utils.go b/app/cmd/utils.go new file mode 100644 index 0000000..d0970fa --- /dev/null +++ b/app/cmd/utils.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + + "github.com/apernet/hysteria/extras/utils" +) + +// convBandwidth handles both string and int types for bandwidth. +// When using string, it will be parsed as a bandwidth string with units. +// When using int, it will be parsed as a raw bandwidth in bytes per second. +// It does NOT support float types. +func convBandwidth(bw interface{}) (uint64, error) { + switch bwT := bw.(type) { + case string: + return utils.StringToBps(bwT) + case int: + return uint64(bwT), nil + default: + return 0, fmt.Errorf("invalid type %T for bandwidth", bwT) + } +} + +type configError struct { + Field string + Err error +} + +func (e configError) Error() string { + return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err) +} + +func (e configError) Unwrap() error { + return e.Err +} diff --git a/app/go.mod b/app/go.mod index 933c293..1e03b8a 100644 --- a/app/go.mod +++ b/app/go.mod @@ -3,95 +3,56 @@ module github.com/apernet/hysteria/app go 1.20 require ( - github.com/LiamHaworth/go-tproxy v0.0.0-20190726054950-ef7efd7f24ed - github.com/antonfisher/nested-logrus-formatter v1.3.1 github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000 + github.com/apernet/hysteria/extras v0.0.0-00010101000000-000000000000 github.com/caddyserver/certmagic v0.17.2 - github.com/docker/go-units v0.5.0 - github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 - github.com/elazarl/goproxy/ext v0.0.0-20221015165544-a0805db90819 - github.com/folbricht/routedns v0.1.21-0.20230220022436-4ae86ce30d53 - github.com/fsnotify/fsnotify v1.6.0 - github.com/oschwald/geoip2-golang v1.8.0 - github.com/prometheus/client_golang v1.14.0 - github.com/quic-go/quic-go v0.34.0 - github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.6.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.15.0 - github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a - github.com/xjasonlyu/tun2socks/v2 v2.5.0 - github.com/yosuke-furukawa/json5 v0.1.1 - go.uber.org/zap v1.23.0 - gvisor.dev/gvisor v0.0.0-20230401011607-0333bf067633 + github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 + go.uber.org/zap v1.24.0 ) require ( - github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/coreos/go-iptables v0.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f // indirect - github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jtacoma/uritemplates v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.1.1 // indirect github.com/libdns/libdns v0.2.1 // indirect - github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mholt/acmez v1.0.4 // indirect - github.com/miekg/dns v1.1.50 // indirect + github.com/miekg/dns v1.1.51 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/onsi/ginkgo/v2 v2.8.0 // indirect - github.com/oschwald/maxminddb-golang v1.10.0 // indirect + github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect - github.com/pion/dtls/v2 v2.2.4 // indirect - github.com/pion/logging v0.2.2 // indirect - github.com/pion/transport/v2 v2.0.0 // indirect - github.com/pion/udp v0.1.4 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/quic-go/quic-go v0.0.0-00010101000000-000000000000 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect - github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.6.0 // indirect - golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect - golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect - google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.4.0 // indirect + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect + golang.org/x/mod v0.7.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.3.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/apernet/hysteria/core => ../core/ - replace github.com/quic-go/quic-go => github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 -replace github.com/LiamHaworth/go-tproxy => github.com/apernet/go-tproxy v0.0.0-20221025153553-ed04a2935f88 +replace github.com/apernet/hysteria/core => ../core -replace github.com/elazarl/goproxy => github.com/apernet/goproxy v0.0.0-20221124043924-155acfaf278f +replace github.com/apernet/hysteria/extras => ../extras diff --git a/app/go.sum b/app/go.sum index 6d41f98..8b11518 100644 --- a/app/go.sum +++ b/app/go.sum @@ -38,33 +38,13 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91 h1:vX+gnvBc56EbWYrmlhYbFYRaeikAke1GL84N4BEYOFE= -github.com/RackSec/srslog v0.0.0-20180709174129-a4725f04ec91/go.mod h1:cDLGBht23g0XQdLjzn6xOGXDkLK182YfINAaZEQLCHQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ= -github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA= -github.com/apernet/go-tproxy v0.0.0-20221025153553-ed04a2935f88 h1:YNsl7PMiU9x/0CleMHJ7GUdS8y1aRTFwTxdSmLLEijQ= -github.com/apernet/go-tproxy v0.0.0-20221025153553-ed04a2935f88/go.mod h1:uxH+nFzlJug5OHjPYmzKwvVVb9wOToeGuLNVeerwWtc= -github.com/apernet/goproxy v0.0.0-20221124043924-155acfaf278f h1:v3Bn97M5KWzdVajNphf3PxoHdsRF/RzBVovIsH/DEvY= -github.com/apernet/goproxy v0.0.0-20221124043924-155acfaf278f/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 h1:3KFetJ/lUFn0m9xTFg+rMmz2nyHg+D2boJX0Rp4OF6c= github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -72,44 +52,24 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= -github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/elazarl/goproxy/ext v0.0.0-20221015165544-a0805db90819 h1:PBc3oUutXxwCibSLQCmpunGvruDnoS6kdnaL7a0xwKY= -github.com/elazarl/goproxy/ext v0.0.0-20221015165544-a0805db90819/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/folbricht/routedns v0.1.21-0.20230220022436-4ae86ce30d53 h1:jbMwVtjBl/cQM+l+xjGtV2C3jdpOTCZW2U3kIUf7Czg= -github.com/folbricht/routedns v0.1.21-0.20230220022436-4ae86ce30d53/go.mod h1:Kig320CyqKR4G/JQ9G5QLJaUkkGoySJjgUXRjGnnQ0I= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -137,13 +97,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -153,11 +109,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -171,8 +123,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f h1:gl1DCiSk+mrXXBGPm6CEeS2MkJuMVzAOrXg34oVj1QI= -github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -180,133 +132,62 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= -github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtacoma/uritemplates v1.0.0 h1:xwx5sBF7pPAb0Uj8lDC1Q/aBPpOFyQza7OC705ZlLCo= -github.com/jtacoma/uritemplates v1.0.0/go.mod h1:IhIICdE9OcvgUnGwTtJxgBQ+VrTrti5PcbLVSJianO8= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0= github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo= +github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI= -github.com/onsi/ginkgo/v2 v2.8.0/go.mod h1:6JsQiECmxCa3V5st74AL/AmsV482EDdVrGaVW6z3oYU= -github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= -github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= -github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= -github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= -github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= -github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg= -github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4= -github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= -github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= -github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -314,7 +195,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -330,14 +210,8 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= -github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a h1:BOqgJ4jku0LHPDoR51RD8Mxmo0LHxCzJT/M9MemYdHo= -github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a/go.mod h1:7NloQcrxaZYKURWph5HLxVDlIwMHJXCPkeWPtpftsIg= -github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe h1:gMWxZxBFRAXqoGkwkYlPX2zvyyKNWJpxOxCrjqJkm5A= -github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4= -github.com/xjasonlyu/tun2socks/v2 v2.5.0 h1:eV/8PUZnRBd7CA3x6eE8xnHOHg072MzWMv+en+c5pPg= -github.com/xjasonlyu/tun2socks/v2 v2.5.0/go.mod h1:iDkMNAYEUDN651NClepI2eRqwUnk3qVKyex/NzubffQ= -github.com/yosuke-furukawa/json5 v0.1.1 h1:0F9mNwTvOuDNH243hoPqvf+dxa5QsKnZzU20uNsh3ZI= -github.com/yosuke-furukawa/json5 v0.1.1/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= +github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -351,17 +225,16 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -370,9 +243,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -383,8 +255,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 h1:BEABXpNXLEz0WxtA+6CQIz2xkg80e+1zrhWyMcq8VzE= -golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -410,11 +282,10 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -422,7 +293,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -446,16 +316,11 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -465,8 +330,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -480,13 +343,11 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -495,7 +356,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -508,8 +368,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -517,48 +375,34 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -608,18 +452,13 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= -golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= -golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= -golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -708,31 +547,19 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ= -google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20230401011607-0333bf067633 h1:trSjCkJT04PkOY8WayMOlk9MV6SK2LqvMBRnoi9jy5E= -gvisor.dev/gvisor v0.0.0-20230401011607-0333bf067633/go.mod h1:pzr6sy8gDLfVmDAg8OYrlKvGEHw5C3PGTiBXBTCx76Q= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/app/http/server.go b/app/http/server.go deleted file mode 100644 index 5e08e12..0000000 --- a/app/http/server.go +++ /dev/null @@ -1,91 +0,0 @@ -package http - -import ( - "errors" - "fmt" - "net" - "net/http" - "time" - - "github.com/apernet/hysteria/core/transport" - "github.com/apernet/hysteria/core/utils" - - "github.com/elazarl/goproxy/ext/auth" - - "github.com/apernet/hysteria/core/acl" - "github.com/apernet/hysteria/core/cs" - "github.com/elazarl/goproxy" -) - -func NewProxyHTTPServer(hyClient *cs.Client, transport *transport.ClientTransport, idleTimeout time.Duration, - aclEngine *acl.Engine, - basicAuthFunc func(user, password string) bool, - newDialFunc func(reqAddr string, action acl.Action, arg string), - proxyErrorFunc func(reqAddr string, err error), -) (*goproxy.ProxyHttpServer, error) { - proxy := goproxy.NewProxyHttpServer() - proxy.Logger = &nopLogger{} - proxy.NonproxyHandler = http.NotFoundHandler() - proxy.Tr = &http.Transport{ - Dial: func(network, addr string) (conn net.Conn, err error) { - defer func() { - if err != nil { - proxyErrorFunc(addr, err) - } - }() - // Parse addr string - host, port, err := utils.SplitHostPort(addr) - if err != nil { - return nil, err - } - // ACL - action, arg := acl.ActionProxy, "" - var ipAddr *net.IPAddr - var resErr error - if aclEngine != nil { - action, arg, _, ipAddr, resErr = aclEngine.ResolveAndMatch(host, port, false) - // Doesn't always matter if the resolution fails, as we may send it through HyClient - } - newDialFunc(addr, action, arg) - // Handle according to the action - switch action { - case acl.ActionDirect: - if resErr != nil { - return nil, resErr - } - return transport.DialTCP(&net.TCPAddr{ - IP: ipAddr.IP, - Port: int(port), - Zone: ipAddr.Zone, - }) - case acl.ActionProxy: - return hyClient.DialTCP(addr) - case acl.ActionBlock: - return nil, errors.New("blocked by ACL") - case acl.ActionHijack: - hijackIPAddr, err := transport.ResolveIPAddr(arg) - if err != nil { - return nil, err - } - return transport.DialTCP(&net.TCPAddr{ - IP: hijackIPAddr.IP, - Port: int(port), - Zone: hijackIPAddr.Zone, - }) - default: - return nil, fmt.Errorf("unknown action %d", action) - } - }, - IdleConnTimeout: idleTimeout, - // Disable HTTP2 support? ref: https://github.com/elazarl/goproxy/issues/361 - } - proxy.ConnectDial = nil - if basicAuthFunc != nil { - auth.ProxyBasic(proxy, "hysteria", basicAuthFunc) - } - return proxy, nil -} - -type nopLogger struct{} - -func (n *nopLogger) Printf(format string, v ...interface{}) {} diff --git a/app/internal/socks5/server.go b/app/internal/socks5/server.go new file mode 100644 index 0000000..5fefb45 --- /dev/null +++ b/app/internal/socks5/server.go @@ -0,0 +1,294 @@ +package socks5 + +import ( + "encoding/binary" + "io" + "net" + + "github.com/txthinking/socks5" + + "github.com/apernet/hysteria/core/client" +) + +const udpBufferSize = 4096 + +// Server is a SOCKS5 server using a Hysteria client as outbound. +type Server struct { + HyClient client.Client + AuthFunc func(username, password string) bool // nil = no authentication + DisableUDP bool + EventLogger EventLogger +} + +type EventLogger interface { + TCPRequest(addr net.Addr, reqAddr string) + TCPError(addr net.Addr, reqAddr string, err error) + UDPRequest(addr net.Addr) + UDPError(addr net.Addr, err error) +} + +func (s *Server) Serve(listener net.Listener) error { + for { + conn, err := listener.Accept() + if err != nil { + return err + } + go s.dispatch(conn) + } +} + +func (s *Server) dispatch(conn net.Conn) { + ok, _ := s.negotiate(conn) + if !ok { + _ = conn.Close() + return + } + // Negotiation ok, get and handle the request + req, err := socks5.NewRequestFrom(conn) + if err != nil { + _ = conn.Close() + return + } + switch req.Cmd { + case socks5.CmdConnect: // TCP + s.handleTCP(conn, req) + case socks5.CmdUDP: // UDP + if s.DisableUDP { + _ = sendSimpleReply(conn, socks5.RepCommandNotSupported) + _ = conn.Close() + return + } + s.handleUDP(conn, req) + default: + _ = sendSimpleReply(conn, socks5.RepCommandNotSupported) + _ = conn.Close() + } +} + +func (s *Server) negotiate(conn net.Conn) (bool, error) { + req, err := socks5.NewNegotiationRequestFrom(conn) + if err != nil { + return false, err + } + var serverMethod byte + if s.AuthFunc != nil { + serverMethod = socks5.MethodUsernamePassword + } else { + serverMethod = socks5.MethodNone + } + // Look for the supported method in the client request + supported := false + for _, m := range req.Methods { + if m == serverMethod { + supported = true + break + } + } + if !supported { + // No supported method found, reject the client + rep := socks5.NewNegotiationReply(socks5.MethodUnsupportAll) + _, err := rep.WriteTo(conn) + return false, err + } + // OK, send the method we chose + rep := socks5.NewNegotiationReply(serverMethod) + _, err = rep.WriteTo(conn) + if err != nil { + return false, err + } + // If we chose the username/password method, authenticate the client + if serverMethod == socks5.MethodUsernamePassword { + req, err := socks5.NewUserPassNegotiationRequestFrom(conn) + if err != nil { + return false, err + } + ok := s.AuthFunc(string(req.Uname), string(req.Passwd)) + if ok { + rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess) + _, err := rep.WriteTo(conn) + if err != nil { + return false, err + } + } else { + rep := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure) + _, err := rep.WriteTo(conn) + return false, err + } + } + return true, nil +} + +func (s *Server) handleTCP(conn net.Conn, req *socks5.Request) { + defer conn.Close() + + addr := req.Address() + + // TCP request & error log + if s.EventLogger != nil { + s.EventLogger.TCPRequest(conn.RemoteAddr(), addr) + } + var closeErr error + defer func() { + if s.EventLogger != nil { + s.EventLogger.TCPError(conn.RemoteAddr(), addr, closeErr) + } + }() + + // Dial + rConn, err := s.HyClient.DialTCP(addr) + if err != nil { + _ = sendSimpleReply(conn, socks5.RepHostUnreachable) + closeErr = err + return + } + defer rConn.Close() + + // Send reply and start relaying + _ = sendSimpleReply(conn, socks5.RepSuccess) + copyErrChan := make(chan error, 2) + go func() { + _, err := io.Copy(rConn, conn) + copyErrChan <- err + }() + go func() { + _, err := io.Copy(conn, rConn) + copyErrChan <- err + }() + closeErr = <-copyErrChan +} + +func (s *Server) handleUDP(conn net.Conn, req *socks5.Request) { + defer conn.Close() + + // UDP request & error log + if s.EventLogger != nil { + s.EventLogger.UDPRequest(conn.RemoteAddr()) + } + var closeErr error + defer func() { + if s.EventLogger != nil { + s.EventLogger.UDPError(conn.RemoteAddr(), closeErr) + } + }() + + // Start UDP relay server + // SOCKS5 UDP requires the server to return the UDP bind address and port in the reply. + // We bind to the same address that our TCP server listens on (but a different port). + host, _, err := net.SplitHostPort(conn.LocalAddr().String()) + if err != nil { + // Is this even possible? + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + udpAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, "0")) + if err != nil { + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + defer udpConn.Close() + + // HyClient UDP session + hyUDP, err := s.HyClient.ListenUDP() + if err != nil { + _ = sendSimpleReply(conn, socks5.RepServerFailure) + closeErr = err + return + } + defer hyUDP.Close() + + // Send reply + _ = sendUDPReply(conn, udpConn.LocalAddr().(*net.UDPAddr)) + + // UDP relay & SOCKS5 connection holder + errChan := make(chan error, 2) + go func() { + err := s.udpServer(udpConn, hyUDP) + errChan <- err + }() + go func() { + _, err := io.Copy(io.Discard, conn) + errChan <- err + }() + closeErr = <-errChan +} + +func (s *Server) udpServer(udpConn *net.UDPConn, hyUDP client.HyUDPConn) error { + var clientAddr *net.UDPAddr + buf := make([]byte, udpBufferSize) + // local -> remote + for { + n, cAddr, err := udpConn.ReadFromUDP(buf) + if err != nil { + return err + } + d, err := socks5.NewDatagramFromBytes(buf[:n]) + if err != nil || d.Frag != 0 { + // Ignore bad packets + // Also we don't support SOCKS5 UDP fragmentation for now + continue + } + if clientAddr == nil { + // Before the first packet, we don't know what IP the client will use to send us packets, + // so we don't know what IP to return packets to. + // We treat whoever sends us the first packet as our client. + clientAddr = cAddr + // Now that we know the client's address, we can start the + // remote -> local direction. + go func() { + for { + bs, from, err := hyUDP.Receive() + if err != nil { + // Close the UDP conn so that the local -> remote direction will exit + _ = udpConn.Close() + return + } + atyp, addr, port, err := socks5.ParseAddress(from) + if err != nil { + continue + } + d := socks5.NewDatagram(atyp, addr, port, bs) + _, _ = udpConn.WriteToUDP(d.Bytes(), clientAddr) + } + }() + } else if !clientAddr.IP.Equal(cAddr.IP) || clientAddr.Port != cAddr.Port { + // Not our client, ignore + continue + } + // Send to remote + _ = hyUDP.Send(d.Data, d.Address()) + } +} + +// sendSimpleReply sends a SOCKS5 reply with the given reply code. +// It does not contain bind address or port, so it's not suitable for successful UDP requests. +func sendSimpleReply(conn net.Conn, rep byte) error { + p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00}) + _, err := p.WriteTo(conn) + return err +} + +// sendUDPReply sends a SOCKS5 reply with the given reply code and bind address/port. +func sendUDPReply(conn net.Conn, addr *net.UDPAddr) error { + var atyp byte + var bndAddr, bndPort []byte + if ip4 := addr.IP.To4(); ip4 != nil { + atyp = socks5.ATYPIPv4 + bndAddr = ip4 + } else { + atyp = socks5.ATYPIPv6 + bndAddr = addr.IP + } + bndPort = make([]byte, 2) + binary.BigEndian.PutUint16(bndPort, uint16(addr.Port)) + p := socks5.NewReply(socks5.RepSuccess, atyp, bndAddr, bndPort) + _, err := p.WriteTo(conn) + return err +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..6d1554b --- /dev/null +++ b/app/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/apernet/hysteria/app/cmd" + +func main() { + cmd.Execute() +} diff --git a/docs/socks5/check.py b/app/misc/socks5_test.py similarity index 79% rename from docs/socks5/check.py rename to app/misc/socks5_test.py index be1b6a0..ef9562a 100644 --- a/docs/socks5/check.py +++ b/app/misc/socks5_test.py @@ -1,16 +1,15 @@ import socket -import time - import socks +import time TARGET = "1.1.1.1" -def check_tcp() -> None: +def test_tcp() -> None: s = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) - print(f"Sending HTTP request to {TARGET}") + print(f"TCP - Sending HTTP request to {TARGET}") start = time.time() s.connect((TARGET, 80)) s.send(b"GET / HTTP/1.1\r\nHost: " + TARGET.encode() + b"\r\n\r\n") @@ -20,26 +19,26 @@ def check_tcp() -> None: elif not data.startswith(b"HTTP/1.1 "): print("Invalid response received") else: - print("Response received") + print("TCP test passed") end = time.time() s.close() print(f"Time: {round((end - start) * 1000, 2)} ms") -def check_udp() -> None: +def test_udp() -> None: s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM) s.set_proxy(socks.SOCKS5, "127.0.0.1", 1080) req = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05\x62\x61\x69\x64\x75\x03\x63\x6f\x6d\x00\x00\x01\x00\x01" - print(f"Sending DNS request to {TARGET}") + print(f"UDP - Sending DNS request to {TARGET}") start = time.time() s.sendto(req, (TARGET, 53)) (rsp, address) = s.recvfrom(4096) if address[0] == TARGET and address[1] == 53 and rsp[0] == req[0] and rsp[1] == req[1]: - print("UDP check passed") + print("UDP test passed") else: - print("Invalid response") + print("Invalid response received") end = time.time() s.close() @@ -47,5 +46,5 @@ def check_udp() -> None: if __name__ == "__main__": - check_tcp() - check_udp() + test_tcp() + test_udp() diff --git a/app/redirect/getsockopt_linux.go b/app/redirect/getsockopt_linux.go deleted file mode 100644 index 33c8cf4..0000000 --- a/app/redirect/getsockopt_linux.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !386 -// +build !386 - -package redirect - -import ( - "syscall" - "unsafe" -) - -func getsockopt(s uintptr, level uintptr, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { - _, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) - if e != 0 { - err = e - } - return -} diff --git a/app/redirect/getsockopt_linux_386.go b/app/redirect/getsockopt_linux_386.go deleted file mode 100644 index de930ec..0000000 --- a/app/redirect/getsockopt_linux_386.go +++ /dev/null @@ -1,22 +0,0 @@ -package redirect - -import ( - "syscall" - "unsafe" -) - -const ( - SYS_GETSOCKOPT = 15 -) - -// we cannot call socketcall with syscall.Syscall6, it always fails with EFAULT. -// we have to call syscall.socketcall with this trick. -func syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) - -func getsockopt(s uintptr, level uintptr, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { - _, e := syscall_socketcall(SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) - if e != 0 { - err = e - } - return -} diff --git a/app/redirect/origdst_linux.go b/app/redirect/origdst_linux.go deleted file mode 100644 index 3907bc7..0000000 --- a/app/redirect/origdst_linux.go +++ /dev/null @@ -1,33 +0,0 @@ -package redirect - -import ( - "syscall" - "unsafe" -) - -const ( - SO_ORIGINAL_DST = 80 - IP6T_SO_ORIGINAL_DST = 80 -) - -type sockAddr struct { - family uint16 - port [2]byte // big endian regardless of host byte order - data [24]byte // check sockaddr_in or sockaddr_in6 for more information -} - -func getOrigDst(fd uintptr) (*sockAddr, error) { - var addr sockAddr - addrSize := uint32(unsafe.Sizeof(addr)) - // try IPv6 first - err := getsockopt(fd, syscall.SOL_IPV6, IP6T_SO_ORIGINAL_DST, unsafe.Pointer(&addr), &addrSize) - if err != nil { - // try IPv4 - err = getsockopt(fd, syscall.SOL_IP, SO_ORIGINAL_DST, unsafe.Pointer(&addr), &addrSize) - if err != nil { - // failed - return nil, err - } - } - return &addr, nil -} diff --git a/app/redirect/syscall_socketcall_linux_386.s b/app/redirect/syscall_socketcall_linux_386.s deleted file mode 100644 index 2dab43a..0000000 --- a/app/redirect/syscall_socketcall_linux_386.s +++ /dev/null @@ -1,7 +0,0 @@ -//go:build gc -// +build gc - -#include "textflag.h" - -TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36 - JMP syscall·socketcall(SB) diff --git a/app/redirect/tcp_linux.go b/app/redirect/tcp_linux.go deleted file mode 100644 index 0845690..0000000 --- a/app/redirect/tcp_linux.go +++ /dev/null @@ -1,97 +0,0 @@ -package redirect - -import ( - "encoding/binary" - "errors" - "net" - "syscall" - "time" - - "github.com/apernet/hysteria/core/cs" - "github.com/apernet/hysteria/core/utils" -) - -type TCPRedirect struct { - HyClient *cs.Client - ListenAddr *net.TCPAddr - Timeout time.Duration - - ConnFunc func(addr, reqAddr net.Addr) - ErrorFunc func(addr, reqAddr net.Addr, err error) -} - -func NewTCPRedirect(hyClient *cs.Client, listen string, timeout time.Duration, - connFunc func(addr, reqAddr net.Addr), - errorFunc func(addr, reqAddr net.Addr, err error), -) (*TCPRedirect, error) { - tAddr, err := net.ResolveTCPAddr("tcp", listen) - if err != nil { - return nil, err - } - r := &TCPRedirect{ - HyClient: hyClient, - ListenAddr: tAddr, - Timeout: timeout, - ConnFunc: connFunc, - ErrorFunc: errorFunc, - } - return r, nil -} - -func (r *TCPRedirect) ListenAndServe() error { - listener, err := net.ListenTCP("tcp", r.ListenAddr) - if err != nil { - return err - } - defer listener.Close() - for { - c, err := listener.Accept() - if err != nil { - return err - } - go func() { - defer c.Close() - dest, err := getDestAddr(c.(*net.TCPConn)) - if err != nil || dest.IP.IsLoopback() { - // Silently drop the connection if we failed to get the destination address, - // or if it's a loopback address (not a redirected connection). - return - } - r.ConnFunc(c.RemoteAddr(), dest) - rc, err := r.HyClient.DialTCP(dest.String()) - if err != nil { - r.ErrorFunc(c.RemoteAddr(), dest, err) - return - } - defer rc.Close() - err = utils.PipePairWithTimeout(c, rc, r.Timeout) - r.ErrorFunc(c.RemoteAddr(), dest, err) - }() - } -} - -func getDestAddr(conn *net.TCPConn) (*net.TCPAddr, error) { - rc, err := conn.SyscallConn() - if err != nil { - return nil, err - } - var addr *sockAddr - var err2 error - err = rc.Control(func(fd uintptr) { - addr, err2 = getOrigDst(fd) - }) - if err != nil { - return nil, err - } - if err2 != nil { - return nil, err2 - } - switch addr.family { - case syscall.AF_INET: - return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil - case syscall.AF_INET6: - return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil - default: - return nil, errors.New("unknown address family") - } -} diff --git a/app/redirect/tcp_stub.go b/app/redirect/tcp_stub.go deleted file mode 100644 index 353abb1..0000000 --- a/app/redirect/tcp_stub.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !linux -// +build !linux - -package redirect - -import ( - "errors" - "net" - "time" - - "github.com/apernet/hysteria/core/cs" -) - -type TCPRedirect struct{} - -func NewTCPRedirect(hyClient *cs.Client, listen string, timeout time.Duration, - connFunc func(addr, reqAddr net.Addr), - errorFunc func(addr, reqAddr net.Addr, err error), -) (*TCPRedirect, error) { - return nil, errors.New("not supported on the current system") -} - -func (r *TCPRedirect) ListenAndServe() error { - return nil -} diff --git a/app/relay/tcp.go b/app/relay/tcp.go deleted file mode 100644 index d09ce9b..0000000 --- a/app/relay/tcp.go +++ /dev/null @@ -1,63 +0,0 @@ -package relay - -import ( - "net" - "time" - - "github.com/apernet/hysteria/core/cs" - "github.com/apernet/hysteria/core/utils" -) - -type TCPRelay struct { - HyClient *cs.Client - ListenAddr *net.TCPAddr - Remote string - Timeout time.Duration - - ConnFunc func(addr net.Addr) - ErrorFunc func(addr net.Addr, err error) -} - -func NewTCPRelay(hyClient *cs.Client, listen, remote string, timeout time.Duration, - connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error), -) (*TCPRelay, error) { - tAddr, err := net.ResolveTCPAddr("tcp", listen) - if err != nil { - return nil, err - } - r := &TCPRelay{ - HyClient: hyClient, - ListenAddr: tAddr, - Remote: remote, - Timeout: timeout, - ConnFunc: connFunc, - ErrorFunc: errorFunc, - } - return r, nil -} - -func (r *TCPRelay) ListenAndServe() error { - listener, err := net.ListenTCP("tcp", r.ListenAddr) - if err != nil { - return err - } - defer listener.Close() - for { - c, err := listener.AcceptTCP() - if err != nil { - return err - } - go func() { - defer c.Close() - r.ConnFunc(c.RemoteAddr()) - rc, err := r.HyClient.DialTCP(r.Remote) - if err != nil { - r.ErrorFunc(c.RemoteAddr(), err) - return - } - defer rc.Close() - err = utils.PipePairWithTimeout(c, rc, r.Timeout) - r.ErrorFunc(c.RemoteAddr(), err) - }() - } -} diff --git a/app/relay/udp.go b/app/relay/udp.go deleted file mode 100644 index a165c32..0000000 --- a/app/relay/udp.go +++ /dev/null @@ -1,124 +0,0 @@ -package relay - -import ( - "errors" - "net" - "sync" - "sync/atomic" - "time" - - "github.com/apernet/hysteria/core/cs" -) - -const udpBufferSize = 4096 - -var ErrTimeout = errors.New("inactivity timeout") - -type UDPRelay struct { - HyClient *cs.Client - ListenAddr *net.UDPAddr - Remote string - Timeout time.Duration - - ConnFunc func(addr net.Addr) - ErrorFunc func(addr net.Addr, err error) -} - -func NewUDPRelay(hyClient *cs.Client, listen, remote string, timeout time.Duration, - connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error), -) (*UDPRelay, error) { - uAddr, err := net.ResolveUDPAddr("udp", listen) - if err != nil { - return nil, err - } - r := &UDPRelay{ - HyClient: hyClient, - ListenAddr: uAddr, - Remote: remote, - Timeout: timeout, - ConnFunc: connFunc, - ErrorFunc: errorFunc, - } - if timeout == 0 { - r.Timeout = 1 * time.Minute - } - return r, nil -} - -type connEntry struct { - HyConn cs.HyUDPConn - Deadline atomic.Value -} - -func (r *UDPRelay) ListenAndServe() error { - conn, err := net.ListenUDP("udp", r.ListenAddr) - if err != nil { - return err - } - defer conn.Close() - // src <-> HyClient HyUDPConn - connMap := make(map[string]*connEntry) - var connMapMutex sync.RWMutex - // Read loop - buf := make([]byte, udpBufferSize) - for { - n, rAddr, err := conn.ReadFromUDP(buf) - if n > 0 { - connMapMutex.RLock() - entry := connMap[rAddr.String()] - connMapMutex.RUnlock() - if entry != nil { - // Existing conn - entry.Deadline.Store(time.Now().Add(r.Timeout)) - _ = entry.HyConn.WriteTo(buf[:n], r.Remote) - } else { - // New - r.ConnFunc(rAddr) - hyConn, err := r.HyClient.DialUDP() - if err != nil { - r.ErrorFunc(rAddr, err) - } else { - // Add it to the map - entry := &connEntry{HyConn: hyConn} - entry.Deadline.Store(time.Now().Add(r.Timeout)) - connMapMutex.Lock() - connMap[rAddr.String()] = entry - connMapMutex.Unlock() - // Start remote to local - go func() { - for { - bs, _, err := hyConn.ReadFrom() - if err != nil { - break - } - entry.Deadline.Store(time.Now().Add(r.Timeout)) - _, _ = conn.WriteToUDP(bs, rAddr) - } - }() - // Timeout cleanup routine - go func() { - for { - ttl := entry.Deadline.Load().(time.Time).Sub(time.Now()) - if ttl <= 0 { - // Time to die - connMapMutex.Lock() - _ = hyConn.Close() - delete(connMap, rAddr.String()) - connMapMutex.Unlock() - r.ErrorFunc(rAddr, ErrTimeout) - return - } else { - time.Sleep(ttl) - } - } - }() - // Send the packet - _ = hyConn.WriteTo(buf[:n], r.Remote) - } - } - } - if err != nil { - return err - } - } -} diff --git a/app/server.example.yaml b/app/server.example.yaml new file mode 100644 index 0000000..179e419 --- /dev/null +++ b/app/server.example.yaml @@ -0,0 +1,35 @@ +listen: :443 + +# tls: +# cert: my.crt +# key: my.key + +acme: + domains: + - my.example.com + email: hackerman@abcd.com + # ca: LetsEncrypt + # disableHTTP: false + # disableTLSALPN: false + # altHTTPPort: 80 + # altTLSALPNPort: 443 + # dir: "custom_dir" + +# quic: +# initStreamReceiveWindow: 8388608 +# maxStreamReceiveWindow: 8388608 +# initConnReceiveWindow: 20971520 +# maxConnReceiveWindow: 20971520 +# maxIdleTimeout: 130s +# maxIncomingStreams: 1024 +# disablePathMTUDiscovery: false + +# bandwidth: +# up: "100 mbps" +# down: "100 mbps" +# +# disableUDP: false + +auth: + type: "password" + password: "hello world" diff --git a/app/socks5/server.go b/app/socks5/server.go deleted file mode 100644 index 58255d8..0000000 --- a/app/socks5/server.go +++ /dev/null @@ -1,442 +0,0 @@ -package socks5 - -import ( - "encoding/binary" - "errors" - "fmt" - "strconv" - - "github.com/apernet/hysteria/core/acl" - "github.com/apernet/hysteria/core/cs" - "github.com/apernet/hysteria/core/transport" - "github.com/apernet/hysteria/core/utils" -) - -import ( - "net" - "time" - - "github.com/txthinking/socks5" -) - -const udpBufferSize = 4096 - -var ( - ErrUnsupportedCmd = errors.New("unsupported command") - ErrUserPassAuth = errors.New("invalid username or password") -) - -type Server struct { - HyClient *cs.Client - Transport *transport.ClientTransport - AuthFunc func(username, password string) bool - Method byte - TCPAddr *net.TCPAddr - TCPTimeout time.Duration - ACLEngine *acl.Engine - DisableUDP bool - - TCPRequestFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string) - TCPErrorFunc func(addr net.Addr, reqAddr string, err error) - UDPAssociateFunc func(addr net.Addr) - UDPErrorFunc func(addr net.Addr, err error) - - tcpListener *net.TCPListener -} - -func NewServer(hyClient *cs.Client, transport *transport.ClientTransport, addr string, - authFunc func(username, password string) bool, tcpTimeout time.Duration, - aclEngine *acl.Engine, disableUDP bool, - tcpReqFunc func(addr net.Addr, reqAddr string, action acl.Action, arg string), - tcpErrorFunc func(addr net.Addr, reqAddr string, err error), - udpAssocFunc func(addr net.Addr), udpErrorFunc func(addr net.Addr, err error), -) (*Server, error) { - tAddr, err := net.ResolveTCPAddr("tcp", addr) - if err != nil { - return nil, err - } - m := socks5.MethodNone - if authFunc != nil { - m = socks5.MethodUsernamePassword - } - s := &Server{ - HyClient: hyClient, - Transport: transport, - AuthFunc: authFunc, - Method: m, - TCPAddr: tAddr, - TCPTimeout: tcpTimeout, - ACLEngine: aclEngine, - DisableUDP: disableUDP, - TCPRequestFunc: tcpReqFunc, - TCPErrorFunc: tcpErrorFunc, - UDPAssociateFunc: udpAssocFunc, - UDPErrorFunc: udpErrorFunc, - } - return s, nil -} - -func (s *Server) negotiate(c *net.TCPConn) error { - rq, err := socks5.NewNegotiationRequestFrom(c) - if err != nil { - return err - } - var got bool - var m byte - for _, m = range rq.Methods { - if m == s.Method { - got = true - } - } - if !got { - rp := socks5.NewNegotiationReply(socks5.MethodUnsupportAll) - if _, err := rp.WriteTo(c); err != nil { - return err - } - } - rp := socks5.NewNegotiationReply(s.Method) - if _, err := rp.WriteTo(c); err != nil { - return err - } - - if s.Method == socks5.MethodUsernamePassword { - urq, err := socks5.NewUserPassNegotiationRequestFrom(c) - if err != nil { - return err - } - if !s.AuthFunc(string(urq.Uname), string(urq.Passwd)) { - urp := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusFailure) - if _, err := urp.WriteTo(c); err != nil { - return err - } - return ErrUserPassAuth - } - urp := socks5.NewUserPassNegotiationReply(socks5.UserPassStatusSuccess) - if _, err := urp.WriteTo(c); err != nil { - return err - } - } - return nil -} - -func (s *Server) ListenAndServe() error { - var err error - s.tcpListener, err = net.ListenTCP("tcp", s.TCPAddr) - if err != nil { - return err - } - defer s.tcpListener.Close() - for { - c, err := s.tcpListener.AcceptTCP() - if err != nil { - return err - } - go func() { - defer c.Close() - if s.TCPTimeout != 0 { - if err := c.SetDeadline(time.Now().Add(s.TCPTimeout)); err != nil { - return - } - } - if err := s.negotiate(c); err != nil { - return - } - r, err := socks5.NewRequestFrom(c) - if err != nil { - return - } - _ = s.handle(c, r) - }() - } -} - -func (s *Server) handle(c *net.TCPConn, r *socks5.Request) error { - if r.Cmd == socks5.CmdConnect { - // TCP - return s.handleTCP(c, r) - } else if r.Cmd == socks5.CmdUDP { - // UDP - if !s.DisableUDP { - return s.handleUDP(c, r) - } else { - _ = sendReply(c, socks5.RepCommandNotSupported) - return ErrUnsupportedCmd - } - } else { - _ = sendReply(c, socks5.RepCommandNotSupported) - return ErrUnsupportedCmd - } -} - -func (s *Server) handleTCP(c *net.TCPConn, r *socks5.Request) error { - host, port, addr := parseRequestAddress(r) - action, arg := acl.ActionProxy, "" - var ipAddr *net.IPAddr - var resErr error - if s.ACLEngine != nil { - action, arg, _, ipAddr, resErr = s.ACLEngine.ResolveAndMatch(host, port, false) - // Doesn't always matter if the resolution fails, as we may send it through HyClient - } - s.TCPRequestFunc(c.RemoteAddr(), addr, action, arg) - var closeErr error - defer func() { - s.TCPErrorFunc(c.RemoteAddr(), addr, closeErr) - }() - // Handle according to the action - switch action { - case acl.ActionDirect: - if resErr != nil { - _ = sendReply(c, socks5.RepHostUnreachable) - closeErr = resErr - return resErr - } - rc, err := s.Transport.DialTCP(&net.TCPAddr{ - IP: ipAddr.IP, - Port: int(port), - Zone: ipAddr.Zone, - }) - if err != nil { - _ = sendReply(c, socks5.RepHostUnreachable) - closeErr = err - return err - } - defer rc.Close() - _ = sendReply(c, socks5.RepSuccess) - closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout) - return nil - case acl.ActionProxy: - rc, err := s.HyClient.DialTCP(addr) - if err != nil { - _ = sendReply(c, socks5.RepHostUnreachable) - closeErr = err - return err - } - defer rc.Close() - _ = sendReply(c, socks5.RepSuccess) - closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout) - return nil - case acl.ActionBlock: - _ = sendReply(c, socks5.RepHostUnreachable) - closeErr = errors.New("blocked in ACL") - return nil - case acl.ActionHijack: - hijackIPAddr, err := s.Transport.ResolveIPAddr(arg) - if err != nil { - _ = sendReply(c, socks5.RepHostUnreachable) - closeErr = err - return err - } - rc, err := s.Transport.DialTCP(&net.TCPAddr{ - IP: hijackIPAddr.IP, - Port: int(port), - Zone: hijackIPAddr.Zone, - }) - if err != nil { - _ = sendReply(c, socks5.RepHostUnreachable) - closeErr = err - return err - } - defer rc.Close() - _ = sendReply(c, socks5.RepSuccess) - closeErr = utils.PipePairWithTimeout(c, rc, s.TCPTimeout) - return nil - default: - _ = sendReply(c, socks5.RepServerFailure) - closeErr = fmt.Errorf("unknown action %d", action) - return nil - } -} - -func (s *Server) handleUDP(c *net.TCPConn, r *socks5.Request) error { - s.UDPAssociateFunc(c.RemoteAddr()) - var closeErr error - defer func() { - s.UDPErrorFunc(c.RemoteAddr(), closeErr) - }() - // Start local UDP server - udpConn, err := net.ListenUDP("udp", &net.UDPAddr{ - IP: s.TCPAddr.IP, - Zone: s.TCPAddr.Zone, - }) - if err != nil { - _ = sendReply(c, socks5.RepServerFailure) - closeErr = err - return err - } - defer udpConn.Close() - // Local UDP relay conn for ACL Direct - var localRelayConn *net.UDPConn - if s.ACLEngine != nil { - localRelayConn, err = s.Transport.ListenUDP() - if err != nil { - _ = sendReply(c, socks5.RepServerFailure) - closeErr = err - return err - } - defer localRelayConn.Close() - } - // HyClient UDP session - hyUDP, err := s.HyClient.DialUDP() - if err != nil { - _ = sendReply(c, socks5.RepServerFailure) - closeErr = err - return err - } - defer hyUDP.Close() - // Send UDP server addr to the client - // Same IP as TCP but a different port - tcpLocalAddr := c.LocalAddr().(*net.TCPAddr) - var atyp byte - var addr, port []byte - if ip4 := tcpLocalAddr.IP.To4(); ip4 != nil { - atyp = socks5.ATYPIPv4 - addr = ip4 - } else if ip6 := tcpLocalAddr.IP.To16(); ip6 != nil { - atyp = socks5.ATYPIPv6 - addr = ip6 - } else { - _ = sendReply(c, socks5.RepServerFailure) - closeErr = errors.New("invalid local addr") - return closeErr - } - port = make([]byte, 2) - binary.BigEndian.PutUint16(port, uint16(udpConn.LocalAddr().(*net.UDPAddr).Port)) - _, _ = socks5.NewReply(socks5.RepSuccess, atyp, addr, port).WriteTo(c) - // Let UDP server do its job, we hold the TCP connection here - go s.udpServer(udpConn, localRelayConn, hyUDP) - if s.TCPTimeout != 0 { - // Disable TCP timeout for UDP holder - _ = c.SetDeadline(time.Time{}) - } - buf := make([]byte, 1024) - for { - _, err := c.Read(buf) - if err != nil { - closeErr = err - break - } - } - // As the TCP connection closes, so does the UDP server & HyClient session - return nil -} - -func (s *Server) udpServer(clientConn *net.UDPConn, localRelayConn *net.UDPConn, hyUDP cs.HyUDPConn) { - var clientAddr *net.UDPAddr - buf := make([]byte, udpBufferSize) - // Local to remote - for { - n, cAddr, err := clientConn.ReadFromUDP(buf) - if err != nil { - break - } - d, err := socks5.NewDatagramFromBytes(buf[:n]) - if err != nil || d.Frag != 0 { - // Ignore bad packets - continue - } - if clientAddr == nil { - // Whoever sends the first valid packet is our client - clientAddr = cAddr - // Start remote to local - go func() { - for { - bs, from, err := hyUDP.ReadFrom() - if err != nil { - break - } - atyp, addr, port, err := socks5.ParseAddress(from) - if err != nil { - continue - } - d := socks5.NewDatagram(atyp, addr, port, bs) - _, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr) - } - }() - if localRelayConn != nil { - go func() { - buf := make([]byte, udpBufferSize) - for { - n, from, err := localRelayConn.ReadFrom(buf) - if n > 0 { - atyp, addr, port, err := socks5.ParseAddress(from.String()) - if err != nil { - continue - } - d := socks5.NewDatagram(atyp, addr, port, buf[:n]) - _, _ = clientConn.WriteToUDP(d.Bytes(), clientAddr) - } - if err != nil { - break - } - } - }() - } - } else if cAddr.String() != clientAddr.String() { - // Not our client, bye - continue - } - host, port, addr := parseDatagramRequestAddress(d) - action, arg := acl.ActionProxy, "" - var ipAddr *net.IPAddr - var resErr error - if s.ACLEngine != nil && localRelayConn != nil { - action, arg, _, ipAddr, resErr = s.ACLEngine.ResolveAndMatch(host, port, true) - // Doesn't always matter if the resolution fails, as we may send it through HyClient - } - // Handle according to the action - switch action { - case acl.ActionDirect: - if resErr != nil { - return - } - _, _ = localRelayConn.WriteToUDP(d.Data, &net.UDPAddr{ - IP: ipAddr.IP, - Port: int(port), - Zone: ipAddr.Zone, - }) - case acl.ActionProxy: - _ = hyUDP.WriteTo(d.Data, addr) - case acl.ActionBlock: - // Do nothing - case acl.ActionHijack: - hijackIPAddr, err := s.Transport.ResolveIPAddr(arg) - if err == nil { - _, _ = localRelayConn.WriteToUDP(d.Data, &net.UDPAddr{ - IP: hijackIPAddr.IP, - Port: int(port), - Zone: hijackIPAddr.Zone, - }) - } - default: - // Do nothing - } - } -} - -func sendReply(conn *net.TCPConn, rep byte) error { - p := socks5.NewReply(rep, socks5.ATYPIPv4, []byte{0x00, 0x00, 0x00, 0x00}, []byte{0x00, 0x00}) - _, err := p.WriteTo(conn) - return err -} - -func parseRequestAddress(r *socks5.Request) (host string, port uint16, addr string) { - p := binary.BigEndian.Uint16(r.DstPort) - if r.Atyp == socks5.ATYPDomain { - d := string(r.DstAddr[1:]) - return d, p, net.JoinHostPort(d, strconv.Itoa(int(p))) - } else { - ipStr := net.IP(r.DstAddr).String() - return ipStr, p, net.JoinHostPort(ipStr, strconv.Itoa(int(p))) - } -} - -func parseDatagramRequestAddress(r *socks5.Datagram) (host string, port uint16, addr string) { - p := binary.BigEndian.Uint16(r.DstPort) - if r.Atyp == socks5.ATYPDomain { - d := string(r.DstAddr[1:]) - return d, p, net.JoinHostPort(d, strconv.Itoa(int(p))) - } else { - ipStr := net.IP(r.DstAddr).String() - return ipStr, p, net.JoinHostPort(ipStr, strconv.Itoa(int(p))) - } -} diff --git a/app/tproxy/tcp_linux.go b/app/tproxy/tcp_linux.go deleted file mode 100644 index c090a97..0000000 --- a/app/tproxy/tcp_linux.go +++ /dev/null @@ -1,66 +0,0 @@ -package tproxy - -import ( - "net" - "time" - - "github.com/LiamHaworth/go-tproxy" - "github.com/apernet/hysteria/core/cs" - "github.com/apernet/hysteria/core/utils" -) - -type TCPTProxy struct { - HyClient *cs.Client - ListenAddr *net.TCPAddr - Timeout time.Duration - - ConnFunc func(addr, reqAddr net.Addr) - ErrorFunc func(addr, reqAddr net.Addr, err error) -} - -func NewTCPTProxy(hyClient *cs.Client, listen string, timeout time.Duration, - connFunc func(addr, reqAddr net.Addr), - errorFunc func(addr, reqAddr net.Addr, err error), -) (*TCPTProxy, error) { - tAddr, err := net.ResolveTCPAddr("tcp", listen) - if err != nil { - return nil, err - } - r := &TCPTProxy{ - HyClient: hyClient, - ListenAddr: tAddr, - Timeout: timeout, - ConnFunc: connFunc, - ErrorFunc: errorFunc, - } - return r, nil -} - -func (r *TCPTProxy) ListenAndServe() error { - listener, err := tproxy.ListenTCP("tcp", r.ListenAddr) - if err != nil { - return err - } - defer listener.Close() - for { - c, err := listener.Accept() - if err != nil { - return err - } - go func() { - defer c.Close() - // Under TPROXY mode, we are effectively acting as the remote server - // So our LocalAddr is actually the target to which the user is trying to connect - // and our RemoteAddr is the local address where the user initiates the connection - r.ConnFunc(c.RemoteAddr(), c.LocalAddr()) - rc, err := r.HyClient.DialTCP(c.LocalAddr().String()) - if err != nil { - r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err) - return - } - defer rc.Close() - err = utils.PipePairWithTimeout(c, rc, r.Timeout) - r.ErrorFunc(c.RemoteAddr(), c.LocalAddr(), err) - }() - } -} diff --git a/app/tproxy/tcp_stub.go b/app/tproxy/tcp_stub.go deleted file mode 100644 index 5f4f30d..0000000 --- a/app/tproxy/tcp_stub.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !linux -// +build !linux - -package tproxy - -import ( - "errors" - "net" - "time" - - "github.com/apernet/hysteria/core/cs" -) - -type TCPTProxy struct{} - -func NewTCPTProxy(hyClient *cs.Client, listen string, timeout time.Duration, - connFunc func(addr, reqAddr net.Addr), - errorFunc func(addr, reqAddr net.Addr, err error), -) (*TCPTProxy, error) { - return nil, errors.New("not supported on the current system") -} - -func (r *TCPTProxy) ListenAndServe() error { - return nil -} diff --git a/app/tproxy/udp_linux.go b/app/tproxy/udp_linux.go deleted file mode 100644 index b67500d..0000000 --- a/app/tproxy/udp_linux.go +++ /dev/null @@ -1,115 +0,0 @@ -package tproxy - -import ( - "net" - "time" - - "github.com/LiamHaworth/go-tproxy" - "github.com/apernet/hysteria/core/cs" -) - -const udpBufferSize = 4096 - -type UDPTProxy struct { - HyClient *cs.Client - ListenAddr *net.UDPAddr - Timeout time.Duration - - ConnFunc func(addr, reqAddr net.Addr) - ErrorFunc func(addr, reqAddr net.Addr, err error) -} - -func NewUDPTProxy(hyClient *cs.Client, listen string, timeout time.Duration, - connFunc func(addr, reqAddr net.Addr), - errorFunc func(addr, reqAddr net.Addr, err error), -) (*UDPTProxy, error) { - uAddr, err := net.ResolveUDPAddr("udp", listen) - if err != nil { - return nil, err - } - r := &UDPTProxy{ - HyClient: hyClient, - ListenAddr: uAddr, - Timeout: timeout, - ConnFunc: connFunc, - ErrorFunc: errorFunc, - } - if timeout == 0 { - r.Timeout = 1 * time.Minute - } - return r, nil -} - -func (r *UDPTProxy) ListenAndServe() error { - conn, err := tproxy.ListenUDP("udp", r.ListenAddr) - if err != nil { - return err - } - defer conn.Close() - // Read loop - buf := make([]byte, udpBufferSize) - for { - n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf) // Huge Caveat!! This essentially works as TCP's Accept here - won't repeat for the same srcAddr/dstAddr pair - because and only because we have tproxy.DialUDP("udp", dstAddr, srcAddr) to take over the connection below - if n > 0 { - r.ConnFunc(srcAddr, dstAddr) - localConn, err := tproxy.DialUDP("udp", dstAddr, srcAddr) - if err != nil { - r.ErrorFunc(srcAddr, dstAddr, err) - continue - } - hyConn, err := r.HyClient.DialUDP() - if err != nil { - r.ErrorFunc(srcAddr, dstAddr, err) - _ = localConn.Close() - continue - } - _ = hyConn.WriteTo(buf[:n], dstAddr.String()) - - errChan := make(chan error, 2) - // Start remote to local - go func() { - for { - bs, _, err := hyConn.ReadFrom() - if err != nil { - errChan <- err - return - } - _, err = localConn.Write(bs) - if err != nil { - errChan <- err - return - } - _ = localConn.SetDeadline(time.Now().Add(r.Timeout)) - } - }() - // Start local to remote - go func() { - for { - _ = localConn.SetDeadline(time.Now().Add(r.Timeout)) - n, err := localConn.Read(buf) - if n > 0 { - err := hyConn.WriteTo(buf[:n], dstAddr.String()) - if err != nil { - errChan <- err - return - } - } - if err != nil { - errChan <- err - return - } - } - }() - // Error cleanup routine - go func() { - err := <-errChan - _ = localConn.Close() - _ = hyConn.Close() - r.ErrorFunc(srcAddr, dstAddr, err) - }() - } - if err != nil { - return err - } - } -} diff --git a/app/tproxy/udp_stub.go b/app/tproxy/udp_stub.go deleted file mode 100644 index d794dfb..0000000 --- a/app/tproxy/udp_stub.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !linux -// +build !linux - -package tproxy - -import ( - "errors" - "net" - "time" - - "github.com/apernet/hysteria/core/cs" -) - -var ErrTimeout = errors.New("inactivity timeout") - -type UDPTProxy struct{} - -func NewUDPTProxy(hyClient *cs.Client, listen string, timeout time.Duration, - connFunc func(addr, reqAddr net.Addr), errorFunc func(addr, reqAddr net.Addr, err error), -) (*UDPTProxy, error) { - return nil, errors.New("not supported on the current system") -} - -func (r *UDPTProxy) ListenAndServe() error { - return nil -} diff --git a/app/tun/server.go b/app/tun/server.go deleted file mode 100644 index d3958d5..0000000 --- a/app/tun/server.go +++ /dev/null @@ -1,156 +0,0 @@ -//go:build gpl -// +build gpl - -package tun - -import ( - "fmt" - "net" - "os" - "os/signal" - "strconv" - "syscall" - "time" - - "github.com/xjasonlyu/tun2socks/v2/core/option" - - "github.com/apernet/hysteria/core/cs" - t2score "github.com/xjasonlyu/tun2socks/v2/core" - "github.com/xjasonlyu/tun2socks/v2/core/adapter" - "github.com/xjasonlyu/tun2socks/v2/core/device" - "github.com/xjasonlyu/tun2socks/v2/core/device/fdbased" - "github.com/xjasonlyu/tun2socks/v2/core/device/tun" - "gvisor.dev/gvisor/pkg/tcpip/stack" -) - -var _ adapter.TransportHandler = (*Server)(nil) - -type Server struct { - HyClient *cs.Client - Timeout time.Duration - DeviceInfo DeviceInfo - - RequestFunc func(addr net.Addr, reqAddr string) - ErrorFunc func(addr net.Addr, reqAddr string, err error) -} - -const ( - MTU = 1500 -) - -const ( - DeviceTypeFd = iota - DeviceTypeName -) - -type DeviceInfo struct { - Type int - Fd int - Name string - MTU uint32 - TCPSendBufferSize int - TCPReceiveBufferSize int - TCPModerateReceiveBuffer bool -} - -func (d *DeviceInfo) Open() (dev device.Device, err error) { - switch d.Type { - case DeviceTypeFd: - dev, err = fdbased.Open(strconv.Itoa(d.Fd), d.MTU) - case DeviceTypeName: - dev, err = tun.Open(d.Name, d.MTU) - default: - err = fmt.Errorf("unknown device type: %d", d.Type) - } - return -} - -func NewServerWithTunFd(hyClient *cs.Client, timeout time.Duration, tunFd int, mtu uint32, - tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool, -) (*Server, error) { - if mtu == 0 { - mtu = MTU - } - s := &Server{ - HyClient: hyClient, - Timeout: timeout, - DeviceInfo: DeviceInfo{ - Type: DeviceTypeFd, - Fd: tunFd, - MTU: mtu, - TCPSendBufferSize: tcpSendBufferSize, - TCPReceiveBufferSize: tcpReceiveBufferSize, - TCPModerateReceiveBuffer: tcpModerateReceiveBuffer, - }, - } - return s, nil -} - -func NewServer(hyClient *cs.Client, timeout time.Duration, name string, mtu uint32, - tcpSendBufferSize, tcpReceiveBufferSize int, tcpModerateReceiveBuffer bool, -) (*Server, error) { - if mtu == 0 { - mtu = MTU - } - s := &Server{ - HyClient: hyClient, - Timeout: timeout, - DeviceInfo: DeviceInfo{ - Type: DeviceTypeName, - Name: name, - MTU: mtu, - TCPSendBufferSize: tcpSendBufferSize, - TCPReceiveBufferSize: tcpReceiveBufferSize, - TCPModerateReceiveBuffer: tcpModerateReceiveBuffer, - }, - } - return s, nil -} - -func (s *Server) ListenAndServe() error { - var dev device.Device - var st *stack.Stack - - defer func() { - if dev != nil { - _ = dev.Close() - } - if st != nil { - st.Close() - st.Wait() - } - }() - - dev, err := s.DeviceInfo.Open() - if err != nil { - return err - } - - var opts []option.Option - if s.DeviceInfo.TCPSendBufferSize > 0 { - opts = append(opts, option.WithTCPSendBufferSize(s.DeviceInfo.TCPSendBufferSize)) - } - if s.DeviceInfo.TCPReceiveBufferSize > 0 { - opts = append(opts, option.WithTCPReceiveBufferSize(s.DeviceInfo.TCPReceiveBufferSize)) - } - if s.DeviceInfo.TCPModerateReceiveBuffer { - opts = append(opts, option.WithTCPModerateReceiveBuffer(s.DeviceInfo.TCPModerateReceiveBuffer)) - } - - t2sconf := t2score.Config{ - LinkEndpoint: dev, - TransportHandler: s, - Options: opts, - } - - st, err = t2score.CreateStack(&t2sconf) - if err != nil { - return err - } - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh - - return nil -} diff --git a/app/tun/tcp.go b/app/tun/tcp.go deleted file mode 100644 index 7a59989..0000000 --- a/app/tun/tcp.go +++ /dev/null @@ -1,48 +0,0 @@ -//go:build gpl -// +build gpl - -package tun - -import ( - "net" - - "github.com/apernet/hysteria/core/utils" - "github.com/xjasonlyu/tun2socks/v2/core/adapter" -) - -func (s *Server) HandleTCP(localConn adapter.TCPConn) { - go s.handleTCPConn(localConn) -} - -func (s *Server) handleTCPConn(localConn adapter.TCPConn) { - defer localConn.Close() - - id := localConn.ID() - remoteAddr := net.TCPAddr{ - IP: net.IP(id.LocalAddress), - Port: int(id.LocalPort), - } - localAddr := net.TCPAddr{ - IP: net.IP(id.RemoteAddress), - Port: int(id.RemotePort), - } - - if s.RequestFunc != nil { - s.RequestFunc(&localAddr, remoteAddr.String()) - } - - var err error - defer func() { - if s.ErrorFunc != nil && err != nil { - s.ErrorFunc(&localAddr, remoteAddr.String(), err) - } - }() - - rc, err := s.HyClient.DialTCP(remoteAddr.String()) - if err != nil { - return - } - defer rc.Close() - - err = utils.PipePairWithTimeout(localConn, rc, s.Timeout) -} diff --git a/app/tun/udp.go b/app/tun/udp.go deleted file mode 100644 index 78f3f31..0000000 --- a/app/tun/udp.go +++ /dev/null @@ -1,114 +0,0 @@ -//go:build gpl -// +build gpl - -package tun - -import ( - "fmt" - "net" - "strconv" - "time" - - "github.com/apernet/hysteria/core/cs" - "github.com/xjasonlyu/tun2socks/v2/core/adapter" -) - -const udpBufferSize = 4096 - -func (s *Server) HandleUDP(conn adapter.UDPConn) { - go s.handleUDPConn(conn) -} - -func (s *Server) handleUDPConn(conn adapter.UDPConn) { - defer conn.Close() - - id := conn.ID() - remoteAddr := net.UDPAddr{ - IP: net.IP(id.LocalAddress), - Port: int(id.LocalPort), - } - localAddr := net.UDPAddr{ - IP: net.IP(id.RemoteAddress), - Port: int(id.RemotePort), - } - - if s.RequestFunc != nil { - s.RequestFunc(&localAddr, remoteAddr.String()) - } - - var err error - defer func() { - if s.ErrorFunc != nil && err != nil { - s.ErrorFunc(&localAddr, remoteAddr.String(), err) - } - }() - - rc, err := s.HyClient.DialUDP() - if err != nil { - return - } - defer rc.Close() - - err = s.relayUDP(conn, rc, &remoteAddr, s.Timeout) -} - -func (s *Server) relayUDP(lc adapter.UDPConn, rc cs.HyUDPConn, to *net.UDPAddr, timeout time.Duration) (err error) { - errChan := make(chan error, 2) - // local => remote - go func() { - buf := make([]byte, udpBufferSize) - for { - if timeout != 0 { - _ = lc.SetDeadline(time.Now().Add(timeout)) - n, err := lc.Read(buf) - if n > 0 { - err = rc.WriteTo(buf[:n], to.String()) - if err != nil { - errChan <- err - return - } - } - if err != nil { - errChan <- err - return - } - } - } - }() - // remote => local - go func() { - for { - pkt, addr, err := rc.ReadFrom() - if err != nil { - errChan <- err - return - } - if pkt != nil { - host, portStr, err := net.SplitHostPort(addr) - if err != nil { - errChan <- err - return - } - port, err := strconv.Atoi(portStr) - if err != nil { - errChan <- fmt.Errorf("cannot parse as port: %s", portStr) - return - } - - // adapter.UDPConn doesn't support WriteFrom() yet, - // so we check the src address and behavior like a symmetric NAT - if !to.IP.Equal(net.ParseIP(host)) || to.Port != port { - // drop the packet silently - continue - } - - _, err = lc.Write(pkt) - if err != nil { - errChan <- err - return - } - } - } - }() - return <-errChan -} diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 2f2335c..0000000 --- a/build.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -# Hysteria build script for Windows (PowerShell) - -# Environment variable options: -# - HY_APP_VERSION: App version -# - HY_APP_COMMIT: App commit hash -# - HY_APP_PLATFORMS: Platforms to build for (e.g. "windows/amd64,linux/amd64,darwin/amd64") - -function PlatformToEnv($os, $arch) { - $env:CGO_ENABLED = 0 - $env:GOOS = $os - $env:GOARCH = $arch - - switch -Regex ($arch) { - "arm" { - $env:GOARM = "7" - } - "armv5" { - $env:GOARM = "5" - $env:GOARCH = "arm" - } - "armv6" { - $env:GOARM = "6" - $env:GOARCH = "arm" - } - "armv7" { - $env:GOARM = "7" - $env:GOARCH = "arm" - } - "mips(le)?" { - $env:GOMIPS = "" - } - "mips-sf" { - $env:GOMIPS = "softfloat" - $env:GOARCH = "mips" - } - "mipsle-sf" { - $env:GOMIPS = "softfloat" - $env:GOARCH = "mipsle" - } - "amd64" { - $env:GOAMD64 = "" - $env:GOARCH = "amd64" - } - "amd64-avx" { - $env:GOAMD64 = "v3" - $env:GOARCH = "amd64" - } - } -} - -if (!(Get-Command go -ErrorAction SilentlyContinue)) { - Write-Host "Error: go is not installed." -ForegroundColor Red - exit 1 -} - -if (!(Get-Command git -ErrorAction SilentlyContinue)) { - Write-Host "Error: git is not installed." -ForegroundColor Red - exit 1 -} -if (!(git rev-parse --is-inside-work-tree 2>$null)) { - Write-Host "Error: not in a git repository." -ForegroundColor Red - exit 1 -} - -$ldflags = "-s -w -X 'main.appDate=$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")'" -if ($env:HY_APP_VERSION) { - $ldflags += " -X 'main.appVersion=$($env:HY_APP_VERSION)'" -} -else { - $ldflags += " -X 'main.appVersion=$(git describe --tags --always --match "v*")'" -} -if ($env:HY_APP_COMMIT) { - $ldflags += " -X 'main.appCommit=$($env:HY_APP_COMMIT)'" -} -else { - $ldflags += " -X 'main.appCommit=$(git rev-parse HEAD)'" -} - -if ($env:HY_APP_PLATFORMS) { - $platforms = $env:HY_APP_PLATFORMS.Split(",") -} -else { - $goos = go env GOOS - $goarch = go env GOARCH - $platforms = @("$goos/$goarch") -} - -if (Test-Path build) { - Remove-Item -Recurse -Force build -} -New-Item -ItemType Directory -Force -Path build - -Write-Host "Starting build..." -ForegroundColor Green - -foreach ($platform in $platforms) { - $os = $platform.Split("/")[0] - $arch = $platform.Split("/")[1] - PlatformToEnv $os $arch - Write-Host "Building $os/$arch" -ForegroundColor Green - $output = "build/hysteria-$os-$arch" - if ($os -eq "windows") { - $output = "$output.exe" - } - go build -o $output -tags=gpl -ldflags $ldflags -trimpath ./app/cmd/ - if ($LastExitCode -ne 0) { - Write-Host "Error: failed to build $os/$arch" -ForegroundColor Red - exit 1 - } -} - -Write-Host "Build complete." -ForegroundColor Green - -Get-ChildItem -Path build | Format-Table -AutoSize diff --git a/build.sh b/build.sh deleted file mode 100755 index 48c5a94..0000000 --- a/build.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Hysteria build script for Linux -# Environment variable options: -# - HY_APP_VERSION: App version -# - HY_APP_COMMIT: App commit hash -# - HY_APP_PLATFORMS: Platforms to build for (e.g. "windows/amd64,linux/amd64,darwin/amd64") - -export LC_ALL=C -export LC_DATE=C - -has_command() { - local cmd="$1" - type -P "$cmd" > /dev/null 2>&1 -} - -if ! has_command go; then - echo 'Error: go is not installed.' >&2 - exit 1 -fi - -if ! has_command git; then - echo 'Error: git is not installed.' >&2 - exit 1 -fi - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo 'Error: not in a git repository.' >&2 - exit 1 -fi - - -platform_to_env() { - local os="$1" - local arch="$2" - local env="GOOS=$os GOARCH=$arch CGO_ENABLED=0" - - case "$arch" in - arm) - env+=" GOARM=7 GOARCH=arm" - ;; - armv5) - env+=" GOARM=5 GOARCH=arm" - ;; - armv6) - env+=" GOARM=6 GOARCH=arm" - ;; - armv7) - env+=" GOARM=7 GOARCH=arm" - ;; - mips | mipsle) - env+=" GOMIPS=" - ;; - mips-sf) - env+=" GOMIPS=softfloat GOARCH=mips" - ;; - mipsle-sf) - env+=" GOMIPS=softfloat GOARCH=mipsle" - ;; - amd64) - env+=" GOAMD64= GOARCH=amd64" - ;; - amd64-avx) - env+=" GOAMD64=v3 GOARCH=amd64" - ;; - esac - - echo "$env" -} - -make_ldflags() { - local ldflags="-s -w -X 'main.appDate=$(date -u '+%F %T')'" - if [ -n "$HY_APP_VERSION" ]; then - ldflags="$ldflags -X 'main.appVersion=$HY_APP_VERSION'" - else - ldflags="$ldflags -X 'main.appVersion=$(git describe --tags --always --match 'v*')'" - fi - if [ -n "$HY_APP_COMMIT" ]; then - ldflags="$ldflags -X 'main.appCommit=$HY_APP_COMMIT'" - else - ldflags="$ldflags -X 'main.appCommit=$(git rev-parse HEAD)'" - fi - echo "$ldflags" -} - -build_for_platform() { - local platform="$1" - local ldflags="$2" - - local GOOS="${platform%/*}" - local GOARCH="${platform#*/}" - if [[ -z "$GOOS" || -z "$GOARCH" ]]; then - echo "Invalid platform $platform" >&2 - return 1 - fi - echo "Building $GOOS/$GOARCH" - local output="build/hysteria-$GOOS-$GOARCH" - if [[ "$GOOS" = "windows" ]]; then - output="$output.exe" - fi - local envs="$(platform_to_env "$GOOS" "$GOARCH")" - local exit_val=0 - env $envs go build -o "$output" -tags=gpl -ldflags "$ldflags" -trimpath ./app/cmd/ || exit_val=$? - if [[ "$exit_val" -ne 0 ]]; then - echo "Error: failed to build $GOOS/$GOARCH" >&2 - return $exit_val - fi -} - - -if [ -z "$HY_APP_PLATFORMS" ]; then - HY_APP_PLATFORMS="$(go env GOOS)/$(go env GOARCH)" -fi -platforms=(${HY_APP_PLATFORMS//,/ }) -ldflags="$(make_ldflags)" - -mkdir -p build -rm -rf build/* - -echo "Starting build..." - -for platform in "${platforms[@]}"; do - build_for_platform "$platform" "$ldflags" -done - -echo "Build complete." - -ls -lh build/ | awk '{print $9, $5}' diff --git a/core/acl/engine.go b/core/acl/engine.go deleted file mode 100644 index 679c971..0000000 --- a/core/acl/engine.go +++ /dev/null @@ -1,143 +0,0 @@ -package acl - -import ( - "bufio" - "net" - "os" - "strings" - - lru "github.com/hashicorp/golang-lru/v2" - - "github.com/apernet/hysteria/core/utils" - "github.com/oschwald/geoip2-golang" -) - -const entryCacheSize = 1024 - -type Engine struct { - DefaultAction Action - Entries []Entry - Cache *lru.ARCCache[cacheKey, cacheValue] - ResolveIPAddr func(string) (*net.IPAddr, error) - GeoIPReader *geoip2.Reader -} - -type cacheKey struct { - Host string - Port uint16 - IsUDP bool -} - -type cacheValue struct { - Action Action - Arg string -} - -func LoadFromFile(filename string, resolveIPAddr func(string) (*net.IPAddr, error), geoIPLoadFunc func() (*geoip2.Reader, error)) (*Engine, error) { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - scanner := bufio.NewScanner(f) - entries := make([]Entry, 0, 1024) - var geoIPReader *geoip2.Reader - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if len(line) == 0 || strings.HasPrefix(line, "#") { - // Ignore empty lines & comments - continue - } - entry, err := ParseEntry(line) - if err != nil { - return nil, err - } - if _, ok := entry.Matcher.(*countryMatcher); ok && geoIPReader == nil { - geoIPReader, err = geoIPLoadFunc() // lazy load GeoIP reader only when needed - if err != nil { - return nil, err - } - } - entries = append(entries, entry) - } - cache, err := lru.NewARC[cacheKey, cacheValue](entryCacheSize) - if err != nil { - return nil, err - } - return &Engine{ - DefaultAction: ActionProxy, - Entries: entries, - Cache: cache, - ResolveIPAddr: resolveIPAddr, - GeoIPReader: geoIPReader, - }, nil -} - -// action, arg, isDomain, resolvedIP, error -func (e *Engine) ResolveAndMatch(host string, port uint16, isUDP bool) (Action, string, bool, *net.IPAddr, error) { - ip, zone := utils.ParseIPZone(host) - if ip == nil { - // Domain - ipAddr, err := e.ResolveIPAddr(host) - if ce, ok := e.Cache.Get(cacheKey{host, port, isUDP}); ok { - // Cache hit - return ce.Action, ce.Arg, true, ipAddr, err - } - for _, entry := range e.Entries { - mReq := MatchRequest{ - Domain: host, - Port: port, - DB: e.GeoIPReader, - } - if ipAddr != nil { - mReq.IP = ipAddr.IP - } - if isUDP { - mReq.Protocol = ProtocolUDP - } else { - mReq.Protocol = ProtocolTCP - } - if entry.Match(mReq) { - e.Cache.Add(cacheKey{host, port, isUDP}, - cacheValue{entry.Action, entry.ActionArg}) - return entry.Action, entry.ActionArg, true, ipAddr, err - } - } - e.Cache.Add(cacheKey{host, port, isUDP}, cacheValue{e.DefaultAction, ""}) - return e.DefaultAction, "", true, ipAddr, err - } else { - // IP - if ce, ok := e.Cache.Get(cacheKey{ip.String(), port, isUDP}); ok { - // Cache hit - return ce.Action, ce.Arg, false, &net.IPAddr{ - IP: ip, - Zone: zone, - }, nil - } - for _, entry := range e.Entries { - mReq := MatchRequest{ - IP: ip, - Port: port, - DB: e.GeoIPReader, - } - if isUDP { - mReq.Protocol = ProtocolUDP - } else { - mReq.Protocol = ProtocolTCP - } - if entry.Match(mReq) { - e.Cache.Add(cacheKey{ip.String(), port, isUDP}, - cacheValue{entry.Action, entry.ActionArg}) - return entry.Action, entry.ActionArg, false, &net.IPAddr{ - IP: ip, - Zone: zone, - }, nil - } - } - e.Cache.Add(cacheKey{ip.String(), port, isUDP}, cacheValue{e.DefaultAction, ""}) - return e.DefaultAction, "", false, &net.IPAddr{ - IP: ip, - Zone: zone, - }, nil - } -} diff --git a/core/acl/engine_test.go b/core/acl/engine_test.go deleted file mode 100644 index 0fd9fe7..0000000 --- a/core/acl/engine_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package acl - -import ( - "errors" - "net" - "strings" - "testing" - - lru "github.com/hashicorp/golang-lru/v2" -) - -func TestEngine_ResolveAndMatch(t *testing.T) { - cache, _ := lru.NewARC[cacheKey, cacheValue](entryCacheSize) - e := &Engine{ - DefaultAction: ActionDirect, - Entries: []Entry{ - { - Action: ActionProxy, - ActionArg: "", - Matcher: &domainMatcher{ - matcherBase: matcherBase{ - Protocol: ProtocolTCP, - Port: 443, - }, - Domain: "google.com", - Suffix: false, - }, - }, - { - Action: ActionHijack, - ActionArg: "good.org", - Matcher: &domainMatcher{ - matcherBase: matcherBase{}, - Domain: "evil.corp", - Suffix: true, - }, - }, - { - Action: ActionProxy, - ActionArg: "", - Matcher: &netMatcher{ - matcherBase: matcherBase{}, - Net: &net.IPNet{ - IP: net.ParseIP("10.0.0.0"), - Mask: net.CIDRMask(8, 32), - }, - }, - }, - { - Action: ActionBlock, - ActionArg: "", - Matcher: &allMatcher{}, - }, - }, - Cache: cache, - ResolveIPAddr: func(s string) (*net.IPAddr, error) { - if strings.Contains(s, "evil.corp") { - return nil, errors.New("resolve error") - } - return net.ResolveIPAddr("ip", s) - }, - } - tests := []struct { - name string - host string - port uint16 - isUDP bool - wantAction Action - wantArg string - wantErr bool - }{ - { - name: "domain proxy", - host: "google.com", - port: 443, - isUDP: false, - wantAction: ActionProxy, - wantArg: "", - }, - { - name: "domain block", - host: "google.com", - port: 80, - isUDP: false, - wantAction: ActionBlock, - wantArg: "", - }, - { - name: "domain suffix 1", - host: "evil.corp", - port: 8899, - isUDP: true, - wantAction: ActionHijack, - wantArg: "good.org", - wantErr: true, - }, - { - name: "domain suffix 2", - host: "notevil.corp", - port: 22, - isUDP: false, - wantAction: ActionBlock, - wantArg: "", - wantErr: true, - }, - { - name: "domain suffix 3", - host: "im.real.evil.corp", - port: 443, - isUDP: true, - wantAction: ActionHijack, - wantArg: "good.org", - wantErr: true, - }, - { - name: "ip match", - host: "10.2.3.4", - port: 80, - isUDP: false, - wantAction: ActionProxy, - wantArg: "", - }, - { - name: "ip mismatch", - host: "100.5.6.0", - port: 1234, - isUDP: false, - wantAction: ActionBlock, - wantArg: "", - }, - { - name: "domain proxy cache", - host: "google.com", - port: 443, - isUDP: false, - wantAction: ActionProxy, - wantArg: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotAction, gotArg, _, _, err := e.ResolveAndMatch(tt.host, tt.port, tt.isUDP) - if (err != nil) != tt.wantErr { - t.Errorf("ResolveAndMatch() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotAction != tt.wantAction { - t.Errorf("ResolveAndMatch() gotAction = %v, wantAction %v", gotAction, tt.wantAction) - } - if gotArg != tt.wantArg { - t.Errorf("ResolveAndMatch() gotArg = %v, wantAction %v", gotArg, tt.wantArg) - } - }) - } -} diff --git a/core/acl/entry.go b/core/acl/entry.go deleted file mode 100644 index 5cec192..0000000 --- a/core/acl/entry.go +++ /dev/null @@ -1,334 +0,0 @@ -package acl - -import ( - "errors" - "fmt" - "net" - "strconv" - "strings" - - "github.com/oschwald/geoip2-golang" -) - -type ( - Action byte - Protocol byte -) - -const ( - ActionDirect = Action(iota) - ActionProxy - ActionBlock - ActionHijack -) - -const ( - ProtocolAll = Protocol(iota) - ProtocolTCP - ProtocolUDP -) - -var protocolPortAliases = map[string]string{ - "echo": "*/7", - "ftp-data": "*/20", - "ftp": "*/21", - "ssh": "*/22", - "telnet": "*/23", - "domain": "*/53", - "dns": "*/53", - "http": "*/80", - "sftp": "*/115", - "ntp": "*/123", - "https": "*/443", - "quic": "udp/443", - "socks": "*/1080", -} - -type Entry struct { - Action Action - ActionArg string - Matcher Matcher -} - -type MatchRequest struct { - IP net.IP - Domain string - - Protocol Protocol - Port uint16 - - DB *geoip2.Reader -} - -type Matcher interface { - Match(MatchRequest) bool -} - -type matcherBase struct { - Protocol Protocol - Port uint16 // 0 for all ports -} - -func (m *matcherBase) MatchProtocolPort(p Protocol, port uint16) bool { - return (m.Protocol == ProtocolAll || m.Protocol == p) && (m.Port == 0 || m.Port == port) -} - -func parseProtocolPort(s string) (Protocol, uint16, error) { - if protocolPortAliases[s] != "" { - s = protocolPortAliases[s] - } - if len(s) == 0 || s == "*" { - return ProtocolAll, 0, nil - } - parts := strings.Split(s, "/") - if len(parts) != 2 { - return ProtocolAll, 0, errors.New("invalid protocol/port syntax") - } - protocol := ProtocolAll - switch parts[0] { - case "tcp": - protocol = ProtocolTCP - case "udp": - protocol = ProtocolUDP - case "*": - protocol = ProtocolAll - default: - return ProtocolAll, 0, errors.New("invalid protocol") - } - if parts[1] == "*" { - return protocol, 0, nil - } - port, err := strconv.ParseUint(parts[1], 10, 16) - if err != nil { - return ProtocolAll, 0, errors.New("invalid port") - } - return protocol, uint16(port), nil -} - -type netMatcher struct { - matcherBase - Net *net.IPNet -} - -func (m *netMatcher) Match(r MatchRequest) bool { - if r.IP == nil { - return false - } - return m.Net.Contains(r.IP) && m.MatchProtocolPort(r.Protocol, r.Port) -} - -type domainMatcher struct { - matcherBase - Domain string - Suffix bool -} - -func (m *domainMatcher) Match(r MatchRequest) bool { - if len(r.Domain) == 0 { - return false - } - domain := strings.ToLower(r.Domain) - return (m.Domain == domain || (m.Suffix && strings.HasSuffix(domain, "."+m.Domain))) && - m.MatchProtocolPort(r.Protocol, r.Port) -} - -type countryMatcher struct { - matcherBase - Country string // ISO 3166-1 alpha-2 country code, upper case -} - -func (m *countryMatcher) Match(r MatchRequest) bool { - if r.IP == nil || r.DB == nil { - return false - } - c, err := r.DB.Country(r.IP) - if err != nil { - return false - } - return c.Country.IsoCode == m.Country && m.MatchProtocolPort(r.Protocol, r.Port) -} - -type allMatcher struct { - matcherBase -} - -func (m *allMatcher) Match(r MatchRequest) bool { - return m.MatchProtocolPort(r.Protocol, r.Port) -} - -func (e Entry) Match(r MatchRequest) bool { - return e.Matcher.Match(r) -} - -func ParseEntry(s string) (Entry, error) { - fields := strings.Fields(s) - if len(fields) < 2 { - return Entry{}, fmt.Errorf("expected at least 2 fields, got %d", len(fields)) - } - e := Entry{} - action := fields[0] - conds := fields[1:] - switch strings.ToLower(action) { - case "direct": - e.Action = ActionDirect - case "proxy": - e.Action = ActionProxy - case "block": - e.Action = ActionBlock - case "hijack": - if len(conds) < 2 { - return Entry{}, fmt.Errorf("hijack requires at least 3 fields, got %d", len(fields)) - } - e.Action = ActionHijack - e.ActionArg = conds[len(conds)-1] - conds = conds[:len(conds)-1] - default: - return Entry{}, fmt.Errorf("invalid action %s", fields[0]) - } - m, err := condsToMatcher(conds) - if err != nil { - return Entry{}, err - } - e.Matcher = m - return e, nil -} - -func condsToMatcher(conds []string) (Matcher, error) { - if len(conds) < 1 { - return nil, errors.New("no condition specified") - } - typ, args := conds[0], conds[1:] - switch strings.ToLower(typ) { - case "domain": - // domain - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for domain: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &domainMatcher{ - matcherBase: mb, - Domain: args[0], - Suffix: false, - }, nil - case "domain-suffix": - // domain-suffix - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for domain-suffix: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &domainMatcher{ - matcherBase: mb, - Domain: args[0], - Suffix: true, - }, nil - case "cidr": - // cidr - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for cidr: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - _, ipNet, err := net.ParseCIDR(args[0]) - if err != nil { - return nil, err - } - return &netMatcher{ - matcherBase: mb, - Net: ipNet, - }, nil - case "ip": - // ip - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for ip: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - ip := net.ParseIP(args[0]) - if ip == nil { - return nil, fmt.Errorf("invalid ip: %s", args[0]) - } - var ipNet *net.IPNet - if ip.To4() != nil { - ipNet = &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(32, 32), - } - } else { - ipNet = &net.IPNet{ - IP: ip, - Mask: net.CIDRMask(128, 128), - } - } - return &netMatcher{ - matcherBase: mb, - Net: ipNet, - }, nil - case "country": - // country - if len(args) == 0 || len(args) > 2 { - return nil, fmt.Errorf("invalid number of arguments for country: %d, expected 1 or 2", len(args)) - } - mb := matcherBase{} - if len(args) == 2 { - protocol, port, err := parseProtocolPort(args[1]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &countryMatcher{ - matcherBase: mb, - Country: strings.ToUpper(args[0]), - }, nil - case "all": - // all - if len(args) > 1 { - return nil, fmt.Errorf("invalid number of arguments for all: %d, expected 0 or 1", len(args)) - } - mb := matcherBase{} - if len(args) == 1 { - protocol, port, err := parseProtocolPort(args[0]) - if err != nil { - return nil, err - } - mb.Protocol = protocol - mb.Port = port - } - return &allMatcher{ - matcherBase: mb, - }, nil - default: - return nil, fmt.Errorf("invalid condition type: %s", typ) - } -} diff --git a/core/acl/entry_test.go b/core/acl/entry_test.go deleted file mode 100644 index d620197..0000000 --- a/core/acl/entry_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package acl - -import ( - "net" - "reflect" - "testing" -) - -func TestParseEntry(t *testing.T) { - _, ok3net, _ := net.ParseCIDR("8.8.8.0/24") - - type args struct { - s string - } - tests := []struct { - name string - args args - want Entry - wantErr bool - }{ - {name: "empty", args: args{""}, want: Entry{}, wantErr: true}, - { - name: "ok 1", args: args{"direct domain-suffix google.com"}, - want: Entry{ActionDirect, "", &domainMatcher{ - matcherBase: matcherBase{}, - Domain: "google.com", - Suffix: true, - }}, - wantErr: false, - }, - { - name: "ok 2", args: args{"proxy domain shithole"}, - want: Entry{ActionProxy, "", &domainMatcher{ - matcherBase: matcherBase{}, - Domain: "shithole", - Suffix: false, - }}, - wantErr: false, - }, - { - name: "ok 3", args: args{"block cidr 8.8.8.0/24 */53"}, - want: Entry{ActionBlock, "", &netMatcher{ - matcherBase: matcherBase{ProtocolAll, 53}, - Net: ok3net, - }}, - wantErr: false, - }, - { - name: "ok 4", args: args{"hijack all udp/* udpblackhole.net"}, - want: Entry{ActionHijack, "udpblackhole.net", &allMatcher{ - matcherBase: matcherBase{ProtocolUDP, 0}, - }}, - wantErr: false, - }, - { - name: "err 1", args: args{"what the heck"}, - want: Entry{}, - wantErr: true, - }, - { - name: "err 2", args: args{"proxy sucks ass"}, - want: Entry{}, - wantErr: true, - }, - { - name: "err 3", args: args{"block ip 999.999.999.999"}, - want: Entry{}, - wantErr: true, - }, - { - name: "err 4", args: args{"hijack domain google.com"}, - want: Entry{}, - wantErr: true, - }, - { - name: "err 5", args: args{"hijack domain google.com bing.com 123"}, - want: Entry{}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ParseEntry(tt.args.s) - if (err != nil) != tt.wantErr { - t.Errorf("ParseEntry() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ParseEntry() got = %v, wantAction %v", got, tt.want) - } - }) - } -} diff --git a/core/client/client.go b/core/client/client.go new file mode 100644 index 0000000..4b69bd7 --- /dev/null +++ b/core/client/client.go @@ -0,0 +1,439 @@ +package client + +import ( + "context" + "crypto/tls" + "errors" + "io" + "math/rand" + "net" + "net/http" + "net/url" + "sync" + "time" + + coreErrs "github.com/apernet/hysteria/core/errors" + "github.com/apernet/hysteria/core/internal/congestion" + "github.com/apernet/hysteria/core/internal/frag" + "github.com/apernet/hysteria/core/internal/protocol" + "github.com/apernet/hysteria/core/internal/utils" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" +) + +const ( + udpMessageChanSize = 1024 +) + +type Client interface { + DialTCP(addr string) (net.Conn, error) + ListenUDP() (HyUDPConn, error) + Close() error +} + +type HyUDPConn interface { + Receive() ([]byte, string, error) + Send([]byte, string) error + Close() error +} + +func NewClient(config *Config) (Client, error) { + if err := config.fill(); err != nil { + return nil, err + } + c := &clientImpl{ + config: config, + } + c.conn = &autoReconnectConn{ + Connect: c.connect, + } + return c, nil +} + +type clientImpl struct { + config *Config + conn *autoReconnectConn + + udpSM udpSessionManager +} + +type udpSessionEntry struct { + Ch chan *protocol.UDPMessage + D *frag.Defragger + Closed bool +} + +type udpSessionManager struct { + mutex sync.RWMutex + m map[uint32]*udpSessionEntry +} + +func (m *udpSessionManager) Init() { + m.mutex.Lock() + defer m.mutex.Unlock() + m.m = make(map[uint32]*udpSessionEntry) +} + +// Add returns both a channel for receiving messages and a function to close the channel & delete the session. +func (m *udpSessionManager) Add(id uint32) (<-chan *protocol.UDPMessage, func()) { + m.mutex.Lock() + defer m.mutex.Unlock() + + // Important: make sure we add and delete the channel in the same map, + // as the map may be replaced by Init() at any time. + currentM := m.m + + entry := &udpSessionEntry{ + Ch: make(chan *protocol.UDPMessage, udpMessageChanSize), + D: &frag.Defragger{}, + Closed: false, + } + currentM[id] = entry + + return entry.Ch, func() { + m.mutex.Lock() + defer m.mutex.Unlock() + if entry.Closed { + // Double close a channel will panic, + // so we need a flag to make sure we only close it once. + return + } + entry.Closed = true + close(entry.Ch) + delete(currentM, id) + } +} + +func (m *udpSessionManager) Feed(msg *protocol.UDPMessage) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + entry, ok := m.m[msg.SessionID] + if !ok { + // No such session, drop the message + return + } + dfMsg := entry.D.Feed(msg) + if dfMsg == nil { + // Not a complete message yet + return + } + select { + case entry.Ch <- dfMsg: + // OK + default: + // Channel is full, drop the message + } +} + +func (c *clientImpl) connect() (quic.Connection, func(), error) { + // Use a new packet conn for each connection, + // remember to close it after the QUIC connection is closed. + pktConn, err := c.config.ConnFactory.New(c.config.ServerAddr) + if err != nil { + return nil, nil, err + } + // Convert config to TLS config & QUIC config + tlsConfig := &tls.Config{ + InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify, + RootCAs: c.config.TLSConfig.RootCAs, + } + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow, + MaxStreamReceiveWindow: c.config.QUICConfig.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: c.config.QUICConfig.InitialConnectionReceiveWindow, + MaxConnectionReceiveWindow: c.config.QUICConfig.MaxConnectionReceiveWindow, + MaxIdleTimeout: c.config.QUICConfig.MaxIdleTimeout, + KeepAlivePeriod: c.config.QUICConfig.KeepAlivePeriod, + DisablePathMTUDiscovery: c.config.QUICConfig.DisablePathMTUDiscovery, + EnableDatagrams: true, + } + // Prepare RoundTripper + var conn quic.EarlyConnection + rt := &http3.RoundTripper{ + EnableDatagrams: true, + TLSClientConfig: tlsConfig, + QuicConfig: quicConfig, + Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + qc, err := quic.DialEarlyContext(ctx, pktConn, c.config.ServerAddr, c.config.ServerName, tlsCfg, cfg) + if err != nil { + return nil, err + } + conn = qc + return qc, nil + }, + } + // Send auth HTTP request + req := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "https", + Host: protocol.URLHost, + Path: protocol.URLPath, + }, + Header: make(http.Header), + } + protocol.AuthRequestDataToHeader(req.Header, c.config.Auth, c.config.BandwidthConfig.MaxRx) + resp, err := rt.RoundTrip(req) + if err != nil { + if conn != nil { + _ = conn.CloseWithError(0, "") + } + _ = pktConn.Close() + return nil, nil, &coreErrs.ConnectError{Err: err} + } + if resp.StatusCode != protocol.StatusAuthOK { + _ = conn.CloseWithError(0, "") + _ = pktConn.Close() + return nil, nil, &coreErrs.AuthError{StatusCode: resp.StatusCode} + } + // Auth OK + serverRx := protocol.AuthResponseDataFromHeader(resp.Header) + // actualTx = min(serverRx, clientTx) + actualTx := serverRx + if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx { + actualTx = c.config.BandwidthConfig.MaxTx + } + // Set congestion control when applicable + if actualTx > 0 { + conn.SetCongestionControl(congestion.NewBrutalSender(actualTx)) + } + _ = resp.Body.Close() + + c.udpSM.Init() + go c.udpLoop(conn) + + return conn, func() { + _ = conn.CloseWithError(0, "") + _ = pktConn.Close() + }, nil +} + +func (c *clientImpl) udpLoop(conn quic.Connection) { + for { + msg, err := conn.ReceiveMessage() + if err != nil { + return + } + c.handleUDPMessage(msg) + } +} + +// client <- remote direction +func (c *clientImpl) handleUDPMessage(msg []byte) { + udpMsg, err := protocol.ParseUDPMessage(msg) + if err != nil { + return + } + c.udpSM.Feed(udpMsg) +} + +// openStream wraps the stream with QStream, which handles Close() properly +func (c *clientImpl) openStream() (quic.Connection, quic.Stream, error) { + qc, stream, err := c.conn.OpenStream() + if err != nil { + return nil, nil, err + } + + return qc, &utils.QStream{Stream: stream}, nil +} + +func (c *clientImpl) DialTCP(addr string) (net.Conn, error) { + qc, stream, err := c.openStream() + if err != nil { + return nil, err + } + // Send request + err = protocol.WriteTCPRequest(stream, addr) + if err != nil { + _ = stream.Close() + return nil, err + } + if c.config.FastOpen { + // Don't wait for the response when fast open is enabled. + // Return the connection immediately, defer the response handling + // to the first Read() call. + return &tcpConn{ + Orig: stream, + PseudoLocalAddr: qc.LocalAddr(), + PseudoRemoteAddr: qc.RemoteAddr(), + Established: false, + }, nil + } + // Read response + ok, msg, err := protocol.ReadTCPResponse(stream) + if err != nil { + _ = stream.Close() + return nil, err + } + if !ok { + _ = stream.Close() + return nil, coreErrs.DialError{Message: msg} + } + return &tcpConn{ + Orig: stream, + PseudoLocalAddr: qc.LocalAddr(), + PseudoRemoteAddr: qc.RemoteAddr(), + Established: true, + }, nil +} + +func (c *clientImpl) ListenUDP() (HyUDPConn, error) { + qc, stream, err := c.openStream() + if err != nil { + return nil, err + } + // Send request + err = protocol.WriteUDPRequest(stream) + if err != nil { + _ = stream.Close() + return nil, err + } + // Read response + ok, sessionID, msg, err := protocol.ReadUDPResponse(stream) + if err != nil { + _ = stream.Close() + return nil, err + } + if !ok { + _ = stream.Close() + return nil, coreErrs.DialError{Message: msg} + } + + ch, closeFunc := c.udpSM.Add(sessionID) + uc := &udpConn{ + QC: qc, + Stream: stream, + SessionID: sessionID, + Ch: ch, + CloseFunc: closeFunc, + SendBuf: make([]byte, protocol.MaxUDPSize), + } + go uc.Hold() + return uc, nil +} + +func (c *clientImpl) Close() error { + return c.conn.Close() +} + +type tcpConn struct { + Orig quic.Stream + PseudoLocalAddr net.Addr + PseudoRemoteAddr net.Addr + Established bool +} + +func (c *tcpConn) Read(b []byte) (n int, err error) { + if !c.Established { + // Read response + ok, msg, err := protocol.ReadTCPResponse(c.Orig) + if err != nil { + return 0, err + } + if !ok { + return 0, coreErrs.DialError{Message: msg} + } + c.Established = true + } + return c.Orig.Read(b) +} + +func (c *tcpConn) Write(b []byte) (n int, err error) { + return c.Orig.Write(b) +} + +func (c *tcpConn) Close() error { + return c.Orig.Close() +} + +func (c *tcpConn) LocalAddr() net.Addr { + return c.PseudoLocalAddr +} + +func (c *tcpConn) RemoteAddr() net.Addr { + return c.PseudoRemoteAddr +} + +func (c *tcpConn) SetDeadline(t time.Time) error { + return c.Orig.SetDeadline(t) +} + +func (c *tcpConn) SetReadDeadline(t time.Time) error { + return c.Orig.SetReadDeadline(t) +} + +func (c *tcpConn) SetWriteDeadline(t time.Time) error { + return c.Orig.SetWriteDeadline(t) +} + +type udpConn struct { + QC quic.Connection + Stream quic.Stream + SessionID uint32 + Ch <-chan *protocol.UDPMessage + CloseFunc func() + SendBuf []byte +} + +func (c *udpConn) Hold() { + // Hold (drain) the stream until someone closes it. + // Closing the stream is the signal to stop the UDP session. + _, _ = io.Copy(io.Discard, c.Stream) + _ = c.Close() +} + +func (c *udpConn) Receive() ([]byte, string, error) { + msg := <-c.Ch + if msg == nil { + // Closed + return nil, "", io.EOF + } + return msg.Data, msg.Addr, nil +} + +// Send is not thread-safe as it uses a shared send buffer for now. +func (c *udpConn) Send(data []byte, addr string) error { + // Try no frag first + msg := protocol.UDPMessage{ + SessionID: c.SessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: addr, + Data: data, + } + n := msg.Serialize(c.SendBuf) + if n < 0 { + // Message even larger than MaxUDPSize, drop it + // Maybe we should return an error in the future? + return nil + } + sendErr := c.QC.SendMessage(c.SendBuf[:n]) + if sendErr == nil { + // All good + return nil + } + var errTooLarge quic.ErrMessageTooLarge + if errors.As(sendErr, &errTooLarge) { + // Message too large, try fragmentation + msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 + fMsgs := frag.FragUDPMessage(msg, int(errTooLarge)) + for _, fMsg := range fMsgs { + n = fMsg.Serialize(c.SendBuf) + err := c.QC.SendMessage(c.SendBuf[:n]) + if err != nil { + return err + } + } + return nil + } + // Other error + return sendErr +} + +func (c *udpConn) Close() error { + c.CloseFunc() + return c.Stream.Close() +} diff --git a/core/client/config.go b/core/client/config.go new file mode 100644 index 0000000..efe4e5d --- /dev/null +++ b/core/client/config.go @@ -0,0 +1,107 @@ +package client + +import ( + "crypto/x509" + "net" + "time" + + "github.com/apernet/hysteria/core/errors" + "github.com/apernet/hysteria/core/internal/pmtud" +) + +const ( + defaultStreamReceiveWindow = 8388608 // 8MB + defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB + defaultMaxIdleTimeout = 30 * time.Second + defaultKeepAlivePeriod = 10 * time.Second +) + +type Config struct { + ConnFactory ConnFactory + ServerAddr net.Addr + ServerName string // host or host:port + Auth string + TLSConfig TLSConfig + QUICConfig QUICConfig + BandwidthConfig BandwidthConfig + FastOpen bool +} + +// fill fills the fields that are not set by the user with default values when possible, +// and returns an error if the user has not set a required field. +func (c *Config) fill() error { + if c.ConnFactory == nil { + c.ConnFactory = &udpConnFactory{} + } + if c.ServerAddr == nil { + return errors.ConfigError{Field: "ServerAddr", Reason: "must be set"} + } + if c.ServerName == "" { + return errors.ConfigError{Field: "ServerName", Reason: "must be set"} + } + if c.QUICConfig.InitialStreamReceiveWindow == 0 { + c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.InitialStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxStreamReceiveWindow == 0 { + c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.MaxStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.InitialConnectionReceiveWindow == 0 { + c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxConnectionReceiveWindow == 0 { + c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxIdleTimeout == 0 { + c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout + } else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second { + return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"} + } + if c.QUICConfig.KeepAlivePeriod == 0 { + c.QUICConfig.KeepAlivePeriod = defaultKeepAlivePeriod + } else if c.QUICConfig.KeepAlivePeriod < 2*time.Second || c.QUICConfig.KeepAlivePeriod > 60*time.Second { + return errors.ConfigError{Field: "QUICConfig.KeepAlivePeriod", Reason: "must be between 2s and 60s"} + } + c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery + return nil +} + +type ConnFactory interface { + New(net.Addr) (net.PacketConn, error) +} + +type udpConnFactory struct{} + +func (f *udpConnFactory) New(addr net.Addr) (net.PacketConn, error) { + return net.ListenUDP("udp", nil) +} + +// TLSConfig contains the TLS configuration fields that we want to expose to the user. +type TLSConfig struct { + InsecureSkipVerify bool + RootCAs *x509.CertPool +} + +// QUICConfig contains the QUIC configuration fields that we want to expose to the user. +type QUICConfig struct { + InitialStreamReceiveWindow uint64 + MaxStreamReceiveWindow uint64 + InitialConnectionReceiveWindow uint64 + MaxConnectionReceiveWindow uint64 + MaxIdleTimeout time.Duration + KeepAlivePeriod time.Duration + DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. +} + +// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second. +type BandwidthConfig struct { + MaxTx uint64 + MaxRx uint64 +} diff --git a/core/client/reconnect.go b/core/client/reconnect.go new file mode 100644 index 0000000..7ea6943 --- /dev/null +++ b/core/client/reconnect.go @@ -0,0 +1,68 @@ +package client + +import ( + "net" + "sync" + + "github.com/quic-go/quic-go" +) + +// autoReconnectConn is a wrapper of quic.Connection that automatically reconnects +// when a non-temporary error (usually a timeout) occurs. +type autoReconnectConn struct { + // Connect is called whenever a new QUIC connection is needed. + // It should return a new QUIC connection, a function to close the connection + // (and potentially other underlying resources), and an error if one occurred. + Connect func() (quic.Connection, func(), error) + + conn quic.Connection + closeFunc func() + connMutex sync.RWMutex +} + +func (c *autoReconnectConn) OpenStream() (quic.Connection, quic.Stream, error) { + c.connMutex.Lock() + defer c.connMutex.Unlock() + // First time? + if c.conn == nil { + conn, closeFunc, err := c.Connect() + if err != nil { + return nil, nil, err + } + c.conn = conn + c.closeFunc = closeFunc + } + stream, err := c.conn.OpenStream() + if err == nil { + // All is good + return c.conn, stream, nil + } else if nErr, ok := err.(net.Error); ok && nErr.Temporary() { + // Temporary error, just pass the error to the caller + return nil, nil, err + } else { + // Permanent error + // Close the previous connection, + // reconnect and try again (only once) + c.closeFunc() + conn, closeFunc, err := c.Connect() + if err != nil { + return nil, nil, err + } + c.conn = conn + c.closeFunc = closeFunc + stream, err = c.conn.OpenStream() + return c.conn, stream, err + } +} + +func (c *autoReconnectConn) Close() error { + c.connMutex.Lock() + defer c.connMutex.Unlock() + if c.conn == nil { + return nil + } + c.closeFunc() + c.conn = nil + c.closeFunc = nil + return nil +} diff --git a/core/cs/client.go b/core/cs/client.go deleted file mode 100644 index 93d3249..0000000 --- a/core/cs/client.go +++ /dev/null @@ -1,447 +0,0 @@ -package cs - -import ( - "bytes" - "context" - "crypto/tls" - "errors" - "fmt" - "math/rand" - "net" - "strconv" - "sync" - "time" - - "github.com/apernet/hysteria/core/pktconns" - - "github.com/apernet/hysteria/core/congestion" - - "github.com/apernet/hysteria/core/pmtud" - "github.com/apernet/hysteria/core/utils" - "github.com/lunixbochs/struc" - "github.com/quic-go/quic-go" -) - -var ErrClosed = errors.New("closed") - -type Client struct { - serverAddr string - - sendBPS, recvBPS uint64 - auth []byte - fastOpen bool - - tlsConfig *tls.Config - quicConfig *quic.Config - - pktConnFunc pktconns.ClientPacketConnFunc - - reconnectMutex sync.Mutex - pktConn net.PacketConn - quicConn quic.Connection - closed bool - - udpSessionMutex sync.RWMutex - udpSessionMap map[uint32]chan *udpMessage - udpDefragger defragger - - quicReconnectFunc func(err error) -} - -func NewClient(serverAddr string, auth []byte, tlsConfig *tls.Config, quicConfig *quic.Config, - pktConnFunc pktconns.ClientPacketConnFunc, sendBPS uint64, recvBPS uint64, fastOpen bool, lazyStart bool, - quicReconnectFunc func(err error), -) (*Client, error) { - quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery - c := &Client{ - serverAddr: serverAddr, - sendBPS: sendBPS, - recvBPS: recvBPS, - auth: auth, - fastOpen: fastOpen, - tlsConfig: tlsConfig, - quicConfig: quicConfig, - pktConnFunc: pktConnFunc, - quicReconnectFunc: quicReconnectFunc, - } - if lazyStart { - return c, nil - } - if err := c.connect(); err != nil { - return nil, err - } - return c, nil -} - -func (c *Client) connect() error { - // Clear previous connection - if c.quicConn != nil { - _ = c.quicConn.CloseWithError(0, "") - } - if c.pktConn != nil { - _ = c.pktConn.Close() - } - // New connection - pktConn, sAddr, err := c.pktConnFunc(c.serverAddr) - if err != nil { - return err - } - // Dial QUIC - quicConn, err := quic.Dial(pktConn, sAddr, c.serverAddr, c.tlsConfig, c.quicConfig) - if err != nil { - _ = pktConn.Close() - return err - } - // Control stream - ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout) - stream, err := quicConn.OpenStreamSync(ctx) - ctxCancel() - if err != nil { - _ = qErrorProtocol.Send(quicConn) - _ = pktConn.Close() - return err - } - ok, msg, err := c.handleControlStream(quicConn, stream) - if err != nil { - _ = qErrorProtocol.Send(quicConn) - _ = pktConn.Close() - return err - } - if !ok { - _ = qErrorAuth.Send(quicConn) - _ = pktConn.Close() - return fmt.Errorf("auth error: %s", msg) - } - // All good - c.udpSessionMap = make(map[uint32]chan *udpMessage) - go c.handleMessage(quicConn) - c.pktConn = pktConn - c.quicConn = quicConn - return nil -} - -func (c *Client) handleControlStream(qc quic.Connection, stream quic.Stream) (bool, string, error) { - // Send protocol version - _, err := stream.Write([]byte{protocolVersion}) - if err != nil { - return false, "", err - } - // Send client hello - err = struc.Pack(stream, &clientHello{ - Rate: maxRate{ - SendBPS: c.sendBPS, - RecvBPS: c.recvBPS, - }, - Auth: c.auth, - }) - if err != nil { - return false, "", err - } - // Receive server hello - var sh serverHello - err = struc.Unpack(stream, &sh) - if err != nil { - return false, "", err - } - // Set the congestion accordingly - if sh.OK { - qc.SetCongestionControl(congestion.NewBrutalSender(sh.Rate.RecvBPS)) - } - return sh.OK, sh.Message, nil -} - -func (c *Client) handleMessage(qc quic.Connection) { - for { - msg, err := qc.ReceiveMessage() - if err != nil { - break - } - var udpMsg udpMessage - err = struc.Unpack(bytes.NewBuffer(msg), &udpMsg) - if err != nil { - continue - } - dfMsg := c.udpDefragger.Feed(udpMsg) - if dfMsg == nil { - continue - } - c.udpSessionMutex.RLock() - ch, ok := c.udpSessionMap[dfMsg.SessionID] - if ok { - select { - case ch <- dfMsg: - // OK - default: - // Silently drop the message when the channel is full - } - } - c.udpSessionMutex.RUnlock() - } -} - -func (c *Client) openStreamWithReconnect() (quic.Connection, quic.Stream, error) { - c.reconnectMutex.Lock() - defer c.reconnectMutex.Unlock() - if c.closed { - return nil, nil, ErrClosed - } - if c.quicConn != nil { - stream, err := c.quicConn.OpenStream() - if err == nil { - // All good - return c.quicConn, &qStream{stream}, nil - } - // Something is wrong - if nErr, ok := err.(net.Error); ok && nErr.Temporary() { - // Temporary error, just return - return nil, nil, err - } - if c.quicReconnectFunc != nil { - c.quicReconnectFunc(err) - } - } - // Permanent error, need to reconnect - if err := c.connect(); err != nil { - // Still error, oops - return nil, nil, err - } - // We are not going to try again even if it still fails the second time - stream, err := c.quicConn.OpenStream() - return c.quicConn, &qStream{stream}, err -} - -func (c *Client) DialTCP(addr string) (net.Conn, error) { - host, port, err := utils.SplitHostPort(addr) - if err != nil { - return nil, err - } - session, stream, err := c.openStreamWithReconnect() - if err != nil { - return nil, err - } - // Send request - err = struc.Pack(stream, &clientRequest{ - UDP: false, - Host: host, - Port: port, - }) - if err != nil { - _ = stream.Close() - return nil, err - } - // If fast open is enabled, we return the stream immediately - // and defer the response handling to the first Read() call - if !c.fastOpen { - // Read response - var sr serverResponse - err = struc.Unpack(stream, &sr) - if err != nil { - _ = stream.Close() - return nil, err - } - if !sr.OK { - _ = stream.Close() - return nil, fmt.Errorf("connection rejected: %s", sr.Message) - } - } - return &hyTCPConn{ - Orig: stream, - PseudoLocalAddr: session.LocalAddr(), - PseudoRemoteAddr: session.RemoteAddr(), - Established: !c.fastOpen, - }, nil -} - -func (c *Client) DialUDP() (HyUDPConn, error) { - session, stream, err := c.openStreamWithReconnect() - if err != nil { - return nil, err - } - // Send request - err = struc.Pack(stream, &clientRequest{ - UDP: true, - }) - if err != nil { - _ = stream.Close() - return nil, err - } - // Read response - var sr serverResponse - err = struc.Unpack(stream, &sr) - if err != nil { - _ = stream.Close() - return nil, err - } - if !sr.OK { - _ = stream.Close() - return nil, fmt.Errorf("connection rejected: %s", sr.Message) - } - - // Create a session in the map - c.udpSessionMutex.Lock() - nCh := make(chan *udpMessage, 1024) - // Store the current session map for CloseFunc below - // to ensure that we are adding and removing sessions on the same map, - // as reconnecting will reassign the map - sessionMap := c.udpSessionMap - sessionMap[sr.UDPSessionID] = nCh - c.udpSessionMutex.Unlock() - - pktConn := &hyUDPConn{ - Session: session, - Stream: stream, - CloseFunc: func() { - c.udpSessionMutex.Lock() - if ch, ok := sessionMap[sr.UDPSessionID]; ok { - close(ch) - delete(sessionMap, sr.UDPSessionID) - } - c.udpSessionMutex.Unlock() - }, - UDPSessionID: sr.UDPSessionID, - MsgCh: nCh, - } - go pktConn.Hold() - return pktConn, nil -} - -func (c *Client) Close() error { - c.reconnectMutex.Lock() - defer c.reconnectMutex.Unlock() - err := qErrorGeneric.Send(c.quicConn) - _ = c.pktConn.Close() - c.closed = true - return err -} - -// hyTCPConn wraps a QUIC stream and implements net.Conn returned by Client.DialTCP -type hyTCPConn struct { - Orig quic.Stream - PseudoLocalAddr net.Addr - PseudoRemoteAddr net.Addr - Established bool -} - -func (w *hyTCPConn) Read(b []byte) (n int, err error) { - if !w.Established { - var sr serverResponse - err := struc.Unpack(w.Orig, &sr) - if err != nil { - _ = w.Close() - return 0, err - } - if !sr.OK { - _ = w.Close() - return 0, fmt.Errorf("connection rejected: %s", sr.Message) - } - w.Established = true - } - return w.Orig.Read(b) -} - -func (w *hyTCPConn) Write(b []byte) (n int, err error) { - return w.Orig.Write(b) -} - -func (w *hyTCPConn) Close() error { - return w.Orig.Close() -} - -func (w *hyTCPConn) LocalAddr() net.Addr { - return w.PseudoLocalAddr -} - -func (w *hyTCPConn) RemoteAddr() net.Addr { - return w.PseudoRemoteAddr -} - -func (w *hyTCPConn) SetDeadline(t time.Time) error { - return w.Orig.SetDeadline(t) -} - -func (w *hyTCPConn) SetReadDeadline(t time.Time) error { - return w.Orig.SetReadDeadline(t) -} - -func (w *hyTCPConn) SetWriteDeadline(t time.Time) error { - return w.Orig.SetWriteDeadline(t) -} - -type HyUDPConn interface { - ReadFrom() ([]byte, string, error) - WriteTo([]byte, string) error - Close() error -} - -type hyUDPConn struct { - Session quic.Connection - Stream quic.Stream - CloseFunc func() - UDPSessionID uint32 - MsgCh <-chan *udpMessage -} - -func (c *hyUDPConn) Hold() { - // Hold the stream until it's closed - buf := make([]byte, 1024) - for { - _, err := c.Stream.Read(buf) - if err != nil { - break - } - } - _ = c.Close() -} - -func (c *hyUDPConn) ReadFrom() ([]byte, string, error) { - msg := <-c.MsgCh - if msg == nil { - // Closed - return nil, "", ErrClosed - } - return msg.Data, net.JoinHostPort(msg.Host, strconv.Itoa(int(msg.Port))), nil -} - -func (c *hyUDPConn) WriteTo(p []byte, addr string) error { - host, port, err := utils.SplitHostPort(addr) - if err != nil { - return err - } - msg := udpMessage{ - SessionID: c.UDPSessionID, - Host: host, - Port: port, - FragCount: 1, - Data: p, - } - // try no frag first - var msgBuf bytes.Buffer - _ = struc.Pack(&msgBuf, &msg) - err = c.Session.SendMessage(msgBuf.Bytes()) - if err != nil { - if errSize, ok := err.(quic.ErrMessageTooLarge); ok { - // need to frag - msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 - fragMsgs := fragUDPMessage(msg, int(errSize)) - for _, fragMsg := range fragMsgs { - msgBuf.Reset() - _ = struc.Pack(&msgBuf, &fragMsg) - err = c.Session.SendMessage(msgBuf.Bytes()) - if err != nil { - return err - } - } - return nil - } else { - // some other error - return err - } - } else { - return nil - } -} - -func (c *hyUDPConn) Close() error { - c.CloseFunc() - return c.Stream.Close() -} diff --git a/core/cs/frag.go b/core/cs/frag.go deleted file mode 100644 index 82275e3..0000000 --- a/core/cs/frag.go +++ /dev/null @@ -1,67 +0,0 @@ -package cs - -func fragUDPMessage(m udpMessage, maxSize int) []udpMessage { - if m.Size() <= maxSize { - return []udpMessage{m} - } - fullPayload := m.Data - maxPayloadSize := maxSize - m.HeaderSize() - off := 0 - fragID := uint8(0) - fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up - var frags []udpMessage - for off < len(fullPayload) { - payloadSize := len(fullPayload) - off - if payloadSize > maxPayloadSize { - payloadSize = maxPayloadSize - } - frag := m - frag.FragID = fragID - frag.FragCount = fragCount - frag.DataLen = uint16(payloadSize) - frag.Data = fullPayload[off : off+payloadSize] - frags = append(frags, frag) - off += payloadSize - fragID++ - } - return frags -} - -type defragger struct { - msgID uint16 - frags []*udpMessage - count uint8 -} - -func (d *defragger) Feed(m udpMessage) *udpMessage { - if m.FragCount <= 1 { - return &m - } - if m.FragID >= m.FragCount { - // wtf is this? - return nil - } - if m.MsgID != d.msgID || m.FragCount != uint8(len(d.frags)) { - // new message, clear previous state - d.msgID = m.MsgID - d.frags = make([]*udpMessage, m.FragCount) - d.count = 1 - d.frags[m.FragID] = &m - } else if d.frags[m.FragID] == nil { - d.frags[m.FragID] = &m - d.count++ - if int(d.count) == len(d.frags) { - // all fragments received, assemble - var data []byte - for _, frag := range d.frags { - data = append(data, frag.Data...) - } - m.DataLen = uint16(len(data)) - m.Data = data - m.FragID = 0 - m.FragCount = 1 - return &m - } - } - return nil -} diff --git a/core/cs/frag_test.go b/core/cs/frag_test.go deleted file mode 100644 index 884a322..0000000 --- a/core/cs/frag_test.go +++ /dev/null @@ -1,390 +0,0 @@ -package cs - -import ( - "reflect" - "testing" -) - -func Test_fragUDPMessage(t *testing.T) { - type args struct { - m udpMessage - maxSize int - } - tests := []struct { - name string - args args - want []udpMessage - }{ - { - "no frag", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - 100, - }, - []udpMessage{ - { - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - }, - }, - { - "2 frags", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - 22, - }, - []udpMessage{ - { - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 2, - DataLen: 4, - Data: []byte("hell"), - }, - { - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 1, - FragCount: 2, - DataLen: 1, - Data: []byte("o"), - }, - }, - }, - { - "4 frags", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 20, - Data: []byte("wow wow wow lol lmao"), - }, - 23, - }, - []udpMessage{ - { - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 4, - DataLen: 5, - Data: []byte("wow w"), - }, - { - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 1, - FragCount: 4, - DataLen: 5, - Data: []byte("ow wo"), - }, - { - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 2, - FragCount: 4, - DataLen: 5, - Data: []byte("w lol"), - }, - { - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 3, - FragCount: 4, - DataLen: 5, - Data: []byte(" lmao"), - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := fragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) { - t.Errorf("fragUDPMessage() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_defragger_Feed(t *testing.T) { - d := &defragger{} - type args struct { - m udpMessage - } - tests := []struct { - name string - args args - want *udpMessage - }{ - { - "no frag", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - }, - &udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 123, - FragID: 0, - FragCount: 1, - DataLen: 5, - Data: []byte("hello"), - }, - }, - { - "frag 0 - 1/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 0, - FragID: 0, - FragCount: 2, - DataLen: 5, - Data: []byte("ilove"), - }, - }, - nil, - }, - { - "frag 0 - 2/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 0, - FragID: 1, - FragCount: 2, - DataLen: 6, - Data: []byte("nobody"), - }, - }, - &udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 0, - FragID: 0, - FragCount: 1, - DataLen: 11, - Data: []byte("ilovenobody"), - }, - }, - { - "frag 1 - 1/3", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 0, - FragCount: 3, - DataLen: 5, - Data: []byte("hello"), - }, - }, - nil, - }, - { - "frag 1 - 2/3", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 1, - FragCount: 3, - DataLen: 8, - Data: []byte(" shitty "), - }, - }, - nil, - }, - { - "frag 1 - 3/3", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 2, - FragCount: 3, - DataLen: 7, - Data: []byte("world!!"), - }, - }, - &udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 666, - FragID: 0, - FragCount: 1, - DataLen: 20, - Data: []byte("hello shitty world!!"), - }, - }, - { - "frag 2 - 1/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 0, - FragCount: 2, - DataLen: 5, - Data: []byte("hello"), - }, - }, - nil, - }, - { - "frag 3 - 2/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 778, - FragID: 1, - FragCount: 2, - DataLen: 5, - Data: []byte(" moto"), - }, - }, - nil, - }, - { - "frag 2 - 2/2", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 1, - FragCount: 2, - DataLen: 5, - Data: []byte(" moto"), - }, - }, - nil, - }, - { - "frag 2 - 1/2 re", - args{ - udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 0, - FragCount: 2, - DataLen: 5, - Data: []byte("hello"), - }, - }, - &udpMessage{ - SessionID: 123, - HostLen: 4, - Host: "test", - Port: 123, - MsgID: 777, - FragID: 0, - FragCount: 1, - DataLen: 10, - Data: []byte("hello moto"), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Feed() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/core/cs/protocol.go b/core/cs/protocol.go deleted file mode 100644 index 414a32c..0000000 --- a/core/cs/protocol.go +++ /dev/null @@ -1,79 +0,0 @@ -package cs - -import ( - "time" - - "github.com/quic-go/quic-go" -) - -const ( - protocolVersion = uint8(3) - protocolTimeout = 10 * time.Second -) - -type qError struct { - Code quic.ApplicationErrorCode - Msg string -} - -func (e qError) Send(c quic.Connection) error { - return c.CloseWithError(e.Code, e.Msg) -} - -var ( - qErrorGeneric = qError{0, ""} - qErrorProtocol = qError{1, "protocol error"} - qErrorAuth = qError{2, "auth error"} -) - -type maxRate struct { - SendBPS uint64 - RecvBPS uint64 -} - -type clientHello struct { - Rate maxRate - AuthLen uint16 `struc:"sizeof=Auth"` - Auth []byte -} - -type serverHello struct { - OK bool - Rate maxRate - MessageLen uint16 `struc:"sizeof=Message"` - Message string -} - -type clientRequest struct { - UDP bool - HostLen uint16 `struc:"sizeof=Host"` - Host string - Port uint16 -} - -type serverResponse struct { - OK bool - UDPSessionID uint32 - MessageLen uint16 `struc:"sizeof=Message"` - Message string -} - -type udpMessage struct { - SessionID uint32 - HostLen uint16 `struc:"sizeof=Host"` - Host string - Port uint16 - MsgID uint16 // doesn't matter when not fragmented, but must not be 0 when fragmented - FragID uint8 // doesn't matter when not fragmented, starts at 0 when fragmented - FragCount uint8 // must be 1 when not fragmented - DataLen uint16 `struc:"sizeof=Data"` - Data []byte -} - -func (m udpMessage) HeaderSize() int { - return 4 + 2 + len(m.Host) + 2 + 2 + 1 + 1 + 2 -} - -func (m udpMessage) Size() int { - return m.HeaderSize() + len(m.Data) -} diff --git a/core/cs/server.go b/core/cs/server.go deleted file mode 100644 index 4922c13..0000000 --- a/core/cs/server.go +++ /dev/null @@ -1,178 +0,0 @@ -package cs - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "net" - - "github.com/apernet/hysteria/core/congestion" - - "github.com/apernet/hysteria/core/acl" - "github.com/apernet/hysteria/core/pmtud" - "github.com/apernet/hysteria/core/transport" - "github.com/lunixbochs/struc" - "github.com/quic-go/quic-go" -) - -type ( - ConnectFunc func(addr net.Addr, auth []byte, sSend uint64, sRecv uint64) (bool, string) - DisconnectFunc func(addr net.Addr, auth []byte, err error) - TCPRequestFunc func(addr net.Addr, auth []byte, reqAddr string, action acl.Action, arg string) - TCPErrorFunc func(addr net.Addr, auth []byte, reqAddr string, err error) - UDPRequestFunc func(addr net.Addr, auth []byte, sessionID uint32) - UDPErrorFunc func(addr net.Addr, auth []byte, sessionID uint32, err error) -) - -type TrafficCounter interface { - Rx(auth string, n int) - Tx(auth string, n int) - IncConn(auth string) // increase connection count - DecConn(auth string) // decrease connection count -} - -type Server struct { - transport *transport.ServerTransport - sendBPS, recvBPS uint64 - disableUDP bool - aclEngine *acl.Engine - - connectFunc ConnectFunc - disconnectFunc DisconnectFunc - tcpRequestFunc TCPRequestFunc - tcpErrorFunc TCPErrorFunc - udpRequestFunc UDPRequestFunc - udpErrorFunc UDPErrorFunc - - trafficCounter TrafficCounter - - pktConn net.PacketConn - listener quic.Listener -} - -func NewServer(tlsConfig *tls.Config, quicConfig *quic.Config, - pktConn net.PacketConn, transport *transport.ServerTransport, - sendBPS uint64, recvBPS uint64, disableUDP bool, aclEngine *acl.Engine, - connectFunc ConnectFunc, disconnectFunc DisconnectFunc, - tcpRequestFunc TCPRequestFunc, tcpErrorFunc TCPErrorFunc, - udpRequestFunc UDPRequestFunc, udpErrorFunc UDPErrorFunc, - trafficCounter TrafficCounter, -) (*Server, error) { - quicConfig.DisablePathMTUDiscovery = quicConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery - listener, err := quic.Listen(pktConn, tlsConfig, quicConfig) - if err != nil { - _ = pktConn.Close() - return nil, err - } - s := &Server{ - pktConn: pktConn, - listener: listener, - transport: transport, - sendBPS: sendBPS, - recvBPS: recvBPS, - disableUDP: disableUDP, - aclEngine: aclEngine, - connectFunc: connectFunc, - disconnectFunc: disconnectFunc, - tcpRequestFunc: tcpRequestFunc, - tcpErrorFunc: tcpErrorFunc, - udpRequestFunc: udpRequestFunc, - udpErrorFunc: udpErrorFunc, - trafficCounter: trafficCounter, - } - return s, nil -} - -func (s *Server) Serve() error { - for { - cc, err := s.listener.Accept(context.Background()) - if err != nil { - return err - } - go s.handleClient(cc) - } -} - -func (s *Server) Close() error { - err := s.listener.Close() - _ = s.pktConn.Close() - return err -} - -func (s *Server) handleClient(cc quic.Connection) { - // Expect the client to create a control stream to send its own information - ctx, ctxCancel := context.WithTimeout(context.Background(), protocolTimeout) - stream, err := cc.AcceptStream(ctx) - ctxCancel() - if err != nil { - _ = qErrorProtocol.Send(cc) - return - } - // Handle the control stream - auth, ok, err := s.handleControlStream(cc, stream) - if err != nil { - _ = qErrorProtocol.Send(cc) - return - } - if !ok { - _ = qErrorAuth.Send(cc) - return - } - // Start accepting streams and messages - sc := newServerClient(cc, s.transport, auth, s.disableUDP, s.aclEngine, - s.tcpRequestFunc, s.tcpErrorFunc, s.udpRequestFunc, s.udpErrorFunc, - s.trafficCounter) - err = sc.Run() - _ = qErrorGeneric.Send(cc) - s.disconnectFunc(cc.RemoteAddr(), auth, err) -} - -// Auth & negotiate speed -func (s *Server) handleControlStream(cc quic.Connection, stream quic.Stream) ([]byte, bool, error) { - // Check version - vb := make([]byte, 1) - _, err := stream.Read(vb) - if err != nil { - return nil, false, err - } - if vb[0] != protocolVersion { - return nil, false, fmt.Errorf("unsupported protocol version %d, expecting %d", vb[0], protocolVersion) - } - // Parse client hello - var ch clientHello - err = struc.Unpack(stream, &ch) - if err != nil { - return nil, false, err - } - // Speed - if ch.Rate.SendBPS == 0 || ch.Rate.RecvBPS == 0 { - return nil, false, errors.New("invalid rate from client") - } - serverSendBPS, serverRecvBPS := ch.Rate.RecvBPS, ch.Rate.SendBPS - if s.sendBPS > 0 && serverSendBPS > s.sendBPS { - serverSendBPS = s.sendBPS - } - if s.recvBPS > 0 && serverRecvBPS > s.recvBPS { - serverRecvBPS = s.recvBPS - } - // Auth - ok, msg := s.connectFunc(cc.RemoteAddr(), ch.Auth, serverSendBPS, serverRecvBPS) - // Response - err = struc.Pack(stream, &serverHello{ - OK: ok, - Rate: maxRate{ - SendBPS: serverSendBPS, - RecvBPS: serverRecvBPS, - }, - Message: msg, - }) - if err != nil { - return nil, false, err - } - // Set the congestion accordingly - if ok { - cc.SetCongestionControl(congestion.NewBrutalSender(serverSendBPS)) - } - return ch.Auth, ok, nil -} diff --git a/core/cs/server_client.go b/core/cs/server_client.go deleted file mode 100644 index db109b1..0000000 --- a/core/cs/server_client.go +++ /dev/null @@ -1,391 +0,0 @@ -package cs - -import ( - "bytes" - "context" - "encoding/base64" - "math/rand" - "net" - "strconv" - "sync" - - "github.com/apernet/hysteria/core/acl" - "github.com/apernet/hysteria/core/transport" - "github.com/apernet/hysteria/core/utils" - "github.com/lunixbochs/struc" - "github.com/quic-go/quic-go" -) - -const udpBufferSize = 4096 - -type serverClient struct { - CC quic.Connection - Transport *transport.ServerTransport - Auth []byte - AuthLabel string // Base64 encoded auth - DisableUDP bool - ACLEngine *acl.Engine - CTCPRequestFunc TCPRequestFunc - CTCPErrorFunc TCPErrorFunc - CUDPRequestFunc UDPRequestFunc - CUDPErrorFunc UDPErrorFunc - - TrafficCounter TrafficCounter - - udpSessionMutex sync.RWMutex - udpSessionMap map[uint32]transport.STPacketConn - nextUDPSessionID uint32 - udpDefragger defragger -} - -func newServerClient(cc quic.Connection, tr *transport.ServerTransport, auth []byte, disableUDP bool, ACLEngine *acl.Engine, - CTCPRequestFunc TCPRequestFunc, CTCPErrorFunc TCPErrorFunc, - CUDPRequestFunc UDPRequestFunc, CUDPErrorFunc UDPErrorFunc, - TrafficCounter TrafficCounter, -) *serverClient { - sc := &serverClient{ - CC: cc, - Transport: tr, - Auth: auth, - AuthLabel: base64.StdEncoding.EncodeToString(auth), - DisableUDP: disableUDP, - ACLEngine: ACLEngine, - CTCPRequestFunc: CTCPRequestFunc, - CTCPErrorFunc: CTCPErrorFunc, - CUDPRequestFunc: CUDPRequestFunc, - CUDPErrorFunc: CUDPErrorFunc, - TrafficCounter: TrafficCounter, - udpSessionMap: make(map[uint32]transport.STPacketConn), - } - return sc -} - -func (c *serverClient) ClientAddr() net.Addr { - // quic.Connection's remote address may change since we have connection migration now, - // so logs need to dynamically get the remote address every time. - return c.CC.RemoteAddr() -} - -func (c *serverClient) Run() error { - if !c.DisableUDP { - go func() { - for { - msg, err := c.CC.ReceiveMessage() - if err != nil { - break - } - c.handleMessage(msg) - } - }() - } - for { - stream, err := c.CC.AcceptStream(context.Background()) - if err != nil { - return err - } - if c.TrafficCounter != nil { - c.TrafficCounter.IncConn(c.AuthLabel) - } - go func() { - stream := &qStream{stream} - c.handleStream(stream) - _ = stream.Close() - if c.TrafficCounter != nil { - c.TrafficCounter.DecConn(c.AuthLabel) - } - }() - } -} - -func (c *serverClient) handleStream(stream quic.Stream) { - // Read request - var req clientRequest - err := struc.Unpack(stream, &req) - if err != nil { - return - } - if !req.UDP { - // TCP connection - c.handleTCP(stream, req.Host, req.Port) - } else if !c.DisableUDP { - // UDP connection - c.handleUDP(stream) - } else { - // UDP disabled - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: "UDP disabled", - }) - } -} - -func (c *serverClient) handleMessage(msg []byte) { - var udpMsg udpMessage - err := struc.Unpack(bytes.NewBuffer(msg), &udpMsg) - if err != nil { - return - } - dfMsg := c.udpDefragger.Feed(udpMsg) - if dfMsg == nil { - return - } - c.udpSessionMutex.RLock() - conn, ok := c.udpSessionMap[dfMsg.SessionID] - c.udpSessionMutex.RUnlock() - if ok { - // Session found, send the message - action, arg := acl.ActionDirect, "" - var isDomain bool - var ipAddr *net.IPAddr - var err error - if c.ACLEngine != nil { - action, arg, isDomain, ipAddr, err = c.ACLEngine.ResolveAndMatch(dfMsg.Host, dfMsg.Port, true) - } else if c.Transport.ProxyEnabled() { // Case for SOCKS5 outbound - ipAddr, isDomain = c.Transport.ParseIPAddr(dfMsg.Host) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain - err = nil - } else { - ipAddr, isDomain, err = c.Transport.ResolveIPAddr(dfMsg.Host) - } - if err != nil { - return - } - switch action { - case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side - addrEx := &transport.AddrEx{ - IPAddr: ipAddr, - Port: int(dfMsg.Port), - } - if isDomain { - addrEx.Domain = dfMsg.Host - } - _, _ = conn.WriteTo(dfMsg.Data, addrEx) - if c.TrafficCounter != nil { - c.TrafficCounter.Tx(c.AuthLabel, len(dfMsg.Data)) - } - case acl.ActionBlock: - // Do nothing - case acl.ActionHijack: - var isDomain bool - var hijackIPAddr *net.IPAddr - var err error - if c.Transport.ProxyEnabled() { // Case for domain requests + SOCKS5 outbound - hijackIPAddr, isDomain = c.Transport.ParseIPAddr(arg) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain - err = nil - } else { - hijackIPAddr, isDomain, err = c.Transport.ResolveIPAddr(arg) - } - if err == nil { - addrEx := &transport.AddrEx{ - IPAddr: hijackIPAddr, - Port: int(dfMsg.Port), - } - if isDomain { - addrEx.Domain = arg - } - _, _ = conn.WriteTo(dfMsg.Data, addrEx) - if c.TrafficCounter != nil { - c.TrafficCounter.Tx(c.AuthLabel, len(dfMsg.Data)) - } - } - default: - // Do nothing - } - } -} - -func (c *serverClient) handleTCP(stream quic.Stream, host string, port uint16) { - addrStr := net.JoinHostPort(host, strconv.Itoa(int(port))) - action, arg := acl.ActionDirect, "" - var isDomain bool - var ipAddr *net.IPAddr - var err error - if c.ACLEngine != nil { - action, arg, isDomain, ipAddr, err = c.ACLEngine.ResolveAndMatch(host, port, false) - } else if c.Transport.ProxyEnabled() { // Case for domain requests + SOCKS5 outbound - ipAddr, isDomain = c.Transport.ParseIPAddr(host) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain - err = nil - } else { - ipAddr, isDomain, err = c.Transport.ResolveIPAddr(host) - } - if err != nil { - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: "host resolution failure", - }) - c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err) - return - } - c.CTCPRequestFunc(c.ClientAddr(), c.Auth, addrStr, action, arg) - - var conn net.Conn // Connection to be piped - switch action { - case acl.ActionDirect, acl.ActionProxy: // Treat proxy as direct on server side - addrEx := &transport.AddrEx{ - IPAddr: ipAddr, - Port: int(port), - } - if isDomain { - addrEx.Domain = host - } - conn, err = c.Transport.DialTCP(addrEx) - if err != nil { - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: err.Error(), - }) - c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err) - return - } - case acl.ActionBlock: - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: "blocked by ACL", - }) - return - case acl.ActionHijack: - var isDomain bool - var hijackIPAddr *net.IPAddr - var err error - if c.Transport.ProxyEnabled() { // Case for domain requests + SOCKS5 outbound - hijackIPAddr, isDomain = c.Transport.ParseIPAddr(arg) // It is safe to leave ipAddr as nil since addrExToSOCKS5Addr will ignore it when there is a domain - err = nil - } else { - hijackIPAddr, isDomain, err = c.Transport.ResolveIPAddr(arg) - } - if err != nil { - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: err.Error(), - }) - c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err) - return - } - addrEx := &transport.AddrEx{ - IPAddr: hijackIPAddr, - Port: int(port), - } - if isDomain { - addrEx.Domain = arg - } - conn, err = c.Transport.DialTCP(addrEx) - if err != nil { - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: err.Error(), - }) - c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err) - return - } - default: - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: "ACL error", - }) - return - } - // So far so good if we reach here - defer conn.Close() - err = struc.Pack(stream, &serverResponse{ - OK: true, - }) - if err != nil { - return - } - if c.TrafficCounter != nil { - err = utils.Pipe2Way(stream, conn, func(i int) { - if i > 0 { - c.TrafficCounter.Tx(c.AuthLabel, i) - } else { - c.TrafficCounter.Rx(c.AuthLabel, -i) - } - }) - } else { - err = utils.Pipe2Way(stream, conn, nil) - } - c.CTCPErrorFunc(c.ClientAddr(), c.Auth, addrStr, err) -} - -func (c *serverClient) handleUDP(stream quic.Stream) { - // Like in SOCKS5, the stream here is only used to maintain the UDP session. No need to read anything from it - conn, err := c.Transport.ListenUDP() - if err != nil { - _ = struc.Pack(stream, &serverResponse{ - OK: false, - Message: "UDP initialization failed", - }) - c.CUDPErrorFunc(c.ClientAddr(), c.Auth, 0, err) - return - } - defer conn.Close() - - var id uint32 - c.udpSessionMutex.Lock() - id = c.nextUDPSessionID - c.udpSessionMap[id] = conn - c.nextUDPSessionID += 1 - c.udpSessionMutex.Unlock() - - err = struc.Pack(stream, &serverResponse{ - OK: true, - UDPSessionID: id, - }) - if err != nil { - return - } - c.CUDPRequestFunc(c.ClientAddr(), c.Auth, id) - - // Receive UDP packets, send them to the client - go func() { - buf := make([]byte, udpBufferSize) - for { - n, rAddr, err := conn.ReadFrom(buf) - if n > 0 { - var msgBuf bytes.Buffer - msg := udpMessage{ - SessionID: id, - Host: rAddr.IP.String(), - Port: uint16(rAddr.Port), - FragCount: 1, - Data: buf[:n], - } - // try no frag first - _ = struc.Pack(&msgBuf, &msg) - sendErr := c.CC.SendMessage(msgBuf.Bytes()) - if sendErr != nil { - if errSize, ok := sendErr.(quic.ErrMessageTooLarge); ok { - // need to frag - msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 - fragMsgs := fragUDPMessage(msg, int(errSize)) - for _, fragMsg := range fragMsgs { - msgBuf.Reset() - _ = struc.Pack(&msgBuf, &fragMsg) - _ = c.CC.SendMessage(msgBuf.Bytes()) - } - } - } - if c.TrafficCounter != nil { - c.TrafficCounter.Rx(c.AuthLabel, n) - } - } - if err != nil { - break - } - } - _ = stream.Close() - }() - - // Hold the stream until it's closed by the client - buf := make([]byte, 1024) - for { - _, err = stream.Read(buf) - if err != nil { - break - } - } - c.CUDPErrorFunc(c.ClientAddr(), c.Auth, id, err) - - // Remove the session - c.udpSessionMutex.Lock() - delete(c.udpSessionMap, id) - c.udpSessionMutex.Unlock() -} diff --git a/core/cs/stream.go b/core/cs/stream.go deleted file mode 100644 index e055512..0000000 --- a/core/cs/stream.go +++ /dev/null @@ -1,58 +0,0 @@ -package cs - -import ( - "context" - "time" - - "github.com/quic-go/quic-go" -) - -// qStream is a wrapper of quic.Stream that handles Close() correctly. -// quic-go's quic.Stream.Close() only closes the write side of the stream, -// NOT the read side. This would cause the pipe(s) to hang at Read() even -// after the stream is supposedly "closed". -// Ref: https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/stream.go -type qStream struct { - Stream quic.Stream -} - -func (s *qStream) StreamID() quic.StreamID { - return s.Stream.StreamID() -} - -func (s *qStream) Read(p []byte) (n int, err error) { - return s.Stream.Read(p) -} - -func (s *qStream) CancelRead(code quic.StreamErrorCode) { - s.Stream.CancelRead(code) -} - -func (s *qStream) SetReadDeadline(t time.Time) error { - return s.Stream.SetReadDeadline(t) -} - -func (s *qStream) Write(p []byte) (n int, err error) { - return s.Stream.Write(p) -} - -func (s *qStream) Close() error { - s.Stream.CancelRead(0) - return s.Stream.Close() -} - -func (s *qStream) CancelWrite(code quic.StreamErrorCode) { - s.Stream.CancelWrite(code) -} - -func (s *qStream) Context() context.Context { - return s.Stream.Context() -} - -func (s *qStream) SetWriteDeadline(t time.Time) error { - return s.Stream.SetWriteDeadline(t) -} - -func (s *qStream) SetDeadline(t time.Time) error { - return s.Stream.SetDeadline(t) -} diff --git a/core/errors/errors.go b/core/errors/errors.go new file mode 100644 index 0000000..094ce1f --- /dev/null +++ b/core/errors/errors.go @@ -0,0 +1,58 @@ +package errors + +import ( + "fmt" + "strconv" +) + +// ConfigError is returned when a configuration field is invalid. +type ConfigError struct { + Field string + Reason string +} + +func (c ConfigError) Error() string { + return fmt.Sprintf("invalid config: %s: %s", c.Field, c.Reason) +} + +// ConnectError is returned when the client fails to connect to the server. +type ConnectError struct { + Err error +} + +func (c ConnectError) Error() string { + return "connect error: " + c.Err.Error() +} + +func (c ConnectError) Unwrap() error { + return c.Err +} + +// AuthError is returned when the client fails to authenticate with the server. +type AuthError struct { + StatusCode int +} + +func (a AuthError) Error() string { + return "authentication error, HTTP status code: " + strconv.Itoa(a.StatusCode) +} + +// DialError is returned when the server rejects the client's dial request. +// This applies to both TCP and UDP. +type DialError struct { + Message string +} + +func (c DialError) Error() string { + return "dial error: " + c.Message +} + +// ProtocolError is returned when the server/client runs into an unexpected +// or malformed request/response/message. +type ProtocolError struct { + Message string +} + +func (p ProtocolError) Error() string { + return "protocol error: " + p.Message +} diff --git a/core/go.mod b/core/go.mod index 1f89634..0a16cfb 100644 --- a/core/go.mod +++ b/core/go.mod @@ -3,34 +3,28 @@ module github.com/apernet/hysteria/core go 1.20 require ( - github.com/coreos/go-iptables v0.6.0 - github.com/google/gopacket v1.1.19 - github.com/hashicorp/golang-lru/v2 v2.0.1 - github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 - github.com/oschwald/geoip2-golang v1.8.0 - github.com/quic-go/quic-go v0.34.0 - github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a - golang.org/x/sys v0.7.0 + github.com/quic-go/quic-go v0.0.0-00010101000000-000000000000 + golang.org/x/time v0.3.0 ) require ( github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f // indirect - github.com/onsi/ginkgo/v2 v2.8.0 // indirect - github.com/oschwald/maxminddb-golang v1.10.0 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/onsi/ginkgo/v2 v2.2.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect github.com/stretchr/testify v1.8.1 // indirect - github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect - github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/tools v0.6.0 // indirect - google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d // indirect + golang.org/x/crypto v0.4.0 // indirect + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect + golang.org/x/mod v0.7.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.3.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect ) replace github.com/quic-go/quic-go => github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 diff --git a/core/go.sum b/core/go.sum index 1a28ac9..7339984 100644 --- a/core/go.sum +++ b/core/go.sum @@ -1,11 +1,11 @@ github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 h1:3KFetJ/lUFn0m9xTFg+rMmz2nyHg+D2boJX0Rp4OF6c= github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g= -github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= -github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -14,25 +14,17 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f h1:gl1DCiSk+mrXXBGPm6CEeS2MkJuMVzAOrXg34oVj1QI= -github.com/google/pprof v0.0.0-20230131232505-5a9e8f65f08f/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= -github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= -github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= -github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI= -github.com/onsi/ginkgo/v2 v2.8.0/go.mod h1:6JsQiECmxCa3V5st74AL/AmsV482EDdVrGaVW6z3oYU= -github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= -github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= -github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= -github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= -github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= @@ -45,55 +37,50 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= -github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= -github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a h1:BOqgJ4jku0LHPDoR51RD8Mxmo0LHxCzJT/M9MemYdHo= -github.com/txthinking/socks5 v0.0.0-20220212043548-414499347d4a/go.mod h1:7NloQcrxaZYKURWph5HLxVDlIwMHJXCPkeWPtpftsIg= -github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe h1:gMWxZxBFRAXqoGkwkYlPX2zvyyKNWJpxOxCrjqJkm5A= -github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 h1:BEABXpNXLEz0WxtA+6CQIz2xkg80e+1zrhWyMcq8VzE= -golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ= -google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/core/congestion/brutal.go b/core/internal/congestion/brutal.go similarity index 100% rename from core/congestion/brutal.go rename to core/internal/congestion/brutal.go diff --git a/core/congestion/pacer.go b/core/internal/congestion/pacer.go similarity index 100% rename from core/congestion/pacer.go rename to core/internal/congestion/pacer.go diff --git a/core/internal/frag/frag.go b/core/internal/frag/frag.go new file mode 100644 index 0000000..3493519 --- /dev/null +++ b/core/internal/frag/frag.go @@ -0,0 +1,77 @@ +package frag + +import ( + "github.com/apernet/hysteria/core/internal/protocol" +) + +func FragUDPMessage(m protocol.UDPMessage, maxSize int) []protocol.UDPMessage { + if m.Size() <= maxSize { + return []protocol.UDPMessage{m} + } + fullPayload := m.Data + maxPayloadSize := maxSize - m.HeaderSize() + off := 0 + fragID := uint8(0) + fragCount := uint8((len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize) // round up + frags := make([]protocol.UDPMessage, fragCount) + for off < len(fullPayload) { + payloadSize := len(fullPayload) - off + if payloadSize > maxPayloadSize { + payloadSize = maxPayloadSize + } + frag := m + frag.FragID = fragID + frag.FragCount = fragCount + frag.Data = fullPayload[off : off+payloadSize] + frags[fragID] = frag + off += payloadSize + fragID++ + } + return frags +} + +// Defragger handles the defragmentation of UDP messages. +// The current implementation can only handle one packet ID at a time. +// If another packet arrives before a packet has received all fragments +// in their entirety, any previous state is discarded. +type Defragger struct { + pktID uint16 + frags []*protocol.UDPMessage + count uint8 + size int // data size +} + +func (d *Defragger) Feed(m *protocol.UDPMessage) *protocol.UDPMessage { + if m.FragCount <= 1 { + return m + } + if m.FragID >= m.FragCount { + // wtf is this? + return nil + } + if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) { + // new message, clear previous state + d.pktID = m.PacketID + d.frags = make([]*protocol.UDPMessage, m.FragCount) + d.frags[m.FragID] = m + d.count = 1 + d.size = len(m.Data) + } else if d.frags[m.FragID] == nil { + d.frags[m.FragID] = m + d.count++ + d.size += len(m.Data) + if int(d.count) == len(d.frags) { + // all fragments received, assemble + data := make([]byte, d.size) + off := 0 + for _, frag := range d.frags { + off += copy(data[off:], frag.Data) + } + m.Data = data + m.FragID = 0 + m.FragCount = 1 + return m + } + } + return nil +} diff --git a/core/internal/frag/frag_test.go b/core/internal/frag/frag_test.go new file mode 100644 index 0000000..48eb004 --- /dev/null +++ b/core/internal/frag/frag_test.go @@ -0,0 +1,336 @@ +package frag + +import ( + "reflect" + "testing" + + "github.com/apernet/hysteria/core/internal/protocol" +) + +func TestFragUDPMessage(t *testing.T) { + type args struct { + m protocol.UDPMessage + maxSize int + } + tests := []struct { + name string + args args + want []protocol.UDPMessage + }{ + { + "no frag", + args{ + protocol.UDPMessage{ + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + 100, + }, + []protocol.UDPMessage{ + { + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + }, + }, + { + "2 frags", + args{ + protocol.UDPMessage{ + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + 20, + }, + []protocol.UDPMessage{ + { + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 2, + Addr: "test:123", + Data: []byte("hel"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("lo"), + }, + }, + }, + { + "4 frags", + args{ + protocol.UDPMessage{ + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("abcdefgh"), + }, + 19, + }, + []protocol.UDPMessage{ + { + SessionID: 123, + PacketID: 123, + FragID: 0, + FragCount: 4, + Addr: "test:123", + Data: []byte("ab"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 1, + FragCount: 4, + Addr: "test:123", + Data: []byte("cd"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 2, + FragCount: 4, + Addr: "test:123", + Data: []byte("ef"), + }, + { + SessionID: 123, + PacketID: 123, + FragID: 3, + FragCount: 4, + Addr: "test:123", + Data: []byte("gh"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FragUDPMessage(tt.args.m, tt.args.maxSize); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FragUDPMessage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDefragger(t *testing.T) { + type args struct { + m *protocol.UDPMessage + } + tests := []struct { + name string + args args + want *protocol.UDPMessage + }{ + { + "no frag", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello"), + }, + }, + { + "frag 0 - 1/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 2, + Addr: "test:123", + Data: []byte("hello "), + }, + }, + nil, + }, + { + "frag 0 - 2/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("moto"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("hello moto"), + }, + }, + { + "frag 1 - 1/3", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 3, + Addr: "test:123", + Data: []byte("deco"), + }, + }, + nil, + }, + { + "frag 1 - 2/3", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 1, + FragCount: 3, + Addr: "test:123", + Data: []byte("*"), + }, + }, + nil, + }, + { + "frag 1 - 3/3", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 2, + FragCount: 3, + Addr: "test:123", + Data: []byte("27"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 987, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("deco*27"), + }, + }, + { + "frag 2 - 1/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("shinsekai"), + }, + }, + nil, + }, + { + "frag 3 - 2/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 244, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte("what???"), + }, + }, + nil, + }, + { + "frag 2 - 2/2", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 1, + FragCount: 2, + Addr: "test:123", + Data: []byte(" annaijo"), + }, + }, + nil, + }, + { + "invalid id", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 88, + FragCount: 2, + Addr: "test:123", + Data: []byte("shinsekai"), + }, + }, + nil, + }, + { + "frag 2 - 1/2 re", + args{ + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 0, + FragCount: 2, + Addr: "test:123", + Data: []byte("shinsekai"), + }, + }, + &protocol.UDPMessage{ + SessionID: 123, + PacketID: 233, + FragID: 0, + FragCount: 1, + Addr: "test:123", + Data: []byte("shinsekai annaijo"), + }, + }, + } + + d := &Defragger{} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := d.Feed(tt.args.m); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Feed() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/core/internal/integration_tests/close_test.go b/core/internal/integration_tests/close_test.go new file mode 100644 index 0000000..a0a0008 --- /dev/null +++ b/core/internal/integration_tests/close_test.go @@ -0,0 +1,191 @@ +package integration_tests + +import ( + "bytes" + "crypto/rand" + "io" + "net" + "testing" + + "github.com/apernet/hysteria/core/client" + "github.com/apernet/hysteria/core/server" +) + +// TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly. +// In other words, closing one of the client/remote connections should cause the other to close as well. +func TestClientServerTCPClose(t *testing.T) { + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "password", + ID: "nobody", + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + ServerName: udpAddr.String(), + Auth: "password", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + t.Run("Close local", func(t *testing.T) { + // TCP sink server + sinkAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 33344} + sinkListener, err := net.ListenTCP("tcp", sinkAddr) + if err != nil { + t.Fatal("error creating sink server:", err) + } + sinkCh := make(chan sinkEvent, 1) + sinkServer := &tcpSinkServer{ + Listener: sinkListener, + Ch: sinkCh, + } + defer sinkServer.Close() + go sinkServer.Serve() + + // Generate some random data + sData := make([]byte, 1024000) + _, err = rand.Read(sData) + if err != nil { + t.Fatal("error generating random data:", err) + } + + // Dial and send data to TCP sink server + conn, err := c.DialTCP(sinkAddr.String()) + if err != nil { + t.Fatal("error dialing TCP:", err) + } + defer conn.Close() + _, err = conn.Write(sData) + if err != nil { + t.Fatal("error writing to TCP:", err) + } + + // Close the connection + // This should cause the sink server to send an event to the channel + _ = conn.Close() + event := <-sinkCh + if event.Err != nil { + t.Fatal("non-nil error received from sink server:", event.Err) + } + if !bytes.Equal(event.Data, sData) { + t.Fatal("data mismatch") + } + }) + + t.Run("Close remote", func(t *testing.T) { + // Generate some random data + sData := make([]byte, 1024000) + _, err = rand.Read(sData) + if err != nil { + t.Fatal("error generating random data:", err) + } + + // TCP sender server + senderAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 33345} + senderListener, err := net.ListenTCP("tcp", senderAddr) + if err != nil { + t.Fatal("error creating sender server:", err) + } + senderServer := &tcpSenderServer{ + Listener: senderListener, + Data: sData, + } + defer senderServer.Close() + go senderServer.Serve() + + // Dial and read data from TCP sender server + conn, err := c.DialTCP(senderAddr.String()) + if err != nil { + t.Fatal("error dialing TCP:", err) + } + defer conn.Close() + rData, err := io.ReadAll(conn) + if err != nil { + t.Fatal("error reading from TCP:", err) + } + if !bytes.Equal(rData, sData) { + t.Fatal("data mismatch") + } + }) +} + +// TestClientServerUDPClose is the same as TestClientServerTCPClose, but for UDP. +// Checking for UDP close is a bit tricky, so we will rely on the server event for now. +func TestClientServerUDPClose(t *testing.T) { + urCh := make(chan udpRequestEvent, 1) + ueCh := make(chan udpErrorEvent, 1) + + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "password", + ID: "nobody", + }, + EventLogger: &channelEventLogger{ + UDPRequestEventCh: urCh, + UDPErrorEventCh: ueCh, + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + ServerName: udpAddr.String(), + Auth: "password", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + // Listen UDP and close it, then check the server events + conn, err := c.ListenUDP() + if err != nil { + t.Fatal("error listening UDP:", err) + } + _ = conn.Close() + + reqEvent := <-urCh + if reqEvent.ID != "nobody" { + t.Fatal("incorrect ID in request event") + } + errEvent := <-ueCh + if errEvent.ID != "nobody" { + t.Fatal("incorrect ID in error event") + } + if errEvent.Err != nil { + t.Fatal("non-nil error received from server:", errEvent.Err) + } +} diff --git a/core/internal/integration_tests/masq_test.go b/core/internal/integration_tests/masq_test.go new file mode 100644 index 0000000..4d23dfb --- /dev/null +++ b/core/internal/integration_tests/masq_test.go @@ -0,0 +1,130 @@ +package integration_tests + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/apernet/hysteria/core/internal/protocol" + "github.com/apernet/hysteria/core/server" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" +) + +// TestServerMasquerade is a test to ensure that the server behaves as a normal +// HTTP/3 server when dealing with an unauthenticated client. This is mainly to +// confirm that the server does not expose itself to active probers. +func TestServerMasquerade(t *testing.T) { + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "password", + ID: "nobody", + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // QUIC connection & RoundTripper + var conn quic.EarlyConnection + rt := &http3.RoundTripper{ + EnableDatagrams: true, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + qc, err := quic.DialAddrEarlyContext(ctx, udpAddr.String(), tlsCfg, cfg) + if err != nil { + return nil, err + } + conn = qc + return qc, nil + }, + } + defer rt.Close() // This will close the QUIC connection + + // Send the bogus request + // We expect 404 (from the default handler) + req := &http.Request{ + Method: http.MethodPost, + URL: &url.URL{ + Scheme: "https", + Host: protocol.URLHost, + Path: protocol.URLPath, + }, + Header: make(http.Header), + } + resp, err := rt.RoundTrip(req) + if err != nil { + t.Fatal("error sending request:", err) + } + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected status %d, got %d", http.StatusNotFound, resp.StatusCode) + } + for k := range resp.Header { + // Make sure no strange headers are sent + if strings.Contains(k, "Hysteria") { + t.Fatal("expected no Hysteria headers, got", k) + } + } + + buf := make([]byte, 1024) + + // We send a TCP request anyway, see if we get a response + tcpStream, err := conn.OpenStream() + if err != nil { + t.Fatal("error opening stream:", err) + } + defer tcpStream.Close() + err = protocol.WriteTCPRequest(tcpStream, "www.google.com:443") + if err != nil { + t.Fatal("error sending request:", err) + } + + // We should receive nothing + _ = tcpStream.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err := tcpStream.Read(buf) + if n != 0 { + t.Fatal("expected no response, got", n) + } + if nErr, ok := err.(net.Error); !ok || !nErr.Timeout() { + t.Fatal("expected timeout, got", err) + } + + // Try UDP request + udpStream, err := conn.OpenStream() + if err != nil { + t.Fatal("error opening stream:", err) + } + defer udpStream.Close() + err = protocol.WriteUDPRequest(udpStream) + if err != nil { + t.Fatal("error sending request:", err) + } + + // We should receive nothing + _ = udpStream.SetReadDeadline(time.Now().Add(2 * time.Second)) + n, err = udpStream.Read(buf) + if n != 0 { + t.Fatal("expected no response, got", n) + } + if nErr, ok := err.(net.Error); !ok || !nErr.Timeout() { + t.Fatal("expected timeout, got", err) + } +} diff --git a/core/internal/integration_tests/smoke_test.go b/core/internal/integration_tests/smoke_test.go new file mode 100644 index 0000000..b9c4e62 --- /dev/null +++ b/core/internal/integration_tests/smoke_test.go @@ -0,0 +1,229 @@ +package integration_tests + +import ( + "errors" + "io" + "net" + "testing" + + "github.com/apernet/hysteria/core/client" + coreErrs "github.com/apernet/hysteria/core/errors" + "github.com/apernet/hysteria/core/server" +) + +// Smoke tests that act as a sanity check for client & server to ensure they can talk to each other correctly. + +// TestClientNoServer tests how the client handles a server that doesn't exist. +// The client should still be able to be created, but TCP & UDP requests should fail. +func TestClientNoServer(t *testing.T) { + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514}, + ServerName: "not_a_real_server", + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + var cErr *coreErrs.ConnectError + + // Try TCP + _, err = c.DialTCP("google.com:443") + if !errors.As(err, &cErr) { + t.Fatal("expected connect error from DialTCP") + } + + // Try UDP + _, err = c.ListenUDP() + if !errors.As(err, &cErr) { + t.Fatal("expected connect error from ListenUDP") + } +} + +// TestClientServerBadAuth tests two things: +// - The server uses Authenticator when a client connects. +// - How the client handles failed authentication. +func TestClientServerBadAuth(t *testing.T) { + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "correct password", + ID: "nobody", + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + ServerName: udpAddr.String(), + Auth: "wrong password", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + var aErr *coreErrs.AuthError + + // Try TCP + _, err = c.DialTCP("google.com:443") + if !errors.As(err, &aErr) { + t.Fatal("expected auth error from DialTCP") + } + + // Try UDP + _, err = c.ListenUDP() + if !errors.As(err, &aErr) { + t.Fatal("expected auth error from ListenUDP") + } +} + +// TestClientServerTCPEcho tests TCP forwarding using a TCP echo server. +func TestClientServerTCPEcho(t *testing.T) { + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "password", + ID: "nobody", + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // Create TCP echo server + echoTCPAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14515} + echoListener, err := net.ListenTCP("tcp", echoTCPAddr) + if err != nil { + t.Fatal("error creating TCP echo server:", err) + } + echoServer := &tcpEchoServer{Listener: echoListener} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + ServerName: udpAddr.String(), + Auth: "password", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + // Dial TCP + conn, err := c.DialTCP(echoTCPAddr.String()) + if err != nil { + t.Fatal("error dialing TCP:", err) + } + defer conn.Close() + + // Send and receive data + sData := []byte("hello world") + _, err = conn.Write(sData) + if err != nil { + t.Fatal("error writing to TCP:", err) + } + rData := make([]byte, len(sData)) + _, err = io.ReadFull(conn, rData) + if err != nil { + t.Fatal("error reading from TCP:", err) + } + if string(rData) != string(sData) { + t.Fatalf("expected %q, got %q", sData, rData) + } +} + +// TestClientServerUDPEcho tests UDP forwarding using a UDP echo server. +func TestClientServerUDPEcho(t *testing.T) { + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "password", + ID: "nobody", + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // Create UDP echo server + echoUDPAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 55555} + echoConn, err := net.ListenUDP("udp", echoUDPAddr) + if err != nil { + t.Fatal("error creating UDP echo server:", err) + } + echoServer := &udpEchoServer{Conn: echoConn} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + ServerName: udpAddr.String(), + Auth: "password", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + // Listen UDP + conn, err := c.ListenUDP() + if err != nil { + t.Fatal("error listening UDP:", err) + } + defer conn.Close() + + // Send and receive data + sData := []byte("hello world") + err = conn.Send(sData, echoUDPAddr.String()) + if err != nil { + t.Fatal("error sending UDP:", err) + } + rData, rAddr, err := conn.Receive() + if err != nil { + t.Fatal("error receiving UDP:", err) + } + if string(rData) != string(sData) { + t.Fatalf("expected %q, got %q", sData, rData) + } + if rAddr != echoUDPAddr.String() { + t.Fatalf("expected %q, got %q", echoUDPAddr.String(), rAddr) + } +} diff --git a/core/internal/integration_tests/stress_test.go b/core/internal/integration_tests/stress_test.go new file mode 100644 index 0000000..b71ae2d --- /dev/null +++ b/core/internal/integration_tests/stress_test.go @@ -0,0 +1,291 @@ +package integration_tests + +import ( + "context" + "crypto/rand" + "fmt" + "io" + "net" + "sync" + "testing" + + "golang.org/x/time/rate" + + "github.com/apernet/hysteria/core/client" + "github.com/apernet/hysteria/core/server" +) + +type tcpStressor struct { + DialFunc func() (net.Conn, error) + Size int + Parallel int + Iterations int +} + +func (s *tcpStressor) Run(t *testing.T) { + // Make some random data + sData := make([]byte, s.Size) + _, err := rand.Read(sData) + if err != nil { + t.Fatal("error generating random data:", err) + } + + // Run iterations + for i := 0; i < s.Iterations; i++ { + var wg sync.WaitGroup + errChan := make(chan error, s.Parallel) + for j := 0; j < s.Parallel; j++ { + wg.Add(1) + go func() { + defer wg.Done() + + conn, err := s.DialFunc() + if err != nil { + errChan <- err + return + } + defer conn.Close() + go conn.Write(sData) + + rData := make([]byte, len(sData)) + _, err = io.ReadFull(conn, rData) + if err != nil { + errChan <- err + return + } + }() + } + wg.Wait() + + if len(errChan) > 0 { + t.Fatal("error reading from TCP:", <-errChan) + } + } +} + +type udpStressor struct { + ListenFunc func() (client.HyUDPConn, error) + ServerAddr string + Size int + Count int + Parallel int + Iterations int +} + +func (s *udpStressor) Run(t *testing.T) { + // Make some random data + sData := make([]byte, s.Size) + _, err := rand.Read(sData) + if err != nil { + t.Fatal("error generating random data:", err) + } + + // Due to UDP's unreliability, we need to limit the rate of sending + // to reduce packet loss. This is hardcoded to 1 MiB/s for now. + limiter := rate.NewLimiter(1048576, 1048576) + + // Run iterations + for i := 0; i < s.Iterations; i++ { + var wg sync.WaitGroup + errChan := make(chan error, s.Parallel) + for j := 0; j < s.Parallel; j++ { + wg.Add(1) + go func() { + defer wg.Done() + + conn, err := s.ListenFunc() + if err != nil { + errChan <- err + return + } + defer conn.Close() + go func() { + // Sending routine + for i := 0; i < s.Count; i++ { + _ = limiter.WaitN(context.Background(), len(sData)) + _ = conn.Send(sData, s.ServerAddr) + } + }() + + minCount := s.Count * 8 / 10 // Tolerate 20% packet loss + for i := 0; i < minCount; i++ { + rData, _, err := conn.Receive() + if err != nil { + errChan <- err + return + } + if len(rData) != len(sData) { + errChan <- fmt.Errorf("incomplete data received: %d/%d bytes", len(rData), len(sData)) + return + } + } + }() + } + wg.Wait() + + if len(errChan) > 0 { + t.Fatal("error reading from UDP:", <-errChan) + } + } +} + +func TestClientServerTCPStress(t *testing.T) { + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "password", + ID: "nobody", + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // Create TCP echo server + echoTCPAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14515} + echoListener, err := net.ListenTCP("tcp", echoTCPAddr) + if err != nil { + t.Fatal("error creating TCP echo server:", err) + } + echoServer := &tcpEchoServer{Listener: echoListener} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + ServerName: udpAddr.String(), + Auth: "password", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + dialFunc := func() (net.Conn, error) { + return c.DialTCP(echoTCPAddr.String()) + } + + t.Run("Single 500m", (&tcpStressor{DialFunc: dialFunc, Size: 524288000, Parallel: 1, Iterations: 1}).Run) + + t.Run("Sequential 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1, Iterations: 1000}).Run) + t.Run("Sequential 10000x100k", (&tcpStressor{DialFunc: dialFunc, Size: 102400, Parallel: 1, Iterations: 10000}).Run) + + t.Run("Parallel 100x10m", (&tcpStressor{DialFunc: dialFunc, Size: 10485760, Parallel: 100, Iterations: 1}).Run) + t.Run("Parallel 1000x1m", (&tcpStressor{DialFunc: dialFunc, Size: 1048576, Parallel: 1000, Iterations: 1}).Run) +} + +func TestClientServerUDPStress(t *testing.T) { + // Create server + udpAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14514} + udpConn, err := net.ListenUDP("udp", udpAddr) + if err != nil { + t.Fatal("error creating server:", err) + } + s, err := server.NewServer(&server.Config{ + TLSConfig: serverTLSConfig(), + Conn: udpConn, + Authenticator: &pwAuthenticator{ + Password: "password", + ID: "nobody", + }, + }) + if err != nil { + t.Fatal("error creating server:", err) + } + defer s.Close() + go s.Serve() + + // Create UDP echo server + echoUDPAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 14515} + echoListener, err := net.ListenUDP("udp", echoUDPAddr) + if err != nil { + t.Fatal("error creating UDP echo server:", err) + } + echoServer := &udpEchoServer{Conn: echoListener} + defer echoServer.Close() + go echoServer.Serve() + + // Create client + c, err := client.NewClient(&client.Config{ + ServerAddr: udpAddr, + ServerName: udpAddr.String(), + Auth: "password", + TLSConfig: client.TLSConfig{InsecureSkipVerify: true}, + }) + if err != nil { + t.Fatal("error creating client:", err) + } + defer c.Close() + + t.Run("Single 1000x100b", (&udpStressor{ + ListenFunc: c.ListenUDP, + ServerAddr: echoUDPAddr.String(), + Size: 100, + Count: 1000, + Parallel: 1, + Iterations: 1, + }).Run) + t.Run("Single 1000x3k", (&udpStressor{ + ListenFunc: c.ListenUDP, + ServerAddr: echoUDPAddr.String(), + Size: 3000, + Count: 1000, + Parallel: 1, + Iterations: 1, + }).Run) + + t.Run("5 Sequential 1000x100b", (&udpStressor{ + ListenFunc: c.ListenUDP, + ServerAddr: echoUDPAddr.String(), + Size: 100, + Count: 1000, + Parallel: 1, + Iterations: 5, + }).Run) + t.Run("5 Sequential 200x3k", (&udpStressor{ + ListenFunc: c.ListenUDP, + ServerAddr: echoUDPAddr.String(), + Size: 3000, + Count: 200, + Parallel: 1, + Iterations: 5, + }).Run) + + t.Run("2 Sequential 5 Parallel 1000x100b", (&udpStressor{ + ListenFunc: c.ListenUDP, + ServerAddr: echoUDPAddr.String(), + Size: 100, + Count: 1000, + Parallel: 5, + Iterations: 2, + }).Run) + + t.Run("2 Sequential 5 Parallel 200x3k", (&udpStressor{ + ListenFunc: c.ListenUDP, + ServerAddr: echoUDPAddr.String(), + Size: 3000, + Count: 200, + Parallel: 5, + Iterations: 2, + }).Run) + + t.Run("10 Sequential 5 Parallel 200x3k", (&udpStressor{ + ListenFunc: c.ListenUDP, + ServerAddr: echoUDPAddr.String(), + Size: 3000, + Count: 200, + Parallel: 5, + Iterations: 10, + }).Run) +} diff --git a/core/internal/integration_tests/test.crt b/core/internal/integration_tests/test.crt new file mode 100644 index 0000000..ecb00ed --- /dev/null +++ b/core/internal/integration_tests/test.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwTCCAqmgAwIBAgIUMeefneiCXWS2ovxNN+fJcdrOIfAwDQYJKoZIhvcNAQEL +BQAwcDELMAkGA1UEBhMCVFcxEzARBgNVBAgMClNvbWUtU3RhdGUxGTAXBgNVBAoM +EFJhbmRvbSBTdHVmZiBMTEMxEjAQBgNVBAMMCWxvY2FsaG9zdDEdMBsGCSqGSIb3 +DQEJARYOcG9vcGVyQHNoaXQuY2MwHhcNMjMwNDI3MDAyMDQ1WhcNMzMwNDI0MDAy +MDQ1WjBwMQswCQYDVQQGEwJUVzETMBEGA1UECAwKU29tZS1TdGF0ZTEZMBcGA1UE +CgwQUmFuZG9tIFN0dWZmIExMQzESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZI +hvcNAQkBFg5wb29wZXJAc2hpdC5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOU9/4AT/6fDKyEyZMMLFzUEVC8ZDJHoKZ+3g65ZFQLxRKqlEdhvOwq4 +ZsxYF0sceUPDAsdrT+km0l1jAvq6u82n6xQQ60HpKe6hOvDX7KS0dPcKa+nfEa0W +DKamBB+TzxB2dBfBNS1oUU74nBb7ttpJiKnOpRJ0/J+CwslvhJzq04AUXC/W1CtW +CbZBg1JjY0fCN+Oy1WjEqMtRSB6k5Ipk40a8NcsqReBOMZChR8elruZ09sIlA6tf +jICOKToDVBmkjJ8m/GnxfV8MeLoK83M2VA73njsS6q9qe9KDVgIVQmifwi6JUb7N +o0A6f2Z47AWJmvq4goHJtnQ3fyoeIsMCAwEAAaNTMFEwHQYDVR0OBBYEFPrBsm6v +M29fKA3is22tK8yHYQaDMB8GA1UdIwQYMBaAFPrBsm6vM29fKA3is22tK8yHYQaD +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJvOwj0Tf8l9AWvf +1ZLyW0K3m5oJAoUayjlLP9q7KHgJHWd4QXxg4ApUDo523m4Own3FwtN06KCMqlxc +luDJi27ghRzZ8bpB9fUujikC1rs1oWYRz/K+JSO1VItan+azm9AQRj+nNepjUiT4 +FjvRif+inC4392tcKuwrqiUFmLIggtFZdsLeKUL+hRGCRjY4BZw0d1sjjPtyVNUD +UMVO8pxlCV0NU4Nmt3vulD4YshAXM+Y8yX/vPRnaNGoRrbRgCg2VORRGaZVjQMHD +OLMvqM7pFKnVg0uiSbQ3xbQJ8WeX620zKI0So2+kZt9HoI+46gd7BdNfl7mmd6K7 +ydYKuI8= +-----END CERTIFICATE----- diff --git a/core/internal/integration_tests/test.key b/core/internal/integration_tests/test.key new file mode 100644 index 0000000..d471f50 --- /dev/null +++ b/core/internal/integration_tests/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5T3/gBP/p8MrITJkwwsXNQRULxkMkegpn7eDrlkVAvFEqqUR +2G87CrhmzFgXSxx5Q8MCx2tP6SbSXWMC+rq7zafrFBDrQekp7qE68NfspLR09wpr +6d8RrRYMpqYEH5PPEHZ0F8E1LWhRTvicFvu22kmIqc6lEnT8n4LCyW+EnOrTgBRc +L9bUK1YJtkGDUmNjR8I347LVaMSoy1FIHqTkimTjRrw1yypF4E4xkKFHx6Wu5nT2 +wiUDq1+MgI4pOgNUGaSMnyb8afF9Xwx4ugrzczZUDveeOxLqr2p70oNWAhVCaJ/C +LolRvs2jQDp/ZnjsBYma+riCgcm2dDd/Kh4iwwIDAQABAoIBABjiU/vJL/U8AFCI +MdviNlCw+ZprM6wa8Xm+5/JjBR7epb+IT5mY6WXOgoon/c9PdfJfFswi3/fFGQy+ +FLK21nAKjEAPXho3fy/CHK3MIon2dMPkQ7aNWlPZkuH8H3J2DwIQeaWieW1GZ50U +64yrIjwrw0P7hHuua0W9YfuPuWt29YpW5g6ilSRE0kdTzoB6TgMzlVRj6RWbxWLX +erwYFesSpLPiQrozK2yywlQsvRV2AxTlf5woJyRTyCqcao5jNZOJJl0mqeGKNKbu +1iYGtZl9aj1XIRxUt+JB2IMKNJasygIp+GRLUDCHKh8RVFwRlVaSNcWbfLDuyNWW +T3lUEjECgYEA84mrs4TLuPfklsQM4WPBdN/2Ud1r0Zn/W8icHcVc/DCFXbcV4aPA +g4yyyyEkyTac2RSbSp+rfUk/pJcG6CVjwaiRIPehdtcLIUP34EdIrwPrPT7/uWVA +o/Hp1ANSILecknQXeE1qDlHVeGAq2k3vAQH2J0m7lMfar7QCBTMTMHcCgYEA8PkO +Uj9+/LoHod2eb4raH29wntis31X5FX/C/8HlmFmQplxfMxpRckzDYQELdHvDggNY +ZQo6pdE22MjCu2bk9AHa2ukMyieWm/mPe46Upr1YV2o5cWnfFFNa/LP2Ii/dWY5V +rFNsHFnrnwcWymX7OKo0Xb8xYnKhKZJAFwSpXxUCgYBPMjXj6wtU20g6vwZxRT9k +AnDXrmmhf7LK5jHefJAAcsbr8t3qwpWYMejypZSQ2nGnJkxZuBLMa0WHAJX+aCpI +j8iiL+USAFxeNPwmswev4lZdVF9Uqtiad9DSYUIT4aHI/nejZ4lVnscMnjlRRIa0 +jS6/F/soJtW2zZLangFfgQKBgCOSAAUwDkSsCThhiGOasXv2bT9laI9HF4+O3m/2 +ZTfJ8Mo91GesuN0Qa77D8rbtFfz5FXFEw0d6zIfPir8y/xTtuSqbQCIPGfJIMl/g +uhyq0oGE0pnlMOLFMyceQXTmb9wqYIchgVHmDBvbZgfWafEBXt1/vYB0v0ltpzw+ +menJAoGBAI0hx3+mrFgA+xJBEk4oexAlro1qbNWoR7BCmLQtd49jG3eZQu4JxWH2 +kh58AIXzLl0X9t4pfMYasYL6jBGvw+AqNdo2krpiL7MWEE8w8FP/wibzqmuloziB +T7BZuCZjpcAM0IxLmQeeUK0LF0mihcqvssxveaet46mj7QoA7bGQ +-----END RSA PRIVATE KEY----- diff --git a/core/internal/integration_tests/utils_test.go b/core/internal/integration_tests/utils_test.go new file mode 100644 index 0000000..fb90dcb --- /dev/null +++ b/core/internal/integration_tests/utils_test.go @@ -0,0 +1,250 @@ +package integration_tests + +import ( + "bytes" + "crypto/tls" + "io" + "net" + + "github.com/apernet/hysteria/core/server" +) + +// This file provides utilities for the integration tests. + +const ( + testCertFile = "test.crt" + testKeyFile = "test.key" +) + +func serverTLSConfig() server.TLSConfig { + cert, err := tls.LoadX509KeyPair(testCertFile, testKeyFile) + if err != nil { + panic(err) + } + return server.TLSConfig{ + Certificates: []tls.Certificate{cert}, + } +} + +type pwAuthenticator struct { + Password string + ID string +} + +func (a *pwAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + if auth != a.Password { + return false, "" + } + return true, a.ID +} + +// tcpEchoServer is a TCP server that echoes what it reads from the connection. +// It will never actively close the connection. +type tcpEchoServer struct { + Listener net.Listener +} + +func (s *tcpEchoServer) Serve() error { + for { + conn, err := s.Listener.Accept() + if err != nil { + return err + } + go func() { + _, _ = io.Copy(conn, conn) + _ = conn.Close() + }() + } +} + +func (s *tcpEchoServer) Close() error { + return s.Listener.Close() +} + +type sinkEvent struct { + Data []byte + Err error +} + +// tcpSinkServer is a TCP server that reads data from the connection, +// and sends what it read to the channel when the connection is closed. +type tcpSinkServer struct { + Listener net.Listener + Ch chan<- sinkEvent +} + +func (s *tcpSinkServer) Serve() error { + for { + conn, err := s.Listener.Accept() + if err != nil { + return err + } + go func() { + var buf bytes.Buffer + _, err := io.Copy(&buf, conn) + _ = conn.Close() + s.Ch <- sinkEvent{Data: buf.Bytes(), Err: err} + }() + } +} + +func (s *tcpSinkServer) Close() error { + return s.Listener.Close() +} + +// tcpSenderServer is a TCP server that sends data to the connection, +// and closes the connection when all data has been sent. +type tcpSenderServer struct { + Listener net.Listener + Data []byte +} + +func (s *tcpSenderServer) Serve() error { + for { + conn, err := s.Listener.Accept() + if err != nil { + return err + } + go func() { + _, _ = conn.Write(s.Data) + _ = conn.Close() + }() + } +} + +func (s *tcpSenderServer) Close() error { + return s.Listener.Close() +} + +// udpEchoServer is a UDP server that echoes what it reads from the connection. +// It will never actively close the connection. +type udpEchoServer struct { + Conn net.PacketConn +} + +func (s *udpEchoServer) Serve() error { + buf := make([]byte, 65536) + for { + n, addr, err := s.Conn.ReadFrom(buf) + if err != nil { + return err + } + _, err = s.Conn.WriteTo(buf[:n], addr) + if err != nil { + return err + } + } +} + +func (s *udpEchoServer) Close() error { + return s.Conn.Close() +} + +type connectEvent struct { + Addr net.Addr + ID string + TX uint64 +} + +type disconnectEvent struct { + Addr net.Addr + ID string + Err error +} + +type tcpRequestEvent struct { + Addr net.Addr + ID string + ReqAddr string +} + +type tcpErrorEvent struct { + Addr net.Addr + ID string + ReqAddr string + Err error +} + +type udpRequestEvent struct { + Addr net.Addr + ID string + SessionID uint32 +} + +type udpErrorEvent struct { + Addr net.Addr + ID string + SessionID uint32 + Err error +} + +type channelEventLogger struct { + ConnectEventCh chan connectEvent + DisconnectEventCh chan disconnectEvent + TCPRequestEventCh chan tcpRequestEvent + TCPErrorEventCh chan tcpErrorEvent + UDPRequestEventCh chan udpRequestEvent + UDPErrorEventCh chan udpErrorEvent +} + +func (l *channelEventLogger) Connect(addr net.Addr, id string, tx uint64) { + if l.ConnectEventCh != nil { + l.ConnectEventCh <- connectEvent{ + Addr: addr, + ID: id, + TX: tx, + } + } +} + +func (l *channelEventLogger) Disconnect(addr net.Addr, id string, err error) { + if l.DisconnectEventCh != nil { + l.DisconnectEventCh <- disconnectEvent{ + Addr: addr, + ID: id, + Err: err, + } + } +} + +func (l *channelEventLogger) TCPRequest(addr net.Addr, id, reqAddr string) { + if l.TCPRequestEventCh != nil { + l.TCPRequestEventCh <- tcpRequestEvent{ + Addr: addr, + ID: id, + ReqAddr: reqAddr, + } + } +} + +func (l *channelEventLogger) TCPError(addr net.Addr, id, reqAddr string, err error) { + if l.TCPErrorEventCh != nil { + l.TCPErrorEventCh <- tcpErrorEvent{ + Addr: addr, + ID: id, + ReqAddr: reqAddr, + Err: err, + } + } +} + +func (l *channelEventLogger) UDPRequest(addr net.Addr, id string, sessionID uint32) { + if l.UDPRequestEventCh != nil { + l.UDPRequestEventCh <- udpRequestEvent{ + Addr: addr, + ID: id, + SessionID: sessionID, + } + } +} + +func (l *channelEventLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) { + if l.UDPErrorEventCh != nil { + l.UDPErrorEventCh <- udpErrorEvent{ + Addr: addr, + ID: id, + SessionID: sessionID, + Err: err, + } + } +} diff --git a/core/internal/pmtud/avail.go b/core/internal/pmtud/avail.go new file mode 100644 index 0000000..8a0e013 --- /dev/null +++ b/core/internal/pmtud/avail.go @@ -0,0 +1,7 @@ +//go:build linux || windows + +package pmtud + +const ( + DisablePathMTUDiscovery = false +) diff --git a/core/internal/pmtud/unavail.go b/core/internal/pmtud/unavail.go new file mode 100644 index 0000000..f49edb7 --- /dev/null +++ b/core/internal/pmtud/unavail.go @@ -0,0 +1,12 @@ +//go:build !linux && !windows + +package pmtud + +// quic-go's MTU discovery is enabled by default across all platforms. However, our testing has found that on certain +// platforms (e.g. macOS) the DF bit is not set. As a result, probe packets that should never be fragmented are still +// fragmented and transmitted. So we have decided to enable MTU discovery only on Linux and Windows for now, as we have +// verified its functionality on these platforms. + +const ( + DisablePathMTUDiscovery = true +) diff --git a/core/internal/protocol/http.go b/core/internal/protocol/http.go new file mode 100644 index 0000000..c5e092c --- /dev/null +++ b/core/internal/protocol/http.go @@ -0,0 +1,36 @@ +package protocol + +import ( + "net/http" + "strconv" +) + +const ( + URLHost = "hysteria" + URLPath = "/auth" + + HeaderAuth = "Hysteria-Auth" + HeaderCCRX = "Hysteria-CC-RX" + + StatusAuthOK = 233 +) + +func AuthRequestDataFromHeader(h http.Header) (auth string, rx uint64) { + auth = h.Get(HeaderAuth) + rx, _ = strconv.ParseUint(h.Get(HeaderCCRX), 10, 64) + return +} + +func AuthRequestDataToHeader(h http.Header, auth string, rx uint64) { + h.Set(HeaderAuth, auth) + h.Set(HeaderCCRX, strconv.FormatUint(rx, 10)) +} + +func AuthResponseDataFromHeader(h http.Header) (rx uint64) { + rx, _ = strconv.ParseUint(h.Get(HeaderCCRX), 10, 64) + return +} + +func AuthResponseDataToHeader(h http.Header, rx uint64) { + h.Set(HeaderCCRX, strconv.FormatUint(rx, 10)) +} diff --git a/core/internal/protocol/proxy.go b/core/internal/protocol/proxy.go new file mode 100644 index 0000000..634be85 --- /dev/null +++ b/core/internal/protocol/proxy.go @@ -0,0 +1,261 @@ +package protocol + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/apernet/hysteria/core/errors" + + "github.com/quic-go/quic-go/quicvarint" +) + +const ( + FrameTypeTCPRequest = 0x401 + FrameTypeUDPRequest = 0x402 + + MaxAddressLength = 2048 // for preventing DoS attack by sending a very large address length + MaxMessageLength = 2048 // for preventing DoS attack by sending a very large message length + + MaxUDPSize = 4096 + + maxVarInt1 = 63 + maxVarInt2 = 16383 + maxVarInt4 = 1073741823 + maxVarInt8 = 4611686018427387903 +) + +// TCPRequest format: +// 0x401 (QUIC varint) +// Address length (QUIC varint) +// Address (bytes) + +func ReadTCPRequest(r io.Reader) (string, error) { + bReader := quicvarint.NewReader(r) + l, err := quicvarint.Read(bReader) + if err != nil { + return "", err + } + if l == 0 || l > MaxAddressLength { + return "", errors.ProtocolError{Message: "invalid address length"} + } + buf := make([]byte, l) + _, err = io.ReadFull(r, buf) + return string(buf), err +} + +func WriteTCPRequest(w io.Writer, addr string) error { + l := len(addr) + sz := int(quicvarint.Len(FrameTypeTCPRequest)) + int(quicvarint.Len(uint64(l))) + l + buf := make([]byte, sz) + i := varintPut(buf, FrameTypeTCPRequest) + i += varintPut(buf[i:], uint64(l)) + copy(buf[i:], addr) + _, err := w.Write(buf) + return err +} + +// TCPResponse format: +// Status (byte, 0=ok, 1=error) +// Message length (QUIC varint) +// Message (bytes) + +func ReadTCPResponse(r io.Reader) (bool, string, error) { + var status [1]byte + if _, err := io.ReadFull(r, status[:]); err != nil { + return false, "", err + } + bReader := quicvarint.NewReader(r) + l, err := quicvarint.Read(bReader) + if err != nil { + return false, "", err + } + if l == 0 { + // No message is ok + return status[0] == 0, "", nil + } + if l > MaxMessageLength { + return false, "", errors.ProtocolError{Message: "invalid message length"} + } + buf := make([]byte, l) + _, err = io.ReadFull(r, buf) + return status[0] == 0, string(buf), err +} + +func WriteTCPResponse(w io.Writer, ok bool, msg string) error { + l := len(msg) + sz := 1 + int(quicvarint.Len(uint64(l))) + l + buf := make([]byte, sz) + if ok { + buf[0] = 0 + } else { + buf[0] = 1 + } + i := varintPut(buf[1:], uint64(l)) + copy(buf[1+i:], msg) + _, err := w.Write(buf) + return err +} + +// UDPRequest format: +// 0x402 (QUIC varint) + +// Nothing to read + +func WriteUDPRequest(w io.Writer) error { + buf := make([]byte, quicvarint.Len(FrameTypeUDPRequest)) + varintPut(buf, FrameTypeUDPRequest) + _, err := w.Write(buf) + return err +} + +// UDPResponse format: +// Status (byte, 0=ok, 1=error) +// Session ID (uint32 BE) +// Message length (QUIC varint) +// Message (bytes) + +func ReadUDPResponse(r io.Reader) (bool, uint32, string, error) { + var status [1]byte + if _, err := io.ReadFull(r, status[:]); err != nil { + return false, 0, "", err + } + var sessionID uint32 + if err := binary.Read(r, binary.BigEndian, &sessionID); err != nil { + return false, 0, "", err + } + bReader := quicvarint.NewReader(r) + l, err := quicvarint.Read(bReader) + if err != nil { + return false, 0, "", err + } + if l == 0 { + // No message is ok + return status[0] == 0, sessionID, "", nil + } + if l > MaxMessageLength { + return false, 0, "", errors.ProtocolError{Message: "invalid message length"} + } + buf := make([]byte, l) + _, err = io.ReadFull(r, buf) + return status[0] == 0, sessionID, string(buf), err +} + +func WriteUDPResponse(w io.Writer, ok bool, sessionID uint32, msg string) error { + l := len(msg) + buf := make([]byte, 5+int(quicvarint.Len(uint64(l)))+l) + if ok { + buf[0] = 0 + } else { + buf[0] = 1 + } + binary.BigEndian.PutUint32(buf[1:], sessionID) + i := varintPut(buf[5:], uint64(l)) + copy(buf[5+i:], msg) + _, err := w.Write(buf) + return err +} + +// UDPMessage format: +// Session ID (uint32 BE) +// Packet ID (uint16 BE) +// Fragment ID (uint8) +// Fragment count (uint8) +// Address length (QUIC varint) +// Address (bytes) +// Data... + +type UDPMessage struct { + SessionID uint32 // 4 + PacketID uint16 // 2 + FragID uint8 // 1 + FragCount uint8 // 1 + Addr string // varint + bytes + Data []byte +} + +func (m *UDPMessage) HeaderSize() int { + lAddr := len(m.Addr) + return 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr +} + +func (m *UDPMessage) Size() int { + return m.HeaderSize() + len(m.Data) +} + +func (m *UDPMessage) Serialize(buf []byte) int { + // Make sure the buffer is big enough + if len(buf) < m.Size() { + return -1 + } + binary.BigEndian.PutUint32(buf, m.SessionID) + binary.BigEndian.PutUint16(buf[4:], m.PacketID) + buf[6] = m.FragID + buf[7] = m.FragCount + i := varintPut(buf[8:], uint64(len(m.Addr))) + i += copy(buf[8+i:], m.Addr) + i += copy(buf[8+i:], m.Data) + return 8 + i +} + +func ParseUDPMessage(msg []byte) (*UDPMessage, error) { + m := &UDPMessage{} + buf := bytes.NewBuffer(msg) + if err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil { + return nil, err + } + if err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil { + return nil, err + } + lAddr, err := quicvarint.Read(buf) + if err != nil { + return nil, err + } + if lAddr == 0 || lAddr > MaxMessageLength { + return nil, errors.ProtocolError{Message: "invalid address length"} + } + bs := buf.Bytes() + m.Addr = string(bs[:lAddr]) + m.Data = bs[lAddr:] + return m, nil +} + +// varintPut is like quicvarint.Append, but instead of appending to a slice, +// it writes to a fixed-size buffer. Returns the number of bytes written. +func varintPut(b []byte, i uint64) int { + if i <= maxVarInt1 { + b[0] = uint8(i) + return 1 + } + if i <= maxVarInt2 { + b[0] = uint8(i>>8) | 0x40 + b[1] = uint8(i) + return 2 + } + if i <= maxVarInt4 { + b[0] = uint8(i>>24) | 0x80 + b[1] = uint8(i >> 16) + b[2] = uint8(i >> 8) + b[3] = uint8(i) + return 4 + } + if i <= maxVarInt8 { + b[0] = uint8(i>>56) | 0xc0 + b[1] = uint8(i >> 48) + b[2] = uint8(i >> 40) + b[3] = uint8(i >> 32) + b[4] = uint8(i >> 24) + b[5] = uint8(i >> 16) + b[6] = uint8(i >> 8) + b[7] = uint8(i) + return 8 + } + panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i)) +} diff --git a/core/internal/protocol/proxy_test.go b/core/internal/protocol/proxy_test.go new file mode 100644 index 0000000..41f8224 --- /dev/null +++ b/core/internal/protocol/proxy_test.go @@ -0,0 +1,557 @@ +package protocol + +import ( + "bytes" + "io" + "reflect" + "testing" +) + +func TestReadTCPRequest(t *testing.T) { + type args struct { + r io.Reader + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "normal 1", + args: args{ + r: bytes.NewReader([]byte("\x05hello")), + }, + want: "hello", + wantErr: false, + }, + { + name: "normal 2", + args: args{ + r: bytes.NewReader([]byte("\x41\x25We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People")), + }, + want: "We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People", + wantErr: false, + }, + { + name: "empty", + args: args{ + r: bytes.NewReader([]byte("\x00")), + }, + want: "", + wantErr: true, + }, + { + name: "incomplete", + args: args{ + r: bytes.NewReader([]byte("\x06oh no")), + }, + want: "oh no\x00", + wantErr: true, + }, + { + name: "too long", + args: args{ + r: bytes.NewReader([]byte("\x66\x77\x88Whatever")), + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ReadTCPRequest(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("ReadTCPRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadTCPRequest() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWriteTCPRequest(t *testing.T) { + type args struct { + addr string + } + tests := []struct { + name string + args args + wantW string + wantErr bool + }{ + { + name: "normal 1", + args: args{ + addr: "hello", + }, + wantW: "\x44\x01\x05hello", + wantErr: false, + }, + { + name: "normal 2", + args: args{ + addr: "We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People", + }, + wantW: "\x44\x01\x41\x25We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People We the People", + wantErr: false, + }, + { + name: "empty", + args: args{ + addr: "", + }, + wantW: "\x44\x01\x00", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := WriteTCPRequest(w, tt.args.addr) + if (err != nil) != tt.wantErr { + t.Errorf("WriteTCPRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("WriteTCPRequest() gotW = %v, want %v", []byte(gotW), tt.wantW) + } + }) + } +} + +func TestUDPMessage(t *testing.T) { + t.Run("buffer too small", func(t *testing.T) { + // Make sure Serialize returns -1 when the buffer is too small. + tBuf := make([]byte, 20) + if (&UDPMessage{ + SessionID: 66, + PacketID: 77, + FragID: 2, + FragCount: 5, + Addr: "random_addr", + Data: []byte("random_data"), + }).Serialize(tBuf) != -1 { + t.Error("Serialize() did not return -1 when the buffer was too small") + } + }) + + type fields struct { + SessionID uint32 + PacketID uint16 + FragID uint8 + FragCount uint8 + Addr string + Data []byte + } + tests := []struct { + name string + fields fields + want []byte + }{ + { + name: "test 1", + fields: fields{ + SessionID: 1, + PacketID: 1, + FragID: 0, + FragCount: 1, + Addr: "example.com:80", + Data: []byte("GET /nothing HTTP/1.1\r\n"), + }, + want: []byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0xe, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x38, 0x30, 0x47, 0x45, 0x54, 0x20, 0x2f, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31, 0xd, 0xa}, + }, + { + name: "test 2", + fields: fields{ + SessionID: 1329655244, + Addr: "some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long_some_random_goofy_ahh_address_which_is_very_long:9000", + PacketID: 62233, + FragID: 8, + FragCount: 19, + Data: []byte("God is great, beer is good, and people are crazy."), + }, + want: []byte{0x4f, 0x40, 0xed, 0xcc, 0xf3, 0x19, 0x8, 0x13, 0x41, 0xee, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x5f, 0x73, 0x6f, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x67, 0x6f, 0x6f, 0x66, 0x79, 0x5f, 0x61, 0x68, 0x68, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x77, 0x68, 0x69, 0x63, 0x68, 0x5f, 0x69, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6c, 0x6f, 0x6e, 0x67, 0x3a, 0x39, 0x30, 0x30, 0x30, 0x47, 0x6f, 0x64, 0x20, 0x69, 0x73, 0x20, 0x67, 0x72, 0x65, 0x61, 0x74, 0x2c, 0x20, 0x62, 0x65, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x67, 0x6f, 0x6f, 0x64, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x70, 0x65, 0x6f, 0x70, 0x6c, 0x65, 0x20, 0x61, 0x72, 0x65, 0x20, 0x63, 0x72, 0x61, 0x7a, 0x79, 0x2e}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &UDPMessage{ + SessionID: tt.fields.SessionID, + Addr: tt.fields.Addr, + PacketID: tt.fields.PacketID, + FragID: tt.fields.FragID, + FragCount: tt.fields.FragCount, + Data: tt.fields.Data, + } + // Serialize + buf := make([]byte, MaxUDPSize) + n := m.Serialize(buf) + if got := buf[:n]; !reflect.DeepEqual(got, tt.want) { + t.Errorf("Serialize() = %v, want %v", got, tt.want) + } + // Parse back + if m2, err := ParseUDPMessage(tt.want); err != nil { + t.Errorf("ParseUDPMessage() error = %v", err) + } else { + if !reflect.DeepEqual(m2, m) { + t.Errorf("ParseUDPMessage() = %v, want %v", m2, m) + } + } + }) + } +} + +// TestUDPMessageMalformed is to make sure ParseUDPMessage() fails (but not panic) on malformed data. +func TestUDPMessageMalformed(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + { + name: "empty", + data: []byte{}, + }, + { + name: "zeroes 1", + data: []byte{0, 0, 0, 0}, + }, + { + name: "zeroes 2", + data: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + { + name: "incomplete 1", + data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55}, + }, + { + name: "incomplete 2", + data: []byte{0x66, 0xCC, 0xFF, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x90, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := ParseUDPMessage(tt.data); err == nil { + t.Errorf("ParseUDPMessage() should fail") + } + }) + } +} + +func TestReadTCPResponse(t *testing.T) { + type args struct { + r io.Reader + } + tests := []struct { + name string + args args + want bool + want1 string + wantErr bool + }{ + { + name: "success 1", + args: args{ + r: bytes.NewReader([]byte("\x00\x00")), + }, + want: true, + want1: "", + wantErr: false, + }, + { + name: "success 2", + args: args{ + r: bytes.NewReader([]byte("\x00\x12are ya winning son")), + }, + want: true, + want1: "are ya winning son", + wantErr: false, + }, + { + name: "failure 1", + args: args{ + r: bytes.NewReader([]byte("\x01\x00")), + }, + want: false, + want1: "", + wantErr: false, + }, + { + name: "failure 2", + args: args{ + r: bytes.NewReader([]byte("\x01\x15you ain't winning son")), + }, + want: false, + want1: "you ain't winning son", + wantErr: false, + }, + { + name: "incomplete", + args: args{ + r: bytes.NewReader([]byte("\x01\x25princess peach is in another castle")), + }, + want: false, + want1: "princess peach is in another castle\x00\x00", + wantErr: true, + }, + { + name: "too long", + args: args{ + r: bytes.NewReader([]byte("\xAA\xBB\xCCrandom stuff")), + }, + want: false, + want1: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := ReadTCPResponse(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("ReadTCPResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadTCPResponse() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("ReadTCPResponse() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestWriteTCPResponse(t *testing.T) { + type args struct { + ok bool + msg string + } + tests := []struct { + name string + args args + wantW string + wantErr bool + }{ + { + name: "success 1", + args: args{ + ok: true, + msg: "", + }, + wantW: "\x00\x00", + }, + { + name: "success 2", + args: args{ + ok: true, + msg: "Welcome XDXDXD", + }, + wantW: "\x00\x0EWelcome XDXDXD", + }, + { + name: "failure 1", + args: args{ + ok: false, + msg: "", + }, + wantW: "\x01\x00", + }, + { + name: "failure 2", + args: args{ + ok: false, + msg: "me trying to find who u are: ...", + }, + wantW: "\x01\x20me trying to find who u are: ...", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := WriteTCPResponse(w, tt.args.ok, tt.args.msg) + if (err != nil) != tt.wantErr { + t.Errorf("WriteTCPResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("WriteTCPResponse() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} + +func TestWriteUDPRequest(t *testing.T) { + tests := []struct { + name string + wantW string + wantErr bool + }{ + { + name: "normal", + wantW: "\x44\x02", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := WriteUDPRequest(w) + if (err != nil) != tt.wantErr { + t.Errorf("WriteUDPRequest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("WriteUDPRequest() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} + +func TestReadUDPResponse(t *testing.T) { + type args struct { + r io.Reader + } + tests := []struct { + name string + args args + want bool + want1 uint32 + want2 string + wantErr bool + }{ + { + name: "success 1", + args: args{ + r: bytes.NewReader([]byte("\x00\x00\x00\x00\x02\x00")), + }, + want: true, + want1: 2, + want2: "", + wantErr: false, + }, + { + name: "success 2", + args: args{ + r: bytes.NewReader([]byte("\x00\x00\x00\x00\x03\x0EWelcome XDXDXD")), + }, + want: true, + want1: 3, + want2: "Welcome XDXDXD", + wantErr: false, + }, + { + name: "failure", + args: args{ + r: bytes.NewReader([]byte("\x01\x00\x00\x00\x01\x20me trying to find who u are: ...")), + }, + want: false, + want1: 1, + want2: "me trying to find who u are: ...", + wantErr: false, + }, + { + name: "incomplete", + args: args{ + r: bytes.NewReader([]byte("\x00\x00\x00\x00\x02")), + }, + want: false, + want1: 0, + want2: "", + wantErr: true, + }, + { + name: "too long", + args: args{ + r: bytes.NewReader([]byte("\x00\x00\x00\x00\x02\xCC\xFF\x66no cap")), + }, + want: false, + want1: 0, + want2: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, got2, err := ReadUDPResponse(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("ReadUDPResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadUDPResponse() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("ReadUDPResponse() got1 = %v, want %v", got1, tt.want1) + } + if got2 != tt.want2 { + t.Errorf("ReadUDPResponse() got2 = %v, want %v", got2, tt.want2) + } + }) + } +} + +func TestWriteUDPResponse(t *testing.T) { + type args struct { + ok bool + sessionID uint32 + msg string + } + tests := []struct { + name string + args args + wantW string + wantErr bool + }{ + { + name: "success 1", + args: args{ + ok: true, + sessionID: 88, + msg: "", + }, + wantW: "\x00\x00\x00\x00\x58\x00", + }, + { + name: "success 2", + args: args{ + ok: true, + sessionID: 233, + msg: "together forever", + }, + wantW: "\x00\x00\x00\x00\xE9\x10together forever", + }, + { + name: "failure 1", + args: args{ + ok: false, + sessionID: 1, + msg: "", + }, + wantW: "\x01\x00\x00\x00\x01\x00", + }, + { + name: "failure 2", + args: args{ + ok: false, + sessionID: 696969, + msg: "run away run away", + }, + wantW: "\x01\x00\x0A\xA2\x89\x11run away run away", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + err := WriteUDPResponse(w, tt.args.ok, tt.args.sessionID, tt.args.msg) + if (err != nil) != tt.wantErr { + t.Errorf("WriteUDPResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotW := w.String(); gotW != tt.wantW { + t.Errorf("WriteUDPResponse() gotW = %v, want %v", gotW, tt.wantW) + } + }) + } +} diff --git a/core/internal/utils/qstream.go b/core/internal/utils/qstream.go new file mode 100644 index 0000000..6325b33 --- /dev/null +++ b/core/internal/utils/qstream.go @@ -0,0 +1,62 @@ +package utils + +import ( + "context" + "time" + + "github.com/quic-go/quic-go" +) + +// QStream is a wrapper of quic.Stream that handles Close() in a way that +// makes more sense to us. By default, quic.Stream's Close() only closes +// the write side of the stream, not the read side. And if there is unread +// data, the stream is not really considered closed until either the data +// is drained or CancelRead() is called. +// References: +// - https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quic/stream.go +// - https://github.com/quic-go/quic-go/issues/3558 +// - https://github.com/quic-go/quic-go/issues/1599 +type QStream struct { + Stream quic.Stream +} + +func (s *QStream) StreamID() quic.StreamID { + return s.Stream.StreamID() +} + +func (s *QStream) Read(p []byte) (n int, err error) { + return s.Stream.Read(p) +} + +func (s *QStream) CancelRead(code quic.StreamErrorCode) { + s.Stream.CancelRead(code) +} + +func (s *QStream) SetReadDeadline(t time.Time) error { + return s.Stream.SetReadDeadline(t) +} + +func (s *QStream) Write(p []byte) (n int, err error) { + return s.Stream.Write(p) +} + +func (s *QStream) Close() error { + s.Stream.CancelRead(0) + return s.Stream.Close() +} + +func (s *QStream) CancelWrite(code quic.StreamErrorCode) { + s.Stream.CancelWrite(code) +} + +func (s *QStream) Context() context.Context { + return s.Stream.Context() +} + +func (s *QStream) SetWriteDeadline(t time.Time) error { + return s.Stream.SetWriteDeadline(t) +} + +func (s *QStream) SetDeadline(t time.Time) error { + return s.Stream.SetDeadline(t) +} diff --git a/core/pktconns/faketcp/LICENSE b/core/pktconns/faketcp/LICENSE deleted file mode 100644 index 79fbecb..0000000 --- a/core/pktconns/faketcp/LICENSE +++ /dev/null @@ -1 +0,0 @@ -Grabbed from https://github.com/xtaci/tcpraw with modifications \ No newline at end of file diff --git a/core/pktconns/faketcp/obfs.go b/core/pktconns/faketcp/obfs.go deleted file mode 100644 index ead2281..0000000 --- a/core/pktconns/faketcp/obfs.go +++ /dev/null @@ -1,95 +0,0 @@ -package faketcp - -import ( - "net" - "sync" - "syscall" - "time" - - "github.com/apernet/hysteria/core/pktconns/obfs" -) - -const udpBufferSize = 4096 - -type ObfsFakeTCPPacketConn struct { - orig *TCPConn - obfs obfs.Obfuscator - - readBuf []byte - readMutex sync.Mutex - writeBuf []byte - writeMutex sync.Mutex -} - -func NewObfsFakeTCPConn(orig *TCPConn, obfs obfs.Obfuscator) *ObfsFakeTCPPacketConn { - return &ObfsFakeTCPPacketConn{ - orig: orig, - obfs: obfs, - readBuf: make([]byte, udpBufferSize), - writeBuf: make([]byte, udpBufferSize), - } -} - -func (c *ObfsFakeTCPPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { - for { - c.readMutex.Lock() - n, addr, err := c.orig.ReadFrom(c.readBuf) - if n <= 0 { - c.readMutex.Unlock() - return 0, addr, err - } - newN := c.obfs.Deobfuscate(c.readBuf[:n], p) - c.readMutex.Unlock() - if newN > 0 { - // Valid packet - return newN, addr, err - } else if err != nil { - // Not valid and orig.ReadFrom had some error - return 0, addr, err - } - } -} - -func (c *ObfsFakeTCPPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.writeMutex.Lock() - bn := c.obfs.Obfuscate(p, c.writeBuf) - _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) - c.writeMutex.Unlock() - if err != nil { - return 0, err - } else { - return len(p), nil - } -} - -func (c *ObfsFakeTCPPacketConn) Close() error { - return c.orig.Close() -} - -func (c *ObfsFakeTCPPacketConn) LocalAddr() net.Addr { - return c.orig.LocalAddr() -} - -func (c *ObfsFakeTCPPacketConn) SetDeadline(t time.Time) error { - return c.orig.SetDeadline(t) -} - -func (c *ObfsFakeTCPPacketConn) SetReadDeadline(t time.Time) error { - return c.orig.SetReadDeadline(t) -} - -func (c *ObfsFakeTCPPacketConn) SetWriteDeadline(t time.Time) error { - return c.orig.SetWriteDeadline(t) -} - -func (c *ObfsFakeTCPPacketConn) SetReadBuffer(bytes int) error { - return c.orig.SetReadBuffer(bytes) -} - -func (c *ObfsFakeTCPPacketConn) SetWriteBuffer(bytes int) error { - return c.orig.SetWriteBuffer(bytes) -} - -func (c *ObfsFakeTCPPacketConn) SyscallConn() (syscall.RawConn, error) { - return c.orig.SyscallConn() -} diff --git a/core/pktconns/faketcp/tcp_linux.go b/core/pktconns/faketcp/tcp_linux.go deleted file mode 100644 index dadb091..0000000 --- a/core/pktconns/faketcp/tcp_linux.go +++ /dev/null @@ -1,616 +0,0 @@ -//go:build linux -// +build linux - -package faketcp - -import ( - "crypto/rand" - "encoding/binary" - "errors" - "fmt" - "io" - "io/ioutil" - "net" - "sync" - "sync/atomic" - "syscall" - "time" - - "github.com/coreos/go-iptables/iptables" - "github.com/google/gopacket" - "github.com/google/gopacket/layers" -) - -var ( - errOpNotImplemented = errors.New("operation not implemented") - errTimeout = errors.New("timeout") - expire = time.Minute -) - -// a message from NIC -type message struct { - bts []byte - addr net.Addr -} - -// a tcp flow information of a connection pair -type tcpFlow struct { - conn *net.TCPConn // the related system TCP connection of this flow - handle *net.IPConn // the handle to send packets - seq uint32 // TCP sequence number - ack uint32 // TCP acknowledge number - networkLayer gopacket.SerializableLayer // network layer header for tx - ts time.Time // last packet incoming time - buf gopacket.SerializeBuffer // a buffer for write - tcpHeader layers.TCP -} - -// TCPConn defines a TCP-packet oriented connection -type TCPConn struct { - die chan struct{} - dieOnce sync.Once - - // the main golang sockets - tcpconn *net.TCPConn // from net.Dial - listener *net.TCPListener // from net.Listen - - // handles - handles []*net.IPConn - - // packets captured from all related NICs will be delivered to this channel - chMessage chan message - - // all TCP flows - flowTable map[string]*tcpFlow - flowsLock sync.Mutex - - // iptables - iptables *iptables.IPTables - iprule []string - - ip6tables *iptables.IPTables - ip6rule []string - - // deadlines - readDeadline atomic.Value - writeDeadline atomic.Value - - // serialization - opts gopacket.SerializeOptions -} - -// lockflow locks the flow table and apply function `f` to the entry, and create one if not exist -func (conn *TCPConn) lockflow(addr net.Addr, f func(e *tcpFlow)) { - key := addr.String() - conn.flowsLock.Lock() - e := conn.flowTable[key] - if e == nil { // entry first visit - e = new(tcpFlow) - e.ts = time.Now() - e.buf = gopacket.NewSerializeBuffer() - } - f(e) - conn.flowTable[key] = e - conn.flowsLock.Unlock() -} - -// clean expired flows -func (conn *TCPConn) cleaner() { - ticker := time.NewTicker(time.Minute) - select { - case <-conn.die: - return - case <-ticker.C: - conn.flowsLock.Lock() - for k, v := range conn.flowTable { - if time.Now().Sub(v.ts) > expire { - if v.conn != nil { - setTTL(v.conn, 64) - v.conn.Close() - } - delete(conn.flowTable, k) - } - } - conn.flowsLock.Unlock() - } -} - -// captureFlow capture every inbound packets based on rules of BPF -func (conn *TCPConn) captureFlow(handle *net.IPConn, port int) { - buf := make([]byte, 2048) - opt := gopacket.DecodeOptions{NoCopy: true, Lazy: true} - for { - n, addr, err := handle.ReadFromIP(buf) - if err != nil { - return - } - - // try decoding TCP frame from buf[:n] - packet := gopacket.NewPacket(buf[:n], layers.LayerTypeTCP, opt) - transport := packet.TransportLayer() - tcp, ok := transport.(*layers.TCP) - if !ok { - continue - } - - // port filtering - if int(tcp.DstPort) != port { - continue - } - - // address building - var src net.TCPAddr - src.IP = addr.IP - src.Port = int(tcp.SrcPort) - - var orphan bool - // flow maintaince - conn.lockflow(&src, func(e *tcpFlow) { - if e.conn == nil { // make sure it's related to net.TCPConn - orphan = true // mark as orphan if it's not related net.TCPConn - } - - // to keep track of TCP header related to this source - e.ts = time.Now() - if tcp.ACK { - e.seq = tcp.Ack - } - if tcp.SYN { - e.ack = tcp.Seq + 1 - } - if tcp.PSH { - if e.ack == tcp.Seq { - e.ack = tcp.Seq + uint32(len(tcp.Payload)) - } - } - e.handle = handle - }) - - // push data if it's not orphan - if !orphan && tcp.PSH { - payload := make([]byte, len(tcp.Payload)) - copy(payload, tcp.Payload) - select { - case conn.chMessage <- message{payload, &src}: - case <-conn.die: - return - } - } - } -} - -// ReadFrom implements the PacketConn ReadFrom method. -func (conn *TCPConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - var timer *time.Timer - var deadline <-chan time.Time - if d, ok := conn.readDeadline.Load().(time.Time); ok && !d.IsZero() { - timer = time.NewTimer(time.Until(d)) - defer timer.Stop() - deadline = timer.C - } - - select { - case <-deadline: - return 0, nil, errTimeout - case <-conn.die: - return 0, nil, io.EOF - case packet := <-conn.chMessage: - n = copy(p, packet.bts) - return n, packet.addr, nil - } -} - -// WriteTo implements the PacketConn WriteTo method. -func (conn *TCPConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - var deadline <-chan time.Time - if d, ok := conn.writeDeadline.Load().(time.Time); ok && !d.IsZero() { - timer := time.NewTimer(time.Until(d)) - defer timer.Stop() - deadline = timer.C - } - - select { - case <-deadline: - return 0, errTimeout - case <-conn.die: - return 0, io.EOF - default: - raddr, err := net.ResolveTCPAddr("tcp", addr.String()) - if err != nil { - return 0, err - } - - var lport int - if conn.tcpconn != nil { - lport = conn.tcpconn.LocalAddr().(*net.TCPAddr).Port - } else { - lport = conn.listener.Addr().(*net.TCPAddr).Port - } - - conn.lockflow(addr, func(e *tcpFlow) { - // if the flow doesn't have handle , assume this packet has lost, without notification - if e.handle == nil { - n = len(p) - return - } - - // build tcp header with local and remote port - e.tcpHeader.SrcPort = layers.TCPPort(lport) - e.tcpHeader.DstPort = layers.TCPPort(raddr.Port) - binary.Read(rand.Reader, binary.LittleEndian, &e.tcpHeader.Window) - e.tcpHeader.Window |= 0x8000 // make sure it's larger than 32768 - e.tcpHeader.Ack = e.ack - e.tcpHeader.Seq = e.seq - e.tcpHeader.PSH = true - e.tcpHeader.ACK = true - - // build IP header with src & dst ip for TCP checksum - if raddr.IP.To4() != nil { - ip := &layers.IPv4{ - Protocol: layers.IPProtocolTCP, - SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To4(), - DstIP: raddr.IP.To4(), - } - e.tcpHeader.SetNetworkLayerForChecksum(ip) - } else { - ip := &layers.IPv6{ - NextHeader: layers.IPProtocolTCP, - SrcIP: e.handle.LocalAddr().(*net.IPAddr).IP.To16(), - DstIP: raddr.IP.To16(), - } - e.tcpHeader.SetNetworkLayerForChecksum(ip) - } - - e.buf.Clear() - gopacket.SerializeLayers(e.buf, conn.opts, &e.tcpHeader, gopacket.Payload(p)) - if conn.tcpconn != nil { - _, err = e.handle.Write(e.buf.Bytes()) - } else { - _, err = e.handle.WriteToIP(e.buf.Bytes(), &net.IPAddr{IP: raddr.IP}) - } - // increase seq in flow - e.seq += uint32(len(p)) - n = len(p) - }) - } - return -} - -// Close closes the connection. -func (conn *TCPConn) Close() error { - var err error - conn.dieOnce.Do(func() { - // signal closing - close(conn.die) - - // close all established tcp connections - if conn.tcpconn != nil { // client - setTTL(conn.tcpconn, 64) - err = conn.tcpconn.Close() - } else if conn.listener != nil { - err = conn.listener.Close() // server - conn.flowsLock.Lock() - for k, v := range conn.flowTable { - if v.conn != nil { - setTTL(v.conn, 64) - v.conn.Close() - } - delete(conn.flowTable, k) - } - conn.flowsLock.Unlock() - } - - // close handles - for k := range conn.handles { - conn.handles[k].Close() - } - - // delete iptable - if conn.iptables != nil { - conn.iptables.Delete("filter", "OUTPUT", conn.iprule...) - } - if conn.ip6tables != nil { - conn.ip6tables.Delete("filter", "OUTPUT", conn.ip6rule...) - } - }) - return err -} - -// LocalAddr returns the local network address. -func (conn *TCPConn) LocalAddr() net.Addr { - if conn.tcpconn != nil { - return conn.tcpconn.LocalAddr() - } else if conn.listener != nil { - return conn.listener.Addr() - } - return nil -} - -// SetDeadline implements the Conn SetDeadline method. -func (conn *TCPConn) SetDeadline(t time.Time) error { - if err := conn.SetReadDeadline(t); err != nil { - return err - } - if err := conn.SetWriteDeadline(t); err != nil { - return err - } - return nil -} - -// SetReadDeadline implements the Conn SetReadDeadline method. -func (conn *TCPConn) SetReadDeadline(t time.Time) error { - conn.readDeadline.Store(t) - return nil -} - -// SetWriteDeadline implements the Conn SetWriteDeadline method. -func (conn *TCPConn) SetWriteDeadline(t time.Time) error { - conn.writeDeadline.Store(t) - return nil -} - -// SetDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header. -func (conn *TCPConn) SetDSCP(dscp int) error { - for k := range conn.handles { - if err := setDSCP(conn.handles[k], dscp); err != nil { - return err - } - } - return nil -} - -// SetReadBuffer sets the size of the operating system's receive buffer associated with the connection. -func (conn *TCPConn) SetReadBuffer(bytes int) error { - var err error - for k := range conn.handles { - if err := conn.handles[k].SetReadBuffer(bytes); err != nil { - return err - } - } - return err -} - -// SetWriteBuffer sets the size of the operating system's transmit buffer associated with the connection. -func (conn *TCPConn) SetWriteBuffer(bytes int) error { - var err error - for k := range conn.handles { - if err := conn.handles[k].SetWriteBuffer(bytes); err != nil { - return err - } - } - return err -} - -func (conn *TCPConn) SyscallConn() (syscall.RawConn, error) { - if len(conn.handles) == 0 { - return nil, errors.New("no handles") - // How is it possible? - } - return conn.handles[0].SyscallConn() -} - -// Dial connects to the remote TCP port, -// and returns a single packet-oriented connection -func Dial(network, address string) (*TCPConn, error) { - // remote address resolve - raddr, err := net.ResolveTCPAddr(network, address) - if err != nil { - return nil, err - } - - // AF_INET - handle, err := net.DialIP("ip:tcp", nil, &net.IPAddr{IP: raddr.IP}) - if err != nil { - return nil, err - } - - // create an established tcp connection - // will hack this tcp connection for packet transmission - tcpconn, err := net.DialTCP(network, nil, raddr) - if err != nil { - return nil, err - } - - // fields - conn := new(TCPConn) - conn.die = make(chan struct{}) - conn.flowTable = make(map[string]*tcpFlow) - conn.tcpconn = tcpconn - conn.chMessage = make(chan message) - conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn }) - conn.handles = append(conn.handles, handle) - conn.opts = gopacket.SerializeOptions{ - FixLengths: true, - ComputeChecksums: true, - } - go conn.captureFlow(handle, tcpconn.LocalAddr().(*net.TCPAddr).Port) - go conn.cleaner() - - // iptables - err = setTTL(tcpconn, 1) - if err != nil { - return nil, err - } - - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil { - rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.iprule = rule - conn.iptables = ipt - } - } - } - } - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil { - rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "-d", raddr.IP.String(), "--dport", fmt.Sprint(raddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.ip6rule = rule - conn.ip6tables = ipt - } - } - } - } - - // discard everything - go io.Copy(ioutil.Discard, tcpconn) - - return conn, nil -} - -// Listen acts like net.ListenTCP, -// and returns a single packet-oriented connection -func Listen(network, address string) (*TCPConn, error) { - // fields - conn := new(TCPConn) - conn.flowTable = make(map[string]*tcpFlow) - conn.die = make(chan struct{}) - conn.chMessage = make(chan message) - conn.opts = gopacket.SerializeOptions{ - FixLengths: true, - ComputeChecksums: true, - } - - // resolve address - laddr, err := net.ResolveTCPAddr(network, address) - if err != nil { - return nil, err - } - - // AF_INET - ifaces, err := net.Interfaces() - if err != nil { - return nil, err - } - - if laddr.IP == nil || laddr.IP.IsUnspecified() { // if address is not specified, capture on all ifaces - var lasterr error - for _, iface := range ifaces { - if addrs, err := iface.Addrs(); err == nil { - for _, addr := range addrs { - if ipaddr, ok := addr.(*net.IPNet); ok { - if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: ipaddr.IP}); err == nil { - conn.handles = append(conn.handles, handle) - go conn.captureFlow(handle, laddr.Port) - } else { - lasterr = err - } - } - } - } - } - if len(conn.handles) == 0 { - return nil, lasterr - } - } else { - if handle, err := net.ListenIP("ip:tcp", &net.IPAddr{IP: laddr.IP}); err == nil { - conn.handles = append(conn.handles, handle) - go conn.captureFlow(handle, laddr.Port) - } else { - return nil, err - } - } - - // start listening - l, err := net.ListenTCP(network, laddr) - if err != nil { - return nil, err - } - - conn.listener = l - - // start cleaner - go conn.cleaner() - - // iptables drop packets marked with TTL = 1 - // TODO: what if iptables is not available, the next hop will send back ICMP Time Exceeded, - // is this still an acceptable behavior? - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4); err == nil { - rule := []string{"-m", "ttl", "--ttl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.iprule = rule - conn.iptables = ipt - } - } - } - } - if ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv6); err == nil { - rule := []string{"-m", "hl", "--hl-eq", "1", "-p", "tcp", "--sport", fmt.Sprint(laddr.Port), "-j", "DROP"} - if exists, err := ipt.Exists("filter", "OUTPUT", rule...); err == nil { - if !exists { - if err = ipt.Append("filter", "OUTPUT", rule...); err == nil { - conn.ip6rule = rule - conn.ip6tables = ipt - } - } - } - } - - // discard everything in original connection - go func() { - for { - tcpconn, err := l.AcceptTCP() - if err != nil { - return - } - - // if we cannot set TTL = 1, the only thing reasonable is panic - if err := setTTL(tcpconn, 1); err != nil { - panic(err) - } - - // record net.Conn - conn.lockflow(tcpconn.RemoteAddr(), func(e *tcpFlow) { e.conn = tcpconn }) - - // discard everything - go io.Copy(ioutil.Discard, tcpconn) - } - }() - - return conn, nil -} - -// setTTL sets the Time-To-Live field on a given connection -func setTTL(c *net.TCPConn, ttl int) error { - raw, err := c.SyscallConn() - if err != nil { - return err - } - addr := c.LocalAddr().(*net.TCPAddr) - - if addr.IP.To4() == nil { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_UNICAST_HOPS, ttl) - }) - } else { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TTL, ttl) - }) - } - return err -} - -// setDSCP sets the 6bit DSCP field in IPv4 header, or 8bit Traffic Class in IPv6 header. -func setDSCP(c *net.IPConn, dscp int) error { - raw, err := c.SyscallConn() - if err != nil { - return err - } - addr := c.LocalAddr().(*net.IPAddr) - - if addr.IP.To4() == nil { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_TCLASS, dscp) - }) - } else { - raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, dscp<<2) - }) - } - return err -} diff --git a/core/pktconns/faketcp/tcp_stub.go b/core/pktconns/faketcp/tcp_stub.go deleted file mode 100644 index 9bc5507..0000000 --- a/core/pktconns/faketcp/tcp_stub.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build !linux -// +build !linux - -package faketcp - -import ( - "errors" - "net" -) - -type TCPConn struct{ *net.UDPConn } - -// Dial connects to the remote TCP port, -// and returns a single packet-oriented connection -func Dial(network, address string) (*TCPConn, error) { - return nil, errors.New("faketcp is not supported on this platform") -} - -func Listen(network, address string) (*TCPConn, error) { - return nil, errors.New("faketcp is not supported on this platform") -} diff --git a/core/pktconns/faketcp/tcp_test.go b/core/pktconns/faketcp/tcp_test.go deleted file mode 100644 index fa850b8..0000000 --- a/core/pktconns/faketcp/tcp_test.go +++ /dev/null @@ -1,198 +0,0 @@ -//go:build linux -// +build linux - -package faketcp - -import ( - "log" - "net" - "net/http" - _ "net/http/pprof" - "testing" -) - -// const testPortStream = "127.0.0.1:3456" -// const testPortPacket = "127.0.0.1:3457" - -const ( - testPortStream = "127.0.0.1:3456" - portServerPacket = "[::]:3457" - portRemotePacket = "127.0.0.1:3457" -) - -func init() { - startTCPServer() - startTCPRawServer() - go func() { - log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) - }() -} - -func startTCPServer() net.Listener { - l, err := net.Listen("tcp", testPortStream) - if err != nil { - log.Panicln(err) - } - - go func() { - defer l.Close() - for { - conn, err := l.Accept() - if err != nil { - log.Println(err) - return - } - - go handleRequest(conn) - } - }() - return l -} - -func startTCPRawServer() *TCPConn { - conn, err := Listen("tcp", portServerPacket) - if err != nil { - log.Panicln(err) - } - err = conn.SetReadBuffer(1024 * 1024) - if err != nil { - log.Println(err) - } - err = conn.SetWriteBuffer(1024 * 1024) - if err != nil { - log.Println(err) - } - - go func() { - defer conn.Close() - buf := make([]byte, 1024) - for { - n, addr, err := conn.ReadFrom(buf) - if err != nil { - log.Println("server readfrom:", err) - return - } - // echo - n, err = conn.WriteTo(buf[:n], addr) - if err != nil { - log.Println("server writeTo:", err) - return - } - } - }() - return conn -} - -func handleRequest(conn net.Conn) { - defer conn.Close() - - for { - buf := make([]byte, 1024) - size, err := conn.Read(buf) - if err != nil { - log.Println("handleRequest:", err) - return - } - data := buf[:size] - conn.Write(data) - } -} - -func TestDialTCPStream(t *testing.T) { - conn, err := Dial("tcp", testPortStream) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - addr, err := net.ResolveTCPAddr("tcp", testPortStream) - if err != nil { - t.Fatal(err) - } - - n, err := conn.WriteTo([]byte("abc"), addr) - if err != nil { - t.Fatal(n, err) - } - - buf := make([]byte, 1024) - if n, addr, err := conn.ReadFrom(buf); err != nil { - t.Fatal(n, addr, err) - } else { - log.Println(string(buf[:n]), "from:", addr) - } -} - -func TestDialToTCPPacket(t *testing.T) { - conn, err := Dial("tcp", portRemotePacket) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - addr, err := net.ResolveTCPAddr("tcp", portRemotePacket) - if err != nil { - t.Fatal(err) - } - - n, err := conn.WriteTo([]byte("abc"), addr) - if err != nil { - t.Fatal(n, err) - } - log.Println("written") - - buf := make([]byte, 1024) - log.Println("readfrom buf") - if n, addr, err := conn.ReadFrom(buf); err != nil { - log.Println(err) - t.Fatal(n, addr, err) - } else { - log.Println(string(buf[:n]), "from:", addr) - } - - log.Println("complete") -} - -func TestSettings(t *testing.T) { - conn, err := Dial("tcp", portRemotePacket) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - if err := conn.SetDSCP(46); err != nil { - log.Fatal("SetDSCP:", err) - } - if err := conn.SetReadBuffer(4096); err != nil { - log.Fatal("SetReaderBuffer:", err) - } - if err := conn.SetWriteBuffer(4096); err != nil { - log.Fatal("SetWriteBuffer:", err) - } -} - -func BenchmarkEcho(b *testing.B) { - conn, err := Dial("tcp", portRemotePacket) - if err != nil { - b.Fatal(err) - } - defer conn.Close() - - addr, err := net.ResolveTCPAddr("tcp", portRemotePacket) - if err != nil { - b.Fatal(err) - } - - buf := make([]byte, 1024) - b.ReportAllocs() - b.SetBytes(int64(len(buf))) - for i := 0; i < b.N; i++ { - n, err := conn.WriteTo(buf, addr) - if err != nil { - b.Fatal(n, err) - } - - if n, addr, err := conn.ReadFrom(buf); err != nil { - b.Fatal(n, addr, err) - } - } -} diff --git a/core/pktconns/funcs.go b/core/pktconns/funcs.go deleted file mode 100644 index b0675f1..0000000 --- a/core/pktconns/funcs.go +++ /dev/null @@ -1,189 +0,0 @@ -package pktconns - -import ( - "net" - "strings" - "time" - - "github.com/apernet/hysteria/core/pktconns/faketcp" - "github.com/apernet/hysteria/core/pktconns/obfs" - "github.com/apernet/hysteria/core/pktconns/udp" - "github.com/apernet/hysteria/core/pktconns/wechat" -) - -type ( - ClientPacketConnFunc func(server string) (net.PacketConn, net.Addr, error) - ServerPacketConnFunc func(listen string) (net.PacketConn, error) -) - -type ( - ClientPacketConnFuncFactory func(obfsPassword string, hopInterval time.Duration) ClientPacketConnFunc - ServerPacketConnFuncFactory func(obfsPassword string) ServerPacketConnFunc -) - -func NewClientUDPConnFunc(obfsPassword string, hopInterval time.Duration) ClientPacketConnFunc { - if obfsPassword == "" { - return func(server string) (net.PacketConn, net.Addr, error) { - if isMultiPortAddr(server) { - return udp.NewObfsUDPHopClientPacketConn(server, hopInterval, nil) - } - sAddr, err := net.ResolveUDPAddr("udp", server) - if err != nil { - return nil, nil, err - } - udpConn, err := net.ListenUDP("udp", nil) - return udpConn, sAddr, err - } - } else { - return func(server string) (net.PacketConn, net.Addr, error) { - if isMultiPortAddr(server) { - ob := obfs.NewXPlusObfuscator([]byte(obfsPassword)) - return udp.NewObfsUDPHopClientPacketConn(server, hopInterval, ob) - } - sAddr, err := net.ResolveUDPAddr("udp", server) - if err != nil { - return nil, nil, err - } - udpConn, err := net.ListenUDP("udp", nil) - if err != nil { - return nil, nil, err - } - ob := obfs.NewXPlusObfuscator([]byte(obfsPassword)) - return udp.NewObfsUDPConn(udpConn, ob), sAddr, nil - } - } -} - -func NewClientWeChatConnFunc(obfsPassword string, hopInterval time.Duration) ClientPacketConnFunc { - if obfsPassword == "" { - return func(server string) (net.PacketConn, net.Addr, error) { - sAddr, err := net.ResolveUDPAddr("udp", server) - if err != nil { - return nil, nil, err - } - udpConn, err := net.ListenUDP("udp", nil) - if err != nil { - return nil, nil, err - } - return wechat.NewObfsWeChatUDPConn(udpConn, nil), sAddr, nil - } - } else { - return func(server string) (net.PacketConn, net.Addr, error) { - sAddr, err := net.ResolveUDPAddr("udp", server) - if err != nil { - return nil, nil, err - } - udpConn, err := net.ListenUDP("udp", nil) - if err != nil { - return nil, nil, err - } - ob := obfs.NewXPlusObfuscator([]byte(obfsPassword)) - return wechat.NewObfsWeChatUDPConn(udpConn, ob), sAddr, nil - } - } -} - -func NewClientFakeTCPConnFunc(obfsPassword string, hopInterval time.Duration) ClientPacketConnFunc { - if obfsPassword == "" { - return func(server string) (net.PacketConn, net.Addr, error) { - sAddr, err := net.ResolveTCPAddr("tcp", server) - if err != nil { - return nil, nil, err - } - fTCPConn, err := faketcp.Dial("tcp", server) - return fTCPConn, sAddr, err - } - } else { - return func(server string) (net.PacketConn, net.Addr, error) { - sAddr, err := net.ResolveTCPAddr("tcp", server) - if err != nil { - return nil, nil, err - } - fTCPConn, err := faketcp.Dial("tcp", server) - if err != nil { - return nil, nil, err - } - ob := obfs.NewXPlusObfuscator([]byte(obfsPassword)) - return faketcp.NewObfsFakeTCPConn(fTCPConn, ob), sAddr, nil - } - } -} - -func NewServerUDPConnFunc(obfsPassword string) ServerPacketConnFunc { - if obfsPassword == "" { - return func(listen string) (net.PacketConn, error) { - laddrU, err := net.ResolveUDPAddr("udp", listen) - if err != nil { - return nil, err - } - return net.ListenUDP("udp", laddrU) - } - } else { - return func(listen string) (net.PacketConn, error) { - ob := obfs.NewXPlusObfuscator([]byte(obfsPassword)) - laddrU, err := net.ResolveUDPAddr("udp", listen) - if err != nil { - return nil, err - } - udpConn, err := net.ListenUDP("udp", laddrU) - if err != nil { - return nil, err - } - return udp.NewObfsUDPConn(udpConn, ob), nil - } - } -} - -func NewServerWeChatConnFunc(obfsPassword string) ServerPacketConnFunc { - if obfsPassword == "" { - return func(listen string) (net.PacketConn, error) { - laddrU, err := net.ResolveUDPAddr("udp", listen) - if err != nil { - return nil, err - } - udpConn, err := net.ListenUDP("udp", laddrU) - if err != nil { - return nil, err - } - return wechat.NewObfsWeChatUDPConn(udpConn, nil), nil - } - } else { - return func(listen string) (net.PacketConn, error) { - ob := obfs.NewXPlusObfuscator([]byte(obfsPassword)) - laddrU, err := net.ResolveUDPAddr("udp", listen) - if err != nil { - return nil, err - } - udpConn, err := net.ListenUDP("udp", laddrU) - if err != nil { - return nil, err - } - return wechat.NewObfsWeChatUDPConn(udpConn, ob), nil - } - } -} - -func NewServerFakeTCPConnFunc(obfsPassword string) ServerPacketConnFunc { - if obfsPassword == "" { - return func(listen string) (net.PacketConn, error) { - return faketcp.Listen("tcp", listen) - } - } else { - return func(listen string) (net.PacketConn, error) { - ob := obfs.NewXPlusObfuscator([]byte(obfsPassword)) - fakeTCPListener, err := faketcp.Listen("tcp", listen) - if err != nil { - return nil, err - } - return faketcp.NewObfsFakeTCPConn(fakeTCPListener, ob), nil - } - } -} - -func isMultiPortAddr(addr string) bool { - _, portStr, err := net.SplitHostPort(addr) - if err == nil && (strings.Contains(portStr, ",") || strings.Contains(portStr, "-")) { - return true - } - return false -} diff --git a/core/pktconns/obfs/obfs.go b/core/pktconns/obfs/obfs.go deleted file mode 100644 index 2829560..0000000 --- a/core/pktconns/obfs/obfs.go +++ /dev/null @@ -1,58 +0,0 @@ -package obfs - -import ( - "crypto/sha256" - "math/rand" - "sync" - "time" -) - -type Obfuscator interface { - Deobfuscate(in []byte, out []byte) int - Obfuscate(in []byte, out []byte) int -} - -const xpSaltLen = 16 - -// XPlusObfuscator obfuscates payload using one-time keys generated from hashing a pre-shared key and random salt. -// Packet format: [salt][obfuscated payload] -type XPlusObfuscator struct { - Key []byte - RandSrc *rand.Rand - - lk sync.Mutex -} - -func NewXPlusObfuscator(key []byte) *XPlusObfuscator { - return &XPlusObfuscator{ - Key: key, - RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), - } -} - -func (x *XPlusObfuscator) Deobfuscate(in []byte, out []byte) int { - outLen := len(in) - xpSaltLen - if outLen <= 0 || len(out) < outLen { - return 0 - } - key := sha256.Sum256(append(x.Key, in[:xpSaltLen]...)) - for i, c := range in[xpSaltLen:] { - out[i] = c ^ key[i%sha256.Size] - } - return outLen -} - -func (x *XPlusObfuscator) Obfuscate(in []byte, out []byte) int { - outLen := len(in) + xpSaltLen - if len(out) < outLen { - return 0 - } - x.lk.Lock() - _, _ = x.RandSrc.Read(out[:xpSaltLen]) - x.lk.Unlock() - key := sha256.Sum256(append(x.Key, out[:xpSaltLen]...)) - for i, c := range in { - out[i+xpSaltLen] = c ^ key[i%sha256.Size] - } - return outLen -} diff --git a/core/pktconns/obfs/obfs_test.go b/core/pktconns/obfs/obfs_test.go deleted file mode 100644 index c1cf629..0000000 --- a/core/pktconns/obfs/obfs_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package obfs - -import ( - "bytes" - "testing" -) - -func TestXPlusObfuscator(t *testing.T) { - x := NewXPlusObfuscator([]byte("Vaundy")) - tests := []struct { - name string - p []byte - }{ - {name: "1", p: []byte("HelloWorld")}, - {name: "2", p: []byte("Regret is just a horrible attempt at time travel that ends with you feeling like crap")}, - {name: "3", p: []byte("To be, or not to be, that is the question:\nWhether 'tis nobler in the mind to suffer\n" + - "The slings and arrows of outrageous fortune,\nOr to take arms against a sea of troubles\n" + - "And by opposing end them. To die—to sleep,\nNo more; and by a sleep to say we end")}, - {name: "empty", p: []byte("")}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := make([]byte, 10240) - n := x.Obfuscate(tt.p, buf) - n2 := x.Deobfuscate(buf[:n], buf[n:]) - if !bytes.Equal(tt.p, buf[n:n+n2]) { - t.Errorf("Inconsistent deobfuscate result: got %v, want %v", buf[n:n+n2], tt.p) - } - }) - } -} diff --git a/core/pktconns/udp/hop.go b/core/pktconns/udp/hop.go deleted file mode 100644 index a893514..0000000 --- a/core/pktconns/udp/hop.go +++ /dev/null @@ -1,353 +0,0 @@ -package udp - -import ( - "errors" - "math/rand" - "net" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/apernet/hysteria/core/pktconns/obfs" -) - -const ( - packetQueueSize = 1024 -) - -// ObfsUDPHopClientPacketConn is the UDP port-hopping packet connection for client side. -// It hops to a different local & server port every once in a while. -type ObfsUDPHopClientPacketConn struct { - serverAddr net.Addr // Combined udpHopAddr - serverAddrs []net.Addr - hopInterval time.Duration - - obfs obfs.Obfuscator - - connMutex sync.RWMutex - prevConn net.PacketConn - currentConn net.PacketConn - addrIndex int - - readBufferSize int - writeBufferSize int - - recvQueue chan *udpPacket - closeChan chan struct{} - closed bool - - bufPool sync.Pool -} - -type udpHopAddr string - -func (a *udpHopAddr) Network() string { - return "udp-hop" -} - -func (a *udpHopAddr) String() string { - return string(*a) -} - -type udpPacket struct { - buf []byte - n int - addr net.Addr -} - -func NewObfsUDPHopClientPacketConn(server string, hopInterval time.Duration, obfs obfs.Obfuscator) (*ObfsUDPHopClientPacketConn, net.Addr, error) { - host, ports, err := parseAddr(server) - if err != nil { - return nil, nil, err - } - // Resolve the server IP address, then attach the ports to UDP addresses - ip, err := net.ResolveIPAddr("ip", host) - if err != nil { - return nil, nil, err - } - serverAddrs := make([]net.Addr, len(ports)) - for i, port := range ports { - serverAddrs[i] = &net.UDPAddr{ - IP: ip.IP, - Port: int(port), - } - } - hopAddr := udpHopAddr(server) - conn := &ObfsUDPHopClientPacketConn{ - serverAddr: &hopAddr, - serverAddrs: serverAddrs, - hopInterval: hopInterval, - obfs: obfs, - addrIndex: rand.Intn(len(serverAddrs)), - recvQueue: make(chan *udpPacket, packetQueueSize), - closeChan: make(chan struct{}), - bufPool: sync.Pool{ - New: func() interface{} { - return make([]byte, udpBufferSize) - }, - }, - } - curConn, err := net.ListenUDP("udp", nil) - if err != nil { - return nil, nil, err - } - if obfs != nil { - conn.currentConn = NewObfsUDPConn(curConn, obfs) - } else { - conn.currentConn = curConn - } - go conn.recvRoutine(conn.currentConn) - go conn.hopRoutine() - return conn, conn.serverAddr, nil -} - -func (c *ObfsUDPHopClientPacketConn) recvRoutine(conn net.PacketConn) { - for { - buf := c.bufPool.Get().([]byte) - n, addr, err := conn.ReadFrom(buf) - if err != nil { - return - } - select { - case c.recvQueue <- &udpPacket{buf, n, addr}: - default: - // Drop the packet if the queue is full - c.bufPool.Put(buf) - } - } -} - -func (c *ObfsUDPHopClientPacketConn) hopRoutine() { - ticker := time.NewTicker(c.hopInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - c.hop() - case <-c.closeChan: - return - } - } -} - -func (c *ObfsUDPHopClientPacketConn) hop() { - c.connMutex.Lock() - defer c.connMutex.Unlock() - if c.closed { - return - } - newConn, err := net.ListenUDP("udp", nil) - if err != nil { - // Skip this hop if failed to listen - return - } - // Close prevConn, - // prevConn <- currentConn - // currentConn <- newConn - // update addrIndex - // - // We need to keep receiving packets from the previous connection, - // because otherwise there will be packet loss due to the time gap - // between we hop to a new port and the server acknowledges this change. - if c.prevConn != nil { - _ = c.prevConn.Close() // recvRoutine will exit on error - } - c.prevConn = c.currentConn - if c.obfs != nil { - c.currentConn = NewObfsUDPConn(newConn, c.obfs) - } else { - c.currentConn = newConn - } - // Set buffer sizes if previously set - if c.readBufferSize > 0 { - _ = trySetPacketConnReadBuffer(c.currentConn, c.readBufferSize) - } - if c.writeBufferSize > 0 { - _ = trySetPacketConnWriteBuffer(c.currentConn, c.writeBufferSize) - } - go c.recvRoutine(c.currentConn) - c.addrIndex = rand.Intn(len(c.serverAddrs)) -} - -func (c *ObfsUDPHopClientPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { - for { - select { - case p := <-c.recvQueue: - /* - // Check if the packet is from one of the server addresses - for _, addr := range c.serverAddrs { - if addr.String() == p.addr.String() { - // Copy the packet to the buffer - n := copy(b, p.buf[:p.n]) - c.bufPool.Put(p.buf) - return n, c.serverAddr, nil - } - } - // Drop the packet, continue - c.bufPool.Put(p.buf) - */ - // The above code was causing performance issues when the range is large, - // so we skip the check for now. Should probably still check by using a map - // or something in the future. - n := copy(b, p.buf[:p.n]) - c.bufPool.Put(p.buf) - return n, c.serverAddr, nil - case <-c.closeChan: - return 0, nil, net.ErrClosed - } - // Ignore packets from other addresses - } -} - -func (c *ObfsUDPHopClientPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { - c.connMutex.RLock() - defer c.connMutex.RUnlock() - if c.closed { - return 0, net.ErrClosed - } - /* - // Check if the address is the server address - if addr.String() != c.serverAddr.String() { - return 0, net.ErrWriteToConnected - } - */ - // Skip the check for now, always write to the server - return c.currentConn.WriteTo(b, c.serverAddrs[c.addrIndex]) -} - -func (c *ObfsUDPHopClientPacketConn) Close() error { - c.connMutex.Lock() - defer c.connMutex.Unlock() - if c.closed { - return nil - } - // Close prevConn and currentConn - // Close closeChan to unblock ReadFrom & hopRoutine - // Set closed flag to true to prevent double close - if c.prevConn != nil { - _ = c.prevConn.Close() - } - err := c.currentConn.Close() - close(c.closeChan) - c.closed = true - c.serverAddrs = nil // For GC - return err -} - -func (c *ObfsUDPHopClientPacketConn) LocalAddr() net.Addr { - c.connMutex.RLock() - defer c.connMutex.RUnlock() - return c.currentConn.LocalAddr() -} - -func (c *ObfsUDPHopClientPacketConn) SetReadDeadline(t time.Time) error { - // Not supported - return nil -} - -func (c *ObfsUDPHopClientPacketConn) SetWriteDeadline(t time.Time) error { - // Not supported - return nil -} - -func (c *ObfsUDPHopClientPacketConn) SetDeadline(t time.Time) error { - err := c.SetReadDeadline(t) - if err != nil { - return err - } - return c.SetWriteDeadline(t) -} - -func (c *ObfsUDPHopClientPacketConn) SetReadBuffer(bytes int) error { - c.connMutex.Lock() - defer c.connMutex.Unlock() - c.readBufferSize = bytes - if c.prevConn != nil { - _ = trySetPacketConnReadBuffer(c.prevConn, bytes) - } - return trySetPacketConnReadBuffer(c.currentConn, bytes) -} - -func (c *ObfsUDPHopClientPacketConn) SetWriteBuffer(bytes int) error { - c.connMutex.Lock() - defer c.connMutex.Unlock() - c.writeBufferSize = bytes - if c.prevConn != nil { - _ = trySetPacketConnWriteBuffer(c.prevConn, bytes) - } - return trySetPacketConnWriteBuffer(c.currentConn, bytes) -} - -func (c *ObfsUDPHopClientPacketConn) SyscallConn() (syscall.RawConn, error) { - c.connMutex.RLock() - defer c.connMutex.RUnlock() - sc, ok := c.currentConn.(syscall.Conn) - if !ok { - return nil, errors.New("not supported") - } - return sc.SyscallConn() -} - -func trySetPacketConnReadBuffer(pc net.PacketConn, bytes int) error { - sc, ok := pc.(interface { - SetReadBuffer(bytes int) error - }) - if ok { - return sc.SetReadBuffer(bytes) - } - return nil -} - -func trySetPacketConnWriteBuffer(pc net.PacketConn, bytes int) error { - sc, ok := pc.(interface { - SetWriteBuffer(bytes int) error - }) - if ok { - return sc.SetWriteBuffer(bytes) - } - return nil -} - -// parseAddr parses the multi-port server address and returns the host and ports. -// Supports both comma-separated single ports and dash-separated port ranges. -// Format: "host:port1,port2-port3,port4" -func parseAddr(addr string) (host string, ports []uint16, err error) { - host, portStr, err := net.SplitHostPort(addr) - if err != nil { - return "", nil, err - } - portStrs := strings.Split(portStr, ",") - for _, portStr := range portStrs { - if strings.Contains(portStr, "-") { - // Port range - portRange := strings.Split(portStr, "-") - if len(portRange) != 2 { - return "", nil, net.InvalidAddrError("invalid port range") - } - start, err := strconv.ParseUint(portRange[0], 10, 16) - if err != nil { - return "", nil, net.InvalidAddrError("invalid port range") - } - end, err := strconv.ParseUint(portRange[1], 10, 16) - if err != nil { - return "", nil, net.InvalidAddrError("invalid port range") - } - if start > end { - start, end = end, start - } - for i := start; i <= end; i++ { - ports = append(ports, uint16(i)) - } - } else { - // Single port - port, err := strconv.ParseUint(portStr, 10, 16) - if err != nil { - return "", nil, net.InvalidAddrError("invalid port") - } - ports = append(ports, uint16(port)) - } - } - return host, ports, nil -} diff --git a/core/pktconns/udp/hop_test.go b/core/pktconns/udp/hop_test.go deleted file mode 100644 index cacf5a5..0000000 --- a/core/pktconns/udp/hop_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package udp - -import ( - "reflect" - "testing" -) - -func Test_parseAddr(t *testing.T) { - tests := []struct { - name string - addr string - wantHost string - wantPorts []uint16 - wantErr bool - }{ - { - name: "empty", - addr: "", - wantHost: "", - wantPorts: nil, - wantErr: true, - }, - { - name: "host only", - addr: "example.com", - wantHost: "", - wantPorts: nil, - wantErr: true, - }, - { - name: "single port", - addr: "example.com:1234", - wantHost: "example.com", - wantPorts: []uint16{1234}, - wantErr: false, - }, - { - name: "multi ports", - addr: "example.com:1234,5678,9999", - wantHost: "example.com", - wantPorts: []uint16{1234, 5678, 9999}, - wantErr: false, - }, - { - name: "multi ports with range", - addr: "example.com:1234,5678-5685,9999", - wantHost: "example.com", - wantPorts: []uint16{1234, 5678, 5679, 5680, 5681, 5682, 5683, 5684, 5685, 9999}, - wantErr: false, - }, - { - name: "range single port", - addr: "example.com:1234-1234", - wantHost: "example.com", - wantPorts: []uint16{1234}, - wantErr: false, - }, - { - name: "range reversed", - addr: "example.com:8003-8000", - wantHost: "example.com", - wantPorts: []uint16{8000, 8001, 8002, 8003}, - wantErr: false, - }, - { - name: "invalid port", - addr: "example.com:1234,5678,9999,invalid", - wantHost: "", - wantPorts: nil, - wantErr: true, - }, - { - name: "invalid port range", - addr: "example.com:1234,5678,9999,8000-8002-8004", - wantHost: "", - wantPorts: nil, - wantErr: true, - }, - { - name: "invalid port range 2", - addr: "example.com:1234,5678,9999,8000-woot", - wantHost: "", - wantPorts: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotHost, gotPorts, err := parseAddr(tt.addr) - if (err != nil) != tt.wantErr { - t.Errorf("parseAddr() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotHost != tt.wantHost { - t.Errorf("parseAddr() gotHost = %v, want %v", gotHost, tt.wantHost) - } - if !reflect.DeepEqual(gotPorts, tt.wantPorts) { - t.Errorf("parseAddr() gotPorts = %v, want %v", gotPorts, tt.wantPorts) - } - }) - } -} diff --git a/core/pktconns/udp/obfs.go b/core/pktconns/udp/obfs.go deleted file mode 100644 index 9f1165e..0000000 --- a/core/pktconns/udp/obfs.go +++ /dev/null @@ -1,100 +0,0 @@ -package udp - -import ( - "net" - "os" - "sync" - "syscall" - "time" - - "github.com/apernet/hysteria/core/pktconns/obfs" -) - -const udpBufferSize = 4096 - -type ObfsUDPPacketConn struct { - orig *net.UDPConn - obfs obfs.Obfuscator - - readBuf []byte - readMutex sync.Mutex - writeBuf []byte - writeMutex sync.Mutex -} - -func NewObfsUDPConn(orig *net.UDPConn, obfs obfs.Obfuscator) *ObfsUDPPacketConn { - return &ObfsUDPPacketConn{ - orig: orig, - obfs: obfs, - readBuf: make([]byte, udpBufferSize), - writeBuf: make([]byte, udpBufferSize), - } -} - -func (c *ObfsUDPPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { - for { - c.readMutex.Lock() - n, addr, err := c.orig.ReadFrom(c.readBuf) - if n <= 0 { - c.readMutex.Unlock() - return 0, addr, err - } - newN := c.obfs.Deobfuscate(c.readBuf[:n], p) - c.readMutex.Unlock() - if newN > 0 { - // Valid packet - return newN, addr, err - } else if err != nil { - // Not valid and orig.ReadFrom had some error - return 0, addr, err - } - } -} - -func (c *ObfsUDPPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.writeMutex.Lock() - bn := c.obfs.Obfuscate(p, c.writeBuf) - _, err = c.orig.WriteTo(c.writeBuf[:bn], addr) - c.writeMutex.Unlock() - if err != nil { - return 0, err - } else { - return len(p), nil - } -} - -func (c *ObfsUDPPacketConn) Close() error { - return c.orig.Close() -} - -func (c *ObfsUDPPacketConn) LocalAddr() net.Addr { - return c.orig.LocalAddr() -} - -func (c *ObfsUDPPacketConn) SetDeadline(t time.Time) error { - return c.orig.SetDeadline(t) -} - -func (c *ObfsUDPPacketConn) SetReadDeadline(t time.Time) error { - return c.orig.SetReadDeadline(t) -} - -func (c *ObfsUDPPacketConn) SetWriteDeadline(t time.Time) error { - return c.orig.SetWriteDeadline(t) -} - -func (c *ObfsUDPPacketConn) SetReadBuffer(bytes int) error { - return c.orig.SetReadBuffer(bytes) -} - -func (c *ObfsUDPPacketConn) SetWriteBuffer(bytes int) error { - return c.orig.SetWriteBuffer(bytes) -} - -func (c *ObfsUDPPacketConn) SyscallConn() (syscall.RawConn, error) { - return c.orig.SyscallConn() -} - -func (c *ObfsUDPPacketConn) File() (f *os.File, err error) { - return c.orig.File() -} diff --git a/core/pktconns/wechat/obfs.go b/core/pktconns/wechat/obfs.go deleted file mode 100644 index e6935f7..0000000 --- a/core/pktconns/wechat/obfs.go +++ /dev/null @@ -1,127 +0,0 @@ -package wechat - -import ( - "encoding/binary" - "math/rand" - "net" - "os" - "sync" - "syscall" - "time" - - "github.com/apernet/hysteria/core/pktconns/obfs" -) - -const udpBufferSize = 4096 - -// ObfsWeChatUDPPacketConn is still a UDP packet conn, but it adds WeChat video call header to each packet. -// Obfs in this case can be nil -type ObfsWeChatUDPPacketConn struct { - orig *net.UDPConn - obfs obfs.Obfuscator - - readBuf []byte - readMutex sync.Mutex - writeBuf []byte - writeMutex sync.Mutex - sn uint32 -} - -func NewObfsWeChatUDPConn(orig *net.UDPConn, obfs obfs.Obfuscator) *ObfsWeChatUDPPacketConn { - return &ObfsWeChatUDPPacketConn{ - orig: orig, - obfs: obfs, - readBuf: make([]byte, udpBufferSize), - writeBuf: make([]byte, udpBufferSize), - sn: rand.Uint32() & 0xFFFF, - } -} - -func (c *ObfsWeChatUDPPacketConn) ReadFrom(p []byte) (int, net.Addr, error) { - for { - c.readMutex.Lock() - n, addr, err := c.orig.ReadFrom(c.readBuf) - if n <= 13 { - c.readMutex.Unlock() - return 0, addr, err - } - var newN int - if c.obfs != nil { - newN = c.obfs.Deobfuscate(c.readBuf[13:n], p) - } else { - newN = copy(p, c.readBuf[13:n]) - } - c.readMutex.Unlock() - if newN > 0 { - // Valid packet - return newN, addr, err - } else if err != nil { - // Not valid and orig.ReadFrom had some error - return 0, addr, err - } - } -} - -func (c *ObfsWeChatUDPPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - c.writeMutex.Lock() - c.writeBuf[0] = 0xa1 - c.writeBuf[1] = 0x08 - binary.BigEndian.PutUint32(c.writeBuf[2:], c.sn) - c.sn++ - c.writeBuf[6] = 0x00 - c.writeBuf[7] = 0x10 - c.writeBuf[8] = 0x11 - c.writeBuf[9] = 0x18 - c.writeBuf[10] = 0x30 - c.writeBuf[11] = 0x22 - c.writeBuf[12] = 0x30 - var bn int - if c.obfs != nil { - bn = c.obfs.Obfuscate(p, c.writeBuf[13:]) - } else { - bn = copy(c.writeBuf[13:], p) - } - _, err = c.orig.WriteTo(c.writeBuf[:13+bn], addr) - c.writeMutex.Unlock() - if err != nil { - return 0, err - } else { - return len(p), nil - } -} - -func (c *ObfsWeChatUDPPacketConn) Close() error { - return c.orig.Close() -} - -func (c *ObfsWeChatUDPPacketConn) LocalAddr() net.Addr { - return c.orig.LocalAddr() -} - -func (c *ObfsWeChatUDPPacketConn) SetDeadline(t time.Time) error { - return c.orig.SetDeadline(t) -} - -func (c *ObfsWeChatUDPPacketConn) SetReadDeadline(t time.Time) error { - return c.orig.SetReadDeadline(t) -} - -func (c *ObfsWeChatUDPPacketConn) SetWriteDeadline(t time.Time) error { - return c.orig.SetWriteDeadline(t) -} - -func (c *ObfsWeChatUDPPacketConn) SetReadBuffer(bytes int) error { - return c.orig.SetReadBuffer(bytes) -} - -func (c *ObfsWeChatUDPPacketConn) SetWriteBuffer(bytes int) error { - return c.orig.SetWriteBuffer(bytes) -} - -func (c *ObfsWeChatUDPPacketConn) SyscallConn() (syscall.RawConn, error) { - return c.orig.SyscallConn() -} - -func (c *ObfsWeChatUDPPacketConn) File() (f *os.File, err error) { - return c.orig.File() -} diff --git a/core/pmtud/avail.go b/core/pmtud/avail.go deleted file mode 100644 index 5f39f9c..0000000 --- a/core/pmtud/avail.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build linux || windows -// +build linux windows - -package pmtud - -// quic-go's MTU discovery is by default enabled on all platforms. However, we found that it -// does not set DF bit correctly on some platforms (macOS for example), which causes the probe -// packets (which should never be fragmented) to be fragmented and sent anyway. So here in -// Hysteria we only enable it on Linux and Windows for now, where we have tested it and can -// confirm that it works correctly. - -const ( - DisablePathMTUDiscovery = false -) diff --git a/core/pmtud/unavail.go b/core/pmtud/unavail.go deleted file mode 100644 index 2221361..0000000 --- a/core/pmtud/unavail.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !linux && !windows -// +build !linux,!windows - -package pmtud - -const ( - DisablePathMTUDiscovery = true -) diff --git a/core/server/config.go b/core/server/config.go new file mode 100644 index 0000000..edc62c6 --- /dev/null +++ b/core/server/config.go @@ -0,0 +1,172 @@ +package server + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/apernet/hysteria/core/errors" + "github.com/apernet/hysteria/core/internal/pmtud" +) + +const ( + defaultStreamReceiveWindow = 8388608 // 8MB + defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB + defaultMaxIdleTimeout = 30 * time.Second + defaultMaxIncomingStreams = 1024 +) + +type Config struct { + TLSConfig TLSConfig + QUICConfig QUICConfig + Conn net.PacketConn + Outbound Outbound + BandwidthConfig BandwidthConfig + DisableUDP bool + Authenticator Authenticator + EventLogger EventLogger + // TODO: TrafficLogger + MasqHandler http.Handler +} + +// fill fills the fields that are not set by the user with default values when possible, +// and returns an error if the user has not set a required field, or if a field is invalid. +func (c *Config) fill() error { + if len(c.TLSConfig.Certificates) == 0 && c.TLSConfig.GetCertificate == nil { + return errors.ConfigError{Field: "TLSConfig", Reason: "must set at least one of Certificates or GetCertificate"} + } + if c.QUICConfig.InitialStreamReceiveWindow == 0 { + c.QUICConfig.InitialStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.InitialStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxStreamReceiveWindow == 0 { + c.QUICConfig.MaxStreamReceiveWindow = defaultStreamReceiveWindow + } else if c.QUICConfig.MaxStreamReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxStreamReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.InitialConnectionReceiveWindow == 0 { + c.QUICConfig.InitialConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.InitialConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.InitialConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxConnectionReceiveWindow == 0 { + c.QUICConfig.MaxConnectionReceiveWindow = defaultConnReceiveWindow + } else if c.QUICConfig.MaxConnectionReceiveWindow < 16384 { + return errors.ConfigError{Field: "QUICConfig.MaxConnectionReceiveWindow", Reason: "must be at least 16384"} + } + if c.QUICConfig.MaxIdleTimeout == 0 { + c.QUICConfig.MaxIdleTimeout = defaultMaxIdleTimeout + } else if c.QUICConfig.MaxIdleTimeout < 4*time.Second || c.QUICConfig.MaxIdleTimeout > 120*time.Second { + return errors.ConfigError{Field: "QUICConfig.MaxIdleTimeout", Reason: "must be between 4s and 120s"} + } + if c.QUICConfig.MaxIncomingStreams == 0 { + c.QUICConfig.MaxIncomingStreams = defaultMaxIncomingStreams + } else if c.QUICConfig.MaxIncomingStreams < 8 { + return errors.ConfigError{Field: "QUICConfig.MaxIncomingStreams", Reason: "must be at least 8"} + } + c.QUICConfig.DisablePathMTUDiscovery = c.QUICConfig.DisablePathMTUDiscovery || pmtud.DisablePathMTUDiscovery + if c.Conn == nil { + return errors.ConfigError{Field: "Conn", Reason: "must be set"} + } + if c.Outbound == nil { + c.Outbound = &defaultOutbound{} + } + if c.BandwidthConfig.MaxTx != 0 && c.BandwidthConfig.MaxTx < 65536 { + return errors.ConfigError{Field: "BandwidthConfig.MaxTx", Reason: "must be at least 65536"} + } + if c.BandwidthConfig.MaxRx != 0 && c.BandwidthConfig.MaxRx < 65536 { + return errors.ConfigError{Field: "BandwidthConfig.MaxRx", Reason: "must be at least 65536"} + } + if c.Authenticator == nil { + return errors.ConfigError{Field: "Authenticator", Reason: "must be set"} + } + return nil +} + +// TLSConfig contains the TLS configuration fields that we want to expose to the user. +type TLSConfig struct { + Certificates []tls.Certificate + GetCertificate func(info *tls.ClientHelloInfo) (*tls.Certificate, error) +} + +// QUICConfig contains the QUIC configuration fields that we want to expose to the user. +type QUICConfig struct { + InitialStreamReceiveWindow uint64 + MaxStreamReceiveWindow uint64 + InitialConnectionReceiveWindow uint64 + MaxConnectionReceiveWindow uint64 + MaxIdleTimeout time.Duration + MaxIncomingStreams int64 + DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms. +} + +// Outbound provides the implementation of how the server should connect to remote servers. +type Outbound interface { + DialTCP(reqAddr string) (net.Conn, error) + ListenUDP() (UDPConn, error) +} + +// UDPConn is like net.PacketConn, but uses string for addresses. +type UDPConn interface { + ReadFrom(b []byte) (int, string, error) + WriteTo(b []byte, addr string) (int, error) + Close() error +} + +type defaultOutbound struct{} + +func (o *defaultOutbound) DialTCP(reqAddr string) (net.Conn, error) { + return net.Dial("tcp", reqAddr) +} + +func (o *defaultOutbound) ListenUDP() (UDPConn, error) { + conn, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, err + } + return &defaultUDPConn{conn}, nil +} + +type defaultUDPConn struct { + *net.UDPConn +} + +func (c *defaultUDPConn) ReadFrom(b []byte) (int, string, error) { + n, addr, err := c.UDPConn.ReadFrom(b) + if addr != nil { + return n, addr.String(), err + } else { + return n, "", err + } +} + +func (c *defaultUDPConn) WriteTo(b []byte, addr string) (int, error) { + uAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { + return 0, err + } + return c.UDPConn.WriteTo(b, uAddr) +} + +// BandwidthConfig describes the maximum bandwidth that the server can use, in bytes per second. +type BandwidthConfig struct { + MaxTx uint64 + MaxRx uint64 +} + +// Authenticator is an interface that provides authentication logic. +type Authenticator interface { + Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) +} + +// EventLogger is an interface that provides logging logic. +type EventLogger interface { + Connect(addr net.Addr, id string, tx uint64) + Disconnect(addr net.Addr, id string, err error) + TCPRequest(addr net.Addr, id, reqAddr string) + TCPError(addr net.Addr, id, reqAddr string, err error) + UDPRequest(addr net.Addr, id string, sessionID uint32) + UDPError(addr net.Addr, id string, sessionID uint32, err error) +} diff --git a/core/server/server.go b/core/server/server.go new file mode 100644 index 0000000..01b4da3 --- /dev/null +++ b/core/server/server.go @@ -0,0 +1,386 @@ +package server + +import ( + "context" + "crypto/tls" + "errors" + "io" + "math/rand" + "net/http" + "sync" + + "github.com/apernet/hysteria/core/internal/congestion" + "github.com/apernet/hysteria/core/internal/frag" + "github.com/apernet/hysteria/core/internal/protocol" + "github.com/apernet/hysteria/core/internal/utils" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" +) + +type Server interface { + Serve() error + Close() error +} + +func NewServer(config *Config) (Server, error) { + if err := config.fill(); err != nil { + return nil, err + } + tlsConfig := http3.ConfigureTLSConfig(&tls.Config{ + Certificates: config.TLSConfig.Certificates, + GetCertificate: config.TLSConfig.GetCertificate, + }) + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: config.QUICConfig.InitialStreamReceiveWindow, + MaxStreamReceiveWindow: config.QUICConfig.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: config.QUICConfig.InitialConnectionReceiveWindow, + MaxConnectionReceiveWindow: config.QUICConfig.MaxConnectionReceiveWindow, + MaxIdleTimeout: config.QUICConfig.MaxIdleTimeout, + MaxIncomingStreams: config.QUICConfig.MaxIncomingStreams, + DisablePathMTUDiscovery: config.QUICConfig.DisablePathMTUDiscovery, + EnableDatagrams: true, + } + listener, err := quic.Listen(config.Conn, tlsConfig, quicConfig) + if err != nil { + _ = config.Conn.Close() + return nil, err + } + return &serverImpl{ + config: config, + listener: listener, + }, nil +} + +type serverImpl struct { + config *Config + listener quic.Listener +} + +func (s *serverImpl) Serve() error { + for { + conn, err := s.listener.Accept(context.Background()) + if err != nil { + return err + } + go s.handleClient(conn) + } +} + +func (s *serverImpl) Close() error { + err := s.listener.Close() + _ = s.config.Conn.Close() + return err +} + +func (s *serverImpl) handleClient(conn quic.Connection) { + handler := newH3sHandler(s.config, conn) + h3s := http3.Server{ + EnableDatagrams: true, + Handler: handler, + StreamHijacker: handler.ProxyStreamHijacker, + } + err := h3s.ServeQUICConn(conn) + // If the client is authenticated, we need to log the disconnect event + if handler.authenticated && s.config.EventLogger != nil { + s.config.EventLogger.Disconnect(conn.RemoteAddr(), handler.authID, err) + } + _ = conn.CloseWithError(0, "") +} + +type h3sHandler struct { + config *Config + conn quic.Connection + + authenticated bool + authID string + + udpOnce sync.Once + udpSM udpSessionManager +} + +func newH3sHandler(config *Config, conn quic.Connection) *h3sHandler { + return &h3sHandler{ + config: config, + conn: conn, + udpSM: udpSessionManager{ + listenFunc: config.Outbound.ListenUDP, + m: make(map[uint32]*udpSessionEntry), + }, + } +} + +type udpSessionEntry struct { + Conn UDPConn + D *frag.Defragger + Closed bool +} + +type udpSessionManager struct { + listenFunc func() (UDPConn, error) + mutex sync.RWMutex + m map[uint32]*udpSessionEntry + nextID uint32 +} + +// Add returns the session ID, the UDP connection and a function to close the UDP connection & delete the session. +func (m *udpSessionManager) Add() (uint32, UDPConn, func(), error) { + conn, err := m.listenFunc() + if err != nil { + return 0, nil, nil, err + } + + m.mutex.Lock() + defer m.mutex.Unlock() + id := m.nextID + m.nextID++ + entry := &udpSessionEntry{ + Conn: conn, + D: &frag.Defragger{}, + Closed: false, + } + m.m[id] = entry + + return id, conn, func() { + m.mutex.Lock() + defer m.mutex.Unlock() + if entry.Closed { + // Already closed + return + } + entry.Closed = true + _ = conn.Close() + delete(m.m, id) + }, nil +} + +func (m *udpSessionManager) Feed(msg *protocol.UDPMessage) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + entry, ok := m.m[msg.SessionID] + if !ok { + // No such session, drop the message + return + } + dfMsg := entry.D.Feed(msg) + if dfMsg == nil { + // Not a complete message yet + return + } + _, _ = entry.Conn.WriteTo(dfMsg.Data, dfMsg.Addr) +} + +func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost && r.Host == protocol.URLHost && r.URL.Path == protocol.URLPath { + if h.authenticated { + // Already authenticated + protocol.AuthResponseDataToHeader(w.Header(), h.config.BandwidthConfig.MaxRx) + w.WriteHeader(protocol.StatusAuthOK) + return + } + auth, clientRx := protocol.AuthRequestDataFromHeader(r.Header) + // actualTx = min(serverTx, clientRx) + actualTx := clientRx + if h.config.BandwidthConfig.MaxTx > 0 && actualTx > h.config.BandwidthConfig.MaxTx { + actualTx = h.config.BandwidthConfig.MaxTx + } + ok, id := h.config.Authenticator.Authenticate(h.conn.RemoteAddr(), auth, actualTx) + if ok { + // Set authenticated flag + h.authenticated = true + h.authID = id + // Update congestion control when applicable + if actualTx > 0 { + h.conn.SetCongestionControl(congestion.NewBrutalSender(actualTx)) + } + // Auth OK, send response + protocol.AuthResponseDataToHeader(w.Header(), h.config.BandwidthConfig.MaxRx) + w.WriteHeader(protocol.StatusAuthOK) + // Call event logger + if h.config.EventLogger != nil { + h.config.EventLogger.Connect(h.conn.RemoteAddr(), id, actualTx) + } + // Start UDP loop if UDP is not disabled + // We use sync.Once to make sure that only one goroutine is started, + // as ServeHTTP may be called by multiple goroutines simultaneously + if !h.config.DisableUDP { + h.udpOnce.Do(func() { + go h.udpLoop() + }) + } + } else { + // Auth failed, pretend to be a normal HTTP server + h.masqHandler(w, r) + } + } else { + // Not an auth request, pretend to be a normal HTTP server + h.masqHandler(w, r) + } +} + +func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, conn quic.Connection, stream quic.Stream, err error) (bool, error) { + if err != nil || !h.authenticated { + return false, nil + } + + // Wraps the stream with QStream, which handles Close() properly + stream = &utils.QStream{Stream: stream} + + switch ft { + case protocol.FrameTypeTCPRequest: + go h.handleTCPRequest(stream) + return true, nil + case protocol.FrameTypeUDPRequest: + go h.handleUDPRequest(stream) + return true, nil + default: + return false, nil + } +} + +func (h *h3sHandler) handleTCPRequest(stream quic.Stream) { + // Read request + reqAddr, err := protocol.ReadTCPRequest(stream) + if err != nil { + _ = stream.Close() + return + } + // Log the event + if h.config.EventLogger != nil { + h.config.EventLogger.TCPRequest(h.conn.RemoteAddr(), h.authID, reqAddr) + } + // Dial target + tConn, err := h.config.Outbound.DialTCP(reqAddr) + if err != nil { + _ = protocol.WriteTCPResponse(stream, false, err.Error()) + _ = stream.Close() + // Log the error + if h.config.EventLogger != nil { + h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err) + } + return + } + _ = protocol.WriteTCPResponse(stream, true, "") + // Start proxying + copyErrChan := make(chan error, 2) + go func() { + _, err := io.Copy(tConn, stream) + copyErrChan <- err + }() + go func() { + _, err := io.Copy(stream, tConn) + copyErrChan <- err + }() + // Block until one of the copy goroutines exits + err = <-copyErrChan + if h.config.EventLogger != nil { + h.config.EventLogger.TCPError(h.conn.RemoteAddr(), h.authID, reqAddr, err) + } + // Cleanup + _ = tConn.Close() + _ = stream.Close() +} + +func (h *h3sHandler) handleUDPRequest(stream quic.Stream) { + if h.config.DisableUDP { + // UDP is disabled, send error message and close the stream + _ = protocol.WriteUDPResponse(stream, false, 0, "UDP is disabled on this server") + _ = stream.Close() + return + } + // Add to session manager + sessionID, conn, connCloseFunc, err := h.udpSM.Add() + if err != nil { + _ = protocol.WriteUDPResponse(stream, false, 0, err.Error()) + _ = stream.Close() + return + } + // Send response + _ = protocol.WriteUDPResponse(stream, true, sessionID, "") + // Call event logger + if h.config.EventLogger != nil { + h.config.EventLogger.UDPRequest(h.conn.RemoteAddr(), h.authID, sessionID) + } + + // client <- remote direction + go func() { + udpBuf := make([]byte, protocol.MaxUDPSize) + msgBuf := make([]byte, protocol.MaxUDPSize) + for { + udpN, rAddr, err := conn.ReadFrom(udpBuf) + if udpN > 0 { + // Try no frag first + msg := protocol.UDPMessage{ + SessionID: sessionID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: rAddr, + Data: udpBuf[:udpN], + } + msgN := msg.Serialize(msgBuf) + if msgN < 0 { + // Message even larger than MaxUDPSize, drop it + continue + } + sendErr := h.conn.SendMessage(msgBuf[:msgN]) + var errTooLarge quic.ErrMessageTooLarge + if errors.As(sendErr, &errTooLarge) { + // Message too large, try fragmentation + msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1 + fMsgs := frag.FragUDPMessage(msg, int(errTooLarge)) + for _, fMsg := range fMsgs { + msgN = fMsg.Serialize(msgBuf) + _ = h.conn.SendMessage(msgBuf[:msgN]) + } + } + } + if err != nil { + break + } + } + connCloseFunc() + _ = stream.Close() + }() + + // Hold (drain) the stream until the client closes it. + // Closing the stream is the signal to stop the UDP session. + _, err = io.Copy(io.Discard, stream) + // Call event logger + if h.config.EventLogger != nil { + h.config.EventLogger.UDPError(h.conn.RemoteAddr(), h.authID, sessionID, err) + } + + // Cleanup + connCloseFunc() + _ = stream.Close() +} + +func (h *h3sHandler) udpLoop() { + for { + msg, err := h.conn.ReceiveMessage() + if err != nil { + return + } + h.handleUDPMessage(msg) + } +} + +// client -> remote direction +func (h *h3sHandler) handleUDPMessage(msg []byte) { + udpMsg, err := protocol.ParseUDPMessage(msg) + if err != nil { + return + } + h.udpSM.Feed(udpMsg) +} + +func (h *h3sHandler) masqHandler(w http.ResponseWriter, r *http.Request) { + if h.config.MasqHandler != nil { + h.config.MasqHandler.ServeHTTP(w, r) + } else { + // Return 404 for everything + http.NotFound(w, r) + } +} diff --git a/core/sockopt/sockopt.go b/core/sockopt/sockopt.go deleted file mode 100644 index db97810..0000000 --- a/core/sockopt/sockopt.go +++ /dev/null @@ -1,23 +0,0 @@ -package sockopt - -import ( - "net" - "syscall" -) - -// https://github.com/v2fly/v2ray-core/blob/4e247840821f3dd326722d4db02ee3c237074fc2/transport/internet/config.pb.go#L420-L426 - -func BindDialer(d *net.Dialer, intf *net.Interface) { - d.Control = func(network, address string, c syscall.RawConn) error { - return bindRawConn(network, c, intf) - } -} - -func BindUDPConn(network string, conn *net.UDPConn, intf *net.Interface) error { - c, err := conn.SyscallConn() - if err != nil { - return err - } - - return bindRawConn(network, c, intf) -} diff --git a/core/sockopt/sockopt_linux.go b/core/sockopt/sockopt_linux.go deleted file mode 100644 index e7df7a8..0000000 --- a/core/sockopt/sockopt_linux.go +++ /dev/null @@ -1,22 +0,0 @@ -package sockopt - -import ( - "net" - "syscall" - - "golang.org/x/sys/unix" -) - -func bindRawConn(network string, c syscall.RawConn, bindIface *net.Interface) error { - var err1, err2 error - err1 = c.Control(func(fd uintptr) { - if bindIface != nil { - err2 = unix.BindToDevice(int(fd), bindIface.Name) - } - }) - if err1 != nil { - return err1 - } else { - return err2 - } -} diff --git a/core/sockopt/sockopt_others.go b/core/sockopt/sockopt_others.go deleted file mode 100644 index af0a107..0000000 --- a/core/sockopt/sockopt_others.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !linux - -package sockopt - -import ( - "errors" - "net" - "syscall" -) - -func bindRawConn(network string, c syscall.RawConn, bindIface *net.Interface) error { - return errors.New("binding interface is not supported on the current system") -} diff --git a/core/transport/client.go b/core/transport/client.go deleted file mode 100644 index 49ab83e..0000000 --- a/core/transport/client.go +++ /dev/null @@ -1,34 +0,0 @@ -package transport - -import ( - "net" - "time" -) - -type ClientTransport struct { - Dialer *net.Dialer - ResolvePreference ResolvePreference -} - -var DefaultClientTransport = &ClientTransport{ - Dialer: &net.Dialer{ - Timeout: 8 * time.Second, - }, - ResolvePreference: ResolvePreferenceDefault, -} - -func (ct *ClientTransport) ResolveIPAddr(address string) (*net.IPAddr, error) { - return resolveIPAddrWithPreference(address, ct.ResolvePreference) -} - -func (ct *ClientTransport) DialTCP(raddr *net.TCPAddr) (*net.TCPConn, error) { - conn, err := ct.Dialer.Dial("tcp", raddr.String()) - if err != nil { - return nil, err - } - return conn.(*net.TCPConn), nil -} - -func (ct *ClientTransport) ListenUDP() (*net.UDPConn, error) { - return net.ListenUDP("udp", nil) -} diff --git a/core/transport/resolve.go b/core/transport/resolve.go deleted file mode 100644 index 601dd07..0000000 --- a/core/transport/resolve.go +++ /dev/null @@ -1,98 +0,0 @@ -package transport - -import ( - "context" - "errors" - "fmt" - "net" - "time" -) - -type ResolvePreference int - -const ( - ResolvePreferenceDefault = ResolvePreference(iota) - ResolvePreferenceIPv4 - ResolvePreferenceIPv6 - ResolvePreferenceIPv4OrIPv6 - ResolvePreferenceIPv6OrIPv4 - - ResolveTimeout = 8 * time.Second -) - -var ( - errNoIPv4Addr = errors.New("no IPv4 address") - errNoIPv6Addr = errors.New("no IPv6 address") - errNoAddr = errors.New("no address") -) - -func resolveIPAddrWithPreference(host string, pref ResolvePreference) (*net.IPAddr, error) { - if pref == ResolvePreferenceDefault { - return net.ResolveIPAddr("ip", host) - } - ctx, cancel := context.WithTimeout(context.Background(), ResolveTimeout) - ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) - cancel() - if err != nil { - return nil, err - } - var ip4, ip6 *net.IPAddr - for i := range ips { - ip := &ips[i] - is4 := ip.IP.To4() != nil - if ip4 == nil && is4 { - ip4 = ip - } else if ip6 == nil && !is4 { - ip6 = ip - } - if ip4 != nil && ip6 != nil { - break - } - } - switch pref { - case ResolvePreferenceIPv4: - if ip4 == nil { - return nil, errNoIPv4Addr - } - return ip4, nil - case ResolvePreferenceIPv6: - if ip6 == nil { - return nil, errNoIPv6Addr - } - return ip6, nil - case ResolvePreferenceIPv4OrIPv6: - if ip4 == nil { - if ip6 == nil { - return nil, errNoAddr - } else { - return ip6, nil - } - } - return ip4, nil - case ResolvePreferenceIPv6OrIPv4: - if ip6 == nil { - if ip4 == nil { - return nil, errNoAddr - } else { - return ip4, nil - } - } - return ip6, nil - } - return nil, errNoAddr -} - -func ResolvePreferenceFromString(preference string) (ResolvePreference, error) { - switch preference { - case "4": - return ResolvePreferenceIPv4, nil - case "6": - return ResolvePreferenceIPv6, nil - case "46": - return ResolvePreferenceIPv4OrIPv6, nil - case "64": - return ResolvePreferenceIPv6OrIPv4, nil - default: - return ResolvePreferenceDefault, fmt.Errorf("invalid preference: %s", preference) - } -} diff --git a/core/transport/server.go b/core/transport/server.go deleted file mode 100644 index 3b9f79c..0000000 --- a/core/transport/server.go +++ /dev/null @@ -1,128 +0,0 @@ -package transport - -import ( - "net" - "strconv" - "time" - - "github.com/apernet/hysteria/core/sockopt" - "github.com/apernet/hysteria/core/utils" -) - -type ServerTransport struct { - Dialer *net.Dialer - SOCKS5Client *SOCKS5Client - ResolvePreference ResolvePreference - LocalUDPAddr *net.UDPAddr - LocalUDPIntf *net.Interface -} - -// AddrEx is like net.TCPAddr or net.UDPAddr, but with additional domain information for SOCKS5. -// At least one of Domain and IPAddr must be non-empty. -type AddrEx struct { - Domain string - IPAddr *net.IPAddr - Port int -} - -func (a *AddrEx) String() string { - if a == nil { - return "" - } - var ip string - if a.IPAddr != nil { - ip = a.IPAddr.String() - } - return net.JoinHostPort(ip, strconv.Itoa(a.Port)) -} - -type STPacketConn interface { - ReadFrom([]byte) (int, *net.UDPAddr, error) - WriteTo([]byte, *AddrEx) (int, error) - Close() error -} - -type udpSTPacketConn struct { - Conn *net.UDPConn -} - -func (c *udpSTPacketConn) ReadFrom(bytes []byte) (int, *net.UDPAddr, error) { - return c.Conn.ReadFromUDP(bytes) -} - -func (c *udpSTPacketConn) WriteTo(bytes []byte, ex *AddrEx) (int, error) { - return c.Conn.WriteToUDP(bytes, &net.UDPAddr{ - IP: ex.IPAddr.IP, - Port: ex.Port, - Zone: ex.IPAddr.Zone, - }) -} - -func (c *udpSTPacketConn) Close() error { - return c.Conn.Close() -} - -var DefaultServerTransport = &ServerTransport{ - Dialer: &net.Dialer{ - Timeout: 8 * time.Second, - }, - ResolvePreference: ResolvePreferenceDefault, -} - -func (st *ServerTransport) ParseIPAddr(address string) (*net.IPAddr, bool) { - ip, zone := utils.ParseIPZone(address) - if ip != nil { - return &net.IPAddr{IP: ip, Zone: zone}, false - } - return nil, true -} - -func (st *ServerTransport) ResolveIPAddr(address string) (*net.IPAddr, bool, error) { - ip, isDomain := st.ParseIPAddr(address) - if !isDomain { - return ip, false, nil - } - ipAddr, err := resolveIPAddrWithPreference(address, st.ResolvePreference) - return ipAddr, true, err -} - -func (st *ServerTransport) DialTCP(raddr *AddrEx) (*net.TCPConn, error) { - if st.SOCKS5Client != nil { - conn, err := st.SOCKS5Client.DialTCP(raddr) - if err != nil { - return nil, err - } - return conn.(*net.TCPConn), nil - } else { - conn, err := st.Dialer.Dial("tcp", raddr.String()) - if err != nil { - return nil, err - } - return conn.(*net.TCPConn), nil - } -} - -func (st *ServerTransport) ListenUDP() (STPacketConn, error) { - if st.SOCKS5Client != nil { - return st.SOCKS5Client.ListenUDP() - } else { - conn, err := net.ListenUDP("udp", st.LocalUDPAddr) - if err != nil { - return nil, err - } - if st.LocalUDPIntf != nil { - err = sockopt.BindUDPConn("udp", conn, st.LocalUDPIntf) - if err != nil { - _ = conn.Close() - return nil, err - } - } - return &udpSTPacketConn{ - Conn: conn, - }, nil - } -} - -func (st *ServerTransport) ProxyEnabled() bool { - return st.SOCKS5Client != nil -} diff --git a/core/transport/socks5.go b/core/transport/socks5.go deleted file mode 100644 index 43d5c6b..0000000 --- a/core/transport/socks5.go +++ /dev/null @@ -1,279 +0,0 @@ -package transport - -import ( - "encoding/binary" - "errors" - "fmt" - "net" - "time" - - "github.com/txthinking/socks5" -) - -const ( - negTimeout = 8 * time.Second -) - -type SOCKS5Client struct { - Dialer *net.Dialer - ServerAddr string - Username string - Password string -} - -func NewSOCKS5Client(serverAddr string, username string, password string) *SOCKS5Client { - return &SOCKS5Client{ - Dialer: &net.Dialer{ - Timeout: 8 * time.Second, - }, - ServerAddr: serverAddr, - Username: username, - Password: password, - } -} - -func (c *SOCKS5Client) negotiate(conn net.Conn) error { - m := []byte{socks5.MethodNone} - if c.Username != "" && c.Password != "" { - m = append(m, socks5.MethodUsernamePassword) - } - rq := socks5.NewNegotiationRequest(m) - _, err := rq.WriteTo(conn) - if err != nil { - return err - } - rs, err := socks5.NewNegotiationReplyFrom(conn) - if err != nil { - return err - } - if rs.Method == socks5.MethodUsernamePassword { - urq := socks5.NewUserPassNegotiationRequest([]byte(c.Username), []byte(c.Password)) - _, err = urq.WriteTo(conn) - if err != nil { - return err - } - urs, err := socks5.NewUserPassNegotiationReplyFrom(conn) - if err != nil { - return err - } - if urs.Status != socks5.UserPassStatusSuccess { - return errors.New("username or password error") - } - } else if rs.Method != socks5.MethodNone { - return errors.New("unsupported auth method") - } - return nil -} - -func (c *SOCKS5Client) request(conn net.Conn, r *socks5.Request) (*socks5.Reply, error) { - if _, err := r.WriteTo(conn); err != nil { - return nil, err - } - reply, err := socks5.NewReplyFrom(conn) - if err != nil { - return nil, err - } - return reply, nil -} - -func (c *SOCKS5Client) DialTCP(raddr *AddrEx) (net.Conn, error) { - conn, err := c.Dialer.Dial("tcp", c.ServerAddr) - if err != nil { - return nil, err - } - if err := conn.SetDeadline(time.Now().Add(negTimeout)); err != nil { - _ = conn.Close() - return nil, err - } - err = c.negotiate(conn) - if err != nil { - _ = conn.Close() - return nil, err - } - atyp, addr, port, err := addrExToSOCKS5Addr(raddr) - if err != nil { - _ = conn.Close() - return nil, err - } - r := socks5.NewRequest(socks5.CmdConnect, atyp, addr, port) - reply, err := c.request(conn, r) - if err != nil { - _ = conn.Close() - return nil, err - } - if reply.Rep != socks5.RepSuccess { - _ = conn.Close() - return nil, fmt.Errorf("request failed: %d", reply.Rep) - } - // Negotiation succeed, disable timeout - if err := conn.SetDeadline(time.Time{}); err != nil { - _ = conn.Close() - return nil, err - } - return conn, nil -} - -func (c *SOCKS5Client) ListenUDP() (STPacketConn, error) { - conn, err := c.Dialer.Dial("tcp", c.ServerAddr) - if err != nil { - return nil, err - } - if err := conn.SetDeadline(time.Now().Add(negTimeout)); err != nil { - _ = conn.Close() - return nil, err - } - err = c.negotiate(conn) - if err != nil { - _ = conn.Close() - return nil, err - } - var zeroIPv4 [4]byte - var zeroPort [2]byte - r := socks5.NewRequest(socks5.CmdUDP, socks5.ATYPIPv4, zeroIPv4[:], zeroPort[:]) - reply, err := c.request(conn, r) - if err != nil { - _ = conn.Close() - return nil, err - } - if reply.Rep != socks5.RepSuccess { - _ = conn.Close() - return nil, fmt.Errorf("request failed: %d", reply.Rep) - } - // Negotiation succeed, disable timeout - if err := conn.SetDeadline(time.Time{}); err != nil { - _ = conn.Close() - return nil, err - } - udpRelayAddr, err := socks5AddrToUDPAddr(reply.Atyp, reply.BndAddr, reply.BndPort) - if err != nil { - _ = conn.Close() - return nil, err - } - udpConn, err := c.Dialer.Dial("udp", udpRelayAddr.String()) - if err != nil { - _ = conn.Close() - return nil, err - } - sc := &socks5UDPConn{ - tcpConn: conn, - udpConn: udpConn, - } - go sc.hold() - return sc, nil -} - -type socks5UDPConn struct { - tcpConn net.Conn - udpConn net.Conn -} - -func (c *socks5UDPConn) hold() { - buf := make([]byte, 1024) - for { - _, err := c.tcpConn.Read(buf) - if err != nil { - break - } - } - _ = c.tcpConn.Close() - _ = c.udpConn.Close() -} - -func (c *socks5UDPConn) ReadFrom(b []byte) (int, *net.UDPAddr, error) { - n, err := c.udpConn.Read(b) - if err != nil { - return 0, nil, err - } - d, err := socks5.NewDatagramFromBytes(b[:n]) - if err != nil { - return 0, nil, err - } - addr, err := socks5AddrToUDPAddr(d.Atyp, d.DstAddr, d.DstPort) - if err != nil { - return 0, nil, err - } - n = copy(b, d.Data) - return n, addr, nil -} - -func (c *socks5UDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { - atyp, dstAddr, dstPort, err := addrExToSOCKS5Addr(addr) - if err != nil { - return 0, err - } - d := socks5.NewDatagram(atyp, dstAddr, dstPort, b) - _, err = c.udpConn.Write(d.Bytes()) - if err != nil { - return 0, err - } - return len(b), nil -} - -func (c *socks5UDPConn) Close() error { - _ = c.tcpConn.Close() - _ = c.udpConn.Close() - return nil -} - -func socks5AddrToUDPAddr(atyp byte, addr []byte, port []byte) (*net.UDPAddr, error) { - clone := func(b []byte) []byte { - c := make([]byte, len(b)) - copy(c, b) - return c - } - iPort := int(binary.BigEndian.Uint16(port)) - switch atyp { - case socks5.ATYPIPv4: - if len(addr) != 4 { - return nil, errors.New("invalid ipv4 address") - } - return &net.UDPAddr{ - IP: clone(addr), - Port: iPort, - }, nil - case socks5.ATYPIPv6: - if len(addr) != 16 { - return nil, errors.New("invalid ipv6 address") - } - return &net.UDPAddr{ - IP: clone(addr), - Port: iPort, - }, nil - case socks5.ATYPDomain: - if len(addr) <= 1 { - return nil, errors.New("invalid domain address") - } - ipAddr, err := net.ResolveIPAddr("ip", string(addr[1:])) - if err != nil { - return nil, err - } - return &net.UDPAddr{ - IP: ipAddr.IP, - Port: iPort, - Zone: ipAddr.Zone, - }, nil - default: - return nil, errors.New("unsupported address type") - } -} - -func addrExToSOCKS5Addr(addr *AddrEx) (byte, []byte, []byte, error) { - sport := make([]byte, 2) - binary.BigEndian.PutUint16(sport, uint16(addr.Port)) - if len(addr.Domain) > 0 { - return socks5.ATYPDomain, []byte(addr.Domain), sport, nil - } else { - var atyp byte - var saddr []byte - if ip4 := addr.IPAddr.IP.To4(); ip4 != nil { - atyp = socks5.ATYPIPv4 - saddr = ip4 - } else if ip6 := addr.IPAddr.IP.To16(); ip6 != nil { - atyp = socks5.ATYPIPv6 - saddr = ip6 - } else { - return 0, nil, nil, errors.New("unsupported address type") - } - return atyp, saddr, sport, nil - } -} diff --git a/core/utils/misc.go b/core/utils/misc.go deleted file mode 100644 index 29c7cf0..0000000 --- a/core/utils/misc.go +++ /dev/null @@ -1,42 +0,0 @@ -package utils - -import ( - "net" - "strconv" -) - -func SplitHostPort(hostport string) (string, uint16, error) { - host, port, err := net.SplitHostPort(hostport) - if err != nil { - return "", 0, err - } - portUint, err := strconv.ParseUint(port, 10, 16) - if err != nil { - return "", 0, err - } - return host, uint16(portUint), err -} - -func ParseIPZone(s string) (net.IP, string) { - s, zone := splitHostZone(s) - return net.ParseIP(s), zone -} - -func splitHostZone(s string) (host, zone string) { - if i := last(s, '%'); i > 0 { - host, zone = s[:i], s[i+1:] - } else { - host = s - } - return -} - -func last(s string, b byte) int { - i := len(s) - for i--; i >= 0; i-- { - if s[i] == b { - break - } - } - return i -} diff --git a/core/utils/pipe.go b/core/utils/pipe.go deleted file mode 100644 index 55c95ee..0000000 --- a/core/utils/pipe.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "io" - "net" - "time" -) - -const PipeBufferSize = 32 * 1024 - -func Pipe(src, dst io.ReadWriter, count func(int)) error { - buf := make([]byte, PipeBufferSize) - for { - rn, err := src.Read(buf) - if rn > 0 { - if count != nil { - count(rn) - } - _, err := dst.Write(buf[:rn]) - if err != nil { - return err - } - } - if err != nil { - return err - } - } -} - -// count: positive numbers for rw1 to rw2, negative numbers for rw2 to re1 -func Pipe2Way(rw1, rw2 io.ReadWriter, count func(int)) error { - errChan := make(chan error, 2) - go func() { - var revCount func(int) - if count != nil { - revCount = func(i int) { - count(-i) - } - } - errChan <- Pipe(rw2, rw1, revCount) - }() - go func() { - errChan <- Pipe(rw1, rw2, count) - }() - // We only need the first error - return <-errChan -} - -func PipePairWithTimeout(conn net.Conn, 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 -} diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 72cbdc8..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,10 +0,0 @@ -version: '3.9' -services: - hysteria: - image: tobyxdd/hysteria - container_name: hysteria - restart: always - network_mode: "host" - volumes: - - ./hysteria.json:/etc/hysteria.json - command: ["server", "--config", "/etc/hysteria.json"] diff --git a/docs/bench/bench.png b/docs/bench/bench.png deleted file mode 100644 index eeb2842ef209214629555bec38a4d136f5b9359d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12521 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⁣ho@yV~wNzSs-V#t2d$lQpdqqaZY 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 C90NK4 diff --git a/docs/logos/whitebg_black.png b/docs/logos/whitebg_black.png deleted file mode 100644 index 29e5a948d7692c3d5f23ebd7f0e91a4119f86545..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110656 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

T+KEE>-iL*>TL|Fd> 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 >ySMU~R7N>< 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 ~F8 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 CTdE=e diff --git a/extras/auth/password.go b/extras/auth/password.go new file mode 100644 index 0000000..00fd826 --- /dev/null +++ b/extras/auth/password.go @@ -0,0 +1,22 @@ +package auth + +import ( + "net" + + "github.com/apernet/hysteria/core/server" +) + +var _ server.Authenticator = &PasswordAuthenticator{} + +// PasswordAuthenticator is a simple authenticator that checks the password against a single string. +type PasswordAuthenticator struct { + Password string +} + +func (a *PasswordAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + if auth == a.Password { + return true, "user" + } else { + return false, "" + } +} diff --git a/extras/go.mod b/extras/go.mod new file mode 100644 index 0000000..c841665 --- /dev/null +++ b/extras/go.mod @@ -0,0 +1,27 @@ +module github.com/apernet/hysteria/extras + +go 1.20 + +require github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000 + +require ( + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/onsi/ginkgo/v2 v2.2.0 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qtls-go1-19 v0.3.2 // indirect + github.com/quic-go/qtls-go1-20 v0.2.2 // indirect + github.com/quic-go/quic-go v0.0.0-00010101000000-000000000000 // indirect + golang.org/x/crypto v0.4.0 // indirect + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect + golang.org/x/mod v0.7.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.3.0 // indirect +) + +replace github.com/quic-go/quic-go => github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 + +replace github.com/apernet/hysteria/core => ../core diff --git a/extras/go.sum b/extras/go.sum new file mode 100644 index 0000000..e61296f --- /dev/null +++ b/extras/go.sum @@ -0,0 +1,73 @@ +github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473 h1:3KFetJ/lUFn0m9xTFg+rMmz2nyHg+D2boJX0Rp4OF6c= +github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= +github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= +github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/extras/utils/bpsconv.go b/extras/utils/bpsconv.go new file mode 100644 index 0000000..9147ef2 --- /dev/null +++ b/extras/utils/bpsconv.go @@ -0,0 +1,52 @@ +package utils + +import ( + "errors" + "strconv" + "strings" +) + +const ( + Byte = 1.0 << (10 * iota) + Kilobyte + Megabyte + Gigabyte + Terabyte +) + +// StringToBps converts a string to a bandwidth value in bytes per second. +// E.g. "100 Mbps", "512 kbps", "1g" are all valid. +func StringToBps(s string) (uint64, error) { + s = strings.ToLower(strings.TrimSpace(s)) + spl := 0 + for i, c := range s { + if c < '0' || c > '9' { + spl = i + break + } + } + if spl == 0 { + // No unit or no value + return 0, errors.New("invalid format") + } + v, err := strconv.ParseUint(s[:spl], 10, 64) + if err != nil { + return 0, err + } + unit := strings.TrimSpace(s[spl:]) + + switch strings.ToLower(unit) { + case "b", "bps": + return v * Byte / 8, nil + case "k", "kb", "kbps": + return v * Kilobyte / 8, nil + case "m", "mb", "mbps": + return v * Megabyte / 8, nil + case "g", "gb", "gbps": + return v * Gigabyte / 8, nil + case "t", "tb", "tbps": + return v * Terabyte / 8, nil + default: + return 0, errors.New("unsupported unit") + } +} diff --git a/extras/utils/bpsconv_test.go b/extras/utils/bpsconv_test.go new file mode 100644 index 0000000..2de4d3d --- /dev/null +++ b/extras/utils/bpsconv_test.go @@ -0,0 +1,40 @@ +package utils + +import "testing" + +func TestStringToBps(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want uint64 + wantErr bool + }{ + {"bps", args{"800 bps"}, 100, false}, + {"kbps", args{"800 kbps"}, 102400, false}, + {"mbps", args{"800 mbps"}, 104857600, false}, + {"gbps", args{"800 gbps"}, 107374182400, false}, + {"tbps", args{"800 tbps"}, 109951162777600, false}, + {"mbps simp", args{"100m"}, 13107200, false}, + {"gbps simp upper", args{"2G"}, 268435456, false}, + {"invalid 1", args{"damn"}, 0, true}, + {"invalid 2", args{"6444"}, 0, true}, + {"invalid 3", args{"5.4 mbps"}, 0, true}, + {"invalid 4", args{"kbps"}, 0, true}, + {"invalid 5", args{"1234 5678 gbps"}, 0, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := StringToBps(tt.args.s) + if (err != nil) != tt.wantErr { + t.Errorf("StringToBps() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("StringToBps() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.work b/go.work index 368d244..4dd2b2f 100644 --- a/go.work +++ b/go.work @@ -3,4 +3,5 @@ go 1.20 use ( ./app ./core + ./extras ) diff --git a/go.work.sum b/go.work.sum index d92176a..a8b39af 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,33 +1,12 @@ -github.com/apernet/quic-go v0.34.1-0.20230507231629-ec008b7e8473/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g= -github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= -github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= -github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= -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/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gvisor.dev/gvisor v0.0.0-20220722234115-e3e6499abbba h1:XCAVnJl9nmC1CC4g5ycIXDeqiLHiz3n/5zH1ZKLOxDM= -gvisor.dev/gvisor v0.0.0-20220722234115-e3e6499abbba/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/install_server.sh b/install_server.sh deleted file mode 100755 index ecda629..0000000 --- a/install_server.sh +++ /dev/null @@ -1,1006 +0,0 @@ -#!/usr/bin/env bash -# -# install_server.sh - hysteria server install script -# Try `install_server.sh --help` for usage. -# -# SPDX-License-Identifier: MIT -# Copyright (c) 2022 Aperture Internet Laboratory -# - -set -e - - -### -# SCRIPT CONFIGURATION -### - -# Basename of this script -SCRIPT_NAME="$(basename "$0")" - -# Command line arguments of this script -SCRIPT_ARGS=("$@") - -# Path for installing executable -EXECUTABLE_INSTALL_PATH="/usr/local/bin/hysteria" - -# Paths to install systemd files -SYSTEMD_SERVICES_DIR="/etc/systemd/system" - -# Directory to store hysteria config file -CONFIG_DIR="/etc/hysteria" - -# URLs of GitHub -REPO_URL="https://github.com/apernet/hysteria" -API_BASE_URL="https://api.github.com/repos/apernet/hysteria" - -# curl command line flags. -# To using a proxy, please specify ALL_PROXY in the environ variable, such like: -# export ALL_PROXY=socks5h://192.0.2.1:1080 -CURL_FLAGS=(-L -f -q --retry 5 --retry-delay 10 --retry-max-time 60) - - -### -# AUTO DETECTED GLOBAL VARIABLE -### - -# Package manager -PACKAGE_MANAGEMENT_INSTALL="${PACKAGE_MANAGEMENT_INSTALL:-}" - -# Operating System of current machine, supported: linux -OPERATING_SYSTEM="${OPERATING_SYSTEM:-}" - -# Architecture of current machine, supported: 386, amd64, arm, arm64, mipsle, s390x -ARCHITECTURE="${ARCHITECTURE:-}" - -# User for running hysteria -HYSTERIA_USER="${HYSTERIA_USER:-}" - -# Directory for ACME certificates storage -HYSTERIA_HOME_DIR="${HYSTERIA_HOME_DIR:-}" - - -### -# ARGUMENTS -### - -# Supported operation: install, remove, check_update -OPERATION= - -# User specified version to install -VERSION= - -# Force install even if installed -FORCE= - -# User specified binary to install -LOCAL_FILE= - - -### -# COMMAND REPLACEMENT & UTILITIES -### - -has_command() { - local _command=$1 - - type -P "$_command" > /dev/null 2>&1 -} - -curl() { - command curl "${CURL_FLAGS[@]}" "$@" -} - -mktemp() { - command mktemp "$@" "hyservinst.XXXXXXXXXX" -} - -tput() { - if has_command tput; then - command tput "$@" - fi -} - -tred() { - tput setaf 1 -} - -tgreen() { - tput setaf 2 -} - -tyellow() { - tput setaf 3 -} - -tblue() { - tput setaf 4 -} - -taoi() { - tput setaf 6 -} - -tbold() { - tput bold -} - -treset() { - tput sgr0 -} - -note() { - local _msg="$1" - - echo -e "$SCRIPT_NAME: $(tbold)note: $_msg$(treset)" -} - -warning() { - local _msg="$1" - - echo -e "$SCRIPT_NAME: $(tyellow)warning: $_msg$(treset)" -} - -error() { - local _msg="$1" - - echo -e "$SCRIPT_NAME: $(tred)error: $_msg$(treset)" -} - -has_prefix() { - local _s="$1" - local _prefix="$2" - - if [[ -z "$_prefix" ]]; then - return 0 - fi - - if [[ -z "$_s" ]]; then - return 1 - fi - - [[ "x$_s" != "x${_s#"$_prefix"}" ]] -} - -systemctl() { - if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]] || ! has_command systemctl; then - warning "Ignored systemd command: systemctl $@" - return - fi - - command systemctl "$@" -} - -show_argument_error_and_exit() { - local _error_msg="$1" - - error "$_error_msg" - echo "Try \"$0 --help\" for the usage." >&2 - exit 22 -} - -install_content() { - local _install_flags="$1" - local _content="$2" - local _destination="$3" - - local _tmpfile="$(mktemp)" - - echo -ne "Install $_destination ... " - echo "$_content" > "$_tmpfile" - if install "$_install_flags" "$_tmpfile" "$_destination"; then - echo -e "ok" - fi - - rm -f "$_tmpfile" -} - -remove_file() { - local _target="$1" - - echo -ne "Remove $_target ... " - if rm "$_target"; then - echo -e "ok" - fi -} - -exec_sudo() { - # exec sudo with configurable environ preserved. - local _saved_ifs="$IFS" - IFS=$'\n' - local _preserved_env=( - $(env | grep "^PACKAGE_MANAGEMENT_INSTALL=" || true) - $(env | grep "^OPERATING_SYSTEM=" || true) - $(env | grep "^ARCHITECTURE=" || true) - $(env | grep "^HYSTERIA_\w*=" || true) - $(env | grep "^FORCE_\w*=" || true) - ) - IFS="$_saved_ifs" - - exec sudo env \ - "${_preserved_env[@]}" \ - "$@" -} - -detect_package_manager() { - if [[ -n "$PACKAGE_MANAGEMENT_INSTALL" ]]; then - return 0 - fi - - if has_command apt; then - PACKAGE_MANAGEMENT_INSTALL='apt -y --no-install-recommends install' - return 0 - fi - - if has_command dnf; then - PACKAGE_MANAGEMENT_INSTALL='dnf -y install' - return 0 - fi - - if has_command yum; then - PACKAGE_MANAGEMENT_INSTALL='yum -y install' - return 0 - fi - - if has_command zypper; then - PACKAGE_MANAGEMENT_INSTALL='zypper install -y --no-recommends' - return 0 - fi - - if has_command pacman; then - PACKAGE_MANAGEMENT_INSTALL='pacman -Syu --noconfirm' - return 0 - fi - - return 1 -} - -install_software() { - local _package_name="$1" - - if ! detect_package_manager; then - error "Supported package manager is not detected, please install the following package manually:" - echo - echo -e "\t* $_package_name" - echo - exit 65 - fi - - echo "Installing missing dependence '$_package_name' with '$PACKAGE_MANAGEMENT_INSTALL' ... " - if $PACKAGE_MANAGEMENT_INSTALL "$_package_name"; then - echo "ok" - else - error "Cannot install '$_package_name' with detected package manager, please install it manually." - exit 65 - fi -} - -is_user_exists() { - local _user="$1" - - id "$_user" > /dev/null 2>&1 -} - -check_permission() { - if [[ "$UID" -eq '0' ]]; then - return - fi - - note "The user currently executing this script is not root." - - case "$FORCE_NO_ROOT" in - '1') - warning "FORCE_NO_ROOT=1 is specified, we will process without root and you may encounter the insufficient privilege error." - ;; - *) - if has_command sudo; then - note "Re-running this script with sudo, you can also specify FORCE_NO_ROOT=1 to force this script running with current user." - exec_sudo "$0" "${SCRIPT_ARGS[@]}" - else - error "Please run this script with root or specify FORCE_NO_ROOT=1 to force this script running with current user." - exit 13 - fi - ;; - esac -} - -check_environment_operating_system() { - if [[ -n "$OPERATING_SYSTEM" ]]; then - warning "OPERATING_SYSTEM=$OPERATING_SYSTEM is specified, opreating system detection will not be perform." - return - fi - - if [[ "x$(uname)" == "xLinux" ]]; then - OPERATING_SYSTEM=linux - return - fi - - error "This script only supports Linux." - note "Specify OPERATING_SYSTEM=[linux|darwin|freebsd|windows] to bypass this check and force this script running on this $(uname)." - exit 95 -} - -check_environment_architecture() { - if [[ -n "$ARCHITECTURE" ]]; then - warning "ARCHITECTURE=$ARCHITECTURE is specified, architecture detection will not be performed." - return - fi - - case "$(uname -m)" in - 'i386' | 'i686') - ARCHITECTURE='386' - ;; - 'amd64' | 'x86_64') - ARCHITECTURE='amd64' - ;; - 'armv5tel' | 'armv6l' | 'armv7' | 'armv7l') - ARCHITECTURE='arm' - ;; - 'armv8' | 'aarch64') - ARCHITECTURE='arm64' - ;; - 'mips' | 'mipsle' | 'mips64' | 'mips64le') - ARCHITECTURE='mipsle' - ;; - 's390x') - ARCHITECTURE='s390x' - ;; - *) - error "The architecture '$(uname -a)' is not supported." - note "Specify ARCHITECTURE= to bypass this check and force this script running on this $(uname -m)." - exit 8 - ;; - esac -} - -check_environment_systemd() { - if [[ -d "/run/systemd/system" ]] || grep -q systemd <(ls -l /sbin/init); then - return - fi - - case "$FORCE_NO_SYSTEMD" in - '1') - warning "FORCE_NO_SYSTEMD=1 is specified, we will process as normal even if systemd is not detected by us." - ;; - '2') - warning "FORCE_NO_SYSTEMD=2 is specified, we will process but all systemd related command will not be executed." - ;; - *) - error "This script only supports Linux distributions with systemd." - note "Specify FORCE_NO_SYSTEMD=1 to disable this check and force this script running as systemd is detected." - note "Specify FORCE_NO_SYSTEMD=2 to disable this check along with all systemd related commands." - ;; - esac -} - -check_environment_curl() { - if has_command curl; then - return - fi - - install_software curl -} - -check_environment_grep() { - if has_command grep; then - return - fi - - install_software grep -} - -check_environment() { - check_environment_operating_system - check_environment_architecture - check_environment_systemd - check_environment_curl - check_environment_grep -} - -vercmp_segment() { - local _lhs="$1" - local _rhs="$2" - - if [[ "x$_lhs" == "x$_rhs" ]]; then - echo 0 - return - fi - if [[ -z "$_lhs" ]]; then - echo -1 - return - fi - if [[ -z "$_rhs" ]]; then - echo 1 - return - fi - - local _lhs_num="${_lhs//[A-Za-z]*/}" - local _rhs_num="${_rhs//[A-Za-z]*/}" - - if [[ "x$_lhs_num" == "x$_rhs_num" ]]; then - echo 0 - return - fi - if [[ -z "$_lhs_num" ]]; then - echo -1 - return - fi - if [[ -z "$_rhs_num" ]]; then - echo 1 - return - fi - local _numcmp=$(($_lhs_num - $_rhs_num)) - if [[ "$_numcmp" -ne 0 ]]; then - echo "$_numcmp" - return - fi - - local _lhs_suffix="${_lhs#"$_lhs_num"}" - local _rhs_suffix="${_rhs#"$_rhs_num"}" - - if [[ "x$_lhs_suffix" == "x$_rhs_suffix" ]]; then - echo 0 - return - fi - if [[ -z "$_lhs_suffix" ]]; then - echo 1 - return - fi - if [[ -z "$_rhs_suffix" ]]; then - echo -1 - return - fi - if [[ "$_lhs_suffix" < "$_rhs_suffix" ]]; then - echo -1 - return - fi - echo 1 -} - -vercmp() { - local _lhs=${1#v} - local _rhs=${2#v} - - while [[ -n "$_lhs" && -n "$_rhs" ]]; do - local _clhs="${_lhs/.*/}" - local _crhs="${_rhs/.*/}" - - local _segcmp="$(vercmp_segment "$_clhs" "$_crhs")" - if [[ "$_segcmp" -ne 0 ]]; then - echo "$_segcmp" - return - fi - - _lhs="${_lhs#"$_clhs"}" - _lhs="${_lhs#.}" - _rhs="${_rhs#"$_crhs"}" - _rhs="${_rhs#.}" - done - - if [[ "x$_lhs" == "x$_rhs" ]]; then - echo 0 - return - fi - - if [[ -z "$_lhs" ]]; then - echo -1 - return - fi - - if [[ -z "$_rhs" ]]; then - echo 1 - return - fi - - return -} - -check_hysteria_user() { - local _default_hysteria_user="$1" - - if [[ -n "$HYSTERIA_USER" ]]; then - return - fi - - if [[ ! -e "$SYSTEMD_SERVICES_DIR/hysteria-server.service" ]]; then - HYSTERIA_USER="$_default_hysteria_user" - return - fi - - HYSTERIA_USER="$(grep -o '^User=\w*' "$SYSTEMD_SERVICES_DIR/hysteria-server.service" | tail -1 | cut -d '=' -f 2 || true)" - - if [[ -z "$HYSTERIA_USER" ]]; then - HYSTERIA_USER="$_default_hysteria_user" - fi -} - -check_hysteria_homedir() { - local _default_hysteria_homedir="$1" - - if [[ -n "$HYSTERIA_HOME_DIR" ]]; then - return - fi - - if ! is_user_exists "$HYSTERIA_USER"; then - HYSTERIA_HOME_DIR="$_default_hysteria_homedir" - return - fi - - HYSTERIA_HOME_DIR="$(eval echo ~"$HYSTERIA_USER")" -} - - -### -# ARGUMENTS PARSER -### - -show_usage_and_exit() { - echo - echo -e "\t$(tbold)$SCRIPT_NAME$(treset) - hysteria server install script" - echo - echo -e "Usage:" - echo - echo -e "$(tbold)Install hysteria$(treset)" - echo -e "\t$0 [ -f | -l | --version ]" - echo -e "Flags:" - echo -e "\t-f, --force\tForce re-install latest or specified version even if it has been installed." - echo -e "\t-l, --local \tInstall specified hysteria binary instead of download it." - echo -e "\t--version \tInstall specified version instead of the latest." - echo - echo -e "$(tbold)Remove hysteria$(treset)" - echo -e "\t$0 --remove" - echo - echo -e "$(tbold)Check for the update$(treset)" - echo -e "\t$0 -c" - echo -e "\t$0 --check" - echo - echo -e "$(tbold)Show this help$(treset)" - echo -e "\t$0 -h" - echo -e "\t$0 --help" - exit 0 -} - -parse_arguments() { - while [[ "$#" -gt '0' ]]; do - case "$1" in - '--remove') - if [[ -n "$OPERATION" && "$OPERATION" != 'remove' ]]; then - show_argument_error_and_exit "Option '--remove' is conflicted with other options." - fi - OPERATION='remove' - ;; - '--version') - VERSION="$2" - if [[ -z "$VERSION" ]]; then - show_argument_error_and_exit "Please specify the version for option '--version'." - fi - shift - if ! has_prefix "$VERSION" 'v'; then - show_argument_error_and_exit "Version numbers should begin with 'v' (such like 'v1.3.1'), got '$VERSION'" - fi - ;; - '-c' | '--check') - if [[ -n "$OPERATION" && "$OPERATION" != 'check' ]]; then - show_argument_error_and_exit "Option '-c' or '--check' is conflicted with other option." - fi - OPERATION='check_update' - ;; - '-f' | '--force') - FORCE='1' - ;; - '-h' | '--help') - show_usage_and_exit - ;; - '-l' | '--local') - LOCAL_FILE="$2" - if [[ -z "$LOCAL_FILE" ]]; then - show_argument_error_and_exit "Please specify the local binary to install for option '-l' or '--local'." - fi - break - ;; - *) - show_argument_error_and_exit "Unknown option '$1'" - ;; - esac - shift - done - - if [[ -z "$OPERATION" ]]; then - OPERATION='install' - fi - - # validate arguments - case "$OPERATION" in - 'install') - if [[ -n "$VERSION" && -n "$LOCAL_FILE" ]]; then - show_argument_error_and_exit '--version and --local cannot be specified together.' - fi - ;; - *) - if [[ -n "$VERSION" ]]; then - show_argument_error_and_exit "--version is only avaiable when install." - fi - if [[ -n "$LOCAL_FILE" ]]; then - show_argument_error_and_exit "--local is only avaiable when install." - fi - ;; - esac -} - - -### -# FILE TEMPLATES -### - -# /etc/systemd/system/hysteria-server.service -tpl_hysteria_server_service_base() { - local _config_name="$1" - - cat << EOF -[Unit] -Description=Hysteria Server Service (${_config_name}.json) -After=network.target - -[Service] -Type=simple -ExecStart=$EXECUTABLE_INSTALL_PATH -config ${_config_name}.json server -WorkingDirectory=$CONFIG_DIR -User=$HYSTERIA_USER -Group=$HYSTERIA_USER -Environment=HYSTERIA_LOG_LEVEL=info -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW -NoNewPrivileges=true - -[Install] -WantedBy=multi-user.target -EOF -} - -# /etc/systemd/system/hysteria-server.service -tpl_hysteria_server_service() { - tpl_hysteria_server_service_base 'config' -} - -# /etc/systemd/system/hysteria-server@.service -tpl_hysteria_server_x_service() { - tpl_hysteria_server_service_base '%i' -} - -# /etc/hysteria/config.json -tpl_etc_hysteria_config_json() { - cat << EOF -{ - "listen": ":36712", - "acme": { - "domains": [ - "your.domain.com" - ], - "email": "your@email.com" - }, - "obfs": "8ZuA2Zpqhuk8yakXvMjDqEXBwY" -} -EOF -} - - -### -# SYSTEMD -### - -get_running_services() { - if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then - return - fi - - systemctl list-units --state=active --plain --no-legend \ - | grep -o "hysteria-server@*[^\s]*.service" || true -} - -restart_running_services() { - if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then - return - fi - - echo "Restarting running service ... " - - for service in $(get_running_services); do - echo -ne "Restarting $service ... " - systemctl restart "$service" - echo "done" - done -} - -stop_running_services() { - if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then - return - fi - - echo "Stopping running service ... " - - for service in $(get_running_services); do - echo -ne "Stopping $service ... " - systemctl stop "$service" - echo "done" - done -} - - -### -# HYSTERIA & GITHUB API -### - -is_hysteria_installed() { - # RETURN VALUE - # 0: hysteria is installed - # 1: hysteria is not installed - - if [[ -f "$EXECUTABLE_INSTALL_PATH" || -h "$EXECUTABLE_INSTALL_PATH" ]]; then - return 0 - fi - return 1 -} - -get_installed_version() { - if is_hysteria_installed; then - "$EXECUTABLE_INSTALL_PATH" -v | cut -d ' ' -f 3 - fi -} - -get_latest_version() { - if [[ -n "$VERSION" ]]; then - echo "$VERSION" - return - fi - - local _tmpfile=$(mktemp) - if ! curl -sS -H 'Accept: application/vnd.github.v3+json' "$API_BASE_URL/releases/latest" -o "$_tmpfile"; then - error "Failed to get latest release, please check your network." - exit 11 - fi - - local _latest_version=$(grep 'tag_name' "$_tmpfile" | head -1 | grep -o '"v.*"') - _latest_version=${_latest_version#'"'} - _latest_version=${_latest_version%'"'} - - if [[ -n "$_latest_version" ]]; then - echo "$_latest_version" - fi - - rm -f "$_tmpfile" -} - -download_hysteria() { - local _version="$1" - local _destination="$2" - - local _download_url="$REPO_URL/releases/download/$_version/hysteria-$OPERATING_SYSTEM-$ARCHITECTURE" - echo "Downloading hysteria archive: $_download_url ..." - if ! curl -R -H 'Cache-Control: no-cache' "$_download_url" -o "$_destination"; then - error "Download failed! Please check your network and try again." - return 11 - fi - return 0 -} - -check_update() { - # RETURN VALUE - # 0: update available - # 1: installed version is latest - - echo -ne "Checking for installed version ... " - local _installed_version="$(get_installed_version)" - if [[ -n "$_installed_version" ]]; then - echo "$_installed_version" - else - echo "not installed" - fi - - echo -ne "Checking for latest version ... " - local _latest_version="$(get_latest_version)" - if [[ -n "$_latest_version" ]]; then - echo "$_latest_version" - VERSION="$_latest_version" - else - echo "failed" - return 1 - fi - - local _vercmp="$(vercmp "$_installed_version" "$_latest_version")" - if [[ "$_vercmp" -lt 0 ]]; then - return 0 - fi - - return 1 -} - - -### -# ENTRY -### - -perform_install_hysteria_binary() { - if [[ -n "$LOCAL_FILE" ]]; then - note "Performing local install: $LOCAL_FILE" - - echo -ne "Installing hysteria executable ... " - - if install -Dm755 "$LOCAL_FILE" "$EXECUTABLE_INSTALL_PATH"; then - echo "ok" - else - exit 2 - fi - - return - fi - - local _tmpfile=$(mktemp) - - if ! download_hysteria "$VERSION" "$_tmpfile"; then - rm -f "$_tmpfile" - exit 11 - fi - - echo -ne "Installing hysteria executable ... " - - if install -Dm755 "$_tmpfile" "$EXECUTABLE_INSTALL_PATH"; then - echo "ok" - else - exit 13 - fi - - rm -f "$_tmpfile" -} - -perform_remove_hysteria_binary() { - remove_file "$EXECUTABLE_INSTALL_PATH" -} - -perform_install_hysteria_example_config() { - if [[ ! -d "$CONFIG_DIR" ]]; then - install_content -Dm644 "$(tpl_etc_hysteria_config_json)" "$CONFIG_DIR/config.json" - fi -} - -perform_install_hysteria_systemd() { - if [[ "x$FORCE_NO_SYSTEMD" == "x2" ]]; then - return - fi - - install_content -Dm644 "$(tpl_hysteria_server_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server.service" - install_content -Dm644 "$(tpl_hysteria_server_x_service)" "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" - - systemctl daemon-reload -} - -perform_remove_hysteria_systemd() { - remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server.service" - remove_file "$SYSTEMD_SERVICES_DIR/hysteria-server@.service" - - systemctl daemon-reload -} - -perform_install_hysteria_home_legacy() { - if ! is_user_exists "$HYSTERIA_USER"; then - echo -ne "Creating user $HYSTERIA_USER ... " - useradd -r -d "$HYSTERIA_HOME_DIR" -m "$HYSTERIA_USER" - echo "ok" - fi -} - -perform_install() { - local _is_frash_install - if ! is_hysteria_installed; then - _is_frash_install=1 - fi - - local _is_update_required - - if [[ -n "$LOCAL_FILE" ]] || [[ -n "$VERSION" ]] || check_update; then - _is_update_required=1 - fi - - if [[ "x$FORCE" == "x1" ]]; then - if [[ -z "$_is_update_required" ]]; then - note "Option '--force' is specified, re-install even if installed version is the latest." - fi - _is_update_required=1 - fi - - if [[ -z "$_is_update_required" ]]; then - echo "$(tgreen)Installed version is up-to-dated, there is nothing to do.$(treset)" - return - fi - - perform_install_hysteria_binary - perform_install_hysteria_example_config - perform_install_hysteria_home_legacy - perform_install_hysteria_systemd - - if [[ -n "$_is_frash_install" ]]; then - echo - echo -e "$(tbold)Congratulation! Hysteria has been successfully installed on your server.$(treset)" - echo - echo -e "What's next?" - echo - echo -e "\t+ Check out the latest quick start guide at $(tblue)https://hysteria.network/docs/quick-start/$(treset)" - echo -e "\t+ Edit server config file at $(tred)$CONFIG_DIR/config.json$(treset)" - echo -e "\t+ Start your hysteria server with $(tred)systemctl start hysteria-server.service$(treset)" - echo -e "\t+ Configure hysteria start on system boot with $(tred)systemctl enable hysteria-server.service$(treset)" - echo - else - restart_running_services - - echo - echo -e "$(tbold)Hysteria has been successfully update to $VERSION.$(treset)" - echo - echo -e "Check out the latest changelog $(tblue)https://github.com/apernet/hysteria/blob/master/CHANGELOG.md$(treset)" - echo - fi -} - -perform_remove() { - perform_remove_hysteria_binary - stop_running_services - perform_remove_hysteria_systemd - - echo - echo -e "$(tbold)Congratulation! Hysteria has been successfully removed from your server.$(treset)" - echo - echo -e "You still need to remove configuration files and ACME certificates manually with the following commands:" - echo - echo -e "\t$(tred)rm -rf "$CONFIG_DIR"$(treset)" - if [[ "x$HYSTERIA_USER" != "xroot" ]]; then - echo -e "\t$(tred)userdel -r "$HYSTERIA_USER"$(treset)" - fi - if [[ "x$FORCE_NO_SYSTEMD" != "x2" ]]; then - echo - echo -e "You still might need to disable all related systemd services with the following commands:" - echo - echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server.service$(treset)" - echo -e "\t$(tred)rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server@*.service$(treset)" - echo -e "\t$(tred)systemctl daemon-reload$(treset)" - fi - echo -} - -perform_check_update() { - if check_update; then - echo - echo -e "$(tbold)Update available: $VERSION$(treset)" - echo - echo -e "$(tgreen)You can download and install the latest version by execute this script without any arguments.$(treset)" - echo - else - echo - echo "$(tgreen)Installed version is up-to-dated.$(treset)" - echo - fi -} - -main() { - parse_arguments "$@" - - check_permission - check_environment - check_hysteria_user "hysteria" - check_hysteria_homedir "/var/lib/$HYSTERIA_USER" - - case "$OPERATION" in - "install") - perform_install - ;; - "remove") - perform_remove - ;; - "check_update") - perform_check_update - ;; - *) - error "Unknown operation '$OPERATION'." - ;; - esac -} - -main "$@" - -# vim:set ft=bash ts=2 sw=2 sts=2 et: diff --git a/tag.ps1 b/tag.ps1 deleted file mode 100644 index 076dd8d..0000000 --- a/tag.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -# Release tagging script for Windows (PowerShell) - -# Usage: -# ./tag.ps1 - -if (!(Get-Command git -ErrorAction SilentlyContinue)) { - Write-Host "Error: git is not installed." -ForegroundColor Red - exit 1 -} -if (!(git rev-parse --is-inside-work-tree 2>$null)) { - Write-Host "Error: not in a git repository." -ForegroundColor Red - exit 1 -} - -if ($args.Length -eq 0) { - Write-Host "Error: no version argument given." -ForegroundColor Red - exit 1 -} -if ($args[0] -notmatch "^[v]?[0-9]+\.[0-9]+\.[0-9]+$") { - Write-Host "Error: invalid version argument." -ForegroundColor Red - exit 1 -} -if ($args[0] -notmatch "^[v]") { - $args[0] = "v" + $args[0] -} - -$version = $args[0] -$tags = @($version, "app/$version", "core/$version") - -foreach ($tag in $tags) { - Write-Host "Tagging $tag..." - git tag $tag -} - -Write-Host "Done." -ForegroundColor Green diff --git a/tag.sh b/tag.sh deleted file mode 100644 index 76b9473..0000000 --- a/tag.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# Release tagging script for Linux - -# Usage: -# ./tag.sh - -if ! [ -x "$(command -v git)" ]; then - echo 'Error: git is not installed.' >&2 - exit 1 -fi -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo 'Error: not in a git repository.' >&2 - exit 1 -fi - -if [ "$#" -eq 0 ]; then - echo "Error: no version argument given." >&2 - exit 1 -fi -if ! [[ $1 =~ ^[v]?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: invalid version argument." >&2 - exit 1 -fi -if ! [[ $1 =~ ^[v] ]]; then - version="v$1" -else - version="$1" -fi - -tags=($version "app/$version" "core/$version") - -for tag in "${tags[@]}"; do - echo "Tagging $tag..." - git tag "$tag" -done - -echo "Done."