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/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..eebbf945 --- /dev/null +++ b/docs/configuration/outbound/hysteria.md @@ -0,0 +1,169 @@ +### 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" + } + ] +} +``` + +### 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/inbound/hysteria.go b/inbound/hysteria.go index f3fb51ce..5f4ff3e6 100644 --- a/inbound/hysteria.go +++ b/inbound/hysteria.go @@ -52,7 +52,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL MaxConnectionReceiveWindow: options.ReceiveWindowClient, MaxIncomingStreams: int64(options.MaxConnClient), KeepAlivePeriod: hysteria.KeepAlivePeriod, - DisablePathMTUDiscovery: options.DisableMTUDiscovery, + DisablePathMTUDiscovery: options.DisableMTUDiscovery || !(C.IsLinux || C.IsWindows), EnableDatagrams: true, } if options.ReceiveWindowConn == 0 { @@ -113,7 +113,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL udpSessions: make(map[uint32]chan *hysteria.UDPMessage), } if options.TLS == nil || !options.TLS.Enabled { - return nil, ErrTLSRequired + return nil, errTLSRequired } if len(options.TLS.ALPN) == 0 { options.TLS.ALPN = []string{hysteria.DefaultALPN} diff --git a/inbound/naive.go b/inbound/naive.go index a1537562..785acb5a 100644 --- a/inbound/naive.go +++ b/inbound/naive.go @@ -45,10 +45,7 @@ type Naive struct { h3Server any } -var ( - ErrTLSRequired = 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,10 +58,10 @@ 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, ErrTLSRequired + 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)) if err != nil { 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 index f01a7b83..9ab9a41e 100644 --- a/option/hysteria.go +++ b/option/hysteria.go @@ -19,20 +19,16 @@ type HysteriaInboundOptions struct { 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"` - ALPN string `json:"alpn,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - CustomCA string `json:"ca,omitempty"` - CustomCAStr string `json:"ca_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"` + 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/outbound/hysteria.go b/outbound/hysteria.go index bdf51b5c..6fa13230 100644 --- a/outbound/hysteria.go +++ b/outbound/hysteria.go @@ -5,9 +5,7 @@ package outbound import ( "context" "crypto/tls" - "crypto/x509" "net" - "os" "sync" "github.com/sagernet/quic-go" @@ -45,35 +43,20 @@ type Hysteria struct { 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) { - tlsConfig := &tls.Config{ - ServerName: options.ServerName, - InsecureSkipVerify: options.Insecure, - MinVersion: tls.VersionTLS13, + if options.TLS == nil || !options.TLS.Enabled { + return nil, errTLSRequired } - if options.ALPN != "" { - tlsConfig.NextProtos = []string{options.ALPN} - } else { + 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} } - var ca []byte - var err error - if options.CustomCA != "" { - ca, err = os.ReadFile(options.CustomCA) - if err != nil { - return nil, err - } - } - if options.CustomCAStr != "" { - ca = []byte(options.CustomCAStr) - } - if len(ca) > 0 { - cp := x509.NewCertPool() - if !cp.AppendCertsFromPEM(ca) { - return nil, E.New("parse ca failed") - } - tlsConfig.RootCAs = cp - } quicConfig := &quic.Config{ InitialStreamReceiveWindow: options.ReceiveWindowConn, MaxStreamReceiveWindow: options.ReceiveWindowConn,