Refactor hop route registration and synchronization
Kim committed
Apr 16, 2026 at 21:11 UTC
65014b487986aa238481c6a77a7c47f56b3ae4b4
11 files changed
+371
-382
portal/api_server.go
+58
-99
@@ -19,6 +19,7 @@ import (
19
"github.com/gosuda/portal-tunnel/v2/portal/auth"
20
"github.com/gosuda/portal-tunnel/v2/portal/discovery"
21
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
22
+ "github.com/gosuda/portal-tunnel/v2/portal/overlay"
23
"github.com/gosuda/portal-tunnel/v2/portal/transport"
24
"github.com/gosuda/portal-tunnel/v2/types"
25
"github.com/gosuda/portal-tunnel/v2/utils"
@@ -111,6 +112,8 @@ func (s *Server) apiHandler(base *http.ServeMux, keylessSignerHandler http.Handl
112
s.handleRenew(w, r)
113
case types.PathSDKUnregister:
114
s.handleUnregister(w, r)
115
+ case types.PathSDKHop:
116
+ s.handleHop(w, r)
117
case types.PathSDKConnect:
118
s.handleConnect(w, r)
119
case types.PathDiscovery:
@@ -137,18 +140,6 @@ func (s *Server) apiHandler(base *http.ServeMux, keylessSignerHandler http.Handl
140
})
141
}
142
140
-func (s *Server) hopRegistryHandler() http.Handler {
141
- apiHandler := s.apiHandler(nil, nil)
142
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143
- switch strings.TrimSpace(r.URL.Path) {
144
- case types.PathSDKRegisterChallenge, types.PathSDKRegister, types.PathSDKRenew, types.PathSDKUnregister:
145
- apiHandler.ServeHTTP(w, r)
146
- default:
147
- utils.WriteAPIError(w, http.StatusNotFound, types.APIErrorCodeInvalidRequest, "unsupported hop registry path")
148
- }
149
- })
150
-}
151
-
143
func (s *Server) handleRoot(w http.ResponseWriter, _ *http.Request) {
144
utils.WriteAPIData(w, http.StatusOK, map[string]any{
145
"service": "portal-relay",
@@ -341,30 +332,6 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
332
if !ok {
333
return
334
}
344
- if req.Hop != nil {
345
- hop := req.Hop
346
- req.Hop = hop.Next
347
- status, body, err := s.forwardHopRegistry(r.Context(), hop, types.PathSDKRegister, req)
348
- if err != nil {
349
- writeAPIErrorResponse(w, err)
350
- return
351
- }
352
- if status >= http.StatusOK && status < http.StatusMultipleChoices {
353
- var resp struct {
354
- ExpiresAt time.Time `json:"expires_at"`
355
- }
356
- if err := utils.DecodeAPIData(body, &resp); err != nil {
357
- utils.WriteAPIError(w, http.StatusBadGateway, types.APIErrorCodeInvalidRequest, err.Error())
358
- return
359
- }
360
- if err := s.registry.RegisterHopRoute(hop, resp.ExpiresAt, time.Now()); err != nil {
361
- writeAPIErrorResponse(w, err)
362
- return
363
- }
364
- }
365
- utils.WriteRawAPIResponse(w, status, body)
366
- return
367
- }
335
336
challenge, err := s.registry.consumeVerifiedRegisterChallenge(req)
337
if err != nil {
@@ -403,17 +370,6 @@ func (s *Server) handleRegisterChallenge(w http.ResponseWriter, r *http.Request)
370
if !ok {
371
return
372
}
406
- if req.Hop != nil {
407
- hop := req.Hop
408
- req.Hop = hop.Next
409
- status, body, err := s.forwardHopRegistry(r.Context(), hop, types.PathSDKRegisterChallenge, req)
410
- if err != nil {
411
- writeAPIErrorResponse(w, err)
412
- return
413
- }
414
- utils.WriteRawAPIResponse(w, status, body)
415
- return
416
- }
373
374
scheme := "https"
375
if r.TLS == nil {
@@ -466,30 +422,6 @@ func (s *Server) handleRenew(w http.ResponseWriter, r *http.Request) {
422
if !ok {
423
return
424
}
469
- if req.Hop != nil {
470
- hop := req.Hop
471
- req.Hop = hop.Next
472
- status, body, err := s.forwardHopRegistry(r.Context(), hop, types.PathSDKRenew, req)
473
- if err != nil {
474
- writeAPIErrorResponse(w, err)
475
- return
476
- }
477
- if status >= http.StatusOK && status < http.StatusMultipleChoices {
478
- var resp struct {
479
- ExpiresAt time.Time `json:"expires_at"`
480
- }
481
- if err := utils.DecodeAPIData(body, &resp); err != nil {
482
- utils.WriteAPIError(w, http.StatusBadGateway, types.APIErrorCodeInvalidRequest, err.Error())
483
- return
484
- }
485
- if err := s.registry.RegisterHopRoute(hop, resp.ExpiresAt, time.Now()); err != nil {
486
- writeAPIErrorResponse(w, err)
487
- return
488
- }
489
- }
490
- utils.WriteRawAPIResponse(w, status, body)
491
- return
492
- }
425
426
claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, s.identity.PublicKey, s.cfg.PortalURL, time.Now().UTC())
427
if err != nil {
@@ -527,20 +459,6 @@ func (s *Server) handleUnregister(w http.ResponseWriter, r *http.Request) {
459
if !ok {
460
return
461
}
530
- if req.Hop != nil {
531
- hop := req.Hop
532
- req.Hop = hop.Next
533
- status, body, err := s.forwardHopRegistry(r.Context(), hop, types.PathSDKUnregister, req)
534
- if err != nil {
535
- writeAPIErrorResponse(w, err)
536
- return
537
- }
538
- if status >= http.StatusOK && status < http.StatusMultipleChoices {
539
- s.registry.DeleteHopRoute(hop)
540
- }
541
- utils.WriteRawAPIResponse(w, status, body)
542
- return
543
- }
462
claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, s.identity.PublicKey, s.cfg.PortalURL, time.Now().UTC())
463
if err != nil {
464
utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUnauthorized, errUnauthorized.Error())
@@ -568,35 +486,76 @@ func (s *Server) handleUnregister(w http.ResponseWriter, r *http.Request) {
486
utils.WriteAPIData(w, http.StatusOK, map[string]any{})
487
}
488
571
-func (s *Server) forwardHopRegistry(ctx context.Context, route *types.HopRoute, path string, payload any) (int, []byte, error) {
489
+func (s *Server) handleHop(w http.ResponseWriter, r *http.Request) {
490
+ switch r.Method {
491
+ case http.MethodPost, http.MethodDelete:
492
+ default:
493
+ utils.MethodNotAllowedError().Write(w)
494
+ return
495
+ }
496
if s.hopMux == nil || s.overlay == nil || s.relaySet == nil {
573
- return 0, nil, errFeatureUnavailable
497
+ utils.WriteAPIError(w, http.StatusServiceUnavailable, types.APIErrorCodeFeatureUnavailable, errFeatureUnavailable.Error())
498
+ return
499
+ }
500
+ if _, ok := s.extractAllowedClientIP(w, r); !ok {
501
+ return
502
+ }
503
+
504
+ route, ok := utils.DecodeJSONRequest[types.HopRoute](w, r, defaultControlBodyLimit)
505
+ if !ok {
506
+ return
507
+ }
508
+ route, err := overlay.VerifyHopRoute(r.Method, route)
509
+ if errors.Is(err, overlay.ErrHopRouteSignatureInvalid) {
510
+ utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUnauthorized, "hop route signature is invalid")
511
+ return
512
}
575
- if route == nil {
576
- return 0, nil, errors.New("hop route is required")
513
+ if err != nil {
514
+ utils.InvalidRequestError(err).Write(w)
515
+ return
516
+ }
517
+ if route.RelayURL != s.cfg.PortalURL {
518
+ utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUnauthorized, "hop route relay url does not match receiving relay")
519
+ return
520
+ }
521
+
522
+ if r.Method == http.MethodDelete {
523
+ s.registry.DeleteHopRoute(&route)
524
+ utils.WriteAPIData(w, http.StatusOK, map[string]any{})
525
+ return
526
+ }
527
+
528
+ now := time.Now().UTC()
529
+ if !route.ExpiresAt.UTC().After(now) {
530
+ utils.InvalidRequestError(errors.New("route expiry must be in the future")).Write(w)
531
+ return
532
}
533
forwardRelay, err := auth.VerifyRelayDescriptor(route.ForwardRelay)
534
if err != nil {
580
- return 0, nil, fmt.Errorf("forward relay: %w", err)
535
+ utils.InvalidRequestError(fmt.Errorf("forward relay: %w", err)).Write(w)
536
+ return
537
}
538
if !forwardRelay.SupportsOverlayPeer ||
539
strings.TrimSpace(forwardRelay.WireGuardPublicKey) == "" ||
540
strings.TrimSpace(forwardRelay.WireGuardEndpoint) == "" ||
541
strings.TrimSpace(forwardRelay.OverlayIPv4) == "" {
586
- return 0, nil, errors.New("forward relay wireguard overlay metadata is required")
542
+ utils.InvalidRequestError(errors.New("forward relay wireguard overlay metadata is required")).Write(w)
543
+ return
544
}
588
- if err := s.relaySet.InsertAnnounced(forwardRelay, time.Now().UTC()); err != nil {
589
- return 0, nil, fmt.Errorf("forward relay: %w", err)
545
+ route.ForwardRelay = forwardRelay
546
+ if err := s.relaySet.InsertAnnounced(forwardRelay, now); err != nil {
547
+ utils.InvalidRequestError(fmt.Errorf("forward relay: %w", err)).Write(w)
548
+ return
549
}
550
if err := s.overlay.Sync(s.relaySet.OverlayPeerStates()); err != nil {
592
- return 0, nil, err
551
+ utils.WriteAPIError(w, http.StatusInternalServerError, types.APIErrorCodeInternal, err.Error())
552
+ return
553
}
594
-
595
- status, respBody, err := s.hopMux.OpenRegistry(ctx, forwardRelay.OverlayIPv4, path, payload)
596
- if err != nil {
597
- return 0, nil, &apiError{types.APIErrorCodeFeatureUnavailable, err.Error(), http.StatusBadGateway}
554
+ if err := s.registry.RegisterHopRoute(&route, now); err != nil {
555
+ writeAPIErrorResponse(w, err)
556
+ return
557
}
599
- return status, respBody, nil
558
+ utils.WriteAPIData(w, http.StatusOK, map[string]any{})
559
}
560
561
func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) {
portal/lease.go
+22
-8
@@ -207,15 +207,19 @@ func (r *leaseRegistry) RecordByHopToken(token string, now time.Time) (*leaseRec
207
return nil, false
208
}
209
210
-func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, expiresAt, now time.Time) error {
210
+func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) error {
211
if route == nil {
212
return errors.New("hop route is required")
213
}
214
+ ownerKey, err := utils.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
215
+ if err != nil {
216
+ return err
217
+ }
218
matchHostname := utils.NormalizeHostname(route.MatchHostname)
219
matchToken := strings.TrimSpace(route.MatchToken)
220
overlayIPv4 := strings.TrimSpace(route.ForwardRelay.OverlayIPv4)
221
forwardToken := strings.TrimSpace(route.ForwardToken)
218
- expiresAt = expiresAt.UTC()
222
+ expiresAt := route.ExpiresAt.UTC()
223
224
switch {
225
case r == nil:
@@ -238,19 +242,24 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, expiresAt, now t
242
record := &leaseRecord{
243
Hostname: matchHostname,
244
ExpiresAt: expiresAt,
245
+ hopOwnerKey: ownerKey,
246
hopToken: matchToken,
247
hopNextOverlayIPv4: overlayIPv4,
248
hopNextToken: forwardToken,
249
}
250
if matchHostname != "" {
246
- if existing := r.lookupLocked(matchHostname); existing != nil && !existing.isHopForward() && now.Before(existing.ExpiresAt) {
247
- return errHostnameConflict
251
+ if existing := r.lookupLocked(matchHostname); existing != nil && now.Before(existing.ExpiresAt) {
252
+ if !existing.isHopForward() || existing.hopOwnerKey != ownerKey {
253
+ return errHostnameConflict
254
+ }
255
}
256
r.recordsByHostname[matchHostname] = record
257
return nil
258
}
252
- if existing := r.recordsByHopToken[matchToken]; existing != nil && !existing.isHopForward() && now.Before(existing.ExpiresAt) {
253
- return errors.New("hop token conflict")
259
+ if existing := r.recordsByHopToken[matchToken]; existing != nil && now.Before(existing.ExpiresAt) {
260
+ if !existing.isHopForward() || existing.hopOwnerKey != ownerKey {
261
+ return errors.New("hop token conflict")
262
+ }
263
}
264
r.recordsByHopToken[matchToken] = record
265
return nil
@@ -260,17 +269,21 @@ func (r *leaseRegistry) DeleteHopRoute(route *types.HopRoute) {
269
if r == nil || route == nil {
270
return
271
}
272
+ ownerKey, err := utils.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
273
+ if err != nil {
274
+ return
275
+ }
276
hostname := utils.NormalizeHostname(route.MatchHostname)
277
token := strings.TrimSpace(route.MatchToken)
278
279
r.mu.Lock()
280
if hostname != "" {
268
- if record := r.recordsByHostname[hostname]; record != nil && record.isHopForward() {
281
+ if record := r.recordsByHostname[hostname]; record != nil && record.isHopForward() && record.hopOwnerKey == ownerKey {
282
delete(r.recordsByHostname, hostname)
283
}
284
}
285
if token != "" {
273
- if record := r.recordsByHopToken[token]; record != nil && record.isHopForward() {
286
+ if record := r.recordsByHopToken[token]; record != nil && record.isHopForward() && record.hopOwnerKey == ownerKey {
287
delete(r.recordsByHopToken, token)
288
}
289
}
@@ -487,6 +500,7 @@ type leaseRecord struct {
500
Metadata types.LeaseMetadata
501
502
hopToken string
503
+ hopOwnerKey string
504
hopNextOverlayIPv4 string
505
hopNextToken string
506
portal/overlay/hop.go
new
+126
@@ -0,0 +1,126 @@
1
+package overlay
2
+
3
+import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "strings"
10
+ "time"
11
+
12
+ "github.com/gosuda/portal-tunnel/v2/types"
13
+ "github.com/gosuda/portal-tunnel/v2/utils"
14
+)
15
+
16
+var ErrHopRouteSignatureInvalid = errors.New("hop route signature is invalid")
17
+
18
+func SignHopRoute(method string, route types.HopRoute, identity types.Identity, expiresAt time.Time) (types.HopRoute, error) {
19
+ route.ExpiresAt = expiresAt.UTC()
20
+ route.Signature = ""
21
+ route.OwnerPublicKey = ""
22
+
23
+ route, err := normalizeHopRoute(route, false)
24
+ if err != nil {
25
+ return types.HopRoute{}, err
26
+ }
27
+ owner, err := deriveHopRouteOwner(identity, route)
28
+ if err != nil {
29
+ return types.HopRoute{}, err
30
+ }
31
+ route.OwnerPublicKey = owner.PublicKey
32
+
33
+ route, err = normalizeHopRoute(route, true)
34
+ if err != nil {
35
+ return types.HopRoute{}, err
36
+ }
37
+ payload, err := types.HopRouteBytes(method, route)
38
+ if err != nil {
39
+ return types.HopRoute{}, err
40
+ }
41
+ route.Signature, err = utils.SignSHA256Secp256k1DER(payload, owner.PrivateKey)
42
+ if err != nil {
43
+ return types.HopRoute{}, err
44
+ }
45
+ return route, nil
46
+}
47
+
48
+func VerifyHopRoute(method string, route types.HopRoute) (types.HopRoute, error) {
49
+ signature := strings.TrimSpace(route.Signature)
50
+ route.Signature = ""
51
+
52
+ route, err := normalizeHopRoute(route, true)
53
+ if err != nil {
54
+ return types.HopRoute{}, err
55
+ }
56
+ payload, err := types.HopRouteBytes(method, route)
57
+ if err != nil {
58
+ return types.HopRoute{}, err
59
+ }
60
+ if err := utils.VerifySHA256Secp256k1DER(payload, route.OwnerPublicKey, signature); err != nil {
61
+ return types.HopRoute{}, ErrHopRouteSignatureInvalid
62
+ }
63
+ route.Signature = signature
64
+ return route, nil
65
+}
66
+
67
+func normalizeHopRoute(route types.HopRoute, requireOwner bool) (types.HopRoute, error) {
68
+ ownerPublicKey := strings.ToLower(utils.TrimHexPrefix(strings.TrimSpace(route.OwnerPublicKey)))
69
+ if ownerPublicKey != "" {
70
+ if _, err := utils.ParseSecp256k1PublicKeyHex(ownerPublicKey); err != nil {
71
+ return types.HopRoute{}, fmt.Errorf("hop route owner public key: %w", err)
72
+ }
73
+ } else if requireOwner {
74
+ return types.HopRoute{}, errors.New("hop route owner public key is required")
75
+ }
76
+
77
+ relayURL, err := utils.NormalizeRelayURL(route.RelayURL)
78
+ if err != nil {
79
+ return types.HopRoute{}, fmt.Errorf("hop relay url: %w", err)
80
+ }
81
+
82
+ route.OwnerPublicKey = ownerPublicKey
83
+ route.RelayURL = relayURL
84
+ route.MatchHostname = utils.NormalizeHostname(route.MatchHostname)
85
+ route.MatchToken = strings.TrimSpace(route.MatchToken)
86
+ route.ForwardToken = strings.TrimSpace(route.ForwardToken)
87
+ route.ExpiresAt = route.ExpiresAt.UTC()
88
+ route.Signature = strings.TrimSpace(route.Signature)
89
+ return route, nil
90
+}
91
+
92
+func deriveHopRouteOwner(identity types.Identity, route types.HopRoute) (types.Identity, error) {
93
+ nonce, err := hopRouteOwnerNonce(route)
94
+ if err != nil {
95
+ return types.Identity{}, err
96
+ }
97
+ for counter := range 4 {
98
+ token, err := identity.DeriveToken(fmt.Sprintf("hop-owner:%s:%d", nonce, counter))
99
+ if err != nil {
100
+ return types.Identity{}, err
101
+ }
102
+ seed := sha256.Sum256([]byte(token))
103
+ owner, err := utils.ResolveSecp256k1Identity(hex.EncodeToString(seed[:]))
104
+ if err == nil {
105
+ return owner, nil
106
+ }
107
+ }
108
+ return types.Identity{}, errors.New("derive hop route owner key")
109
+}
110
+
111
+func hopRouteOwnerNonce(route types.HopRoute) (string, error) {
112
+ payload := struct {
113
+ RelayURL string `json:"relay_url"`
114
+ MatchHostname string `json:"match_hostname"`
115
+ MatchToken string `json:"match_token"`
116
+ }{
117
+ RelayURL: route.RelayURL,
118
+ MatchHostname: route.MatchHostname,
119
+ MatchToken: route.MatchToken,
120
+ }
121
+ encoded, err := json.Marshal(payload)
122
+ if err != nil {
123
+ return "", err
124
+ }
125
+ return string(encoded), nil
126
+}
portal/overlay/hop_mux.go
+2
-154
@@ -1,7 +1,6 @@
1
package overlay
2
3
import (
4
- "bytes"
4
"context"
5
"encoding/binary"
6
"encoding/json"
@@ -9,7 +8,6 @@ import (
8
"fmt"
9
"io"
10
"net"
12
- "net/http"
11
"strings"
12
"sync"
13
"time"
@@ -21,21 +19,18 @@ import (
19
const (
20
hopProtocolVersion = 1
21
hopPrefaceLimit = 4 << 10
24
- hopRegistryFrameLimit = 8 << 20
22
hopIncomingBuffer = 128
23
defaultPrefaceTimeout = 2 * time.Second
24
)
25
26
const (
27
hopModeTLSStream = "tls-stream"
31
- hopModeRegistry = "registry"
28
)
29
30
type HopMux struct {
31
listener net.Listener
32
overlay *Overlay
33
incoming chan HopStream
38
- registry chan HopRegistryStream
34
35
mu sync.Mutex
36
outbound map[string]*yamux.Session
@@ -53,28 +48,6 @@ type HopStream struct {
48
Token string
49
}
50
56
-type HopRegistryStream struct {
57
- Path string
58
- Body []byte
59
- conn net.Conn
60
-}
61
-
62
-type hopRegistryHTTPResponse struct {
63
- header http.Header
64
- status int
65
- body bytes.Buffer
66
-}
67
-
68
-type hopRegistryRequest struct {
69
- Path string `json:"path"`
70
- Body []byte `json:"body"`
71
-}
72
-
73
-type hopRegistryResponse struct {
74
- Status int `json:"status"`
75
- Body []byte `json:"body"`
76
-}
77
-
51
func NewHopMux(overlay *Overlay) (*HopMux, error) {
52
if overlay == nil || overlay.stack == nil {
53
return nil, errors.New("overlay is required for multi-hop mux")
@@ -87,7 +60,6 @@ func NewHopMux(overlay *Overlay) (*HopMux, error) {
60
listener: listener,
61
overlay: overlay,
62
incoming: make(chan HopStream, hopIncomingBuffer),
90
- registry: make(chan HopRegistryStream, hopIncomingBuffer),
63
outbound: make(map[string]*yamux.Session),
64
done: make(chan struct{}),
65
}, nil
@@ -111,8 +83,8 @@ func (m *HopMux) Serve(ctx context.Context) error {
83
case errors.Is(err, net.ErrClosed):
84
return nil
85
default:
114
- if ctx.Err() != nil {
115
- return nil
86
+ if ctxErr := ctx.Err(); ctxErr != nil {
87
+ return ctxErr
88
}
89
return fmt.Errorf("accept hop mux connection: %w", err)
90
}
@@ -132,19 +104,6 @@ func (m *HopMux) Accept(ctx context.Context) (HopStream, error) {
104
}
105
}
106
135
-func (m *HopMux) AcceptRegistry(ctx context.Context) (HopRegistryStream, error) {
136
- if m == nil {
137
- <-ctx.Done()
138
- return HopRegistryStream{}, ctx.Err()
139
- }
140
- select {
141
- case stream := <-m.registry:
142
- return stream, nil
143
- case <-ctx.Done():
144
- return HopRegistryStream{}, ctx.Err()
145
- }
146
-}
147
-
107
func (m *HopMux) Close() error {
108
if m == nil {
109
return nil
@@ -204,40 +163,6 @@ func (m *HopMux) OpenStream(ctx context.Context, overlayIPv4, token string) (net
163
return stream, nil
164
}
165
207
-func (m *HopMux) OpenRegistry(ctx context.Context, overlayIPv4, path string, payload any) (int, []byte, error) {
208
- path = strings.TrimSpace(path)
209
- if path == "" {
210
- return 0, nil, errors.New("registry path is required")
211
- }
212
- body, err := json.Marshal(payload)
213
- if err != nil {
214
- return 0, nil, err
215
- }
216
-
217
- stream, err := m.openYamuxStream(ctx, overlayIPv4)
218
- if err != nil {
219
- return 0, nil, err
220
- }
221
- defer stream.Close()
222
-
223
- preface := hopPreface{
224
- Version: hopProtocolVersion,
225
- Mode: hopModeRegistry,
226
- }
227
- if err := writeFramedJSON(stream, preface, hopPrefaceLimit); err != nil {
228
- return 0, nil, err
229
- }
230
- if err := writeFramedJSON(stream, hopRegistryRequest{Path: path, Body: body}, hopRegistryFrameLimit); err != nil {
231
- return 0, nil, err
232
- }
233
-
234
- var resp hopRegistryResponse
235
- if err := readFramedJSON(stream, &resp, hopRegistryFrameLimit); err != nil {
236
- return 0, nil, err
237
- }
238
- return resp.Status, resp.Body, nil
239
-}
240
-
166
func (m *HopMux) openYamuxStream(ctx context.Context, overlayIPv4 string) (*yamux.Stream, error) {
167
overlayIPv4 = strings.TrimSpace(overlayIPv4)
168
if overlayIPv4 == "" {
@@ -379,21 +304,6 @@ func (m *HopMux) handleStream(ctx context.Context, stream *yamux.Stream) {
304
Conn: stream,
305
Token: preface.Token,
306
})
382
- case hopModeRegistry:
383
- var req hopRegistryRequest
384
- if err := readFramedJSON(stream, &req, hopRegistryFrameLimit); err != nil {
385
- _ = stream.Close()
386
- return
387
- }
388
- if strings.TrimSpace(req.Path) == "" {
389
- _ = stream.Close()
390
- return
391
- }
392
- m.deliverRegistry(ctx, HopRegistryStream{
393
- Path: req.Path,
394
- Body: req.Body,
395
- conn: stream,
396
- })
307
default:
308
_ = stream.Close()
309
}
@@ -407,68 +317,6 @@ func (m *HopMux) deliver(ctx context.Context, stream HopStream) {
317
}
318
}
319
410
-func (m *HopMux) deliverRegistry(ctx context.Context, stream HopRegistryStream) {
411
- select {
412
- case m.registry <- stream:
413
- case <-ctx.Done():
414
- _ = stream.conn.Close()
415
- }
416
-}
417
-
418
-func (s HopRegistryStream) Respond(status int, body []byte) error {
419
- if s.conn == nil {
420
- return net.ErrClosed
421
- }
422
- defer s.conn.Close()
423
- return writeFramedJSON(s.conn, hopRegistryResponse{Status: status, Body: body}, hopRegistryFrameLimit)
424
-}
425
-
426
-func (s HopRegistryStream) ServeRegistryHTTP(ctx context.Context, host string, handler http.Handler) error {
427
- if handler == nil {
428
- if s.conn != nil {
429
- _ = s.conn.Close()
430
- }
431
- return errors.New("hop registry handler is required")
432
- }
433
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+strings.TrimSpace(host)+s.Path, bytes.NewReader(s.Body))
434
- if err != nil {
435
- if s.conn != nil {
436
- _ = s.conn.Close()
437
- }
438
- return err
439
- }
440
- req.RemoteAddr = "127.0.0.1:0"
441
- req.Header.Set("Content-Type", "application/json")
442
-
443
- resp := &hopRegistryHTTPResponse{header: make(http.Header)}
444
- handler.ServeHTTP(resp, req)
445
- return s.Respond(resp.Status(), resp.body.Bytes())
446
-}
447
-
448
-func (r *hopRegistryHTTPResponse) Header() http.Header {
449
- return r.header
450
-}
451
-
452
-func (r *hopRegistryHTTPResponse) Write(body []byte) (int, error) {
453
- if r.status == 0 {
454
- r.status = http.StatusOK
455
- }
456
- return r.body.Write(body)
457
-}
458
-
459
-func (r *hopRegistryHTTPResponse) WriteHeader(status int) {
460
- if r.status == 0 {
461
- r.status = status
462
- }
463
-}
464
-
465
-func (r *hopRegistryHTTPResponse) Status() int {
466
- if r.status == 0 {
467
- return http.StatusOK
468
- }
469
- return r.status
470
-}
471
-
320
func hopYamuxConfig() *yamux.Config {
321
cfg := yamux.DefaultConfig()
322
cfg.Logger = nil
portal/server.go
+2
-19
@@ -511,8 +511,8 @@ func (s *Server) runHopMux(ctx context.Context) error {
511
for {
512
stream, err := s.hopMux.Accept(groupCtx)
513
if err != nil {
514
- if groupCtx.Err() != nil {
515
- return nil
514
+ if ctxErr := groupCtx.Err(); ctxErr != nil {
515
+ return ctxErr
516
}
517
return err
518
}
@@ -524,23 +524,6 @@ func (s *Server) runHopMux(ctx context.Context) error {
524
}(stream)
525
}
526
})
527
- group.Go(func() error {
528
- registryHandler := s.hopRegistryHandler()
529
- for {
530
- stream, err := s.hopMux.AcceptRegistry(groupCtx)
531
- if err != nil {
532
- if groupCtx.Err() != nil {
533
- return nil
534
- }
535
- return err
536
- }
537
- go func(stream overlay.HopRegistryStream) {
538
- if err := stream.ServeRegistryHTTP(groupCtx, s.identity.Name, registryHandler); err != nil {
539
- log.Warn().Err(err).Str("path", stream.Path).Msg("respond hop registry stream")
540
- }
541
- }(stream)
542
- }
543
- })
527
return group.Wait()
528
}
529
sdk/api_client.go
+89
-43
@@ -6,9 +6,12 @@ import (
6
"fmt"
7
"net"
8
"net/http"
9
+ "net/url"
10
+ "slices"
11
"strings"
12
"time"
13
14
+ "github.com/gosuda/portal-tunnel/v2/portal/overlay"
15
"github.com/gosuda/portal-tunnel/v2/types"
16
"github.com/gosuda/portal-tunnel/v2/utils"
17
)
@@ -30,18 +33,12 @@ var errRelayIncompatible = errors.New("relay is incompatible")
33
// API call creates fresh TCP connections. Call this after detecting a system
34
// sleep/wake cycle where pooled connections are almost certainly dead.
35
func (l *listener) resetTransport() {
33
- if l.leaseHTTPClient != nil && l.leaseHTTPClient != l.httpClient {
34
- if transport, ok := l.leaseHTTPClient.Transport.(*http.Transport); ok {
35
- transport.CloseIdleConnections()
36
- }
37
- }
36
if l.httpClient != nil {
37
if transport, ok := l.httpClient.Transport.(*http.Transport); ok {
38
transport.CloseIdleConnections()
39
}
40
}
41
l.httpClient = nil
44
- l.leaseHTTPClient = nil
42
l.tlsConfig = nil
43
}
44
@@ -82,32 +79,16 @@ func (l *listener) initHTTPTransport(ctx context.Context) error {
79
return fmt.Errorf("%w: relay sdk protocol version mismatch: relay=%q client=%q", errRelayIncompatible, protocolVersion, types.SDKVersion)
80
}
81
85
- if l.leaseAPIURL == nil {
86
- l.leaseAPIURL = l.relayURL
87
- }
88
- leaseHTTPClient := httpClient
89
- if l.leaseAPIURL.String() != l.relayURL.String() {
90
- _, leaseHTTPClient, err = utils.NewHTTPTLSClient(bootstrapCtx, l.leaseAPIURL, l.requestTimeout)
91
- if err != nil {
92
- if transport, ok := httpClient.Transport.(*http.Transport); ok {
93
- transport.CloseIdleConnections()
94
- }
95
- return err
96
- }
97
- }
98
-
82
l.httpClient = httpClient
100
- l.leaseHTTPClient = leaseHTTPClient
83
l.tlsConfig = tlsConfig
84
return nil
85
}
86
105
-func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnabled, tcpEnabled bool) (types.RegisterResponse, *types.HopRoute, error) {
106
- var err error
87
+func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnabled, tcpEnabled bool) (types.RegisterResponse, []types.HopRoute, error) {
88
var exitHopToken string
89
var publicHostname string
90
var keylessURL string
110
- var hop *types.HopRoute
91
+ var hopRoutes []types.HopRoute
92
if len(l.multiHop) > 0 {
93
if len(l.multiHop) < 2 {
94
return types.RegisterResponse{}, nil, errors.New("multi-hop requires at least entry and exit relay urls")
@@ -134,6 +115,7 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
115
hopPath = append(hopPath, desc)
116
}
117
118
+ var err error
119
publicHostname, err = utils.LeaseHostname(l.identity.Name, utils.PortalRootHost(hopPath[0].APIHTTPSAddr))
120
if err != nil {
121
return types.RegisterResponse{}, nil, err
@@ -148,34 +130,32 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
130
}
131
tokens[i] = "hpt_" + token
132
}
151
- exitHopToken = tokens[len(tokens)-1]
133
153
- var next *types.HopRoute
154
- for i := len(hopPath) - 2; i >= 0; i-- {
155
- route := &types.HopRoute{
134
+ hopRoutes = make([]types.HopRoute, 0, len(tokens))
135
+ for i := range tokens {
136
+ route := types.HopRoute{
137
+ RelayURL: hopPath[i].APIHTTPSAddr,
138
ForwardRelay: hopPath[i+1],
139
ForwardToken: tokens[i],
158
- Next: next,
140
}
141
if i == 0 {
142
route.MatchHostname = publicHostname
143
} else {
144
route.MatchToken = tokens[i-1]
145
}
165
- hop = route
166
- next = route
146
+ hopRoutes = append(hopRoutes, route)
147
}
148
+ exitHopToken = tokens[len(tokens)-1]
149
}
150
151
var challenge types.RegisterChallengeResponse
171
- if err := utils.HTTPDoAPIPath(ctx, l.leaseHTTPClient, l.leaseAPIURL, http.MethodPost, types.PathSDKRegisterChallenge, types.RegisterChallengeRequest{
152
+ if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRegisterChallenge, types.RegisterChallengeRequest{
153
Identity: l.identity,
154
Metadata: l.metadata,
155
TTL: int(ttl / time.Second),
156
UDPEnabled: udpEnabled,
157
TCPEnabled: tcpEnabled,
158
HopToken: exitHopToken,
178
- Hop: hop,
159
}, nil, &challenge); err != nil {
160
return types.RegisterResponse{}, nil, err
161
}
@@ -186,38 +166,104 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
166
}
167
168
var resp types.RegisterResponse
189
- if err := utils.HTTPDoAPIPath(ctx, l.leaseHTTPClient, l.leaseAPIURL, http.MethodPost, types.PathSDKRegister, types.RegisterRequest{
169
+ if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRegister, types.RegisterRequest{
170
ChallengeID: challenge.ChallengeID,
171
SIWEMessage: challenge.SIWEMessage,
172
SIWESignature: signature,
173
ReportedIP: utils.ResolvePublicIP(ctx),
194
- Hop: hop,
174
}, nil, &resp); err != nil {
175
return types.RegisterResponse{}, nil, err
176
}
198
- if hop != nil {
177
+ if len(hopRoutes) > 0 {
178
+ if err := l.syncHopRoutes(ctx, http.MethodPost, resp.ExpiresAt, hopRoutes); err != nil {
179
+ _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
180
+ return types.RegisterResponse{}, nil, err
181
+ }
182
resp.Hostname = publicHostname
183
resp.KeylessURL = keylessURL
184
}
202
- return resp, hop, nil
185
+ return resp, hopRoutes, nil
186
}
187
205
-func (l *listener) renewRegisteredLease(ctx context.Context, ttl time.Duration, accessToken string, hop *types.HopRoute) (types.RenewResponse, error) {
188
+func (l *listener) renewRegisteredLease(ctx context.Context, ttl time.Duration, accessToken string, hopRoutes []types.HopRoute) (types.RenewResponse, error) {
189
var resp types.RenewResponse
207
- if err := utils.HTTPDoAPIPath(ctx, l.leaseHTTPClient, l.leaseAPIURL, http.MethodPost, types.PathSDKRenew, types.RenewRequest{
190
+ if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRenew, types.RenewRequest{
191
AccessToken: accessToken,
192
TTL: int(ttl / time.Second),
193
ReportedIP: utils.ResolvePublicIP(ctx),
211
- Hop: hop,
194
}, nil, &resp); err != nil {
195
return types.RenewResponse{}, err
196
}
197
+ if err := l.syncHopRoutes(ctx, http.MethodPost, resp.ExpiresAt, hopRoutes); err != nil {
198
+ return types.RenewResponse{}, err
199
+ }
200
return resp, nil
201
}
202
218
-func (l *listener) unregisterLease(ctx context.Context, accessToken string, hop *types.HopRoute) error {
219
- return utils.HTTPDoAPIPath(ctx, l.leaseHTTPClient, l.leaseAPIURL, http.MethodPost, types.PathSDKUnregister, types.UnregisterRequest{
203
+func (l *listener) unregisterLease(ctx context.Context, accessToken string, hopRoutes []types.HopRoute) error {
204
+ var unregisterErr error
205
+ if err := l.syncHopRoutes(ctx, http.MethodDelete, time.Time{}, hopRoutes); err != nil {
206
+ unregisterErr = errors.Join(unregisterErr, err)
207
+ }
208
+ err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKUnregister, types.UnregisterRequest{
209
AccessToken: accessToken,
221
- Hop: hop,
210
}, nil, nil)
211
+ return errors.Join(unregisterErr, err)
212
+}
213
+
214
+func (l *listener) syncHopRoutes(ctx context.Context, method string, expiresAt time.Time, routes []types.HopRoute) error {
215
+ if len(routes) == 0 {
216
+ return nil
217
+ }
218
+
219
+ orderedRoutes := routes
220
+ if method == http.MethodPost {
221
+ orderedRoutes = append([]types.HopRoute(nil), routes...)
222
+ slices.Reverse(orderedRoutes)
223
+ }
224
+
225
+ var syncErr error
226
+ for _, unsignedRoute := range orderedRoutes {
227
+ route, err := overlay.SignHopRoute(method, unsignedRoute, l.identity, expiresAt)
228
+ if err != nil {
229
+ if method == http.MethodDelete {
230
+ syncErr = errors.Join(syncErr, err)
231
+ continue
232
+ }
233
+ return err
234
+ }
235
+ relayURL, err := url.Parse(route.RelayURL)
236
+ if err != nil {
237
+ err = fmt.Errorf("parse hop route relay url: %w", err)
238
+ if method == http.MethodDelete {
239
+ syncErr = errors.Join(syncErr, err)
240
+ continue
241
+ }
242
+ return err
243
+ }
244
+
245
+ bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
246
+ _, client, err := utils.NewHTTPTLSClient(bootstrapCtx, relayURL, l.requestTimeout)
247
+ cancel()
248
+ if err != nil {
249
+ if method == http.MethodDelete {
250
+ syncErr = errors.Join(syncErr, err)
251
+ continue
252
+ }
253
+ return err
254
+ }
255
+ transport, _ := client.Transport.(*http.Transport)
256
+ err = utils.HTTPDoAPIPath(ctx, client, relayURL, method, types.PathSDKHop, route, nil, nil)
257
+ if transport != nil {
258
+ transport.CloseIdleConnections()
259
+ }
260
+ if err != nil {
261
+ if method == http.MethodDelete {
262
+ syncErr = errors.Join(syncErr, err)
263
+ continue
264
+ }
265
+ return err
266
+ }
267
+ }
268
+ return syncErr
269
}
sdk/expose.go
+12
-3
@@ -72,9 +72,18 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
72
if err != nil {
73
return nil, err
74
}
75
- multiHop, err := utils.NormalizeRelayURLs(cfg.MultiHop...)
76
- if err != nil {
77
- return nil, fmt.Errorf("normalize multi-hop relay urls: %w", err)
75
+ var multiHop []string
76
+ for _, input := range cfg.MultiHop {
77
+ for _, part := range utils.SplitCSV(input) {
78
+ relayURL, err := utils.NormalizeRelayURL(part)
79
+ if err != nil {
80
+ return nil, fmt.Errorf("normalize multi-hop relay url: %w", err)
81
+ }
82
+ if slices.Contains(multiHop, relayURL) {
83
+ return nil, fmt.Errorf("multi-hop relay url repeated: %s", relayURL)
84
+ }
85
+ multiHop = append(multiHop, relayURL)
86
+ }
87
}
88
if len(multiHop) == 1 {
89
return nil, errors.New("multi-hop requires at least entry and exit relay urls")
sdk/listener.go
+13
-30
@@ -52,7 +52,6 @@ type listener struct {
52
closeOnce sync.Once
53
54
relayURL *url.URL
55
- leaseAPIURL *url.URL
55
identity types.Identity
56
metadata types.LeaseMetadata
57
relaySet *discovery.RelaySet
@@ -71,9 +70,8 @@ type listener struct {
70
datagram *transport.ClientDatagram
71
mitmManager *mitmManager
72
74
- httpClient *http.Client
75
- leaseHTTPClient *http.Client
76
- tlsConfig *tls.Config
73
+ httpClient *http.Client
74
+ tlsConfig *tls.Config
75
76
leaseMu sync.RWMutex
77
lease *listenerLease
@@ -101,25 +99,10 @@ func newListener(ctx context.Context, relayURL string, cfg listenerConfig) (*lis
99
cancel()
100
return nil, fmt.Errorf("parse relay url: %w", err)
101
}
104
- leaseAPIURL := relayurl
105
- if len(cfg.MultiHop) > 0 {
106
- normalizedEntryURL, err := utils.NormalizeRelayURL(cfg.MultiHop[0])
107
- if err != nil {
108
- cancel()
109
- return nil, fmt.Errorf("normalize multi-hop entry relay url: %w", err)
110
- }
111
- leaseAPIURL, err = url.Parse(normalizedEntryURL)
112
- if err != nil {
113
- cancel()
114
- return nil, fmt.Errorf("parse multi-hop entry relay url: %w", err)
115
- }
116
- }
117
-
102
l := &listener{
103
cancel: cancel,
104
doneCh: listenerCtx.Done(),
105
relayURL: relayurl,
122
- leaseAPIURL: leaseAPIURL,
106
identity: cfg.Identity,
107
metadata: cfg.Metadata,
108
relaySet: cfg.relaySet,
@@ -247,7 +230,7 @@ func (l *listener) Close() error {
230
231
if lease != nil && lease.hostname != "" && l.identity.Key() != "" && lease.accessToken != "" {
232
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
250
- closeErr = errors.Join(closeErr, l.unregisterLease(ctx, lease.accessToken, lease.hop))
233
+ closeErr = errors.Join(closeErr, l.unregisterLease(ctx, lease.accessToken, lease.hopRoutes))
234
cancel()
235
}
236
if lease != nil && lease.tlsCloser != nil {
@@ -267,7 +250,7 @@ type listenerLease struct {
250
publicURLBase *url.URL
251
tlsConfig *tls.Config
252
tlsCloser io.Closer
270
- hop *types.HopRoute
253
+ hopRoutes []types.HopRoute
254
}
255
256
func (l *listener) clearLease(reason string) *listenerLease {
@@ -720,7 +703,7 @@ func (l *listener) renewLease(ctx context.Context) error {
703
}
704
705
requestCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
723
- resp, err := l.renewRegisteredLease(requestCtx, l.leaseTTL, lease.accessToken, lease.hop)
706
+ resp, err := l.renewRegisteredLease(requestCtx, l.leaseTTL, lease.accessToken, lease.hopRoutes)
707
cancel()
708
if err != nil {
709
if errors.Is(err, &types.APIRequestError{Code: types.APIErrorCodeLeaseNotFound}) {
@@ -751,7 +734,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
734
return err
735
}
736
754
- resp, hop, err := l.registerLease(ctx, l.leaseTTL, l.udpEnabled, l.tcpEnabled)
737
+ resp, hopRoutes, err := l.registerLease(ctx, l.leaseTTL, l.udpEnabled, l.tcpEnabled)
738
if err != nil {
739
return err
740
}
@@ -761,22 +744,22 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
744
}
745
registeredIdentity, err := utils.NormalizeIdentity(resp.Identity)
746
if err != nil {
764
- _ = l.unregisterLease(context.Background(), resp.AccessToken, hop)
747
+ _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
748
return err
749
}
750
if registeredIdentity.Key() != l.identity.Key() {
768
- _ = l.unregisterLease(context.Background(), resp.AccessToken, hop)
751
+ _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
752
return errors.New("relay returned mismatched lease identity")
753
}
754
if l.udpEnabled && !resp.UDPEnabled {
772
- _ = l.unregisterLease(context.Background(), resp.AccessToken, hop)
755
+ _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
756
return &types.APIRequestError{
757
Code: types.APIErrorCodeFeatureUnavailable,
758
Message: "relay did not enable required udp support",
759
}
760
}
761
if l.udpEnabled && resp.SNIPort <= 0 {
779
- _ = l.unregisterLease(context.Background(), resp.AccessToken, hop)
762
+ _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
763
return errors.New("relay did not return sni port for udp transport")
764
}
765
keylessURL := strings.TrimSpace(resp.KeylessURL)
@@ -791,7 +774,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
774
}
775
tlsConf, tenantTLSCloser, err := keyless.BuildClientTLSConfig(keylessURL, []string{resp.Hostname})
776
if err != nil {
794
- _ = l.unregisterLease(context.Background(), resp.AccessToken, hop)
777
+ _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
778
if tenantTLSCloser != nil {
779
_ = tenantTLSCloser.Close()
780
}
@@ -799,7 +782,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
782
}
783
784
if ctx.Err() != nil {
802
- _ = l.unregisterLease(context.Background(), resp.AccessToken, hop)
785
+ _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
786
if tenantTLSCloser != nil {
787
_ = tenantTLSCloser.Close()
788
}
@@ -813,7 +796,7 @@ func (l *listener) registerAndConfigure(ctx context.Context) error {
796
publicURLBase: publicURLBase,
797
tlsConfig: tlsConf,
798
tlsCloser: tenantTLSCloser,
816
- hop: hop,
799
+ hopRoutes: hopRoutes,
800
}
801
if l.udpEnabled {
802
next.sniPort = resp.SNIPort
types/api.go
+46
-17
@@ -1,6 +1,7 @@
1
package types
2
3
import (
4
+ "encoding/json"
5
"fmt"
6
"strings"
7
"time"
@@ -56,11 +57,10 @@ func (e *APIRequestError) Is(target error) bool {
57
}
58
59
type RegisterRequest struct {
59
- ChallengeID string `json:"challenge_id"`
60
- SIWEMessage string `json:"siwe_message"`
61
- SIWESignature string `json:"siwe_signature"`
62
- ReportedIP string `json:"reported_ip,omitempty"`
63
- Hop *HopRoute `json:"hop,omitempty"`
60
+ ChallengeID string `json:"challenge_id"`
61
+ SIWEMessage string `json:"siwe_message"`
62
+ SIWESignature string `json:"siwe_signature"`
63
+ ReportedIP string `json:"reported_ip,omitempty"`
64
}
65
66
type RegisterChallengeRequest struct {
@@ -70,7 +70,6 @@ type RegisterChallengeRequest struct {
70
UDPEnabled bool `json:"udp_enabled,omitempty"`
71
TCPEnabled bool `json:"tcp_enabled,omitempty"`
72
HopToken string `json:"hop_token,omitempty"`
73
- Hop *HopRoute `json:"hop,omitempty"`
73
}
74
75
type RegisterChallengeResponse struct {
@@ -118,10 +117,9 @@ type QUICControlResponse struct {
117
}
118
119
type RenewRequest struct {
121
- AccessToken string `json:"access_token"`
122
- TTL int `json:"ttl,omitempty"`
123
- ReportedIP string `json:"reported_ip,omitempty"`
124
- Hop *HopRoute `json:"hop,omitempty"`
120
+ AccessToken string `json:"access_token"`
121
+ TTL int `json:"ttl,omitempty"`
122
+ ReportedIP string `json:"reported_ip,omitempty"`
123
}
124
125
type RenewResponse struct {
@@ -130,16 +128,47 @@ type RenewResponse struct {
128
}
129
130
type UnregisterRequest struct {
133
- AccessToken string `json:"access_token"`
134
- Hop *HopRoute `json:"hop,omitempty"`
131
+ AccessToken string `json:"access_token"`
132
}
133
134
type HopRoute struct {
138
- MatchHostname string `json:"match_hostname,omitempty"`
139
- MatchToken string `json:"match_token,omitempty"`
140
- ForwardRelay RelayDescriptor `json:"forward_relay"`
141
- ForwardToken string `json:"forward_token"`
142
- Next *HopRoute `json:"next,omitempty"`
135
+ OwnerPublicKey string `json:"owner_public_key,omitempty"`
136
+ RelayURL string `json:"relay_url"`
137
+ MatchHostname string `json:"match_hostname,omitempty"`
138
+ MatchToken string `json:"match_token,omitempty"`
139
+ ForwardRelay RelayDescriptor `json:"forward_relay"`
140
+ ForwardToken string `json:"forward_token"`
141
+ ExpiresAt time.Time `json:"expires_at,omitempty"`
142
+ Signature string `json:"signature,omitempty"`
143
+}
144
+
145
+func HopRouteBytes(method string, route HopRoute) ([]byte, error) {
146
+ forwardRelay, err := CanonicalBytes(route.ForwardRelay)
147
+ if err != nil {
148
+ return nil, err
149
+ }
150
+ payload := struct {
151
+ Purpose string `json:"purpose"`
152
+ Method string `json:"method"`
153
+ OwnerPublicKey string `json:"owner_public_key"`
154
+ RelayURL string `json:"relay_url"`
155
+ MatchHostname string `json:"match_hostname"`
156
+ MatchToken string `json:"match_token"`
157
+ ForwardRelay json.RawMessage `json:"forward_relay"`
158
+ ForwardToken string `json:"forward_token"`
159
+ ExpiresAtUnixNano int64 `json:"expires_at_unix_nano"`
160
+ }{
161
+ Purpose: "portal hop route v1",
162
+ Method: strings.ToUpper(strings.TrimSpace(method)),
163
+ OwnerPublicKey: strings.TrimSpace(route.OwnerPublicKey),
164
+ RelayURL: strings.TrimSpace(route.RelayURL),
165
+ MatchHostname: strings.TrimSpace(route.MatchHostname),
166
+ MatchToken: strings.TrimSpace(route.MatchToken),
167
+ ForwardRelay: json.RawMessage(forwardRelay),
168
+ ForwardToken: strings.TrimSpace(route.ForwardToken),
169
+ ExpiresAtUnixNano: route.ExpiresAt.UTC().UnixNano(),
170
+ }
171
+ return json.Marshal(payload)
172
}
173
174
type DomainResponse struct {
types/paths.go
+1
@@ -33,6 +33,7 @@ const (
33
PathSDKRegister = "/sdk/register"
34
PathSDKRenew = "/sdk/renew"
35
PathSDKUnregister = "/sdk/unregister"
36
+ PathSDKHop = "/sdk/hop"
37
PathSDKConnect = "/sdk/connect"
38
PathDiscovery = "/discovery"
39
PathDiscoveryAnnounce = "/discovery/announce"
utils/api.go
-9
@@ -39,15 +39,6 @@ func WriteAPIError(w http.ResponseWriter, status int, code, message string) {
39
})
40
}
41
42
-func WriteRawAPIResponse(w http.ResponseWriter, status int, body []byte) {
43
- if status <= 0 {
44
- status = http.StatusBadGateway
45
- }
46
- w.Header().Set("Content-Type", "application/json")
47
- w.WriteHeader(status)
48
- _, _ = w.Write(body)
49
-}
50
-
42
func MethodNotAllowedError() APIErrorResponse {
43
return APIErrorResponse{
44
Status: http.StatusMethodNotAllowed,