Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example for SOCKS proxy chaining #573

Merged
merged 20 commits into from
Dec 20, 2024
Merged

Conversation

yuisofull
Copy link
Contributor

@yuisofull yuisofull commented Dec 18, 2024

I add an example demonstrating how to configure goproxy to forward both HTTP and HTTPS traffic through a SOCKS5 proxy. Since the SOCKS5 dialer doesn't handle DNS resolution, I manually resolve the target addresses before passing them to the SOCKS5 proxy.

@ErikPelli
Copy link
Collaborator

Instead of using net/proxy, can you directly use the standard library (https://pkg.go.dev/net/http#Transport), where they states that support socks5 as a proxy schema?
It's enough to change the transport with one that has this proxy function that returns a socks URL

@ErikPelli
Copy link
Collaborator

Maybe you can make a variant of cascadeproxy where the end proxy is a socks server, it's up to you

@ErikPelli
Copy link
Collaborator

Oh, last thing, I've rewritten the cascadeproxy in the rewrite-examples branch. rewrite-examples

If you decide to base the code on this example, use the code from this branch and not from the master, please

@yuisofull
Copy link
Contributor Author

Thanks alot for your help. I'm having issue on creating ConnectDial without the socks client

@ErikPelli
Copy link
Collaborator

What's the issue?

@yuisofull
Copy link
Contributor Author

Im doing sth like:

proxyServer.Tr.Proxy = func(r *http.Request) (*url.URL, error) {
		Url := &url.URL{
			Scheme: "socks5",
			Host:   socksAddr,
		}
		if auth.User != "" {
			Url.User = url.UserPassword(auth.User, auth.Password)
		}
		return Url, nil
	}

Without the ConnectDial, it will only redirect the http request to socks5. And in the case that instead of using net/proxy, Ill try to do the same thing with func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler, pass in the socks5 proxy address, and then implement the raw handshake with socks and connect to it

@ErikPelli
Copy link
Collaborator

Yes, ConnectDial is required, this morning I updated the cascadeproxy example to include it, I confirm that it's required

@yuisofull
Copy link
Contributor Author

I have rewritten the code and implemented the ConnectDial

@ErikPelli
Copy link
Collaborator

Can you call the exampe cascadeproxy-socks instead of the current name?

Comment on lines 1 to 253
return nil, err
}
if b[0] != Version5 {
return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0])))
}
if cmdErr := Reply(b[1]); cmdErr != StatusSucceeded {
return nil, errors.New("unknown error " + cmdErr.String())
}
if b[2] != 0 {
return nil, errors.New("non-zero reserved field")
}
l := 2
var a Addr
switch b[3] {
case AddrTypeIPv4:
l += net.IPv4len
a.IP = make(net.IP, net.IPv4len)
case AddrTypeIPv6:
l += net.IPv6len
a.IP = make(net.IP, net.IPv6len)
case AddrTypeFQDN:
if _, err := io.ReadFull(c, b[:1]); err != nil {
return nil, err
}
l += int(b[0])
default:
return nil, errors.New("unknown address type " + strconv.Itoa(int(b[3])))
}
if cap(b) < l {
b = make([]byte, l)
} else {
b = b[:l]
}
if _, err = io.ReadFull(c, b); err != nil {
return nil, err
}
if a.IP != nil {
copy(a.IP, b)
} else {
a.Name = string(b[:len(b)-2])
}
a.Port = int(b[len(b)-2])<<8 | int(b[len(b)-1])
return c, nil
}, nil
}

const (
authUsernamePasswordVersion = 0x01
authStatusSucceeded = 0x00
)

func (up *SocksAuth) Authenticate(ctx context.Context, rw io.ReadWriter, auth AuthMethod) error {
switch auth {
case AuthMethodNotRequired:
return nil
case AuthMethodUsernamePassword:
if len(up.Username) == 0 || len(up.Username) > 255 || len(up.Password) > 255 {
return errors.New("invalid username/password")
}
b := []byte{authUsernamePasswordVersion}
b = append(b, byte(len(up.Username)))
b = append(b, up.Username...)
b = append(b, byte(len(up.Password)))
b = append(b, up.Password...)

if _, err := rw.Write(b); err != nil {
return err
}
if _, err := io.ReadFull(rw, b[:2]); err != nil {
return err
}
if b[0] != authUsernamePasswordVersion {
return errors.New("invalid username/password version")
}
if b[1] != authStatusSucceeded {
return errors.New("username/password authentication failed")
}
return nil
}
return errors.New("unsupported authentication method " + strconv.Itoa(int(auth)))
}

func splitHostPort(address string) (string, int, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return "", 0, err
}
portnum, err := strconv.Atoi(port)
if err != nil {
return "", 0, err
}
if 1 > portnum || portnum > 0xffff {
return "", 0, errors.New("port number out of range " + port)
}
ip, err := net.ResolveIPAddr("ip", host)
if err != nil {
return "", 0, err
}
return ip.String(), portnum, nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of copying the whole file, isn't there something exposed in that packages (or other packages of standard library) that just allows the opening of a dialed connection through a target SOCKS server?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the "net/http" does support but in order to redirect https request I need a socks5 client that can satisfy the ConnectDial

@yuisofull
Copy link
Contributor Author

I dont know why but in my case if I dont resolve the ip address myself it would have something like this:
socks connect tcp 127.0.0.1:1080->www.google.com:443: unknown error host unreachable

@yuisofull
Copy link
Contributor Author

with the standard library I can do something like(work for both http and https):

package main

import (
	"crypto/tls"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

type SocksAuth struct {
	User, Password string
}

func createSocksProxy(socksAddr string, auth SocksAuth) func(r *http.Request) (*url.URL, error) {
	return func(r *http.Request) (*url.URL, error) {
		host := r.URL.Host
		if !strings.ContainsRune(host, ':') {
			host += ":" + r.URL.Port()
		}
		resolvedAddr, _ := resolveAddress(host)
		r.URL.Host = resolvedAddr
		Url := &url.URL{
			Scheme: "socks5",
			Host:   socksAddr,
		}
		if auth.User != "" {
			Url.User = url.UserPassword(auth.User, auth.Password)
		}
		return Url, nil
	}
}
func main() {
	target := flag.String("target", "https://www.google.com", "URL to get")
	proxyAddr := flag.String("proxy", "localhost:1080", "SOCKS5 proxy address to use")
	username := flag.String("user", "", "username for SOCKS5 proxy")
	password := flag.String("pass", "", "password for SOCKS5 proxy")
	flag.Parse()

	auth := SocksAuth{
		User:     *username,
		Password: *password,
	}
	client := &http.Client{
		Transport: &http.Transport{
			Proxy: createSocksProxy(*proxyAddr, auth),
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
			},
		},

		Timeout: 30 * time.Second,
	}
	resolvedAddr, err := resolveAddress(*target)
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest("GET", *target, nil)
	if err != nil {
		log.Fatalf("Failed to create request: %v", err)
	}
	req.URL.Host = resolvedAddr
	r, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer r.Body.Close()
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(body))
}

func resolveAddress(target string) (string, error) {
	u, err := url.Parse(target)
	if err != nil {
		return "", fmt.Errorf("invalid URL: %w", err)
	}
	host, port := u.Hostname(), u.Port()

	ipAddr, err := net.ResolveIPAddr("ip", host)
	if err != nil {
		return "", fmt.Errorf("failed to resolve hostname: %w", err)
	}

	return net.JoinHostPort(ipAddr.String(), port), nil
}

But I havent figured out how to implement ConnectDial with this

@ErikPelli
Copy link
Collaborator

with the standard library I can do something like(work for both http and https):

...
But I havent figured out how to implement ConnectDial with this

We can try to use a different approach:

  1. Intercept the HTTPS request using MITM and avoid to call the destination
  2. When we handle the MITM'd request, we obtain an already parsed req
  3. With this req, instead of let the proxy handle it, we can create a custom HTTP client/transport with the address of the SOCKS proxy specified as proxy
  4. Instead of the default client, we use this custom http client to make the request

What do you think?

@yuisofull
Copy link
Contributor Author

Hmm, Ill try this approach

@ErikPelli
Copy link
Collaborator

If you want, you can also launch a test local SOCKS5 proxy in your example, using this library: https://github.com/things-go/go-socks5

@yuisofull
Copy link
Contributor Author

Im having a local socks5 proxy using dante on linux

@ErikPelli
Copy link
Collaborator

Im having a local socks5 proxy using dante on linux

Yes, but the example should be used by anyone that wants to.
Embedding a socks server inside it would make it easier for the users that want to test it.

@yuisofull
Copy link
Contributor Author

yuisofull commented Dec 20, 2024

Yes, but the example should be used by anyone that wants to. Embedding a socks server inside it would make it easier for the users that want to test it.

I agreed, I will add it, and by the way I think I will use the HijackConnect to redirect the request to socks proxy

@ErikPelli
Copy link
Collaborator

HijackConnect
Don't. It's not good for this use.

@yuisofull
Copy link
Contributor Author

I have removed the socks5 client

@yuisofull
Copy link
Contributor Author

All this logic should be inside the Do / DoFunc function! The connect handler must be only goproxy.AlwaysMitm. This way the proxy server will take over the connection.

Done!

examples/cascadeproxy-socks/socksproxy.go Outdated Show resolved Hide resolved
examples/cascadeproxy-socks/socksproxy.go Outdated Show resolved Hide resolved
@yuisofull
Copy link
Contributor Author

No need for this scheme set and ip resolving part, just define client and then to client.Do

Done

@yuisofull
Copy link
Contributor Author

Can you mention the fact that we do MITM to use the SOCKS proxy and that the proxy forwarding is impemented by the Go standard library?

Ive added

@yuisofull
Copy link
Contributor Author

Ok, you can add a comment about it where you cite this sof answer: https://stackoverflow.com/questions/19595860/http-request-requesturi-field-when-making-request-in-go

Ive added

@ErikPelli
Copy link
Collaborator

Thanks! Now it looks good to me. Sorry If I have bothered you with all the requested changes!

@ErikPelli ErikPelli merged commit 2b1c5fc into elazarl:master Dec 20, 2024
1 check passed
@yuisofull yuisofull deleted the issue-558 branch December 21, 2024 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants