@expo/build-tools 24.2.0 → 24.4.0

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.
Files changed (29) hide show
  1. package/dist/common/projectSources.js +3 -0
  2. package/dist/steps/easFunctions.js +2 -0
  3. package/dist/steps/functions/startIosSimulator.js +34 -2
  4. package/dist/steps/functions/startLocalEgress.d.ts +4 -3
  5. package/dist/steps/functions/startLocalEgress.js +10 -6
  6. package/dist/steps/functions/startSandbox.d.ts +4 -0
  7. package/dist/steps/functions/startSandbox.js +93 -0
  8. package/dist/steps/utils/localEgress.d.ts +24 -0
  9. package/dist/steps/utils/localEgress.js +59 -1
  10. package/dist/steps/utils/localEgressGuard.d.ts +179 -0
  11. package/dist/steps/utils/localEgressGuard.js +537 -0
  12. package/dist/steps/utils/localEgressSession.js +4 -0
  13. package/dist/steps/utils/sandboxDaemon.d.ts +13 -0
  14. package/dist/steps/utils/sandboxDaemon.js +90 -0
  15. package/dist/utils/IosSimulatorUtils.d.ts +34 -0
  16. package/dist/utils/IosSimulatorUtils.js +62 -0
  17. package/package.json +8 -4
  18. package/resources/egress-guard/README.md +83 -0
  19. package/resources/egress-guard/build.sh +30 -0
  20. package/resources/egress-guard/check.c +112 -0
  21. package/resources/egress-guard/guard.c +274 -0
  22. package/resources/egress-guard/policy.c +164 -0
  23. package/resources/egress-guard/policy.h +60 -0
  24. package/resources/egress-guard/tests/guard_insert_test.c +291 -0
  25. package/resources/egress-guard/tests/guard_test.c +177 -0
  26. package/resources/egress-guard/tests/nettest.swift +172 -0
  27. package/resources/egress-guard/tests/policy_test.c +143 -0
  28. package/resources/egress-guard/tests/run-guard-tests.sh +62 -0
  29. package/resources/egress-guard/tests/run-policy-tests.sh +7 -0
@@ -19,6 +19,9 @@ const zod_1 = require("zod");
19
19
  const git_1 = require("./git");
20
20
  const turtleFetch_1 = require("../utils/turtleFetch");
21
21
  async function prepareProjectSourcesAsync(ctx, destinationDirectory) {
22
+ if (ctx.job.projectArchive.type === eas_build_job_1.ArchiveSourceType.NONE) {
23
+ return { handled: true };
24
+ }
22
25
  if (ctx.isLocal) {
23
26
  if (ctx.job.projectArchive.type !== eas_build_job_1.ArchiveSourceType.PATH) {
24
27
  throw new eas_build_job_1.SystemError(`Expected a PATH project source for a local build, received ${ctx.job.projectArchive.type}.`, { trackingCode: 'INVALID_LOCAL_PROJECT_SOURCE' });
@@ -52,6 +52,7 @@ const startIosSimulatorRecordings_1 = require("./functions/startIosSimulatorReco
52
52
  const startLocalEgress_1 = require("./functions/startLocalEgress");
53
53
  const startWebPreviewRemoteSession_1 = require("./functions/startWebPreviewRemoteSession");
54
54
  const startServeSimMetrics_1 = require("./functions/startServeSimMetrics");
55
+ const startSandbox_1 = require("./functions/startSandbox");
55
56
  const collectServeSimMetrics_1 = require("./functions/collectServeSimMetrics");
56
57
  const uploadArtifact_1 = require("./functions/uploadArtifact");
57
58
  const uploadDeviceRunSessionScreenRecordings_1 = require("./functions/uploadDeviceRunSessionScreenRecordings");
@@ -106,6 +107,7 @@ function getEasFunctions(ctx) {
106
107
  (0, uploadDeviceRunSessionScreenRecordings_1.createUploadDeviceRunSessionScreenRecordingsBuildFunction)(ctx),
107
108
  (0, startWebPreviewRemoteSession_1.createStartWebPreviewRemoteSessionBuildFunction)(ctx),
108
109
  (0, startServeSimMetrics_1.createStartServeSimMetricsBuildFunction)(),
110
+ (0, startSandbox_1.createStartSandboxBuildFunction)(ctx),
109
111
  (0, collectServeSimMetrics_1.createCollectServeSimMetricsBuildFunction)(ctx),
110
112
  (0, installMaestro_1.createInstallMaestroBuildFunction)(),
111
113
  (0, installPods_1.createInstallPodsBuildFunction)(),
@@ -7,6 +7,8 @@ exports.createStartIosSimulatorBuildFunction = createStartIosSimulatorBuildFunct
7
7
  const steps_1 = require("@expo/steps");
8
8
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
9
  const lodash_1 = require("lodash");
10
+ const localEgress_1 = require("../utils/localEgress");
11
+ const localEgressGuard_1 = require("../utils/localEgressGuard");
10
12
  const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
11
13
  function createStartIosSimulatorBuildFunction() {
12
14
  return new steps_1.BuildFunction({
@@ -61,9 +63,10 @@ function createStartIosSimulatorBuildFunction() {
61
63
  env,
62
64
  });
63
65
  }
64
- const { udid } = await IosSimulatorUtils_1.IosSimulatorUtils.startAsync({
66
+ const udid = await bootWithLocalEgressAsync({
65
67
  deviceIdentifier: originalDeviceIdentifier,
66
68
  env,
69
+ logger,
67
70
  });
68
71
  try {
69
72
  await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid, env });
@@ -97,9 +100,10 @@ function createStartIosSimulatorBuildFunction() {
97
100
  env,
98
101
  });
99
102
  }
100
- const { udid: cloneUdid } = await IosSimulatorUtils_1.IosSimulatorUtils.startAsync({
103
+ const cloneUdid = await bootWithLocalEgressAsync({
101
104
  deviceIdentifier: cloneDeviceName,
102
105
  env,
106
+ logger,
103
107
  });
104
108
  try {
105
109
  await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid: cloneUdid, env });
@@ -118,6 +122,34 @@ function createStartIosSimulatorBuildFunction() {
118
122
  },
119
123
  });
120
124
  }
125
+ /**
126
+ * Boot a device with the local egress environment in place before anything
127
+ * inside it starts. `simctl boot` returns once the simulator's launchd is up
128
+ * and before it has spawned anything else, which is the only moment at which
129
+ * launchd environment reaches every process of the boot. The proxy variables
130
+ * and the guard are set in that gap; then the boot is waited for and the
131
+ * guard is verified in a fresh process. Nothing here applies when no local
132
+ * egress session is active.
133
+ */
134
+ async function bootWithLocalEgressAsync({ deviceIdentifier, env, logger, }) {
135
+ const udid = await IosSimulatorUtils_1.IosSimulatorUtils.resolveUdidAsync({ deviceIdentifier, env });
136
+ // The guard and proxy variables ride the boot itself, so launchd has them
137
+ // before it spawns its first process.
138
+ const launchdEnvironment = await (0, localEgressGuard_1.resolveLocalEgressBootEnvironmentAsync)();
139
+ await IosSimulatorUtils_1.IosSimulatorUtils.bootAsync({
140
+ deviceIdentifier: udid,
141
+ env,
142
+ launchdEnvironment: launchdEnvironment ?? {},
143
+ });
144
+ // Also set them through launchctl, for a device that was already booted.
145
+ const guardInstalled = await (0, localEgressGuard_1.installLocalEgressGuardAsync)({ udid, env, logger });
146
+ await (0, localEgress_1.configureSimulatorProxyEnvironmentAsync)({ udid, env, logger });
147
+ await IosSimulatorUtils_1.IosSimulatorUtils.startAsync({ deviceIdentifier: udid, env });
148
+ if (guardInstalled) {
149
+ await (0, localEgressGuard_1.verifyLocalEgressGuardAsync)({ udid, env, logger });
150
+ }
151
+ return udid;
152
+ }
121
153
  async function findMostGenericIphoneUuidAsync({ env, }) {
122
154
  const availableSimulatorDevices = await IosSimulatorUtils_1.IosSimulatorUtils.getAvailableDevicesAsync({
123
155
  env,
@@ -2,9 +2,10 @@ import { BuildFunction } from '@expo/steps';
2
2
  /**
3
3
  * Points the device host's system HTTP(S) proxy at the EAS CLI's egress client.
4
4
  * Must run before `eas/start_ios_simulator`: the simulator reads the system proxy
5
- * at boot. Nothing else on the host changes, so requests from libraries that
6
- * bypass the system proxy are not covered; the shared session monitor reports
7
- * them. The shared session cleanup releases the resources started here when
5
+ * at boot. Once a simulator boots, `eas/start_ios_simulator` also sets proxy
6
+ * environment variables inside it for clients that read them (gRPC, libcurl)
7
+ * and installs the local egress guard, which refuses connections that ignore
8
+ * both. The shared session cleanup releases the resources started here when
8
9
  * the session ends, with a job finalizer as a fallback.
9
10
  */
10
11
  export declare function createStartLocalEgressBuildFunction(): BuildFunction;
@@ -9,6 +9,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_os_1 = __importDefault(require("node:os"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
11
  const localEgress_1 = require("../utils/localEgress");
12
+ const localEgressGuard_1 = require("../utils/localEgressGuard");
12
13
  const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
13
14
  /**
14
15
  * The downloader and ngrok SDK do not accept an AbortSignal. Stop awaiting them
@@ -50,9 +51,10 @@ function awaitLocalEgressAcquisitionAsync(pending, signal, disposeAbandonedAsync
50
51
  /**
51
52
  * Points the device host's system HTTP(S) proxy at the EAS CLI's egress client.
52
53
  * Must run before `eas/start_ios_simulator`: the simulator reads the system proxy
53
- * at boot. Nothing else on the host changes, so requests from libraries that
54
- * bypass the system proxy are not covered; the shared session monitor reports
55
- * them. The shared session cleanup releases the resources started here when
54
+ * at boot. Once a simulator boots, `eas/start_ios_simulator` also sets proxy
55
+ * environment variables inside it for clients that read them (gRPC, libcurl)
56
+ * and installs the local egress guard, which refuses connections that ignore
57
+ * both. The shared session cleanup releases the resources started here when
56
58
  * the session ends, with a job finalizer as a fallback.
57
59
  */
58
60
  function createStartLocalEgressBuildFunction() {
@@ -77,6 +79,7 @@ function createStartLocalEgressBuildFunction() {
77
79
  const lifetimeSignal = (0, localEgress_1.registerLocalEgressResources)(async () => {
78
80
  await setupFinished;
79
81
  const results = await Promise.allSettled([
82
+ Promise.resolve().then(() => (0, localEgressGuard_1.stopLocalEgressGuardRelaysAsync)(logger)),
80
83
  Promise.resolve().then(() => tunnel?.stopAsync()),
81
84
  Promise.resolve().then(() => server?.stopAsync()),
82
85
  ]);
@@ -139,9 +142,10 @@ function createStartLocalEgressBuildFunction() {
139
142
  startupSignal.throwIfAborted();
140
143
  logger.info(`Local egress is configured on network service "${service}". HTTP(S) and WebSocket ` +
141
144
  'requests that honor the system proxy (WebKit, URLSession and other CFNetwork clients) ' +
142
- 'fail until the EAS CLI egress client connects, then exit from that machine. Requests ' +
143
- 'from libraries that bypass the system proxy are not covered and exit from this worker. ' +
144
- 'Connection sampling may report these bypasses, but can miss short connections and unconnected UDP traffic.');
145
+ 'fail until the EAS CLI egress client connects, then exit from that machine. Once the ' +
146
+ 'Simulator boots, proxy environment variables are set inside it for clients that read ' +
147
+ 'them (gRPC, libcurl), and the local egress guard is installed so that connections which ' +
148
+ 'ignore both are refused in the process that makes them and reported here.');
145
149
  }
146
150
  catch (error) {
147
151
  finishSetup();
@@ -0,0 +1,4 @@
1
+ import { BuildFunction } from '@expo/steps';
2
+ import { CustomBuildContext } from '../../customBuildContext';
3
+ export declare function createStartSandboxBuildFunction(ctx: CustomBuildContext): BuildFunction;
4
+ export declare function markSandboxReadyAsync(ctx: CustomBuildContext, sandboxId: string, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createStartSandboxBuildFunction = createStartSandboxBuildFunction;
4
+ exports.markSandboxReadyAsync = markSandboxReadyAsync;
5
+ const eas_build_job_1 = require("@expo/eas-build-job");
6
+ const steps_1 = require("@expo/steps");
7
+ const gql_tada_1 = require("gql.tada");
8
+ const logPhase_1 = require("../../utils/logPhase");
9
+ const sandboxDaemon_1 = require("../utils/sandboxDaemon");
10
+ const MARK_SANDBOX_READY_MUTATION = (0, gql_tada_1.graphql)(`
11
+ mutation MarkSandboxReady($sandboxId: ID!) {
12
+ sandbox {
13
+ markSandboxReady(sandboxId: $sandboxId) {
14
+ id
15
+ }
16
+ }
17
+ }
18
+ `);
19
+ const RECONNECT_DELAY_MS = 1_000;
20
+ const AWAIT_SANDBOX_STEP_NAME = 'Await sandbox completion';
21
+ function createStartSandboxBuildFunction(ctx) {
22
+ return new steps_1.BuildFunction({
23
+ namespace: 'eas',
24
+ id: 'start_sandbox',
25
+ name: 'Start sandbox daemon',
26
+ __metricsId: 'eas/start_sandbox',
27
+ inputProviders: [
28
+ steps_1.BuildStepInput.createProvider({
29
+ id: 'sandbox_id',
30
+ required: true,
31
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
32
+ }),
33
+ ],
34
+ fn: async (stepCtx, { inputs, signal }) => {
35
+ const sandboxToken = ctx.env.__EAS_SANDBOX_MCP_TOKEN;
36
+ if (!sandboxToken) {
37
+ throw new eas_build_job_1.SystemError('__EAS_SANDBOX_MCP_TOKEN is required to start the sandbox daemon.');
38
+ }
39
+ const mcpServerUrl = ctx.mcpServerUrl;
40
+ if (!mcpServerUrl) {
41
+ throw new eas_build_job_1.SystemError('MCP server URL is required to start the sandbox daemon.');
42
+ }
43
+ const sandboxId = String(inputs.sandbox_id.value);
44
+ const daemon = await (0, sandboxDaemon_1.startSandboxDaemonAsync)({
45
+ credential: sandboxToken,
46
+ serverUrl: mcpServerUrl,
47
+ reconnectDelayMs: RECONNECT_DELAY_MS,
48
+ logger: stepCtx.logger,
49
+ signal,
50
+ });
51
+ try {
52
+ await daemon.ready;
53
+ signal?.throwIfAborted();
54
+ await markSandboxReadyAsync(ctx, sandboxId, signal);
55
+ signal?.throwIfAborted();
56
+ stepCtx.logger.info('Sandbox daemon started.');
57
+ await (0, logPhase_1.withLogPhaseAsync)(stepCtx.logger, AWAIT_SANDBOX_STEP_NAME, async () => {
58
+ await waitForCancellationAsync(signal);
59
+ });
60
+ }
61
+ finally {
62
+ await daemon.stopAsync();
63
+ }
64
+ },
65
+ });
66
+ }
67
+ async function markSandboxReadyAsync(ctx, sandboxId, signal) {
68
+ signal?.throwIfAborted();
69
+ const result = await ctx.graphqlClient
70
+ .mutation(MARK_SANDBOX_READY_MUTATION, { sandboxId }, signal
71
+ ? {
72
+ fetch: (input, init) => fetch(input, {
73
+ ...init,
74
+ signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal,
75
+ }),
76
+ }
77
+ : undefined)
78
+ .toPromise();
79
+ signal?.throwIfAborted();
80
+ if (result.error) {
81
+ throw new eas_build_job_1.SystemError(`Failed to mark sandbox ${sandboxId} as ready.`, {
82
+ cause: result.error,
83
+ });
84
+ }
85
+ }
86
+ async function waitForCancellationAsync(signal) {
87
+ if (signal?.aborted) {
88
+ return;
89
+ }
90
+ await new Promise(resolve => {
91
+ signal?.addEventListener('abort', () => resolve(), { once: true });
92
+ });
93
+ }
@@ -1,5 +1,6 @@
1
1
  import { bunyan } from '@expo/logger';
2
2
  import { BuildStepEnv } from '@expo/steps';
3
+ import { type IosSimulatorUuid } from '../../utils/IosSimulatorUtils';
3
4
  import { type DetachedProcessHandle } from './remoteDeviceRunSession';
4
5
  /**
5
6
  * Local egress provides an HTTP(S) proxy through the machine running the EAS CLI.
@@ -101,6 +102,29 @@ export declare function configureSystemProxyAsync({ env, logger, port, signal, }
101
102
  }): Promise<{
102
103
  service: string;
103
104
  }>;
105
+ export declare const LOCAL_EGRESS_NO_PROXY = "localhost,127.0.0.1,::1";
106
+ /**
107
+ * Proxy environment for processes inside the simulator. CFNetwork ignores these
108
+ * variables and follows the system proxy, but clients with their own network
109
+ * stack that read them (gRPC and therefore Firestore, libcurl, Go) then send
110
+ * HTTP CONNECT requests to the same loopback port. Loopback is excluded so the
111
+ * ports forwarded for `--egress-allow` behave the same on both paths. Lowercase
112
+ * and uppercase forms are both set because clients differ in which they read.
113
+ */
114
+ export declare function buildLocalEgressSimulatorEnvironment(port: number): Record<string, string>;
115
+ /**
116
+ * Set the local egress proxy environment in a booted simulator when a local
117
+ * egress session is active. Returns false when local egress is not configured
118
+ * or the environment could not be set. Failure is a warning, not an error: the
119
+ * session still works, but clients that read these variables then exit from
120
+ * this worker instead of the egress client, which the warning spells out.
121
+ */
122
+ export declare function configureSimulatorProxyEnvironmentAsync({ udid, env, logger, handoffPath, }: {
123
+ udid: IosSimulatorUuid;
124
+ env: NodeJS.ProcessEnv;
125
+ logger: bunyan;
126
+ handoffPath?: string;
127
+ }): Promise<boolean>;
104
128
  export declare function writeLocalEgressHandoffAsync(handoff: LocalEgressHandoff, handoffPath?: string): Promise<void>;
105
129
  export declare function readLocalEgressHandoffAsync(handoffPath?: string): Promise<LocalEgressHandoff | null>;
106
130
  /** remoteConfig fields the CLI needs to start the egress client. */
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CHISEL_VERSION = exports.LOCAL_EGRESS_HANDOFF_PATH = exports.LOCAL_EGRESS_USERNAME = exports.LOCAL_EGRESS_PROXY_PORT = exports.LOCAL_EGRESS_PROXY_HOST = void 0;
6
+ exports.LOCAL_EGRESS_NO_PROXY = exports.CHISEL_VERSION = exports.LOCAL_EGRESS_HANDOFF_PATH = exports.LOCAL_EGRESS_USERNAME = exports.LOCAL_EGRESS_PROXY_PORT = exports.LOCAL_EGRESS_PROXY_HOST = void 0;
7
7
  exports.getChiselAssetName = getChiselAssetName;
8
8
  exports.getChiselDownloadUrl = getChiselDownloadUrl;
9
9
  exports.downloadChiselAsync = downloadChiselAsync;
@@ -16,6 +16,8 @@ exports.parseNetworkServiceNameForDevice = parseNetworkServiceNameForDevice;
16
16
  exports.resolveActiveNetworkServiceNameAsync = resolveActiveNetworkServiceNameAsync;
17
17
  exports.buildNetworksetupProxyArgs = buildNetworksetupProxyArgs;
18
18
  exports.configureSystemProxyAsync = configureSystemProxyAsync;
19
+ exports.buildLocalEgressSimulatorEnvironment = buildLocalEgressSimulatorEnvironment;
20
+ exports.configureSimulatorProxyEnvironmentAsync = configureSimulatorProxyEnvironmentAsync;
19
21
  exports.writeLocalEgressHandoffAsync = writeLocalEgressHandoffAsync;
20
22
  exports.readLocalEgressHandoffAsync = readLocalEgressHandoffAsync;
21
23
  exports.buildEgressRemoteConfigFields = buildEgressRemoteConfigFields;
@@ -37,6 +39,7 @@ const node_os_1 = __importDefault(require("node:os"));
37
39
  const node_path_1 = __importDefault(require("node:path"));
38
40
  const promises_1 = require("node:stream/promises");
39
41
  const node_zlib_1 = __importDefault(require("node:zlib"));
42
+ const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
40
43
  const retry_1 = require("../../utils/retry");
41
44
  const remoteDeviceRunSession_1 = require("./remoteDeviceRunSession");
42
45
  /**
@@ -253,6 +256,61 @@ async function configureSystemProxyAsync({ env, logger, port, signal, }) {
253
256
  logger.info(`System proxy for "${service}" set to ${exports.LOCAL_EGRESS_PROXY_HOST}:${port}.`);
254
257
  return { service };
255
258
  }
259
+ exports.LOCAL_EGRESS_NO_PROXY = 'localhost,127.0.0.1,::1';
260
+ /**
261
+ * Proxy environment for processes inside the simulator. CFNetwork ignores these
262
+ * variables and follows the system proxy, but clients with their own network
263
+ * stack that read them (gRPC and therefore Firestore, libcurl, Go) then send
264
+ * HTTP CONNECT requests to the same loopback port. Loopback is excluded so the
265
+ * ports forwarded for `--egress-allow` behave the same on both paths. Lowercase
266
+ * and uppercase forms are both set because clients differ in which they read.
267
+ */
268
+ function buildLocalEgressSimulatorEnvironment(port) {
269
+ const proxyUrl = `http://${exports.LOCAL_EGRESS_PROXY_HOST}:${port}`;
270
+ return {
271
+ http_proxy: proxyUrl,
272
+ https_proxy: proxyUrl,
273
+ HTTP_PROXY: proxyUrl,
274
+ HTTPS_PROXY: proxyUrl,
275
+ grpc_proxy: proxyUrl,
276
+ no_proxy: exports.LOCAL_EGRESS_NO_PROXY,
277
+ NO_PROXY: exports.LOCAL_EGRESS_NO_PROXY,
278
+ };
279
+ }
280
+ /**
281
+ * Set the local egress proxy environment in a booted simulator when a local
282
+ * egress session is active. Returns false when local egress is not configured
283
+ * or the environment could not be set. Failure is a warning, not an error: the
284
+ * session still works, but clients that read these variables then exit from
285
+ * this worker instead of the egress client, which the warning spells out.
286
+ */
287
+ async function configureSimulatorProxyEnvironmentAsync({ udid, env, logger, handoffPath = exports.LOCAL_EGRESS_HANDOFF_PATH, }) {
288
+ let handoff;
289
+ try {
290
+ handoff = await readLocalEgressHandoffAsync(handoffPath);
291
+ }
292
+ catch (err) {
293
+ logger.warn({ err }, 'Local egress: could not read the local egress handoff, so proxy environment variables were ' +
294
+ 'not set in the Simulator. Clients that read http_proxy/https_proxy (for example gRPC and ' +
295
+ 'libcurl) will bypass local egress and exit from this worker.');
296
+ return false;
297
+ }
298
+ if (!handoff) {
299
+ return false;
300
+ }
301
+ const variables = buildLocalEgressSimulatorEnvironment(handoff.port);
302
+ try {
303
+ await IosSimulatorUtils_1.IosSimulatorUtils.setLaunchdEnvironmentAsync({ udid, env, variables });
304
+ }
305
+ catch (err) {
306
+ logger.warn({ err }, 'Local egress: could not set proxy environment variables in the Simulator. Clients that read ' +
307
+ 'http_proxy/https_proxy (for example gRPC and libcurl) will bypass local egress and exit from ' +
308
+ 'this worker. CFNetwork clients (WebKit, URLSession) still follow the system proxy.');
309
+ return false;
310
+ }
311
+ logger.info(`Local egress: proxy environment variables set in the Simulator (${Object.keys(variables).join(', ')}), so clients that read them, such as gRPC and libcurl, also exit from the egress client.`);
312
+ return true;
313
+ }
256
314
  async function writeLocalEgressHandoffAsync(handoff, handoffPath = exports.LOCAL_EGRESS_HANDOFF_PATH) {
257
315
  await node_fs_1.default.promises.writeFile(handoffPath, JSON.stringify(handoff), {
258
316
  encoding: 'utf8',
@@ -0,0 +1,179 @@
1
+ import { type bunyan } from '@expo/logger';
2
+ import { type IosSimulatorUuid } from '../../utils/IosSimulatorUtils';
3
+ /**
4
+ * Worker side of the local egress guard: a dylib injected into every process
5
+ * the simulator launches, which refuses connections that do not go to
6
+ * loopback (where the proxy and the `--egress-allow` forwards live) and
7
+ * records one event per destination per process. This module installs it
8
+ * through the simulator's launchd environment and relays its events into the
9
+ * session log. See resources/egress-guard/README.md.
10
+ */
11
+ export declare const EGRESS_GUARD_LIBRARY_FILE = "egress-guard.dylib";
12
+ export declare const EGRESS_GUARD_CHECK_FILE = "egress-guard-check";
13
+ export declare const EGRESS_GUARD_LOG_ENV = "EAS_EGRESS_GUARD_LOG";
14
+ export declare const EGRESS_GUARD_MODE_ENV = "EAS_EGRESS_GUARD_MODE";
15
+ export declare const LOCAL_EGRESS_GUARD_LOG_PATH: string;
16
+ export type EgressGuardMode = 'block' | 'log';
17
+ export declare function buildGuardLaunchdEnvironment({ libraryPath, logPath, mode, }: {
18
+ libraryPath: string;
19
+ logPath: string;
20
+ mode: EgressGuardMode;
21
+ }): Record<string, string>;
22
+ export type GuardEvent = {
23
+ process: string;
24
+ pid: number;
25
+ function: string;
26
+ action: 'blocked' | 'logged';
27
+ peer: string;
28
+ /** Image names above the interposer, innermost first. */
29
+ callers: string[];
30
+ };
31
+ /** One tab-separated line written by the guard; see policy.h for the format. */
32
+ export declare function parseGuardLogLine(line: string): GuardEvent | null;
33
+ /** The packaged library, next to the compiled package like record-sim. */
34
+ export declare function resolveEgressGuardLibraryAsync(binDir?: string): Promise<string | null>;
35
+ /** The packaged self-check binary, built alongside the library. */
36
+ export declare function resolveEgressGuardCheckAsync(binDir?: string): Promise<string | null>;
37
+ /**
38
+ * Turns guard events into session log lines: one line the first time a
39
+ * process reaches a destination through a given call, counts after that, and
40
+ * a summary at the end.
41
+ */
42
+ export declare class GuardEventRelay {
43
+ private readonly limit;
44
+ private readonly seen;
45
+ private readonly peers;
46
+ private readonly processes;
47
+ private blocked;
48
+ private logged;
49
+ private suppressed;
50
+ private logger;
51
+ constructor(logger: bunyan, limit?: number);
52
+ /**
53
+ * Log through a different step's logger from now on. Refusals happen for
54
+ * the life of the session, so they belong to the step that runs the
55
+ * session, not the one that booted the simulator minutes earlier.
56
+ */
57
+ setLogger(logger: bunyan): void;
58
+ handle(event: GuardEvent): void;
59
+ summary(): {
60
+ blocked: number;
61
+ logged: number;
62
+ distinct: number;
63
+ suppressed: number;
64
+ };
65
+ logSummary(): void;
66
+ }
67
+ /**
68
+ * Polls a file the simulator processes append to and delivers whole lines.
69
+ * Tolerates the file not existing yet, partial trailing lines, and truncation.
70
+ */
71
+ export declare class GuardLogTailer {
72
+ private readonly path;
73
+ private readonly onLine;
74
+ private readonly intervalMs;
75
+ private offset;
76
+ private partial;
77
+ private timer;
78
+ private reading;
79
+ constructor({ path: filePath, onLine, intervalMs, }: {
80
+ path: string;
81
+ onLine: (line: string) => void;
82
+ intervalMs?: number;
83
+ });
84
+ start(): void;
85
+ stopAsync(): Promise<void>;
86
+ private readAsync;
87
+ }
88
+ /**
89
+ * The environment the simulator's launchd must have from its first process:
90
+ * the guard and the proxy variables. Pass it to `IosSimulatorUtils.bootAsync`,
91
+ * which hands it to launchd before anything is spawned; `launchctl setenv`
92
+ * after boot only reaches later processes. Returns null when no local egress
93
+ * session is active. Throws when the guard library is not packaged, since a
94
+ * local egress session without it would silently leak.
95
+ */
96
+ export declare function resolveLocalEgressBootEnvironmentAsync({ handoffPath, libraryPath, logPath, mode, }?: {
97
+ handoffPath?: string;
98
+ /** Explicit library path, `null` for "not available"; resolved from the package when omitted. */
99
+ libraryPath?: string | null;
100
+ logPath?: string;
101
+ mode?: EgressGuardMode;
102
+ }): Promise<Record<string, string> | null>;
103
+ /**
104
+ * Install the guard into a simulator when a local egress session is active,
105
+ * and start relaying its events into the session log. Returns false when
106
+ * there is no local egress session. Throws when the library is not packaged
107
+ * or launchd could not be configured: a local egress session without the
108
+ * guard would silently leak, so it must not start. Only an unwritable event
109
+ * log is a warning, since refusals still happen and only reporting is lost.
110
+ *
111
+ * Call this as soon as `simctl boot` returns: launchd is up and nothing else
112
+ * has started, so every process the boot spawns inherits the guard. Verify
113
+ * with `verifyLocalEgressGuardAsync` once boot completes.
114
+ */
115
+ export declare function installLocalEgressGuardAsync({ udid, env, logger, handoffPath, libraryPath, logPath, mode, tailIntervalMs, }: {
116
+ udid: IosSimulatorUuid;
117
+ env: NodeJS.ProcessEnv;
118
+ logger: bunyan;
119
+ handoffPath?: string;
120
+ /** Explicit library path, `null` for "not available"; resolved from the package when omitted. */
121
+ libraryPath?: string | null;
122
+ logPath?: string;
123
+ mode?: EgressGuardMode;
124
+ tailIntervalMs?: number;
125
+ }): Promise<boolean>;
126
+ export type GuardCoverage = {
127
+ /** Simulator processes with the guard library mapped. */
128
+ covered: string[];
129
+ /** Simulator processes without it: started before the guard was installed. */
130
+ uncovered: string[];
131
+ /** Pids behind `uncovered`, for comparing samples. */
132
+ uncoveredPids: number[];
133
+ };
134
+ /**
135
+ * Which simulator processes have the guard library mapped, from
136
+ * `ps -axo pid=,ppid=,comm=` and `lsof -nP -a -p <pids> -d txt -F pn` output.
137
+ * A process is covered when any of its mapped images is the guard library.
138
+ */
139
+ export declare function parseGuardCoverage(psOutput: string, lsofOutput: string): GuardCoverage;
140
+ /**
141
+ * A process caught between fork and exec has nothing mapped yet and looks
142
+ * uncovered for an instant. Two samples a moment apart separate those from
143
+ * processes that really run without the guard: only pids uncovered in both
144
+ * count, reported with the later sample's names.
145
+ */
146
+ export declare function mergeGuardCoverageSamples(first: GuardCoverage, second: GuardCoverage): GuardCoverage;
147
+ /**
148
+ * Measure and log guard coverage across the simulator's processes. Observation
149
+ * only: the uncovered set is whatever started before the guard was installed,
150
+ * which is nothing when installation runs right after `simctl boot`.
151
+ */
152
+ export declare function reportLocalEgressGuardCoverageAsync({ env, logger, sampleIntervalMs, }: {
153
+ env: NodeJS.ProcessEnv;
154
+ logger: bunyan;
155
+ sampleIntervalMs?: number;
156
+ }): Promise<GuardCoverage | null>;
157
+ /**
158
+ * Run the packaged self-check inside the simulator: it must find the guard
159
+ * loaded in a fresh process and see it behave as `mode` says. Throws when the
160
+ * check binary is missing or the check fails, which fails the session.
161
+ */
162
+ export declare function verifyLocalEgressGuardAsync({ udid, env, logger, mode, checkPath, }: {
163
+ udid: IosSimulatorUuid;
164
+ env: NodeJS.ProcessEnv;
165
+ logger: bunyan;
166
+ mode?: EgressGuardMode;
167
+ /** Explicit check binary path, `null` for "not available"; resolved from the package when omitted. */
168
+ checkPath?: string | null;
169
+ }): Promise<void>;
170
+ /**
171
+ * Attribute guard events to the step that runs the session from now on. The
172
+ * relay starts under the simulator boot step's logger; once the session step
173
+ * takes over, its lines should appear under that step in the job log, next
174
+ * to the rest of the session's output, rather than under a step that already
175
+ * finished.
176
+ */
177
+ export declare function rebindLocalEgressGuardRelays(logger: bunyan): void;
178
+ /** Stop relaying and write each relay's summary; called from the session cleanup. */
179
+ export declare function stopLocalEgressGuardRelaysAsync(logger: bunyan): Promise<void>;