diff --git a/cmd/client.go b/cmd/client.go index 3bb7614..69b3ec6 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -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") } diff --git a/cmd/config.go b/cmd/config.go index ce5edb6..a8023b0 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -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") } diff --git a/pkg/redirect/tcp_linux.go b/pkg/redirect/tcp_linux.go new file mode 100644 index 0000000..4f87193 --- /dev/null +++ b/pkg/redirect/tcp_linux.go @@ -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") + } +} diff --git a/pkg/redirect/tcp_stub.go b/pkg/redirect/tcp_stub.go new file mode 100644 index 0000000..a7aaa1a --- /dev/null +++ b/pkg/redirect/tcp_stub.go @@ -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 +}