From d6cd1605262fa3c265a728699ee03a0a8d4881b8 Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Sat, 2 Aug 2025 09:06:28 +0300 Subject: [PATCH 01/10] migrated to wzshiming/socks5 from net/proxy because of udp support for socks5 proxy --- dialer_linux.go | 29 ++++++++++++++++++++++++ dialer_nonlinux.go | 14 ++++++++++++ go.mod | 3 ++- go.sum | 2 ++ gohpts.go | 43 ++++++++++++++++++----------------- helpers.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++ tproxy_linux.go | 20 +---------------- tproxy_nonlinux.go | 6 ----- 8 files changed, 127 insertions(+), 46 deletions(-) create mode 100644 dialer_linux.go create mode 100644 dialer_nonlinux.go diff --git a/dialer_linux.go b/dialer_linux.go new file mode 100644 index 0000000..d3491fc --- /dev/null +++ b/dialer_linux.go @@ -0,0 +1,29 @@ +//go:build linux +// +build linux + +package gohpts + +import ( + "net" + "syscall" + "time" + + "golang.org/x/sys/unix" +) + +func getBaseDialer(timeout time.Duration, mark uint) *net.Dialer { + var dialer *net.Dialer + if mark > 0 { + dialer = &net.Dialer{ + Timeout: timeout, + Control: func(_, _ string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, int(mark)) + }) + }, + } + } else { + dialer = &net.Dialer{Timeout: timeout} + } + return dialer +} diff --git a/dialer_nonlinux.go b/dialer_nonlinux.go new file mode 100644 index 0000000..3061a14 --- /dev/null +++ b/dialer_nonlinux.go @@ -0,0 +1,14 @@ +//go:build !linux +// +build !linux + +package gohpts + +import ( + "net" + "time" +) + +func getBaseDialer(timeout time.Duration, mark uint) *net.Dialer { + _ = mark + return &net.Dialer{Timeout: timeout} +} diff --git a/go.mod b/go.mod index 6bd3717..291add7 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/shadowy-pycoder/colors v0.0.1 github.com/shadowy-pycoder/mshark v0.0.10 - golang.org/x/net v0.40.0 + github.com/wzshiming/socks5 v0.5.2 golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 ) @@ -21,5 +21,6 @@ require ( github.com/mdlayher/packet v1.1.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/pkg/errors v0.9.1 // indirect + golang.org/x/net v0.40.0 // indirect golang.org/x/sync v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index 09a471b..235f479 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/shadowy-pycoder/mshark v0.0.10 h1:pLMIsgfvnO0oKeBNdy0fTGQsx//6scCPT52 github.com/shadowy-pycoder/mshark v0.0.10/go.mod h1:FqbHFdsx0zMnrZZH0+oPzaFcleP4O+tUWv8i5gxo87k= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wzshiming/socks5 v0.5.2 h1:LtoowVNwAmkIQSkP1r1Wg435xUmC+tfRxorNW30KtnM= +github.com/wzshiming/socks5 v0.5.2/go.mod h1:BvCAqlzocQN5xwLjBZDBbvWlrx8sCYSSbHEOf2wZgT0= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= diff --git a/gohpts.go b/gohpts.go index 4de72a5..9216305 100644 --- a/gohpts.go +++ b/gohpts.go @@ -31,7 +31,7 @@ import ( "github.com/shadowy-pycoder/mshark/arpspoof" "github.com/shadowy-pycoder/mshark/layers" "github.com/shadowy-pycoder/mshark/network" - "golang.org/x/net/proxy" + "github.com/wzshiming/socks5" ) const ( @@ -127,7 +127,7 @@ type proxyapp struct { httpServer *http.Server sockClient *http.Client httpClient *http.Client - sockDialer proxy.Dialer + sockDialer *socks5.Dialer logger *zerolog.Logger snifflogger *zerolog.Logger certFile string @@ -390,11 +390,11 @@ func New(conf *Config) *proxyapp { if err != nil { p.logger.Fatal().Err(err).Msg("") } - auth := proxy.Auth{ + auth := Auth{ User: conf.User, Password: conf.Pass, } - dialer, err := proxy.SOCKS5("tcp", addrSOCKS, &auth, getBaseDialer(timeout, p.mark)) + dialer, err := newSOCKS5Dialer(addrSOCKS, &auth, getBaseDialer(timeout, p.mark)) if err != nil { p.logger.Fatal().Err(err).Msg("Unable to create SOCKS5 dialer") } @@ -402,7 +402,7 @@ func New(conf *Config) *proxyapp { if !tproxyonly { p.sockClient = &http.Client{ Transport: &http.Transport{ - Dial: dialer.Dial, + DialContext: dialer.DialContext, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse @@ -764,7 +764,7 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) { } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - dstConn, err = sockDialer.(proxy.ContextDialer).DialContext(ctx, "tcp", r.Host) + dstConn, err = sockDialer.DialContext(ctx, "tcp", r.Host) if err != nil { p.logger.Error().Err(err).Msgf("Failed connecting to %s", r.Host) http.Error(w, err.Error(), http.StatusServiceUnavailable) @@ -849,18 +849,17 @@ func (p *proxyapp) updateSocksList() { p.mu.Lock() defer p.mu.Unlock() p.availProxyList = p.availProxyList[:0] - var base proxy.Dialer = getBaseDialer(timeout, p.mark) - var dialer proxy.Dialer + var dialer *socks5.Dialer var err error failed := 0 chainType := p.proxychain.Type ctl := colorizeChainType(chainType, p.nocolor) for _, pr := range p.proxylist { - auth := proxy.Auth{ + auth := Auth{ User: pr.Username, Password: pr.Password, } - dialer, err = proxy.SOCKS5("tcp", pr.Address, &auth, base) + dialer, err = newSOCKS5Dialer(pr.Address, &auth, getBaseDialer(timeout, p.mark)) if err != nil { p.logger.Error().Err(err).Msgf("%s Unable to create SOCKS5 dialer %s", ctl, pr.Address) failed++ @@ -868,7 +867,7 @@ func (p *proxyapp) updateSocksList() { } ctx, cancel := context.WithTimeout(context.Background(), hopTimeout) defer cancel() - conn, err := dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", pr.Address) + conn, err := dialer.DialContext(ctx, "tcp", pr.Address) if err != nil && !errors.Is(err, io.EOF) { // check for EOF to include localhost SOCKS5 in the chain p.logger.Error().Err(err).Msgf("%s Unable to connect to %s", ctl, pr.Address) failed++ @@ -890,11 +889,11 @@ func (p *proxyapp) updateSocksList() { } currentDialer := dialer for _, pr := range p.proxylist[failed+1:] { - auth := proxy.Auth{ + auth := Auth{ User: pr.Username, Password: pr.Password, } - dialer, err = proxy.SOCKS5("tcp", pr.Address, &auth, currentDialer) + dialer, err = newSOCKS5Dialer(pr.Address, &auth, currentDialer) if err != nil { p.logger.Error().Err(err).Msgf("%s Unable to create SOCKS5 dialer %s", ctl, pr.Address) continue @@ -902,7 +901,7 @@ func (p *proxyapp) updateSocksList() { // https://github.com/golang/go/issues/37549#issuecomment-1178745487 ctx, cancel := context.WithTimeout(context.Background(), hopTimeout) defer cancel() - conn, err := dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", pr.Address) + conn, err := dialer.DialContext(ctx, "tcp", pr.Address) if err != nil { p.logger.Error().Err(err).Msgf("%s Unable to connect to %s", ctl, pr.Address) if conn != nil { @@ -929,7 +928,7 @@ func shuffle(vals []proxyEntry) { } } -func (p *proxyapp) getSocks() (proxy.Dialer, *http.Client, error) { +func (p *proxyapp) getSocks() (*socks5.Dialer, *http.Client, error) { if p.proxylist == nil { return p.sockDialer, p.sockClient, nil } @@ -984,14 +983,18 @@ func (p *proxyapp) getSocks() (proxy.Dialer, *http.Client, error) { p.logger.Error().Msgf("%s Not all SOCKS5 Proxy available", ctl) return nil, nil, fmt.Errorf("not all socks5 proxy available") } - var dialer proxy.Dialer = getBaseDialer(timeout, p.mark) + var dialer *socks5.Dialer var err error - for _, pr := range copyProxyList { - auth := proxy.Auth{ + for i, pr := range copyProxyList { + auth := Auth{ User: pr.Username, Password: pr.Password, } - dialer, err = proxy.SOCKS5("tcp", pr.Address, &auth, dialer) + if i > 0 { + dialer, err = newSOCKS5Dialer(pr.Address, &auth, dialer) + } else { + dialer, err = newSOCKS5Dialer(pr.Address, &auth, getBaseDialer(timeout, p.mark)) + } if err != nil { p.logger.Error().Err(err).Msgf("%s Unable to create SOCKS5 dialer %s", ctl, pr.Address) return nil, nil, err @@ -999,7 +1002,7 @@ func (p *proxyapp) getSocks() (proxy.Dialer, *http.Client, error) { } socks := &http.Client{ Transport: &http.Transport{ - Dial: dialer.Dial, + DialContext: dialer.DialContext, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse diff --git a/helpers.go b/helpers.go index 6fbcaae..9aadcd5 100644 --- a/helpers.go +++ b/helpers.go @@ -1,15 +1,19 @@ package gohpts import ( + "context" "encoding/base64" + "errors" "fmt" "net" "net/http" + "net/netip" "os" "strconv" "strings" "github.com/shadowy-pycoder/mshark/network" + "github.com/wzshiming/socks5" ) // Hop-by-hop headers @@ -129,3 +133,55 @@ func parseProxyAuth(auth string) (username, password string, ok bool) { } return username, password, true } + +func splitHostPort(address string) (string, int, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return "", 0, err + } + portnum, err := strconv.Atoi(port) + if err != nil { + return "", 0, err + } + if 1 > portnum || portnum > 0xffff { + return "", 0, errors.New("port number out of range " + port) + } + return host, portnum, nil +} + +type Auth struct { + User, Password string +} + +type ContextDialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +var ( + _ ContextDialer = &socks5.Dialer{} + _ ContextDialer = &net.Dialer{} +) + +func newSOCKS5Dialer(address string, auth *Auth, forward ContextDialer) (*socks5.Dialer, error) { + d := &socks5.Dialer{ + ProxyNetwork: "tcp", + IsResolve: false, + } + host, port, err := splitHostPort(address) + if err != nil { + return nil, err + } + ip, err := netip.ParseAddr(host) + if err == nil { + host = ip.String() + } + d.ProxyAddress = net.JoinHostPort(host, strconv.Itoa(port)) + if auth != nil { + d.Username = auth.User + d.Password = auth.Password + } + if forward != nil { + d.ProxyDial = forward.DialContext + } + return d, nil +} diff --git a/tproxy_linux.go b/tproxy_linux.go index 17ee869..8f0a025 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -21,7 +21,6 @@ import ( "github.com/shadowy-pycoder/mshark/layers" "github.com/shadowy-pycoder/mshark/network" - "golang.org/x/net/proxy" "golang.org/x/sys/unix" ) @@ -171,7 +170,7 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - dstConn, err = sockDialer.(proxy.ContextDialer).DialContext(ctx, "tcp", dst) + dstConn, err = sockDialer.DialContext(ctx, "tcp", dst) if err != nil { ts.p.logger.Error().Err(err).Msgf("[%s] Failed connecting to %s", ts.p.tproxyMode, dst) return @@ -240,23 +239,6 @@ func (ts *tproxyServer) Shutdown() { } } -func getBaseDialer(timeout time.Duration, mark uint) *net.Dialer { - var dialer *net.Dialer - if mark > 0 { - dialer = &net.Dialer{ - Timeout: timeout, - Control: func(_, _ string, c syscall.RawConn) error { - return c.Control(func(fd uintptr) { - unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, int(mark)) - }) - }, - } - } else { - dialer = &net.Dialer{Timeout: timeout} - } - return dialer -} - func (ts *tproxyServer) createSysctlOptCmd(opt, value, setex string, opts map[string]string) *exec.Cmd { cmdCat := exec.Command("bash", "-c", fmt.Sprintf(` cat /proc/sys/%s diff --git a/tproxy_nonlinux.go b/tproxy_nonlinux.go index 32e08d3..974a1c6 100644 --- a/tproxy_nonlinux.go +++ b/tproxy_nonlinux.go @@ -8,7 +8,6 @@ import ( "os/exec" "sync" "syscall" - "time" ) type tproxyServer struct { @@ -43,11 +42,6 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { func (ts *tproxyServer) Shutdown() {} -func getBaseDialer(timeout time.Duration, mark uint) *net.Dialer { - _ = mark - return &net.Dialer{Timeout: timeout} -} - func (ts *tproxyServer) createSysctlOptCmd(opt, value, setex string, opts map[string]string) *exec.Cmd { _ = opt _ = value From 0dcb6165eb58bb5628bc954983bfa8634fb66d86 Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Sun, 3 Aug 2025 19:25:22 +0300 Subject: [PATCH 02/10] initial logic for UDP proxy support --- cmd/gohpts/cli.go | 22 +- gohpts.go | 219 +++++++++++++++++- helpers.go | 19 ++ tproxy_linux.go | 179 ++++----------- tproxy_udp_linux.go | 534 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 816 insertions(+), 157 deletions(-) create mode 100644 tproxy_udp_linux.go diff --git a/cmd/gohpts/cli.go b/cmd/gohpts/cli.go index e40d7b9..3d80897 100644 --- a/cmd/gohpts/cli.go +++ b/cmd/gohpts/cli.go @@ -62,6 +62,7 @@ const usageTproxy string = ` TProxy: -t Address of transparent proxy server (it starts along with HTTP proxy server) -T Address of transparent proxy server (no HTTP) + -Tu Address of transparent UDP proxy server -M Transparent proxy mode: (redirect, tproxy) -auto Automatically setup iptables for transparent proxy (requires elevated privileges) -arpspoof Enable ARP spoof proxy for selected targets (Example: "targets 10.0.0.1,10.0.0.5-10,192.168.1.*,192.168.10.0/24;fullduplex false;debug true") @@ -106,6 +107,7 @@ func root(args []string) error { if runtime.GOOS == tproxyOS { flags.StringVar(&conf.TProxy, "t", "", "Address of transparent proxy server (it starts along with HTTP proxy server)") flags.StringVar(&conf.TProxyOnly, "T", "", "Address of transparent proxy server (no HTTP)") + flags.StringVar(&conf.TProxyUDP, "Tu", "", "Address of transparent UDP proxy server") flags.Func("M", fmt.Sprintf("Transparent proxy mode: %s", gohpts.SupportedTProxyModes), func(flagValue string) error { if !slices.Contains(gohpts.SupportedTProxyModes, flagValue) { fmt.Fprintf(os.Stderr, "%s: %s is not supported (type '%s -h' for help)\n", app, flagValue, app) @@ -176,19 +178,27 @@ func root(args []string) error { return fmt.Errorf("transparent proxy mode is not provided: -M flag") } } + if seen["Tu"] { + if !seen["M"] { + return fmt.Errorf("transparent proxy mode is not provided: -M flag") + } + if conf.TProxyMode != "tproxy" { + return fmt.Errorf("transparent UDP proxy require tproxy mode") + } + } if seen["M"] { - if !seen["t"] && !seen["T"] { - return fmt.Errorf("transparent proxy mode requires -t or -T flag") + if !seen["t"] && !seen["T"] && !seen["Tu"] { + return fmt.Errorf("transparent proxy mode requires -t, -T or -Tu flag") } } if seen["auto"] { - if !seen["t"] && !seen["T"] { - return fmt.Errorf("-auto requires -t or -T flag") + if !seen["t"] && !seen["T"] && !seen["Tu"] { + return fmt.Errorf("-auto requires -t, -T or -Tu flag") } } if seen["mark"] { - if !seen["t"] && !seen["T"] { - return fmt.Errorf("-mark requires -t or -T flag") + if !seen["t"] && !seen["T"] && !seen["Tu"] { + return fmt.Errorf("-mark requires -t, -T or -Tu flag") } } if seen["f"] { diff --git a/gohpts.go b/gohpts.go index 9216305..2a1a594 100644 --- a/gohpts.go +++ b/gohpts.go @@ -13,10 +13,12 @@ import ( "fmt" "io" "log" + "maps" "math/rand" "net" "net/http" "os" + "os/exec" "os/signal" "runtime" "slices" @@ -65,6 +67,7 @@ type Config struct { ServerConfPath string TProxy string TProxyOnly string + TProxyUDP string TProxyMode string Auto bool Mark uint @@ -135,6 +138,7 @@ type proxyapp struct { httpServerAddr string iface *net.Interface tproxyAddr string + tproxyAddrUDP string tproxyMode string auto bool mark uint @@ -249,10 +253,11 @@ func New(conf *Config) *proxyapp { p.logger.Fatal().Msg("Cannot specify TPRoxy and TProxyOnly at the same time") } else if runtime.GOOS == "linux" && conf.TProxyMode != "" && !slices.Contains(SupportedTProxyModes, conf.TProxyMode) { p.logger.Fatal().Msg("Incorrect TProxyMode provided") - } else if runtime.GOOS != "linux" && (conf.TProxy != "" || conf.TProxyOnly != "" || conf.TProxyMode != "") { + } else if runtime.GOOS != "linux" && (conf.TProxy != "" || conf.TProxyOnly != "" || conf.TProxyMode != "" || conf.TProxyUDP != "") { conf.TProxy = "" conf.TProxyOnly = "" conf.TProxyMode = "" + conf.TProxyUDP = "" p.logger.Warn().Msgf("[%s] functionality only available on linux systems", conf.TProxyMode) } p.tproxyMode = conf.TProxyMode @@ -268,6 +273,15 @@ func New(conf *Config) *proxyapp { if err != nil { p.logger.Fatal().Err(err).Msg("") } + if conf.TProxyUDP != "" { + if p.tproxyMode != "tproxy" { + p.logger.Warn().Msgf("[%s] transparent UDP server only supports tproxy mode", conf.TProxyMode) + } + p.tproxyAddrUDP, err = getFullAddress(conf.TProxyUDP, "", true) + if err != nil { + p.logger.Fatal().Err(err).Msg("") + } + } } else { p.tproxyAddr, err = getFullAddress(tAddr, "", false) if err != nil { @@ -484,6 +498,9 @@ func New(conf *Config) *proxyapp { p.logger.Info().Msgf("REDIRECT: %s", p.tproxyAddr) } } + if p.tproxyAddrUDP != "" { + p.logger.Info().Msgf("TPROXY (UDP): %s", p.tproxyAddrUDP) + } return &p } @@ -496,11 +513,21 @@ func (p *proxyapp) Run() { go p.arpspoofer.Start() } var tproxyServer *tproxyServer - var output map[string]string + opts := make(map[string]string, 5) + if p.auto { + p.applyCommonRedirectRules(opts) + } if p.tproxyAddr != "" { tproxyServer = newTproxyServer(p) if p.auto { - output = tproxyServer.applyRedirectRules() + tproxyServer.applyRedirectRules(opts) + } + } + var tproxyServerUDP *tproxyServerUDP + if p.tproxyAddrUDP != "" { + tproxyServerUDP = newTproxyServerUDP(p) + if p.auto { + tproxyServerUDP.applyRedirectRules(opts) } } if p.proxylist != nil { @@ -525,15 +552,31 @@ func (p *proxyapp) Run() { } close(p.closeConn) if tproxyServer != nil { + p.logger.Info().Msgf("[tcp %s] Server is shutting down...", p.tproxyMode) if p.auto { - err := tproxyServer.clearRedirectRules(output) + err := tproxyServer.clearRedirectRules() if err != nil { p.logger.Error().Err(err).Msg("Failed clearing iptables rules") } } - p.logger.Info().Msgf("[%s] Server is shutting down...", p.tproxyMode) tproxyServer.Shutdown() } + if tproxyServerUDP != nil { + p.logger.Info().Msgf("[udp %s] Server is shutting down...", p.tproxyMode) + if p.auto { + err := tproxyServerUDP.clearRedirectRules() + if err != nil { + p.logger.Error().Err(err).Msg("Failed clearing iptables rules") + } + } + tproxyServerUDP.Shutdown() + } + if p.auto { + err := p.clearCommonRedirectRules(opts) + if err != nil { + p.logger.Error().Err(err).Msg("Failed clearing iptables rules") + } + } p.logger.Info().Msg("Server is shutting down...") ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) @@ -547,6 +590,9 @@ func (p *proxyapp) Run() { if tproxyServer != nil { go tproxyServer.ListenAndServe() } + if tproxyServerUDP != nil { + go tproxyServerUDP.ListenAndServe() + } if p.user != "" && p.pass != "" { p.httpServer.Handler = p.proxyAuth(p.handler()) } else { @@ -571,18 +617,43 @@ func (p *proxyapp) Run() { p.logger.Error().Err(err).Msg("Failed stopping arp spoofer") } } + close(p.closeConn) + if tproxyServer != nil { + p.logger.Info().Msgf("[tcp %s] Server is shutting down...", p.tproxyMode) + if p.auto { + err := tproxyServer.clearRedirectRules() + if err != nil { + p.logger.Error().Err(err).Msg("Failed clearing iptables rules") + } + } + tproxyServer.Shutdown() + } + if tproxyServerUDP != nil { + p.logger.Info().Msgf("[udp %s] Server is shutting down...", p.tproxyMode) + if p.auto { + err := tproxyServerUDP.clearRedirectRules() + if err != nil { + p.logger.Error().Err(err).Msg("Failed clearing iptables rules") + } + } + tproxyServerUDP.Shutdown() + } if p.auto { - err := tproxyServer.clearRedirectRules(output) + err := p.clearCommonRedirectRules(opts) if err != nil { p.logger.Error().Err(err).Msg("Failed clearing iptables rules") } } - close(p.closeConn) - p.logger.Info().Msgf("[%s] Server is shutting down...", p.tproxyMode) - tproxyServer.Shutdown() close(done) }() - tproxyServer.ListenAndServe() + if tproxyServer != nil && tproxyServerUDP != nil { + go tproxyServerUDP.ListenAndServe() + tproxyServer.ListenAndServe() + } else if tproxyServer != nil { + tproxyServer.ListenAndServe() + } else { + tproxyServerUDP.ListenAndServe() + } } <-done } @@ -749,7 +820,9 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) { var dstConn net.Conn var err error if network.IsLocalAddress(r.Host) { - dstConn, err = getBaseDialer(timeout, p.mark).Dial("tcp", r.Host) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + dstConn, err = getBaseDialer(timeout, p.mark).DialContext(ctx, "tcp", r.Host) if err != nil { p.logger.Error().Err(err).Msgf("Failed connecting to %s", r.Host) http.Error(w, err.Error(), http.StatusServiceUnavailable) @@ -1317,3 +1390,127 @@ func (p *proxyapp) proxyAuth(next http.HandlerFunc) http.HandlerFunc { http.Error(w, "Proxy Authentication Required", http.StatusProxyAuthRequired) }) } + +func (p *proxyapp) applyCommonRedirectRules(opts map[string]string) { + var setex string + if p.debug { + setex = "set -ex" + } + if p.tproxyMode == "tproxy" { + cmdClear := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t mangle -F DIVERT 2>/dev/null || true + iptables -t mangle -X DIVERT 2>/dev/null || true + + ip rule del fwmark 1 lookup 100 2>/dev/null || true + ip route flush table 100 2>/dev/null || true + `, setex)) + cmdClear.Stdout = os.Stdout + cmdClear.Stderr = os.Stderr + if err := cmdClear.Run(); err != nil { + p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + } + cmdInit0 := exec.Command("bash", "-c", fmt.Sprintf(` + %s + ip rule add fwmark 1 lookup 100 2>/dev/null || true + ip route add local 0.0.0.0/0 dev lo table 100 2>/dev/null || true + + iptables -t mangle -N DIVERT 2>/dev/null || true + iptables -t mangle -F DIVERT 2>/dev/null || true + iptables -t mangle -A DIVERT -j MARK --set-mark 1 + iptables -t mangle -A DIVERT -j ACCEPT + `, setex)) + cmdInit0.Stdout = os.Stdout + cmdInit0.Stderr = os.Stderr + if err := cmdInit0.Run(); err != nil { + p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + } + } + + _ = createSysctlOptCmd("net.ipv4.ip_forward", "1", setex, opts, p.debug).Run() + cmdClearForward := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t filter -F GOHPTS 2>/dev/null || true + iptables -t filter -D FORWARD -j GOHPTS 2>/dev/null || true + iptables -t filter -X GOHPTS 2>/dev/null || true + `, setex)) + cmdClearForward.Stdout = os.Stdout + cmdClearForward.Stderr = os.Stderr + if err := cmdClearForward.Run(); err != nil { + p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + } + var iface *net.Interface + var err error + if p.iface != nil { + iface = p.iface + } else { + iface, err = network.GetDefaultInterface() + if err != nil { + p.logger.Fatal().Err(err).Msg("failed getting default network interface") + } + } + cmdForwardFilter := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t filter -N GOHPTS 2>/dev/null + iptables -t filter -F GOHPTS + iptables -t filter -A FORWARD -j GOHPTS + iptables -t filter -A GOHPTS -i %s -j ACCEPT + iptables -t filter -A GOHPTS -o %s -j ACCEPT + `, setex, iface.Name, iface.Name)) + cmdForwardFilter.Stdout = os.Stdout + cmdForwardFilter.Stderr = os.Stderr + if err := cmdForwardFilter.Run(); err != nil { + p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + } +} + +func (p *proxyapp) clearCommonRedirectRules(opts map[string]string) error { + var setex string + if p.debug { + setex = "set -ex" + } + cmdClear := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t filter -F GOHPTS 2>/dev/null || true + iptables -t filter -D FORWARD -j GOHPTS 2>/dev/null || true + iptables -t filter -X GOHPTS 2>/dev/null || true + `, setex)) + cmdClear.Stdout = os.Stdout + cmdClear.Stderr = os.Stderr + if err := cmdClear.Run(); err != nil { + p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + } + cmds := make([]string, 0, len(opts)) + for _, cmd := range slices.Sorted(maps.Keys(opts)) { + cmds = append(cmds, fmt.Sprintf("sysctl -w %s=%s", cmd, opts[cmd])) + } + cmdRestoreOpts := exec.Command("bash", "-c", fmt.Sprintf(` + %s + %s + `, setex, strings.Join(cmds, "\n"))) + cmdRestoreOpts.Stdout = os.Stdout + cmdRestoreOpts.Stderr = os.Stderr + if !p.debug { + cmdRestoreOpts.Stdout = nil + } + _ = cmdRestoreOpts.Run() + if p.tproxyMode == "tproxy" { + cmd := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t mangle -F DIVERT 2>/dev/null || true + iptables -t mangle -X DIVERT 2>/dev/null || true + + ip rule del fwmark 1 lookup 100 2>/dev/null || true + ip route flush table 100 2>/dev/null || true + `, setex)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if !p.debug { + cmd.Stdout = nil + } + if err := cmd.Run(); err != nil { + return err + } + } + return nil +} diff --git a/helpers.go b/helpers.go index 9aadcd5..8add753 100644 --- a/helpers.go +++ b/helpers.go @@ -9,6 +9,7 @@ import ( "net/http" "net/netip" "os" + "os/exec" "strconv" "strings" @@ -185,3 +186,21 @@ func newSOCKS5Dialer(address string, auth *Auth, forward ContextDialer) (*socks5 } return d, nil } + +func createSysctlOptCmd(opt, value, setex string, opts map[string]string, debug bool) *exec.Cmd { + cmdCat := exec.Command("bash", "-c", fmt.Sprintf(` + cat /proc/sys/%s + `, strings.ReplaceAll(opt, ".", "/"))) + output, _ := cmdCat.CombinedOutput() + opts[opt] = string(output) + cmd := exec.Command("bash", "-c", fmt.Sprintf(` + %s + sysctl -w %s=%s + `, setex, opt, value)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if debug { + cmd.Stdout = nil + } + return cmd +} diff --git a/tproxy_linux.go b/tproxy_linux.go index 8f0a025..dbed485 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -7,13 +7,10 @@ import ( "context" "errors" "fmt" - "maps" "net" "net/netip" "os" "os/exec" - "slices" - "strings" "sync" "syscall" "time" @@ -118,11 +115,11 @@ func (ts *tproxyServer) getOriginalDst(rawConn syscall.RawConn) (string, error) optlen := uint32(unsafe.Sizeof(originalDst)) err := getsockopt(int(fd), unix.SOL_IP, unix.SO_ORIGINAL_DST, unsafe.Pointer(&originalDst), &optlen) if err != nil { - ts.p.logger.Error().Err(err).Msgf("[%s] getsockopt SO_ORIGINAL_DST failed", ts.p.tproxyMode) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] getsockopt SO_ORIGINAL_DST failed", ts.p.tproxyMode) } }) if err != nil { - ts.p.logger.Error().Err(err).Msgf("[%s] Failed invoking control connection", ts.p.tproxyMode) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed invoking control connection", ts.p.tproxyMode) return "", err } dstHost := netip.AddrFrom4(originalDst.Addr) @@ -141,38 +138,40 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { case "redirect": rawConn, err := srcConn.(*net.TCPConn).SyscallConn() if err != nil { - ts.p.logger.Error().Err(err).Msgf("[%s] Failed to get raw connection", ts.p.tproxyMode) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed to get raw connection", ts.p.tproxyMode) return } dst, err = ts.getOriginalDst(rawConn) if err != nil { - ts.p.logger.Error().Err(err).Msgf("[%s] Failed to get destination address", ts.p.tproxyMode) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed to get destination address", ts.p.tproxyMode) return } - ts.p.logger.Debug().Msgf("[%s] getsockopt SO_ORIGINAL_DST %s", ts.p.tproxyMode, dst) + ts.p.logger.Debug().Msgf("[tcp %s] getsockopt SO_ORIGINAL_DST %s", ts.p.tproxyMode, dst) case "tproxy": dst = srcConn.LocalAddr().String() - ts.p.logger.Debug().Msgf("[%s] IP_TRANSPARENT %s", ts.p.tproxyMode, dst) + ts.p.logger.Debug().Msgf("[tcp %s] IP_TRANSPARENT %s", ts.p.tproxyMode, dst) default: ts.p.logger.Fatal().Msg("Unknown tproxyMode") } if network.IsLocalAddress(dst) { - dstConn, err = getBaseDialer(timeout, ts.p.mark).Dial("tcp", dst) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + dstConn, err = getBaseDialer(timeout, ts.p.mark).DialContext(ctx, "tcp", dst) if err != nil { - ts.p.logger.Error().Err(err).Msgf("[%s] Failed connecting to %s", ts.p.tproxyMode, dst) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed connecting to %s", ts.p.tproxyMode, dst) return } } else { sockDialer, _, err := ts.p.getSocks() if err != nil { - ts.p.logger.Error().Err(err).Msgf("[%s] Failed getting SOCKS5 client", ts.p.tproxyMode) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed getting SOCKS5 client", ts.p.tproxyMode) return } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() dstConn, err = sockDialer.DialContext(ctx, "tcp", dst) if err != nil { - ts.p.logger.Error().Err(err).Msgf("[%s] Failed connecting to %s", ts.p.tproxyMode, dst) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed connecting to %s", ts.p.tproxyMode, dst) return } } @@ -181,7 +180,7 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { dstConnStr := fmt.Sprintf("%s→ %s→ %s", dstConn.LocalAddr().String(), dstConn.RemoteAddr().String(), dst) srcConnStr := fmt.Sprintf("%s→ %s", srcConn.RemoteAddr().String(), srcConn.LocalAddr().String()) - ts.p.logger.Debug().Msgf("[%s] src: %s - dst: %s", ts.p.tproxyMode, srcConnStr, dstConnStr) + ts.p.logger.Debug().Msgf("[tcp %s] src: %s - dst: %s", ts.p.tproxyMode, srcConnStr, dstConnStr) reqChan := make(chan layers.Layer) respChan := make(chan layers.Layer) @@ -231,42 +230,20 @@ func (ts *tproxyServer) Shutdown() { select { case <-done: - ts.p.logger.Info().Msgf("[%s] Server gracefully shutdown", ts.p.tproxyMode) + ts.p.logger.Info().Msgf("[tcp %s] Server gracefully shutdown", ts.p.tproxyMode) return case <-time.After(shutdownTimeout): - ts.p.logger.Error().Msgf("[%s] Server timed out waiting for connections to finish", ts.p.tproxyMode) + ts.p.logger.Error().Msgf("[tcp %s] Server timed out waiting for connections to finish", ts.p.tproxyMode) return } } -func (ts *tproxyServer) createSysctlOptCmd(opt, value, setex string, opts map[string]string) *exec.Cmd { - cmdCat := exec.Command("bash", "-c", fmt.Sprintf(` - cat /proc/sys/%s - `, strings.ReplaceAll(opt, ".", "/"))) - output, err := cmdCat.CombinedOutput() - if err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") - } - opts[opt] = string(output) - cmd := exec.Command("bash", "-c", fmt.Sprintf(` - %s - sysctl -w %s=%s - `, setex, opt, value)) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if !ts.p.debug { - cmd.Stdout = nil - } - return cmd -} - -func (ts *tproxyServer) applyRedirectRules() map[string]string { +func (ts *tproxyServer) applyRedirectRules(opts map[string]string) { _, tproxyPort, _ := net.SplitHostPort(ts.p.tproxyAddr) var setex string if ts.p.debug { setex = "set -ex" } - ipv4Settings := make(map[string]string, 5) switch ts.p.tproxyMode { case "redirect": cmdClear := exec.Command("bash", "-c", fmt.Sprintf(` @@ -279,20 +256,20 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmdClear.Stdout = os.Stdout cmdClear.Stderr = os.Stderr if err := cmdClear.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } cmdInit := exec.Command("bash", "-c", fmt.Sprintf(` %s iptables -t nat -N GOHPTS 2>/dev/null iptables -t nat -F GOHPTS - iptables -t nat -A GOHPTS -d 127.0.0.0/8 -j RETURN + iptables -t nat -A GOHPTS -p tcp -d 127.0.0.0/8 -j RETURN iptables -t nat -A GOHPTS -p tcp --dport 22 -j RETURN `, setex)) cmdInit.Stdout = os.Stdout cmdInit.Stderr = os.Stderr if err := cmdInit.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } if ts.p.httpServerAddr != "" { _, httpPort, _ := net.SplitHostPort(ts.p.httpServerAddr) @@ -303,7 +280,7 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmdHTTP.Stdout = os.Stdout cmdHTTP.Stderr = os.Stderr if err := cmdHTTP.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } } if ts.p.mark > 0 { @@ -314,7 +291,7 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmdMark.Stdout = os.Stdout cmdMark.Stderr = os.Stderr if err := cmdMark.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } } else { cmd0 := exec.Command("bash", "-c", fmt.Sprintf(` @@ -324,7 +301,7 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmd0.Stdout = os.Stdout cmd0.Stderr = os.Stderr if err := cmd0.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } if len(ts.p.proxylist) > 0 { for _, pr := range ts.p.proxylist { @@ -336,7 +313,7 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmd1.Stdout = os.Stdout cmd1.Stderr = os.Stderr if err := cmd1.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } if ts.p.proxychain.Type == "strict" { break @@ -349,7 +326,7 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { if command -v docker >/dev/null 2>&1 then for subnet in $(docker network inspect $(docker network ls -q) --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'); do - iptables -t nat -A GOHPTS -d "$subnet" -j RETURN + iptables -t nat -A GOHPTS -p tcp -d "$subnet" -j RETURN done fi @@ -364,59 +341,47 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmdDocker.Stdout = os.Stdout cmdDocker.Stderr = os.Stderr if err := cmdDocker.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } case "tproxy": cmdClear := exec.Command("bash", "-c", fmt.Sprintf(` %s iptables -t mangle -D PREROUTING -p tcp -m socket -j DIVERT 2>/dev/null || true iptables -t mangle -D PREROUTING -p tcp -j GOHPTS 2>/dev/null || true - iptables -t mangle -F DIVERT 2>/dev/null || true iptables -t mangle -F GOHPTS 2>/dev/null || true - iptables -t mangle -X DIVERT 2>/dev/null || true iptables -t mangle -X GOHPTS 2>/dev/null || true - - ip rule del fwmark 1 lookup 100 2>/dev/null || true - ip route flush table 100 2>/dev/null || true `, setex)) cmdClear.Stdout = os.Stdout cmdClear.Stderr = os.Stderr if err := cmdClear.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } cmdInit0 := exec.Command("bash", "-c", fmt.Sprintf(` %s - ip rule add fwmark 1 lookup 100 2>/dev/null || true - ip route add local 0.0.0.0/0 dev lo table 100 2>/dev/null || true - - iptables -t mangle -N DIVERT 2>/dev/null || true - iptables -t mangle -F DIVERT - iptables -t mangle -A DIVERT -j MARK --set-mark 1 - iptables -t mangle -A DIVERT -j ACCEPT - iptables -t mangle -N GOHPTS 2>/dev/null || true iptables -t mangle -F GOHPTS - iptables -t mangle -A GOHPTS -d 127.0.0.0/8 -j RETURN - iptables -t mangle -A GOHPTS -d 224.0.0.0/4 -j RETURN - iptables -t mangle -A GOHPTS -d 255.255.255.255/32 -j RETURN + + iptables -t mangle -A GOHPTS -p tcp -d 127.0.0.0/8 -j RETURN + iptables -t mangle -A GOHPTS -p tcp -d 224.0.0.0/4 -j RETURN + iptables -t mangle -A GOHPTS -p tcp -d 255.255.255.255/32 -j RETURN `, setex)) cmdInit0.Stdout = os.Stdout cmdInit0.Stderr = os.Stderr if err := cmdInit0.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } cmdDocker := exec.Command("bash", "-c", fmt.Sprintf(` %s if command -v docker >/dev/null 2>&1 then for subnet in $(docker network inspect $(docker network ls -q) --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'); do - iptables -t mangle -A GOHPTS -d "$subnet" -j RETURN + iptables -t mangle -A GOHPTS -p tcp -d "$subnet" -j RETURN done fi`, setex)) cmdDocker.Stdout = os.Stdout cmdDocker.Stderr = os.Stderr if err := cmdDocker.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } cmdInit := exec.Command("bash", "-c", fmt.Sprintf(` %s @@ -429,12 +394,11 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmdInit.Stdout = os.Stdout cmdInit.Stderr = os.Stderr if err := cmdInit.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + ts.p.logger.Fatal().Err(err).Msgf("[tcp %s] Failed while configuring iptables. Are you root?", ts.p.tproxyMode) } default: ts.p.logger.Fatal().Msgf("Unreachable, unknown mode: %s", ts.p.tproxyMode) } - _ = ts.createSysctlOptCmd("net.ipv4.ip_forward", "1", setex, ipv4Settings).Run() cmdCheckBBR := exec.Command("bash", "-c", fmt.Sprintf(` %s lsmod | grep -q '^tcp_bbr' || modprobe tcp_bbr @@ -445,77 +409,17 @@ func (ts *tproxyServer) applyRedirectRules() map[string]string { cmdCheckBBR.Stdout = nil } _ = cmdCheckBBR.Run() - _ = ts.createSysctlOptCmd("net.ipv4.tcp_congestion_control", "bbr", setex, ipv4Settings).Run() - _ = ts.createSysctlOptCmd("net.core.default_qdisc", "fq", setex, ipv4Settings).Run() - _ = ts.createSysctlOptCmd("net.ipv4.tcp_tw_reuse", "1", setex, ipv4Settings).Run() - _ = ts.createSysctlOptCmd("net.ipv4.tcp_fin_timeout", "15", setex, ipv4Settings).Run() - cmdClearForward := exec.Command("bash", "-c", fmt.Sprintf(` - %s - iptables -t filter -F GOHPTS 2>/dev/null || true - iptables -t filter -D FORWARD -j GOHPTS 2>/dev/null || true - iptables -t filter -X GOHPTS 2>/dev/null || true - `, setex)) - cmdClearForward.Stdout = os.Stdout - cmdClearForward.Stderr = os.Stderr - if err := cmdClearForward.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") - } - var iface *net.Interface - var err error - if ts.p.iface != nil { - iface = ts.p.iface - } else { - iface, err = network.GetDefaultInterface() - if err != nil { - ts.p.logger.Fatal().Err(err).Msg("failed getting default network interface") - } - } - cmdForwardFilter := exec.Command("bash", "-c", fmt.Sprintf(` - %s - iptables -t filter -N GOHPTS 2>/dev/null - iptables -t filter -F GOHPTS - iptables -t filter -A FORWARD -j GOHPTS - iptables -t filter -A GOHPTS -i %s -j ACCEPT - iptables -t filter -A GOHPTS -o %s -j ACCEPT - `, setex, iface.Name, iface.Name)) - cmdForwardFilter.Stdout = os.Stdout - cmdForwardFilter.Stderr = os.Stderr - if err := cmdForwardFilter.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") - } - return ipv4Settings + _ = createSysctlOptCmd("net.ipv4.tcp_congestion_control", "bbr", setex, opts, ts.p.debug).Run() + _ = createSysctlOptCmd("net.core.default_qdisc", "fq", setex, opts, ts.p.debug).Run() + _ = createSysctlOptCmd("net.ipv4.tcp_tw_reuse", "1", setex, opts, ts.p.debug).Run() + _ = createSysctlOptCmd("net.ipv4.tcp_fin_timeout", "15", setex, opts, ts.p.debug).Run() } -func (ts *tproxyServer) clearRedirectRules(opts map[string]string) error { +func (ts *tproxyServer) clearRedirectRules() error { var setex string if ts.p.debug { setex = "set -ex" } - cmdClear := exec.Command("bash", "-c", fmt.Sprintf(` - %s - iptables -t filter -F GOHPTS 2>/dev/null || true - iptables -t filter -D FORWARD -j GOHPTS 2>/dev/null || true - iptables -t filter -X GOHPTS 2>/dev/null || true - `, setex)) - cmdClear.Stdout = os.Stdout - cmdClear.Stderr = os.Stderr - if err := cmdClear.Run(); err != nil { - ts.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") - } - cmds := make([]string, 0, len(opts)) - for _, cmd := range slices.Sorted(maps.Keys(opts)) { - cmds = append(cmds, fmt.Sprintf("sysctl -w %s=%s", cmd, opts[cmd])) - } - cmdRestoreOpts := exec.Command("bash", "-c", fmt.Sprintf(` - %s - %s - `, setex, strings.Join(cmds, "\n"))) - cmdRestoreOpts.Stdout = os.Stdout - cmdRestoreOpts.Stderr = os.Stderr - if !ts.p.debug { - cmdRestoreOpts.Stdout = nil - } - _ = cmdRestoreOpts.Run() var cmd *exec.Cmd switch ts.p.tproxyMode { case "redirect": @@ -533,13 +437,8 @@ func (ts *tproxyServer) clearRedirectRules(opts map[string]string) error { %s iptables -t mangle -D PREROUTING -p tcp -m socket -j DIVERT 2>/dev/null || true iptables -t mangle -D PREROUTING -p tcp -j GOHPTS 2>/dev/null || true - iptables -t mangle -F DIVERT 2>/dev/null || true iptables -t mangle -F GOHPTS 2>/dev/null || true - iptables -t mangle -X DIVERT 2>/dev/null || true iptables -t mangle -X GOHPTS 2>/dev/null || true - - ip rule del fwmark 1 lookup 100 2>/dev/null || true - ip route flush table 100 2>/dev/null || true `, setex)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go new file mode 100644 index 0000000..d9cee1c --- /dev/null +++ b/tproxy_udp_linux.go @@ -0,0 +1,534 @@ +//go:build linux +// +build linux + +package gohpts + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/netip" + "os" + "os/exec" + "sync" + "syscall" + "time" + "unsafe" + + "github.com/shadowy-pycoder/mshark/network" + "github.com/wzshiming/socks5" + "golang.org/x/sys/unix" +) + +var ( + googleDNSAddr *net.UDPAddr = &net.UDPAddr{IP: net.ParseIP("8.8.8.8"), Port: 53} + idleUDPConnectionsTimeout time.Duration = 60 * time.Second +) + +type udpConn struct { + *socks5.UDPConn + clientAddr *net.UDPAddr + dstAddr *net.UDPAddr + lastSeen time.Time +} + +func (uc *udpConn) ClientAddr() *net.UDPAddr { + return uc.clientAddr +} + +func (uc *udpConn) DstAddr() *net.UDPAddr { + return uc.dstAddr +} + +func newUDPConn(clientAddr *net.UDPAddr, dstAddr *net.UDPAddr, sockDialer *socks5.Dialer) (*udpConn, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + conn, err := sockDialer.DialContext(ctx, "udp", dstAddr.String()) + if err != nil { + return nil, err + } + relayConn, ok := conn.(*socks5.UDPConn) + if !ok { + return nil, fmt.Errorf("failed obtaining relay connection") + } + return &udpConn{UDPConn: relayConn, clientAddr: clientAddr, dstAddr: dstAddr, lastSeen: time.Now()}, nil +} + +type udpConnections struct { + wg sync.WaitGroup + quit chan struct{} + sync.RWMutex + clients map[string]*udpConn +} + +func (ucs *udpConnections) Add(conn *udpConn) { + ucs.Lock() + ucs.clients[fmt.Sprintf("%s,%s", conn.ClientAddr(), conn.DstAddr())] = conn + ucs.Unlock() +} + +func (ucs *udpConnections) Get(clientAddr, dstAddr *net.UDPAddr) (*udpConn, bool) { + ucs.RLock() + defer ucs.RUnlock() + conn, ok := ucs.clients[fmt.Sprintf("%s,%s", clientAddr, dstAddr)] + return conn, ok +} + +func (ucs *udpConnections) Remove(conn *udpConn) { + ucs.Lock() + delete(ucs.clients, fmt.Sprintf("%s,%s", conn.ClientAddr(), conn.DstAddr())) + ucs.Unlock() +} + +func (ucs *udpConnections) UpdateLastSeen(conn *udpConn) { + ucs.Lock() + conn.lastSeen = time.Now() + ucs.Unlock() +} + +func (ucs *udpConnections) RemoveByAddr(addr string) { + ucs.Lock() + delete(ucs.clients, addr) + ucs.Unlock() +} + +func (ucs *udpConnections) Cleanup() { + ucs.wg.Add(1) + t := time.NewTicker(idleUDPConnectionsTimeout) + for { + select { + case <-ucs.quit: + ucs.Lock() + for _, conn := range ucs.clients { + conn.Close() + } + ucs.Unlock() + ucs.wg.Done() + return + case <-t.C: + ucs.Lock() + for k, conn := range ucs.clients { + if time.Since(conn.lastSeen) > idleUDPConnectionsTimeout { + conn.Close() + ucs.RemoveByAddr(k) + } + } + ucs.Unlock() + } + } +} + +type tproxyServerUDP struct { + conn *net.UDPConn + quit chan struct{} + wg sync.WaitGroup + p *proxyapp + clients *udpConnections + iface *net.Interface + gwConn *net.UDPConn + gwDNS *net.UDPAddr +} + +func newTproxyServerUDP(p *proxyapp) *tproxyServerUDP { + tsu := &tproxyServerUDP{ + quit: make(chan struct{}), + p: p, + } + lc := net.ListenConfig{ + Control: func(network, address string, conn syscall.RawConn) error { + var operr error + if err := conn.Control(func(fd uintptr) { + operr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1) + operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_RECVORIGDSTADDR, 1) + }); err != nil { + return err + } + return operr + }, + } + pconn, err := lc.ListenPacket(context.Background(), "udp4", tsu.p.tproxyAddrUDP) + if err != nil { + var msg string + if errors.Is(err, unix.EPERM) { + msg = "try `sudo setcap 'cap_net_admin+ep` for the binary or run with sudo:" + } + tsu.p.logger.Fatal().Err(err).Msg(msg) + } + tsu.conn = pconn.(*net.UDPConn) + tsu.clients = &udpConnections{quit: tsu.quit, clients: make(map[string]*udpConn)} + if tsu.p.iface != nil { + tsu.iface = tsu.p.iface + } else { + tsu.iface, err = network.GetDefaultInterface() + if err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] Failed getting default interface", tsu.p.tproxyMode) + } + } + gw, err := network.GetGatewayIPv4FromInterface(tsu.iface.Name) + if err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] failed getting gateway from %s", tsu.p.tproxyMode, tsu.iface.Name) + } + tsu.gwDNS = &net.UDPAddr{IP: net.ParseIP(gw.String()), Port: 53} + lc = net.ListenConfig{ + Control: func(network, address string, conn syscall.RawConn) error { + var operr error + if err := conn.Control(func(fd uintptr) { + operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1) + operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_FREEBIND, 1) + operr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + }); err != nil { + return err + } + return operr + }, + } + pconn, err = lc.ListenPacket(context.Background(), "udp4", tsu.gwDNS.String()) + if err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] failed listening on gateway DNS", tsu.p.tproxyMode) + } + tsu.gwConn = pconn.(*net.UDPConn) + return tsu +} + +func (tsu *tproxyServerUDP) handleDNSConnections() { + tsu.wg.Add(1) + defer tsu.wg.Done() + buf := make([]byte, 4096) + for { + select { + case <-tsu.quit: + return + default: + n, srcAddr, err := tsu.gwConn.ReadFromUDP(buf) + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed reading UDP message", tsu.p.tproxyMode) + continue + } + tsu.p.logger.Debug().Msgf("[udp %s] Got connection from %s", tsu.p.tproxyMode, srcAddr) + conn, err := net.DialUDP("udp", nil, googleDNSAddr) + if err != nil { + tsu.p.logger.Error(). + Err(err). + Msgf("[udp %s] Failed creating connection from %s to %s", tsu.p.tproxyMode, srcAddr, googleDNSAddr) + continue + } + _, err = conn.Write(buf[:n]) + if err != nil { + tsu.p.logger.Error(). + Err(err). + Msgf("[udp %s] Failed writing message from %s to %s", tsu.p.tproxyMode, srcAddr, googleDNSAddr) + continue + } + go tsu.handleDNSConnection(conn, srcAddr) + } + } +} + +func (tsu *tproxyServerUDP) ListenAndServe() { + tsu.wg.Add(1) + defer tsu.wg.Done() + go tsu.clients.Cleanup() + go tsu.handleDNSConnections() + buf := make([]byte, 4096) + oob := make([]byte, 1500) + for { + select { + case <-tsu.quit: + return + default: + n, oobn, _, srcAddr, err := tsu.conn.ReadMsgUDP(buf, oob) + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed reading UDP message", tsu.p.tproxyMode) + continue + } + tsu.p.logger.Debug().Msgf("[udp %s] Got connection from %s", tsu.p.tproxyMode, srcAddr) + dstAddr, err := tsu.getOriginalDst(oob[:oobn]) + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed getting original destination", tsu.p.tproxyMode) + continue + } + tsu.p.logger.Debug().Msgf("[udp %s] IP_TRANSPARENT %s", tsu.p.tproxyMode, dstAddr) + conn, found := tsu.clients.Get(srcAddr, dstAddr) + if !found { + sockDialer, _, err := tsu.p.getSocks() + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed getting SOCKS5 client", tsu.p.tproxyMode) + continue + } + conn, err = newUDPConn(srcAddr, dstAddr, sockDialer) + if err != nil { + tsu.p.logger.Error(). + Err(err). + Msgf("[udp %s] Failed creating UDP connection for %s", tsu.p.tproxyMode, srcAddr) + continue + } + tsu.clients.Add(conn) + go tsu.handleConnection(conn) + } else { + tsu.p.logger.Debug().Msgf("[udp %s] Found connection for %s", tsu.p.tproxyMode, srcAddr) + } + _, err = conn.WriteToUDP(buf[:n], dstAddr) + if err != nil { + tsu.p.logger.Error(). + Err(err). + Msgf("[udp %s] failed sending message from %s to %s", tsu.p.tproxyMode, srcAddr, dstAddr) + continue + } + tsu.clients.UpdateLastSeen(conn) + } + } +} + +func (tsu *tproxyServerUDP) handleConnection(conn *udpConn) { + tsu.wg.Add(1) + defer tsu.wg.Done() + buf := make([]byte, 4096) + var written int64 +readLoop: + for { + select { + case <-tsu.quit: + return + default: + er := conn.SetReadDeadline(time.Now().Add(readTimeout)) + if er != nil { + if errors.Is(er, net.ErrClosed) { + return + } + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] failed setting read deadline %s→ %s", tsu.p.tproxyMode, conn.dstAddr, conn.clientAddr) + break readLoop + } + nr, er := conn.Read(buf) + if nr > 0 { + er := tsu.conn.SetWriteDeadline(time.Now().Add(writeTimeout)) + if er != nil { + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] failed setting write deadline %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) + break readLoop + } + nw, ew := tsu.conn.WriteToUDP(buf[0:nr], conn.clientAddr) + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + ew = errInvalidWrite + } + } + written += int64(nw) + if ew != nil { + if errors.Is(ew, net.ErrClosed) { + return + } + if ne, ok := ew.(net.Error); ok && ne.Timeout() { + break readLoop + } + } + if nr != nw { + tsu.p.logger.Debug().Err(io.ErrShortWrite).Msgf("[udp %s] failed sending message %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) + break readLoop + } + } + if er != nil { + if ne, ok := er.(net.Error); ok && ne.Timeout() { + break readLoop + } + if errors.Is(er, net.ErrClosed) { + return + } + if er == io.EOF { + break readLoop + } + break readLoop + } + } + } + conn.Close() + tsu.clients.Remove(conn) +} + +func (tsu *tproxyServerUDP) handleDNSConnection(conn *net.UDPConn, srcAddr *net.UDPAddr) { + tsu.wg.Add(1) + defer tsu.wg.Done() + defer conn.Close() + buf := make([]byte, 4096) + var written int64 + er := conn.SetReadDeadline(time.Now().Add(readTimeout)) + if er != nil { + tsu.p.logger.Debug(). + Err(er). + Msgf("[udp %s] failed setting read deadline %s→ %s", tsu.p.tproxyMode, googleDNSAddr, conn.LocalAddr()) + return + } + nr, er := conn.Read(buf) + if nr > 0 { + er := tsu.conn.SetWriteDeadline(time.Now().Add(writeTimeout)) + if er != nil { + tsu.p.logger.Debug(). + Err(er). + Msgf("[udp %s] failed setting write deadline %s→ %s", tsu.p.tproxyMode, googleDNSAddr, srcAddr) + return + } + nw, ew := tsu.gwConn.WriteToUDP(buf[0:nr], srcAddr) + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + ew = errInvalidWrite + } + } + written += int64(nw) + if ew != nil { + return + } + if nr != nw { + tsu.p.logger.Debug(). + Err(io.ErrShortWrite). + Msgf("[udp %s] failed sending message %s→ %s", tsu.p.tproxyMode, googleDNSAddr, conn.LocalAddr()) + return + } + } + if er != nil { + return + } +} + +func (tsu *tproxyServerUDP) Shutdown() { + close(tsu.quit) + done := make(chan struct{}) + go func() { + tsu.wg.Wait() + close(done) + }() + + select { + case <-done: + tsu.p.logger.Info().Msgf("[udp %s] Server gracefully shutdown", tsu.p.tproxyMode) + return + case <-time.After(shutdownTimeout): + tsu.p.logger.Error().Msgf("[udp %s] Server timed out waiting for connections to finish", tsu.p.tproxyMode) + return + } +} + +func (tsu *tproxyServerUDP) getOriginalDst(oob []byte) (*net.UDPAddr, error) { + cmsgs, err := unix.ParseSocketControlMessage(oob) + if err != nil { + return nil, err + } + for _, cmsg := range cmsgs { + if cmsg.Header.Level == unix.SOL_IP && cmsg.Header.Type == unix.IP_RECVORIGDSTADDR { + originalDst := &syscall.RawSockaddrInet4{} + copy((*[unsafe.Sizeof(*originalDst)]byte)(unsafe.Pointer(originalDst))[:], cmsg.Data) + dstHost := netip.AddrFrom4(originalDst.Addr) + dstPort := uint16(originalDst.Port<<8) | originalDst.Port>>8 + dstAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", dstHost, dstPort)) + if err != nil { + return nil, err + } + return dstAddr, nil + } + } + return nil, fmt.Errorf("original destination not found") +} + +func (tsu *tproxyServerUDP) applyRedirectRules(opts map[string]string) { + _, tproxyPortUDP, _ := net.SplitHostPort(tsu.p.tproxyAddrUDP) + var setex string + if tsu.p.debug { + setex = "set -ex" + } + switch tsu.p.tproxyMode { + case "redirect": + tsu.p.logger.Fatal().Msgf("Unsupported mode: %s", tsu.p.tproxyMode) + case "tproxy": + cmdClear := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t mangle -D PREROUTING -p udp -m socket -j DIVERT 2>/dev/null || true + iptables -t mangle -D PREROUTING -p udp -j GOHPTS_UDP 2>/dev/null || true + iptables -t mangle -F GOHPTS_UDP 2>/dev/null || true + iptables -t mangle -X GOHPTS_UDP 2>/dev/null || true + iptables -t nat -D PREROUTING -p udp -j GOHPTS_UDP 2>/dev/null || true + iptables -t nat -F GOHPTS_UDP 2>/dev/null || true + iptables -t nat -X GOHPTS_UDP 2>/dev/null || true + `, setex)) + cmdClear.Stdout = os.Stdout + cmdClear.Stderr = os.Stderr + if err := cmdClear.Run(); err != nil { + tsu.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + } + prefix, err := network.GetIPv4PrefixFromInterface(tsu.iface) + if err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("failed getting host from %s", tsu.iface.Name) + } + cmdInit0 := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t mangle -N GOHPTS_UDP 2>/dev/null || true + iptables -t mangle -F GOHPTS_UDP + + iptables -t mangle -A GOHPTS_UDP -p udp -d 127.0.0.0/8 -j RETURN + iptables -t mangle -A GOHPTS_UDP -p udp -d 224.0.0.0/4 -j RETURN + iptables -t mangle -A GOHPTS_UDP -p udp -d 255.255.255.255/32 -j RETURN + iptables -t mangle -A GOHPTS_UDP -p udp -d %s -j RETURN + `, setex, prefix.Masked())) + cmdInit0.Stdout = os.Stdout + cmdInit0.Stderr = os.Stderr + if err := cmdInit0.Run(); err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] Failed while configuring iptables. Are you root?", tsu.p.tproxyMode) + } + cmdDocker := exec.Command("bash", "-c", fmt.Sprintf(` + %s + if command -v docker >/dev/null 2>&1 + then + for subnet in $(docker network inspect $(docker network ls -q) --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'); do + iptables -t mangle -A GOHPTS_UDP -p udp -d "$subnet" -j RETURN + done + fi`, setex)) + cmdDocker.Stdout = os.Stdout + cmdDocker.Stderr = os.Stderr + if err := cmdDocker.Run(); err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] Failed while configuring iptables. Are you root?", tsu.p.tproxyMode) + } + + cmdInit := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t mangle -A GOHPTS_UDP -p udp -m mark --mark %d -j RETURN + iptables -t mangle -A GOHPTS_UDP -s %s -p udp -j TPROXY --on-port %s --tproxy-mark 1 + + iptables -t mangle -A PREROUTING -p udp -m socket -j DIVERT + iptables -t mangle -A PREROUTING -p udp -j GOHPTS_UDP + `, setex, tsu.p.mark, prefix.Masked(), tproxyPortUDP)) + cmdInit.Stdout = os.Stdout + cmdInit.Stderr = os.Stderr + if err := cmdInit.Run(); err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] Failed while configuring iptables. Are you root?", tsu.p.tproxyMode) + } + _ = createSysctlOptCmd("net.ipv4.ip_nonlocal_bind", "1", setex, opts, tsu.p.debug).Run() + default: + tsu.p.logger.Fatal().Msgf("Unreachable, unknown mode: %s", tsu.p.tproxyMode) + } +} + +func (tsu *tproxyServerUDP) clearRedirectRules() error { + var setex string + if tsu.p.debug { + setex = "set -ex" + } + if tsu.p.tproxyMode == "tproxy" { + cmd := exec.Command("bash", "-c", fmt.Sprintf(` + %s + iptables -t mangle -D PREROUTING -p udp -m socket -j DIVERT 2>/dev/null || true + iptables -t mangle -D PREROUTING -p udp -j GOHPTS_UDP 2>/dev/null || true + iptables -t mangle -F GOHPTS_UDP 2>/dev/null || true + iptables -t mangle -X GOHPTS_UDP 2>/dev/null || true + `, setex)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if !tsu.p.debug { + cmd.Stdout = nil + } + if err := cmd.Run(); err != nil { + return err + } + } + return nil +} From 496f8e9c053bd9e786c805f716608d3b753d9c0b Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:23:34 +0300 Subject: [PATCH 03/10] fixed more race conditions, improved logging --- colorize.go | 2 +- go.mod | 2 +- go.sum | 4 +- helpers.go | 2 +- tproxy_linux.go | 17 ++- tproxy_udp_linux.go | 343 +++++++++++++++++++++++++++++--------------- 6 files changed, 242 insertions(+), 128 deletions(-) diff --git a/colorize.go b/colorize.go index 50d4247..37ea49e 100644 --- a/colorize.go +++ b/colorize.go @@ -18,7 +18,7 @@ import ( var ( ipPortPattern = regexp.MustCompile( - `\b(?:\d{1,3}\.){3}\d{1,3}(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?\b`, + `(?:\[(?:[0-9a-fA-F:.]+)\]|(?:\d{1,3}\.){3}\d{1,3})(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?`, ) domainPattern = regexp.MustCompile( `\b(?:[a-zA-Z0-9-]{1,63}\.)+(?:com|net|org|io|co|uk|ru|de|edu|gov|info|biz|dev|app|ai)(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?\b`, diff --git a/go.mod b/go.mod index 291add7..5e55972 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/rs/zerolog v1.34.0 github.com/shadowy-pycoder/colors v0.0.1 - github.com/shadowy-pycoder/mshark v0.0.10 + github.com/shadowy-pycoder/mshark v0.0.12 github.com/wzshiming/socks5 v0.5.2 golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 diff --git a/go.sum b/go.sum index 235f479..bfb16fe 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/shadowy-pycoder/colors v0.0.1 h1:weCj/YIOupqy4BSP8KuVzr20fC+cuAv/tArz7bhhkP4= github.com/shadowy-pycoder/colors v0.0.1/go.mod h1:lkrJS1PY2oVigNLTT6pkbF7B/v0YcU2LD5PZnss1Q4U= -github.com/shadowy-pycoder/mshark v0.0.10 h1:pLMIsgfvnO0oKeBNdy0fTGQsx//6scCPT52g93CqyT4= -github.com/shadowy-pycoder/mshark v0.0.10/go.mod h1:FqbHFdsx0zMnrZZH0+oPzaFcleP4O+tUWv8i5gxo87k= +github.com/shadowy-pycoder/mshark v0.0.12 h1:1zPAQLhKu1pxAZTm5oqUahywJ7SIpx9ds9/ytz5TwaU= +github.com/shadowy-pycoder/mshark v0.0.12/go.mod h1:FqbHFdsx0zMnrZZH0+oPzaFcleP4O+tUWv8i5gxo87k= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/wzshiming/socks5 v0.5.2 h1:LtoowVNwAmkIQSkP1r1Wg435xUmC+tfRxorNW30KtnM= diff --git a/helpers.go b/helpers.go index 8add753..4e1eaf5 100644 --- a/helpers.go +++ b/helpers.go @@ -199,7 +199,7 @@ func createSysctlOptCmd(opt, value, setex string, opts map[string]string, debug `, setex, opt, value)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if debug { + if !debug { cmd.Stdout = nil } return cmd diff --git a/tproxy_linux.go b/tproxy_linux.go index dbed485..b69b07f 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -12,6 +12,7 @@ import ( "os" "os/exec" "sync" + "sync/atomic" "syscall" "time" "unsafe" @@ -22,10 +23,11 @@ import ( ) type tproxyServer struct { - listener net.Listener - quit chan struct{} - wg sync.WaitGroup - p *proxyapp + listener net.Listener + quit chan struct{} + wg sync.WaitGroup + p *proxyapp + startingFlag atomic.Bool } func newTproxyServer(p *proxyapp) *tproxyServer { @@ -63,8 +65,10 @@ func newTproxyServer(p *proxyapp) *tproxyServer { } func (ts *tproxyServer) ListenAndServe() { + ts.startingFlag.Store(true) ts.wg.Add(1) go ts.serve() + ts.startingFlag.Store(false) } func (ts *tproxyServer) serve() { @@ -146,10 +150,8 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed to get destination address", ts.p.tproxyMode) return } - ts.p.logger.Debug().Msgf("[tcp %s] getsockopt SO_ORIGINAL_DST %s", ts.p.tproxyMode, dst) case "tproxy": dst = srcConn.LocalAddr().String() - ts.p.logger.Debug().Msgf("[tcp %s] IP_TRANSPARENT %s", ts.p.tproxyMode, dst) default: ts.p.logger.Fatal().Msg("Unknown tproxyMode") } @@ -220,6 +222,9 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { } func (ts *tproxyServer) Shutdown() { + for ts.startingFlag.Load() { + time.Sleep(50 * time.Millisecond) + } close(ts.quit) ts.listener.Close() done := make(chan struct{}) diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go index d9cee1c..122795b 100644 --- a/tproxy_udp_linux.go +++ b/tproxy_udp_linux.go @@ -13,6 +13,7 @@ import ( "os" "os/exec" "sync" + "sync/atomic" "syscall" "time" "unsafe" @@ -22,30 +23,27 @@ import ( "golang.org/x/sys/unix" ) -var ( - googleDNSAddr *net.UDPAddr = &net.UDPAddr{IP: net.ParseIP("8.8.8.8"), Port: 53} - idleUDPConnectionsTimeout time.Duration = 60 * time.Second +const ( + readTimeoutUDP time.Duration = 5 * time.Second + writeTimeoutUDP time.Duration = 5 * time.Second + idleTimeoutUDP time.Duration = 30 * time.Second + udpBufferSize int = 4096 ) +var googleDNSAddr *net.UDPAddr = &net.UDPAddr{IP: net.ParseIP("8.8.8.8"), Port: 53} + type udpConn struct { *socks5.UDPConn clientAddr *net.UDPAddr dstAddr *net.UDPAddr lastSeen time.Time -} - -func (uc *udpConn) ClientAddr() *net.UDPAddr { - return uc.clientAddr -} - -func (uc *udpConn) DstAddr() *net.UDPAddr { - return uc.dstAddr + written atomic.Uint64 } func newUDPConn(clientAddr *net.UDPAddr, dstAddr *net.UDPAddr, sockDialer *socks5.Dialer) (*udpConn, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - conn, err := sockDialer.DialContext(ctx, "udp", dstAddr.String()) + conn, err := sockDialer.DialContext(ctx, "udp4", dstAddr.String()) if err != nil { return nil, err } @@ -65,7 +63,7 @@ type udpConnections struct { func (ucs *udpConnections) Add(conn *udpConn) { ucs.Lock() - ucs.clients[fmt.Sprintf("%s,%s", conn.ClientAddr(), conn.DstAddr())] = conn + ucs.clients[fmt.Sprintf("%s,%s", conn.clientAddr, conn.dstAddr)] = conn ucs.Unlock() } @@ -78,7 +76,7 @@ func (ucs *udpConnections) Get(clientAddr, dstAddr *net.UDPAddr) (*udpConn, bool func (ucs *udpConnections) Remove(conn *udpConn) { ucs.Lock() - delete(ucs.clients, fmt.Sprintf("%s,%s", conn.ClientAddr(), conn.DstAddr())) + delete(ucs.clients, fmt.Sprintf("%s,%s", conn.clientAddr, conn.dstAddr)) ucs.Unlock() } @@ -96,7 +94,7 @@ func (ucs *udpConnections) RemoveByAddr(addr string) { func (ucs *udpConnections) Cleanup() { ucs.wg.Add(1) - t := time.NewTicker(idleUDPConnectionsTimeout) + t := time.NewTicker(idleTimeoutUDP) for { select { case <-ucs.quit: @@ -110,7 +108,7 @@ func (ucs *udpConnections) Cleanup() { case <-t.C: ucs.Lock() for k, conn := range ucs.clients { - if time.Since(conn.lastSeen) > idleUDPConnectionsTimeout { + if time.Since(conn.lastSeen) > idleTimeoutUDP { conn.Close() ucs.RemoveByAddr(k) } @@ -121,14 +119,15 @@ func (ucs *udpConnections) Cleanup() { } type tproxyServerUDP struct { - conn *net.UDPConn - quit chan struct{} - wg sync.WaitGroup - p *proxyapp - clients *udpConnections - iface *net.Interface - gwConn *net.UDPConn - gwDNS *net.UDPAddr + conn *net.UDPConn + quit chan struct{} + wg sync.WaitGroup + p *proxyapp + clients *udpConnections + iface *net.Interface + gwConn *net.UDPConn + gwDNS *net.UDPAddr + startingFlag atomic.Bool } func newTproxyServerUDP(p *proxyapp) *tproxyServerUDP { @@ -193,119 +192,129 @@ func newTproxyServerUDP(p *proxyapp) *tproxyServerUDP { return tsu } -func (tsu *tproxyServerUDP) handleDNSConnections() { - tsu.wg.Add(1) - defer tsu.wg.Done() - buf := make([]byte, 4096) - for { - select { - case <-tsu.quit: - return - default: - n, srcAddr, err := tsu.gwConn.ReadFromUDP(buf) - if err != nil { - tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed reading UDP message", tsu.p.tproxyMode) - continue - } - tsu.p.logger.Debug().Msgf("[udp %s] Got connection from %s", tsu.p.tproxyMode, srcAddr) - conn, err := net.DialUDP("udp", nil, googleDNSAddr) - if err != nil { - tsu.p.logger.Error(). - Err(err). - Msgf("[udp %s] Failed creating connection from %s to %s", tsu.p.tproxyMode, srcAddr, googleDNSAddr) - continue - } - _, err = conn.Write(buf[:n]) - if err != nil { - tsu.p.logger.Error(). - Err(err). - Msgf("[udp %s] Failed writing message from %s to %s", tsu.p.tproxyMode, srcAddr, googleDNSAddr) - continue - } - go tsu.handleDNSConnection(conn, srcAddr) - } - } -} - func (tsu *tproxyServerUDP) ListenAndServe() { + tsu.startingFlag.Store(true) tsu.wg.Add(1) - defer tsu.wg.Done() go tsu.clients.Cleanup() - go tsu.handleDNSConnections() - buf := make([]byte, 4096) + go func() { + tsu.listenAndServeDNS() + tsu.wg.Done() + }() + buf := make([]byte, udpBufferSize) oob := make([]byte, 1500) + tsu.startingFlag.Store(false) for { select { case <-tsu.quit: + tsu.wg.Done() return default: - n, oobn, _, srcAddr, err := tsu.conn.ReadMsgUDP(buf, oob) - if err != nil { - tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed reading UDP message", tsu.p.tproxyMode) - continue - } - tsu.p.logger.Debug().Msgf("[udp %s] Got connection from %s", tsu.p.tproxyMode, srcAddr) - dstAddr, err := tsu.getOriginalDst(oob[:oobn]) + err := tsu.conn.SetReadDeadline(time.Now().Add(readTimeoutUDP)) if err != nil { - tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed getting original destination", tsu.p.tproxyMode) + if errors.Is(err, net.ErrClosed) { + continue + } + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed setting read deadline", tsu.p.tproxyMode) continue } - tsu.p.logger.Debug().Msgf("[udp %s] IP_TRANSPARENT %s", tsu.p.tproxyMode, dstAddr) - conn, found := tsu.clients.Get(srcAddr, dstAddr) - if !found { - sockDialer, _, err := tsu.p.getSocks() + n, oobn, _, srcAddr, er := tsu.conn.ReadMsgUDP(buf, oob) + if n > 0 { + dstAddr, err := tsu.getOriginalDst(oob[:oobn]) + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed getting original destination", tsu.p.tproxyMode) + continue + } + conn, found := tsu.clients.Get(srcAddr, dstAddr) + if !found { + sockDialer, _, err := tsu.p.getSocks() + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed getting SOCKS5 client for %s→ %s", tsu.p.tproxyMode, srcAddr, dstAddr) + continue + } + conn, err = newUDPConn(srcAddr, dstAddr, sockDialer) + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed creating UDP connection for %s→ %s", tsu.p.tproxyMode, srcAddr, dstAddr) + continue + } + tsu.clients.Add(conn) + go func() { + tsu.handleConnection(conn) + tsu.wg.Done() + }() + } + srcConnStr := fmt.Sprintf("%s→ %s", srcAddr, dstAddr) + dstConnStr := fmt.Sprintf("%s→ %s→ %s", tsu.conn.LocalAddr(), conn.LocalAddr(), dstAddr) + tsu.p.logger.Debug().Msgf("[udp %s] src: %s - dst: %s", tsu.p.tproxyMode, srcConnStr, dstConnStr) + err = conn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) if err != nil { - tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed getting SOCKS5 client", tsu.p.tproxyMode) + if errors.Is(err, net.ErrClosed) { + continue + } + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed setting write deadline", tsu.p.tproxyMode) continue } - conn, err = newUDPConn(srcAddr, dstAddr, sockDialer) + nw, err := conn.WriteToUDP(buf[:n], dstAddr) if err != nil { - tsu.p.logger.Error(). - Err(err). - Msgf("[udp %s] Failed creating UDP connection for %s", tsu.p.tproxyMode, srcAddr) + if ne, ok := err.(net.Error); ok && ne.Timeout() { + continue + } + if errors.Is(err, net.ErrClosed) { + continue + } + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, srcAddr, dstAddr) continue } - tsu.clients.Add(conn) - go tsu.handleConnection(conn) - } else { - tsu.p.logger.Debug().Msgf("[udp %s] Found connection for %s", tsu.p.tproxyMode, srcAddr) + conn.written.Add(uint64(nw)) + tsu.clients.UpdateLastSeen(conn) } - _, err = conn.WriteToUDP(buf[:n], dstAddr) - if err != nil { - tsu.p.logger.Error(). - Err(err). - Msgf("[udp %s] failed sending message from %s to %s", tsu.p.tproxyMode, srcAddr, dstAddr) + if er != nil { + if ne, ok := er.(net.Error); ok && ne.Timeout() { + continue + } + if errors.Is(err, net.ErrClosed) { + continue + } + if errors.Is(er, io.EOF) { + continue + } + tsu.p.logger.Error().Err(er).Msgf("[udp %s] Failed reading UDP message", tsu.p.tproxyMode) continue } - tsu.clients.UpdateLastSeen(conn) } } } func (tsu *tproxyServerUDP) handleConnection(conn *udpConn) { tsu.wg.Add(1) - defer tsu.wg.Done() - buf := make([]byte, 4096) - var written int64 + buf := make([]byte, udpBufferSize) + defer func() { + srcConnStr := fmt.Sprintf("%s→ %s", conn.clientAddr, conn.dstAddr) + dstConnStr := fmt.Sprintf("%s→ %s→ %s", tsu.conn.LocalAddr(), conn.LocalAddr(), conn.dstAddr) + tsu.p.logger.Debug().Msgf("[udp %s] Copied %s for src: %s - dst: %s", + tsu.p.tproxyMode, + prettifyBytes(int64(conn.written.Load())), + srcConnStr, + dstConnStr) + }() readLoop: for { select { case <-tsu.quit: return default: - er := conn.SetReadDeadline(time.Now().Add(readTimeout)) + er := conn.SetReadDeadline(time.Now().Add(readTimeoutUDP)) if er != nil { if errors.Is(er, net.ErrClosed) { return } - tsu.p.logger.Debug().Err(er).Msgf("[udp %s] failed setting read deadline %s→ %s", tsu.p.tproxyMode, conn.dstAddr, conn.clientAddr) + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting read deadline %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), tsu.conn.LocalAddr()) break readLoop } nr, er := conn.Read(buf) if nr > 0 { - er := tsu.conn.SetWriteDeadline(time.Now().Add(writeTimeout)) + er := tsu.conn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) if er != nil { - tsu.p.logger.Debug().Err(er).Msgf("[udp %s] failed setting write deadline %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) break readLoop } nw, ew := tsu.conn.WriteToUDP(buf[0:nr], conn.clientAddr) @@ -315,7 +324,7 @@ readLoop: ew = errInvalidWrite } } - written += int64(nw) + conn.written.Add(uint64(nw)) if ew != nil { if errors.Is(ew, net.ErrClosed) { return @@ -325,7 +334,7 @@ readLoop: } } if nr != nw { - tsu.p.logger.Debug().Err(io.ErrShortWrite).Msgf("[udp %s] failed sending message %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) + tsu.p.logger.Debug().Err(io.ErrShortWrite).Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) break readLoop } } @@ -336,7 +345,7 @@ readLoop: if errors.Is(er, net.ErrClosed) { return } - if er == io.EOF { + if errors.Is(er, io.EOF) { break readLoop } break readLoop @@ -347,43 +356,141 @@ readLoop: tsu.clients.Remove(conn) } -func (tsu *tproxyServerUDP) handleDNSConnection(conn *net.UDPConn, srcAddr *net.UDPAddr) { +type dnsConn struct { + *net.UDPConn + clientAddr *net.UDPAddr + written atomic.Uint64 +} + +func newDNSConn(clientAddr *net.UDPAddr, mark uint) (*dnsConn, error) { + dialer := getBaseDialer(timeout, mark) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + conn, err := dialer.DialContext(ctx, "udp4", googleDNSAddr.String()) + if err != nil { + return nil, err + } + udpConn, ok := conn.(*net.UDPConn) + if !ok { + return nil, fmt.Errorf("failed obtaining dns connection") + } + return &dnsConn{UDPConn: udpConn, clientAddr: clientAddr}, nil +} + +func (tsu *tproxyServerUDP) listenAndServeDNS() { + tsu.wg.Add(1) + buf := make([]byte, udpBufferSize) + for { + select { + case <-tsu.quit: + return + default: + err := tsu.gwConn.SetReadDeadline(time.Now().Add(readTimeoutUDP)) + if err != nil { + if errors.Is(err, net.ErrClosed) { + continue + } + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed setting read deadline", tsu.p.tproxyMode) + continue + } + n, srcAddr, er := tsu.gwConn.ReadFromUDP(buf) + if n > 0 { + conn, err := newDNSConn(srcAddr, tsu.p.mark) + if err != nil { + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed creating UDP connection %s→ %s", tsu.p.tproxyMode, srcAddr, googleDNSAddr) + continue + } + srcConnStr := fmt.Sprintf("%s→ %s", srcAddr, tsu.gwConn.LocalAddr()) + dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), googleDNSAddr) + tsu.p.logger.Debug().Msgf("[udp %s] src: %s - dst: %s", tsu.p.tproxyMode, srcConnStr, dstConnStr) + err = conn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) + if err != nil { + if errors.Is(err, net.ErrClosed) { + continue + } + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed setting write deadline", tsu.p.tproxyMode) + continue + } + nw, err := conn.Write(buf[:n]) + if err != nil { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + continue + } + if errors.Is(err, net.ErrClosed) { + continue + } + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), googleDNSAddr) + continue + } + conn.written.Add(uint64(nw)) + go func() { + tsu.handleDNSConnection(conn) + tsu.wg.Done() + }() + } + if er != nil { + if ne, ok := er.(net.Error); ok && ne.Timeout() { + continue + } + if errors.Is(err, net.ErrClosed) { + continue + } + if errors.Is(er, io.EOF) { + continue + } + tsu.p.logger.Error().Err(er).Msgf("[udp %s] Failed reading UDP message", tsu.p.tproxyMode) + continue + } + } + } +} + +func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { tsu.wg.Add(1) - defer tsu.wg.Done() - defer conn.Close() - buf := make([]byte, 4096) - var written int64 - er := conn.SetReadDeadline(time.Now().Add(readTimeout)) + defer func() { + srcConnStr := fmt.Sprintf("%s→ %s", conn.clientAddr, tsu.gwConn.LocalAddr()) + dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), googleDNSAddr) + tsu.p.logger.Debug().Msgf("[udp %s] Copied %s for src: %s - dst: %s", + tsu.p.tproxyMode, + prettifyBytes(int64(conn.written.Load())), + srcConnStr, + dstConnStr) + conn.Close() + }() + buf := make([]byte, udpBufferSize) + er := conn.SetReadDeadline(time.Now().Add(readTimeoutUDP)) if er != nil { - tsu.p.logger.Debug(). - Err(er). - Msgf("[udp %s] failed setting read deadline %s→ %s", tsu.p.tproxyMode, googleDNSAddr, conn.LocalAddr()) + if errors.Is(er, net.ErrClosed) { + return + } + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting read deadline %s→ %s", tsu.p.tproxyMode, googleDNSAddr, conn.LocalAddr()) return } nr, er := conn.Read(buf) if nr > 0 { - er := tsu.conn.SetWriteDeadline(time.Now().Add(writeTimeout)) + er := tsu.gwConn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) if er != nil { - tsu.p.logger.Debug(). - Err(er). - Msgf("[udp %s] failed setting write deadline %s→ %s", tsu.p.tproxyMode, googleDNSAddr, srcAddr) + if errors.Is(er, net.ErrClosed) { + return + } + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.clientAddr) return } - nw, ew := tsu.gwConn.WriteToUDP(buf[0:nr], srcAddr) + nw, ew := tsu.gwConn.WriteToUDP(buf[0:nr], conn.clientAddr) if nw < 0 || nr < nw { nw = 0 if ew == nil { ew = errInvalidWrite } } - written += int64(nw) + conn.written.Add(uint64(nw)) if ew != nil { return } if nr != nw { tsu.p.logger.Debug(). Err(io.ErrShortWrite). - Msgf("[udp %s] failed sending message %s→ %s", tsu.p.tproxyMode, googleDNSAddr, conn.LocalAddr()) + Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.clientAddr) return } } @@ -393,13 +500,15 @@ func (tsu *tproxyServerUDP) handleDNSConnection(conn *net.UDPConn, srcAddr *net. } func (tsu *tproxyServerUDP) Shutdown() { + for tsu.startingFlag.Load() { + time.Sleep(50 * time.Millisecond) + } close(tsu.quit) done := make(chan struct{}) go func() { tsu.wg.Wait() close(done) }() - select { case <-done: tsu.p.logger.Info().Msgf("[udp %s] Server gracefully shutdown", tsu.p.tproxyMode) @@ -454,11 +563,11 @@ func (tsu *tproxyServerUDP) applyRedirectRules(opts map[string]string) { cmdClear.Stdout = os.Stdout cmdClear.Stderr = os.Stderr if err := cmdClear.Run(); err != nil { - tsu.p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] Failed while configuring iptables. Are you root?", tsu.p.tproxyMode) } prefix, err := network.GetIPv4PrefixFromInterface(tsu.iface) if err != nil { - tsu.p.logger.Fatal().Err(err).Msgf("failed getting host from %s", tsu.iface.Name) + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] Failed getting host from %s", tsu.p.tproxyMode, tsu.iface.Name) } cmdInit0 := exec.Command("bash", "-c", fmt.Sprintf(` %s From 0f99e3c8a57461046975609aaa628bc61d9e03fc Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:38:08 +0300 Subject: [PATCH 04/10] made variables naming more consistent --- tproxy_udp_linux.go | 65 ++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go index 122795b..ba3ca7e 100644 --- a/tproxy_udp_linux.go +++ b/tproxy_udp_linux.go @@ -34,13 +34,13 @@ var googleDNSAddr *net.UDPAddr = &net.UDPAddr{IP: net.ParseIP("8.8.8.8"), Port: type udpConn struct { *socks5.UDPConn - clientAddr *net.UDPAddr - dstAddr *net.UDPAddr - lastSeen time.Time - written atomic.Uint64 + srcAddr *net.UDPAddr + dstAddr *net.UDPAddr + lastSeen time.Time + written atomic.Uint64 } -func newUDPConn(clientAddr *net.UDPAddr, dstAddr *net.UDPAddr, sockDialer *socks5.Dialer) (*udpConn, error) { +func newUDPConn(srcAddr *net.UDPAddr, dstAddr *net.UDPAddr, sockDialer *socks5.Dialer) (*udpConn, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() conn, err := sockDialer.DialContext(ctx, "udp4", dstAddr.String()) @@ -51,7 +51,7 @@ func newUDPConn(clientAddr *net.UDPAddr, dstAddr *net.UDPAddr, sockDialer *socks if !ok { return nil, fmt.Errorf("failed obtaining relay connection") } - return &udpConn{UDPConn: relayConn, clientAddr: clientAddr, dstAddr: dstAddr, lastSeen: time.Now()}, nil + return &udpConn{UDPConn: relayConn, srcAddr: srcAddr, dstAddr: dstAddr, lastSeen: time.Now()}, nil } type udpConnections struct { @@ -63,20 +63,20 @@ type udpConnections struct { func (ucs *udpConnections) Add(conn *udpConn) { ucs.Lock() - ucs.clients[fmt.Sprintf("%s,%s", conn.clientAddr, conn.dstAddr)] = conn + ucs.clients[fmt.Sprintf("%s,%s", conn.srcAddr, conn.dstAddr)] = conn ucs.Unlock() } -func (ucs *udpConnections) Get(clientAddr, dstAddr *net.UDPAddr) (*udpConn, bool) { +func (ucs *udpConnections) Get(srcAddr, dstAddr *net.UDPAddr) (*udpConn, bool) { ucs.RLock() defer ucs.RUnlock() - conn, ok := ucs.clients[fmt.Sprintf("%s,%s", clientAddr, dstAddr)] + conn, ok := ucs.clients[fmt.Sprintf("%s,%s", srcAddr, dstAddr)] return conn, ok } func (ucs *udpConnections) Remove(conn *udpConn) { ucs.Lock() - delete(ucs.clients, fmt.Sprintf("%s,%s", conn.clientAddr, conn.dstAddr)) + delete(ucs.clients, fmt.Sprintf("%s,%s", conn.srcAddr, conn.dstAddr)) ucs.Unlock() } @@ -288,13 +288,9 @@ func (tsu *tproxyServerUDP) handleConnection(conn *udpConn) { tsu.wg.Add(1) buf := make([]byte, udpBufferSize) defer func() { - srcConnStr := fmt.Sprintf("%s→ %s", conn.clientAddr, conn.dstAddr) + srcConnStr := fmt.Sprintf("%s→ %s", conn.srcAddr, conn.dstAddr) dstConnStr := fmt.Sprintf("%s→ %s→ %s", tsu.conn.LocalAddr(), conn.LocalAddr(), conn.dstAddr) - tsu.p.logger.Debug().Msgf("[udp %s] Copied %s for src: %s - dst: %s", - tsu.p.tproxyMode, - prettifyBytes(int64(conn.written.Load())), - srcConnStr, - dstConnStr) + tsu.p.logger.Debug().Msgf("Copied %s for udp src: %s - dst: %s", prettifyBytes(int64(conn.written.Load())), srcConnStr, dstConnStr) }() readLoop: for { @@ -314,10 +310,10 @@ readLoop: if nr > 0 { er := tsu.conn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) if er != nil { - tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.srcAddr) break readLoop } - nw, ew := tsu.conn.WriteToUDP(buf[0:nr], conn.clientAddr) + nw, ew := tsu.conn.WriteToUDP(buf[0:nr], conn.srcAddr) if nw < 0 || nr < nw { nw = 0 if ew == nil { @@ -334,7 +330,7 @@ readLoop: } } if nr != nw { - tsu.p.logger.Debug().Err(io.ErrShortWrite).Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.clientAddr) + tsu.p.logger.Debug().Err(io.ErrShortWrite).Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.srcAddr) break readLoop } } @@ -358,11 +354,12 @@ readLoop: type dnsConn struct { *net.UDPConn - clientAddr *net.UDPAddr - written atomic.Uint64 + srcAddr *net.UDPAddr + dstAddr *net.UDPAddr + written atomic.Uint64 } -func newDNSConn(clientAddr *net.UDPAddr, mark uint) (*dnsConn, error) { +func newDNSConn(srcAddr *net.UDPAddr, mark uint) (*dnsConn, error) { dialer := getBaseDialer(timeout, mark) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -374,7 +371,7 @@ func newDNSConn(clientAddr *net.UDPAddr, mark uint) (*dnsConn, error) { if !ok { return nil, fmt.Errorf("failed obtaining dns connection") } - return &dnsConn{UDPConn: udpConn, clientAddr: clientAddr}, nil + return &dnsConn{UDPConn: udpConn, srcAddr: srcAddr, dstAddr: googleDNSAddr}, nil } func (tsu *tproxyServerUDP) listenAndServeDNS() { @@ -401,7 +398,7 @@ func (tsu *tproxyServerUDP) listenAndServeDNS() { continue } srcConnStr := fmt.Sprintf("%s→ %s", srcAddr, tsu.gwConn.LocalAddr()) - dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), googleDNSAddr) + dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), conn.dstAddr) tsu.p.logger.Debug().Msgf("[udp %s] src: %s - dst: %s", tsu.p.tproxyMode, srcConnStr, dstConnStr) err = conn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) if err != nil { @@ -419,7 +416,7 @@ func (tsu *tproxyServerUDP) listenAndServeDNS() { if errors.Is(err, net.ErrClosed) { continue } - tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), googleDNSAddr) + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.dstAddr) continue } conn.written.Add(uint64(nw)) @@ -448,13 +445,9 @@ func (tsu *tproxyServerUDP) listenAndServeDNS() { func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { tsu.wg.Add(1) defer func() { - srcConnStr := fmt.Sprintf("%s→ %s", conn.clientAddr, tsu.gwConn.LocalAddr()) - dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), googleDNSAddr) - tsu.p.logger.Debug().Msgf("[udp %s] Copied %s for src: %s - dst: %s", - tsu.p.tproxyMode, - prettifyBytes(int64(conn.written.Load())), - srcConnStr, - dstConnStr) + srcConnStr := fmt.Sprintf("%s→ %s", conn.srcAddr, tsu.gwConn.LocalAddr()) + dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), conn.dstAddr) + tsu.p.logger.Debug().Msgf("Copied %s for udp src: %s - dst: %s", prettifyBytes(int64(conn.written.Load())), srcConnStr, dstConnStr) conn.Close() }() buf := make([]byte, udpBufferSize) @@ -463,7 +456,7 @@ func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { if errors.Is(er, net.ErrClosed) { return } - tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting read deadline %s→ %s", tsu.p.tproxyMode, googleDNSAddr, conn.LocalAddr()) + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting read deadline %s→ %s", tsu.p.tproxyMode, conn.dstAddr, conn.LocalAddr()) return } nr, er := conn.Read(buf) @@ -473,10 +466,10 @@ func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { if errors.Is(er, net.ErrClosed) { return } - tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.clientAddr) + tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.srcAddr) return } - nw, ew := tsu.gwConn.WriteToUDP(buf[0:nr], conn.clientAddr) + nw, ew := tsu.gwConn.WriteToUDP(buf[0:nr], conn.srcAddr) if nw < 0 || nr < nw { nw = 0 if ew == nil { @@ -490,7 +483,7 @@ func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { if nr != nw { tsu.p.logger.Debug(). Err(io.ErrShortWrite). - Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.clientAddr) + Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.srcAddr) return } } From 60169bf7ea8b85c6e7246d6844336a976233d14f Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Tue, 5 Aug 2025 10:39:09 +0300 Subject: [PATCH 05/10] deleted deletion if some rules that have been deleted previously --- tproxy_udp_linux.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go index ba3ca7e..7193386 100644 --- a/tproxy_udp_linux.go +++ b/tproxy_udp_linux.go @@ -549,9 +549,6 @@ func (tsu *tproxyServerUDP) applyRedirectRules(opts map[string]string) { iptables -t mangle -D PREROUTING -p udp -j GOHPTS_UDP 2>/dev/null || true iptables -t mangle -F GOHPTS_UDP 2>/dev/null || true iptables -t mangle -X GOHPTS_UDP 2>/dev/null || true - iptables -t nat -D PREROUTING -p udp -j GOHPTS_UDP 2>/dev/null || true - iptables -t nat -F GOHPTS_UDP 2>/dev/null || true - iptables -t nat -X GOHPTS_UDP 2>/dev/null || true `, setex)) cmdClear.Stdout = os.Stdout cmdClear.Stderr = os.Stderr From 6831fda1977d20ec617b17cd76923910469dad8b Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:10:32 +0300 Subject: [PATCH 06/10] renamed some methods, updated README.md withn instructions on how to test UDP connection --- README.md | 33 +++++++++++++++++++++++++++++++-- gohpts.go | 12 ++++++------ tproxy_linux.go | 4 ++-- tproxy_nonlinux.go | 30 ++---------------------------- tproxy_udp_linux.go | 4 ++-- tproxy_udp_nonlinux.go | 25 +++++++++++++++++++++++++ version.go | 2 +- 7 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 tproxy_udp_nonlinux.go diff --git a/README.md b/README.md index ee6e38c..0582198 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - [Transparent proxy](#transparent-proxy) - [redirect (via NAT and SO_ORIGINAL_DST)](#redirect-via-nat-and-so_original_dst) - [tproxy (via MANGLE and IP_TRANSPARENT)](#tproxy-via-mangle-and-ip_transparent) + - [UDP support](#udp-support) - [ARP spoofing](#arp-spoofing) - [Traffic sniffing](#traffic-sniffing) - [JSON format](#json-format) @@ -62,8 +63,11 @@ Specify http server in proxy configuration of Postman - **Transparent proxy**\ Supports `redirect` (SO_ORIGINAL_DST) and `tproxy` (IP_TRANSPARENT) modes +- **TCP and UDP Transparent proxy**\ + `tproxy` (IP_TRANSPARENT) handles TCP and UDP traffic + - **Traffic sniffing**\ - Proxy is able to parse HTTP headers and TLS handshake metadata + Proxy is able to parse HTTP headers, TLS handshake, DNS messages and more - **ARP spoofing**\ Proxy entire subnets with ARP spoofing approach @@ -101,7 +105,7 @@ You can download the binary for your platform from [Releases](https://github.com Example: ```shell -GOHPTS_RELEASE=v1.9.4; wget -v https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases/download/$GOHPTS_RELEASE/gohpts-$GOHPTS_RELEASE-linux-amd64.tar.gz -O gohpts && tar xvzf gohpts && mv -f gohpts-$GOHPTS_RELEASE-linux-amd64 gohpts && ./gohpts -h +GOHPTS_RELEASE=v2.0.0; wget -v https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases/download/$GOHPTS_RELEASE/gohpts-$GOHPTS_RELEASE-linux-amd64.tar.gz -O gohpts && tar xvzf gohpts && mv -f gohpts-$GOHPTS_RELEASE-linux-amd64 gohpts && ./gohpts -h ``` Alternatively, you can install it using `go install` command (requires Go [1.24](https://go.dev/doc/install) or later): @@ -168,6 +172,7 @@ Options: TProxy: -t Address of transparent proxy server (it starts along with HTTP proxy server) -T Address of transparent proxy server (no HTTP) + -Tu Address of transparent UDP proxy server -M Transparent proxy mode: (redirect, tproxy) -auto Automatically setup iptables for transparent proxy (requires elevated privileges) -arpspoof Enable ARP spoof proxy for selected targets (Example: "targets 10.0.0.1,10.0.0.5-10,192.168.1.*,192.168.10.0/24;fullduplex false;debug true") @@ -521,6 +526,30 @@ sudo bettercap -eval "net.probe on;net.recon on;set arp.spoof.fullduplex true;ar Check proxy logs for traffic from other devices from your LAN +### UDP support + +`GoHPTS` has UDP support that can be enabled in `tproxy` mode. For this setup to work you need to connect to a socks5 server capable of serving UDP connections (`UDP ASSOCIATE`). For example, you can use [https://github.com/wzshiming/socks5](https://github.com/wzshiming/socks5) to deploy UDP capable UDP server on some remote or local machine. Once you have the server to connect to, run the following command: + +```shell +sudo env PATH=$PATH gohpts -s remote -Tu :8989 -M tproxy -auto -mark 100 -d +``` + +This command will configure your operating system and setup server on `0.0.0.0:8989` address. + +To test it locally, you can combine UDP transparent proxy with `-arpspoof` flag. For example: + +1. Setup VM on your system with any Linux distributive that supports `tproxy` (Kali Linux, for instance). +2. Enable `Bridged` network so that VM could access your host machine. +3. Move `gohpts` binary to VM (via `ssh`, for instance) or build it there in case of different OS/arch. +4. On your VM run the following command: + +```shell +# Do not forget to replace and with actual addresses +sudo ./gohpts -s -T 8888 -Tu :8989 -M tproxy -sniff -body -auto -mark 100 -d -arpspoof "targets ;fullduplex true;debug false" +``` + +4. Check connection on your host machine, the traffic should go through Kali machine. + ## Traffic sniffing [[Back]](#table-of-contents) diff --git a/gohpts.go b/gohpts.go index 2a1a594..4372c4e 100644 --- a/gohpts.go +++ b/gohpts.go @@ -520,14 +520,14 @@ func (p *proxyapp) Run() { if p.tproxyAddr != "" { tproxyServer = newTproxyServer(p) if p.auto { - tproxyServer.applyRedirectRules(opts) + tproxyServer.ApplyRedirectRules(opts) } } var tproxyServerUDP *tproxyServerUDP if p.tproxyAddrUDP != "" { tproxyServerUDP = newTproxyServerUDP(p) if p.auto { - tproxyServerUDP.applyRedirectRules(opts) + tproxyServerUDP.ApplyRedirectRules(opts) } } if p.proxylist != nil { @@ -554,7 +554,7 @@ func (p *proxyapp) Run() { if tproxyServer != nil { p.logger.Info().Msgf("[tcp %s] Server is shutting down...", p.tproxyMode) if p.auto { - err := tproxyServer.clearRedirectRules() + err := tproxyServer.ClearRedirectRules() if err != nil { p.logger.Error().Err(err).Msg("Failed clearing iptables rules") } @@ -564,7 +564,7 @@ func (p *proxyapp) Run() { if tproxyServerUDP != nil { p.logger.Info().Msgf("[udp %s] Server is shutting down...", p.tproxyMode) if p.auto { - err := tproxyServerUDP.clearRedirectRules() + err := tproxyServerUDP.ClearRedirectRules() if err != nil { p.logger.Error().Err(err).Msg("Failed clearing iptables rules") } @@ -621,7 +621,7 @@ func (p *proxyapp) Run() { if tproxyServer != nil { p.logger.Info().Msgf("[tcp %s] Server is shutting down...", p.tproxyMode) if p.auto { - err := tproxyServer.clearRedirectRules() + err := tproxyServer.ClearRedirectRules() if err != nil { p.logger.Error().Err(err).Msg("Failed clearing iptables rules") } @@ -631,7 +631,7 @@ func (p *proxyapp) Run() { if tproxyServerUDP != nil { p.logger.Info().Msgf("[udp %s] Server is shutting down...", p.tproxyMode) if p.auto { - err := tproxyServerUDP.clearRedirectRules() + err := tproxyServerUDP.ClearRedirectRules() if err != nil { p.logger.Error().Err(err).Msg("Failed clearing iptables rules") } diff --git a/tproxy_linux.go b/tproxy_linux.go index b69b07f..30e26d1 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -243,7 +243,7 @@ func (ts *tproxyServer) Shutdown() { } } -func (ts *tproxyServer) applyRedirectRules(opts map[string]string) { +func (ts *tproxyServer) ApplyRedirectRules(opts map[string]string) { _, tproxyPort, _ := net.SplitHostPort(ts.p.tproxyAddr) var setex string if ts.p.debug { @@ -420,7 +420,7 @@ func (ts *tproxyServer) applyRedirectRules(opts map[string]string) { _ = createSysctlOptCmd("net.ipv4.tcp_fin_timeout", "15", setex, opts, ts.p.debug).Run() } -func (ts *tproxyServer) clearRedirectRules() error { +func (ts *tproxyServer) ClearRedirectRules() error { var setex string if ts.p.debug { setex = "set -ex" diff --git a/tproxy_nonlinux.go b/tproxy_nonlinux.go index 974a1c6..b203469 100644 --- a/tproxy_nonlinux.go +++ b/tproxy_nonlinux.go @@ -5,9 +5,7 @@ package gohpts import ( "net" - "os/exec" "sync" - "syscall" ) type tproxyServer struct { @@ -23,39 +21,15 @@ func newTproxyServer(p *proxyapp) *tproxyServer { } func (ts *tproxyServer) ListenAndServe() { - ts.serve() -} - -func (ts *tproxyServer) serve() { - ts.handleConnection(nil) -} - -func (ts *tproxyServer) getOriginalDst(rawConn syscall.RawConn) (string, error) { - _ = rawConn - return "", nil -} - -func (ts *tproxyServer) handleConnection(srcConn net.Conn) { - _ = srcConn - ts.getOriginalDst(nil) } func (ts *tproxyServer) Shutdown() {} -func (ts *tproxyServer) createSysctlOptCmd(opt, value, setex string, opts map[string]string) *exec.Cmd { - _ = opt - _ = value - _ = setex +func (ts *tproxyServer) ApplyRedirectRules(opts map[string]string) map[string]string { _ = opts return nil } -func (ts *tproxyServer) applyRedirectRules() map[string]string { - _ = ts.createSysctlOptCmd("", "", "", nil) - return nil -} - -func (ts *tproxyServer) clearRedirectRules(opts map[string]string) error { - _ = opts +func (ts *tproxyServer) ClearRedirectRules() error { return nil } diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go index 7193386..26b6550 100644 --- a/tproxy_udp_linux.go +++ b/tproxy_udp_linux.go @@ -533,7 +533,7 @@ func (tsu *tproxyServerUDP) getOriginalDst(oob []byte) (*net.UDPAddr, error) { return nil, fmt.Errorf("original destination not found") } -func (tsu *tproxyServerUDP) applyRedirectRules(opts map[string]string) { +func (tsu *tproxyServerUDP) ApplyRedirectRules(opts map[string]string) { _, tproxyPortUDP, _ := net.SplitHostPort(tsu.p.tproxyAddrUDP) var setex string if tsu.p.debug { @@ -607,7 +607,7 @@ func (tsu *tproxyServerUDP) applyRedirectRules(opts map[string]string) { } } -func (tsu *tproxyServerUDP) clearRedirectRules() error { +func (tsu *tproxyServerUDP) ClearRedirectRules() error { var setex string if tsu.p.debug { setex = "set -ex" diff --git a/tproxy_udp_nonlinux.go b/tproxy_udp_nonlinux.go new file mode 100644 index 0000000..b472088 --- /dev/null +++ b/tproxy_udp_nonlinux.go @@ -0,0 +1,25 @@ +//go:build !linux +// +build !linux + +package gohpts + +type tproxyServerUDP struct{} + +func newTproxyServerUDP(p *proxyapp) *tproxyServerUDP { + _ = p + return nil +} + +func (tsu *tproxyServerUDP) ListenAndServe() { +} + +func (tsu *tproxyServerUDP) Shutdown() { +} + +func (tsu *tproxyServerUDP) ApplyRedirectRules(opts map[string]string) { + _ = opts +} + +func (tsu *tproxyServerUDP) ClearRedirectRules() error { + return nil +} diff --git a/version.go b/version.go index a9462f7..b798dc5 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package gohpts -const Version string = "gohpts v1.9.4" +const Version string = "gohpts v2.0.0" From 587de6a93cfeb8fc3ee9a87432e59742319e08bd Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Sun, 10 Aug 2025 07:03:45 +0300 Subject: [PATCH 07/10] added dns parsing but in leaky manner --- README.md | 4 +- colorize.go | 102 ++++++++++++++++++++++++++++++++--- gohpts.go | 47 ++++++++++++++-- tproxy_linux.go | 4 +- tproxy_udp_linux.go | 129 +++++++++++++++++++++++++++++++++++++++----- 5 files changed, 261 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0582198..a22a59b 100644 --- a/README.md +++ b/README.md @@ -528,7 +528,7 @@ Check proxy logs for traffic from other devices from your LAN ### UDP support -`GoHPTS` has UDP support that can be enabled in `tproxy` mode. For this setup to work you need to connect to a socks5 server capable of serving UDP connections (`UDP ASSOCIATE`). For example, you can use [https://github.com/wzshiming/socks5](https://github.com/wzshiming/socks5) to deploy UDP capable UDP server on some remote or local machine. Once you have the server to connect to, run the following command: +`GoHPTS` has UDP support that can be enabled in `tproxy` mode. For this setup to work you need to connect to a socks5 server capable of serving UDP connections (`UDP ASSOCIATE`). For example, you can use [https://github.com/wzshiming/socks5](https://github.com/wzshiming/socks5) to deploy UDP capable socks5 server on some remote or local machine. Once you have the server to connect to, run the following command: ```shell sudo env PATH=$PATH gohpts -s remote -Tu :8989 -M tproxy -auto -mark 100 -d @@ -539,7 +539,7 @@ This command will configure your operating system and setup server on `0.0.0.0:8 To test it locally, you can combine UDP transparent proxy with `-arpspoof` flag. For example: 1. Setup VM on your system with any Linux distributive that supports `tproxy` (Kali Linux, for instance). -2. Enable `Bridged` network so that VM could access your host machine. +2. Enable `bridged` network so that VM could access your host machine. 3. Move `gohpts` binary to VM (via `ssh`, for instance) or build it there in case of different OS/arch. 4. On your VM run the following command: diff --git a/colorize.go b/colorize.go index 37ea49e..bedea10 100644 --- a/colorize.go +++ b/colorize.go @@ -21,7 +21,7 @@ var ( `(?:\[(?:[0-9a-fA-F:.]+)\]|(?:\d{1,3}\.){3}\d{1,3})(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?`, ) domainPattern = regexp.MustCompile( - `\b(?:[a-zA-Z0-9-]{1,63}\.)+(?:com|net|org|io|co|uk|ru|de|edu|gov|info|biz|dev|app|ai)(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?\b`, + `\b(?:[a-zA-Z0-9-]{1,63}\.)+(?:com|net|org|io|co|uk|ru|de|edu|gov|info|biz|dev|app|ai|tv)(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?\b`, ) jwtPattern = regexp.MustCompile(`\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b`) authPattern = regexp.MustCompile( @@ -187,9 +187,9 @@ func colorizeHTTP( func colorizeTLS(req *layers.TLSClientHello, resp *layers.TLSServerHello, id string, nocolor bool) string { var sb strings.Builder + sb.WriteString(fmt.Sprintf("%s ", colorizeTimestamp(time.Now(), nocolor))) + sb.WriteString(id) if nocolor { - sb.WriteString(fmt.Sprintf("%s ", colorizeTimestamp(time.Now(), nocolor))) - sb.WriteString(id) sb.WriteString(fmt.Sprintf(" %s ", req.TypeDesc)) if req.Length > 0 { sb.WriteString(fmt.Sprintf(" Len: %d", req.Length)) @@ -224,8 +224,6 @@ func colorizeTLS(req *layers.TLSClientHello, resp *layers.TLSServerHello, id str sb.WriteString(fmt.Sprintf(" ExtLen: %d", resp.ExtensionLength)) } } else { - sb.WriteString(fmt.Sprintf("%s ", colorizeTimestamp(time.Now(), nocolor))) - sb.WriteString(id) sb.WriteString(colors.Magenta(fmt.Sprintf(" %s ", req.TypeDesc)).Bold()) if req.Length > 0 { sb.WriteString(colors.BeigeBg(fmt.Sprintf(" Len: %d", req.Length)).String()) @@ -263,6 +261,98 @@ func colorizeTLS(req *layers.TLSClientHello, resp *layers.TLSServerHello, id str return sb.String() } +func colorizeRData(rec *layers.ResourceRecord) string { + var rdata string + switch rd := rec.RData.(type) { + case *layers.RDataA: + case *layers.RDataAAAA: + rdata = fmt.Sprintf("%s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(rd.Address.String())) + case *layers.RDataNS: + rdata = fmt.Sprintf("%s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(rd.NsdName)) + case *layers.RDataCNAME: + rdata = fmt.Sprintf("%s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(rd.CName)) + case *layers.RDataSOA: + rdata = fmt.Sprintf("%s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(rd.PrimaryNS)) + case *layers.RDataMX: + rdata = fmt.Sprintf("%s %s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(fmt.Sprintf("%d", rd.Preference)), colors.Gray(rd.Exchange)) + case *layers.RDataTXT: + rdata = fmt.Sprintf("%s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(rd.TxtData)) + default: + rdata = fmt.Sprintf("%s ", colors.LightBlue(rec.Type.Name)) + } + return rdata +} + +func colorizeDNS(req, resp *layers.DNSMessage, id string, nocolor bool) string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("%s ", colorizeTimestamp(time.Now(), nocolor))) + sb.WriteString(id) + if nocolor { + sb.WriteString(fmt.Sprintf(" DNS %s (%s) %#04x ", req.Flags.OPCodeDesc, req.Flags.QRDesc, req.TransactionID)) + for _, rec := range req.Questions { + sb.WriteString(fmt.Sprintf("%s %s ", rec.Type.Name, rec.Name)) + } + for _, rec := range req.AnswerRRs { + sb.WriteString(rec.Summary()) + } + for _, rec := range req.AuthorityRRs { + sb.WriteString(rec.Summary()) + } + for _, rec := range req.AdditionalRRs { + sb.WriteString(rec.Summary()) + } + sb.WriteString("\n") + sb.WriteString(fmt.Sprintf("%s ", colorizeTimestamp(time.Now(), nocolor))) + sb.WriteString(id) + sb.WriteString(fmt.Sprintf(" DNS %s (%s) %#04x ", resp.Flags.OPCodeDesc, resp.Flags.QRDesc, resp.TransactionID)) + for _, rec := range resp.Questions { + sb.WriteString(fmt.Sprintf("%s %s ", rec.Type.Name, rec.Name)) + } + for _, rec := range resp.AnswerRRs { + sb.WriteString(rec.Summary()) + } + for _, rec := range resp.AuthorityRRs { + sb.WriteString(rec.Summary()) + } + for _, rec := range resp.AdditionalRRs { + sb.WriteString(rec.Summary()) + } + } else { + sb.WriteString(colors.Gray(fmt.Sprintf(" DNS %s (%s)", req.Flags.OPCodeDesc, req.Flags.QRDesc)).Bold()) + sb.WriteString(colors.Beige(fmt.Sprintf(" %#04x ", req.TransactionID)).String()) + for _, rec := range req.Questions { + sb.WriteString(fmt.Sprintf("%s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(rec.Name))) + } + for _, rec := range req.AnswerRRs { + sb.WriteString(colorizeRData(rec)) + } + for _, rec := range req.AuthorityRRs { + sb.WriteString(colorizeRData(rec)) + } + for _, rec := range req.AdditionalRRs { + sb.WriteString(colorizeRData(rec)) + } + sb.WriteString("\n") + sb.WriteString(fmt.Sprintf("%s ", colorizeTimestamp(time.Now(), nocolor))) + sb.WriteString(id) + sb.WriteString(colors.Blue(fmt.Sprintf(" DNS %s (%s)", resp.Flags.OPCodeDesc, resp.Flags.QRDesc)).Bold()) + sb.WriteString(colors.Beige(fmt.Sprintf(" %#04x ", resp.TransactionID)).String()) + for _, rec := range resp.Questions { + sb.WriteString(fmt.Sprintf("%s %s ", colors.LightBlue(rec.Type.Name), colors.Gray(rec.Name))) + } + for _, rec := range resp.AnswerRRs { + sb.WriteString(colorizeRData(rec)) + } + for _, rec := range resp.AuthorityRRs { + sb.WriteString(colorizeRData(rec)) + } + for _, rec := range resp.AdditionalRRs { + sb.WriteString(colorizeRData(rec)) + } + } + return sb.String() +} + func highlightPatterns(line string, nocolor bool) (string, bool) { matched := false @@ -377,7 +467,7 @@ func colorizeConnections(srcRemote, srcLocal, dstRemote, dstLocal net.Addr, id s } func colorizeConnectionsTransparent( - srcRemote, srcLocal, dstRemote, dstLocal net.Addr, + srcRemote, srcLocal, dstLocal, dstRemote net.Addr, dst, id string, nocolor bool, diff --git a/gohpts.go b/gohpts.go index 4372c4e..4a4a517 100644 --- a/gohpts.go +++ b/gohpts.go @@ -288,6 +288,9 @@ func New(conf *Config) *proxyapp { p.logger.Fatal().Err(err).Msg("") } } + if network.AddrEqual(p.tproxyAddr, p.tproxyAddrUDP) { + p.logger.Fatal().Msgf("%s: address already in use", p.tproxyAddrUDP) + } p.auto = conf.Auto if p.auto && runtime.GOOS != "linux" { p.logger.Fatal().Msg("Auto setup is available only on linux systems") @@ -690,7 +693,6 @@ func (p *proxyapp) handleForward(w http.ResponseWriter, r *http.Request) { var resp *http.Response var chunked bool var respBodySaved []byte - p.httpClient.Timeout = timeout if network.IsLocalAddress(r.Host) { resp = p.doReq(w, req, nil) } else { @@ -879,7 +881,7 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) { if p.json { sniffdata = append( sniffdata, - fmt.Sprintf("{\"connection\":{\"src_remote\":%s,\"src_local\":%s,\"dst_local\":%s,\"dst_remote\":%s}}", + fmt.Sprintf("{\"connection\":{\"src_remote\":%q,\"src_local\":%q,\"dst_local\":%q,\"dst_remote\":%q}}", srcConn.RemoteAddr(), srcConn.LocalAddr(), dstConn.LocalAddr(), dstConn.RemoteAddr()), ) j, err := json.Marshal(&layers.HTTPMessage{Request: r}) @@ -1205,6 +1207,21 @@ func (p *proxyapp) gatherSniffData(req, resp layers.Layer, sniffdata *[]string, *sniffdata = append(*sniffdata, colorizeTLS(chs, shs, id, p.nocolor)) } } + case *layers.DNSMessage: + rest := resp.(*layers.DNSMessage) + if p.json { + j1, err := json.Marshal(reqt) + if err != nil { + return err + } + j2, err := json.Marshal(rest) + if err != nil { + return err + } + *sniffdata = append(*sniffdata, string(j1), string(j2)) + } else { + *sniffdata = append(*sniffdata, colorizeDNS(reqt, rest, id, p.nocolor)) + } } return nil } @@ -1212,7 +1229,7 @@ func (p *proxyapp) gatherSniffData(req, resp layers.Layer, sniffdata *[]string, func (p *proxyapp) sniffreporter(wg *sync.WaitGroup, sniffdata *[]string, reqChan, respChan <-chan layers.Layer, id string) { defer wg.Done() sniffdatalen := len(*sniffdata) - var reqTLSQueue, respTLSQueue, reqHTTPQueue, respHTTPQueue []layers.Layer + var reqTLSQueue, respTLSQueue, reqHTTPQueue, respHTTPQueue, reqDNSQueue, respDNSQueue []layers.Layer for { select { case req, ok := <-reqChan: @@ -1224,6 +1241,8 @@ func (p *proxyapp) sniffreporter(wg *sync.WaitGroup, sniffdata *[]string, reqCha reqTLSQueue = append(reqTLSQueue, req) case *layers.HTTPMessage: reqHTTPQueue = append(reqHTTPQueue, req) + case *layers.DNSMessage: + reqDNSQueue = append(reqDNSQueue, req) } } case resp, ok := <-respChan: @@ -1245,6 +1264,12 @@ func (p *proxyapp) sniffreporter(wg *sync.WaitGroup, sniffdata *[]string, reqCha } else if len(reqHTTPQueue) == 0 && len(respHTTPQueue) == 1 { respHTTPQueue = respHTTPQueue[1:] } + case *layers.DNSMessage: + if len(reqDNSQueue) > 0 || len(respDNSQueue) == 0 { + respDNSQueue = append(respDNSQueue, resp) + } else if len(reqDNSQueue) == 0 && len(respDNSQueue) == 1 { + respDNSQueue = respDNSQueue[1:] + } } } } @@ -1270,6 +1295,22 @@ func (p *proxyapp) sniffreporter(wg *sync.WaitGroup, sniffdata *[]string, reqCha reqTLSQueue = reqTLSQueue[1:] respTLSQueue = respTLSQueue[1:] + err := p.gatherSniffData(req, resp, sniffdata, id) + if err == nil && len(*sniffdata) > sniffdatalen { + if p.json { + p.snifflogger.Log().Msg(fmt.Sprintf("[%s]", strings.Join(*sniffdata, ","))) + } else { + p.snifflogger.Log().Msg(strings.Join(*sniffdata, "\n")) + } + } + *sniffdata = (*sniffdata)[:sniffdatalen] + } + if len(reqDNSQueue) > 0 && len(respDNSQueue) > 0 { + req := reqDNSQueue[0] + resp := respDNSQueue[0] + reqDNSQueue = reqDNSQueue[1:] + respDNSQueue = respDNSQueue[1:] + err := p.gatherSniffData(req, resp, sniffdata, id) if err == nil && len(*sniffdata) > sniffdatalen { if p.json { diff --git a/tproxy_linux.go b/tproxy_linux.go index 30e26d1..9f7efc6 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -198,7 +198,7 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { sniffheader = append( sniffheader, fmt.Sprintf( - "{\"connection\":{\"tproxy_mode\":%s,\"src_remote\":%s,\"src_local\":%s,\"dst_local\":%s,\"dst_remote\":%s,\"original_dst\":%s}}", + "{\"connection\":{\"tproxy_mode\":%q,\"src_remote\":%q,\"src_local\":%q,\"dst_local\":%q,\"dst_remote\":%q,\"original_dst\":%q}}", ts.p.tproxyMode, srcConn.RemoteAddr(), srcConn.LocalAddr(), @@ -211,8 +211,8 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { connections := colorizeConnectionsTransparent( srcConn.RemoteAddr(), srcConn.LocalAddr(), - dstConn.RemoteAddr(), dstConn.LocalAddr(), + dstConn.RemoteAddr(), dst, id, ts.p.nocolor) sniffheader = append(sniffheader, connections) } diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go index 26b6550..e82f48b 100644 --- a/tproxy_udp_linux.go +++ b/tproxy_udp_linux.go @@ -18,6 +18,7 @@ import ( "time" "unsafe" + "github.com/shadowy-pycoder/mshark/layers" "github.com/shadowy-pycoder/mshark/network" "github.com/wzshiming/socks5" "golang.org/x/sys/unix" @@ -38,6 +39,18 @@ type udpConn struct { dstAddr *net.UDPAddr lastSeen time.Time written atomic.Uint64 + reqChan chan layers.Layer + respChan chan layers.Layer +} + +func (uc *udpConn) SrcPort() *uint16 { + srcPort := uint16(uc.dstAddr.Port) + return &srcPort +} + +func (uc *udpConn) DstPort() *uint16 { + dstPort := uint16(uc.dstAddr.Port) + return &dstPort } func newUDPConn(srcAddr *net.UDPAddr, dstAddr *net.UDPAddr, sockDialer *socks5.Dialer) (*udpConn, error) { @@ -239,7 +252,6 @@ func (tsu *tproxyServerUDP) ListenAndServe() { tsu.clients.Add(conn) go func() { tsu.handleConnection(conn) - tsu.wg.Done() }() } srcConnStr := fmt.Sprintf("%s→ %s", srcAddr, dstAddr) @@ -253,6 +265,38 @@ func (tsu *tproxyServerUDP) ListenAndServe() { tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed setting write deadline", tsu.p.tproxyMode) continue } + if tsu.p.sniff { + if next := layers.ParseNextLayer(buf[:n], conn.SrcPort(), conn.DstPort()); next != nil { + tsu.wg.Add(1) + sniffheader := make([]string, 0, 3) + id := getID(tsu.p.nocolor) + if tsu.p.json { + sniffheader = append( + sniffheader, + fmt.Sprintf( + "{\"connection\":{\"tproxy_mode\":%q,\"src_remote\":%q,\"src_local\":%q,\"dst_local\":%q,\"dst_remote\":%q,\"original_dst\":%s}}", + tsu.p.tproxyMode, + srcAddr, + conn.dstAddr, + tsu.conn.LocalAddr(), + conn.LocalAddr(), + conn.dstAddr, + ), + ) + } else { + connections := colorizeConnectionsTransparent( + srcAddr, + conn.dstAddr, + tsu.conn.LocalAddr(), + conn.LocalAddr(), + conn.dstAddr.String(), + id, tsu.p.nocolor) + sniffheader = append(sniffheader, connections) + } + go tsu.p.sniffreporter(&tsu.wg, &sniffheader, conn.reqChan, conn.respChan, id) + conn.reqChan <- next + } + } nw, err := conn.WriteToUDP(buf[:n], dstAddr) if err != nil { if ne, ok := err.(net.Error); ok && ne.Timeout() { @@ -291,6 +335,7 @@ func (tsu *tproxyServerUDP) handleConnection(conn *udpConn) { srcConnStr := fmt.Sprintf("%s→ %s", conn.srcAddr, conn.dstAddr) dstConnStr := fmt.Sprintf("%s→ %s→ %s", tsu.conn.LocalAddr(), conn.LocalAddr(), conn.dstAddr) tsu.p.logger.Debug().Msgf("Copied %s for udp src: %s - dst: %s", prettifyBytes(int64(conn.written.Load())), srcConnStr, dstConnStr) + tsu.wg.Done() }() readLoop: for { @@ -313,6 +358,11 @@ readLoop: tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, tsu.conn.LocalAddr(), conn.srcAddr) break readLoop } + if tsu.p.sniff { + if next := layers.ParseNextLayer(buf[:nr], conn.SrcPort(), conn.DstPort()); next != nil { + conn.respChan <- next + } + } nw, ew := tsu.conn.WriteToUDP(buf[0:nr], conn.srcAddr) if nw < 0 || nr < nw { nw = 0 @@ -354,9 +404,17 @@ readLoop: type dnsConn struct { *net.UDPConn - srcAddr *net.UDPAddr - dstAddr *net.UDPAddr - written atomic.Uint64 + srcAddr *net.UDPAddr + dstAddr *net.UDPAddr + written atomic.Uint64 + reqChan chan layers.Layer + respChan chan layers.Layer +} + +func (dc *dnsConn) close() error { + close(dc.reqChan) + close(dc.respChan) + return dc.Close() } func newDNSConn(srcAddr *net.UDPAddr, mark uint) (*dnsConn, error) { @@ -371,7 +429,13 @@ func newDNSConn(srcAddr *net.UDPAddr, mark uint) (*dnsConn, error) { if !ok { return nil, fmt.Errorf("failed obtaining dns connection") } - return &dnsConn{UDPConn: udpConn, srcAddr: srcAddr, dstAddr: googleDNSAddr}, nil + return &dnsConn{ + UDPConn: udpConn, + srcAddr: srcAddr, + dstAddr: googleDNSAddr, + reqChan: make(chan layers.Layer), + respChan: make(chan layers.Layer), + }, nil } func (tsu *tproxyServerUDP) listenAndServeDNS() { @@ -398,7 +462,7 @@ func (tsu *tproxyServerUDP) listenAndServeDNS() { continue } srcConnStr := fmt.Sprintf("%s→ %s", srcAddr, tsu.gwConn.LocalAddr()) - dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), conn.dstAddr) + dstConnStr := fmt.Sprintf("%s→ %s", conn.LocalAddr(), conn.dstAddr) tsu.p.logger.Debug().Msgf("[udp %s] src: %s - dst: %s", tsu.p.tproxyMode, srcConnStr, dstConnStr) err = conn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) if err != nil { @@ -408,6 +472,41 @@ func (tsu *tproxyServerUDP) listenAndServeDNS() { tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed setting write deadline", tsu.p.tproxyMode) continue } + if tsu.p.sniff { + dns := &layers.DNSMessage{} + if err := dns.Parse(buf[:n]); err == nil { + tsu.wg.Add(1) + sniffheader := make([]string, 0, 3) + id := getID(tsu.p.nocolor) + if tsu.p.json { + sniffheader = append( + sniffheader, + fmt.Sprintf( + "{\"connection\":{\"tproxy_mode\":%q,\"src_remote\":%q,\"src_local\":%q,\"dst_local\":%q,\"dst_remote\":%q,\"original_dst\":%q}}", + tsu.p.tproxyMode, + srcAddr, + tsu.gwConn.LocalAddr(), + conn.LocalAddr(), + conn.dstAddr, + tsu.gwConn.LocalAddr(), + ), + ) + } else { + connections := colorizeConnectionsTransparent( + srcAddr, + tsu.gwConn.LocalAddr(), + conn.LocalAddr(), + conn.dstAddr, + tsu.gwConn.LocalAddr().String(), + id, tsu.p.nocolor) + sniffheader = append(sniffheader, connections) + } + go tsu.p.sniffreporter(&tsu.wg, &sniffheader, conn.reqChan, conn.respChan, id) + conn.reqChan <- dns + } else { + tsu.p.logger.Error().Err(err).Msgf("%v", buf[:n]) + } + } nw, err := conn.Write(buf[:n]) if err != nil { if ne, ok := err.(net.Error); ok && ne.Timeout() { @@ -420,10 +519,7 @@ func (tsu *tproxyServerUDP) listenAndServeDNS() { continue } conn.written.Add(uint64(nw)) - go func() { - tsu.handleDNSConnection(conn) - tsu.wg.Done() - }() + go tsu.handleDNSConnection(conn) } if er != nil { if ne, ok := er.(net.Error); ok && ne.Timeout() { @@ -446,9 +542,10 @@ func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { tsu.wg.Add(1) defer func() { srcConnStr := fmt.Sprintf("%s→ %s", conn.srcAddr, tsu.gwConn.LocalAddr()) - dstConnStr := fmt.Sprintf("%s→ %s→ %s", conn.LocalAddr(), tsu.gwConn.LocalAddr(), conn.dstAddr) + dstConnStr := fmt.Sprintf("%s→ %s", conn.LocalAddr(), conn.dstAddr) tsu.p.logger.Debug().Msgf("Copied %s for udp src: %s - dst: %s", prettifyBytes(int64(conn.written.Load())), srcConnStr, dstConnStr) - conn.Close() + conn.close() + tsu.wg.Done() }() buf := make([]byte, udpBufferSize) er := conn.SetReadDeadline(time.Now().Add(readTimeoutUDP)) @@ -469,6 +566,14 @@ func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { tsu.p.logger.Debug().Err(er).Msgf("[udp %s] Failed setting write deadline %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.srcAddr) return } + if tsu.p.sniff { + dns := &layers.DNSMessage{} + if err := dns.Parse(buf[:nr]); err == nil { + conn.respChan <- dns + } else { + tsu.p.logger.Error().Err(err).Msgf("%v", buf[:nr]) + } + } nw, ew := tsu.gwConn.WriteToUDP(buf[0:nr], conn.srcAddr) if nw < 0 || nr < nw { nw = 0 From 25e227c8ad32fe4b6b8ab693a1891ab89299a0c2 Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:01:16 +0300 Subject: [PATCH 08/10] udp proxy now dials router directly for DNS requests in arpspoof mode --- go.mod | 3 +- go.sum | 8 ++++-- tproxy_udp_linux.go | 68 +++++++++++++++++++++++---------------------- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 5e55972..3e9c370 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/rs/zerolog v1.34.0 github.com/shadowy-pycoder/colors v0.0.1 - github.com/shadowy-pycoder/mshark v0.0.12 + github.com/shadowy-pycoder/mshark v0.0.13 github.com/wzshiming/socks5 v0.5.2 golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 @@ -20,6 +20,7 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/mdlayher/packet v1.1.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect + github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 // indirect github.com/pkg/errors v0.9.1 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sync v0.16.0 // indirect diff --git a/go.sum b/go.sum index bfb16fe..ce397ac 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A= +github.com/gopacket/gopacket v1.2.0/go.mod h1:BrAKEy5EOGQ76LSqh7DMAr7z0NNPdczWm2GxCG7+I8M= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/malfunkt/iprange v0.9.0 h1:VCs0PKLUPotNVQTpVNszsut4lP7OCGNBwX+lOYBrnVQ= @@ -21,6 +23,8 @@ github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 h1:p4VuaitqUAqSZSomd7Wb4BPV/Jj7Hno2/iqtfX7DZJI= +github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5/go.mod h1:zIAoVKeWP0mz4zXY50UYQt6NLg2uwKRswMDcGEqOms4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -30,8 +34,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/shadowy-pycoder/colors v0.0.1 h1:weCj/YIOupqy4BSP8KuVzr20fC+cuAv/tArz7bhhkP4= github.com/shadowy-pycoder/colors v0.0.1/go.mod h1:lkrJS1PY2oVigNLTT6pkbF7B/v0YcU2LD5PZnss1Q4U= -github.com/shadowy-pycoder/mshark v0.0.12 h1:1zPAQLhKu1pxAZTm5oqUahywJ7SIpx9ds9/ytz5TwaU= -github.com/shadowy-pycoder/mshark v0.0.12/go.mod h1:FqbHFdsx0zMnrZZH0+oPzaFcleP4O+tUWv8i5gxo87k= +github.com/shadowy-pycoder/mshark v0.0.13 h1:ROEuey/Th4YAmfRg8Xc17aboMs5fknQho4mNBC9h+KE= +github.com/shadowy-pycoder/mshark v0.0.13/go.mod h1:FqbHFdsx0zMnrZZH0+oPzaFcleP4O+tUWv8i5gxo87k= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/wzshiming/socks5 v0.5.2 h1:LtoowVNwAmkIQSkP1r1Wg435xUmC+tfRxorNW30KtnM= diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go index e82f48b..c934ec1 100644 --- a/tproxy_udp_linux.go +++ b/tproxy_udp_linux.go @@ -31,8 +31,6 @@ const ( udpBufferSize int = 4096 ) -var googleDNSAddr *net.UDPAddr = &net.UDPAddr{IP: net.ParseIP("8.8.8.8"), Port: 53} - type udpConn struct { *socks5.UDPConn srcAddr *net.UDPAddr @@ -179,29 +177,31 @@ func newTproxyServerUDP(p *proxyapp) *tproxyServerUDP { tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] Failed getting default interface", tsu.p.tproxyMode) } } - gw, err := network.GetGatewayIPv4FromInterface(tsu.iface.Name) - if err != nil { - tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] failed getting gateway from %s", tsu.p.tproxyMode, tsu.iface.Name) - } - tsu.gwDNS = &net.UDPAddr{IP: net.ParseIP(gw.String()), Port: 53} - lc = net.ListenConfig{ - Control: func(network, address string, conn syscall.RawConn) error { - var operr error - if err := conn.Control(func(fd uintptr) { - operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1) - operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_FREEBIND, 1) - operr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) - }); err != nil { - return err - } - return operr - }, - } - pconn, err = lc.ListenPacket(context.Background(), "udp4", tsu.gwDNS.String()) - if err != nil { - tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] failed listening on gateway DNS", tsu.p.tproxyMode) + if tsu.p.arpspoofer != nil { + gw, err := network.GetGatewayIPv4FromInterface(tsu.iface.Name) + if err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] failed getting gateway from %s", tsu.p.tproxyMode, tsu.iface.Name) + } + tsu.gwDNS = &net.UDPAddr{IP: net.ParseIP(gw.String()), Port: 53} + lc = net.ListenConfig{ + Control: func(network, address string, conn syscall.RawConn) error { + var operr error + if err := conn.Control(func(fd uintptr) { + operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1) + operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_FREEBIND, 1) + operr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + }); err != nil { + return err + } + return operr + }, + } + pconn, err = lc.ListenPacket(context.Background(), "udp4", tsu.gwDNS.String()) + if err != nil { + tsu.p.logger.Fatal().Err(err).Msgf("[udp %s] failed listening on gateway DNS", tsu.p.tproxyMode) + } + tsu.gwConn = pconn.(*net.UDPConn) } - tsu.gwConn = pconn.(*net.UDPConn) return tsu } @@ -209,10 +209,12 @@ func (tsu *tproxyServerUDP) ListenAndServe() { tsu.startingFlag.Store(true) tsu.wg.Add(1) go tsu.clients.Cleanup() - go func() { - tsu.listenAndServeDNS() - tsu.wg.Done() - }() + if tsu.p.arpspoofer != nil { + go func() { + tsu.listenAndServeDNS() + tsu.wg.Done() + }() + } buf := make([]byte, udpBufferSize) oob := make([]byte, 1500) tsu.startingFlag.Store(false) @@ -417,11 +419,11 @@ func (dc *dnsConn) close() error { return dc.Close() } -func newDNSConn(srcAddr *net.UDPAddr, mark uint) (*dnsConn, error) { +func newDNSConn(srcAddr, dstAddr *net.UDPAddr, mark uint) (*dnsConn, error) { dialer := getBaseDialer(timeout, mark) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - conn, err := dialer.DialContext(ctx, "udp4", googleDNSAddr.String()) + conn, err := dialer.DialContext(ctx, "udp4", dstAddr.String()) if err != nil { return nil, err } @@ -432,7 +434,7 @@ func newDNSConn(srcAddr *net.UDPAddr, mark uint) (*dnsConn, error) { return &dnsConn{ UDPConn: udpConn, srcAddr: srcAddr, - dstAddr: googleDNSAddr, + dstAddr: dstAddr, reqChan: make(chan layers.Layer), respChan: make(chan layers.Layer), }, nil @@ -456,9 +458,9 @@ func (tsu *tproxyServerUDP) listenAndServeDNS() { } n, srcAddr, er := tsu.gwConn.ReadFromUDP(buf) if n > 0 { - conn, err := newDNSConn(srcAddr, tsu.p.mark) + conn, err := newDNSConn(srcAddr, tsu.gwDNS, tsu.p.mark) if err != nil { - tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed creating UDP connection %s→ %s", tsu.p.tproxyMode, srcAddr, googleDNSAddr) + tsu.p.logger.Error().Err(err).Msgf("[udp %s] Failed creating UDP connection %s→ %s", tsu.p.tproxyMode, srcAddr, tsu.gwDNS) continue } srcConnStr := fmt.Sprintf("%s→ %s", srcAddr, tsu.gwConn.LocalAddr()) From 4616f8fbb7dc3f233e786aaf60e39f974c441bcd Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:31:46 +0300 Subject: [PATCH 09/10] fixed error handling for dns connections --- tproxy_udp_linux.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tproxy_udp_linux.go b/tproxy_udp_linux.go index c934ec1..0759943 100644 --- a/tproxy_udp_linux.go +++ b/tproxy_udp_linux.go @@ -62,7 +62,14 @@ func newUDPConn(srcAddr *net.UDPAddr, dstAddr *net.UDPAddr, sockDialer *socks5.D if !ok { return nil, fmt.Errorf("failed obtaining relay connection") } - return &udpConn{UDPConn: relayConn, srcAddr: srcAddr, dstAddr: dstAddr, lastSeen: time.Now()}, nil + return &udpConn{ + UDPConn: relayConn, + srcAddr: srcAddr, + dstAddr: dstAddr, + lastSeen: time.Now(), + reqChan: make(chan layers.Layer), + respChan: make(chan layers.Layer), + }, nil } type udpConnections struct { @@ -585,7 +592,12 @@ func (tsu *tproxyServerUDP) handleDNSConnection(conn *dnsConn) { } conn.written.Add(uint64(nw)) if ew != nil { - return + if errors.Is(ew, net.ErrClosed) { + return + } + if ne, ok := ew.(net.Error); ok && ne.Timeout() { + return + } } if nr != nw { tsu.p.logger.Debug(). From c134773e56e2c0bd3bb5156163e682773710bdb8 Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:49:27 +0300 Subject: [PATCH 10/10] fixed vesrion --- README.md | 2 +- version.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a22a59b..a901a46 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ You can download the binary for your platform from [Releases](https://github.com Example: ```shell -GOHPTS_RELEASE=v2.0.0; wget -v https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases/download/$GOHPTS_RELEASE/gohpts-$GOHPTS_RELEASE-linux-amd64.tar.gz -O gohpts && tar xvzf gohpts && mv -f gohpts-$GOHPTS_RELEASE-linux-amd64 gohpts && ./gohpts -h +GOHPTS_RELEASE=v1.10.0; wget -v https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases/download/$GOHPTS_RELEASE/gohpts-$GOHPTS_RELEASE-linux-amd64.tar.gz -O gohpts && tar xvzf gohpts && mv -f gohpts-$GOHPTS_RELEASE-linux-amd64 gohpts && ./gohpts -h ``` Alternatively, you can install it using `go install` command (requires Go [1.24](https://go.dev/doc/install) or later): diff --git a/version.go b/version.go index b798dc5..63bc2ac 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package gohpts -const Version string = "gohpts v2.0.0" +const Version string = "gohpts v1.10.0"