diff --git a/app/cmd/client.go b/app/cmd/client.go index 7277e12..823249a 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -9,7 +9,6 @@ import ( "os" "strconv" "strings" - "sync" "time" "github.com/spf13/cobra" @@ -18,6 +17,7 @@ import ( "github.com/apernet/hysteria/app/internal/forwarding" "github.com/apernet/hysteria/app/internal/http" + "github.com/apernet/hysteria/app/internal/redirect" "github.com/apernet/hysteria/app/internal/socks5" "github.com/apernet/hysteria/app/internal/tproxy" "github.com/apernet/hysteria/app/internal/url" @@ -63,6 +63,7 @@ type clientConfig struct { UDPForwarding []udpForwardingEntry `mapstructure:"udpForwarding"` TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"` UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"` + TCPRedirect *tcpRedirectConfig `mapstructure:"tcpRedirect"` } type clientConfigTransportUDP struct { @@ -139,6 +140,10 @@ type udpTProxyConfig struct { Timeout time.Duration `mapstructure:"timeout"` } +type tcpRedirectConfig struct { + Listen string `mapstructure:"listen"` +} + func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error { if c.Server == "" { return configError{Field: "server", Err: errors.New("server address is empty")} @@ -418,75 +423,81 @@ func runClient(cmd *cobra.Command, args []string) { utils.PrintQR(uri) } - // Modes - var wg sync.WaitGroup - hasMode := false - + // Register modes + var runner clientModeRunner if config.SOCKS5 != nil { - hasMode = true - wg.Add(1) - go func() { - defer wg.Done() - if err := clientSOCKS5(*config.SOCKS5, c); err != nil { - logger.Fatal("failed to run SOCKS5 server", zap.Error(err)) - } - }() + runner.Add("SOCKS5 server", func() error { + return clientSOCKS5(*config.SOCKS5, c) + }) } if config.HTTP != nil { - hasMode = true - wg.Add(1) - go func() { - defer wg.Done() - if err := clientHTTP(*config.HTTP, c); err != nil { - logger.Fatal("failed to run HTTP proxy server", zap.Error(err)) - } - }() + runner.Add("HTTP proxy server", func() error { + return clientHTTP(*config.HTTP, c) + }) } if len(config.TCPForwarding) > 0 { - hasMode = true - wg.Add(1) - go func() { - defer wg.Done() - if err := clientTCPForwarding(config.TCPForwarding, c); err != nil { - logger.Fatal("failed to run TCP forwarding", zap.Error(err)) - } - }() + runner.Add("TCP forwarding", func() error { + return clientTCPForwarding(config.TCPForwarding, c) + }) } if len(config.UDPForwarding) > 0 { - hasMode = true - wg.Add(1) - go func() { - defer wg.Done() - if err := clientUDPForwarding(config.UDPForwarding, c); err != nil { - logger.Fatal("failed to run UDP forwarding", zap.Error(err)) - } - }() + runner.Add("UDP forwarding", func() error { + return clientUDPForwarding(config.UDPForwarding, c) + }) } if config.TCPTProxy != nil { - hasMode = true - wg.Add(1) - go func() { - defer wg.Done() - if err := clientTCPTProxy(*config.TCPTProxy, c); err != nil { - logger.Fatal("failed to run TCP transparent proxy", zap.Error(err)) - } - }() + runner.Add("TCP transparent proxy", func() error { + return clientTCPTProxy(*config.TCPTProxy, c) + }) } if config.UDPTProxy != nil { - hasMode = true - wg.Add(1) - go func() { - defer wg.Done() - if err := clientUDPTProxy(*config.UDPTProxy, c); err != nil { - logger.Fatal("failed to run UDP transparent proxy", zap.Error(err)) - } - }() + runner.Add("UDP transparent proxy", func() error { + return clientUDPTProxy(*config.UDPTProxy, c) + }) + } + if config.TCPRedirect != nil { + runner.Add("TCP redirect", func() error { + return clientTCPRedirect(*config.TCPRedirect, c) + }) } - if !hasMode { + runner.Run() +} + +type clientModeRunner struct { + ModeMap map[string]func() error +} + +func (r *clientModeRunner) Add(name string, f func() error) { + if r.ModeMap == nil { + r.ModeMap = make(map[string]func() error) + } + r.ModeMap[name] = f +} + +func (r *clientModeRunner) Run() { + if len(r.ModeMap) == 0 { logger.Fatal("no mode specified") } - wg.Wait() + + type modeError struct { + Name string + Err error + } + errChan := make(chan modeError, len(r.ModeMap)) + for name, f := range r.ModeMap { + go func(name string, f func() error) { + err := f() + errChan <- modeError{name, err} + }(name, f) + } + // Fatal if any one of the modes fails + for i := 0; i < len(r.ModeMap); i++ { + e := <-errChan + if e.Err != nil { + logger.Fatal("failed to run "+e.Name, zap.Error(e.Err)) + } + } } func clientSOCKS5(config socks5Config, c client.Client) error { @@ -630,6 +641,22 @@ func clientUDPTProxy(config udpTProxyConfig, c client.Client) error { return p.ListenAndServe(laddr) } +func clientTCPRedirect(config tcpRedirectConfig, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + laddr, err := net.ResolveTCPAddr("tcp", config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + p := &redirect.TCPRedirect{ + HyClient: c, + EventLogger: &tcpRedirectLogger{}, + } + logger.Info("TCP redirect listening", zap.String("addr", config.Listen)) + return p.ListenAndServe(laddr) +} + // parseServerAddrString parses server address string. // Server address can be in either "host:port" or "host" format (in which case we assume port 443). func parseServerAddrString(addrStr string) (host, port, hostPort string) { @@ -783,3 +810,17 @@ func (l *udpTProxyLogger) Error(addr, reqAddr net.Addr, err error) { logger.Error("UDP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) } } + +type tcpRedirectLogger struct{} + +func (l *tcpRedirectLogger) Connect(addr, reqAddr net.Addr) { + logger.Debug("TCP redirect connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) +} + +func (l *tcpRedirectLogger) Error(addr, reqAddr net.Addr, err error) { + if err == nil { + logger.Debug("TCP redirect closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) + } else { + logger.Error("TCP redirect error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) + } +} diff --git a/app/cmd/client_test.go b/app/cmd/client_test.go index 05c77e7..faee251 100644 --- a/app/cmd/client_test.go +++ b/app/cmd/client_test.go @@ -85,6 +85,9 @@ func TestClientConfig(t *testing.T) { Listen: "127.0.0.1:2501", Timeout: 20 * time.Second, }, + TCPRedirect: &tcpRedirectConfig{ + Listen: "127.0.0.1:3500", + }, }) } diff --git a/app/cmd/client_test.yaml b/app/cmd/client_test.yaml index c5c675b..3e2f4aa 100644 --- a/app/cmd/client_test.yaml +++ b/app/cmd/client_test.yaml @@ -62,3 +62,6 @@ tcpTProxy: udpTProxy: listen: 127.0.0.1:2501 timeout: 20s + +tcpRedirect: + listen: 127.0.0.1:3500 diff --git a/app/internal/redirect/getsockopt_linux.go b/app/internal/redirect/getsockopt_linux.go new file mode 100644 index 0000000..b84593e --- /dev/null +++ b/app/internal/redirect/getsockopt_linux.go @@ -0,0 +1,17 @@ +//go:build !386 +// +build !386 + +package redirect + +import ( + "syscall" + "unsafe" +) + +func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { + _, _, e := syscall.Syscall6(syscall.SYS_GETSOCKOPT, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) + if e != 0 { + err = e + } + return +} diff --git a/app/internal/redirect/getsockopt_linux_386.go b/app/internal/redirect/getsockopt_linux_386.go new file mode 100644 index 0000000..f3832ec --- /dev/null +++ b/app/internal/redirect/getsockopt_linux_386.go @@ -0,0 +1,23 @@ +package redirect + +import ( + "syscall" + "unsafe" +) + +const ( + sysGetsockopt = 15 +) + +// On 386 we cannot call socketcall with syscall.Syscall6, as it always fails with EFAULT. +// Use our own syscall.socketcall hack instead. + +func syscall_socketcall(call int, a0, a1, a2, a3, a4, a5 uintptr) (n int, err syscall.Errno) + +func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err error) { + _, e := syscall_socketcall(sysGetsockopt, s, level, name, uintptr(val), uintptr(unsafe.Pointer(vallen)), 0) + if e != 0 { + err = e + } + return +} diff --git a/app/internal/redirect/syscall_socketcall_linux_386.s b/app/internal/redirect/syscall_socketcall_linux_386.s new file mode 100644 index 0000000..2dab43a --- /dev/null +++ b/app/internal/redirect/syscall_socketcall_linux_386.s @@ -0,0 +1,7 @@ +//go:build gc +// +build gc + +#include "textflag.h" + +TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36 + JMP syscall·socketcall(SB) diff --git a/app/internal/redirect/tcp_linux.go b/app/internal/redirect/tcp_linux.go new file mode 100644 index 0000000..8445fa3 --- /dev/null +++ b/app/internal/redirect/tcp_linux.go @@ -0,0 +1,126 @@ +package redirect + +import ( + "encoding/binary" + "errors" + "io" + "net" + "syscall" + "unsafe" + + "github.com/apernet/hysteria/core/client" +) + +const ( + soOriginalDst = 80 + soOriginalDstV6 = 80 +) + +type TCPRedirect struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error { + listener, err := net.ListenTCP("tcp", laddr) + if err != nil { + return err + } + defer listener.Close() + for { + c, err := listener.AcceptTCP() + if err != nil { + return err + } + go r.handle(c) + } +} + +func (r *TCPRedirect) handle(conn *net.TCPConn) { + defer conn.Close() + dstAddr, err := getDstAddr(conn) + if err != nil { + // Fail silently if we can't get the original destination. + // Maybe we should print something to the log? + return + } + if r.EventLogger != nil { + r.EventLogger.Connect(conn.RemoteAddr(), dstAddr) + } + var closeErr error + defer func() { + if r.EventLogger != nil { + r.EventLogger.Error(conn.RemoteAddr(), dstAddr, closeErr) + } + }() + + rc, err := r.HyClient.TCP(dstAddr.String()) + if err != nil { + closeErr = err + return + } + defer rc.Close() + + // Start forwarding + copyErrChan := make(chan error, 2) + go func() { + _, copyErr := io.Copy(rc, conn) + copyErrChan <- copyErr + }() + go func() { + _, copyErr := io.Copy(conn, rc) + copyErrChan <- copyErr + }() + closeErr = <-copyErrChan +} + +type sockAddr struct { + family uint16 + port [2]byte // always big endian regardless of platform + data [24]byte // sockaddr_in or sockaddr_in6 +} + +func getOriginalDst(fd uintptr) (*sockAddr, error) { + var addr sockAddr + addrSize := uint32(unsafe.Sizeof(addr)) + // Try IPv6 first + err := getsockopt(fd, syscall.SOL_IPV6, soOriginalDstV6, unsafe.Pointer(&addr), &addrSize) + if err == nil { + return &addr, nil + } + // Then IPv4 + err = getsockopt(fd, syscall.SOL_IP, soOriginalDst, unsafe.Pointer(&addr), &addrSize) + return &addr, err +} + +// getDstAddr returns the original destination of a redirected TCP connection. +func getDstAddr(conn *net.TCPConn) (*net.TCPAddr, error) { + rc, err := conn.SyscallConn() + if err != nil { + return nil, err + } + var addr *sockAddr + var err2 error + err = rc.Control(func(fd uintptr) { + addr, err2 = getOriginalDst(fd) + }) + 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("address family not IPv4 or IPv6") + } +} diff --git a/app/internal/redirect/tcp_others.go b/app/internal/redirect/tcp_others.go new file mode 100644 index 0000000..ddb770f --- /dev/null +++ b/app/internal/redirect/tcp_others.go @@ -0,0 +1,24 @@ +//go:build !linux + +package redirect + +import ( + "errors" + "net" + + "github.com/apernet/hysteria/core/client" +) + +type TCPRedirect struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPRedirect) ListenAndServe(laddr *net.TCPAddr) error { + return errors.New("not supported on this platform") +}