mirror of
https://github.com/apernet/hysteria.git
synced 2025-04-03 20:47:38 +03:00
feat: TCP redirect mode
This commit is contained in:
parent
1736e6026e
commit
83a6e5f9a9
8 changed files with 299 additions and 55 deletions
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -62,3 +62,6 @@ tcpTProxy:
|
|||
udpTProxy:
|
||||
listen: 127.0.0.1:2501
|
||||
timeout: 20s
|
||||
|
||||
tcpRedirect:
|
||||
listen: 127.0.0.1:3500
|
||||
|
|
17
app/internal/redirect/getsockopt_linux.go
Normal file
17
app/internal/redirect/getsockopt_linux.go
Normal 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
|
||||
}
|
23
app/internal/redirect/getsockopt_linux_386.go
Normal file
23
app/internal/redirect/getsockopt_linux_386.go
Normal 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
|
||||
}
|
7
app/internal/redirect/syscall_socketcall_linux_386.s
Normal file
7
app/internal/redirect/syscall_socketcall_linux_386.s
Normal file
|
@ -0,0 +1,7 @@
|
|||
//go:build gc
|
||||
// +build gc
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·syscall_socketcall(SB),NOSPLIT,$0-36
|
||||
JMP syscall·socketcall(SB)
|
126
app/internal/redirect/tcp_linux.go
Normal file
126
app/internal/redirect/tcp_linux.go
Normal 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")
|
||||
}
|
||||
}
|
24
app/internal/redirect/tcp_others.go
Normal file
24
app/internal/redirect/tcp_others.go
Normal 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")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue