dialguard

package
v0.3.0 Latest Latest
Warning

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

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

Documentation

Overview

Package dialguard blocks the addresses a platform-initiated outbound connection must never reach.

Anything that dials a URL supplied by a customer — a vault credential's MCP server or token endpoint, an agent's `mcp_servers` entry — is an SSRF vector, and one that returns response bodies is a full-read vector. The guard refuses the address classes that are never a legitimate third-party endpoint but are prime exfiltration targets: loopback (the platform's own surfaces), link-local (cloud metadata, 169.254.169.254 / fe80::/10), the unspecified address, and multicast.

Two properties matter more than the list. The check runs on the *resolved* IP at connect time (net.Dialer.Control), on every dial, so DNS rebinding cannot slip a blocked address past a name that resolved innocently a moment earlier. And RFC 1918 private ranges are deliberately allowed: this platform's premise is on-prem / in-VPC operation (CLAUDE.md), where MCP servers and token endpoints legitimately live on the operator's own private network — the address-based guard is therefore not the network policy, only the floor beneath it (an agent's egress policy is enforced separately, and refusing RFC 1918 here would break the deployment model rather than protect it).

Redirects are a separate matter this package does not address: a caller that follows one replays its request body to a new target, which a per-hop IP check cannot see is wrong. Callers refuse to follow them (http.ErrUseLastResponse).

Index

Constants

This section is empty.

Variables

View Source
var ErrRefused = errors.New("disallowed address")

ErrRefused is wrapped by every refusal this guard produces, so a caller can tell a destination that can never be dialled from a network that may recover: no retry makes a refused address reachable. For an address the guard refuses by class the sentinel is the phrase the message already ended with, so that text is unchanged; the unreadable-address refusal gains it as a suffix, which no caller can reach through Control (net.ParseIP rejects it first) and which nothing asserts on.

Functions

func Control

func Control(allow func(net.IP) error) func(network, address string, c syscall.RawConn) error

Control builds a net.Dialer Control hook that runs allow on the resolved address of every dial — the address the connection is about to be made to rather than the name it came from, which is what makes DNS rebinding ineffective. allow is a parameter rather than a hard-wired call so a caller can hand in a closure over its own overridable seam (a test pointing at an httptest server on loopback needs one); the closure itself is captured once and called per dial, so a seam it reads is consulted afresh on each one.

func IPAllowed

func IPAllowed(ip net.IP) error

IPAllowed reports whether a resolved address may be dialed, returning an error naming the refusal. The text names the resolved address and says only that it is disallowed — never which class matched it, and never whether anything is listening there: the refusal is produced before any connect(2), so it reads identically either way and a caller that surfaces it cannot become an internal-host oracle.

Types

This section is empty.

Jump to

Keyboard shortcuts

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