main
go 362 lines 12 KB
Raw
1 package sdk
2
3 import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/base32"
8 "errors"
9 "fmt"
10 "net"
11 "net/http"
12 "net/url"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/gosuda/portal-tunnel/v2/portal/auth"
18 "github.com/gosuda/portal-tunnel/v2/portal/identity"
19 "github.com/gosuda/portal-tunnel/v2/types"
20 "github.com/gosuda/portal-tunnel/v2/utils"
21 )
22
23 const (
24 defaultDialTimeout = 5 * time.Second
25 defaultRequestTimeout = 15 * time.Second
26 defaultHandshakeTimeout = 15 * time.Second
27 defaultLeaseTTL = 2 * time.Minute
28 defaultRenewBefore = 30 * time.Second
29 defaultReadyTarget = 2
30 defaultRetryWait = 3 * time.Second
31 defaultHTTPShutdownTimeout = 5 * time.Second
32 )
33
34 var errRelayIncompatible = errors.New("relay is incompatible")
35
36 // resetTransport tears down the cached HTTP client and TLS config so the next
37 // API call creates fresh TCP connections. Call this after detecting a system
38 // sleep/wake cycle where pooled connections are almost certainly dead.
39 func (l *listener) resetTransport() {
40 if l.httpTransport != nil {
41 l.httpTransport.CloseIdleConnections()
42 }
43 l.httpClient = nil
44 l.httpTransport = nil
45 l.tlsConfig = nil
46 }
47
48 func (l *listener) initHTTPTransport(ctx context.Context) error {
49 if l.httpClient != nil {
50 return nil
51 }
52
53 bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
54 defer cancel()
55
56 tlsConfig, httpClient, httpTransport, err := utils.NewHTTPTLSClient(bootstrapCtx, l.relayURL, l.requestTimeout)
57 if err != nil {
58 return err
59 }
60
61 var domainResp types.DomainResponse
62 if err := utils.HTTPDoAPIPath(ctx, httpClient, l.relayURL, http.MethodGet, types.PathSDKDomain, nil, nil, &domainResp); err != nil {
63 httpTransport.CloseIdleConnections()
64 err = fmt.Errorf("check relay compatibility: %w", err)
65 var netErr net.Error
66 var apiErr *types.APIRequestError
67 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.As(err, &netErr) {
68 return err
69 }
70 if errors.As(err, &apiErr) && apiErr.StatusCode >= 500 {
71 return err
72 }
73 return fmt.Errorf("%w: %w", errRelayIncompatible, err)
74 }
75 protocolVersion := strings.TrimSpace(domainResp.ProtocolVersion)
76 if protocolVersion != types.SDKVersion {
77 httpTransport.CloseIdleConnections()
78 return fmt.Errorf("%w: relay sdk protocol version mismatch: relay=%q client=%q", errRelayIncompatible, protocolVersion, types.SDKVersion)
79 }
80
81 l.releaseVersion = strings.TrimSpace(domainResp.ReleaseVersion)
82
83 l.httpClient = httpClient
84 l.httpTransport = httpTransport
85 l.tlsConfig = tlsConfig
86 return nil
87 }
88
89 func (l *listener) buildHopRoutes(hopPath []types.RelayDescriptor, publicHostname, routeHostname string, echConfigList []byte) ([]types.HopRoute, string, error) {
90 if len(hopPath) < 2 {
91 return nil, "", errors.New("multi-hop requires at least entry and exit relay urls")
92 }
93 hopRoutes := make([]types.HopRoute, 0, len(hopPath)-1)
94 var previousHopToken string
95 for i := 0; i < len(hopPath)-1; i++ {
96 token, err := identity.DeriveToken(
97 l.identity,
98 "hop-token",
99 publicHostname,
100 strconv.Itoa(i),
101 hopPath[i].APIHTTPSAddr,
102 hopPath[i+1].APIHTTPSAddr,
103 )
104 if err != nil {
105 return nil, "", err
106 }
107 forwardToken := "hpt_" + token
108 route := types.HopRoute{
109 RelayURL: hopPath[i].APIHTTPSAddr,
110 ForwardRelay: hopPath[i+1],
111 ForwardToken: forwardToken,
112 }
113 if i == 0 {
114 route.PublicHostname = publicHostname
115 route.RouteHostname = routeHostname
116 route.HostnameHash = utils.HostnameHash(publicHostname)
117 route.ECHConfigList = bytes.Clone(echConfigList)
118 route.Metadata = l.metadataSnapshot()
119 route.Metadata.Hide = true
120 } else {
121 route.MatchToken = previousHopToken
122 }
123 hopRoutes = append(hopRoutes, route)
124 previousHopToken = forwardToken
125 }
126 return hopRoutes, previousHopToken, nil
127 }
128
129 func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnabled, tcpEnabled bool) (types.RegisterResponse, []types.HopRoute, string, string, error) {
130 var exitHopToken string
131 var publicHostname string
132 var routeHostname string
133 var rootHostname string
134 var hopRoutes []types.HopRoute
135 multiHop := l.route.MultiHop()
136 var hopPath []types.RelayDescriptor
137 streamLease := !udpEnabled && !tcpEnabled
138 registerIdentity := l.identity
139 if len(multiHop) > 0 {
140 if !streamLease {
141 return types.RegisterResponse{}, nil, "", "", errors.New("multi-hop requires stream lease")
142 }
143 if len(multiHop) < 2 {
144 return types.RegisterResponse{}, nil, "", "", errors.New("multi-hop requires at least entry and exit relay urls")
145 }
146 if l.relaySet == nil {
147 return types.RegisterResponse{}, nil, "", "", errors.New("multi-hop relay set is unavailable")
148 }
149
150 now := time.Now().UTC()
151 hopPath = make([]types.RelayDescriptor, 0, len(multiHop))
152 for i, relayURL := range multiHop {
153 desc, ok := l.relaySet.OverlayRelayDescriptor(relayURL, now)
154 if !ok {
155 return types.RegisterResponse{}, nil, "", "", fmt.Errorf("multi-hop relay %d descriptor is unavailable", i)
156 }
157 hopPath = append(hopPath, desc)
158 }
159
160 rootHostname = utils.PortalRootHost(hopPath[0].APIHTTPSAddr)
161 } else {
162 rootHostname = utils.PortalRootHost(l.relayURL.String())
163 }
164
165 var err error
166 publicHostname, err = utils.LeaseHostname(l.identity.Name, rootHostname)
167 if err != nil {
168 return types.RegisterResponse{}, nil, "", "", err
169 }
170 if streamLease {
171 routeToken, err := identity.DeriveToken(l.identity, "ech-route", publicHostname, rootHostname)
172 if err != nil {
173 return types.RegisterResponse{}, nil, "", "", err
174 }
175 routeSum := sha256.Sum256([]byte(routeToken))
176 routeLabel := "ech-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(routeSum[:20]))
177 routeHostname, err = utils.LeaseHostname(routeLabel, rootHostname)
178 if err != nil {
179 return types.RegisterResponse{}, nil, "", "", err
180 }
181 }
182 var echConfigList []byte
183 if streamLease {
184 _, echConfigList, err = l.tenantECHMaterials(publicHostname, routeHostname)
185 if err != nil {
186 return types.RegisterResponse{}, nil, "", "", err
187 }
188 }
189
190 if len(multiHop) > 0 {
191 var err error
192 hopRoutes, exitHopToken, err = l.buildHopRoutes(hopPath, publicHostname, routeHostname, echConfigList)
193 if err != nil {
194 return types.RegisterResponse{}, nil, "", "", err
195 }
196 }
197
198 registerReq := types.RegisterChallengeRequest{
199 Identity: registerIdentity,
200 Metadata: l.metadataSnapshot(),
201 TTL: int(ttl / time.Second),
202 UDPEnabled: udpEnabled,
203 TCPEnabled: tcpEnabled,
204 HopToken: exitHopToken,
205 }
206 if streamLease && len(multiHop) == 0 {
207 registerReq.RouteHostname = routeHostname
208 registerReq.HostnameHash = utils.HostnameHash(publicHostname)
209 registerReq.ECHConfigList = bytes.Clone(echConfigList)
210 }
211
212 var challenge types.RegisterChallengeResponse
213 if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRegisterChallenge, registerReq, nil, &challenge); err != nil {
214 return types.RegisterResponse{}, nil, "", "", err
215 }
216
217 authority, err := identity.NewLocalAuthority(l.identity)
218 if err != nil {
219 return types.RegisterResponse{}, nil, "", "", err
220 }
221 signature, err := authority.SignEthereumPersonalMessage(challenge.SIWEMessage)
222 if err != nil {
223 return types.RegisterResponse{}, nil, "", "", err
224 }
225
226 var resp types.RegisterResponse
227 if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRegister, types.RegisterRequest{
228 ChallengeID: challenge.ChallengeID,
229 SIWEMessage: challenge.SIWEMessage,
230 SIWESignature: signature,
231 ReportedIP: utils.ResolvePublicIP(ctx),
232 }, nil, &resp); err != nil {
233 return types.RegisterResponse{}, nil, "", "", err
234 }
235 registeredIdentity, err := identity.NormalizeIdentity(resp.Identity)
236 if err != nil {
237 _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
238 return types.RegisterResponse{}, nil, "", "", err
239 }
240 if registeredIdentity.Key() != registerIdentity.Key() {
241 _ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
242 return types.RegisterResponse{}, nil, "", "", errors.New("relay returned mismatched lease identity")
243 }
244 return resp, hopRoutes, publicHostname, routeHostname, nil
245 }
246
247 func (l *listener) renewRegisteredLease(ctx context.Context, ttl time.Duration, accessToken string) (types.RenewResponse, error) {
248 var resp types.RenewResponse
249 req := newRenewRequest(ttl, accessToken, utils.ResolvePublicIP(ctx), l.metadataSnapshot())
250 if err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKRenew, req, nil, &resp); err != nil {
251 return types.RenewResponse{}, err
252 }
253 return resp, nil
254 }
255
256 func newRenewRequest(ttl time.Duration, accessToken, reportedIP string, metadata types.LeaseMetadata) types.RenewRequest {
257 return types.RenewRequest{
258 AccessToken: accessToken,
259 TTL: int(ttl / time.Second),
260 ReportedIP: reportedIP,
261 Metadata: metadata.Copy(),
262 }
263 }
264
265 func (l *listener) unregisterLease(ctx context.Context, accessToken string, hopRoutes []types.HopRoute) error {
266 hopErr := l.unregisterHopRoutes(ctx, hopRoutes)
267 err := utils.HTTPDoAPIPath(ctx, l.httpClient, l.relayURL, http.MethodPost, types.PathSDKUnregister, types.UnregisterRequest{
268 AccessToken: accessToken,
269 }, nil, nil)
270 return errors.Join(hopErr, err)
271 }
272
273 func (l *listener) registerHopRoutes(ctx context.Context, expiresAt time.Time, routes []types.HopRoute) (string, int, error) {
274 if l.relaySet == nil {
275 return "", 0, errors.New("multi-hop relay set is unavailable")
276 }
277 authority, err := identity.NewLocalAuthority(l.identity)
278 if err != nil {
279 return "", 0, err
280 }
281
282 now := time.Now().UTC()
283 for i := len(routes) - 1; i >= 0; i-- {
284 route := routes[i]
285 desc, ok := l.relaySet.OverlayRelayDescriptor(route.ForwardRelay.APIHTTPSAddr, now)
286 if !ok {
287 return "", 0, fmt.Errorf("multi-hop forward relay %d descriptor is unavailable", i)
288 }
289 route.ForwardRelay = desc
290 route.FirstSeenAt = expiresAt.Add(-30 * time.Second)
291 if i == 0 {
292 route.Metadata = l.metadataSnapshot()
293 route.Metadata.Hide = true
294 }
295 route, err := auth.SignHopRoute(http.MethodPost, route, authority, expiresAt)
296 if err != nil {
297 return "", 0, err
298 }
299 relayURL, err := url.Parse(route.RelayURL)
300 if err != nil {
301 return "", 0, fmt.Errorf("parse hop route relay url: %w", err)
302 }
303
304 bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
305 _, client, transport, err := utils.NewHTTPTLSClient(bootstrapCtx, relayURL, l.requestTimeout)
306 cancel()
307 if err != nil {
308 return "", 0, err
309 }
310 var hopResp types.HopRouteResponse
311 err = utils.HTTPDoAPIPath(ctx, client, relayURL, http.MethodPost, types.PathSDKHop, route, nil, &hopResp)
312 transport.CloseIdleConnections()
313 if err != nil {
314 return "", 0, err
315 }
316 if route.MatchToken != "" || route.RouteHostname == "" {
317 continue
318 }
319 if hopResp.AccessToken == "" {
320 return "", 0, errors.New("entry relay did not return access token")
321 }
322 if hopResp.SNIPort <= 0 {
323 return "", 0, errors.New("entry relay did not return sni port")
324 }
325 return hopResp.AccessToken, hopResp.SNIPort, nil
326 }
327 return "", 0, errors.New("entry hop route did not return access token")
328 }
329
330 func (l *listener) unregisterHopRoutes(ctx context.Context, routes []types.HopRoute) error {
331 var unregisterErr error
332 authority, err := identity.NewLocalAuthority(l.identity)
333 if err != nil {
334 return err
335 }
336 for _, route := range routes {
337 route, err := auth.SignHopRoute(http.MethodDelete, route, authority, time.Time{})
338 if err != nil {
339 unregisterErr = errors.Join(unregisterErr, err)
340 continue
341 }
342 relayURL, err := url.Parse(route.RelayURL)
343 if err != nil {
344 unregisterErr = errors.Join(unregisterErr, fmt.Errorf("parse hop route relay url: %w", err))
345 continue
346 }
347
348 bootstrapCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout+defaultHandshakeTimeout)
349 _, client, transport, err := utils.NewHTTPTLSClient(bootstrapCtx, relayURL, l.requestTimeout)
350 cancel()
351 if err != nil {
352 unregisterErr = errors.Join(unregisterErr, err)
353 continue
354 }
355 err = utils.HTTPDoAPIPath(ctx, client, relayURL, http.MethodDelete, types.PathSDKHop, route, nil, nil)
356 transport.CloseIdleConnections()
357 if err != nil {
358 unregisterErr = errors.Join(unregisterErr, err)
359 }
360 }
361 return unregisterErr
362 }