Documentation
¶
Overview ¶
Package netguard builds HTTP clients and validates URLs for user-configured outbound requests such as webhooks, refusing loopback, private, link-local, and other internal addresses to prevent server-side request forgery. The check runs at dial time, so redirects and DNS rebinding are covered.
Index ¶
- Constants
- Variables
- func AllowPrivate() bool
- func IsBlocked(addr netip.Addr) bool
- func IsPrivateNetworkAllowed(addr netip.Addr) bool
- func IsPrivateTargetAllowed(addr netip.Addr) bool
- func NewClient() *http.Client
- func NewClientAllowing(allowEnvs ...string) *http.Client
- func NewClientPrivateNetworks() *http.Client
- func NewClientPrivateTarget() *http.Client
- func ValidateURL(ctx context.Context, raw string) error
Examples ¶
Constants ¶
const AllowPrivateEnv = "APP_NOTIFY_ALLOW_PRIVATE_NETWORKS"
AllowPrivateEnv names the env var that, when true, lets outbound notification requests reach internal addresses (for example a self-hosted chat server on a private IP).
Variables ¶
var ErrBlockedAddress = errors.New("netguard: destination address is internal and not allowed")
ErrBlockedAddress is returned when a dial targets an internal address while AllowPrivateEnv is not enabled.
var ErrInvalidURL = errors.New("netguard: not an absolute http or https URL")
ErrInvalidURL is returned by ValidateURL for a URL that is not an absolute http(s) URL with a host.
Functions ¶
func AllowPrivate ¶
func AllowPrivate() bool
AllowPrivate reports whether AllowPrivateEnv is set to a true value.
func IsBlocked ¶
IsBlocked reports whether addr is an address an outbound notification must not reach unless AllowPrivateEnv is enabled.
Example ¶
package main
import (
"fmt"
"net/netip"
"github.com/GLINCKER/levelrail/kit/netguard"
)
func main() {
for _, s := range []string{"127.0.0.1", "10.0.0.5", "169.254.169.254", "93.184.216.34"} {
fmt.Println(s, netguard.IsBlocked(netip.MustParseAddr(s)))
}
}
Output: 127.0.0.1 true 10.0.0.5 true 169.254.169.254 true 93.184.216.34 false
func IsPrivateNetworkAllowed ¶ added in v0.9.0
IsPrivateNetworkAllowed is IsPrivateTargetAllowed without loopback: public and private-range addresses pass, loopback, link-local and metadata do not.
func IsPrivateTargetAllowed ¶ added in v0.9.0
IsPrivateTargetAllowed reports whether addr may be dialed by a feature that talks to one operator-configured private service: loopback and private ranges pass, link-local, unspecified, multicast and metadata addresses do not.
func NewClient ¶
NewClient returns an HTTP client whose every connection, including redirects, is refused when it targets an internal address. It ignores HTTP(S)_PROXY, since a proxy would dial the destination on our behalf and bypass the check.
func NewClientAllowing ¶
NewClientAllowing is NewClient with its own opt-in env vars, so one feature can allow private addresses without relaxing the others. The first name is the one shown in the refusal message.
func NewClientPrivateNetworks ¶ added in v0.9.0
NewClientPrivateNetworks allows private-range dials (LAN services) but still refuses loopback, link-local and metadata addresses.
func NewClientPrivateTarget ¶ added in v0.9.0
NewClientPrivateTarget is NewClient for a single configured internal service. It allows private and loopback dials but never link-local or metadata addresses, and it does not change the guard for other callers.
func ValidateURL ¶
ValidateURL rejects, at configuration time, a URL every later send would refuse: a non-http(s) URL, a loopback or private IP literal, localhost, or a hostname whose every resolved address is internal. A hostname that does not resolve here is accepted, since resolution can differ at send time.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"github.com/GLINCKER/levelrail/kit/netguard"
)
func main() {
err := netguard.ValidateURL(context.Background(), "http://127.0.0.1:8080/hook")
fmt.Println(errors.Is(err, netguard.ErrBlockedAddress))
err = netguard.ValidateURL(context.Background(), "ftp://example.com/hook")
fmt.Println(errors.Is(err, netguard.ErrInvalidURL))
}
Output: true true
Types ¶
This section is empty.