Skip to content
Source
Sign inSign up
Open History to see the last commit

routes.test.ts

470 lines17.1 KB

1import { EnvironmentId } from "@t3tools/contracts";
2import { describe, expect, it } from "@effect/vitest";
3import * as Option from "effect/Option";
4​
5import {
6 BearerConnectionProfile,
7 type ConnectionCatalogEntry,
8 type ConnectionRoute,
9} from "./catalog.ts";
10import { pullRequestRoutingConnectionKey } from "./pullRequestRoutingPermissions.ts";
11import { BearerConnectionTarget, RelayConnectionTarget } from "./model.ts";
12import {
13 connectionRouteId,
14 connectionRouteKind,
15 connectionRouteLabel,
16 credentialConnectionId,
17 insertRoute,
18 entryWithRoutes,
19 isNearbyAddress,
20 mergeLearnedRoutes,
21 pendingRouteOrder,
22 removedWithRelay,
23 routeKeepsItsHost,
24 routesAfterRemoving,
25 sendsTokenInClear,
26 upsertRoute,
27 usableRoutes,
28} from "./routes.ts";
29​
30const ENVIRONMENT_ID = EnvironmentId.make("environment-1");
31​
32function direct(id: string, httpBaseUrl: string): ConnectionRoute {
33 return {
34 target: new BearerConnectionTarget({
35 environmentId: ENVIRONMENT_ID,
36 label: "Desk",
37 connectionId: id,
38 }),
39 profile: Option.some(
40 new BearerConnectionProfile({
41 connectionId: id,
42 environmentId: ENVIRONMENT_ID,
43 label: "Desk",
44 httpBaseUrl,
45 wsBaseUrl: httpBaseUrl.replace(/^http/, "ws"),
46 }),
47 ),
48 };
50​
51const RELAY: ConnectionRoute = {
52 target: new RelayConnectionTarget({ environmentId: ENVIRONMENT_ID, label: "Desk" }),
53 profile: Option.none(),
54};
55const LAN = direct("lan", "http://192.168.1.10:3773/");
56const TAILNET = direct("tailnet", "https://desk.tail1234.ts.net/");
57const PUBLIC = direct("public", "https://desk.example.com/");
58​
59function learned(id: string, httpBaseUrl: string): ConnectionRoute {
60 const route = direct(id, httpBaseUrl);
61 const profile = Option.getOrThrow(route.profile) as BearerConnectionProfile;
62 return {
63 target: route.target,
64 profile: Option.some(new BearerConnectionProfile({ ...profile, learned: true })),
65 };
67​
68describe("connection routes", () => {
69 it("classifies direct routes by address", () => {
70 expect(connectionRouteKind(LAN)).toBe("lan");
71 // Tailscale, Cloudflare WARP and Mesh, and other VPNs share 100.64.0.0/10.
72 expect(connectionRouteKind(direct("ip", "http://100.101.102.103:3773/"))).toBe("vpn");
73 expect(connectionRouteLabel(direct("mesh", "http://100.96.0.1:3773/"))).toBe("VPN");
74 expect(connectionRouteKind(TAILNET)).toBe("tailnet");
75 expect(connectionRouteKind(PUBLIC)).toBe("public");
76 expect(connectionRouteKind(direct("lo", "http://127.0.0.1:3773/"))).toBe("loopback");
77 expect(connectionRouteKind(direct("ts6", "http://[fd7a:115c:a1e0::1]:3773/"))).toBe("tailnet");
78 expect(connectionRouteLabel(TAILNET)).toBe("Tailscale");
79 expect(connectionRouteLabel(RELAY)).toBe("T3 Connect");
80 });
81​
82 it("knows which addresses reach the same machine on every network", () => {
83 const tailnetIp = direct("ts-ip", "http://100.101.102.103:3773/");
84 const profile = Option.getOrThrow(tailnetIp.profile) as BearerConnectionProfile;
85 const marked = {
86 target: tailnetIp.target,
87 profile: Option.some(new BearerConnectionProfile({ ...profile, network: "tailscale" })),
88 };
89 expect(routeKeepsItsHost(PUBLIC)).toBe(true);
90 expect(routeKeepsItsHost(direct("ts-tls", "https://desk.tail1234.ts.net/"))).toBe(true);
91 expect(routeKeepsItsHost(direct("lo", "http://127.0.0.1:3773/"))).toBe(true);
92 // With Tailscale down, a plain http tailnet address can reach another host:
93 // carrier networks use 100.64.0.0/10 too, and nothing proves who answers.
94 expect(routeKeepsItsHost(direct("ts", "http://desk.tail1234.ts.net:3773/"))).toBe(false);
95 expect(routeKeepsItsHost(marked)).toBe(false);
96 expect(routeKeepsItsHost(tailnetIp)).toBe(false);
97 expect(routeKeepsItsHost(LAN)).toBe(false);
98 expect(routeKeepsItsHost(direct("http", "http://desk.example.com/"))).toBe(false);
99 expect(routeKeepsItsHost(RELAY)).toBe(false);
100 });
101​
102 it("treats only loopback and LAN addresses as nearby", () => {
103 expect(isNearbyAddress("http://127.0.0.1:3773/")).toBe(true);
104 expect(isNearbyAddress("http://192.168.1.10:3773/")).toBe(true);
105 expect(isNearbyAddress("http://100.101.102.103:3773/")).toBe(false);
106 expect(isNearbyAddress("https://desk.tail1234.ts.net/")).toBe(false);
107 expect(isNearbyAddress("https://desk.example.com/")).toBe(false);
108 expect(isNearbyAddress("not a url")).toBe(false);
109 });
110​
111 it("places a new route after faster kinds and ahead of T3 Connect", () => {
112 expect(insertRoute([RELAY], LAN)).toEqual([LAN, RELAY]);
113 expect(insertRoute([LAN, RELAY], TAILNET)).toEqual([LAN, TAILNET, RELAY]);
114 expect(insertRoute([TAILNET], LAN)).toEqual([LAN, TAILNET]);
115 expect(insertRoute([LAN, TAILNET], RELAY)).toEqual([LAN, TAILNET, RELAY]);
116 });
117​
118 it("keeps a user's order when a saved route is replaced", () => {
119 const repaired = direct("lan", "http://192.168.1.11:3773/");
120 expect(upsertRoute([RELAY, LAN], repaired)).toEqual([RELAY, repaired]);
121 });
122​
123 it("puts a newly paired route ahead of saved routes of the same kind", () => {
124 const pasted = direct("lan-2", "http://192.168.1.11:3773/");
125 expect(upsertRoute([LAN, TAILNET], pasted)).toEqual([pasted, LAN, TAILNET]);
126 });
127​
128 it("keeps a newly paired route behind a different kind of the same rank", () => {
129 const vpn = direct("vpn", "http://100.101.102.103:3773/");
130 const pasted = direct("ts-2", "http://[fd7a:115c:a1e0::2]:3773/");
131 expect(upsertRoute([vpn, TAILNET], pasted)).toEqual([vpn, pasted, TAILNET]);
132 });
133​
134 it("shows a dragged order only until the saved routes catch up or change", () => {
135 const dragged = ["tailnet", "lan"];
136 expect(pendingRouteOrder(dragged, ["lan", "tailnet"])).toBe(dragged);
137 expect(pendingRouteOrder(dragged, ["tailnet", "lan"])).toBeNull();
138 expect(pendingRouteOrder(dragged, ["lan", "public"])).toBeNull();
139 expect(pendingRouteOrder(dragged, ["lan", "tailnet", "public"])).toBeNull();
140 });
141});
142​
143describe("learned routes", () => {
144 const relayOnly: ConnectionCatalogEntry = {
145 target: RELAY.target,
146 profile: RELAY.profile,
147 enabled: true,
148 };
149 const ids = (routes: ReadonlyArray<ConnectionRoute> | null) =>
150 routes?.map((route) => connectionRouteId(route.target)) ?? null;
151 const profileOf = (route: ConnectionRoute) => Option.getOrThrow(route.profile);
152​
153 it("learns a LAN address over T3 Connect, ahead of it, using the T3 Connect credential", () => {
154 const routes = mergeLearnedRoutes({
155 entry: relayOnly,
156 activeRoute: RELAY,
157 reported: [{ httpBaseUrl: "http://192.168.1.10:3773/" }],
158 allowInsecure: true,
159 });
160 expect(ids(routes)).toEqual([`learned:${ENVIRONMENT_ID}:http://192.168.1.10:3773`, "relay"]);
161 expect(profileOf(routes![0]!)).toMatchObject({
162 learned: true,
163 authorization: "t3-connect",
164 wsBaseUrl: "ws://192.168.1.10:3773/",
165 });
166 });
167​
168 it("replaces a learned LAN address when the server reports a new one", () => {
169 const first = mergeLearnedRoutes({
170 entry: relayOnly,
171 activeRoute: RELAY,
172 reported: [{ httpBaseUrl: "http://192.168.1.10:3773/" }],
173 allowInsecure: true,
174 })!;
175 const entry: ConnectionCatalogEntry = {
176 ...relayOnly,
177 target: first[0]!.target,
178 profile: first[0]!.profile,
179 alternateRoutes: first.slice(1),
180 };
181 const moved = mergeLearnedRoutes({
182 entry,
183 activeRoute: RELAY,
184 reported: [{ httpBaseUrl: "http://10.0.0.42:3773/" }],
185 allowInsecure: true,
186 });
187 expect(ids(moved)).toEqual([`learned:${ENVIRONMENT_ID}:http://10.0.0.42:3773`, "relay"]);
188 });
189​
190 it("keeps a learned route where the user moved it while the server reports it", () => {
191 const lan = { httpBaseUrl: "http://192.168.1.10:3773/" };
192 const first = mergeLearnedRoutes({
193 entry: relayOnly,
194 activeRoute: RELAY,
195 reported: [lan],
196 allowInsecure: true,
197 })!;
198 const reordered = entryWithRoutes(relayOnly, [first[1]!, first[0]!]);
199 expect(
200 mergeLearnedRoutes({
201 entry: reordered,
202 activeRoute: RELAY,
203 reported: [lan],
204 allowInsecure: true,
205 }),
206 ).toBeNull();
207 const next = mergeLearnedRoutes({
208 entry: reordered,
209 activeRoute: RELAY,
210 reported: [lan, { httpBaseUrl: "http://100.101.102.103:3773/" }],
211 allowInsecure: true,
212 });
213 expect(ids(next)).toEqual([
214 `learned:${ENVIRONMENT_ID}:http://100.101.102.103:3773`,
215 "relay",
216 `learned:${ENVIRONMENT_ID}:http://192.168.1.10:3773`,
217 ]);
218 });
219​
220 it("labels a learned address Tailscale only while the server reports it as tailnet", () => {
221 const tailscale = { kind: "tailnet", httpBaseUrl: "http://100.101.102.103:3773/" };
222 const mesh = { kind: "lan", httpBaseUrl: "http://100.96.0.1:3773/" };
223 const first = mergeLearnedRoutes({
224 entry: relayOnly,
225 activeRoute: RELAY,
226 reported: [tailscale, mesh],
227 allowInsecure: true,
228 })!;
229 expect(first.map(connectionRouteLabel)).toEqual(["Tailscale", "VPN", "T3 Connect"]);
230​
231 const corrected = mergeLearnedRoutes({
232 entry: entryWithRoutes(relayOnly, first),
233 activeRoute: RELAY,
234 reported: [{ ...tailscale, kind: "lan" }, mesh],
235 allowInsecure: true,
236 })!;
237 expect(ids(corrected)).toEqual(ids(first));
238 expect(corrected.map(connectionRouteLabel)).toEqual(["VPN", "VPN", "T3 Connect"]);
239 expect(
240 mergeLearnedRoutes({
241 entry: entryWithRoutes(relayOnly, corrected),
242 activeRoute: RELAY,
243 reported: [{ ...tailscale, kind: "lan" }, mesh],
244 allowInsecure: true,
245 }),
246 ).toBeNull();
247 });
248​
249 it("leaves user routes alone and does not learn an address already saved", () => {
250 const entry: ConnectionCatalogEntry = {
251 target: LAN.target,
252 profile: LAN.profile,
253 alternateRoutes: [RELAY],
254 enabled: true,
255 };
256 expect(
257 mergeLearnedRoutes({
258 entry,
259 activeRoute: RELAY,
260 reported: [{ httpBaseUrl: "http://192.168.1.10:3773" }],
261 allowInsecure: true,
262 }),
263 ).toBeNull();
264 expect(
265 mergeLearnedRoutes({ entry, activeRoute: RELAY, reported: [], allowInsecure: true }),
266 ).toBeNull();
267 });
268​
269 it("borrows the paired token when learned over a bearer route", () => {
270 const entry: ConnectionCatalogEntry = {
271 ...relayOnly,
272 target: LAN.target,
273 profile: LAN.profile,
274 };
275 const routes = mergeLearnedRoutes({
276 entry,
277 activeRoute: LAN,
278 reported: [{ httpBaseUrl: "https://desk.tail1234.ts.net/" }],
279 allowInsecure: true,
280 })!;
281 const learned = routes.find((route) => connectionRouteKind(route) === "tailnet")!;
282 expect(profileOf(learned)).not.toHaveProperty("authorization");
283 expect(credentialConnectionId(connectionRouteId(learned.target))).toBe("lan");
284 });
285​
286 it("keeps a token paired over HTTPS off plain HTTP addresses outside the tailnet", () => {
287 const entry = entryWithRoutes(relayOnly, [PUBLIC, RELAY]);
288 const routes = mergeLearnedRoutes({
289 entry,
290 activeRoute: PUBLIC,
291 reported: [
292 { kind: "lan", httpBaseUrl: "http://192.168.1.10:3773/" },
293 { kind: "tailnet", httpBaseUrl: "http://100.101.102.103:3773/" },
294 { kind: "tailnet", httpBaseUrl: "https://desk.tail1234.ts.net/" },
295 ],
296 allowInsecure: true,
297 });
298 expect(ids(routes)).toEqual([
299 `learned:${ENVIRONMENT_ID}:http://100.101.102.103:3773@public`,
300 `learned:${ENVIRONMENT_ID}:https://desk.tail1234.ts.net@public`,
301 "public",
302 "relay",
303 ]);
304 });
305​
306 it("drops a stale plain HTTP route borrowing an HTTPS-paired token while another route is active", () => {
307 const lanOrigin = "http://10.0.0.42:3773";
308 const stale = learned(`learned:${ENVIRONMENT_ID}:${lanOrigin}@public`, `${lanOrigin}/`);
309 const entry = entryWithRoutes(relayOnly, [stale, PUBLIC, RELAY]);
310 expect(sendsTokenInClear([stale, PUBLIC, RELAY], stale)).toBe(true);
311 expect(ids(usableRoutes(entry))).toEqual(["public", "relay"]);
312 const routes = mergeLearnedRoutes({
313 entry,
314 activeRoute: RELAY,
315 reported: [{ kind: "lan", httpBaseUrl: `${lanOrigin}/` }],
316 allowInsecure: true,
317 });
318 // The stale route is gone; the address is learned again with T3 Connect's credential.
319 expect(ids(routes)).toEqual([`learned:${ENVIRONMENT_ID}:${lanOrigin}`, "public", "relay"]);
320 expect(profileOf(routes![0]!)).toMatchObject({ authorization: "t3-connect" });
321 });
322​
323 it("keeps plain HTTP routes that don't borrow the active HTTPS-paired token", () => {
324 const lanOrigin = "http://10.0.0.42:3773";
325 const overLan = learned(`learned:${ENVIRONMENT_ID}:${lanOrigin}@lan`, `${lanOrigin}/`);
326 const routes = mergeLearnedRoutes({
327 entry: entryWithRoutes(relayOnly, [overLan, LAN, PUBLIC, RELAY]),
328 activeRoute: PUBLIC,
329 reported: [{ kind: "lan", httpBaseUrl: `${lanOrigin}/` }],
330 allowInsecure: true,
331 });
332 expect(routes).toBeNull();
333 });
334​
335 it("learns plain HTTP LAN addresses over a token paired over plain HTTP", () => {
336 const entry = entryWithRoutes(relayOnly, [LAN, RELAY]);
337 const routes = mergeLearnedRoutes({
338 entry,
339 activeRoute: LAN,
340 reported: [{ kind: "lan", httpBaseUrl: "http://10.0.0.42:3773/" }],
341 allowInsecure: true,
342 });
343 expect(ids(routes)).toEqual([
344 "lan",
345 `learned:${ENVIRONMENT_ID}:http://10.0.0.42:3773@lan`,
346 "relay",
347 ]);
348 });
349​
350 it("skips plain HTTP from an HTTPS page and never learns loopback", () => {
351 expect(
352 mergeLearnedRoutes({
353 entry: relayOnly,
354 activeRoute: RELAY,
355 reported: [
356 { httpBaseUrl: "http://192.168.1.10:3773/" },
357 { httpBaseUrl: "http://127.0.0.1:3773/" },
358 ],
359 allowInsecure: false,
360 }),
361 ).toBeNull();
362 });
363​
364 it("removes learned routes along with the route whose credential they borrow", () => {
365 const overRelay = mergeLearnedRoutes({
366 entry: relayOnly,
367 activeRoute: RELAY,
368 reported: [{ httpBaseUrl: "http://192.168.1.10:3773/" }],
369 allowInsecure: true,
370 })!;
371 expect(routesAfterRemoving(overRelay, "relay")).toEqual([]);
372​
373 const paired: ConnectionCatalogEntry = {
374 ...relayOnly,
375 target: LAN.target,
376 profile: LAN.profile,
377 alternateRoutes: [RELAY],
378 };
379 const overLan = mergeLearnedRoutes({
380 entry: paired,
381 activeRoute: LAN,
382 reported: [{ httpBaseUrl: "https://desk.tail1234.ts.net/" }],
383 allowInsecure: true,
384 })!;
385 expect(ids(routesAfterRemoving(overLan, "lan"))).toEqual(["relay"]);
386 expect(ids(routesAfterRemoving(overLan, "relay"))).toHaveLength(2);
387 });
388​
389 it("counts an environment reached only through T3 Connect as removed with it", () => {
390 const learned = mergeLearnedRoutes({
391 entry: relayOnly,
392 activeRoute: RELAY,
393 reported: [{ httpBaseUrl: "http://192.168.1.10:3773/" }],
394 allowInsecure: true,
395 })!;
396 expect(removedWithRelay(relayOnly)).toBe(true);
397 expect(removedWithRelay(entryWithRoutes(relayOnly, learned))).toBe(true);
398 expect(removedWithRelay(entryWithRoutes(relayOnly, [LAN, RELAY]))).toBe(false);
399 });
400​
401 it("keeps pull request routing trust when a route is learned", () => {
402 const learned = mergeLearnedRoutes({
403 entry: relayOnly,
404 activeRoute: RELAY,
405 reported: [{ httpBaseUrl: "http://192.168.1.10:3773/" }],
406 allowInsecure: true,
407 })!;
408 const withLearned: ConnectionCatalogEntry = {
409 ...relayOnly,
410 target: learned[0]!.target,
411 profile: learned[0]!.profile,
412 alternateRoutes: learned.slice(1),
413 };
414 expect(pullRequestRoutingConnectionKey(withLearned)).toBe(
415 pullRequestRoutingConnectionKey(relayOnly),
416 );
417 });
418​
419 it("keeps the T3 Connect credential when learning over a learned T3 Connect route", () => {
420 const first = mergeLearnedRoutes({
421 entry: relayOnly,
422 activeRoute: RELAY,
423 reported: [{ httpBaseUrl: "http://192.168.1.10:3773/" }],
424 allowInsecure: true,
425 })!;
426 const learnedLan = first[0]!;
427 const entry: ConnectionCatalogEntry = {
428 ...relayOnly,
429 target: learnedLan.target,
430 profile: learnedLan.profile,
431 alternateRoutes: first.slice(1),
432 };
433 const next = mergeLearnedRoutes({
434 entry,
435 activeRoute: learnedLan,
436 reported: [
437 { httpBaseUrl: "http://192.168.1.10:3773/" },
438 { httpBaseUrl: "https://desk.tail1234.ts.net/" },
439 ],
440 allowInsecure: true,
441 })!;
442 for (const route of next.filter((candidate) => connectionRouteKind(candidate) !== "relay")) {
443 expect(profileOf(route)).toMatchObject({ authorization: "t3-connect" });
444 expect(connectionRouteId(route.target)).not.toContain("@");
445 }
446 });
447​
448 it("saves a scheme change on the same host as a new address", () => {
449 const first = mergeLearnedRoutes({
450 entry: relayOnly,
451 activeRoute: RELAY,
452 reported: [{ httpBaseUrl: "http://desk.local:3773/" }],
453 allowInsecure: true,
454 })!;
455 const entry: ConnectionCatalogEntry = {
456 ...relayOnly,
457 target: first[0]!.target,
458 profile: first[0]!.profile,
459 alternateRoutes: first.slice(1),
460 };
461 const moved = mergeLearnedRoutes({
462 entry,
463 activeRoute: RELAY,
464 reported: [{ httpBaseUrl: "https://desk.local:3773/" }],
465 allowInsecure: true,
466 });
467 expect(moved).not.toBeNull();
468 expect(profileOf(moved![0]!)).toMatchObject({ httpBaseUrl: "https://desk.local:3773/" });
469 });
470});