diff --git a/app/client.example.yaml b/app/client.example.yaml index ec6a6a8c40..6867f49088 100644 --- a/app/client.example.yaml +++ b/app/client.example.yaml @@ -39,11 +39,23 @@ http: # password: pass # realm: my_private_realm -forwarding: - - listen: 127.0.0.1:6666 - remote: 127.0.0.1:5201 - protocol: tcp +tcpForwarding: + - listen: 127.0.0.1:8088 + remote: example.com:80 + - listen: 127.0.0.1:9099 + remote: example.com:90 + +udpForwarding: - listen: 127.0.0.1:5353 - remote: 1.1.1.1:53 - protocol: udp - udpTimeout: 30s + remote: example.com:53 + timeout: 50s + - listen: 127.0.0.1:6464 + remote: example.com:64 + timeout: 20s + +tcpTProxy: + listen: 127.0.0.1:2500 + +udpTProxy: + listen: 127.0.0.1:2501 + timeout: 20s diff --git a/app/cmd/client.go b/app/cmd/client.go index 92fb0db527..8fc986b7cb 100644 --- a/app/cmd/client.go +++ b/app/cmd/client.go @@ -18,6 +18,7 @@ import ( "github.com/apernet/hysteria/app/internal/forwarding" "github.com/apernet/hysteria/app/internal/http" "github.com/apernet/hysteria/app/internal/socks5" + "github.com/apernet/hysteria/app/internal/tproxy" "github.com/apernet/hysteria/core/client" "github.com/apernet/hysteria/extras/obfs" ) @@ -43,17 +44,20 @@ func initClientFlags() { } type clientConfig struct { - Server string `mapstructure:"server"` - Auth string `mapstructure:"auth"` - Obfs clientConfigObfs `mapstructure:"obfs"` - TLS clientConfigTLS `mapstructure:"tls"` - QUIC clientConfigQUIC `mapstructure:"quic"` - Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"` - FastOpen bool `mapstructure:"fastOpen"` - Lazy bool `mapstructure:"lazy"` - SOCKS5 *socks5Config `mapstructure:"socks5"` - HTTP *httpConfig `mapstructure:"http"` - Forwarding []forwardingEntry `mapstructure:"forwarding"` + Server string `mapstructure:"server"` + Auth string `mapstructure:"auth"` + Obfs clientConfigObfs `mapstructure:"obfs"` + TLS clientConfigTLS `mapstructure:"tls"` + QUIC clientConfigQUIC `mapstructure:"quic"` + Bandwidth clientConfigBandwidth `mapstructure:"bandwidth"` + FastOpen bool `mapstructure:"fastOpen"` + Lazy bool `mapstructure:"lazy"` + SOCKS5 *socks5Config `mapstructure:"socks5"` + HTTP *httpConfig `mapstructure:"http"` + TCPForwarding []tcpForwardingEntry `mapstructure:"tcpForwarding"` + UDPForwarding []udpForwardingEntry `mapstructure:"udpForwarding"` + TCPTProxy *tcpTProxyConfig `mapstructure:"tcpTProxy"` + UDPTProxy *udpTProxyConfig `mapstructure:"udpTProxy"` } type clientConfigObfsSalamander struct { @@ -100,11 +104,24 @@ type httpConfig struct { Realm string `mapstructure:"realm"` } -type forwardingEntry struct { - Listen string `mapstructure:"listen"` - Remote string `mapstructure:"remote"` - Protocol string `mapstructure:"protocol"` - UDPTimeout time.Duration `mapstructure:"udpTimeout"` +type tcpForwardingEntry struct { + Listen string `mapstructure:"listen"` + Remote string `mapstructure:"remote"` +} + +type udpForwardingEntry struct { + Listen string `mapstructure:"listen"` + Remote string `mapstructure:"remote"` + Timeout time.Duration `mapstructure:"timeout"` +} + +type tcpTProxyConfig struct { + Listen string `mapstructure:"listen"` +} + +type udpTProxyConfig struct { + Listen string `mapstructure:"listen"` + Timeout time.Duration `mapstructure:"timeout"` } func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error { @@ -355,13 +372,43 @@ func runClient(cmd *cobra.Command, args []string) { } }() } - if len(config.Forwarding) > 0 { + if len(config.TCPForwarding) > 0 { hasMode = true wg.Add(1) go func() { defer wg.Done() - if err := clientForwarding(config.Forwarding, c); err != nil { - logger.Fatal("failed to run forwarding", zap.Error(err)) + if err := clientTCPForwarding(config.TCPForwarding, c); err != nil { + logger.Fatal("failed to run TCP forwarding", zap.Error(err)) + } + }() + } + 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)) + } + }() + } + 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)) + } + }() + } + 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)) } }() } @@ -425,7 +472,7 @@ func clientHTTP(config httpConfig, c client.Client) error { return h.Serve(l) } -func clientForwarding(entries []forwardingEntry, c client.Client) error { +func clientTCPForwarding(entries []tcpForwardingEntry, c client.Client) error { errChan := make(chan error, len(entries)) for _, e := range entries { if e.Listen == "" { @@ -434,44 +481,85 @@ func clientForwarding(entries []forwardingEntry, c client.Client) error { if e.Remote == "" { return configError{Field: "remote", Err: errors.New("remote address is empty")} } - switch strings.ToLower(e.Protocol) { - case "tcp": - l, err := net.Listen("tcp", e.Listen) - if err != nil { - return configError{Field: "listen", Err: err} - } - logger.Info("TCP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) - go func(remote string) { - t := &forwarding.TCPTunnel{ - HyClient: c, - Remote: remote, - EventLogger: &tcpLogger{}, - } - errChan <- t.Serve(l) - }(e.Remote) - case "udp": - l, err := net.ListenPacket("udp", e.Listen) - if err != nil { - return configError{Field: "listen", Err: err} + l, err := net.Listen("tcp", e.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + logger.Info("TCP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) + go func(remote string) { + t := &forwarding.TCPTunnel{ + HyClient: c, + Remote: remote, + EventLogger: &tcpLogger{}, } - logger.Info("UDP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) - go func(remote string, timeout time.Duration) { - u := &forwarding.UDPTunnel{ - HyClient: c, - Remote: remote, - Timeout: timeout, - EventLogger: &udpLogger{}, - } - errChan <- u.Serve(l) - }(e.Remote, e.UDPTimeout) - default: - return configError{Field: "protocol", Err: errors.New("unsupported protocol")} + errChan <- t.Serve(l) + }(e.Remote) + } + // Return if any one of the forwarding fails + return <-errChan +} + +func clientUDPForwarding(entries []udpForwardingEntry, c client.Client) error { + errChan := make(chan error, len(entries)) + for _, e := range entries { + if e.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} } + if e.Remote == "" { + return configError{Field: "remote", Err: errors.New("remote address is empty")} + } + l, err := net.ListenPacket("udp", e.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + logger.Info("UDP forwarding listening", zap.String("addr", e.Listen), zap.String("remote", e.Remote)) + go func(remote string, timeout time.Duration) { + u := &forwarding.UDPTunnel{ + HyClient: c, + Remote: remote, + Timeout: timeout, + EventLogger: &udpLogger{}, + } + errChan <- u.Serve(l) + }(e.Remote, e.Timeout) } // Return if any one of the forwarding fails return <-errChan } +func clientTCPTProxy(config tcpTProxyConfig, 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 := &tproxy.TCPTProxy{ + HyClient: c, + EventLogger: &tcpTProxyLogger{}, + } + logger.Info("TCP transparent proxy listening", zap.String("addr", config.Listen)) + return p.ListenAndServe(laddr) +} + +func clientUDPTProxy(config udpTProxyConfig, c client.Client) error { + if config.Listen == "" { + return configError{Field: "listen", Err: errors.New("listen address is empty")} + } + laddr, err := net.ResolveUDPAddr("udp", config.Listen) + if err != nil { + return configError{Field: "listen", Err: err} + } + p := &tproxy.UDPTProxy{ + HyClient: c, + Timeout: config.Timeout, + EventLogger: &udpTProxyLogger{}, + } + logger.Info("UDP transparent proxy 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, hostPort string) { @@ -584,3 +672,31 @@ func (l *udpLogger) Error(addr net.Addr, err error) { logger.Error("UDP forwarding error", zap.String("addr", addr.String()), zap.Error(err)) } } + +type tcpTProxyLogger struct{} + +func (l *tcpTProxyLogger) Connect(addr, reqAddr net.Addr) { + logger.Debug("TCP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) +} + +func (l *tcpTProxyLogger) Error(addr, reqAddr net.Addr, err error) { + if err == nil { + logger.Debug("TCP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) + } else { + logger.Error("TCP transparent proxy error", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String()), zap.Error(err)) + } +} + +type udpTProxyLogger struct{} + +func (l *udpTProxyLogger) Connect(addr, reqAddr net.Addr) { + logger.Debug("UDP transparent proxy connect", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) +} + +func (l *udpTProxyLogger) Error(addr, reqAddr net.Addr, err error) { + if err == nil { + logger.Debug("UDP transparent proxy closed", zap.String("addr", addr.String()), zap.String("reqAddr", reqAddr.String())) + } else { + logger.Error("UDP transparent proxy 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 a994233a6d..ecbf9ce246 100644 --- a/app/cmd/client_test.go +++ b/app/cmd/client_test.go @@ -58,19 +58,26 @@ func TestClientConfig(t *testing.T) { Password: "bruh", Realm: "martian", }, - Forwarding: []forwardingEntry{ + TCPForwarding: []tcpForwardingEntry{ { - Listen: "127.0.0.1:8088", - Remote: "internal.example.com:80", - Protocol: "tcp", + Listen: "127.0.0.1:8088", + Remote: "internal.example.com:80", }, + }, + UDPForwarding: []udpForwardingEntry{ { - Listen: "127.0.0.1:5353", - Remote: "internal.example.com:53", - Protocol: "udp", - UDPTimeout: 50 * time.Second, + Listen: "127.0.0.1:5353", + Remote: "internal.example.com:53", + Timeout: 50 * time.Second, }, }, + TCPTProxy: &tcpTProxyConfig{ + Listen: "127.0.0.1:2500", + }, + UDPTProxy: &udpTProxyConfig{ + Listen: "127.0.0.1:2501", + Timeout: 20 * time.Second, + }, }) } diff --git a/app/cmd/client_test.yaml b/app/cmd/client_test.yaml index 45a0736741..9b56c250d3 100644 --- a/app/cmd/client_test.yaml +++ b/app/cmd/client_test.yaml @@ -41,11 +41,18 @@ http: password: bruh realm: martian -forwarding: +tcpForwarding: - listen: 127.0.0.1:8088 remote: internal.example.com:80 - protocol: tcp + +udpForwarding: - listen: 127.0.0.1:5353 remote: internal.example.com:53 - protocol: udp - udpTimeout: 50s + timeout: 50s + +tcpTProxy: + listen: 127.0.0.1:2500 + +udpTProxy: + listen: 127.0.0.1:2501 + timeout: 20s diff --git a/app/go.mod b/app/go.mod index 04d3ba0699..55aab58fda 100644 --- a/app/go.mod +++ b/app/go.mod @@ -15,6 +15,7 @@ require ( ) require ( + github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f // indirect github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect diff --git a/app/go.sum b/app/go.sum index db6c7ed3eb..63137d686d 100644 --- a/app/go.sum +++ b/app/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f h1:uVh0qpEslrWjgzx9vOcyCqsOY3c9kofDZ1n+qaw35ZY= +github.com/apernet/go-tproxy v0.0.0-20230809025308-8f4723fd742f/go.mod h1:xkkq9D4ygcldQQhKS/w9CadiCKwCngU7K9E3DaKahpM= github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e h1:hWrd6A3QZQX2pXT1JJA2x1vgqNf5jZH8po0oa2GsbeI= github.com/apernet/quic-go v0.37.5-0.20230809210726-5508a358d07e/go.mod h1:Gqxx9qMiutRcTLNlbdPwuI9dF8+GV2GQG+5mVW0E34I= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= diff --git a/app/internal/tproxy/tcp_linux.go b/app/internal/tproxy/tcp_linux.go new file mode 100644 index 0000000000..4c922f1706 --- /dev/null +++ b/app/internal/tproxy/tcp_linux.go @@ -0,0 +1,69 @@ +package tproxy + +import ( + "io" + "net" + + "github.com/apernet/go-tproxy" + "github.com/apernet/hysteria/core/client" +) + +type TCPTProxy struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error { + listener, err := tproxy.ListenTCP("tcp", laddr) + if err != nil { + return err + } + defer listener.Close() + for { + c, err := listener.Accept() + if err != nil { + return err + } + go r.handle(c) + } +} + +func (r *TCPTProxy) handle(conn net.Conn) { + defer conn.Close() + // In TProxy mode, we are masquerading as the remote server. + // So LocalAddr is actually the target the user is trying to connect to, + // and RemoteAddr is the local address. + if r.EventLogger != nil { + r.EventLogger.Connect(conn.RemoteAddr(), conn.LocalAddr()) + } + var closeErr error + defer func() { + if r.EventLogger != nil { + r.EventLogger.Error(conn.RemoteAddr(), conn.LocalAddr(), closeErr) + } + }() + + rc, err := r.HyClient.TCP(conn.LocalAddr().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 +} diff --git a/app/internal/tproxy/tcp_others.go b/app/internal/tproxy/tcp_others.go new file mode 100644 index 0000000000..27e98c5b33 --- /dev/null +++ b/app/internal/tproxy/tcp_others.go @@ -0,0 +1,24 @@ +//go:build !linux + +package tproxy + +import ( + "errors" + "net" + + "github.com/apernet/hysteria/core/client" +) + +type TCPTProxy struct { + HyClient client.Client + EventLogger TCPEventLogger +} + +type TCPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *TCPTProxy) ListenAndServe(laddr *net.TCPAddr) error { + return errors.New("not supported on this platform") +} diff --git a/app/internal/tproxy/udp_linux.go b/app/internal/tproxy/udp_linux.go new file mode 100644 index 0000000000..c3665671d0 --- /dev/null +++ b/app/internal/tproxy/udp_linux.go @@ -0,0 +1,138 @@ +package tproxy + +import ( + "errors" + "net" + "time" + + "github.com/apernet/go-tproxy" + "github.com/apernet/hysteria/core/client" +) + +const ( + udpBufferSize = 4096 + defaultTimeout = 60 * time.Second +) + +type UDPTProxy struct { + HyClient client.Client + Timeout time.Duration + EventLogger UDPEventLogger +} + +type UDPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error { + conn, err := tproxy.ListenUDP("udp", laddr) + if err != nil { + return err + } + defer conn.Close() + buf := make([]byte, udpBufferSize) + for { + // We will only get the first packet of each src/dst pair here, + // because newPair will create a TProxy connection and take over + // the src/dst pair. Later packets will be sent there instead of here. + n, srcAddr, dstAddr, err := tproxy.ReadFromUDP(conn, buf) + if err != nil { + return err + } + r.newPair(srcAddr, dstAddr, buf[:n]) + } +} + +func (r *UDPTProxy) newPair(srcAddr, dstAddr *net.UDPAddr, initPkt []byte) { + if r.EventLogger != nil { + r.EventLogger.Connect(srcAddr, dstAddr) + } + var closeErr error + defer func() { + // If closeErr is nil, it means we at least successfully sent the first packet + // and started forwarding, in which case we don't call the error logger. + if r.EventLogger != nil && closeErr != nil { + r.EventLogger.Error(srcAddr, dstAddr, closeErr) + } + }() + conn, err := tproxy.DialUDP("udp", dstAddr, srcAddr) + if err != nil { + closeErr = err + return + } + hyConn, err := r.HyClient.UDP() + if err != nil { + _ = conn.Close() + closeErr = err + return + } + // Send the first packet + err = hyConn.Send(initPkt, dstAddr.String()) + if err != nil { + _ = conn.Close() + _ = hyConn.Close() + closeErr = err + return + } + // Start forwarding + go r.forwarding(conn, hyConn, dstAddr.String()) +} + +func (r *UDPTProxy) forwarding(conn *net.UDPConn, hyConn client.HyUDPConn, dst string) { + errChan := make(chan error, 2) + // Local <- Remote + go func() { + for { + bs, _, err := hyConn.Receive() + if err != nil { + errChan <- err + return + } + _, err = conn.Write(bs) + if err != nil { + errChan <- err + return + } + _ = r.updateConnDeadline(conn) + } + }() + // Local -> Remote + go func() { + buf := make([]byte, udpBufferSize) + for { + _ = r.updateConnDeadline(conn) + n, err := conn.Read(buf) + if n > 0 { + err := hyConn.Send(buf[:n], dst) + if err != nil { + errChan <- err + return + } + } + if err != nil { + errChan <- err + return + } + } + }() + err := <-errChan + _ = conn.Close() + _ = hyConn.Close() + if r.EventLogger != nil { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + // We don't consider deadline exceeded (timeout) an error + err = nil + } + r.EventLogger.Error(conn.LocalAddr(), conn.RemoteAddr(), err) + } +} + +func (r *UDPTProxy) updateConnDeadline(conn *net.UDPConn) error { + if r.Timeout == 0 { + return conn.SetReadDeadline(time.Now().Add(defaultTimeout)) + } else { + return conn.SetReadDeadline(time.Now().Add(r.Timeout)) + } +} diff --git a/app/internal/tproxy/udp_others.go b/app/internal/tproxy/udp_others.go new file mode 100644 index 0000000000..db9cedbe44 --- /dev/null +++ b/app/internal/tproxy/udp_others.go @@ -0,0 +1,26 @@ +//go:build !linux + +package tproxy + +import ( + "errors" + "net" + "time" + + "github.com/apernet/hysteria/core/client" +) + +type UDPTProxy struct { + HyClient client.Client + Timeout time.Duration + EventLogger UDPEventLogger +} + +type UDPEventLogger interface { + Connect(addr, reqAddr net.Addr) + Error(addr, reqAddr net.Addr, err error) +} + +func (r *UDPTProxy) ListenAndServe(laddr *net.UDPAddr) error { + return errors.New("not supported on this platform") +}