diff --git a/README.md b/README.md index 181b4dd..bc70ce8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ Specify http server in proxy configuration of Postman - **Proxy Chain functionality** Supports `strict`, `dynamic`, `random`, `round_robin` chains of SOCKS5 proxy +- **Transparent proxy** + Supports `redirect` (SO_ORIGINAL_DST) and `tproxy` (IP_TRANSPARENT) modes + - **DNS Leak Protection** DNS resolution occurs on SOCKS5 server side. @@ -65,7 +68,7 @@ You can download the binary for your platform from [Releases](https://github.com Example: ```shell -HPTS_RELEASE=v1.5.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 +HPTS_RELEASE=v1.6.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): @@ -102,27 +105,31 @@ GitHub: https://github.com/shadowy-pycoder/go-http-proxy-to-socks Usage: gohpts [OPTIONS] Options: -h Show this help message and exit. + -M value + Transparent proxy mode: [redirect tproxy] + -T string + 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") -s string - Address of SOCKS5 proxy server (default "127.0.0.1:1080") + Address of SOCKS5 proxy server (default "127.0.0.1:1080") + -t string + 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 ``` -## Example - ### Configuration via CLI flags ```shell @@ -217,6 +224,152 @@ server: To learn more about proxy chains visit [Proxychains Github](https://github.com/rofl0r/proxychains-ng) +## Transparent proxy + +> 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)_ + +This functionality available only on Linux systems and requires additional setup (`iptables`, ip route, etc) + +`-T address` flag specifies the address of transparent proxy server (`GoHPTS` will be running without HTTP server). + +`-t address` flag specifies the address of transparent proxy server (`HTTP` proxy and other functionality stays the same). + +In other words, `-T` spins up a single server, but `-t` two servers, `http` and `tcp`. + +There are two modes `redirect` and `tproxy` that can be specified with `-M` flag + +## `redirect` (via _NAT_ and _SO_ORIGINAL_DST_) + +In this mode proxying happens with `iptables` `nat` table and `REDIRECT` target. Host of incoming packet changes to the address of running `redirect` transparent proxy, but it also contains original destination that can be retrieved with `getsockopt(SO_ORIGINAL_DST)` + +To run `GoHPTS` in this mode you use `-t` or `-T` flags with `-M redirect` + +### Example + +```shell +# run the proxy +gohpts -s 1080 -t 1090 -M redirect -d +``` + +```shell +# run socks5 server on 127.0.0.1:1080 +ssh remote -D 1080 -Nf +``` + +Setup your operating system: + +```shell +# commands below require elevated privileges (you can run it with `sudo -i`) + +#enable ip forwarding +sysctl -w net.ipv4.ip_forward=1 + +# create `GOHPTS` nat chain +iptables -t nat -N GOHPTS + +# set no redirection rules for local, http proxy, ssh and redirect procy itself +iptables -t nat -A GOHPTS -d 127.0.0.0/8 -j RETURN +iptables -t nat -A GOHPTS -p tcp --dport 8080 -j RETURN +iptables -t nat -A GOHPTS -p tcp --dport 1090 -j RETURN +iptables -t nat -A GOHPTS -p tcp --dport 22 -j RETURN + +# redirect traffic to transparent proxy +iptables -t nat -A GOHPTS -p tcp -j REDIRECT --to-ports 1090 + +# setup prerouting by adding our proxy +iptables -t nat -A PREROUTING -p tcp -j GOHPTS + +# intercept local traffic for testing +iptables -t nat -A OUTPUT -p tcp -j GOHPTS +``` + +Test connection: + +```shell +#traffic should be redirected via 127.0.0.1:1090 +curl http://example.com +``` + +```shell +#traffic should be redirected via 127.0.0.1:8080 +curl --proxy http://127.0.0.1:8080 http://example.com +``` + +Undo everything: + +```shell +sysctl -w net.ipv4.ip_forward=0 +iptables -t nat -D PREROUTING -p tcp -j GOHPTS +iptables -t nat -D OUTPUT -p tcp -j GOHPTS +iptables -t nat -F GOHPTS +iptables -t nat -X GOHPTS +``` + +## `tproxy` (via _MANGLE_ and _IP_TRANSPARENT_) + +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: + +```shell +sudo setcap 'cap_net_admin+ep' ~/go/bin/gohpts +``` + +To run `GoHPTS` in this mode you use `-t` or `-T` flags with `-M tproxy` + +### Example + +```shell +# run the proxy +gohpts -s 1080 -T 0.0.0.0:1090 -M tproxy -d +``` + +```shell +# run socks5 server on 127.0.0.1:1080 +ssh remote -D 1080 -Nf +``` + +Setup your operating system: + +```shell +ip netns exec ns-client ip route add default via 10.0.0.1 +sysctl -w net.ipv4.ip_forward=1 + +iptables -t mangle -A PREROUTING -i veth1 -p tcp -j TPROXY --on-port 1090 --tproxy-mark 0x1/0x1 + +ip rule add fwmark 1 lookup 100 +ip route add local 0.0.0.0/0 dev lo table 100 +``` + +Test connection: + +```shell +ip netns exec ns-client curl http://1.1.1.1 +``` + +Undo everything: + +```shell +sysctl -w net.ipv4.ip_forward=0 +iptables -t mangle -F +ip rule del fwmark 1 lookup 100 +ip route flush table 100 +ip netns del ns-client +ip link del veth1 +``` + +## Links + +Learn more about transparent proxies by visiting the following links: + +- [Transparent proxy support in Linux Kernel](https://docs.kernel.org/networking/tproxy.html) +- [Transparent proxy tutorial by Gost](https://latest.gost.run/en/tutorials/redirect/) +- [Simple tproxy example](https://github.com/FarFetchd/simple_tproxy_example) +- [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) + ## License MIT diff --git a/cmd/gohpts/cli.go b/cmd/gohpts/cli.go index a93f1bf..3a0e573 100644 --- a/cmd/gohpts/cli.go +++ b/cmd/gohpts/cli.go @@ -4,6 +4,8 @@ import ( "flag" "fmt" "os" + "runtime" + "slices" gohpts "github.com/shadowy-pycoder/go-http-proxy-to-socks" "golang.org/x/term" @@ -13,6 +15,7 @@ const ( app string = "gohpts" addrSOCKS = "127.0.0.1:1080" addrHTTP = "127.0.0.1:8080" + tproxyOS = "linux" ) const usagePrefix string = ` _____ _ _ _____ _______ _____ @@ -40,6 +43,18 @@ func root(args []string) error { 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") + 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.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) + os.Exit(2) + } + conf.TProxyMode = flagValue + return nil + }) + } flags.BoolFunc("d", "Show logs in DEBUG mode", func(flagValue string) error { conf.Debug = true return nil @@ -64,9 +79,35 @@ func root(args []string) error { } seen := make(map[string]bool) flags.Visit(func(f *flag.Flag) { seen[f.Name] = true }) + if seen["t"] && seen["T"] { + return fmt.Errorf("cannot specify both -t and -T flags") + } + if seen["t"] { + if !seen["M"] { + return fmt.Errorf("Transparent proxy mode is not provided: -M flag") + } + } + 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 and -j flags") + } + } + if !seen["M"] { + return fmt.Errorf("Transparent proxy mode is not provided: -M flag") + } + } + if seen["M"] { + if !seen["t"] && !seen["T"] { + return fmt.Errorf("Transparent proxy mode requires -t or -T flag") + } + } if seen["f"] { 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 and -j flags") + } return fmt.Errorf("-f flag only works with -d and -j flags") } } @@ -89,6 +130,7 @@ func root(args []string) error { conf.ServerPass = string(bytepw) fmt.Print("\033[2K\r") } + p := gohpts.New(&conf) p.Run() return nil diff --git a/go.mod b/go.mod index 9b258a9..65e7c13 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/goccy/go-yaml v1.18.0 github.com/rs/zerolog v1.34.0 golang.org/x/net v0.40.0 + golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 ) require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - golang.org/x/sys v0.33.0 // indirect ) diff --git a/gohpts.go b/gohpts.go index 28b299f..defe8e7 100644 --- a/gohpts.go +++ b/gohpts.go @@ -15,6 +15,7 @@ import ( "net/http" "os" "os/signal" + "runtime" "slices" "strconv" "strings" @@ -28,8 +29,8 @@ import ( ) const ( - readTimeout time.Duration = 10 * time.Second - writeTimeout time.Duration = 10 * time.Second + readTimeout time.Duration = 3 * time.Second + writeTimeout time.Duration = 3 * time.Second timeout time.Duration = 10 * time.Second hopTimeout time.Duration = 3 * time.Second flushTimeout time.Duration = 10 * time.Millisecond @@ -38,7 +39,11 @@ const ( rrIndexMax uint32 = 1_000_000 ) -var supportedChainTypes = []string{"strict", "dynamic", "random", "round_robin"} +var ( + supportedChainTypes = []string{"strict", "dynamic", "random", "round_robin"} + SupportedTProxyModes = []string{"redirect", "tproxy"} + errInvalidWrite = errors.New("invalid write result") +) // Hop-by-hop headers // https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1 @@ -72,7 +77,7 @@ func delHopHeaders(header http.Header) { // https://datatracker.ietf.org/doc/html/rfc7230#section-6.1 func delConnectionHeaders(h http.Header) { for _, f := range h["Connection"] { - for _, sf := range strings.Split(f, ",") { + for sf := range strings.SplitSeq(f, ",") { if sf = strings.TrimSpace(sf); sf != "" { h.Del(sf) } @@ -109,6 +114,8 @@ type proxyapp struct { certFile string keyFile string httpServerAddr string + tproxyAddr string + tproxyMode string user string pass string proxychain chain @@ -123,7 +130,17 @@ type proxyapp struct { func (p *proxyapp) printProxyChain(pc []proxyEntry) string { var sb strings.Builder sb.WriteString("client -> ") - sb.WriteString(p.httpServerAddr) + if p.httpServerAddr != "" { + sb.WriteString(p.httpServerAddr) + if p.tproxyAddr != "" { + sb.WriteString(" | ") + sb.WriteString(p.tproxyAddr) + sb.WriteString(fmt.Sprintf(" (%s)", p.tproxyMode)) + } + } else if p.tproxyAddr != "" { + sb.WriteString(p.tproxyAddr) + sb.WriteString(fmt.Sprintf(" (%s)", p.tproxyMode)) + } sb.WriteString(" -> ") for _, pe := range pc { sb.WriteString(pe.String()) @@ -217,6 +234,10 @@ func (p *proxyapp) getSocks() (proxy.Dialer, *http.Client, error) { p.mu.RLock() defer p.mu.RUnlock() chainType := p.proxychain.Type + if len(p.availProxyList) == 0 { + p.logger.Error().Msgf("[%s] No SOCKS5 Proxy available", chainType) + return nil, nil, fmt.Errorf("no socks5 proxy available") + } var chainLength int if p.proxychain.Length > len(p.availProxyList) || p.proxychain.Length <= 0 { chainLength = len(p.availProxyList) @@ -245,7 +266,7 @@ func (p *proxyapp) getSocks() (proxy.Dialer, *http.Client, error) { } } startIdx := int(start % uint32(len(p.availProxyList))) - for i := 0; i < chainLength; i++ { + for i := range chainLength { idx := (startIdx + i) % len(p.availProxyList) copyProxyList = append(copyProxyList, p.availProxyList[idx]) } @@ -269,7 +290,7 @@ func (p *proxyapp) getSocks() (proxy.Dialer, *http.Client, error) { } dialer, err = proxy.SOCKS5("tcp", pr.Address, &auth, dialer) if err != nil { - p.logger.Error().Err(err).Msgf("[%s] Unable to create SOCKS5 dialer %s", &chainType, pr.Address) + p.logger.Error().Err(err).Msgf("[%s] Unable to create SOCKS5 dialer %s", chainType, pr.Address) return nil, nil, err } } @@ -452,7 +473,9 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) return } - dstConn, err = sockDialer.Dial("tcp", r.Host) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + dstConn, err = sockDialer.(proxy.ContextDialer).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) @@ -489,9 +512,56 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) { wg.Wait() } -func (p *proxyapp) transfer(wg *sync.WaitGroup, destination io.Writer, source io.Reader, destName, srcName string) { +func (p *proxyapp) copyWithTimeout(dst net.Conn, src net.Conn) (written int64, err error) { + buf := make([]byte, 32*1024) + for { + er := src.SetReadDeadline(time.Now().Add(readTimeout)) + if er != nil { + err = er + break + } + nr, er := src.Read(buf) + if nr > 0 { + er := dst.SetWriteDeadline(time.Now().Add(writeTimeout)) + if er != nil { + err = er + break + } + nw, ew := dst.Write(buf[0:nr]) + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + ew = errInvalidWrite + } + } + written += int64(nw) + if ew != nil { + if ne, ok := ew.(net.Error); ok && ne.Timeout() { + err = ne + break + } + } + if nr != nw { + err = io.ErrShortWrite + break + } + } + if er != nil { + if ne, ok := err.(net.Error); ok && ne.Timeout() { + err = er + break + } + if er == io.EOF { + break + } + } + } + return written, err +} + +func (p *proxyapp) transfer(wg *sync.WaitGroup, dst net.Conn, src net.Conn, destName, srcName string) { defer wg.Done() - n, err := io.Copy(destination, source) + n, err := p.copyWithTimeout(dst, src) if err != nil { p.logger.Error().Err(err).Msgf("Error during copy from %s to %s: %v", srcName, destName, err) } @@ -562,6 +632,10 @@ func (p *proxyapp) Run() { done := make(chan bool) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) + var tproxyServer *tproxyServer + if p.tproxyAddr != "" { + tproxyServer = newTproxyServer(p) + } if p.proxylist != nil { chainType := p.proxychain.Type go func() { @@ -572,34 +646,51 @@ func (p *proxyapp) Run() { } }() } - go func() { - <-quit - p.logger.Info().Msg("Server is shutting down...") - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + if p.httpServer != nil { + go func() { + <-quit + if tproxyServer != nil { + p.logger.Info().Msg("[tproxy] Server is shutting down...") + tproxyServer.Shutdown() + } + p.logger.Info().Msg("Server is shutting down...") + ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - p.httpServer.SetKeepAlivesEnabled(false) - if err := p.httpServer.Shutdown(ctx); err != nil { - p.logger.Fatal().Err(err).Msg("Could not gracefully shutdown the server") + defer cancel() + p.httpServer.SetKeepAlivesEnabled(false) + if err := p.httpServer.Shutdown(ctx); err != nil { + p.logger.Fatal().Err(err).Msg("Could not gracefully shutdown the server") + } + close(done) + }() + if tproxyServer != nil { + go tproxyServer.ListenAndServe() } - close(done) - }() - if p.user != "" && p.pass != "" { - p.httpServer.Handler = p.proxyAuth(p.handler()) - } else { - p.httpServer.Handler = p.handler() - } - if p.certFile != "" && p.keyFile != "" { - if err := p.httpServer.ListenAndServeTLS(p.certFile, p.keyFile); err != nil && err != http.ErrServerClosed { - p.logger.Fatal().Err(err).Msg("Unable to start HTTPS server") + if p.user != "" && p.pass != "" { + p.httpServer.Handler = p.proxyAuth(p.handler()) + } else { + p.httpServer.Handler = p.handler() } - } else { - if err := p.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - p.logger.Fatal().Err(err).Msg("Unable to start HTTP server") + if p.certFile != "" && p.keyFile != "" { + if err := p.httpServer.ListenAndServeTLS(p.certFile, p.keyFile); err != nil && err != http.ErrServerClosed { + p.logger.Fatal().Err(err).Msg("Unable to start HTTPS server") + } + } else { + if err := p.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + p.logger.Fatal().Err(err).Msg("Unable to start HTTP server") + } } + p.logger.Info().Msg("Server stopped") + } else { + go func() { + <-quit + p.logger.Info().Msg("[tproxy] Server is shutting down...") + tproxyServer.Shutdown() + close(done) + }() + tproxyServer.ListenAndServe() } <-done - p.logger.Info().Msg("Server stopped") } type Config struct { @@ -614,7 +705,11 @@ type Config struct { CertFile string KeyFile string ServerConfPath string + TProxy string + TProxyOnly string + TProxyMode string } + type logWriter struct { } @@ -659,6 +754,9 @@ type serverConfig struct { } func getFullAddress(v string) string { + if v == "" { + return "" + } var addr string i, err := strconv.Atoi(v) if err == nil { @@ -697,12 +795,28 @@ func New(conf *Config) *proxyapp { } logger = zerolog.New(output).With().Timestamp().Logger() } - zerolog.SetGlobalLevel(zerolog.InfoLevel) if conf.Debug { zerolog.SetGlobalLevel(zerolog.DebugLevel) } p.logger = &logger + 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) { + p.logger.Fatal().Msg("Incorrect TProxyMode provided") + } else if runtime.GOOS != "linux" && (conf.TProxy != "" || conf.TProxyOnly != "" || conf.TProxyMode != "") { + conf.TProxy = "" + conf.TProxyOnly = "" + conf.TProxyMode = "" + p.logger.Warn().Msg("[tproxy] functionality only available on linux system") + } + p.tproxyMode = conf.TProxyMode + tproxyonly := conf.TProxyOnly != "" + if tproxyonly { + p.tproxyAddr = getFullAddress(conf.TProxyOnly) + } else { + p.tproxyAddr = getFullAddress(conf.TProxy) + } var addrHTTP, addrSOCKS, certFile, keyFile string if conf.ServerConfPath != "" { var sconf serverConfig @@ -714,15 +828,17 @@ func New(conf *Config) *proxyapp { if err != nil { p.logger.Fatal().Err(err).Msg("[server config] Parsing failed") } - if sconf.Server.Address == "" { - p.logger.Fatal().Err(err).Msg("[server config] Server address is empty") + if !tproxyonly { + if sconf.Server.Address == "" { + p.logger.Fatal().Err(err).Msg("[server config] Server address is empty") + } + addrHTTP = getFullAddress(sconf.Server.Address) + p.httpServerAddr = addrHTTP + certFile = expandPath(sconf.Server.CertFile) + keyFile = expandPath(sconf.Server.KeyFile) + p.user = sconf.Server.Username + p.pass = sconf.Server.Password } - addrHTTP = getFullAddress(sconf.Server.Address) - p.httpServerAddr = addrHTTP - certFile = expandPath(sconf.Server.CertFile) - keyFile = expandPath(sconf.Server.KeyFile) - p.user = sconf.Server.Username - p.pass = sconf.Server.Password p.proxychain = sconf.Chain p.proxylist = sconf.ProxyList p.availProxyList = make([]proxyEntry, 0, len(p.proxylist)) @@ -746,13 +862,15 @@ func New(conf *Config) *proxyapp { } p.rrIndexReset = rrIndexMax } else { + if !tproxyonly { + addrHTTP = getFullAddress(conf.AddrHTTP) + p.httpServerAddr = addrHTTP + certFile = expandPath(conf.CertFile) + keyFile = expandPath(conf.KeyFile) + p.user = conf.ServerUser + p.pass = conf.ServerPass + } addrSOCKS = getFullAddress(conf.AddrSOCKS) - addrHTTP = getFullAddress(conf.AddrHTTP) - p.httpServerAddr = addrHTTP - certFile = expandPath(conf.CertFile) - keyFile = expandPath(conf.KeyFile) - p.user = conf.ServerUser - p.pass = conf.ServerPass auth := proxy.Auth{ User: conf.User, Password: conf.Pass, @@ -762,54 +880,63 @@ func New(conf *Config) *proxyapp { p.logger.Fatal().Err(err).Msg("Unable to create SOCKS5 dialer") } p.sockDialer = dialer - p.sockClient = &http.Client{ + if !tproxyonly { + p.sockClient = &http.Client{ + Transport: &http.Transport{ + Dial: dialer.Dial, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + } + } + if !tproxyonly { + hs := &http.Server{ + Addr: addrHTTP, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + Protocols: new(http.Protocols), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + }, + }, + } + hs.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + hs.Protocols.SetHTTP1(true) + p.httpServer = hs + p.httpClient = &http.Client{ Transport: &http.Transport{ - Dial: dialer.Dial, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } } - hs := &http.Server{ - Addr: addrHTTP, - ReadTimeout: readTimeout, - WriteTimeout: writeTimeout, - MaxHeaderBytes: 1 << 20, - Protocols: new(http.Protocols), - TLSConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, - CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, - CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, - }, - }, - } - hs.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) - hs.Protocols.SetHTTP1(true) - p.httpServer = hs - p.httpClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } if conf.ServerConfPath != "" { p.logger.Info().Msgf("SOCKS5 Proxy [%s] chain: %s", p.proxychain.Type, addrSOCKS) } else { p.logger.Info().Msgf("SOCKS5 Proxy: %s", addrSOCKS) } - if certFile != "" && keyFile != "" { - p.certFile = certFile - p.keyFile = keyFile - p.logger.Info().Msgf("HTTPS Proxy: %s", p.httpServerAddr) - } else { - p.logger.Info().Msgf("HTTP Proxy: %s", p.httpServerAddr) + if !tproxyonly { + if certFile != "" && keyFile != "" { + p.certFile = certFile + p.keyFile = keyFile + p.logger.Info().Msgf("HTTPS Proxy: %s", p.httpServerAddr) + } else { + p.logger.Info().Msgf("HTTP Proxy: %s", p.httpServerAddr) + } + } + if p.tproxyAddr != "" { + p.logger.Info().Msgf("TPROXY: %s", p.tproxyAddr) } return &p } diff --git a/resources/example_gohpts.yaml b/resources/example_gohpts.yaml index 3d08f45..d4b6c9d 100644 --- a/resources/example_gohpts.yaml +++ b/resources/example_gohpts.yaml @@ -34,7 +34,7 @@ proxy_list: - address: 127.0.0.1:1081 - address: :1082 # empty host means localhost server: - address: 127.0.0.1:8080 # the only required field in this section + address: 127.0.0.1:8080 # the only required field in this section (ignored when -T flag specified) # these are for adding basic authentication username: username password: password diff --git a/tproxy_linux.go b/tproxy_linux.go new file mode 100644 index 0000000..315ad2d --- /dev/null +++ b/tproxy_linux.go @@ -0,0 +1,196 @@ +//go:build linux +// +build linux + +package gohpts + +import ( + "context" + "errors" + "fmt" + "net" + "net/netip" + "sync" + "syscall" + "time" + "unsafe" + + "golang.org/x/net/proxy" + "golang.org/x/sys/unix" +) + +type tproxyServer struct { + listener net.Listener + quit chan struct{} + wg sync.WaitGroup + pa *proxyapp +} + +func newTproxyServer(pa *proxyapp) *tproxyServer { + ts := &tproxyServer{ + quit: make(chan struct{}), + pa: pa, + } + // https://iximiuz.com/en/posts/go-net-http-setsockopt-example/ + 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.IPPROTO_TCP, unix.TCP_USER_TIMEOUT, int(timeout.Milliseconds())) + operr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + if ts.pa.tproxyMode == "tproxy" { + operr = unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1) + } + }); err != nil { + return err + } + return operr + }, + } + + ln, err := lc.Listen(context.Background(), "tcp4", ts.pa.tproxyAddr) + if err != nil { + var msg string + if errors.Is(err, unix.EPERM) { + msg = "try `sudo setcap 'cap_net_admin+ep` for the binary:" + } + ts.pa.logger.Fatal().Err(err).Msg(msg) + } + ts.listener = ln + return ts +} + +func (ts *tproxyServer) ListenAndServe() { + ts.wg.Add(1) + go ts.serve() +} + +func (ts *tproxyServer) serve() { + defer ts.wg.Done() + + for { + conn, err := ts.listener.Accept() + if err != nil { + select { + case <-ts.quit: + return + default: + ts.pa.logger.Error().Err(err).Msg("") + } + } else { + ts.wg.Add(1) + err := conn.SetDeadline(time.Now().Add(timeout)) + if err != nil { + ts.pa.logger.Error().Err(err).Msg("") + } + go func() { + ts.handleConnection(conn) + ts.wg.Done() + }() + } + } +} + +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) + if e != 0 { + return e + } + return nil +} + +func (ts *tproxyServer) getOriginalDst(rawConn syscall.RawConn) (string, error) { + var originalDst unix.RawSockaddrInet4 + err := rawConn.Control(func(fd uintptr) { + optlen := uint32(unsafe.Sizeof(originalDst)) + err := getsockopt(int(fd), unix.SOL_IP, unix.SO_ORIGINAL_DST, unsafe.Pointer(&originalDst), &optlen) + if err != nil { + ts.pa.logger.Error().Err(err).Msg("[tproxy] getsockopt SO_ORIGINAL_DST failed") + } + }) + if err != nil { + ts.pa.logger.Error().Err(err).Msg("[tproxy] Failed invoking control connection") + return "", err + } + dstHost := netip.AddrFrom4(originalDst.Addr) + dstPort := uint16(originalDst.Port<<8) | originalDst.Port>>8 + return fmt.Sprintf("%s:%d", dstHost, dstPort), nil +} + +func (ts *tproxyServer) handleConnection(srcConn net.Conn) { + var ( + dstConn net.Conn + dst string + err error + ) + defer srcConn.Close() + switch ts.pa.tproxyMode { + case "redirect": + rawConn, err := srcConn.(*net.TCPConn).SyscallConn() + if err != nil { + ts.pa.logger.Error().Err(err).Msg("[tproxy] Failed to get raw connection") + return + } + dst, err = ts.getOriginalDst(rawConn) + if err != nil { + ts.pa.logger.Error().Err(err).Msg("[tproxy] Failed to get destination address") + return + } + ts.pa.logger.Debug().Msgf("[tproxy] getsockopt SO_ORIGINAL_DST %s", dst) + case "tproxy": + dst = srcConn.LocalAddr().String() + ts.pa.logger.Debug().Msgf("[tproxy] IP_TRANSPARENT %s", dst) + default: + ts.pa.logger.Fatal().Msg("Unknown tproxyMode") + } + if isLocalAddress(dst) { + dstConn, err = net.DialTimeout("tcp", dst, timeout) + if err != nil { + ts.pa.logger.Error().Err(err).Msgf("[tproxy] Failed connecting to %s", dst) + return + } + } else { + sockDialer, _, err := ts.pa.getSocks() + if err != nil { + ts.pa.logger.Error().Err(err).Msg("[tproxy] Failed getting SOCKS5 client") + return + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + dstConn, err = sockDialer.(proxy.ContextDialer).DialContext(ctx, "tcp", dst) + if err != nil { + ts.pa.logger.Error().Err(err).Msgf("[tproxy] Failed connecting to %s", dst) + return + } + } + defer dstConn.Close() + + dstConnStr := fmt.Sprintf("%s->%s->%s", dstConn.LocalAddr().String(), dstConn.RemoteAddr().String(), dst) + srcConnStr := fmt.Sprintf("%s->%s", srcConn.LocalAddr().String(), srcConn.RemoteAddr().String()) + + ts.pa.logger.Debug().Msgf("[tproxy] src: %s - dst: %s", srcConnStr, dstConnStr) + + 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) + wg.Wait() +} + +func (ts *tproxyServer) Shutdown() { + close(ts.quit) + ts.listener.Close() + done := make(chan struct{}) + go func() { + ts.wg.Wait() + close(done) + }() + + select { + case <-done: + ts.pa.logger.Info().Msg("[tproxy] Server gracefully shutdown") + return + case <-time.After(timeout): + ts.pa.logger.Error().Msg("[tproxy] Server timed out waiting for connections to finish") + return + } +} diff --git a/tproxy_nonlinux.go b/tproxy_nonlinux.go new file mode 100644 index 0000000..d789aa8 --- /dev/null +++ b/tproxy_nonlinux.go @@ -0,0 +1,42 @@ +//go:build !linux +// +build !linux + +package gohpts + +import ( + "net" + "sync" + "syscall" +) + +type tproxyServer struct { + listener net.Listener + quit chan struct{} + wg sync.WaitGroup + pa *proxyapp +} + +func newTproxyServer(pa *proxyapp) *tproxyServer { + _ = pa + return nil +} + +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() {} diff --git a/version.go b/version.go index d511b7d..a176753 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package gohpts -const Version string = "gohpts v1.5.0" +const Version string = "gohpts v1.6.0"