@limrun/api 0.48.4 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/build-settings.d.mts.map +1 -1
- package/build-settings.d.ts.map +1 -1
- package/build-settings.js +4 -5
- package/build-settings.js.map +1 -1
- package/build-settings.mjs +4 -5
- package/build-settings.mjs.map +1 -1
- package/client.d.mts +8 -5
- package/client.d.mts.map +1 -1
- package/client.d.ts +8 -5
- package/client.d.ts.map +1 -1
- package/client.js +4 -4
- package/client.js.map +1 -1
- package/client.mjs +2 -2
- package/client.mjs.map +1 -1
- package/destination-tunnel-dialer.d.mts +30 -2
- package/destination-tunnel-dialer.d.mts.map +1 -1
- package/destination-tunnel-dialer.d.ts +30 -2
- package/destination-tunnel-dialer.d.ts.map +1 -1
- package/destination-tunnel-dialer.js +349 -65
- package/destination-tunnel-dialer.js.map +1 -1
- package/destination-tunnel-dialer.mjs +345 -64
- package/destination-tunnel-dialer.mjs.map +1 -1
- package/destination-tunnel-har.d.mts +23 -0
- package/destination-tunnel-har.d.mts.map +1 -0
- package/destination-tunnel-har.d.ts +23 -0
- package/destination-tunnel-har.d.ts.map +1 -0
- package/destination-tunnel-har.js +164 -0
- package/destination-tunnel-har.js.map +1 -0
- package/destination-tunnel-har.mjs +160 -0
- package/destination-tunnel-har.mjs.map +1 -0
- package/destination-tunnel-inspection.d.mts +71 -0
- package/destination-tunnel-inspection.d.mts.map +1 -0
- package/destination-tunnel-inspection.d.ts +71 -0
- package/destination-tunnel-inspection.d.ts.map +1 -0
- package/destination-tunnel-inspection.js +267 -0
- package/destination-tunnel-inspection.js.map +1 -0
- package/destination-tunnel-inspection.mjs +261 -0
- package/destination-tunnel-inspection.mjs.map +1 -0
- package/destination-tunnel.d.mts +119 -14
- package/destination-tunnel.d.mts.map +1 -1
- package/destination-tunnel.d.ts +119 -14
- package/destination-tunnel.d.ts.map +1 -1
- package/destination-tunnel.js +407 -98
- package/destination-tunnel.js.map +1 -1
- package/destination-tunnel.mjs +374 -73
- package/destination-tunnel.mjs.map +1 -1
- package/exec-client.d.mts +9 -0
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts +9 -0
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js.map +1 -1
- package/exec-client.mjs.map +1 -1
- package/index.d.mts +8 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +8 -1
- package/index.d.ts.map +1 -1
- package/index.js +26 -2
- package/index.js.map +1 -1
- package/index.mjs +7 -1
- package/index.mjs.map +1 -1
- package/instance-client.d.mts +107 -1
- package/instance-client.d.mts.map +1 -1
- package/instance-client.d.ts +107 -1
- package/instance-client.d.ts.map +1 -1
- package/instance-client.js +83 -1
- package/instance-client.js.map +1 -1
- package/instance-client.mjs +83 -1
- package/instance-client.mjs.map +1 -1
- package/internal/destination-tunnel-management.d.mts +4 -3
- package/internal/destination-tunnel-management.d.mts.map +1 -1
- package/internal/destination-tunnel-management.d.ts +4 -3
- package/internal/destination-tunnel-management.d.ts.map +1 -1
- package/internal/destination-tunnel-management.js +64 -46
- package/internal/destination-tunnel-management.js.map +1 -1
- package/internal/destination-tunnel-management.mjs +55 -37
- package/internal/destination-tunnel-management.mjs.map +1 -1
- package/internal/destination-tunnel-url.d.mts +2 -0
- package/internal/destination-tunnel-url.d.mts.map +1 -1
- package/internal/destination-tunnel-url.d.ts +2 -0
- package/internal/destination-tunnel-url.d.ts.map +1 -1
- package/internal/destination-tunnel-url.js +45 -5
- package/internal/destination-tunnel-url.js.map +1 -1
- package/internal/destination-tunnel-url.mjs +44 -5
- package/internal/destination-tunnel-url.mjs.map +1 -1
- package/internal/destination-tunnel-wire-reader.d.mts +23 -0
- package/internal/destination-tunnel-wire-reader.d.mts.map +1 -0
- package/internal/destination-tunnel-wire-reader.d.ts +23 -0
- package/internal/destination-tunnel-wire-reader.d.ts.map +1 -0
- package/internal/destination-tunnel-wire-reader.js +110 -0
- package/internal/destination-tunnel-wire-reader.js.map +1 -0
- package/internal/destination-tunnel-wire-reader.mjs +93 -0
- package/internal/destination-tunnel-wire-reader.mjs.map +1 -0
- package/internal/persist-option.d.mts +14 -0
- package/internal/persist-option.d.mts.map +1 -0
- package/internal/persist-option.d.ts +14 -0
- package/internal/persist-option.d.ts.map +1 -0
- package/internal/persist-option.js +18 -0
- package/internal/persist-option.js.map +1 -0
- package/internal/persist-option.mjs +15 -0
- package/internal/persist-option.mjs.map +1 -0
- package/internal/proxy-transport.d.mts +27 -0
- package/internal/proxy-transport.d.mts.map +1 -1
- package/internal/proxy-transport.d.ts +27 -0
- package/internal/proxy-transport.d.ts.map +1 -1
- package/internal/proxy-transport.js +206 -0
- package/internal/proxy-transport.js.map +1 -1
- package/internal/proxy-transport.mjs +205 -0
- package/internal/proxy-transport.mjs.map +1 -1
- package/internal/session-stream.d.mts +39 -0
- package/internal/session-stream.d.mts.map +1 -0
- package/internal/session-stream.d.ts +39 -0
- package/internal/session-stream.d.ts.map +1 -0
- package/internal/session-stream.js +144 -0
- package/internal/session-stream.js.map +1 -0
- package/internal/session-stream.mjs +139 -0
- package/internal/session-stream.mjs.map +1 -0
- package/internal/tslib.js +4 -4
- package/ios-client.d.mts +63 -4
- package/ios-client.d.mts.map +1 -1
- package/ios-client.d.ts +63 -4
- package/ios-client.d.ts.map +1 -1
- package/ios-client.js +48 -2
- package/ios-client.js.map +1 -1
- package/ios-client.mjs +48 -2
- package/ios-client.mjs.map +1 -1
- package/package.json +22 -1
- package/resources/android-instances-helpers.d.mts +25 -0
- package/resources/android-instances-helpers.d.mts.map +1 -0
- package/resources/android-instances-helpers.d.ts +25 -0
- package/resources/android-instances-helpers.d.ts.map +1 -0
- package/resources/android-instances-helpers.js +37 -0
- package/resources/android-instances-helpers.js.map +1 -0
- package/resources/android-instances-helpers.mjs +33 -0
- package/resources/android-instances-helpers.mjs.map +1 -0
- package/resources/index.d.mts +6 -3
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +6 -3
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +6 -5
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +3 -3
- package/resources/index.mjs.map +1 -1
- package/resources/ios-instances-helpers.d.mts +25 -0
- package/resources/ios-instances-helpers.d.mts.map +1 -0
- package/resources/ios-instances-helpers.d.ts +25 -0
- package/resources/ios-instances-helpers.d.ts.map +1 -0
- package/resources/ios-instances-helpers.js +37 -0
- package/resources/ios-instances-helpers.js.map +1 -0
- package/resources/ios-instances-helpers.mjs +33 -0
- package/resources/ios-instances-helpers.mjs.map +1 -0
- package/resources/session-artifacts.d.mts +70 -0
- package/resources/session-artifacts.d.mts.map +1 -0
- package/resources/session-artifacts.d.ts +70 -0
- package/resources/session-artifacts.d.ts.map +1 -0
- package/resources/session-artifacts.js +8 -0
- package/resources/session-artifacts.js.map +1 -0
- package/resources/session-artifacts.mjs +7 -0
- package/resources/session-artifacts.mjs.map +1 -0
- package/resources/xcode-instances-helpers.d.mts +55 -0
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +55 -0
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +49 -3
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +47 -2
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/build-settings.ts +4 -5
- package/src/client.ts +12 -2
- package/src/destination-tunnel-dialer.ts +403 -64
- package/src/destination-tunnel-har.ts +200 -0
- package/src/destination-tunnel-inspection.ts +367 -0
- package/src/destination-tunnel.ts +510 -88
- package/src/exec-client.ts +9 -0
- package/src/index.ts +58 -0
- package/src/instance-client.ts +273 -5
- package/src/internal/destination-tunnel-management.ts +67 -43
- package/src/internal/destination-tunnel-url.ts +61 -5
- package/src/internal/destination-tunnel-wire-reader.ts +109 -0
- package/src/internal/persist-option.ts +26 -0
- package/src/internal/proxy-transport.ts +248 -0
- package/src/internal/session-stream.ts +157 -0
- package/src/ios-client.ts +129 -8
- package/src/resources/android-instances-helpers.ts +45 -0
- package/src/resources/index.ts +14 -2
- package/src/resources/ios-instances-helpers.ts +45 -0
- package/src/resources/session-artifacts.ts +95 -0
- package/src/resources/xcode-instances-helpers.ts +116 -5
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -1,14 +1,99 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
1
2
|
import net from 'net';
|
|
3
|
+
import { domainToASCII } from 'url';
|
|
4
|
+
import {
|
|
5
|
+
DestinationTunnelProtocolError,
|
|
6
|
+
readArray,
|
|
7
|
+
readBoolean,
|
|
8
|
+
readInteger,
|
|
9
|
+
readOptionalNonNegativeInteger,
|
|
10
|
+
readOptionalString,
|
|
11
|
+
readRecord,
|
|
12
|
+
readString,
|
|
13
|
+
} from './internal/destination-tunnel-wire-reader';
|
|
14
|
+
|
|
15
|
+
export { DestinationTunnelProtocolError } from './internal/destination-tunnel-wire-reader';
|
|
2
16
|
|
|
3
17
|
export const DESTINATION_TUNNEL_VERSION = 1;
|
|
4
18
|
export const DESTINATION_TUNNEL_MAX_ROUTES = 10;
|
|
19
|
+
export const DESTINATION_TUNNEL_MAX_DOMAINS = 64;
|
|
5
20
|
export const DESTINATION_TUNNEL_CONN_ID_HEADER_BYTES = 4;
|
|
21
|
+
/**
|
|
22
|
+
* IPv4 range used by the device side for synthetic (fake) DNS answers of
|
|
23
|
+
* matched domain selectors.
|
|
24
|
+
*/
|
|
25
|
+
export const DESTINATION_TUNNEL_FAKE_RANGE = '198.18.0.0/15';
|
|
26
|
+
/** Upper bound for per-flow receive windows advertised in open/openOk. */
|
|
27
|
+
export const DESTINATION_TUNNEL_MAX_WINDOW = 64 * 1024 * 1024;
|
|
28
|
+
/** Default receive window advertised by generic SDK tunnel clients. */
|
|
29
|
+
export const DESTINATION_TUNNEL_DEFAULT_WINDOW = 1024 * 1024;
|
|
30
|
+
/** Upper bound for a single windowUpdate increment. */
|
|
31
|
+
export const DESTINATION_TUNNEL_MAX_WINDOW_INCREMENT = 0x7fff_ffff;
|
|
32
|
+
export const DESTINATION_TUNNEL_DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
33
|
+
export const DESTINATION_TUNNEL_MAX_BODY_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
export const DESTINATION_TUNNEL_DEFAULT_TTL_SECONDS = 72 * 60 * 60;
|
|
35
|
+
export const DESTINATION_TUNNEL_MAX_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
6
36
|
|
|
7
37
|
export interface DestinationTunnelRoute {
|
|
8
38
|
host: string;
|
|
9
39
|
port: number;
|
|
10
40
|
}
|
|
11
41
|
|
|
42
|
+
/** Canonical selector values carried by the public protocol. */
|
|
43
|
+
export type DestinationTunnelSelectors = string[];
|
|
44
|
+
|
|
45
|
+
/** Internal classification used to enforce and dial canonical selectors. */
|
|
46
|
+
export interface DestinationTunnelSelectorCatalog {
|
|
47
|
+
routes?: DestinationTunnelRoute[];
|
|
48
|
+
domains?: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DestinationTunnelInspectionConfig {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
captureBodies: boolean;
|
|
54
|
+
maxBodyBytes: number;
|
|
55
|
+
persist: boolean;
|
|
56
|
+
ttlSeconds: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type DestinationTunnelTransportType = 'tcp' | 'tls';
|
|
60
|
+
|
|
61
|
+
export type DestinationTunnelTransportRequest =
|
|
62
|
+
| { type: 'tcp' }
|
|
63
|
+
| {
|
|
64
|
+
type: 'tls';
|
|
65
|
+
serverName: string;
|
|
66
|
+
alpnProtocols: string[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export interface DestinationTunnelTransportResult {
|
|
70
|
+
type: DestinationTunnelTransportType;
|
|
71
|
+
alpnProtocol?: string;
|
|
72
|
+
remoteAddress?: string;
|
|
73
|
+
dnsMs?: number;
|
|
74
|
+
connectMs?: number;
|
|
75
|
+
tlsMs?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type DestinationTunnelSelectorKind = 'route' | 'domain';
|
|
79
|
+
|
|
80
|
+
export type DestinationTunnelBindStatus = 'ok' | 'conflict' | 'error';
|
|
81
|
+
|
|
82
|
+
/** Per-address bind outcome the device server reports for exact routes. */
|
|
83
|
+
export interface DestinationTunnelBindReport {
|
|
84
|
+
address: string;
|
|
85
|
+
status: DestinationTunnelBindStatus;
|
|
86
|
+
osCode?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Normalized selector the server acknowledges in `ready` and status. */
|
|
90
|
+
export interface DestinationTunnelSelectorReport {
|
|
91
|
+
id: string;
|
|
92
|
+
kind: DestinationTunnelSelectorKind;
|
|
93
|
+
value: string;
|
|
94
|
+
binds?: DestinationTunnelBindReport[];
|
|
95
|
+
}
|
|
96
|
+
|
|
12
97
|
export type DestinationTunnelOpenFailureReason =
|
|
13
98
|
| 'dns_not_found'
|
|
14
99
|
| 'dns_temporary_failure'
|
|
@@ -18,7 +103,10 @@ export type DestinationTunnelOpenFailureReason =
|
|
|
18
103
|
| 'unreachable'
|
|
19
104
|
| 'permission_denied'
|
|
20
105
|
| 'resource_exhausted'
|
|
21
|
-
| '
|
|
106
|
+
| 'selector_not_allowed'
|
|
107
|
+
| 'tls_handshake_failed'
|
|
108
|
+
| 'tls_validation_failed'
|
|
109
|
+
| 'tls_protocol_error'
|
|
22
110
|
| 'cancelled'
|
|
23
111
|
| 'internal'
|
|
24
112
|
| (string & {});
|
|
@@ -34,16 +122,27 @@ export type DestinationTunnelResetReason =
|
|
|
34
122
|
export type DestinationTunnelSessionErrorCode =
|
|
35
123
|
| 'unsupported_version'
|
|
36
124
|
| 'invalid_message'
|
|
37
|
-
| '
|
|
38
|
-
| 'route_capacity'
|
|
125
|
+
| 'invalid_selector'
|
|
39
126
|
| 'already_active'
|
|
40
127
|
| 'unavailable'
|
|
41
128
|
| 'internal'
|
|
42
129
|
| (string & {});
|
|
43
130
|
|
|
44
131
|
export type DestinationTunnelClientMessage =
|
|
45
|
-
| {
|
|
46
|
-
|
|
132
|
+
| {
|
|
133
|
+
type: 'start';
|
|
134
|
+
version: number;
|
|
135
|
+
selectors: DestinationTunnelSelectors;
|
|
136
|
+
inspection: DestinationTunnelInspectionConfig;
|
|
137
|
+
/** Default per-flow receive window this client grants. */
|
|
138
|
+
window: number;
|
|
139
|
+
}
|
|
140
|
+
| {
|
|
141
|
+
type: 'openOk';
|
|
142
|
+
connId: number;
|
|
143
|
+
transport: DestinationTunnelTransportResult;
|
|
144
|
+
window: number;
|
|
145
|
+
}
|
|
47
146
|
| {
|
|
48
147
|
type: 'openFail';
|
|
49
148
|
connId: number;
|
|
@@ -51,43 +150,47 @@ export type DestinationTunnelClientMessage =
|
|
|
51
150
|
osCode?: string;
|
|
52
151
|
}
|
|
53
152
|
| { type: 'fin'; connId: number }
|
|
54
|
-
| { type: 'reset'; connId: number; reason: DestinationTunnelResetReason; osCode?: string }
|
|
153
|
+
| { type: 'reset'; connId: number; reason: DestinationTunnelResetReason; osCode?: string }
|
|
154
|
+
| { type: 'windowUpdate'; connId: number; increment: number };
|
|
55
155
|
|
|
56
156
|
export type DestinationTunnelServerMessage =
|
|
57
|
-
| {
|
|
157
|
+
| {
|
|
158
|
+
type: 'ready';
|
|
159
|
+
version: number;
|
|
160
|
+
tunnelId: string;
|
|
161
|
+
selectors: DestinationTunnelSelectorReport[];
|
|
162
|
+
configHash: string;
|
|
163
|
+
}
|
|
58
164
|
| {
|
|
59
165
|
type: 'open';
|
|
60
166
|
connId: number;
|
|
61
|
-
|
|
167
|
+
selectorId: string;
|
|
62
168
|
host: string;
|
|
63
169
|
port: number;
|
|
64
|
-
|
|
170
|
+
transport: DestinationTunnelTransportRequest;
|
|
171
|
+
/** Server's receive window for this flow. */
|
|
172
|
+
window: number;
|
|
65
173
|
}
|
|
66
174
|
| { type: 'fin'; connId: number }
|
|
67
175
|
| { type: 'reset'; connId: number; reason: DestinationTunnelResetReason; osCode?: string }
|
|
176
|
+
| { type: 'windowUpdate'; connId: number; increment: number }
|
|
68
177
|
| { type: 'error'; code: DestinationTunnelSessionErrorCode };
|
|
69
178
|
|
|
70
|
-
export type
|
|
179
|
+
export type DestinationTunnelSelectorErrorCode =
|
|
71
180
|
| 'empty'
|
|
72
181
|
| 'too_many'
|
|
73
182
|
| 'invalid_host'
|
|
74
183
|
| 'invalid_port'
|
|
184
|
+
| 'invalid_domain'
|
|
75
185
|
| 'duplicate';
|
|
76
186
|
|
|
77
|
-
export class
|
|
187
|
+
export class DestinationTunnelSelectorError extends Error {
|
|
78
188
|
constructor(
|
|
79
|
-
readonly code:
|
|
189
|
+
readonly code: DestinationTunnelSelectorErrorCode,
|
|
80
190
|
message: string,
|
|
81
191
|
) {
|
|
82
192
|
super(message);
|
|
83
|
-
this.name = '
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export class DestinationTunnelProtocolError extends Error {
|
|
88
|
-
constructor(message: string) {
|
|
89
|
-
super(message);
|
|
90
|
-
this.name = 'DestinationTunnelProtocolError';
|
|
193
|
+
this.name = 'DestinationTunnelSelectorError';
|
|
91
194
|
}
|
|
92
195
|
}
|
|
93
196
|
|
|
@@ -117,12 +220,13 @@ export function decodeDestinationTunnelDataFrame(frame: Buffer): {
|
|
|
117
220
|
|
|
118
221
|
export function validateDestinationTunnelRoutes(
|
|
119
222
|
routes: readonly DestinationTunnelRoute[],
|
|
223
|
+
options: { minPort?: number } = {},
|
|
120
224
|
): DestinationTunnelRoute[] {
|
|
121
225
|
if (routes.length === 0) {
|
|
122
|
-
throw new
|
|
226
|
+
throw new DestinationTunnelSelectorError('empty', 'at least one tunnel route is required');
|
|
123
227
|
}
|
|
124
228
|
if (routes.length > DESTINATION_TUNNEL_MAX_ROUTES) {
|
|
125
|
-
throw new
|
|
229
|
+
throw new DestinationTunnelSelectorError(
|
|
126
230
|
'too_many',
|
|
127
231
|
`at most ${DESTINATION_TUNNEL_MAX_ROUTES} tunnel routes are allowed`,
|
|
128
232
|
);
|
|
@@ -131,10 +235,10 @@ export function validateDestinationTunnelRoutes(
|
|
|
131
235
|
const canonicalRoutes: DestinationTunnelRoute[] = [];
|
|
132
236
|
const seen = new Set<string>();
|
|
133
237
|
for (const route of routes) {
|
|
134
|
-
const canonical = canonicalizeDestinationTunnelRoute(route);
|
|
238
|
+
const canonical = canonicalizeDestinationTunnelRoute(route, options.minPort);
|
|
135
239
|
const key = `${canonical.host}\0${canonical.port}`;
|
|
136
240
|
if (seen.has(key)) {
|
|
137
|
-
throw new
|
|
241
|
+
throw new DestinationTunnelSelectorError(
|
|
138
242
|
'duplicate',
|
|
139
243
|
`duplicate tunnel route ${canonical.host}:${canonical.port}`,
|
|
140
244
|
);
|
|
@@ -145,17 +249,188 @@ export function validateDestinationTunnelRoutes(
|
|
|
145
249
|
return canonicalRoutes;
|
|
146
250
|
}
|
|
147
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Validate and canonicalize a full selector set. Selector policy specific to
|
|
254
|
+
* one product (such as the Android bind-listener minimum route port) is
|
|
255
|
+
* expressed via options rather than separate message shapes.
|
|
256
|
+
*/
|
|
257
|
+
export function validateDestinationTunnelSelectors(
|
|
258
|
+
selectors: readonly string[],
|
|
259
|
+
options: { minRoutePort?: number } = {},
|
|
260
|
+
): DestinationTunnelSelectors {
|
|
261
|
+
if (!Array.isArray(selectors)) {
|
|
262
|
+
throw new DestinationTunnelSelectorError('invalid_host', 'tunnel selectors must be an array');
|
|
263
|
+
}
|
|
264
|
+
if (selectors.length === 0) {
|
|
265
|
+
throw new DestinationTunnelSelectorError('empty', 'at least one tunnel selector is required');
|
|
266
|
+
}
|
|
267
|
+
const canonical: string[] = [];
|
|
268
|
+
const seen = new Set<string>();
|
|
269
|
+
let routeCount = 0;
|
|
270
|
+
let domainCount = 0;
|
|
271
|
+
for (const selector of selectors) {
|
|
272
|
+
const parsed = parseDestinationTunnelSelector(selector, options.minRoutePort);
|
|
273
|
+
if (parsed.route) routeCount++;
|
|
274
|
+
else domainCount++;
|
|
275
|
+
if (routeCount > DESTINATION_TUNNEL_MAX_ROUTES) {
|
|
276
|
+
throw new DestinationTunnelSelectorError(
|
|
277
|
+
'too_many',
|
|
278
|
+
`at most ${DESTINATION_TUNNEL_MAX_ROUTES} exact tunnel selectors are allowed`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (domainCount > DESTINATION_TUNNEL_MAX_DOMAINS) {
|
|
282
|
+
throw new DestinationTunnelSelectorError(
|
|
283
|
+
'too_many',
|
|
284
|
+
`at most ${DESTINATION_TUNNEL_MAX_DOMAINS} domain tunnel selectors are allowed`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (seen.has(parsed.value)) {
|
|
288
|
+
throw new DestinationTunnelSelectorError('duplicate', `duplicate tunnel selector ${parsed.value}`);
|
|
289
|
+
}
|
|
290
|
+
seen.add(parsed.value);
|
|
291
|
+
canonical.push(parsed.value);
|
|
292
|
+
}
|
|
293
|
+
return canonical;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function classifyDestinationTunnelSelectors(
|
|
297
|
+
selectors: readonly string[],
|
|
298
|
+
options: { minRoutePort?: number } = {},
|
|
299
|
+
): DestinationTunnelSelectorCatalog {
|
|
300
|
+
const canonical = validateDestinationTunnelSelectors(selectors, options);
|
|
301
|
+
const routes: DestinationTunnelRoute[] = [];
|
|
302
|
+
const domains: string[] = [];
|
|
303
|
+
for (const selector of canonical) {
|
|
304
|
+
const parsed = parseDestinationTunnelSelector(selector, options.minRoutePort);
|
|
305
|
+
if (parsed.route) routes.push(parsed.route);
|
|
306
|
+
else domains.push(parsed.domain!);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
...(routes.length > 0 ? { routes } : {}),
|
|
310
|
+
...(domains.length > 0 ? { domains } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function validateDestinationTunnelDomains(domains: readonly string[]): string[] {
|
|
315
|
+
if (domains.length > DESTINATION_TUNNEL_MAX_DOMAINS) {
|
|
316
|
+
throw new DestinationTunnelSelectorError(
|
|
317
|
+
'too_many',
|
|
318
|
+
`at most ${DESTINATION_TUNNEL_MAX_DOMAINS} tunnel domains are allowed`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const canonical: string[] = [];
|
|
322
|
+
const seen = new Set<string>();
|
|
323
|
+
for (const domain of domains) {
|
|
324
|
+
const normalized = canonicalizeDestinationTunnelDomain(domain);
|
|
325
|
+
if (seen.has(normalized)) {
|
|
326
|
+
throw new DestinationTunnelSelectorError('duplicate', `duplicate tunnel domain ${normalized}`);
|
|
327
|
+
}
|
|
328
|
+
seen.add(normalized);
|
|
329
|
+
canonical.push(normalized);
|
|
330
|
+
}
|
|
331
|
+
return canonical;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Deterministic hash of the canonical selector configuration. Servers echo it
|
|
336
|
+
* in `ready` so clients can detect config mismatches across reconnects. The
|
|
337
|
+
* hashed JSON has fixed key order.
|
|
338
|
+
*/
|
|
339
|
+
export function destinationTunnelConfigHash(
|
|
340
|
+
selectors: DestinationTunnelSelectors,
|
|
341
|
+
inspection: DestinationTunnelInspectionConfig = disabledDestinationTunnelInspection(),
|
|
342
|
+
): string {
|
|
343
|
+
const canonical = validateDestinationTunnelSelectors(selectors);
|
|
344
|
+
const canonicalInspection = normalizeDestinationTunnelInspection(inspection);
|
|
345
|
+
const parts: string[] = [
|
|
346
|
+
`"version":${DESTINATION_TUNNEL_VERSION}`,
|
|
347
|
+
`"selectors":[${canonical.map((selector) => JSON.stringify(selector)).join(',')}]`,
|
|
348
|
+
];
|
|
349
|
+
parts.push(
|
|
350
|
+
`"inspection":{"enabled":${canonicalInspection.enabled},"captureBodies":${canonicalInspection.captureBodies},"maxBodyBytes":${canonicalInspection.maxBodyBytes},"persist":${canonicalInspection.persist},"ttlSeconds":${canonicalInspection.ttlSeconds}}`,
|
|
351
|
+
);
|
|
352
|
+
return crypto
|
|
353
|
+
.createHash('sha256')
|
|
354
|
+
.update(`{${parts.join(',')}}`, 'utf8')
|
|
355
|
+
.digest('hex');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function normalizeDestinationTunnelInspection(
|
|
359
|
+
inspection: Partial<DestinationTunnelInspectionConfig>,
|
|
360
|
+
): DestinationTunnelInspectionConfig {
|
|
361
|
+
const enabled = inspection.enabled ?? false;
|
|
362
|
+
const captureBodies = inspection.captureBodies ?? false;
|
|
363
|
+
const maxBodyBytes = inspection.maxBodyBytes ?? DESTINATION_TUNNEL_DEFAULT_MAX_BODY_BYTES;
|
|
364
|
+
const persist = inspection.persist ?? false;
|
|
365
|
+
const ttlSeconds = inspection.ttlSeconds ?? DESTINATION_TUNNEL_DEFAULT_TTL_SECONDS;
|
|
366
|
+
if (captureBodies && !enabled) {
|
|
367
|
+
throw new DestinationTunnelProtocolError('captureBodies requires inspection to be enabled');
|
|
368
|
+
}
|
|
369
|
+
if (persist && !enabled) {
|
|
370
|
+
throw new DestinationTunnelProtocolError('persist requires inspection to be enabled');
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
!Number.isInteger(maxBodyBytes) ||
|
|
374
|
+
maxBodyBytes < 1 ||
|
|
375
|
+
maxBodyBytes > DESTINATION_TUNNEL_MAX_BODY_BYTES
|
|
376
|
+
) {
|
|
377
|
+
throw new DestinationTunnelProtocolError(
|
|
378
|
+
`maxBodyBytes must be an integer between 1 and ${DESTINATION_TUNNEL_MAX_BODY_BYTES}`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > DESTINATION_TUNNEL_MAX_TTL_SECONDS) {
|
|
382
|
+
throw new DestinationTunnelProtocolError(
|
|
383
|
+
`ttlSeconds must be an integer between 1 and ${DESTINATION_TUNNEL_MAX_TTL_SECONDS}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return { enabled, captureBodies, maxBodyBytes, persist, ttlSeconds };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function disabledDestinationTunnelInspection(): DestinationTunnelInspectionConfig {
|
|
390
|
+
return normalizeDestinationTunnelInspection({ enabled: false, captureBodies: false });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Selector IDs are one-based positions in the canonical selector array. */
|
|
394
|
+
export function destinationTunnelSelectorIds(selectors: DestinationTunnelSelectors): string[] {
|
|
395
|
+
return validateDestinationTunnelSelectors(selectors).map((_, index) => `selector-${index + 1}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function destinationTunnelDomainMatches(pattern: string, host: string): boolean {
|
|
399
|
+
const candidate = host.toLowerCase();
|
|
400
|
+
if (pattern.startsWith('*.')) {
|
|
401
|
+
const base = pattern.slice(2);
|
|
402
|
+
return candidate.length > base.length + 1 && candidate.endsWith(`.${base}`);
|
|
403
|
+
}
|
|
404
|
+
return candidate === pattern;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Verify a server OPEN against the negotiated selectors. Every OPEN must name
|
|
409
|
+
* a known selector ID and a target that the selector actually covers.
|
|
410
|
+
*/
|
|
148
411
|
export function assertDestinationTunnelOpenAllowed(
|
|
149
412
|
message: Extract<DestinationTunnelServerMessage, { type: 'open' }>,
|
|
150
|
-
|
|
151
|
-
):
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
413
|
+
selectors: DestinationTunnelSelectors,
|
|
414
|
+
): DestinationTunnelSelectorKind {
|
|
415
|
+
const canonical = validateDestinationTunnelSelectors(selectors);
|
|
416
|
+
const match = /^selector-([1-9]\d*)$/.exec(message.selectorId);
|
|
417
|
+
const index = match?.[1] ? Number(match[1]) - 1 : -1;
|
|
418
|
+
const fail = (): never => {
|
|
155
419
|
throw new DestinationTunnelProtocolError(
|
|
156
|
-
`server requested undeclared
|
|
420
|
+
`server requested undeclared selector ${message.selectorId} ${message.host}:${message.port}`,
|
|
157
421
|
);
|
|
422
|
+
};
|
|
423
|
+
if (index < 0) fail();
|
|
424
|
+
if (message.port === 53) fail();
|
|
425
|
+
const selector = canonical[index];
|
|
426
|
+
if (selector === undefined) return fail();
|
|
427
|
+
const parsed = parseDestinationTunnelSelector(selector);
|
|
428
|
+
if (parsed.route) {
|
|
429
|
+
if (message.host !== parsed.route.host || message.port !== parsed.route.port) fail();
|
|
430
|
+
return 'route';
|
|
158
431
|
}
|
|
432
|
+
if (!destinationTunnelDomainMatches(parsed.domain!, message.host)) fail();
|
|
433
|
+
return 'domain';
|
|
159
434
|
}
|
|
160
435
|
|
|
161
436
|
export function assertDestinationTunnelReady(
|
|
@@ -176,10 +451,30 @@ export function encodeDestinationTunnelClientMessage(message: DestinationTunnelC
|
|
|
176
451
|
if (version !== DESTINATION_TUNNEL_VERSION) {
|
|
177
452
|
throw new DestinationTunnelProtocolError(`unsupported tunnel version ${version}`);
|
|
178
453
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
454
|
+
const selectors = validateDestinationTunnelSelectors(
|
|
455
|
+
readArray(record, 'selectors').map((value) => {
|
|
456
|
+
if (typeof value !== 'string') {
|
|
457
|
+
throw new DestinationTunnelProtocolError('tunnel selector must be a string');
|
|
458
|
+
}
|
|
459
|
+
return value;
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
const inspection = readInspectionConfig(record);
|
|
463
|
+
return JSON.stringify({
|
|
464
|
+
type,
|
|
465
|
+
version,
|
|
466
|
+
selectors,
|
|
467
|
+
inspection,
|
|
468
|
+
window: readWindow(record),
|
|
469
|
+
});
|
|
181
470
|
}
|
|
182
471
|
case 'openOk':
|
|
472
|
+
return JSON.stringify({
|
|
473
|
+
type,
|
|
474
|
+
connId: readConnectionId(record),
|
|
475
|
+
transport: readTransportResult(record),
|
|
476
|
+
window: readWindow(record),
|
|
477
|
+
});
|
|
183
478
|
case 'fin':
|
|
184
479
|
return JSON.stringify({ type, connId: readConnectionId(record) });
|
|
185
480
|
case 'openFail':
|
|
@@ -190,6 +485,12 @@ export function encodeDestinationTunnelClientMessage(message: DestinationTunnelC
|
|
|
190
485
|
reason: readString(record, 'reason'),
|
|
191
486
|
...readOptionalString(record, 'osCode'),
|
|
192
487
|
});
|
|
488
|
+
case 'windowUpdate':
|
|
489
|
+
return JSON.stringify({
|
|
490
|
+
type,
|
|
491
|
+
connId: readConnectionId(record),
|
|
492
|
+
increment: readWindowIncrement(record),
|
|
493
|
+
});
|
|
193
494
|
default:
|
|
194
495
|
throw new DestinationTunnelProtocolError(`unknown tunnel control message type ${type}`);
|
|
195
496
|
}
|
|
@@ -205,15 +506,20 @@ export function decodeDestinationTunnelServerMessage(value: unknown): Destinatio
|
|
|
205
506
|
type,
|
|
206
507
|
version: readInteger(message, 'version'),
|
|
207
508
|
tunnelId: readString(message, 'tunnelId'),
|
|
509
|
+
selectors: readArray(message, 'selectors').map((value, index) =>
|
|
510
|
+
readSelectorReport(value, `selector-${index + 1}`),
|
|
511
|
+
),
|
|
512
|
+
configHash: readString(message, 'configHash'),
|
|
208
513
|
};
|
|
209
514
|
case 'open':
|
|
210
515
|
return {
|
|
211
516
|
type,
|
|
212
517
|
connId: readConnectionId(message),
|
|
213
|
-
|
|
518
|
+
selectorId: readString(message, 'selectorId'),
|
|
214
519
|
host: readString(message, 'host'),
|
|
215
520
|
port: readPort(message, 'port'),
|
|
216
|
-
|
|
521
|
+
transport: readTransportRequest(message),
|
|
522
|
+
window: readWindow(message),
|
|
217
523
|
};
|
|
218
524
|
case 'fin':
|
|
219
525
|
return { type, connId: readConnectionId(message) };
|
|
@@ -224,6 +530,12 @@ export function decodeDestinationTunnelServerMessage(value: unknown): Destinatio
|
|
|
224
530
|
reason: readString(message, 'reason'),
|
|
225
531
|
...readOptionalString(message, 'osCode'),
|
|
226
532
|
};
|
|
533
|
+
case 'windowUpdate':
|
|
534
|
+
return {
|
|
535
|
+
type,
|
|
536
|
+
connId: readConnectionId(message),
|
|
537
|
+
increment: readWindowIncrement(message),
|
|
538
|
+
};
|
|
227
539
|
case 'error':
|
|
228
540
|
return { type, code: readString(message, 'code') };
|
|
229
541
|
default:
|
|
@@ -231,9 +543,49 @@ export function decodeDestinationTunnelServerMessage(value: unknown): Destinatio
|
|
|
231
543
|
}
|
|
232
544
|
}
|
|
233
545
|
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
546
|
+
function parseDestinationTunnelSelector(
|
|
547
|
+
selector: string,
|
|
548
|
+
minRoutePort = 1,
|
|
549
|
+
): { value: string; route?: DestinationTunnelRoute; domain?: string } {
|
|
550
|
+
if (typeof selector !== 'string' || selector.length === 0) {
|
|
551
|
+
throw new DestinationTunnelSelectorError('empty', 'tunnel selector must not be empty');
|
|
552
|
+
}
|
|
553
|
+
if (!selector.startsWith('[') && !selector.includes(':')) {
|
|
554
|
+
const domain = canonicalizeDestinationTunnelDomain(selector);
|
|
555
|
+
return { value: domain, domain };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let host: string;
|
|
559
|
+
let portText: string;
|
|
560
|
+
if (selector.startsWith('[')) {
|
|
561
|
+
const match = /^\[([^\]]+)\]:(\d+)$/.exec(selector);
|
|
562
|
+
if (!match?.[1] || !match[2]) {
|
|
563
|
+
throw new DestinationTunnelSelectorError('invalid_host', `invalid tunnel selector ${selector}`);
|
|
564
|
+
}
|
|
565
|
+
host = match[1];
|
|
566
|
+
portText = match[2];
|
|
567
|
+
} else {
|
|
568
|
+
const separator = selector.lastIndexOf(':');
|
|
569
|
+
if (separator <= 0 || selector.indexOf(':') !== separator) {
|
|
570
|
+
throw new DestinationTunnelSelectorError('invalid_host', `invalid tunnel selector ${selector}`);
|
|
571
|
+
}
|
|
572
|
+
host = selector.slice(0, separator);
|
|
573
|
+
portText = selector.slice(separator + 1);
|
|
574
|
+
}
|
|
575
|
+
if (!/^\d+$/.test(portText)) {
|
|
576
|
+
throw new DestinationTunnelSelectorError('invalid_port', `invalid tunnel selector port ${portText}`);
|
|
577
|
+
}
|
|
578
|
+
const route = canonicalizeDestinationTunnelRoute({ host, port: Number(portText) }, minRoutePort);
|
|
579
|
+
const formattedHost = route.host.includes(':') ? `[${route.host}]` : route.host;
|
|
580
|
+
return { value: `${formattedHost}:${route.port}`, route };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function canonicalizeDestinationTunnelRoute(
|
|
584
|
+
route: DestinationTunnelRoute,
|
|
585
|
+
minPort = 1,
|
|
586
|
+
): DestinationTunnelRoute {
|
|
587
|
+
if (!Number.isInteger(route.port) || route.port < minPort || route.port > 65_535 || route.port === 53) {
|
|
588
|
+
throw new DestinationTunnelSelectorError('invalid_port', `invalid tunnel route port ${route.port}`);
|
|
237
589
|
}
|
|
238
590
|
|
|
239
591
|
const asciiHost = Buffer.byteLength(route.host, 'utf8') === route.host.length;
|
|
@@ -249,12 +601,40 @@ function canonicalizeDestinationTunnelRoute(route: DestinationTunnelRoute): Dest
|
|
|
249
601
|
const hostname = new URL(`http://[${route.host}]/`).hostname;
|
|
250
602
|
const canonical = hostname.slice(1, -1);
|
|
251
603
|
if (canonical !== '::1' && /^::(?:[0-9a-f]{1,4}:)?[0-9a-f]{1,4}$/.test(canonical)) {
|
|
252
|
-
throw new
|
|
604
|
+
throw new DestinationTunnelSelectorError('invalid_host', `invalid tunnel route host ${route.host}`);
|
|
253
605
|
}
|
|
254
606
|
return { host: canonicalizeIPv6(canonical), port: route.port };
|
|
255
607
|
}
|
|
256
608
|
|
|
257
|
-
throw new
|
|
609
|
+
throw new DestinationTunnelSelectorError('invalid_host', `invalid tunnel route host ${route.host}`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function canonicalizeDestinationTunnelDomain(domain: string): string {
|
|
613
|
+
const invalid = (): never => {
|
|
614
|
+
throw new DestinationTunnelSelectorError('invalid_domain', `invalid tunnel domain ${domain}`);
|
|
615
|
+
};
|
|
616
|
+
if (typeof domain !== 'string' || domain.length === 0 || domain.length > 260) invalid();
|
|
617
|
+
// ASCII only: reject anything IDNA mapping would change to avoid ambiguity
|
|
618
|
+
// between implementations. Users provide punycode (xn--) names directly.
|
|
619
|
+
if (Buffer.byteLength(domain, 'utf8') !== domain.length) invalid();
|
|
620
|
+
const lowered = domain.toLowerCase();
|
|
621
|
+
const wildcard = lowered.startsWith('*.');
|
|
622
|
+
const base = wildcard ? lowered.slice(2) : lowered;
|
|
623
|
+
if (base.length === 0 || base.length > 253) invalid();
|
|
624
|
+
if (base.includes('*')) invalid();
|
|
625
|
+
if (base.endsWith('.') || base.startsWith('.')) invalid();
|
|
626
|
+
if (net.isIP(base) !== 0) invalid();
|
|
627
|
+
if (base === 'localhost') invalid();
|
|
628
|
+
if (domainToASCII(base) !== base) invalid();
|
|
629
|
+
const labels = base.split('.');
|
|
630
|
+
for (const label of labels) {
|
|
631
|
+
if (label.length === 0 || label.length > 63) invalid();
|
|
632
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(label)) invalid();
|
|
633
|
+
}
|
|
634
|
+
// All-numeric final labels would be ambiguous with IPv4 literals.
|
|
635
|
+
const finalLabel = labels[labels.length - 1];
|
|
636
|
+
if (finalLabel !== undefined && /^\d+$/.test(finalLabel)) invalid();
|
|
637
|
+
return wildcard ? `*.${base}` : base;
|
|
258
638
|
}
|
|
259
639
|
|
|
260
640
|
function canonicalizeIPv6(host: string): string {
|
|
@@ -267,62 +647,92 @@ function canonicalizeIPv6(host: string): string {
|
|
|
267
647
|
return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
|
|
268
648
|
}
|
|
269
649
|
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
650
|
+
function readSelectorReport(value: unknown, expectedId: string): DestinationTunnelSelectorReport {
|
|
651
|
+
const report = readRecord(value, 'tunnel selector');
|
|
652
|
+
const id = readString(report, 'id');
|
|
653
|
+
if (id !== expectedId) {
|
|
654
|
+
throw new DestinationTunnelProtocolError(`invalid tunnel selector id ${id}`);
|
|
274
655
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
656
|
+
const kind = readString(report, 'kind');
|
|
657
|
+
if (kind !== 'route' && kind !== 'domain') {
|
|
658
|
+
throw new DestinationTunnelProtocolError(`invalid tunnel selector kind ${kind}`);
|
|
659
|
+
}
|
|
660
|
+
const result: DestinationTunnelSelectorReport = {
|
|
661
|
+
id,
|
|
662
|
+
kind,
|
|
663
|
+
value: readString(report, 'value'),
|
|
283
664
|
};
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
function readRecord(value: unknown, name: string): Record<string, unknown> {
|
|
287
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
288
|
-
throw new DestinationTunnelProtocolError(`${name} must be an object`);
|
|
665
|
+
if (report['binds'] !== undefined) {
|
|
666
|
+
result.binds = readArray(report, 'binds').map(readBindReport);
|
|
289
667
|
}
|
|
290
|
-
return
|
|
668
|
+
return result;
|
|
291
669
|
}
|
|
292
670
|
|
|
293
|
-
function
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
671
|
+
function readBindReport(value: unknown): DestinationTunnelBindReport {
|
|
672
|
+
const bind = readRecord(value, 'tunnel bind report');
|
|
673
|
+
const status = readString(bind, 'status');
|
|
674
|
+
if (status !== 'ok' && status !== 'conflict' && status !== 'error') {
|
|
675
|
+
throw new DestinationTunnelProtocolError(`invalid tunnel bind status ${status}`);
|
|
297
676
|
}
|
|
298
|
-
return
|
|
677
|
+
return {
|
|
678
|
+
address: readString(bind, 'address'),
|
|
679
|
+
status,
|
|
680
|
+
...readOptionalString(bind, 'osCode'),
|
|
681
|
+
};
|
|
299
682
|
}
|
|
300
683
|
|
|
301
|
-
function
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
684
|
+
function readInspectionConfig(record: Record<string, unknown>): DestinationTunnelInspectionConfig {
|
|
685
|
+
const inspection = readRecord(record['inspection'], 'inspection');
|
|
686
|
+
const enabled = readBoolean(inspection, 'enabled');
|
|
687
|
+
const captureBodies = readBoolean(inspection, 'captureBodies');
|
|
688
|
+
const maxBodyBytes = readInteger(inspection, 'maxBodyBytes');
|
|
689
|
+
const persist = readBoolean(inspection, 'persist');
|
|
690
|
+
const ttlSeconds = readInteger(inspection, 'ttlSeconds');
|
|
691
|
+
return normalizeDestinationTunnelInspection({
|
|
692
|
+
enabled,
|
|
693
|
+
captureBodies,
|
|
694
|
+
maxBodyBytes,
|
|
695
|
+
persist,
|
|
696
|
+
ttlSeconds,
|
|
697
|
+
});
|
|
307
698
|
}
|
|
308
699
|
|
|
309
|
-
function
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
700
|
+
function readTransportRequest(record: Record<string, unknown>): DestinationTunnelTransportRequest {
|
|
701
|
+
const transport = readRecord(record['transport'], 'transport');
|
|
702
|
+
const type = readString(transport, 'type');
|
|
703
|
+
if (type === 'tcp') {
|
|
704
|
+
return { type };
|
|
313
705
|
}
|
|
314
|
-
if (
|
|
315
|
-
|
|
706
|
+
if (type === 'tls') {
|
|
707
|
+
const serverName = readString(transport, 'serverName');
|
|
708
|
+
if (serverName.length === 0) {
|
|
709
|
+
throw new DestinationTunnelProtocolError('TLS transport requires serverName');
|
|
710
|
+
}
|
|
711
|
+
const alpnProtocols = readArray(transport, 'alpnProtocols').map((protocol) => {
|
|
712
|
+
if (typeof protocol !== 'string' || protocol.length === 0) {
|
|
713
|
+
throw new DestinationTunnelProtocolError('alpnProtocols must contain non-empty strings');
|
|
714
|
+
}
|
|
715
|
+
return protocol;
|
|
716
|
+
});
|
|
717
|
+
return { type, serverName, alpnProtocols };
|
|
316
718
|
}
|
|
317
|
-
|
|
719
|
+
throw new DestinationTunnelProtocolError(`unsupported tunnel transport ${type}`);
|
|
318
720
|
}
|
|
319
721
|
|
|
320
|
-
function
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
722
|
+
function readTransportResult(record: Record<string, unknown>): DestinationTunnelTransportResult {
|
|
723
|
+
const transport = readRecord(record['transport'], 'transport');
|
|
724
|
+
const type = readString(transport, 'type');
|
|
725
|
+
if (type !== 'tcp' && type !== 'tls') {
|
|
726
|
+
throw new DestinationTunnelProtocolError(`unsupported tunnel transport ${type}`);
|
|
324
727
|
}
|
|
325
|
-
return
|
|
728
|
+
return {
|
|
729
|
+
type,
|
|
730
|
+
...readOptionalString(transport, 'alpnProtocol'),
|
|
731
|
+
...readOptionalString(transport, 'remoteAddress'),
|
|
732
|
+
...readOptionalNonNegativeInteger(transport, 'dnsMs'),
|
|
733
|
+
...readOptionalNonNegativeInteger(transport, 'connectMs'),
|
|
734
|
+
...readOptionalNonNegativeInteger(transport, 'tlsMs'),
|
|
735
|
+
};
|
|
326
736
|
}
|
|
327
737
|
|
|
328
738
|
function readPort(record: Record<string, unknown>, key: string): number {
|
|
@@ -333,6 +743,26 @@ function readPort(record: Record<string, unknown>, key: string): number {
|
|
|
333
743
|
return value;
|
|
334
744
|
}
|
|
335
745
|
|
|
746
|
+
function readWindow(record: Record<string, unknown>): number {
|
|
747
|
+
const value = readInteger(record, 'window');
|
|
748
|
+
if (value < 1 || value > DESTINATION_TUNNEL_MAX_WINDOW) {
|
|
749
|
+
throw new DestinationTunnelProtocolError(
|
|
750
|
+
`window must be an integer between 1 and ${DESTINATION_TUNNEL_MAX_WINDOW}`,
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
return value;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function readWindowIncrement(record: Record<string, unknown>): number {
|
|
757
|
+
const value = readInteger(record, 'increment');
|
|
758
|
+
if (value < 1 || value > DESTINATION_TUNNEL_MAX_WINDOW_INCREMENT) {
|
|
759
|
+
throw new DestinationTunnelProtocolError(
|
|
760
|
+
`increment must be an integer between 1 and ${DESTINATION_TUNNEL_MAX_WINDOW_INCREMENT}`,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return value;
|
|
764
|
+
}
|
|
765
|
+
|
|
336
766
|
function readConnectionId(record: Record<string, unknown>): number {
|
|
337
767
|
const value = readInteger(record, 'connId');
|
|
338
768
|
validateConnectionId(value);
|
|
@@ -344,11 +774,3 @@ function validateConnectionId(value: number): void {
|
|
|
344
774
|
throw new DestinationTunnelProtocolError('connId must be an unsigned 32-bit integer');
|
|
345
775
|
}
|
|
346
776
|
}
|
|
347
|
-
|
|
348
|
-
function readTCP(record: Record<string, unknown>): 'tcp' {
|
|
349
|
-
const value = readString(record, 'proto');
|
|
350
|
-
if (value !== 'tcp') {
|
|
351
|
-
throw new DestinationTunnelProtocolError(`unsupported tunnel transport ${value}`);
|
|
352
|
-
}
|
|
353
|
-
return value;
|
|
354
|
-
}
|