1package proxy
2
3import (
4 "errors"
5 "net"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/gordon/internal/domain"
12)
13
14func TestIsBlockedTarget_IPAddresses(t *testing.T) {
15 tests := []struct {
16 name string
17 host string
18 blocked bool
19 }{
20 // IPv4 loopback - should block
21 {"IPv4 loopback 127.0.0.1", "127.0.0.1", true},
22 {"IPv4 loopback 127.0.0.2", "127.0.0.2", true},
23 {"IPv4 loopback 127.255.255.255", "127.255.255.255", true},
24
25 // IPv4 private networks - should block
26 {"10.0.0.0/8 network", "10.0.0.1", true},
27 {"10.0.0.0/8 network edge", "10.255.255.255", true},
28 {"172.16.0.0/12 network start", "172.16.0.1", true},
29 {"172.16.0.0/12 network middle", "172.20.0.1", true},
30 {"172.16.0.0/12 network end", "172.31.255.255", true},
31 {"192.168.0.0/16 network", "192.168.1.1", true},
32 {"192.168.0.0/16 network edge", "192.168.255.255", true},
33
34 // AWS/GCP/Azure metadata endpoint - should block
35 {"AWS metadata endpoint", "169.254.169.254", true},
36 {"Link-local address", "169.254.0.1", true},
37
38 // IPv6 loopback - should block
39 {"IPv6 loopback", "::1", true},
40
41 // IPv6 unique local - should block
42 {"IPv6 unique local fc00", "fc00::1", true},
43 {"IPv6 unique local fd00", "fd00::1", true},
44
45 // IPv6 link-local - should block
46 {"IPv6 link-local", "fe80::1", true},
47
48 // Public IPs - should allow
49 {"Google DNS", "8.8.8.8", false},
50 {"Cloudflare DNS", "1.1.1.1", false},
51 {"Public IP", "203.0.113.1", false},
52 {"Public IPv6", "2001:4860:4860::8888", false},
53
54 // Edge cases for 172.16.0.0/12
55 {"Just outside 172.16.0.0/12", "172.15.255.255", false},
56 {"Just outside 172.16.0.0/12 upper", "172.32.0.0", false},
57 }
58
59 for _, tt := range tests {
60 t.Run(tt.name, func(t *testing.T) {
61 blocked, err := IsBlockedTarget(tt.host)
62 require.NoError(t, err)
63 assert.Equal(t, tt.blocked, blocked, "IsBlockedTarget(%q) = %v, want %v", tt.host, blocked, tt.blocked)
64 })
65 }
66}
67
68func TestIsBlockedTarget_Hostnames(t *testing.T) {
69 tests := []struct {
70 name string
71 host string
72 wantBlocked bool
73 wantErr bool
74 }{
75 // Localhost hostname - should block
76 {
77 name: "localhost",
78 host: "localhost",
79 wantBlocked: true,
80 wantErr: false,
81 },
82
83 // Public domains - should allow (assuming they resolve to public IPs)
84 {
85 name: "google.com",
86 host: "google.com",
87 wantBlocked: false,
88 wantErr: false,
89 },
90
91 // Non-existent domain - should error and block
92 {
93 name: "non-existent domain",
94 host: "this-domain-definitely-does-not-exist-12345.invalid",
95 wantBlocked: true,
96 wantErr: true,
97 },
98 }
99
100 for _, tt := range tests {
101 t.Run(tt.name, func(t *testing.T) {
102 blocked, err := IsBlockedTarget(tt.host)
103
104 if tt.wantErr {
105 assert.Error(t, err)
106 }
107
108 assert.Equal(t, tt.wantBlocked, blocked, "IsBlockedTarget(%q) blocked = %v, want %v", tt.host, blocked, tt.wantBlocked)
109 })
110 }
111}
112
113func TestValidateExternalRouteTarget(t *testing.T) {
114 tests := []struct {
115 name string
116 host string
117 wantErr error
118 }{
119 // Blocked targets
120 {
121 name: "loopback IP",
122 host: "127.0.0.1",
123 wantErr: domain.ErrSSRFBlocked,
124 },
125 {
126 name: "private network",
127 host: "10.0.0.1",
128 wantErr: domain.ErrSSRFBlocked,
129 },
130 {
131 name: "AWS metadata",
132 host: "169.254.169.254",
133 wantErr: domain.ErrSSRFBlocked,
134 },
135 {
136 name: "localhost",
137 host: "localhost",
138 wantErr: domain.ErrSSRFBlocked,
139 },
140
141 // Allowed targets
142 {
143 name: "public IP",
144 host: "8.8.8.8",
145 wantErr: nil,
146 },
147 }
148
149 for _, tt := range tests {
150 t.Run(tt.name, func(t *testing.T) {
151 err := ValidateExternalRouteTarget(tt.host)
152
153 if tt.wantErr != nil {
154 require.Error(t, err)
155 assert.True(t, errors.Is(err, tt.wantErr), "expected error %v, got %v", tt.wantErr, err)
156 } else {
157 assert.NoError(t, err)
158 }
159 })
160 }
161}
162
163func TestResolveAndValidateHost_IPAddresses(t *testing.T) {
164 tests := []struct {
165 name string
166 host string
167 wantIP string
168 wantErr bool
169 }{
170 {"public IP returned as-is", "203.0.113.1", "203.0.113.1", false},
171 {"blocked loopback IP", "127.0.0.1", "", true},
172 {"blocked private IP", "10.0.0.1", "", true},
173 {"blocked metadata IP", "169.254.169.254", "", true},
174 }
175 for _, tt := range tests {
176 t.Run(tt.name, func(t *testing.T) {
177 ip, err := ResolveAndValidateHost(tt.host)
178 if tt.wantErr {
179 assert.Error(t, err)
180 assert.True(t, errors.Is(err, domain.ErrSSRFBlocked))
181 } else {
182 assert.NoError(t, err)
183 assert.Equal(t, tt.wantIP, ip)
184 }
185 })
186 }
187}
188
189func TestResolveAndValidateHost_Hostname(t *testing.T) {
190 // localhost should resolve to a blocked IP
191 _, err := ResolveAndValidateHost("localhost")
192 assert.Error(t, err)
193 assert.True(t, errors.Is(err, domain.ErrSSRFBlocked))
194
195 // Non-existent domain should error
196 _, err = ResolveAndValidateHost("this-domain-definitely-does-not-exist-12345.invalid")
197 assert.Error(t, err)
198 assert.True(t, errors.Is(err, domain.ErrSSRFBlocked))
199}
200
201func TestIsBlockedIP(t *testing.T) {
202 tests := []struct {
203 ip string
204 blocked bool
205 }{
206 // Should block
207 {"127.0.0.1", true},
208 {"10.0.0.1", true},
209 {"172.16.0.1", true},
210 {"192.168.1.1", true},
211 {"169.254.169.254", true},
212 {"0.0.0.0", true},
213
214 // Should allow
215 {"8.8.8.8", false},
216 {"1.1.1.1", false},
217 {"203.0.113.1", false},
218 }
219
220 for _, tt := range tests {
221 t.Run(tt.ip, func(t *testing.T) {
222 ip := net.ParseIP(tt.ip)
223 require.NotNil(t, ip, "failed to parse IP: %s", tt.ip)
224
225 blocked := isBlockedIP(ip)
226 assert.Equal(t, tt.blocked, blocked, "isBlockedIP(%s) = %v, want %v", tt.ip, blocked, tt.blocked)
227 })
228 }
229}