From 95f7a34cc51e295e9ed9627cdf4409d6af7f699c Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:57:13 +0300 Subject: [PATCH 1/3] updated README with table of contents --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index df703e0..4c2ef71 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,20 @@ ![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) +## Table of contents + +- [Introduction](#introduction) +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) + - [Configuration via CLI flags](#configuration-via-cli-flags) + - [Configuration via YAML file](#configuration-via-yaml-file) +- [Transparent proxy](#usage) + - [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) +- [Links](#links) +- [License](#license) + ## Introduction `GoHPTS` CLI tool is a bridge between HTTP clients and a SOCKS5 proxy server or multiple servers (chain). It listens locally as an HTTP proxy, accepts standard HTTP From bec8ce10de437eac7bdaa7d6069ad11d4b8a2792 Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:08:43 +0300 Subject: [PATCH 2/3] updated README with table of contents 2 --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 4c2ef71..4cff042 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,13 @@ - [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) - [Links](#links) +- [Contributing](#contributing) - [License](#license) ## Introduction +[[Back]](#table-of-contents) + `GoHPTS` CLI tool is a bridge between HTTP clients and a SOCKS5 proxy server or multiple servers (chain). It listens locally as an HTTP proxy, accepts standard HTTP or HTTPS (via CONNECT) requests and forwards the connection through a SOCKS5 proxy. Inspired by [http-proxy-to-socks](https://github.com/oyyd/http-proxy-to-socks) and [Proxychains](https://github.com/rofl0r/proxychains-ng) @@ -45,6 +48,8 @@ Specify http server in proxy configuration of Postman ## Features +[[Back]](#table-of-contents) + - **Proxy Chain functionality** Supports `strict`, `dynamic`, `random`, `round_robin` chains of SOCKS5 proxy @@ -77,6 +82,8 @@ Specify http server in proxy configuration of Postman ## Installation +[[Back]](#table-of-contents) + You can download the binary for your platform from [Releases](https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases) page. Example: @@ -104,6 +111,8 @@ make build ## Usage +[[Back]](#table-of-contents) + ```shell gohpts -h _____ _ _ _____ _______ _____ @@ -149,6 +158,8 @@ Options: ### Configuration via CLI flags +[[Back]](#table-of-contents) + ```shell gohpts -s 1080 -l 8080 -d -j ``` @@ -205,6 +216,8 @@ kill $(pidof gohpts) ### Configuration via YAML file +[[Back]](#table-of-contents) + Run http proxy in SOCKS5 proxy chain mode (specify server settings via YAML configuration file) ```shell @@ -263,6 +276,8 @@ To learn more about proxy chains visit [Proxychains Github](https://github.com/r ## Transparent proxy +[[Back]](#table-of-contents) + > Also known as an `intercepting proxy`, `inline proxy`, or `forced proxy`, a transparent proxy intercepts normal application layer communication without requiring any special client configuration. Clients need not be aware of the existence of the proxy. A transparent proxy is normally located between the client and the Internet, with the proxy performing some of the functions of a gateway or router > > -- _From [Wiki](https://en.wikipedia.org/wiki/Proxy_server)_ @@ -346,6 +361,8 @@ iptables -t nat -X GOHPTS ## `tproxy` (via _MANGLE_ and _IP_TRANSPARENT_) +[[Back]](#table-of-contents) + In this mode proxying happens with `iptables` `mangle` table and `TPROXY` target. Transparent proxy sees destination address as is, it is not being rewrited by the kernel. For this to work the proxy binds with socket option `IP_TRANSPARENT`, `iptables` intercepts traffic using TPROXY target, routing rules tell marked packets to go to the local proxy without changing their original destination. This mode requires elevated privileges to run `GoHPTS`. You can do that by running the follwing command: @@ -399,6 +416,8 @@ ip link del veth1 ## Links +[[Back]](#table-of-contents) + Learn more about transparent proxies by visiting the following links: - [Transparent proxy support in Linux Kernel](https://docs.kernel.org/networking/tproxy.html) @@ -407,6 +426,20 @@ Learn more about transparent proxies by visiting the following links: - [Golang TProxy](https://github.com/KatelynHaworth/go-tproxy) - [Transparent Proxy Implementation using eBPF and Go](https://medium.com/all-things-ebpf/building-a-transparent-proxy-with-ebpf-50a012237e76) +## Contributing + +[[Back]](#table-of-contents) + +Are you a developer? + +- Fork the repository +- Create your feature branch: `git switch -c my-new-feature` +- Commit your changes: `git commit -am 'Add some feature'` +- Push to the branch: `git push origin my-new-feature` +- Submit a pull request + ## License +[[Back]](#table-of-contents) + MIT From 2d60cce9cd6a53f60b2146a888263b840598dfb3 Mon Sep 17 00:00:00 2001 From: shadowy-pycoder <35629483+shadowy-pycoder@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:05:37 +0300 Subject: [PATCH 3/3] added basic sniffing functionality --- README.md | 180 +++++++++++++++++++++++++++++++++++++++++----- cmd/gohpts/cli.go | 18 ++++- go.mod | 1 + go.sum | 10 +++ gohpts.go | 126 ++++++++++++++++++++++++++++---- tproxy_linux.go | 13 +++- version.go | 2 +- 7 files changed, 315 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 4c2ef71..ffb24bb 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ - [Usage](#usage) - [Configuration via CLI flags](#configuration-via-cli-flags) - [Configuration via YAML file](#configuration-via-yaml-file) -- [Transparent proxy](#usage) +- [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) +- [Traffic sniffing](#traffic-sniffing) - [Links](#links) - [License](#license) @@ -51,6 +52,9 @@ Specify http server in proxy configuration of Postman - **Transparent proxy** Supports `redirect` (SO_ORIGINAL_DST) and `tproxy` (IP_TRANSPARENT) modes +- **Traffic sniffing** + Proxy is able to parse HTTP headers and TLS handshake metadata + - **DNS Leak Protection** DNS resolution occurs on SOCKS5 server side. @@ -82,7 +86,7 @@ You can download the binary for your platform from [Releases](https://github.com Example: ```shell -HPTS_RELEASE=v1.6.1; 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.7.0; 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): @@ -119,32 +123,36 @@ GitHub: https://github.com/shadowy-pycoder/go-http-proxy-to-socks Usage: gohpts [OPTIONS] Options: -h Show this help message and exit. - -D Run as a daemon (provide -logfile to see logs) + -D Run as a daemon (provide -logfile to see logs) -M value - Transparent proxy mode: [redirect tproxy] + Transparent proxy mode: [redirect tproxy] -T string - Address of transparent proxy server (no HTTP) + Address of transparent proxy server (no HTTP) -U string - User for HTTP proxy (basic auth). This flag invokes prompt for password (not echoed to terminal) + User for HTTP proxy (basic auth). This flag invokes prompt for password (not echoed to terminal) -c string - Path to certificate PEM encoded file - -d Show logs in DEBUG mode + Path to certificate PEM encoded file + -d Show logs in DEBUG mode -f string - Path to server configuration file in YAML format - -j Show logs in JSON format + Path to server configuration file in YAML format + -j Show logs in JSON format -k string - Path to private key PEM encoded file + Path to private key PEM encoded file -l string - Address of HTTP proxy server (default "127.0.0.1:8080") + Address of HTTP proxy server (default "127.0.0.1:8080") -logfile string - Log file path (Default: stdout) + Log file path (Default: stdout) -s string - Address of SOCKS5 proxy server (default "127.0.0.1:1080") + Address of SOCKS5 proxy server (default "127.0.0.1:1080") + -sniff + Enable traffic sniffing for HTTP and TLS + -snifflog string + Sniffed traffic log file path (Default: the same as -logfile) -t string - Address of transparent proxy server (it starts along with HTTP proxy server) + Address of transparent proxy server (it starts along with HTTP proxy server) -u string - User for SOCKS5 proxy authentication. This flag invokes prompt for password (not echoed to terminal) - -v print version + User for SOCKS5 proxy authentication. This flag invokes prompt for password (not echoed to terminal) + -v print version ``` ### Configuration via CLI flags @@ -397,6 +405,144 @@ ip netns del ns-client ip link del veth1 ``` +## Traffic sniffing + +[[Back]](#table-of-contents) + +`GoHPTS` proxy allows one to capture and monitor traffic that goes through the service. This procces is known as `traffic sniffing`, `packet sniffing` or just `sniffing`. In particular, proxy tries to identify whether it is a plain text (HTTP) or TLS traffic, and after identification is done, it parses request/response metadata and writes it to the file or console. In the case of `GoHTPS` proxy a parsed metadata looks like the following (TLS Handshake): + +```json +[ + { + "connection": { + "tproxy_mode": "redirect", + "src_local": "127.0.0.1:8888", + "src_remote": "192.168.0.107:51142", + "dst_local": "127.0.0.1:56256", + "dst_remote": "127.0.0.1:1080", + "original_dst": "216.58.209.206:443" + } + }, + { + "tls_request": { + "sni": "www.youtube.com", + "type": "Client hello (1)", + "version": "TLS 1.2 (0x0303)", + "session_id": "2670a6779b4346e5e84d46890ad2aaf7a53b08adcfe0c9f6868c2d9882242e39", + "cipher_suites": [ + "TLS_AES_128_GCM_SHA256 (0x1301)", + "TLS_CHACHA20_POLY1305_SHA256 (0x1303)", + "TLS_AES_256_GCM_SHA384 (0x1302)", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca9)", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (0xc02c)", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)", + "TLS_RSA_WITH_AES_128_GCM_SHA256 (0x9c)", + "TLS_RSA_WITH_AES_256_GCM_SHA384 (0x9d)", + "TLS_RSA_WITH_AES_128_CBC_SHA (0x2f)", + "TLS_RSA_WITH_AES_256_CBC_SHA (0x35)" + ], + "extensions": [ + "server_name (0)", + "extended_master_secret (23)", + "renegotiation_info (65281)", + "supported_groups (10)", + "ec_point_formats (11)", + "session_ticket (35)", + "application_layer_protocol_negotiation (16)", + "status_request (5)", + "delegated_credential (34)", + "signed_certificate_timestamp (18)", + "key_share (51)", + "supported_versions (43)", + "signature_algorithms (13)", + "psk_key_exchange_modes (45)", + "record_size_limit (28)", + "compress_certificate (27)", + "encrypted_client_hello (65037)" + ], + "alpn": ["h2", "http/1.1"] + } + }, + { + "tls_response": { + "type": "Server hello (2)", + "version": "TLS 1.2 (0x0303)", + "session_id": "2670a6779b4346e5e84d46890ad2aaf7a53b08adcfe0c9f6868c2d9882242e39", + "cipher_suite": "TLS_AES_128_GCM_SHA256 (0x1301)", + "extensions": ["key_share (51)", "supported_versions (43)"], + "supported_version": "TLS 1.3 (0x0304)" + } + } +] +``` + +And HTTP request with curl: + +```json +[ + { + "connection": { + "tproxy_mode": "redirect", + "src_local": "127.0.0.1:8888", + "src_remote": "192.168.0.107:45736", + "dst_local": "127.0.0.1:37640", + "dst_remote": "127.0.0.1:1080", + "original_dst": "96.7.128.198:80" + } + }, + { + "http_request": { + "host": "example.com", + "uri": "/", + "method": "GET", + "proto": "HTTP/1.1", + "header": { + "Accept": ["*/*"], + "My": ["Header"], + "User-Agent": ["curl/7.81.0"] + } + } + }, + { + "http_response": { + "proto": "HTTP/1.1", + "status": "200 OK", + "content-length": 1256, + "header": { + "Cache-Control": ["max-age=2880"], + "Connection": ["keep-alive"], + "Content-Length": ["1256"], + "Content-Type": ["text/html"], + "Date": ["Tue, 17 Jun 2025 14:43:24 GMT"], + "Etag": ["\"84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134\""], + "Last-Modified": ["Mon, 13 Jan 2025 20:11:20 GMT"] + } + } + } +] +``` + +Usage as simple as specifying `-sniff` flag along with regular flags + +```shell +gohpts -d -t 8888 -M redirect -sniff +``` + +You can also specify a file to which write sniffed traffic: + +```shell +gohpts -d -sniff -snifflog ~/sniff.log +``` + +Please note that for now sniffing only visible with `-d` flag, it may change in the future. + ## Links Learn more about transparent proxies by visiting the following links: diff --git a/cmd/gohpts/cli.go b/cmd/gohpts/cli.go index 0e90468..2790384 100644 --- a/cmd/gohpts/cli.go +++ b/cmd/gohpts/cli.go @@ -59,6 +59,8 @@ func root(args []string) error { 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.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.BoolFunc("v", "print version", func(flagValue string) error { fmt.Println(gohpts.Version) os.Exit(0) @@ -86,7 +88,7 @@ func root(args []string) error { if seen["T"] { for _, da := range []string{"U", "c", "k", "l"} { if seen[da] { - return fmt.Errorf("-T flag only works with -s, -u, -f, -M, -d, -D, -logfile and -j flags") + return fmt.Errorf("-T flag only works with -s, -u, -f, -M, -d, -D, -logfile, -sniff, -snifflog and -j flags") } } if !seen["M"] { @@ -102,9 +104,9 @@ func root(args []string) error { for _, da := range []string{"s", "u", "U", "c", "k", "l"} { if seen[da] { if runtime.GOOS == tproxyOS { - return fmt.Errorf("-f flag only works with -t, -T, -M, -d, -D, -logfile and -j flags") + return fmt.Errorf("-f flag only works with -t, -T, -M, -d, -D, -logfile, -sniff, -snifflog and -j flags") } - return fmt.Errorf("-f flag only works with -d, -D, -logfile and -j flags") + return fmt.Errorf("-f flag only works with -d, -D, -logfile, -sniff, -snifflog and -j flags") } } } @@ -131,6 +133,16 @@ func root(args []string) error { conf.ServerPass = string(bytepw) fmt.Print("\033[2K\r") } + if seen["sniff"] { + if !seen["d"] { + return fmt.Errorf("Traffic sniffing requires debug mode") + } + } + if seen["snifflog"] { + if !seen["sniff"] { + return fmt.Errorf("-snifflog only works with -sniff flag") + } + } if *daemon { if os.Getenv("GOHPTS_DAEMON") != "1" { diff --git a/go.mod b/go.mod index 65e7c13..c50256e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.1 require ( github.com/goccy/go-yaml v1.18.0 github.com/rs/zerolog v1.34.0 + github.com/shadowy-pycoder/mshark v0.0.4 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 e57f174..7f73dce 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -8,9 +10,15 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +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/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= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -20,3 +28,5 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gohpts.go b/gohpts.go index 0991f4b..453fab2 100644 --- a/gohpts.go +++ b/gohpts.go @@ -6,6 +6,7 @@ import ( "crypto/subtle" "crypto/tls" "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -25,6 +26,7 @@ import ( "github.com/goccy/go-yaml" "github.com/rs/zerolog" + "github.com/shadowy-pycoder/mshark/layers" "golang.org/x/net/proxy" ) @@ -111,6 +113,7 @@ type proxyapp struct { httpClient *http.Client sockDialer proxy.Dialer logger *zerolog.Logger + snifflogger *zerolog.Logger certFile string keyFile string httpServerAddr string @@ -122,6 +125,7 @@ type proxyapp struct { proxylist []proxyEntry rrIndex uint32 rrIndexReset uint32 + sniff bool mu sync.RWMutex availProxyList []proxyEntry @@ -504,15 +508,88 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) { p.logger.Debug().Msgf("%s - %s - %s", r.Proto, r.Method, r.Host) p.logger.Debug().Msgf("src: %s - dst: %s", srcConnStr, dstConnStr) - + reqChan := make(chan []byte) + respChan := make(chan []byte) var wg sync.WaitGroup wg.Add(2) - go p.transfer(&wg, dstConn, srcConn, dstConnStr, srcConnStr) - go p.transfer(&wg, srcConn, dstConn, srcConnStr, dstConnStr) + go p.transfer(&wg, dstConn, srcConn, dstConnStr, srcConnStr, reqChan) + go p.transfer(&wg, srcConn, dstConn, srcConnStr, dstConnStr, respChan) + if p.sniff { + wg.Add(1) + sniffheader := make([]string, 0, 4) + sniffheader = append(sniffheader, fmt.Sprintf("{\"connection\":{\"src_local\":%q,\"src_remote\":%q,\"dst_local\":%q,\"dst_remote\":%q}}", + srcConn.LocalAddr(), srcConn.RemoteAddr(), dstConn.LocalAddr(), dstConn.RemoteAddr())) + j, err := json.Marshal(&layers.HTTPMessage{Request: r}) + if err == nil { + sniffheader = append(sniffheader, string(j)) + } + go p.sniffreporter(&wg, &sniffheader, reqChan, respChan) + } wg.Wait() } -func (p *proxyapp) copyWithTimeout(dst net.Conn, src net.Conn) (written int64, err error) { +func (p *proxyapp) sniffreporter(wg *sync.WaitGroup, sniffheader *[]string, reqChan <-chan []byte, respChan <-chan []byte) { + defer wg.Done() + sniffheaderlen := len(*sniffheader) + for { + req, okreq := <-reqChan // if resp comes first it blocks + resp, okresp := <-respChan + if !okreq || !okresp { + return + } + *sniffheader = append(*sniffheader, string(req), string(resp)) + p.snifflogger.Debug().Msg(fmt.Sprintf("[%s]", strings.Join(*sniffheader, ","))) + *sniffheader = (*sniffheader)[:sniffheaderlen] + } +} + +func sniff(data []byte, logger *zerolog.Logger) ([]byte, error) { + // TODO: check if it is http or tls beforehand + h := &layers.HTTPMessage{} + if err := h.Parse(data); err == nil && !h.IsEmpty() { + j, err := json.Marshal(h) + if err == nil { + return j, nil + } + } + m := &layers.TLSMessage{} + if err := m.Parse(data); err != nil { + return nil, err + } + if len(m.Records) > 0 { + hsrec := m.Records[0] + if hsrec.ContentType == layers.HandshakeTLSVal { // TODO: add more cases, parse all records + parser := layers.HSTLSParserByType(hsrec.Data[0]) + switch parser.(type) { + case *layers.TLSClientHello: + tc := parser.(*layers.TLSClientHello) + err := tc.ParseHS(hsrec.Data) + if err != nil { + return nil, err + } + j, err := json.Marshal(tc) + if err != nil { + return nil, err + } + return j, nil + case *layers.TLSServerHello: + ts := parser.(*layers.TLSServerHello) + err := ts.ParseHS(hsrec.Data) + if err != nil { + return nil, err + } + j, err := json.Marshal(ts) + if err != nil { + return nil, err + } + return j, nil + } + } + } + return nil, fmt.Errorf("failed sniffing traffic") +} + +func (p *proxyapp) copyWithTimeout(dst net.Conn, src net.Conn, msgChan chan<- []byte) (written int64, err error) { buf := make([]byte, 32*1024) for { er := src.SetReadDeadline(time.Now().Add(readTimeout)) @@ -527,6 +604,12 @@ func (p *proxyapp) copyWithTimeout(dst net.Conn, src net.Conn) (written int64, e err = er break } + if p.sniff { + s, err := sniff(buf[0:nr], p.logger) + if err == nil { + msgChan <- s + } + } nw, ew := dst.Write(buf[0:nr]) if nw < 0 || nr < nw { nw = 0 @@ -559,9 +642,12 @@ func (p *proxyapp) copyWithTimeout(dst net.Conn, src net.Conn) (written int64, e return written, err } -func (p *proxyapp) transfer(wg *sync.WaitGroup, dst net.Conn, src net.Conn, destName, srcName string) { - defer wg.Done() - n, err := p.copyWithTimeout(dst, src) +func (p *proxyapp) transfer(wg *sync.WaitGroup, dst net.Conn, src net.Conn, destName, srcName string, msgChan chan<- []byte) { + defer func() { + wg.Done() + close(msgChan) + }() + n, err := p.copyWithTimeout(dst, src, msgChan) if err != nil { p.logger.Error().Err(err).Msgf("Error during copy from %s to %s: %v", srcName, destName, err) } @@ -709,6 +795,8 @@ type Config struct { LogFilePath string Debug bool Json bool + Sniff bool + SniffLogFile string } type logWriter struct { @@ -783,9 +871,11 @@ func expandPath(p string) string { } func New(conf *Config) *proxyapp { - var logger zerolog.Logger + var logger, snifflogger zerolog.Logger var p proxyapp var logfile *os.File = os.Stdout + var snifflog *os.File + p.sniff = conf.Sniff if conf.LogFilePath != "" { f, err := os.OpenFile(conf.LogFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { @@ -793,26 +883,38 @@ func New(conf *Config) *proxyapp { } 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) + if err != nil { + log.Fatalf("Failed to open sniff log file: %v", err) + } + snifflog = f + } else { + snifflog = logfile + } if conf.Json { log.SetFlags(0) jsonWriter := jsonLogWriter{file: logfile} log.SetOutput(jsonWriter) logger = zerolog.New(logfile).With().Timestamp().Logger() + snifflogger = zerolog.New(snifflog).With().Timestamp().Logger() } else { log.SetFlags(0) logWriter := logWriter{file: logfile} log.SetOutput(logWriter) - output := zerolog.ConsoleWriter{Out: logfile, TimeFormat: time.RFC3339, NoColor: true} - output.FormatLevel = func(i any) string { - return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) - } + noColor := logfile != os.Stdout + sniffNoColor := snifflog != os.Stdout + output := zerolog.ConsoleWriter{Out: logfile, TimeFormat: time.RFC3339, NoColor: noColor} logger = zerolog.New(output).With().Timestamp().Logger() + sniffoutput := zerolog.ConsoleWriter{Out: snifflog, TimeFormat: time.RFC3339, NoColor: sniffNoColor} + snifflogger = zerolog.New(sniffoutput).With().Timestamp().Logger() } zerolog.SetGlobalLevel(zerolog.InfoLevel) if conf.Debug { zerolog.SetGlobalLevel(zerolog.DebugLevel) } p.logger = &logger + p.snifflogger = &snifflogger if runtime.GOOS == "linux" && conf.TProxy != "" && conf.TProxyOnly != "" { 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) { diff --git a/tproxy_linux.go b/tproxy_linux.go index 315ad2d..43b1c96 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -169,10 +169,19 @@ func (ts *tproxyServer) handleConnection(srcConn net.Conn) { ts.pa.logger.Debug().Msgf("[tproxy] src: %s - dst: %s", srcConnStr, dstConnStr) + reqChan := make(chan []byte) + respChan := make(chan []byte) var wg sync.WaitGroup wg.Add(2) - go ts.pa.transfer(&wg, dstConn, srcConn, dstConnStr, srcConnStr) - go ts.pa.transfer(&wg, srcConn, dstConn, srcConnStr, dstConnStr) + go ts.pa.transfer(&wg, dstConn, srcConn, dstConnStr, srcConnStr, reqChan) + go ts.pa.transfer(&wg, srcConn, dstConn, srcConnStr, dstConnStr, respChan) + if ts.pa.sniff { + wg.Add(1) + sniffheader := make([]string, 0, 3) + sniffheader = append(sniffheader, fmt.Sprintf("{\"connection\":{\"tproxy_mode\":%q,\"src_local\":%q,\"src_remote\":%q,\"dst_local\":%q,\"dst_remote\":%q,\"original_dst\":%q}}", + ts.pa.tproxyMode, srcConn.LocalAddr(), srcConn.RemoteAddr(), dstConn.LocalAddr(), dstConn.RemoteAddr(), dst)) + go ts.pa.sniffreporter(&wg, &sniffheader, reqChan, respChan) + } wg.Wait() } diff --git a/version.go b/version.go index f9be0c1..79dc9e6 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package gohpts -const Version string = "gohpts v1.6.1" +const Version string = "gohpts v1.7.0"