diff --git a/common/dialer/tls.go b/common/dialer/tls.go index 679e8a34..b9ed5b26 100644 --- a/common/dialer/tls.go +++ b/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 } diff --git a/constant/proxy.go b/constant/proxy.go index f5024059..fa4d25c6 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -15,6 +15,7 @@ const ( TypeTrojan = "trojan" TypeNaive = "naive" TypeWireGuard = "wireguard" + TypeHysteria = "hysteria" ) const ( diff --git a/docs/configuration/inbound/hysteria.md b/docs/configuration/inbound/hysteria.md new file mode 100644 index 00000000..50b76413 --- /dev/null +++ b/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). \ No newline at end of file diff --git a/docs/configuration/inbound/index.md b/docs/configuration/inbound/index.md index a38c59bc..0648b2cc 100644 --- a/docs/configuration/inbound/index.md +++ b/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) | diff --git a/docs/configuration/outbound/hysteria.md b/docs/configuration/outbound/hysteria.md new file mode 100644 index 00000000..6d3b2e88 --- /dev/null +++ b/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`. \ No newline at end of file diff --git a/docs/configuration/outbound/index.md b/docs/configuration/outbound/index.md index 17bf5556..6c74c047 100644 --- a/docs/configuration/outbound/index.md +++ b/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) | diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 2ca75939..e49805e8 100644 --- a/docs/configuration/shared/tls.md +++ b/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. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index e5f061d2..fe1b0a89 100644 --- a/docs/index.md +++ b/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 diff --git a/go.mod b/go.mod index ff5f380e..4fb5f781 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 96928e1f..57619d66 100644 --- a/go.sum +++ b/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= diff --git a/inbound/builder.go b/inbound/builder.go index ee51d67e..ae5b1a6e 100644 --- a/inbound/builder.go +++ b/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) } diff --git a/inbound/http.go b/inbound/http.go index ed27c1e6..52b0fbc3 100644 --- a/inbound/http.go +++ b/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 } diff --git a/inbound/hysteria.go b/inbound/hysteria.go new file mode 100644 index 00000000..60ff71fa --- /dev/null +++ b/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), + ) +} diff --git a/inbound/hysteria_stub.go b/inbound/hysteria_stub.go new file mode 100644 index 00000000..5a619115 --- /dev/null +++ b/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`) +} diff --git a/inbound/naive.go b/inbound/naive.go index 660447f6..9fe17c9c 100644 --- a/inbound/naive.go +++ b/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() diff --git a/inbound/tls.go b/inbound/tls.go index 811bae9c..f80b62b8 100644 --- a/inbound/tls.go +++ b/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 err != nil { - return nil, E.Cause(err, "read key") + 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 } - key = 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, "parse x509 key pair") + } + tlsConfig.Certificates = []tls.Certificate{keyPair} } - 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} return &TLSConfig{ - config: &tlsConfig, + config: tlsConfig, logger: logger, + acmeService: acmeService, certificate: certificate, key: key, certificatePath: options.CertificatePath, diff --git a/inbound/tls_acme.go b/inbound/tls_acme.go new file mode 100644 index 00000000..e24b7579 --- /dev/null +++ b/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 +} diff --git a/inbound/tls_acme_stub.go b/inbound/tls_acme_stub.go new file mode 100644 index 00000000..f787aa14 --- /dev/null +++ b/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`) +} diff --git a/inbound/trojan.go b/inbound/trojan.go index 17ee7788..58b01316 100644 --- a/inbound/trojan.go +++ b/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 } diff --git a/inbound/vmess.go b/inbound/vmess.go index 28e4a53b..bedb5150 100644 --- a/inbound/vmess.go +++ b/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 } diff --git a/mkdocs.yml b/mkdocs.yml index a15def59..c99d261a 100644 --- a/mkdocs.yml +++ b/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: diff --git a/option/hysteria.go b/option/hysteria.go new file mode 100644 index 00000000..9ab9a41e --- /dev/null +++ b/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"` +} diff --git a/option/inbound.go b/option/inbound.go index 57f7ac92..2ac440c5 100644 --- a/option/inbound.go +++ b/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) } diff --git a/option/outbound.go b/option/outbound.go index e735e130..f1f84409 100644 --- a/option/outbound.go +++ b/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: diff --git a/option/tls.go b/option/tls.go index 4b470e60..b403a2dc 100644 --- a/option/tls.go +++ b/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) { diff --git a/outbound/builder.go b/outbound/builder.go index 843faa3f..97735dae 100644 --- a/outbound/builder.go +++ b/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: diff --git a/outbound/hysteria.go b/outbound/hysteria.go new file mode 100644 index 00000000..6fa13230 --- /dev/null +++ b/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) +} diff --git a/outbound/hysteria_stub.go b/outbound/hysteria_stub.go new file mode 100644 index 00000000..ae2d62b4 --- /dev/null +++ b/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`) +} diff --git a/test/box_test.go b/test/box_test.go index 58ab0df1..37f85956 100644 --- a/test/box_test.go +++ b/test/box_test.go @@ -35,14 +35,6 @@ func startInstance(t *testing.T, options option.Options) { }) } -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)) -} - 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) { @@ -51,36 +43,34 @@ 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, 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, testLargeDataWithConn(t, testPort, dialTCP)) +} + +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)) + } + 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)) +} + func testSuitWg(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) { diff --git a/test/clash_test.go b/test/clash_test.go index bd76aa19..b27df7ea 100644 --- a/test/clash_test.go +++ b/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 diff --git a/test/config/hysteria-client.json b/test/config/hysteria-client.json new file mode 100644 index 00000000..3328c510 --- /dev/null +++ b/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" +} \ No newline at end of file diff --git a/test/config/hysteria-server.json b/test/config/hysteria-server.json new file mode 100644 index 00000000..e33624a2 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/test/go.mod b/test/go.mod index 3cc565b3..c23f3ad1 100644 --- a/test/go.mod +++ b/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 diff --git a/test/go.sum b/test/go.sum index 26bb66f5..91333728 100644 --- a/test/go.sum +++ b/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= diff --git a/test/hysteria_test.go b/test/hysteria_test.go new file mode 100644 index 00000000..057d12ca --- /dev/null +++ b/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) +} diff --git a/transport/hysteria/brutal.go b/transport/hysteria/brutal.go new file mode 100644 index 00000000..0e6dc794 --- /dev/null +++ b/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 +} diff --git a/transport/hysteria/frag.go b/transport/hysteria/frag.go new file mode 100644 index 00000000..721341f1 --- /dev/null +++ b/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 +} diff --git a/transport/hysteria/pacer.go b/transport/hysteria/pacer.go new file mode 100644 index 00000000..7e67f7f4 --- /dev/null +++ b/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 +} diff --git a/transport/hysteria/protocol.go b/transport/hysteria/protocol.go new file mode 100644 index 00000000..d3893b80 --- /dev/null +++ b/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) +} diff --git a/transport/hysteria/speed.go b/transport/hysteria/speed.go new file mode 100644 index 00000000..161e0d58 --- /dev/null +++ b/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 +} diff --git a/transport/hysteria/wrap.go b/transport/hysteria/wrap.go new file mode 100644 index 00000000..c280cf1c --- /dev/null +++ b/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 +} diff --git a/transport/hysteria/xplus.go b/transport/hysteria/xplus.go new file mode 100644 index 00000000..90a9a325 --- /dev/null +++ b/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) +}