Documentation
¶
Overview ¶
Package networking provides outbound HTTP client construction with an SSRF/egress policy: private-IP and link-local dial blocking, a same-host redirect policy, and a body-capped JSON fetch helper.
Status: Alpha. The API may change without notice.
Index ¶
- Constants
- Variables
- func AddressReferencesPrivateIp(address string) error
- func FindAvailable() int
- func FindAvailableListener() (*net.TCPListener, error)
- func FindOrUseListener(port int) (*net.TCPListener, error)
- func FindOrUsePort(port int) (int, error)
- func GetProcessOnPort(port int) (int, error)
- func IsAvailable(port int) bool
- func IsHTTPError(err error, statusCode int) bool
- func IsLocalhost(host string) bool
- func IsLoopbackHost(host string) bool
- func IsPreRegisteredClient(clientID string) bool
- func IsPrivateIP(ip net.IP) bool
- func IsURL(input string) bool
- func NewHTTPError(statusCode int, url, message string) error
- func NewPrivateIPBlockingDialContext() func(ctx context.Context, network, addr string) (net.Conn, error)
- func ParsePortSpec(portSpec string) (string, int, error)
- func SameHostRedirectPolicy() func(req *http.Request, via []*http.Request) error
- func TargetIsPrivate(ctx context.Context, rawURL string) bool
- func ValidateCallbackPort(callbackPort int, clientID string) error
- func ValidateEndpointURL(endpoint string) error
- func ValidateEndpointURLWithInsecure(endpoint string, insecureAllowHTTP bool) error
- func ValidateEndpointURLWithInsecureAndReader(endpoint string, insecureAllowHTTP bool, reader env.Reader) error
- func ValidateEndpointURLWithReader(endpoint string, reader env.Reader) error
- func ValidateHTTPSURL(rawURL string) error
- func ValidateIssuerURL(rawURL string) error
- func ValidateLoopbackAddress(addr string) error
- type FetchOption
- type FetchResult
- type HTTPClient
- type HTTPError
- type HttpClientBuilder
- func (b *HttpClientBuilder) Build() (*http.Client, error)
- func (b *HttpClientBuilder) WithCABundle(path string) *HttpClientBuilder
- func (b *HttpClientBuilder) WithEnvReader(reader env.Reader) *HttpClientBuilder
- func (b *HttpClientBuilder) WithInsecureAllowHTTP(allow bool) *HttpClientBuilder
- func (b *HttpClientBuilder) WithPrivateIPs(allow bool) *HttpClientBuilder
- func (b *HttpClientBuilder) WithTimeout(timeout time.Duration) *HttpClientBuilder
- func (b *HttpClientBuilder) WithTokenFromFile(path string) *HttpClientBuilder
- type ValidatingTransport
Constants ¶
const ( // MinPort is the minimum port number to use MinPort = 10000 // MaxPort is the maximum port number to use MaxPort = 65535 // MaxAttempts is the maximum number of attempts to find an available port MaxAttempts = 10 )
const HttpScheme = "http"
HttpScheme is the HTTP scheme
const HttpTimeout = 30 * time.Second
HttpTimeout is the timeout for outgoing HTTP requests
const HttpsScheme = "https"
HttpsScheme is the HTTPS scheme
const MaxRedirects = 10
MaxRedirects bounds how many HTTP redirects an SSRF-guarded client follows before giving up. Matches the cap used by the transparent proxy data path.
Variables ¶
var ErrPrivateIPAddress = errors.New("the provided URL redirects to a private IP address, which is not allowed")
ErrPrivateIPAddress is returned when the provided URL redirects to a private IP address, which is not allowed.
var ErrRedirectRefused = errors.New("redirect refused")
ErrRedirectRefused is wrapped by SameHostRedirectPolicy when it declines to follow a redirect, so callers can match it with errors.Is.
Functions ¶
func AddressReferencesPrivateIp ¶
AddressReferencesPrivateIp returns an error if the address references a private IP address
func FindAvailable ¶
func FindAvailable() int
FindAvailable finds an available port.
Known limitation: like IsAvailable, this has a bind-then-close-then-return race — the port can be taken by another process between this call returning and the caller binding it. Prefer FindAvailableListener when you will bind the port yourself shortly afterward in the same process.
func FindAvailableListener ¶
func FindAvailableListener() (*net.TCPListener, error)
FindAvailableListener finds an available port and returns it as a still-open *net.TCPListener, closing the find-then-bind race that FindAvailable has: since the listener stays open, nothing else can grab the port before the caller is ready to use it. The caller is responsible for closing the listener (or handing it to something like http.Serve).
func FindOrUseListener ¶
func FindOrUseListener(port int) (*net.TCPListener, error)
FindOrUseListener is the race-free counterpart to FindOrUsePort: if port is 0, it behaves like FindAvailableListener; otherwise it tries to bind the requested port directly, falling back to FindAvailableListener only if that port is unavailable. The returned listener is still open; the caller is responsible for closing it.
func FindOrUsePort ¶
FindOrUsePort checks if the provided port is available or finds an available port if none is provided. If port is 0, it will find an available port. If port is not 0, it will check if the port is available. Returns the selected port and an error if any.
Known limitation: this has the same bind-then-close-then-return race as IsAvailable and FindAvailable. Prefer FindOrUseListener when you will bind the port yourself shortly afterward in the same process.
func GetProcessOnPort ¶
GetProcessOnPort returns the PID of the process listening on the given TCP port. Returns 0 if the port is free or if the holder cannot be determined. Uses gopsutil which provides cross-platform support (Linux: /proc, Windows: GetExtendedTcpTable, Darwin/FreeBSD: lsof).
func IsAvailable ¶
IsAvailable checks if a port is available.
Known limitation: this binds a probe listener, closes it, then returns a bool — there is a time-of-check-to-time-of-use window between the close and whenever the caller actually binds the port, during which another process can grab it. Callers that intend to bind the port themselves moments later in the same process should prefer FindAvailableListener/FindOrUseListener instead, which keep the listener open until the caller is ready and so close that window entirely.
func IsHTTPError ¶
IsHTTPError checks if an error is an HTTPError with the specified status code. If statusCode is 0, it matches any HTTPError.
func IsLocalhost ¶
IsLocalhost checks whether a host is localhost or a literal loopback address. It accepts plain-host and host:port forms without resolving hostnames.
func IsLoopbackHost ¶
IsLoopbackHost reports whether the Host header value refers to a loopback address. It is intended for DNS-rebinding guards on loopback-only listeners. It accepts the hostname "localhost" (case-insensitive), any 127.x.x.x address, and the IPv6 loopback ::1. Both plain-host and host:port forms are accepted. Hostnames other than "localhost" are NOT resolved.
func IsPreRegisteredClient ¶
IsPreRegisteredClient determines if the OAuth client is pre-registered (has client ID)
func IsPrivateIP ¶
IsPrivateIP reports whether ip is a private, loopback, link-local, unspecified, or otherwise reserved/non-public address.
NAT64- and 6to4-translated addresses are evaluated by the IPv4 address they embed. A translated address whose embedded IPv4 is private or link-local is treated as private, because it reaches that internal IPv4 through its translator. NAT64 addresses embed the IPv4 in their low 32 bits; 6to4 addresses embed it in bytes 2 through 5. Teredo addresses are blocked wholesale because their embedded client IPv4 is obfuscated and cannot be safely derived from the IPv6 literal.
func NewHTTPError ¶
NewHTTPError creates a new HTTP error.
func NewPrivateIPBlockingDialContext ¶
func NewPrivateIPBlockingDialContext() func(ctx context.Context, network, addr string) (net.Conn, error)
NewPrivateIPBlockingDialContext returns a DialContext that refuses to connect to private, loopback, or link-local addresses. The check runs after DNS resolution on the address actually being dialed, so it also defends against DNS rebinding and is re-applied on every redirect hop. Pair it with Transport.DisableKeepAlives so a pooled connection cannot skip the check on a later request.
Use this on clients that fetch a URL derived from untrusted input when the operator-configured target is public; SameHostRedirectPolicy is the redirect-following counterpart.
func ParsePortSpec ¶
ParsePortSpec parses a port specification string in the format "hostPort:containerPort" or just "containerPort". Returns the host port string and container port integer. If only a container port is provided, a random available host port is selected locally via FindAvailable.
Host port 0 (explicit "0:containerPort" form) is passed through unchanged as the string "0" and is NOT resolved to a concrete port here — this is intentional. Docker's own PortBinding.HostPort treats "0" as "assign a port dynamically at container start", which is a distinct mechanism from this function's own container-only path (no ":") that calls FindAvailable to pick a host port up front.
func SameHostRedirectPolicy ¶
SameHostRedirectPolicy returns a value for http.Client.CheckRedirect that follows only same-host redirects, refuses HTTPS-to-HTTP downgrades, and caps the chain at MaxRedirects.
Any client that fetches a URL derived from an untrusted remote server — auth discovery probes, RFC 9728 resource-metadata fetches, OIDC issuer discovery — must install this. Validating only the originally-supplied URL is not enough: a malicious server can return a 30x that points the request at an internal address (cloud IMDS, RFC1918 services), and the host-side client would follow it (CWE-918). Restricting redirects to the same host as the original request keeps the request on the endpoint the operator actually configured.
This mirrors the data-plane guard in the toolhive proxy transport (github.com/stacklok/toolhive pkg/transport/proxy/transparent, followRedirects); if you change this policy, check the corresponding guard there too.
func TargetIsPrivate ¶
TargetIsPrivate reports whether the host in rawURL refers to — or resolves to — a private, loopback, or link-local address. It is used to detect when an operator has deliberately pointed ToolHive at an internal target, so that discovery fetches derived from untrusted server input may also be allowed to reach internal addresses for that deployment.
IP literals and "localhost" are classified without DNS. Hostnames are resolved and reported private if ANY resolved address is private. Unparsable input or resolution failure returns false (treat as public — the SSRF guard then stays engaged, failing secure).
func ValidateCallbackPort ¶
ValidateCallbackPort validates that the specified callback port is valid and available. It checks that the port is within the valid range (1-65535) and, for pre-registered clients (with clientID), it returns an error if the port is not available.
func ValidateEndpointURL ¶
ValidateEndpointURL validates that an endpoint URL is secure. It reads INSECURE_DISABLE_URL_VALIDATION from the process environment; use ValidateEndpointURLWithReader to inject a different env.Reader (e.g. in tests).
func ValidateEndpointURLWithInsecure ¶
ValidateEndpointURLWithInsecure validates that an endpoint URL is secure, allowing HTTP if insecureAllowHTTP is true. WARNING: This is insecure and should NEVER be used in production. It reads INSECURE_DISABLE_URL_VALIDATION from the process environment; use ValidateEndpointURLWithInsecureAndReader to inject a different env.Reader.
func ValidateEndpointURLWithInsecureAndReader ¶
func ValidateEndpointURLWithInsecureAndReader(endpoint string, insecureAllowHTTP bool, reader env.Reader) error
ValidateEndpointURLWithInsecureAndReader validates that an endpoint URL is secure, allowing HTTP if insecureAllowHTTP is true, reading INSECURE_DISABLE_URL_VALIDATION via reader instead of the process environment directly. WARNING: insecureAllowHTTP is insecure and should NEVER be used in production.
func ValidateEndpointURLWithReader ¶
ValidateEndpointURLWithReader validates that an endpoint URL is secure, reading INSECURE_DISABLE_URL_VALIDATION via reader instead of the process environment directly.
func ValidateHTTPSURL ¶
ValidateHTTPSURL checks that rawURL is a valid URL using the https scheme. Unlike ValidateEndpointURL, no localhost exception is made — HTTPS is always required (suitable for gateway URLs and other production endpoints).
func ValidateIssuerURL ¶
ValidateIssuerURL validates that an OIDC issuer URL is well-formed and uses HTTPS. HTTP is permitted only for localhost (development). Per OIDC Core Section 3.1.2.1 and RFC 8414 Section 2, the issuer MUST use the "https" scheme.
func ValidateLoopbackAddress ¶
ValidateLoopbackAddress returns an error if addr (a host:port string) does not contain a literal loopback IP address. Both IPv4 (127.x.x.x) and IPv6 (::1) loopback addresses are accepted. Hostnames (including "localhost") are not resolved and will be rejected.
Types ¶
type FetchOption ¶
type FetchOption func(*fetchOptions)
FetchOption configures a fetch request.
func WithErrorHandler ¶
func WithErrorHandler(handler func(*http.Response, []byte) error) FetchOption
WithErrorHandler sets a custom error handler for non-200 responses. The handler receives the response and body, and should return an error. If the handler returns nil, the default HTTPError will be returned. This is useful for parsing structured error responses (e.g., OAuth error responses).
func WithHeader ¶
func WithHeader(key, value string) FetchOption
WithHeader adds a single header to the request.
func WithMaxResponseSize ¶
func WithMaxResponseSize(size int64) FetchOption
WithMaxResponseSize sets a custom maximum response body size in bytes. The default is 1 MB. Use this to enforce tighter limits for endpoints that are expected to return small documents (e.g. OAuth metadata, CIMD documents).
func WithMethod ¶
func WithMethod(method string) FetchOption
WithMethod sets the HTTP method for the request.
type FetchResult ¶
type FetchResult[T any] struct { // Data is the parsed JSON response body. Data T // Headers are the response headers. Headers http.Header }
FetchResult contains the result of a successful JSON fetch operation.
func FetchJSON ¶
func FetchJSON[T any]( ctx context.Context, client HTTPClient, requestURL string, opts ...FetchOption, ) (*FetchResult[T], error)
FetchJSON performs an HTTP request and parses the JSON response body. It sets the Accept header to application/json by default. For non-200 responses, it returns an HTTPError or the result of a custom error handler.
func FetchJSONWithForm ¶
func FetchJSONWithForm[T any]( ctx context.Context, client HTTPClient, requestURL string, formData url.Values, opts ...FetchOption, ) (*FetchResult[T], error)
FetchJSONWithForm performs a POST request with form-urlencoded body and parses JSON response. This is a convenience wrapper around FetchJSON for token endpoints and similar APIs. It sets Content-Type to application/x-www-form-urlencoded and Accept to application/json.
type HTTPClient ¶
HTTPClient is an interface for making HTTP requests. This interface is satisfied by *http.Client and allows for dependency injection in testing.
type HTTPError ¶
type HTTPError struct {
// StatusCode is the HTTP status code.
StatusCode int
// Message is a description of the error (may be a preview of the response body).
Message string
// URL is the requested URL.
URL string
}
HTTPError represents an HTTP error response with status code, URL, and message.
type HttpClientBuilder ¶
type HttpClientBuilder struct {
// contains filtered or unexported fields
}
HttpClientBuilder provides a fluent interface for building HTTP clients
func NewHostScopedClientBuilder ¶
func NewHostScopedClientBuilder(host string, allowPrivateIPs, insecureAllowHTTP bool) *HttpClientBuilder
NewHostScopedClientBuilder returns an HttpClientBuilder pre-configured with the SSRF-guard policy (CWE-918) appropriate for dialing host. By default the returned builder blocks plain HTTP and connections to private/loopback/ link-local IP ranges. Both gates are relaxed automatically for loopback hosts (development/testing) and when INSECURE_DISABLE_URL_VALIDATION is set.
allowPrivateIPs widens only the private-IP gate — for example an in-cluster provider reachable solely over an RFC-1918 address — without enabling plain HTTP for non-loopback hosts. insecureAllowHTTP additionally permits plain-HTTP for non-loopback hosts and must never be set in production.
The returned builder is not yet built: callers may chain further options (e.g. WithTimeout, WithEnvReader) before calling Build. This is the single source of truth for the host-scoped guard policy shared by the upstream OAuth2/OIDC providers and the DCR resolver so the two paths cannot drift.
func NewHostScopedClientBuilderWithReader ¶
func NewHostScopedClientBuilderWithReader( host string, allowPrivateIPs, insecureAllowHTTP bool, reader env.Reader, ) *HttpClientBuilder
NewHostScopedClientBuilderWithReader is identical to NewHostScopedClientBuilder but takes the env.Reader used to evaluate INSECURE_DISABLE_URL_VALIDATION explicitly, so callers can inject a fake reader for tests instead of relying on a later WithEnvReader call, which would be too late to affect this constructor's own allowInsecure computation.
func NewHttpClientBuilder ¶
func NewHttpClientBuilder() *HttpClientBuilder
NewHttpClientBuilder returns a new HttpClientBuilder
func (*HttpClientBuilder) Build ¶
func (b *HttpClientBuilder) Build() (*http.Client, error)
Build creates the configured HTTP client.
When private IPs are disallowed, Build installs the per-dial SSRF guard (NewPrivateIPBlockingDialContext's underlying check) AND disables HTTP keep-alives in the same branch. The guard validates the address on each new dial; a pooled, kept-alive connection skips that dial entirely on subsequent requests, silently bypassing the check. Pairing the two in one branch makes "guard installed with keep-alives left on" structurally unreachable rather than merely discouraged in documentation. When private IPs are allowed, no guard is installed, so keep-alives may remain enabled.
When WithTokenFromFile configures an auth token, the returned client automatically installs SameHostRedirectPolicy as CheckRedirect. Without this, oauth2.Transport re-adds the Authorization: Bearer header on every redirected request, and http.Client follows cross-host redirects by default — so a malicious or compromised server could redirect the request to an attacker-controlled host and walk off with the bearer token. There is no legitimate reason for a bearer-token-carrying client to need to follow a cross-host redirect. Callers authenticating via other mechanisms (e.g. a custom RoundTripper that adds its own headers) should layer SameHostRedirectPolicy themselves if they need the same protection; Build otherwise stays redirect-policy-neutral by default.
func (*HttpClientBuilder) WithCABundle ¶
func (b *HttpClientBuilder) WithCABundle(path string) *HttpClientBuilder
WithCABundle sets the CA certificate bundle path
func (*HttpClientBuilder) WithEnvReader ¶
func (b *HttpClientBuilder) WithEnvReader(reader env.Reader) *HttpClientBuilder
WithEnvReader sets the env.Reader used to read INSECURE_DISABLE_URL_VALIDATION. Defaults to the real OS environment; inject a fake reader in tests to avoid mutating process-wide state. A nil reader is normalized to &env.OSReader{} so callers (including NewHostScopedClientBuilderWithReader) can't panic on a nil-interface method call.
func (*HttpClientBuilder) WithInsecureAllowHTTP ¶
func (b *HttpClientBuilder) WithInsecureAllowHTTP(allow bool) *HttpClientBuilder
WithInsecureAllowHTTP allows HTTP (non-HTTPS) URLs WARNING: This is insecure and should NEVER be used in production
func (*HttpClientBuilder) WithPrivateIPs ¶
func (b *HttpClientBuilder) WithPrivateIPs(allow bool) *HttpClientBuilder
WithPrivateIPs allows connections to private IP addresses
func (*HttpClientBuilder) WithTimeout ¶
func (b *HttpClientBuilder) WithTimeout(timeout time.Duration) *HttpClientBuilder
WithTimeout sets the HTTP client timeout
func (*HttpClientBuilder) WithTokenFromFile ¶
func (b *HttpClientBuilder) WithTokenFromFile(path string) *HttpClientBuilder
WithTokenFromFile sets the auth token file path
type ValidatingTransport ¶
type ValidatingTransport struct {
Transport http.RoundTripper
InsecureAllowHTTP bool
// EnvReader is consulted for INSECURE_DISABLE_URL_VALIDATION. A nil value
// falls back to the real OS environment, so the zero value of this struct
// preserves the historical os.Getenv-backed behavior.
EnvReader env.Reader
}
ValidatingTransport is for validating URLs prior to request