Net.http: DialContext

Hi,

I’m looking for a way to find out the local (ephemeral) TCP port number used in a HTTP client connection; I’m using this to be able to match results from a webcrawler with pcaps. I did find the following way to do it, and it seems to work, but I’m not sure if maybe I’m missing something and could run into problems in the future; the idea is to install a hook between the transport’s DialContext and net’s Dial:

func myDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
	conn, err := net.Dial(network, addr) // forward to original TCP dialer
	localAddr := conn.LocalAddr().String()
	localPort, _ := strconv.Atoi(localAddr[strings.Index(localAddr, ":")+1:])
	fmt.Printf("Local port: %d\n", localPort)
	return conn, err
}

...
	tr := &http.Transport{DialContext: myDialContext, ...}
	client := &http.client{Transport: tr, ...}
	...
	resp, err := client.Do(req)
...

Is this the optimal way to do it, or is there additional stuff in myDialContext that I should add?

In addition, what’s the best way to get the local port back to the caller? A global variable is not thread safe (and I will have several goroutines in the end), and I see no way to store that information in ctx, as this is defined and initialized outside my control… thread local store would be possible, but only using special modules, and not recommended.
Thanks, Andy

Hi Andy,

That’s an OK way to do what you want. The reason why it’s so complicated and ugly is that you are not sure you have a port at all.

For example, if your connection is a unix socket, there is no port. For that reason, I would write safer code: first check that your network variable is either “tcp” or “udp”. Secondly, check the result of strings.Index (might be smaller than zero) and the error returned by Atoi.

If all these are successful, return your connection, your local port and the error as nil.

There is no need for a thread local store (in Go there are no local threads), just pass around a struct pointer with the context data you need.

Makes sense?

That makes sense, thank you. My problem about passing around a struct pointer is how to actually do it: myDialContext is a callback function where I can’t change the prototypes/arguments (like adding another structure to store the information). I thought about embedding context.Context into my own context structure myContext with context.Context as anonymous field, but that is not accepted (cannot use myDialContext (type func(myContext, string, string) (net.Conn, error)) as type func(context.Context, string, string) (net.Conn, error) in field value) - and even if it were, I would not get this structure back in my original call (the client.Do(..)), so I don’t know how to pass this information to that location. But as I’m new to the concept, I might just miss something trivial?

I also thought about using a channel for that, but how does the callback function know which channel to store the data? The best I can currently think about is a global map with at least the hostname is key (as this is in some way available at both ends). Not a good solution though.

You can use a closure or a method value:

https://play.golang.org/p/ulWUzdvOOI0

Hi Jakob,
Really cool, that works perfectly… I wasn’t aware that a method in your own structure can be used as a callback function and then use that as closure :wink: I’m not sure what your next element is required for? I ‘simply’ tried (and it seems to work, though not yet heavily tested):

type portStr struct {
	localPort int
}

func (portInfo *portStr) myDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
	conn, err := net.Dial(network, addr) // forward to original TCP dialer
	localAddr := conn.LocalAddr().String()
	portInfo.localPort, _ := strconv.Atoi(localAddr[strings.Index(localAddr, ":")+1:])

	return conn, err
}

...
	var portInfo portStr
	tr := &http.Transport{DialContext: portInfo.myDialContext, ...}
	client := &http.client{Transport: tr, ...}
	...
	resp, err := client.Do(req)
	fmt.Printf("Local port: %d\n", portInfo.localPort)
...

Just to get an empty dialer for the DialContext method, and someplace to set dialer parameters.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.