diff --git a/README.md b/README.md index 62146b0..8088f2d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![GitHub Release](https://img.shields.io/github/v/release/shadowy-pycoder/go-http-proxy-to-socks) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/shadowy-pycoder/go-http-proxy-to-socks/total) -

MrGopher +

MrGopher ## Table of contents @@ -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) + - [ARP spoofing](#arp-spoofing) - [Traffic sniffing](#traffic-sniffing) - [JSON format](#json-format) - [Colored format](#colored-format) @@ -97,7 +98,7 @@ You can download the binary for your platform from [Releases](https://github.com Example: ```shell -HPTS_RELEASE=v1.8.3; wget -v https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases/download/$HPTS_RELEASE/gohpts-$HPTS_RELEASE-linux-amd64.tar.gz -O gohpts && tar xvzf gohpts && mv -f gohpts-$HPTS_RELEASE-linux-amd64 gohpts && ./gohpts -h +HPTS_RELEASE=v1.8.4; wget -v https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases/download/$HPTS_RELEASE/gohpts-$HPTS_RELEASE-linux-amd64.tar.gz -O gohpts && tar xvzf gohpts && mv -f gohpts-$HPTS_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): @@ -164,6 +165,7 @@ Options: -T Address of transparent proxy server (no HTTP) -M Transparent proxy mode: (redirect, tproxy) -auto Automatically setup iptables for transparent proxy (requires elevated privileges) + -arp Automatically setup iptables to proxy ARP spoofed traffic (use tools like bettercap to perform actual attack) -mark Set mark for each packet sent through transparent proxy (Default: redirect 0, tproxy 100) ``` @@ -479,6 +481,25 @@ else fi ``` +### ARP spoofing + +`GoHPTS` can be used with tools like [Bettercap](https://github.com/bettercap/bettercap) to proxy ARP spoofed traffic. + +Run the proxy with `-arp` flag + +```shell +ssh remote -D 1080 -Nf +sudo env PATH=$PATH gohpts -d -T 8888 -M tproxy -sniff -body -auto -mark 100 -arp +``` + +Run `bettercap` + +```shell +sudo bettercap -eval "set net.probe on;set net.recon on;arp.spoof on" +``` + +Check proxy logs for traffic from other devices from your LAN + ## Traffic sniffing [[Back]](#table-of-contents) diff --git a/cmd/gohpts/cli.go b/cmd/gohpts/cli.go index e969a84..444e7ae 100644 --- a/cmd/gohpts/cli.go +++ b/cmd/gohpts/cli.go @@ -17,31 +17,32 @@ const ( addrHTTP string = "127.0.0.1:8080" tproxyOS string = "linux" ) -const usagePrefix string = ` _____ _ _ _____ _______ _____ + +const usagePrefix string = ` _____ _ _ _____ _______ _____ / ____| | | | | __ \__ __/ ____| - | | __ ___ | |__| | |__) | | | | (___ - | | |_ |/ _ \| __ | ___/ | | \___ \ + | | __ ___ | |__| | |__) | | | | (___ + | | |_ |/ _ \| __ | ___/ | | \___ \ | |__| | (_) | | | | | | | ____) | - \_____|\___/|_| |_|_| |_| |_____/ - -GoHPTS (HTTP(S) Proxy to SOCKS5 proxy) by shadowy-pycoder + \_____|\___/|_| |_|_| |_| |_____/ + +GoHPTS (HTTP(S) Proxy to SOCKS5 proxy) by shadowy-pycoder GitHub: https://github.com/shadowy-pycoder/go-http-proxy-to-socks -Usage: gohpts [OPTIONS] +Usage: gohpts [OPTIONS] Options: -h Show this help message and exit -v Show version and build information -D Run as a daemon (provide -logfile to see logs) Proxy: - -l Address of HTTP proxy server (default "127.0.0.1:8080") + -l Address of HTTP proxy server (default "127.0.0.1:8080") -s Address of SOCKS5 proxy server (default "127.0.0.1:1080") -c Path to certificate PEM encoded file -k Path to private key PEM encoded file -U User for HTTP proxy (basic auth). This flag invokes prompt for password (not echoed to terminal) -u User for SOCKS5 proxy authentication. This flag invokes prompt for password (not echoed to terminal) -f Path to server configuration file in YAML format (overrides other proxy flags) - + Logs: -d Show logs in DEBUG mode -j Show logs in JSON format @@ -53,12 +54,14 @@ Options: -snifflog Sniffed traffic log file path (Default: the same as -logfile) -body Collect request and response body for HTTP traffic (credentials, tokens, etc) ` + 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) -M Transparent proxy mode: (redirect, tproxy) -auto Automatically setup iptables for transparent proxy (requires elevated privileges) + -arp Automatically setup iptables to proxy ARP spoofed traffic (use tools like bettercap to perform actual attack) -mark Set mark for each packet sent through transparent proxy (Default: redirect 0, tproxy 100) ` @@ -66,12 +69,27 @@ func root(args []string) error { conf := gohpts.Config{} flags := flag.NewFlagSet(app, flag.ExitOnError) flags.StringVar(&conf.AddrSOCKS, "s", addrSOCKS, "Address of SOCKS5 proxy server") - flags.StringVar(&conf.User, "u", "", "User for SOCKS5 proxy authentication. This flag invokes prompt for password (not echoed to terminal)") + flags.StringVar( + &conf.User, + "u", + "", + "User for SOCKS5 proxy authentication. This flag invokes prompt for password (not echoed to terminal)", + ) flags.StringVar(&conf.AddrHTTP, "l", addrHTTP, "Address of HTTP proxy server") - flags.StringVar(&conf.ServerUser, "U", "", "User for HTTP proxy (basic auth). This flag invokes prompt for password (not echoed to terminal)") + flags.StringVar( + &conf.ServerUser, + "U", + "", + "User for HTTP proxy (basic auth). This flag invokes prompt for password (not echoed to terminal)", + ) flags.StringVar(&conf.CertFile, "c", "", "Path to certificate PEM encoded file") flags.StringVar(&conf.KeyFile, "k", "", "Path to private key PEM encoded file") - flags.StringVar(&conf.ServerConfPath, "f", "", "Path to server configuration file in YAML format (overrides other proxy flags)") + flags.StringVar( + &conf.ServerConfPath, + "f", + "", + "Path to server configuration file in YAML format (overrides other proxy flags)", + ) daemon := flags.Bool("D", false, "Run as a daemon (provide -logfile to see logs)") if runtime.GOOS == tproxyOS { flags.StringVar(&conf.TProxy, "t", "", "Address of transparent proxy server (it starts along with HTTP proxy server)") @@ -84,12 +102,28 @@ func root(args []string) error { conf.TProxyMode = flagValue return nil }) - flags.BoolVar(&conf.Auto, "auto", false, "Automatically setup iptables for transparent proxy (requires elevated privileges)") - flags.UintVar(&conf.Mark, "mark", 0, "Set mark for each packet sent through transparent proxy (Default: redirect 0, tproxy 100)") + flags.BoolVar( + &conf.Auto, + "auto", + false, + "Automatically setup iptables for transparent proxy (requires elevated privileges)", + ) + flags.UintVar( + &conf.Mark, + "mark", + 0, + "Set mark for each packet sent through transparent proxy (Default: redirect 0, tproxy 100)", + ) + flags.BoolVar( + &conf.ARP, + "arp", + false, + "Automatically setup iptables to proxy ARP spoofed traffic (use tools like bettercap to perform actual attack)", + ) } flags.StringVar(&conf.LogFilePath, "logfile", "", "Log file path (Default: stdout)") flags.BoolVar(&conf.Debug, "d", false, "Show logs in DEBUG mode") - flags.BoolVar(&conf.Json, "j", false, "Show logs in JSON format") + flags.BoolVar(&conf.JSON, "j", false, "Show logs in JSON format") flags.BoolVar(&conf.Sniff, "sniff", false, "Enable traffic sniffing for HTTP and TLS") flags.StringVar(&conf.SniffLogFile, "snifflog", "", "Sniffed traffic log file path (Default: the same as -logfile)") flags.BoolVar(&conf.NoColor, "nocolor", false, "Disable colored output for logs (no effect if -j flag specified)") @@ -145,6 +179,11 @@ func root(args []string) error { return fmt.Errorf("-mark requires -t or -T flag") } } + if seen["arp"] { + if !seen["auto"] { + return fmt.Errorf("-arp requires -auto flag") + } + } if seen["f"] { for _, da := range []string{"s", "u", "U", "c", "k", "l"} { if seen[da] { diff --git a/go.mod b/go.mod index c779676..cdbcd2a 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.4 + github.com/shadowy-pycoder/mshark v0.0.5 golang.org/x/net v0.40.0 golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 diff --git a/go.sum b/go.sum index 6694189..992a941 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,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.4 h1:2yw6am1jt6n1GPHdLfFU1oDajv+zQ/23V0l0imFAeJY= -github.com/shadowy-pycoder/mshark v0.0.4/go.mod h1:fRWGQuU4BFjz9pTfrvwIT2AtmWWd99PEvdlgv+24vTE= +github.com/shadowy-pycoder/mshark v0.0.5 h1:D7L+vW6DsE/OMwxThQLenNJdHKHzufHFWGuL033GKhQ= +github.com/shadowy-pycoder/mshark v0.0.5/go.mod h1:fRWGQuU4BFjz9pTfrvwIT2AtmWWd99PEvdlgv+24vTE= 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= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= diff --git a/gohpts.go b/gohpts.go index b94da93..1a58680 100644 --- a/gohpts.go +++ b/gohpts.go @@ -1,3 +1,4 @@ +// Package gohpts transform SOCKS5 proxy into HTTP(S) proxy with support for Transparent Proxy (Redirect and TProxy), Proxychains and Traffic Sniffing package gohpts import ( @@ -52,11 +53,19 @@ var ( supportedChainTypes = []string{"strict", "dynamic", "random", "round_robin"} SupportedTProxyModes = []string{"redirect", "tproxy"} errInvalidWrite = errors.New("invalid write result") - 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`) - 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`) - jwtPattern = regexp.MustCompile(`\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b`) - authPattern = regexp.MustCompile(`(?i)(?:"|')?(authorization|auth[_-]?token|access[_-]?token|api[_-]?key|secret|token)(?:"|')?\s*[:=]\s*(?:"|')?([^\s"'&]+)`) - credsPattern = regexp.MustCompile(`(?i)(?:"|')?(username|user|login|email|password|pass|pwd)(?:"|')?\s*[:=]\s*(?:"|')?([^\s"'&]+)`) + 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`, + ) + 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`, + ) + jwtPattern = regexp.MustCompile(`\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b`) + authPattern = regexp.MustCompile( + `(?i)(?:"|')?(authorization|auth[_-]?token|access[_-]?token|api[_-]?key|secret|token)(?:"|')?\s*[:=]\s*(?:"|')?([^\s"'&]+)`, + ) + credsPattern = regexp.MustCompile( + `(?i)(?:"|')?(username|user|login|email|password|pass|pwd)(?:"|')?\s*[:=]\s*(?:"|')?([^\s"'&]+)`, + ) ) // Hop-by-hop headers @@ -88,9 +97,10 @@ type Config struct { TProxyMode string Auto bool Mark uint + ARP bool LogFilePath string Debug bool - Json bool + JSON bool Sniff bool SniffLogFile string NoColor bool @@ -111,6 +121,7 @@ type proxyapp struct { tproxyMode string auto bool mark uint + arp bool user string pass string proxychain chain @@ -152,12 +163,12 @@ func randColor() func(string) *colors.Color { return rColors[randIndex] } -func (p *proxyapp) getId() string { +func (p *proxyapp) getID() string { id := uuid.New() if p.nocolor { - return fmt.Sprintf("%s", colors.WrapBrackets(id.String())) + return colors.WrapBrackets(id.String()) } - return randColor()(fmt.Sprintf("%s", colors.WrapBrackets(id.String()))).String() + return randColor()(colors.WrapBrackets(id.String())).String() } func (p *proxyapp) colorizeStatus(code int, status string, bg bool) string { @@ -181,7 +192,13 @@ func (p *proxyapp) colorizeStatus(code int, status string, bg bool) string { return status } -func (p *proxyapp) colorizeHTTP(req *http.Request, resp *http.Response, reqBodySaved, respBodySaved *[]byte, id string, ts bool) string { +func (p *proxyapp) colorizeHTTP( + req *http.Request, + resp *http.Response, + reqBodySaved, respBodySaved *[]byte, + id string, + ts bool, +) string { var sb strings.Builder if ts { sb.WriteString(fmt.Sprintf("%s ", p.colorizeTimestamp())) @@ -190,7 +207,7 @@ func (p *proxyapp) colorizeHTTP(req *http.Request, resp *http.Response, reqBodyS sb.WriteString(id) sb.WriteString(fmt.Sprintf(" %s %s %s ", req.Method, req.URL, req.Proto)) if req.UserAgent() != "" { - sb.WriteString(fmt.Sprintf("%s", colors.WrapBrackets(req.UserAgent()))) + sb.WriteString(colors.WrapBrackets(req.UserAgent())) } if req.ContentLength > 0 { sb.WriteString(fmt.Sprintf(" Len: %d", req.ContentLength)) @@ -224,7 +241,7 @@ func (p *proxyapp) colorizeHTTP(req *http.Request, resp *http.Response, reqBodyS sb.WriteString(colors.YellowBg(fmt.Sprintf("%s ", req.URL)).String()) sb.WriteString(colors.BlueBg(fmt.Sprintf("%s ", req.Proto)).String()) if req.UserAgent() != "" { - sb.WriteString(colors.Gray(fmt.Sprintf("%s", colors.WrapBrackets(req.UserAgent()))).String()) + sb.WriteString(colors.Gray(colors.WrapBrackets(req.UserAgent())).String()) } if req.ContentLength > 0 { sb.WriteString(colors.BeigeBg(fmt.Sprintf(" Len: %d", req.ContentLength)).String()) @@ -386,7 +403,10 @@ func (p *proxyapp) colorizeTunnel(req, resp layers.Layer, sniffheader *[]string, switch reqt := req.(type) { case *layers.HTTPMessage: var reqBodySaved, respBodySaved []byte - rest := resp.(*layers.HTTPMessage) + rest, ok := resp.(*layers.HTTPMessage) + if !ok { + return fmt.Errorf("failed parsing HTTP response") + } if p.body { reqBodySaved, _ = io.ReadAll(reqt.Request.Body) respBodySaved, _ = io.ReadAll(rest.Response.Body) @@ -415,31 +435,27 @@ func (p *proxyapp) colorizeTunnel(req, resp layers.Layer, sniffheader *[]string, case *layers.TLSMessage: var chs *layers.TLSClientHello var shs *layers.TLSServerHello - if len(reqt.Records) > 0 { - hsrec := reqt.Records[0] - if hsrec.ContentType == layers.HandshakeTLSVal { // TODO: add more cases, parse all records - switch parser := layers.HSTLSParserByType(hsrec.Data[0]).(type) { - case *layers.TLSClientHello: - err := parser.ParseHS(hsrec.Data) - if err != nil { - return err - } - chs = parser + hsrec := reqt.Records[0] // len(Records) > 0 after dispatch + if hsrec.ContentType == layers.HandshakeTLSVal { // TODO: add more cases, parse all records + switch parser := layers.HSTLSParserByType(hsrec.Data[0]).(type) { + case *layers.TLSClientHello: + err := parser.ParseHS(hsrec.Data) + if err != nil { + return err } + chs = parser } } rest := resp.(*layers.TLSMessage) - if len(rest.Records) > 0 { - hsrec := rest.Records[0] - if hsrec.ContentType == layers.HandshakeTLSVal { - switch parser := layers.HSTLSParserByType(hsrec.Data[0]).(type) { - case *layers.TLSServerHello: - err := parser.ParseHS(hsrec.Data) - if err != nil { - return err - } - shs = parser + hsrec = rest.Records[0] + if hsrec.ContentType == layers.HandshakeTLSVal { + switch parser := layers.HSTLSParserByType(hsrec.Data[0]).(type) { + case *layers.TLSServerHello: + err := parser.ParseHS(hsrec.Data) + if err != nil { + return err } + shs = parser } } if chs != nil && shs != nil { @@ -814,7 +830,7 @@ func (p *proxyapp) handleForward(w http.ResponseWriter, r *http.Request) { } p.snifflogger.Log().Msg(fmt.Sprintf("[%s]", strings.Join(sniffheader, ","))) } else { - id := p.getId() + id := p.getID() p.snifflogger.Log().Msg(p.colorizeHTTP(req, resp, &reqBodySaved, &respBodySaved, id, false)) } } @@ -943,10 +959,13 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) { if p.sniff { wg.Add(1) sniffheader := make([]string, 0, 6) - id := p.getId() + id := p.getID() if p.json { - sniffheader = append(sniffheader, fmt.Sprintf("{\"connection\":{\"src_remote\":%s,\"src_local\":%s,\"dst_local\":%s,\"dst_remote\":%s}}", - srcConn.RemoteAddr(), srcConn.LocalAddr(), dstConn.LocalAddr(), dstConn.RemoteAddr())) + sniffheader = append( + sniffheader, + fmt.Sprintf("{\"connection\":{\"src_remote\":%s,\"src_local\":%s,\"dst_local\":%s,\"dst_remote\":%s}}", + srcConn.RemoteAddr(), srcConn.LocalAddr(), dstConn.LocalAddr(), dstConn.RemoteAddr()), + ) j, err := json.Marshal(&layers.HTTPMessage{Request: r}) if err == nil { sniffheader = append(sniffheader, string(j)) @@ -1009,7 +1028,7 @@ func (p *proxyapp) sniffreporter(wg *sync.WaitGroup, sniffheader *[]string, reqC if p.json { p.snifflogger.Log().Msg(fmt.Sprintf("[%s]", strings.Join(*sniffheader, ","))) } else { - p.snifflogger.Log().Msg(fmt.Sprintf("%s", strings.Join(*sniffheader, "\n"))) + p.snifflogger.Log().Msg(strings.Join(*sniffheader, "\n")) } } *sniffheader = (*sniffheader)[:sniffheaderlen] @@ -1024,7 +1043,7 @@ func dispatch(data []byte) (layers.Layer, error) { return h, nil } m := &layers.TLSMessage{} - if err := m.Parse(data); err == nil { + if err := m.Parse(data); err == nil && len(m.Records) > 0 { return m, nil } return nil, fmt.Errorf("failed sniffing traffic") @@ -1089,7 +1108,13 @@ readLoop: return written, err } -func (p *proxyapp) transfer(wg *sync.WaitGroup, dst net.Conn, src net.Conn, destName, srcName string, msgChan chan<- layers.Layer) { +func (p *proxyapp) transfer( + wg *sync.WaitGroup, + dst net.Conn, + src net.Conn, + destName, srcName string, + msgChan chan<- layers.Layer, +) { defer func() { wg.Done() close(msgChan) @@ -1173,10 +1198,10 @@ func (p *proxyapp) applyRedirectRules() string { } cmdInit := exec.Command("bash", "-c", ` set -ex - 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 -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 --dport 22 -j RETURN `) cmdInit.Stdout = os.Stdout @@ -1186,13 +1211,13 @@ func (p *proxyapp) applyRedirectRules() string { } if p.httpServerAddr != "" { _, httpPort, _ := net.SplitHostPort(p.httpServerAddr) - cmdHttp := exec.Command("bash", "-c", fmt.Sprintf(` + cmdHTTP := exec.Command("bash", "-c", fmt.Sprintf(` set -ex iptables -t nat -A GOHPTS -p tcp --dport %s -j RETURN `, httpPort)) - cmdHttp.Stdout = os.Stdout - cmdHttp.Stderr = os.Stderr - if err := cmdHttp.Run(); err != nil { + cmdHTTP.Stdout = os.Stdout + cmdHTTP.Stderr = os.Stderr + if err := cmdHTTP.Run(); err != nil { p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") } } @@ -1241,7 +1266,7 @@ func (p *proxyapp) applyRedirectRules() string { 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 done - fi + fi iptables -t nat -A GOHPTS -p tcp -j REDIRECT --to-ports %s @@ -1338,10 +1363,53 @@ func (p *proxyapp) applyRedirectRules() string { cmdForward.Stdout = os.Stdout cmdForward.Stderr = os.Stderr _ = cmdForward.Run() + if p.arp { + cmdClear := exec.Command("bash", "-c", ` + set -ex + 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 + `) + 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?") + } + iface, err := getDefaultInterface() + if err != nil { + p.logger.Fatal().Err(err).Msg("failed getting default network interface") + } + cmdForward := exec.Command("bash", "-c", fmt.Sprintf(` + set -ex + 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 + `, iface.Name, iface.Name)) + cmdForward.Stdout = os.Stdout + cmdForward.Stderr = os.Stderr + if err := cmdForward.Run(); err != nil { + p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?") + } + } return string(output) } func (p *proxyapp) clearRedirectRules(output string) error { + if p.arp { + cmdClear := exec.Command("bash", "-c", ` + set -ex + 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 + `) + 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?") + } + } var cmd *exec.Cmd switch p.tproxyMode { case "redirect": @@ -1514,7 +1582,7 @@ func getFullAddress(v string, all bool) (string, error) { if v == "" { return "", nil } - var ip string = "127.0.0.1" + ip := "127.0.0.1" if all { ip = "0.0.0.0" } @@ -1549,21 +1617,21 @@ func expandPath(p string) string { func New(conf *Config) *proxyapp { var logger, snifflogger zerolog.Logger var p proxyapp - var logfile *os.File = os.Stdout + logfile := os.Stdout var snifflog *os.File var err error p.sniff = conf.Sniff p.body = conf.Body - p.json = conf.Json + p.json = conf.JSON if conf.LogFilePath != "" { - f, err := os.OpenFile(conf.LogFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + f, err := os.OpenFile(conf.LogFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { log.Fatalf("Failed to open log file: %v", err) } logfile = f } if conf.SniffLogFile != "" && conf.SniffLogFile != conf.LogFilePath { - f, err := os.OpenFile(conf.SniffLogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + f, err := os.OpenFile(conf.SniffLogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { log.Fatalf("Failed to open sniff log file: %v", err) } @@ -1571,8 +1639,8 @@ func New(conf *Config) *proxyapp { } else { snifflog = logfile } - p.nocolor = conf.Json || conf.NoColor - if conf.Json { + p.nocolor = conf.JSON || conf.NoColor + if conf.JSON { log.SetFlags(0) jsonWriter := jsonLogWriter{file: logfile} log.SetOutput(jsonWriter) @@ -1597,7 +1665,7 @@ func New(conf *Config) *proxyapp { } s := i.(string) if p.nocolor { - return fmt.Sprintf("%s", s) + return s } result := ipPortPattern.ReplaceAllStringFunc(s, func(match string) string { return colors.Gray(match).String() @@ -1615,7 +1683,7 @@ func New(conf *Config) *proxyapp { output.FormatErrFieldValue = func(i any) string { s := i.(string) if p.nocolor { - return fmt.Sprintf("%s", s) + return s } result := ipPortPattern.ReplaceAllStringFunc(s, func(match string) string { return colors.Red(match).String() @@ -1624,7 +1692,6 @@ func New(conf *Config) *proxyapp { return colors.Red(match).String() }) return result - } logger = zerolog.New(output).With().Timestamp().Logger() sniffoutput := zerolog.ConsoleWriter{Out: snifflog, TimeFormat: time.RFC3339, NoColor: p.nocolor, PartsExclude: []string{"level"}} @@ -1648,7 +1715,7 @@ func New(conf *Config) *proxyapp { sniffoutput.FormatErrFieldValue = func(i any) string { s := i.(string) if p.nocolor { - return fmt.Sprintf("%s", s) + return s } result := ipPortPattern.ReplaceAllStringFunc(s, func(match string) string { return colors.Red(match).String() @@ -1657,7 +1724,6 @@ func New(conf *Config) *proxyapp { return colors.Red(match).String() }) return result - } snifflogger = zerolog.New(sniffoutput).With().Timestamp().Logger() } @@ -1680,7 +1746,7 @@ func New(conf *Config) *proxyapp { p.tproxyMode = conf.TProxyMode tproxyonly := conf.TProxyOnly != "" if tproxyonly { - if p.tproxyMode == "tproxy" { + if p.tproxyMode != "" { p.tproxyAddr, err = getFullAddress(conf.TProxyOnly, true) if err != nil { p.logger.Fatal().Err(err).Msg("") @@ -1692,7 +1758,7 @@ func New(conf *Config) *proxyapp { } } } else { - if p.tproxyMode == "tproxy" { + if p.tproxyMode != "" { p.tproxyAddr, err = getFullAddress(conf.TProxy, true) if err != nil { p.logger.Fatal().Err(err).Msg("") @@ -1718,6 +1784,12 @@ func New(conf *Config) *proxyapp { if p.mark == 0 && p.tproxyMode == "tproxy" { p.mark = 100 } + p.arp = conf.ARP + if p.arp && runtime.GOOS != "linux" { + p.logger.Fatal().Msg("ARP setup is available only for linux system") + } else if p.arp && !p.auto { + p.logger.Fatal().Msg("ARP setup requires auto configuration") + } var addrHTTP, addrSOCKS, certFile, keyFile string if conf.ServerConfPath != "" { var sconf serverConfig diff --git a/resources/.gitignore b/resources/.gitignore index 9a964a5..262e398 100644 --- a/resources/.gitignore +++ b/resources/.gitignore @@ -1,4 +1,4 @@ * !example_gohpts.yaml !sniffing_color.png -!mr_gopher.png +!mr_gopher*.png diff --git a/resources/mr_gopher_small.png b/resources/mr_gopher_small.png new file mode 100644 index 0000000..747c4a0 Binary files /dev/null and b/resources/mr_gopher_small.png differ diff --git a/tproxy_linux.go b/tproxy_linux.go index d06a436..cab13ce 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -4,11 +4,13 @@ package gohpts import ( + "bufio" "context" "errors" "fmt" "net" "net/netip" + "os" "strings" "sync" "syscall" @@ -94,7 +96,15 @@ func (ts *tproxyServer) serve() { } func getsockopt(s int, level int, optname int, optval unsafe.Pointer, optlen *uint32) (err error) { - _, _, e := unix.Syscall6(unix.SYS_GETSOCKOPT, uintptr(s), uintptr(level), uintptr(optname), uintptr(optval), uintptr(unsafe.Pointer(optlen)), 0) + _, _, e := unix.Syscall6( + unix.SYS_GETSOCKOPT, + uintptr(s), + uintptr(level), + uintptr(optname), + uintptr(optval), + uintptr(unsafe.Pointer(optlen)), + 0, + ) if e != 0 { return e } @@ -181,10 +191,20 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { if ts.pa.sniff { wg.Add(1) sniffheader := make([]string, 0, 6) - id := ts.pa.getId() + id := ts.pa.getID() if ts.pa.json { - sniffheader = append(sniffheader, fmt.Sprintf("{\"connection\":{\"tproxy_mode\":%s,\"src_remote\":%s,\"src_local\":%s,\"dst_local\":%s,\"dst_remote\":%s,\"original_dst\":%s}}", - ts.pa.tproxyMode, srcConn.RemoteAddr(), srcConn.LocalAddr(), dstConn.LocalAddr(), dstConn.RemoteAddr(), dst)) + sniffheader = append( + sniffheader, + fmt.Sprintf( + "{\"connection\":{\"tproxy_mode\":%s,\"src_remote\":%s,\"src_local\":%s,\"dst_local\":%s,\"dst_remote\":%s,\"original_dst\":%s}}", + ts.pa.tproxyMode, + srcConn.RemoteAddr(), + srcConn.LocalAddr(), + dstConn.LocalAddr(), + dstConn.RemoteAddr(), + dst, + ), + ) } else { var sb strings.Builder if ts.pa.nocolor { @@ -239,3 +259,23 @@ func getBaseDialer(timeout time.Duration, mark uint) *net.Dialer { } return dialer } + +func getDefaultInterface() (*net.Interface, error) { + f, err := os.Open("/proc/net/route") + if err != nil { + return nil, err + } + defer f.Close() + + defaultInterface := "" + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) >= 2 && fields[1] == "00000000" { + defaultInterface = fields[0] + break + } + } + return net.InterfaceByName(defaultInterface) +} diff --git a/tproxy_nonlinux.go b/tproxy_nonlinux.go index 850883e..76cbaa2 100644 --- a/tproxy_nonlinux.go +++ b/tproxy_nonlinux.go @@ -4,6 +4,7 @@ package gohpts import ( + "fmt" "net" "sync" "syscall" @@ -46,3 +47,7 @@ func getBaseDialer(timeout time.Duration, mark uint) *net.Dialer { _ = mark return &net.Dialer{Timeout: timeout} } + +func getDefaultInterface() (*net.Interface, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/version.go b/version.go index ed72300..fdcff4e 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package gohpts -const Version string = "gohpts v1.8.3" +const Version string = "gohpts v1.8.4"