// Package netguard decides which network destinations a server-side feature // may be pointed at. Anything that makes outbound connections on a user's behalf // (webhooks, mail delivery) must refuse internal addresses by default, or it can // be used to probe the machine's own network and cloud metadata services. package netguard import ( "context" "errors" "net" "net/http" "strconv" "time" ) var internal = func() []*net.IPNet { var out []*net.IPNet for _, c := range []string{"0.0.0.0/8", "100.64.0.0/10", "192.0.0.0/24", "198.18.0.0/15", "240.0.0.0/4", "64:ff9b::/96", "2001:db8::/32"} { _, n, _ := net.ParseCIDR(c) out = append(out, n) } return out }() // Blocked reports whether ip is private, loopback, link-local, multicast, // unspecified, or in a range reserved for carrier-grade NAT (which includes // the WireGuard tunnel network) and similar. func Blocked(ip net.IP) bool { if v4 := ip.To4(); v4 != nil { ip = v4 } if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() { return true } for _, n := range internal { if n.Contains(ip) { return true } } return false } // ErrBlocked is returned when every address a name resolves to is internal. var ErrBlocked = errors.New("not allowed: that address is on an internal network") // DialContext resolves the host itself, checks every address, and connects to // the one it checked, so a name that changes between the check and the // connection (DNS rebinding) can't slip through. func DialContext(allowPrivate bool) func(ctx context.Context, network, addr string) (net.Conn, error) { return func(ctx context.Context, network, addr string) (net.Conn, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } var ips []net.IP if ip := net.ParseIP(host); ip != nil { ips = []net.IP{ip} } else { res, err := net.DefaultResolver.LookupIPAddr(ctx, host) if err != nil { return nil, err } for _, r := range res { ips = append(ips, r.IP) } } var last error = errors.New("no addresses") blocked := false d := net.Dialer{Timeout: 10 * time.Second} for _, ip := range ips { if !allowPrivate && Blocked(ip) { blocked = true continue } c, err := d.DialContext(ctx, network, net.JoinHostPort(ip.String(), port)) if err == nil { return c, nil } last = err } if blocked && !allowPrivate { return nil, ErrBlocked } return nil, last } } // Client is an HTTP client for user-supplied URLs: guarded dialing, a short // timeout, and no following of redirects (a redirect could point somewhere internal). func Client(allowPrivate bool) *http.Client { return &http.Client{ Timeout: 12 * time.Second, Transport: &http.Transport{DialContext: DialContext(allowPrivate), TLSHandshakeTimeout: 10 * time.Second, DisableKeepAlives: true}, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, } } // PortOf is a small helper for URLs without an explicit port. func PortOf(scheme, port string) string { if port != "" { return port } if scheme == "https" { return strconv.Itoa(443) } return strconv.Itoa(80) }