Documentation
¶
Overview ¶
Package netbind resolves the outbound networking flags (--bind / --family) into a Binder that pins where the CLI's connections come from and which IP family they use, so a customer on a multi-homed host can pin traffic to a link (reliability) and spread calls across source IPs to raise the effective rate limit (the public endpoints are rate-limited per source IP).
The governing rule ¶
--bind is a SINGLE source — one IP address or one interface; --family says "use only this IP family". Keeping --bind to one token keeps dual-stack simple: the binder never needs two distinct per-family source addresses, so every dial delegates to the stdlib dialer (no hand-rolled Happy-Eyeballs). The consequence:
- an address binds exactly its own family, so --bind <v4-address> narrows to IPv4 (a v6 connection can't use a v4 source, and must not silently fall back to the default source);
- an interface device-bind (see below) covers BOTH families through one control hook, so the interface is the way to bind dual-stack;
- where device binding is unavailable (unprivileged Linux, other platforms) only a single-family source IP is possible, so a dual-stack interface there is refused rather than half-bound (see the table).
--family × --bind behavior ¶
--family --bind effective source -------- ------ --------- ------ dualstack (none) dualstack OS default (unchanged) dualstack 203.0.113.7 (v4) ipv4* v4 = that address dualstack 2001:db8::5 (v6) ipv6* v6 = that address dualstack eth0 (device-bind) dualstack both families bound to eth0 dualstack eth0 (fallback, one fam) that fam* that family's source IP dualstack eth0 (fallback, v4+v6) ERROR needs privilege/addr/family ipv4 (none) ipv4 default v4 source ipv4 203.0.113.7 ipv4 v4 = that address ipv4 eth0 (has a v4 addr) ipv4 v4 via eth0 ipv6 ... (symmetric to ipv4) ipv6 ...
Entries marked * are narrowed from dualstack by the single-family source. Rejected up front (exit 2): a list (--bind a,b — bind an interface for dual-stack); --family ipv4 with a v6 address or v6-only interface (and vice versa); an unknown or down interface; --family ipvN with an interface that has no address in family N; and the fallback dual-stack-interface case above (the fix names grant-privilege / bind-an-address / pick-a-family). A device-bound interface stays dual-stack — it is the one --bind that covers both families; a family the device lacks simply fails to dial there (it never leaks).
Source-binding mechanics ¶
A source IP is bound with net.Dialer.LocalAddr. An interface is bound at the device level where that is possible and unprivileged — macOS (IP_BOUND_IF/IPV6_BOUND_IF) and Windows (IP_UNICAST_IF/IPV6_UNICAST_IF) always, Linux (SO_BINDTODEVICE) only with root/CAP_NET_RAW. Without that privilege (and on other platforms) it falls back to binding the interface's own source IP; egress then follows the routing table rather than the device, and Resolve discloses this through warn. The IP family is applied by substituting tcp4/tcp6 for the dialed network.
The one seam ¶
Everything is built from Binder.DialContext — the signature net.Dialer, http.Transport, and golang.org/x/net/proxy.ContextDialer all share. Transport/WSTransport and Doer/WSClient build the REST and WebSocket clients; FamilyDoer builds a client pinned to one explicit TCP family (with the binder's source for that family) for the ip/doctor probes that test families independently; when the binder pins a single family it refuses the other at dial time, so the family the caller asks for can never escape the binding. Because the transport builders consume only that one dial seam, a forward-dialer proxy can wrap DialContext with no caller change.
Validation and the no-leak guarantee ¶
Resolve front-loads every detectable mistake (the rejected combinations above) so they fail fast with a fix-it message instead of surfacing later as a confusing dial error; only genuinely route-dependent failures (no route to host) reach dial time. Outbound never uses a family or source the config excluded — the narrowing rule closes the per-family default-source fallback, the family pin closes cross-family leaks, a fallback that finds no usable source address errors rather than dialing the default source, and FamilyDoer refuses an explicit family the binder excludes rather than dial it unbound. The one documented exception is DNS: name resolution goes out the default path (the OS/stdlib resolver), and only the resulting TCP connect honors --bind/--family.
Proxies ¶
Every outbound transport — REST (Transport), WebSocket (WSTransport), and the ip/doctor probes (FamilyDoer) — sets Proxy: http.ProxyFromEnvironment, so the standard HTTP(S)_PROXY/NO_PROXY environment is honored uniformly. When a proxy is in use, --bind/--family apply to the connection to the proxy (the proxy then originates the connection to the API).
A zero Config (no --bind, dual-stack) resolves to a nil *Binder: callers leave their default dialers in place, so the package is inert until a flag is set.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func FamilyDoer ¶
FamilyDoer returns an HTTP client whose connections are pinned to the given TCP family ("tcp4"/"tcp6") and bound to b's source for that family. b may be nil (no source binding — just the family pin). It is the family-pinned dialer the ip/doctor diagnostics use to probe one family at a time; the family pin lives here so all dial construction stays in this package. The caller supplies the per-probe timeout (the command layer resolves --timeout / its default). Like the REST/WS transports it honors the standard HTTP(S)_PROXY/NO_PROXY environment, so the probes report what the proxied calls actually see (the family pin then applies to the connection to the proxy).
When b pins a single family (a v4/v6 --bind or --family), a request for the OTHER family is refused at dial time rather than dialing it out the default source — the no-leak guarantee is enforced here, not left to every caller to cap. (b == nil carries no such constraint: it is only a family pin, with no source to leak.) Callers already cap to b.Family().Networks(); this makes a miss a clean "family unavailable" dial error instead of a silent leak.
Types ¶
type Binder ¶
type Binder struct {
// contains filtered or unexported fields
}
Binder is the resolved outbound networking customization. It is consumed two ways: Transport/WSTransport/Doer/WSClient build the bound HTTP+WS clients (family chosen by Config.Family), and FamilyDoer(b, network, …) builds a client pinned to one explicit family for the ip/doctor probes. A nil *Binder means "use the OS defaults" — callers skip overriding their dialers, and FamilyDoer(nil, …) still pins the family without a source binding.
func Resolve ¶
Resolve validates a Config and returns the Binder it describes, or a nil Binder when the config requests nothing (default source, dual-stack). warn receives non-fatal advisories (e.g. the unprivileged-Linux device-bind fallback); it may be nil. All detectable mistakes — a multi-source list (--bind is a single source), a family/literal mismatch, an unknown or down interface, an interface with no address in the wanted family, and a dual-stack interface where only a single-family source IP is possible — are returned as errors here so they fail fast instead of surfacing as a confusing dial-time failure later.
func (*Binder) DialContext ¶
DialContext is the one seam every bound client is built from — the signature net.Dialer, http.Transport.DialContext, and golang.org/x/net/proxy.ContextDialer all share. It applies the configured IP family (substituting tcp4/tcp6 for the network http hands it) and the per-family source binding. Because every bound client routes through this one method, a forward-dialer proxy can wrap it without changing the transport builders below.
func (*Binder) Doer ¶
Doer is the bound HTTP client for REST calls (no client-level timeout; the per-attempt deadline is carried on the request context, matching the default client this replaces).
func (*Binder) Family ¶
Family returns the resolved IP family (after any dual→single narrowing in Resolve). The ip/doctor diagnostics read it to cap their per-family probing to what --family permits — no request may go out a family the user excluded.
func (*Binder) Transport ¶
Transport returns the bound HTTP transport for REST calls: a clone of the stdlib default (preserving its connection pooling and HTTP/2 attempt) with the dial seam swapped in and Proxy set explicitly to http.ProxyFromEnvironment.
func (*Binder) WSTransport ¶
WSTransport is the bound transport for the WebSocket handshake. The Upgrade is HTTP/1.1, so HTTP/2 is disabled — a hand-built transport with a custom DialContext does not auto-enable it, and this makes that explicit.
type Config ¶
type Config struct {
// Bind is the raw --bind value: a SINGLE source IP or interface name
// (optionally "addr!"/"host!"/"if!"-prefixed). An address binds its own family;
// an interface binds both families when device binding is available, else its
// source IP for one family. Empty = OS default source.
Bind string
// Family is the requested IP family.
Family Family
}
Config is the unresolved networking request from the CLI flags/env.
type Family ¶
type Family int
Family is the requested outbound IP family.
func ParseFamily ¶
ParseFamily maps a flag/env string to a Family. "" and dualstack/dual map to FamilyDual; ipv4/v4/4 and ipv6/v6/6 to the pinned families.