diff --git a/README.md b/README.md index 62146b0..8088f2d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@   -
+
## 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"