feat: TCP redirect implementation

This commit is contained in:
Toby 2022-06-06 18:09:34 -07:00
parent 3cd20f6cdb
commit 575de280ff
4 changed files with 184 additions and 1 deletions

View File

@ -6,6 +6,7 @@ import (
"errors"
"github.com/oschwald/geoip2-golang"
"github.com/tobyxdd/hysteria/pkg/pmtud_fix"
"github.com/tobyxdd/hysteria/pkg/redirect"
"github.com/yosuke-furukawa/json5/encoding/json5"
"io"
"io/ioutil"
@ -435,6 +436,38 @@ func client(config *clientConfig) {
}()
}
if len(config.TCPRedirect.Listen) > 0 {
go func() {
rl, err := redirect.NewTCPRedirect(client, config.TCPRedirect.Listen,
time.Duration(config.TCPRedirect.Timeout)*time.Second,
func(addr, reqAddr net.Addr) {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
"dst": reqAddr.String(),
}).Debug("TCP Redirect request")
},
func(addr, reqAddr net.Addr, err error) {
if err != io.EOF {
logrus.WithFields(logrus.Fields{
"error": err,
"src": addr.String(),
"dst": reqAddr.String(),
}).Info("TCP Redirect error")
} else {
logrus.WithFields(logrus.Fields{
"src": addr.String(),
"dst": reqAddr.String(),
}).Debug("TCP Redirect EOF")
}
})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to initialize TCP Redirect")
}
logrus.WithField("addr", config.TCPRedirect.Listen).Info("TCP Redirect up and running")
errChan <- rl.ListenAndServe()
}()
}
err := <-errChan
logrus.WithField("error", err).Fatal("Client shutdown")
}

View File

@ -175,6 +175,10 @@ type clientConfig struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"tproxy_udp"`
TCPRedirect struct {
Listen string `json:"listen"`
Timeout int `json:"timeout"`
} `json:"redirect_tcp"`
ACL string `json:"acl"`
MMDB string `json:"mmdb"`
Obfs string `json:"obfs"`
@ -216,7 +220,8 @@ func (c *clientConfig) Check() error {
if len(c.SOCKS5.Listen) == 0 && len(c.HTTP.Listen) == 0 && len(c.TUN.Name) == 0 &&
len(c.TCPRelay.Listen) == 0 && len(c.UDPRelay.Listen) == 0 &&
len(c.TCPRelays) == 0 && len(c.UDPRelays) == 0 &&
len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 {
len(c.TCPTProxy.Listen) == 0 && len(c.UDPTProxy.Listen) == 0 &&
len(c.TCPRedirect.Listen) == 0 {
return errors.New("please enable at least one mode")
}
if c.SOCKS5.Timeout != 0 && c.SOCKS5.Timeout <= 4 {
@ -256,6 +261,9 @@ func (c *clientConfig) Check() error {
if c.UDPTProxy.Timeout != 0 && c.UDPTProxy.Timeout <= 4 {
return errors.New("invalid UDP TProxy timeout")
}
if c.TCPRedirect.Timeout != 0 && c.TCPRedirect.Timeout <= 4 {
return errors.New("invalid TCP Redirect timeout")
}
if len(c.Server) == 0 {
return errors.New("no server address")
}

119
pkg/redirect/tcp_linux.go Normal file
View File

@ -0,0 +1,119 @@
package redirect
import (
"encoding/binary"
"errors"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/utils"
"net"
"syscall"
"time"
"unsafe"
)
const (
SO_ORIGINAL_DST = 80
IP6T_SO_ORIGINAL_DST = 80
)
type TCPRedirect struct {
HyClient *core.Client
ListenAddr *net.TCPAddr
Timeout time.Duration
ConnFunc func(addr, reqAddr net.Addr)
ErrorFunc func(addr, reqAddr net.Addr, err error)
}
func NewTCPRedirect(hyClient *core.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error)) (*TCPRedirect, error) {
tAddr, err := net.ResolveTCPAddr("tcp", listen)
if err != nil {
return nil, err
}
r := &TCPRedirect{
HyClient: hyClient,
ListenAddr: tAddr,
Timeout: timeout,
ConnFunc: connFunc,
ErrorFunc: errorFunc,
}
return r, nil
}
func (r *TCPRedirect) ListenAndServe() error {
listener, err := net.ListenTCP("tcp", r.ListenAddr)
if err != nil {
return err
}
defer listener.Close()
for {
c, err := listener.Accept()
if err != nil {
return err
}
go func() {
defer c.Close()
dest, err := getDestAddr(c.(*net.TCPConn))
if err != nil || dest.IP.IsLoopback() {
// Silently drop the connection if we failed to get the destination address,
// or if it's a loopback address (not a redirected connection).
return
}
r.ConnFunc(c.RemoteAddr(), dest)
rc, err := r.HyClient.DialTCP(dest.String())
if err != nil {
r.ErrorFunc(c.RemoteAddr(), dest, err)
return
}
defer rc.Close()
err = utils.PipePairWithTimeout(c, rc, r.Timeout)
r.ErrorFunc(c.RemoteAddr(), dest, err)
}()
}
}
type sockAddr struct {
family uint16
port [2]byte // big endian regardless of host byte order
data [24]byte // check sockaddr_in or sockaddr_in6 for more information
}
func getDestAddr(conn *net.TCPConn) (*net.TCPAddr, error) {
rc, err := conn.SyscallConn()
if err != nil {
return nil, err
}
var addr sockAddr
addrSize := uint32(unsafe.Sizeof(addr))
var err2 error
err = rc.Control(func(fd uintptr) {
// try IPv6 first
_, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.SOL_IPV6, IP6T_SO_ORIGINAL_DST,
uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)), 0)
if err != 0 {
// try IPv4
_, _, err = syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.SOL_IP, SO_ORIGINAL_DST,
uintptr(unsafe.Pointer(&addr)), uintptr(unsafe.Pointer(&addrSize)), 0)
if err != 0 {
// failed
err2 = err
}
}
})
if err != nil {
return nil, err
}
if err2 != nil {
return nil, err2
}
switch addr.family {
case syscall.AF_INET:
return &net.TCPAddr{IP: addr.data[:4], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
case syscall.AF_INET6:
return &net.TCPAddr{IP: addr.data[4:20], Port: int(binary.BigEndian.Uint16(addr.port[:]))}, nil
default:
return nil, errors.New("unknown address family")
}
}

23
pkg/redirect/tcp_stub.go Normal file
View File

@ -0,0 +1,23 @@
//go:build !linux
// +build !linux
package redirect
import (
"errors"
"github.com/tobyxdd/hysteria/pkg/core"
"net"
"time"
)
type TCPRedirect struct{}
func NewTCPRedirect(hyClient *core.Client, listen string, timeout time.Duration,
connFunc func(addr, reqAddr net.Addr),
errorFunc func(addr, reqAddr net.Addr, err error)) (*TCPRedirect, error) {
return nil, errors.New("not supported on the current system")
}
func (r *TCPRedirect) ListenAndServe() error {
return nil
}