diff --git a/README.md b/README.md index 4a6557f..2a65e19 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ You can download the binary for your platform from [Releases](https://github.com Example: ```shell -HPTS_RELEASE=v1.9.2; 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 +GOHPTS_RELEASE=v1.9.3; wget -v https://github.com/shadowy-pycoder/go-http-proxy-to-socks/releases/download/$GOHPTS_RELEASE/gohpts-$GOHPTS_RELEASE-linux-amd64.tar.gz -O gohpts && tar xvzf gohpts && mv -f gohpts-$GOHPTS_RELEASE-linux-amd64 gohpts && ./gohpts -h ``` Alternatively, you can install it using `go install` command (requires Go [1.24](https://go.dev/doc/install) or later): @@ -142,15 +142,17 @@ Options: -h Show this help message and exit -v Show version and build information -D Run as a daemon (provide -logfile to see logs) + -I Display list of network interfaces and exit Proxy: - -l Address of HTTP proxy server (default "127.0.0.1:8080") - -s Address of SOCKS5 proxy server (default "127.0.0.1:1080") + -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) + -i Bind proxy to specific network interface (either by interface name or index) + -f Path to server configuration file in YAML format (overrides proxy flags above) Logs: -d Show logs in DEBUG mode @@ -279,7 +281,8 @@ 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) + interface: "eth0" # if specified, overrides server address # these are for adding basic authentication username: username password: password diff --git a/cmd/gohpts/cli.go b/cmd/gohpts/cli.go index 43102a8..f8019fb 100644 --- a/cmd/gohpts/cli.go +++ b/cmd/gohpts/cli.go @@ -8,6 +8,7 @@ import ( "slices" gohpts "github.com/shadowy-pycoder/go-http-proxy-to-socks" + "github.com/shadowy-pycoder/mshark/network" "golang.org/x/term" ) @@ -33,15 +34,17 @@ Options: -h Show this help message and exit -v Show version and build information -D Run as a daemon (provide -logfile to see logs) + -I Display list of network interfaces and exit Proxy: - -l Address of HTTP proxy server (default "127.0.0.1:8080") - -s Address of SOCKS5 proxy server (default "127.0.0.1:1080") + -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) + -i Bind proxy to specific network interface (either by interface name or index) + -f Path to server configuration file in YAML format (overrides proxy flags above) Logs: -d Show logs in DEBUG mode @@ -90,6 +93,15 @@ func root(args []string) error { "", "Path to server configuration file in YAML format (overrides other proxy flags)", ) + flags.StringVar(&conf.Interface, "i", "", "Bind proxy to specific network interface") + flags.BoolFunc("I", "Display list of network interfaces and exit", func(flagValue string) error { + if err := network.DisplayInterfaces(); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", app, err) + os.Exit(2) + } + os.Exit(0) + return nil + }) 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)") @@ -157,7 +169,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, -sniff, -snifflog and -j flags") + return fmt.Errorf("-T flag does not work with -U, -c, -k, -l flags") } } if !seen["M"] { @@ -180,12 +192,9 @@ func root(args []string) error { } } if seen["f"] { - for _, da := range []string{"s", "u", "U", "c", "k", "l"} { + for _, da := range []string{"s", "u", "U", "c", "k", "l", "i"} { if seen[da] { - if runtime.GOOS == tproxyOS { - 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, -sniff, -snifflog and -j flags") + return fmt.Errorf("-f flag does not work with other proxy flags specified") } } } diff --git a/go.mod b/go.mod index b27dfec..f526f67 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.8 + github.com/shadowy-pycoder/mshark v0.0.9 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 ff783d4..921720b 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,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.8 h1:7kuVgX9Qp4Q9nGl9Gi7UOaNFUnOF0I2Vpfmc4X3GLug= -github.com/shadowy-pycoder/mshark v0.0.8/go.mod h1:FqbHFdsx0zMnrZZH0+oPzaFcleP4O+tUWv8i5gxo87k= +github.com/shadowy-pycoder/mshark v0.0.9 h1:mMHmkqUpkSlkt74DaSkNjhvO0nJ0AxZiYPH6QbllB9A= +github.com/shadowy-pycoder/mshark v0.0.9/go.mod h1:FqbHFdsx0zMnrZZH0+oPzaFcleP4O+tUWv8i5gxo87k= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= diff --git a/gohpts.go b/gohpts.go index b68186a..1866948 100644 --- a/gohpts.go +++ b/gohpts.go @@ -36,6 +36,7 @@ import ( "github.com/shadowy-pycoder/colors" "github.com/shadowy-pycoder/mshark/arpspoof" "github.com/shadowy-pycoder/mshark/layers" + "github.com/shadowy-pycoder/mshark/network" "golang.org/x/net/proxy" ) @@ -94,6 +95,7 @@ type Config struct { ServerPass string CertFile string KeyFile string + Interface string ServerConfPath string TProxy string TProxyOnly string @@ -120,6 +122,7 @@ type proxyapp struct { certFile string keyFile string httpServerAddr string + iface *net.Interface tproxyAddr string tproxyMode string auto bool @@ -1349,7 +1352,7 @@ func (p *proxyapp) applyRedirectRules() string { iptables -t mangle -X GOHPTS 2>/dev/null || true ip rule del fwmark 1 lookup 100 2>/dev/null || true - ip route flush table 100 || true + ip route flush table 100 2>/dev/null || true `) cmdClear.Stdout = os.Stdout cmdClear.Stderr = os.Stderr @@ -1431,9 +1434,14 @@ func (p *proxyapp) applyRedirectRules() string { if err := cmdClearForward.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") + var iface *net.Interface + if p.iface != nil { + iface = p.iface + } else { + iface, err = getDefaultInterface() + if err != nil { + p.logger.Fatal().Err(err).Msg("failed getting default network interface") + } } cmdForwardFilter := exec.Command("bash", "-c", fmt.Sprintf(` set -ex @@ -1487,7 +1495,7 @@ func (p *proxyapp) clearRedirectRules(output string) error { iptables -t mangle -X GOHPTS 2>/dev/null || true ip rule del fwmark 1 lookup 100 2>/dev/null || true - ip route flush table 100 || true + ip route flush table 100 2>/dev/null || true sysctl -w net.ipv4.ip_forward=%s `, output)) cmd.Stdout = os.Stdout @@ -1629,11 +1637,12 @@ func (pe proxyEntry) String() string { } type server struct { - Address string `yaml:"address"` - Username string `yaml:"username,omitempty"` - Password string `yaml:"password,omitempty"` - CertFile string `yaml:"cert_file,omitempty"` - KeyFile string `yaml:"key_file,omitempty"` + Address string `yaml:"address"` + Interface string `yaml:"interface,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + CertFile string `yaml:"cert_file,omitempty"` + KeyFile string `yaml:"key_file,omitempty"` } type chain struct { Type string `yaml:"type"` @@ -1646,30 +1655,34 @@ type serverConfig struct { Server server `yaml:"server"` } -func getFullAddress(v string, all bool) (string, error) { +func getFullAddress(v, ip string, all bool) (string, error) { if v == "" { return "", nil } - ip := "127.0.0.1" + ipAddr := "127.0.0.1" if all { - ip = "0.0.0.0" + ipAddr = "0.0.0.0" } if port, err := strconv.Atoi(v); err == nil { - return fmt.Sprintf("%s:%d", ip, port), nil + if ip != "" { + return fmt.Sprintf("%s:%d", ip, port), nil + } else { + return fmt.Sprintf("%s:%d", ipAddr, port), nil + } } host, port, err := net.SplitHostPort(v) if err != nil { return "", err } - if host != "" && port == "" { + if port == "" { return "", fmt.Errorf("port is missing") } - if host != "" && port != "" { - return v, nil - } else if port != "" { + if ip != "" { return fmt.Sprintf("%s:%s", ip, port), nil + } else if host == "" { + return fmt.Sprintf("%s:%s", ipAddr, port), nil } - return "", fmt.Errorf("failed parsing address") + return fmt.Sprintf("%s:%s", host, port), nil } func expandPath(p string) string { @@ -1682,6 +1695,17 @@ func expandPath(p string) string { return p } +func getAddressFromInterface(iface *net.Interface) (string, error) { + if iface == nil { + return "", nil + } + prefix, err := network.GetIPv4PrefixFromInterface(iface) + if err != nil { + return "", err + } + return prefix.Addr().String(), nil +} + func New(conf *Config) *proxyapp { var logger, snifflogger zerolog.Logger var p proxyapp @@ -1814,42 +1838,34 @@ func New(conf *Config) *proxyapp { conf.TProxy = "" conf.TProxyOnly = "" conf.TProxyMode = "" - p.logger.Warn().Msgf("[%s] functionality only available on linux system", conf.TProxyMode) + p.logger.Warn().Msgf("[%s] functionality only available on linux systems", conf.TProxyMode) } p.tproxyMode = conf.TProxyMode tproxyonly := conf.TProxyOnly != "" + var tAddr string if tproxyonly { - if p.tproxyMode != "" { - p.tproxyAddr, err = getFullAddress(conf.TProxyOnly, true) - if err != nil { - p.logger.Fatal().Err(err).Msg("") - } - } else { - p.tproxyAddr, err = getFullAddress(conf.TProxyOnly, false) - if err != nil { - p.logger.Fatal().Err(err).Msg("") - } + tAddr = conf.TProxyOnly + } else { + tAddr = conf.TProxy + } + if p.tproxyMode != "" { + p.tproxyAddr, err = getFullAddress(tAddr, "", true) + if err != nil { + p.logger.Fatal().Err(err).Msg("") } } else { - if p.tproxyMode != "" { - p.tproxyAddr, err = getFullAddress(conf.TProxy, true) - if err != nil { - p.logger.Fatal().Err(err).Msg("") - } - } else { - p.tproxyAddr, err = getFullAddress(conf.TProxy, false) - if err != nil { - p.logger.Fatal().Err(err).Msg("") - } + p.tproxyAddr, err = getFullAddress(tAddr, "", false) + if err != nil { + p.logger.Fatal().Err(err).Msg("") } } p.auto = conf.Auto if p.auto && runtime.GOOS != "linux" { - p.logger.Fatal().Msg("Auto setup is available only for linux system") + p.logger.Fatal().Msg("Auto setup is available only on linux systems") } p.mark = conf.Mark if p.mark > 0 && runtime.GOOS != "linux" { - p.logger.Fatal().Msg("SO_MARK is available only for linux system") + p.logger.Fatal().Msg("SO_MARK is available only on linux systems") } if p.mark > 0xFFFFFFFF { p.logger.Fatal().Msg("SO_MARK is out of range") @@ -1857,58 +1873,40 @@ func New(conf *Config) *proxyapp { if p.mark == 0 && p.tproxyMode == "tproxy" { p.mark = 100 } - if conf.ARPSpoof != "" { - if runtime.GOOS != "linux" { - p.logger.Fatal().Msg("ARP spoof setup is available only for linux system") - } - if !p.auto { - p.logger.Warn().Msg("ARP spoof setup requires iptables configuration") - } - asc := &arpspoof.ARPSpoofConfig{Logger: p.logger} - errMsg := `Failed parsing arp options. Example: "targets 10.0.0.1,10.0.0.5-10,192.168.1.*,192.168.10.0/24;fullduplex false;debug true"` - for opt := range strings.SplitSeq(strings.ToLower(conf.ARPSpoof), ";") { - keyval := strings.SplitN(strings.Trim(opt, " "), " ", 2) - if len(keyval) < 2 { - p.logger.Fatal().Msg(errMsg) - } - key := keyval[0] - val := keyval[1] - switch key { - case "targets": - asc.Targets = val - case "fullduplex": - if val == "true" { - asc.FullDuplex = true - } - case "debug": - if val == "true" { - asc.Debug = true - } - default: - p.logger.Fatal().Msg(errMsg) - } - } - p.arpspoofer, err = arpspoof.NewARPSpoofer(asc) - if err != nil { - p.logger.Fatal().Err(err).Msg("Failed creating arp spoofer") - } - } var addrHTTP, addrSOCKS, certFile, keyFile string if conf.ServerConfPath != "" { var sconf serverConfig yamlFile, err := os.ReadFile(expandPath(conf.ServerConfPath)) if err != nil { - p.logger.Fatal().Err(err).Msg("[server config] Parsing failed") + p.logger.Fatal().Err(err).Msg("[yaml config] Parsing failed") } err = yaml.Unmarshal(yamlFile, &sconf) if err != nil { - p.logger.Fatal().Err(err).Msg("[server config] Parsing failed") + p.logger.Fatal().Err(err).Msg("[yaml config] Parsing failed") } if !tproxyonly { if sconf.Server.Address == "" { - p.logger.Fatal().Err(err).Msg("[server config] Server address is empty") + p.logger.Fatal().Err(err).Msg("[yaml config] Server address is empty") + } + if sconf.Server.Interface != "" && sconf.Server.Interface != "any" && conf.Interface != "0" { + p.iface, err = net.InterfaceByName(sconf.Server.Interface) + if err != nil { + if ifIdx, err := strconv.Atoi(sconf.Server.Interface); err == nil { + p.iface, err = net.InterfaceByIndex(ifIdx) + if err != nil { + p.logger.Warn().Err(err).Msgf("Failed binding to %s, using default interface", sconf.Server.Interface) + } + } else { + p.logger.Warn().Err(err).Msgf("Failed binding to %s, using default interface", sconf.Server.Interface) + } + } } - addrHTTP, err = getFullAddress(sconf.Server.Address, false) + iAddr, err := getAddressFromInterface(p.iface) + if err != nil { + p.iface = nil + p.logger.Warn().Err(err).Msgf("Failed binding to %s, using default interface", sconf.Server.Interface) + } + addrHTTP, err = getFullAddress(sconf.Server.Address, iAddr, false) if err != nil { p.logger.Fatal().Err(err).Msg("") } @@ -1922,11 +1920,11 @@ func New(conf *Config) *proxyapp { p.proxylist = sconf.ProxyList p.availProxyList = make([]proxyEntry, 0, len(p.proxylist)) if len(p.proxylist) == 0 { - p.logger.Fatal().Msg("[server config] Proxy list is empty") + p.logger.Fatal().Msg("[yaml config] Proxy list is empty") } seen := make(map[string]struct{}) for idx, pr := range p.proxylist { - addr, err := getFullAddress(pr.Address, false) + addr, err := getFullAddress(pr.Address, "", false) if err != nil { p.logger.Fatal().Err(err).Msg("") } @@ -1934,18 +1932,36 @@ func New(conf *Config) *proxyapp { seen[addr] = struct{}{} p.proxylist[idx].Address = addr } else { - p.logger.Fatal().Msgf("[server config] Duplicate entry `%s`", addr) + p.logger.Fatal().Msgf("[yaml config] Duplicate entry `%s`", addr) } } addrSOCKS = p.printProxyChain(p.proxylist) chainType := p.proxychain.Type if !slices.Contains(supportedChainTypes, chainType) { - p.logger.Fatal().Msgf("[server config] Chain type `%s` is not supported", chainType) + p.logger.Fatal().Msgf("[yaml config] Chain type `%s` is not supported", chainType) } p.rrIndexReset = rrIndexMax } else { if !tproxyonly { - addrHTTP, err = getFullAddress(conf.AddrHTTP, false) + if conf.Interface != "" && conf.Interface != "any" && conf.Interface != "0" { + p.iface, err = net.InterfaceByName(conf.Interface) + if err != nil { + if ifIdx, err := strconv.Atoi(conf.Interface); err == nil { + p.iface, err = net.InterfaceByIndex(ifIdx) + if err != nil { + p.logger.Warn().Err(err).Msgf("Failed binding to %s, using default interface", conf.Interface) + } + } else { + p.logger.Warn().Err(err).Msgf("Failed binding to %s, using default interface", conf.Interface) + } + } + } + iAddr, err := getAddressFromInterface(p.iface) + if err != nil { + p.logger.Warn().Err(err).Msgf("Failed binding to %s, using default interface", conf.Interface) + p.iface = nil + } + addrHTTP, err = getFullAddress(conf.AddrHTTP, iAddr, false) if err != nil { p.logger.Fatal().Err(err).Msg("") } @@ -1955,7 +1971,7 @@ func New(conf *Config) *proxyapp { p.user = conf.ServerUser p.pass = conf.ServerPass } - addrSOCKS, err = getFullAddress(conf.AddrSOCKS, false) + addrSOCKS, err = getFullAddress(conf.AddrSOCKS, "", false) if err != nil { p.logger.Fatal().Err(err).Msg("") } @@ -2011,6 +2027,45 @@ func New(conf *Config) *proxyapp { Timeout: timeout, } } + if conf.ARPSpoof != "" { + if runtime.GOOS != "linux" { + p.logger.Fatal().Msg("ARP spoof setup is available only on linux systems") + } + if !p.auto { + p.logger.Warn().Msg("ARP spoof setup requires iptables configuration") + } + asc := &arpspoof.ARPSpoofConfig{Logger: p.logger} + errMsg := `Failed parsing arp options. Example: "targets 10.0.0.1,10.0.0.5-10,192.168.1.*,192.168.10.0/24;fullduplex false;debug true"` + for opt := range strings.SplitSeq(strings.ToLower(conf.ARPSpoof), ";") { + keyval := strings.SplitN(strings.Trim(opt, " "), " ", 2) + if len(keyval) < 2 { + p.logger.Fatal().Msg(errMsg) + } + key := keyval[0] + val := keyval[1] + switch key { + case "targets": + asc.Targets = val + case "fullduplex": + if val == "true" { + asc.FullDuplex = true + } + case "debug": + if val == "true" { + asc.Debug = true + } + default: + p.logger.Fatal().Msg(errMsg) + } + } + if p.iface != nil { + asc.Interface = p.iface.Name + } + p.arpspoofer, err = arpspoof.NewARPSpoofer(asc) + if err != nil { + p.logger.Fatal().Err(err).Msg("Failed creating arp spoofer") + } + } if conf.ServerConfPath != "" { p.logger.Info().Msgf("SOCKS5 Proxy [%s] chain: %s", p.proxychain.Type, addrSOCKS) } else { diff --git a/resources/example_gohpts.yaml b/resources/example_gohpts.yaml index d4b6c9d..fae35bf 100644 --- a/resources/example_gohpts.yaml +++ b/resources/example_gohpts.yaml @@ -1,9 +1,9 @@ # Explanations for chains taken from /etc/proxychains4.conf - + # strict - Each connection will be done via chained proxies # all proxies chained in the order as they appear in the list # all proxies must be online to play in chain - + # dynamic - Each connection will be done via chained proxies # all proxies chained in the order as they appear in the list # at least one proxy must be online to play in chain @@ -35,8 +35,9 @@ proxy_list: - address: :1082 # empty host means localhost server: address: 127.0.0.1:8080 # the only required field in this section (ignored when -T flag specified) + interface: "eth0" # if specified, overrides server address # these are for adding basic authentication - username: username + username: username password: password # comment out these to use HTTP instead of HTTPS cert_file: ~/local.crt diff --git a/tproxy_linux.go b/tproxy_linux.go index 542aa46..8aa92cc 100644 --- a/tproxy_linux.go +++ b/tproxy_linux.go @@ -4,13 +4,11 @@ package gohpts import ( - "bufio" "context" "errors" "fmt" "net" "net/netip" - "os" "strings" "sync" "syscall" @@ -19,6 +17,7 @@ import ( "github.com/shadowy-pycoder/colors" "github.com/shadowy-pycoder/mshark/layers" + "github.com/shadowy-pycoder/mshark/network" "golang.org/x/net/proxy" "golang.org/x/sys/unix" ) @@ -261,21 +260,5 @@ func getBaseDialer(timeout time.Duration, mark uint) *net.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) + return network.GetDefaultInterface() } diff --git a/version.go b/version.go index 0366c84..ab305fa 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package gohpts -const Version string = "gohpts v1.9.2" +const Version string = "gohpts v1.9.3"