@expo/build-tools 24.3.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.
- package/dist/common/projectSources.js +3 -0
- package/dist/steps/functions/startIosSimulator.js +34 -2
- package/dist/steps/functions/startLocalEgress.d.ts +4 -3
- package/dist/steps/functions/startLocalEgress.js +10 -6
- package/dist/steps/utils/localEgress.d.ts +24 -0
- package/dist/steps/utils/localEgress.js +59 -1
- package/dist/steps/utils/localEgressGuard.d.ts +179 -0
- package/dist/steps/utils/localEgressGuard.js +537 -0
- package/dist/steps/utils/localEgressSession.js +4 -0
- package/dist/utils/IosSimulatorUtils.d.ts +34 -0
- package/dist/utils/IosSimulatorUtils.js +62 -0
- package/package.json +4 -2
- package/resources/egress-guard/README.md +83 -0
- package/resources/egress-guard/build.sh +30 -0
- package/resources/egress-guard/check.c +112 -0
- package/resources/egress-guard/guard.c +274 -0
- package/resources/egress-guard/policy.c +164 -0
- package/resources/egress-guard/policy.h +60 -0
- package/resources/egress-guard/tests/guard_insert_test.c +291 -0
- package/resources/egress-guard/tests/guard_test.c +177 -0
- package/resources/egress-guard/tests/nettest.swift +172 -0
- package/resources/egress-guard/tests/policy_test.c +143 -0
- package/resources/egress-guard/tests/run-guard-tests.sh +62 -0
- package/resources/egress-guard/tests/run-policy-tests.sh +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "24.
|
|
3
|
+
"version": "24.4.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"prebuild": "yarn gql",
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"build:record-sim": "mkdir -p bin && record_sim_bin_path=$(swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build --show-bin-path) && swift build -c release --package-path resources/record-sim --build-path resources/record-sim/.build && cp \"$record_sim_bin_path/record-sim\" bin/record-sim && chmod +x bin/record-sim",
|
|
27
|
+
"build:egress-guard": "resources/egress-guard/build.sh",
|
|
28
|
+
"test:egress-guard": "resources/egress-guard/tests/run-policy-tests.sh && resources/egress-guard/tests/run-guard-tests.sh",
|
|
27
29
|
"typecheck": "tsc",
|
|
28
30
|
"generate-appium-commands": "mise exec node@22.22.0 -- node scripts/generate-appium-commands.js",
|
|
29
31
|
"prepack": "rimraf dist \"*.tsbuildinfo\" && yarn gql && tsc -p tsconfig.build.json",
|
|
@@ -104,5 +106,5 @@
|
|
|
104
106
|
"typescript": "^5.5.4",
|
|
105
107
|
"uuid": "^9.0.1"
|
|
106
108
|
},
|
|
107
|
-
"gitHead": "
|
|
109
|
+
"gitHead": "e7efe143366b0c908814e61dd3fb4b207600b773"
|
|
108
110
|
}
|
|
@@ -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
|
+
}
|