diff --git a/README.md b/README.md index ee6e38c..a901a46 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=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): @@ -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 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 +``` + +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/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/colorize.go b/colorize.go index 50d4247..bedea10 100644 --- a/colorize.go +++ b/colorize.go @@ -18,10 +18,10 @@ 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`, + `\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/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..3e9c370 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ 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 - golang.org/x/net v0.40.0 + 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,8 @@ 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 09a471b..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,10 +34,12 @@ 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.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= +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..4a4a517 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" @@ -31,7 +33,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 ( @@ -65,6 +67,7 @@ type Config struct { ServerConfPath string TProxy string TProxyOnly string + TProxyUDP string TProxyMode string Auto bool Mark uint @@ -127,7 +130,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 @@ -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,12 +273,24 @@ 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 { 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") @@ -390,11 +407,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 +419,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 @@ -484,6 +501,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 +516,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 +555,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 +593,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 +620,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 } @@ -619,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 { @@ -749,7 +822,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) @@ -764,7 +839,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) @@ -806,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}) @@ -849,18 +924,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 +942,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 +964,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 +976,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 +1003,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 +1058,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 +1077,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 @@ -1129,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 } @@ -1136,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: @@ -1148,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: @@ -1169,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:] + } } } } @@ -1194,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 { @@ -1314,3 +1431,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 6fbcaae..4e1eaf5 100644 --- a/helpers.go +++ b/helpers.go @@ -1,15 +1,20 @@ package gohpts import ( + "context" "encoding/base64" + "errors" "fmt" "net" "net/http" + "net/netip" "os" + "os/exec" "strconv" "strings" "github.com/shadowy-pycoder/mshark/network" + "github.com/wzshiming/socks5" ) // Hop-by-hop headers @@ -129,3 +134,73 @@ 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 +} + +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 17ee869..9f7efc6 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -7,29 +7,27 @@ import ( "context" "errors" "fmt" - "maps" "net" "net/netip" "os" "os/exec" - "slices" - "strings" "sync" + "sync/atomic" "syscall" "time" "unsafe" "github.com/shadowy-pycoder/mshark/layers" "github.com/shadowy-pycoder/mshark/network" - "golang.org/x/net/proxy" "golang.org/x/sys/unix" ) 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 { @@ -67,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() { @@ -119,11 +119,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) @@ -142,38 +142,38 @@ 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) case "tproxy": dst = srcConn.LocalAddr().String() - ts.p.logger.Debug().Msgf("[%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.(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) + ts.p.logger.Error().Err(err).Msgf("[tcp %s] Failed connecting to %s", ts.p.tproxyMode, dst) return } } @@ -182,7 +182,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) @@ -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) } @@ -222,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{}) @@ -232,59 +235,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 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 - `, 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(` @@ -297,20 +261,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) @@ -321,7 +285,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 { @@ -332,7 +296,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(` @@ -342,7 +306,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 { @@ -354,7 +318,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 @@ -367,7 +331,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 @@ -382,59 +346,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 @@ -447,12 +399,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 @@ -463,77 +414,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": @@ -551,13 +442,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_nonlinux.go b/tproxy_nonlinux.go index 32e08d3..b203469 100644 --- a/tproxy_nonlinux.go +++ b/tproxy_nonlinux.go @@ -5,10 +5,7 @@ package gohpts import ( "net" - "os/exec" "sync" - "syscall" - "time" ) type tproxyServer struct { @@ -24,44 +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 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 - _ = 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 new file mode 100644 index 0000000..0759943 --- /dev/null +++ b/tproxy_udp_linux.go @@ -0,0 +1,752 @@ +//go:build linux +// +build linux + +package gohpts + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/netip" + "os" + "os/exec" + "sync" + "sync/atomic" + "syscall" + "time" + "unsafe" + + "github.com/shadowy-pycoder/mshark/layers" + "github.com/shadowy-pycoder/mshark/network" + "github.com/wzshiming/socks5" + "golang.org/x/sys/unix" +) + +const ( + readTimeoutUDP time.Duration = 5 * time.Second + writeTimeoutUDP time.Duration = 5 * time.Second + idleTimeoutUDP time.Duration = 30 * time.Second + udpBufferSize int = 4096 +) + +type udpConn struct { + *socks5.UDPConn + srcAddr *net.UDPAddr + 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) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + conn, err := sockDialer.DialContext(ctx, "udp4", 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, + srcAddr: srcAddr, + dstAddr: dstAddr, + lastSeen: time.Now(), + reqChan: make(chan layers.Layer), + respChan: make(chan layers.Layer), + }, 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.srcAddr, conn.dstAddr)] = conn + ucs.Unlock() +} + +func (ucs *udpConnections) Get(srcAddr, dstAddr *net.UDPAddr) (*udpConn, bool) { + ucs.RLock() + defer ucs.RUnlock() + 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.srcAddr, 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(idleTimeoutUDP) + 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) > idleTimeoutUDP { + 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 + startingFlag atomic.Bool +} + +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) + } + } + 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) + } + return tsu +} + +func (tsu *tproxyServerUDP) ListenAndServe() { + tsu.startingFlag.Store(true) + tsu.wg.Add(1) + go tsu.clients.Cleanup() + if tsu.p.arpspoofer != nil { + 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: + err := tsu.conn.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, 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) + }() + } + 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 { + if errors.Is(err, net.ErrClosed) { + continue + } + 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() { + 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 + } + conn.written.Add(uint64(nw)) + tsu.clients.UpdateLastSeen(conn) + } + 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) handleConnection(conn *udpConn) { + tsu.wg.Add(1) + buf := make([]byte, udpBufferSize) + defer func() { + 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 { + select { + case <-tsu.quit: + return + default: + 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.LocalAddr(), tsu.conn.LocalAddr()) + break readLoop + } + nr, er := conn.Read(buf) + 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.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 + if ew == nil { + ew = errInvalidWrite + } + } + conn.written.Add(uint64(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.srcAddr) + break readLoop + } + } + if er != nil { + if ne, ok := er.(net.Error); ok && ne.Timeout() { + break readLoop + } + if errors.Is(er, net.ErrClosed) { + return + } + if errors.Is(er, io.EOF) { + break readLoop + } + break readLoop + } + } + } + conn.Close() + tsu.clients.Remove(conn) +} + +type dnsConn struct { + *net.UDPConn + 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, 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", dstAddr.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, + srcAddr: srcAddr, + dstAddr: dstAddr, + reqChan: make(chan layers.Layer), + respChan: make(chan layers.Layer), + }, 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.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, tsu.gwDNS) + continue + } + srcConnStr := fmt.Sprintf("%s→ %s", srcAddr, tsu.gwConn.LocalAddr()) + 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 { + if errors.Is(err, net.ErrClosed) { + continue + } + 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() { + 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(), conn.dstAddr) + continue + } + conn.written.Add(uint64(nw)) + go tsu.handleDNSConnection(conn) + } + 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 func() { + srcConnStr := fmt.Sprintf("%s→ %s", conn.srcAddr, tsu.gwConn.LocalAddr()) + 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() + tsu.wg.Done() + }() + buf := make([]byte, udpBufferSize) + 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.LocalAddr()) + return + } + nr, er := conn.Read(buf) + if nr > 0 { + er := tsu.gwConn.SetWriteDeadline(time.Now().Add(writeTimeoutUDP)) + if er != nil { + 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.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 + if ew == nil { + ew = errInvalidWrite + } + } + conn.written.Add(uint64(nw)) + if ew != nil { + if errors.Is(ew, net.ErrClosed) { + return + } + if ne, ok := ew.(net.Error); ok && ne.Timeout() { + return + } + } + if nr != nw { + tsu.p.logger.Debug(). + Err(io.ErrShortWrite). + Msgf("[udp %s] Failed sending message %s→ %s", tsu.p.tproxyMode, conn.LocalAddr(), conn.srcAddr) + return + } + } + if er != nil { + return + } +} + +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) + 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 + `, setex)) + cmdClear.Stdout = os.Stdout + cmdClear.Stderr = os.Stderr + if err := cmdClear.Run(); err != nil { + 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("[udp %s] Failed getting host from %s", tsu.p.tproxyMode, 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 +} 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..63bc2ac 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package gohpts -const Version string = "gohpts v1.9.4" +const Version string = "gohpts v1.10.0"