skip to content

What can net.Dialer.Control do that wrapping net.Dial cannot, and when does it run?

level: seniorimportance: nice to knowfreq 24%

answer

  1. a gap a wrapper cannot stand in
  2. the socket exists, the connection does not
  3. it sees where you are really going
  4. returning an error stops that attempt
  5. called once per address tried

basics

~20 s

net.Dialer.Control is a hook called after the socket is created but before it connects, once per address tried, with the resolved address and a raw handle. It can adjust the socket or abort that attempt by returning an error.

solid answer

~50 s

`net.Dialer.Control` has the signature `func(network, address string, c syscall.RawConn) error`, and the dialer calls it in a window a wrapper cannot reach: the socket exists but the connection has not been made. Two things become possible there. You can act on the raw handle before the connect happens, which is the only correct moment for anything that must be in place beforehand. More commonly useful, `address` is the **resolved** endpoint for this attempt — an IP and port, not the hostname you passed to Dial — so you can inspect it and return an error to abort. Checking a wrapper's argument instead is unsafe, because the name you validated and the address finally connected to need not be the same. Control runs once per address tried. Go 1.20 added `ControlContext`, the same hook with the dial's `context.Context` first.

code

go · 17 lines
go
d := &net.Dialer{
	Control: func(network, address string, c syscall.RawConn) error {
		host, _, err := net.SplitHostPort(address)
		if err != nil {
			return err
		}
		ip := net.ParseIP(host) // address is already resolved here
		if ip == nil {
			return fmt.Errorf("unresolved address %q", address)
		}
		if ip.IsLoopback() || ip.IsPrivate() {
			return fmt.Errorf("refusing to dial %s", ip)
		}
		return nil
	},
}
conn, err := d.DialContext(ctx, "tcp", target)

go deeper

for a junior

Know that net.Dialer is the configurable form of net.Dial and that it exposes hooks a plain function call does not. You are unlikely to be asked for the details at this level.

for a middle

Be able to state the signature and the window: after the socket is created, before connect, once per address attempted, with a raw handle you may use only inside the callback.

for a senior

Show why the hook exists — the resolved address is the only thing that describes where the connection really goes — and know that returning an error aborts the attempt and surfaces from Dial.

for a principal

Decide where such a policy lives: a shared vetted dialer that every outbound client is built from, rather than a check each service reimplements, and be clear about what it can and cannot guarantee once traffic leaves the process.

## The hook and its window ```go type Dialer struct { // ... Control func(network, address string, c syscall.RawConn) error } ``` A dial goes through several stages: resolve the destination to one or more addresses, create a socket, optionally bind a local address, connect, hand back a `net.Conn`. `Control` is invoked in the gap between *socket created* and *connect attempted*, for each address the dialer tries. That window is unreachable from outside. A helper that wraps `net.Dial` runs before the socket exists or after the connection is complete — never in between. Two capabilities follow from being inside it. ### 1. Touching the socket before it is used The third parameter is a `syscall.RawConn`, whose `Control(f func(fd uintptr))` method runs `f` with the underlying descriptor while keeping the runtime's bookkeeping intact. Anything that must be configured *before* the connection is established has to happen here, because afterwards it is too late. You never get a bare descriptor to keep — the callback borrows it for the duration of the call, which is what stops the dialer's own lifecycle management from being subverted. ### 2. Vetting the address that will actually be dialled This is the property that matters most in review. Consider a service that fetches a URL supplied by a user and must refuse to reach internal addresses: ```go d := &net.Dialer{ Control: func(network, address string, c syscall.RawConn) error { host, _, err := net.SplitHostPort(address) if err != nil { return err } ip := net.ParseIP(host) if ip == nil { return fmt.Errorf("unresolved address %q", address) } if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { return fmt.Errorf("refusing to dial %s", ip) } return nil }, } conn, err := d.DialContext(ctx, "tcp", target) ``` Inside Control, `address` is an IP and port, already resolved. That is why `net.ParseIP` is guaranteed to work on the host part here, and why this check cannot be fooled the way a check on the *hostname* can: the name you inspected before dialling and the address the dialer ends up using are two different facts, and only the second one describes where bytes actually go. Returning a non-nil error aborts that attempt. The error surfaces from Dial wrapped in the usual `*net.OpError`, so callers see the dial fail. If the destination has multiple addresses, the dialer moves on to the next one and Control is called again for it — a fact worth remembering when the hook logs, since one dial can produce several lines. ## `ControlContext` and the listening side Go 1.20 added: ```go ControlContext func(ctx context.Context, network, address string, c syscall.RawConn) error ``` Same window, plus the context passed to `DialContext`, which lets the hook read request-scoped values or observe cancellation. When both fields are set, `ControlContext` takes precedence and `Control` is ignored — so set one, not both. `net.ListenConfig` carries the same idea for the other direction: its `Control` field runs after the listening socket is created and before it is bound, and `ListenConfig.Listen(ctx, network, address)` is the entry point. The listening hook is where settings that must precede the bind belong. ## Where the hook is plumbed in real code An `http.Client` does not dial directly; its `*http.Transport` does, through `DialContext`. So the way to give an HTTP client a vetted dialer is to build the Dialer with the Control hook and assign `transport.DialContext = d.DialContext`. Doing that on a transport you construct yourself, rather than mutating `http.DefaultTransport`, keeps the policy scoped to the client that should have it. ## What to say in an interview Name the window — after socket creation, before connect, per address attempted. Say that `address` is resolved rather than the original hostname, and that returning an error aborts the attempt. Mention `ControlContext` for the context-aware variant and `net.ListenConfig.Control` for the listening side. The nuance that separates a good answer is *why* the resolved address matters: validating the string you passed to Dial is not the same as constraining the socket that gets connected.

  • Why is the address parameter inside Control safer to validate than the string you passed to Dial?
    Inside Control the address is the resolved IP and port for the attempt that is about to happen, so it describes where bytes will actually go. Validating the hostname beforehand only tells you about a name; the address the dialer ultimately uses is a separate fact and need not match what an earlier lookup produced. The hook closes that gap.
  • How many times does Control run for one call to Dial?
    Once per address the dialer attempts. A destination that resolves to several addresses, or a dual-stack host tried over both families, invokes the hook for each attempt until one connects or all fail. Hooks that log or count must expect several invocations per dial, and must be safe for concurrent use since one Dialer is typically shared.
  • How do you attach a dialer with a Control hook to an HTTP client?
    Construct your own `*http.Transport` and set its `DialContext` field to the dialer's method: `tr.DialContext = d.DialContext`, then use that transport in the `http.Client` you build. Keep the change on a transport you own rather than mutating the package-level default, so the policy applies only to the client that is meant to have it.
  • Is there an equivalent hook for the listening side?
    Yes — `net.ListenConfig` has a `Control` field with the same signature, invoked after the listening socket is created and before it is bound, and you listen through `ListenConfig.Listen(ctx, network, address)`. That is the place for anything that must be in effect before the bind happens rather than after.

A wrapper checks the address on the envelope; the Control hook stands at the door and looks at the street the courier is actually walking down. Only the second one can send the courier back.

saying these in an interview costs you the question

  • Thinks Control runs after the connection is established
  • Believes it receives the original hostname rather than a resolved address
  • Assumes returning an error is only logged, not fatal to the attempt
  • Expects exactly one call per Dial regardless of addresses
  • Tries to keep the descriptor beyond the callback