diff --git a/README.md b/README.md index 0386f6a..8178256 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [中文 README](README.zh.md) -Hysteria is a TCP relay & SOCKS5/HTTP proxy tool optimized for networks of poor quality (e.g. satellite connections, +Hysteria is a TCP/UDP relay & SOCKS5/HTTP proxy tool optimized for networks of poor quality (e.g. satellite connections, congested public Wi-Fi, connecting from China to servers abroad) powered by a custom version of QUIC protocol. It is essentially a spiritual successor of my abandoned project https://github.com/dragonite-network/dragonite-java @@ -87,14 +87,19 @@ Same as the server side, create a `config.json` under the root directory of the "http": { "listen": "127.0.0.1:8080" }, - "relay": { + "relay_tcp": { "listen": "127.0.0.1:2222", "remote": "123.123.123.123:22" + }, + "relay_udp": { + "listen": "127.0.0.1:5333", + "remote": "8.8.8.8:53" } } ``` -This config enables a SOCKS5 proxy (with both TCP & UDP support), an HTTP proxy, and a TCP relay to `123.123.123.123:22` +This config enables a SOCKS5 proxy (with both TCP & UDP support), an HTTP proxy, a TCP relay to `123.123.123.123:22` and +a UDP relay to `8.8.8.8:53` at the same time. Please modify or remove these entries according to your actual needs. If your server certificate is not issued by a trusted CA, you need to specify the CA used @@ -217,11 +222,16 @@ hysteria_traffic_uplink_bytes_total{auth="aGFja2VyISE="} 37452 "cert": "/home/ubuntu/my_cert.crt", // Cert file (HTTPS proxy) "key": "/home/ubuntu/my_key.crt" // Key file (HTTPS proxy) }, - "relay": { - "listen": "127.0.0.1:2222", // Relay listen address - "remote": "123.123.123.123:22", // Relay remote address + "relay_tcp": { + "listen": "127.0.0.1:2222", // TCP relay Listen address + "remote": "123.123.123.123:22", // TCP relay remote address "timeout": 300 // TCP timeout in seconds }, + "relay_udp": { + "listen": "127.0.0.1:5333", // UDP relay Listen address + "remote": "8.8.8.8:53", // UDP relay remote address + "timeout": 60 // UDP session timeout in seconds + }, "acl": "my_list.acl", // See ACL below "obfs": "AMOGUS", // Obfuscation password "auth": "[BASE64]", // Authentication payload in Base64 diff --git a/README.zh.md b/README.zh.md index f38bb3e..de5e122 100644 --- a/README.zh.md +++ b/README.zh.md @@ -14,7 +14,7 @@ [6]: https://t.me/hysteria_github -Hysteria 是专门针对恶劣网络环境进行优化的 TCP 连接转发和代理工具(双边加速),比如卫星网络、拥挤的公共 Wi-Fi、在中国连接国外服务器等。 +Hysteria 是专门针对恶劣网络环境进行优化的 TCP/UDP 转发和代理工具(双边加速),比如卫星网络、拥挤的公共 Wi-Fi、在中国连接国外服务器等。 基于修改版的 QUIC 协议。 是我此前弃坑的项目 https://github.com/dragonite-network/dragonite-java 的续作。 @@ -80,14 +80,19 @@ Hysteria 是专门针对恶劣网络环境进行优化的 TCP 连接转发和代 "http": { "listen": "127.0.0.1:8080" }, - "relay": { + "relay_tcp": { "listen": "127.0.0.1:2222", "remote": "123.123.123.123:22" + }, + "relay_udp": { + "listen": "127.0.0.1:5333", + "remote": "8.8.8.8:53" } } ``` -这个配置同时开了 SOCK5 (支持 TCP & UDP) 代理,HTTP 代理和到 `123.123.123.123:22` 的 TCP 转发。请根据自己实际需要修改和删减。 +这个配置同时开了 SOCK5 (支持 TCP & UDP) 代理,HTTP 代理,到 `123.123.123.123:22` 的 TCP 转发和到 `8.8.8.8:53` 的 UDP 转发。 +请根据自己实际需要修改和删减。 如果你的服务端证书不是由受信任的 CA 签发的,需要用 `"ca": "/path/to/file.ca"` 指定使用的 CA 或者用 `"insecure": true` 忽略所有 证书错误(不推荐)。 @@ -205,11 +210,16 @@ hysteria_traffic_uplink_bytes_total{auth="aGFja2VyISE="} 37452 "cert": "/home/ubuntu/my_cert.crt", // 证书 (变为 HTTPS 代理) "key": "/home/ubuntu/my_key.crt" // 证书密钥 (变为 HTTPS 代理) }, - "relay": { - "listen": "127.0.0.1:2222", // 转发监听地址 - "remote": "123.123.123.123:22", // 转发目标地址 + "relay_tcp": { + "listen": "127.0.0.1:2222", // TCP 转发监听地址 + "remote": "123.123.123.123:22", // TCP 转发目标地址 "timeout": 300 // TCP 超时秒数 }, + "relay_udp": { + "listen": "127.0.0.1:5333", // UDP 转发监听地址 + "remote": "8.8.8.8:53", // UDP 转发目标地址 + "timeout": 60 // UDP 超时秒数 + }, "acl": "my_list.acl", // 见下文 ACL "obfs": "AMOGUS", // 混淆密码 "auth": "[BASE64]", // Base64 验证密钥 diff --git a/cmd/client.go b/cmd/client.go index 9ea3da5..d7b39e9 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -181,10 +181,10 @@ func client(config *clientConfig) { }() } - if len(config.Relay.Listen) > 0 { + if len(config.TCPRelay.Listen) > 0 { go func() { - rl, err := relay.NewRelay(client, config.Relay.Listen, config.Relay.Remote, - time.Duration(config.Relay.Timeout)*time.Second, + rl, err := relay.NewTCPRelay(client, config.TCPRelay.Listen, config.TCPRelay.Remote, + time.Duration(config.TCPRelay.Timeout)*time.Second, func(addr net.Addr) { logrus.WithFields(logrus.Fields{ "src": addr.String(), @@ -201,12 +201,40 @@ func client(config *clientConfig) { "src": addr.String(), }).Debug("TCP relay EOF") } - }) if err != nil { logrus.WithField("error", err).Fatal("Failed to initialize TCP relay") } - logrus.WithField("addr", config.Relay.Listen).Info("TCP relay up and running") + logrus.WithField("addr", config.TCPRelay.Listen).Info("TCP relay up and running") + errChan <- rl.ListenAndServe() + }() + } + + if len(config.UDPRelay.Listen) > 0 { + go func() { + rl, err := relay.NewUDPRelay(client, config.UDPRelay.Listen, config.UDPRelay.Remote, + time.Duration(config.UDPRelay.Timeout)*time.Second, + func(addr net.Addr) { + logrus.WithFields(logrus.Fields{ + "src": addr.String(), + }).Debug("UDP relay request") + }, + func(addr net.Addr, err error) { + if err != relay.ErrTimeout { + logrus.WithFields(logrus.Fields{ + "error": err, + "src": addr.String(), + }).Info("UDP relay error") + } else { + logrus.WithFields(logrus.Fields{ + "src": addr.String(), + }).Debug("UDP relay session closed") + } + }) + if err != nil { + logrus.WithField("error", err).Fatal("Failed to initialize UDP relay") + } + logrus.WithField("addr", config.UDPRelay.Listen).Info("UDP relay up and running") errChan <- rl.ListenAndServe() }() } diff --git a/cmd/config.go b/cmd/config.go index 1e9c543..dfceb63 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -80,11 +80,16 @@ type clientConfig struct { Cert string `json:"cert"` Key string `json:"key"` } `json:"http"` - Relay struct { + TCPRelay struct { Listen string `json:"listen"` Remote string `json:"remote"` Timeout int `json:"timeout"` - } `json:"relay"` + } `json:"relay_tcp"` + UDPRelay struct { + Listen string `json:"listen"` + Remote string `json:"remote"` + Timeout int `json:"timeout"` + } `json:"relay_udp"` ACL string `json:"acl"` Obfs string `json:"obfs"` Auth []byte `json:"auth"` @@ -96,11 +101,15 @@ type clientConfig struct { } func (c *clientConfig) Check() error { - if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.Relay.Listen) == 0 { - return errors.New("no SOCKS5, HTTP or relay listen address") + if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && + len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 { + return errors.New("no SOCKS5, HTTP, TCP relay or UDP relay listen address") } - if len(c.Relay.Listen) > 0 && len(c.Relay.Remote) == 0 { - return errors.New("no relay remote address") + if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 { + return errors.New("no TCP relay remote address") + } + if len(c.UDPRelay.Listen) > 0 && len(c.UDPRelay.Remote) == 0 { + return errors.New("no UDP relay remote address") } if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout <= 4 { return errors.New("invalid SOCKS5 timeout") @@ -108,8 +117,11 @@ func (c *clientConfig) Check() error { if c.HTTP.Timeout != 0 && c.HTTP.Timeout <= 4 { return errors.New("invalid HTTP timeout") } - if c.Relay.Timeout != 0 && c.Relay.Timeout <= 4 { - return errors.New("invalid relay timeout") + if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout <= 4 { + return errors.New("invalid TCP relay timeout") + } + if c.UDPRelay.Timeout != 0 && c.UDPRelay.Timeout <= 4 { + return errors.New("invalid UDP relay timeout") } if len(c.Server) == 0 { return errors.New("no server address") diff --git a/pkg/relay/relay.go b/pkg/relay/tcp.go similarity index 73% rename from pkg/relay/relay.go rename to pkg/relay/tcp.go index ba168b0..9e16349 100644 --- a/pkg/relay/relay.go +++ b/pkg/relay/tcp.go @@ -7,7 +7,7 @@ import ( "time" ) -type Relay struct { +type TCPRelay struct { HyClient *core.Client ListenAddr *net.TCPAddr Remote string @@ -15,17 +15,15 @@ type Relay struct { ConnFunc func(addr net.Addr) ErrorFunc func(addr net.Addr, err error) - - tcpListener *net.TCPListener } -func NewRelay(hyClient *core.Client, listen, remote string, timeout time.Duration, - connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error)) (*Relay, error) { +func NewTCPRelay(hyClient *core.Client, listen, remote string, timeout time.Duration, + connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error)) (*TCPRelay, error) { tAddr, err := net.ResolveTCPAddr("tcp", listen) if err != nil { return nil, err } - r := &Relay{ + r := &TCPRelay{ HyClient: hyClient, ListenAddr: tAddr, Remote: remote, @@ -36,15 +34,14 @@ func NewRelay(hyClient *core.Client, listen, remote string, timeout time.Duratio return r, nil } -func (r *Relay) ListenAndServe() error { - var err error - r.tcpListener, err = net.ListenTCP("tcp", r.ListenAddr) +func (r *TCPRelay) ListenAndServe() error { + listener, err := net.ListenTCP("tcp", r.ListenAddr) if err != nil { return err } - defer r.tcpListener.Close() + defer listener.Close() for { - c, err := r.tcpListener.AcceptTCP() + c, err := listener.AcceptTCP() if err != nil { return err } diff --git a/pkg/relay/udp.go b/pkg/relay/udp.go new file mode 100644 index 0000000..6dfd589 --- /dev/null +++ b/pkg/relay/udp.go @@ -0,0 +1,135 @@ +package relay + +import ( + "errors" + "github.com/tobyxdd/hysteria/pkg/core" + "net" + "sync" + "sync/atomic" + "time" +) + +const udpBufferSize = 65535 + +const udpMinTimeout = 4 * time.Second + +var ErrTimeout = errors.New("inactivity timeout") + +type UDPRelay struct { + HyClient *core.Client + ListenAddr *net.UDPAddr + Remote string + Timeout time.Duration + + ConnFunc func(addr net.Addr) + ErrorFunc func(addr net.Addr, err error) +} + +func NewUDPRelay(hyClient *core.Client, listen, remote string, timeout time.Duration, + connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error)) (*UDPRelay, error) { + uAddr, err := net.ResolveUDPAddr("udp", listen) + if err != nil { + return nil, err + } + r := &UDPRelay{ + HyClient: hyClient, + ListenAddr: uAddr, + Remote: remote, + Timeout: timeout, + ConnFunc: connFunc, + ErrorFunc: errorFunc, + } + if timeout == 0 { + r.Timeout = 1 * time.Minute + } else if timeout < udpMinTimeout { + r.Timeout = udpMinTimeout + } + return r, nil +} + +type cmEntry struct { + HyConn core.UDPConn + Addr *net.UDPAddr + LastActiveTime atomic.Value +} + +func (r *UDPRelay) ListenAndServe() error { + conn, err := net.ListenUDP("udp", r.ListenAddr) + if err != nil { + return err + } + defer conn.Close() + // src <-> HyClient UDPConn + connMap := make(map[string]*cmEntry) + var connMapMutex sync.RWMutex + // Timeout cleanup routine + stopChan := make(chan bool) + defer close(stopChan) + go func() { + ticker := time.NewTicker(udpMinTimeout) + defer ticker.Stop() + for { + select { + case <-stopChan: + return + case t := <-ticker.C: + allowedLAT := t.Add(-r.Timeout) + connMapMutex.Lock() + for k, v := range connMap { + if v.LastActiveTime.Load().(time.Time).Before(allowedLAT) { + // Timeout + r.ErrorFunc(v.Addr, ErrTimeout) + _ = v.HyConn.Close() + delete(connMap, k) + } + } + connMapMutex.Unlock() + } + } + }() + // Read loop + buf := make([]byte, udpBufferSize) + for { + n, rAddr, err := conn.ReadFromUDP(buf) + if n > 0 { + connMapMutex.RLock() + cme := connMap[rAddr.String()] + connMapMutex.RUnlock() + if cme != nil { + // Existing conn + cme.LastActiveTime.Store(time.Now()) + _ = cme.HyConn.WriteTo(buf[:n], r.Remote) + } else { + // New + r.ConnFunc(rAddr) + hyConn, err := r.HyClient.DialUDP() + if err != nil { + r.ErrorFunc(rAddr, err) + } else { + // Add it to the map + ent := &cmEntry{HyConn: hyConn, Addr: rAddr} + ent.LastActiveTime.Store(time.Now()) + connMapMutex.Lock() + connMap[rAddr.String()] = ent + connMapMutex.Unlock() + // Start remote to local + go func() { + for { + bs, _, err := hyConn.ReadFrom() + if err != nil { + break + } + ent.LastActiveTime.Store(time.Now()) + _, _ = conn.WriteToUDP(bs, rAddr) + } + }() + // Send the packet + _ = hyConn.WriteTo(buf[:n], r.Remote) + } + } + } + if err != nil { + return err + } + } +}