netbind

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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

func FamilyDoer(b *Binder, network string, timeoutMs int) *http.Client

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

func Resolve(cfg Config, warn func(string)) (*Binder, error)

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

func (b *Binder) DialContext(ctx context.Context, network, addr string) (net.Conn, error)

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

func (b *Binder) Doer() *http.Client

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

func (b *Binder) Family() 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

func (b *Binder) Transport() *http.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) WSClient

func (b *Binder) WSClient() *http.Client

WSClient is the bound HTTP client for the WebSocket handshake.

func (*Binder) WSTransport

func (b *Binder) WSTransport() *http.Transport

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.

func (Config) IsZero

func (c Config) IsZero() bool

IsZero reports whether the config requests no customization at all (default source, dual-stack) — Resolve then returns a nil Binder.

type Family

type Family int

Family is the requested outbound IP family.

const (
	// FamilyDual dials dual-stack (the default, OS Happy-Eyeballs behavior).
	FamilyDual Family = iota
	// FamilyV4 forces IPv4 (A records only).
	FamilyV4
	// FamilyV6 forces IPv6 (AAAA records only).
	FamilyV6
)

func ParseFamily

func ParseFamily(s string) (Family, error)

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.

func (Family) Networks

func (f Family) Networks() []string

Networks lists the TCP networks ("tcp4"/"tcp6") this family permits — dual permits both. It is the single source for capping per-family probing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL