networking

package
v0.0.39 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

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

View Source
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
)
View Source
const HttpScheme = "http"

HttpScheme is the HTTP scheme

View Source
const HttpTimeout = 30 * time.Second

HttpTimeout is the timeout for outgoing HTTP requests

View Source
const HttpsScheme = "https"

HttpsScheme is the HTTPS scheme

View Source
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

View Source
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.

View Source
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

func AddressReferencesPrivateIp(address string) error

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

func FindOrUsePort(port int) (int, error)

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

func GetProcessOnPort(port int) (int, error)

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

func IsAvailable(port int) bool

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

func IsHTTPError(err error, statusCode int) bool

IsHTTPError checks if an error is an HTTPError with the specified status code. If statusCode is 0, it matches any HTTPError.

func IsLocalhost

func IsLocalhost(host string) bool

IsLocalhost checks if a host is a loopback address (for development). Recognised forms: "localhost", "localhost:<port>", "127.0.0.1", "127.0.0.1:<port>", "[::1]", "[::1]:<port>".

func IsLoopbackHost

func IsLoopbackHost(host string) bool

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

func IsPreRegisteredClient(clientID string) bool

IsPreRegisteredClient determines if the OAuth client is pre-registered (has client ID)

func IsPrivateIP

func IsPrivateIP(ip net.IP) bool

IsPrivateIP reports whether ip is a private, loopback, link-local, unspecified, or otherwise reserved/non-public address.

NAT64-translated addresses are evaluated by the IPv4 address they embed: a NAT64 address whose low 32 bits map to a private/link-local IPv4 (e.g. 64:ff9b:1::a9fe:a9fe -> 169.254.169.254, the cloud metadata endpoint) is treated as private, because behind a NAT64 gateway it reaches exactly that internal IPv4, while NAT64 addresses embedding a genuinely public IPv4 remain allowed. This /96 decoding covers the well-known 64:ff9b::/96 (RFC 6052) and the 64:ff9b:1::/96 sub-prefix of the RFC 8215 local-use range; the rest of 64:ff9b:1::/48 uses a non-/96 embedding that cannot be decoded from the address alone and is blocked wholesale (see privateIPBlocks).

func IsURL

func IsURL(input string) bool

IsURL checks if the input is a valid HTTP or HTTPS URL

func NewHTTPError

func NewHTTPError(statusCode int, url, message string) error

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

func ParsePortSpec(portSpec string) (string, int, error)

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

func SameHostRedirectPolicy() func(req *http.Request, via []*http.Request) error

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

func TargetIsPrivate(ctx context.Context, rawURL string) bool

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

func ValidateCallbackPort(callbackPort int, clientID string) error

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

func ValidateEndpointURL(endpoint string) error

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

func ValidateEndpointURLWithInsecure(endpoint string, insecureAllowHTTP bool) error

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

func ValidateEndpointURLWithReader(endpoint string, reader env.Reader) error

ValidateEndpointURLWithReader validates that an endpoint URL is secure, reading INSECURE_DISABLE_URL_VALIDATION via reader instead of the process environment directly.

func ValidateHTTPSURL

func ValidateHTTPSURL(rawURL string) error

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

func ValidateIssuerURL(rawURL string) error

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

func ValidateLoopbackAddress(addr string) error

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 WithBody

func WithBody(body io.Reader) FetchOption

WithBody sets the request body.

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

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

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.

func (*HTTPError) Error

func (e *HTTPError) Error() string

Error implements the error interface.

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

func (*ValidatingTransport) RoundTrip

func (t *ValidatingTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip validates the request URL prior to forwarding

Jump to

Keyboard shortcuts

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