main
go 394 lines 13.3 KB
Raw
1 package portal
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "net"
8 "testing"
9 "time"
10
11 "github.com/gosuda/portal-tunnel/v2/portal/identity"
12 "github.com/gosuda/portal-tunnel/v2/portal/policy"
13 "github.com/gosuda/portal-tunnel/v2/portal/transport"
14 "github.com/gosuda/portal-tunnel/v2/types"
15 "github.com/gosuda/portal-tunnel/v2/utils"
16 )
17
18 func newTestRegistry(t *testing.T) *leaseRegistry {
19 t.Helper()
20 relay, err := identity.LoadOrCreateRelayIdentity(t.TempDir(), "example.com", false)
21 if err != nil {
22 t.Fatalf("LoadOrCreateRelayIdentity() error = %v", err)
23 }
24 relayAuthority, err := identity.NewLocalAuthority(relay.Identity)
25 if err != nil {
26 t.Fatalf("identity.NewLocalAuthority() error = %v", err)
27 }
28 registry, err := newLeaseRegistry(false, false, 10000, 10100, relay.Name, 443, relayAuthority, "https://example.com", false, "")
29 if err != nil {
30 t.Fatalf("newLeaseRegistry() error = %v", err)
31 }
32 return registry
33 }
34
35 func newTestLeaseIdentity(t *testing.T, name string) types.Identity {
36 t.Helper()
37 testIdentity, err := identity.ResolveSecp256k1Identity("")
38 if err != nil {
39 t.Fatalf("identity.ResolveSecp256k1Identity() error = %v", err)
40 }
41 testIdentity.Name = name
42 return testIdentity
43 }
44
45 func TestLeaseRegistryLifecycle(t *testing.T) {
46 t.Parallel()
47
48 registry := newTestRegistry(t)
49 runtime := registry.policy
50 record, registered, err := registry.Register(types.RegisterChallengeRequest{
51 Identity: newTestLeaseIdentity(t, "demo"),
52 }, "203.0.113.10", "")
53 if err != nil {
54 t.Fatalf("Register() error = %v", err)
55 }
56
57 lookedUp, ok := registry.Lookup("demo.example.com")
58 if !ok || lookedUp != record {
59 t.Fatalf("Lookup() = %v, %v, want registered lease", lookedUp, ok)
60 }
61
62 renewed, err := registry.Renew(types.RenewRequest{
63 AccessToken: registered.AccessToken,
64 TTL: int(time.Minute / time.Second),
65 }, "203.0.113.11")
66 if err != nil {
67 t.Fatalf("Renew() error = %v", err)
68 }
69 if record.ClientIP != "203.0.113.11" {
70 t.Fatalf("Renew() client ip = %q, want %q", record.ClientIP, "203.0.113.11")
71 }
72 if !renewed.ExpiresAt.Equal(record.ExpiresAt) {
73 t.Fatalf("Renew() expires at = %v, want %v", renewed.ExpiresAt, record.ExpiresAt)
74 }
75 if renewed.AccessToken == "" {
76 t.Fatal("Renew() access token is empty")
77 }
78 if got := runtime.IPFilter().IdentityIP(record.Key()); got != "203.0.113.11" {
79 t.Fatalf("Renew() did not register client IP for lease")
80 }
81
82 removed, err := registry.Unregister(types.UnregisterRequest{AccessToken: renewed.AccessToken})
83 if err != nil {
84 t.Fatalf("Unregister() error = %v", err)
85 }
86 if removed != record {
87 t.Fatalf("Unregister() record = %v, want original record", removed)
88 }
89
90 if _, ok := registry.Lookup("demo.example.com"); ok {
91 t.Fatal("Lookup() after Unregister() = true, want false")
92 }
93 if got := runtime.IPFilter().IdentityIP(record.Key()); got != "" {
94 t.Fatalf("Unregister() lease IP = %q, want empty", got)
95 }
96 }
97
98 func TestLeaseRegistryAutomaticECHRouteFallsBackToPlainSNI(t *testing.T) {
99 t.Parallel()
100
101 registry := newTestRegistry(t)
102 routeHostname := "ech-auto-ech.example.com"
103 publicHostname := "auto-ech.example.com"
104 record, _, err := registry.Register(types.RegisterChallengeRequest{
105 Identity: newTestLeaseIdentity(t, "auto-ech"),
106 RouteHostname: routeHostname,
107 HostnameHash: utils.HostnameHash(publicHostname),
108 }, "203.0.113.10", "")
109 if err != nil {
110 t.Fatalf("Register() error = %v", err)
111 }
112 if record.Hostname != routeHostname {
113 t.Fatalf("Register() route hostname = %q, want %q", record.Hostname, routeHostname)
114 }
115 if record.Hostname == publicHostname {
116 t.Fatalf("Register() route hostname = public hostname = %q", record.Hostname)
117 }
118 if lookedUp, ok := registry.Lookup(publicHostname); !ok || lookedUp != record {
119 t.Fatalf("Lookup(public hostname) = %v, %v, want fallback lease", lookedUp, ok)
120 }
121 lookedUp, ok := registry.Lookup(record.Hostname)
122 if !ok || lookedUp != record {
123 t.Fatalf("Lookup(route hostname) = %v, %v, want registered lease", lookedUp, ok)
124 }
125 leases := registry.PublicLeases(time.Now())
126 if len(leases) != 1 {
127 t.Fatalf("PublicLeases() length = %d, want 1", len(leases))
128 }
129 if leases[0].Hostname != publicHostname {
130 t.Fatalf("PublicLeases()[0].Hostname = %q, want %q", leases[0].Hostname, publicHostname)
131 }
132
133 policyLeases := registry.PolicyLeases(time.Now())
134 if len(policyLeases) != 1 {
135 t.Fatalf("PolicyLeases() length = %d, want 1", len(policyLeases))
136 }
137 if policyLeases[0].Hostname != publicHostname {
138 t.Fatalf("PolicyLeases()[0] hostname = %q, want %q", policyLeases[0].Hostname, publicHostname)
139 }
140
141 if _, _, err := registry.Register(types.RegisterChallengeRequest{
142 Identity: newTestLeaseIdentity(t, "hash-only"),
143 HostnameHash: utils.HostnameHash("hash-only.example.com"),
144 }, "203.0.113.10", ""); err == nil {
145 t.Fatal("Register(fallback hash only) error = nil, want error")
146 }
147
148 if _, _, err := registry.Register(types.RegisterChallengeRequest{
149 Identity: newTestLeaseIdentity(t, "attacker"),
150 RouteHostname: "ech-attacker.example.com",
151 HostnameHash: utils.HostnameHash("victim.example.com"),
152 }, "203.0.113.10", ""); err == nil {
153 t.Fatal("Register(mismatched hostname hash) error = nil, want error")
154 }
155 if lookedUp, ok := registry.Lookup("victim.example.com"); ok {
156 t.Fatalf("Lookup(victim hostname) = %v, true; mismatched hash must not route", lookedUp)
157 }
158 }
159
160 func TestLeaseRegistryHopRouteCanExposeECHAndPlainSNIFallback(t *testing.T) {
161 t.Parallel()
162
163 registry := newTestRegistry(t)
164 owner := newTestLeaseIdentity(t, "multi-hop-owner")
165 wgPrivate, err := identity.GenerateWireGuardPrivateKey()
166 if err != nil {
167 t.Fatalf("identity.GenerateWireGuardPrivateKey() error = %v", err)
168 }
169 wgPublic, err := identity.WireGuardPublicKeyFromPrivate(wgPrivate)
170 if err != nil {
171 t.Fatalf("identity.WireGuardPublicKeyFromPrivate() error = %v", err)
172 }
173 now := time.Now()
174 baseRoute := types.HopRoute{
175 OwnerPublicKey: owner.PublicKey,
176 ForwardRelay: types.RelayDescriptor{
177 APIHTTPSAddr: "https://next.example.com",
178 WireGuardPublicKey: wgPublic,
179 },
180 ForwardToken: "hpt_forward",
181 FirstSeenAt: now,
182 ExpiresAt: now.Add(time.Minute),
183 }
184 route := baseRoute
185 route.RouteHostname = "ech-demo.example.com"
186 route.PublicHostname = "demo.example.com"
187 route.HostnameHash = utils.HostnameHash("demo.example.com")
188 route.Metadata.Hide = true
189
190 if _, err := registry.RegisterHopRoute(&route, now); err != nil {
191 t.Fatalf("RegisterHopRoute() error = %v", err)
192 }
193 hashOnlyRoute := baseRoute
194 hashOnlyRoute.HostnameHash = utils.HostnameHash("hash-only.example.com")
195 if _, err := registry.RegisterHopRoute(&hashOnlyRoute, now); err == nil {
196 t.Fatal("RegisterHopRoute(hash only) error = nil, want error")
197 }
198 missingPublicRoute := baseRoute
199 missingPublicRoute.RouteHostname = "ech-missing-public.example.com"
200 missingPublicRoute.HostnameHash = utils.HostnameHash("missing-public.example.com")
201 if _, err := registry.RegisterHopRoute(&missingPublicRoute, now); err == nil {
202 t.Fatal("RegisterHopRoute(missing public hostname) error = nil, want error")
203 }
204 mismatchedRoute := baseRoute
205 mismatchedRoute.RouteHostname = "ech-attacker.example.com"
206 mismatchedRoute.PublicHostname = "attacker.example.com"
207 mismatchedRoute.HostnameHash = utils.HostnameHash("victim.example.com")
208 if _, err := registry.RegisterHopRoute(&mismatchedRoute, now); err == nil {
209 t.Fatal("RegisterHopRoute(mismatched hostname hash) error = nil, want error")
210 }
211 if lookedUp, ok := registry.Lookup("victim.example.com"); ok {
212 t.Fatalf("Lookup(victim hostname) = %v, true; mismatched hop hash must not route", lookedUp)
213 }
214 if _, ok := registry.Lookup("demo.example.com"); !ok {
215 t.Fatal("Lookup(plain route) = false, want true")
216 }
217 if _, ok := registry.Lookup(route.RouteHostname); !ok {
218 t.Fatal("Lookup(ech route) = false, want true")
219 }
220 leases := registry.PublicLeases(now)
221 if len(leases) != 0 {
222 t.Fatalf("PublicLeases() length = %d, want 0 for hostname-minimized hop routes", len(leases))
223 }
224 }
225
226 func TestLeaseRegistryWildcardAndConflict(t *testing.T) {
227 t.Parallel()
228
229 registry := newTestRegistry(t)
230 wildcardLease := &leaseRecord{
231 Identity: newTestLeaseIdentity(t, "wildcard"),
232 Hostname: "*.example.com",
233 ExpiresAt: time.Now().Add(30 * time.Second),
234 }
235 registry.records = append(registry.records, wildcardLease)
236
237 if _, ok := registry.Lookup("app.example.com"); !ok {
238 t.Fatal("Lookup(one-level wildcard) = false, want true")
239 }
240 if _, ok := registry.Lookup("deep.app.example.com"); ok {
241 t.Fatal("Lookup(multi-level wildcard) = true, want false")
242 }
243
244 if _, _, err := registry.Register(types.RegisterChallengeRequest{
245 Identity: newTestLeaseIdentity(t, "conflict"),
246 }, "203.0.113.10", ""); err != nil {
247 t.Fatalf("Register(conflict first) error = %v", err)
248 }
249 _, _, err := registry.Register(types.RegisterChallengeRequest{
250 Identity: newTestLeaseIdentity(t, "conflict"),
251 }, "203.0.113.11", "")
252 if !errors.Is(err, errHostnameConflict) {
253 t.Fatalf("Register(conflict second) error = %v, want hostname conflict", err)
254 }
255 }
256
257 func TestLeaseRegistryPolicyLeasesAndRoutableUsePolicy(t *testing.T) {
258 t.Parallel()
259
260 registry := newTestRegistry(t)
261 runtime := registry.policy
262 if err := runtime.Approver().SetMode(policy.ModeManual); err != nil {
263 t.Fatalf("SetMode() error = %v", err)
264 }
265 record, _, err := registry.Register(types.RegisterChallengeRequest{
266 Identity: newTestLeaseIdentity(t, "demo"),
267 }, "203.0.113.20", "")
268 if err != nil {
269 t.Fatalf("Register() error = %v", err)
270 }
271
272 if registry.policy.IsIdentityRoutable(record.Key()) {
273 t.Fatal("policy.IsIdentityRoutable() = true, want false before approval")
274 }
275
276 leases := registry.PolicyLeases(time.Now())
277 if len(leases) != 1 {
278 t.Fatalf("PolicyLeases() length = %d, want 1", len(leases))
279 }
280 if leases[0].IsApproved {
281 t.Fatal("PolicyLeases()[0].IsApproved = true, want false before approval")
282 }
283 if got := runtime.IPFilter().IdentityIP(record.Key()); got != "203.0.113.20" {
284 t.Fatalf("Register() lease IP = %q, want %q", got, "203.0.113.20")
285 }
286
287 runtime.Approver().Approve(record.Key())
288 if !registry.policy.IsIdentityRoutable(record.Key()) {
289 t.Fatal("policy.IsIdentityRoutable() = false, want true after approval")
290 }
291
292 leases = registry.PolicyLeases(time.Now())
293 if len(leases) != 1 {
294 t.Fatalf("PolicyLeases() length = %d, want 1", len(leases))
295 }
296 if !leases[0].IsApproved {
297 t.Fatal("PolicyLeases()[0].IsApproved = false, want true after approval")
298 }
299 }
300
301 func TestLeaseRegistryPublicLeasesIncludesIngressRouteInManualApproval(t *testing.T) {
302 t.Parallel()
303
304 registry := newTestRegistry(t)
305 if err := registry.policy.Approver().SetMode(policy.ModeManual); err != nil {
306 t.Fatalf("SetMode() error = %v", err)
307 }
308 route := &leaseRecord{
309 Identity: newTestLeaseIdentity(t, "demo"),
310 Hostname: "demo.example.com",
311 ExpiresAt: time.Now().Add(30 * time.Second),
312 }
313 registry.records = append(registry.records, route)
314
315 leases := registry.PublicLeases(time.Now())
316 if len(leases) != 1 {
317 t.Fatalf("PublicLeases() length = %d, want 1", len(leases))
318 }
319 if leases[0].Hostname != route.Hostname {
320 t.Fatalf("PublicLeases()[0].Hostname = %q, want %q", leases[0].Hostname, route.Hostname)
321 }
322 }
323
324 func TestLeaseRegistryCleanupExpiredClosesBroker(t *testing.T) {
325 t.Parallel()
326
327 registry := newTestRegistry(t)
328 record := &leaseRecord{
329 Identity: newTestLeaseIdentity(t, "expired"),
330 Hostname: "expired.example.com",
331 ExpiresAt: time.Now().Add(-time.Second),
332 stream: transport.NewRelayStream("addr-expired", time.Minute, 1),
333 }
334 registry.records = append(registry.records, record)
335
336 registry.cleanupExpired(time.Now())
337
338 if _, ok := registry.Lookup("expired.example.com"); ok {
339 t.Fatal("Lookup() after cleanupExpired() = true, want false")
340 }
341 if _, err := record.stream.Claim(context.Background()); !errors.Is(err, net.ErrClosed) {
342 t.Fatalf("Claim() after cleanupExpired() error = %v, want %v", err, net.ErrClosed)
343 }
344 }
345
346 func TestIssueRegisterChallengeBoundsPendingPerIP(t *testing.T) {
347 t.Parallel()
348
349 registry := newTestRegistry(t)
350 clientIP := "203.0.113.50"
351 for i := 0; i < defaultRegisterChallengeOutstandingPerIP; i++ {
352 _, err := registry.issueRegisterChallenge(types.RegisterChallengeRequest{
353 Identity: newTestLeaseIdentity(t, fmt.Sprintf("demo-%d", i)),
354 }, "example.com", "https://example.com"+types.PathSDKRegister, clientIP)
355 if err != nil {
356 t.Fatalf("issueRegisterChallenge(%d) error = %v", i, err)
357 }
358 }
359
360 _, err := registry.issueRegisterChallenge(types.RegisterChallengeRequest{
361 Identity: newTestLeaseIdentity(t, "overflow"),
362 }, "example.com", "https://example.com"+types.PathSDKRegister, clientIP)
363 if !errors.Is(err, errRegisterChallengePending) {
364 t.Fatalf("issueRegisterChallenge() error = %v, want pending limit", err)
365 }
366
367 expiredAt := time.Now().Add(-time.Second)
368 registry.mu.Lock()
369 for _, record := range registry.records {
370 if record == nil || record.registerChallenge == nil {
371 continue
372 }
373 record.ExpiresAt = expiredAt
374 record.registerChallenge.ExpiresAt = expiredAt
375 }
376 registry.mu.Unlock()
377
378 _, err = registry.issueRegisterChallenge(types.RegisterChallengeRequest{
379 Identity: newTestLeaseIdentity(t, "after-cleanup"),
380 }, "example.com", "https://example.com"+types.PathSDKRegister, clientIP)
381 if err != nil {
382 t.Fatalf("issueRegisterChallenge() after expired cleanup error = %v", err)
383 }
384 }
385
386 func TestServerRunRegistryJanitorRejectsNonPositiveInterval(t *testing.T) {
387 t.Parallel()
388
389 server := &Server{registry: newTestRegistry(t)}
390 err := server.runRegistryJanitor(context.Background(), 0)
391 if err == nil {
392 t.Fatal("runRegistryJanitor() error = nil, want validation error")
393 }
394 }