1// Package proxy implements the reverse proxy use case.
2package proxy
3
4import (
5 "fmt"
6 "net"
7 "sync"
8
9 "github.com/bnema/gordon/internal/domain"
10)
11
12// blockedCIDRStrings contains CIDR blocks that should never be proxied to.
13// This prevents SSRF attacks targeting internal services or cloud metadata endpoints.
14var blockedCIDRStrings = []string{
15 // IPv4 loopback
16 "127.0.0.0/8",
17 // IPv4 private networks (RFC 1918)
18 "10.0.0.0/8",
19 "172.16.0.0/12",
20 "192.168.0.0/16",
21 // IPv4 link-local (cloud metadata endpoints like AWS/GCP/Azure)
22 "169.254.0.0/16",
23 // IPv4 localhost alternative
24 "0.0.0.0/8",
25 // IPv6 loopback
26 "::1/128",
27 // IPv6 unique local addresses (RFC 4193)
28 "fc00::/7",
29 // IPv6 link-local
30 "fe80::/10",
31}
32
33var (
34 blockedCIDRs []*net.IPNet
35 blockedCIDRsOnce sync.Once
36)
37
38// initBlockedCIDRs parses the blocked CIDR strings into net.IPNet.
39// Called lazily on first use.
40func initBlockedCIDRs() {
41 blockedCIDRsOnce.Do(func() {
42 blockedCIDRs = make([]*net.IPNet, 0, len(blockedCIDRStrings))
43 for _, cidr := range blockedCIDRStrings {
44 _, network, err := net.ParseCIDR(cidr)
45 if err != nil {
46 // This should never happen with our hardcoded CIDRs
47 panic(fmt.Sprintf("invalid blocked CIDR: %s: %v", cidr, err))
48 }
49 blockedCIDRs = append(blockedCIDRs, network)
50 }
51 })
52}
53
54// isBlockedIP checks if an IP address is in any of the blocked CIDR ranges.
55func isBlockedIP(ip net.IP) bool {
56 initBlockedCIDRs()
57 for _, network := range blockedCIDRs {
58 if network.Contains(ip) {
59 return true
60 }
61 }
62 return false
63}
64
65// IsBlockedTarget checks if a target host is in a blocked network range.
66// It handles both IP addresses and hostnames (via DNS resolution).
67// Returns (blocked, error) where error indicates DNS resolution failure.
68//
69// SECURITY: This function is used to prevent SSRF attacks by blocking
70// requests to internal networks, localhost, and cloud metadata endpoints.
71func IsBlockedTarget(host string) (bool, error) {
72 initBlockedCIDRs()
73
74 // First, try to parse as IP directly
75 if ip := net.ParseIP(host); ip != nil {
76 return isBlockedIP(ip), nil
77 }
78
79 // Not an IP address, resolve DNS
80 ips, err := net.LookupIP(host)
81 if err != nil {
82 // DNS resolution failed - block by default for safety
83 // This prevents DNS rebinding attacks where an attacker controls
84 // a domain that initially resolves to a public IP, then later
85 // resolves to an internal IP.
86 return true, fmt.Errorf("DNS resolution failed for %s: %w", host, err)
87 }
88
89 if len(ips) == 0 {
90 // No IPs resolved - block for safety
91 return true, fmt.Errorf("no IP addresses found for %s", host)
92 }
93
94 // Check all resolved IPs - block if ANY resolve to a blocked network
95 for _, ip := range ips {
96 if isBlockedIP(ip) {
97 return true, nil
98 }
99 }
100
101 return false, nil
102}
103
104// ValidateExternalRouteTarget validates that an external route target
105// is safe to proxy to. Returns domain.ErrSSRFBlocked if the target
106// resolves to an internal/blocked network.
107func ValidateExternalRouteTarget(host string) error {
108 blocked, err := IsBlockedTarget(host)
109 if err != nil {
110 // DNS error - treat as blocked for security
111 return fmt.Errorf("%w: %v", domain.ErrSSRFBlocked, err)
112 }
113 if blocked {
114 return domain.ErrSSRFBlocked
115 }
116 return nil
117}
118
119// ResolveAndValidateHost resolves DNS for a hostname and returns the first
120// resolved IP that is not in a blocked CIDR range. If all resolved IPs are
121// blocked, returns ErrSSRFBlocked. For raw IP addresses, validates and returns
122// them directly.
123//
124// SECURITY: This function pins DNS resolution to prevent TOCTOU/DNS-rebinding
125// attacks where a hostname resolves to a public IP during validation but to a
126// private IP when the proxy actually connects.
127func ResolveAndValidateHost(host string) (string, error) {
128 initBlockedCIDRs()
129
130 // If already an IP address, validate and return directly
131 if ip := net.ParseIP(host); ip != nil {
132 if isBlockedIP(ip) {
133 return "", domain.ErrSSRFBlocked
134 }
135 return host, nil
136 }
137
138 // Resolve DNS
139 ips, err := net.LookupIP(host)
140 if err != nil {
141 return "", fmt.Errorf("%w: DNS resolution failed for %s: %v", domain.ErrSSRFBlocked, host, err)
142 }
143 if len(ips) == 0 {
144 return "", fmt.Errorf("%w: no IP addresses found for %s", domain.ErrSSRFBlocked, host)
145 }
146
147 // Find the first non-blocked IP
148 for _, ip := range ips {
149 if !isBlockedIP(ip) {
150 return ip.String(), nil
151 }
152 }
153
154 return "", domain.ErrSSRFBlocked
155}