浏览代码

Add hysteria and acme TLS certificate issuer (#18)

* Add hysteria client/server
* Add acme TLS certificate issuer
世界 3 年之前
父节点
当前提交
d1c3dd0ee1

+ 13 - 6
common/dialer/tls.go

@@ -20,11 +20,7 @@ type TLSDialer struct {
 	config *tls.Config
 }
 
-func NewTLS(dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
-	if !options.Enabled {
-		return dialer, nil
-	}
-
+func TLSConfig(serverAddress string, options option.OutboundTLSOptions) (*tls.Config, error) {
 	var serverName string
 	if options.ServerName != "" {
 		serverName = options.ServerName
@@ -105,9 +101,20 @@ func NewTLS(dialer N.Dialer, serverAddress string, options option.OutboundTLSOpt
 		}
 		tlsConfig.RootCAs = certPool
 	}
+	return &tlsConfig, nil
+}
+
+func NewTLS(dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
+	if !options.Enabled {
+		return dialer, nil
+	}
+	tlsConfig, err := TLSConfig(serverAddress, options)
+	if err != nil {
+		return nil, err
+	}
 	return &TLSDialer{
 		dialer: dialer,
-		config: &tlsConfig,
+		config: tlsConfig,
 	}, nil
 }
 

+ 1 - 0
constant/proxy.go

@@ -15,6 +15,7 @@ const (
 	TypeTrojan      = "trojan"
 	TypeNaive       = "naive"
 	TypeWireGuard   = "wireguard"
+	TypeHysteria    = "hysteria"
 )
 
 const (

+ 138 - 0
docs/configuration/inbound/hysteria.md

@@ -0,0 +1,138 @@
+### Structure
+
+```json
+{
+  "inbounds": [
+    {
+      "type": "hysteria",
+      "tag": "hysteria-in",
+      
+      "listen": "::",
+      "listen_port": 443,
+      "sniff": false,
+      "sniff_override_destination": false,
+      "domain_strategy": "prefer_ipv6",
+      
+      "up": "100 Mbps",
+      "up_mbps": 100,
+      "down": "100 Mbps",
+      "down_mbps": 100,
+      "obfs": "fuck me till the daylight",
+      "auth": "",
+      "auth_str": "password",
+      "recv_window_conn": 0,
+      "recv_window_client": 0,
+      "max_conn_client": 0,
+      "disable_mtu_discovery": false,
+      "tls": {}
+    }
+  ]
+}
+```
+
+!!! warning ""
+
+    QUIC, which is required by hysteria is not included by default, see [Installation](/#Installation).
+
+### Listen Fields
+
+#### listen
+
+==Required==
+
+Listen address.
+
+#### listen_port
+
+==Required==
+
+Listen port.
+
+#### sniff
+
+Enable sniffing.
+
+See [Sniff](/configuration/route/sniff/) for details.
+
+#### sniff_override_destination
+
+Override the connection destination address with the sniffed domain.
+
+If the domain name is invalid (like tor), this will not work.
+
+#### domain_strategy
+
+One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
+
+If set, the requested domain name will be resolved to IP before routing.
+
+If `sniff_override_destination` is in effect, its value will be taken as a fallback.
+
+### Hysteria Fields
+
+#### up, down
+
+==Required==
+
+Format: `[Integer] [Unit]` e.g. `100 Mbps, 640 KBps, 2 Gbps`
+
+Supported units (case sensitive, b = bits, B = bytes, 8b=1B):
+
+    bps (bits per second)
+    Bps (bytes per second)
+    Kbps (kilobits per second)
+    KBps (kilobytes per second)
+    Mbps (megabits per second)
+    MBps (megabytes per second)
+    Gbps (gigabits per second)
+    GBps (gigabytes per second)
+    Tbps (terabits per second)
+    TBps (terabytes per second)
+
+#### up_mbps, down_mbps
+
+==Required==
+
+`up, down` in Mbps.
+
+#### obfs
+
+Obfuscated password.
+
+#### auth
+
+Authentication password, in base64.
+
+#### auth_str
+
+Authentication password.
+
+#### recv_window_conn
+
+The QUIC stream-level flow control window for receiving data.
+
+`15728640 (15 MB/s)` will be used if empty.
+
+#### recv_window_client
+
+The QUIC connection-level flow control window for receiving data.
+
+`67108864 (64 MB/s)` will be used if empty.
+
+#### max_conn_client
+
+The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.
+
+`1024` will be used if empty.
+
+#### disable_mtu_discovery
+
+Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
+
+Force enabled on for systems other than Linux and Windows (according to upstream).
+
+#### tls
+
+==Required==
+
+TLS configuration, see [TLS inbound structure](/configuration/shared/tls/#inbound-structure).

+ 1 - 0
docs/configuration/inbound/index.md

@@ -23,6 +23,7 @@
 | `vmess`       | [VMess](./vmess)             |
 | `trojan`      | [Trojan](./trojan)           |
 | `naive`       | [Naive](./naive)             |
+| `hysteria`    | [Hysteria](./hysteria)       |
 | `tun`         | [Tun](./tun)                 |
 | `redirect`    | [Redirect](./redirect)       |
 | `tproxy`      | [TProxy](./tproxy)           |

+ 173 - 0
docs/configuration/outbound/hysteria.md

@@ -0,0 +1,173 @@
+### Structure
+
+```json
+{
+  "outbounds": [
+    {
+      "type": "hysteria",
+      "tag": "hysteria-out",
+      
+      "server": "127.0.0.1",
+      "server_port": 1080,
+
+      "up": "100 Mbps",
+      "up_mbps": 100,
+      "down": "100 Mbps",
+      "down_mbps": 100,
+      "obfs": "fuck me till the daylight",
+      "auth": "",
+      "auth_str": "password",
+      "recv_window_conn": 0,
+      "recv_window": 0,
+      "disable_mtu_discovery": false,
+      "network": "tcp",
+      "tls": {},
+      
+      "detour": "upstream-out",
+      "bind_interface": "en0",
+      "routing_mark": 1234,
+      "reuse_addr": false,
+      "connect_timeout": "5s",
+      "domain_strategy": "prefer_ipv6",
+      "fallback_delay": "300ms"
+    }
+  ]
+}
+```
+
+!!! warning ""
+
+    QUIC, which is required by hysteria is not included by default, see [Installation](/#Installation).
+
+### Hysteria Fields
+
+#### server
+
+==Required==
+
+The server address.
+
+#### server_port
+
+==Required==
+
+The server port.
+
+#### up, down
+
+==Required==
+
+Format: `[Integer] [Unit]` e.g. `100 Mbps, 640 KBps, 2 Gbps`
+
+Supported units (case sensitive, b = bits, B = bytes, 8b=1B):
+
+    bps (bits per second)
+    Bps (bytes per second)
+    Kbps (kilobits per second)
+    KBps (kilobytes per second)
+    Mbps (megabits per second)
+    MBps (megabytes per second)
+    Gbps (gigabits per second)
+    GBps (gigabytes per second)
+    Tbps (terabits per second)
+    TBps (terabytes per second)
+
+#### up_mbps, down_mbps
+
+==Required==
+
+`up, down` in Mbps.
+
+#### obfs
+
+Obfuscated password.
+
+#### auth
+
+Authentication password, in base64.
+
+#### auth_str
+
+Authentication password.
+
+#### recv_window_conn
+
+The QUIC stream-level flow control window for receiving data.
+
+`15728640 (15 MB/s)` will be used if empty.
+
+#### recv_window
+
+The QUIC connection-level flow control window for receiving data.
+
+`67108864 (64 MB/s)` will be used if empty.
+
+#### disable_mtu_discovery
+
+Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
+
+Force enabled on for systems other than Linux and Windows (according to upstream).
+
+#### tls
+
+==Required==
+
+TLS configuration, see [TLS inbound structure](/configuration/shared/tls/#inbound-structure).
+
+#### network
+
+Enabled network
+
+One of `tcp` `udp`.
+
+Both is enabled by default.
+
+### Dial Fields
+
+#### detour
+
+The tag of the upstream outbound.
+
+Other dial fields will be ignored when enabled.
+
+#### bind_interface
+
+The network interface to bind to.
+
+#### routing_mark
+
+!!! error ""
+
+    Linux only
+
+The iptables routing mark.
+
+#### reuse_addr
+
+Reuse listener address.
+
+#### connect_timeout
+
+Connect timeout, in golang's Duration format.
+
+A duration string is a possibly signed sequence of
+decimal numbers, each with optional fraction and a unit suffix,
+such as "300ms", "-1.5h" or "2h45m".
+Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+
+#### domain_strategy
+
+One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
+
+If set, the server domain name will be resolved to IP before connecting.
+
+`dns.strategy` will be used if empty.
+
+#### fallback_delay
+
+The length of time to wait before spawning a RFC 6555 Fast Fallback connection.
+That is, is the amount of time to wait for IPv6 to succeed before assuming
+that IPv6 is misconfigured and falling back to IPv4 if `prefer_ipv4` is set.
+If zero, a default delay of 300ms is used.
+
+Only take effect when `domain_strategy` is `prefer_ipv4` or `prefer_ipv6`.

+ 1 - 0
docs/configuration/outbound/index.md

@@ -23,6 +23,7 @@
 | `vmess`       | [VMess](./vmess)             |
 | `trojan`      | [Trojan](./trojan)           |
 | `wireguard`   | [Wireguard](./wireguard)     |
+| `hysteria`    | [Hysteria](./hysteria)       |
 | `dns`         | [DNS](./dns)                 |
 | `selector`    | [Selector](./selector)       |
 

+ 70 - 1
docs/configuration/shared/tls.md

@@ -11,10 +11,25 @@
   "certificate": "",
   "certificate_path": "",
   "key": "",
-  "key_path": ""
+  "key_path": "",
+  "acme": {
+    "domain": [],
+    "data_directory": "",
+    "default_server_name": "",
+    "email": "",
+    "provider": "",
+    "disable_http_challenge": false,
+    "disable_tls_alpn_challenge": false,
+    "alternative_http_port": 0,
+    "alternative_tls_port": 0
+  }
 }
 ```
 
+!!! warning ""
+
+    ACME is not included by default, see [Installation](/#Installation).
+
 ### Outbound Structure
 
 ```json
@@ -59,6 +74,10 @@ Cipher suite values:
 * `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256`
 * `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256`
 
+!!! note ""
+
+    You can ignore the JSON Array [] tag when the content is only one item
+
 ### Fields
 
 #### enabled
@@ -135,6 +154,56 @@ The server private key, in PEM format.
 
 The path to the server private key, in PEM format.
 
+### ACME Fields
+
+#### domain
+
+List of domain.
+
+ACME will be disabled if empty.
+
+#### data_directory
+
+The directory to store ACME data.
+
+`$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic` will be used if empty.
+
+#### default_server_name
+
+Server name to use when choosing a certificate if the ClientHello's ServerName field is empty.
+
+#### email
+
+The email address to use when creating or selecting an existing ACME server account
+
+#### provider
+
+The ACME CA provider to use.
+
+| Value                   | Provider      |
+|-------------------------|---------------|
+| `letsenctypt (default)` | Let's Encrypt |
+| `zerossl`               | ZeroSSL       |
+| `https://...`           | Custom        |
+
+#### disable_http_challenge
+
+Disable all HTTP challenges.
+
+#### disable_tls_alpn_challenge
+
+Disable all TLS-ALPN challenges
+
+#### alternative_http_port
+
+The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a
+listener for the HTTP challenge.
+
+#### alternative_tls_port
+
+The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to
+succeed.
+
 ### Reload
 
 For server configuration, certificate and key will be automatically reloaded if modified.

+ 7 - 6
docs/index.md

@@ -18,12 +18,13 @@ Install with options:
 go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@latest
 ```
 
-| Build Tag                  | Description                                                                                                                                  |
-|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
-| `with_quic`                | Build with QUIC support, see [QUIC and HTTP3 dns transports](./configuration/dns/server) and [Naive inbound](./configuration/inbound/naive). |
-| `with_clash_api`           | Build with Clash api support, see [Experimental](./configuration/experimental#clash-api-fields).                                             |
-| `no_gvisor`                | Build without gVisor tun stack support, see [Tun inbound](./configuration/inbound/tun#stack).                                                |
-| `with_lwip` (CGO required) | Build with LWIP tun stack support, see [Tun inbound](./configuration/inbound/tun#stack).                                                     |
+| Build Tag                  | Description                                                                                                                                                                                                                                                |
+|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `with_quic`                | Build with QUIC support, see [QUIC and HTTP3 dns transports](./configuration/dns/server), [Naive inbound](./configuration/inbound/naive), [Hysteria Inbound](./configuration/inbound/hysteria) and [Hysteria Outbound](./configuration/outbound/hysteria). |
+| `with_acme`                | Build with ACME TLS certificate issuer support, see [TLS](./configuration/shared/tls).                                                                                                                                                                     |
+| `with_clash_api`           | Build with Clash api support, see [Experimental](./configuration/experimental#clash-api-fields).                                                                                                                                                           |
+| `no_gvisor`                | Build without gVisor tun stack support, see [Tun inbound](./configuration/inbound/tun#stack).                                                                                                                                                              |
+| `with_lwip` (CGO required) | Build with LWIP tun stack support, see [Tun inbound](./configuration/inbound/tun#stack).                                                                                                                                                                   |
 
 The binary is built under $GOPATH/bin
 

+ 7 - 4
go.mod

@@ -14,8 +14,9 @@ require (
 	github.com/hashicorp/yamux v0.1.1
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/oschwald/maxminddb-golang v1.10.0
+	github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a
 	github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb
-	github.com/sagernet/sing v0.0.0-20220819003212-2424b1e2fac1
+	github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0
 	github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6
 	github.com/sagernet/sing-tun v0.0.0-20220819003411-1cc817596b08
@@ -38,11 +39,12 @@ require (
 	github.com/golang/mock v1.6.0 // indirect
 	github.com/google/btree v1.0.1 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
-	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
-	github.com/kr/pretty v0.1.0 // indirect
+	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
+	github.com/libdns/libdns v0.2.1 // indirect
 	github.com/marten-seemann/qpack v0.2.1 // indirect
 	github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
 	github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
+	github.com/mholt/acmez v1.0.4 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -50,6 +52,8 @@ require (
 	github.com/sagernet/netlink v0.0.0-20220816152750-7a75378bd31a // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
+	go.uber.org/multierr v1.6.0 // indirect
+	go.uber.org/zap v1.22.0 // indirect
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
 	golang.org/x/text v0.3.7 // indirect
@@ -57,7 +61,6 @@ require (
 	golang.org/x/tools v0.1.10 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	lukechampine.com/blake3 v1.1.7 // indirect

+ 35 - 4
go.sum

@@ -1,5 +1,7 @@
 github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
 github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/database64128/tfo-go v1.1.1 h1:jcaCQBkEZZxV1t2wfOwt41WJKzgcNtLV7nGOm+hmZ3w=
 github.com/database64128/tfo-go v1.1.1/go.mod h1:b1wrRNZr7NKZhWQ8LSTvqo1r2ppLdYXZLIUDCPOgJrI=
@@ -45,13 +47,15 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
-github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
+github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
+github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
 github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
 github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
@@ -60,6 +64,8 @@ github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKA
 github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
 github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
 github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
+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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@@ -73,9 +79,13 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
 github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
 github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
 github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a h1:SE3Xn4GOQ+kxbgGa2Xp0H2CCsx1o2pVTt0f+hmfuHH4=
+github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a/go.mod h1:Q+ZXyesnkjV5B70B1ixk65ecKrlJ2jz0atv3fPKsVVo=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
 github.com/sagernet/netlink v0.0.0-20220816152750-7a75378bd31a h1:iNtsfGMenajBUGZ/1yAzl1v3p+t/7IJ/ilQXq9haRZ8=
@@ -84,8 +94,8 @@ github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb h1:wc0yQ+SBn4TaTY
 github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb/go.mod h1:MIccjRKnPTjWwAOpl+AUGWOkzyTd9tERytudxu+1ra4=
 github.com/sagernet/sing v0.0.0-20220812082120-05f9836bff8f/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
-github.com/sagernet/sing v0.0.0-20220819003212-2424b1e2fac1 h1:+YC0/ygsJc4Z8qhd7ypsbWgMSm+UWN+QK+PW7I19K4Q=
-github.com/sagernet/sing v0.0.0-20220819003212-2424b1e2fac1/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
+github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0 h1:jFtynAm5qU1WXIs4FBxi7nLtTVMNXIv/hgO0V/BxmuE=
+github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9 h1:XgXSOJv8e7+98SJvg1f0luuPR33r4yFcmzxb3R//BTI=
 github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9/go.mod h1:MAHy2IKZAA101t3Gr2x0ldwn6XuAs2cjGzSzHy5RhWk=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 h1:JJfDeYYhWunvtxsU/mOVNTmFQmnzGx9dY034qG6G3g4=
@@ -102,7 +112,9 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -110,8 +122,16 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695AP
 github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
 go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0=
+go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -119,17 +139,20 @@ golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUH
 golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.3.0/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.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
 golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -150,11 +173,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 h1:fqTvyMIIj+HRzMmnzr9NtpHP6uVpvB5fkHcgPDC4nu8=
 golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -163,9 +190,11 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -192,10 +221,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gvisor.dev/gvisor v0.0.0-20220801010827-addd1f7b3e97 h1:zncudP85ZlJelPsgxZXN00Rl5M5j7QuDK27L35Ez01M=

+ 2 - 0
inbound/builder.go

@@ -37,6 +37,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
 		return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions)
 	case C.TypeNaive:
 		return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions)
+	case C.TypeHysteria:
+		return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
 	default:
 		return nil, E.New("unknown inbound type: ", options.Type)
 	}

+ 1 - 1
inbound/http.go

@@ -41,7 +41,7 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge
 		authenticator: auth.NewAuthenticator(options.Users),
 	}
 	if options.TLS != nil {
-		tlsConfig, err := NewTLSConfig(logger, common.PtrValueOrDefault(options.TLS))
+		tlsConfig, err := NewTLSConfig(ctx, logger, common.PtrValueOrDefault(options.TLS))
 		if err != nil {
 			return nil, err
 		}

+ 319 - 0
inbound/hysteria.go

@@ -0,0 +1,319 @@
+//go:build with_quic
+
+package inbound
+
+import (
+	"bytes"
+	"context"
+	"net"
+	"net/netip"
+	"sync"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/quic-go/congestion"
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/hysteria"
+	"github.com/sagernet/sing-dns"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var _ adapter.Inbound = (*Hysteria)(nil)
+
+type Hysteria struct {
+	ctx           context.Context
+	router        adapter.Router
+	logger        log.ContextLogger
+	tag           string
+	listenOptions option.ListenOptions
+	quicConfig    *quic.Config
+	tlsConfig     *TLSConfig
+	authKey       []byte
+	xplusKey      []byte
+	sendBPS       uint64
+	recvBPS       uint64
+	listener      quic.Listener
+	udpAccess     sync.RWMutex
+	udpSessionId  uint32
+	udpSessions   map[uint32]chan *hysteria.UDPMessage
+	udpDefragger  hysteria.Defragger
+}
+
+func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (*Hysteria, error) {
+	quicConfig := &quic.Config{
+		InitialStreamReceiveWindow:     options.ReceiveWindowConn,
+		MaxStreamReceiveWindow:         options.ReceiveWindowConn,
+		InitialConnectionReceiveWindow: options.ReceiveWindowClient,
+		MaxConnectionReceiveWindow:     options.ReceiveWindowClient,
+		MaxIncomingStreams:             int64(options.MaxConnClient),
+		KeepAlivePeriod:                hysteria.KeepAlivePeriod,
+		DisablePathMTUDiscovery:        options.DisableMTUDiscovery || !(C.IsLinux || C.IsWindows),
+		EnableDatagrams:                true,
+	}
+	if options.ReceiveWindowConn == 0 {
+		quicConfig.InitialStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
+		quicConfig.MaxStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
+	}
+	if options.ReceiveWindowClient == 0 {
+		quicConfig.InitialConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
+		quicConfig.MaxConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
+	}
+	if quicConfig.MaxIncomingStreams == 0 {
+		quicConfig.MaxIncomingStreams = hysteria.DefaultMaxIncomingStreams
+	}
+	var auth []byte
+	if len(options.Auth) > 0 {
+		auth = options.Auth
+	} else {
+		auth = []byte(options.AuthString)
+	}
+	var xplus []byte
+	if options.Obfs != "" {
+		xplus = []byte(options.Obfs)
+	}
+	var up, down uint64
+	if len(options.Up) > 0 {
+		up = hysteria.StringToBps(options.Up)
+		if up == 0 {
+			return nil, E.New("invalid up speed format: ", options.Up)
+		}
+	} else {
+		up = uint64(options.UpMbps) * hysteria.MbpsToBps
+	}
+	if len(options.Down) > 0 {
+		down = hysteria.StringToBps(options.Down)
+		if down == 0 {
+			return nil, E.New("invalid down speed format: ", options.Down)
+		}
+	} else {
+		down = uint64(options.DownMbps) * hysteria.MbpsToBps
+	}
+	if up < hysteria.MinSpeedBPS {
+		return nil, E.New("invalid up speed")
+	}
+	if down < hysteria.MinSpeedBPS {
+		return nil, E.New("invalid down speed")
+	}
+	inbound := &Hysteria{
+		ctx:           ctx,
+		router:        router,
+		logger:        logger,
+		tag:           tag,
+		quicConfig:    quicConfig,
+		listenOptions: options.ListenOptions,
+		authKey:       auth,
+		xplusKey:      xplus,
+		sendBPS:       up,
+		recvBPS:       down,
+		udpSessions:   make(map[uint32]chan *hysteria.UDPMessage),
+	}
+	if options.TLS == nil || !options.TLS.Enabled {
+		return nil, errTLSRequired
+	}
+	if len(options.TLS.ALPN) == 0 {
+		options.TLS.ALPN = []string{hysteria.DefaultALPN}
+	}
+	tlsConfig, err := NewTLSConfig(ctx, logger, common.PtrValueOrDefault(options.TLS))
+	if err != nil {
+		return nil, err
+	}
+	inbound.tlsConfig = tlsConfig
+	return inbound, nil
+}
+
+func (h *Hysteria) Type() string {
+	return C.TypeHysteria
+}
+
+func (h *Hysteria) Tag() string {
+	return h.tag
+}
+
+func (h *Hysteria) Start() error {
+	listenAddr := M.SocksaddrFrom(netip.Addr(h.listenOptions.Listen), h.listenOptions.ListenPort)
+	var packetConn net.PacketConn
+	var err error
+	packetConn, err = net.ListenUDP(M.NetworkFromNetAddr("udp", listenAddr.Addr), listenAddr.UDPAddr())
+	if err != nil {
+		return err
+	}
+	if len(h.xplusKey) > 0 {
+		packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
+		packetConn = &hysteria.PacketConnWrapper{PacketConn: packetConn}
+	}
+	err = h.tlsConfig.Start()
+	if err != nil {
+		return err
+	}
+	listener, err := quic.Listen(packetConn, h.tlsConfig.Config(), h.quicConfig)
+	if err != nil {
+		return err
+	}
+	h.listener = listener
+	h.logger.Info("udp server started at ", listener.Addr())
+	go h.acceptLoop()
+	return nil
+}
+
+func (h *Hysteria) acceptLoop() {
+	for {
+		ctx := log.ContextWithNewID(h.ctx)
+		conn, err := h.listener.Accept(ctx)
+		if err != nil {
+			return
+		}
+		h.logger.InfoContext(ctx, "inbound connection from ", conn.RemoteAddr())
+		go func() {
+			hErr := h.accept(ctx, conn)
+			if hErr != nil {
+				conn.CloseWithError(0, "")
+				NewError(h.logger, ctx, E.Cause(hErr, "process connection from ", conn.RemoteAddr()))
+			}
+		}()
+	}
+}
+
+func (h *Hysteria) accept(ctx context.Context, conn quic.Connection) error {
+	controlStream, err := conn.AcceptStream(ctx)
+	if err != nil {
+		return err
+	}
+	clientHello, err := hysteria.ReadClientHello(controlStream)
+	if err != nil {
+		return err
+	}
+	if !bytes.Equal(clientHello.Auth, h.authKey) {
+		err = hysteria.WriteServerHello(controlStream, hysteria.ServerHello{
+			Message: "wrong password",
+		})
+		return E.Errors(E.New("wrong password: ", string(clientHello.Auth)), err)
+	}
+	if clientHello.SendBPS == 0 || clientHello.RecvBPS == 0 {
+		return E.New("invalid rate from client")
+	}
+	serverSendBPS, serverRecvBPS := clientHello.RecvBPS, clientHello.SendBPS
+	if h.sendBPS > 0 && serverSendBPS > h.sendBPS {
+		serverSendBPS = h.sendBPS
+	}
+	if h.recvBPS > 0 && serverRecvBPS > h.recvBPS {
+		serverRecvBPS = h.recvBPS
+	}
+	err = hysteria.WriteServerHello(controlStream, hysteria.ServerHello{
+		OK:      true,
+		SendBPS: serverSendBPS,
+		RecvBPS: serverRecvBPS,
+	})
+	if err != nil {
+		return err
+	}
+	conn.SetCongestionControl(hysteria.NewBrutalSender(congestion.ByteCount(serverSendBPS)))
+	go h.udpRecvLoop(conn)
+	for {
+		var stream quic.Stream
+		stream, err = conn.AcceptStream(ctx)
+		if err != nil {
+			return err
+		}
+		go func() {
+			hErr := h.acceptStream(ctx, conn /*&hysteria.StreamWrapper{Stream: stream}*/, stream)
+			if hErr != nil {
+				stream.Close()
+				NewError(h.logger, ctx, E.Cause(hErr, "process stream from ", conn.RemoteAddr()))
+			}
+		}()
+	}
+}
+
+func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
+	for {
+		packet, err := conn.ReceiveMessage()
+		if err != nil {
+			return
+		}
+		message, err := hysteria.ParseUDPMessage(packet)
+		if err != nil {
+			h.logger.Error("parse udp message: ", err)
+			continue
+		}
+		dfMsg := h.udpDefragger.Feed(message)
+		if dfMsg == nil {
+			continue
+		}
+		h.udpAccess.RLock()
+		ch, ok := h.udpSessions[dfMsg.SessionID]
+		if ok {
+			select {
+			case ch <- dfMsg:
+				// OK
+			default:
+				// Silently drop the message when the channel is full
+			}
+		}
+		h.udpAccess.RUnlock()
+	}
+}
+
+func (h *Hysteria) acceptStream(ctx context.Context, conn quic.Connection, stream quic.Stream) error {
+	request, err := hysteria.ReadClientRequest(stream)
+	if err != nil {
+		return err
+	}
+	err = hysteria.WriteServerResponse(stream, hysteria.ServerResponse{
+		OK: true,
+	})
+	if err != nil {
+		return err
+	}
+	var metadata adapter.InboundContext
+	metadata.Inbound = h.tag
+	metadata.InboundType = C.TypeHysteria
+	metadata.SniffEnabled = h.listenOptions.SniffEnabled
+	metadata.SniffOverrideDestination = h.listenOptions.SniffOverrideDestination
+	metadata.DomainStrategy = dns.DomainStrategy(h.listenOptions.DomainStrategy)
+	metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr())
+	metadata.Destination = M.ParseSocksaddrHostPort(request.Host, request.Port)
+	if !request.UDP {
+		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+		metadata.Network = N.NetworkTCP
+		return h.router.RouteConnection(ctx, hysteria.NewConn(stream, metadata.Destination), metadata)
+	} else {
+		h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
+		var id uint32
+		h.udpAccess.Lock()
+		id = h.udpSessionId
+		nCh := make(chan *hysteria.UDPMessage, 1024)
+		h.udpSessions[id] = nCh
+		h.udpSessionId += 1
+		h.udpAccess.Unlock()
+		metadata.Network = N.NetworkUDP
+		packetConn := hysteria.NewPacketConn(conn, stream, id, metadata.Destination, nCh, common.Closer(func() error {
+			h.udpAccess.Lock()
+			if ch, ok := h.udpSessions[id]; ok {
+				close(ch)
+				delete(h.udpSessions, id)
+			}
+			h.udpAccess.Unlock()
+			return nil
+		}))
+		go packetConn.Hold()
+		return h.router.RoutePacketConnection(ctx, packetConn, metadata)
+	}
+}
+
+func (h *Hysteria) Close() error {
+	h.udpAccess.Lock()
+	for _, session := range h.udpSessions {
+		close(session)
+	}
+	h.udpSessions = make(map[uint32]chan *hysteria.UDPMessage)
+	h.udpAccess.Unlock()
+	return common.Close(
+		h.listener,
+		common.PtrOrNil(h.tlsConfig),
+	)
+}

+ 16 - 0
inbound/hysteria_stub.go

@@ -0,0 +1,16 @@
+//go:build !with_quic
+
+package inbound
+
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) {
+	return nil, E.New(`QUIC is not included in this build, rebuild with -tags with_quic`)
+}

+ 6 - 7
inbound/naive.go

@@ -45,10 +45,7 @@ type Naive struct {
 	h3Server      any
 }
 
-var (
-	ErrNaiveTLSRequired  = E.New("TLS required")
-	ErrNaiveMissingUsers = E.New("missing users")
-)
+var errTLSRequired = E.New("TLS required")
 
 func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (*Naive, error) {
 	inbound := &Naive{
@@ -61,12 +58,12 @@ func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		authenticator: auth.NewAuthenticator(options.Users),
 	}
 	if options.TLS == nil || !options.TLS.Enabled {
-		return nil, ErrNaiveTLSRequired
+		return nil, errTLSRequired
 	}
 	if len(options.Users) == 0 {
-		return nil, ErrNaiveMissingUsers
+		return nil, E.New("missing users")
 	}
-	tlsConfig, err := NewTLSConfig(logger, common.PtrValueOrDefault(options.TLS))
+	tlsConfig, err := NewTLSConfig(ctx, logger, common.PtrValueOrDefault(options.TLS))
 	if err != nil {
 		return nil, err
 	}
@@ -195,6 +192,8 @@ func (n *Naive) newConnection(ctx context.Context, conn net.Conn, source, destin
 	metadata.Network = N.NetworkTCP
 	metadata.Source = source
 	metadata.Destination = destination
+	n.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
+	n.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
 	hErr := n.router.RouteConnection(ctx, conn, metadata)
 	if hErr != nil {
 		conn.Close()

+ 59 - 35
inbound/tls.go

@@ -1,12 +1,14 @@
 package inbound
 
 import (
+	"context"
 	"crypto/tls"
 	"os"
 
 	"github.com/sagernet/sing-box/adapter"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
 
 	"github.com/fsnotify/fsnotify"
@@ -17,6 +19,7 @@ var _ adapter.Service = (*TLSConfig)(nil)
 type TLSConfig struct {
 	config          *tls.Config
 	logger          log.Logger
+	acmeService     adapter.Service
 	certificate     []byte
 	key             []byte
 	certificatePath string
@@ -29,14 +32,18 @@ func (c *TLSConfig) Config() *tls.Config {
 }
 
 func (c *TLSConfig) Start() error {
-	if c.certificatePath == "" && c.keyPath == "" {
+	if c.acmeService != nil {
+		return c.acmeService.Start()
+	} else {
+		if c.certificatePath == "" && c.keyPath == "" {
+			return nil
+		}
+		err := c.startWatcher()
+		if err != nil {
+			c.logger.Warn("create fsnotify watcher: ", err)
+		}
 		return nil
 	}
-	err := c.startWatcher()
-	if err != nil {
-		c.logger.Warn("create fsnotify watcher: ", err)
-	}
-	return nil
 }
 
 func (c *TLSConfig) startWatcher() error {
@@ -109,17 +116,31 @@ func (c *TLSConfig) reloadKeyPair() error {
 }
 
 func (c *TLSConfig) Close() error {
+	if c.acmeService != nil {
+		return c.acmeService.Close()
+	}
 	if c.watcher != nil {
 		return c.watcher.Close()
 	}
 	return nil
 }
 
-func NewTLSConfig(logger log.Logger, options option.InboundTLSOptions) (*TLSConfig, error) {
+func NewTLSConfig(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*TLSConfig, error) {
 	if !options.Enabled {
 		return nil, nil
 	}
-	var tlsConfig tls.Config
+	var tlsConfig *tls.Config
+	var acmeService adapter.Service
+	var err error
+	if options.ACME != nil && len(options.ACME.Domain) > 0 {
+		tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		tlsConfig = &tls.Config{}
+	}
+	tlsConfig.NextProtos = []string{}
 	if options.ServerName != "" {
 		tlsConfig.ServerName = options.ServerName
 	}
@@ -153,39 +174,42 @@ func NewTLSConfig(logger log.Logger, options option.InboundTLSOptions) (*TLSConf
 		}
 	}
 	var certificate []byte
-	if options.Certificate != "" {
-		certificate = []byte(options.Certificate)
-	} else if options.CertificatePath != "" {
-		content, err := os.ReadFile(options.CertificatePath)
-		if err != nil {
-			return nil, E.Cause(err, "read certificate")
-		}
-		certificate = content
-	}
 	var key []byte
-	if options.Key != "" {
-		key = []byte(options.Key)
-	} else if options.KeyPath != "" {
-		content, err := os.ReadFile(options.KeyPath)
+	if acmeService == nil {
+		if options.Certificate != "" {
+			certificate = []byte(options.Certificate)
+		} else if options.CertificatePath != "" {
+			content, err := os.ReadFile(options.CertificatePath)
+			if err != nil {
+				return nil, E.Cause(err, "read certificate")
+			}
+			certificate = content
+		}
+		if options.Key != "" {
+			key = []byte(options.Key)
+		} else if options.KeyPath != "" {
+			content, err := os.ReadFile(options.KeyPath)
+			if err != nil {
+				return nil, E.Cause(err, "read key")
+			}
+			key = content
+		}
+		if certificate == nil {
+			return nil, E.New("missing certificate")
+		}
+		if key == nil {
+			return nil, E.New("missing key")
+		}
+		keyPair, err := tls.X509KeyPair(certificate, key)
 		if err != nil {
-			return nil, E.Cause(err, "read key")
+			return nil, E.Cause(err, "parse x509 key pair")
 		}
-		key = content
-	}
-	if certificate == nil {
-		return nil, E.New("missing certificate")
-	}
-	if key == nil {
-		return nil, E.New("missing key")
-	}
-	keyPair, err := tls.X509KeyPair(certificate, key)
-	if err != nil {
-		return nil, E.Cause(err, "parse x509 key pair")
+		tlsConfig.Certificates = []tls.Certificate{keyPair}
 	}
-	tlsConfig.Certificates = []tls.Certificate{keyPair}
 	return &TLSConfig{
-		config:          &tlsConfig,
+		config:          tlsConfig,
 		logger:          logger,
+		acmeService:     acmeService,
 		certificate:     certificate,
 		key:             key,
 		certificatePath: options.CertificatePath,

+ 66 - 0
inbound/tls_acme.go

@@ -0,0 +1,66 @@
+//go:build with_acme
+
+package inbound
+
+import (
+	"context"
+	"crypto/tls"
+	"strings"
+
+	"github.com/sagernet/certmagic"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+type acmeWrapper struct {
+	ctx    context.Context
+	cfg    *certmagic.Config
+	domain []string
+}
+
+func (w *acmeWrapper) Start() error {
+	return w.cfg.ManageSync(w.ctx, w.domain)
+}
+
+func (w *acmeWrapper) Close() error {
+	w.cfg.Unmanage(w.domain)
+	return nil
+}
+
+func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
+	var acmeServer string
+	switch options.Provider {
+	case "", "letsencrypt":
+		acmeServer = certmagic.LetsEncryptProductionCA
+	case "zerossl":
+		acmeServer = certmagic.ZeroSSLProductionCA
+	default:
+		if !strings.HasPrefix(options.Provider, "https://") {
+			return nil, nil, E.New("unsupported acme provider: " + options.Provider)
+		}
+		acmeServer = options.Provider
+	}
+	var storage certmagic.Storage
+	if options.DataDirectory != "" {
+		storage = &certmagic.FileStorage{
+			Path: options.DataDirectory,
+		}
+	}
+	config := certmagic.New(certmagic.NewCache(certmagic.CacheOptions{}), certmagic.Config{
+		DefaultServerName: options.DefaultServerName,
+		Issuers: []certmagic.Issuer{
+			&certmagic.ACMEIssuer{
+				CA:                      acmeServer,
+				Email:                   options.Email,
+				Agreed:                  true,
+				DisableHTTPChallenge:    options.DisableHTTPChallenge,
+				DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
+				AltHTTPPort:             int(options.AlternativeHTTPPort),
+				AltTLSALPNPort:          int(options.AlternativeTLSPort),
+			},
+		},
+		Storage: storage,
+	})
+	return config.TLSConfig(), &acmeWrapper{ctx, config, options.Domain}, nil
+}

+ 16 - 0
inbound/tls_acme_stub.go

@@ -0,0 +1,16 @@
+//go:build !with_acme
+
+package inbound
+
+import (
+	"context"
+	"crypto/tls"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
+	return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
+}

+ 1 - 1
inbound/trojan.go

@@ -50,7 +50,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
 		return nil, err
 	}
 	if options.TLS != nil {
-		tlsConfig, err := NewTLSConfig(logger, common.PtrValueOrDefault(options.TLS))
+		tlsConfig, err := NewTLSConfig(ctx, logger, common.PtrValueOrDefault(options.TLS))
 		if err != nil {
 			return nil, err
 		}

+ 1 - 1
inbound/vmess.go

@@ -52,7 +52,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
 		return nil, err
 	}
 	if options.TLS != nil {
-		tlsConfig, err := NewTLSConfig(logger, common.PtrValueOrDefault(options.TLS))
+		tlsConfig, err := NewTLSConfig(ctx, logger, common.PtrValueOrDefault(options.TLS))
 		if err != nil {
 			return nil, err
 		}

+ 2 - 0
mkdocs.yml

@@ -50,6 +50,7 @@ nav:
           - VMess: configuration/inbound/vmess.md
           - Trojan: configuration/inbound/trojan.md
           - Naive: configuration/inbound/naive.md
+          - Hysteria: configuration/inbound/hysteria.md
           - Tun: configuration/inbound/tun.md
           - Redirect: configuration/inbound/redirect.md
           - TProxy: configuration/inbound/tproxy.md
@@ -63,6 +64,7 @@ nav:
           - VMess: configuration/outbound/vmess.md
           - Trojan: configuration/outbound/trojan.md
           - WireGuard: configuration/outbound/wireguard.md
+          - Hysteria: configuration/outbound/hysteria.md
           - DNS: configuration/outbound/dns.md
           - Selector: configuration/outbound/selector.md
       - Route:

+ 34 - 0
option/hysteria.go

@@ -0,0 +1,34 @@
+package option
+
+type HysteriaInboundOptions struct {
+	ListenOptions
+	Up                  string             `json:"up,omitempty"`
+	UpMbps              int                `json:"up_mbps,omitempty"`
+	Down                string             `json:"down,omitempty"`
+	DownMbps            int                `json:"down_mbps,omitempty"`
+	Obfs                string             `json:"obfs,omitempty"`
+	Auth                []byte             `json:"auth,omitempty"`
+	AuthString          string             `json:"auth_str,omitempty"`
+	ReceiveWindowConn   uint64             `json:"recv_window_conn,omitempty"`
+	ReceiveWindowClient uint64             `json:"recv_window_client,omitempty"`
+	MaxConnClient       int                `json:"max_conn_client,omitempty"`
+	DisableMTUDiscovery bool               `json:"disable_mtu_discovery,omitempty"`
+	TLS                 *InboundTLSOptions `json:"tls,omitempty"`
+}
+
+type HysteriaOutboundOptions struct {
+	OutboundDialerOptions
+	ServerOptions
+	Up                  string              `json:"up,omitempty"`
+	UpMbps              int                 `json:"up_mbps,omitempty"`
+	Down                string              `json:"down,omitempty"`
+	DownMbps            int                 `json:"down_mbps,omitempty"`
+	Obfs                string              `json:"obfs,omitempty"`
+	Auth                []byte              `json:"auth,omitempty"`
+	AuthString          string              `json:"auth_str,omitempty"`
+	ReceiveWindowConn   uint64              `json:"recv_window_conn,omitempty"`
+	ReceiveWindow       uint64              `json:"recv_window,omitempty"`
+	DisableMTUDiscovery bool                `json:"disable_mtu_discovery,omitempty"`
+	Network             NetworkList         `json:"network,omitempty"`
+	TLS                 *OutboundTLSOptions `json:"tls,omitempty"`
+}

+ 5 - 0
option/inbound.go

@@ -20,6 +20,7 @@ type _Inbound struct {
 	VMessOptions       VMessInboundOptions       `json:"-"`
 	TrojanOptions      TrojanInboundOptions      `json:"-"`
 	NaiveOptions       NaiveInboundOptions       `json:"-"`
+	HysteriaOptions    HysteriaInboundOptions    `json:"-"`
 }
 
 type Inbound _Inbound
@@ -49,6 +50,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
 		v = h.TrojanOptions
 	case C.TypeNaive:
 		v = h.NaiveOptions
+	case C.TypeHysteria:
+		v = h.HysteriaOptions
 	default:
 		return nil, E.New("unknown inbound type: ", h.Type)
 	}
@@ -84,6 +87,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.TrojanOptions
 	case C.TypeNaive:
 		v = &h.NaiveOptions
+	case C.TypeHysteria:
+		v = &h.HysteriaOptions
 	default:
 		return E.New("unknown inbound type: ", h.Type)
 	}

+ 5 - 0
option/outbound.go

@@ -17,6 +17,7 @@ type _Outbound struct {
 	VMessOptions       VMessOutboundOptions       `json:"-"`
 	TrojanOptions      TrojanOutboundOptions      `json:"-"`
 	WireGuardOptions   WireGuardOutboundOptions   `json:"-"`
+	HysteriaOutbound   HysteriaOutboundOptions    `json:"-"`
 	SelectorOptions    SelectorOutboundOptions    `json:"-"`
 }
 
@@ -41,6 +42,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
 		v = h.TrojanOptions
 	case C.TypeWireGuard:
 		v = h.WireGuardOptions
+	case C.TypeHysteria:
+		v = h.HysteriaOutbound
 	case C.TypeSelector:
 		v = h.SelectorOptions
 	default:
@@ -72,6 +75,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.TrojanOptions
 	case C.TypeWireGuard:
 		v = &h.WireGuardOptions
+	case C.TypeHysteria:
+		v = &h.HysteriaOutbound
 	case C.TypeSelector:
 		v = &h.SelectorOptions
 	default:

+ 33 - 20
option/tls.go

@@ -7,29 +7,42 @@ import (
 )
 
 type InboundTLSOptions struct {
-	Enabled         bool     `json:"enabled,omitempty"`
-	ServerName      string   `json:"server_name,omitempty"`
-	ALPN            []string `json:"alpn,omitempty"`
-	MinVersion      string   `json:"min_version,omitempty"`
-	MaxVersion      string   `json:"max_version,omitempty"`
-	CipherSuites    []string `json:"cipher_suites,omitempty"`
-	Certificate     string   `json:"certificate,omitempty"`
-	CertificatePath string   `json:"certificate_path,omitempty"`
-	Key             string   `json:"key,omitempty"`
-	KeyPath         string   `json:"key_path,omitempty"`
+	Enabled         bool                `json:"enabled,omitempty"`
+	ServerName      string              `json:"server_name,omitempty"`
+	ALPN            Listable[string]    `json:"alpn,omitempty"`
+	MinVersion      string              `json:"min_version,omitempty"`
+	MaxVersion      string              `json:"max_version,omitempty"`
+	CipherSuites    Listable[string]    `json:"cipher_suites,omitempty"`
+	Certificate     string              `json:"certificate,omitempty"`
+	CertificatePath string              `json:"certificate_path,omitempty"`
+	Key             string              `json:"key,omitempty"`
+	KeyPath         string              `json:"key_path,omitempty"`
+	ACME            *InboundACMEOptions `json:"acme,omitempty"`
+}
+
+type InboundACMEOptions struct {
+	Domain                  Listable[string] `json:"domain,omitempty"`
+	DataDirectory           string           `json:"data_directory,omitempty"`
+	DefaultServerName       string           `json:"default_server_name,omitempty"`
+	Email                   string           `json:"email,omitempty"`
+	Provider                string           `json:"provider,omitempty"`
+	DisableHTTPChallenge    bool             `json:"disable_http_challenge,omitempty"`
+	DisableTLSALPNChallenge bool             `json:"disable_tls_alpn_challenge,omitempty"`
+	AlternativeHTTPPort     uint16           `json:"alternative_http_port,omitempty"`
+	AlternativeTLSPort      uint16           `json:"alternative_tls_port,omitempty"`
 }
 
 type OutboundTLSOptions struct {
-	Enabled         bool     `json:"enabled,omitempty"`
-	DisableSNI      bool     `json:"disable_sni,omitempty"`
-	ServerName      string   `json:"server_name,omitempty"`
-	Insecure        bool     `json:"insecure,omitempty"`
-	ALPN            []string `json:"alpn,omitempty"`
-	MinVersion      string   `json:"min_version,omitempty"`
-	MaxVersion      string   `json:"max_version,omitempty"`
-	CipherSuites    []string `json:"cipher_suites,omitempty"`
-	Certificate     string   `json:"certificate,omitempty"`
-	CertificatePath string   `json:"certificate_path,omitempty"`
+	Enabled         bool             `json:"enabled,omitempty"`
+	DisableSNI      bool             `json:"disable_sni,omitempty"`
+	ServerName      string           `json:"server_name,omitempty"`
+	Insecure        bool             `json:"insecure,omitempty"`
+	ALPN            Listable[string] `json:"alpn,omitempty"`
+	MinVersion      string           `json:"min_version,omitempty"`
+	MaxVersion      string           `json:"max_version,omitempty"`
+	CipherSuites    Listable[string] `json:"cipher_suites,omitempty"`
+	Certificate     string           `json:"certificate,omitempty"`
+	CertificatePath string           `json:"certificate_path,omitempty"`
 }
 
 func ParseTLSVersion(version string) (uint16, error) {

+ 2 - 0
outbound/builder.go

@@ -33,6 +33,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
 		return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions)
 	case C.TypeWireGuard:
 		return NewWireGuard(ctx, router, logger, options.Tag, options.WireGuardOptions)
+	case C.TypeHysteria:
+		return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOutbound)
 	case C.TypeSelector:
 		return NewSelector(router, logger, options.Tag, options.SelectorOptions)
 	default:

+ 346 - 0
outbound/hysteria.go

@@ -0,0 +1,346 @@
+//go:build with_quic
+
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"net"
+	"sync"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/quic-go/congestion"
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/dialer"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/transport/hysteria"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+var _ adapter.Outbound = (*Hysteria)(nil)
+
+type Hysteria struct {
+	myOutboundAdapter
+	ctx          context.Context
+	dialer       N.Dialer
+	serverAddr   M.Socksaddr
+	tlsConfig    *tls.Config
+	quicConfig   *quic.Config
+	authKey      []byte
+	xplusKey     []byte
+	sendBPS      uint64
+	recvBPS      uint64
+	connAccess   sync.Mutex
+	conn         quic.Connection
+	udpAccess    sync.RWMutex
+	udpSessions  map[uint32]chan *hysteria.UDPMessage
+	udpDefragger hysteria.Defragger
+}
+
+var errTLSRequired = E.New("TLS required")
+
+func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (*Hysteria, error) {
+	if options.TLS == nil || !options.TLS.Enabled {
+		return nil, errTLSRequired
+	}
+	tlsConfig, err := dialer.TLSConfig(options.Server, common.PtrValueOrDefault(options.TLS))
+	if err != nil {
+		return nil, err
+	}
+	tlsConfig.MinVersion = tls.VersionTLS13
+	if len(tlsConfig.NextProtos) == 0 {
+		tlsConfig.NextProtos = []string{hysteria.DefaultALPN}
+	}
+	quicConfig := &quic.Config{
+		InitialStreamReceiveWindow:     options.ReceiveWindowConn,
+		MaxStreamReceiveWindow:         options.ReceiveWindowConn,
+		InitialConnectionReceiveWindow: options.ReceiveWindow,
+		MaxConnectionReceiveWindow:     options.ReceiveWindow,
+		KeepAlivePeriod:                hysteria.KeepAlivePeriod,
+		DisablePathMTUDiscovery:        options.DisableMTUDiscovery,
+		EnableDatagrams:                true,
+	}
+	if options.ReceiveWindowConn == 0 {
+		quicConfig.InitialStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
+		quicConfig.MaxStreamReceiveWindow = hysteria.DefaultStreamReceiveWindow
+	}
+	if options.ReceiveWindow == 0 {
+		quicConfig.InitialConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
+		quicConfig.MaxConnectionReceiveWindow = hysteria.DefaultConnectionReceiveWindow
+	}
+	if quicConfig.MaxIncomingStreams == 0 {
+		quicConfig.MaxIncomingStreams = hysteria.DefaultMaxIncomingStreams
+	}
+	var auth []byte
+	if len(options.Auth) > 0 {
+		auth = options.Auth
+	} else {
+		auth = []byte(options.AuthString)
+	}
+	var xplus []byte
+	if options.Obfs != "" {
+		xplus = []byte(options.Obfs)
+	}
+	var up, down uint64
+	if len(options.Up) > 0 {
+		up = hysteria.StringToBps(options.Up)
+		if up == 0 {
+			return nil, E.New("invalid up speed format: ", options.Up)
+		}
+	} else {
+		up = uint64(options.UpMbps) * hysteria.MbpsToBps
+	}
+	if len(options.Down) > 0 {
+		down = hysteria.StringToBps(options.Down)
+		if down == 0 {
+			return nil, E.New("invalid down speed format: ", options.Down)
+		}
+	} else {
+		down = uint64(options.DownMbps) * hysteria.MbpsToBps
+	}
+	if up < hysteria.MinSpeedBPS {
+		return nil, E.New("invalid up speed")
+	}
+	if down < hysteria.MinSpeedBPS {
+		return nil, E.New("invalid down speed")
+	}
+	return &Hysteria{
+		myOutboundAdapter: myOutboundAdapter{
+			protocol: C.TypeHysteria,
+			network:  options.Network.Build(),
+			router:   router,
+			logger:   logger,
+			tag:      tag,
+		},
+		ctx:        ctx,
+		dialer:     dialer.NewOutbound(router, options.OutboundDialerOptions),
+		serverAddr: options.ServerOptions.Build(),
+		tlsConfig:  tlsConfig,
+		quicConfig: quicConfig,
+		authKey:    auth,
+		xplusKey:   xplus,
+		sendBPS:    up,
+		recvBPS:    down,
+	}, nil
+}
+
+func (h *Hysteria) offer(ctx context.Context) (quic.Connection, error) {
+	conn := h.conn
+	if conn != nil && !common.Done(conn.Context()) {
+		return conn, nil
+	}
+	h.connAccess.Lock()
+	defer h.connAccess.Unlock()
+	h.udpAccess.Lock()
+	defer h.udpAccess.Unlock()
+	conn = h.conn
+	if conn != nil && !common.Done(conn.Context()) {
+		return conn, nil
+	}
+	conn, err := h.offerNew(ctx)
+	if err != nil {
+		return nil, err
+	}
+	h.conn = conn
+	if common.Contains(h.network, N.NetworkUDP) {
+		for _, session := range h.udpSessions {
+			close(session)
+		}
+		h.udpSessions = make(map[uint32]chan *hysteria.UDPMessage)
+		h.udpDefragger = hysteria.Defragger{}
+		go h.udpRecvLoop(conn)
+	}
+	return conn, nil
+}
+
+func (h *Hysteria) offerNew(ctx context.Context) (quic.Connection, error) {
+	udpConn, err := h.dialer.DialContext(h.ctx, "udp", h.serverAddr)
+	if err != nil {
+		return nil, err
+	}
+	var packetConn net.PacketConn
+	packetConn = bufio.NewUnbindPacketConn(udpConn)
+	if h.xplusKey != nil {
+		packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
+	}
+	packetConn = &hysteria.PacketConnWrapper{PacketConn: packetConn}
+	quicConn, err := quic.Dial(packetConn, udpConn.RemoteAddr(), h.serverAddr.AddrString(), h.tlsConfig, h.quicConfig)
+	if err != nil {
+		packetConn.Close()
+		return nil, err
+	}
+	controlStream, err := quicConn.OpenStreamSync(ctx)
+	if err != nil {
+		packetConn.Close()
+		return nil, err
+	}
+	err = hysteria.WriteClientHello(controlStream, hysteria.ClientHello{
+		SendBPS: h.sendBPS,
+		RecvBPS: h.recvBPS,
+		Auth:    h.authKey,
+	})
+	if err != nil {
+		packetConn.Close()
+		return nil, err
+	}
+	serverHello, err := hysteria.ReadServerHello(controlStream)
+	if err != nil {
+		packetConn.Close()
+		return nil, err
+	}
+	if !serverHello.OK {
+		packetConn.Close()
+		return nil, E.New("remote error: ", serverHello.Message)
+	}
+	quicConn.SetCongestionControl(hysteria.NewBrutalSender(congestion.ByteCount(serverHello.RecvBPS)))
+	return quicConn, nil
+}
+
+func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
+	for {
+		packet, err := conn.ReceiveMessage()
+		if err != nil {
+			return
+		}
+		message, err := hysteria.ParseUDPMessage(packet)
+		if err != nil {
+			h.logger.Error("parse udp message: ", err)
+			continue
+		}
+		dfMsg := h.udpDefragger.Feed(message)
+		if dfMsg == nil {
+			continue
+		}
+		h.udpAccess.RLock()
+		ch, ok := h.udpSessions[dfMsg.SessionID]
+		if ok {
+			select {
+			case ch <- dfMsg:
+				// OK
+			default:
+				// Silently drop the message when the channel is full
+			}
+		}
+		h.udpAccess.RUnlock()
+	}
+}
+
+func (h *Hysteria) Close() error {
+	h.connAccess.Lock()
+	defer h.connAccess.Unlock()
+	h.udpAccess.Lock()
+	defer h.udpAccess.Unlock()
+	if h.conn != nil {
+		h.conn.CloseWithError(0, "")
+	}
+	for _, session := range h.udpSessions {
+		close(session)
+	}
+	h.udpSessions = make(map[uint32]chan *hysteria.UDPMessage)
+	return nil
+}
+
+func (h *Hysteria) open(ctx context.Context) (quic.Connection, quic.Stream, error) {
+	conn, err := h.offer(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+	stream, err := conn.OpenStream()
+	if err != nil {
+		return nil, nil, err
+	}
+	return conn, &hysteria.StreamWrapper{Stream: stream}, nil
+}
+
+func (h *Hysteria) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+		h.logger.InfoContext(ctx, "outbound connection to ", destination)
+		_, stream, err := h.open(ctx)
+		if err != nil {
+			return nil, err
+		}
+		err = hysteria.WriteClientRequest(stream, hysteria.ClientRequest{
+			Host: destination.AddrString(),
+			Port: destination.Port,
+		})
+		if err != nil {
+			stream.Close()
+			return nil, err
+		}
+		response, err := hysteria.ReadServerResponse(stream)
+		if err != nil {
+			stream.Close()
+			return nil, err
+		}
+		if !response.OK {
+			stream.Close()
+			return nil, E.New("remote error: ", response.Message)
+		}
+		return hysteria.NewConn(stream, destination), nil
+	case N.NetworkUDP:
+		conn, err := h.ListenPacket(ctx, destination)
+		if err != nil {
+			return nil, err
+		}
+		return conn.(*hysteria.PacketConn), nil
+	default:
+		return nil, E.New("unsupported network: ", network)
+	}
+}
+
+func (h *Hysteria) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
+	conn, stream, err := h.open(ctx)
+	if err != nil {
+		return nil, err
+	}
+	err = hysteria.WriteClientRequest(stream, hysteria.ClientRequest{
+		UDP:  true,
+		Host: destination.AddrString(),
+		Port: destination.Port,
+	})
+	if err != nil {
+		stream.Close()
+		return nil, err
+	}
+	var response *hysteria.ServerResponse
+	response, err = hysteria.ReadServerResponse(stream)
+	if err != nil {
+		stream.Close()
+		return nil, err
+	}
+	if !response.OK {
+		stream.Close()
+		return nil, E.New("remote error: ", response.Message)
+	}
+	h.udpAccess.Lock()
+	nCh := make(chan *hysteria.UDPMessage, 1024)
+	h.udpSessions[response.UDPSessionID] = nCh
+	h.udpAccess.Unlock()
+	packetConn := hysteria.NewPacketConn(conn, stream, response.UDPSessionID, destination, nCh, common.Closer(func() error {
+		h.udpAccess.Lock()
+		if ch, ok := h.udpSessions[response.UDPSessionID]; ok {
+			close(ch)
+			delete(h.udpSessions, response.UDPSessionID)
+		}
+		h.udpAccess.Unlock()
+		return nil
+	}))
+	go packetConn.Hold()
+	return packetConn, nil
+}
+
+func (h *Hysteria) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+	return NewConnection(ctx, h, conn, metadata)
+}
+
+func (h *Hysteria) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+	return NewPacketConnection(ctx, h, conn, metadata)
+}

+ 16 - 0
outbound/hysteria_stub.go

@@ -0,0 +1,16 @@
+//go:build !with_quic
+
+package outbound
+
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) {
+	return nil, E.New(`QUIC is not included in this build, rebuild with -tags with_quic`)
+}

+ 20 - 30
test/box_test.go

@@ -35,15 +35,31 @@ func startInstance(t *testing.T, options option.Options) {
 	})
 }
 
+func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
+	dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
+	dialTCP := func() (net.Conn, error) {
+		return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
+	}
+	dialUDP := func() (net.PacketConn, error) {
+		return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
+	}
+	// require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
+	// require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
+	require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
+	require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
+
+	// require.NoError(t, testPacketConnTimeout(t, dialUDP))
+}
+
 func testTCP(t *testing.T, clientPort uint16, testPort uint16) {
 	dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
 	dialTCP := func() (net.Conn, error) {
 		return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
 	}
-	require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
+	require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
 }
 
-func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
+func testSuitHy(t *testing.T, clientPort uint16, testPort uint16) {
 	dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
 	dialTCP := func() (net.Conn, error) {
 		return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
@@ -51,34 +67,8 @@ func testSuit(t *testing.T, clientPort uint16, testPort uint16) {
 	dialUDP := func() (net.PacketConn, error) {
 		return dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
 	}
-	/*t.Run("tcp", func(t *testing.T) {
-		t.Parallel()
-		var err error
-		for retry := 0; retry < 3; retry++ {
-			err = testLargeDataWithConn(t, testPort, dialTCP)
-			if err == nil {
-				break
-			}
-		}
-		require.NoError(t, err)
-	})
-	t.Run("udp", func(t *testing.T) {
-		t.Parallel()
-		var err error
-		for retry := 0; retry < 3; retry++ {
-			err = testLargeDataWithPacketConn(t, testPort, dialUDP)
-			if err == nil {
-				break
-			}
-		}
-		require.NoError(t, err)
-	})*/
-	//require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
-	//require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
-	require.NoError(t, testLargeDataWithConn(t, testPort, dialTCP))
-	require.NoError(t, testLargeDataWithPacketConn(t, testPort, dialUDP))
-
-	// require.NoError(t, testPacketConnTimeout(t, dialUDP))
+	require.NoError(t, testPingPongWithConn(t, testPort, dialTCP))
+	require.NoError(t, testPingPongWithPacketConn(t, testPort, dialUDP))
 }
 
 func testSuitWg(t *testing.T, clientPort uint16, testPort uint16) {

+ 10 - 1
test/clash_test.go

@@ -7,6 +7,8 @@ import (
 	"errors"
 	"io"
 	"net"
+	"net/http"
+	_ "net/http/pprof"
 	"net/netip"
 	"sync"
 	"testing"
@@ -32,6 +34,7 @@ const (
 	ImageTrojan                = "trojangfw/trojan:latest"
 	ImageNaive                 = "pocat/naiveproxy:client"
 	ImageBoringTun             = "ghcr.io/ntkme/boringtun:edge"
+	ImageHysteria              = "tobyxdd/hysteria:latest"
 )
 
 var allImages = []string{
@@ -41,6 +44,7 @@ var allImages = []string{
 	ImageTrojan,
 	ImageNaive,
 	ImageBoringTun,
+	ImageHysteria,
 }
 
 var localIP = netip.MustParseAddr("127.0.0.1")
@@ -89,6 +93,12 @@ func init() {
 
 		io.Copy(io.Discard, imageStream)
 	}
+	go func() {
+		err = http.ListenAndServe("0.0.0.0:8965", nil)
+		if err != nil {
+			log.Debug(err)
+		}
+	}()
 }
 
 func newPingPongPair() (chan []byte, chan []byte, func(t *testing.T) error) {
@@ -379,7 +389,6 @@ func testLargeDataWithPacketConn(t *testing.T, port uint16, pcc func() (net.Pack
 			mux.Lock()
 			hashMap[i] = hash[:]
 			mux.Unlock()
-			println("write ti ", addr.String())
 			if _, err = pc.WriteTo(buf, addr); err != nil {
 				t.Log(err)
 				continue

+ 12 - 0
test/config/hysteria-client.json

@@ -0,0 +1,12 @@
+{
+  "server": "127.0.0.1:10000",
+  "auth_str": "password",
+  "obfs": "fuck me till the daylight",
+  "up_mbps": 100,
+  "down_mbps": 100,
+  "socks5": {
+    "listen": "127.0.0.1:10001"
+  },
+  "server_name": "example.org",
+  "ca": "/etc/hysteria/ca.pem"
+}

+ 9 - 0
test/config/hysteria-server.json

@@ -0,0 +1,9 @@
+{
+  "listen": ":10000",
+  "cert": "/etc/hysteria/cert.pem",
+  "key": "/etc/hysteria/key.pem",
+  "auth_str": "password",
+  "obfs": "fuck me till the daylight",
+  "up_mbps": 100,
+  "down_mbps": 100
+}

+ 7 - 3
test/go.mod

@@ -10,7 +10,7 @@ require (
 	github.com/docker/docker v20.10.17+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/gofrs/uuid v4.2.0+incompatible
-	github.com/sagernet/sing v0.0.0-20220819003212-2424b1e2fac1
+	github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6
 	github.com/spyzhov/ajson v0.7.1
 	github.com/stretchr/testify v1.8.0
@@ -34,12 +34,13 @@ require (
 	github.com/google/btree v1.0.1 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
-	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
-	github.com/kr/text v0.1.0 // indirect
+	github.com/klauspost/cpuid/v2 v2.1.0 // indirect
+	github.com/libdns/libdns v0.2.1 // indirect
 	github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
 	github.com/marten-seemann/qpack v0.2.1 // indirect
 	github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
 	github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
+	github.com/mholt/acmez v1.0.4 // indirect
 	github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
@@ -49,6 +50,7 @@ require (
 	github.com/oschwald/maxminddb-golang v1.10.0 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a // indirect
 	github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
 	github.com/sagernet/netlink v0.0.0-20220816152750-7a75378bd31a // indirect
 	github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb // indirect
@@ -59,6 +61,8 @@ require (
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
+	go.uber.org/multierr v1.6.0 // indirect
+	go.uber.org/zap v1.22.0 // indirect
 	golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect

+ 34 - 4
test/go.sum

@@ -4,6 +4,8 @@ github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6
 github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
 github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/database64128/tfo-go v1.1.1 h1:jcaCQBkEZZxV1t2wfOwt41WJKzgcNtLV7nGOm+hmZ3w=
 github.com/database64128/tfo-go v1.1.1/go.mod h1:b1wrRNZr7NKZhWQ8LSTvqo1r2ppLdYXZLIUDCPOgJrI=
@@ -59,12 +61,15 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
-github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
+github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
+github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
 github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
 github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
@@ -73,6 +78,8 @@ github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKA
 github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
 github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
 github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
+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/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
 github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -99,6 +106,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 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/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a h1:SE3Xn4GOQ+kxbgGa2Xp0H2CCsx1o2pVTt0f+hmfuHH4=
+github.com/sagernet/certmagic v0.0.0-20220819042630-4a57f8b6853a/go.mod h1:Q+ZXyesnkjV5B70B1ixk65ecKrlJ2jz0atv3fPKsVVo=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
 github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
 github.com/sagernet/netlink v0.0.0-20220816152750-7a75378bd31a h1:iNtsfGMenajBUGZ/1yAzl1v3p+t/7IJ/ilQXq9haRZ8=
@@ -107,8 +116,8 @@ github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb h1:wc0yQ+SBn4TaTY
 github.com/sagernet/quic-go v0.0.0-20220818150011-de611ab3e2bb/go.mod h1:MIccjRKnPTjWwAOpl+AUGWOkzyTd9tERytudxu+1ra4=
 github.com/sagernet/sing v0.0.0-20220812082120-05f9836bff8f/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
-github.com/sagernet/sing v0.0.0-20220819003212-2424b1e2fac1 h1:+YC0/ygsJc4Z8qhd7ypsbWgMSm+UWN+QK+PW7I19K4Q=
-github.com/sagernet/sing v0.0.0-20220819003212-2424b1e2fac1/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
+github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0 h1:jFtynAm5qU1WXIs4FBxi7nLtTVMNXIv/hgO0V/BxmuE=
+github.com/sagernet/sing v0.0.0-20220819041823-35c336a016c0/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
 github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9 h1:XgXSOJv8e7+98SJvg1f0luuPR33r4yFcmzxb3R//BTI=
 github.com/sagernet/sing-dns v0.0.0-20220819010310-839eab1578c9/go.mod h1:MAHy2IKZAA101t3Gr2x0ldwn6XuAs2cjGzSzHy5RhWk=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220819002358-7461bb09a8f6 h1:JJfDeYYhWunvtxsU/mOVNTmFQmnzGx9dY034qG6G3g4=
@@ -128,7 +137,9 @@ github.com/spyzhov/ajson v0.7.1/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzy
 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/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -137,8 +148,16 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
 go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0=
+go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -146,6 +165,7 @@ golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUH
 golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -160,6 +180,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
 golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -184,12 +205,16 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 h1:fqTvyMIIj+HRzMmnzr9NtpHP6uVpvB5fkHcgPDC4nu8=
 golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -198,6 +223,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -205,6 +231,7 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -225,15 +252,18 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=

+ 178 - 0
test/hysteria_test.go

@@ -0,0 +1,178 @@
+package main
+
+import (
+	"net/netip"
+	"testing"
+
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+)
+
+func TestHysteriaSelf(t *testing.T) {
+	if !C.QUIC_AVAILABLE {
+		t.Skip("QUIC not included")
+	}
+	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+	startInstance(t, option.Options{
+		Log: &option.LogOptions{
+			Level: "trace",
+		},
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				Tag:  "mixed-in",
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+			{
+				Type: C.TypeHysteria,
+				HysteriaOptions: option.HysteriaInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					UpMbps:     100,
+					DownMbps:   100,
+					AuthString: "password",
+					Obfs:       "fuck me till the daylight",
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeDirect,
+			},
+			{
+				Type: C.TypeHysteria,
+				Tag:  "hy-out",
+				HysteriaOutbound: option.HysteriaOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UpMbps:     100,
+					DownMbps:   100,
+					AuthString: "password",
+					Obfs:       "fuck me till the daylight",
+					CustomCA:   caPem,
+					ServerName: "example.org",
+				},
+			},
+		},
+		Route: &option.RouteOptions{
+			Rules: []option.Rule{
+				{
+					DefaultOptions: option.DefaultRule{
+						Inbound:  []string{"mixed-in"},
+						Outbound: "hy-out",
+					},
+				},
+			},
+		},
+	})
+	testSuitHy(t, clientPort, testPort)
+}
+
+func TestHysteriaInbound(t *testing.T) {
+	if !C.QUIC_AVAILABLE {
+		t.Skip("QUIC not included")
+	}
+	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+	startInstance(t, option.Options{
+		Log: &option.LogOptions{
+			Level: "trace",
+		},
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeHysteria,
+				HysteriaOptions: option.HysteriaInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: serverPort,
+					},
+					UpMbps:     100,
+					DownMbps:   100,
+					AuthString: "password",
+					Obfs:       "fuck me till the daylight",
+					TLS: &option.InboundTLSOptions{
+						Enabled:         true,
+						ServerName:      "example.org",
+						CertificatePath: certPem,
+						KeyPath:         keyPem,
+					},
+				},
+			},
+		},
+	})
+	startDockerContainer(t, DockerOptions{
+		Image: ImageHysteria,
+		Ports: []uint16{serverPort, clientPort},
+		Cmd:   []string{"-c", "/etc/hysteria/config.json", "client"},
+		Bind: map[string]string{
+			"hysteria-client.json": "/etc/hysteria/config.json",
+			caPem:                  "/etc/hysteria/ca.pem",
+		},
+	})
+	testSuit(t, clientPort, testPort)
+}
+
+func TestHysteriaOutbound(t *testing.T) {
+	if !C.QUIC_AVAILABLE {
+		t.Skip("QUIC not included")
+	}
+	caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
+	startDockerContainer(t, DockerOptions{
+		Image: ImageHysteria,
+		Ports: []uint16{serverPort, testPort},
+		Cmd:   []string{"-c", "/etc/hysteria/config.json", "server"},
+		Bind: map[string]string{
+			"hysteria-server.json": "/etc/hysteria/config.json",
+			certPem:                "/etc/hysteria/cert.pem",
+			keyPem:                 "/etc/hysteria/key.pem",
+		},
+	})
+	startInstance(t, option.Options{
+		Log: &option.LogOptions{
+			Level: "trace",
+		},
+		Inbounds: []option.Inbound{
+			{
+				Type: C.TypeMixed,
+				MixedOptions: option.HTTPMixedInboundOptions{
+					ListenOptions: option.ListenOptions{
+						Listen:     option.ListenAddress(netip.IPv4Unspecified()),
+						ListenPort: clientPort,
+					},
+				},
+			},
+		},
+		Outbounds: []option.Outbound{
+			{
+				Type: C.TypeHysteria,
+				HysteriaOutbound: option.HysteriaOutboundOptions{
+					ServerOptions: option.ServerOptions{
+						Server:     "127.0.0.1",
+						ServerPort: serverPort,
+					},
+					UpMbps:     100,
+					DownMbps:   100,
+					AuthString: "password",
+					Obfs:       "fuck me till the daylight",
+					CustomCA:   caPem,
+					ServerName: "example.org",
+				},
+			},
+		},
+	})
+	testSuitHy(t, clientPort, testPort)
+}

+ 149 - 0
transport/hysteria/brutal.go

@@ -0,0 +1,149 @@
+package hysteria
+
+import (
+	"time"
+
+	"github.com/sagernet/quic-go/congestion"
+)
+
+const (
+	initMaxDatagramSize = 1252
+
+	pktInfoSlotCount = 4
+	minSampleCount   = 50
+	minAckRate       = 0.8
+)
+
+type BrutalSender struct {
+	rttStats        congestion.RTTStatsProvider
+	bps             congestion.ByteCount
+	maxDatagramSize congestion.ByteCount
+	pacer           *pacer
+
+	pktInfoSlots [pktInfoSlotCount]pktInfo
+	ackRate      float64
+}
+
+type pktInfo struct {
+	Timestamp int64
+	AckCount  uint64
+	LossCount uint64
+}
+
+func NewBrutalSender(bps congestion.ByteCount) *BrutalSender {
+	bs := &BrutalSender{
+		bps:             bps,
+		maxDatagramSize: initMaxDatagramSize,
+		ackRate:         1,
+	}
+	bs.pacer = newPacer(func() congestion.ByteCount {
+		return congestion.ByteCount(float64(bs.bps) / bs.ackRate)
+	})
+	return bs
+}
+
+func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) {
+	b.rttStats = rttStats
+}
+
+func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time {
+	return b.pacer.TimeUntilSend()
+}
+
+func (b *BrutalSender) HasPacingBudget() bool {
+	return b.pacer.Budget(time.Now()) >= b.maxDatagramSize
+}
+
+func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {
+	return bytesInFlight < b.GetCongestionWindow()
+}
+
+func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {
+	rtt := maxDuration(b.rttStats.LatestRTT(), b.rttStats.SmoothedRTT())
+	if rtt <= 0 {
+		return 10240
+	}
+	return congestion.ByteCount(float64(b.bps) * rtt.Seconds() * 1.5 / b.ackRate)
+}
+
+func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount,
+	packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool,
+) {
+	b.pacer.SentPacket(sentTime, bytes)
+}
+
+func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount,
+	priorInFlight congestion.ByteCount, eventTime time.Time,
+) {
+	currentTimestamp := eventTime.Unix()
+	slot := currentTimestamp % pktInfoSlotCount
+	if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
+		b.pktInfoSlots[slot].AckCount++
+	} else {
+		// uninitialized slot or too old, reset
+		b.pktInfoSlots[slot].Timestamp = currentTimestamp
+		b.pktInfoSlots[slot].AckCount = 1
+		b.pktInfoSlots[slot].LossCount = 0
+	}
+	b.updateAckRate(currentTimestamp)
+}
+
+func (b *BrutalSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount,
+	priorInFlight congestion.ByteCount,
+) {
+	currentTimestamp := time.Now().Unix()
+	slot := currentTimestamp % pktInfoSlotCount
+	if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
+		b.pktInfoSlots[slot].LossCount++
+	} else {
+		// uninitialized slot or too old, reset
+		b.pktInfoSlots[slot].Timestamp = currentTimestamp
+		b.pktInfoSlots[slot].AckCount = 0
+		b.pktInfoSlots[slot].LossCount = 1
+	}
+	b.updateAckRate(currentTimestamp)
+}
+
+func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {
+	b.maxDatagramSize = size
+	b.pacer.SetMaxDatagramSize(size)
+}
+
+func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
+	minTimestamp := currentTimestamp - pktInfoSlotCount
+	var ackCount, lossCount uint64
+	for _, info := range b.pktInfoSlots {
+		if info.Timestamp < minTimestamp {
+			continue
+		}
+		ackCount += info.AckCount
+		lossCount += info.LossCount
+	}
+	if ackCount+lossCount < minSampleCount {
+		b.ackRate = 1
+	}
+	rate := float64(ackCount) / float64(ackCount+lossCount)
+	if rate < minAckRate {
+		b.ackRate = minAckRate
+	}
+	b.ackRate = rate
+}
+
+func (b *BrutalSender) InSlowStart() bool {
+	return false
+}
+
+func (b *BrutalSender) InRecovery() bool {
+	return false
+}
+
+func (b *BrutalSender) MaybeExitSlowStart() {}
+
+func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {}
+
+func maxDuration(a, b time.Duration) time.Duration {
+	if a > b {
+		return a
+	}
+	return b
+}

+ 65 - 0
transport/hysteria/frag.go

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

+ 86 - 0
transport/hysteria/pacer.go

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

+ 510 - 0
transport/hysteria/protocol.go

@@ -0,0 +1,510 @@
+package hysteria
+
+import (
+	"bytes"
+	"encoding/binary"
+	"io"
+	"math/rand"
+	"net"
+	"os"
+	"time"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+const (
+	MbpsToBps                      = 125000
+	MinSpeedBPS                    = 16384
+	DefaultStreamReceiveWindow     = 15728640 // 15 MB/s
+	DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
+	DefaultMaxIncomingStreams      = 1024
+	DefaultALPN                    = "hysteria"
+	KeepAlivePeriod                = 10 * time.Second
+)
+
+const Version = 3
+
+type ClientHello struct {
+	SendBPS uint64
+	RecvBPS uint64
+	Auth    []byte
+}
+
+func WriteClientHello(stream io.Writer, hello ClientHello) error {
+	var requestLen int
+	requestLen += 1 // version
+	requestLen += 8 // sendBPS
+	requestLen += 8 // recvBPS
+	requestLen += 2 // auth len
+	requestLen += len(hello.Auth)
+	_request := buf.StackNewSize(requestLen)
+	defer common.KeepAlive(_request)
+	request := common.Dup(_request)
+	defer request.Release()
+	common.Must(
+		request.WriteByte(Version),
+		binary.Write(request, binary.BigEndian, hello.SendBPS),
+		binary.Write(request, binary.BigEndian, hello.RecvBPS),
+		binary.Write(request, binary.BigEndian, uint16(len(hello.Auth))),
+		common.Error(request.Write(hello.Auth)),
+	)
+	return common.Error(stream.Write(request.Bytes()))
+}
+
+func ReadClientHello(reader io.Reader) (*ClientHello, error) {
+	var version uint8
+	err := binary.Read(reader, binary.BigEndian, &version)
+	if err != nil {
+		return nil, err
+	}
+	if version != Version {
+		return nil, E.New("unsupported client version: ", version)
+	}
+	var clientHello ClientHello
+	err = binary.Read(reader, binary.BigEndian, &clientHello.SendBPS)
+	if err != nil {
+		return nil, err
+	}
+	err = binary.Read(reader, binary.BigEndian, &clientHello.RecvBPS)
+	if err != nil {
+		return nil, err
+	}
+	var authLen uint16
+	err = binary.Read(reader, binary.BigEndian, &authLen)
+	if err != nil {
+		return nil, err
+	}
+	clientHello.Auth = make([]byte, authLen)
+	_, err = io.ReadFull(reader, clientHello.Auth)
+	if err != nil {
+		return nil, err
+	}
+	return &clientHello, nil
+}
+
+type ServerHello struct {
+	OK      bool
+	SendBPS uint64
+	RecvBPS uint64
+	Message string
+}
+
+func ReadServerHello(stream io.Reader) (*ServerHello, error) {
+	var responseLen int
+	responseLen += 1 // ok
+	responseLen += 8 // sendBPS
+	responseLen += 8 // recvBPS
+	responseLen += 2 // message len
+	_response := buf.StackNewSize(responseLen)
+	defer common.KeepAlive(_response)
+	response := common.Dup(_response)
+	defer response.Release()
+	_, err := response.ReadFullFrom(stream, responseLen)
+	if err != nil {
+		return nil, err
+	}
+	var serverHello ServerHello
+	serverHello.OK = response.Byte(0) == 1
+	serverHello.SendBPS = binary.BigEndian.Uint64(response.Range(1, 9))
+	serverHello.RecvBPS = binary.BigEndian.Uint64(response.Range(9, 17))
+	messageLen := binary.BigEndian.Uint16(response.Range(17, 19))
+	if messageLen == 0 {
+		return &serverHello, nil
+	}
+	message := make([]byte, messageLen)
+	_, err = io.ReadFull(stream, message)
+	if err != nil {
+		return nil, err
+	}
+	serverHello.Message = string(message)
+	return &serverHello, nil
+}
+
+func WriteServerHello(stream io.Writer, hello ServerHello) error {
+	var responseLen int
+	responseLen += 1 // ok
+	responseLen += 8 // sendBPS
+	responseLen += 8 // recvBPS
+	responseLen += 2 // message len
+	responseLen += len(hello.Message)
+	_response := buf.StackNewSize(responseLen)
+	defer common.KeepAlive(_response)
+	response := common.Dup(_response)
+	defer response.Release()
+	if hello.OK {
+		common.Must(response.WriteByte(1))
+	} else {
+		common.Must(response.WriteByte(0))
+	}
+	common.Must(
+		binary.Write(response, binary.BigEndian, hello.SendBPS),
+		binary.Write(response, binary.BigEndian, hello.RecvBPS),
+		binary.Write(response, binary.BigEndian, uint16(len(hello.Message))),
+		common.Error(response.WriteString(hello.Message)),
+	)
+	return common.Error(stream.Write(response.Bytes()))
+}
+
+type ClientRequest struct {
+	UDP  bool
+	Host string
+	Port uint16
+}
+
+func ReadClientRequest(stream io.Reader) (*ClientRequest, error) {
+	var clientRequest ClientRequest
+	err := binary.Read(stream, binary.BigEndian, &clientRequest.UDP)
+	if err != nil {
+		return nil, err
+	}
+	var hostLen uint16
+	err = binary.Read(stream, binary.BigEndian, &hostLen)
+	if err != nil {
+		return nil, err
+	}
+	host := make([]byte, hostLen)
+	_, err = io.ReadFull(stream, host)
+	if err != nil {
+		return nil, err
+	}
+	clientRequest.Host = string(host)
+	err = binary.Read(stream, binary.BigEndian, &clientRequest.Port)
+	if err != nil {
+		return nil, err
+	}
+	return &clientRequest, nil
+}
+
+func WriteClientRequest(stream io.Writer, request ClientRequest) error {
+	var requestLen int
+	requestLen += 1 // udp
+	requestLen += 2 // host len
+	requestLen += len(request.Host)
+	requestLen += 2 // port
+	_buffer := buf.StackNewSize(requestLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	if request.UDP {
+		common.Must(buffer.WriteByte(1))
+	} else {
+		common.Must(buffer.WriteByte(0))
+	}
+	common.Must(
+		binary.Write(buffer, binary.BigEndian, uint16(len(request.Host))),
+		common.Error(buffer.WriteString(request.Host)),
+		binary.Write(buffer, binary.BigEndian, request.Port),
+	)
+	return common.Error(stream.Write(buffer.Bytes()))
+}
+
+type ServerResponse struct {
+	OK           bool
+	UDPSessionID uint32
+	Message      string
+}
+
+func ReadServerResponse(stream io.Reader) (*ServerResponse, error) {
+	var responseLen int
+	responseLen += 1 // ok
+	responseLen += 4 // udp session id
+	responseLen += 2 // message len
+	_response := buf.StackNewSize(responseLen)
+	defer common.KeepAlive(_response)
+	response := common.Dup(_response)
+	defer response.Release()
+	_, err := response.ReadFullFrom(stream, responseLen)
+	if err != nil {
+		return nil, err
+	}
+	var serverResponse ServerResponse
+	serverResponse.OK = response.Byte(0) == 1
+	serverResponse.UDPSessionID = binary.BigEndian.Uint32(response.Range(1, 5))
+	messageLen := binary.BigEndian.Uint16(response.Range(5, 7))
+	if messageLen == 0 {
+		return &serverResponse, nil
+	}
+	message := make([]byte, messageLen)
+	_, err = io.ReadFull(stream, message)
+	if err != nil {
+		return nil, err
+	}
+	serverResponse.Message = string(message)
+	return &serverResponse, nil
+}
+
+func WriteServerResponse(stream io.Writer, response ServerResponse) error {
+	var responseLen int
+	responseLen += 1 // ok
+	responseLen += 4 // udp session id
+	responseLen += 2 // message len
+	responseLen += len(response.Message)
+	_buffer := buf.StackNewSize(responseLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	if response.OK {
+		common.Must(buffer.WriteByte(1))
+	} else {
+		common.Must(buffer.WriteByte(0))
+	}
+	common.Must(
+		binary.Write(buffer, binary.BigEndian, response.UDPSessionID),
+		binary.Write(buffer, binary.BigEndian, uint16(len(response.Message))),
+		common.Error(buffer.WriteString(response.Message)),
+	)
+	return common.Error(stream.Write(buffer.Bytes()))
+}
+
+type UDPMessage struct {
+	SessionID uint32
+	Host      string
+	Port      uint16
+	MsgID     uint16 // doesn't matter when not fragmented, but must not be 0 when fragmented
+	FragID    uint8  // doesn't matter when not fragmented, starts at 0 when fragmented
+	FragCount uint8  // must be 1 when not fragmented
+	Data      []byte
+}
+
+func (m UDPMessage) HeaderSize() int {
+	return 4 + 2 + len(m.Host) + 2 + 2 + 1 + 1 + 2
+}
+
+func (m UDPMessage) Size() int {
+	return m.HeaderSize() + len(m.Data)
+}
+
+func ParseUDPMessage(packet []byte) (message UDPMessage, err error) {
+	reader := bytes.NewReader(packet)
+	err = binary.Read(reader, binary.BigEndian, &message.SessionID)
+	if err != nil {
+		return
+	}
+	var hostLen uint16
+	err = binary.Read(reader, binary.BigEndian, &hostLen)
+	if err != nil {
+		return
+	}
+	_, err = reader.Seek(int64(hostLen), io.SeekCurrent)
+	if err != nil {
+		return
+	}
+	message.Host = string(packet[6 : 6+hostLen])
+	err = binary.Read(reader, binary.BigEndian, &message.Port)
+	if err != nil {
+		return
+	}
+	err = binary.Read(reader, binary.BigEndian, &message.MsgID)
+	if err != nil {
+		return
+	}
+	err = binary.Read(reader, binary.BigEndian, &message.FragID)
+	if err != nil {
+		return
+	}
+	err = binary.Read(reader, binary.BigEndian, &message.FragCount)
+	if err != nil {
+		return
+	}
+	var dataLen uint16
+	err = binary.Read(reader, binary.BigEndian, &dataLen)
+	if err != nil {
+		return
+	}
+	if reader.Len() != int(dataLen) {
+		err = E.New("invalid data length")
+	}
+	dataOffset := int(reader.Size()) - reader.Len()
+	message.Data = packet[dataOffset:]
+	return
+}
+
+func WriteUDPMessage(conn quic.Connection, message UDPMessage) error {
+	var messageLen int
+	messageLen += 4 // session id
+	messageLen += 2 // host len
+	messageLen += len(message.Host)
+	messageLen += 2 // port
+	messageLen += 2 // msg id
+	messageLen += 1 // frag id
+	messageLen += 1 // frag count
+	messageLen += 2 // data len
+	messageLen += len(message.Data)
+	_buffer := buf.StackNewSize(messageLen)
+	defer common.KeepAlive(_buffer)
+	buffer := common.Dup(_buffer)
+	defer buffer.Release()
+	err := writeUDPMessage(conn, message, buffer)
+	if errSize, ok := err.(quic.ErrMessageToLarge); ok {
+		// need to frag
+		message.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1
+		fragMsgs := FragUDPMessage(message, int(errSize))
+		for _, fragMsg := range fragMsgs {
+			buffer.FullReset()
+			err = writeUDPMessage(conn, fragMsg, buffer)
+			if err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+	return err
+}
+
+func writeUDPMessage(conn quic.Connection, message UDPMessage, buffer *buf.Buffer) error {
+	common.Must(
+		binary.Write(buffer, binary.BigEndian, message.SessionID),
+		binary.Write(buffer, binary.BigEndian, uint16(len(message.Host))),
+		common.Error(buffer.WriteString(message.Host)),
+		binary.Write(buffer, binary.BigEndian, message.Port),
+		binary.Write(buffer, binary.BigEndian, message.MsgID),
+		binary.Write(buffer, binary.BigEndian, message.FragID),
+		binary.Write(buffer, binary.BigEndian, message.FragCount),
+		binary.Write(buffer, binary.BigEndian, uint16(len(message.Data))),
+		common.Error(buffer.Write(message.Data)),
+	)
+	return conn.SendMessage(buffer.Bytes())
+}
+
+var _ net.Conn = (*Conn)(nil)
+
+type Conn struct {
+	quic.Stream
+	destination     M.Socksaddr
+	responseWritten bool
+}
+
+func NewConn(stream quic.Stream, destination M.Socksaddr) *Conn {
+	return &Conn{
+		Stream:      stream,
+		destination: destination,
+	}
+}
+
+func (c *Conn) LocalAddr() net.Addr {
+	return nil
+}
+
+func (c *Conn) RemoteAddr() net.Addr {
+	return c.destination.TCPAddr()
+}
+
+func (c *Conn) ReaderReplaceable() bool {
+	return true
+}
+
+func (c *Conn) WriterReplaceable() bool {
+	return true
+}
+
+func (c *Conn) Upstream() any {
+	return c.Stream
+}
+
+type PacketConn struct {
+	session     quic.Connection
+	stream      quic.Stream
+	sessionId   uint32
+	destination M.Socksaddr
+	msgCh       <-chan *UDPMessage
+	closer      io.Closer
+}
+
+func NewPacketConn(session quic.Connection, stream quic.Stream, sessionId uint32, destination M.Socksaddr, msgCh <-chan *UDPMessage, closer io.Closer) *PacketConn {
+	return &PacketConn{
+		session:     session,
+		stream:      stream,
+		sessionId:   sessionId,
+		destination: destination,
+		msgCh:       msgCh,
+		closer:      closer,
+	}
+}
+
+func (c *PacketConn) Hold() {
+	// Hold the stream until it's closed
+	buf := make([]byte, 1024)
+	for {
+		_, err := c.stream.Read(buf)
+		if err != nil {
+			break
+		}
+	}
+	_ = c.Close()
+}
+
+func (c *PacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
+	msg := <-c.msgCh
+	if msg == nil {
+		err = net.ErrClosed
+		return
+	}
+	err = common.Error(buffer.Write(msg.Data))
+	destination = M.ParseSocksaddrHostPort(msg.Host, msg.Port)
+	return
+}
+
+func (c *PacketConn) ReadPacketThreadSafe() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
+	msg := <-c.msgCh
+	if msg == nil {
+		err = net.ErrClosed
+		return
+	}
+	buffer = buf.As(msg.Data)
+	destination = M.ParseSocksaddrHostPort(msg.Host, msg.Port)
+	return
+}
+
+func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+	return WriteUDPMessage(c.session, UDPMessage{
+		SessionID: c.sessionId,
+		Host:      destination.Unwrap().AddrString(),
+		Port:      destination.Port,
+		FragCount: 1,
+		Data:      buffer.Bytes(),
+	})
+}
+
+func (c *PacketConn) LocalAddr() net.Addr {
+	return nil
+}
+
+func (c *PacketConn) RemoteAddr() net.Addr {
+	return c.destination.UDPAddr()
+}
+
+func (c *PacketConn) SetDeadline(t time.Time) error {
+	return os.ErrInvalid
+}
+
+func (c *PacketConn) SetReadDeadline(t time.Time) error {
+	return os.ErrInvalid
+}
+
+func (c *PacketConn) SetWriteDeadline(t time.Time) error {
+	return os.ErrInvalid
+}
+
+func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	return 0, nil, os.ErrInvalid
+}
+
+func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	return 0, os.ErrInvalid
+}
+
+func (c *PacketConn) Read(b []byte) (n int, err error) {
+	return 0, os.ErrInvalid
+}
+
+func (c *PacketConn) Write(b []byte) (n int, err error) {
+	return 0, os.ErrInvalid
+}
+
+func (c *PacketConn) Close() error {
+	return common.Close(c.stream, c.closer)
+}

+ 36 - 0
transport/hysteria/speed.go

@@ -0,0 +1,36 @@
+package hysteria
+
+import (
+	"regexp"
+	"strconv"
+)
+
+func StringToBps(s string) uint64 {
+	if s == "" {
+		return 0
+	}
+	m := regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`).FindStringSubmatch(s)
+	if m == nil {
+		return 0
+	}
+	var n uint64
+	switch m[2] {
+	case "K":
+		n = 1 << 10
+	case "M":
+		n = 1 << 20
+	case "G":
+		n = 1 << 30
+	case "T":
+		n = 1 << 40
+	default:
+		n = 1
+	}
+	v, _ := strconv.ParseUint(m[1], 10, 64)
+	n = v * n
+	if m[3] == "b" {
+		// Bits, need to convert to bytes
+		n = n >> 3
+	}
+	return n
+}

+ 56 - 0
transport/hysteria/wrap.go

@@ -0,0 +1,56 @@
+package hysteria
+
+import (
+	"net"
+	"os"
+	"syscall"
+
+	"github.com/sagernet/quic-go"
+	"github.com/sagernet/sing/common"
+)
+
+type PacketConnWrapper struct {
+	net.PacketConn
+}
+
+func (c *PacketConnWrapper) SetReadBuffer(bytes int) error {
+	return common.MustCast[*net.UDPConn](c.PacketConn).SetReadBuffer(bytes)
+}
+
+func (c *PacketConnWrapper) SetWriteBuffer(bytes int) error {
+	return common.MustCast[*net.UDPConn](c.PacketConn).SetWriteBuffer(bytes)
+}
+
+func (c *PacketConnWrapper) SyscallConn() (syscall.RawConn, error) {
+	return common.MustCast[*net.UDPConn](c.PacketConn).SyscallConn()
+}
+
+func (c *PacketConnWrapper) File() (f *os.File, err error) {
+	return common.MustCast[*net.UDPConn](c.PacketConn).File()
+}
+
+func (c *PacketConnWrapper) Upstream() any {
+	return c.PacketConn
+}
+
+type StreamWrapper struct {
+	quic.Stream
+}
+
+func (s *StreamWrapper) Upstream() any {
+	return s.Stream
+}
+
+func (s *StreamWrapper) ReaderReplaceable() bool {
+	return true
+}
+
+func (s *StreamWrapper) WriterReplaceable() bool {
+	return true
+}
+
+func (s *StreamWrapper) Close() error {
+	s.CancelRead(0)
+	s.Stream.Close()
+	return nil
+}

+ 119 - 0
transport/hysteria/xplus.go

@@ -0,0 +1,119 @@
+package hysteria
+
+import (
+	"crypto/sha256"
+	"math/rand"
+	"net"
+	"sync"
+	"time"
+
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/bufio"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+)
+
+const xplusSaltLen = 16
+
+var errInalidPacket = E.New("invalid packet")
+
+func NewXPlusPacketConn(conn net.PacketConn, key []byte) net.PacketConn {
+	vectorisedWriter, isVectorised := bufio.CreateVectorisedPacketWriter(conn)
+	if isVectorised {
+		return &VectorisedXPlusConn{
+			XPlusPacketConn: XPlusPacketConn{
+				PacketConn: conn,
+				key:        key,
+				rand:       rand.New(rand.NewSource(time.Now().UnixNano())),
+			},
+			writer: vectorisedWriter,
+		}
+	} else {
+		return &XPlusPacketConn{
+			PacketConn: conn,
+			key:        key,
+			rand:       rand.New(rand.NewSource(time.Now().UnixNano())),
+		}
+	}
+}
+
+type XPlusPacketConn struct {
+	net.PacketConn
+	key        []byte
+	randAccess sync.Mutex
+	rand       *rand.Rand
+}
+
+func (c *XPlusPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	n, addr, err = c.PacketConn.ReadFrom(p)
+	if err != nil {
+		return
+	} else if n < xplusSaltLen {
+		return 0, nil, errInalidPacket
+	}
+	key := sha256.Sum256(append(c.key, p[:xplusSaltLen]...))
+	for i := range p[xplusSaltLen:] {
+		p[i] = p[xplusSaltLen+i] ^ key[i%sha256.Size]
+	}
+	n -= xplusSaltLen
+	return
+}
+
+func (c *XPlusPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	// can't use unsafe buffer on WriteTo
+	buffer := buf.NewSize(len(p) + xplusSaltLen)
+	defer buffer.Release()
+	salt := buffer.Extend(xplusSaltLen)
+	c.randAccess.Lock()
+	_, _ = c.rand.Read(salt)
+	c.randAccess.Unlock()
+	key := sha256.Sum256(append(c.key, salt...))
+	for i := range p {
+		common.Must(buffer.WriteByte(p[i] ^ key[i%sha256.Size]))
+	}
+	return c.PacketConn.WriteTo(buffer.Bytes(), addr)
+}
+
+func (c *XPlusPacketConn) Upstream() any {
+	return c.PacketConn
+}
+
+type VectorisedXPlusConn struct {
+	XPlusPacketConn
+	writer N.VectorisedPacketWriter
+}
+
+func (c *VectorisedXPlusConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	header := buf.NewSize(xplusSaltLen)
+	defer header.Release()
+	salt := header.Extend(xplusSaltLen)
+	c.randAccess.Lock()
+	_, _ = c.rand.Read(salt)
+	c.randAccess.Unlock()
+	key := sha256.Sum256(append(c.key, salt...))
+	for i := range p {
+		p[i] ^= key[i%sha256.Size]
+	}
+	return bufio.WriteVectorisedPacket(c.writer, [][]byte{header.Bytes(), p}, M.SocksaddrFromNet(addr))
+}
+
+func (c *VectorisedXPlusConn) WriteVectorisedPacket(buffers []*buf.Buffer, destination M.Socksaddr) error {
+	header := buf.NewSize(xplusSaltLen)
+	salt := header.Extend(xplusSaltLen)
+	c.randAccess.Lock()
+	_, _ = c.rand.Read(salt)
+	c.randAccess.Unlock()
+	key := sha256.Sum256(append(c.key, salt...))
+	var index int
+	for _, buffer := range buffers {
+		data := buffer.Bytes()
+		for i := range data {
+			data[i] ^= key[index%sha256.Size]
+			index++
+		}
+	}
+	buffers = append([]*buf.Buffer{header}, buffers...)
+	return c.writer.WriteVectorisedPacket(buffers, destination)
+}