Merge pull request #54 from tobyxdd/wip-udp-relay

UDP Relay
This commit is contained in:
Toby 2021-04-21 21:59:15 -07:00 committed by GitHub
commit 0626a3e505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 228 additions and 36 deletions

View File

@ -16,7 +16,7 @@
[中文 README](README.zh.md) [中文 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. 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 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": { "http": {
"listen": "127.0.0.1:8080" "listen": "127.0.0.1:8080"
}, },
"relay": { "relay_tcp": {
"listen": "127.0.0.1:2222", "listen": "127.0.0.1:2222",
"remote": "123.123.123.123:22" "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. 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 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) "cert": "/home/ubuntu/my_cert.crt", // Cert file (HTTPS proxy)
"key": "/home/ubuntu/my_key.crt" // Key file (HTTPS proxy) "key": "/home/ubuntu/my_key.crt" // Key file (HTTPS proxy)
}, },
"relay": { "relay_tcp": {
"listen": "127.0.0.1:2222", // Relay listen address "listen": "127.0.0.1:2222", // TCP relay Listen address
"remote": "123.123.123.123:22", // Relay remote address "remote": "123.123.123.123:22", // TCP relay remote address
"timeout": 300 // TCP timeout in seconds "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 "acl": "my_list.acl", // See ACL below
"obfs": "AMOGUS", // Obfuscation password "obfs": "AMOGUS", // Obfuscation password
"auth": "[BASE64]", // Authentication payload in Base64 "auth": "[BASE64]", // Authentication payload in Base64

View File

@ -14,7 +14,7 @@
[6]: https://t.me/hysteria_github [6]: https://t.me/hysteria_github
Hysteria 是专门针对恶劣网络环境进行优化的 TCP 连接转发和代理工具(双边加速),比如卫星网络、拥挤的公共 Wi-Fi、在中国连接国外服务器等。 Hysteria 是专门针对恶劣网络环境进行优化的 TCP/UDP 转发和代理工具(双边加速),比如卫星网络、拥挤的公共 Wi-Fi、在中国连接国外服务器等。
基于修改版的 QUIC 协议。 基于修改版的 QUIC 协议。
是我此前弃坑的项目 https://github.com/dragonite-network/dragonite-java 的续作。 是我此前弃坑的项目 https://github.com/dragonite-network/dragonite-java 的续作。
@ -80,14 +80,19 @@ Hysteria 是专门针对恶劣网络环境进行优化的 TCP 连接转发和代
"http": { "http": {
"listen": "127.0.0.1:8080" "listen": "127.0.0.1:8080"
}, },
"relay": { "relay_tcp": {
"listen": "127.0.0.1:2222", "listen": "127.0.0.1:2222",
"remote": "123.123.123.123:22" "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` 忽略所有 如果你的服务端证书不是由受信任的 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 代理) "cert": "/home/ubuntu/my_cert.crt", // 证书 (变为 HTTPS 代理)
"key": "/home/ubuntu/my_key.crt" // 证书密钥 (变为 HTTPS 代理) "key": "/home/ubuntu/my_key.crt" // 证书密钥 (变为 HTTPS 代理)
}, },
"relay": { "relay_tcp": {
"listen": "127.0.0.1:2222", // 转发监听地址 "listen": "127.0.0.1:2222", // TCP 转发监听地址
"remote": "123.123.123.123:22", // 转发目标地址 "remote": "123.123.123.123:22", // TCP 转发目标地址
"timeout": 300 // 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 "acl": "my_list.acl", // 见下文 ACL
"obfs": "AMOGUS", // 混淆密码 "obfs": "AMOGUS", // 混淆密码
"auth": "[BASE64]", // Base64 验证密钥 "auth": "[BASE64]", // Base64 验证密钥

View File

@ -181,10 +181,10 @@ func client(config *clientConfig) {
}() }()
} }
if len(config.Relay.Listen) > 0 { if len(config.TCPRelay.Listen) > 0 {
go func() { go func() {
rl, err := relay.NewRelay(client, config.Relay.Listen, config.Relay.Remote, rl, err := relay.NewTCPRelay(client, config.TCPRelay.Listen, config.TCPRelay.Remote,
time.Duration(config.Relay.Timeout)*time.Second, time.Duration(config.TCPRelay.Timeout)*time.Second,
func(addr net.Addr) { func(addr net.Addr) {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"src": addr.String(), "src": addr.String(),
@ -201,12 +201,40 @@ func client(config *clientConfig) {
"src": addr.String(), "src": addr.String(),
}).Debug("TCP relay EOF") }).Debug("TCP relay EOF")
} }
}) })
if err != nil { if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP relay") 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() errChan <- rl.ListenAndServe()
}() }()
} }

View File

@ -80,11 +80,16 @@ type clientConfig struct {
Cert string `json:"cert"` Cert string `json:"cert"`
Key string `json:"key"` Key string `json:"key"`
} `json:"http"` } `json:"http"`
Relay struct { TCPRelay struct {
Listen string `json:"listen"` Listen string `json:"listen"`
Remote string `json:"remote"` Remote string `json:"remote"`
Timeout int `json:"timeout"` 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"` ACL string `json:"acl"`
Obfs string `json:"obfs"` Obfs string `json:"obfs"`
Auth []byte `json:"auth"` Auth []byte `json:"auth"`
@ -96,11 +101,15 @@ type clientConfig struct {
} }
func (c *clientConfig) Check() error { func (c *clientConfig) Check() error {
if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.Relay.Listen) == 0 { if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 &&
return errors.New("no SOCKS5, HTTP or relay listen address") 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 { if len(c.TCPRelay.Listen) > 0 && len(c.TCPRelay.Remote) == 0 {
return errors.New("no relay remote address") 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 { if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout <= 4 {
return errors.New("invalid SOCKS5 timeout") return errors.New("invalid SOCKS5 timeout")
@ -108,8 +117,11 @@ func (c *clientConfig) Check() error {
if c.HTTP.Timeout != 0 && c.HTTP.Timeout <= 4 { if c.HTTP.Timeout != 0 && c.HTTP.Timeout <= 4 {
return errors.New("invalid HTTP timeout") return errors.New("invalid HTTP timeout")
} }
if c.Relay.Timeout != 0 && c.Relay.Timeout <= 4 { if c.TCPRelay.Timeout != 0 && c.TCPRelay.Timeout <= 4 {
return errors.New("invalid relay timeout") 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 { if len(c.Server) == 0 {
return errors.New("no server address") return errors.New("no server address")

View File

@ -7,7 +7,7 @@ import (
"time" "time"
) )
type Relay struct { type TCPRelay struct {
HyClient *core.Client HyClient *core.Client
ListenAddr *net.TCPAddr ListenAddr *net.TCPAddr
Remote string Remote string
@ -15,17 +15,15 @@ type Relay struct {
ConnFunc func(addr net.Addr) ConnFunc func(addr net.Addr)
ErrorFunc func(addr net.Addr, err error) ErrorFunc func(addr net.Addr, err error)
tcpListener *net.TCPListener
} }
func NewRelay(hyClient *core.Client, listen, remote string, timeout time.Duration, func NewTCPRelay(hyClient *core.Client, listen, remote string, timeout time.Duration,
connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error)) (*Relay, error) { connFunc func(addr net.Addr), errorFunc func(addr net.Addr, err error)) (*TCPRelay, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen) tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := &Relay{ r := &TCPRelay{
HyClient: hyClient, HyClient: hyClient,
ListenAddr: tAddr, ListenAddr: tAddr,
Remote: remote, Remote: remote,
@ -36,15 +34,14 @@ func NewRelay(hyClient *core.Client, listen, remote string, timeout time.Duratio
return r, nil return r, nil
} }
func (r *Relay) ListenAndServe() error { func (r *TCPRelay) ListenAndServe() error {
var err error listener, err := net.ListenTCP("tcp", r.ListenAddr)
r.tcpListener, err = net.ListenTCP("tcp", r.ListenAddr)
if err != nil { if err != nil {
return err return err
} }
defer r.tcpListener.Close() defer listener.Close()
for { for {
c, err := r.tcpListener.AcceptTCP() c, err := listener.AcceptTCP()
if err != nil { if err != nil {
return err return err
} }

135
pkg/relay/udp.go Normal file
View File

@ -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
}
}
}