diff --git a/.github/workflows/autotag.yaml b/.github/workflows/autotag.yaml
new file mode 100644
index 0000000..a066a74
--- /dev/null
+++ b/.github/workflows/autotag.yaml
@@ -0,0 +1,104 @@
+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}`);
+ }
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 642829d..e5689e7 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -19,7 +19,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
- go-version: "1.21"
+ go-version: "1.24"
- name: Setup Python # This is for the build script
uses: actions/setup-python@v5
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 55e2aba..a966d86 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,7 +23,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
- go-version: "1.21"
+ go-version: "1.24"
- name: Setup Python # This is for the build script
uses: actions/setup-python@v5
diff --git a/README.md b/README.md
index f8521af..94fa335 100644
--- a/README.md
+++ b/README.md
@@ -48,8 +48,8 @@
-
🤗 Cards on the table
-
We have well-documented specifications and code for developers to contribute and build their own apps. And a helpful community, too.
+
🤗 Chill and supportive
+
We have well-documented specifications and code for developers to contribute and/or build their own apps. And a helpful community, too.
diff --git a/app/LICENSE.md b/app/LICENSE.md
new file mode 100644
index 0000000..208e8f2
--- /dev/null
+++ b/app/LICENSE.md
@@ -0,0 +1,7 @@
+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.
diff --git a/app/cmd/client.go b/app/cmd/client.go
index b6745ab..05fec80 100644
--- a/app/cmd/client.go
+++ b/app/cmd/client.go
@@ -9,30 +9,32 @@ import (
"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/internal/forwarding"
- "github.com/apernet/hysteria/app/internal/http"
- "github.com/apernet/hysteria/app/internal/proxymux"
- "github.com/apernet/hysteria/app/internal/redirect"
- "github.com/apernet/hysteria/app/internal/sockopts"
- "github.com/apernet/hysteria/app/internal/socks5"
- "github.com/apernet/hysteria/app/internal/tproxy"
- "github.com/apernet/hysteria/app/internal/tun"
- "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/correctnet"
- "github.com/apernet/hysteria/extras/obfs"
- "github.com/apernet/hysteria/extras/transport/udphop"
+ "github.com/apernet/hysteria/app/v2/internal/forwarding"
+ "github.com/apernet/hysteria/app/v2/internal/http"
+ "github.com/apernet/hysteria/app/v2/internal/proxymux"
+ "github.com/apernet/hysteria/app/v2/internal/redirect"
+ "github.com/apernet/hysteria/app/v2/internal/sockopts"
+ "github.com/apernet/hysteria/app/v2/internal/socks5"
+ "github.com/apernet/hysteria/app/v2/internal/tproxy"
+ "github.com/apernet/hysteria/app/v2/internal/tun"
+ "github.com/apernet/hysteria/app/v2/internal/url"
+ "github.com/apernet/hysteria/app/v2/internal/utils"
+ "github.com/apernet/hysteria/core/v2/client"
+ "github.com/apernet/hysteria/extras/v2/correctnet"
+ "github.com/apernet/hysteria/extras/v2/obfs"
+ "github.com/apernet/hysteria/extras/v2/transport/udphop"
)
// Client flags
@@ -391,7 +393,11 @@ func (c *clientConfig) parseURI() bool {
return false
}
if u.User != nil {
- c.Auth = u.User.String()
+ auth, err := url.QueryUnescape(u.User.String())
+ if err != nil {
+ return false
+ }
+ c.Auth = auth
}
c.Server = u.Host
q := u.Query()
@@ -464,8 +470,10 @@ func runClient(cmd *cobra.Command, args []string) {
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)
}
@@ -512,13 +520,42 @@ func runClient(cmd *cobra.Command, args []string) {
})
}
- runner.Run()
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
+ defer signal.Stop(signalChan)
+
+ runnerChan := make(chan clientModeRunnerResult, 1)
+ go func() {
+ runnerChan <- runner.Run()
+ }()
+
+ select {
+ case <-signalChan:
+ logger.Info("received signal, shutting down gracefully")
+ case r := <-runnerChan:
+ if r.OK {
+ logger.Info(r.Msg)
+ } else {
+ _ = c.Close() // Close the client here as Fatal will exit the program without running defer
+ if r.Err != nil {
+ logger.Fatal(r.Msg, zap.Error(r.Err))
+ } else {
+ logger.Fatal(r.Msg)
+ }
+ }
+ }
}
type clientModeRunner struct {
ModeMap map[string]func() error
}
+type clientModeRunnerResult struct {
+ OK bool
+ Msg string
+ Err error
+}
+
func (r *clientModeRunner) Add(name string, f func() error) {
if r.ModeMap == nil {
r.ModeMap = make(map[string]func() error)
@@ -526,9 +563,9 @@ func (r *clientModeRunner) Add(name string, f func() error) {
r.ModeMap[name] = f
}
-func (r *clientModeRunner) Run() {
+func (r *clientModeRunner) Run() clientModeRunnerResult {
if len(r.ModeMap) == 0 {
- logger.Fatal("no mode specified")
+ return clientModeRunnerResult{OK: false, Msg: "no mode specified"}
}
type modeError struct {
@@ -546,9 +583,13 @@ func (r *clientModeRunner) Run() {
for i := 0; i < len(r.ModeMap); i++ {
e := <-errChan
if e.Err != nil {
- logger.Fatal("failed to run "+e.Name, zap.Error(e.Err))
+ return clientModeRunnerResult{OK: false, Msg: "failed to run " + e.Name, Err: e.Err}
}
}
+
+ // We don't really have any such cases, as currently none of our modes would stop on themselves without error.
+ // But we leave the possibility here for future expansion.
+ return clientModeRunnerResult{OK: true, Msg: "finished without error"}
}
func clientSOCKS5(config socks5Config, c client.Client) error {
diff --git a/app/cmd/ping.go b/app/cmd/ping.go
index 856595b..db45052 100644
--- a/app/cmd/ping.go
+++ b/app/cmd/ping.go
@@ -7,7 +7,7 @@ import (
"github.com/spf13/viper"
"go.uber.org/zap"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
// pingCmd represents the ping command
diff --git a/app/cmd/root.go b/app/cmd/root.go
index cc7f39f..13f9705 100644
--- a/app/cmd/root.go
+++ b/app/cmd/root.go
@@ -29,20 +29,24 @@ const (
var (
// These values will be injected by the build system
- appVersion = "Unknown"
- appDate = "Unknown"
- appType = "Unknown" // aka channel
- appCommit = "Unknown"
- appPlatform = "Unknown"
- appArch = "Unknown"
+ appVersion = "Unknown"
+ appDate = "Unknown"
+ appType = "Unknown" // aka channel
+ appToolchain = "Unknown"
+ appCommit = "Unknown"
+ appPlatform = "Unknown"
+ appArch = "Unknown"
+ libVersion = "Unknown"
appVersionLong = fmt.Sprintf("Version:\t%s\n"+
"BuildDate:\t%s\n"+
"BuildType:\t%s\n"+
+ "Toolchain:\t%s\n"+
"CommitHash:\t%s\n"+
"Platform:\t%s\n"+
- "Architecture:\t%s",
- appVersion, appDate, appType, appCommit, appPlatform, appArch)
+ "Architecture:\t%s\n"+
+ "Libraries:\tquic-go=%s",
+ appVersion, appDate, appType, appToolchain, appCommit, appPlatform, appArch, libVersion)
appAboutLong = fmt.Sprintf("%s\n%s\n%s\n\n%s", appLogo, appDesc, appAuthors, appVersionLong)
)
diff --git a/app/cmd/server.go b/app/cmd/server.go
index 9b6665e..a2aa9a4 100644
--- a/app/cmd/server.go
+++ b/app/cmd/server.go
@@ -10,24 +10,33 @@ import (
"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/internal/utils"
- "github.com/apernet/hysteria/core/server"
- "github.com/apernet/hysteria/extras/auth"
- "github.com/apernet/hysteria/extras/correctnet"
- "github.com/apernet/hysteria/extras/masq"
- "github.com/apernet/hysteria/extras/obfs"
- "github.com/apernet/hysteria/extras/outbounds"
- "github.com/apernet/hysteria/extras/trafficlogger"
+ "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 (
@@ -57,6 +66,7 @@ type serverConfig struct {
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"`
@@ -73,20 +83,44 @@ type serverConfigObfs struct {
}
type serverConfigTLS struct {
- Cert string `mapstructure:"cert"`
- Key string `mapstructure:"key"`
+ Cert string `mapstructure:"cert"`
+ Key string `mapstructure:"key"`
+ SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict"
}
type serverConfigACME struct {
- Domains []string `mapstructure:"domains"`
- Email string `mapstructure:"email"`
- CA string `mapstructure:"ca"`
- DisableHTTP bool `mapstructure:"disableHTTP"`
- DisableTLSALPN bool `mapstructure:"disableTLSALPN"`
- ListenHost string `mapstructure:"listenHost"`
- AltHTTPPort int `mapstructure:"altHTTPPort"`
- AltTLSALPNPort int `mapstructure:"altTLSALPNPort"`
- Dir string `mapstructure:"dir"`
+ // Common fields
+ Domains []string `mapstructure:"domains"`
+ Email string `mapstructure:"email"`
+ CA string `mapstructure:"ca"`
+ ListenHost string `mapstructure:"listenHost"`
+ Dir string `mapstructure:"dir"`
+
+ // Type selection
+ Type string `mapstructure:"type"`
+ HTTP serverConfigACMEHTTP `mapstructure:"http"`
+ TLS serverConfigACMETLS `mapstructure:"tls"`
+ DNS serverConfigACMEDNS `mapstructure:"dns"`
+
+ // Legacy fields for backwards compatibility
+ // Only applicable when Type is empty
+ DisableHTTP bool `mapstructure:"disableHTTP"`
+ DisableTLSALPN bool `mapstructure:"disableTLSALPN"`
+ AltHTTPPort int `mapstructure:"altHTTPPort"`
+ AltTLSALPNPort int `mapstructure:"altTLSALPNPort"`
+}
+
+type serverConfigACMEHTTP struct {
+ AltPort int `mapstructure:"altPort"`
+}
+
+type serverConfigACMETLS struct {
+ AltPort int `mapstructure:"altPort"`
+}
+
+type serverConfigACMEDNS struct {
+ Name string `mapstructure:"name"`
+ Config map[string]string `mapstructure:"config"`
}
type serverConfigQUIC struct {
@@ -149,6 +183,14 @@ 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"`
@@ -162,6 +204,7 @@ type serverConfigOutboundDirect struct {
BindIPv4 string `mapstructure:"bindIPv4"`
BindIPv6 string `mapstructure:"bindIPv6"`
BindDevice string `mapstructure:"bindDevice"`
+ FastOpen bool `mapstructure:"fastOpen"`
}
type serverConfigOutboundSOCKS5 struct {
@@ -195,6 +238,7 @@ type serverConfigMasqueradeFile struct {
type serverConfigMasqueradeProxy struct {
URL string `mapstructure:"url"`
RewriteHost bool `mapstructure:"rewriteHost"`
+ Insecure bool `mapstructure:"insecure"`
}
type serverConfigMasqueradeString struct {
@@ -250,16 +294,45 @@ 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()
+ 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 = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
- cert, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key)
- return &cert, err
- }
+ hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate
} else {
// ACME
dataDir := c.ACME.Dir
@@ -277,14 +350,10 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
Logger: logger,
}
cmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{
- Email: c.ACME.Email,
- Agreed: true,
- DisableHTTPChallenge: c.ACME.DisableHTTP,
- DisableTLSALPNChallenge: c.ACME.DisableTLSALPN,
- ListenHost: c.ACME.ListenHost,
- AltHTTPPort: c.ACME.AltHTTPPort,
- AltTLSALPNPort: c.ACME.AltTLSALPNPort,
- Logger: logger,
+ Email: c.ACME.Email,
+ Agreed: true,
+ ListenHost: c.ACME.ListenHost,
+ Logger: logger,
})
switch strings.ToLower(c.ACME.CA) {
case "letsencrypt", "le", "":
@@ -298,8 +367,82 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
}
cmIssuer.ExternalAccount = eab
default:
- return configError{Field: "acme.ca", Err: errors.New("unknown CA")}
+ return configError{Field: "acme.ca", Err: errors.New("unsupported CA")}
}
+
+ switch strings.ToLower(c.ACME.Type) {
+ case "http":
+ cmIssuer.DisableHTTPChallenge = false
+ cmIssuer.DisableTLSALPNChallenge = true
+ cmIssuer.DNS01Solver = nil
+ cmIssuer.AltHTTPPort = c.ACME.HTTP.AltPort
+ case "tls":
+ cmIssuer.DisableHTTPChallenge = true
+ cmIssuer.DisableTLSALPNChallenge = false
+ cmIssuer.DNS01Solver = nil
+ cmIssuer.AltTLSALPNPort = c.ACME.TLS.AltPort
+ case "dns":
+ cmIssuer.DisableHTTPChallenge = true
+ cmIssuer.DisableTLSALPNChallenge = true
+ if c.ACME.DNS.Name == "" {
+ return configError{Field: "acme.dns.name", Err: errors.New("empty DNS provider name")}
+ }
+ if c.ACME.DNS.Config == nil {
+ return configError{Field: "acme.dns.config", Err: errors.New("empty DNS provider config")}
+ }
+ switch strings.ToLower(c.ACME.DNS.Name) {
+ case "cloudflare":
+ cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
+ DNSProvider: &cloudflare.Provider{
+ APIToken: c.ACME.DNS.Config["cloudflare_api_token"],
+ },
+ }
+ case "duckdns":
+ cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
+ DNSProvider: &duckdns.Provider{
+ APIToken: c.ACME.DNS.Config["duckdns_api_token"],
+ OverrideDomain: c.ACME.DNS.Config["duckdns_override_domain"],
+ },
+ }
+ case "gandi":
+ cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
+ DNSProvider: &gandi.Provider{
+ BearerToken: c.ACME.DNS.Config["gandi_api_token"],
+ },
+ }
+ case "godaddy":
+ cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
+ DNSProvider: &godaddy.Provider{
+ APIToken: c.ACME.DNS.Config["godaddy_api_token"],
+ },
+ }
+ case "namedotcom":
+ cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
+ DNSProvider: &namedotcom.Provider{
+ Token: c.ACME.DNS.Config["namedotcom_token"],
+ User: c.ACME.DNS.Config["namedotcom_user"],
+ Server: c.ACME.DNS.Config["namedotcom_server"],
+ },
+ }
+ case "vultr":
+ cmIssuer.DNS01Solver = &certmagic.DNS01Solver{
+ DNSProvider: &vultr.Provider{
+ APIToken: c.ACME.DNS.Config["vultr_api_token"],
+ },
+ }
+ default:
+ return configError{Field: "acme.dns.name", Err: errors.New("unsupported DNS provider")}
+ }
+ case "":
+ // Legacy compatibility mode
+ cmIssuer.DisableHTTPChallenge = c.ACME.DisableHTTP
+ cmIssuer.DisableTLSALPNChallenge = c.ACME.DisableTLSALPN
+ cmIssuer.AltHTTPPort = c.ACME.AltHTTPPort
+ cmIssuer.AltTLSALPNPort = c.ACME.AltTLSALPNPort
+ default:
+ return configError{Field: "acme.type", Err: errors.New("unsupported ACME type")}
+ }
+
cmCfg.Issuers = []certmagic.Issuer{cmIssuer}
cmCache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
@@ -377,18 +520,18 @@ func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error {
}
func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) {
- var mode outbounds.DirectOutboundMode
+ opts := outbounds.DirectOutboundOptions{}
switch strings.ToLower(c.Mode) {
case "", "auto":
- mode = outbounds.DirectOutboundModeAuto
+ opts.Mode = outbounds.DirectOutboundModeAuto
case "64":
- mode = outbounds.DirectOutboundMode64
+ opts.Mode = outbounds.DirectOutboundMode64
case "46":
- mode = outbounds.DirectOutboundMode46
+ opts.Mode = outbounds.DirectOutboundMode46
case "6":
- mode = outbounds.DirectOutboundMode6
+ opts.Mode = outbounds.DirectOutboundMode6
case "4":
- mode = outbounds.DirectOutboundMode4
+ opts.Mode = outbounds.DirectOutboundMode4
default:
return nil, configError{Field: "outbounds.direct.mode", Err: errors.New("unsupported mode")}
}
@@ -405,12 +548,14 @@ 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")}
}
- return outbounds.NewDirectOutboundBindToIPs(mode, ip4, ip6)
+ opts.BindIP4 = ip4
+ opts.BindIP6 = ip6
}
if bindDevice {
- return outbounds.NewDirectOutboundBindToDevice(mode, c.BindDevice)
+ opts.DeviceName = c.BindDevice
}
- return outbounds.NewDirectOutboundSimple(mode), nil
+ opts.FastOpen = c.FastOpen
+ return outbounds.NewDirectOutboundWithOptions(opts)
}
func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) {
@@ -427,6 +572,29 @@ func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.P
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:
@@ -587,7 +755,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.UserPassAuthenticator{Users: c.Auth.UserPass}
+ hyConfig.Authenticator = auth.NewUserPassAuthenticator(c.Auth.UserPass)
return nil
case "http", "https":
if c.Auth.HTTP.URL == "" {
@@ -640,6 +808,28 @@ 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)
@@ -649,6 +839,7 @@ 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)
@@ -707,6 +898,7 @@ func (c *serverConfig) Config() (*server.Config, error) {
c.fillConn,
c.fillTLSConfig,
c.fillQUICConfig,
+ c.fillRequestHook,
c.fillOutboundConfig,
c.fillBandwidthConfig,
c.fillIgnoreClientBandwidth,
diff --git a/app/cmd/server_test.go b/app/cmd/server_test.go
index 935a998..5849a38 100644
--- a/app/cmd/server_test.go
+++ b/app/cmd/server_test.go
@@ -26,21 +26,37 @@ func TestServerConfig(t *testing.T) {
},
},
TLS: &serverConfigTLS{
- Cert: "some.crt",
- Key: "some.key",
+ Cert: "some.crt",
+ Key: "some.key",
+ SNIGuard: "strict",
},
ACME: &serverConfigACME{
Domains: []string{
"sub1.example.com",
"sub2.example.com",
},
- Email: "haha@cringe.net",
- CA: "zero",
+ Email: "haha@cringe.net",
+ CA: "zero",
+ ListenHost: "127.0.0.9",
+ Dir: "random_dir",
+ Type: "dns",
+ HTTP: serverConfigACMEHTTP{
+ AltPort: 8888,
+ },
+ TLS: serverConfigACMETLS{
+ AltPort: 44333,
+ },
+ DNS: serverConfigACMEDNS{
+ Name: "gomommy",
+ Config: map[string]string{
+ "key1": "value1",
+ "key2": "value2",
+ },
+ },
DisableHTTP: true,
DisableTLSALPN: true,
- AltHTTPPort: 9980,
- AltTLSALPNPort: 9443,
- Dir: "random_dir",
+ AltHTTPPort: 8080,
+ AltTLSALPNPort: 4433,
},
QUIC: serverConfigQUIC{
InitStreamReceiveWindow: 77881,
@@ -96,6 +112,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{
@@ -115,6 +138,7 @@ func TestServerConfig(t *testing.T) {
BindIPv4: "2.4.6.8",
BindIPv6: "0:0:0:0:0:ffff:0204:0608",
BindDevice: "eth233",
+ FastOpen: true,
},
},
{
@@ -147,6 +171,7 @@ func TestServerConfig(t *testing.T) {
Proxy: serverConfigMasqueradeProxy{
URL: "https://some.site.net",
RewriteHost: true,
+ Insecure: true,
},
String: serverConfigMasqueradeString{
Content: "aint nothin here",
diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml
index 1ab5d5f..b989b97 100644
--- a/app/cmd/server_test.yaml
+++ b/app/cmd/server_test.yaml
@@ -8,6 +8,7 @@ obfs:
tls:
cert: some.crt
key: some.key
+ sniGuard: strict
acme:
domains:
@@ -15,11 +16,22 @@ 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: 9980
- altTLSALPNPort: 9443
- dir: random_dir
+ altHTTPPort: 8080
+ altTLSALPNPort: 4433
quic:
initStreamReceiveWindow: 77881
@@ -72,6 +84,13 @@ 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:
@@ -89,6 +108,7 @@ outbounds:
bindIPv4: 2.4.6.8
bindIPv6: 0:0:0:0:0:ffff:0204:0608
bindDevice: eth233
+ fastOpen: true
- name: badstuff
type: socks5
socks5:
@@ -112,6 +132,7 @@ masquerade:
proxy:
url: https://some.site.net
rewriteHost: true
+ insecure: true
string:
content: aint nothin here
headers:
diff --git a/app/cmd/share.go b/app/cmd/share.go
new file mode 100644
index 0000000..ad96e80
--- /dev/null
+++ b/app/cmd/share.go
@@ -0,0 +1,55 @@
+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)
+ }
+}
diff --git a/app/cmd/speedtest.go b/app/cmd/speedtest.go
index 86bdbdb..f5c37fb 100644
--- a/app/cmd/speedtest.go
+++ b/app/cmd/speedtest.go
@@ -3,16 +3,19 @@ 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/client"
- hyErrors "github.com/apernet/hysteria/core/errors"
- "github.com/apernet/hysteria/extras/outbounds"
- "github.com/apernet/hysteria/extras/outbounds/speedtest"
+ "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 (
@@ -68,11 +71,26 @@ func runSpeedtest(cmd *cobra.Command, args []string) {
zap.Bool("udpEnabled", info.UDPEnabled),
zap.Uint64("tx", info.Tx))
- if !skipDownload {
- runDownloadTest(c)
- }
- if !skipUpload {
- runUploadTest(c)
+ 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")
}
}
@@ -152,8 +170,8 @@ func formatSpeed(bytes uint32, duration time.Duration, useBytes bool) string {
speed *= 8
}
unitIndex := 0
- for speed > 1024 && unitIndex < len(units)-1 {
- speed /= 1024
+ for speed > 1000 && unitIndex < len(units)-1 {
+ speed /= 1000
unitIndex++
}
return fmt.Sprintf("%.2f %s", speed, units[unitIndex])
diff --git a/app/cmd/update.go b/app/cmd/update.go
index 3b26740..11eadd0 100644
--- a/app/cmd/update.go
+++ b/app/cmd/update.go
@@ -6,8 +6,8 @@ import (
"github.com/spf13/cobra"
"go.uber.org/zap"
- "github.com/apernet/hysteria/app/internal/utils"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/app/v2/internal/utils"
+ "github.com/apernet/hysteria/core/v2/client"
)
const (
diff --git a/app/go.mod b/app/go.mod
index 97c5bbb..25d1fd3 100644
--- a/app/go.mod
+++ b/app/go.mod
@@ -1,71 +1,92 @@
-module github.com/apernet/hysteria/app
+module github.com/apernet/hysteria/app/v2
-go 1.21
+go 1.23
+
+toolchain go1.24.2
require (
github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f
- github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000
- github.com/apernet/hysteria/extras v0.0.0-00010101000000-000000000000
+ 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/caddyserver/certmagic v0.17.2
+ github.com/libdns/cloudflare v0.1.1
+ github.com/libdns/duckdns v0.2.0
+ github.com/libdns/gandi v1.0.3
+ github.com/libdns/godaddy v1.0.3
+ github.com/libdns/namedotcom v0.3.3
+ github.com/libdns/vultr v1.0.0
github.com/mdp/qrterminal/v3 v3.1.1
github.com/mholt/acmez v1.0.4
github.com/sagernet/sing v0.3.2
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
- github.com/stretchr/testify v1.8.4
+ github.com/stretchr/testify v1.9.0
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
go.uber.org/zap v1.24.0
- golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
- golang.org/x/sys v0.17.0
+ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
+ golang.org/x/sys v0.25.0
)
require (
- github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822 // indirect
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431 // 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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+ github.com/google/go-querystring v1.1.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.1 // indirect
+ github.com/libdns/libdns v0.2.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
- github.com/miekg/dns v1.1.55 // indirect
+ github.com/miekg/dns v1.1.59 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // 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.4.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/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.0 // indirect
+ github.com/stretchr/objx v0.5.2 // 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
go.uber.org/atomic v1.11.0 // indirect
- go.uber.org/mock v0.4.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.19.0 // indirect
- golang.org/x/mod v0.12.0 // indirect
- golang.org/x/net v0.21.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- golang.org/x/tools v0.11.1 // indirect
- google.golang.org/protobuf v1.33.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/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
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 => ../core
+replace github.com/apernet/hysteria/core/v2 => ../core
-replace github.com/apernet/hysteria/extras => ../extras
+replace github.com/apernet/hysteria/extras/v2 => ../extras
diff --git a/app/go.sum b/app/go.sum
index 4f2b0fe..9bc7ae7 100644
--- a/app/go.sum
+++ b/app/go.sum
@@ -38,10 +38,12 @@ 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.42.1-0.20240323215309-32a339817822 h1:+ZSzRxSMg1+fLTQKcIUvD2cCCgS+1rtyRhs+NL5oBgA=
-github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822/go.mod h1:j3QaAM7sVJqptDQyIQRWA6mASCfuxoHJszn67JQh1GE=
+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/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
@@ -55,10 +57,16 @@ 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=
@@ -68,6 +76,8 @@ 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=
@@ -106,8 +116,8 @@ 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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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=
@@ -119,8 +129,10 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/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=
@@ -141,6 +153,12 @@ 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=
@@ -154,31 +172,49 @@ 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.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
-github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
+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/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.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
-github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
+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/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/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=
@@ -194,11 +230,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.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
@@ -220,8 +258,9 @@ 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=
@@ -231,8 +270,8 @@ 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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+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/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=
@@ -241,6 +280,8 @@ github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc
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=
@@ -259,8 +300,8 @@ 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.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
+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=
@@ -277,8 +318,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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+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-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=
@@ -289,8 +330,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-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/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/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=
@@ -317,8 +358,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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.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/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=
@@ -331,6 +372,7 @@ 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=
@@ -354,8 +396,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.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+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/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=
@@ -365,6 +407,8 @@ 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=
@@ -378,8 +422,8 @@ 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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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=
@@ -389,6 +433,7 @@ 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=
@@ -422,8 +467,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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/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=
@@ -435,13 +480,11 @@ 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+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.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -462,6 +505,7 @@ 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=
@@ -492,8 +536,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
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.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc=
-golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
+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/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=
@@ -586,12 +630,12 @@ 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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/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=
diff --git a/app/internal/forwarding/tcp.go b/app/internal/forwarding/tcp.go
index 7d22d33..8936385 100644
--- a/app/internal/forwarding/tcp.go
+++ b/app/internal/forwarding/tcp.go
@@ -4,7 +4,7 @@ import (
"io"
"net"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
type TCPTunnel struct {
diff --git a/app/internal/forwarding/tcp_test.go b/app/internal/forwarding/tcp_test.go
index 91ab0a1..075b233 100644
--- a/app/internal/forwarding/tcp_test.go
+++ b/app/internal/forwarding/tcp_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/apernet/hysteria/app/internal/utils_test"
+ "github.com/apernet/hysteria/app/v2/internal/utils_test"
)
func TestTCPTunnel(t *testing.T) {
diff --git a/app/internal/forwarding/udp.go b/app/internal/forwarding/udp.go
index 2f859e9..35886a8 100644
--- a/app/internal/forwarding/udp.go
+++ b/app/internal/forwarding/udp.go
@@ -6,7 +6,7 @@ import (
"sync/atomic"
"time"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
const (
diff --git a/app/internal/forwarding/udp_test.go b/app/internal/forwarding/udp_test.go
index feb0c20..ba4f3ba 100644
--- a/app/internal/forwarding/udp_test.go
+++ b/app/internal/forwarding/udp_test.go
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/apernet/hysteria/app/internal/utils_test"
+ "github.com/apernet/hysteria/app/v2/internal/utils_test"
)
func TestUDPTunnel(t *testing.T) {
diff --git a/app/internal/http/server.go b/app/internal/http/server.go
index 4f11d14..0b5e411 100644
--- a/app/internal/http/server.go
+++ b/app/internal/http/server.go
@@ -12,7 +12,7 @@ import (
"strings"
"time"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
const (
@@ -278,6 +278,11 @@ 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)
}
diff --git a/app/internal/http/server_test.go b/app/internal/http/server_test.go
index 960bef7..43f6e16 100644
--- a/app/internal/http/server_test.go
+++ b/app/internal/http/server_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
const (
diff --git a/app/internal/proxymux/internal/mocks/mock_Conn.go b/app/internal/proxymux/internal/mocks/mock_Conn.go
index 0bcbe65..13e363e 100644
--- a/app/internal/proxymux/internal/mocks/mock_Conn.go
+++ b/app/internal/proxymux/internal/mocks/mock_Conn.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.2. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
diff --git a/app/internal/proxymux/internal/mocks/mock_Listener.go b/app/internal/proxymux/internal/mocks/mock_Listener.go
index e4ca2f4..842b88f 100644
--- a/app/internal/proxymux/internal/mocks/mock_Listener.go
+++ b/app/internal/proxymux/internal/mocks/mock_Listener.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.2. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
diff --git a/app/internal/proxymux/manager.go b/app/internal/proxymux/manager.go
index 92df918..23f4ad6 100644
--- a/app/internal/proxymux/manager.go
+++ b/app/internal/proxymux/manager.go
@@ -4,7 +4,7 @@ import (
"net"
"sync"
- "github.com/apernet/hysteria/extras/correctnet"
+ "github.com/apernet/hysteria/extras/v2/correctnet"
)
type muxManager struct {
diff --git a/app/internal/proxymux/mux_test.go b/app/internal/proxymux/mux_test.go
index 46cbf95..7b57237 100644
--- a/app/internal/proxymux/mux_test.go
+++ b/app/internal/proxymux/mux_test.go
@@ -7,7 +7,7 @@ import (
"testing"
"time"
- "github.com/apernet/hysteria/app/internal/proxymux/internal/mocks"
+ "github.com/apernet/hysteria/app/v2/internal/proxymux/internal/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
diff --git a/app/internal/redirect/tcp_linux.go b/app/internal/redirect/tcp_linux.go
index 8445fa3..5351543 100644
--- a/app/internal/redirect/tcp_linux.go
+++ b/app/internal/redirect/tcp_linux.go
@@ -8,7 +8,7 @@ import (
"syscall"
"unsafe"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
const (
diff --git a/app/internal/redirect/tcp_others.go b/app/internal/redirect/tcp_others.go
index ddb770f..f5cf6f9 100644
--- a/app/internal/redirect/tcp_others.go
+++ b/app/internal/redirect/tcp_others.go
@@ -6,7 +6,7 @@ import (
"errors"
"net"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
type TCPRedirect struct {
diff --git a/app/internal/socks5/server.go b/app/internal/socks5/server.go
index 84b58ed..0fc7de6 100644
--- a/app/internal/socks5/server.go
+++ b/app/internal/socks5/server.go
@@ -7,7 +7,7 @@ import (
"github.com/txthinking/socks5"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
const udpBufferSize = 4096
diff --git a/app/internal/socks5/server_test.go b/app/internal/socks5/server_test.go
index 7aec82a..1290058 100644
--- a/app/internal/socks5/server_test.go
+++ b/app/internal/socks5/server_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
- "github.com/apernet/hysteria/app/internal/utils_test"
+ "github.com/apernet/hysteria/app/v2/internal/utils_test"
)
func TestServer(t *testing.T) {
diff --git a/app/internal/tproxy/tcp_linux.go b/app/internal/tproxy/tcp_linux.go
index 4c922f1..4d8c1e9 100644
--- a/app/internal/tproxy/tcp_linux.go
+++ b/app/internal/tproxy/tcp_linux.go
@@ -5,7 +5,7 @@ import (
"net"
"github.com/apernet/go-tproxy"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
type TCPTProxy struct {
diff --git a/app/internal/tproxy/tcp_others.go b/app/internal/tproxy/tcp_others.go
index 27e98c5..f33de6a 100644
--- a/app/internal/tproxy/tcp_others.go
+++ b/app/internal/tproxy/tcp_others.go
@@ -6,7 +6,7 @@ import (
"errors"
"net"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
type TCPTProxy struct {
diff --git a/app/internal/tproxy/udp_linux.go b/app/internal/tproxy/udp_linux.go
index 03e2f42..827bea2 100644
--- a/app/internal/tproxy/udp_linux.go
+++ b/app/internal/tproxy/udp_linux.go
@@ -6,7 +6,7 @@ import (
"time"
"github.com/apernet/go-tproxy"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
const (
diff --git a/app/internal/tproxy/udp_others.go b/app/internal/tproxy/udp_others.go
index db9cedb..3d267e6 100644
--- a/app/internal/tproxy/udp_others.go
+++ b/app/internal/tproxy/udp_others.go
@@ -7,7 +7,7 @@ import (
"net"
"time"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
type UDPTProxy struct {
diff --git a/app/internal/tun/check_ipv6_others.go b/app/internal/tun/check_ipv6_others.go
new file mode 100644
index 0000000..7cfd9ab
--- /dev/null
+++ b/app/internal/tun/check_ipv6_others.go
@@ -0,0 +1,14 @@
+//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
+}
diff --git a/app/internal/tun/check_ipv6_unix.go b/app/internal/tun/check_ipv6_unix.go
new file mode 100644
index 0000000..8fdffaf
--- /dev/null
+++ b/app/internal/tun/check_ipv6_unix.go
@@ -0,0 +1,16 @@
+//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
+}
diff --git a/app/internal/tun/check_ipv6_windows.go b/app/internal/tun/check_ipv6_windows.go
new file mode 100644
index 0000000..d488d7e
--- /dev/null
+++ b/app/internal/tun/check_ipv6_windows.go
@@ -0,0 +1,24 @@
+//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
+}
diff --git a/app/internal/tun/server.go b/app/internal/tun/server.go
index 4ae5393..a999051 100644
--- a/app/internal/tun/server.go
+++ b/app/internal/tun/server.go
@@ -14,7 +14,7 @@ import (
"github.com/sagernet/sing/common/network"
"go.uber.org/zap"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
type Server struct {
@@ -49,6 +49,10 @@ type EventLogger interface {
}
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,
diff --git a/app/internal/utils/certloader.go b/app/internal/utils/certloader.go
new file mode 100644
index 0000000..fb41a3c
--- /dev/null
+++ b/app/internal/utils/certloader.go
@@ -0,0 +1,198 @@
+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
+}
diff --git a/app/internal/utils/certloader_test.go b/app/internal/utils/certloader_test.go
new file mode 100644
index 0000000..3a8e26b
--- /dev/null
+++ b/app/internal/utils/certloader_test.go
@@ -0,0 +1,139 @@
+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
+}
diff --git a/app/internal/utils/certloader_test_gencert.py b/app/internal/utils/certloader_test_gencert.py
new file mode 100644
index 0000000..d4d5695
--- /dev/null
+++ b/app/internal/utils/certloader_test_gencert.py
@@ -0,0 +1,134 @@
+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()
diff --git a/app/internal/utils/certloader_test_tlsclient.py b/app/internal/utils/certloader_test_tlsclient.py
new file mode 100644
index 0000000..3b7efd6
--- /dev/null
+++ b/app/internal/utils/certloader_test_tlsclient.py
@@ -0,0 +1,60 @@
+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()
diff --git a/app/internal/utils/geoloader.go b/app/internal/utils/geoloader.go
index fa9775b..468c68a 100644
--- a/app/internal/utils/geoloader.go
+++ b/app/internal/utils/geoloader.go
@@ -7,8 +7,8 @@ import (
"os"
"time"
- "github.com/apernet/hysteria/extras/outbounds/acl"
- "github.com/apernet/hysteria/extras/outbounds/acl/v2geo"
+ "github.com/apernet/hysteria/extras/v2/outbounds/acl"
+ "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
)
const (
diff --git a/app/internal/utils/testcerts/.gitignore b/app/internal/utils/testcerts/.gitignore
new file mode 100644
index 0000000..082821a
--- /dev/null
+++ b/app/internal/utils/testcerts/.gitignore
@@ -0,0 +1,3 @@
+# This directory is used for certificate generation in certloader_test.go
+/*
+!/.gitignore
diff --git a/app/internal/utils/update.go b/app/internal/utils/update.go
index 8887cb4..60da91f 100644
--- a/app/internal/utils/update.go
+++ b/app/internal/utils/update.go
@@ -8,7 +8,7 @@ import (
"net/http"
"time"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
const (
diff --git a/app/internal/utils_test/mock.go b/app/internal/utils_test/mock.go
index 04adad6..06057d8 100644
--- a/app/internal/utils_test/mock.go
+++ b/app/internal/utils_test/mock.go
@@ -5,7 +5,7 @@ import (
"net"
"time"
- "github.com/apernet/hysteria/core/client"
+ "github.com/apernet/hysteria/core/v2/client"
)
type MockEchoHyClient struct{}
diff --git a/app/main.go b/app/main.go
index 6d1554b..81d8168 100644
--- a/app/main.go
+++ b/app/main.go
@@ -1,6 +1,6 @@
package main
-import "github.com/apernet/hysteria/app/cmd"
+import "github.com/apernet/hysteria/app/v2/cmd"
func main() {
cmd.Execute()
diff --git a/core/LICENSE.md b/core/LICENSE.md
new file mode 100644
index 0000000..208e8f2
--- /dev/null
+++ b/core/LICENSE.md
@@ -0,0 +1,7 @@
+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.
diff --git a/core/client/.mockery.yaml b/core/client/.mockery.yaml
index 9500c13..299e6f9 100644
--- a/core/client/.mockery.yaml
+++ b/core/client/.mockery.yaml
@@ -2,7 +2,7 @@ with-expecter: true
inpackage: true
dir: .
packages:
- github.com/apernet/hysteria/core/client:
+ github.com/apernet/hysteria/core/v2/client:
interfaces:
udpIO:
config:
diff --git a/core/client/client.go b/core/client/client.go
index 4148f05..3691d1e 100644
--- a/core/client/client.go
+++ b/core/client/client.go
@@ -3,15 +3,16 @@ package client
import (
"context"
"crypto/tls"
+ "errors"
"net"
"net/http"
"net/url"
"time"
- 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"
+ coreErrs "github.com/apernet/hysteria/core/v2/errors"
+ "github.com/apernet/hysteria/core/v2/internal/congestion"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
+ "github.com/apernet/hysteria/core/v2/internal/utils"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
@@ -83,13 +84,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 {
@@ -222,18 +223,21 @@ func (c *clientImpl) Close() error {
return nil
}
+var nonPermanentErrors = []error{
+ quic.StreamLimitReachedError{},
+}
+
// wrapIfConnectionClosed checks if the error returned by quic-go
-// indicates that the QUIC connection has been permanently closed,
-// and if so, wraps the error with coreErrs.ClosedError.
-// PITFALL: sometimes quic-go has "internal errors" that are not net.Error,
-// but we still need to treat them as ClosedError.
+// is recoverable (listed in nonPermanentErrors) or permanent.
+// Recoverable errors are returned as-is,
+// permanent ones are wrapped as ClosedError.
func wrapIfConnectionClosed(err error) error {
- netErr, ok := err.(net.Error)
- if !ok || !netErr.Temporary() {
- return coreErrs.ClosedError{Err: err}
- } else {
- return err
+ for _, e := range nonPermanentErrors {
+ if errors.Is(err, e) {
+ return err
+ }
}
+ return coreErrs.ClosedError{Err: err}
}
type tcpConn struct {
diff --git a/core/client/config.go b/core/client/config.go
index 41a72ba..7270c30 100644
--- a/core/client/config.go
+++ b/core/client/config.go
@@ -5,8 +5,8 @@ import (
"net"
"time"
- "github.com/apernet/hysteria/core/errors"
- "github.com/apernet/hysteria/core/internal/pmtud"
+ "github.com/apernet/hysteria/core/v2/errors"
+ "github.com/apernet/hysteria/core/v2/internal/pmtud"
)
const (
diff --git a/core/client/mock_udpIO.go b/core/client/mock_udpIO.go
index 6e0b1ba..aa1444e 100644
--- a/core/client/mock_udpIO.go
+++ b/core/client/mock_udpIO.go
@@ -1,9 +1,9 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package client
import (
- protocol "github.com/apernet/hysteria/core/internal/protocol"
+ protocol "github.com/apernet/hysteria/core/v2/internal/protocol"
mock "github.com/stretchr/testify/mock"
)
@@ -24,6 +24,10 @@ 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 {
@@ -77,6 +81,10 @@ 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)
diff --git a/core/client/reconnect.go b/core/client/reconnect.go
index 05d60b3..91c7eb4 100644
--- a/core/client/reconnect.go
+++ b/core/client/reconnect.go
@@ -4,7 +4,7 @@ import (
"net"
"sync"
- coreErrs "github.com/apernet/hysteria/core/errors"
+ coreErrs "github.com/apernet/hysteria/core/v2/errors"
)
// reconnectableClientImpl is a wrapper of Client, which can reconnect when the connection is closed,
diff --git a/core/client/udp.go b/core/client/udp.go
index dfe574f..ca98095 100644
--- a/core/client/udp.go
+++ b/core/client/udp.go
@@ -8,9 +8,9 @@ import (
"github.com/apernet/quic-go"
- coreErrs "github.com/apernet/hysteria/core/errors"
- "github.com/apernet/hysteria/core/internal/frag"
- "github.com/apernet/hysteria/core/internal/protocol"
+ coreErrs "github.com/apernet/hysteria/core/v2/errors"
+ "github.com/apernet/hysteria/core/v2/internal/frag"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
)
const (
diff --git a/core/client/udp_test.go b/core/client/udp_test.go
index 0ed9659..af1a6d0 100644
--- a/core/client/udp_test.go
+++ b/core/client/udp_test.go
@@ -10,8 +10,8 @@ import (
"github.com/stretchr/testify/mock"
"go.uber.org/goleak"
- coreErrs "github.com/apernet/hysteria/core/errors"
- "github.com/apernet/hysteria/core/internal/protocol"
+ coreErrs "github.com/apernet/hysteria/core/v2/errors"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
)
func TestUDPSessionManager(t *testing.T) {
diff --git a/core/go.mod b/core/go.mod
index 91d4cb9..a6ba575 100644
--- a/core/go.mod
+++ b/core/go.mod
@@ -1,33 +1,37 @@
-module github.com/apernet/hysteria/core
+module github.com/apernet/hysteria/core/v2
-go 1.21
+go 1.23
+
+toolchain go1.24.2
require (
- github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822
- github.com/stretchr/testify v1.8.4
+ github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431
+ github.com/stretchr/testify v1.9.0
go.uber.org/goleak v1.2.1
- golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
+ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/time v0.5.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/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
- github.com/kr/text v0.2.0 // indirect
- github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
+ github.com/kr/pretty v0.3.1 // 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.4.0 // indirect
- github.com/stretchr/objx v0.5.0 // indirect
- go.uber.org/mock v0.4.0 // indirect
- golang.org/x/crypto v0.19.0 // indirect
- golang.org/x/mod v0.12.0 // indirect
- golang.org/x/net v0.21.0 // indirect
- golang.org/x/sys v0.17.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- golang.org/x/tools v0.11.1 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
- gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // 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
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/core/go.sum b/core/go.sum
index 908b18e..f44407d 100644
--- a/core/go.sum
+++ b/core/go.sum
@@ -1,5 +1,5 @@
-github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822 h1:+ZSzRxSMg1+fLTQKcIUvD2cCCgS+1rtyRhs+NL5oBgA=
-github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822/go.mod h1:j3QaAM7sVJqptDQyIQRWA6mASCfuxoHJszn67JQh1GE=
+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/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=
@@ -11,62 +11,66 @@ 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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/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.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+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/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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+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/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+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.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc=
-golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.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=
diff --git a/core/internal/congestion/bbr/bbr_sender.go b/core/internal/congestion/bbr/bbr_sender.go
index 4afb078..8f58e1f 100644
--- a/core/internal/congestion/bbr/bbr_sender.go
+++ b/core/internal/congestion/bbr/bbr_sender.go
@@ -4,11 +4,13 @@ import (
"fmt"
"math/rand"
"net"
+ "os"
+ "strconv"
"time"
"github.com/apernet/quic-go/congestion"
- "github.com/apernet/hysteria/core/internal/congestion/common"
+ "github.com/apernet/hysteria/core/v2/internal/congestion/common"
)
// BbrSender implements BBR congestion control algorithm. BBR aims to estimate
@@ -37,6 +39,8 @@ const (
derivedHighGain = 2.773
// The newly derived CWND gain for STARTUP, 2.
derivedHighCWNDGain = 2.0
+
+ debugEnv = "HYSTERIA_BBR_DEBUG"
)
// The cycle of gains used during the PROBE_BW stage.
@@ -61,7 +65,7 @@ const (
// Flag.
defaultStartupFullLossCount = 8
quicBbr2DefaultLossThreshold = 0.02
- maxBbrBurstPackets = 3
+ maxBbrBurstPackets = 10
)
type bbrMode int
@@ -237,6 +241,8 @@ type bbrSender struct {
maxDatagramSize congestion.ByteCount
// Recorded on packet sent. equivalent |unacked_packets_->bytes_in_flight()|
bytesInFlight congestion.ByteCount
+
+ debug bool
}
var _ congestion.CongestionControl = &bbrSender{}
@@ -259,6 +265,7 @@ func newBbrSender(
initialCongestionWindow,
initialMaxCongestionWindow congestion.ByteCount,
) *bbrSender {
+ debug, _ := strconv.ParseBool(os.Getenv(debugEnv))
b := &bbrSender{
clock: clock,
mode: bbrModeStartup,
@@ -284,6 +291,7 @@ func newBbrSender(
cwndToCalculateMinPacingRate: initialCongestionWindow,
maxCongestionWindowWithNetworkParametersAdjusted: initialMaxCongestionWindow,
maxDatagramSize: initialMaxDatagramSize,
+ debug: debug,
}
b.pacer = common.NewPacer(b.bandwidthForPacer)
@@ -411,7 +419,7 @@ func (b *bbrSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, even
// packet in lost_packets.
var lastPacketSendState sendTimeState
- b.maybeApplimited(priorInFlight)
+ b.maybeAppLimited(priorInFlight)
// Update bytesInFlight
b.bytesInFlight = priorInFlight
@@ -539,7 +547,7 @@ func (b *bbrSender) setDrainGain(drainGain float64) {
b.drainGain = drainGain
}
-// What's the current estimated bandwidth in bytes per second.
+// Get the current bandwidth estimate. Note that Bandwidth is in bits per second.
func (b *bbrSender) bandwidthEstimate() Bandwidth {
return b.maxBandwidth.GetBest()
}
@@ -607,6 +615,10 @@ func (b *bbrSender) enterStartupMode(now time.Time) {
// b.maybeTraceStateChange(logging.CongestionStateStartup)
b.pacingGain = b.highGain
b.congestionWindowGain = b.highCwndGain
+
+ if b.debug {
+ b.debugPrint("Phase: STARTUP")
+ }
}
// Enters the PROBE_BW mode.
@@ -625,6 +637,10 @@ func (b *bbrSender) enterProbeBandwidthMode(now time.Time) {
b.lastCycleStart = now
b.pacingGain = pacingGain[b.cycleCurrentOffset]
+
+ if b.debug {
+ b.debugPrint("Phase: PROBE_BW")
+ }
}
// Updates the round-trip counter if a round-trip has passed. Returns true if
@@ -698,14 +714,8 @@ func (b *bbrSender) checkIfFullBandwidthReached(lastPacketSendState *sendTimeSta
}
}
-func (b *bbrSender) maybeApplimited(bytesInFlight congestion.ByteCount) {
- congestionWindow := b.GetCongestionWindow()
- if bytesInFlight >= congestionWindow {
- return
- }
- availableBytes := congestionWindow - bytesInFlight
- drainLimited := b.mode == bbrModeDrain && bytesInFlight > congestionWindow/2
- if !drainLimited || availableBytes > maxBbrBurstPackets*b.maxDatagramSize {
+func (b *bbrSender) maybeAppLimited(bytesInFlight congestion.ByteCount) {
+ if bytesInFlight < b.getTargetCongestionWindow(1) {
b.sampler.OnAppLimited()
}
}
@@ -718,6 +728,10 @@ func (b *bbrSender) maybeExitStartupOrDrain(now time.Time) {
// b.maybeTraceStateChange(logging.CongestionStateDrain)
b.pacingGain = b.drainGain
b.congestionWindowGain = b.highCwndGain
+
+ if b.debug {
+ b.debugPrint("Phase: DRAIN")
+ }
}
if b.mode == bbrModeDrain && b.bytesInFlight <= b.getTargetCongestionWindow(1) {
b.enterProbeBandwidthMode(now)
@@ -733,6 +747,12 @@ func (b *bbrSender) maybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRtt
// Do not decide on the time to exit PROBE_RTT until the |bytes_in_flight|
// is at the target small value.
b.exitProbeRttAt = time.Time{}
+
+ if b.debug {
+ b.debugPrint("BandwidthEstimate: %s, CongestionWindowGain: %.2f, PacingGain: %.2f, PacingRate: %s",
+ formatSpeed(b.bandwidthEstimate()), b.congestionWindowGain, b.pacingGain, formatSpeed(b.PacingRate()))
+ b.debugPrint("Phase: PROBE_RTT")
+ }
}
if b.mode == bbrModeProbeRtt {
@@ -754,6 +774,9 @@ func (b *bbrSender) maybeEnterOrExitProbeRtt(now time.Time, isRoundStart, minRtt
}
if now.Sub(b.exitProbeRttAt) >= 0 && b.probeRttRoundPassed {
b.minRttTimestamp = now
+ if b.debug {
+ b.debugPrint("MinRTT: %s", b.getMinRtt())
+ }
if !b.isAtFullBandwidth {
b.enterStartupMode(now)
} else {
@@ -925,6 +948,12 @@ func (b *bbrSender) shouldExitStartupDueToLoss(lastPacketSendState *sendTimeStat
return false
}
+func (b *bbrSender) debugPrint(format string, a ...any) {
+ fmt.Printf("[BBRSender] [%s] %s\n",
+ time.Now().Format("15:04:05"),
+ fmt.Sprintf(format, a...))
+}
+
func bdpFromRttAndBandwidth(rtt time.Duration, bandwidth Bandwidth) congestion.ByteCount {
return congestion.ByteCount(rtt) * congestion.ByteCount(bandwidth) / congestion.ByteCount(BytesPerSecond) / congestion.ByteCount(time.Second)
}
@@ -942,3 +971,14 @@ func GetInitialPacketSize(addr net.Addr) congestion.ByteCount {
return congestion.MinInitialPacketSize
}
}
+
+func formatSpeed(bw Bandwidth) string {
+ bwf := float64(bw)
+ units := []string{"bps", "Kbps", "Mbps", "Gbps"}
+ unitIndex := 0
+ for bwf > 1000 && unitIndex < len(units)-1 {
+ bwf /= 1000
+ unitIndex++
+ }
+ return fmt.Sprintf("%.2f %s", bwf, units[unitIndex])
+}
diff --git a/core/internal/congestion/bbr/packet_number_indexed_queue.go b/core/internal/congestion/bbr/packet_number_indexed_queue.go
index 86fe52d..08b99de 100644
--- a/core/internal/congestion/bbr/packet_number_indexed_queue.go
+++ b/core/internal/congestion/bbr/packet_number_indexed_queue.go
@@ -152,7 +152,7 @@ func (p *packetNumberIndexedQueue[T]) EntrySlotsUsed() int {
return p.entries.Len()
}
-// LastPacket returns packet number of the first entry in the queue.
+// FirstPacket returns packet number of the first entry in the queue.
func (p *packetNumberIndexedQueue[T]) FirstPacket() (packetNumber congestion.PacketNumber) {
return p.firstPacket
}
diff --git a/core/internal/congestion/brutal/brutal.go b/core/internal/congestion/brutal/brutal.go
index 4edd45d..de591a9 100644
--- a/core/internal/congestion/brutal/brutal.go
+++ b/core/internal/congestion/brutal/brutal.go
@@ -6,7 +6,7 @@ import (
"strconv"
"time"
- "github.com/apernet/hysteria/core/internal/congestion/common"
+ "github.com/apernet/hysteria/core/v2/internal/congestion/common"
"github.com/apernet/quic-go/congestion"
)
diff --git a/core/internal/congestion/common/pacer.go b/core/internal/congestion/common/pacer.go
index 4e089a3..6adbd81 100644
--- a/core/internal/congestion/common/pacer.go
+++ b/core/internal/congestion/common/pacer.go
@@ -1,14 +1,14 @@
package common
import (
- "math"
"time"
"github.com/apernet/quic-go/congestion"
)
const (
- maxBurstPackets = 10
+ maxBurstPackets = 10
+ maxBurstPacingDelayMultiplier = 4
)
// Pacer implements a token bucket pacing algorithm.
@@ -46,12 +46,12 @@ func (p *Pacer) Budget(now time.Time) congestion.ByteCount {
if budget < 0 { // protect against overflows
budget = congestion.ByteCount(1<<62 - 1)
}
- return minByteCount(p.maxBurstSize(), budget)
+ return min(p.maxBurstSize(), budget)
}
func (p *Pacer) maxBurstSize() congestion.ByteCount {
- return maxByteCount(
- congestion.ByteCount((congestion.MinPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9,
+ return max(
+ congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9,
maxBurstPackets*p.maxDatagramSize,
)
}
@@ -62,34 +62,18 @@ func (p *Pacer) TimeUntilSend() time.Time {
if p.budgetAtLastSent >= p.maxDatagramSize {
return time.Time{}
}
- return p.lastSentTime.Add(maxDuration(
- congestion.MinPacingDelay,
- time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/
- float64(p.getBandwidth())))*time.Nanosecond,
- ))
+ diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent)
+ bw := uint64(p.getBandwidth())
+ // We might need to round up this value.
+ // Otherwise, we might have a budget (slightly) smaller than the datagram size when the timer expires.
+ d := diff / bw
+ // this is effectively a math.Ceil, but using only integer math
+ if diff%bw > 0 {
+ d++
+ }
+ return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond))
}
func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) {
p.maxDatagramSize = s
}
-
-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
-}
diff --git a/core/internal/congestion/utils.go b/core/internal/congestion/utils.go
index 48ddaa2..1e06060 100644
--- a/core/internal/congestion/utils.go
+++ b/core/internal/congestion/utils.go
@@ -1,8 +1,8 @@
package congestion
import (
- "github.com/apernet/hysteria/core/internal/congestion/bbr"
- "github.com/apernet/hysteria/core/internal/congestion/brutal"
+ "github.com/apernet/hysteria/core/v2/internal/congestion/bbr"
+ "github.com/apernet/hysteria/core/v2/internal/congestion/brutal"
"github.com/apernet/quic-go"
)
diff --git a/core/internal/frag/frag.go b/core/internal/frag/frag.go
index 75c8dac..237ba06 100644
--- a/core/internal/frag/frag.go
+++ b/core/internal/frag/frag.go
@@ -1,7 +1,7 @@
package frag
import (
- "github.com/apernet/hysteria/core/internal/protocol"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
)
func FragUDPMessage(m *protocol.UDPMessage, maxSize int) []protocol.UDPMessage {
diff --git a/core/internal/frag/frag_test.go b/core/internal/frag/frag_test.go
index b77eeb3..0dc0cd7 100644
--- a/core/internal/frag/frag_test.go
+++ b/core/internal/frag/frag_test.go
@@ -4,7 +4,7 @@ import (
"reflect"
"testing"
- "github.com/apernet/hysteria/core/internal/protocol"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
)
func TestFragUDPMessage(t *testing.T) {
diff --git a/core/internal/integration_tests/.mockery.yaml b/core/internal/integration_tests/.mockery.yaml
index b1da36f..550a725 100644
--- a/core/internal/integration_tests/.mockery.yaml
+++ b/core/internal/integration_tests/.mockery.yaml
@@ -7,7 +7,7 @@ packages:
Conn:
config:
mockname: MockConn
- github.com/apernet/hysteria/core/server:
+ github.com/apernet/hysteria/core/v2/server:
interfaces:
Outbound:
config:
@@ -24,3 +24,6 @@ packages:
TrafficLogger:
config:
mockname: MockTrafficLogger
+ RequestHook:
+ config:
+ mockname: MockRequestHook
\ No newline at end of file
diff --git a/core/internal/integration_tests/close_test.go b/core/internal/integration_tests/close_test.go
index ab6d016..ac7f84b 100644
--- a/core/internal/integration_tests/close_test.go
+++ b/core/internal/integration_tests/close_test.go
@@ -9,10 +9,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
- "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"
+ "github.com/apernet/hysteria/core/v2/client"
+ "github.com/apernet/hysteria/core/v2/errors"
+ "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
+ "github.com/apernet/hysteria/core/v2/server"
)
// TestClientServerTCPClose tests whether the client/server propagates the close of a connection correctly.
diff --git a/core/internal/integration_tests/hook_test.go b/core/internal/integration_tests/hook_test.go
new file mode 100644
index 0000000..64affe8
--- /dev/null
+++ b/core/internal/integration_tests/hook_test.go
@@ -0,0 +1,147 @@
+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)
+}
diff --git a/core/internal/integration_tests/masq_test.go b/core/internal/integration_tests/masq_test.go
index e507da9..e3dee14 100644
--- a/core/internal/integration_tests/masq_test.go
+++ b/core/internal/integration_tests/masq_test.go
@@ -12,9 +12,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
- "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/hysteria/core/v2/internal/integration_tests/mocks"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
+ "github.com/apernet/hysteria/core/v2/server"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
@@ -41,7 +41,6 @@ func TestServerMasquerade(t *testing.T) {
// QUIC connection & RoundTripper
var conn quic.EarlyConnection
rt := &http3.RoundTripper{
- EnableDatagrams: true,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
diff --git a/core/internal/integration_tests/mocks/mock_Authenticator.go b/core/internal/integration_tests/mocks/mock_Authenticator.go
index 20329f7..b42c737 100644
--- a/core/internal/integration_tests/mocks/mock_Authenticator.go
+++ b/core/internal/integration_tests/mocks/mock_Authenticator.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
@@ -25,6 +25,10 @@ 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 {
diff --git a/core/internal/integration_tests/mocks/mock_Conn.go b/core/internal/integration_tests/mocks/mock_Conn.go
index 6840332..13e363e 100644
--- a/core/internal/integration_tests/mocks/mock_Conn.go
+++ b/core/internal/integration_tests/mocks/mock_Conn.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
@@ -27,6 +27,10 @@ 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()
@@ -68,6 +72,10 @@ 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()
@@ -111,6 +119,10 @@ 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 {
@@ -163,6 +175,10 @@ 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()
@@ -206,6 +222,10 @@ 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)
@@ -248,6 +268,10 @@ 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)
@@ -290,6 +314,10 @@ 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)
@@ -332,6 +360,10 @@ 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 {
diff --git a/core/internal/integration_tests/mocks/mock_EventLogger.go b/core/internal/integration_tests/mocks/mock_EventLogger.go
index cf63773..c9f6920 100644
--- a/core/internal/integration_tests/mocks/mock_EventLogger.go
+++ b/core/internal/integration_tests/mocks/mock_EventLogger.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
diff --git a/core/internal/integration_tests/mocks/mock_Outbound.go b/core/internal/integration_tests/mocks/mock_Outbound.go
index 32a747b..52f7178 100644
--- a/core/internal/integration_tests/mocks/mock_Outbound.go
+++ b/core/internal/integration_tests/mocks/mock_Outbound.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
@@ -7,7 +7,7 @@ import (
mock "github.com/stretchr/testify/mock"
- server "github.com/apernet/hysteria/core/server"
+ server "github.com/apernet/hysteria/core/v2/server"
)
// MockOutbound is an autogenerated mock type for the Outbound type
@@ -27,6 +27,10 @@ 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 {
@@ -81,6 +85,10 @@ 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 {
diff --git a/core/internal/integration_tests/mocks/mock_RequestHook.go b/core/internal/integration_tests/mocks/mock_RequestHook.go
new file mode 100644
index 0000000..5418eaf
--- /dev/null
+++ b/core/internal/integration_tests/mocks/mock_RequestHook.go
@@ -0,0 +1,188 @@
+// 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
+}
diff --git a/core/internal/integration_tests/mocks/mock_TrafficLogger.go b/core/internal/integration_tests/mocks/mock_TrafficLogger.go
index 0188ab3..1ed977e 100644
--- a/core/internal/integration_tests/mocks/mock_TrafficLogger.go
+++ b/core/internal/integration_tests/mocks/mock_TrafficLogger.go
@@ -1,8 +1,13 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
-import mock "github.com/stretchr/testify/mock"
+import (
+ quic "github.com/apernet/quic-go"
+ mock "github.com/stretchr/testify/mock"
+
+ server "github.com/apernet/hysteria/core/v2/server"
+)
// MockTrafficLogger is an autogenerated mock type for the TrafficLogger type
type MockTrafficLogger struct {
@@ -17,10 +22,48 @@ func (_m *MockTrafficLogger) EXPECT() *MockTrafficLogger_Expecter {
return &MockTrafficLogger_Expecter{mock: &_m.Mock}
}
-// Log provides a mock function with given fields: id, tx, rx
-func (_m *MockTrafficLogger) Log(id string, tx uint64, rx uint64) bool {
+// 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 {
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)
@@ -31,32 +74,99 @@ func (_m *MockTrafficLogger) Log(id string, tx uint64, rx uint64) bool {
return r0
}
-// MockTrafficLogger_Log_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Log'
-type MockTrafficLogger_Log_Call struct {
+// MockTrafficLogger_LogTraffic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LogTraffic'
+type MockTrafficLogger_LogTraffic_Call struct {
*mock.Call
}
-// Log is a helper method to define mock.On call
+// LogTraffic is a helper method to define mock.On call
// - id string
// - tx uint64
// - rx uint64
-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 (_e *MockTrafficLogger_Expecter) LogTraffic(id interface{}, tx interface{}, rx interface{}) *MockTrafficLogger_LogTraffic_Call {
+ return &MockTrafficLogger_LogTraffic_Call{Call: _e.mock.On("LogTraffic", id, tx, rx)}
}
-func (_c *MockTrafficLogger_Log_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_Log_Call {
+func (_c *MockTrafficLogger_LogTraffic_Call) Run(run func(id string, tx uint64, rx uint64)) *MockTrafficLogger_LogTraffic_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(uint64), args[2].(uint64))
})
return _c
}
-func (_c *MockTrafficLogger_Log_Call) Return(ok bool) *MockTrafficLogger_Log_Call {
+func (_c *MockTrafficLogger_LogTraffic_Call) Return(ok bool) *MockTrafficLogger_LogTraffic_Call {
_c.Call.Return(ok)
return _c
}
-func (_c *MockTrafficLogger_Log_Call) RunAndReturn(run func(string, uint64, uint64) bool) *MockTrafficLogger_Log_Call {
+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 {
_c.Call.Return(run)
return _c
}
diff --git a/core/internal/integration_tests/mocks/mock_UDPConn.go b/core/internal/integration_tests/mocks/mock_UDPConn.go
index 808c18b..b883870 100644
--- a/core/internal/integration_tests/mocks/mock_UDPConn.go
+++ b/core/internal/integration_tests/mocks/mock_UDPConn.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package mocks
@@ -21,6 +21,10 @@ 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()
@@ -62,6 +66,10 @@ 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
@@ -121,6 +129,10 @@ 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 {
diff --git a/core/internal/integration_tests/smoke_test.go b/core/internal/integration_tests/smoke_test.go
index ab204cb..5288b61 100644
--- a/core/internal/integration_tests/smoke_test.go
+++ b/core/internal/integration_tests/smoke_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
- "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"
+ "github.com/apernet/hysteria/core/v2/client"
+ coreErrs "github.com/apernet/hysteria/core/v2/errors"
+ "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
+ "github.com/apernet/hysteria/core/v2/server"
)
// Smoke tests that act as a sanity check for client & server to ensure they can talk to each other correctly.
diff --git a/core/internal/integration_tests/stress_test.go b/core/internal/integration_tests/stress_test.go
index f10ac3a..cf44ad6 100644
--- a/core/internal/integration_tests/stress_test.go
+++ b/core/internal/integration_tests/stress_test.go
@@ -13,9 +13,9 @@ import (
"github.com/stretchr/testify/mock"
"golang.org/x/time/rate"
- "github.com/apernet/hysteria/core/client"
- "github.com/apernet/hysteria/core/internal/integration_tests/mocks"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/client"
+ "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
+ "github.com/apernet/hysteria/core/v2/server"
)
type tcpStressor struct {
diff --git a/core/internal/integration_tests/trafficlogger_test.go b/core/internal/integration_tests/trafficlogger_test.go
index abbea8c..841f4ff 100644
--- a/core/internal/integration_tests/trafficlogger_test.go
+++ b/core/internal/integration_tests/trafficlogger_test.go
@@ -9,9 +9,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
- "github.com/apernet/hysteria/core/client"
- "github.com/apernet/hysteria/core/internal/integration_tests/mocks"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/client"
+ "github.com/apernet/hysteria/core/v2/internal/integration_tests/mocks"
+ "github.com/apernet/hysteria/core/v2/server"
)
// TestClientServerTrafficLoggerTCP tests that the traffic logger is correctly called for TCP connections,
@@ -36,6 +36,7 @@ func TestClientServerTrafficLoggerTCP(t *testing.T) {
go s.Serve()
// Create client
+ trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once()
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
@@ -61,12 +62,13 @@ func TestClientServerTrafficLoggerTCP(t *testing.T) {
return nil
})
serverOb.EXPECT().TCP(addr).Return(sobConn, nil).Once()
+ trafficLogger.EXPECT().TraceStream(mock.Anything, mock.Anything).Return().Once()
conn, err := c.TCP(addr)
assert.NoError(t, err)
// Client reads from server
- trafficLogger.EXPECT().Log("nobody", uint64(0), uint64(11)).Return(true).Once()
+ trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(11)).Return(true).Once()
sobConnCh <- []byte("knock knock")
buf := make([]byte, 100)
n, err := conn.Read(buf)
@@ -75,7 +77,7 @@ func TestClientServerTrafficLoggerTCP(t *testing.T) {
assert.Equal(t, "knock knock", string(buf[:n]))
// Client writes to server
- trafficLogger.EXPECT().Log("nobody", uint64(12), uint64(0)).Return(true).Once()
+ trafficLogger.EXPECT().LogTraffic("nobody", uint64(12), uint64(0)).Return(true).Once()
sobConn.EXPECT().Write([]byte("who is there")).Return(12, nil).Once()
n, err = conn.Write([]byte("who is there"))
assert.NoError(t, err)
@@ -83,7 +85,9 @@ func TestClientServerTrafficLoggerTCP(t *testing.T) {
time.Sleep(1 * time.Second) // Need some time for the server to receive the data
// Client reads from server again but blocked
- trafficLogger.EXPECT().Log("nobody", uint64(0), uint64(4)).Return(false).Once()
+ trafficLogger.EXPECT().UntraceStream(mock.Anything).Return().Once()
+ trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once()
+ trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once()
sobConnCh <- []byte("nope")
n, err = conn.Read(buf)
assert.Zero(t, n)
@@ -116,6 +120,7 @@ func TestClientServerTrafficLoggerUDP(t *testing.T) {
go s.Serve()
// Create client
+ trafficLogger.EXPECT().LogOnlineState("nobody", true).Return().Once()
c, _, err := client.NewClient(&client.Config{
ServerAddr: udpAddr,
TLSConfig: client.TLSConfig{InsecureSkipVerify: true},
@@ -146,14 +151,14 @@ func TestClientServerTrafficLoggerUDP(t *testing.T) {
assert.NoError(t, err)
// Client writes to server
- trafficLogger.EXPECT().Log("nobody", uint64(9), uint64(0)).Return(true).Once()
+ trafficLogger.EXPECT().LogTraffic("nobody", uint64(9), uint64(0)).Return(true).Once()
sobConn.EXPECT().WriteTo([]byte("small sad"), addr).Return(9, nil).Once()
err = conn.Send([]byte("small sad"), addr)
assert.NoError(t, err)
time.Sleep(1 * time.Second) // Need some time for the server to receive the data
// Client reads from server
- trafficLogger.EXPECT().Log("nobody", uint64(0), uint64(7)).Return(true).Once()
+ trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(7)).Return(true).Once()
sobConnCh <- []byte("big mad")
bs, rAddr, err := conn.Receive()
assert.NoError(t, err)
@@ -161,7 +166,8 @@ func TestClientServerTrafficLoggerUDP(t *testing.T) {
assert.Equal(t, "big mad", string(bs))
// Client reads from server again but blocked
- trafficLogger.EXPECT().Log("nobody", uint64(0), uint64(4)).Return(false).Once()
+ trafficLogger.EXPECT().LogTraffic("nobody", uint64(0), uint64(4)).Return(false).Once()
+ trafficLogger.EXPECT().LogOnlineState("nobody", false).Return().Once()
sobConnCh <- []byte("nope")
bs, rAddr, err = conn.Receive()
assert.Equal(t, err, io.EOF)
diff --git a/core/internal/integration_tests/utils_test.go b/core/internal/integration_tests/utils_test.go
index a1aa160..d7ecb68 100644
--- a/core/internal/integration_tests/utils_test.go
+++ b/core/internal/integration_tests/utils_test.go
@@ -5,7 +5,7 @@ import (
"io"
"net"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/server"
)
// This file provides utilities for the integration tests.
diff --git a/core/internal/protocol/proxy.go b/core/internal/protocol/proxy.go
index 4a78fc2..9335c47 100644
--- a/core/internal/protocol/proxy.go
+++ b/core/internal/protocol/proxy.go
@@ -6,7 +6,7 @@ import (
"fmt"
"io"
- "github.com/apernet/hysteria/core/errors"
+ "github.com/apernet/hysteria/core/v2/errors"
"github.com/apernet/quic-go/quicvarint"
)
diff --git a/core/internal/utils/atomic.go b/core/internal/utils/atomic.go
index e3c3d97..7739013 100644
--- a/core/internal/utils/atomic.go
+++ b/core/internal/utils/atomic.go
@@ -22,3 +22,33 @@ func (t *AtomicTime) Set(new time.Time) {
func (t *AtomicTime) Get() time.Time {
return t.v.Load().(time.Time)
}
+
+type Atomic[T any] struct {
+ v atomic.Value
+}
+
+func (a *Atomic[T]) Load() T {
+ value := a.v.Load()
+ if value == nil {
+ var zero T
+ return zero
+ }
+ return value.(T)
+}
+
+func (a *Atomic[T]) Store(value T) {
+ a.v.Store(value)
+}
+
+func (a *Atomic[T]) Swap(new T) T {
+ old := a.v.Swap(new)
+ if old == nil {
+ var zero T
+ return zero
+ }
+ return old.(T)
+}
+
+func (a *Atomic[T]) CompareAndSwap(old, new T) bool {
+ return a.v.CompareAndSwap(old, new)
+}
diff --git a/core/server/.mockery.yaml b/core/server/.mockery.yaml
index b7cf743..d73136a 100644
--- a/core/server/.mockery.yaml
+++ b/core/server/.mockery.yaml
@@ -2,7 +2,7 @@ with-expecter: true
inpackage: true
dir: .
packages:
- github.com/apernet/hysteria/core/server:
+ github.com/apernet/hysteria/core/v2/server:
interfaces:
udpIO:
config:
diff --git a/core/server/config.go b/core/server/config.go
index f647f0d..a01f478 100644
--- a/core/server/config.go
+++ b/core/server/config.go
@@ -4,10 +4,13 @@ import (
"crypto/tls"
"net"
"net/http"
+ "sync/atomic"
"time"
- "github.com/apernet/hysteria/core/errors"
- "github.com/apernet/hysteria/core/internal/pmtud"
+ "github.com/apernet/hysteria/core/v2/errors"
+ "github.com/apernet/hysteria/core/v2/internal/pmtud"
+ "github.com/apernet/hysteria/core/v2/internal/utils"
+ "github.com/apernet/quic-go"
)
const (
@@ -22,6 +25,7 @@ type Config struct {
TLSConfig TLSConfig
QUICConfig QUICConfig
Conn net.PacketConn
+ RequestHook RequestHook
Outbound Outbound
BandwidthConfig BandwidthConfig
IgnoreClientBandwidth bool
@@ -110,6 +114,19 @@ type QUICConfig struct {
DisablePathMTUDiscovery bool // The server may still override this to true on unsupported platforms.
}
+// RequestHook allows filtering and modifying requests before the server connects to the remote.
+// A request will only be hooked if Check returns true.
+// The returned byte slice, if not empty, will be sent to the remote before proxying - this is
+// mainly for "putting back" the content read from the client for sniffing, etc.
+// Return a non-nil error to abort the connection.
+// Note that due to the current architectural limitations, it can only inspect the first packet
+// of a UDP connection. It also cannot put back any data as the first packet is always sent as-is.
+type RequestHook interface {
+ Check(isUDP bool, reqAddr string) bool
+ TCP(stream quic.Stream, reqAddr *string) ([]byte, error)
+ UDP(data []byte, reqAddr *string) error
+}
+
// Outbound provides the implementation of how the server should connect to remote servers.
// Although UDP includes a reqAddr, the implementation does not necessarily have to use it
// to make a "connected" UDP connection that does not accept packets from other addresses.
@@ -195,5 +212,68 @@ type EventLogger interface {
// bandwidth limits or post-connection authentication, for example.
// The implementation of this interface must be thread-safe.
type TrafficLogger interface {
- Log(id string, tx, rx uint64) (ok bool)
+ LogTraffic(id string, tx, rx uint64) (ok bool)
+ LogOnlineState(id string, online bool)
+ TraceStream(stream quic.Stream, stats *StreamStats)
+ UntraceStream(stream quic.Stream)
+}
+
+type StreamState int
+
+const (
+ // StreamStateInitial indicates the initial state of a stream.
+ // Client has opened the stream, but we have not received the proxy request yet.
+ StreamStateInitial StreamState = iota
+
+ // StreamStateHooking indicates that the hook (usually sniff) is processing.
+ // Client has sent the proxy request, but sniff requires more data to complete.
+ StreamStateHooking
+
+ // StreamStateConnecting indicates that we are connecting to the proxy target.
+ StreamStateConnecting
+
+ // StreamStateEstablished indicates the proxy is established.
+ StreamStateEstablished
+
+ // StreamStateClosed indicates the stream is closed.
+ StreamStateClosed
+)
+
+func (s StreamState) String() string {
+ switch s {
+ case StreamStateInitial:
+ return "init"
+ case StreamStateHooking:
+ return "hook"
+ case StreamStateConnecting:
+ return "connect"
+ case StreamStateEstablished:
+ return "estab"
+ case StreamStateClosed:
+ return "closed"
+ default:
+ return "unknown"
+ }
+}
+
+type StreamStats struct {
+ State utils.Atomic[StreamState]
+
+ AuthID string
+ ConnID uint32
+ InitialTime time.Time
+
+ ReqAddr utils.Atomic[string]
+ HookedReqAddr utils.Atomic[string]
+
+ Tx atomic.Uint64
+ Rx atomic.Uint64
+
+ LastActiveTime utils.Atomic[time.Time]
+}
+
+func (s *StreamStats) setHookedReqAddr(addr string) {
+ if addr != s.ReqAddr.Load() {
+ s.HookedReqAddr.Store(addr)
+ }
}
diff --git a/core/server/copy.go b/core/server/copy.go
index 25831e5..7123fc8 100644
--- a/core/server/copy.go
+++ b/core/server/copy.go
@@ -3,6 +3,7 @@ package server
import (
"errors"
"io"
+ "time"
)
var errDisconnect = errors.New("traffic logger requested disconnect")
@@ -31,23 +32,27 @@ func copyBufferLog(dst io.Writer, src io.Reader, log func(n uint64) bool) error
}
}
-func copyTwoWayWithLogger(id string, serverRw, remoteRw io.ReadWriter, l TrafficLogger) error {
+func copyTwoWayEx(id string, serverRw, remoteRw io.ReadWriter, l TrafficLogger, stats *StreamStats) error {
errChan := make(chan error, 2)
go func() {
errChan <- copyBufferLog(serverRw, remoteRw, func(n uint64) bool {
- return l.Log(id, 0, n)
+ stats.LastActiveTime.Store(time.Now())
+ stats.Rx.Add(n)
+ return l.LogTraffic(id, 0, n)
})
}()
go func() {
errChan <- copyBufferLog(remoteRw, serverRw, func(n uint64) bool {
- return l.Log(id, n, 0)
+ stats.LastActiveTime.Store(time.Now())
+ stats.Tx.Add(n)
+ return l.LogTraffic(id, n, 0)
})
}()
// Block until one of the two goroutines returns
return <-errChan
}
-// copyTwoWay is the "fast-path" version of copyTwoWayWithLogger that does not log traffic.
+// copyTwoWay is the "fast-path" version of copyTwoWayEx that does not log traffic or update stream stats.
// It uses the built-in io.Copy instead of our own copyBufferLog.
func copyTwoWay(serverRw, remoteRw io.ReadWriter) error {
errChan := make(chan error, 2)
diff --git a/core/server/mock_UDPConn.go b/core/server/mock_UDPConn.go
index 34a34ee..7299975 100644
--- a/core/server/mock_UDPConn.go
+++ b/core/server/mock_UDPConn.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package server
@@ -21,6 +21,10 @@ 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()
@@ -62,6 +66,10 @@ 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
@@ -121,6 +129,10 @@ 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 {
diff --git a/core/server/mock_udpEventLogger.go b/core/server/mock_udpEventLogger.go
index 08f3ff8..5a54b0b 100644
--- a/core/server/mock_udpEventLogger.go
+++ b/core/server/mock_udpEventLogger.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package server
diff --git a/core/server/mock_udpIO.go b/core/server/mock_udpIO.go
index ddd44b3..bbdcf94 100644
--- a/core/server/mock_udpIO.go
+++ b/core/server/mock_udpIO.go
@@ -1,9 +1,9 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package server
import (
- protocol "github.com/apernet/hysteria/core/internal/protocol"
+ protocol "github.com/apernet/hysteria/core/v2/internal/protocol"
mock "github.com/stretchr/testify/mock"
)
@@ -20,10 +20,61 @@ func (_m *mockUDPIO) EXPECT() *mockUDPIO_Expecter {
return &mockUDPIO_Expecter{mock: &_m.Mock}
}
+// Hook provides a mock function with given fields: data, reqAddr
+func (_m *mockUDPIO) Hook(data []byte, reqAddr *string) error {
+ ret := _m.Called(data, reqAddr)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Hook")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func([]byte, *string) error); ok {
+ r0 = rf(data, reqAddr)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// mockUDPIO_Hook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Hook'
+type mockUDPIO_Hook_Call struct {
+ *mock.Call
+}
+
+// Hook is a helper method to define mock.On call
+// - data []byte
+// - reqAddr *string
+func (_e *mockUDPIO_Expecter) Hook(data interface{}, reqAddr interface{}) *mockUDPIO_Hook_Call {
+ return &mockUDPIO_Hook_Call{Call: _e.mock.On("Hook", data, reqAddr)}
+}
+
+func (_c *mockUDPIO_Hook_Call) Run(run func(data []byte, reqAddr *string)) *mockUDPIO_Hook_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].([]byte), args[1].(*string))
+ })
+ return _c
+}
+
+func (_c *mockUDPIO_Hook_Call) Return(_a0 error) *mockUDPIO_Hook_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *mockUDPIO_Hook_Call) RunAndReturn(run func([]byte, *string) error) *mockUDPIO_Hook_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
// ReceiveMessage provides a mock function with given fields:
func (_m *mockUDPIO) ReceiveMessage() (*protocol.UDPMessage, error) {
ret := _m.Called()
+ if len(ret) == 0 {
+ panic("no return value specified for ReceiveMessage")
+ }
+
var r0 *protocol.UDPMessage
var r1 error
if rf, ok := ret.Get(0).(func() (*protocol.UDPMessage, error)); ok {
@@ -77,6 +128,10 @@ 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)
@@ -120,6 +175,10 @@ func (_c *mockUDPIO_SendMessage_Call) RunAndReturn(run func([]byte, *protocol.UD
func (_m *mockUDPIO) UDP(reqAddr string) (UDPConn, error) {
ret := _m.Called(reqAddr)
+ if len(ret) == 0 {
+ panic("no return value specified for UDP")
+ }
+
var r0 UDPConn
var r1 error
if rf, ok := ret.Get(0).(func(string) (UDPConn, error)); ok {
diff --git a/core/server/server.go b/core/server/server.go
index 812606a..89645e0 100644
--- a/core/server/server.go
+++ b/core/server/server.go
@@ -3,15 +3,17 @@ package server
import (
"context"
"crypto/tls"
+ "math/rand"
"net/http"
"sync"
+ "time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
- "github.com/apernet/hysteria/core/internal/congestion"
- "github.com/apernet/hysteria/core/internal/protocol"
- "github.com/apernet/hysteria/core/internal/utils"
+ "github.com/apernet/hysteria/core/v2/internal/congestion"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
+ "github.com/apernet/hysteria/core/v2/internal/utils"
)
const (
@@ -41,6 +43,7 @@ func NewServer(config *Config) (Server, error) {
MaxIncomingStreams: config.QUICConfig.MaxIncomingStreams,
DisablePathMTUDiscovery: config.QUICConfig.DisablePathMTUDiscovery,
EnableDatagrams: true,
+ DisablePathManager: true,
}
listener, err := quic.Listen(config.Conn, tlsConfig, quicConfig)
if err != nil {
@@ -77,14 +80,18 @@ func (s *serverImpl) Close() error {
func (s *serverImpl) handleClient(conn quic.Connection) {
handler := newH3sHandler(s.config, conn)
h3s := http3.Server{
- EnableDatagrams: true,
- Handler: handler,
- StreamHijacker: handler.ProxyStreamHijacker,
+ Handler: handler,
+ StreamHijacker: handler.ProxyStreamHijacker,
}
err := h3s.ServeQUICConn(conn)
// If the client is authenticated, we need to log the disconnect event
- if handler.authenticated && s.config.EventLogger != nil {
- s.config.EventLogger.Disconnect(conn.RemoteAddr(), handler.authID, err)
+ if handler.authenticated {
+ if tl := s.config.TrafficLogger; tl != nil {
+ tl.LogOnlineState(handler.authID, false)
+ }
+ if el := s.config.EventLogger; el != nil {
+ el.Disconnect(conn.RemoteAddr(), handler.authID, err)
+ }
}
_ = conn.CloseWithError(closeErrCodeOK, "")
}
@@ -96,6 +103,7 @@ type h3sHandler struct {
authenticated bool
authMutex sync.Mutex
authID string
+ connID uint32 // a random id for dump streams
udpSM *udpSessionManager // Only set after authentication
}
@@ -104,6 +112,7 @@ func newH3sHandler(config *Config, conn quic.Connection) *h3sHandler {
return &h3sHandler{
config: config,
conn: conn,
+ connID: rand.Uint32(),
}
}
@@ -154,8 +163,11 @@ func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
})
w.WriteHeader(protocol.StatusAuthOK)
// Call event logger
- if h.config.EventLogger != nil {
- h.config.EventLogger.Connect(h.conn.RemoteAddr(), id, actualTx)
+ if tl := h.config.TrafficLogger; tl != nil {
+ tl.LogOnlineState(id, true)
+ }
+ if el := h.config.EventLogger; el != nil {
+ el.Connect(h.conn.RemoteAddr(), id, actualTx)
}
// Initialize UDP session manager (if UDP is enabled)
// We use sync.Once to make sure that only one goroutine is started,
@@ -163,7 +175,7 @@ func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !h.config.DisableUDP {
go func() {
sm := newUDPSessionManager(
- &udpIOImpl{h.conn, id, h.config.TrafficLogger, h.config.Outbound},
+ &udpIOImpl{h.conn, id, h.config.TrafficLogger, h.config.RequestHook, h.config.Outbound},
&udpEventLoggerImpl{h.conn, id, h.config.EventLogger},
h.config.UDPIdleTimeout)
h.udpSM = sm
@@ -180,7 +192,7 @@ func (h *h3sHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
-func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, conn quic.Connection, stream quic.Stream, err error) (bool, error) {
+func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, id quic.ConnectionTracingID, stream quic.Stream, err error) (bool, error) {
if err != nil || !h.authenticated {
return false, nil
}
@@ -198,20 +210,59 @@ func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, conn quic.Connectio
}
func (h *h3sHandler) handleTCPRequest(stream quic.Stream) {
+ trafficLogger := h.config.TrafficLogger
+ streamStats := &StreamStats{
+ AuthID: h.authID,
+ ConnID: h.connID,
+ InitialTime: time.Now(),
+ }
+ streamStats.State.Store(StreamStateInitial)
+ streamStats.LastActiveTime.Store(time.Now())
+ defer func() {
+ streamStats.State.Store(StreamStateClosed)
+ }()
+ if trafficLogger != nil {
+ trafficLogger.TraceStream(stream, streamStats)
+ defer trafficLogger.UntraceStream(stream)
+ }
+
// Read request
reqAddr, err := protocol.ReadTCPRequest(stream)
if err != nil {
_ = stream.Close()
return
}
+ streamStats.ReqAddr.Store(reqAddr)
+ // Call the hook if set
+ var putback []byte
+ var hooked bool
+ if h.config.RequestHook != nil {
+ hooked = h.config.RequestHook.Check(false, reqAddr)
+ // When the hook is enabled, the server should always accept a connection
+ // so that the client will send whatever request the hook wants to see.
+ // This is essentially a server-side fast-open.
+ if hooked {
+ streamStats.State.Store(StreamStateHooking)
+ _ = protocol.WriteTCPResponse(stream, true, "RequestHook enabled")
+ putback, err = h.config.RequestHook.TCP(stream, &reqAddr)
+ if err != nil {
+ _ = stream.Close()
+ return
+ }
+ streamStats.setHookedReqAddr(reqAddr)
+ }
+ }
// Log the event
if h.config.EventLogger != nil {
h.config.EventLogger.TCPRequest(h.conn.RemoteAddr(), h.authID, reqAddr)
}
// Dial target
+ streamStats.State.Store(StreamStateConnecting)
tConn, err := h.config.Outbound.TCP(reqAddr)
if err != nil {
- _ = protocol.WriteTCPResponse(stream, false, err.Error())
+ if !hooked {
+ _ = protocol.WriteTCPResponse(stream, false, err.Error())
+ }
_ = stream.Close()
// Log the error
if h.config.EventLogger != nil {
@@ -219,10 +270,18 @@ func (h *h3sHandler) handleTCPRequest(stream quic.Stream) {
}
return
}
- _ = protocol.WriteTCPResponse(stream, true, "")
+ if !hooked {
+ _ = protocol.WriteTCPResponse(stream, true, "Connected")
+ }
+ streamStats.State.Store(StreamStateEstablished)
+ // Put back the data if the hook requested
+ if len(putback) > 0 {
+ n, _ := tConn.Write(putback)
+ streamStats.Tx.Add(uint64(n))
+ }
// Start proxying
- if h.config.TrafficLogger != nil {
- err = copyTwoWayWithLogger(h.authID, stream, tConn, h.config.TrafficLogger)
+ if trafficLogger != nil {
+ err = copyTwoWayEx(h.authID, stream, tConn, trafficLogger, streamStats)
} else {
// Use the fast path if no traffic logger is set
err = copyTwoWay(stream, tConn)
@@ -253,6 +312,7 @@ type udpIOImpl struct {
Conn quic.Connection
AuthID string
TrafficLogger TrafficLogger
+ RequestHook RequestHook
Outbound Outbound
}
@@ -269,7 +329,7 @@ func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {
continue
}
if io.TrafficLogger != nil {
- ok := io.TrafficLogger.Log(io.AuthID, uint64(len(udpMsg.Data)), 0)
+ ok := io.TrafficLogger.LogTraffic(io.AuthID, uint64(len(udpMsg.Data)), 0)
if !ok {
// TrafficLogger requested to disconnect the client
_ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "")
@@ -282,7 +342,7 @@ func (io *udpIOImpl) ReceiveMessage() (*protocol.UDPMessage, error) {
func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {
if io.TrafficLogger != nil {
- ok := io.TrafficLogger.Log(io.AuthID, 0, uint64(len(msg.Data)))
+ ok := io.TrafficLogger.LogTraffic(io.AuthID, 0, uint64(len(msg.Data)))
if !ok {
// TrafficLogger requested to disconnect the client
_ = io.Conn.CloseWithError(closeErrCodeTrafficLimitReached, "")
@@ -297,6 +357,14 @@ func (io *udpIOImpl) SendMessage(buf []byte, msg *protocol.UDPMessage) error {
return io.Conn.SendDatagram(buf[:msgN])
}
+func (io *udpIOImpl) Hook(data []byte, reqAddr *string) error {
+ if io.RequestHook != nil && io.RequestHook.Check(true, *reqAddr) {
+ return io.RequestHook.UDP(data, reqAddr)
+ } else {
+ return nil
+ }
+}
+
func (io *udpIOImpl) UDP(reqAddr string) (UDPConn, error) {
return io.Outbound.UDP(reqAddr)
}
diff --git a/core/server/udp.go b/core/server/udp.go
index bea586b..14efc9e 100644
--- a/core/server/udp.go
+++ b/core/server/udp.go
@@ -8,9 +8,9 @@ import (
"github.com/apernet/quic-go"
- "github.com/apernet/hysteria/core/internal/frag"
- "github.com/apernet/hysteria/core/internal/protocol"
- "github.com/apernet/hysteria/core/internal/utils"
+ "github.com/apernet/hysteria/core/v2/internal/frag"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
+ "github.com/apernet/hysteria/core/v2/internal/utils"
)
const (
@@ -20,6 +20,7 @@ const (
type udpIO interface {
ReceiveMessage() (*protocol.UDPMessage, error)
SendMessage([]byte, *protocol.UDPMessage) error
+ Hook(data []byte, reqAddr *string) error
UDP(reqAddr string) (UDPConn, error)
}
@@ -29,11 +30,58 @@ type udpEventLogger interface {
}
type udpSessionEntry struct {
- ID uint32
- Conn UDPConn
- D *frag.Defragger
- Last *utils.AtomicTime
- Timeout bool // true if the session is closed due to timeout
+ ID uint32
+ OverrideAddr string // Ignore the address in the UDP message, always use this if not empty
+ OriginalAddr string // The original address in the UDP message
+ D *frag.Defragger
+ Last *utils.AtomicTime
+ IO udpIO
+
+ DialFunc func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error)
+ ExitFunc func(err error)
+
+ conn UDPConn
+ connLock sync.Mutex
+ closed bool
+}
+
+func newUDPSessionEntry(
+ id uint32, io udpIO,
+ dialFunc func(string, []byte) (UDPConn, string, error),
+ exitFunc func(error),
+) (e *udpSessionEntry) {
+ e = &udpSessionEntry{
+ ID: id,
+ D: &frag.Defragger{},
+ Last: utils.NewAtomicTime(time.Now()),
+ IO: io,
+
+ DialFunc: dialFunc,
+ ExitFunc: exitFunc,
+ }
+
+ return
+}
+
+// CloseWithErr closes the session and calls ExitFunc with the given error.
+// A nil error indicates the session is cleaned up due to timeout.
+func (e *udpSessionEntry) CloseWithErr(err error) {
+ // We need this lock to ensure not to create conn after session exit
+ e.connLock.Lock()
+
+ if e.closed {
+ // Already closed
+ e.connLock.Unlock()
+ return
+ }
+
+ e.closed = true
+ if e.conn != nil {
+ _ = e.conn.Close()
+ }
+ e.connLock.Unlock()
+
+ e.ExitFunc(err)
}
// Feed feeds a UDP message to the session.
@@ -47,23 +95,78 @@ func (e *udpSessionEntry) Feed(msg *protocol.UDPMessage) (int, error) {
if dfMsg == nil {
return 0, nil
}
- return e.Conn.WriteTo(dfMsg.Data, dfMsg.Addr)
+
+ if e.conn == nil {
+ err := e.initConn(dfMsg)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ addr := dfMsg.Addr
+ if e.OverrideAddr != "" {
+ addr = e.OverrideAddr
+ }
+
+ return e.conn.WriteTo(dfMsg.Data, addr)
}
-// ReceiveLoop receives incoming UDP packets, packs them into UDP messages,
-// and sends using the provided io.
-// Exit and returns error when either the underlying UDP connection returns
-// error (e.g. closed), or the provided io returns error when sending.
-func (e *udpSessionEntry) ReceiveLoop(io udpIO) error {
+// initConn initializes the UDP connection of the session.
+// If no error is returned, the e.conn is set to the new connection.
+func (e *udpSessionEntry) initConn(firstMsg *protocol.UDPMessage) error {
+ // We need this lock to ensure not to create conn after session exit
+ e.connLock.Lock()
+
+ if e.closed {
+ e.connLock.Unlock()
+ return errors.New("session is closed")
+ }
+
+ conn, actualAddr, err := e.DialFunc(firstMsg.Addr, firstMsg.Data)
+ if err != nil {
+ // Fail fast if DialFunc failed
+ // (usually indicates the connection has been rejected by the ACL)
+ e.connLock.Unlock()
+ // CloseWithErr acquires the connLock again
+ e.CloseWithErr(err)
+ return err
+ }
+
+ e.conn = conn
+
+ if firstMsg.Addr != actualAddr {
+ // Hook changed the address, enable address override
+ e.OverrideAddr = actualAddr
+ e.OriginalAddr = firstMsg.Addr
+ }
+ go e.receiveLoop()
+
+ e.connLock.Unlock()
+ return nil
+}
+
+// receiveLoop receives incoming UDP packets, packs them into UDP messages,
+// and sends using the IO.
+// Exit when either the underlying UDP connection returns error (e.g. closed),
+// or the IO returns error when sending.
+func (e *udpSessionEntry) receiveLoop() {
udpBuf := make([]byte, protocol.MaxUDPSize)
msgBuf := make([]byte, protocol.MaxUDPSize)
for {
- udpN, rAddr, err := e.Conn.ReadFrom(udpBuf)
+ udpN, rAddr, err := e.conn.ReadFrom(udpBuf)
if err != nil {
- return err
+ e.CloseWithErr(err)
+ return
}
e.Last.Set(time.Now())
+ if e.OriginalAddr != "" {
+ // Use the original address in the opposite direction,
+ // otherwise the QUIC clients or NAT on the client side
+ // may not treat it as the same UDP session.
+ rAddr = e.OriginalAddr
+ }
+
msg := &protocol.UDPMessage{
SessionID: e.ID,
PacketID: 0,
@@ -72,9 +175,10 @@ func (e *udpSessionEntry) ReceiveLoop(io udpIO) error {
Addr: rAddr,
Data: udpBuf[:udpN],
}
- err = sendMessageAutoFrag(io, msgBuf, msg)
+ err = sendMessageAutoFrag(e.IO, msgBuf, msg)
if err != nil {
- return err
+ e.CloseWithErr(err)
+ return
}
}
}
@@ -155,19 +259,23 @@ func (m *udpSessionManager) idleCleanupLoop(stopCh <-chan struct{}) {
}
func (m *udpSessionManager) cleanup(idleOnly bool) {
+ timeoutEntry := make([]*udpSessionEntry, 0, len(m.m))
+
// We use RLock here as we are only scanning the map, not deleting from it.
m.mutex.RLock()
- defer m.mutex.RUnlock()
-
now := time.Now()
for _, entry := range m.m {
if !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout {
- entry.Timeout = true
- _ = entry.Conn.Close()
- // Closing the connection here will cause the ReceiveLoop to exit,
- // and the session will be removed from the map there.
+ timeoutEntry = append(timeoutEntry, entry)
}
}
+ m.mutex.RUnlock()
+
+ for _, entry := range timeoutEntry {
+ // This eventually calls entry.ExitFunc,
+ // where the m.mutex will be locked again to remove the entry from the map.
+ entry.CloseWithErr(nil)
+ }
}
func (m *udpSessionManager) feed(msg *protocol.UDPMessage) {
@@ -177,35 +285,31 @@ func (m *udpSessionManager) feed(msg *protocol.UDPMessage) {
// Create a new session if not exists
if entry == nil {
- m.eventLogger.New(msg.SessionID, msg.Addr)
- conn, err := m.io.UDP(msg.Addr)
- if err != nil {
- m.eventLogger.Close(msg.SessionID, err)
+ dialFunc := func(addr string, firstMsgData []byte) (conn UDPConn, actualAddr string, err error) {
+ // Call the hook
+ err = m.io.Hook(firstMsgData, &addr)
+ if err != nil {
+ return
+ }
+ actualAddr = addr
+ // Log the event
+ m.eventLogger.New(msg.SessionID, addr)
+ // Dial target
+ conn, err = m.io.UDP(addr)
return
}
- entry = &udpSessionEntry{
- ID: msg.SessionID,
- Conn: conn,
- D: &frag.Defragger{},
- Last: utils.NewAtomicTime(time.Now()),
- }
- // Start the receive loop for this session
- go func() {
- err := entry.ReceiveLoop(m.io)
- if !entry.Timeout {
- _ = entry.Conn.Close()
- m.eventLogger.Close(entry.ID, err)
- } else {
- // Connection already closed by timeout cleanup,
- // no need to close again here.
- // Use nil error to indicate timeout.
- m.eventLogger.Close(entry.ID, nil)
- }
+ exitFunc := func(err error) {
+ // Log the event
+ m.eventLogger.Close(entry.ID, err)
+
// Remove the session from the map
m.mutex.Lock()
delete(m.m, entry.ID)
m.mutex.Unlock()
- }()
+ }
+
+ entry = newUDPSessionEntry(msg.SessionID, m.io, dialFunc, exitFunc)
+
// Insert the session into the map
m.mutex.Lock()
m.m[msg.SessionID] = entry
diff --git a/core/server/udp_test.go b/core/server/udp_test.go
index ccfb8cf..8aa899f 100644
--- a/core/server/udp_test.go
+++ b/core/server/udp_test.go
@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/mock"
"go.uber.org/goleak"
- "github.com/apernet/hysteria/core/internal/protocol"
+ "github.com/apernet/hysteria/core/v2/internal/protocol"
)
func TestUDPSessionManager(t *testing.T) {
@@ -49,6 +49,7 @@ func TestUDPSessionManager(t *testing.T) {
eventLogger.EXPECT().New(msg1.SessionID, msg1.Addr).Return().Once()
udpConn1 := newMockUDPConn(t)
udpConn1Ch := make(chan []byte, 1)
+ io.EXPECT().Hook(msg1.Data, &msg1.Addr).Return(nil).Once()
io.EXPECT().UDP(msg1.Addr).Return(udpConn1, nil).Once()
udpConn1.EXPECT().WriteTo(msg1.Data, msg1.Addr).Return(5, nil).Once()
udpConn1.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) {
@@ -65,31 +66,44 @@ func TestUDPSessionManager(t *testing.T) {
msgCh <- msg1
udpConn1Ch <- []byte("hi back")
- msg2 := &protocol.UDPMessage{
+ msg2data := []byte("how are you doing?")
+ msg2_1 := &protocol.UDPMessage{
SessionID: 5678,
PacketID: 0,
FragID: 0,
- FragCount: 1,
+ FragCount: 2,
Addr: "address2.net:12450",
- Data: []byte("how are you"),
+ Data: msg2data[:6],
}
- eventLogger.EXPECT().New(msg2.SessionID, msg2.Addr).Return().Once()
+ msg2_2 := &protocol.UDPMessage{
+ SessionID: 5678,
+ PacketID: 0,
+ FragID: 1,
+ FragCount: 2,
+ Addr: "address2.net:12450",
+ Data: msg2data[6:],
+ }
+
+ eventLogger.EXPECT().New(msg2_1.SessionID, msg2_1.Addr).Return().Once()
udpConn2 := newMockUDPConn(t)
udpConn2Ch := make(chan []byte, 1)
- io.EXPECT().UDP(msg2.Addr).Return(udpConn2, nil).Once()
- udpConn2.EXPECT().WriteTo(msg2.Data, msg2.Addr).Return(11, nil).Once()
+ // On fragmentation, make sure hook gets the whole message
+ io.EXPECT().Hook(msg2data, &msg2_1.Addr).Return(nil).Once()
+ io.EXPECT().UDP(msg2_1.Addr).Return(udpConn2, nil).Once()
+ udpConn2.EXPECT().WriteTo(msg2data, msg2_1.Addr).Return(11, nil).Once()
udpConn2.EXPECT().ReadFrom(mock.Anything).RunAndReturn(func(b []byte) (int, string, error) {
- return udpReadFunc(msg2.Addr, udpConn2Ch, b)
+ return udpReadFunc(msg2_1.Addr, udpConn2Ch, b)
})
io.EXPECT().SendMessage(mock.Anything, &protocol.UDPMessage{
- SessionID: msg2.SessionID,
+ SessionID: msg2_1.SessionID,
PacketID: 0,
FragID: 0,
FragCount: 1,
- Addr: msg2.Addr,
+ Addr: msg2_1.Addr,
Data: []byte("im fine"),
}).Return(nil).Once()
- msgCh <- msg2
+ msgCh <- msg2_1
+ msgCh <- msg2_2
udpConn2Ch <- []byte("im fine")
msg3 := &protocol.UDPMessage{
@@ -122,7 +136,7 @@ func TestUDPSessionManager(t *testing.T) {
return nil
}).Once()
eventLogger.EXPECT().Close(msg1.SessionID, nil).Once()
- eventLogger.EXPECT().Close(msg2.SessionID, nil).Once()
+ eventLogger.EXPECT().Close(msg2_1.SessionID, nil).Once()
time.Sleep(3 * time.Second) // Wait for timeout
mock.AssertExpectationsForObjects(t, io, eventLogger, udpConn1, udpConn2)
@@ -139,6 +153,7 @@ func TestUDPSessionManager(t *testing.T) {
}
eventLogger.EXPECT().New(msg4.SessionID, msg4.Addr).Return().Once()
udpConn4 := newMockUDPConn(t)
+ io.EXPECT().Hook(msg4.Data, &msg4.Addr).Return(nil).Once()
io.EXPECT().UDP(msg4.Addr).Return(udpConn4, nil).Once()
udpConn4.EXPECT().WriteTo(msg4.Data, msg4.Addr).Return(12, nil).Once()
udpConn4.EXPECT().ReadFrom(mock.Anything).Return(0, "", errUDPClosed).Once()
@@ -160,6 +175,7 @@ func TestUDPSessionManager(t *testing.T) {
Data: []byte("babe i miss you"),
}
eventLogger.EXPECT().New(msg5.SessionID, msg5.Addr).Return().Once()
+ io.EXPECT().Hook(msg5.Data, &msg5.Addr).Return(nil).Once()
io.EXPECT().UDP(msg5.Addr).Return(nil, errUDPIO).Once()
eventLogger.EXPECT().Close(msg5.SessionID, errUDPIO).Once()
msgCh <- msg5
diff --git a/extras/LICENSE.md b/extras/LICENSE.md
new file mode 100644
index 0000000..208e8f2
--- /dev/null
+++ b/extras/LICENSE.md
@@ -0,0 +1,7 @@
+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.
diff --git a/extras/auth/command.go b/extras/auth/command.go
index 9c4d193..a981a37 100644
--- a/extras/auth/command.go
+++ b/extras/auth/command.go
@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/server"
)
var _ server.Authenticator = &CommandAuthenticator{}
diff --git a/extras/auth/http.go b/extras/auth/http.go
index 0212b7c..379a8d7 100644
--- a/extras/auth/http.go
+++ b/extras/auth/http.go
@@ -10,7 +10,7 @@ import (
"net/http"
"time"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/server"
)
const (
diff --git a/extras/auth/password.go b/extras/auth/password.go
index 00fd826..c00f399 100644
--- a/extras/auth/password.go
+++ b/extras/auth/password.go
@@ -3,7 +3,7 @@ package auth
import (
"net"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/server"
)
var _ server.Authenticator = &PasswordAuthenticator{}
diff --git a/extras/auth/userpass.go b/extras/auth/userpass.go
index 86f011e..9d11cd9 100644
--- a/extras/auth/userpass.go
+++ b/extras/auth/userpass.go
@@ -4,7 +4,7 @@ import (
"net"
"strings"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/server"
)
const (
@@ -16,7 +16,17 @@ var _ server.Authenticator = &UserPassAuthenticator{}
// UserPassAuthenticator checks the provided auth string against a map of username/password pairs.
// The format of the auth string must be "username:password".
type UserPassAuthenticator struct {
- Users map[string]string
+ users map[string]string
+}
+
+func NewUserPassAuthenticator(users map[string]string) *UserPassAuthenticator {
+ // Usernames are case-insensitive, as they are already lowercased by viper.
+ // Lowercase it again on our own to make it explicit.
+ lcUsers := make(map[string]string, len(users))
+ for user, pass := range users {
+ lcUsers[strings.ToLower(user)] = pass
+ }
+ return &UserPassAuthenticator{users: lcUsers}
}
func (a *UserPassAuthenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) {
@@ -24,7 +34,7 @@ func (a *UserPassAuthenticator) Authenticate(addr net.Addr, auth string, tx uint
if !ok {
return false, ""
}
- rp, ok := a.Users[u]
+ rp, ok := a.users[u]
if !ok || rp != p {
return false, ""
}
@@ -36,5 +46,6 @@ func splitUserPass(auth string) (user, pass string, ok bool) {
if len(rs) != 2 {
return "", "", false
}
- return rs[0], rs[1], true
+ // Usernames are case-insensitive
+ return strings.ToLower(rs[0]), rs[1], true
}
diff --git a/extras/auth/userpass_test.go b/extras/auth/userpass_test.go
index 05f788e..0f1b568 100644
--- a/extras/auth/userpass_test.go
+++ b/extras/auth/userpass_test.go
@@ -85,12 +85,26 @@ func TestUserPassAuthenticator(t *testing.T) {
wantOk: false,
wantId: "",
},
+ {
+ name: "case insensitive username",
+ fields: fields{
+ Users: map[string]string{
+ "gawR": "gura",
+ "fubuki": "shirakami",
+ },
+ },
+ args: args{
+ addr: nil,
+ auth: "Gawr:gura",
+ tx: 0,
+ },
+ wantOk: true,
+ wantId: "gawr",
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- a := &UserPassAuthenticator{
- Users: tt.fields.Users,
- }
+ a := NewUserPassAuthenticator(tt.fields.Users)
gotOk, gotId := a.Authenticate(tt.args.addr, tt.args.auth, tt.args.tx)
if gotOk != tt.wantOk {
t.Errorf("Authenticate() gotOk = %v, want %v", gotOk, tt.wantOk)
diff --git a/extras/go.mod b/extras/go.mod
index bc21044..23b060a 100644
--- a/extras/go.mod
+++ b/extras/go.mod
@@ -1,37 +1,47 @@
-module github.com/apernet/hysteria/extras
+module github.com/apernet/hysteria/extras/v2
-go 1.21
+go 1.23
+
+toolchain go1.24.2
require (
- github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000
+ github.com/apernet/hysteria/core/v2 v2.0.0-00010101000000-000000000000
+ github.com/apernet/quic-go v0.52.1-0.20250607183305-9320c9d14431
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
+ github.com/database64128/tfo-go/v2 v2.2.2
github.com/hashicorp/golang-lru/v2 v2.0.5
- github.com/miekg/dns v1.1.55
- github.com/stretchr/testify v1.8.4
+ github.com/miekg/dns v1.1.59
+ github.com/refraction-networking/utls v1.6.6
+ github.com/stretchr/testify v1.9.0
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
- golang.org/x/crypto v0.19.0
- golang.org/x/net v0.21.0
- google.golang.org/protobuf v1.33.0
+ golang.org/x/crypto v0.26.0
+ golang.org/x/net v0.28.0
+ google.golang.org/protobuf v1.34.1
)
require (
- github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822 // indirect
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/cloudflare/circl v1.3.9 // indirect
+ github.com/database64128/netx-go v0.0.0-20240905055117-62795b8b054a // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/kr/text v0.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/quic-go/qpack v0.4.0 // indirect
- github.com/stretchr/objx v0.5.0 // indirect
+ github.com/quic-go/qpack v0.5.1 // indirect
+ github.com/stretchr/objx v0.5.2 // indirect
github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect
- go.uber.org/mock v0.4.0 // indirect
- golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
- golang.org/x/mod v0.12.0 // indirect
- golang.org/x/sys v0.17.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- golang.org/x/tools v0.11.1 // indirect
+ go.uber.org/mock v0.5.0 // indirect
+ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
+ golang.org/x/mod v0.18.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
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-replace github.com/apernet/hysteria/core => ../core
+replace github.com/apernet/hysteria/core/v2 => ../core
diff --git a/extras/go.sum b/extras/go.sum
index 390cc97..ac6d04a 100644
--- a/extras/go.sum
+++ b/extras/go.sum
@@ -1,10 +1,19 @@
-github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822 h1:+ZSzRxSMg1+fLTQKcIUvD2cCCgS+1rtyRhs+NL5oBgA=
-github.com/apernet/quic-go v0.42.1-0.20240323215309-32a339817822/go.mod h1:j3QaAM7sVJqptDQyIQRWA6mASCfuxoHJszn67JQh1GE=
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/apernet/quic-go v0.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/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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
+github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+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=
@@ -12,22 +21,24 @@ 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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
-github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
-github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
-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/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
+github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
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=
@@ -36,17 +47,18 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
+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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-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/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/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=
@@ -54,29 +66,29 @@ github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
-go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
+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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-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/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.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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+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.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -84,8 +96,8 @@ 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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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/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=
@@ -93,22 +105,20 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-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/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
-golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc=
-golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
+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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.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=
diff --git a/extras/masq/server.go b/extras/masq/server.go
index 3acb177..6cd842d 100644
--- a/extras/masq/server.go
+++ b/extras/masq/server.go
@@ -7,7 +7,7 @@ import (
"net"
"net/http"
- "github.com/apernet/hysteria/extras/correctnet"
+ "github.com/apernet/hysteria/extras/v2/correctnet"
)
// MasqTCPServer covers the TCP parts of a standard web server (TCP based HTTP/HTTPS).
diff --git a/extras/outbounds/.mockery.yaml b/extras/outbounds/.mockery.yaml
index c9e0b71..5f5cc6e 100644
--- a/extras/outbounds/.mockery.yaml
+++ b/extras/outbounds/.mockery.yaml
@@ -2,7 +2,7 @@ with-expecter: true
inpackage: true
dir: .
packages:
- github.com/apernet/hysteria/extras/outbounds:
+ github.com/apernet/hysteria/extras/v2/outbounds:
interfaces:
PluggableOutbound:
config:
diff --git a/extras/outbounds/acl.go b/extras/outbounds/acl.go
index a4dd21c..ecdeaaa 100644
--- a/extras/outbounds/acl.go
+++ b/extras/outbounds/acl.go
@@ -6,7 +6,7 @@ import (
"os"
"strings"
- "github.com/apernet/hysteria/extras/outbounds/acl"
+ "github.com/apernet/hysteria/extras/v2/outbounds/acl"
)
const (
diff --git a/extras/outbounds/acl/compile.go b/extras/outbounds/acl/compile.go
index cdd0a94..caee138 100644
--- a/extras/outbounds/acl/compile.go
+++ b/extras/outbounds/acl/compile.go
@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
- "github.com/apernet/hysteria/extras/outbounds/acl/v2geo"
+ "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
lru "github.com/hashicorp/golang-lru/v2"
)
diff --git a/extras/outbounds/acl/compile_test.go b/extras/outbounds/acl/compile_test.go
index a11c4e8..bf51f68 100644
--- a/extras/outbounds/acl/compile_test.go
+++ b/extras/outbounds/acl/compile_test.go
@@ -4,7 +4,7 @@ import (
"net"
"testing"
- "github.com/apernet/hysteria/extras/outbounds/acl/v2geo"
+ "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
"github.com/stretchr/testify/assert"
)
diff --git a/extras/outbounds/acl/matchers_v2geo.go b/extras/outbounds/acl/matchers_v2geo.go
index df83105..ad79b68 100644
--- a/extras/outbounds/acl/matchers_v2geo.go
+++ b/extras/outbounds/acl/matchers_v2geo.go
@@ -8,7 +8,7 @@ import (
"sort"
"strings"
- "github.com/apernet/hysteria/extras/outbounds/acl/v2geo"
+ "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
)
var _ hostMatcher = (*geoipMatcher)(nil)
diff --git a/extras/outbounds/acl/matchers_v2geo_test.go b/extras/outbounds/acl/matchers_v2geo_test.go
index 4970baf..cc59512 100644
--- a/extras/outbounds/acl/matchers_v2geo_test.go
+++ b/extras/outbounds/acl/matchers_v2geo_test.go
@@ -4,7 +4,7 @@ import (
"net"
"testing"
- "github.com/apernet/hysteria/extras/outbounds/acl/v2geo"
+ "github.com/apernet/hysteria/extras/v2/outbounds/acl/v2geo"
"github.com/stretchr/testify/assert"
)
diff --git a/extras/outbounds/fastopen.go b/extras/outbounds/fastopen.go
new file mode 100644
index 0000000..1d5d1ee
--- /dev/null
+++ b/extras/outbounds/fastopen.go
@@ -0,0 +1,229 @@
+package outbounds
+
+import (
+ "net"
+ "sync"
+ "time"
+
+ "github.com/database64128/tfo-go/v2"
+)
+
+type fastOpenDialer struct {
+ dialer *tfo.Dialer
+}
+
+func newFastOpenDialer(netDialer *net.Dialer) *fastOpenDialer {
+ return &fastOpenDialer{
+ dialer: &tfo.Dialer{
+ Dialer: *netDialer,
+ },
+ }
+}
+
+// Dial returns immediately without actually establishing a connection.
+// The connection will be established by the first Write() call.
+func (d *fastOpenDialer) Dial(network, address string) (net.Conn, error) {
+ return &fastOpenConn{
+ dialer: d.dialer,
+ network: network,
+ address: address,
+ readyChan: make(chan struct{}),
+ }, nil
+}
+
+type fastOpenConn struct {
+ dialer *tfo.Dialer
+ network string
+ address string
+
+ conn net.Conn
+ connLock sync.RWMutex
+ readyChan chan struct{}
+
+ // States before connection ready
+ deadline *time.Time
+ readDeadline *time.Time
+ writeDeadline *time.Time
+}
+
+func (c *fastOpenConn) Read(b []byte) (n int, err error) {
+ c.connLock.RLock()
+ conn := c.conn
+ c.connLock.RUnlock()
+
+ if conn != nil {
+ return conn.Read(b)
+ }
+
+ // Wait until the connection is ready or closed
+ <-c.readyChan
+
+ if c.conn == nil {
+ // This is equivalent to isClosedBeforeReady() == true
+ return 0, net.ErrClosed
+ }
+
+ return c.conn.Read(b)
+}
+
+func (c *fastOpenConn) Write(b []byte) (n int, err error) {
+ c.connLock.RLock()
+ conn := c.conn
+ c.connLock.RUnlock()
+
+ if conn != nil {
+ return conn.Write(b)
+ }
+
+ c.connLock.RLock()
+ closed := c.isClosedBeforeReady()
+ c.connLock.RUnlock()
+
+ if closed {
+ return 0, net.ErrClosed
+ }
+
+ c.connLock.Lock()
+ defer c.connLock.Unlock()
+
+ if c.isClosedBeforeReady() {
+ // Closed by other goroutine
+ return 0, net.ErrClosed
+ }
+
+ conn = c.conn
+ if conn != nil {
+ // Established by other goroutine
+ return conn.Write(b)
+ }
+
+ conn, err = c.dialer.Dial(c.network, c.address, b)
+ if err != nil {
+ close(c.readyChan)
+ return 0, err
+ }
+
+ // Apply pre-set states
+ if c.deadline != nil {
+ _ = conn.SetDeadline(*c.deadline)
+ }
+ if c.readDeadline != nil {
+ _ = conn.SetReadDeadline(*c.readDeadline)
+ }
+ if c.writeDeadline != nil {
+ _ = conn.SetWriteDeadline(*c.writeDeadline)
+ }
+
+ c.conn = conn
+ close(c.readyChan)
+ return len(b), nil
+}
+
+func (c *fastOpenConn) Close() error {
+ c.connLock.RLock()
+ defer c.connLock.RUnlock()
+
+ if c.isClosedBeforeReady() {
+ return net.ErrClosed
+ }
+
+ if c.conn != nil {
+ return c.conn.Close()
+ }
+
+ close(c.readyChan)
+ return nil
+}
+
+// isClosedBeforeReady returns true if the connection is closed before the real connection is established.
+// This function should be called with connLock.RLock().
+func (c *fastOpenConn) isClosedBeforeReady() bool {
+ select {
+ case <-c.readyChan:
+ if c.conn == nil {
+ return true
+ }
+ default:
+ }
+ return false
+}
+
+func (c *fastOpenConn) LocalAddr() net.Addr {
+ c.connLock.RLock()
+ defer c.connLock.RUnlock()
+
+ if c.conn != nil {
+ return c.conn.LocalAddr()
+ }
+
+ return nil
+}
+
+func (c *fastOpenConn) RemoteAddr() net.Addr {
+ c.connLock.RLock()
+ conn := c.conn
+ c.connLock.RUnlock()
+
+ if conn != nil {
+ return conn.RemoteAddr()
+ }
+
+ addr, err := net.ResolveTCPAddr(c.network, c.address)
+ if err != nil {
+ return nil
+ }
+ return addr
+}
+
+func (c *fastOpenConn) SetDeadline(t time.Time) error {
+ c.connLock.RLock()
+ defer c.connLock.RUnlock()
+
+ c.deadline = &t
+
+ if c.conn != nil {
+ return c.conn.SetDeadline(t)
+ }
+
+ if c.isClosedBeforeReady() {
+ return net.ErrClosed
+ }
+
+ return nil
+}
+
+func (c *fastOpenConn) SetReadDeadline(t time.Time) error {
+ c.connLock.RLock()
+ defer c.connLock.RUnlock()
+
+ c.readDeadline = &t
+
+ if c.conn != nil {
+ return c.conn.SetReadDeadline(t)
+ }
+
+ if c.isClosedBeforeReady() {
+ return net.ErrClosed
+ }
+
+ return nil
+}
+
+func (c *fastOpenConn) SetWriteDeadline(t time.Time) error {
+ c.connLock.RLock()
+ defer c.connLock.RUnlock()
+
+ c.writeDeadline = &t
+
+ if c.conn != nil {
+ return c.conn.SetWriteDeadline(t)
+ }
+
+ if c.isClosedBeforeReady() {
+ return net.ErrClosed
+ }
+
+ return nil
+}
+
+var _ net.Conn = (*fastOpenConn)(nil)
diff --git a/extras/outbounds/interface.go b/extras/outbounds/interface.go
index dbe89cb..1870290 100644
--- a/extras/outbounds/interface.go
+++ b/extras/outbounds/interface.go
@@ -4,7 +4,7 @@ import (
"net"
"strconv"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/server"
)
// The PluggableOutbound system is designed to function in a chain-like manner.
diff --git a/extras/outbounds/mock_PluggableOutbound.go b/extras/outbounds/mock_PluggableOutbound.go
index c8620f2..79b38e4 100644
--- a/extras/outbounds/mock_PluggableOutbound.go
+++ b/extras/outbounds/mock_PluggableOutbound.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package outbounds
@@ -25,6 +25,10 @@ func (_m *mockPluggableOutbound) EXPECT() *mockPluggableOutbound_Expecter {
func (_m *mockPluggableOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
ret := _m.Called(reqAddr)
+ if len(ret) == 0 {
+ panic("no return value specified for TCP")
+ }
+
var r0 net.Conn
var r1 error
if rf, ok := ret.Get(0).(func(*AddrEx) (net.Conn, error)); ok {
@@ -79,6 +83,10 @@ func (_c *mockPluggableOutbound_TCP_Call) RunAndReturn(run func(*AddrEx) (net.Co
func (_m *mockPluggableOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
ret := _m.Called(reqAddr)
+ if len(ret) == 0 {
+ panic("no return value specified for UDP")
+ }
+
var r0 UDPConn
var r1 error
if rf, ok := ret.Get(0).(func(*AddrEx) (UDPConn, error)); ok {
diff --git a/extras/outbounds/mock_UDPConn.go b/extras/outbounds/mock_UDPConn.go
index e71e1da..d450322 100644
--- a/extras/outbounds/mock_UDPConn.go
+++ b/extras/outbounds/mock_UDPConn.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.32.0. DO NOT EDIT.
+// Code generated by mockery v2.43.0. DO NOT EDIT.
package outbounds
@@ -21,6 +21,10 @@ 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()
@@ -62,6 +66,10 @@ func (_c *mockUDPConn_Close_Call) RunAndReturn(run func() error) *mockUDPConn_Cl
func (_m *mockUDPConn) ReadFrom(b []byte) (int, *AddrEx, error) {
ret := _m.Called(b)
+ if len(ret) == 0 {
+ panic("no return value specified for ReadFrom")
+ }
+
var r0 int
var r1 *AddrEx
var r2 error
@@ -123,6 +131,10 @@ func (_c *mockUDPConn_ReadFrom_Call) RunAndReturn(run func([]byte) (int, *AddrEx
func (_m *mockUDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) {
ret := _m.Called(b, addr)
+ if len(ret) == 0 {
+ panic("no return value specified for WriteTo")
+ }
+
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte, *AddrEx) (int, error)); ok {
diff --git a/extras/outbounds/ob_direct.go b/extras/outbounds/ob_direct.go
index b80ac00..de7ddd2 100644
--- a/extras/outbounds/ob_direct.go
+++ b/extras/outbounds/ob_direct.go
@@ -35,8 +35,8 @@ type directOutbound struct {
Mode DirectOutboundMode
// Dialer4 and Dialer6 are used for IPv4 and IPv6 TCP connections respectively.
- Dialer4 *net.Dialer
- Dialer6 *net.Dialer
+ DialFunc4 func(network, address string) (net.Conn, error)
+ DialFunc6 func(network, address string) (net.Conn, error)
// DeviceName & BindIPs are for UDP connections. They don't use dialers, so we
// need to bind them when creating the connection.
@@ -45,6 +45,16 @@ type directOutbound struct {
BindIP6 net.IP
}
+type DirectOutboundOptions struct {
+ Mode DirectOutboundMode
+
+ DeviceName string
+ BindIP4 net.IP
+ BindIP6 net.IP
+
+ FastOpen bool
+}
+
type noAddressError struct {
IPv4 bool
IPv6 bool
@@ -84,6 +94,57 @@ func (e resolveError) Unwrap() error {
return e.Err
}
+func NewDirectOutboundWithOptions(opts DirectOutboundOptions) (PluggableOutbound, error) {
+ dialer4 := &net.Dialer{
+ Timeout: defaultDialerTimeout,
+ }
+ if opts.BindIP4 != nil {
+ if opts.BindIP4.To4() == nil {
+ return nil, errors.New("BindIP4 must be an IPv4 address")
+ }
+ dialer4.LocalAddr = &net.TCPAddr{
+ IP: opts.BindIP4,
+ }
+ }
+ dialer6 := &net.Dialer{
+ Timeout: defaultDialerTimeout,
+ }
+ if opts.BindIP6 != nil {
+ if opts.BindIP6.To4() != nil {
+ return nil, errors.New("BindIP6 must be an IPv6 address")
+ }
+ dialer6.LocalAddr = &net.TCPAddr{
+ IP: opts.BindIP6,
+ }
+ }
+ if opts.DeviceName != "" {
+ err := dialerBindToDevice(dialer4, opts.DeviceName)
+ if err != nil {
+ return nil, err
+ }
+ err = dialerBindToDevice(dialer6, opts.DeviceName)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ dialFunc4 := dialer4.Dial
+ dialFunc6 := dialer6.Dial
+ if opts.FastOpen {
+ dialFunc4 = newFastOpenDialer(dialer4).Dial
+ dialFunc6 = newFastOpenDialer(dialer6).Dial
+ }
+
+ return &directOutbound{
+ Mode: opts.Mode,
+ DialFunc4: dialFunc4,
+ DialFunc6: dialFunc6,
+ DeviceName: opts.DeviceName,
+ BindIP4: opts.BindIP4,
+ BindIP6: opts.BindIP6,
+ }, nil
+}
+
// NewDirectOutboundSimple creates a new directOutbound with the given mode,
// without binding to a specific device. Works on all platforms.
func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound {
@@ -91,9 +152,9 @@ func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound {
Timeout: defaultDialerTimeout,
}
return &directOutbound{
- Mode: mode,
- Dialer4: d,
- Dialer6: d,
+ Mode: mode,
+ DialFunc4: d.Dial,
+ DialFunc6: d.Dial,
}
}
@@ -102,34 +163,20 @@ func NewDirectOutboundSimple(mode DirectOutboundMode) PluggableOutbound {
// can be nil, in which case the directOutbound will not bind to a specific address
// for that family.
func NewDirectOutboundBindToIPs(mode DirectOutboundMode, bindIP4, bindIP6 net.IP) (PluggableOutbound, error) {
- if bindIP4 != nil && bindIP4.To4() == nil {
- return nil, errors.New("bindIP4 must be an IPv4 address")
- }
- if bindIP6 != nil && bindIP6.To4() != nil {
- return nil, errors.New("bindIP6 must be an IPv6 address")
- }
- ob := &directOutbound{
- Mode: mode,
- Dialer4: &net.Dialer{
- Timeout: defaultDialerTimeout,
- },
- Dialer6: &net.Dialer{
- Timeout: defaultDialerTimeout,
- },
+ return NewDirectOutboundWithOptions(DirectOutboundOptions{
+ Mode: mode,
BindIP4: bindIP4,
BindIP6: bindIP6,
- }
- if bindIP4 != nil {
- ob.Dialer4.LocalAddr = &net.TCPAddr{
- IP: bindIP4,
- }
- }
- if bindIP6 != nil {
- ob.Dialer6.LocalAddr = &net.TCPAddr{
- IP: bindIP6,
- }
- }
- return ob, nil
+ })
+}
+
+// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode,
+// and binds to the given device. Only works on Linux.
+func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) {
+ return NewDirectOutboundWithOptions(DirectOutboundOptions{
+ Mode: mode,
+ DeviceName: deviceName,
+ })
}
// resolve is our built-in DNS resolver for handling the case when
@@ -201,9 +248,9 @@ func (d *directOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
func (d *directOutbound) dialTCP(ip net.IP, port uint16) (net.Conn, error) {
if ip.To4() != nil {
- return d.Dialer4.Dial("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
+ return d.DialFunc4("tcp4", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
} else {
- return d.Dialer6.Dial("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
+ return d.DialFunc6("tcp6", net.JoinHostPort(ip.String(), strconv.Itoa(int(port))))
}
}
diff --git a/extras/outbounds/ob_direct_linux.go b/extras/outbounds/ob_direct_linux.go
index 33b7d09..5607e50 100644
--- a/extras/outbounds/ob_direct_linux.go
+++ b/extras/outbounds/ob_direct_linux.go
@@ -6,31 +6,31 @@ import (
"syscall"
)
-// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode,
-// and binds to the given device. Only works on Linux.
-func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) {
+func dialerBindToDevice(dialer *net.Dialer, deviceName string) error {
if err := verifyDeviceName(deviceName); err != nil {
- return nil, err
+ return err
}
- d := &net.Dialer{
- Timeout: defaultDialerTimeout,
- Control: func(network, address string, c syscall.RawConn) error {
- var errBind error
- err := c.Control(func(fd uintptr) {
- errBind = syscall.BindToDevice(int(fd), deviceName)
- })
+
+ originControl := dialer.Control
+ dialer.Control = func(network, address string, c syscall.RawConn) error {
+ if originControl != nil {
+ // Chaining other control function
+ err := originControl(network, address, c)
if err != nil {
return err
}
- return errBind
- },
+ }
+
+ var errBind error
+ err := c.Control(func(fd uintptr) {
+ errBind = syscall.BindToDevice(int(fd), deviceName)
+ })
+ if err != nil {
+ return err
+ }
+ return errBind
}
- return &directOutbound{
- Mode: mode,
- Dialer4: d,
- Dialer6: d,
- DeviceName: deviceName,
- }, nil
+ return nil
}
func verifyDeviceName(deviceName string) error {
diff --git a/extras/outbounds/ob_direct_others.go b/extras/outbounds/ob_direct_others.go
index b416c30..eeedc84 100644
--- a/extras/outbounds/ob_direct_others.go
+++ b/extras/outbounds/ob_direct_others.go
@@ -7,11 +7,8 @@ import (
"net"
)
-// NewDirectOutboundBindToDevice creates a new directOutbound with the given mode,
-// and binds to the given device. This doesn't work on non-Linux platforms, so this
-// is just a stub function that always returns an error.
-func NewDirectOutboundBindToDevice(mode DirectOutboundMode, deviceName string) (PluggableOutbound, error) {
- return nil, errors.New("binding to device is not supported on this platform")
+func dialerBindToDevice(dialer *net.Dialer, deviceName string) error {
+ return errors.New("binding to device is not supported on this platform")
}
func udpConnBindToDevice(conn *net.UDPConn, deviceName string) error {
diff --git a/extras/outbounds/speedtest.go b/extras/outbounds/speedtest.go
index b788424..162f4dc 100644
--- a/extras/outbounds/speedtest.go
+++ b/extras/outbounds/speedtest.go
@@ -3,7 +3,7 @@ package outbounds
import (
"net"
- "github.com/apernet/hysteria/extras/outbounds/speedtest"
+ "github.com/apernet/hysteria/extras/v2/outbounds/speedtest"
)
const (
diff --git a/extras/sniff/.mockery.yaml b/extras/sniff/.mockery.yaml
new file mode 100644
index 0000000..c866d1d
--- /dev/null
+++ b/extras/sniff/.mockery.yaml
@@ -0,0 +1,12 @@
+with-expecter: true
+dir: .
+outpkg: sniff
+packages:
+ github.com/apernet/quic-go:
+ interfaces:
+ Stream:
+ config:
+ mockname: mockStream
+ replace-type: # internal package alias dirty fix
+ - github.com/apernet/quic-go/internal/protocol=github.com/apernet/quic-go
+ - github.com/apernet/quic-go/internal/qerr=github.com/apernet/quic-go
diff --git a/extras/sniff/internal/quic/LICENSE b/extras/sniff/internal/quic/LICENSE
new file mode 100644
index 0000000..43970c4
--- /dev/null
+++ b/extras/sniff/internal/quic/LICENSE
@@ -0,0 +1,31 @@
+Author:: Cuong Manh Le
+Copyright:: Copyright (c) 2023, Cuong Manh Le
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the @organization@ nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/extras/sniff/internal/quic/README.md b/extras/sniff/internal/quic/README.md
new file mode 100644
index 0000000..8f3a5e2
--- /dev/null
+++ b/extras/sniff/internal/quic/README.md
@@ -0,0 +1 @@
+The code here is from https://github.com/cuonglm/quicsni with various modifications.
\ No newline at end of file
diff --git a/extras/sniff/internal/quic/header.go b/extras/sniff/internal/quic/header.go
new file mode 100644
index 0000000..c1a5e7c
--- /dev/null
+++ b/extras/sniff/internal/quic/header.go
@@ -0,0 +1,105 @@
+package quic
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "io"
+
+ "github.com/apernet/quic-go/quicvarint"
+)
+
+// The Header represents a QUIC header.
+type Header struct {
+ Type uint8
+ Version uint32
+ SrcConnectionID []byte
+ DestConnectionID []byte
+ Length int64
+ Token []byte
+}
+
+// ParseInitialHeader parses the initial packet of a QUIC connection,
+// return the initial header and number of bytes read so far.
+func ParseInitialHeader(data []byte) (*Header, int64, error) {
+ br := bytes.NewReader(data)
+ hdr, err := parseLongHeader(br)
+ if err != nil {
+ return nil, 0, err
+ }
+ n := int64(len(data) - br.Len())
+ return hdr, n, nil
+}
+
+func parseLongHeader(b *bytes.Reader) (*Header, error) {
+ typeByte, err := b.ReadByte()
+ if err != nil {
+ return nil, err
+ }
+ h := &Header{}
+ ver, err := beUint32(b)
+ if err != nil {
+ return nil, err
+ }
+ h.Version = ver
+ if h.Version != 0 && typeByte&0x40 == 0 {
+ return nil, errors.New("not a QUIC packet")
+ }
+ destConnIDLen, err := b.ReadByte()
+ if err != nil {
+ return nil, err
+ }
+ h.DestConnectionID = make([]byte, int(destConnIDLen))
+ if err := readConnectionID(b, h.DestConnectionID); err != nil {
+ return nil, err
+ }
+ srcConnIDLen, err := b.ReadByte()
+ if err != nil {
+ return nil, err
+ }
+ h.SrcConnectionID = make([]byte, int(srcConnIDLen))
+ if err := readConnectionID(b, h.SrcConnectionID); err != nil {
+ return nil, err
+ }
+
+ initialPacketType := byte(0b00)
+ if h.Version == V2 {
+ initialPacketType = 0b01
+ }
+ if (typeByte >> 4 & 0b11) == initialPacketType {
+ tokenLen, err := quicvarint.Read(b)
+ if err != nil {
+ return nil, err
+ }
+ if tokenLen > uint64(b.Len()) {
+ return nil, io.EOF
+ }
+ h.Token = make([]byte, tokenLen)
+ if _, err := io.ReadFull(b, h.Token); err != nil {
+ return nil, err
+ }
+ }
+
+ pl, err := quicvarint.Read(b)
+ if err != nil {
+ return nil, err
+ }
+ h.Length = int64(pl)
+ return h, err
+}
+
+func readConnectionID(r io.Reader, cid []byte) error {
+ _, err := io.ReadFull(r, cid)
+ if err == io.ErrUnexpectedEOF {
+ return io.EOF
+ }
+ return nil
+}
+
+func beUint32(r io.Reader) (uint32, error) {
+ b := make([]byte, 4)
+ if _, err := io.ReadFull(r, b); err != nil {
+ return 0, err
+ }
+ return binary.BigEndian.Uint32(b), nil
+}
diff --git a/extras/sniff/internal/quic/packet_protector.go b/extras/sniff/internal/quic/packet_protector.go
new file mode 100644
index 0000000..42de841
--- /dev/null
+++ b/extras/sniff/internal/quic/packet_protector.go
@@ -0,0 +1,193 @@
+package quic
+
+import (
+ "crypto"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "hash"
+
+ "golang.org/x/crypto/chacha20"
+ "golang.org/x/crypto/chacha20poly1305"
+ "golang.org/x/crypto/cryptobyte"
+ "golang.org/x/crypto/hkdf"
+)
+
+// NewProtectionKey creates a new ProtectionKey.
+func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
+ return newProtectionKey(suite, secret, v)
+}
+
+// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key
+// is used for encrypt/decrypt Initial Packet only.
+//
+// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets
+func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) {
+ return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v)
+}
+
+// NewPacketProtector creates a new PacketProtector.
+func NewPacketProtector(key *ProtectionKey) *PacketProtector {
+ return &PacketProtector{key: key}
+}
+
+// PacketProtector is used for protecting a QUIC packet.
+//
+// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection
+type PacketProtector struct {
+ key *ProtectionKey
+}
+
+// UnProtect decrypts a QUIC packet.
+func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) {
+ if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 {
+ return nil, errors.New("packet with long header is too small")
+ }
+
+ // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample
+ sampleOffset := pnOffset + 4
+ sample := packet[sampleOffset : sampleOffset+16]
+
+ // https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati
+ mask := pp.key.headerProtection(sample)
+ if isLongHeader(packet[0]) {
+ // Long header: 4 bits masked
+ packet[0] ^= mask[0] & 0x0f
+ } else {
+ // Short header: 5 bits masked
+ packet[0] ^= mask[0] & 0x1f
+ }
+
+ pnLen := packet[0]&0x3 + 1
+ pn := int64(0)
+ for i := uint8(0); i < pnLen; i++ {
+ packet[pnOffset:][i] ^= mask[1+i]
+ pn = (pn << 8) | int64(packet[pnOffset:][i])
+ }
+ pn = decodePacketNumber(pnMax, pn, pnLen)
+ hdr := packet[:pnOffset+int64(pnLen)]
+ payload := packet[pnOffset:][pnLen:]
+ dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr)
+ if err != nil {
+ return nil, fmt.Errorf("decryption failed: %w", err)
+ }
+ return dec, nil
+}
+
+// ProtectionKey is the key used to protect a QUIC packet.
+type ProtectionKey struct {
+ aead cipher.AEAD
+ headerProtection func(sample []byte) (mask []byte)
+ iv []byte
+}
+
+// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage
+//
+// "The 62 bits of the reconstructed QUIC packet number in network byte order are
+// left-padded with zeros to the size of the IV. The exclusive OR of the padded
+// packet number and the IV forms the AEAD nonce."
+func (pk *ProtectionKey) nonce(pn int64) []byte {
+ nonce := make([]byte, len(pk.iv))
+ binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn))
+ for i := range pk.iv {
+ nonce[i] ^= pk.iv[i]
+ }
+ return nonce
+}
+
+func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
+ switch suite {
+ case tls.TLS_AES_128_GCM_SHA256:
+ key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16)
+ c, err := aes.NewCipher(key)
+ if err != nil {
+ panic(err)
+ }
+ aead, err := cipher.NewGCM(c)
+ if err != nil {
+ panic(err)
+ }
+ iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
+ hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16)
+ hp, err := aes.NewCipher(hpKey)
+ if err != nil {
+ panic(err)
+ }
+ k := &ProtectionKey{}
+ k.aead = aead
+ // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection
+ k.headerProtection = func(sample []byte) []byte {
+ mask := make([]byte, hp.BlockSize())
+ hp.Encrypt(mask, sample)
+ return mask
+ }
+ k.iv = iv
+ return k, nil
+ case tls.TLS_CHACHA20_POLY1305_SHA256:
+ key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize)
+ aead, err := chacha20poly1305.New(key)
+ if err != nil {
+ return nil, err
+ }
+ iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
+ hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize)
+ k := &ProtectionKey{}
+ k.aead = aead
+ // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote
+ k.headerProtection = func(sample []byte) []byte {
+ nonce := sample[4:16]
+ c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce)
+ if err != nil {
+ panic(err)
+ }
+ c.SetCounter(binary.LittleEndian.Uint32(sample[:4]))
+ mask := make([]byte, 5)
+ c.XORKeyStream(mask, mask)
+ return mask
+ }
+ k.iv = iv
+ return k, nil
+ }
+ return nil, errors.New("not supported cipher suite")
+}
+
+// decodePacketNumber decode the packet number after header protection removed.
+//
+// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a
+func decodePacketNumber(largest, truncated int64, nbits uint8) int64 {
+ expected := largest + 1
+ win := int64(1 << (nbits * 8))
+ hwin := win / 2
+ mask := win - 1
+ candidate := (expected &^ mask) | truncated
+ switch {
+ case candidate <= expected-hwin && candidate < (1<<62)-win:
+ return candidate + win
+ case candidate > expected+hwin && candidate >= win:
+ return candidate - win
+ }
+ return candidate
+}
+
+// Copied from crypto/tls/key_schedule.go.
+func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte {
+ var hkdfLabel cryptobyte.Builder
+ hkdfLabel.AddUint16(uint16(length))
+ hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+ b.AddBytes([]byte("tls13 "))
+ b.AddBytes([]byte(label))
+ })
+ hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
+ b.AddBytes(context)
+ })
+ out := make([]byte, length)
+ n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out)
+ if err != nil || n != length {
+ panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
+ }
+ return out
+}
diff --git a/extras/sniff/internal/quic/packet_protector_test.go b/extras/sniff/internal/quic/packet_protector_test.go
new file mode 100644
index 0000000..bc355d2
--- /dev/null
+++ b/extras/sniff/internal/quic/packet_protector_test.go
@@ -0,0 +1,94 @@
+package quic
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/tls"
+ "encoding/hex"
+ "strings"
+ "testing"
+ "unicode"
+
+ "golang.org/x/crypto/hkdf"
+)
+
+func TestInitialPacketProtector_UnProtect(t *testing.T) {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial
+ protect := mustHexDecodeString(`
+ c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7
+ 6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498
+ 44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5
+ be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e
+ fd8142fafc0f76
+ `)
+ unProtect := mustHexDecodeString(`
+ 02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739
+ 88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94
+ 0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00
+ 020304
+ `)
+
+ connID := mustHexDecodeString(`8394c8f03e515708`)
+
+ packet := append([]byte{}, protect...)
+ hdr, offset, err := ParseInitialHeader(packet)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version))
+ serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size())
+ key, err := NewInitialProtectionKey(serverSecret, hdr.Version)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pp := NewPacketProtector(key)
+ got, err := pp.UnProtect(protect, offset, 1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(got, unProtect) {
+ t.Error("UnProtect returns wrong result")
+ }
+}
+
+func TestPacketProtectorShortHeader_UnProtect(t *testing.T) {
+ // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea
+ protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`)
+ unProtect := mustHexDecodeString(`01`)
+ hdr := mustHexDecodeString(`4200bff4`)
+
+ secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`)
+ k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pnLen := int(hdr[0]&0x03) + 1
+ offset := len(hdr) - pnLen
+ pp := NewPacketProtector(k)
+ got, err := pp.UnProtect(protect, int64(offset), 654360564)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(got, unProtect) {
+ t.Error("UnProtect returns wrong result")
+ }
+}
+
+func mustHexDecodeString(s string) []byte {
+ b, err := hex.DecodeString(normalizeHex(s))
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
+func normalizeHex(s string) string {
+ return strings.Map(func(c rune) rune {
+ if unicode.IsSpace(c) {
+ return -1
+ }
+ return c
+ }, s)
+}
diff --git a/extras/sniff/internal/quic/payload.go b/extras/sniff/internal/quic/payload.go
new file mode 100644
index 0000000..453b714
--- /dev/null
+++ b/extras/sniff/internal/quic/payload.go
@@ -0,0 +1,122 @@
+package quic
+
+import (
+ "bytes"
+ "crypto"
+ "errors"
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/apernet/quic-go/quicvarint"
+ "golang.org/x/crypto/hkdf"
+)
+
+func ReadCryptoPayload(packet []byte) ([]byte, error) {
+ hdr, offset, err := ParseInitialHeader(packet)
+ if err != nil {
+ return nil, err
+ }
+ // Some sanity checks
+ if hdr.Version != V1 && hdr.Version != V2 {
+ return nil, fmt.Errorf("unsupported version: %x", hdr.Version)
+ }
+ if offset == 0 || hdr.Length == 0 {
+ return nil, errors.New("invalid packet")
+ }
+
+ initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version))
+ clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size())
+ key, err := NewInitialProtectionKey(clientSecret, hdr.Version)
+ if err != nil {
+ return nil, fmt.Errorf("NewInitialProtectionKey: %w", err)
+ }
+ pp := NewPacketProtector(key)
+ // https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial
+ //
+ // "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2"
+ if int64(len(packet)) < offset+hdr.Length {
+ return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length)
+ }
+ unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2)
+ if err != nil {
+ return nil, err
+ }
+ frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload))
+ if err != nil {
+ return nil, err
+ }
+ data := assembleCryptoFrames(frs)
+ if data == nil {
+ return nil, errors.New("unable to assemble crypto frames")
+ }
+ return data, nil
+}
+
+const (
+ paddingFrameType = 0x00
+ pingFrameType = 0x01
+ cryptoFrameType = 0x06
+)
+
+type cryptoFrame struct {
+ Offset int64
+ Data []byte
+}
+
+func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {
+ var frames []cryptoFrame
+ for r.Len() > 0 {
+ typ, err := quicvarint.Read(r)
+ if err != nil {
+ return nil, err
+ }
+ if typ == paddingFrameType || typ == pingFrameType {
+ continue
+ }
+ if typ != cryptoFrameType {
+ return nil, fmt.Errorf("encountered unexpected frame type: %d", typ)
+ }
+ var frame cryptoFrame
+ offset, err := quicvarint.Read(r)
+ if err != nil {
+ return nil, err
+ }
+ frame.Offset = int64(offset)
+ dataLen, err := quicvarint.Read(r)
+ if err != nil {
+ return nil, err
+ }
+ frame.Data = make([]byte, dataLen)
+ if _, err := io.ReadFull(r, frame.Data); err != nil {
+ return nil, err
+ }
+ frames = append(frames, frame)
+ }
+ return frames, nil
+}
+
+// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).
+// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.
+func assembleCryptoFrames(frames []cryptoFrame) []byte {
+ if len(frames) == 0 {
+ return nil
+ }
+ if len(frames) == 1 {
+ return frames[0].Data
+ }
+ // sort the frames by offset
+ sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })
+ // check if the frames are contiguous
+ for i := 1; i < len(frames); i++ {
+ if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) {
+ return nil
+ }
+ }
+ // concatenate the frames
+ data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data)))
+ for _, frame := range frames {
+ copy(data[frame.Offset:], frame.Data)
+ }
+ return data
+}
diff --git a/extras/sniff/internal/quic/quic.go b/extras/sniff/internal/quic/quic.go
new file mode 100644
index 0000000..1cfa103
--- /dev/null
+++ b/extras/sniff/internal/quic/quic.go
@@ -0,0 +1,59 @@
+package quic
+
+const (
+ V1 uint32 = 0x1
+ V2 uint32 = 0x6b3343cf
+
+ hkdfLabelKeyV1 = "quic key"
+ hkdfLabelKeyV2 = "quicv2 key"
+ hkdfLabelIVV1 = "quic iv"
+ hkdfLabelIVV2 = "quicv2 iv"
+ hkdfLabelHPV1 = "quic hp"
+ hkdfLabelHPV2 = "quicv2 hp"
+)
+
+var (
+ quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
+ // https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets
+ quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
+ // https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2
+ quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}
+)
+
+// isLongHeader reports whether b is the first byte of a long header packet.
+func isLongHeader(b byte) bool {
+ return b&0x80 > 0
+}
+
+func getSalt(v uint32) []byte {
+ switch v {
+ case V1:
+ return quicSaltV1
+ case V2:
+ return quicSaltV2
+ }
+ return quicSaltOld
+}
+
+func keyLabel(v uint32) string {
+ kl := hkdfLabelKeyV1
+ if v == V2 {
+ kl = hkdfLabelKeyV2
+ }
+ return kl
+}
+
+func ivLabel(v uint32) string {
+ ivl := hkdfLabelIVV1
+ if v == V2 {
+ ivl = hkdfLabelIVV2
+ }
+ return ivl
+}
+
+func headerProtectionLabel(v uint32) string {
+ if v == V2 {
+ return hkdfLabelHPV2
+ }
+ return hkdfLabelHPV1
+}
diff --git a/extras/sniff/mock_Stream.go b/extras/sniff/mock_Stream.go
new file mode 100644
index 0000000..8b21e95
--- /dev/null
+++ b/extras/sniff/mock_Stream.go
@@ -0,0 +1,492 @@
+// Code generated by mockery v2.43.0. DO NOT EDIT.
+
+package sniff
+
+import (
+ context "context"
+
+ qerr "github.com/apernet/quic-go"
+ mock "github.com/stretchr/testify/mock"
+
+ time "time"
+)
+
+// mockStream is an autogenerated mock type for the Stream type
+type mockStream struct {
+ mock.Mock
+}
+
+type mockStream_Expecter struct {
+ mock *mock.Mock
+}
+
+func (_m *mockStream) EXPECT() *mockStream_Expecter {
+ return &mockStream_Expecter{mock: &_m.Mock}
+}
+
+// CancelRead provides a mock function with given fields: _a0
+func (_m *mockStream) CancelRead(_a0 qerr.StreamErrorCode) {
+ _m.Called(_a0)
+}
+
+// mockStream_CancelRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelRead'
+type mockStream_CancelRead_Call struct {
+ *mock.Call
+}
+
+// CancelRead is a helper method to define mock.On call
+// - _a0 qerr.StreamErrorCode
+func (_e *mockStream_Expecter) CancelRead(_a0 interface{}) *mockStream_CancelRead_Call {
+ return &mockStream_CancelRead_Call{Call: _e.mock.On("CancelRead", _a0)}
+}
+
+func (_c *mockStream_CancelRead_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelRead_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(qerr.StreamErrorCode))
+ })
+ return _c
+}
+
+func (_c *mockStream_CancelRead_Call) Return() *mockStream_CancelRead_Call {
+ _c.Call.Return()
+ return _c
+}
+
+func (_c *mockStream_CancelRead_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelRead_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// CancelWrite provides a mock function with given fields: _a0
+func (_m *mockStream) CancelWrite(_a0 qerr.StreamErrorCode) {
+ _m.Called(_a0)
+}
+
+// mockStream_CancelWrite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelWrite'
+type mockStream_CancelWrite_Call struct {
+ *mock.Call
+}
+
+// CancelWrite is a helper method to define mock.On call
+// - _a0 qerr.StreamErrorCode
+func (_e *mockStream_Expecter) CancelWrite(_a0 interface{}) *mockStream_CancelWrite_Call {
+ return &mockStream_CancelWrite_Call{Call: _e.mock.On("CancelWrite", _a0)}
+}
+
+func (_c *mockStream_CancelWrite_Call) Run(run func(_a0 qerr.StreamErrorCode)) *mockStream_CancelWrite_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(qerr.StreamErrorCode))
+ })
+ return _c
+}
+
+func (_c *mockStream_CancelWrite_Call) Return() *mockStream_CancelWrite_Call {
+ _c.Call.Return()
+ return _c
+}
+
+func (_c *mockStream_CancelWrite_Call) RunAndReturn(run func(qerr.StreamErrorCode)) *mockStream_CancelWrite_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// Close provides a mock function with given fields:
+func (_m *mockStream) Close() error {
+ ret := _m.Called()
+
+ if len(ret) == 0 {
+ panic("no return value specified for Close")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func() error); ok {
+ r0 = rf()
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// mockStream_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
+type mockStream_Close_Call struct {
+ *mock.Call
+}
+
+// Close is a helper method to define mock.On call
+func (_e *mockStream_Expecter) Close() *mockStream_Close_Call {
+ return &mockStream_Close_Call{Call: _e.mock.On("Close")}
+}
+
+func (_c *mockStream_Close_Call) Run(run func()) *mockStream_Close_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run()
+ })
+ return _c
+}
+
+func (_c *mockStream_Close_Call) Return(_a0 error) *mockStream_Close_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *mockStream_Close_Call) RunAndReturn(run func() error) *mockStream_Close_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// Context provides a mock function with given fields:
+func (_m *mockStream) Context() context.Context {
+ ret := _m.Called()
+
+ if len(ret) == 0 {
+ panic("no return value specified for Context")
+ }
+
+ var r0 context.Context
+ if rf, ok := ret.Get(0).(func() context.Context); ok {
+ r0 = rf()
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(context.Context)
+ }
+ }
+
+ return r0
+}
+
+// mockStream_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context'
+type mockStream_Context_Call struct {
+ *mock.Call
+}
+
+// Context is a helper method to define mock.On call
+func (_e *mockStream_Expecter) Context() *mockStream_Context_Call {
+ return &mockStream_Context_Call{Call: _e.mock.On("Context")}
+}
+
+func (_c *mockStream_Context_Call) Run(run func()) *mockStream_Context_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run()
+ })
+ return _c
+}
+
+func (_c *mockStream_Context_Call) Return(_a0 context.Context) *mockStream_Context_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *mockStream_Context_Call) RunAndReturn(run func() context.Context) *mockStream_Context_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// Read provides a mock function with given fields: p
+func (_m *mockStream) Read(p []byte) (int, error) {
+ ret := _m.Called(p)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Read")
+ }
+
+ var r0 int
+ var r1 error
+ if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
+ return rf(p)
+ }
+ if rf, ok := ret.Get(0).(func([]byte) int); ok {
+ r0 = rf(p)
+ } else {
+ r0 = ret.Get(0).(int)
+ }
+
+ if rf, ok := ret.Get(1).(func([]byte) error); ok {
+ r1 = rf(p)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// mockStream_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
+type mockStream_Read_Call struct {
+ *mock.Call
+}
+
+// Read is a helper method to define mock.On call
+// - p []byte
+func (_e *mockStream_Expecter) Read(p interface{}) *mockStream_Read_Call {
+ return &mockStream_Read_Call{Call: _e.mock.On("Read", p)}
+}
+
+func (_c *mockStream_Read_Call) Run(run func(p []byte)) *mockStream_Read_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].([]byte))
+ })
+ return _c
+}
+
+func (_c *mockStream_Read_Call) Return(n int, err error) *mockStream_Read_Call {
+ _c.Call.Return(n, err)
+ return _c
+}
+
+func (_c *mockStream_Read_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Read_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// SetDeadline provides a mock function with given fields: t
+func (_m *mockStream) SetDeadline(t time.Time) error {
+ ret := _m.Called(t)
+
+ if len(ret) == 0 {
+ panic("no return value specified for SetDeadline")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(time.Time) error); ok {
+ r0 = rf(t)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// mockStream_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'
+type mockStream_SetDeadline_Call struct {
+ *mock.Call
+}
+
+// SetDeadline is a helper method to define mock.On call
+// - t time.Time
+func (_e *mockStream_Expecter) SetDeadline(t interface{}) *mockStream_SetDeadline_Call {
+ return &mockStream_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)}
+}
+
+func (_c *mockStream_SetDeadline_Call) Run(run func(t time.Time)) *mockStream_SetDeadline_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(time.Time))
+ })
+ return _c
+}
+
+func (_c *mockStream_SetDeadline_Call) Return(_a0 error) *mockStream_SetDeadline_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *mockStream_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetDeadline_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// SetReadDeadline provides a mock function with given fields: t
+func (_m *mockStream) SetReadDeadline(t time.Time) error {
+ ret := _m.Called(t)
+
+ if len(ret) == 0 {
+ panic("no return value specified for SetReadDeadline")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(time.Time) error); ok {
+ r0 = rf(t)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// mockStream_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'
+type mockStream_SetReadDeadline_Call struct {
+ *mock.Call
+}
+
+// SetReadDeadline is a helper method to define mock.On call
+// - t time.Time
+func (_e *mockStream_Expecter) SetReadDeadline(t interface{}) *mockStream_SetReadDeadline_Call {
+ return &mockStream_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)}
+}
+
+func (_c *mockStream_SetReadDeadline_Call) Run(run func(t time.Time)) *mockStream_SetReadDeadline_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(time.Time))
+ })
+ return _c
+}
+
+func (_c *mockStream_SetReadDeadline_Call) Return(_a0 error) *mockStream_SetReadDeadline_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *mockStream_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetReadDeadline_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// SetWriteDeadline provides a mock function with given fields: t
+func (_m *mockStream) SetWriteDeadline(t time.Time) error {
+ ret := _m.Called(t)
+
+ if len(ret) == 0 {
+ panic("no return value specified for SetWriteDeadline")
+ }
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(time.Time) error); ok {
+ r0 = rf(t)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// mockStream_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'
+type mockStream_SetWriteDeadline_Call struct {
+ *mock.Call
+}
+
+// SetWriteDeadline is a helper method to define mock.On call
+// - t time.Time
+func (_e *mockStream_Expecter) SetWriteDeadline(t interface{}) *mockStream_SetWriteDeadline_Call {
+ return &mockStream_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)}
+}
+
+func (_c *mockStream_SetWriteDeadline_Call) Run(run func(t time.Time)) *mockStream_SetWriteDeadline_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].(time.Time))
+ })
+ return _c
+}
+
+func (_c *mockStream_SetWriteDeadline_Call) Return(_a0 error) *mockStream_SetWriteDeadline_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *mockStream_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *mockStream_SetWriteDeadline_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// StreamID provides a mock function with given fields:
+func (_m *mockStream) StreamID() qerr.StreamID {
+ ret := _m.Called()
+
+ if len(ret) == 0 {
+ panic("no return value specified for StreamID")
+ }
+
+ var r0 qerr.StreamID
+ if rf, ok := ret.Get(0).(func() qerr.StreamID); ok {
+ r0 = rf()
+ } else {
+ r0 = ret.Get(0).(qerr.StreamID)
+ }
+
+ return r0
+}
+
+// mockStream_StreamID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StreamID'
+type mockStream_StreamID_Call struct {
+ *mock.Call
+}
+
+// StreamID is a helper method to define mock.On call
+func (_e *mockStream_Expecter) StreamID() *mockStream_StreamID_Call {
+ return &mockStream_StreamID_Call{Call: _e.mock.On("StreamID")}
+}
+
+func (_c *mockStream_StreamID_Call) Run(run func()) *mockStream_StreamID_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run()
+ })
+ return _c
+}
+
+func (_c *mockStream_StreamID_Call) Return(_a0 qerr.StreamID) *mockStream_StreamID_Call {
+ _c.Call.Return(_a0)
+ return _c
+}
+
+func (_c *mockStream_StreamID_Call) RunAndReturn(run func() qerr.StreamID) *mockStream_StreamID_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// Write provides a mock function with given fields: p
+func (_m *mockStream) Write(p []byte) (int, error) {
+ ret := _m.Called(p)
+
+ if len(ret) == 0 {
+ panic("no return value specified for Write")
+ }
+
+ var r0 int
+ var r1 error
+ if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
+ return rf(p)
+ }
+ if rf, ok := ret.Get(0).(func([]byte) int); ok {
+ r0 = rf(p)
+ } else {
+ r0 = ret.Get(0).(int)
+ }
+
+ if rf, ok := ret.Get(1).(func([]byte) error); ok {
+ r1 = rf(p)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
+// mockStream_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
+type mockStream_Write_Call struct {
+ *mock.Call
+}
+
+// Write is a helper method to define mock.On call
+// - p []byte
+func (_e *mockStream_Expecter) Write(p interface{}) *mockStream_Write_Call {
+ return &mockStream_Write_Call{Call: _e.mock.On("Write", p)}
+}
+
+func (_c *mockStream_Write_Call) Run(run func(p []byte)) *mockStream_Write_Call {
+ _c.Call.Run(func(args mock.Arguments) {
+ run(args[0].([]byte))
+ })
+ return _c
+}
+
+func (_c *mockStream_Write_Call) Return(n int, err error) *mockStream_Write_Call {
+ _c.Call.Return(n, err)
+ return _c
+}
+
+func (_c *mockStream_Write_Call) RunAndReturn(run func([]byte) (int, error)) *mockStream_Write_Call {
+ _c.Call.Return(run)
+ return _c
+}
+
+// newMockStream creates a new instance of mockStream. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func newMockStream(t interface {
+ mock.TestingT
+ Cleanup(func())
+}) *mockStream {
+ mock := &mockStream{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}
diff --git a/extras/sniff/sniff.go b/extras/sniff/sniff.go
new file mode 100644
index 0000000..9994b8a
--- /dev/null
+++ b/extras/sniff/sniff.go
@@ -0,0 +1,199 @@
+package sniff
+
+import (
+ "bufio"
+ "io"
+ "net"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/apernet/quic-go"
+ utls "github.com/refraction-networking/utls"
+
+ "github.com/apernet/hysteria/core/v2/server"
+ quicInternal "github.com/apernet/hysteria/extras/v2/sniff/internal/quic"
+ "github.com/apernet/hysteria/extras/v2/utils"
+)
+
+const (
+ sniffDefaultTimeout = 4 * time.Second
+)
+
+var _ server.RequestHook = (*Sniffer)(nil)
+
+// Sniffer is a server core RequestHook that performs packet inspection and possibly
+// rewrites the request address based on what's in the protocol header.
+// This is mainly for inbounds that inherently cannot get domain information (e.g. TUN),
+// in which case sniffing can restore the domains and apply ACLs correctly.
+// Currently supports HTTP, HTTPS (TLS) and QUIC.
+type Sniffer struct {
+ Timeout time.Duration
+ RewriteDomain bool // Whether to rewrite the address even when it's already a domain
+ TCPPorts utils.PortUnion
+ UDPPorts utils.PortUnion
+}
+
+func (h *Sniffer) isDomain(addr string) bool {
+ host, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ return false
+ }
+ return net.ParseIP(host) == nil
+}
+
+func (h *Sniffer) isHTTP(buf []byte) bool {
+ if len(buf) < 3 {
+ return false
+ }
+ // First 3 bytes should be English letters (whatever HTTP method)
+ for _, b := range buf[:3] {
+ if (b < 'A' || b > 'Z') && (b < 'a' || b > 'z') {
+ return false
+ }
+ }
+ return true
+}
+
+func (h *Sniffer) isTLS(buf []byte) bool {
+ if len(buf) < 3 {
+ return false
+ }
+ return buf[0] >= 0x16 && buf[0] <= 0x17 &&
+ buf[1] == 0x03 && buf[2] <= 0x09
+}
+
+func (h *Sniffer) Check(isUDP bool, reqAddr string) bool {
+ // @ means it's internal (e.g. speed test)
+ if strings.HasPrefix(reqAddr, "@") {
+ return false
+ }
+ host, port, err := net.SplitHostPort(reqAddr)
+ if err != nil {
+ return false
+ }
+ if !h.RewriteDomain && net.ParseIP(host) == nil {
+ // Is a domain and domain rewriting is disabled
+ return false
+ }
+ portNum, err := strconv.Atoi(port)
+ if err != nil {
+ return false
+ }
+ if isUDP {
+ return h.UDPPorts == nil || h.UDPPorts.Contains(uint16(portNum))
+ } else {
+ return h.TCPPorts == nil || h.TCPPorts.Contains(uint16(portNum))
+ }
+}
+
+func (h *Sniffer) TCP(stream quic.Stream, reqAddr *string) ([]byte, error) {
+ var err error
+ if h.Timeout == 0 {
+ err = stream.SetReadDeadline(time.Now().Add(sniffDefaultTimeout))
+ } else {
+ err = stream.SetReadDeadline(time.Now().Add(h.Timeout))
+ }
+ if err != nil {
+ return nil, err
+ }
+ // Make sure to reset the deadline after sniffing
+ defer stream.SetReadDeadline(time.Time{})
+ // Read 3 bytes to determine the protocol
+ pre := make([]byte, 3)
+ n, err := io.ReadFull(stream, pre)
+ if err != nil {
+ // Not enough within the timeout, just return what we have
+ return pre[:n], nil
+ }
+ if h.isHTTP(pre) {
+ // HTTP
+ tr := &teeReader{Stream: stream, Pre: pre}
+ req, _ := http.ReadRequest(bufio.NewReader(tr))
+ if req != nil && req.Host != "" {
+ // req.Host can be host:port, in which case we need to extract the host part
+ host, _, err := net.SplitHostPort(req.Host)
+ if err != nil {
+ // No port, just use the whole string
+ host = req.Host
+ }
+ _, port, err := net.SplitHostPort(*reqAddr)
+ if err != nil {
+ return nil, err
+ }
+ *reqAddr = net.JoinHostPort(host, port)
+ }
+ return tr.Buffer(), nil
+ } else if h.isTLS(pre) {
+ // TLS
+ // Need to read 2 more bytes (content length)
+ pre = append(pre, make([]byte, 2)...)
+ n, err = io.ReadFull(stream, pre[3:])
+ if err != nil {
+ // Not enough within the timeout, just return what we have
+ return pre[:3+n], nil
+ }
+ contentLength := int(pre[3])<<8 | int(pre[4])
+ pre = append(pre, make([]byte, contentLength)...)
+ n, err = io.ReadFull(stream, pre[5:])
+ if err != nil {
+ // Not enough within the timeout, just return what we have
+ return pre[:5+n], nil
+ }
+ clientHello := utls.UnmarshalClientHello(pre[5:])
+ if clientHello != nil && clientHello.ServerName != "" {
+ _, port, err := net.SplitHostPort(*reqAddr)
+ if err != nil {
+ return nil, err
+ }
+ *reqAddr = net.JoinHostPort(clientHello.ServerName, port)
+ }
+ return pre, nil
+ } else {
+ // Unrecognized protocol, just return what we have
+ return pre, nil
+ }
+}
+
+func (h *Sniffer) UDP(data []byte, reqAddr *string) error {
+ pl, err := quicInternal.ReadCryptoPayload(data)
+ if err != nil || len(pl) < 4 || pl[0] != 0x01 {
+ // Unrecognized protocol, incomplete payload or not a client hello
+ return nil
+ }
+ clientHello := utls.UnmarshalClientHello(pl)
+ if clientHello != nil && clientHello.ServerName != "" {
+ _, port, err := net.SplitHostPort(*reqAddr)
+ if err != nil {
+ return err
+ }
+ *reqAddr = net.JoinHostPort(clientHello.ServerName, port)
+ }
+ return nil
+}
+
+type teeReader struct {
+ Stream quic.Stream
+ Pre []byte
+
+ buf []byte
+}
+
+func (c *teeReader) Read(b []byte) (n int, err error) {
+ if len(c.Pre) > 0 {
+ n = copy(b, c.Pre)
+ c.Pre = c.Pre[n:]
+ c.buf = append(c.buf, b[:n]...)
+ return n, nil
+ }
+ n, err = c.Stream.Read(b)
+ if n > 0 {
+ c.buf = append(c.buf, b[:n]...)
+ }
+ return n, err
+}
+
+func (c *teeReader) Buffer() []byte {
+ return append(c.Pre, c.buf...)
+}
diff --git a/extras/sniff/sniff_test.go b/extras/sniff/sniff_test.go
new file mode 100644
index 0000000..445660b
--- /dev/null
+++ b/extras/sniff/sniff_test.go
@@ -0,0 +1,147 @@
+package sniff
+
+import (
+ "encoding/base64"
+ "io"
+ "testing"
+ "time"
+
+ "github.com/apernet/hysteria/extras/v2/utils"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func TestSnifferCheck(t *testing.T) {
+ sniffer := &Sniffer{
+ Timeout: 1 * time.Second,
+ RewriteDomain: false,
+ TCPPorts: nil, // nil = all
+ UDPPorts: nil, // nil = all
+ }
+
+ assert.True(t, sniffer.Check(false, "1.1.1.1:80"))
+ assert.False(t, sniffer.Check(false, "example.com:443"))
+
+ sniffer.RewriteDomain = true
+ assert.True(t, sniffer.Check(false, "example.com:443"))
+
+ sniffer.TCPPorts = []utils.PortRange{{80, 80}}
+ assert.True(t, sniffer.Check(false, "google.com:80"))
+ assert.False(t, sniffer.Check(false, "google.com:443"))
+
+ sniffer.UDPPorts = []utils.PortRange{{443, 443}}
+ assert.True(t, sniffer.Check(true, "google.com:443"))
+ assert.False(t, sniffer.Check(true, "google.com:80"))
+}
+
+func TestSnifferTCP(t *testing.T) {
+ sniffer := &Sniffer{
+ Timeout: 1 * time.Second,
+ RewriteDomain: false,
+ }
+
+ buf := &[]byte{}
+
+ // Test HTTP
+ *buf = []byte("POST /hello HTTP/1.1\r\n" +
+ "Host: example.com\r\n" +
+ "User-Agent: mamamiya\r\n" +
+ "Content-Length: 27\r\n" +
+ "Connection: keep-alive\r\n\r\n" +
+ "param1=value1¶m2=value2")
+ index := 0
+ stream := &mockStream{}
+ stream.EXPECT().SetReadDeadline(mock.Anything).Return(nil)
+ stream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
+ if index < len(*buf) {
+ n := copy(bs, (*buf)[index:])
+ index += n
+ return n, nil
+ } else {
+ return 0, io.EOF
+ }
+ })
+
+ // Rewrite IP to domain
+ reqAddr := "111.111.111.111:80"
+ putback, err := sniffer.TCP(stream, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, *buf, putback)
+ assert.Equal(t, "example.com:80", reqAddr)
+
+ // Test HTTP with Host as host:port
+ *buf = []byte("GET / HTTP/1.1\r\n" +
+ "Host: example.com:8080\r\n" +
+ "User-Agent: test-agent\r\n" +
+ "Accept: */*\r\n\r\n")
+ index = 0
+ reqAddr = "222.222.222.222:10086"
+ putback, err = sniffer.TCP(stream, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, *buf, putback)
+ assert.Equal(t, "example.com:10086", reqAddr)
+
+ // Test TLS
+ *buf, err = base64.StdEncoding.DecodeString("FgMBARcBAAETAwPJL2jlt1OAo+Rslkjv/aqKiTthKMaCKg2Gvd+uALDbDCDdY+UIk8ouadEB9fC3j52Y1i7SJZqGIgBRIS6kKieYrAAoEwITAcAswCvAMMAvwCTAI8AowCfACsAJwBTAEwCdAJwAPQA8ADUALwEAAKIAAAAOAAwAAAlpcGluZm8uaW8ABQAFAQAAAAAAKwAJCAMEAwMDAgMBAA0AGgAYCAQIBQgGBAEFAQIBBAMFAwIDAgIGAQYDACMAAAAKAAgABgAdABcAGAAQAAsACQhodHRwLzEuMQAzACYAJAAdACBguQbqNJNyamYxYcrBFpBP7pWv5TgZsP9gwGtMYNKVBQAxAAAAFwAA/wEAAQAALQACAQE=")
+ assert.NoError(t, err)
+ index = 0
+ reqAddr = "222.222.222.222:443"
+ putback, err = sniffer.TCP(stream, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, *buf, putback)
+ assert.Equal(t, "ipinfo.io:443", reqAddr)
+
+ // Test unrecognized 1
+ *buf = []byte("Wait It's All Ohio? Always Has Been.")
+ index = 0
+ reqAddr = "123.123.123.123:123"
+ putback, err = sniffer.TCP(stream, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, *buf, putback)
+ assert.Equal(t, "123.123.123.123:123", reqAddr)
+
+ // Test unrecognized 2
+ *buf = []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a")
+ index = 0
+ reqAddr = "45.45.45.45:45"
+ putback, err = sniffer.TCP(stream, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("\x01\x02\x03"), putback)
+ assert.Equal(t, "45.45.45.45:45", reqAddr)
+
+ // Test timeout
+ blockStream := &mockStream{}
+ blockStream.EXPECT().SetReadDeadline(mock.Anything).Return(nil)
+ blockStream.EXPECT().Read(mock.Anything).RunAndReturn(func(bs []byte) (int, error) {
+ time.Sleep(2 * time.Second)
+ return 0, io.EOF
+ })
+ reqAddr = "66.66.66.66:66"
+ putback, err = sniffer.TCP(blockStream, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte{}, putback)
+ assert.Equal(t, "66.66.66.66:66", reqAddr)
+}
+
+func TestSnifferUDP(t *testing.T) {
+ sniffer := &Sniffer{
+ Timeout: 1 * time.Second,
+ RewriteDomain: false,
+ }
+
+ // Test QUIC
+ reqAddr := "2.3.4.5:443"
+ pkt, err := base64.StdEncoding.DecodeString("ygAAAAEIwugWgPS7ulYAAES8hY891uwgGE9GG4CPOLd+nsDe28raso24lCSFmlFwYQG1uF39ikbL13/R9ZTghYmTl+jEbr6F9TxxRiOgpTmKRmh6aKZiIiVfy5pVRckovaI8lq0WRoW9xoFNTyYtQP8TVJ3bLCK+zUqpquEQSyWf7CE43ywayyMpE9UlIoPXFWCoopXLM1SvzdQ+17P51N9KR7m4emti4DWWTBLMQOvrwd2HEEkbiZdRO1wf6ZXJlIat5dN0R/6uod60OFPO+u+awvq67MoMReC7+5I/xWI+xx6o4JpnZNn6YPG8Gqi8hS6doNcAAdtD8h5eMLuHCCgkpX3QVjjfWtcOhtw9xKjU43HhUPwzUTv+JDLgwuTQCTmlfYlb3B+pk4b2I9si0tJ0SBuYaZ2VQPtZbj2hpGXw3gn11pbN8xsbKkQL50+Scd4dGJxWQlGaJHeaU5WOCkxLXc635z8m5XO/CBHVYPGp4pfwfwNUgbe5WF+3MaUIlDB8dMfsnrO0BmZPo379jVx0SFLTAiS8wAdHib1WNEY8qKYnTWuiyxYg1GZEhJt0nXmI+8f0eJq42DgHBWC+Rf5rRBr/Sf25o3mFAmTUaul0Woo9/CIrpT73B63N91xd9A77i4ru995YG8l9Hen+eLtpDU9Q9376nwMDYBzeYG9U/Rn0Urbm6q4hmAgV/xlNJ2rAyDS+yLnwqD6I0PRy8bZJEttcidb/SkOyrpgMiAzWeT+SO+c/k+Y8H0UTRa05faZUrhuUaym9wAcaIVRA6nFI+fejfjVp+7afFv+kWn3vCqQEij+CRHuxkltrixZMD2rfYj6NUW7TTYBtPRtuV/V0ZIDjRR26vr4K+0D84+l3c0mA/l6nmpP5kkco3nmpdjtQN6sGXL7+5o0nnsftX5d6/n5mLyEpP+AEDl1zk3iqkS62RsITwql6DMMoGbSDdUpMclCIeM0vlo3CkxGMO7QA9ruVeNddkL3EWMivl+uxO43sXEEqYQHVl4N75y63t05GOf7/gm9Kb/BJ8MpG9ViEkVYaskQCzi3D8bVpzo8FfTj8te8B6c3ikc/cm7r8k0ZcZpr+YiLGDYq+0ilHxpqJfmq8dPkSvxdzLcUSvy7+LMQ/TTobRSF7L4JhtDKck0+00vl9H35Tkh9N+MsVtpKdWyoqZ4XaK2Nx1M6AieczXpdFc0y7lYPoUfF4IeW8WzeVUclol5ElYjkyFz/lDOGAe1bF2g5AYaGWCPiGleVZknNdD5ihB8W8Mfkt1pEwq2S97AHrppqkf/VoIfZzeqH8wUFw8fDDrZIpnoa0rW7HfwIQaqJhPCyB9Z6TVbV4x9UWmaHfVAcinCK/7o10dtaj3rvEqcUC/iPceGq3Tqv/p9GGNJ+Ci2JBjXqNxYr893Llk75VdPD9pM6y1SM0P80oXNy32VMtafkFFST8GpvvqWcxUJ93kzaY8RmU1g3XFOImSU2utU6+FUQ2Pn5uLwcfT2cTYfTpPGh+WXjSbZ6trqdEMEsLHybuPo2UN4WpVLXVQma3kSaHQggcLlEip8GhEUAy/xCb2eKqhI4HkDpDjwDnDVKufWlnRaOHf58cc8Woi+WT8JTOkHC+nBEG6fKRPHDG08U5yayIQIjI")
+ assert.NoError(t, err)
+ err = sniffer.UDP(pkt, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, "www.notion.so:443", reqAddr)
+
+ // Test unrecognized
+ pkt = []byte("oh my sweet summer child")
+ reqAddr = "90.90.90.90:90"
+ err = sniffer.UDP(pkt, &reqAddr)
+ assert.NoError(t, err)
+ assert.Equal(t, "90.90.90.90:90", reqAddr)
+}
diff --git a/extras/trafficlogger/http.go b/extras/trafficlogger/http.go
index 428dd94..8b26217 100644
--- a/extras/trafficlogger/http.go
+++ b/extras/trafficlogger/http.go
@@ -1,12 +1,18 @@
package trafficlogger
import (
+ "cmp"
"encoding/json"
+ "fmt"
"net/http"
+ "slices"
"strconv"
+ "strings"
"sync"
+ "time"
- "github.com/apernet/hysteria/core/server"
+ "github.com/apernet/hysteria/core/v2/server"
+ "github.com/apernet/quic-go"
)
const (
@@ -22,17 +28,21 @@ type TrafficStatsServer interface {
func NewTrafficStatsServer(secret string) TrafficStatsServer {
return &trafficStatsServerImpl{
- StatsMap: make(map[string]*trafficStatsEntry),
- KickMap: make(map[string]struct{}),
- Secret: secret,
+ StatsMap: make(map[string]*trafficStatsEntry),
+ KickMap: make(map[string]struct{}),
+ OnlineMap: make(map[string]int),
+ StreamMap: make(map[quic.Stream]*server.StreamStats),
+ Secret: secret,
}
}
type trafficStatsServerImpl struct {
- Mutex sync.RWMutex
- StatsMap map[string]*trafficStatsEntry
- KickMap map[string]struct{}
- Secret string
+ Mutex sync.RWMutex
+ StatsMap map[string]*trafficStatsEntry
+ OnlineMap map[string]int
+ StreamMap map[quic.Stream]*server.StreamStats
+ KickMap map[string]struct{}
+ Secret string
}
type trafficStatsEntry struct {
@@ -40,7 +50,7 @@ type trafficStatsEntry struct {
Rx uint64 `json:"rx"`
}
-func (s *trafficStatsServerImpl) Log(id string, tx, rx uint64) (ok bool) {
+func (s *trafficStatsServerImpl) LogTraffic(id string, tx, rx uint64) (ok bool) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
@@ -61,6 +71,35 @@ func (s *trafficStatsServerImpl) Log(id string, tx, rx uint64) (ok bool) {
return true
}
+// LogOnlineState updates the online state to the online map.
+func (s *trafficStatsServerImpl) LogOnlineState(id string, online bool) {
+ s.Mutex.Lock()
+ defer s.Mutex.Unlock()
+
+ if online {
+ s.OnlineMap[id]++
+ } else {
+ s.OnlineMap[id]--
+ if s.OnlineMap[id] <= 0 {
+ delete(s.OnlineMap, id)
+ }
+ }
+}
+
+func (s *trafficStatsServerImpl) TraceStream(stream quic.Stream, stats *server.StreamStats) {
+ s.Mutex.Lock()
+ defer s.Mutex.Unlock()
+
+ s.StreamMap[stream] = stats
+}
+
+func (s *trafficStatsServerImpl) UntraceStream(stream quic.Stream) {
+ s.Mutex.Lock()
+ defer s.Mutex.Unlock()
+
+ delete(s.StreamMap, stream)
+}
+
func (s *trafficStatsServerImpl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.Secret != "" && r.Header.Get("Authorization") != s.Secret {
http.Error(w, "unauthorized", http.StatusUnauthorized)
@@ -78,6 +117,14 @@ func (s *trafficStatsServerImpl) ServeHTTP(w http.ResponseWriter, r *http.Reques
s.kick(w, r)
return
}
+ if r.Method == http.MethodGet && r.URL.Path == "/online" {
+ s.getOnline(w, r)
+ return
+ }
+ if r.Method == http.MethodGet && r.URL.Path == "/dump/streams" {
+ s.getDumpStreams(w, r)
+ return
+ }
http.NotFound(w, r)
}
@@ -103,6 +150,139 @@ func (s *trafficStatsServerImpl) getTraffic(w http.ResponseWriter, r *http.Reque
_, _ = w.Write(jb)
}
+func (s *trafficStatsServerImpl) getOnline(w http.ResponseWriter, r *http.Request) {
+ s.Mutex.RLock()
+ defer s.Mutex.RUnlock()
+
+ jb, err := json.Marshal(s.OnlineMap)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ _, _ = w.Write(jb)
+}
+
+type dumpStreamEntry struct {
+ State string `json:"state"`
+
+ Auth string `json:"auth"`
+ Connection uint32 `json:"connection"`
+ Stream uint64 `json:"stream"`
+
+ ReqAddr string `json:"req_addr"`
+ HookedReqAddr string `json:"hooked_req_addr"`
+
+ Tx uint64 `json:"tx"`
+ Rx uint64 `json:"rx"`
+
+ InitialAt string `json:"initial_at"`
+ LastActiveAt string `json:"last_active_at"`
+
+ // for text/plain output
+ initialTime time.Time
+ lastActiveTime time.Time
+}
+
+func (e *dumpStreamEntry) fromStreamStats(stream quic.Stream, s *server.StreamStats) {
+ e.State = s.State.Load().String()
+ e.Auth = s.AuthID
+ e.Connection = s.ConnID
+ e.Stream = uint64(stream.StreamID())
+ e.ReqAddr = s.ReqAddr.Load()
+ e.HookedReqAddr = s.HookedReqAddr.Load()
+ e.Tx = s.Tx.Load()
+ e.Rx = s.Rx.Load()
+ e.initialTime = s.InitialTime
+ e.lastActiveTime = s.LastActiveTime.Load()
+ e.InitialAt = e.initialTime.Format(time.RFC3339Nano)
+ e.LastActiveAt = e.lastActiveTime.Format(time.RFC3339Nano)
+}
+
+func formatDumpStreamLine(state, auth, connection, stream, reqAddr, hookedReqAddr, tx, rx, lifetime, lastActive string) string {
+ return fmt.Sprintf("%-8s %-12s %12s %8s %12s %12s %12s %12s %-16s %s", state, auth, connection, stream, tx, rx, lifetime, lastActive, reqAddr, hookedReqAddr)
+}
+
+func (e *dumpStreamEntry) String() string {
+ stateText := strings.ToUpper(e.State)
+ connectionText := fmt.Sprintf("%08X", e.Connection)
+ streamText := strconv.FormatUint(e.Stream, 10)
+ reqAddrText := e.ReqAddr
+ if reqAddrText == "" {
+ reqAddrText = "-"
+ }
+ hookedReqAddrText := e.HookedReqAddr
+ if hookedReqAddrText == "" {
+ hookedReqAddrText = "-"
+ }
+ txText := strconv.FormatUint(e.Tx, 10)
+ rxText := strconv.FormatUint(e.Rx, 10)
+ lifetime := time.Now().Sub(e.initialTime)
+ if lifetime < 10*time.Minute {
+ lifetime = lifetime.Round(time.Millisecond)
+ } else {
+ lifetime = lifetime.Round(time.Second)
+ }
+ lastActive := time.Now().Sub(e.lastActiveTime)
+ if lastActive < 10*time.Minute {
+ lastActive = lastActive.Round(time.Millisecond)
+ } else {
+ lastActive = lastActive.Round(time.Second)
+ }
+
+ return formatDumpStreamLine(stateText, e.Auth, connectionText, streamText, reqAddrText, hookedReqAddrText, txText, rxText, lifetime.String(), lastActive.String())
+}
+
+func (s *trafficStatsServerImpl) getDumpStreams(w http.ResponseWriter, r *http.Request) {
+ var entries []dumpStreamEntry
+
+ s.Mutex.RLock()
+ entries = make([]dumpStreamEntry, len(s.StreamMap))
+ index := 0
+ for stream, stats := range s.StreamMap {
+ entries[index].fromStreamStats(stream, stats)
+ index++
+ }
+ s.Mutex.RUnlock()
+
+ slices.SortFunc(entries, func(lhs, rhs dumpStreamEntry) int {
+ if ret := cmp.Compare(lhs.Auth, rhs.Auth); ret != 0 {
+ return ret
+ }
+ if ret := cmp.Compare(lhs.Connection, rhs.Connection); ret != 0 {
+ return ret
+ }
+ if ret := cmp.Compare(lhs.Stream, rhs.Stream); ret != 0 {
+ return ret
+ }
+ return 0
+ })
+
+ accept := r.Header.Get("Accept")
+
+ if strings.Contains(accept, "text/plain") {
+ // Generate netstat-like output for humans
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+
+ // Print table header
+ _, _ = fmt.Fprintln(w, formatDumpStreamLine("State", "Auth", "Connection", "Stream", "Req-Addr", "Hooked-Req-Addr", "TX-Bytes", "RX-Bytes", "Lifetime", "Last-Active"))
+ for _, entry := range entries {
+ _, _ = fmt.Fprintln(w, entry.String())
+ }
+ return
+ }
+
+ // Response with json by default
+ wrapper := struct {
+ Streams []dumpStreamEntry `json:"streams"`
+ }{entries}
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ err := json.NewEncoder(w).Encode(&wrapper)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
func (s *trafficStatsServerImpl) kick(w http.ResponseWriter, r *http.Request) {
var ids []string
err := json.NewDecoder(r.Body).Decode(&ids)
diff --git a/extras/transport/udphop/addr.go b/extras/transport/udphop/addr.go
index 3c70472..afde26a 100644
--- a/extras/transport/udphop/addr.go
+++ b/extras/transport/udphop/addr.go
@@ -3,8 +3,8 @@ package udphop
import (
"fmt"
"net"
- "strconv"
- "strings"
+
+ "github.com/apernet/hysteria/extras/v2/utils"
)
type InvalidPortError struct {
@@ -57,36 +57,11 @@ func ResolveUDPHopAddr(addr string) (*UDPHopAddr, error) {
PortStr: portStr,
}
- portStrs := strings.Split(portStr, ",")
- for _, portStr := range portStrs {
- if strings.Contains(portStr, "-") {
- // Port range
- portRange := strings.Split(portStr, "-")
- if len(portRange) != 2 {
- return nil, InvalidPortError{portStr}
- }
- start, err := strconv.ParseUint(portRange[0], 10, 16)
- if err != nil {
- return nil, InvalidPortError{portStr}
- }
- end, err := strconv.ParseUint(portRange[1], 10, 16)
- if err != nil {
- return nil, InvalidPortError{portStr}
- }
- if start > end {
- start, end = end, start
- }
- for i := start; i <= end; i++ {
- result.Ports = append(result.Ports, uint16(i))
- }
- } else {
- // Single port
- port, err := strconv.ParseUint(portStr, 10, 16)
- if err != nil {
- return nil, InvalidPortError{portStr}
- }
- result.Ports = append(result.Ports, uint16(port))
- }
+ pu := utils.ParsePortUnion(portStr)
+ if pu == nil {
+ return nil, InvalidPortError{portStr}
}
+ result.Ports = pu.Ports()
+
return result, nil
}
diff --git a/extras/transport/udphop/addr_test.go b/extras/transport/udphop/addr_test.go
deleted file mode 100644
index 94a1016..0000000
--- a/extras/transport/udphop/addr_test.go
+++ /dev/null
@@ -1,132 +0,0 @@
-package udphop
-
-import (
- "net"
- "reflect"
- "testing"
-)
-
-func TestResolveUDPHopAddr(t *testing.T) {
- type args struct {
- addr string
- }
- tests := []struct {
- name string
- args args
- want *UDPHopAddr
- wantErr bool
- }{
- {
- name: "empty",
- args: args{
- addr: "",
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "no port",
- args: args{
- addr: "8.8.8.8",
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "single port",
- args: args{
- addr: "8.8.4.4:1234",
- },
- want: &UDPHopAddr{
- IP: net.ParseIP("8.8.4.4"),
- Ports: []uint16{1234},
- PortStr: "1234",
- },
- wantErr: false,
- },
- {
- name: "multiple ports",
- args: args{
- addr: "8.8.3.3:1234,5678,9012",
- },
- want: &UDPHopAddr{
- IP: net.ParseIP("8.8.3.3"),
- Ports: []uint16{1234, 5678, 9012},
- PortStr: "1234,5678,9012",
- },
- wantErr: false,
- },
- {
- name: "port range",
- args: args{
- addr: "1.2.3.4:1234-1240",
- },
- want: &UDPHopAddr{
- IP: net.ParseIP("1.2.3.4"),
- Ports: []uint16{1234, 1235, 1236, 1237, 1238, 1239, 1240},
- PortStr: "1234-1240",
- },
- wantErr: false,
- },
- {
- name: "port range reversed",
- args: args{
- addr: "123.123.123.123:9990-9980",
- },
- want: &UDPHopAddr{
- IP: net.ParseIP("123.123.123.123"),
- Ports: []uint16{9980, 9981, 9982, 9983, 9984, 9985, 9986, 9987, 9988, 9989, 9990},
- PortStr: "9990-9980",
- },
- wantErr: false,
- },
- {
- name: "port range & port list",
- args: args{
- addr: "9.9.9.9:1234-1236,5678,9012",
- },
- want: &UDPHopAddr{
- IP: net.ParseIP("9.9.9.9"),
- Ports: []uint16{1234, 1235, 1236, 5678, 9012},
- PortStr: "1234-1236,5678,9012",
- },
- wantErr: false,
- },
- {
- name: "invalid port",
- args: args{
- addr: "5.5.5.5:1234,bs",
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "invalid port range 1",
- args: args{
- addr: "6.6.6.6:7788-bbss",
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "invalid port range 2",
- args: args{
- addr: "1.0.0.1:8899-9002-9005",
- },
- want: nil,
- wantErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := ResolveUDPHopAddr(tt.args.addr)
- if (err != nil) != tt.wantErr {
- t.Errorf("ParseUDPHopAddr() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("ParseUDPHopAddr() got = %v, want %v", got, tt.want)
- }
- })
- }
-}
diff --git a/extras/utils/portunion.go b/extras/utils/portunion.go
new file mode 100644
index 0000000..f76a6fd
--- /dev/null
+++ b/extras/utils/portunion.go
@@ -0,0 +1,107 @@
+package utils
+
+import (
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// PortUnion is a collection of multiple port ranges.
+type PortUnion []PortRange
+
+// PortRange represents a range of ports.
+// Start and End are inclusive. [Start, End]
+type PortRange struct {
+ Start, End uint16
+}
+
+// ParsePortUnion parses a string of comma-separated port ranges (or single ports) into a PortUnion.
+// Returns nil if the input is invalid.
+// The returned PortUnion is guaranteed to be normalized.
+func ParsePortUnion(s string) PortUnion {
+ if s == "all" || s == "*" {
+ // Wildcard special case
+ return PortUnion{PortRange{0, 65535}}
+ }
+ var result PortUnion
+ portStrs := strings.Split(s, ",")
+ for _, portStr := range portStrs {
+ if strings.Contains(portStr, "-") {
+ // Port range
+ portRange := strings.Split(portStr, "-")
+ if len(portRange) != 2 {
+ return nil
+ }
+ start, err := strconv.ParseUint(portRange[0], 10, 16)
+ if err != nil {
+ return nil
+ }
+ end, err := strconv.ParseUint(portRange[1], 10, 16)
+ if err != nil {
+ return nil
+ }
+ if start > end {
+ start, end = end, start
+ }
+ result = append(result, PortRange{uint16(start), uint16(end)})
+ } else {
+ // Single port
+ port, err := strconv.ParseUint(portStr, 10, 16)
+ if err != nil {
+ return nil
+ }
+ result = append(result, PortRange{uint16(port), uint16(port)})
+ }
+ }
+ if result == nil {
+ return nil
+ }
+ return result.Normalize()
+}
+
+// Normalize normalizes a PortUnion.
+// No overlapping ranges, ranges are sorted from low to high.
+func (u PortUnion) Normalize() PortUnion {
+ if len(u) == 0 {
+ return u
+ }
+ sort.Slice(u, func(i, j int) bool {
+ if u[i].Start == u[j].Start {
+ return u[i].End < u[j].End
+ }
+ return u[i].Start < u[j].Start
+ })
+ normalized := PortUnion{u[0]}
+ for _, current := range u[1:] {
+ last := &normalized[len(normalized)-1]
+ if uint32(current.Start) <= uint32(last.End)+1 {
+ if current.End > last.End {
+ last.End = current.End
+ }
+ } else {
+ normalized = append(normalized, current)
+ }
+ }
+ return normalized
+}
+
+// Ports returns all ports in the PortUnion as a slice.
+func (u PortUnion) Ports() []uint16 {
+ var ports []uint16
+ for _, r := range u {
+ for i := uint32(r.Start); i <= uint32(r.End); i++ {
+ ports = append(ports, uint16(i))
+ }
+ }
+ return ports
+}
+
+// Contains returns true if the PortUnion contains the given port.
+func (u PortUnion) Contains(port uint16) bool {
+ for _, r := range u {
+ if port >= r.Start && port <= r.End {
+ return true
+ }
+ }
+ return false
+}
diff --git a/extras/utils/portunion_test.go b/extras/utils/portunion_test.go
new file mode 100644
index 0000000..ba056a3
--- /dev/null
+++ b/extras/utils/portunion_test.go
@@ -0,0 +1,150 @@
+package utils
+
+import (
+ "reflect"
+ "slices"
+ "testing"
+)
+
+func TestParsePortUnion(t *testing.T) {
+ tests := []struct {
+ name string
+ s string
+ want PortUnion
+ }{
+ {
+ name: "empty",
+ s: "",
+ want: nil,
+ },
+ {
+ name: "all 1",
+ s: "all",
+ want: PortUnion{{0, 65535}},
+ },
+ {
+ name: "all 2",
+ s: "*",
+ want: PortUnion{{0, 65535}},
+ },
+ {
+ name: "single port",
+ s: "1234",
+ want: PortUnion{{1234, 1234}},
+ },
+ {
+ name: "multiple ports (unsorted)",
+ s: "5678,1234,9012",
+ want: PortUnion{{1234, 1234}, {5678, 5678}, {9012, 9012}},
+ },
+ {
+ name: "one range",
+ s: "1234-1240",
+ want: PortUnion{{1234, 1240}},
+ },
+ {
+ name: "one range (reversed)",
+ s: "1240-1234",
+ want: PortUnion{{1234, 1240}},
+ },
+ {
+ name: "multiple ports and ranges (reversed, unsorted, overlapping)",
+ s: "5678,1200-1236,9100-9012,1234-1240",
+ want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}},
+ },
+ {
+ name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping)",
+ s: "5678,1200-1236,65531-65535,65532-65534,9100-9012,1234-1240",
+ want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}},
+ },
+ {
+ name: "multiple ports and ranges with 65535 (reversed, unsorted, overlapping) 2",
+ s: "5678,1200-1236,65532-65535,65531-65534,9100-9012,1234-1240",
+ want: PortUnion{{1200, 1240}, {5678, 5678}, {9012, 9100}, {65531, 65535}},
+ },
+ {
+ name: "invalid 1",
+ s: "1234-",
+ want: nil,
+ },
+ {
+ name: "invalid 2",
+ s: "1234-ggez",
+ want: nil,
+ },
+ {
+ name: "invalid 3",
+ s: "233,",
+ want: nil,
+ },
+ {
+ name: "invalid 4",
+ s: "1234-1240-1250",
+ want: nil,
+ },
+ {
+ name: "invalid 5",
+ s: "-,,",
+ want: nil,
+ },
+ {
+ name: "invalid 6",
+ s: "http",
+ want: nil,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ParsePortUnion(tt.s); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ParsePortUnion() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestPortUnion_Ports(t *testing.T) {
+ tests := []struct {
+ name string
+ pu PortUnion
+ want []uint16
+ }{
+ {
+ name: "single port",
+ pu: PortUnion{{1234, 1234}},
+ want: []uint16{1234},
+ },
+ {
+ name: "multiple ports",
+ pu: PortUnion{{1234, 1236}},
+ want: []uint16{1234, 1235, 1236},
+ },
+ {
+ name: "multiple ports and ranges",
+ pu: PortUnion{{1234, 1236}, {5678, 5680}, {9000, 9002}},
+ want: []uint16{1234, 1235, 1236, 5678, 5679, 5680, 9000, 9001, 9002},
+ },
+ {
+ name: "single port 65535",
+ pu: PortUnion{{65535, 65535}},
+ want: []uint16{65535},
+ },
+ {
+ name: "port range with 65535",
+ pu: PortUnion{{65530, 65535}},
+ want: []uint16{65530, 65531, 65532, 65533, 65534, 65535},
+ },
+ {
+ name: "multiple ports and ranges with 65535",
+ pu: PortUnion{{65530, 65535}, {1234, 1236}},
+ want: []uint16{65530, 65531, 65532, 65533, 65534, 65535, 1234, 1235, 1236},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.pu.Ports(); !slices.Equal(got, tt.want) {
+ t.Errorf("PortUnion.Ports() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/go.work b/go.work
index bde9a9c..1d4fa8b 100644
--- a/go.work
+++ b/go.work
@@ -1,4 +1,6 @@
-go 1.21
+go 1.23
+
+toolchain go1.24.2
use (
./app
diff --git a/go.work.sum b/go.work.sum
index 62f146c..95d2882 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -7,6 +7,8 @@ cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePzt
cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
@@ -35,11 +37,17 @@ github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8
github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
+github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
+github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
@@ -53,6 +61,8 @@ github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
+github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
+github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI=
@@ -83,7 +93,9 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
@@ -108,8 +120,6 @@ github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/consul/api v1.18.0 h1:R7PPNzTCeN6VuQNDwwhZWJvzCtGSrNpJqfb22h3yH9g=
github.com/hashicorp/consul/api v1.18.0/go.mod h1:owRRGJ9M5xReDC5nfT8FTJrNAPbT4NM6p/k+d03q2v4=
-github.com/hashicorp/go-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.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
@@ -123,6 +133,8 @@ github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfE
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
+github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
+github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -136,8 +148,6 @@ github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+L
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mattn/go-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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@@ -151,14 +161,31 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
+github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
+github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -241,6 +268,7 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
@@ -248,8 +276,10 @@ golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -258,6 +288,9 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
@@ -265,6 +298,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -273,6 +308,12 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
+golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
@@ -284,7 +325,16 @@ golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
+golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
+golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -293,6 +343,9 @@ golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
@@ -314,13 +367,16 @@ google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9M
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk=
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/hyperbole.py b/hyperbole.py
index 978ef8c..13dcd0a 100755
--- a/hyperbole.py
+++ b/hyperbole.py
@@ -29,7 +29,7 @@ BUILD_DIR = "build"
CORE_SRC_DIR = "./core"
EXTRAS_SRC_DIR = "./extras"
APP_SRC_DIR = "./app"
-APP_SRC_CMD_PKG = "github.com/apernet/hysteria/app/cmd"
+APP_SRC_CMD_PKG = "github.com/apernet/hysteria/app/v2/cmd"
MODULE_SRC_DIRS = [CORE_SRC_DIR, EXTRAS_SRC_DIR, APP_SRC_DIR]
@@ -74,6 +74,9 @@ ARCH_ALIASES = {
"GOARCH": "amd64",
"GOAMD64": "v3",
},
+ "loong64": {
+ "GOARCH": "loong64",
+ },
}
@@ -145,12 +148,33 @@ def get_app_commit():
return app_commit
+def get_toolchain():
+ try:
+ output = subprocess.check_output(["go", "version"]).decode().strip()
+ if output.startswith("go version "):
+ output = output[11:]
+ return output
+ except Exception:
+ return "Unknown"
+
+
def get_current_os_arch():
d_os = subprocess.check_output(["go", "env", "GOOS"]).decode().strip()
d_arch = subprocess.check_output(["go", "env", "GOARCH"]).decode().strip()
return (d_os, d_arch)
+def get_lib_version():
+ try:
+ with open(CORE_SRC_DIR + "/go.mod") as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith("github.com/apernet/quic-go"):
+ return line.split(" ")[1].strip()
+ except Exception:
+ return "Unknown"
+
+
def get_app_platforms():
platforms = os.environ.get("HY_APP_PLATFORMS")
if not platforms:
@@ -176,8 +200,12 @@ def cmd_build(pprof=False, release=False, race=False):
os.makedirs(BUILD_DIR, exist_ok=True)
app_version = get_app_version()
- app_date = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
+ app_date = datetime.datetime.now(datetime.timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%SZ"
+ )
+ app_toolchain = get_toolchain()
app_commit = get_app_commit()
+ lib_version = get_lib_version()
ldflags = [
"-X",
@@ -190,7 +218,11 @@ def cmd_build(pprof=False, release=False, race=False):
+ ("release" if release else "dev")
+ ("-pprof" if pprof else ""),
"-X",
+ '"' + APP_SRC_CMD_PKG + ".appToolchain=" + app_toolchain + '"',
+ "-X",
APP_SRC_CMD_PKG + ".appCommit=" + app_commit,
+ "-X",
+ APP_SRC_CMD_PKG + ".libVersion=" + lib_version,
]
if release:
ldflags.append("-s")
@@ -267,8 +299,12 @@ def cmd_run(args, pprof=False, race=False):
return
app_version = get_app_version()
- app_date = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
+ app_date = datetime.datetime.now(datetime.timezone.utc).strftime(
+ "%Y-%m-%dT%H:%M:%SZ"
+ )
+ app_toolchain = get_toolchain()
app_commit = get_app_commit()
+ lib_version = get_lib_version()
current_os, current_arch = get_current_os_arch()
@@ -280,11 +316,15 @@ def cmd_run(args, pprof=False, race=False):
"-X",
APP_SRC_CMD_PKG + ".appType=dev-run",
"-X",
+ '"' + APP_SRC_CMD_PKG + ".appToolchain=" + app_toolchain + '"',
+ "-X",
APP_SRC_CMD_PKG + ".appCommit=" + app_commit,
"-X",
APP_SRC_CMD_PKG + ".appPlatform=" + current_os,
"-X",
APP_SRC_CMD_PKG + ".appArch=" + current_arch,
+ "-X",
+ APP_SRC_CMD_PKG + ".libVersion=" + lib_version,
]
cmd = ["go", "run", "-ldflags", " ".join(ldflags)]
diff --git a/platforms.txt b/platforms.txt
index ea0ddf3..9059333 100644
--- a/platforms.txt
+++ b/platforms.txt
@@ -22,6 +22,7 @@ linux/s390x
linux/mipsle
linux/mipsle-sf
linux/riscv64
+linux/loong64
# Android
android/386
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..44ee651
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,11 @@
+blinker==1.8.2
+cffi==1.17.0
+click==8.1.7
+cryptography==43.0.0
+Flask==3.0.3
+itsdangerous==2.2.0
+Jinja2==3.1.4
+MarkupSafe==2.1.5
+pycparser==2.22
+PySocks==1.7.1
+Werkzeug==3.0.4
diff --git a/scripts/install_server.sh b/scripts/install_server.sh
index b71f075..93c06d9 100644
--- a/scripts/install_server.sh
+++ b/scripts/install_server.sh
@@ -187,6 +187,51 @@ chcon() {
command chcon "$@"
}
+get_systemd_version() {
+ if ! has_command systemctl; then
+ return
+ fi
+
+ command systemctl --version | head -1 | cut -d ' ' -f 2
+}
+
+systemd_unit_working_directory() {
+ local _systemd_version="$(get_systemd_version || true)"
+
+ # WorkingDirectory=~ requires systemd v227 or later.
+ # (released on Oct 2015, only CentOS 7 use an earlier version)
+ # ref: systemd/systemd@5f5d8eab1f2f5f5e088bc301533b3e4636de96c7
+ if [[ -n "$_systemd_version" && "$_systemd_version" -lt "227" ]]; then
+ echo "$HYSTERIA_HOME_DIR"
+ return
+ fi
+
+ echo "~"
+}
+
+get_selinux_context() {
+ local _file="$1"
+
+ local _lsres="$(ls -dZ "$_file" | head -1)"
+ local _sectx=''
+ case "$(echo "$_lsres" | wc -w)" in
+ 2)
+ _sectx="$(echo "$_lsres" | cut -d ' ' -f 1)"
+ ;;
+ 5)
+ _sectx="$(echo "$_lsres" | cut -d ' ' -f 4)"
+ ;;
+ *)
+ ;;
+ esac
+
+ if [[ "x$_sectx" == "x?" ]]; then
+ _sectx=""
+ fi
+
+ echo "$_sectx"
+}
+
show_argument_error_and_exit() {
local _error_msg="$1"
@@ -391,6 +436,9 @@ check_environment_architecture() {
's390x')
ARCHITECTURE='s390x'
;;
+ 'loongarch64')
+ ARCHITECTURE='loong64'
+ ;;
*)
error "The architecture '$(uname -a)' is not supported."
note "Specify ARCHITECTURE= to bypass this check and force this script to run on this $(uname -m)."
@@ -420,7 +468,7 @@ check_environment_systemd() {
}
check_environment_selinux() {
- if ! has_command chcon; then
+ if ! has_command getenforce; then
return
fi
@@ -433,8 +481,8 @@ check_environment_selinux() {
if [[ -z "$SECONTEXT_SYSTEMD_UNIT" ]]; then
if [[ -z "$FORCE_NO_SYSTEMD" ]] && [[ -e "$SYSTEMD_SERVICES_DIR" ]]; then
- local _sectx="$(ls -ldZ "$SYSTEMD_SERVICES_DIR" | cut -d ' ' -f 5)"
- if [[ "x$_sectx" == "x?" ]]; then
+ local _sectx="$(get_selinux_context "$SYSTEMD_SERVICES_DIR")"
+ if [[ -z "$_sectx" ]]; then
warning "Failed to obtain SEContext of $SYSTEMD_SERVICES_DIR"
else
SECONTEXT_SYSTEMD_UNIT="$_sectx"
@@ -715,7 +763,7 @@ After=network.target
[Service]
Type=simple
ExecStart=$EXECUTABLE_INSTALL_PATH server --config ${CONFIG_DIR}/${_config_name}.yaml
-WorkingDirectory=~
+WorkingDirectory=$(systemd_unit_working_directory)
User=$HYSTERIA_USER
Group=$HYSTERIA_USER
Environment=HYSTERIA_LOG_LEVEL=info
@@ -827,7 +875,7 @@ is_hysteria1_version() {
get_installed_version() {
if is_hysteria_installed; then
if "$EXECUTABLE_INSTALL_PATH" version > /dev/null 2>&1; then
- "$EXECUTABLE_INSTALL_PATH" version | grep Version | grep -o 'v[.0-9]*'
+ "$EXECUTABLE_INSTALL_PATH" version | grep '^Version' | grep -o 'v[.0-9]*'
elif "$EXECUTABLE_INSTALL_PATH" -v > /dev/null 2>&1; then
# hysteria 1
"$EXECUTABLE_INSTALL_PATH" -v | cut -d ' ' -f 3
@@ -1000,22 +1048,26 @@ perform_install() {
_is_update_required=1
fi
- if [[ -z "$_is_update_required" ]]; then
- echo "$(tgreen)Installed version is up-to-date, there is nothing to do.$(treset)"
- return
- fi
-
if is_hysteria1_version "$VERSION"; then
error "This script can only install Hysteria 2."
exit 95
fi
- perform_install_hysteria_binary
+ if [[ -n "$_is_update_required" ]]; then
+ perform_install_hysteria_binary
+ fi
+
+ # Always install additional files, regardless of $_is_update_required.
+ # This allows changes to be made with environment variables (e.g. change HYSTERIA_USER without --force).
perform_install_hysteria_example_config
perform_install_hysteria_home_legacy
perform_install_hysteria_systemd
- if [[ -n "$_is_frash_install" ]]; then
+ if [[ -z "$_is_update_required" ]]; then
+ echo
+ echo "$(tgreen)Installed version is up-to-date, there is nothing to do.$(treset)"
+ echo
+ elif [[ -n "$_is_frash_install" ]]; then
echo
echo -e "$(tbold)Congratulation! Hysteria 2 has been successfully installed on your server.$(treset)"
echo