Skip to content

Commit

Permalink
Merge pull request #54 from tobyxdd/wip-udp-relay
Browse files Browse the repository at this point in the history
UDP Relay
  • Loading branch information
tobyxdd authored Apr 22, 2021
2 parents 78f6eec + b3f0306 commit 0626a3e
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 36 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 的续作。
Expand Down Expand Up @@ -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` 忽略所有
证书错误(不推荐)。
Expand Down Expand Up @@ -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 验证密钥
Expand Down
38 changes: 33 additions & 5 deletions cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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()
}()
}
Expand Down
28 changes: 20 additions & 8 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -96,20 +101,27 @@ 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")
}
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")
Expand Down
19 changes: 8 additions & 11 deletions pkg/relay/relay.go → pkg/relay/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,23 @@ import (
"time"
)

type Relay struct {
type TCPRelay struct {
HyClient *core.Client
ListenAddr *net.TCPAddr
Remote string
Timeout time.Duration

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,
Expand All @@ -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
}
Expand Down
135 changes: 135 additions & 0 deletions pkg/relay/udp.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}

0 comments on commit 0626a3e

Please sign in to comment.