Documentation
¶
Overview ¶
Package netguard is the one answer to "may filex dial this address?".
Two callers ask it, for the same reason: a URL an administrator or a plugin hands us must not become a probe of the server's own network. An install URL of http://169.254.169.254/… or http://10.0.0.5:9200/… is the SSRF shape, and so is a plugin's http_request to a public name that resolves to a private address. The check therefore runs on the DIALLED address, after DNS, and on every redirect hop — never on the string.
It lives in its own package so the plugin downloads (internal/plugin and internal/wasmplugin share DownloadClient) and the app-plugin outbound transport (internal/wasmplugin) cannot drift apart: one list of refused ranges, one guarded dialer, one redirect rule, one error.
Index ¶
- Constants
- Variables
- func Control(network, address string, _ syscall.RawConn) error
- func DialContext(dialer *net.Dialer) func(ctx context.Context, network, addr string) (net.Conn, error)
- func Private(ip net.IP) bool
- func Refused(ip net.IP) bool
- func Transport(responseHeaderTimeout time.Duration) *http.Transport
- type Policy
- func (p Policy) APIClient(timeout time.Duration) *http.Client
- func (p Policy) DialContext(dialer *net.Dialer) func(ctx context.Context, network, addr string) (net.Conn, error)
- func (p Policy) DownloadClient(timeout time.Duration) *http.Client
- func (p Policy) Refused(ip net.IP) bool
- func (p Policy) Transport(responseHeaderTimeout time.Duration) *http.Transport
Constants ¶
const MaxRedirects = 5
MaxRedirects is how many redirects a download follows.
Variables ¶
var ErrDowngrade = errors.New("a redirect from https to plain http is not followed")
ErrDowngrade is a redirect from https:// to plain http://.
var ErrPrivateTarget = errors.New("resolves to a private or local address")
ErrPrivateTarget is what a guarded dial answers for a refused address. Callers match it with errors.Is to turn it into their own wording.
Functions ¶
func Control ¶ added in v0.50.0
Control is a net.Dialer Control function that refuses a connection to an address Refused names. It runs on the RESOLVED address, right before the socket connects, so a name that resolves to something else between a check and the dial (DNS rebinding) is caught too. For clients that take a *net.Dialer and no dial function (an LDAP client).
func DialContext ¶
func DialContext(dialer *net.Dialer) func(ctx context.Context, network, addr string) (net.Conn, error)
DialContext is a net.Dialer's DialContext wrapped in the Refused check: the name is resolved here, every answer is checked, and the connection is made to the address that passed — so nothing can re-resolve to something else between the check and the dial.
func Private ¶
Private says whether an address is inside the private network — loopback, link-local, RFC 1918 or ULA. This is the WEAKER question, asked about a destination filex will send credentials to: plain http:// is tolerable there and nowhere else. It deliberately does not include multicast or the unspecified address, which are not destinations at all.
func Refused ¶
Refused reports whether a resolved address is one an outbound request may never reach: loopback, link-local (v4 169.254/16 and v6 fe80::/10, which is where cloud metadata lives), the RFC 1918 / ULA private ranges, multicast and the unspecified address.
A nil IP counts as refused: "we could not tell" is not permission.
⚠⚠ Not only RFC 1918. net.IP.IsPrivate calls the shared address space 100.64.0.0/10 public, and that is where overlay meshes live (Cloudflare WARP 100.96/12, Tailscale, carrier-grade NAT). The other ranges below are not routable destinations either, and the IPv6 spellings that wrap an IPv4 address (NAT64, 6to4) are judged by the address they wrap.
Types ¶
type Policy ¶ added in v0.50.0
type Policy struct {
// Loopback lets a download reach this machine - 127.0.0.0/8 and ::1,
// nothing else that is private. Development and the end-to-end tests serve
// plugin sources from a loopback server (FILEX_PLUGIN_LOOPBACK_SOURCES); a
// server in service never needs it, and with it on, a manifest that names
// a loopback address makes filex send GET requests to its own services.
Loopback bool
}
Policy widens what a guarded dial may reach beyond the public internet. The zero value is the guard itself: public addresses only. An app's own requests (http_request, asset_fetch) always use the zero value; only the plugin DOWNLOADS (DownloadClient) take one from configuration.
func (Policy) APIClient ¶ added in v0.52.0
APIClient is DownloadClient's sibling for an API filex SENDS something to - a body, a secret (an app store's license check carries a license key) - and reads an answer from: every dial guarded the same way, but a redirect is never followed. It is the answer (http.ErrUseLastResponse), so a 307 or 308 cannot make the request, body and all, go to whatever host the response names (an open redirect at the API, a hijacked route).
func (Policy) DialContext ¶ added in v0.50.0
func (p Policy) DialContext(dialer *net.Dialer) func(ctx context.Context, network, addr string) (net.Conn, error)
DialContext is the guarded dial under this policy.
func (Policy) DownloadClient ¶ added in v0.50.0
DownloadClient is the ONE client every plugin download goes through: a storage plugin's binary, its source's feed and build, an app's manifest, module and interface bundle - for an administrator's install, an install request an API key leaves, and the daily update check alike. Each dial is guarded (after DNS, so a public name that resolves inward is refused too), every redirect hop dials through the same guard, the chain is capped at MaxRedirects, and a redirect never leaves https for plain http: a download that started encrypted is not finished in the clear, where anything on the path may answer instead. timeout bounds the whole download, body included; the callers cap the size.