| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- package netsafe
- import (
- "context"
- "errors"
- "fmt"
- "net"
- "net/netip"
- "regexp"
- "strings"
- "time"
- )
- // ErrPrivateAddressBlocked marks a failed dial where the guard refused at least
- // one resolved address, so a caller offering an opt-in can tell it apart from an
- // ordinary connection failure.
- var ErrPrivateAddressBlocked = errors.New("blocked private/internal address")
- // Ranges Go's net.IP predicates do not treat as internal. The transition
- // mechanisms here are deprecated (RFC 7526) or local-use, so none carry public traffic.
- var blockedPrefixes = []netip.Prefix{
- netip.MustParsePrefix("100.64.0.0/10"), // CGNAT (RFC 6598)
- netip.MustParsePrefix("2002::/16"), // 6to4 (RFC 3056)
- netip.MustParsePrefix("2001::/32"), // Teredo (RFC 4380)
- netip.MustParsePrefix("64:ff9b:1::/48"), // NAT64 local-use (RFC 8215)
- netip.MustParsePrefix("fec0::/10"), // site-local (RFC 3879)
- }
- // Judged by the IPv4 it embeds rather than blocked outright: on a DNS64 network
- // every public IPv4 host resolves into this prefix (RFC 6052 mandates /96 here).
- var nat64WellKnown = netip.MustParsePrefix("64:ff9b::/96")
- func IsBlockedIP(ip net.IP) bool {
- if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||
- ip.IsLinkLocalMulticast() || ip.IsUnspecified() {
- return true
- }
- addr, ok := netip.AddrFromSlice(ip)
- if !ok {
- return false
- }
- addr = addr.Unmap()
- for _, prefix := range blockedPrefixes {
- if prefix.Contains(addr) {
- return true
- }
- }
- if nat64WellKnown.Contains(addr) {
- embedded := addr.As16()
- return IsBlockedIP(net.IP(embedded[12:16]))
- }
- return false
- }
- type allowPrivateCtxKey struct{}
- func ContextWithAllowPrivate(ctx context.Context, allow bool) context.Context {
- return context.WithValue(ctx, allowPrivateCtxKey{}, allow)
- }
- func AllowPrivateFromContext(ctx context.Context) bool {
- v, _ := ctx.Value(allowPrivateCtxKey{}).(bool)
- return v
- }
- var defaultDialer = &net.Dialer{Timeout: 10 * time.Second}
- func SSRFGuardedDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
- host, port, err := net.SplitHostPort(addr)
- if err != nil {
- return nil, err
- }
- allowPrivate := AllowPrivateFromContext(ctx)
- var ips []net.IPAddr
- if ip := net.ParseIP(host); ip != nil {
- ips = []net.IPAddr{{IP: ip}}
- } else {
- ips, err = net.DefaultResolver.LookupIPAddr(ctx, host)
- if err != nil {
- return nil, err
- }
- }
- var lastErr, blockedErr error
- for _, ipAddr := range ips {
- if !allowPrivate && IsBlockedIP(ipAddr.IP) {
- blockedErr = fmt.Errorf("%w %s", ErrPrivateAddressBlocked, ipAddr.IP)
- continue
- }
- conn, derr := defaultDialer.DialContext(ctx, network, net.JoinHostPort(ipAddr.IP.String(), port))
- if derr == nil {
- return conn, nil
- }
- lastErr = derr
- }
- // A dual-stack name can mix refused and merely unreachable addresses, so the
- // refusal is reported alongside instead of being lost to the last failure.
- if blockedErr != nil {
- if lastErr != nil {
- return nil, fmt.Errorf("%w; %w", blockedErr, lastErr)
- }
- return nil, blockedErr
- }
- if lastErr == nil {
- lastErr = fmt.Errorf("no usable address for %s", host)
- }
- return nil, lastErr
- }
- var hostnamePattern = regexp.MustCompile(`^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$`)
- func NormalizeHost(addr string) (string, error) {
- addr = strings.TrimSpace(addr)
- if addr == "" {
- return "", fmt.Errorf("address is required")
- }
- if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
- addr = addr[1 : len(addr)-1]
- }
- if ip := net.ParseIP(addr); ip != nil {
- return ip.String(), nil
- }
- if len(addr) > 253 || !hostnamePattern.MatchString(addr) {
- return "", fmt.Errorf("invalid host %q", addr)
- }
- return addr, nil
- }
|