@expo/build-tools 24.3.0 → 24.5.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 (33) hide show
  1. package/dist/common/projectSources.js +3 -0
  2. package/dist/steps/functions/startIosSimulator.js +34 -2
  3. package/dist/steps/functions/startLocalEgress.d.ts +4 -3
  4. package/dist/steps/functions/startLocalEgress.js +10 -6
  5. package/dist/steps/functions/startSandbox.js +5 -2
  6. package/dist/steps/utils/localEgress.d.ts +24 -0
  7. package/dist/steps/utils/localEgress.js +59 -1
  8. package/dist/steps/utils/localEgressGuard.d.ts +179 -0
  9. package/dist/steps/utils/localEgressGuard.js +537 -0
  10. package/dist/steps/utils/localEgressSession.js +4 -0
  11. package/dist/steps/utils/sandboxCommandImplementations.d.ts +12 -0
  12. package/dist/steps/utils/sandboxCommandImplementations.js +33 -0
  13. package/dist/steps/utils/sandboxDaemon.d.ts +2 -0
  14. package/dist/steps/utils/sandboxDaemon.js +70 -3
  15. package/dist/steps/utils/shellSessionManager.d.ts +29 -0
  16. package/dist/steps/utils/shellSessionManager.js +269 -0
  17. package/dist/utils/IosSimulatorUtils.d.ts +34 -0
  18. package/dist/utils/IosSimulatorUtils.js +62 -0
  19. package/dist/utils/processes.d.ts +4 -1
  20. package/dist/utils/processes.js +7 -4
  21. package/package.json +7 -4
  22. package/resources/egress-guard/README.md +83 -0
  23. package/resources/egress-guard/build.sh +30 -0
  24. package/resources/egress-guard/check.c +112 -0
  25. package/resources/egress-guard/guard.c +274 -0
  26. package/resources/egress-guard/policy.c +164 -0
  27. package/resources/egress-guard/policy.h +60 -0
  28. package/resources/egress-guard/tests/guard_insert_test.c +291 -0
  29. package/resources/egress-guard/tests/guard_test.c +177 -0
  30. package/resources/egress-guard/tests/nettest.swift +172 -0
  31. package/resources/egress-guard/tests/policy_test.c +143 -0
  32. package/resources/egress-guard/tests/run-guard-tests.sh +62 -0
  33. package/resources/egress-guard/tests/run-policy-tests.sh +7 -0
@@ -0,0 +1,83 @@
1
+ # egress-guard
2
+
3
+ A small dylib injected into every process the iOS Simulator launches during a
4
+ `--egress local` session. It interposes the socket calls that open outbound
5
+ traffic (`connect`, `connectx`, `sendto`, `sendmsg`, and the `$NOCANCEL`
6
+ variants of the last three that libsystem exports alongside them) and refuses
7
+ any destination that is not loopback. A sockaddr whose family was left
8
+ `AF_UNSPEC` is classified as the family its length implies, which is how the
9
+ kernel treats it for TCP `connect` and IPv4 sends; on a datagram socket
10
+ `connect(AF_UNSPEC)` is left to the kernel, which dissolves the association. The system proxy and the `--egress-allow`
11
+ forwards live on loopback, so everything that honors the proxy keeps working
12
+ and everything that bypasses it fails with `ECONNREFUSED` in the process that
13
+ tried, with the calling frameworks recorded.
14
+
15
+ Why interposing: simulator processes share the worker's uid, so a packet
16
+ filter cannot tell them apart from the worker's own traffic. dyld interposing
17
+ from an inserted library reaches calls made inside Apple's own frameworks
18
+ (CFNetwork, Network.framework), and `launchctl setenv DYLD_INSERT_LIBRARIES`
19
+ inside the simulator makes launchd hand the library to every process it
20
+ spawns. The simulator does not enforce library validation, which is what makes
21
+ this possible there and nowhere else.
22
+
23
+ Configuration comes from the environment the simulator's launchd provides:
24
+
25
+ - `EAS_EGRESS_GUARD_LOG`: file the guard appends events to (host path).
26
+ - `EAS_EGRESS_GUARD_MODE`: `block` (default, and what any unknown value means)
27
+ or `log` (observe only).
28
+
29
+ Each event is one tab-separated line:
30
+
31
+ ```
32
+ eas-egress-guard\t<process>\t<pid>\t<function>\t<blocked|logged>\t<peer>\t<caller>,<caller>...
33
+ ```
34
+
35
+ A process lists each `(function, peer)` once, up to 128 of them. When that
36
+ table fills it writes one more line with function `overflow` and the limit as
37
+ the peer; further distinct destinations are still refused but not listed.
38
+
39
+ Layout:
40
+
41
+ - `policy.c` / `policy.h`: classification, mode, formatting, per-process
42
+ dedupe. Pure C, host-testable.
43
+ - `guard.c`: the interposers and the constructor that reads the environment.
44
+ - `check.c`: `egress-guard-check`, run inside the simulator right after the
45
+ guard is installed. Exits non-zero unless the library is loaded in a fresh
46
+ process and behaves as the mode says on `connect`, `connect$NOCANCEL` and a
47
+ `connect` with an `AF_UNSPEC` sockaddr; the worker fails the session on that.
48
+ - `tests/policy_test.c`: host unit tests, `tests/run-policy-tests.sh`.
49
+ - `tests/guard_test.c`, `tests/guard_insert_test.c`: host tests of the
50
+ interposers themselves, `tests/run-guard-tests.sh`. The first compiles
51
+ `guard.c` in and forces the lock schedules (a signal handler making a socket
52
+ call inside the critical section, a fork while another thread holds the lock,
53
+ contention); the second inserts the built library like the simulator does and
54
+ checks descriptor reuse, the `$NOCANCEL` symbols, `AF_UNSPEC` shapes and the
55
+ UDP disconnect. Nothing in them opens a socket to a non-loopback address.
56
+ - `tests/nettest.swift`: probe binary the simulator end-to-end test runs inside
57
+ a device; exercises URLSession, Network.framework, BSD TCP and UDP, DNS.
58
+ - `build.sh`: universal simulator dylib into `packages/build-tools/bin/`.
59
+
60
+ Installation order matters. `simctl` forwards every `SIMCTL_CHILD_`-prefixed
61
+ variable of its own environment to the process it starts, and for `simctl
62
+ boot` that process is the simulator's launchd, so the worker boots with the
63
+ guard and proxy variables in that form and every process of the boot inherits
64
+ them (measured: 176 of 176). `launchctl setenv` after boot is kept for a
65
+ device that was already booted, but by then the boot's own processes have
66
+ started without it. It also cannot change a variable the boot already carried:
67
+ a device booted with one guard configuration keeps it until it is shut down,
68
+ which is why the worker boots devices itself and every configuration in the
69
+ end-to-end test gets its own boot. The self-check runs once boot completes and is followed
70
+ by a coverage report from `lsof`, listing any process without the library. Both the dylib and the check binary are built by
71
+ `packages/worker/package.sh` for the iOS worker tarball and by the
72
+ `test-egress-guard` EAS workflow, not committed.
73
+
74
+ Failure semantics: a missing library, a failed `launchctl setenv`, or a failed
75
+ self-check fails the session, since a `--egress local` session without the
76
+ guard would silently leak. Only an unwritable event log is a warning, because
77
+ refusals still happen and only the reporting is lost. Recording is best-effort
78
+ in the process too: the event log is opened per event rather than held open
79
+ (a process that closes descriptors it does not own, as `launchd_sim` does,
80
+ would otherwise make the guard write into whatever reused the number), and a
81
+ dedupe slot that cannot be taken within a bounded number of attempts (a signal
82
+ handler re-entering the guard, a fork child inheriting a held lock) skips the
83
+ event rather than blocking or aborting the process.
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ # Builds the guard as a universal (arm64 + x86_64) iOS Simulator dylib into
3
+ # packages/build-tools/bin/egress-guard.dylib. Requires Xcode.
4
+ set -euo pipefail
5
+ cd "$(dirname "$0")"
6
+ # Output directory; defaults to packages/build-tools/bin.
7
+ bin_dir="${1:-../../bin}"
8
+ mkdir -p "$bin_dir"
9
+ out=$(mktemp -d)
10
+ sdk=$(xcrun --sdk iphonesimulator --show-sdk-path)
11
+ for arch in arm64 x86_64; do
12
+ xcrun --sdk iphonesimulator clang \
13
+ -std=c11 -Wall -Wextra -Werror -O2 \
14
+ -target "$arch-apple-ios15.0-simulator" -isysroot "$sdk" \
15
+ -dynamiclib -install_name @rpath/egress-guard.dylib \
16
+ -o "$out/egress-guard-$arch.dylib" policy.c guard.c
17
+ done
18
+ lipo -create -output "$bin_dir/egress-guard.dylib" "$out/egress-guard-arm64.dylib" "$out/egress-guard-x86_64.dylib"
19
+ lipo -info "$bin_dir/egress-guard.dylib"
20
+
21
+ # The self-check the worker runs inside the simulator after installing the guard.
22
+ for arch in arm64 x86_64; do
23
+ xcrun --sdk iphonesimulator clang \
24
+ -std=c11 -Wall -Wextra -Werror -O2 \
25
+ -target "$arch-apple-ios15.0-simulator" -isysroot "$sdk" \
26
+ -o "$out/egress-guard-check-$arch" check.c
27
+ done
28
+ lipo -create -output "$bin_dir/egress-guard-check" "$out/egress-guard-check-arm64" "$out/egress-guard-check-x86_64"
29
+ chmod +x "$bin_dir/egress-guard-check"
30
+ lipo -info "$bin_dir/egress-guard-check"
@@ -0,0 +1,112 @@
1
+ // Self-check run inside a simulator right after the guard is installed:
2
+ // proves the guard library is loaded in a freshly spawned process and that it
3
+ // behaves as the requested mode says, on every connect entry point the guard
4
+ // covers. Exit 0 on success; 2 when the library is not loaded; 3 when it is
5
+ // loaded but did not behave; 1 on usage errors.
6
+ //
7
+ // egress-guard-check --mode block|log
8
+ #include <arpa/inet.h>
9
+ #include <dlfcn.h>
10
+ #include <errno.h>
11
+ #include <fcntl.h>
12
+ #include <mach-o/dyld.h>
13
+ #include <netinet/in.h>
14
+ #include <stdio.h>
15
+ #include <string.h>
16
+ #include <sys/socket.h>
17
+ #include <unistd.h>
18
+
19
+ // TEST-NET-1 (RFC 5737): never routed, so without the guard a non-blocking
20
+ // connect reports EINPROGRESS and nothing ever answers.
21
+ #define PROBE_ADDRESS "192.0.2.1"
22
+ #define PROBE_PORT 9
23
+
24
+ typedef int (*connect_fn)(int, const struct sockaddr *, socklen_t);
25
+
26
+ static int guard_loaded(void) {
27
+ for (uint32_t i = 0; i < _dyld_image_count(); i++) {
28
+ const char *name = _dyld_get_image_name(i);
29
+ if (name != NULL && strstr(name, "egress-guard.dylib") != NULL) {
30
+ return 1;
31
+ }
32
+ }
33
+ return 0;
34
+ }
35
+
36
+ // One non-blocking connect to the probe address through `entry`, with the
37
+ // sockaddr family the caller wants. Returns the connect result; errno is
38
+ // stored in *err.
39
+ static int probe(connect_fn entry, sa_family_t family, int *err) {
40
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
41
+ if (fd < 0) {
42
+ *err = errno;
43
+ return -2;
44
+ }
45
+ fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
46
+ struct sockaddr_in to;
47
+ memset(&to, 0, sizeof to);
48
+ to.sin_family = family;
49
+ to.sin_len = sizeof to;
50
+ to.sin_port = htons(PROBE_PORT);
51
+ inet_pton(AF_INET, PROBE_ADDRESS, &to.sin_addr);
52
+ int rc = entry(fd, (struct sockaddr *)&to, sizeof to);
53
+ *err = errno;
54
+ close(fd);
55
+ return rc;
56
+ }
57
+
58
+ int main(int argc, char **argv) {
59
+ const char *mode = "block";
60
+ if (argc == 3 && strcmp(argv[1], "--mode") == 0) {
61
+ mode = argv[2];
62
+ } else if (argc != 1) {
63
+ fprintf(stderr, "usage: egress-guard-check [--mode block|log]\n");
64
+ return 1;
65
+ }
66
+ if (!guard_loaded()) {
67
+ printf("egress-guard-check: the guard library is not loaded in this process\n");
68
+ return 2;
69
+ }
70
+ connect_fn connect_nocancel = (connect_fn)dlsym(RTLD_DEFAULT, "connect$NOCANCEL");
71
+ if (connect_nocancel == NULL) {
72
+ printf("egress-guard-check: connect$NOCANCEL is not exported in this process\n");
73
+ return 3;
74
+ }
75
+
76
+ // The plain call, the non-cancelable variant, and a sockaddr whose family
77
+ // was left AF_UNSPEC, which the kernel connects as IPv4.
78
+ const struct {
79
+ const char *name;
80
+ connect_fn entry;
81
+ sa_family_t family;
82
+ } probes[] = {
83
+ {"connect", connect, AF_INET},
84
+ {"connect$NOCANCEL", connect_nocancel, AF_INET},
85
+ {"connect with AF_UNSPEC", connect, AF_UNSPEC},
86
+ };
87
+ int block = strcmp(mode, "block") == 0;
88
+ for (size_t i = 0; i < sizeof probes / sizeof probes[0]; i++) {
89
+ int err = 0;
90
+ int rc = probe(probes[i].entry, probes[i].family, &err);
91
+ if (rc == -2) {
92
+ printf("egress-guard-check: socket() failed: %s\n", strerror(err));
93
+ return 3;
94
+ }
95
+ if (block && !(rc == -1 && err == ECONNREFUSED)) {
96
+ printf("egress-guard-check: guard loaded but a non-loopback %s was not refused (rc=%d, errno=%d %s)\n",
97
+ probes[i].name, rc, err, strerror(err));
98
+ return 3;
99
+ }
100
+ if (!block && !(rc == 0 || (rc == -1 && err == EINPROGRESS))) {
101
+ printf("egress-guard-check: guard loaded in log mode but %s failed (errno=%d %s)\n",
102
+ probes[i].name, err, strerror(err));
103
+ return 3;
104
+ }
105
+ }
106
+ if (block) {
107
+ printf("egress-guard-check: guard loaded; non-loopback connections are refused\n");
108
+ } else {
109
+ printf("egress-guard-check: guard loaded in log mode; connections pass through\n");
110
+ }
111
+ return 0;
112
+ }
@@ -0,0 +1,274 @@
1
+ // The local egress guard: interposes the calls that open outbound traffic in
2
+ // every simulator process it is inserted into, refuses non-loopback
3
+ // destinations in block mode, and records one event per destination per
4
+ // process. See README.md and policy.h.
5
+ #include "policy.h"
6
+
7
+ #include <dlfcn.h>
8
+ #include <errno.h>
9
+ #include <execinfo.h>
10
+ #include <fcntl.h>
11
+ #include <os/lock.h>
12
+ #include <sched.h>
13
+ #include <stdio.h>
14
+ #include <stdlib.h>
15
+ #include <string.h>
16
+ #include <sys/socket.h>
17
+ #include <sys/types.h>
18
+ #include <unistd.h>
19
+
20
+ #define EG_LOG_ENV "EAS_EGRESS_GUARD_LOG"
21
+ #define EG_MODE_ENV "EAS_EGRESS_GUARD_MODE"
22
+ #define EG_MAX_CALLERS 6
23
+ #define EG_CALLER_LENGTH 64
24
+ #define EG_LOCK_ATTEMPTS 64
25
+
26
+ static int eg_initialized = 0;
27
+ static void *eg_own_image = NULL;
28
+ static char eg_log_path[1024];
29
+ static eg_mode_t eg_mode = EG_MODE_BLOCK;
30
+ static eg_seen_t eg_seen;
31
+ static int eg_overflow_reported = 0;
32
+ static os_unfair_lock eg_lock = OS_UNFAIR_LOCK_INIT;
33
+
34
+ static void eg_init(void) {
35
+ if (eg_initialized) {
36
+ return;
37
+ }
38
+ eg_mode = eg_parse_mode(getenv(EG_MODE_ENV));
39
+ Dl_info self;
40
+ if (dladdr((const void *)eg_init, &self)) {
41
+ eg_own_image = self.dli_fbase;
42
+ }
43
+ const char *log_path = getenv(EG_LOG_ENV);
44
+ // The path is kept rather than an open descriptor: a process that closes
45
+ // descriptors it does not know about (launchd_sim does) would otherwise
46
+ // leave the guard writing into whatever file reused the number.
47
+ if (log_path != NULL && log_path[0] != '\0' && strlen(log_path) < sizeof eg_log_path) {
48
+ strcpy(eg_log_path, log_path);
49
+ }
50
+ eg_initialized = 1;
51
+ }
52
+
53
+ __attribute__((constructor)) static void eg_constructor(void) { eg_init(); }
54
+
55
+ // Image names of the frames above the guard's own, innermost first, with
56
+ // consecutive repeats collapsed: "Network,CFNetwork,MyApp".
57
+ __attribute__((noinline)) static int eg_collect_callers(char names[EG_MAX_CALLERS][EG_CALLER_LENGTH]) {
58
+ void *frames[EG_MAX_CALLERS + 6];
59
+ int frame_count = backtrace(frames, EG_MAX_CALLERS + 6);
60
+ int count = 0;
61
+ for (int i = 1; i < frame_count && count < EG_MAX_CALLERS; i++) {
62
+ Dl_info info;
63
+ const char *image = "?";
64
+ if (dladdr(frames[i], &info)) {
65
+ if (info.dli_fbase == eg_own_image) {
66
+ continue;
67
+ }
68
+ if (info.dli_fname != NULL) {
69
+ const char *slash = strrchr(info.dli_fname, '/');
70
+ image = slash ? slash + 1 : info.dli_fname;
71
+ }
72
+ }
73
+ if (count > 0 && strcmp(names[count - 1], image) == 0) {
74
+ continue;
75
+ }
76
+ strncpy(names[count], image, EG_CALLER_LENGTH - 1);
77
+ names[count][EG_CALLER_LENGTH - 1] = '\0';
78
+ count++;
79
+ }
80
+ return count;
81
+ }
82
+
83
+ // A lock held by this same thread (a signal handler making a socket call
84
+ // while the interrupted call held it) or by a thread that did not survive
85
+ // fork() never becomes available and os_unfair_lock_lock aborts the process
86
+ // in both cases. Give up on recording the event instead; refusal does not
87
+ // depend on it. Ordinary contention over the microsecond-long critical
88
+ // section resolves within the first attempts.
89
+ static int eg_lock_bounded(void) {
90
+ for (int i = 0; i < EG_LOCK_ATTEMPTS; i++) {
91
+ if (os_unfair_lock_trylock(&eg_lock)) {
92
+ return 1;
93
+ }
94
+ sched_yield();
95
+ }
96
+ return 0;
97
+ }
98
+
99
+ // One line in the event log. O_APPEND keeps whole lines intact across
100
+ // processes writing concurrently; the log is opened per event, at most
101
+ // EG_SEEN_CAPACITY + 1 times per process.
102
+ __attribute__((noinline)) static void eg_write_event(const char *function, const char *action,
103
+ const char *peer, int with_callers) {
104
+ if (eg_log_path[0] == '\0') {
105
+ return;
106
+ }
107
+ char names[EG_MAX_CALLERS][EG_CALLER_LENGTH];
108
+ const char *callers[EG_MAX_CALLERS];
109
+ int caller_count = with_callers ? eg_collect_callers(names) : 0;
110
+ for (int i = 0; i < caller_count; i++) {
111
+ callers[i] = names[i];
112
+ }
113
+ char line[1024];
114
+ int n = eg_format_event(line, sizeof line, getprogname(), getpid(), function, action, peer,
115
+ callers, caller_count);
116
+ if (n <= 0) {
117
+ return;
118
+ }
119
+ int fd = open(eg_log_path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, 0644);
120
+ if (fd >= 0) {
121
+ (void)write(fd, line, (size_t)n);
122
+ close(fd);
123
+ }
124
+ }
125
+
126
+ // Returns 1 when the call must be refused.
127
+ __attribute__((noinline)) static int eg_handle(const char *function, const struct sockaddr *sa,
128
+ socklen_t len) {
129
+ eg_class_t cls = eg_classify(sa, len);
130
+ if (cls != EG_REMOTE) {
131
+ return 0;
132
+ }
133
+ eg_init();
134
+ int deny = eg_should_deny(eg_mode, cls);
135
+
136
+ char peer[96];
137
+ if (eg_format_peer(sa, len, peer, sizeof peer) != 0) {
138
+ strncpy(peer, "?", sizeof peer);
139
+ }
140
+ char key[EG_SEEN_KEY_LENGTH];
141
+ snprintf(key, sizeof key, "%s %s", function, peer);
142
+ int fresh = 0;
143
+ int overflowed = 0;
144
+ if (eg_lock_bounded()) {
145
+ fresh = eg_seen_insert(&eg_seen, key);
146
+ if (!fresh && eg_seen.overflow > 0 && !eg_overflow_reported) {
147
+ eg_overflow_reported = 1;
148
+ overflowed = 1;
149
+ }
150
+ os_unfair_lock_unlock(&eg_lock);
151
+ }
152
+
153
+ const char *action = deny ? "blocked" : "logged";
154
+ if (fresh) {
155
+ eg_write_event(function, action, peer, 1);
156
+ } else if (overflowed) {
157
+ // Once per process: the table is full, so further distinct destinations
158
+ // are still refused but no longer listed.
159
+ char limit[64];
160
+ snprintf(limit, sizeof limit, "%d distinct destinations", EG_SEEN_CAPACITY);
161
+ eg_write_event(EG_OVERFLOW_FUNCTION, action, limit, 0);
162
+ }
163
+ return deny;
164
+ }
165
+
166
+ // connect() with an AF_UNSPEC address on a datagram socket dissolves the
167
+ // association and sends nothing; the kernel answers EAFNOSUPPORT. Leave that
168
+ // to the kernel. On a stream socket the kernel connects to the address the
169
+ // length implies, which eg_handle classifies.
170
+ static int eg_dissolves_association(int fd, const struct sockaddr *sa, socklen_t len) {
171
+ if (sa == NULL || len < 2 || sa->sa_family != AF_UNSPEC) {
172
+ return 0;
173
+ }
174
+ int type = 0;
175
+ socklen_t type_len = sizeof type;
176
+ return getsockopt(fd, SOL_SOCKET, SO_TYPE, &type, &type_len) == 0 && type == SOCK_DGRAM;
177
+ }
178
+
179
+ typedef int (*eg_connect_fn)(int, const struct sockaddr *, socklen_t);
180
+ typedef ssize_t (*eg_sendto_fn)(int, const void *, size_t, int, const struct sockaddr *,
181
+ socklen_t);
182
+ typedef ssize_t (*eg_sendmsg_fn)(int, const struct msghdr *, int);
183
+
184
+ // The non-cancelable variants libsystem_kernel exports next to the cancelable
185
+ // ones; dyld interposing is per symbol, so each needs its own entry.
186
+ extern int eg_connect_nocancel_original(int, const struct sockaddr *, socklen_t) __asm__(
187
+ "_connect$NOCANCEL");
188
+ extern ssize_t eg_sendto_nocancel_original(int, const void *, size_t, int,
189
+ const struct sockaddr *, socklen_t) __asm__(
190
+ "_sendto$NOCANCEL");
191
+ extern ssize_t eg_sendmsg_nocancel_original(int, const struct msghdr *, int) __asm__(
192
+ "_sendmsg$NOCANCEL");
193
+
194
+ static int eg_connect_through(eg_connect_fn original, int fd, const struct sockaddr *sa,
195
+ socklen_t len) {
196
+ if (!eg_dissolves_association(fd, sa, len) && eg_handle("connect", sa, len)) {
197
+ errno = ECONNREFUSED;
198
+ return -1;
199
+ }
200
+ return original(fd, sa, len);
201
+ }
202
+
203
+ static int eg_connect(int fd, const struct sockaddr *sa, socklen_t len) {
204
+ return eg_connect_through(connect, fd, sa, len);
205
+ }
206
+
207
+ static int eg_connect_nocancel(int fd, const struct sockaddr *sa, socklen_t len) {
208
+ return eg_connect_through(eg_connect_nocancel_original, fd, sa, len);
209
+ }
210
+
211
+ static int eg_connectx(int s, const sa_endpoints_t *endpoints, sae_associd_t associd,
212
+ unsigned int flags, const struct iovec *iov, unsigned int iovcnt,
213
+ size_t *len, sae_connid_t *connid) {
214
+ if (endpoints != NULL && endpoints->sae_dstaddr != NULL &&
215
+ !eg_dissolves_association(s, endpoints->sae_dstaddr, endpoints->sae_dstaddrlen) &&
216
+ eg_handle("connectx", endpoints->sae_dstaddr, endpoints->sae_dstaddrlen)) {
217
+ errno = ECONNREFUSED;
218
+ return -1;
219
+ }
220
+ return connectx(s, endpoints, associd, flags, iov, iovcnt, len, connid);
221
+ }
222
+
223
+ static ssize_t eg_sendto_through(eg_sendto_fn original, int fd, const void *buf, size_t n,
224
+ int flags, const struct sockaddr *sa, socklen_t len) {
225
+ if (sa != NULL && eg_handle("sendto", sa, len)) {
226
+ errno = ECONNREFUSED;
227
+ return -1;
228
+ }
229
+ return original(fd, buf, n, flags, sa, len);
230
+ }
231
+
232
+ static ssize_t eg_sendto(int fd, const void *buf, size_t n, int flags, const struct sockaddr *sa,
233
+ socklen_t len) {
234
+ return eg_sendto_through(sendto, fd, buf, n, flags, sa, len);
235
+ }
236
+
237
+ static ssize_t eg_sendto_nocancel(int fd, const void *buf, size_t n, int flags,
238
+ const struct sockaddr *sa, socklen_t len) {
239
+ return eg_sendto_through(eg_sendto_nocancel_original, fd, buf, n, flags, sa, len);
240
+ }
241
+
242
+ static ssize_t eg_sendmsg_through(eg_sendmsg_fn original, int fd, const struct msghdr *msg,
243
+ int flags) {
244
+ if (msg != NULL && msg->msg_name != NULL && msg->msg_namelen > 0 &&
245
+ eg_handle("sendmsg", (const struct sockaddr *)msg->msg_name, msg->msg_namelen)) {
246
+ errno = ECONNREFUSED;
247
+ return -1;
248
+ }
249
+ return original(fd, msg, flags);
250
+ }
251
+
252
+ static ssize_t eg_sendmsg(int fd, const struct msghdr *msg, int flags) {
253
+ return eg_sendmsg_through(sendmsg, fd, msg, flags);
254
+ }
255
+
256
+ static ssize_t eg_sendmsg_nocancel(int fd, const struct msghdr *msg, int flags) {
257
+ return eg_sendmsg_through(eg_sendmsg_nocancel_original, fd, msg, flags);
258
+ }
259
+
260
+ // dyld applies these to every image in the process, including the shared
261
+ // cache, which is how calls made inside CFNetwork and Network.framework are
262
+ // caught.
263
+ __attribute__((used)) static const struct {
264
+ const void *replacement;
265
+ const void *original;
266
+ } eg_interposers[] __attribute__((section("__DATA,__interpose"))) = {
267
+ {(const void *)eg_connect, (const void *)connect},
268
+ {(const void *)eg_connect_nocancel, (const void *)eg_connect_nocancel_original},
269
+ {(const void *)eg_connectx, (const void *)connectx},
270
+ {(const void *)eg_sendto, (const void *)sendto},
271
+ {(const void *)eg_sendto_nocancel, (const void *)eg_sendto_nocancel_original},
272
+ {(const void *)eg_sendmsg, (const void *)sendmsg},
273
+ {(const void *)eg_sendmsg_nocancel, (const void *)eg_sendmsg_nocancel_original},
274
+ };
@@ -0,0 +1,164 @@
1
+ #include "policy.h"
2
+
3
+ #include <arpa/inet.h>
4
+ #include <netinet/in.h>
5
+ #include <stdint.h>
6
+ #include <stdio.h>
7
+ #include <string.h>
8
+
9
+ static int is_v4_local(uint32_t host_order) {
10
+ return (host_order >> 24) == 127 || host_order == 0;
11
+ }
12
+
13
+ // XNU accepts a sockaddr whose family is AF_UNSPEC as the family its length
14
+ // implies: TCP connect() translates it on both socket families and IPv4
15
+ // sendto()/sendmsg() deliver with it. Classify it the way the kernel uses it.
16
+ static sa_family_t eg_family(const struct sockaddr *sa, socklen_t len) {
17
+ if (sa->sa_family == AF_UNSPEC) {
18
+ if (len == sizeof(struct sockaddr_in)) {
19
+ return AF_INET;
20
+ }
21
+ if (len == sizeof(struct sockaddr_in6)) {
22
+ return AF_INET6;
23
+ }
24
+ }
25
+ return sa->sa_family;
26
+ }
27
+
28
+ eg_class_t eg_classify(const struct sockaddr *sa, socklen_t len) {
29
+ if (sa == NULL || len < 2) {
30
+ return EG_PASSTHROUGH;
31
+ }
32
+ sa_family_t family = eg_family(sa, len);
33
+ if (family == AF_INET) {
34
+ if (len < sizeof(struct sockaddr_in)) {
35
+ return EG_PASSTHROUGH;
36
+ }
37
+ const struct sockaddr_in *in = (const struct sockaddr_in *)sa;
38
+ return is_v4_local(ntohl(in->sin_addr.s_addr)) ? EG_LOOPBACK : EG_REMOTE;
39
+ }
40
+ if (family == AF_INET6) {
41
+ if (len < sizeof(struct sockaddr_in6)) {
42
+ return EG_PASSTHROUGH;
43
+ }
44
+ const struct in6_addr *a6 = &((const struct sockaddr_in6 *)sa)->sin6_addr;
45
+ if (IN6_IS_ADDR_LOOPBACK(a6) || IN6_IS_ADDR_UNSPECIFIED(a6)) {
46
+ return EG_LOOPBACK;
47
+ }
48
+ if (IN6_IS_ADDR_V4MAPPED(a6)) {
49
+ uint32_t v4;
50
+ memcpy(&v4, &a6->s6_addr[12], sizeof v4);
51
+ return is_v4_local(ntohl(v4)) ? EG_LOOPBACK : EG_REMOTE;
52
+ }
53
+ return EG_REMOTE;
54
+ }
55
+ return EG_PASSTHROUGH;
56
+ }
57
+
58
+ eg_mode_t eg_parse_mode(const char *value) {
59
+ if (value != NULL && strcmp(value, "log") == 0) {
60
+ return EG_MODE_LOG;
61
+ }
62
+ return EG_MODE_BLOCK;
63
+ }
64
+
65
+ int eg_should_deny(eg_mode_t mode, eg_class_t cls) {
66
+ return mode == EG_MODE_BLOCK && cls == EG_REMOTE;
67
+ }
68
+
69
+ int eg_format_peer(const struct sockaddr *sa, socklen_t len, char *out, size_t n) {
70
+ char ip[INET6_ADDRSTRLEN];
71
+ int written;
72
+ if (sa == NULL || len < 2) {
73
+ return -1;
74
+ }
75
+ sa_family_t family = eg_family(sa, len);
76
+ if (family == AF_INET && len >= sizeof(struct sockaddr_in)) {
77
+ const struct sockaddr_in *in = (const struct sockaddr_in *)sa;
78
+ if (inet_ntop(AF_INET, &in->sin_addr, ip, sizeof ip) == NULL) {
79
+ return -1;
80
+ }
81
+ written = snprintf(out, n, "%s:%d", ip, ntohs(in->sin_port));
82
+ } else if (family == AF_INET6 && len >= sizeof(struct sockaddr_in6)) {
83
+ const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)sa;
84
+ if (inet_ntop(AF_INET6, &in6->sin6_addr, ip, sizeof ip) == NULL) {
85
+ return -1;
86
+ }
87
+ written = snprintf(out, n, "[%s]:%d", ip, ntohs(in6->sin6_port));
88
+ } else {
89
+ return -1;
90
+ }
91
+ return (written < 0 || (size_t)written >= n) ? -1 : 0;
92
+ }
93
+
94
+ int eg_seen_insert(eg_seen_t *seen, const char *key) {
95
+ for (int i = 0; i < seen->count; i++) {
96
+ if (strncmp(seen->keys[i], key, EG_SEEN_KEY_LENGTH - 1) == 0) {
97
+ return 0;
98
+ }
99
+ }
100
+ if (seen->count >= EG_SEEN_CAPACITY) {
101
+ seen->overflow++;
102
+ return 0;
103
+ }
104
+ strncpy(seen->keys[seen->count], key, EG_SEEN_KEY_LENGTH - 1);
105
+ seen->keys[seen->count][EG_SEEN_KEY_LENGTH - 1] = '\0';
106
+ seen->count++;
107
+ return 1;
108
+ }
109
+
110
+ // Append `text` to out[*pos], replacing field separators; returns 0 when it fit.
111
+ static int append_field(char *out, size_t n, size_t *pos, const char *text) {
112
+ for (const char *c = text; *c; c++) {
113
+ if (*pos + 1 >= n) {
114
+ return -1;
115
+ }
116
+ out[(*pos)++] = (*c == '\t' || *c == '\n' || *c == '\r') ? ' ' : *c;
117
+ }
118
+ return 0;
119
+ }
120
+
121
+ static int append_raw(char *out, size_t n, size_t *pos, const char *text) {
122
+ size_t len = strlen(text);
123
+ if (*pos + len + 1 > n) {
124
+ return -1;
125
+ }
126
+ memcpy(out + *pos, text, len);
127
+ *pos += len;
128
+ return 0;
129
+ }
130
+
131
+ int eg_format_event(char *out, size_t n, const char *progname, int pid, const char *function,
132
+ const char *action, const char *peer, const char *const *callers,
133
+ int caller_count) {
134
+ size_t pos = 0;
135
+ char pid_text[16];
136
+ snprintf(pid_text, sizeof pid_text, "%d", pid);
137
+ if (append_raw(out, n, &pos, EG_EVENT_PREFIX "\t") != 0 ||
138
+ append_field(out, n, &pos, progname ? progname : "?") != 0 ||
139
+ append_raw(out, n, &pos, "\t") != 0 || append_raw(out, n, &pos, pid_text) != 0 ||
140
+ append_raw(out, n, &pos, "\t") != 0 || append_field(out, n, &pos, function) != 0 ||
141
+ append_raw(out, n, &pos, "\t") != 0 || append_field(out, n, &pos, action) != 0 ||
142
+ append_raw(out, n, &pos, "\t") != 0 || append_field(out, n, &pos, peer) != 0 ||
143
+ append_raw(out, n, &pos, "\t") != 0) {
144
+ return -1;
145
+ }
146
+ for (int i = 0; i < caller_count && callers != NULL; i++) {
147
+ if (i > 0 && append_raw(out, n, &pos, ",") != 0) {
148
+ return -1;
149
+ }
150
+ // Commas separate callers, so they cannot appear inside one.
151
+ for (const char *c = callers[i]; *c; c++) {
152
+ char ch = (*c == ',' || *c == '\t' || *c == '\n' || *c == '\r') ? ' ' : *c;
153
+ if (pos + 1 >= n) {
154
+ return -1;
155
+ }
156
+ out[pos++] = ch;
157
+ }
158
+ }
159
+ if (append_raw(out, n, &pos, "\n") != 0) {
160
+ return -1;
161
+ }
162
+ out[pos] = '\0';
163
+ return (int)pos;
164
+ }