Compare commits

..

No commits in common. "master" and "app/v2.0.1" have entirely different histories.

176 changed files with 2215 additions and 46077 deletions

View file

@ -1,26 +0,0 @@
---
name: Bug report
about: Report anything you think is a bug and needs to be fixed.
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
Attach logs from the client/server when the error occurs.
**Device and Operating System**
What are you using it on.
**Additional context**
Add any other context about the problem here.

View file

@ -1,26 +0,0 @@
---
name: Bug 反馈
about: 反馈任何你认为是 bug 需要修复的问题。
title: ''
labels: bug
assignees: ''
---
**描述问题**
请尽量清晰精准地描述你遇到的问题。
**如何复现**
复现问题的步骤。
**预期行为**
你认为修复后的行为应该是怎样的。
**日志**
附上客户端/服务器端在错误发生前后的日志。
**设备和操作系统**
你在用什么设备和操作系统。
**额外信息**
其他你认为有助于解决问题的信息。

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project.
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,20 +0,0 @@
---
name: 功能请求
about: 为这个项目提出改进意见。
title: ''
labels: enhancement
assignees: ''
---
**你的功能请求是否与某个问题有关?**
请尽量清晰精准地描述你遇到的问题。例如:我家运营商限制 UDP 协议速度,导致 Hysteria 很慢,希望增加 FakeTCP 支持。
**描述你希望的解决方案**
请尽量清晰精准地描述你希望的解决方案。
**有没有其他替代方案**
请尽量清晰精准地描述你认为可能的替代方案。
**额外信息**
其他你认为有助于开发者了解你需求的信息。

View file

@ -1,104 +0,0 @@
name: "Create release tags for nested modules"
on:
push:
tags:
- app/v*.*.*
permissions:
contents: write
jobs:
tag:
name: "Create tags"
runs-on: ubuntu-latest
steps:
- name: "Extract tagbase"
id: extract_tagbase
uses: actions/github-script@v7
with:
script: |
const ref = context.ref;
core.info(`context.ref: ${ref}`);
const refPrefix = 'refs/tags/app/';
if (!ref.startsWith(refPrefix)) {
core.setFailed(`context.ref does not start with ${refPrefix}: ${ref}`);
return;
}
const tagbase = ref.slice(refPrefix.length);
core.info(`tagbase: ${tagbase}`);
core.setOutput('tagbase', tagbase);
- name: "Tagging core/*"
uses: actions/github-script@v7
env:
INPUT_TAGPREFIX: "core/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}
- name: "Tagging extras/*"
uses: actions/github-script@v7
env:
INPUT_TAGPREFIX: "extras/"
INPUT_TAGBASE: ${{ steps.extract_tagbase.outputs.tagbase }}
with:
script: |
const tagbase = core.getInput('tagbase', { required: true });
const tagprefix = core.getInput('tagprefix', { required: true });
const refname = `tags/${tagprefix}${tagbase}`;
core.info(`creating ref ${refname}`);
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/${refname}`,
sha: context.sha
});
core.info(`created ref ${refname}`);
return;
} catch (error) {
core.info(`failed to create ref ${refname}: ${error}`);
}
core.info(`updating ref ${refname}`)
try {
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: refname,
sha: context.sha
});
core.info(`updated ref ${refname}`);
return;
} catch (error) {
core.setFailed(`failed to update ref ${refname}: ${error}`);
}

View file

@ -20,20 +20,20 @@ jobs:
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v4
with:
context: .
push: true

View file

@ -17,24 +17,16 @@ jobs:
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v4
with:
go-version: "1.24"
go-version: "1.21"
- name: Setup Python # This is for the build script
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: "3.11"
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26b
add-to-path: false
- name: Run build script
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
python hyperbole.py build -r
@ -46,7 +38,7 @@ jobs:
done
- name: Archive
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: hysteria-master-${{ github.sha }}
path: build

View file

@ -16,29 +16,17 @@ jobs:
- name: Check out
uses: actions/checkout@v4
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always --match 'app/v*' | sed -n 's|app/\([^/-]*\)\(-.*\)\{0,1\}|\1|p')" >> $GITHUB_OUTPUT
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@v4
with:
go-version: "1.24"
go-version: "1.21"
- name: Setup Python # This is for the build script
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: "3.11"
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26b
add-to-path: false
- name: Run build script
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
export HY_APP_PLATFORMS=$(sed 's/\r$//' platforms.txt | awk '!/^#/ && !/^$/' | paste -sd ",")
python hyperbole.py build -r
@ -49,21 +37,11 @@ jobs:
sha256sum $file >> build/hashes.txt
done
- name: Upload GitHub
uses: softprops/action-gh-release@v2
- name: Upload
uses: softprops/action-gh-release@v1
with:
files: build/*
- name: Upload CF bucket
uses: shallwefootball/upload-s3-action@v1.3.3
with:
aws_key_id: ${{ secrets.CF_KEY_ID }}
aws_secret_access_key: ${{ secrets.CF_KEY }}
aws_bucket: "hydownload"
endpoint: "https://bea223c61d5a41250d127bd67f51dfec.r2.cloudflarestorage.com/"
source_dir: "build"
destination_dir: "app/${{ steps.get_version.outputs.version }}"
- name: Publish to API
run: |
export HY_API_POST_KEY=${{ secrets.HY2_API_POST_KEY }}

View file

@ -66,7 +66,7 @@ After (and only after) a client passes authentication, the server MUST consider
### TCP
For each TCP connection, the client MUST create a new QUIC bidirectional stream and send the following TCPRequest message:
For each TCP connection, the client MUST create a new QUIC unidirectional stream and send the following TCPRequest message:
```
[varint] 0x401 (TCPRequest ID)

View file

@ -23,23 +23,23 @@
<div class="feature-grid">
<div>
<h3>🛠️ Jack of all trades</h3>
<p>Wide range of modes including SOCKS5, HTTP Proxy, TCP/UDP Forwarding, Linux TProxy, TUN - with more features being added constantly.</p>
<h3>🛠️ Packed to the gills</h3>
<p>Expansive range of modes including SOCKS5, HTTP proxy, TCP/UDP forwarding, Linux TProxy - not to mention additional features continually being added.</p>
</div>
<div>
<h3>Blazing fast</h3>
<p>Powered by a customized QUIC protocol, Hysteria is designed to deliver unparalleled performance over unreliable and lossy networks.</p>
<h3>Lightning fast</h3>
<p>Powered by a custom QUIC protocol, Hysteria delivers unparalleled performance over even the most unreliable and lossy networks.</p>
</div>
<div>
<h3>✊ Censorship resistant</h3>
<p>The protocol masquerades as standard HTTP/3 traffic, making it very difficult for censors to detect and block without widespread collateral damage.</p>
<p>Our protocol is designed to masquerade as standard HTTP/3 traffic, making it very difficult to detect and block without widespread collateral damage.</p>
</div>
<div>
<h3>💻 Cross-platform</h3>
<p>We have builds for every major platform and architecture. Deploy anywhere & use everywhere. Not to mention the long list of 3rd party apps.</p>
<p>We have builds for all major platforms and architectures. Deploy anywhere & use everywhere.</p>
</div>
<div>
@ -48,8 +48,8 @@
</div>
<div>
<h3>🤗 Chill and supportive</h3>
<p>We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.</p>
<h3>🤗 Open standards</h3>
<p>We have well-documented specifications and code for developers to contribute and build their own apps.</p>
</div>
</div>

View file

@ -1,7 +0,0 @@
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -5,36 +5,26 @@ import (
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
"os"
"os/signal"
"runtime"
"slices"
"strconv"
"strings"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/app/v2/internal/forwarding"
"github.com/apernet/hysteria/app/v2/internal/http"
"github.com/apernet/hysteria/app/v2/internal/proxymux"
"github.com/apernet/hysteria/app/v2/internal/redirect"
"github.com/apernet/hysteria/app/v2/internal/sockopts"
"github.com/apernet/hysteria/app/v2/internal/socks5"
"github.com/apernet/hysteria/app/v2/internal/tproxy"
"github.com/apernet/hysteria/app/v2/internal/tun"
"github.com/apernet/hysteria/app/v2/internal/url"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/extras/v2/correctnet"
"github.com/apernet/hysteria/extras/v2/obfs"
"github.com/apernet/hysteria/extras/v2/transport/udphop"
"github.com/apernet/hysteria/app/internal/forwarding"
"github.com/apernet/hysteria/app/internal/http"
"github.com/apernet/hysteria/app/internal/redirect"
"github.com/apernet/hysteria/app/internal/socks5"
"github.com/apernet/hysteria/app/internal/tproxy"
"github.com/apernet/hysteria/app/internal/url"
"github.com/apernet/hysteria/app/internal/utils"
"github.com/apernet/hysteria/core/client"
"github.com/apernet/hysteria/extras/obfs"
"github.com/apernet/hysteria/extras/transport/udphop"
)
// Client flags
@ -74,7 +64,6 @@ type clientConfig struct {
TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"`
UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"`
TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"`
TUN *tunConfig `mapstructure:"tun"`
}
type clientConfigTransportUDP struct {
@ -103,20 +92,13 @@ type clientConfigTLS struct {
}
type clientConfigQUIC struct {
InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"`
MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"`
MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"`
KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"`
DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"`
Sockopts clientConfigQUICSockopts `mapstructure:"sockopts"`
}
type clientConfigQUICSockopts struct {
BindInterface *string `mapstructure:"bindInterface"`
FirewallMark *uint32 `mapstructure:"fwmark"`
FdControlUnixSocket *string `mapstructure:"fdControlUnixSocket"`
InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"`
MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"`
InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"`
MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"`
MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"`
KeepAlivePeriod time.Duration `mapstructure:"keepAlivePeriod"`
DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"`
}
type clientConfigBandwidth struct {
@ -162,23 +144,6 @@ type tcpRedirectConfig struct {
Listen string `mapstructure:"listen"`
}
type tunConfig struct {
Name string `mapstructure:"name"`
MTU uint32 `mapstructure:"mtu"`
Timeout time.Duration `mapstructure:"timeout"`
Address struct {
IPv4 string `mapstructure:"ipv4"`
IPv6 string `mapstructure:"ipv6"`
} `mapstructure:"address"`
Route *struct {
Strict bool `mapstructure:"strict"`
IPv4 []string `mapstructure:"ipv4"`
IPv6 []string `mapstructure:"ipv6"`
IPv4Exclude []string `mapstructure:"ipv4Exclude"`
IPv6Exclude []string `mapstructure:"ipv6Exclude"`
} `mapstructure:"route"`
}
func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error {
if c.Server == "" {
return configError{Field: "server", Err: errors.New("server address is empty")}
@ -206,21 +171,6 @@ func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error {
// fillConnFactory must be called after fillServerAddr, as we have different logic
// for ConnFactory depending on whether we have a port hopping address.
func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
so := &sockopts.SocketOptions{
BindInterface: c.QUIC.Sockopts.BindInterface,
FirewallMark: c.QUIC.Sockopts.FirewallMark,
FdControlUnixSocket: c.QUIC.Sockopts.FdControlUnixSocket,
}
if err := so.CheckSupported(); err != nil {
var unsupportedErr *sockopts.UnsupportedError
if errors.As(err, &unsupportedErr) {
return configError{
Field: "quic.sockopts." + unsupportedErr.Field,
Err: errors.New("unsupported on this platform"),
}
}
return configError{Field: "quic.sockopts", Err: err}
}
// Inner PacketConn
var newFunc func(addr net.Addr) (net.PacketConn, error)
switch strings.ToLower(c.Transport.Type) {
@ -228,11 +178,11 @@ func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
if hyConfig.ServerAddr.Network() == "udphop" {
hopAddr := hyConfig.ServerAddr.(*udphop.UDPHopAddr)
newFunc = func(addr net.Addr) (net.PacketConn, error) {
return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval, so.ListenUDP)
return udphop.NewUDPHopPacketConn(hopAddr, c.Transport.UDP.HopInterval)
}
} else {
newFunc = func(addr net.Addr) (net.PacketConn, error) {
return so.ListenUDP()
return net.ListenUDP("udp", nil)
}
}
default:
@ -393,11 +343,7 @@ func (c *clientConfig) parseURI() bool {
return false
}
if u.User != nil {
auth, err := url.QueryUnescape(u.User.String())
if err != nil {
return false
}
c.Auth = auth
c.Auth = u.User.String()
}
c.Server = u.Host
q := u.Query()
@ -451,29 +397,29 @@ func runClient(cmd *cobra.Command, args []string) {
if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
hyConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
c, err := client.NewReconnectableClient(
config.Config,
func(c client.Client, info *client.HandshakeInfo, count int) {
connectLog(info, count)
// On the client side, we start checking for updates after we successfully connect
// to the server, which, depending on whether lazy mode is enabled, may or may not
// be immediately after the client starts. We don't want the update check request
// to interfere with the lazy mode option.
if count == 1 && !disableUpdateCheck {
go runCheckUpdateClient(c)
}
}, config.Lazy)
c, err := client.NewReconnectableClient(hyConfig, func(c client.Client, count int) {
connectLog(count)
// On the client side, we start checking for updates after we successfully connect
// to the server, which, depending on whether lazy mode is enabled, may or may not
// be immediately after the client starts. We don't want the update check request
// to interfere with the lazy mode option.
if count == 1 && !disableUpdateCheck {
go runCheckUpdateClient(c)
}
}, config.Lazy)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
uri := config.URI()
logger.Info("use this URI to share your server", zap.String("uri", uri))
if showQR {
logger.Warn("--qr flag is deprecated and will be removed in future release, " +
"please use `share` subcommand to generate share URI and QR code")
logger.Info("use this URI to share your server", zap.String("uri", uri))
utils.PrintQR(uri)
}
@ -514,48 +460,14 @@ func runClient(cmd *cobra.Command, args []string) {
return clientTCPRedirect(*config.TCPRedirect, c)
})
}
if config.TUN != nil {
runner.Add("TUN", func() error {
return clientTUN(*config.TUN, c)
})
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signalChan)
runnerChan := make(chan clientModeRunnerResult, 1)
go func() {
runnerChan <- runner.Run()
}()
select {
case <-signalChan:
logger.Info("received signal, shutting down gracefully")
case r := <-runnerChan:
if r.OK {
logger.Info(r.Msg)
} else {
_ = c.Close() // Close the client here as Fatal will exit the program without running defer
if r.Err != nil {
logger.Fatal(r.Msg, zap.Error(r.Err))
} else {
logger.Fatal(r.Msg)
}
}
}
runner.Run()
}
type clientModeRunner struct {
ModeMap map[string]func() error
}
type clientModeRunnerResult struct {
OK bool
Msg string
Err error
}
func (r *clientModeRunner) Add(name string, f func() error) {
if r.ModeMap == nil {
r.ModeMap = make(map[string]func() error)
@ -563,9 +475,9 @@ func (r *clientModeRunner) Add(name string, f func() error) {
r.ModeMap[name] = f
}
func (r *clientModeRunner) Run() clientModeRunnerResult {
func (r *clientModeRunner) Run() {
if len(r.ModeMap) == 0 {
return clientModeRunnerResult{OK: false, Msg: "no mode specified"}
logger.Fatal("no mode specified")
}
type modeError struct {
@ -583,20 +495,16 @@ func (r *clientModeRunner) Run() clientModeRunnerResult {
for i := 0; i < len(r.ModeMap); i++ {
e := <-errChan
if e.Err != nil {
return clientModeRunnerResult{OK: false, Msg: "failed to run " + e.Name, Err: e.Err}
logger.Fatal("failed to run "+e.Name, zap.Error(e.Err))
}
}
// We don't really have any such cases, as currently none of our modes would stop on themselves without error.
// But we leave the possibility here for future expansion.
return clientModeRunnerResult{OK: true, Msg: "finished without error"}
}
func clientSOCKS5(config socks5Config, c client.Client) error {
if config.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
l, err := proxymux.ListenSOCKS(config.Listen)
l, err := net.Listen("tcp", config.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
@ -621,7 +529,7 @@ func clientHTTP(config httpConfig, c client.Client) error {
if config.Listen == "" {
return configError{Field: "listen", Err: errors.New("listen address is empty")}
}
l, err := proxymux.ListenHTTP(config.Listen)
l, err := net.Listen("tcp", config.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
@ -654,7 +562,7 @@ func clientTCPForwarding(entries []tcpForwardingEntry, c client.Client) error {
if e.Remote == "" {
return configError{Field: "remote", Err: errors.New("remote address is empty")}
}
l, err := correctnet.Listen("tcp", e.Listen)
l, err := net.Listen("tcp", e.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
@ -681,7 +589,7 @@ func clientUDPForwarding(entries []udpForwardingEntry, c client.Client) error {
if e.Remote == "" {
return configError{Field: "remote", Err: errors.New("remote address is empty")}
}
l, err := correctnet.ListenPacket("udp", e.Listen)
l, err := net.ListenPacket("udp", e.Listen)
if err != nil {
return configError{Field: "listen", Err: err}
}
@ -749,92 +657,6 @@ func clientTCPRedirect(config tcpRedirectConfig, c client.Client) error {
return p.ListenAndServe(laddr)
}
func clientTUN(config tunConfig, c client.Client) error {
supportedPlatforms := []string{"linux", "darwin", "windows", "android"}
if !slices.Contains(supportedPlatforms, runtime.GOOS) {
logger.Error("TUN is not supported on this platform", zap.String("platform", runtime.GOOS))
}
if config.Name == "" {
return configError{Field: "name", Err: errors.New("name is empty")}
}
if config.MTU == 0 {
config.MTU = 1500
}
timeout := int64(config.Timeout.Seconds())
if timeout == 0 {
timeout = 300
}
if config.Address.IPv4 == "" {
config.Address.IPv4 = "100.100.100.101/30"
}
prefix4, err := netip.ParsePrefix(config.Address.IPv4)
if err != nil {
return configError{Field: "address.ipv4", Err: err}
}
if config.Address.IPv6 == "" {
config.Address.IPv6 = "2001::ffff:ffff:ffff:fff1/126"
}
prefix6, err := netip.ParsePrefix(config.Address.IPv6)
if err != nil {
return configError{Field: "address.ipv6", Err: err}
}
server := &tun.Server{
HyClient: c,
EventLogger: &tunLogger{},
Logger: logger,
IfName: config.Name,
MTU: config.MTU,
Timeout: timeout,
Inet4Address: []netip.Prefix{prefix4},
Inet6Address: []netip.Prefix{prefix6},
}
if config.Route != nil {
server.AutoRoute = true
server.StructRoute = config.Route.Strict
parsePrefixes := func(field string, ss []string) ([]netip.Prefix, error) {
var prefixes []netip.Prefix
for i, s := range ss {
var p netip.Prefix
if strings.Contains(s, "/") {
var err error
p, err = netip.ParsePrefix(s)
if err != nil {
return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err}
}
} else {
pa, err := netip.ParseAddr(s)
if err != nil {
return nil, configError{Field: fmt.Sprintf("%s[%d]", field, i), Err: err}
}
p = netip.PrefixFrom(pa, pa.BitLen())
}
prefixes = append(prefixes, p)
}
return prefixes, nil
}
server.Inet4RouteAddress, err = parsePrefixes("route.ipv4", config.Route.IPv4)
if err != nil {
return err
}
server.Inet6RouteAddress, err = parsePrefixes("route.ipv6", config.Route.IPv6)
if err != nil {
return err
}
server.Inet4RouteExcludeAddress, err = parsePrefixes("route.ipv4Exclude", config.Route.IPv4Exclude)
if err != nil {
return err
}
server.Inet6RouteExcludeAddress, err = parsePrefixes("route.ipv6Exclude", config.Route.IPv6Exclude)
if err != nil {
return err
}
}
logger.Info("TUN listening", zap.String("interface", config.Name))
return server.Serve()
}
// parseServerAddrString parses server address string.
// Server address can be in either "host:port" or "host" format (in which case we assume port 443).
func parseServerAddrString(addrStr string) (host, port, hostPort string) {
@ -877,11 +699,8 @@ func (f *adaptiveConnFactory) New(addr net.Addr) (net.PacketConn, error) {
}
}
func connectLog(info *client.HandshakeInfo, count int) {
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx),
zap.Int("count", count))
func connectLog(count int) {
logger.Info("connected to server", zap.Int("count", count))
}
type socks5Logger struct{}
@ -894,7 +713,7 @@ func (l *socks5Logger) TCPError(addr net.Addr, reqAddr string, err error) {
if err == nil {
logger.Debug("SOCKS5 TCP closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("SOCKS5 TCP error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err))
logger.Error("SOCKS5 TCP error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
@ -906,7 +725,7 @@ func (l *socks5Logger) UDPError(addr net.Addr, err error) {
if err == nil {
logger.Debug("SOCKS5 UDP closed", zap.String("addr", addr.String()))
} else {
logger.Warn("SOCKS5 UDP error", zap.String("addr", addr.String()), zap.Error(err))
logger.Error("SOCKS5 UDP error", zap.String("addr", addr.String()), zap.Error(err))
}
}
@ -920,7 +739,7 @@ func (l *httpLogger) ConnectError(addr net.Addr, reqAddr string, err error) {
if err == nil {
logger.Debug("HTTP CONNECT closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("HTTP CONNECT error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err))
logger.Error("HTTP CONNECT error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
@ -932,7 +751,7 @@ func (l *httpLogger) HTTPError(addr net.Addr, reqURL string, err error) {
if err == nil {
logger.Debug("HTTP closed", zap.String("addr", addr.String()), zap.String("reqURL", reqURL))
} else {
logger.Warn("HTTP error", zap.String("addr", addr.String()), zap.String("reqURL", reqURL), zap.Error(err))
logger.Error("HTTP error", zap.String("addr", addr.String()), zap.String("reqURL", reqURL), zap.Error(err))
}
}
@ -946,7 +765,7 @@ func (l *tcpLogger) Error(addr net.Addr, err error) {
if err == nil {
logger.Debug("TCP forwarding closed", zap.String("addr", addr.String()))
} else {
logger.Warn("TCP forwarding error", zap.String("addr", addr.String()), zap.Error(err))
logger.Error("TCP forwarding error", zap.String("addr", addr.String()), zap.Error(err))
}
}
@ -960,7 +779,7 @@ func (l *udpLogger) Error(addr net.Addr, err error) {
if err == nil {
logger.Debug("UDP forwarding closed", zap.String("addr", addr.String()))
} else {
logger.Warn("UDP forwarding error", zap.String("addr", addr.String()), zap.Error(err))
logger.Error("UDP forwarding error", zap.String("addr", addr.String()), zap.Error(err))
}
}
@ -974,7 +793,7 @@ func (l *tcpTProxyLogger) Error(addr, reqAddr net.Addr, err error) {
if err == nil {
logger.Debug("TCP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
} else {
logger.Warn("TCP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
logger.Error("TCP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
}
}
@ -988,7 +807,7 @@ func (l *udpTProxyLogger) Error(addr, reqAddr net.Addr, err error) {
if err == nil {
logger.Debug("UDP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
} else {
logger.Warn("UDP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
logger.Error("UDP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
}
}
@ -1002,32 +821,6 @@ func (l *tcpRedirectLogger) Error(addr, reqAddr net.Addr, err error) {
if err == nil {
logger.Debug("TCP redirect closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()))
} else {
logger.Warn("TCP redirect error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
}
}
type tunLogger struct{}
func (l *tunLogger) TCPRequest(addr, reqAddr string) {
logger.Debug("TUN TCP request", zap.String("addr", addr), zap.String("reqAddr", reqAddr))
}
func (l *tunLogger) TCPError(addr, reqAddr string, err error) {
if err == nil {
logger.Debug("TUN TCP closed", zap.String("addr", addr), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("TUN TCP error", zap.String("addr", addr), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
func (l *tunLogger) UDPRequest(addr string) {
logger.Debug("TUN UDP request", zap.String("addr", addr))
}
func (l *tunLogger) UDPError(addr string, err error) {
if err == nil {
logger.Debug("TUN UDP closed", zap.String("addr", addr))
} else {
logger.Warn("TUN UDP error", zap.String("addr", addr), zap.Error(err))
logger.Error("TCP redirect error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err))
}
}

View file

@ -46,11 +46,6 @@ func TestClientConfig(t *testing.T) {
MaxIdleTimeout: 10 * time.Second,
KeepAlivePeriod: 4 * time.Second,
DisablePathMTUDiscovery: true,
Sockopts: clientConfigQUICSockopts{
BindInterface: stringRef("eth0"),
FirewallMark: uint32Ref(1234),
FdControlUnixSocket: stringRef("test.sock"),
},
},
Bandwidth: clientConfigBandwidth{
Up: "200 mbps",
@ -93,28 +88,6 @@ func TestClientConfig(t *testing.T) {
TCPRedirect: &tcpRedirectConfig{
Listen: "127.0.0.1:3500",
},
TUN: &tunConfig{
Name: "hytun",
MTU: 1500,
Timeout: 60 * time.Second,
Address: struct {
IPv4 string `mapstructure:"ipv4"`
IPv6 string `mapstructure:"ipv6"`
}{IPv4: "100.100.100.101/30", IPv6: "2001::ffff:ffff:ffff:fff1/126"},
Route: &struct {
Strict bool `mapstructure:"strict"`
IPv4 []string `mapstructure:"ipv4"`
IPv6 []string `mapstructure:"ipv6"`
IPv4Exclude []string `mapstructure:"ipv4Exclude"`
IPv6Exclude []string `mapstructure:"ipv6Exclude"`
}{
Strict: true,
IPv4: []string{"0.0.0.0/0"},
IPv6: []string{"2000::/3"},
IPv4Exclude: []string{"192.0.2.1/32"},
IPv6Exclude: []string{"2001:db8::1/128"},
},
},
})
}
@ -194,11 +167,3 @@ func TestClientConfigURI(t *testing.T) {
})
}
}
func stringRef(s string) *string {
return &s
}
func uint32Ref(i uint32) *uint32 {
return &i
}

View file

@ -26,10 +26,6 @@ quic:
maxIdleTimeout: 10s
keepAlivePeriod: 4s
disablePathMTUDiscovery: true
sockopts:
bindInterface: eth0
fwmark: 1234
fdControlUnixSocket: test.sock
bandwidth:
up: 200 mbps
@ -69,17 +65,3 @@ udpTProxy:
tcpRedirect:
listen: 127.0.0.1:3500
tun:
name: "hytun"
mtu: 1500
timeout: 1m
address:
ipv4: 100.100.100.101/30
ipv6: 2001::ffff:ffff:ffff:fff1/126
route:
strict: true
ipv4: [ 0.0.0.0/0 ]
ipv6: [ "2000::/3" ]
ipv4Exclude: [ 192.0.2.1/32 ]
ipv6Exclude: [ "2001:db8::1/128" ]

View file

@ -7,7 +7,7 @@ import (
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
// pingCmd represents the ping command
@ -42,16 +42,13 @@ func runPing(cmd *cobra.Command, args []string) {
logger.Fatal("failed to load client config", zap.Error(err))
}
c, info, err := client.NewClient(hyConfig)
c, err := client.NewClient(hyConfig)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx))
logger.Info("connecting", zap.String("addr", addr))
logger.Info("connecting", zap.String("address", addr))
start := time.Now()
conn, err := c.TCP(addr)
if err != nil {

View file

@ -29,24 +29,20 @@ const (
var (
// These values will be injected by the build system
appVersion = "Unknown"
appDate = "Unknown"
appType = "Unknown" // aka channel
appToolchain = "Unknown"
appCommit = "Unknown"
appPlatform = "Unknown"
appArch = "Unknown"
libVersion = "Unknown"
appVersion = "Unknown"
appDate = "Unknown"
appType = "Unknown" // aka channel
appCommit = "Unknown"
appPlatform = "Unknown"
appArch = "Unknown"
appVersionLong = fmt.Sprintf("Version:\t%s\n"+
"BuildDate:\t%s\n"+
"BuildType:\t%s\n"+
"Toolchain:\t%s\n"+
"CommitHash:\t%s\n"+
"Platform:\t%s\n"+
"Architecture:\t%s\n"+
"Libraries:\tquic-go=%s",
appVersion, appDate, appType, appToolchain, appCommit, appPlatform, appArch, libVersion)
"Architecture:\t%s",
appVersion, appDate, appType, appCommit, appPlatform, appArch)
appAboutLong = fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong)
)
@ -107,7 +103,6 @@ func Execute() {
func init() {
initFlags()
cobra.MousetrapHelpText = "" // Disable the mousetrap so Windows users can run the exe directly by double-clicking
cobra.OnInitialize(initConfig)
cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config
}
@ -125,10 +120,9 @@ func initConfig() {
} else {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...)
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.hysteria")
viper.AddConfigPath("/etc/hysteria/")
viper.AddConfigPath("$HOME/.hysteria")
viper.AddConfigPath(".")
}
}

View file

@ -3,44 +3,25 @@ package cmd
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/caddyserver/certmagic"
"github.com/libdns/cloudflare"
"github.com/libdns/duckdns"
"github.com/libdns/gandi"
"github.com/libdns/godaddy"
"github.com/libdns/namedotcom"
"github.com/libdns/vultr"
"github.com/mholt/acmez/acme"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/hysteria/extras/v2/auth"
"github.com/apernet/hysteria/extras/v2/correctnet"
"github.com/apernet/hysteria/extras/v2/masq"
"github.com/apernet/hysteria/extras/v2/obfs"
"github.com/apernet/hysteria/extras/v2/outbounds"
"github.com/apernet/hysteria/extras/v2/sniff"
"github.com/apernet/hysteria/extras/v2/trafficlogger"
eUtils "github.com/apernet/hysteria/extras/v2/utils"
)
const (
defaultListenAddr = ":443"
"github.com/apernet/hysteria/app/internal/utils"
"github.com/apernet/hysteria/core/server"
"github.com/apernet/hysteria/extras/auth"
"github.com/apernet/hysteria/extras/obfs"
"github.com/apernet/hysteria/extras/outbounds"
"github.com/apernet/hysteria/extras/trafficlogger"
)
var serverCmd = &cobra.Command{
@ -61,12 +42,10 @@ type serverConfig struct {
QUIC serverConfigQUIC `mapstructure:"quic"`
Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"`
IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"`
SpeedTest bool `mapstructure:"speedTest"`
DisableUDP bool `mapstructure:"disableUDP"`
UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"`
Auth serverConfigAuth `mapstructure:"auth"`
Resolver serverConfigResolver `mapstructure:"resolver"`
Sniff serverConfigSniff `mapstructure:"sniff"`
ACL serverConfigACL `mapstructure:"acl"`
Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"`
TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"`
@ -83,44 +62,19 @@ type serverConfigObfs struct {
}
type serverConfigTLS struct {
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict"
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
}
type serverConfigACME struct {
// Common fields
Domains []string `mapstructure:"domains"`
Email string `mapstructure:"email"`
CA string `mapstructure:"ca"`
ListenHost string `mapstructure:"listenHost"`
Dir string `mapstructure:"dir"`
// Type selection
Type string `mapstructure:"type"`
HTTP serverConfigACMEHTTP `mapstructure:"http"`
TLS serverConfigACMETLS `mapstructure:"tls"`
DNS serverConfigACMEDNS `mapstructure:"dns"`
// Legacy fields for backwards compatibility
// Only applicable when Type is empty
DisableHTTP bool `mapstructure:"disableHTTP"`
DisableTLSALPN bool `mapstructure:"disableTLSALPN"`
AltHTTPPort int `mapstructure:"altHTTPPort"`
AltTLSALPNPort int `mapstructure:"altTLSALPNPort"`
}
type serverConfigACMEHTTP struct {
AltPort int `mapstructure:"altPort"`
}
type serverConfigACMETLS struct {
AltPort int `mapstructure:"altPort"`
}
type serverConfigACMEDNS struct {
Name string `mapstructure:"name"`
Config map[string]string `mapstructure:"config"`
Domains []string `mapstructure:"domains"`
Email string `mapstructure:"email"`
CA string `mapstructure:"ca"`
DisableHTTP bool `mapstructure:"disableHTTP"`
DisableTLSALPN bool `mapstructure:"disableTLSALPN"`
AltHTTPPort int `mapstructure:"altHTTPPort"`
AltTLSALPNPort int `mapstructure:"altTLSALPNPort"`
Dir string `mapstructure:"dir"`
}
type serverConfigQUIC struct {
@ -183,20 +137,10 @@ type serverConfigResolver struct {
HTTPS serverConfigResolverHTTPS `mapstructure:"https"`
}
type serverConfigSniff struct {
Enable bool `mapstructure:"enable"`
Timeout time.Duration `mapstructure:"timeout"`
RewriteDomain bool `mapstructure:"rewriteDomain"`
TCPPorts string `mapstructure:"tcpPorts"`
UDPPorts string `mapstructure:"udpPorts"`
}
type serverConfigACL struct {
File string `mapstructure:"file"`
Inline []string `mapstructure:"inline"`
GeoIP string `mapstructure:"geoip"`
GeoSite string `mapstructure:"geosite"`
GeoUpdateInterval time.Duration `mapstructure:"geoUpdateInterval"`
File string `mapstructure:"file"`
Inline []string `mapstructure:"inline"`
GeoIP string `mapstructure:"geoip"`
}
type serverConfigOutboundDirect struct {
@ -204,7 +148,6 @@ type serverConfigOutboundDirect struct {
BindIPv4 string `mapstructure:"bindIPv4"`
BindIPv6 string `mapstructure:"bindIPv6"`
BindDevice string `mapstructure:"bindDevice"`
FastOpen bool `mapstructure:"fastOpen"`
}
type serverConfigOutboundSOCKS5 struct {
@ -213,22 +156,15 @@ type serverConfigOutboundSOCKS5 struct {
Password string `mapstructure:"password"`
}
type serverConfigOutboundHTTP struct {
URL string `mapstructure:"url"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigOutboundEntry struct {
Name string `mapstructure:"name"`
Type string `mapstructure:"type"`
Direct serverConfigOutboundDirect `mapstructure:"direct"`
SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"`
HTTP serverConfigOutboundHTTP `mapstructure:"http"`
}
type serverConfigTrafficStats struct {
Listen string `mapstructure:"listen"`
Secret string `mapstructure:"secret"`
}
type serverConfigMasqueradeFile struct {
@ -238,35 +174,24 @@ type serverConfigMasqueradeFile struct {
type serverConfigMasqueradeProxy struct {
URL string `mapstructure:"url"`
RewriteHost bool `mapstructure:"rewriteHost"`
Insecure bool `mapstructure:"insecure"`
}
type serverConfigMasqueradeString struct {
Content string `mapstructure:"content"`
Headers map[string]string `mapstructure:"headers"`
StatusCode int `mapstructure:"statusCode"`
}
type serverConfigMasquerade struct {
Type string `mapstructure:"type"`
File serverConfigMasqueradeFile `mapstructure:"file"`
Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"`
String serverConfigMasqueradeString `mapstructure:"string"`
ListenHTTP string `mapstructure:"listenHTTP"`
ListenHTTPS string `mapstructure:"listenHTTPS"`
ForceHTTPS bool `mapstructure:"forceHTTPS"`
Type string `mapstructure:"type"`
File serverConfigMasqueradeFile `mapstructure:"file"`
Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"`
}
func (c *serverConfig) fillConn(hyConfig *server.Config) error {
listenAddr := c.Listen
if listenAddr == "" {
listenAddr = defaultListenAddr
listenAddr = ":443"
}
uAddr, err := net.ResolveUDPAddr("udp", listenAddr)
if err != nil {
return configError{Field: "listen", Err: err}
}
conn, err := correctnet.ListenUDP("udp", uAddr)
conn, err := net.ListenUDP("udp", uAddr)
if err != nil {
return configError{Field: "listen", Err: err}
}
@ -294,45 +219,15 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")}
}
if c.TLS != nil {
// SNI guard
var sniGuard utils.SNIGuardFunc
switch strings.ToLower(c.TLS.SNIGuard) {
case "", "dns-san":
sniGuard = utils.SNIGuardDNSSAN
case "strict":
sniGuard = utils.SNIGuardStrict
case "disable":
sniGuard = nil
default:
return configError{Field: "tls.sniGuard", Err: errors.New("unsupported SNI guard")}
}
// Local TLS cert
if c.TLS.Cert == "" || c.TLS.Key == "" {
return configError{Field: "tls", Err: errors.New("empty cert or key path")}
}
certLoader := &utils.LocalCertificateLoader{
CertFile: c.TLS.Cert,
KeyFile: c.TLS.Key,
SNIGuard: sniGuard,
}
// Try loading the cert-key pair here to catch errors early
// (e.g. invalid files or insufficient permissions)
err := certLoader.InitializeCache()
cert, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key)
if err != nil {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
if pathErr.Path == c.TLS.Cert {
return configError{Field: "tls.cert", Err: pathErr}
}
if pathErr.Path == c.TLS.Key {
return configError{Field: "tls.key", Err: pathErr}
}
}
return configError{Field: "tls", Err: err}
}
// Use GetCertificate instead of Certificates so that
// users can update the cert without restarting the server.
hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate
hyConfig.TLSConfig.Certificates = []tls.Certificate{cert}
} else {
// ACME
dataDir := c.ACME.Dir
@ -350,10 +245,13 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
Logger: logger,
}
cmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{
Email: c.ACME.Email,
Agreed: true,
ListenHost: c.ACME.ListenHost,
Logger: logger,
Email: c.ACME.Email,
Agreed: true,
DisableHTTPChallenge: c.ACME.DisableHTTP,
DisableTLSALPNChallenge: c.ACME.DisableTLSALPN,
AltHTTPPort: c.ACME.AltHTTPPort,
AltTLSALPNPort: c.ACME.AltTLSALPNPort,
Logger: logger,
})
switch strings.ToLower(c.ACME.CA) {
case "letsencrypt", "le", "":
@ -361,88 +259,9 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
cmIssuer.CA = certmagic.LetsEncryptProductionCA
case "zerossl", "zero":
cmIssuer.CA = certmagic.ZeroSSLProductionCA
eab, err := genZeroSSLEAB(c.ACME.Email)
if err != nil {
return configError{Field: "acme.ca", Err: err}
}
cmIssuer.ExternalAccount = eab
default:
return configError{Field: "acme.ca", Err: errors.New("unsupported CA")}
return configError{Field: "acme.ca", Err: errors.New("unknown CA")}
}
switch strings.ToLower(c.ACME.Type) {
case "http":
cmIssuer.DisableHTTPChallenge = false
cmIssuer.DisableTLSALPNChallenge = true
cmIssuer.DNS01Solver = nil
cmIssuer.AltHTTPPort = c.ACME.HTTP.AltPort
case "tls":
cmIssuer.DisableHTTPChallenge = true
cmIssuer.DisableTLSALPNChallenge = false
cmIssuer.DNS01Solver = nil
cmIssuer.AltTLSALPNPort = c.ACME.TLS.AltPort
case "dns":
cmIssuer.DisableHTTPChallenge = true
cmIssuer.DisableTLSALPNChallenge = true
if c.ACME.DNS.Name == "" {
return configError{Field: "acme.dns.name", Err: errors.New("empty DNS provider name")}
}
if c.ACME.DNS.Config == nil {
return configError{Field: "acme.dns.config", Err: errors.New("empty DNS provider config")}
}
switch strings.ToLower(c.ACME.DNS.Name) {
case "cloudflare":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &cloudflare.Provider{
APIToken: c.ACME.DNS.Config["cloudflare_api_token"],
},
}
case "duckdns":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &duckdns.Provider{
APIToken: c.ACME.DNS.Config["duckdns_api_token"],
OverrideDomain: c.ACME.DNS.Config["duckdns_override_domain"],
},
}
case "gandi":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &gandi.Provider{
BearerToken: c.ACME.DNS.Config["gandi_api_token"],
},
}
case "godaddy":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &godaddy.Provider{
APIToken: c.ACME.DNS.Config["godaddy_api_token"],
},
}
case "namedotcom":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &namedotcom.Provider{
Token: c.ACME.DNS.Config["namedotcom_token"],
User: c.ACME.DNS.Config["namedotcom_user"],
Server: c.ACME.DNS.Config["namedotcom_server"],
},
}
case "vultr":
cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &vultr.Provider{
APIToken: c.ACME.DNS.Config["vultr_api_token"],
},
}
default:
return configError{Field: "acme.dns.name", Err: errors.New("unsupported DNS provider")}
}
case "":
// Legacy compatibility mode
cmIssuer.DisableHTTPChallenge = c.ACME.DisableHTTP
cmIssuer.DisableTLSALPNChallenge = c.ACME.DisableTLSALPN
cmIssuer.AltHTTPPort = c.ACME.AltHTTPPort
cmIssuer.AltTLSALPNPort = c.ACME.AltTLSALPNPort
default:
return configError{Field: "acme.type", Err: errors.New("unsupported ACME type")}
}
cmCfg.Issuers = []certmagic.Issuer{cmIssuer}
cmCache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
@ -464,48 +283,6 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
return nil
}
func genZeroSSLEAB(email string) (*acme.EAB, error) {
req, err := http.NewRequest(
http.MethodPost,
"https://api.zerossl.com/acme/eab-credentials-email",
strings.NewReader(url.Values{"email": []string{email}}.Encode()),
)
if err != nil {
return nil, fmt.Errorf("failed to creare ZeroSSL EAB request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", certmagic.UserAgent)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send ZeroSSL EAB request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
var result struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
} `json:"error"`
EABKID string `json:"eab_kid"`
EABHMACKey string `json:"eab_hmac_key"`
}
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed decoding ZeroSSL EAB API response: %w", err)
}
if result.Error.Code != 0 {
return nil, fmt.Errorf("failed getting ZeroSSL EAB credentials: HTTP %d: %s (code %d)", resp.StatusCode, result.Error.Type, result.Error.Code)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
}
return &acme.EAB{
KeyID: result.EABKID,
MACKey: result.EABHMACKey,
}, nil
}
func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error {
hyConfig.QUICConfig = server.QUICConfig{
InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow,
@ -520,18 +297,18 @@ func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error {
}
func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) {
opts := outbounds.DirectOutboundOptions{}
var mode outbounds.DirectOutboundMode
switch strings.ToLower(c.Mode) {
case "", "auto":
opts.Mode = outbounds.DirectOutboundModeAuto
mode = outbounds.DirectOutboundModeAuto
case "64":
opts.Mode = outbounds.DirectOutboundMode64
mode = outbounds.DirectOutboundMode64
case "46":
opts.Mode = outbounds.DirectOutboundMode46
mode = outbounds.DirectOutboundMode46
case "6":
opts.Mode = outbounds.DirectOutboundMode6
mode = outbounds.DirectOutboundMode6
case "4":
opts.Mode = outbounds.DirectOutboundMode4
mode = outbounds.DirectOutboundMode4
default:
return nil, configError{Field: "outbounds.direct.mode", Err: errors.New("unsupported mode")}
}
@ -548,14 +325,12 @@ func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outboun
if len(c.BindIPv6) > 0 && ip6 == nil {
return nil, configError{Field: "outbounds.direct.bindIPv6", Err: errors.New("invalid IPv6 address")}
}
opts.BindIP4 = ip4
opts.BindIP6 = ip6
return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6)
}
if bindDevice {
opts.DeviceName = c.BindDevice
return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice)
}
opts.FastOpen = c.FastOpen
return outbounds.NewDirectOutboundWithOptions(opts)
return outbounds.NewDirectOutboundSimple(mode), nil
}
func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) {
@ -565,36 +340,6 @@ func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outboun
return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil
}
func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) {
if c.URL == "" {
return nil, configError{Field: "outbounds.http.url", Err: errors.New("empty http address")}
}
return outbounds.NewHTTPOutbound(c.URL, c.Insecure)
}
func (c *serverConfig) fillRequestHook(hyConfig *server.Config) error {
if c.Sniff.Enable {
s := &sniff.Sniffer{
Timeout: c.Sniff.Timeout,
RewriteDomain: c.Sniff.RewriteDomain,
}
if c.Sniff.TCPPorts != "" {
s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts)
if s.TCPPorts == nil {
return configError{Field: "sniff.tcpPorts", Err: errors.New("invalid port union")}
}
}
if c.Sniff.UDPPorts != "" {
s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts)
if s.UDPPorts == nil {
return configError{Field: "sniff.udpPorts", Err: errors.New("invalid port union")}
}
}
hyConfig.RequestHook = s
}
return nil
}
func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {
// Resolver, ACL, actual outbound are all implemented through the Outbound interface.
// Depending on the config, we build a chain like this:
@ -621,8 +366,6 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {
ob, err = serverConfigOutboundDirectToOutbound(entry.Direct)
case "socks5":
ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5)
case "http":
ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP)
default:
err = configError{Field: "outbounds.type", Err: errors.New("unsupported outbound type")}
}
@ -640,23 +383,21 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {
if c.ACL.File != "" && len(c.ACL.Inline) > 0 {
return configError{Field: "acl", Err: errors.New("cannot set both acl.file and acl.inline")}
}
gLoader := &utils.GeoLoader{
GeoIPFilename: c.ACL.GeoIP,
GeoSiteFilename: c.ACL.GeoSite,
UpdateInterval: c.ACL.GeoUpdateInterval,
DownloadFunc: geoDownloadFunc,
DownloadErrFunc: geoDownloadErrFunc,
gLoader := &utils.GeoIPLoader{
Filename: c.ACL.GeoIP,
DownloadFunc: geoipDownloadFunc,
DownloadErrFunc: geoipDownloadErrFunc,
}
if c.ACL.File != "" {
hasACL = true
acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader)
acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader.Load)
if err != nil {
return configError{Field: "acl.file", Err: err}
}
uOb = acl
} else if len(c.ACL.Inline) > 0 {
hasACL = true
acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader)
acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader.Load)
if err != nil {
return configError{Field: "acl.inline", Err: err}
}
@ -699,11 +440,6 @@ func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error {
return configError{Field: "resolver.type", Err: errors.New("unsupported resolver type")}
}
// Speed test
if c.SpeedTest {
uOb = outbounds.NewSpeedtestHandler(uOb)
}
hyConfig.Outbound = &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb}
return nil
}
@ -755,7 +491,7 @@ func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error {
if len(c.Auth.UserPass) == 0 {
return configError{Field: "auth.userpass", Err: errors.New("empty auth userpass")}
}
hyConfig.Authenticator = auth.NewUserPassAuthenticator(c.Auth.UserPass)
hyConfig.Authenticator = &auth.UserPassAuthenticator{Users: c.Auth.UserPass}
return nil
case "http", "https":
if c.Auth.HTTP.URL == "" {
@ -781,15 +517,13 @@ func (c *serverConfig) fillEventLogger(hyConfig *server.Config) error {
func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error {
if c.TrafficStats.Listen != "" {
tss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret)
tss := trafficlogger.NewTrafficStatsServer()
hyConfig.TrafficLogger = tss
go runTrafficStatsServer(c.TrafficStats.Listen, tss)
}
return nil
}
// fillMasqHandler must be called after fillConn, as we may need to extract the QUIC
// port number from Conn for MasqTCPServer.
func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error {
var handler http.Handler
switch strings.ToLower(c.Masquerade.Type) {
@ -808,28 +542,6 @@ func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error {
if err != nil {
return configError{Field: "masquerade.proxy.url", Err: err}
}
if u.Scheme != "http" && u.Scheme != "https" {
return configError{Field: "masquerade.proxy.url", Err: fmt.Errorf("unsupported protocol scheme \"%s\"", u.Scheme)}
}
transport := http.DefaultTransport
if c.Masquerade.Proxy.Insecure {
transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
// use default configs from http.DefaultTransport
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}
handler = &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(u)
@ -839,55 +551,15 @@ func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error {
r.Out.Host = r.In.Host
}
},
Transport: transport,
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("HTTP reverse proxy error", zap.Error(err))
w.WriteHeader(http.StatusBadGateway)
},
}
case "string":
if c.Masquerade.String.Content == "" {
return configError{Field: "masquerade.string.content", Err: errors.New("empty string content")}
}
if c.Masquerade.String.StatusCode != 0 &&
(c.Masquerade.String.StatusCode < 200 ||
c.Masquerade.String.StatusCode > 599 ||
c.Masquerade.String.StatusCode == 233) {
// 233 is reserved for Hysteria authentication
return configError{Field: "masquerade.string.statusCode", Err: errors.New("invalid status code (must be 200-599, except 233)")}
}
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for k, v := range c.Masquerade.String.Headers {
w.Header().Set(k, v)
}
if c.Masquerade.String.StatusCode != 0 {
w.WriteHeader(c.Masquerade.String.StatusCode)
} else {
w.WriteHeader(http.StatusOK) // Use 200 OK by default
}
_, _ = w.Write([]byte(c.Masquerade.String.Content))
})
default:
return configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")}
}
hyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler, QUIC: true}
if c.Masquerade.ListenHTTP != "" || c.Masquerade.ListenHTTPS != "" {
if c.Masquerade.ListenHTTP != "" && c.Masquerade.ListenHTTPS == "" {
return configError{Field: "masquerade.listenHTTPS", Err: errors.New("having only HTTP server without HTTPS is not supported")}
}
s := masq.MasqTCPServer{
QUICPort: extractPortFromAddr(hyConfig.Conn.LocalAddr().String()),
HTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS),
Handler: &masqHandlerLogWrapper{H: handler, QUIC: false},
TLSConfig: &tls.Config{
Certificates: hyConfig.TLSConfig.Certificates,
GetCertificate: hyConfig.TLSConfig.GetCertificate,
},
ForceHTTPS: c.Masquerade.ForceHTTPS,
}
go runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS)
}
hyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler}
return nil
}
@ -898,7 +570,6 @@ func (c *serverConfig) Config() (*server.Config, error) {
c.fillConn,
c.fillTLSConfig,
c.fillQUICConfig,
c.fillRequestHook,
c.fillOutboundConfig,
c.fillBandwidthConfig,
c.fillIgnoreClientBandwidth,
@ -937,11 +608,7 @@ func runServer(cmd *cobra.Command, args []string) {
if err != nil {
logger.Fatal("failed to initialize server", zap.Error(err))
}
if config.Listen != "" {
logger.Info("server up and running", zap.String("listen", config.Listen))
} else {
logger.Info("server up and running", zap.String("listen", defaultListenAddr))
}
logger.Info("server up and running")
if !disableUpdateCheck {
go runCheckUpdateServer()
@ -954,38 +621,18 @@ func runServer(cmd *cobra.Command, args []string) {
func runTrafficStatsServer(listen string, handler http.Handler) {
logger.Info("traffic stats server up and running", zap.String("listen", listen))
if err := correctnet.HTTPListenAndServe(listen, handler); err != nil {
if err := http.ListenAndServe(listen, handler); err != nil {
logger.Fatal("failed to serve traffic stats", zap.Error(err))
}
}
func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string) {
errChan := make(chan error, 2)
if httpAddr != "" {
go func() {
logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr))
errChan <- s.ListenAndServeHTTP(httpAddr)
}()
}
if httpsAddr != "" {
go func() {
logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr))
errChan <- s.ListenAndServeHTTPS(httpsAddr)
}()
}
err := <-errChan
if err != nil {
logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err))
}
func geoipDownloadFunc(filename, url string) {
logger.Info("downloading GeoIP database", zap.String("filename", filename), zap.String("url", url))
}
func geoDownloadFunc(filename, url string) {
logger.Info("downloading database", zap.String("filename", filename), zap.String("url", url))
}
func geoDownloadErrFunc(err error) {
func geoipDownloadErrFunc(err error) {
if err != nil {
logger.Error("failed to download database", zap.Error(err))
logger.Error("failed to download GeoIP database", zap.Error(err))
}
}
@ -1007,7 +654,7 @@ func (l *serverLogger) TCPError(addr net.Addr, id, reqAddr string, err error) {
if err == nil {
logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr))
} else {
logger.Warn("TCP error", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr), zap.Error(err))
logger.Error("TCP error", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr), zap.Error(err))
}
}
@ -1019,33 +666,15 @@ func (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err
if err == nil {
logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID))
} else {
logger.Warn("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err))
logger.Error("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err))
}
}
type masqHandlerLogWrapper struct {
H http.Handler
QUIC bool
H http.Handler
}
func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger.Debug("masquerade request",
zap.String("addr", r.RemoteAddr),
zap.String("method", r.Method),
zap.String("host", r.Host),
zap.String("url", r.URL.String()),
zap.Bool("quic", m.QUIC))
logger.Debug("masquerade request", zap.String("addr", r.RemoteAddr), zap.String("method", r.Method), zap.String("host", r.Host), zap.String("url", r.URL.String()))
m.H.ServeHTTP(w, r)
}
func extractPortFromAddr(addr string) int {
_, portStr, err := net.SplitHostPort(addr)
if err != nil {
return 0
}
port, err := strconv.Atoi(portStr)
if err != nil {
return 0
}
return port
}

View file

@ -26,37 +26,21 @@ func TestServerConfig(t *testing.T) {
},
},
TLS: &serverConfigTLS{
Cert: "some.crt",
Key: "some.key",
SNIGuard: "strict",
Cert: "some.crt",
Key: "some.key",
},
ACME: &serverConfigACME{
Domains: []string{
"sub1.example.com",
"sub2.example.com",
},
Email: "haha@cringe.net",
CA: "zero",
ListenHost: "127.0.0.9",
Dir: "random_dir",
Type: "dns",
HTTP: serverConfigACMEHTTP{
AltPort: 8888,
},
TLS: serverConfigACMETLS{
AltPort: 44333,
},
DNS: serverConfigACMEDNS{
Name: "gomommy",
Config: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
Email: "haha@cringe.net",
CA: "zero",
DisableHTTP: true,
DisableTLSALPN: true,
AltHTTPPort: 8080,
AltTLSALPNPort: 4433,
AltHTTPPort: 9980,
AltTLSALPNPort: 9443,
Dir: "random_dir",
},
QUIC: serverConfigQUIC{
InitStreamReceiveWindow: 77881,
@ -72,7 +56,6 @@ func TestServerConfig(t *testing.T) {
Down: "100 mbps",
},
IgnoreClientBandwidth: true,
SpeedTest: true,
DisableUDP: true,
UDPIdleTimeout: 120 * time.Second,
Auth: serverConfigAuth{
@ -112,22 +95,13 @@ func TestServerConfig(t *testing.T) {
Insecure: true,
},
},
Sniff: serverConfigSniff{
Enable: true,
Timeout: 1 * time.Second,
RewriteDomain: true,
TCPPorts: "80,443,1000-2000",
UDPPorts: "443",
},
ACL: serverConfigACL{
File: "chnroute.txt",
Inline: []string{
"lmao(ok)",
"kek(cringe,boba,tea)",
},
GeoIP: "some.dat",
GeoSite: "some_site.dat",
GeoUpdateInterval: 168 * time.Hour,
GeoIP: "fake.mmdb",
},
Outbounds: []serverConfigOutboundEntry{
{
@ -138,7 +112,6 @@ func TestServerConfig(t *testing.T) {
BindIPv4: "2.4.6.8",
BindIPv6: "0:0:0:0:0:ffff:0204:0608",
BindDevice: "eth233",
FastOpen: true,
},
},
{
@ -150,18 +123,9 @@ func TestServerConfig(t *testing.T) {
Password: "Elliot Alderson",
},
},
{
Name: "weirdstuff",
Type: "http",
HTTP: serverConfigOutboundHTTP{
URL: "https://eyy.lmao:4443/goofy",
Insecure: true,
},
},
},
TrafficStats: serverConfigTrafficStats{
Listen: ":9999",
Secret: "its_me_mario",
},
Masquerade: serverConfigMasquerade{
Type: "proxy",
@ -171,19 +135,7 @@ func TestServerConfig(t *testing.T) {
Proxy: serverConfigMasqueradeProxy{
URL: "https://some.site.net",
RewriteHost: true,
Insecure: true,
},
String: serverConfigMasqueradeString{
Content: "aint nothin here",
Headers: map[string]string{
"content-type": "text/plain",
"custom-haha": "lol",
},
StatusCode: 418,
},
ListenHTTP: ":80",
ListenHTTPS: ":443",
ForceHTTPS: true,
},
})
}

View file

@ -8,7 +8,6 @@ obfs:
tls:
cert: some.crt
key: some.key
sniGuard: strict
acme:
domains:
@ -16,22 +15,11 @@ acme:
- sub2.example.com
email: haha@cringe.net
ca: zero
listenHost: 127.0.0.9
dir: random_dir
type: dns
http:
altPort: 8888
tls:
altPort: 44333
dns:
name: gomommy
config:
key1: value1
key2: value2
disableHTTP: true
disableTLSALPN: true
altHTTPPort: 8080
altTLSALPNPort: 4433
altHTTPPort: 9980
altTLSALPNPort: 9443
dir: random_dir
quic:
initStreamReceiveWindow: 77881
@ -48,8 +36,6 @@ bandwidth:
ignoreClientBandwidth: true
speedTest: true
disableUDP: true
udpIdleTimeout: 120s
@ -84,21 +70,12 @@ resolver:
sni: real.stuff.net
insecure: true
sniff:
enable: true
timeout: 1s
rewriteDomain: true
tcpPorts: 80,443,1000-2000
udpPorts: 443
acl:
file: chnroute.txt
inline:
- lmao(ok)
- kek(cringe,boba,tea)
geoip: some.dat
geosite: some_site.dat
geoUpdateInterval: 168h
geoip: fake.mmdb
outbounds:
- name: goodstuff
@ -108,22 +85,15 @@ outbounds:
bindIPv4: 2.4.6.8
bindIPv6: 0:0:0:0:0:ffff:0204:0608
bindDevice: eth233
fastOpen: true
- name: badstuff
type: socks5
socks5:
addr: shady.proxy.ru:1080
username: hackerman
password: Elliot Alderson
- name: weirdstuff
type: http
http:
url: https://eyy.lmao:4443/goofy
insecure: true
trafficStats:
listen: :9999
secret: its_me_mario
masquerade:
type: proxy
@ -132,13 +102,3 @@ masquerade:
proxy:
url: https://some.site.net
rewriteHost: true
insecure: true
string:
content: aint nothin here
headers:
content-type: text/plain
custom-haha: lol
statusCode: 418
listenHTTP: :80
listenHTTPS: :443
forceHTTPS: true

View file

@ -1,55 +0,0 @@
package cmd
import (
"fmt"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
var (
noText bool
withQR bool
)
// shareCmd represents the share command
var shareCmd = &cobra.Command{
Use: "share",
Short: "Generate share URI",
Long: "Generate a hysteria2:// URI from a client config for sharing",
Run: runShare,
}
func init() {
initShareFlags()
rootCmd.AddCommand(shareCmd)
}
func initShareFlags() {
shareCmd.Flags().BoolVar(&noText, "notext", false, "do not show URI as text")
shareCmd.Flags().BoolVar(&withQR, "qr", false, "show URI as QR code")
}
func runShare(cmd *cobra.Command, args []string) {
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
if _, err := config.Config(); err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
u := config.URI()
if !noText {
fmt.Println(u)
}
if withQR {
utils.PrintQR(u)
}
}

View file

@ -1,178 +0,0 @@
package cmd
import (
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
hyErrors "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/extras/v2/outbounds"
"github.com/apernet/hysteria/extras/v2/outbounds/speedtest"
)
var (
skipDownload bool
skipUpload bool
dataSize uint32
useBytes bool
speedtestAddr = fmt.Sprintf("%s:%d", outbounds.SpeedtestDest, 0)
)
// speedtestCmd represents the speedtest command
var speedtestCmd = &cobra.Command{
Use: "speedtest",
Short: "Speed test mode",
Long: "Perform a speed test through the proxy server. The server must have speed test support enabled.",
Run: runSpeedtest,
}
func init() {
initSpeedtestFlags()
rootCmd.AddCommand(speedtestCmd)
}
func initSpeedtestFlags() {
speedtestCmd.Flags().BoolVar(&skipDownload, "skip-download", false, "Skip download test")
speedtestCmd.Flags().BoolVar(&skipUpload, "skip-upload", false, "Skip upload test")
speedtestCmd.Flags().Uint32Var(&dataSize, "data-size", 1024*1024*100, "Data size for download and upload tests")
speedtestCmd.Flags().BoolVar(&useBytes, "use-bytes", false, "Use bytes per second instead of bits per second")
}
func runSpeedtest(cmd *cobra.Command, args []string) {
logger.Info("speed test mode")
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read client config", zap.Error(err))
}
var config clientConfig
if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse client config", zap.Error(err))
}
hyConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to load client config", zap.Error(err))
}
c, info, err := client.NewClient(hyConfig)
if err != nil {
logger.Fatal("failed to initialize client", zap.Error(err))
}
defer c.Close()
logger.Info("connected to server",
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx))
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(signalChan)
runChan := make(chan struct{}, 1)
go func() {
if !skipDownload {
runDownloadTest(c)
}
if !skipUpload {
runUploadTest(c)
}
runChan <- struct{}{}
}()
select {
case <-signalChan:
logger.Info("received signal, shutting down gracefully")
case <-runChan:
logger.Info("speed test complete")
}
}
func runDownloadTest(c client.Client) {
logger.Info("performing download test")
downConn, err := c.TCP(speedtestAddr)
if err != nil {
if errors.As(err, &hyErrors.DialError{}) {
logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err))
} else {
logger.Fatal("failed to connect", zap.Error(err))
}
}
defer downConn.Close()
downClient := &speedtest.Client{Conn: downConn}
currentTotal := uint32(0)
err = downClient.Download(dataSize, func(d time.Duration, b uint32, done bool) {
if !done {
currentTotal += b
logger.Info("downloading",
zap.Uint32("bytes", b),
zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)),
zap.String("speed", formatSpeed(b, d, useBytes)))
} else {
logger.Info("download complete",
zap.Uint32("bytes", b),
zap.String("speed", formatSpeed(b, d, useBytes)))
}
})
if err != nil {
logger.Fatal("download test failed", zap.Error(err))
}
logger.Info("download test complete")
}
func runUploadTest(c client.Client) {
logger.Info("performing upload test")
upConn, err := c.TCP(speedtestAddr)
if err != nil {
if errors.As(err, &hyErrors.DialError{}) {
logger.Fatal("failed to connect (server may not support speed test)", zap.Error(err))
} else {
logger.Fatal("failed to connect", zap.Error(err))
}
}
defer upConn.Close()
upClient := &speedtest.Client{Conn: upConn}
currentTotal := uint32(0)
err = upClient.Upload(dataSize, func(d time.Duration, b uint32, done bool) {
if !done {
currentTotal += b
logger.Info("uploading",
zap.Uint32("bytes", b),
zap.String("progress", fmt.Sprintf("%.2f%%", float64(currentTotal)/float64(dataSize)*100)),
zap.String("speed", formatSpeed(b, d, useBytes)))
} else {
logger.Info("upload complete",
zap.Uint32("bytes", b),
zap.String("speed", formatSpeed(b, d, useBytes)))
}
})
if err != nil {
logger.Fatal("upload test failed", zap.Error(err))
}
logger.Info("upload test complete")
}
func formatSpeed(bytes uint32, duration time.Duration, useBytes bool) string {
speed := float64(bytes) / duration.Seconds()
var units []string
if useBytes {
units = []string{"B/s", "KB/s", "MB/s", "GB/s"}
} else {
units = []string{"bps", "Kbps", "Mbps", "Gbps"}
speed *= 8
}
unitIndex := 0
for speed > 1000 && unitIndex < len(units)-1 {
speed /= 1000
unitIndex++
}
return fmt.Sprintf("%.2f %s", speed, units[unitIndex])
}

View file

@ -6,8 +6,8 @@ import (
"github.com/spf13/cobra"
"go.uber.org/zap"
"github.com/apernet/hysteria/app/v2/internal/utils"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/app/internal/utils"
"github.com/apernet/hysteria/core/client"
)
const (

View file

@ -1,92 +1,67 @@
module github.com/apernet/hysteria/app/v2
module github.com/apernet/hysteria/app
go 1.23
toolchain go1.24.2
go 1.20
require (
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f
github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000
github.com/apernet/hysteria/extras/v2 v2.0.0-00010101000000-000000000000
github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad
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/libdns/cloudflare v0.1.1
github.com/libdns/duckdns v0.2.0
github.com/libdns/gandi v1.0.3
github.com/libdns/godaddy v1.0.3
github.com/libdns/namedotcom v0.3.3
github.com/libdns/vultr v1.0.0
github.com/mdp/qrterminal/v3 v3.1.1
github.com/mholt/acmez v1.0.4
github.com/sagernet/sing v0.3.2
github.com/oschwald/geoip2-golang v1.9.0
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.8.4
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/sys v0.25.0
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431 // indirect
github.com/apernet/quic-go v0.38.2-0.20230902022200-c0e542a00ce6 // indirect
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a // indirect
github.com/database64128/tfo-go/v2 v2.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.6 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/mholt/acmez v1.0.4 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/refraction-networking/utls v1.6.6 // indirect
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.3 // 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/stretchr/objx v0.5.2 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/vultr/govultr/v3 v3.6.4 // indirect
github.com/zhangyunhao116/fastrand v0.3.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.11.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/qr v0.2.0 // indirect
)
replace github.com/apernet/hysteria/core/v2 => ../core
replace github.com/apernet/hysteria/core => ../core
replace github.com/apernet/hysteria/extras/v2 => ../extras
replace github.com/apernet/hysteria/extras => ../extras

View file

@ -38,14 +38,10 @@ 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/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY=
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM=
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431 h1:9/jM7e+kVALd7Jfu1c27dcEpT/Fd/Gzq2OsQjKjakKI=
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431/go.mod h1:I/47OIGG5H/IfAm+nz2c6hm6b/NkEhpvptAoiPcY7jQ=
github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad h1:QzQ2sKpc9o42HNRR8ukM5uMC/RzR2HgZd/Nvaqol2C0=
github.com/apernet/sing-tun v0.2.6-0.20240323130332-b9f6511036ad/go.mod h1:S5IydyLSN/QAfvY+r2GoomPJ6hidtXWm/Ad18sJVssk=
github.com/apernet/quic-go v0.38.2-0.20230902022200-c0e542a00ce6 h1:OXAn9a1dhdluj5olg6Gou7q5oN5J7CaTYS7SJPqHP/0=
github.com/apernet/quic-go v0.38.2-0.20230902022200-c0e542a00ce6/go.mod h1:1X8T39iiKfCimEhzNaUrEeq0Pmafu7cjgYJetUbRXYc=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
@ -57,16 +53,10 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a h1:t4SDi0pmNkryzKdM4QF3o5vqSP4GRjeZD/6j3nyxNP0=
github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a/go.mod h1:7K2NQKbabB5mBl41vF6YayYl5g7YpDwc4dQ5iMpP3Lg=
github.com/database64128/tfo-go/v2 v2.2.2 h1:BxynF4qGF5ct3DpPLEG62uyJZ3LQhqaf0Ken+kyy7PM=
github.com/database64128/tfo-go/v2 v2.2.2/go.mod h1:2IW8jppdBwdVMjA08uEyMNnqiAHKUlqAA+J8NrsfktY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -76,19 +66,13 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
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-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -102,6 +86,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -116,8 +102,7 @@ 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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
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/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -129,10 +114,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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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=
@ -153,12 +135,6 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/golang-lru 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.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
@ -172,53 +148,35 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0=
github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/duckdns v0.2.0 h1:vd3pE09G2qTx1Zh1o3LmrivWSByD3Z5FbL7csX5vDgE=
github.com/libdns/duckdns v0.2.0/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs=
github.com/libdns/gandi v1.0.3 h1:FIvipWOg/O4zi75fPRmtcolRKqI6MgrbpFy2p5KYdUk=
github.com/libdns/gandi v1.0.3/go.mod h1:G6dw58Xnji2xX+lb+uZxGbtmfxKllm1CGHE2bOPG3WA=
github.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44=
github.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE=
github.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s=
github.com/libdns/vultr v1.0.0 h1:W8B4+k2bm9ro3bZLSZV9hMOQI+uO6Svu+GmD+Olz7ZI=
github.com/libdns/vultr v1.0.0/go.mod h1:8K1HJExcbeHS4YPkFHRZpqpXZzZ+DZAA0m0VikJgEqk=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc=
github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU=
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
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=
@ -230,20 +188,13 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
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-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/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/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo=
github.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
github.com/spf13/afero v1.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=
@ -258,9 +209,8 @@ 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -270,24 +220,22 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM=
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vultr/govultr/v3 v3.6.4 h1:unvY9eXlBw667ECQZDbBDOIaWB8wkk6Bx+yB0IMKXJ4=
github.com/vultr/govultr/v3 v3.6.4/go.mod h1:rt9v2x114jZmmLAE/h5N5jnxTmsK9ewwS2oQZ0UBQzM=
github.com/yuin/goldmark v1.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=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig=
github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -299,17 +247,12 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -318,8 +261,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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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=
@ -330,8 +273,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-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
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=
@ -358,8 +301,8 @@ 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.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -372,7 +315,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
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=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -396,8 +338,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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=
@ -407,8 +349,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.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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=
@ -422,8 +362,7 @@ 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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=
@ -433,7 +372,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
@ -441,7 +379,6 @@ golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7w
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=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -465,10 +402,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -480,8 +417,8 @@ 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.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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
@ -505,7 +442,6 @@ golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@ -533,11 +469,12 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc=
golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
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=
@ -630,12 +567,10 @@ 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.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
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=

View file

@ -4,7 +4,7 @@ import (
"io"
"net"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
type TCPTunnel struct {

View file

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/app/v2/internal/utils_test"
"github.com/apernet/hysteria/app/internal/utils_test"
)
func TestTCPTunnel(t *testing.T) {

View file

@ -6,7 +6,7 @@ import (
"sync/atomic"
"time"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
const (
@ -75,13 +75,12 @@ type UDPEventLogger interface {
}
func (t *UDPTunnel) Serve(pc net.PacketConn) error {
t.m = make(map[string]*sessionEntry)
stopCh := make(chan struct{})
go t.idleCleanupLoop(stopCh)
defer close(stopCh)
defer t.cleanup(false)
t.m = make(map[string]*sessionEntry)
buf := make([]byte, udpBufferSize)
for {
n, addr, err := pc.ReadFrom(buf)

View file

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/app/v2/internal/utils_test"
"github.com/apernet/hysteria/app/internal/utils_test"
)
func TestUDPTunnel(t *testing.T) {

View file

@ -12,7 +12,7 @@ import (
"strings"
"time"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
const (
@ -278,11 +278,6 @@ func sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error
ProtoMinor: req.ProtoMinor,
Header: http.Header{},
}
// Remove the "Content-Length: 0" header, some clients (e.g. ffmpeg) may not like it.
resp.ContentLength = -1
// Also, prevent the "Connection: close" header.
resp.Close = false
resp.Uncompressed = true
return resp.Write(conn)
}

View file

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
const (

View file

@ -1,12 +0,0 @@
with-expecter: true
dir: internal/mocks
outpkg: mocks
packages:
net:
interfaces:
Listener:
config:
mockname: MockListener
Conn:
config:
mockname: MockConn

View file

@ -1,427 +0,0 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
time "time"
)
// MockConn is an autogenerated mock type for the Conn type
type MockConn struct {
mock.Mock
}
type MockConn_Expecter struct {
mock *mock.Mock
}
func (_m *MockConn) EXPECT() *MockConn_Expecter {
return &MockConn_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with given fields:
func (_m *MockConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockConn_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockConn_Expecter) Close() *MockConn_Close_Call {
return &MockConn_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call {
_c.Call.Return(run)
return _c
}
// LocalAddr provides a mock function with given fields:
func (_m *MockConn) LocalAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LocalAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr'
type MockConn_LocalAddr_Call struct {
*mock.Call
}
// LocalAddr is a helper method to define mock.On call
func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call {
return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")}
}
func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call {
_c.Call.Return(run)
return _c
}
// Read provides a mock function with given fields: b
func (_m *MockConn) Read(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(b)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
type MockConn_Read_Call struct {
*mock.Call
}
// Read is a helper method to define mock.On call
// - b []byte
func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call {
return &MockConn_Read_Call{Call: _e.mock.On("Read", b)}
}
func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call {
_c.Call.Return(run)
return _c
}
// RemoteAddr provides a mock function with given fields:
func (_m *MockConn) RemoteAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for RemoteAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr'
type MockConn_RemoteAddr_Call struct {
*mock.Call
}
// RemoteAddr is a helper method to define mock.On call
func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call {
return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")}
}
func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call {
_c.Call.Return(run)
return _c
}
// SetDeadline provides a mock function with given fields: t
func (_m *MockConn) SetDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'
type MockConn_SetDeadline_Call struct {
*mock.Call
}
// SetDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call {
return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)}
}
func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetReadDeadline provides a mock function with given fields: t
func (_m *MockConn) SetReadDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetReadDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'
type MockConn_SetReadDeadline_Call struct {
*mock.Call
}
// SetReadDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call {
return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)}
}
func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call {
_c.Call.Return(run)
return _c
}
// SetWriteDeadline provides a mock function with given fields: t
func (_m *MockConn) SetWriteDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetWriteDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'
type MockConn_SetWriteDeadline_Call struct {
*mock.Call
}
// SetWriteDeadline is a helper method to define mock.On call
// - t time.Time
func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call {
return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)}
}
func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Time))
})
return _c
}
func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call {
_c.Call.Return(run)
return _c
}
// Write provides a mock function with given fields: b
func (_m *MockConn) Write(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(b)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockConn_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - b []byte
func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call {
return &MockConn_Write_Call{Call: _e.mock.On("Write", b)}
}
func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte))
})
return _c
}
func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call {
_c.Call.Return(n, err)
return _c
}
func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call {
_c.Call.Return(run)
return _c
}
// NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockConn(t interface {
mock.TestingT
Cleanup(func())
}) *MockConn {
mock := &MockConn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -1,185 +0,0 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
import (
net "net"
mock "github.com/stretchr/testify/mock"
)
// MockListener is an autogenerated mock type for the Listener type
type MockListener struct {
mock.Mock
}
type MockListener_Expecter struct {
mock *mock.Mock
}
func (_m *MockListener) EXPECT() *MockListener_Expecter {
return &MockListener_Expecter{mock: &_m.Mock}
}
// Accept provides a mock function with given fields:
func (_m *MockListener) Accept() (net.Conn, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Accept")
}
var r0 net.Conn
var r1 error
if rf, ok := ret.Get(0).(func() (net.Conn, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() net.Conn); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Conn)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'
type MockListener_Accept_Call struct {
*mock.Call
}
// Accept is a helper method to define mock.On call
func (_e *MockListener_Expecter) Accept() *MockListener_Accept_Call {
return &MockListener_Accept_Call{Call: _e.mock.On("Accept")}
}
func (_c *MockListener_Accept_Call) Run(run func()) *MockListener_Accept_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockListener_Accept_Call) Return(_a0 net.Conn, _a1 error) *MockListener_Accept_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockListener_Accept_Call) RunAndReturn(run func() (net.Conn, error)) *MockListener_Accept_Call {
_c.Call.Return(run)
return _c
}
// Addr provides a mock function with given fields:
func (_m *MockListener) Addr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Addr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(net.Addr)
}
}
return r0
}
// MockListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'
type MockListener_Addr_Call struct {
*mock.Call
}
// Addr is a helper method to define mock.On call
func (_e *MockListener_Expecter) Addr() *MockListener_Addr_Call {
return &MockListener_Addr_Call{Call: _e.mock.On("Addr")}
}
func (_c *MockListener_Addr_Call) Run(run func()) *MockListener_Addr_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockListener_Addr_Call) Return(_a0 net.Addr) *MockListener_Addr_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockListener_Addr_Call) RunAndReturn(run func() net.Addr) *MockListener_Addr_Call {
_c.Call.Return(run)
return _c
}
// Close provides a mock function with given fields:
func (_m *MockListener) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MockListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockListener_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockListener_Expecter) Close() *MockListener_Close_Call {
return &MockListener_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockListener_Close_Call) Run(run func()) *MockListener_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockListener_Close_Call) Return(_a0 error) *MockListener_Close_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockListener_Close_Call) RunAndReturn(run func() error) *MockListener_Close_Call {
_c.Call.Return(run)
return _c
}
// NewMockListener creates a new instance of MockListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockListener(t interface {
mock.TestingT
Cleanup(func())
}) *MockListener {
mock := &MockListener{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -1,72 +0,0 @@
package proxymux
import (
"net"
"sync"
"github.com/apernet/hysteria/extras/v2/correctnet"
)
type muxManager struct {
listeners map[string]*muxListener
lock sync.Mutex
}
var globalMuxManager *muxManager
func init() {
globalMuxManager = &muxManager{
listeners: make(map[string]*muxListener),
}
}
func (m *muxManager) GetOrCreate(address string) (*muxListener, error) {
key, err := m.canonicalizeAddrPort(address)
if err != nil {
return nil, err
}
m.lock.Lock()
defer m.lock.Unlock()
if ml, ok := m.listeners[key]; ok {
return ml, nil
}
listener, err := correctnet.Listen("tcp", key)
if err != nil {
return nil, err
}
ml := newMuxListener(listener, func() {
m.lock.Lock()
defer m.lock.Unlock()
delete(m.listeners, key)
})
m.listeners[key] = ml
return ml, nil
}
func (m *muxManager) canonicalizeAddrPort(address string) (string, error) {
taddr, err := net.ResolveTCPAddr("tcp", address)
if err != nil {
return "", err
}
return taddr.String(), nil
}
func ListenHTTP(address string) (net.Listener, error) {
ml, err := globalMuxManager.GetOrCreate(address)
if err != nil {
return nil, err
}
return ml.ListenHTTP()
}
func ListenSOCKS(address string) (net.Listener, error) {
ml, err := globalMuxManager.GetOrCreate(address)
if err != nil {
return nil, err
}
return ml.ListenSOCKS()
}

View file

@ -1,110 +0,0 @@
package proxymux
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestListenSOCKS(t *testing.T) {
address := "127.2.39.129:11081"
sl, err := ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
defer func() {
sl.Close()
}()
hl, err := ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
defer hl.Close()
_, err = ListenSOCKS(address)
if !assert.ErrorIs(t, err, ErrProtocolInUse) {
return
}
sl.Close()
sl, err = ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
}
func TestListenHTTP(t *testing.T) {
address := "127.2.39.129:11082"
hl, err := ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
defer func() {
hl.Close()
}()
sl, err := ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
defer sl.Close()
_, err = ListenHTTP(address)
if !assert.ErrorIs(t, err, ErrProtocolInUse) {
return
}
hl.Close()
hl, err = ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
}
func TestRelease(t *testing.T) {
address := "127.2.39.129:11083"
hl, err := ListenHTTP(address)
if !assert.NoError(t, err) {
return
}
sl, err := ListenSOCKS(address)
if !assert.NoError(t, err) {
return
}
if !assert.True(t, globalMuxManager.testAddressExists(address)) {
return
}
_, err = net.Listen("tcp", address)
if !assert.Error(t, err) {
return
}
hl.Close()
sl.Close()
// Wait for muxListener released
time.Sleep(time.Second)
if !assert.False(t, globalMuxManager.testAddressExists(address)) {
return
}
lis, err := net.Listen("tcp", address)
if !assert.NoError(t, err) {
return
}
defer lis.Close()
}
func (m *muxManager) testAddressExists(address string) bool {
m.lock.Lock()
defer m.lock.Unlock()
_, ok := m.listeners[address]
return ok
}

View file

@ -1,320 +0,0 @@
package proxymux
import (
"errors"
"fmt"
"io"
"net"
"sync"
)
func newMuxListener(listener net.Listener, deleteFunc func()) *muxListener {
l := &muxListener{
base: listener,
acceptChan: make(chan net.Conn),
closeChan: make(chan struct{}),
deleteFunc: deleteFunc,
}
go l.acceptLoop()
go l.mainLoop()
return l
}
type muxListener struct {
lock sync.Mutex
base net.Listener
acceptErr error
acceptChan chan net.Conn
closeChan chan struct{}
socksListener *subListener
httpListener *subListener
deleteFunc func()
}
func (l *muxListener) acceptLoop() {
defer close(l.acceptChan)
for {
conn, err := l.base.Accept()
if err != nil {
l.lock.Lock()
l.acceptErr = err
l.lock.Unlock()
return
}
select {
case <-l.closeChan:
return
case l.acceptChan <- conn:
}
}
}
func (l *muxListener) mainLoop() {
defer func() {
l.deleteFunc()
l.base.Close()
close(l.closeChan)
l.lock.Lock()
defer l.lock.Unlock()
if sl := l.httpListener; sl != nil {
close(sl.acceptChan)
l.httpListener = nil
}
if sl := l.socksListener; sl != nil {
close(sl.acceptChan)
l.socksListener = nil
}
}()
for {
var socksCloseChan, httpCloseChan chan struct{}
if l.httpListener != nil {
httpCloseChan = l.httpListener.closeChan
}
if l.socksListener != nil {
socksCloseChan = l.socksListener.closeChan
}
select {
case <-l.closeChan:
return
case conn, ok := <-l.acceptChan:
if !ok {
return
}
go l.dispatch(conn)
case <-socksCloseChan:
l.lock.Lock()
if socksCloseChan == l.socksListener.closeChan {
// not replaced by another ListenSOCKS()
l.socksListener = nil
}
l.lock.Unlock()
if l.checkIdle() {
return
}
case <-httpCloseChan:
l.lock.Lock()
if httpCloseChan == l.httpListener.closeChan {
// not replaced by another ListenHTTP()
l.httpListener = nil
}
l.lock.Unlock()
if l.checkIdle() {
return
}
}
}
}
func (l *muxListener) dispatch(conn net.Conn) {
var b [1]byte
if _, err := io.ReadFull(conn, b[:]); err != nil {
conn.Close()
return
}
l.lock.Lock()
var target *subListener
if b[0] == 5 {
target = l.socksListener
} else {
target = l.httpListener
}
l.lock.Unlock()
if target == nil {
conn.Close()
return
}
wconn := &connWithOneByte{Conn: conn, b: b[0]}
select {
case <-target.closeChan:
case target.acceptChan <- wconn:
}
}
func (l *muxListener) checkIdle() bool {
l.lock.Lock()
defer l.lock.Unlock()
return l.httpListener == nil && l.socksListener == nil
}
func (l *muxListener) getAndClearAcceptError() error {
l.lock.Lock()
defer l.lock.Unlock()
if l.acceptErr == nil {
return nil
}
err := l.acceptErr
l.acceptErr = nil
return err
}
func (l *muxListener) ListenHTTP() (net.Listener, error) {
l.lock.Lock()
defer l.lock.Unlock()
if l.httpListener != nil {
subListenerPendingClosed := false
select {
case <-l.httpListener.closeChan:
subListenerPendingClosed = true
default:
}
if !subListenerPendingClosed {
return nil, OpErr{
Addr: l.base.Addr(),
Protocol: "http",
Op: "bind-protocol",
Err: ErrProtocolInUse,
}
}
l.httpListener = nil
}
select {
case <-l.closeChan:
return nil, net.ErrClosed
default:
}
sl := newSubListener(l.getAndClearAcceptError, l.base.Addr)
l.httpListener = sl
return sl, nil
}
func (l *muxListener) ListenSOCKS() (net.Listener, error) {
l.lock.Lock()
defer l.lock.Unlock()
if l.socksListener != nil {
subListenerPendingClosed := false
select {
case <-l.socksListener.closeChan:
subListenerPendingClosed = true
default:
}
if !subListenerPendingClosed {
return nil, OpErr{
Addr: l.base.Addr(),
Protocol: "socks",
Op: "bind-protocol",
Err: ErrProtocolInUse,
}
}
l.socksListener = nil
}
select {
case <-l.closeChan:
return nil, net.ErrClosed
default:
}
sl := newSubListener(l.getAndClearAcceptError, l.base.Addr)
l.socksListener = sl
return sl, nil
}
func newSubListener(acceptErrorFunc func() error, addrFunc func() net.Addr) *subListener {
return &subListener{
acceptChan: make(chan net.Conn),
acceptErrorFunc: acceptErrorFunc,
closeChan: make(chan struct{}),
addrFunc: addrFunc,
}
}
type subListener struct {
// receive connections or closure from upstream
acceptChan chan net.Conn
// get an error of Accept() from upstream
acceptErrorFunc func() error
// notify upstream that we are closed
closeChan chan struct{}
// Listener.Addr() implementation of base listener
addrFunc func() net.Addr
}
func (l *subListener) Accept() (net.Conn, error) {
select {
case <-l.closeChan:
// closed by ourselves
return nil, net.ErrClosed
case conn, ok := <-l.acceptChan:
if !ok {
// closed by upstream
if acceptErr := l.acceptErrorFunc(); acceptErr != nil {
return nil, acceptErr
}
return nil, net.ErrClosed
}
return conn, nil
}
}
func (l *subListener) Addr() net.Addr {
return l.addrFunc()
}
// Close implements net.Listener.Close.
// Upstream should use close(l.acceptChan) instead.
func (l *subListener) Close() error {
select {
case <-l.closeChan:
return nil
default:
}
close(l.closeChan)
return nil
}
// connWithOneByte is a net.Conn that returns b for the first read
// request, then forwards everything else to Conn.
type connWithOneByte struct {
net.Conn
b byte
bRead bool
}
func (c *connWithOneByte) Read(bs []byte) (int, error) {
if c.bRead {
return c.Conn.Read(bs)
}
if len(bs) == 0 {
return 0, nil
}
c.bRead = true
bs[0] = c.b
return 1, nil
}
type OpErr struct {
Addr net.Addr
Protocol string
Op string
Err error
}
func (m OpErr) Error() string {
return fmt.Sprintf("mux-listen: %s[%s]: %s: %v", m.Addr, m.Protocol, m.Op, m.Err)
}
func (m OpErr) Unwrap() error {
return m.Err
}
var ErrProtocolInUse = errors.New("protocol already in use")

View file

@ -1,154 +0,0 @@
package proxymux
import (
"bytes"
"net"
"sync"
"testing"
"time"
"github.com/apernet/hysteria/app/v2/internal/proxymux/internal/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
//go:generate mockery
func testMockListener(t *testing.T, connChan <-chan net.Conn) net.Listener {
closedChan := make(chan struct{})
mockListener := mocks.NewMockListener(t)
mockListener.EXPECT().Accept().RunAndReturn(func() (net.Conn, error) {
select {
case <-closedChan:
return nil, net.ErrClosed
case conn, ok := <-connChan:
if !ok {
panic("unexpected closed channel (connChan)")
}
return conn, nil
}
})
mockListener.EXPECT().Close().RunAndReturn(func() error {
select {
case <-closedChan:
default:
close(closedChan)
}
return nil
})
return mockListener
}
func testMockConn(t *testing.T, b []byte) net.Conn {
buf := bytes.NewReader(b)
isClosed := false
mockConn := mocks.NewMockConn(t)
mockConn.EXPECT().Read(mock.Anything).RunAndReturn(func(b []byte) (int, error) {
if isClosed {
return 0, net.ErrClosed
}
return buf.Read(b)
})
mockConn.EXPECT().Close().RunAndReturn(func() error {
isClosed = true
return nil
})
return mockConn
}
func TestMuxHTTP(t *testing.T) {
connChan := make(chan net.Conn)
mockListener := testMockListener(t, connChan)
mockConn := testMockConn(t, []byte("CONNECT example.com:443 HTTP/1.1\r\n\r\n"))
mux := newMuxListener(mockListener, func() {})
hl, err := mux.ListenHTTP()
if !assert.NoError(t, err) {
return
}
sl, err := mux.ListenSOCKS()
if !assert.NoError(t, err) {
return
}
connChan <- mockConn
var socksConn, httpConn net.Conn
var socksErr, httpErr error
var wg sync.WaitGroup
wg.Add(2)
go func() {
socksConn, socksErr = sl.Accept()
wg.Done()
}()
go func() {
httpConn, httpErr = hl.Accept()
wg.Done()
}()
time.Sleep(time.Second)
sl.Close()
hl.Close()
wg.Wait()
assert.Nil(t, socksConn)
assert.ErrorIs(t, socksErr, net.ErrClosed)
assert.NotNil(t, httpConn)
httpConn.Close()
assert.NoError(t, httpErr)
// Wait for muxListener released
<-mux.acceptChan
}
func TestMuxSOCKS(t *testing.T) {
connChan := make(chan net.Conn)
mockListener := testMockListener(t, connChan)
mockConn := testMockConn(t, []byte{0x05, 0x02, 0x00, 0x01}) // SOCKS5 Connect Request: NOAUTH+GSSAPI
mux := newMuxListener(mockListener, func() {})
hl, err := mux.ListenHTTP()
if !assert.NoError(t, err) {
return
}
sl, err := mux.ListenSOCKS()
if !assert.NoError(t, err) {
return
}
connChan <- mockConn
var socksConn, httpConn net.Conn
var socksErr, httpErr error
var wg sync.WaitGroup
wg.Add(2)
go func() {
socksConn, socksErr = sl.Accept()
wg.Done()
}()
go func() {
httpConn, httpErr = hl.Accept()
wg.Done()
}()
time.Sleep(time.Second)
sl.Close()
hl.Close()
wg.Wait()
assert.NotNil(t, socksConn)
socksConn.Close()
assert.NoError(t, socksErr)
assert.Nil(t, httpConn)
assert.ErrorIs(t, httpErr, net.ErrClosed)
// Wait for muxListener released
<-mux.acceptChan
}

View file

@ -8,7 +8,7 @@ import (
"syscall"
"unsafe"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
const (

View file

@ -6,7 +6,7 @@ import (
"errors"
"net"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
type TCPRedirect struct {

View file

@ -1,65 +0,0 @@
import socket
import array
import os
import struct
import sys
def serve(path):
try:
os.unlink(path)
except OSError:
if os.path.exists(path):
raise
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(path)
server.listen()
print(f"Listening on {path}")
try:
while True:
connection, client_address = server.accept()
print(f"Client connected")
try:
# Receiving fd from client
fds = array.array("i")
msg, ancdata, flags, addr = connection.recvmsg(1, socket.CMSG_LEN(struct.calcsize('i')))
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
fd = fds[0]
# We make a call to setsockopt(2) here, so client can verify we have received the fd
# In the real scenario, the server would set things like SO_MARK,
# we use SO_RCVBUF as it doesn't require any special capabilities.
nbytes = struct.pack("i", 2500)
fdsocket = fd_to_socket(fd)
fdsocket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, nbytes)
fdsocket.close()
# The only protocol-like thing specified in the client implementation.
connection.send(b'\x01')
finally:
connection.close()
print("Connection closed")
except KeyboardInterrupt:
print("Exit")
finally:
server.close()
os.unlink(path)
def fd_to_socket(fd):
return socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
if __name__ == "__main__":
if len(sys.argv) < 2:
raise ValueError("unix socket path is required")
serve(sys.argv[1])

View file

@ -1,76 +0,0 @@
package sockopts
import (
"fmt"
"net"
)
type SocketOptions struct {
BindInterface *string
FirewallMark *uint32
FdControlUnixSocket *string
}
// implemented in platform-specific files
var (
bindInterfaceFunc func(c *net.UDPConn, device string) error
firewallMarkFunc func(c *net.UDPConn, fwmark uint32) error
fdControlUnixSocketFunc func(c *net.UDPConn, path string) error
)
func (o *SocketOptions) CheckSupported() (err error) {
if o.BindInterface != nil && bindInterfaceFunc == nil {
return &UnsupportedError{"bindInterface"}
}
if o.FirewallMark != nil && firewallMarkFunc == nil {
return &UnsupportedError{"fwmark"}
}
if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc == nil {
return &UnsupportedError{"fdControlUnixSocket"}
}
return nil
}
type UnsupportedError struct {
Field string
}
func (e *UnsupportedError) Error() string {
return fmt.Sprintf("%s is not supported on this platform", e.Field)
}
func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) {
uconn, err = net.ListenUDP("udp", nil)
if err != nil {
return
}
err = o.applyToUDPConn(uconn.(*net.UDPConn))
if err != nil {
uconn.Close()
uconn = nil
return
}
return
}
func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error {
if o.BindInterface != nil && bindInterfaceFunc != nil {
err := bindInterfaceFunc(c, *o.BindInterface)
if err != nil {
return fmt.Errorf("failed to bind to interface: %w", err)
}
}
if o.FirewallMark != nil && firewallMarkFunc != nil {
err := firewallMarkFunc(c, *o.FirewallMark)
if err != nil {
return fmt.Errorf("failed to set fwmark: %w", err)
}
}
if o.FdControlUnixSocket != nil && fdControlUnixSocketFunc != nil {
err := fdControlUnixSocketFunc(c, *o.FdControlUnixSocket)
if err != nil {
return fmt.Errorf("failed to send fd to control unix socket: %w", err)
}
}
return nil
}

View file

@ -1,96 +0,0 @@
//go:build linux
package sockopts
import (
"fmt"
"net"
"time"
"golang.org/x/exp/constraints"
"golang.org/x/sys/unix"
)
const (
fdControlUnixTimeout = 3 * time.Second
)
func init() {
bindInterfaceFunc = bindInterfaceImpl
firewallMarkFunc = firewallMarkImpl
fdControlUnixSocketFunc = fdControlUnixSocketImpl
}
func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) {
rconn, err := c.SyscallConn()
if err != nil {
return
}
cerr := rconn.Control(func(fd uintptr) {
err = cb(int(fd))
})
if err != nil {
return
}
if cerr != nil {
err = fmt.Errorf("failed to control fd: %w", cerr)
return
}
return
}
func bindInterfaceImpl(c *net.UDPConn, device string) error {
return controlUDPConn(c, func(fd int) error {
return unix.BindToDevice(fd, device)
})
}
func firewallMarkImpl(c *net.UDPConn, fwmark uint32) error {
return controlUDPConn(c, func(fd int) error {
return unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, int(fwmark))
})
}
func fdControlUnixSocketImpl(c *net.UDPConn, path string) error {
return controlUDPConn(c, func(fd int) error {
socketFd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
return fmt.Errorf("failed to create unix socket: %w", err)
}
defer unix.Close(socketFd)
var timeout unix.Timeval
timeUsec := fdControlUnixTimeout.Microseconds()
castAssignInteger(timeUsec/1e6, &timeout.Sec)
// Specifying the type explicitly is not necessary here, but it makes GoLand happy.
castAssignInteger[int64](timeUsec%1e6, &timeout.Usec)
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &timeout)
_ = unix.SetsockoptTimeval(socketFd, unix.SOL_SOCKET, unix.SO_SNDTIMEO, &timeout)
err = unix.Connect(socketFd, &unix.SockaddrUnix{Name: path})
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
err = unix.Sendmsg(socketFd, nil, unix.UnixRights(fd), nil, 0)
if err != nil {
return fmt.Errorf("failed to send: %w", err)
}
dummy := []byte{1}
n, err := unix.Read(socketFd, dummy)
if err != nil {
return fmt.Errorf("failed to receive: %w", err)
}
if n != 1 {
return fmt.Errorf("socket closed unexpectedly")
}
return nil
})
}
func castAssignInteger[F, T constraints.Integer](from F, to *T) {
*to = T(from)
}

View file

@ -1,53 +0,0 @@
//go:build linux
package sockopts
import (
"net"
"os"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
"golang.org/x/sys/unix"
)
func Test_fdControlUnixSocketImpl(t *testing.T) {
sockPath := "./fd_control_unix_socket_test.sock"
defer os.Remove(sockPath)
// Run test server
cmd := exec.Command("python", "fd_control_unix_socket_test.py", sockPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if !assert.NoError(t, err) {
return
}
defer cmd.Process.Kill()
// Wait for the server to start
time.Sleep(1 * time.Second)
so := SocketOptions{
FdControlUnixSocket: &sockPath,
}
conn, err := so.ListenUDP()
if !assert.NoError(t, err) {
return
}
defer conn.Close()
err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) {
rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF)
if err != nil {
return
}
// The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500),
// and kernel will double this value for getsockopt().
assert.Equal(t, 5000, rcvbuf)
return
})
assert.NoError(t, err)
}

View file

@ -7,7 +7,7 @@ import (
"github.com/txthinking/socks5"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
const udpBufferSize = 4096

View file

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/apernet/hysteria/app/v2/internal/utils_test"
"github.com/apernet/hysteria/app/internal/utils_test"
)
func TestServer(t *testing.T) {

View file

@ -5,7 +5,7 @@ import (
"net"
"github.com/apernet/go-tproxy"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
type TCPTProxy struct {

View file

@ -6,7 +6,7 @@ import (
"errors"
"net"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
type TCPTProxy struct {

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/apernet/go-tproxy"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
const (

View file

@ -7,7 +7,7 @@ import (
"net"
"time"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
type UDPTProxy struct {

View file

@ -1,14 +0,0 @@
//go:build !unix && !windows
package tun
import "net"
func isIPv6Supported() bool {
lis, err := net.ListenPacket("udp6", "[::1]:0")
if err != nil {
return false
}
_ = lis.Close()
return true
}

View file

@ -1,16 +0,0 @@
//go:build unix
package tun
import (
"golang.org/x/sys/unix"
)
func isIPv6Supported() bool {
sock, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
if err != nil {
return false
}
_ = unix.Close(sock)
return true
}

View file

@ -1,24 +0,0 @@
//go:build windows
package tun
import (
"golang.org/x/sys/windows"
)
func isIPv6Supported() bool {
var wsaData windows.WSAData
err := windows.WSAStartup(uint32(0x202), &wsaData)
if err != nil {
// Failing silently: it is not our duty to report such errors
return true
}
defer windows.WSACleanup()
sock, err := windows.Socket(windows.AF_INET6, windows.SOCK_DGRAM, windows.IPPROTO_UDP)
if err != nil {
return false
}
_ = windows.Closesocket(sock)
return true
}

View file

@ -1,77 +0,0 @@
package tun
import (
"github.com/sagernet/sing/common/logger"
"go.uber.org/zap"
)
var _ logger.Logger = (*singLogger)(nil)
type singLogger struct {
tag string
zapLogger *zap.Logger
}
func extractSingExceptions(args []any) {
for i, arg := range args {
if err, ok := arg.(error); ok {
args[i] = err.Error()
}
}
}
func (l *singLogger) Trace(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Debug(l.tag, zap.Any("args", args))
}
func (l *singLogger) Debug(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Debug(l.tag, zap.Any("args", args))
}
func (l *singLogger) Info(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Info(l.tag, zap.Any("args", args))
}
func (l *singLogger) Warn(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Warn(l.tag, zap.Any("args", args))
}
func (l *singLogger) Error(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Error(l.tag, zap.Any("args", args))
}
func (l *singLogger) Fatal(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Fatal(l.tag, zap.Any("args", args))
}
func (l *singLogger) Panic(args ...any) {
if l.zapLogger == nil {
return
}
extractSingExceptions(args)
l.zapLogger.Panic(l.tag, zap.Any("args", args))
}

View file

@ -1,234 +0,0 @@
package tun
import (
"context"
"fmt"
"io"
"net"
"net/netip"
tun "github.com/apernet/sing-tun"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
"github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/network"
"go.uber.org/zap"
"github.com/apernet/hysteria/core/v2/client"
)
type Server struct {
HyClient client.Client
EventLogger EventLogger
// for debugging
Logger *zap.Logger
IfName string
MTU uint32
Timeout int64 // in seconds, also applied to TCP in system stack
// required by system stack
Inet4Address []netip.Prefix
Inet6Address []netip.Prefix
// auto route
AutoRoute bool
StructRoute bool
Inet4RouteAddress []netip.Prefix
Inet6RouteAddress []netip.Prefix
Inet4RouteExcludeAddress []netip.Prefix
Inet6RouteExcludeAddress []netip.Prefix
}
type EventLogger interface {
TCPRequest(addr, reqAddr string)
TCPError(addr, reqAddr string, err error)
UDPRequest(addr string)
UDPError(addr string, err error)
}
func (s *Server) Serve() error {
if !isIPv6Supported() {
s.Logger.Warn("tun-pre-check", zap.String("msg", "IPv6 is not supported or enabled on this system, TUN device is created without IPv6 support."))
s.Inet6Address = nil
}
tunOpts := tun.Options{
Name: s.IfName,
Inet4Address: s.Inet4Address,
Inet6Address: s.Inet6Address,
MTU: s.MTU,
GSO: true,
AutoRoute: s.AutoRoute,
StrictRoute: s.StructRoute,
Inet4RouteAddress: s.Inet4RouteAddress,
Inet6RouteAddress: s.Inet6RouteAddress,
Inet4RouteExcludeAddress: s.Inet4RouteExcludeAddress,
Inet6RouteExcludeAddress: s.Inet6RouteExcludeAddress,
Logger: &singLogger{
tag: "tun",
zapLogger: s.Logger,
},
}
tunIf, err := tun.New(tunOpts)
if err != nil {
return fmt.Errorf("failed to create tun interface: %w", err)
}
defer tunIf.Close()
tunStack, err := tun.NewSystem(tun.StackOptions{
Context: context.Background(),
Tun: tunIf,
TunOptions: tunOpts,
UDPTimeout: s.Timeout,
Handler: &tunHandler{s},
Logger: &singLogger{
tag: "tun-stack",
zapLogger: s.Logger,
},
ForwarderBindInterface: true,
InterfaceFinder: &interfaceFinder{},
})
if err != nil {
return fmt.Errorf("failed to create tun stack: %w", err)
}
defer tunStack.Close()
return tunStack.(tun.StackRunner).Run()
}
type tunHandler struct {
*Server
}
var _ tun.Handler = (*tunHandler)(nil)
func (t *tunHandler) NewConnection(ctx context.Context, conn net.Conn, m metadata.Metadata) error {
addr := m.Source.String()
reqAddr := m.Destination.String()
if t.EventLogger != nil {
t.EventLogger.TCPRequest(addr, reqAddr)
}
var closeErr error
defer func() {
if t.EventLogger != nil {
t.EventLogger.TCPError(addr, reqAddr, closeErr)
}
}()
rc, err := t.HyClient.TCP(reqAddr)
if err != nil {
closeErr = err
// the returned err is ignored by caller
return nil
}
defer rc.Close()
// start forwarding
copyErrChan := make(chan error, 3)
go func() {
<-ctx.Done()
copyErrChan <- ctx.Err()
}()
go func() {
_, copyErr := io.Copy(rc, conn)
copyErrChan <- copyErr
}()
go func() {
_, copyErr := io.Copy(conn, rc)
copyErrChan <- copyErr
}()
closeErr = <-copyErrChan
return nil
}
func (t *tunHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, m metadata.Metadata) error {
addr := m.Source.String()
if t.EventLogger != nil {
t.EventLogger.UDPRequest(addr)
}
var closeErr error
defer func() {
if t.EventLogger != nil {
t.EventLogger.UDPError(addr, closeErr)
}
}()
rc, err := t.HyClient.UDP()
if err != nil {
closeErr = err
// the returned err is simply called into NewError again
return nil
}
defer rc.Close()
// start forwarding
copyErrChan := make(chan error, 3)
go func() {
<-ctx.Done()
copyErrChan <- ctx.Err()
}()
// local <- remote
go func() {
for {
bs, from, err := rc.Receive()
if err != nil {
copyErrChan <- err
return
}
var fromAddr metadata.Socksaddr
if ap, perr := netip.ParseAddrPort(from); perr == nil {
fromAddr = metadata.SocksaddrFromNetIP(ap)
} else {
fromAddr.Fqdn = from
}
err = conn.WritePacket(buf.As(bs), fromAddr)
if err != nil {
copyErrChan <- err
return
}
}
}()
// local -> remote
go func() {
buffer := buf.NewPacket()
defer buffer.Release()
for {
buffer.Reset()
addr, err := conn.ReadPacket(buffer)
if err != nil {
copyErrChan <- err
return
}
err = rc.Send(buffer.Bytes(), addr.String())
if err != nil {
copyErrChan <- err
return
}
}
}()
closeErr = <-copyErrChan
return nil
}
func (t *tunHandler) NewError(ctx context.Context, err error) {
// unused
}
type interfaceFinder struct{}
var _ control.InterfaceFinder = (*interfaceFinder)(nil)
func (f *interfaceFinder) InterfaceIndexByName(name string) (int, error) {
ifce, err := net.InterfaceByName(name)
if err != nil {
return -1, err
}
return ifce.Index, nil
}
func (f *interfaceFinder) InterfaceNameByIndex(index int) (string, error) {
ifce, err := net.InterfaceByIndex(index)
if err != nil {
return "", err
}
return ifce.Name, nil
}

View file

@ -8,11 +8,11 @@ import (
)
const (
Byte = 1
Kilobyte = Byte * 1000
Megabyte = Kilobyte * 1000
Gigabyte = Megabyte * 1000
Terabyte = Gigabyte * 1000
Byte = 1.0 << (10 * iota)
Kilobyte
Megabyte
Gigabyte
Terabyte
)
// StringToBps converts a string to a bandwidth value in bytes per second.

View file

@ -13,12 +13,12 @@ func TestStringToBps(t *testing.T) {
wantErr bool
}{
{"bps", args{"800 bps"}, 100, false},
{"kbps", args{"800 kbps"}, 100_000, false},
{"mbps", args{"800 mbps"}, 100_000_000, false},
{"gbps", args{"800 gbps"}, 100_000_000_000, false},
{"tbps", args{"800 tbps"}, 100_000_000_000_000, false},
{"mbps simp", args{"100m"}, 12_500_000, false},
{"gbps simp upper", args{"2G"}, 250_000_000, false},
{"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},

View file

@ -1,198 +0,0 @@
package utils
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"strings"
"sync"
"sync/atomic"
"time"
)
type LocalCertificateLoader struct {
CertFile string
KeyFile string
SNIGuard SNIGuardFunc
lock sync.Mutex
cache atomic.Pointer[localCertificateCache]
}
type SNIGuardFunc func(info *tls.ClientHelloInfo, cert *tls.Certificate) error
// localCertificateCache holds the certificate and its mod times.
// this struct is designed to be read-only.
//
// to update the cache, use LocalCertificateLoader.makeCache and
// update the LocalCertificateLoader.cache field.
type localCertificateCache struct {
certificate *tls.Certificate
certModTime time.Time
keyModTime time.Time
}
func (l *LocalCertificateLoader) InitializeCache() error {
l.lock.Lock()
defer l.lock.Unlock()
cache, err := l.makeCache()
if err != nil {
return err
}
l.cache.Store(cache)
return nil
}
func (l *LocalCertificateLoader) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := l.getCertificateWithCache()
if err != nil {
return nil, err
}
if l.SNIGuard == nil {
return cert, nil
}
err = l.SNIGuard(info, cert)
if err != nil {
return nil, err
}
return cert, nil
}
func (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Time, err error) {
fi, err := os.Stat(l.CertFile)
if err != nil {
err = fmt.Errorf("failed to stat certificate file: %w", err)
return
}
certModTime = fi.ModTime()
fi, err = os.Stat(l.KeyFile)
if err != nil {
err = fmt.Errorf("failed to stat key file: %w", err)
return
}
keyModTime = fi.ModTime()
return
}
func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) {
c := &localCertificateCache{}
c.certModTime, c.keyModTime, err = l.checkModTime()
if err != nil {
return
}
cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile)
if err != nil {
return
}
c.certificate = &cert
if c.certificate.Leaf == nil {
// certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23
c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return
}
}
cache = c
return
}
func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) {
cache := l.cache.Load()
certModTime, keyModTime, terr := l.checkModTime()
if terr != nil {
if cache != nil {
// use cache when file is temporarily unavailable
return cache.certificate, nil
}
return nil, terr
}
if cache != nil && cache.certModTime.Equal(certModTime) && cache.keyModTime.Equal(keyModTime) {
// cache is up-to-date
return cache.certificate, nil
}
if cache != nil {
if !l.lock.TryLock() {
// another goroutine is updating the cache
return cache.certificate, nil
}
} else {
l.lock.Lock()
}
defer l.lock.Unlock()
if l.cache.Load() != cache {
// another goroutine updated the cache
return l.cache.Load().certificate, nil
}
newCache, err := l.makeCache()
if err != nil {
if cache != nil {
// use cache when loading failed
return cache.certificate, nil
}
return nil, err
}
l.cache.Store(newCache)
return newCache.certificate, nil
}
// getNameFromClientHello returns a normalized form of hello.ServerName.
// If hello.ServerName is empty (i.e. client did not use SNI), then the
// associated connection's local address is used to extract an IP address.
//
// ref: https://github.com/caddyserver/certmagic/blob/3bad5b6bb595b09c14bd86ff0b365d302faaf5e2/handshake.go#L838
func getNameFromClientHello(hello *tls.ClientHelloInfo) string {
normalizedName := func(serverName string) string {
return strings.ToLower(strings.TrimSpace(serverName))
}
localIPFromConn := func(c net.Conn) string {
if c == nil {
return ""
}
localAddr := c.LocalAddr().String()
ip, _, err := net.SplitHostPort(localAddr)
if err != nil {
ip = localAddr
}
if scopeIDStart := strings.Index(ip, "%"); scopeIDStart > -1 {
ip = ip[:scopeIDStart]
}
return ip
}
if name := normalizedName(hello.ServerName); name != "" {
return name
}
return localIPFromConn(hello.Conn)
}
func SNIGuardDNSSAN(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
if len(cert.Leaf.DNSNames) == 0 {
return nil
}
return SNIGuardStrict(info, cert)
}
func SNIGuardStrict(info *tls.ClientHelloInfo, cert *tls.Certificate) error {
hostname := getNameFromClientHello(info)
err := cert.Leaf.VerifyHostname(hostname)
if err != nil {
return fmt.Errorf("sni guard: %w", err)
}
return nil
}

View file

@ -1,139 +0,0 @@
package utils
import (
"crypto/tls"
"log"
"net/http"
"os"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const (
testListen = "127.82.39.147:12947"
testCAFile = "./testcerts/ca"
testCertFile = "./testcerts/cert"
testKeyFile = "./testcerts/key"
)
func TestCertificateLoaderPathError(t *testing.T) {
assert.NoError(t, os.RemoveAll(testCertFile))
assert.NoError(t, os.RemoveAll(testKeyFile))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
err := loader.InitializeCache()
var pathErr *os.PathError
assert.ErrorAs(t, err, &pathErr)
}
func TestCertificateLoaderFullChain(t *testing.T) {
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.Error(t, runTestTLSClient("unmatched-sni.example.com"))
assert.Error(t, runTestTLSClient(""))
assert.NoError(t, runTestTLSClient("example.com"))
}
func TestCertificateLoaderNoSAN(t *testing.T) {
assert.NoError(t, generateTestCertificate(nil, "selfsign"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardDNSSAN,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.NoError(t, runTestTLSClient(""))
}
func TestCertificateLoaderReplaceCertificate(t *testing.T) {
assert.NoError(t, generateTestCertificate([]string{"example.com"}, "fullchain"))
loader := LocalCertificateLoader{
CertFile: testCertFile,
KeyFile: testKeyFile,
SNIGuard: SNIGuardStrict,
}
assert.NoError(t, loader.InitializeCache())
lis, err := tls.Listen("tcp", testListen, &tls.Config{
GetCertificate: loader.GetCertificate,
})
assert.NoError(t, err)
defer lis.Close()
go http.Serve(lis, nil)
assert.NoError(t, runTestTLSClient("example.com"))
assert.Error(t, runTestTLSClient("2.example.com"))
assert.NoError(t, generateTestCertificate([]string{"2.example.com"}, "fullchain"))
assert.Error(t, runTestTLSClient("example.com"))
assert.NoError(t, runTestTLSClient("2.example.com"))
}
func generateTestCertificate(dnssan []string, certType string) error {
args := []string{
"certloader_test_gencert.py",
"--ca", testCAFile,
"--cert", testCertFile,
"--key", testKeyFile,
"--type", certType,
}
if len(dnssan) > 0 {
args = append(args, "--dnssan", strings.Join(dnssan, ","))
}
cmd := exec.Command("python", args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Failed to generate test certificate: %s", out)
return err
}
return nil
}
func runTestTLSClient(sni string) error {
args := []string{
"certloader_test_tlsclient.py",
"--server", testListen,
"--ca", testCAFile,
}
if sni != "" {
args = append(args, "--sni", sni)
}
cmd := exec.Command("python", args...)
out, err := cmd.CombinedOutput()
if err != nil {
log.Printf("Failed to run test TLS client: %s", out)
return err
}
return nil
}

View file

@ -1,134 +0,0 @@
import argparse
import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
def create_key():
return ec.generate_private_key(ec.SECP256R1())
def create_certificate(cert_type, subject, issuer, private_key, public_key, dns_san=None):
serial_number = x509.random_serial_number()
not_valid_before = datetime.datetime.now(datetime.UTC)
not_valid_after = not_valid_before + datetime.timedelta(days=365)
subject_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, subject.get('C', 'ZZ')),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject.get('O', 'No Organization')),
x509.NameAttribute(NameOID.COMMON_NAME, subject.get('CN', 'No CommonName')),
])
issuer_name = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, issuer.get('C', 'ZZ')),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, issuer.get('O', 'No Organization')),
x509.NameAttribute(NameOID.COMMON_NAME, issuer.get('CN', 'No CommonName')),
])
builder = x509.CertificateBuilder()
builder = builder.subject_name(subject_name)
builder = builder.issuer_name(issuer_name)
builder = builder.public_key(public_key)
builder = builder.serial_number(serial_number)
builder = builder.not_valid_before(not_valid_before)
builder = builder.not_valid_after(not_valid_after)
if cert_type == 'root':
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=None), critical=True
)
elif cert_type == 'intermediate':
builder = builder.add_extension(
x509.BasicConstraints(ca=True, path_length=0), critical=True
)
elif cert_type == 'leaf':
builder = builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None), critical=True
)
else:
raise ValueError(f'Invalid cert_type: {cert_type}')
if dns_san:
builder = builder.add_extension(
x509.SubjectAlternativeName([x509.DNSName(d) for d in dns_san.split(',')]),
critical=False
)
return builder.sign(private_key=private_key, algorithm=hashes.SHA256())
def main():
parser = argparse.ArgumentParser(description='Generate HTTPS server certificate.')
parser.add_argument('--ca', required=True,
help='Path to write the X509 CA certificate in PEM format')
parser.add_argument('--cert', required=True,
help='Path to write the X509 certificate in PEM format')
parser.add_argument('--key', required=True,
help='Path to write the private key in PEM format')
parser.add_argument('--dnssan', required=False, default=None,
help='Comma-separated list of DNS SANs')
parser.add_argument('--type', required=True, choices=['selfsign', 'fullchain'],
help='Type of certificate to generate')
args = parser.parse_args()
key = create_key()
public_key = key.public_key()
if args.type == 'selfsign':
subject = {"C": "ZZ", "O": "Certificate", "CN": "Certificate"}
cert = create_certificate(
cert_type='root',
subject=subject,
issuer=subject,
private_key=key,
public_key=public_key,
dns_san=args.dnssan)
with open(args.ca, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
with open(args.cert, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
with open(args.key, 'wb') as f:
f.write(
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
elif args.type == 'fullchain':
ca_key = create_key()
ca_public_key = ca_key.public_key()
ca_subject = {"C": "ZZ", "O": "Root CA", "CN": "Root CA"}
ca_cert = create_certificate(
cert_type='root',
subject=ca_subject,
issuer=ca_subject,
private_key=ca_key,
public_key=ca_public_key)
intermediate_key = create_key()
intermediate_public_key = intermediate_key.public_key()
intermediate_subject = {"C": "ZZ", "O": "Intermediate CA", "CN": "Intermediate CA"}
intermediate_cert = create_certificate(
cert_type='intermediate',
subject=intermediate_subject,
issuer=ca_subject,
private_key=ca_key,
public_key=intermediate_public_key)
leaf_subject = {"C": "ZZ", "O": "Leaf Certificate", "CN": "Leaf Certificate"}
cert = create_certificate(
cert_type='leaf',
subject=leaf_subject,
issuer=intermediate_subject,
private_key=intermediate_key,
public_key=public_key,
dns_san=args.dnssan)
with open(args.ca, 'wb') as f:
f.write(ca_cert.public_bytes(Encoding.PEM))
with open(args.cert, 'wb') as f:
f.write(cert.public_bytes(Encoding.PEM))
f.write(intermediate_cert.public_bytes(Encoding.PEM))
with open(args.key, 'wb') as f:
f.write(
key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
if __name__ == "__main__":
main()

View file

@ -1,60 +0,0 @@
import argparse
import ssl
import socket
import sys
def check_tls(server, ca_cert, sni, alpn):
try:
host, port = server.split(":")
port = int(port)
if ca_cert:
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=ca_cert)
context.check_hostname = sni is not None
context.verify_mode = ssl.CERT_REQUIRED
else:
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
if alpn:
context.set_alpn_protocols([p for p in alpn.split(",")])
with socket.create_connection((host, port)) as sock:
with context.wrap_socket(sock, server_hostname=sni) as ssock:
# Verify handshake and certificate
print(f'Connected to {ssock.version()} using {ssock.cipher()}')
print(f'Server certificate validated and details: {ssock.getpeercert()}')
print("OK")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
def main():
parser = argparse.ArgumentParser(description="Test TLS Server")
parser.add_argument("--server", required=True,
help="Server address to test (e.g., 127.1.2.3:8443)")
parser.add_argument("--ca", required=False, default=None,
help="CA certificate file used to validate the server certificate"
"Omit to use insecure connection")
parser.add_argument("--sni", required=False, default=None,
help="SNI to send in ClientHello")
parser.add_argument("--alpn", required=False, default='h2',
help="ALPN to send in ClientHello")
args = parser.parse_args()
exit_status = check_tls(
server=args.server,
ca_cert=args.ca,
sni=args.sni,
alpn=args.alpn)
sys.exit(exit_status)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,70 @@
package utils
import (
"io"
"net/http"
"os"
"github.com/oschwald/geoip2-golang"
)
const (
geoipDefaultFilename = "GeoLite2-Country.mmdb"
geoipDownloadURL = "https://git.io/GeoLite2-Country.mmdb"
)
// GeoIPLoader provides the on-demand GeoIP database loading function required by the ACL engine.
type GeoIPLoader struct {
Filename string
DownloadFunc func(filename, url string) // Called when downloading the GeoIP database.
DownloadErrFunc func(err error) // Called when downloading the GeoIP database succeeds/fails.
db *geoip2.Reader
}
func (l *GeoIPLoader) download() error {
resp, err := http.Get(geoipDownloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
f, err := os.Create(geoipDefaultFilename)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func (l *GeoIPLoader) Load() *geoip2.Reader {
if l.db == nil {
if l.Filename == "" {
// Filename not specified, try default.
if _, err := os.Stat(geoipDefaultFilename); err == nil {
// Default already exists, just use it.
l.Filename = geoipDefaultFilename
} else if os.IsNotExist(err) {
// Default doesn't exist, download it.
l.DownloadFunc(geoipDefaultFilename, geoipDownloadURL)
err := l.download()
l.DownloadErrFunc(err)
if err != nil {
return nil
}
l.Filename = geoipDefaultFilename
} else {
// Other error
return nil
}
}
db, err := geoip2.Open(l.Filename)
if err != nil {
return nil
}
l.db = db
}
return l.db
}

View file

@ -1,172 +0,0 @@
package utils
import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/apernet/hysteria/extras/v2/outbounds/acl"
"github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
)
const (
geoipFilename = "geoip.dat"
geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
geositeFilename = "geosite.dat"
geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
geoDlTmpPattern = ".hysteria-geoloader.dlpart.*"
geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days
)
var _ acl.GeoLoader = (*GeoLoader)(nil)
// GeoLoader provides the on-demand GeoIP/GeoSite database
// loading functionality required by the ACL engine.
// Empty filenames = automatic download from built-in URLs.
type GeoLoader struct {
GeoIPFilename string
GeoSiteFilename string
UpdateInterval time.Duration
DownloadFunc func(filename, url string)
DownloadErrFunc func(err error)
geoipMap map[string]*v2geo.GeoIP
geositeMap map[string]*v2geo.GeoSite
}
func (l *GeoLoader) shouldDownload(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return true
}
if info.Size() == 0 {
// empty files are loadable by v2geo, but we consider it broken
return true
}
dt := time.Now().Sub(info.ModTime())
if l.UpdateInterval == 0 {
return dt > geoDefaultUpdateInterval
} else {
return dt > l.UpdateInterval
}
}
func (l *GeoLoader) downloadAndCheck(filename, url string, checkFunc func(filename string) error) error {
l.DownloadFunc(filename, url)
resp, err := http.Get(url)
if err != nil {
l.DownloadErrFunc(err)
return err
}
defer resp.Body.Close()
f, err := os.CreateTemp(".", geoDlTmpPattern)
if err != nil {
l.DownloadErrFunc(err)
return err
}
defer os.Remove(f.Name())
_, err = io.Copy(f, resp.Body)
if err != nil {
f.Close()
l.DownloadErrFunc(err)
return err
}
f.Close()
err = checkFunc(f.Name())
if err != nil {
l.DownloadErrFunc(fmt.Errorf("integrity check failed: %w", err))
return err
}
err = os.Rename(f.Name(), filename)
if err != nil {
l.DownloadErrFunc(fmt.Errorf("rename failed: %w", err))
return err
}
return nil
}
func (l *GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
if l.geoipMap != nil {
return l.geoipMap, nil
}
autoDL := false
filename := l.GeoIPFilename
if filename == "" {
autoDL = true
filename = geoipFilename
}
if autoDL {
if !l.shouldDownload(filename) {
m, err := v2geo.LoadGeoIP(filename)
if err == nil {
l.geoipMap = m
return m, nil
}
// file is broken, download it again
}
err := l.downloadAndCheck(filename, geoipURL, func(filename string) error {
_, err := v2geo.LoadGeoIP(filename)
return err
})
if err != nil {
// as long as the previous download exists, fallback to it
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
return nil, err
}
}
}
m, err := v2geo.LoadGeoIP(filename)
if err != nil {
return nil, err
}
l.geoipMap = m
return m, nil
}
func (l *GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
if l.geositeMap != nil {
return l.geositeMap, nil
}
autoDL := false
filename := l.GeoSiteFilename
if filename == "" {
autoDL = true
filename = geositeFilename
}
if autoDL {
if !l.shouldDownload(filename) {
m, err := v2geo.LoadGeoSite(filename)
if err == nil {
l.geositeMap = m
return m, nil
}
// file is broken, download it again
}
err := l.downloadAndCheck(filename, geositeURL, func(filename string) error {
_, err := v2geo.LoadGeoSite(filename)
return err
})
if err != nil {
// as long as the previous download exists, fallback to it
if _, serr := os.Stat(filename); os.IsNotExist(serr) {
return nil, err
}
}
}
m, err := v2geo.LoadGeoSite(filename)
if err != nil {
return nil, err
}
l.geositeMap = m
return m, nil
}

View file

@ -1,3 +0,0 @@
# This directory is used for certificate generation in certloader_test.go
/*
!/.gitignore

View file

@ -8,7 +8,7 @@ import (
"net/http"
"time"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
const (

View file

@ -5,7 +5,7 @@ import (
"net"
"time"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/client"
)
type MockEchoHyClient struct{}

View file

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

View file

@ -1,7 +0,0 @@
Copyright 2023 Toby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -2,7 +2,7 @@ with-expecter: true
inpackage: true
dir: .
packages:
github.com/apernet/hysteria/core/v2/client:
github.com/apernet/hysteria/core/client:
interfaces:
udpIO:
config:

View file

@ -3,16 +3,15 @@ package client
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"net/url"
"time"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/congestion"
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/v2/internal/utils"
coreErrs "github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/internal/congestion"
"github.com/apernet/hysteria/core/internal/protocol"
"github.com/apernet/hysteria/core/internal/utils"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
@ -35,23 +34,17 @@ type HyUDPConn interface {
Close() error
}
type HandshakeInfo struct {
UDPEnabled bool
Tx uint64 // 0 if using BBR
}
func NewClient(config *Config) (Client, *HandshakeInfo, error) {
func NewClient(config *Config) (Client, error) {
if err := config.verifyAndFill(); err != nil {
return nil, nil, err
return nil, err
}
c := &clientImpl{
config: config,
}
info, err := c.connect()
if err != nil {
return nil, nil, err
if err := c.connect(); err != nil {
return nil, err
}
return c, info, nil
return c, nil
}
type clientImpl struct {
@ -63,10 +56,10 @@ type clientImpl struct {
udpSM *udpSessionManager
}
func (c *clientImpl) connect() (*HandshakeInfo, error) {
func (c *clientImpl) connect() error {
pktConn, err := c.config.ConnFactory.New(c.config.ServerAddr)
if err != nil {
return nil, err
return err
}
// Convert config to TLS config & QUIC config
tlsConfig := &tls.Config{
@ -84,13 +77,13 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) {
KeepAlivePeriod: c.config.QUICConfig.KeepAlivePeriod,
DisablePathMTUDiscovery: c.config.QUICConfig.DisablePathMTUDiscovery,
EnableDatagrams: true,
DisablePathManager: true,
}
// Prepare RoundTripper
var conn quic.EarlyConnection
rt := &http3.RoundTripper{
EnableDatagrams: true,
TLSClientConfig: tlsConfig,
QUICConfig: quicConfig,
QuicConfig: quicConfig,
Dial: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
qc, err := quic.DialEarly(ctx, pktConn, c.config.ServerAddr, tlsCfg, cfg)
if err != nil {
@ -120,23 +113,22 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) {
_ = conn.CloseWithError(closeErrCodeProtocolError, "")
}
_ = pktConn.Close()
return nil, coreErrs.ConnectError{Err: err}
return coreErrs.ConnectError{Err: err}
}
if resp.StatusCode != protocol.StatusAuthOK {
_ = conn.CloseWithError(closeErrCodeProtocolError, "")
_ = pktConn.Close()
return nil, coreErrs.AuthError{StatusCode: resp.StatusCode}
return coreErrs.AuthError{StatusCode: resp.StatusCode}
}
// Auth OK
authResp := protocol.AuthResponseFromHeader(resp.Header)
var actualTx uint64
if authResp.RxAuto {
// Server asks client to use bandwidth detection,
// ignore local bandwidth config and use BBR
congestion.UseBBR(conn)
} else {
// actualTx = min(serverRx, clientTx)
actualTx = authResp.Rx
actualTx := authResp.Rx
if actualTx == 0 || actualTx > c.config.BandwidthConfig.MaxTx {
// Server doesn't have a limit, or our clientTx is smaller than serverRx
actualTx = c.config.BandwidthConfig.MaxTx
@ -155,10 +147,7 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) {
if authResp.UDPEnabled {
c.udpSM = newUDPSessionManager(&udpIOImpl{Conn: conn})
}
return &HandshakeInfo{
UDPEnabled: authResp.UDPEnabled,
Tx: actualTx,
}, nil
return nil
}
// openStream wraps the stream with QStream, which handles Close() properly
@ -173,13 +162,17 @@ func (c *clientImpl) openStream() (quic.Stream, error) {
func (c *clientImpl) TCP(addr string) (net.Conn, error) {
stream, err := c.openStream()
if err != nil {
return nil, wrapIfConnectionClosed(err)
if isQUICClosedError(err) {
// Connection is dead
return nil, coreErrs.ClosedError{}
}
return nil, err
}
// Send request
err = protocol.WriteTCPRequest(stream, addr)
if err != nil {
_ = stream.Close()
return nil, wrapIfConnectionClosed(err)
return nil, err
}
if c.config.FastOpen {
// Don't wait for the response when fast open is enabled.
@ -196,7 +189,7 @@ func (c *clientImpl) TCP(addr string) (net.Conn, error) {
ok, msg, err := protocol.ReadTCPResponse(stream)
if err != nil {
_ = stream.Close()
return nil, wrapIfConnectionClosed(err)
return nil, err
}
if !ok {
_ = stream.Close()
@ -223,21 +216,15 @@ func (c *clientImpl) Close() error {
return nil
}
var nonPermanentErrors = []error{
quic.StreamLimitReachedError{},
}
// wrapIfConnectionClosed checks if the error returned by quic-go
// is recoverable (listed in nonPermanentErrors) or permanent.
// Recoverable errors are returned as-is,
// permanent ones are wrapped as ClosedError.
func wrapIfConnectionClosed(err error) error {
for _, e := range nonPermanentErrors {
if errors.Is(err, e) {
return err
}
// isQUICClosedError checks if the error returned by OpenStream
// indicates that the QUIC connection is permanently closed.
func isQUICClosedError(err error) bool {
netErr, ok := err.(net.Error)
if !ok {
return true
} else {
return !netErr.Temporary()
}
return coreErrs.ClosedError{Err: err}
}
type tcpConn struct {
@ -296,7 +283,7 @@ type udpIOImpl struct {
func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {
for {
msg, err := io.Conn.ReceiveDatagram(context.Background())
msg, err := io.Conn.ReceiveMessage(context.Background())
if err != nil {
// Connection error, this will stop the session manager
return nil, err
@ -316,5 +303,5 @@ func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {
// Message larger than buffer, silent drop
return nil
}
return io.Conn.SendDatagram(buf[:msgN])
return io.Conn.SendMessage(buf[:msgN])
}

View file

@ -5,8 +5,8 @@ import (
"net"
"time"
"github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/pmtud"
"github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/internal/pmtud"
)
const (

View file

@ -1,9 +1,9 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package client
import (
protocol "github.com/apernet/hysteria/core/v2/internal/protocol"
protocol "github.com/apernet/hysteria/core/internal/protocol"
mock "github.com/stretchr/testify/mock"
)
@ -24,10 +24,6 @@ func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter {
func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ReceiveMessage")
}
var r0 *protocol.UDPMessage
var r1 error
if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok {
@ -81,10 +77,6 @@ func (_c *mockUDPIO_ReceiveMessage_Call) RunAndReturn(run func() (*protocol.UDPM
func (_m *mockUDPIO) SendMessage(_a0 []byte, _a1 *protocol.UDPMessage) error {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for SendMessage")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, *protocol.UDPMessage) error); ok {
r0 = rf(_a0, _a1)

View file

@ -4,27 +4,29 @@ import (
"net"
"sync"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
coreErrs "github.com/apernet/hysteria/core/errors"
)
// reconnectableClientImpl is a wrapper of Client, which can reconnect when the connection is closed,
// except when the caller explicitly calls Close() to permanently close this client.
type reconnectableClientImpl struct {
configFunc func() (*Config, error) // called before connecting
connectedFunc func(Client, *HandshakeInfo, int) // called when successfully connected
config *Config
client Client
count int
connectedFunc func(Client, int) // called when successfully connected
m sync.Mutex
closed bool // permanent close
}
// NewReconnectableClient creates a reconnectable client.
// If lazy is true, the client will not connect until the first call to TCP() or UDP().
// We use a function for config mainly to delay config evaluation
// (which involves DNS resolution) until the actual connection attempt.
func NewReconnectableClient(configFunc func() (*Config, error), connectedFunc func(Client, *HandshakeInfo, int), lazy bool) (Client, error) {
func NewReconnectableClient(config *Config, connectedFunc func(Client, int), lazy bool) (Client, error) {
// Make sure we capture any error in config and return it here,
// so that the caller doesn't have to wait until the first call
// to TCP() or UDP() to get the error (when lazy is true).
if err := config.verifyAndFill(); err != nil {
return nil, err
}
rc := &reconnectableClientImpl{
configFunc: configFunc,
config: config,
connectedFunc: connectedFunc,
}
if !lazy {
@ -39,73 +41,66 @@ func (rc *reconnectableClientImpl) reconnect() error {
if rc.client != nil {
_ = rc.client.Close()
}
var info *HandshakeInfo
config, err := rc.configFunc()
if err != nil {
return err
}
rc.client, info, err = NewClient(config)
var err error
rc.client, err = NewClient(rc.config)
if err != nil {
return err
} else {
rc.count++
if rc.connectedFunc != nil {
rc.connectedFunc(rc, info, rc.count)
rc.connectedFunc(rc, rc.count)
}
return nil
}
}
// clientDo calls f with the current client.
// If the client is nil, it will first reconnect.
// It will also detect if the client is closed, and if so,
// set it to nil for reconnect next time.
func (rc *reconnectableClientImpl) clientDo(f func(Client) (interface{}, error)) (interface{}, error) {
func (rc *reconnectableClientImpl) TCP(addr string) (net.Conn, error) {
rc.m.Lock()
defer rc.m.Unlock()
if rc.closed {
rc.m.Unlock()
return nil, coreErrs.ClosedError{}
}
if rc.client == nil {
// No active connection, connect first
// First time
if err := rc.reconnect(); err != nil {
rc.m.Unlock()
return nil, err
}
}
client := rc.client
rc.m.Unlock()
ret, err := f(client)
conn, err := rc.client.TCP(addr)
if _, ok := err.(coreErrs.ClosedError); ok {
// Connection closed, set client to nil for reconnect next time
rc.m.Lock()
if rc.client == client {
// This check is in case the client is already changed by another goroutine
rc.client = nil
// Connection closed, reconnect
if err := rc.reconnect(); err != nil {
return nil, err
}
rc.m.Unlock()
}
return ret, err
}
func (rc *reconnectableClientImpl) TCP(addr string) (net.Conn, error) {
if c, err := rc.clientDo(func(client Client) (interface{}, error) {
return client.TCP(addr)
}); err != nil {
return nil, err
return rc.client.TCP(addr)
} else {
return c.(net.Conn), nil
// OK or some other temporary error
return conn, err
}
}
func (rc *reconnectableClientImpl) UDP() (HyUDPConn, error) {
if c, err := rc.clientDo(func(client Client) (interface{}, error) {
return client.UDP()
}); err != nil {
return nil, err
rc.m.Lock()
defer rc.m.Unlock()
if rc.closed {
return nil, coreErrs.ClosedError{}
}
if rc.client == nil {
// First time
if err := rc.reconnect(); err != nil {
return nil, err
}
}
conn, err := rc.client.UDP()
if _, ok := err.(coreErrs.ClosedError); ok {
// Connection closed, reconnect
if err := rc.reconnect(); err != nil {
return nil, err
}
return rc.client.UDP()
} else {
return c.(HyUDPConn), nil
// OK or some other temporary error
return conn, err
}
}

View file

@ -8,9 +8,9 @@ import (
"github.com/apernet/quic-go"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/frag"
"github.com/apernet/hysteria/core/v2/internal/protocol"
coreErrs "github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/internal/frag"
"github.com/apernet/hysteria/core/internal/protocol"
)
const (
@ -60,11 +60,11 @@ func (u *udpConn) Send(data []byte, addr string) error {
Data: data,
}
err := u.SendFunc(u.SendBuf, msg)
var errTooLarge *quic.DatagramTooLargeError
var errTooLarge quic.ErrMessageTooLarge
if errors.As(err, &errTooLarge) {
// Message too large, try fragmentation
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := frag.FragUDPMessage(msg, int(errTooLarge.MaxDataLen))
fMsgs := frag.FragUDPMessage(msg, int(errTooLarge))
for _, fMsg := range fMsgs {
err := u.SendFunc(u.SendBuf, &fMsg)
if err != nil {

View file

@ -10,8 +10,8 @@ import (
"github.com/stretchr/testify/mock"
"go.uber.org/goleak"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/protocol"
coreErrs "github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/internal/protocol"
)
func TestUDPSessionManager(t *testing.T) {

View file

@ -48,20 +48,10 @@ func (c DialError) Error() string {
}
// ClosedError is returned when the client attempts to use a closed connection.
type ClosedError struct {
Err error // Can be nil
}
type ClosedError struct{}
func (c ClosedError) Error() string {
if c.Err == nil {
return "connection closed"
} else {
return "connection closed: " + c.Err.Error()
}
}
func (c ClosedError) Unwrap() error {
return c.Err
return "connection closed"
}
// ProtocolError is returned when the server/client runs into an unexpected

View file

@ -1,37 +1,35 @@
module github.com/apernet/hysteria/core/v2
module github.com/apernet/hysteria/core
go 1.23
toolchain go1.24.2
go 1.20
require (
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431
github.com/stretchr/testify v1.9.0
github.com/apernet/quic-go v0.38.2-0.20230902022200-c0e542a00ce6
github.com/stretchr/testify v1.8.4
github.com/zhangyunhao116/fastrand v0.3.0
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/time v0.5.0
golang.org/x/time v0.3.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
github.com/stretchr/objx v0.5.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.11.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -1,5 +1,5 @@
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431 h1:9/jM7e+kVALd7Jfu1c27dcEpT/Fd/Gzq2OsQjKjakKI=
github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431/go.mod h1:I/47OIGG5H/IfAm+nz2c6hm6b/NkEhpvptAoiPcY7jQ=
github.com/apernet/quic-go v0.38.2-0.20230902022200-c0e542a00ce6 h1:OXAn9a1dhdluj5olg6Gou7q5oN5J7CaTYS7SJPqHP/0=
github.com/apernet/quic-go v0.38.2-0.20230902022200-c0e542a00ce6/go.mod h1:1X8T39iiKfCimEhzNaUrEeq0Pmafu7cjgYJetUbRXYc=
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=
@ -8,69 +8,92 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
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/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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/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-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig=
github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.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.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.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.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc=
golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
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.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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=

View file

@ -7,13 +7,11 @@ import (
"github.com/apernet/quic-go/congestion"
)
const (
infBandwidth = Bandwidth(math.MaxUint64)
)
// Bandwidth of a connection
type Bandwidth uint64
const infBandwidth Bandwidth = math.MaxUint64
const (
// BitsPerSecond is 1 bit per second
BitsPerSecond Bandwidth = 1

View file

@ -7,17 +7,11 @@ import (
"github.com/apernet/quic-go/congestion"
)
const (
infRTT = time.Duration(math.MaxInt64)
defaultConnectionStateMapQueueSize = 256
defaultCandidatesBufferSize = 256
)
type roundTripCount uint64
var InfiniteBandwidth = Bandwidth(math.MaxUint64)
// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned
// to the caller when the packet is acked or lost.
type sendTimeState struct {
type SendTimeState struct {
// Whether other states in this object is valid.
isValid bool
// Whether the sender is app limited at the time the packet was sent.
@ -31,260 +25,16 @@ type sendTimeState struct {
totalBytesAcked congestion.ByteCount
// Total number of lost bytes at the time the packet was sent.
totalBytesLost congestion.ByteCount
// Total number of inflight bytes at the time the packet was sent.
// Includes the packet itself.
// It should be equal to |total_bytes_sent| minus the sum of
// |total_bytes_acked|, |total_bytes_lost| and total neutered bytes.
bytesInFlight congestion.ByteCount
}
func newSendTimeState(
isAppLimited bool,
totalBytesSent congestion.ByteCount,
totalBytesAcked congestion.ByteCount,
totalBytesLost congestion.ByteCount,
bytesInFlight congestion.ByteCount,
) *sendTimeState {
return &sendTimeState{
isValid: true,
isAppLimited: isAppLimited,
totalBytesSent: totalBytesSent,
totalBytesAcked: totalBytesAcked,
totalBytesLost: totalBytesLost,
bytesInFlight: bytesInFlight,
}
}
type extraAckedEvent struct {
// The excess bytes acknowlwedged in the time delta for this event.
extraAcked congestion.ByteCount
// The bytes acknowledged and time delta from the event.
bytesAcked congestion.ByteCount
timeDelta time.Duration
// The round trip of the event.
round roundTripCount
}
func maxExtraAckedEventFunc(a, b extraAckedEvent) int {
if a.extraAcked > b.extraAcked {
return 1
} else if a.extraAcked < b.extraAcked {
return -1
}
return 0
}
// BandwidthSample
type bandwidthSample struct {
// The bandwidth at that particular sample. Zero if no valid bandwidth sample
// is available.
bandwidth Bandwidth
// The RTT measurement at this particular sample. Zero if no RTT sample is
// available. Does not correct for delayed ack time.
rtt time.Duration
// |send_rate| is computed from the current packet being acked('P') and an
// earlier packet that is acked before P was sent.
sendRate Bandwidth
// States captured when the packet was sent.
stateAtSend sendTimeState
}
func newBandwidthSample() *bandwidthSample {
return &bandwidthSample{
sendRate: infBandwidth,
}
}
// MaxAckHeightTracker is part of the BandwidthSampler. It is called after every
// ack event to keep track the degree of ack aggregation(a.k.a "ack height").
type maxAckHeightTracker struct {
// Tracks the maximum number of bytes acked faster than the estimated
// bandwidth.
maxAckHeightFilter *WindowedFilter[extraAckedEvent, roundTripCount]
// The time this aggregation started and the number of bytes acked during it.
aggregationEpochStartTime time.Time
aggregationEpochBytes congestion.ByteCount
// The last sent packet number before the current aggregation epoch started.
lastSentPacketNumberBeforeEpoch congestion.PacketNumber
// The number of ack aggregation epochs ever started, including the ongoing
// one. Stats only.
numAckAggregationEpochs uint64
ackAggregationBandwidthThreshold float64
startNewAggregationEpochAfterFullRound bool
reduceExtraAckedOnBandwidthIncrease bool
}
func newMaxAckHeightTracker(windowLength roundTripCount) *maxAckHeightTracker {
return &maxAckHeightTracker{
maxAckHeightFilter: NewWindowedFilter(windowLength, maxExtraAckedEventFunc),
lastSentPacketNumberBeforeEpoch: invalidPacketNumber,
ackAggregationBandwidthThreshold: 1.0,
}
}
func (m *maxAckHeightTracker) Get() congestion.ByteCount {
return m.maxAckHeightFilter.GetBest().extraAcked
}
func (m *maxAckHeightTracker) Update(
bandwidthEstimate Bandwidth,
isNewMaxBandwidth bool,
roundTripCount roundTripCount,
lastSentPacketNumber congestion.PacketNumber,
lastAckedPacketNumber congestion.PacketNumber,
ackTime time.Time,
bytesAcked congestion.ByteCount,
) congestion.ByteCount {
forceNewEpoch := false
if m.reduceExtraAckedOnBandwidthIncrease && isNewMaxBandwidth {
// Save and clear existing entries.
best := m.maxAckHeightFilter.GetBest()
secondBest := m.maxAckHeightFilter.GetSecondBest()
thirdBest := m.maxAckHeightFilter.GetThirdBest()
m.maxAckHeightFilter.Clear()
// Reinsert the heights into the filter after recalculating.
expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, best.timeDelta)
if expectedBytesAcked < best.bytesAcked {
best.extraAcked = best.bytesAcked - expectedBytesAcked
m.maxAckHeightFilter.Update(best, best.round)
}
expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, secondBest.timeDelta)
if expectedBytesAcked < secondBest.bytesAcked {
secondBest.extraAcked = secondBest.bytesAcked - expectedBytesAcked
m.maxAckHeightFilter.Update(secondBest, secondBest.round)
}
expectedBytesAcked = bytesFromBandwidthAndTimeDelta(bandwidthEstimate, thirdBest.timeDelta)
if expectedBytesAcked < thirdBest.bytesAcked {
thirdBest.extraAcked = thirdBest.bytesAcked - expectedBytesAcked
m.maxAckHeightFilter.Update(thirdBest, thirdBest.round)
}
}
// If any packet sent after the start of the epoch has been acked, start a new
// epoch.
if m.startNewAggregationEpochAfterFullRound &&
m.lastSentPacketNumberBeforeEpoch != invalidPacketNumber &&
lastAckedPacketNumber != invalidPacketNumber &&
lastAckedPacketNumber > m.lastSentPacketNumberBeforeEpoch {
forceNewEpoch = true
}
if m.aggregationEpochStartTime.IsZero() || forceNewEpoch {
m.aggregationEpochBytes = bytesAcked
m.aggregationEpochStartTime = ackTime
m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber
m.numAckAggregationEpochs++
return 0
}
// Compute how many bytes are expected to be delivered, assuming max bandwidth
// is correct.
aggregationDelta := ackTime.Sub(m.aggregationEpochStartTime)
expectedBytesAcked := bytesFromBandwidthAndTimeDelta(bandwidthEstimate, aggregationDelta)
// Reset the current aggregation epoch as soon as the ack arrival rate is less
// than or equal to the max bandwidth.
if m.aggregationEpochBytes <= congestion.ByteCount(m.ackAggregationBandwidthThreshold*float64(expectedBytesAcked)) {
// Reset to start measuring a new aggregation epoch.
m.aggregationEpochBytes = bytesAcked
m.aggregationEpochStartTime = ackTime
m.lastSentPacketNumberBeforeEpoch = lastSentPacketNumber
m.numAckAggregationEpochs++
return 0
}
m.aggregationEpochBytes += bytesAcked
// Compute how many extra bytes were delivered vs max bandwidth.
extraBytesAcked := m.aggregationEpochBytes - expectedBytesAcked
newEvent := extraAckedEvent{
extraAcked: expectedBytesAcked,
bytesAcked: m.aggregationEpochBytes,
timeDelta: aggregationDelta,
}
m.maxAckHeightFilter.Update(newEvent, roundTripCount)
return extraBytesAcked
}
func (m *maxAckHeightTracker) SetFilterWindowLength(length roundTripCount) {
m.maxAckHeightFilter.SetWindowLength(length)
}
func (m *maxAckHeightTracker) Reset(newHeight congestion.ByteCount, newTime roundTripCount) {
newEvent := extraAckedEvent{
extraAcked: newHeight,
round: newTime,
}
m.maxAckHeightFilter.Reset(newEvent, newTime)
}
func (m *maxAckHeightTracker) SetAckAggregationBandwidthThreshold(threshold float64) {
m.ackAggregationBandwidthThreshold = threshold
}
func (m *maxAckHeightTracker) SetStartNewAggregationEpochAfterFullRound(value bool) {
m.startNewAggregationEpochAfterFullRound = value
}
func (m *maxAckHeightTracker) SetReduceExtraAckedOnBandwidthIncrease(value bool) {
m.reduceExtraAckedOnBandwidthIncrease = value
}
func (m *maxAckHeightTracker) AckAggregationBandwidthThreshold() float64 {
return m.ackAggregationBandwidthThreshold
}
func (m *maxAckHeightTracker) NumAckAggregationEpochs() uint64 {
return m.numAckAggregationEpochs
}
// AckPoint represents a point on the ack line.
type ackPoint struct {
ackTime time.Time
totalBytesAcked congestion.ByteCount
}
// RecentAckPoints maintains the most recent 2 ack points at distinct times.
type recentAckPoints struct {
ackPoints [2]ackPoint
}
func (r *recentAckPoints) Update(ackTime time.Time, totalBytesAcked congestion.ByteCount) {
if ackTime.Before(r.ackPoints[1].ackTime) {
r.ackPoints[1].ackTime = ackTime
} else if ackTime.After(r.ackPoints[1].ackTime) {
r.ackPoints[0] = r.ackPoints[1]
r.ackPoints[1].ackTime = ackTime
}
r.ackPoints[1].totalBytesAcked = totalBytesAcked
}
func (r *recentAckPoints) Clear() {
r.ackPoints[0] = ackPoint{}
r.ackPoints[1] = ackPoint{}
}
func (r *recentAckPoints) MostRecentPoint() *ackPoint {
return &r.ackPoints[1]
}
func (r *recentAckPoints) LessRecentPoint() *ackPoint {
if r.ackPoints[0].totalBytesAcked != 0 {
return &r.ackPoints[0]
}
return &r.ackPoints[1]
}
// ConnectionStateOnSentPacket represents the information about a sent packet
// and the state of the connection at the moment the packet was sent,
// specifically the information about the most recently acknowledged packet at
// that moment.
type connectionStateOnSentPacket struct {
type ConnectionStateOnSentPacket struct {
packetNumber congestion.PacketNumber
// Time at which the packet is sent.
sentTime time.Time
sendTime time.Time
// Size of the packet.
size congestion.ByteCount
// The value of |totalBytesSentAtLastAckedPacket| at the time the
@ -298,31 +48,25 @@ type connectionStateOnSentPacket struct {
lastAckedPacketAckTime time.Time
// Send time states that are returned to the congestion controller when the
// packet is acked or lost.
sendTimeState sendTimeState
sendTimeState SendTimeState
}
// Snapshot constructor. Records the current state of the bandwidth
// sampler.
// |bytes_in_flight| is the bytes in flight right after the packet is sent.
func newConnectionStateOnSentPacket(
sentTime time.Time,
size congestion.ByteCount,
bytesInFlight congestion.ByteCount,
sampler *bandwidthSampler,
) *connectionStateOnSentPacket {
return &connectionStateOnSentPacket{
sentTime: sentTime,
size: size,
totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket,
lastAckedPacketSentTime: sampler.lastAckedPacketSentTime,
lastAckedPacketAckTime: sampler.lastAckedPacketAckTime,
sendTimeState: *newSendTimeState(
sampler.isAppLimited,
sampler.totalBytesSent,
sampler.totalBytesAcked,
sampler.totalBytesLost,
bytesInFlight,
),
// BandwidthSample
type BandwidthSample struct {
// The bandwidth at that particular sample. Zero if no valid bandwidth sample
// is available.
bandwidth Bandwidth
// The RTT measurement at this particular sample. Zero if no RTT sample is
// available. Does not correct for delayed ack time.
rtt time.Duration
// States captured when the packet was sent.
stateAtSend SendTimeState
}
func NewBandwidthSample() *BandwidthSample {
return &BandwidthSample{
// FIXME: the default value of original code is zero.
rtt: InfiniteRTT,
}
}
@ -408,162 +152,54 @@ func newConnectionStateOnSentPacket(
// up until an ack for a packet that was sent after OnAppLimited() was called.
// Note that while the scenario above is not the only scenario when the
// connection is app-limited, the approach works in other cases too.
type congestionEventSample struct {
// The maximum bandwidth sample from all acked packets.
// QuicBandwidth::Zero() if no samples are available.
sampleMaxBandwidth Bandwidth
// Whether |sample_max_bandwidth| is from a app-limited sample.
sampleIsAppLimited bool
// The minimum rtt sample from all acked packets.
// QuicTime::Delta::Infinite() if no samples are available.
sampleRtt time.Duration
// For each packet p in acked packets, this is the max value of INFLIGHT(p),
// where INFLIGHT(p) is the number of bytes acked while p is inflight.
sampleMaxInflight congestion.ByteCount
// The send state of the largest packet in acked_packets, unless it is
// empty. If acked_packets is empty, it's the send state of the largest
// packet in lost_packets.
lastPacketSendState sendTimeState
// The number of extra bytes acked from this ack event, compared to what is
// expected from the flow's bandwidth. Larger value means more ack
// aggregation.
extraAcked congestion.ByteCount
}
func newCongestionEventSample() *congestionEventSample {
return &congestionEventSample{
sampleRtt: infRTT,
}
}
type bandwidthSampler struct {
type BandwidthSampler struct {
// The total number of congestion controlled bytes sent during the connection.
totalBytesSent congestion.ByteCount
// The total number of congestion controlled bytes which were acknowledged.
totalBytesAcked congestion.ByteCount
// The total number of congestion controlled bytes which were lost.
totalBytesLost congestion.ByteCount
// The total number of congestion controlled bytes which have been neutered.
totalBytesNeutered congestion.ByteCount
// The value of |total_bytes_sent_| at the time the last acknowledged packet
// was sent. Valid only when |last_acked_packet_sent_time_| is valid.
// The value of |totalBytesSent| at the time the last acknowledged packet
// was sent. Valid only when |lastAckedPacketSentTime| is valid.
totalBytesSentAtLastAckedPacket congestion.ByteCount
// The time at which the last acknowledged packet was sent. Set to
// QuicTime::Zero() if no valid timestamp is available.
lastAckedPacketSentTime time.Time
// The time at which the most recent packet was acknowledged.
lastAckedPacketAckTime time.Time
// The most recently sent packet.
lastSentPacket congestion.PacketNumber
// The most recently acked packet.
lastAckedPacket congestion.PacketNumber
lastSendPacket congestion.PacketNumber
// Indicates whether the bandwidth sampler is currently in an app-limited
// phase.
isAppLimited bool
// The packet that will be acknowledged after this one will cause the sampler
// to exit the app-limited phase.
endOfAppLimitedPhase congestion.PacketNumber
// Record of the connection state at the point where each packet in flight was
// sent, indexed by the packet number.
connectionStateMap *packetNumberIndexedQueue[connectionStateOnSentPacket]
recentAckPoints recentAckPoints
a0Candidates RingBuffer[ackPoint]
// Maximum number of tracked packets.
maxTrackedPackets congestion.ByteCount
maxAckHeightTracker *maxAckHeightTracker
totalBytesAckedAfterLastAckEvent congestion.ByteCount
// True if connection option 'BSAO' is set.
overestimateAvoidance bool
// True if connection option 'BBRB' is set.
limitMaxAckHeightTrackerBySendRate bool
connectionStats *ConnectionStates
}
func newBandwidthSampler(maxAckHeightTrackerWindowLength roundTripCount) *bandwidthSampler {
b := &bandwidthSampler{
maxAckHeightTracker: newMaxAckHeightTracker(maxAckHeightTrackerWindowLength),
connectionStateMap: newPacketNumberIndexedQueue[connectionStateOnSentPacket](defaultConnectionStateMapQueueSize),
lastSentPacket: invalidPacketNumber,
lastAckedPacket: invalidPacketNumber,
endOfAppLimitedPhase: invalidPacketNumber,
func NewBandwidthSampler() *BandwidthSampler {
return &BandwidthSampler{
connectionStats: &ConnectionStates{
stats: make(map[congestion.PacketNumber]*ConnectionStateOnSentPacket),
},
}
b.a0Candidates.Init(defaultCandidatesBufferSize)
return b
}
func (b *bandwidthSampler) MaxAckHeight() congestion.ByteCount {
return b.maxAckHeightTracker.Get()
}
// OnPacketSent Inputs the sent packet information into the sampler. Assumes that all
// packets are sent in order. The information about the packet will not be
// released from the sampler until it the packet is either acknowledged or
// declared lost.
func (s *BandwidthSampler) OnPacketSent(sentTime time.Time, lastSentPacket congestion.PacketNumber, sentBytes, bytesInFlight congestion.ByteCount, hasRetransmittableData bool) {
s.lastSendPacket = lastSentPacket
func (b *bandwidthSampler) NumAckAggregationEpochs() uint64 {
return b.maxAckHeightTracker.NumAckAggregationEpochs()
}
func (b *bandwidthSampler) SetMaxAckHeightTrackerWindowLength(length roundTripCount) {
b.maxAckHeightTracker.SetFilterWindowLength(length)
}
func (b *bandwidthSampler) ResetMaxAckHeightTracker(newHeight congestion.ByteCount, newTime roundTripCount) {
b.maxAckHeightTracker.Reset(newHeight, newTime)
}
func (b *bandwidthSampler) SetStartNewAggregationEpochAfterFullRound(value bool) {
b.maxAckHeightTracker.SetStartNewAggregationEpochAfterFullRound(value)
}
func (b *bandwidthSampler) SetLimitMaxAckHeightTrackerBySendRate(value bool) {
b.limitMaxAckHeightTrackerBySendRate = value
}
func (b *bandwidthSampler) SetReduceExtraAckedOnBandwidthIncrease(value bool) {
b.maxAckHeightTracker.SetReduceExtraAckedOnBandwidthIncrease(value)
}
func (b *bandwidthSampler) EnableOverestimateAvoidance() {
if b.overestimateAvoidance {
if !hasRetransmittableData {
return
}
b.overestimateAvoidance = true
b.maxAckHeightTracker.SetAckAggregationBandwidthThreshold(2.0)
}
func (b *bandwidthSampler) IsOverestimateAvoidanceEnabled() bool {
return b.overestimateAvoidance
}
func (b *bandwidthSampler) OnPacketSent(
sentTime time.Time,
packetNumber congestion.PacketNumber,
bytes congestion.ByteCount,
bytesInFlight congestion.ByteCount,
isRetransmittable bool,
) {
b.lastSentPacket = packetNumber
if !isRetransmittable {
return
}
b.totalBytesSent += bytes
s.totalBytesSent += sentBytes
// If there are no packets in flight, the time at which the new transmission
// opens can be treated as the A_0 point for the purpose of bandwidth
@ -572,303 +208,167 @@ func (b *bandwidthSampler) OnPacketSent(
// samples at important points where we would not have them otherwise, most
// importantly at the beginning of the connection.
if bytesInFlight == 0 {
b.lastAckedPacketAckTime = sentTime
if b.overestimateAvoidance {
b.recentAckPoints.Clear()
b.recentAckPoints.Update(sentTime, b.totalBytesAcked)
b.a0Candidates.Clear()
b.a0Candidates.PushBack(*b.recentAckPoints.MostRecentPoint())
}
b.totalBytesSentAtLastAckedPacket = b.totalBytesSent
s.lastAckedPacketAckTime = sentTime
s.totalBytesSentAtLastAckedPacket = s.totalBytesSent
// In this situation ack compression is not a concern, set send rate to
// effectively infinite.
b.lastAckedPacketSentTime = sentTime
s.lastAckedPacketSentTime = sentTime
}
b.connectionStateMap.Emplace(packetNumber, newConnectionStateOnSentPacket(
sentTime,
bytes,
bytesInFlight+bytes,
b,
))
s.connectionStats.Insert(lastSentPacket, sentTime, sentBytes, s)
}
func (b *bandwidthSampler) OnCongestionEvent(
ackTime time.Time,
ackedPackets []congestion.AckedPacketInfo,
lostPackets []congestion.LostPacketInfo,
maxBandwidth Bandwidth,
estBandwidthUpperBound Bandwidth,
roundTripCount roundTripCount,
) congestionEventSample {
eventSample := newCongestionEventSample()
var lastLostPacketSendState sendTimeState
for _, p := range lostPackets {
sendState := b.OnPacketLost(p.PacketNumber, p.BytesLost)
if sendState.isValid {
lastLostPacketSendState = sendState
}
// OnPacketAcked Notifies the sampler that the |lastAckedPacket| is acknowledged. Returns a
// bandwidth sample. If no bandwidth sample is available,
// QuicBandwidth::Zero() is returned.
func (s *BandwidthSampler) OnPacketAcked(ackTime time.Time, lastAckedPacket congestion.PacketNumber) *BandwidthSample {
sentPacketState := s.connectionStats.Get(lastAckedPacket)
if sentPacketState == nil {
return NewBandwidthSample()
}
if len(ackedPackets) == 0 {
// Only populate send state for a loss-only event.
eventSample.lastPacketSendState = lastLostPacketSendState
return *eventSample
}
sample := s.onPacketAckedInner(ackTime, lastAckedPacket, sentPacketState)
s.connectionStats.Remove(lastAckedPacket)
var lastAckedPacketSendState sendTimeState
var maxSendRate Bandwidth
for _, p := range ackedPackets {
sample := b.onPacketAcknowledged(ackTime, p.PacketNumber)
if !sample.stateAtSend.isValid {
continue
}
lastAckedPacketSendState = sample.stateAtSend
if sample.rtt != 0 {
eventSample.sampleRtt = min(eventSample.sampleRtt, sample.rtt)
}
if sample.bandwidth > eventSample.sampleMaxBandwidth {
eventSample.sampleMaxBandwidth = sample.bandwidth
eventSample.sampleIsAppLimited = sample.stateAtSend.isAppLimited
}
if sample.sendRate != infBandwidth {
maxSendRate = max(maxSendRate, sample.sendRate)
}
inflightSample := b.totalBytesAcked - lastAckedPacketSendState.totalBytesAcked
if inflightSample > eventSample.sampleMaxInflight {
eventSample.sampleMaxInflight = inflightSample
}
}
if !lastLostPacketSendState.isValid {
eventSample.lastPacketSendState = lastAckedPacketSendState
} else if !lastAckedPacketSendState.isValid {
eventSample.lastPacketSendState = lastLostPacketSendState
} else {
// If two packets are inflight and an alarm is armed to lose a packet and it
// wakes up late, then the first of two in flight packets could have been
// acknowledged before the wakeup, which re-evaluates loss detection, and
// could declare the later of the two lost.
if lostPackets[len(lostPackets)-1].PacketNumber > ackedPackets[len(ackedPackets)-1].PacketNumber {
eventSample.lastPacketSendState = lastLostPacketSendState
} else {
eventSample.lastPacketSendState = lastAckedPacketSendState
}
}
isNewMaxBandwidth := eventSample.sampleMaxBandwidth > maxBandwidth
maxBandwidth = max(maxBandwidth, eventSample.sampleMaxBandwidth)
if b.limitMaxAckHeightTrackerBySendRate {
maxBandwidth = max(maxBandwidth, maxSendRate)
}
eventSample.extraAcked = b.onAckEventEnd(min(estBandwidthUpperBound, maxBandwidth), isNewMaxBandwidth, roundTripCount)
return *eventSample
return sample
}
func (b *bandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber, bytesLost congestion.ByteCount) (s sendTimeState) {
b.totalBytesLost += bytesLost
if sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber); sentPacketPointer != nil {
sentPacketToSendTimeState(sentPacketPointer, &s)
}
return s
}
// onPacketAckedInner Handles the actual bandwidth calculations, whereas the outer method handles
// retrieving and removing |sentPacket|.
func (s *BandwidthSampler) onPacketAckedInner(ackTime time.Time, lastAckedPacket congestion.PacketNumber, sentPacket *ConnectionStateOnSentPacket) *BandwidthSample {
s.totalBytesAcked += sentPacket.size
func (b *bandwidthSampler) OnPacketNeutered(packetNumber congestion.PacketNumber) {
b.connectionStateMap.Remove(packetNumber, func(sentPacket connectionStateOnSentPacket) {
b.totalBytesNeutered += sentPacket.size
})
}
s.totalBytesSentAtLastAckedPacket = sentPacket.sendTimeState.totalBytesSent
s.lastAckedPacketSentTime = sentPacket.sendTime
s.lastAckedPacketAckTime = ackTime
func (b *bandwidthSampler) OnAppLimited() {
b.isAppLimited = true
b.endOfAppLimitedPhase = b.lastSentPacket
}
func (b *bandwidthSampler) RemoveObsoletePackets(leastUnacked congestion.PacketNumber) {
// A packet can become obsolete when it is removed from QuicUnackedPacketMap's
// view of inflight before it is acked or marked as lost. For example, when
// QuicSentPacketManager::RetransmitCryptoPackets retransmits a crypto packet,
// the packet is removed from QuicUnackedPacketMap's inflight, but is not
// marked as acked or lost in the BandwidthSampler.
b.connectionStateMap.RemoveUpTo(leastUnacked)
}
func (b *bandwidthSampler) TotalBytesSent() congestion.ByteCount {
return b.totalBytesSent
}
func (b *bandwidthSampler) TotalBytesLost() congestion.ByteCount {
return b.totalBytesLost
}
func (b *bandwidthSampler) TotalBytesAcked() congestion.ByteCount {
return b.totalBytesAcked
}
func (b *bandwidthSampler) TotalBytesNeutered() congestion.ByteCount {
return b.totalBytesNeutered
}
func (b *bandwidthSampler) IsAppLimited() bool {
return b.isAppLimited
}
func (b *bandwidthSampler) EndOfAppLimitedPhase() congestion.PacketNumber {
return b.endOfAppLimitedPhase
}
func (b *bandwidthSampler) max_ack_height() congestion.ByteCount {
return b.maxAckHeightTracker.Get()
}
func (b *bandwidthSampler) chooseA0Point(totalBytesAcked congestion.ByteCount, a0 *ackPoint) bool {
if b.a0Candidates.Empty() {
return false
}
if b.a0Candidates.Len() == 1 {
*a0 = *b.a0Candidates.Front()
return true
}
for i := 1; i < b.a0Candidates.Len(); i++ {
if b.a0Candidates.Offset(i).totalBytesAcked > totalBytesAcked {
*a0 = *b.a0Candidates.Offset(i - 1)
if i > 1 {
for j := 0; j < i-1; j++ {
b.a0Candidates.PopFront()
}
}
return true
}
}
*a0 = *b.a0Candidates.Back()
for k := 0; k < b.a0Candidates.Len()-1; k++ {
b.a0Candidates.PopFront()
}
return true
}
func (b *bandwidthSampler) onPacketAcknowledged(ackTime time.Time, packetNumber congestion.PacketNumber) bandwidthSample {
sample := newBandwidthSample()
b.lastAckedPacket = packetNumber
sentPacketPointer := b.connectionStateMap.GetEntry(packetNumber)
if sentPacketPointer == nil {
return *sample
}
// OnPacketAcknowledgedInner
b.totalBytesAcked += sentPacketPointer.size
b.totalBytesSentAtLastAckedPacket = sentPacketPointer.sendTimeState.totalBytesSent
b.lastAckedPacketSentTime = sentPacketPointer.sentTime
b.lastAckedPacketAckTime = ackTime
if b.overestimateAvoidance {
b.recentAckPoints.Update(ackTime, b.totalBytesAcked)
}
if b.isAppLimited {
// Exit app-limited phase in two cases:
// (1) end_of_app_limited_phase_ is not initialized, i.e., so far all
// packets are sent while there are buffered packets or pending data.
// (2) The current acked packet is after the sent packet marked as the end
// of the app limit phase.
if b.endOfAppLimitedPhase == invalidPacketNumber ||
packetNumber > b.endOfAppLimitedPhase {
b.isAppLimited = false
}
// Exit app-limited phase once a packet that was sent while the connection is
// not app-limited is acknowledged.
if s.isAppLimited && lastAckedPacket > s.endOfAppLimitedPhase {
s.isAppLimited = false
}
// There might have been no packets acknowledged at the moment when the
// current packet was sent. In that case, there is no bandwidth sample to
// make.
if sentPacketPointer.lastAckedPacketSentTime.IsZero() {
return *sample
if sentPacket.lastAckedPacketSentTime.IsZero() {
return NewBandwidthSample()
}
// Infinite rate indicates that the sampler is supposed to discard the
// current send rate sample and use only the ack rate.
sendRate := infBandwidth
if sentPacketPointer.sentTime.After(sentPacketPointer.lastAckedPacketSentTime) {
sendRate = BandwidthFromDelta(
sentPacketPointer.sendTimeState.totalBytesSent-sentPacketPointer.totalBytesSentAtLastAckedPacket,
sentPacketPointer.sentTime.Sub(sentPacketPointer.lastAckedPacketSentTime))
}
var a0 ackPoint
if b.overestimateAvoidance && b.chooseA0Point(sentPacketPointer.sendTimeState.totalBytesAcked, &a0) {
} else {
a0.ackTime = sentPacketPointer.lastAckedPacketAckTime
a0.totalBytesAcked = sentPacketPointer.sendTimeState.totalBytesAcked
sendRate := InfiniteBandwidth
if sentPacket.sendTime.After(sentPacket.lastAckedPacketSentTime) {
sendRate = BandwidthFromDelta(sentPacket.sendTimeState.totalBytesSent-sentPacket.totalBytesSentAtLastAckedPacket, sentPacket.sendTime.Sub(sentPacket.lastAckedPacketSentTime))
}
// During the slope calculation, ensure that ack time of the current packet is
// always larger than the time of the previous packet, otherwise division by
// zero or integer underflow can occur.
if ackTime.Sub(a0.ackTime) <= 0 {
return *sample
if !ackTime.After(sentPacket.lastAckedPacketAckTime) {
// TODO(wub): Compare this code count before and after fixing clock jitter
// issue.
// if sentPacket.lastAckedPacketAckTime.Equal(sentPacket.sendTime) {
// This is the 1st packet after quiescense.
// QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 1, 2);
// } else {
// QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 2, 2);
// }
return NewBandwidthSample()
}
ackRate := BandwidthFromDelta(b.totalBytesAcked-a0.totalBytesAcked, ackTime.Sub(a0.ackTime))
ackRate := BandwidthFromDelta(s.totalBytesAcked-sentPacket.sendTimeState.totalBytesAcked,
ackTime.Sub(sentPacket.lastAckedPacketAckTime))
sample.bandwidth = min(sendRate, ackRate)
// Note: this sample does not account for delayed acknowledgement time. This
// means that the RTT measurements here can be artificially high, especially
// on low bandwidth connections.
sample.rtt = ackTime.Sub(sentPacketPointer.sentTime)
sample.sendRate = sendRate
sentPacketToSendTimeState(sentPacketPointer, &sample.stateAtSend)
sample := &BandwidthSample{
bandwidth: minBandwidth(sendRate, ackRate),
rtt: ackTime.Sub(sentPacket.sendTime),
}
return *sample
SentPacketToSendTimeState(sentPacket, &sample.stateAtSend)
return sample
}
func (b *bandwidthSampler) onAckEventEnd(
bandwidthEstimate Bandwidth,
isNewMaxBandwidth bool,
roundTripCount roundTripCount,
) congestion.ByteCount {
newlyAckedBytes := b.totalBytesAcked - b.totalBytesAckedAfterLastAckEvent
if newlyAckedBytes == 0 {
return 0
// OnPacketLost Informs the sampler that a packet is considered lost and it should no
// longer keep track of it.
func (s *BandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber) SendTimeState {
ok, sentPacket := s.connectionStats.Remove(packetNumber)
sendTimeState := SendTimeState{
isValid: ok,
}
b.totalBytesAckedAfterLastAckEvent = b.totalBytesAcked
extraAcked := b.maxAckHeightTracker.Update(
bandwidthEstimate,
isNewMaxBandwidth,
roundTripCount,
b.lastSentPacket,
b.lastAckedPacket,
b.lastAckedPacketAckTime,
newlyAckedBytes)
// If |extra_acked| is zero, i.e. this ack event marks the start of a new ack
// aggregation epoch, save LessRecentPoint, which is the last ack point of the
// previous epoch, as a A0 candidate.
if b.overestimateAvoidance && extraAcked == 0 {
b.a0Candidates.PushBack(*b.recentAckPoints.LessRecentPoint())
if sentPacket != nil {
s.totalBytesLost += sentPacket.size
SentPacketToSendTimeState(sentPacket, &sendTimeState)
}
return extraAcked
return sendTimeState
}
func sentPacketToSendTimeState(sentPacket *connectionStateOnSentPacket, sendTimeState *sendTimeState) {
*sendTimeState = sentPacket.sendTimeState
// OnAppLimited Informs the sampler that the connection is currently app-limited, causing
// the sampler to enter the app-limited phase. The phase will expire by
// itself.
func (s *BandwidthSampler) OnAppLimited() {
s.isAppLimited = true
s.endOfAppLimitedPhase = s.lastSendPacket
}
// SentPacketToSendTimeState Copy a subset of the (private) ConnectionStateOnSentPacket to the (public)
// SendTimeState. Always set send_time_state->is_valid to true.
func SentPacketToSendTimeState(sentPacket *ConnectionStateOnSentPacket, sendTimeState *SendTimeState) {
sendTimeState.isAppLimited = sentPacket.sendTimeState.isAppLimited
sendTimeState.totalBytesSent = sentPacket.sendTimeState.totalBytesSent
sendTimeState.totalBytesAcked = sentPacket.sendTimeState.totalBytesAcked
sendTimeState.totalBytesLost = sentPacket.sendTimeState.totalBytesLost
sendTimeState.isValid = true
}
// BytesFromBandwidthAndTimeDelta calculates the bytes
// from a bandwidth(bits per second) and a time delta
func bytesFromBandwidthAndTimeDelta(bandwidth Bandwidth, delta time.Duration) congestion.ByteCount {
return (congestion.ByteCount(bandwidth) * congestion.ByteCount(delta)) /
(congestion.ByteCount(time.Second) * 8)
// ConnectionStates Record of the connection state at the point where each packet in flight was
// sent, indexed by the packet number.
// FIXME: using LinkedList replace map to fast remove all the packets lower than the specified packet number.
type ConnectionStates struct {
stats map[congestion.PacketNumber]*ConnectionStateOnSentPacket
}
func timeDeltaFromBytesAndBandwidth(bytes congestion.ByteCount, bandwidth Bandwidth) time.Duration {
return time.Duration(bytes*8) * time.Second / time.Duration(bandwidth)
func (s *ConnectionStates) Insert(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) bool {
if _, ok := s.stats[packetNumber]; ok {
return false
}
s.stats[packetNumber] = NewConnectionStateOnSentPacket(packetNumber, sentTime, bytes, sampler)
return true
}
func (s *ConnectionStates) Get(packetNumber congestion.PacketNumber) *ConnectionStateOnSentPacket {
return s.stats[packetNumber]
}
func (s *ConnectionStates) Remove(packetNumber congestion.PacketNumber) (bool, *ConnectionStateOnSentPacket) {
state, ok := s.stats[packetNumber]
if ok {
delete(s.stats, packetNumber)
}
return ok, state
}
func NewConnectionStateOnSentPacket(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) *ConnectionStateOnSentPacket {
return &ConnectionStateOnSentPacket{
packetNumber: packetNumber,
sendTime: sentTime,
size: bytes,
lastAckedPacketSentTime: sampler.lastAckedPacketSentTime,
lastAckedPacketAckTime: sampler.lastAckedPacketAckTime,
totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket,
sendTimeState: SendTimeState{
isValid: true,
isAppLimited: sampler.isAppLimited,
totalBytesSent: sampler.totalBytesSent,
totalBytesAcked: sampler.totalBytesAcked,
totalBytesLost: sampler.totalBytesLost,
},
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,199 +0,0 @@
package bbr
import (
"github.com/apernet/quic-go/congestion"
)
// packetNumberIndexedQueue is a queue of mostly continuous numbered entries
// which supports the following operations:
// - adding elements to the end of the queue, or at some point past the end
// - removing elements in any order
// - retrieving elements
// If all elements are inserted in order, all of the operations above are
// amortized O(1) time.
//
// Internally, the data structure is a deque where each element is marked as
// present or not. The deque starts at the lowest present index. Whenever an
// element is removed, it's marked as not present, and the front of the deque is
// cleared of elements that are not present.
//
// The tail of the queue is not cleared due to the assumption of entries being
// inserted in order, though removing all elements of the queue will return it
// to its initial state.
//
// Note that this data structure is inherently hazardous, since an addition of
// just two entries will cause it to consume all of the memory available.
// Because of that, it is not a general-purpose container and should not be used
// as one.
type entryWrapper[T any] struct {
present bool
entry T
}
type packetNumberIndexedQueue[T any] struct {
entries RingBuffer[entryWrapper[T]]
numberOfPresentEntries int
firstPacket congestion.PacketNumber
}
func newPacketNumberIndexedQueue[T any](size int) *packetNumberIndexedQueue[T] {
q := &packetNumberIndexedQueue[T]{
firstPacket: invalidPacketNumber,
}
q.entries.Init(size)
return q
}
// Emplace inserts data associated |packet_number| into (or past) the end of the
// queue, filling up the missing intermediate entries as necessary. Returns
// true if the element has been inserted successfully, false if it was already
// in the queue or inserted out of order.
func (p *packetNumberIndexedQueue[T]) Emplace(packetNumber congestion.PacketNumber, entry *T) bool {
if packetNumber == invalidPacketNumber || entry == nil {
return false
}
if p.IsEmpty() {
p.entries.PushBack(entryWrapper[T]{
present: true,
entry: *entry,
})
p.numberOfPresentEntries = 1
p.firstPacket = packetNumber
return true
}
// Do not allow insertion out-of-order.
if packetNumber <= p.LastPacket() {
return false
}
// Handle potentially missing elements.
offset := int(packetNumber - p.FirstPacket())
if gap := offset - p.entries.Len(); gap > 0 {
for i := 0; i < gap; i++ {
p.entries.PushBack(entryWrapper[T]{})
}
}
p.entries.PushBack(entryWrapper[T]{
present: true,
entry: *entry,
})
p.numberOfPresentEntries++
return true
}
// GetEntry Retrieve the entry associated with the packet number. Returns the pointer
// to the entry in case of success, or nullptr if the entry does not exist.
func (p *packetNumberIndexedQueue[T]) GetEntry(packetNumber congestion.PacketNumber) *T {
ew := p.getEntryWraper(packetNumber)
if ew == nil {
return nil
}
return &ew.entry
}
// Remove, Same as above, but if an entry is present in the queue, also call f(entry)
// before removing it.
func (p *packetNumberIndexedQueue[T]) Remove(packetNumber congestion.PacketNumber, f func(T)) bool {
ew := p.getEntryWraper(packetNumber)
if ew == nil {
return false
}
if f != nil {
f(ew.entry)
}
ew.present = false
p.numberOfPresentEntries--
if packetNumber == p.FirstPacket() {
p.clearup()
}
return true
}
// RemoveUpTo, but not including |packet_number|.
// Unused slots in the front are also removed, which means when the function
// returns, |first_packet()| can be larger than |packet_number|.
func (p *packetNumberIndexedQueue[T]) RemoveUpTo(packetNumber congestion.PacketNumber) {
for !p.entries.Empty() &&
p.firstPacket != invalidPacketNumber &&
p.firstPacket < packetNumber {
if p.entries.Front().present {
p.numberOfPresentEntries--
}
p.entries.PopFront()
p.firstPacket++
}
p.clearup()
return
}
// IsEmpty return if queue is empty.
func (p *packetNumberIndexedQueue[T]) IsEmpty() bool {
return p.numberOfPresentEntries == 0
}
// NumberOfPresentEntries returns the number of entries in the queue.
func (p *packetNumberIndexedQueue[T]) NumberOfPresentEntries() int {
return p.numberOfPresentEntries
}
// EntrySlotsUsed returns the number of entries allocated in the underlying deque. This is
// proportional to the memory usage of the queue.
func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int {
return p.entries.Len()
}
// FirstPacket returns packet number of the first entry in the queue.
func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) {
return p.firstPacket
}
// LastPacket returns packet number of the last entry ever inserted in the queue. Note that the
// entry in question may have already been removed. Zero if the queue is
// empty.
func (p *packetNumberIndexedQueue[T]) LastPacket() (packetNumber congestion.PacketNumber) {
if p.IsEmpty() {
return invalidPacketNumber
}
return p.firstPacket + congestion.PacketNumber(p.entries.Len()-1)
}
func (p *packetNumberIndexedQueue[T]) clearup() {
for !p.entries.Empty() && !p.entries.Front().present {
p.entries.PopFront()
p.firstPacket++
}
if p.entries.Empty() {
p.firstPacket = invalidPacketNumber
}
}
func (p *packetNumberIndexedQueue[T]) getEntryWraper(packetNumber congestion.PacketNumber) *entryWrapper[T] {
if packetNumber == invalidPacketNumber ||
p.IsEmpty() ||
packetNumber < p.firstPacket {
return nil
}
offset := int(packetNumber - p.firstPacket)
if offset >= p.entries.Len() {
return nil
}
ew := p.entries.Offset(offset)
if ew == nil || !ew.present {
return nil
}
return ew
}

View file

@ -1,118 +0,0 @@
package bbr
// A RingBuffer is a ring buffer.
// It acts as a heap that doesn't cause any allocations.
type RingBuffer[T any] struct {
ring []T
headPos, tailPos int
full bool
}
// Init preallocs a buffer with a certain size.
func (r *RingBuffer[T]) Init(size int) {
r.ring = make([]T, size)
}
// Len returns the number of elements in the ring buffer.
func (r *RingBuffer[T]) Len() int {
if r.full {
return len(r.ring)
}
if r.tailPos >= r.headPos {
return r.tailPos - r.headPos
}
return r.tailPos - r.headPos + len(r.ring)
}
// Empty says if the ring buffer is empty.
func (r *RingBuffer[T]) Empty() bool {
return !r.full && r.headPos == r.tailPos
}
// PushBack adds a new element.
// If the ring buffer is full, its capacity is increased first.
func (r *RingBuffer[T]) PushBack(t T) {
if r.full || len(r.ring) == 0 {
r.grow()
}
r.ring[r.tailPos] = t
r.tailPos++
if r.tailPos == len(r.ring) {
r.tailPos = 0
}
if r.tailPos == r.headPos {
r.full = true
}
}
// PopFront returns the next element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first.
func (r *RingBuffer[T]) PopFront() T {
if r.Empty() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: pop from an empty queue")
}
r.full = false
t := r.ring[r.headPos]
r.ring[r.headPos] = *new(T)
r.headPos++
if r.headPos == len(r.ring) {
r.headPos = 0
}
return t
}
// Offset returns the offset element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first
// and check if the index larger than buffer length.
func (r *RingBuffer[T]) Offset(index int) *T {
if r.Empty() || index >= r.Len() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: offset from invalid index")
}
offset := (r.headPos + index) % len(r.ring)
return &r.ring[offset]
}
// Front returns the front element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first.
func (r *RingBuffer[T]) Front() *T {
if r.Empty() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: front from an empty queue")
}
return &r.ring[r.headPos]
}
// Back returns the back element.
// It must not be called when the buffer is empty, that means that
// callers might need to check if there are elements in the buffer first.
func (r *RingBuffer[T]) Back() *T {
if r.Empty() {
panic("github.com/quic-go/quic-go/internal/utils/ringbuffer: back from an empty queue")
}
return r.Offset(r.Len() - 1)
}
// Grow the maximum size of the queue.
// This method assume the queue is full.
func (r *RingBuffer[T]) grow() {
oldRing := r.ring
newSize := len(oldRing) * 2
if newSize == 0 {
newSize = 1
}
r.ring = make([]T, newSize)
headLen := copy(r.ring, oldRing[r.headPos:])
copy(r.ring[headLen:], oldRing[:r.headPos])
r.headPos, r.tailPos, r.full = 0, len(oldRing), false
}
// Clear removes all elements.
func (r *RingBuffer[T]) Clear() {
var zeroValue T
for i := range r.ring {
r.ring[i] = zeroValue
}
r.headPos, r.tailPos, r.full = 0, 0, false
}

View file

@ -1,162 +1,132 @@
package bbr
import (
"golang.org/x/exp/constraints"
)
// Implements Kathleen Nichols' algorithm for tracking the minimum (or maximum)
// estimate of a stream of samples over some fixed time interval. (E.g.,
// the minimum RTT over the past five minutes.) The algorithm keeps track of
// the best, second best, and third best min (or max) estimates, maintaining an
// invariant that the measurement time of the n'th best >= n-1'th best.
// The algorithm works as follows. On a reset, all three estimates are set to
// the same sample. The second best estimate is then recorded in the second
// quarter of the window, and a third best estimate is recorded in the second
// half of the window, bounding the worst case error when the true min is
// monotonically increasing (or true max is monotonically decreasing) over the
// window.
// WindowedFilter Use the following to construct a windowed filter object of type T.
// For example, a min filter using QuicTime as the time type:
//
// A new best sample replaces all three estimates, since the new best is lower
// (or higher) than everything else in the window and it is the most recent.
// The window thus effectively gets reset on every new min. The same property
// holds true for second best and third best estimates. Specifically, when a
// sample arrives that is better than the second best but not better than the
// best, it replaces the second and third best estimates but not the best
// estimate. Similarly, a sample that is better than the third best estimate
// but not the other estimates replaces only the third best estimate.
// WindowedFilter<T, MinFilter<T>, QuicTime, QuicTime::Delta> ObjectName;
//
// Finally, when the best expires, it is replaced by the second best, which in
// turn is replaced by the third best. The newest sample replaces the third
// best.
type WindowedFilterValue interface {
any
}
type WindowedFilterTime interface {
constraints.Integer | constraints.Float
}
type WindowedFilter[V WindowedFilterValue, T WindowedFilterTime] struct {
// A max filter using 64-bit integers as the time type:
//
// WindowedFilter<T, MaxFilter<T>, uint64_t, int64_t> ObjectName;
//
// Specifically, this template takes four arguments:
// 1. T -- type of the measurement that is being filtered.
// 2. Compare -- MinFilter<T> or MaxFilter<T>, depending on the type of filter
// desired.
// 3. TimeT -- the type used to represent timestamps.
// 4. TimeDeltaT -- the type used to represent continuous time intervals between
// two timestamps. Has to be the type of (a - b) if both |a| and |b| are
// of type TimeT.
type WindowedFilter struct {
// Time length of window.
windowLength T
estimates []entry[V, T]
comparator func(V, V) int
windowLength int64
estimates []Sample
comparator func(int64, int64) bool
}
type entry[V WindowedFilterValue, T WindowedFilterTime] struct {
sample V
time T
type Sample struct {
sample int64
time int64
}
// Compares two values and returns true if the first is greater than or equal
// to the second.
func MaxFilter[O constraints.Ordered](a, b O) int {
if a > b {
return 1
} else if a < b {
return -1
}
return 0
func MaxFilter(a, b int64) bool {
return a >= b
}
// Compares two values and returns true if the first is less than or equal
// to the second.
func MinFilter[O constraints.Ordered](a, b O) int {
if a < b {
return 1
} else if a > b {
return -1
}
return 0
func MinFilter(a, b int64) bool {
return a <= b
}
func NewWindowedFilter[V WindowedFilterValue, T WindowedFilterTime](windowLength T, comparator func(V, V) int) *WindowedFilter[V, T] {
return &WindowedFilter[V, T]{
func NewWindowedFilter(windowLength int64, comparator func(int64, int64) bool) *WindowedFilter {
return &WindowedFilter{
windowLength: windowLength,
estimates: make([]entry[V, T], 3, 3),
estimates: make([]Sample, 3),
comparator: comparator,
}
}
// Changes the window length. Does not update any current samples.
func (f *WindowedFilter[V, T]) SetWindowLength(windowLength T) {
func (f *WindowedFilter) SetWindowLength(windowLength int64) {
f.windowLength = windowLength
}
func (f *WindowedFilter[V, T]) GetBest() V {
func (f *WindowedFilter) GetBest() int64 {
return f.estimates[0].sample
}
func (f *WindowedFilter[V, T]) GetSecondBest() V {
func (f *WindowedFilter) GetSecondBest() int64 {
return f.estimates[1].sample
}
func (f *WindowedFilter[V, T]) GetThirdBest() V {
func (f *WindowedFilter) GetThirdBest() int64 {
return f.estimates[2].sample
}
// Updates best estimates with |sample|, and expires and updates best
// estimates as necessary.
func (f *WindowedFilter[V, T]) Update(newSample V, newTime T) {
// Reset all estimates if they have not yet been initialized, if new sample
// is a new best, or if the newest recorded estimate is too old.
if f.comparator(f.estimates[0].sample, *new(V)) == 0 ||
f.comparator(newSample, f.estimates[0].sample) >= 0 ||
newTime-f.estimates[2].time > f.windowLength {
f.Reset(newSample, newTime)
func (f *WindowedFilter) Update(sample, time int64) {
if f.estimates[0].time == 0 || f.comparator(sample, f.estimates[0].sample) || (time-f.estimates[2].time) > f.windowLength {
f.Reset(sample, time)
return
}
if f.comparator(newSample, f.estimates[1].sample) >= 0 {
f.estimates[1] = entry[V, T]{newSample, newTime}
f.estimates[2] = f.estimates[1]
} else if f.comparator(newSample, f.estimates[2].sample) >= 0 {
f.estimates[2] = entry[V, T]{newSample, newTime}
if f.comparator(sample, f.estimates[1].sample) {
f.estimates[1].sample = sample
f.estimates[1].time = time
f.estimates[2].sample = sample
f.estimates[2].time = time
} else if f.comparator(sample, f.estimates[2].sample) {
f.estimates[2].sample = sample
f.estimates[2].time = time
}
// Expire and update estimates as necessary.
if newTime-f.estimates[0].time > f.windowLength {
if time-f.estimates[0].time > f.windowLength {
// The best estimate hasn't been updated for an entire window, so promote
// second and third best estimates.
f.estimates[0] = f.estimates[1]
f.estimates[1] = f.estimates[2]
f.estimates[2] = entry[V, T]{newSample, newTime}
f.estimates[0].sample = f.estimates[1].sample
f.estimates[0].time = f.estimates[1].time
f.estimates[1].sample = f.estimates[2].sample
f.estimates[1].time = f.estimates[2].time
f.estimates[2].sample = sample
f.estimates[2].time = time
// Need to iterate one more time. Check if the new best estimate is
// outside the window as well, since it may also have been recorded a
// long time ago. Don't need to iterate once more since we cover that
// case at the beginning of the method.
if newTime-f.estimates[0].time > f.windowLength {
f.estimates[0] = f.estimates[1]
f.estimates[1] = f.estimates[2]
if time-f.estimates[0].time > f.windowLength {
f.estimates[0].sample = f.estimates[1].sample
f.estimates[0].time = f.estimates[1].time
f.estimates[1].sample = f.estimates[2].sample
f.estimates[1].time = f.estimates[2].time
}
return
}
if f.comparator(f.estimates[1].sample, f.estimates[0].sample) == 0 &&
newTime-f.estimates[1].time > f.windowLength/4 {
if f.estimates[1].sample == f.estimates[0].sample && time-f.estimates[1].time > f.windowLength>>2 {
// A quarter of the window has passed without a better sample, so the
// second-best estimate is taken from the second quarter of the window.
f.estimates[1] = entry[V, T]{newSample, newTime}
f.estimates[2] = f.estimates[1]
f.estimates[1].sample = sample
f.estimates[1].time = time
f.estimates[2].sample = sample
f.estimates[2].time = time
return
}
if f.comparator(f.estimates[2].sample, f.estimates[1].sample) == 0 &&
newTime-f.estimates[2].time > f.windowLength/2 {
if f.estimates[2].sample == f.estimates[1].sample && time-f.estimates[2].time > f.windowLength>>1 {
// We've passed a half of the window without a better estimate, so take
// a third-best estimate from the second half of the window.
f.estimates[2] = entry[V, T]{newSample, newTime}
f.estimates[2].sample = sample
f.estimates[2].time = time
}
}
// Resets all estimates to new sample.
func (f *WindowedFilter[V, T]) Reset(newSample V, newTime T) {
f.estimates[2] = entry[V, T]{newSample, newTime}
f.estimates[1] = f.estimates[2]
f.estimates[0] = f.estimates[1]
}
func (f *WindowedFilter[V, T]) Clear() {
f.estimates = make([]entry[V, T], 3, 3)
func (f *WindowedFilter) Reset(newSample, newTime int64) {
f.estimates[0].sample = newSample
f.estimates[0].time = newTime
f.estimates[1].sample = newSample
f.estimates[1].time = newTime
f.estimates[2].sample = newSample
f.estimates[2].time = newTime
}

View file

@ -1,24 +1,17 @@
package brutal
import (
"fmt"
"os"
"strconv"
"time"
"github.com/apernet/hysteria/core/v2/internal/congestion/common"
"github.com/apernet/hysteria/core/internal/congestion/common"
"github.com/apernet/quic-go/congestion"
)
const (
pktInfoSlotCount = 5 // slot index is based on seconds, so this is basically how many seconds we sample
minSampleCount = 50
minAckRate = 0.8
congestionWindowMultiplier = 2
debugEnv = "HYSTERIA_BRUTAL_DEBUG"
debugPrintInterval = 2
pktInfoSlotCount = 4
minSampleCount = 50
minAckRate = 0.8
)
var _ congestion.CongestionControl = &BrutalSender{}
@ -31,9 +24,6 @@ type BrutalSender struct {
pktInfoSlots [pktInfoSlotCount]pktInfo
ackRate float64
debug bool
lastAckPrintTimestamp int64
}
type pktInfo struct {
@ -43,12 +33,10 @@ type pktInfo struct {
}
func NewBrutalSender(bps uint64) *BrutalSender {
debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
bs := &BrutalSender{
bps: congestion.ByteCount(bps),
maxDatagramSize: congestion.InitialPacketSizeIPv4,
maxDatagramSize: common.InitMaxDatagramSize,
ackRate: 1,
debug: debug,
}
bs.pacer = common.NewPacer(func() congestion.ByteCount {
return congestion.ByteCount(float64(bs.bps) / bs.ackRate)
@ -69,7 +57,7 @@ func (b *BrutalSender) HasPacingBudget(now time.Time) bool {
}
func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {
return bytesInFlight <= b.GetCongestionWindow()
return bytesInFlight < b.GetCongestionWindow()
}
func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {
@ -77,11 +65,7 @@ func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {
if rtt <= 0 {
return 10240
}
cwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate)
if cwnd < b.maxDatagramSize {
cwnd = b.maxDatagramSize
}
return cwnd
return congestion.ByteCount(float64(b.bps) * rtt.Seconds() * 1.5 / b.ackRate)
}
func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount,
@ -93,26 +77,31 @@ func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion
func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount,
priorInFlight congestion.ByteCount, eventTime time.Time,
) {
// Stub
}
func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount,
priorInFlight congestion.ByteCount,
) {
// Stub
}
func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime time.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {
currentTimestamp := eventTime.Unix()
slot := currentTimestamp % pktInfoSlotCount
if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets))
b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets))
b.pktInfoSlots[slot].AckCount++
} else {
// uninitialized slot or too old, reset
b.pktInfoSlots[slot].Timestamp = currentTimestamp
b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets))
b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets))
b.pktInfoSlots[slot].AckCount = 1
b.pktInfoSlots[slot].LossCount = 0
}
b.updateAckRate(currentTimestamp)
}
func (b *BrutalSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount,
priorInFlight congestion.ByteCount,
) {
currentTimestamp := time.Now().Unix()
slot := currentTimestamp % pktInfoSlotCount
if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
b.pktInfoSlots[slot].LossCount++
} else {
// uninitialized slot or too old, reset
b.pktInfoSlots[slot].Timestamp = currentTimestamp
b.pktInfoSlots[slot].AckCount = 0
b.pktInfoSlots[slot].LossCount = 1
}
b.updateAckRate(currentTimestamp)
}
@ -120,9 +109,6 @@ func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, e
func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {
b.maxDatagramSize = size
b.pacer.SetMaxDatagramSize(size)
if b.debug {
b.debugPrint("SetMaxDatagramSize: %d", size)
}
}
func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
@ -137,29 +123,12 @@ func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
}
if ackCount+lossCount < minSampleCount {
b.ackRate = 1
if b.canPrintAckRate(currentTimestamp) {
b.lastAckPrintTimestamp = currentTimestamp
b.debugPrint("Not enough samples (total=%d, ack=%d, loss=%d, rtt=%d)",
ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())
}
return
}
rate := float64(ackCount) / float64(ackCount+lossCount)
if rate < minAckRate {
b.ackRate = minAckRate
if b.canPrintAckRate(currentTimestamp) {
b.lastAckPrintTimestamp = currentTimestamp
b.debugPrint("ACK rate too low: %.2f, clamped to %.2f (total=%d, ack=%d, loss=%d, rtt=%d)",
rate, minAckRate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())
}
return
}
b.ackRate = rate
if b.canPrintAckRate(currentTimestamp) {
b.lastAckPrintTimestamp = currentTimestamp
b.debugPrint("ACK rate: %.2f (total=%d, ack=%d, loss=%d, rtt=%d)",
rate, ackCount+lossCount, ackCount, lossCount, b.rttStats.SmoothedRTT().Milliseconds())
}
}
func (b *BrutalSender) InSlowStart() bool {
@ -173,13 +142,3 @@ func (b *BrutalSender) InRecovery() bool {
func (b *BrutalSender) MaybeExitSlowStart() {}
func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {}
func (b *BrutalSender) canPrintAckRate(currentTimestamp int64) bool {
return b.debug && currentTimestamp-b.lastAckPrintTimestamp >= debugPrintInterval
}
func (b *BrutalSender) debugPrint(format string, a ...any) {
fmt.Printf("[BrutalSender] [%s] %s\n",
time.Now().Format("15:04:05"),
fmt.Sprintf(format, a...))
}

View file

@ -1,14 +1,17 @@
package common
import (
"math"
"time"
"github.com/apernet/quic-go/congestion"
)
const (
maxBurstPackets = 10
maxBurstPacingDelayMultiplier = 4
InitMaxDatagramSize = 1252
maxBurstPackets = 10
minPacingDelay = time.Millisecond
)
// Pacer implements a token bucket pacing algorithm.
@ -21,8 +24,8 @@ type Pacer struct {
func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer {
p := &Pacer{
budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSizeIPv4,
maxDatagramSize: congestion.InitialPacketSizeIPv4,
budgetAtLastSent: maxBurstPackets * InitMaxDatagramSize,
maxDatagramSize: InitMaxDatagramSize,
getBandwidth: getBandwidth,
}
return p
@ -43,15 +46,12 @@ func (p *Pacer) Budget(now time.Time) congestion.ByteCount {
return p.maxBurstSize()
}
budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9
if budget < 0 { // protect against overflows
budget = congestion.ByteCount(1<<62 - 1)
}
return min(p.maxBurstSize(), budget)
return minByteCount(p.maxBurstSize(), budget)
}
func (p *Pacer) maxBurstSize() congestion.ByteCount {
return max(
congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9,
return maxByteCount(
congestion.ByteCount((minPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9,
maxBurstPackets*p.maxDatagramSize,
)
}
@ -62,18 +62,34 @@ func (p *Pacer) TimeUntilSend() time.Time {
if p.budgetAtLastSent >= p.maxDatagramSize {
return time.Time{}
}
diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent)
bw := uint64(p.getBandwidth())
// We might need to round up this value.
// Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires.
d := diff / bw
// this is effectively a math.Ceil, but using only integer math
if diff%bw > 0 {
d++
}
return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond))
return p.lastSentTime.Add(maxDuration(
minPacingDelay,
time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/
float64(p.getBandwidth())))*time.Nanosecond,
))
}
func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) {
p.maxDatagramSize = s
}
func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount {
if a < b {
return b
}
return a
}
func minByteCount(a, b congestion.ByteCount) congestion.ByteCount {
if a < b {
return a
}
return b
}
func maxDuration(a, b time.Duration) time.Duration {
if a > b {
return a
}
return b
}

View file

@ -1,15 +1,18 @@
package congestion
import (
"github.com/apernet/hysteria/core/v2/internal/congestion/bbr"
"github.com/apernet/hysteria/core/v2/internal/congestion/brutal"
"github.com/apernet/hysteria/core/internal/congestion/bbr"
"github.com/apernet/hysteria/core/internal/congestion/brutal"
"github.com/apernet/hysteria/core/internal/congestion/common"
"github.com/apernet/quic-go"
)
func UseBBR(conn quic.Connection) {
conn.SetCongestionControl(bbr.NewBbrSender(
conn.SetCongestionControl(bbr.NewBBRSender(
bbr.DefaultClock{},
bbr.GetInitialPacketSize(conn.RemoteAddr()),
bbr.InitialCongestionWindow*common.InitMaxDatagramSize,
bbr.DefaultBBRMaxCongestionWindow*common.InitMaxDatagramSize,
))
}

View file

@ -1,7 +1,7 @@
package frag
import (
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/internal/protocol"
)
func FragUDPMessage(m *protocol.UDPMessage, maxSize int) []protocol.UDPMessage {

View file

@ -4,7 +4,7 @@ import (
"reflect"
"testing"
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/internal/protocol"
)
func TestFragUDPMessage(t *testing.T) {

View file

@ -7,7 +7,7 @@ packages:
Conn:
config:
mockname: MockConn
github.com/apernet/hysteria/core/v2/server:
github.com/apernet/hysteria/core/server:
interfaces:
Outbound:
config:
@ -24,6 +24,3 @@ packages:
TrafficLogger:
config:
mockname: MockTrafficLogger
RequestHook:
config:
mockname: MockRequestHook

View file

@ -2,17 +2,16 @@ package integration_tests
import (
"io"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/hysteria/core/client"
"github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/server"
)
// TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly.
@ -35,7 +34,7 @@ func TestClientServerTCPClose(t *testing.T) {
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
@ -49,14 +48,13 @@ func TestClientServerTCPClose(t *testing.T) {
// Server outbound connection should write the same thing, then close.
sobConn := mocks.NewMockConn(t)
sobConnCh := make(chan struct{}) // For close signal only
sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })
sobConn.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
<-sobConnCh
return 0, io.EOF
})
sobConn.EXPECT().Write([]byte("happy")).Return(5, nil)
sobConn.EXPECT().Close().RunAndReturn(func() error {
sobConnChCloseFunc()
close(sobConnCh)
return nil
})
serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()
@ -118,7 +116,7 @@ func TestClientServerUDPIdleTimeout(t *testing.T) {
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
@ -135,7 +133,6 @@ func TestClientServerUDPIdleTimeout(t *testing.T) {
// to trigger the server's UDP idle timeout.
sobConn := mocks.NewMockUDPConn(t)
sobConnCh := make(chan []byte, 1)
sobConnChCloseFunc := sync.OnceFunc(func() { close(sobConnCh) })
sobConn.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(bs []byte) (int, string, error) {
d := <-sobConnCh
if d == nil {
@ -170,7 +167,7 @@ func TestClientServerUDPIdleTimeout(t *testing.T) {
}
// Now we wait for 3 seconds, the server should close the UDP session.
sobConn.EXPECT().Close().RunAndReturn(func() error {
sobConnChCloseFunc()
close(sobConnCh)
return nil
})
eventLogger.EXPECT().UDPError(mock.Anything, mock.Anything, uint32(1), nil).Once()
@ -197,7 +194,7 @@ func TestClientServerClientShutdown(t *testing.T) {
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
@ -226,24 +223,20 @@ func TestClientServerServerShutdown(t *testing.T) {
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
QUICConfig: client.QUICConfig{
MaxIdleTimeout: 4 * time.Second,
},
})
assert.NoError(t, err)
// Close the server - expect the client to return ClosedError for both TCP & UDP calls.
_ = s.Close()
time.Sleep(1 * time.Second)
_, err = c.TCP("whatever")
_, ok := err.(errors.ClosedError)
assert.True(t, ok)
time.Sleep(1 * time.Second) // Allow some time for the error to be propagated to the UDP session manager
_, err = c.UDP()
_, ok = err.(errors.ClosedError)
assert.True(t, ok)

View file

@ -1,147 +0,0 @@
package integration_tests
import (
"io"
"net"
"testing"
"github.com/apernet/hysteria/core/v2/client"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/quic-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestClientServerHookTCP(t *testing.T) {
fakeEchoAddr := "hahanope:6666"
realEchoAddr := "127.0.0.1:22333"
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
hook := mocks.NewMockRequestHook(t)
hook.EXPECT().Check(false, fakeEchoAddr).Return(true).Once()
hook.EXPECT().TCP(mock.Anything, mock.Anything).RunAndReturn(func(stream quic.Stream, s *string) ([]byte, error) {
assert.Equal(t, fakeEchoAddr, *s)
// Change the address
*s = realEchoAddr
// Read the first 5 bytes and replace them with "byeee"
data := make([]byte, 5)
_, err := io.ReadFull(stream, data)
if err != nil {
return nil, err
}
assert.Equal(t, []byte("hello"), data)
return []byte("byeee"), nil
}).Once()
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
RequestHook: hook,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create TCP echo server
echoListener, err := net.Listen("tcp", realEchoAddr)
assert.NoError(t, err)
echoServer := &tcpEchoServer{Listener: echoListener}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
// Dial TCP
conn, err := c.TCP(fakeEchoAddr)
assert.NoError(t, err)
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
_, err = conn.Write(sData)
assert.NoError(t, err)
rData := make([]byte, len(sData))
_, err = io.ReadFull(conn, rData)
assert.NoError(t, err)
assert.Equal(t, []byte("byeee world"), rData)
}
func TestClientServerHookUDP(t *testing.T) {
fakeEchoAddr := "hahanope:6666"
realEchoAddr := "127.0.0.1:22333"
// Create server
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
hook := mocks.NewMockRequestHook(t)
hook.EXPECT().Check(true, fakeEchoAddr).Return(true).Once()
hook.EXPECT().UDP(mock.Anything, mock.Anything).RunAndReturn(func(bytes []byte, s *string) error {
assert.Equal(t, fakeEchoAddr, *s)
assert.Equal(t, []byte("hello world"), bytes)
// Change the address
*s = realEchoAddr
return nil
}).Once()
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
RequestHook: hook,
Authenticator: auth,
})
assert.NoError(t, err)
defer s.Close()
go s.Serve()
// Create UDP echo server
echoConn, err := net.ListenPacket("udp", realEchoAddr)
assert.NoError(t, err)
echoServer := &udpEchoServer{Conn: echoConn}
defer echoServer.Close()
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
assert.NoError(t, err)
defer c.Close()
// Listen UDP
conn, err := c.UDP()
assert.NoError(t, err)
defer conn.Close()
// Send and receive data
sData := []byte("hello world")
err = conn.Send(sData, fakeEchoAddr)
assert.NoError(t, err)
rData, rAddr, err := conn.Receive()
assert.NoError(t, err)
assert.Equal(t, sData, rData)
// Hook address change is transparent,
// the client should still see the fake echo address it sent packets to
assert.Equal(t, fakeEchoAddr, rAddr)
// Subsequent packets should also be sent to the real echo server
sData = []byte("never stop fighting")
err = conn.Send(sData, fakeEchoAddr)
assert.NoError(t, err)
rData, rAddr, err = conn.Receive()
assert.NoError(t, err)
assert.Equal(t, sData, rData)
assert.Equal(t, fakeEchoAddr, rAddr)
}

View file

@ -12,9 +12,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/internal/protocol"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/hysteria/core/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/internal/protocol"
"github.com/apernet/hysteria/core/server"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
@ -41,6 +41,7 @@ func TestServerMasquerade(t *testing.T) {
// QUIC connection & RoundTripper
var conn quic.EarlyConnection
rt := &http3.RoundTripper{
EnableDatagrams: true,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package mocks
@ -25,10 +25,6 @@ func (_m *MockAuthenticator) EXPECT() *MockAuthenticator_Expecter {
func (_m *MockAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (bool, string) {
ret := _m.Called(addr, auth, tx)
if len(ret) == 0 {
panic("no return value specified for Authenticate")
}
var r0 bool
var r1 string
if rf, ok := ret.Get(0).(func(net.Addr, string, uint64) (bool, string)); ok {

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package mocks
@ -27,10 +27,6 @@ func (_m *MockConn) EXPECT() *MockConn_Expecter {
func (_m *MockConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
@ -72,10 +68,6 @@ func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Ca
func (_m *MockConn) LocalAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for LocalAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
@ -119,10 +111,6 @@ func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_L
func (_m *MockConn) Read(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
@ -175,10 +163,6 @@ func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockC
func (_m *MockConn) RemoteAddr() net.Addr {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for RemoteAddr")
}
var r0 net.Addr
if rf, ok := ret.Get(0).(func() net.Addr); ok {
r0 = rf()
@ -222,10 +206,6 @@ func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_
func (_m *MockConn) SetDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
@ -268,10 +248,6 @@ func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *Mo
func (_m *MockConn) SetReadDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetReadDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
@ -314,10 +290,6 @@ func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error)
func (_m *MockConn) SetWriteDeadline(t time.Time) error {
ret := _m.Called(t)
if len(ret) == 0 {
panic("no return value specified for SetWriteDeadline")
}
var r0 error
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
r0 = rf(t)
@ -360,10 +332,6 @@ func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error
func (_m *MockConn) Write(b []byte) (int, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package mocks

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package mocks
@ -7,7 +7,7 @@ import (
mock "github.com/stretchr/testify/mock"
server "github.com/apernet/hysteria/core/v2/server"
server "github.com/apernet/hysteria/core/server"
)
// MockOutbound is an autogenerated mock type for the Outbound type
@ -27,10 +27,6 @@ func (_m *MockOutbound) EXPECT() *MockOutbound_Expecter {
func (_m *MockOutbound) TCP(reqAddr string) (net.Conn, error) {
ret := _m.Called(reqAddr)
if len(ret) == 0 {
panic("no return value specified for TCP")
}
var r0 net.Conn
var r1 error
if rf, ok := ret.Get(0).(func(string) (net.Conn, error)); ok {
@ -85,10 +81,6 @@ func (_c *MockOutbound_TCP_Call) RunAndReturn(run func(string) (net.Conn, error)
func (_m *MockOutbound) UDP(reqAddr string) (server.UDPConn, error) {
ret := _m.Called(reqAddr)
if len(ret) == 0 {
panic("no return value specified for UDP")
}
var r0 server.UDPConn
var r1 error
if rf, ok := ret.Get(0).(func(string) (server.UDPConn, error)); ok {

View file

@ -1,188 +0,0 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
import (
quic "github.com/apernet/quic-go"
mock "github.com/stretchr/testify/mock"
)
// MockRequestHook is an autogenerated mock type for the RequestHook type
type MockRequestHook struct {
mock.Mock
}
type MockRequestHook_Expecter struct {
mock *mock.Mock
}
func (_m *MockRequestHook) EXPECT() *MockRequestHook_Expecter {
return &MockRequestHook_Expecter{mock: &_m.Mock}
}
// Check provides a mock function with given fields: isUDP, reqAddr
func (_m *MockRequestHook) Check(isUDP bool, reqAddr string) bool {
ret := _m.Called(isUDP, reqAddr)
if len(ret) == 0 {
panic("no return value specified for Check")
}
var r0 bool
if rf, ok := ret.Get(0).(func(bool, string) bool); ok {
r0 = rf(isUDP, reqAddr)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockRequestHook_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check'
type MockRequestHook_Check_Call struct {
*mock.Call
}
// Check is a helper method to define mock.On call
// - isUDP bool
// - reqAddr string
func (_e *MockRequestHook_Expecter) Check(isUDP interface{}, reqAddr interface{}) *MockRequestHook_Check_Call {
return &MockRequestHook_Check_Call{Call: _e.mock.On("Check", isUDP, reqAddr)}
}
func (_c *MockRequestHook_Check_Call) Run(run func(isUDP bool, reqAddr string)) *MockRequestHook_Check_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(bool), args[1].(string))
})
return _c
}
func (_c *MockRequestHook_Check_Call) Return(_a0 bool) *MockRequestHook_Check_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRequestHook_Check_Call) RunAndReturn(run func(bool, string) bool) *MockRequestHook_Check_Call {
_c.Call.Return(run)
return _c
}
// TCP provides a mock function with given fields: stream, reqAddr
func (_m *MockRequestHook) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) {
ret := _m.Called(stream, reqAddr)
if len(ret) == 0 {
panic("no return value specified for TCP")
}
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func(quic.Stream, *string) ([]byte, error)); ok {
return rf(stream, reqAddr)
}
if rf, ok := ret.Get(0).(func(quic.Stream, *string) []byte); ok {
r0 = rf(stream, reqAddr)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func(quic.Stream, *string) error); ok {
r1 = rf(stream, reqAddr)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockRequestHook_TCP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TCP'
type MockRequestHook_TCP_Call struct {
*mock.Call
}
// TCP is a helper method to define mock.On call
// - stream quic.Stream
// - reqAddr *string
func (_e *MockRequestHook_Expecter) TCP(stream interface{}, reqAddr interface{}) *MockRequestHook_TCP_Call {
return &MockRequestHook_TCP_Call{Call: _e.mock.On("TCP", stream, reqAddr)}
}
func (_c *MockRequestHook_TCP_Call) Run(run func(stream quic.Stream, reqAddr *string)) *MockRequestHook_TCP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(quic.Stream), args[1].(*string))
})
return _c
}
func (_c *MockRequestHook_TCP_Call) Return(_a0 []byte, _a1 error) *MockRequestHook_TCP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockRequestHook_TCP_Call) RunAndReturn(run func(quic.Stream, *string) ([]byte, error)) *MockRequestHook_TCP_Call {
_c.Call.Return(run)
return _c
}
// UDP provides a mock function with given fields: data, reqAddr
func (_m *MockRequestHook) UDP(data []byte, reqAddr *string) error {
ret := _m.Called(data, reqAddr)
if len(ret) == 0 {
panic("no return value specified for UDP")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, *string) error); ok {
r0 = rf(data, reqAddr)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockRequestHook_UDP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UDP'
type MockRequestHook_UDP_Call struct {
*mock.Call
}
// UDP is a helper method to define mock.On call
// - data []byte
// - reqAddr *string
func (_e *MockRequestHook_Expecter) UDP(data interface{}, reqAddr interface{}) *MockRequestHook_UDP_Call {
return &MockRequestHook_UDP_Call{Call: _e.mock.On("UDP", data, reqAddr)}
}
func (_c *MockRequestHook_UDP_Call) Run(run func(data []byte, reqAddr *string)) *MockRequestHook_UDP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(*string))
})
return _c
}
func (_c *MockRequestHook_UDP_Call) Return(_a0 error) *MockRequestHook_UDP_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockRequestHook_UDP_Call) RunAndReturn(run func([]byte, *string) error) *MockRequestHook_UDP_Call {
_c.Call.Return(run)
return _c
}
// NewMockRequestHook creates a new instance of MockRequestHook. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockRequestHook(t interface {
mock.TestingT
Cleanup(func())
}) *MockRequestHook {
mock := &MockRequestHook{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -1,13 +1,8 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package mocks
import (
quic "github.com/apernet/quic-go"
mock "github.com/stretchr/testify/mock"
server "github.com/apernet/hysteria/core/v2/server"
)
import mock "github.com/stretchr/testify/mock"
// MockTrafficLogger is an autogenerated mock type for the TrafficLogger type
type MockTrafficLogger struct {
@ -22,48 +17,10 @@ func (_m *MockTrafficLogger) EXPECT() *MockTrafficLogger_Expecter {
return &MockTrafficLogger_Expecter{mock: &_m.Mock}
}
// LogOnlineState provides a mock function with given fields: id, online
func (_m *MockTrafficLogger) LogOnlineState(id string, online bool) {
_m.Called(id, online)
}
// MockTrafficLogger_LogOnlineState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogOnlineState'
type MockTrafficLogger_LogOnlineState_Call struct {
*mock.Call
}
// LogOnlineState is a helper method to define mock.On call
// - id string
// - online bool
func (_e *MockTrafficLogger_Expecter) LogOnlineState(id interface{}, online interface{}) *MockTrafficLogger_LogOnlineState_Call {
return &MockTrafficLogger_LogOnlineState_Call{Call: _e.mock.On("LogOnlineState", id, online)}
}
func (_c *MockTrafficLogger_LogOnlineState_Call) Run(run func(id string, online bool)) *MockTrafficLogger_LogOnlineState_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(bool))
})
return _c
}
func (_c *MockTrafficLogger_LogOnlineState_Call) Return() *MockTrafficLogger_LogOnlineState_Call {
_c.Call.Return()
return _c
}
func (_c *MockTrafficLogger_LogOnlineState_Call) RunAndReturn(run func(string, bool)) *MockTrafficLogger_LogOnlineState_Call {
_c.Call.Return(run)
return _c
}
// LogTraffic provides a mock function with given fields: id, tx, rx
func (_m *MockTrafficLogger) LogTraffic(id string, tx uint64, rx uint64) bool {
// Log provides a mock function with given fields: id, tx, rx
func (_m *MockTrafficLogger) Log(id string, tx uint64, rx uint64) bool {
ret := _m.Called(id, tx, rx)
if len(ret) == 0 {
panic("no return value specified for LogTraffic")
}
var r0 bool
if rf, ok := ret.Get(0).(func(string, uint64, uint64) bool); ok {
r0 = rf(id, tx, rx)
@ -74,99 +31,32 @@ func (_m *MockTrafficLogger) LogTraffic(id string, tx uint64, rx uint64) bool {
return r0
}
// MockTrafficLogger_LogTraffic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogTraffic'
type MockTrafficLogger_LogTraffic_Call struct {
// MockTrafficLogger_Log_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Log'
type MockTrafficLogger_Log_Call struct {
*mock.Call
}
// LogTraffic is a helper method to define mock.On call
// Log is a helper method to define mock.On call
// - id string
// - tx uint64
// - rx uint64
func (_e *MockTrafficLogger_Expecter) LogTraffic(id interface{}, tx interface{}, rx interface{}) *MockTrafficLogger_LogTraffic_Call {
return &MockTrafficLogger_LogTraffic_Call{Call: _e.mock.On("LogTraffic", id, tx, rx)}
func (_e *MockTrafficLogger_Expecter) Log(id interface{}, tx interface{}, rx interface{}) *MockTrafficLogger_Log_Call {
return &MockTrafficLogger_Log_Call{Call: _e.mock.On("Log", id, tx, rx)}
}
func (_c *MockTrafficLogger_LogTraffic_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_LogTraffic_Call {
func (_c *MockTrafficLogger_Log_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_Log_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(uint64), args[2].(uint64))
})
return _c
}
func (_c *MockTrafficLogger_LogTraffic_Call) Return(ok bool) *MockTrafficLogger_LogTraffic_Call {
func (_c *MockTrafficLogger_Log_Call) Return(ok bool) *MockTrafficLogger_Log_Call {
_c.Call.Return(ok)
return _c
}
func (_c *MockTrafficLogger_LogTraffic_Call) RunAndReturn(run func(string, uint64, uint64) bool) *MockTrafficLogger_LogTraffic_Call {
_c.Call.Return(run)
return _c
}
// TraceStream provides a mock function with given fields: stream, stats
func (_m *MockTrafficLogger) TraceStream(stream quic.Stream, stats *server.StreamStats) {
_m.Called(stream, stats)
}
// MockTrafficLogger_TraceStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TraceStream'
type MockTrafficLogger_TraceStream_Call struct {
*mock.Call
}
// TraceStream is a helper method to define mock.On call
// - stream quic.Stream
// - stats *server.StreamStats
func (_e *MockTrafficLogger_Expecter) TraceStream(stream interface{}, stats interface{}) *MockTrafficLogger_TraceStream_Call {
return &MockTrafficLogger_TraceStream_Call{Call: _e.mock.On("TraceStream", stream, stats)}
}
func (_c *MockTrafficLogger_TraceStream_Call) Run(run func(stream quic.Stream, stats *server.StreamStats)) *MockTrafficLogger_TraceStream_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(quic.Stream), args[1].(*server.StreamStats))
})
return _c
}
func (_c *MockTrafficLogger_TraceStream_Call) Return() *MockTrafficLogger_TraceStream_Call {
_c.Call.Return()
return _c
}
func (_c *MockTrafficLogger_TraceStream_Call) RunAndReturn(run func(quic.Stream, *server.StreamStats)) *MockTrafficLogger_TraceStream_Call {
_c.Call.Return(run)
return _c
}
// UntraceStream provides a mock function with given fields: stream
func (_m *MockTrafficLogger) UntraceStream(stream quic.Stream) {
_m.Called(stream)
}
// MockTrafficLogger_UntraceStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UntraceStream'
type MockTrafficLogger_UntraceStream_Call struct {
*mock.Call
}
// UntraceStream is a helper method to define mock.On call
// - stream quic.Stream
func (_e *MockTrafficLogger_Expecter) UntraceStream(stream interface{}) *MockTrafficLogger_UntraceStream_Call {
return &MockTrafficLogger_UntraceStream_Call{Call: _e.mock.On("UntraceStream", stream)}
}
func (_c *MockTrafficLogger_UntraceStream_Call) Run(run func(stream quic.Stream)) *MockTrafficLogger_UntraceStream_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(quic.Stream))
})
return _c
}
func (_c *MockTrafficLogger_UntraceStream_Call) Return() *MockTrafficLogger_UntraceStream_Call {
_c.Call.Return()
return _c
}
func (_c *MockTrafficLogger_UntraceStream_Call) RunAndReturn(run func(quic.Stream)) *MockTrafficLogger_UntraceStream_Call {
func (_c *MockTrafficLogger_Log_Call) RunAndReturn(run func(string, uint64, uint64) bool) *MockTrafficLogger_Log_Call {
_c.Call.Return(run)
return _c
}

View file

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.0. DO NOT EDIT.
// Code generated by mockery v2.32.0. DO NOT EDIT.
package mocks
@ -21,10 +21,6 @@ func (_m *MockUDPConn) EXPECT() *MockUDPConn_Expecter {
func (_m *MockUDPConn) Close() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
@ -66,10 +62,6 @@ func (_c *MockUDPConn_Close_Call) RunAndReturn(run func() error) *MockUDPConn_Cl
func (_m *MockUDPConn) ReadFrom(b []byte) (int, string, error) {
ret := _m.Called(b)
if len(ret) == 0 {
panic("no return value specified for ReadFrom")
}
var r0 int
var r1 string
var r2 error
@ -129,10 +121,6 @@ func (_c *MockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, string,
func (_m *MockUDPConn) WriteTo(b []byte, addr string) (int, error) {
ret := _m.Called(b, addr)
if len(ret) == 0 {
panic("no return value specified for WriteTo")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte, string) (int, error)); ok {

View file

@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/apernet/hysteria/core/v2/client"
coreErrs "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
"github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/hysteria/core/client"
coreErrs "github.com/apernet/hysteria/core/errors"
"github.com/apernet/hysteria/core/internal/integration_tests/mocks"
"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.
@ -19,7 +19,7 @@ import (
// TestClientNoServer tests how the client handles a server address it cannot connect to.
// NewClient should return a ConnectError.
func TestClientNoServer(t *testing.T) {
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 55666},
})
assert.Nil(t, c)
@ -46,7 +46,7 @@ func TestClientServerBadAuth(t *testing.T) {
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
Auth: "badpassword",
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
@ -75,7 +75,7 @@ func TestClientServerUDPDisabled(t *testing.T) {
go s.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
@ -113,7 +113,7 @@ func TestClientServerTCPEcho(t *testing.T) {
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
@ -160,7 +160,7 @@ func TestClientServerUDPEcho(t *testing.T) {
go echoServer.Serve()
// Create client
c, _, err := client.NewClient(&client.Config{
c, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
})
@ -181,100 +181,3 @@ func TestClientServerUDPEcho(t *testing.T) {
assert.Equal(t, sData, rData)
assert.Equal(t, echoAddr, rAddr)
}
// TestClientServerHandshakeInfo tests that the client returns the correct handshake info.
func TestClientServerHandshakeInfo(t *testing.T) {
// Create server 1, UDP enabled, unlimited bandwidth
udpConn, udpAddr, err := serverConn()
assert.NoError(t, err)
auth := mocks.NewMockAuthenticator(t)
auth.EXPECT().Authenticate(mock.Anything, mock.Anything, mock.Anything).Return(true, "nobody")
s, err := server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
Authenticator: auth,
})
assert.NoError(t, err)
go s.Serve()
// Create client 1, with specified tx bandwidth
c, info, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
BandwidthConfig: client.BandwidthConfig{
MaxTx: 123456,
},
})
assert.NoError(t, err)
assert.Equal(t, &client.HandshakeInfo{
UDPEnabled: true,
Tx: 123456,
}, info)
// Close server 1 and client 1
_ = s.Close()
_ = c.Close()
// Create server 2, UDP disabled, limited rx bandwidth
udpConn, udpAddr, err = serverConn()
assert.NoError(t, err)
s, err = server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
BandwidthConfig: server.BandwidthConfig{
MaxRx: 100000,
},
DisableUDP: true,
Authenticator: auth,
})
assert.NoError(t, err)
go s.Serve()
// Create client 2, with specified tx bandwidth
c, info, err = client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
BandwidthConfig: client.BandwidthConfig{
MaxTx: 123456,
},
})
assert.NoError(t, err)
assert.Equal(t, &client.HandshakeInfo{
UDPEnabled: false,
Tx: 100000,
}, info)
// Close server 2 and client 2
_ = s.Close()
_ = c.Close()
// Create server 3, UDP enabled, ignore client bandwidth
udpConn, udpAddr, err = serverConn()
assert.NoError(t, err)
s, err = server.NewServer(&server.Config{
TLSConfig: serverTLSConfig(),
Conn: udpConn,
IgnoreClientBandwidth: true,
Authenticator: auth,
})
assert.NoError(t, err)
go s.Serve()
// Create client 3, with specified tx bandwidth
c, info, err = client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
BandwidthConfig: client.BandwidthConfig{
MaxTx: 123456,
},
})
assert.NoError(t, err)
assert.Equal(t, &client.HandshakeInfo{
UDPEnabled: true,
Tx: 0,
}, info)
// Close server 3 and client 3
_ = s.Close()
_ = c.Close()
}

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