1
0

netsafe.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. package netsafe
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net"
  7. "net/netip"
  8. "regexp"
  9. "strings"
  10. "time"
  11. )
  12. // ErrPrivateAddressBlocked marks a failed dial where the guard refused at least
  13. // one resolved address, so a caller offering an opt-in can tell it apart from an
  14. // ordinary connection failure.
  15. var ErrPrivateAddressBlocked = errors.New("blocked private/internal address")
  16. // Ranges Go's net.IP predicates do not treat as internal. The transition
  17. // mechanisms here are deprecated (RFC 7526) or local-use, so none carry public traffic.
  18. var blockedPrefixes = []netip.Prefix{
  19. netip.MustParsePrefix("100.64.0.0/10"), // CGNAT (RFC 6598)
  20. netip.MustParsePrefix("2002::/16"), // 6to4 (RFC 3056)
  21. netip.MustParsePrefix("2001::/32"), // Teredo (RFC 4380)
  22. netip.MustParsePrefix("64:ff9b:1::/48"), // NAT64 local-use (RFC 8215)
  23. netip.MustParsePrefix("fec0::/10"), // site-local (RFC 3879)
  24. }
  25. // Judged by the IPv4 it embeds rather than blocked outright: on a DNS64 network
  26. // every public IPv4 host resolves into this prefix (RFC 6052 mandates /96 here).
  27. var nat64WellKnown = netip.MustParsePrefix("64:ff9b::/96")
  28. func IsBlockedIP(ip net.IP) bool {
  29. if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||
  30. ip.IsLinkLocalMulticast() || ip.IsUnspecified() {
  31. return true
  32. }
  33. addr, ok := netip.AddrFromSlice(ip)
  34. if !ok {
  35. return false
  36. }
  37. addr = addr.Unmap()
  38. for _, prefix := range blockedPrefixes {
  39. if prefix.Contains(addr) {
  40. return true
  41. }
  42. }
  43. if nat64WellKnown.Contains(addr) {
  44. embedded := addr.As16()
  45. return IsBlockedIP(net.IP(embedded[12:16]))
  46. }
  47. return false
  48. }
  49. type allowPrivateCtxKey struct{}
  50. func ContextWithAllowPrivate(ctx context.Context, allow bool) context.Context {
  51. return context.WithValue(ctx, allowPrivateCtxKey{}, allow)
  52. }
  53. func AllowPrivateFromContext(ctx context.Context) bool {
  54. v, _ := ctx.Value(allowPrivateCtxKey{}).(bool)
  55. return v
  56. }
  57. var defaultDialer = &net.Dialer{Timeout: 10 * time.Second}
  58. func SSRFGuardedDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
  59. host, port, err := net.SplitHostPort(addr)
  60. if err != nil {
  61. return nil, err
  62. }
  63. allowPrivate := AllowPrivateFromContext(ctx)
  64. var ips []net.IPAddr
  65. if ip := net.ParseIP(host); ip != nil {
  66. ips = []net.IPAddr{{IP: ip}}
  67. } else {
  68. ips, err = net.DefaultResolver.LookupIPAddr(ctx, host)
  69. if err != nil {
  70. return nil, err
  71. }
  72. }
  73. var lastErr, blockedErr error
  74. for _, ipAddr := range ips {
  75. if !allowPrivate && IsBlockedIP(ipAddr.IP) {
  76. blockedErr = fmt.Errorf("%w %s", ErrPrivateAddressBlocked, ipAddr.IP)
  77. continue
  78. }
  79. conn, derr := defaultDialer.DialContext(ctx, network, net.JoinHostPort(ipAddr.IP.String(), port))
  80. if derr == nil {
  81. return conn, nil
  82. }
  83. lastErr = derr
  84. }
  85. // A dual-stack name can mix refused and merely unreachable addresses, so the
  86. // refusal is reported alongside instead of being lost to the last failure.
  87. if blockedErr != nil {
  88. if lastErr != nil {
  89. return nil, fmt.Errorf("%w; %w", blockedErr, lastErr)
  90. }
  91. return nil, blockedErr
  92. }
  93. if lastErr == nil {
  94. lastErr = fmt.Errorf("no usable address for %s", host)
  95. }
  96. return nil, lastErr
  97. }
  98. 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])?)*$`)
  99. func NormalizeHost(addr string) (string, error) {
  100. addr = strings.TrimSpace(addr)
  101. if addr == "" {
  102. return "", fmt.Errorf("address is required")
  103. }
  104. if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
  105. addr = addr[1 : len(addr)-1]
  106. }
  107. if ip := net.ParseIP(addr); ip != nil {
  108. return ip.String(), nil
  109. }
  110. if len(addr) > 253 || !hostnamePattern.MatchString(addr) {
  111. return "", fmt.Errorf("invalid host %q", addr)
  112. }
  113. return addr, nil
  114. }