Skip to content

Commit ab13522

Browse files
authored
Merge pull request #258 from gosuda/local-multihop-harness
Add local multi-hop relay test harness
2 parents 5d3cffd + 3c82805 commit ab13522

11 files changed

Lines changed: 625 additions & 29 deletions

File tree

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help install fmt vet lint lint-auto test vuln tidy all run build build-frontend build-docs build-tunnel build-server clean load-test
1+
.PHONY: help install fmt vet lint lint-auto test test-local-multihop vuln tidy all run build build-frontend build-docs build-tunnel build-server clean load-test
22

33
.DEFAULT_GOAL := help
44

@@ -15,6 +15,8 @@ help:
1515
@echo " make install - Install Go developer tools used by this repo"
1616
@echo " make fmt - Apply gofmt/goimports"
1717
@echo " make lint-auto - Run autofix lint/format pipeline"
18+
@echo " make test - Run Go tests, including the local multi-hop harness"
19+
@echo " make test-local-multihop - Run the focused local multi-hop relay harness"
1820
@echo " make build - Build everything (frontend, tunnel, server)"
1921
@echo " make build-frontend - Build React frontend (Tailwind CSS 4)"
2022
@echo " make build-docs - Build documentation site (SvelteKit)"
@@ -46,6 +48,9 @@ lint-auto:
4648
test:
4749
go test -v -coverprofile=coverage.out $(GO_PACKAGES)
4850

51+
test-local-multihop:
52+
go test -v ./portal -run 'TestLocalCluster'
53+
4954
vuln:
5055
govulncheck $(GO_PACKAGES)
5156

docs/src/routes/architecture/+page.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,14 @@ Result: raw public UDP exposure with an internal QUIC datagram backhaul. UDP and
300300
- The overlay peer API is plain HTTP on the WireGuard network, not public Internet HTTP. It serves the same discovery payload shape used by public `/discovery`.
301301
- Overlay failure affects inter-relay discovery, mesh synchronization, and multi-hop relay forwarding. Direct tenant TLS routing, keyless TLS, register/renew/connect, and public UDP ingress do not depend on the WireGuard transport path.
302302

303+
### Local multi-hop test harness
304+
305+
`portal/multihop_local_test.go` verifies the multi-hop relay path inside one `go test` process. It starts local relay servers on `127.0.0.1:0`, seeds signed local discovery descriptors, and uses test fakes only for the WireGuard transport layer.
306+
307+
Use `make test-local-multihop` for the focused local harness. The same tests are also included in `make test` through the `./portal/...` package set. Use `go test ./portal ./sdk` when checking the narrower portal and SDK interaction without the full repository test suite.
308+
309+
The harness exercises SDK expose/register logic, `/sdk/hop`, `RelaySet`, signed descriptors, route registration, SNI ingress, hop token matching, and `bridgeLeaseConn`. It intentionally excludes public DNS, ACME provider side effects, public registry bootstrap, and real WireGuard devices. `localRelaySpec` provides relay-level server and descriptor mutation hooks for future policy tests, and the fake overlay records synced peers so fake hop streams only open after the `/sdk/hop` overlay sync path has run.
310+
303311
## Control Plane Flow
304312

305313
### 1. Register

portal/api_server.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ func (s *Server) handleRegisterChallenge(w http.ResponseWriter, r *http.Request)
332332
Path: types.PathSDKRegister,
333333
}).String()
334334

335-
if strings.TrimSpace(req.HopToken) != "" && s.overlay == nil {
335+
if strings.TrimSpace(req.HopToken) != "" && !s.hasHopTransport() {
336336
utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeFeatureUnavailable, errFeatureUnavailable.Error())
337337
return
338338
}
@@ -406,7 +406,7 @@ func (s *Server) handleHop(w http.ResponseWriter, r *http.Request) {
406406
utils.MethodNotAllowedError().Write(w)
407407
return
408408
}
409-
if s.overlay == nil || s.relaySet == nil {
409+
if !s.hasHopTransport() || !s.hasOverlayRuntime() || s.relaySet == nil {
410410
utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeFeatureUnavailable, errFeatureUnavailable.Error())
411411
return
412412
}
@@ -459,7 +459,7 @@ func (s *Server) handleHop(w http.ResponseWriter, r *http.Request) {
459459
utils.InvalidRequestError(fmt.Errorf("forward relay: %w", err)).Write(w)
460460
return
461461
}
462-
if err := s.overlay.Sync(s.relaySet.OverlayPeerStates()); err != nil {
462+
if err := s.syncOverlayPeers(s.relaySet.OverlayPeerStates()); err != nil {
463463
utils.WriteAPIError(w, http.StatusInternalServerError, types.APIErrorCodeInternal, err.Error())
464464
return
465465
}

portal/discovery/refresher.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,26 @@ func (r *Refresher) refreshHTTPS(ctx context.Context) error {
161161
}
162162
continue
163163
}
164+
client := r.httpClient
165+
var closeClient func()
166+
if utils.IsLocalRelayHost(baseURL.Hostname()) {
167+
_, localClient, transport, err := utils.NewHTTPTLSClient(ctx, baseURL, defaultRequestTimeout)
168+
if err != nil {
169+
if recoveryFailures > 0 {
170+
r.logDiscoveryFailure(relayURL, relayURL, recoveryFailures, err)
171+
}
172+
continue
173+
}
174+
client = localClient
175+
closeClient = transport.CloseIdleConnections
176+
}
164177

165178
startedAt := time.Now()
166179
var resp types.DiscoveryResponse
167-
if err := utils.HTTPDoAPIPath(ctx, r.httpClient, baseURL, http.MethodGet, types.PathDiscovery, nil, nil, &resp); err != nil {
180+
if err := utils.HTTPDoAPIPath(ctx, client, baseURL, http.MethodGet, types.PathDiscovery, nil, nil, &resp); err != nil {
181+
if closeClient != nil {
182+
closeClient()
183+
}
168184
if ctx.Err() != nil {
169185
return ctx.Err()
170186
}
@@ -173,6 +189,9 @@ func (r *Refresher) refreshHTTPS(ctx context.Context) error {
173189
}
174190
continue
175191
}
192+
if closeClient != nil {
193+
closeClient()
194+
}
176195
measuredAt := time.Now().UTC()
177196

178197
if _, err := r.relaySet.ApplyRelayDiscoveryResponse(relayURL, resp, measuredAt); err != nil {

portal/lease.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ func (r *leaseRegistry) Renew(req types.RenewRequest, clientIP string) (types.Re
438438
if strings.TrimSpace(reportedIP) != "" {
439439
record.ReportedIP = reportedIP
440440
}
441+
record.Metadata = req.Metadata.Copy()
441442
r.policy.IPFilter().RegisterIdentityIP(leaseKey, clientIP)
442443
identity := record.Identity
443444
r.mu.Unlock()

0 commit comments

Comments
 (0)