feat: TCP redirect mode

This commit is contained in:
Toby 2023-09-10 17:32:30 -07:00
parent 1736e6026e
commit 83a6e5f9a9
8 changed files with 299 additions and 55 deletions

View file

@ -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))
}
}

View file

@ -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",
},
})
}

View file

@ -62,3 +62,6 @@ tcpTProxy:
udpTProxy:
listen: 127.0.0.1:2501
timeout: 20s
tcpRedirect:
listen: 127.0.0.1:3500

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,7 @@
//go:build gc
// +build gc
#include "textflag.h"
TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36
JMP syscall·socketcall(SB)

View file

@ -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")
}
}

View file

@ -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")
}