@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.
@@ -0,0 +1,60 @@
1
+ // Policy and formatting for the local egress guard. Pure functions with no
2
+ // interposing, so they compile and test on macOS as well as in the simulator.
3
+ #ifndef EAS_EGRESS_GUARD_POLICY_H
4
+ #define EAS_EGRESS_GUARD_POLICY_H
5
+
6
+ #include <stddef.h>
7
+ #include <sys/socket.h>
8
+
9
+ // What a destination address is, as far as the guard cares.
10
+ typedef enum {
11
+ EG_PASSTHROUGH = 0, // not an internet address (unix sockets, NULL, short); never touched
12
+ EG_LOOPBACK = 1, // 127.0.0.0/8, ::1, ::ffff:127.x, unspecified; the proxy and forwards live here
13
+ EG_REMOTE = 2, // anything else, including link-local and multicast
14
+ } eg_class_t;
15
+
16
+ typedef enum {
17
+ EG_MODE_BLOCK = 0, // refuse EG_REMOTE with ECONNREFUSED
18
+ EG_MODE_LOG = 1, // observe only
19
+ } eg_mode_t;
20
+
21
+ eg_class_t eg_classify(const struct sockaddr *sa, socklen_t len);
22
+
23
+ // "block" or unset selects block; "log" selects log. Anything else is block:
24
+ // an unknown mode must fail closed.
25
+ eg_mode_t eg_parse_mode(const char *value);
26
+
27
+ int eg_should_deny(eg_mode_t mode, eg_class_t cls);
28
+
29
+ // Writes "1.2.3.4:443" or "[2001:db8::1]:443". Returns 0 on success.
30
+ int eg_format_peer(const struct sockaddr *sa, socklen_t len, char *out, size_t n);
31
+
32
+ // Fixed-capacity set of strings, so a retrying client logs a destination once
33
+ // per process instead of once per attempt.
34
+ #define EG_SEEN_CAPACITY 128
35
+ #define EG_SEEN_KEY_LENGTH 192
36
+ typedef struct {
37
+ char keys[EG_SEEN_CAPACITY][EG_SEEN_KEY_LENGTH];
38
+ int count;
39
+ int overflow; // insertions refused because the table was full
40
+ } eg_seen_t;
41
+
42
+ // 1 when the key was not present and was inserted; 0 when already present or
43
+ // when the table is full (counted in overflow).
44
+ int eg_seen_insert(eg_seen_t *seen, const char *key);
45
+
46
+ #define EG_EVENT_PREFIX "eas-egress-guard"
47
+
48
+ // Function name of the one event a process writes when its seen table fills:
49
+ // from then on distinct destinations are still refused but no longer listed.
50
+ #define EG_OVERFLOW_FUNCTION "overflow"
51
+
52
+ // One tab-separated line, newline terminated:
53
+ // eas-egress-guard\t<progname>\t<pid>\t<function>\t<action>\t<peer>\t<caller>,<caller>...\n
54
+ // Tabs and newlines inside fields are replaced with spaces. Returns the length
55
+ // written, or -1 when it does not fit.
56
+ int eg_format_event(char *out, size_t n, const char *progname, int pid, const char *function,
57
+ const char *action, const char *peer, const char *const *callers,
58
+ int caller_count);
59
+
60
+ #endif
@@ -0,0 +1,291 @@
1
+ // Black-box host tests of the built guard, loaded through
2
+ // DYLD_INSERT_LIBRARIES like the simulator does. Remote calls use fd -1 or are
3
+ // refused before the kernel; the only real sockets are on loopback. Build and
4
+ // run with tests/run-guard-tests.sh.
5
+ //
6
+ // guard-insert-test <case> <log-path> [<extra-path>]
7
+ #include <arpa/inet.h>
8
+ #include <dlfcn.h>
9
+ #include <errno.h>
10
+ #include <fcntl.h>
11
+ #include <netinet/in.h>
12
+ #include <stdio.h>
13
+ #include <stdlib.h>
14
+ #include <string.h>
15
+ #include <sys/socket.h>
16
+ #include <sys/stat.h>
17
+ #include <sys/uio.h>
18
+ #include <unistd.h>
19
+
20
+ typedef int (*connect_fn)(int, const struct sockaddr *, socklen_t);
21
+ typedef ssize_t (*sendto_fn)(int, const void *, size_t, int, const struct sockaddr *, socklen_t);
22
+ typedef ssize_t (*sendmsg_fn)(int, const struct msghdr *, int);
23
+
24
+ static struct sockaddr_in remote(int port) {
25
+ struct sockaddr_in a;
26
+ memset(&a, 0, sizeof a);
27
+ a.sin_family = AF_INET;
28
+ a.sin_len = sizeof a;
29
+ a.sin_port = htons(port);
30
+ inet_pton(AF_INET, "192.0.2.1", &a.sin_addr);
31
+ return a;
32
+ }
33
+
34
+ static struct sockaddr_in6 remote6(int port) {
35
+ struct sockaddr_in6 a;
36
+ memset(&a, 0, sizeof a);
37
+ a.sin6_family = AF_INET6;
38
+ a.sin6_len = sizeof a;
39
+ a.sin6_port = htons(port);
40
+ inet_pton(AF_INET6, "2001:db8::1", &a.sin6_addr);
41
+ return a;
42
+ }
43
+
44
+ static int refused_sendto(int port) {
45
+ struct sockaddr_in to = remote(port);
46
+ errno = 0;
47
+ return sendto(-1, "x", 1, 0, (struct sockaddr *)&to, sizeof to) == -1 && errno == ECONNREFUSED;
48
+ }
49
+
50
+ static off_t file_size(const char *path) {
51
+ struct stat st;
52
+ return stat(path, &st) == 0 ? st.st_size : -1;
53
+ }
54
+
55
+ static int count_lines(const char *path, const char *needle) {
56
+ FILE *f = fopen(path, "r");
57
+ if (f == NULL) {
58
+ return -1;
59
+ }
60
+ int count = 0;
61
+ char line[1024];
62
+ while (fgets(line, sizeof line, f)) {
63
+ if (strstr(line, needle)) {
64
+ count++;
65
+ }
66
+ }
67
+ fclose(f);
68
+ return count;
69
+ }
70
+
71
+ // Daemon-style cleanup: close every descriptor the process does not know
72
+ // about, then open an application file. The guard's event must land in the
73
+ // event log, not in that file.
74
+ static int fd_reuse(const char *log_path, const char *app_path) {
75
+ for (int fd = 3; fd < getdtablesize(); fd++) {
76
+ close(fd);
77
+ }
78
+ int app_fd = open(app_path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
79
+ if (app_fd < 0 || !refused_sendto(9)) {
80
+ return 2;
81
+ }
82
+ close(app_fd);
83
+ if (file_size(app_path) != 0) {
84
+ printf("FAIL fd-reuse: %lld bytes written into an unrelated file\n",
85
+ (long long)file_size(app_path));
86
+ return 1;
87
+ }
88
+ if (count_lines(log_path, "\tsendto\tblocked\t192.0.2.1:9\t") != 1) {
89
+ printf("FAIL fd-reuse: event missing from the event log\n");
90
+ return 1;
91
+ }
92
+ return 0;
93
+ }
94
+
95
+ // Started with stdout closed, as the runner arranges: the process's own stdout
96
+ // output must not end up in the event log, and the event must.
97
+ static int low_fd(const char *log_path) {
98
+ printf("STDOUT-LINE\n");
99
+ fflush(stdout);
100
+ if (!refused_sendto(9)) {
101
+ return 2;
102
+ }
103
+ if (count_lines(log_path, "STDOUT-LINE") != 0) {
104
+ return 1;
105
+ }
106
+ return count_lines(log_path, "\tsendto\tblocked\t192.0.2.1:9\t") == 1 ? 0 : 1;
107
+ }
108
+
109
+ static int nocancel(void) {
110
+ connect_fn nc_connect = (connect_fn)dlsym(RTLD_DEFAULT, "connect$NOCANCEL");
111
+ sendto_fn nc_sendto = (sendto_fn)dlsym(RTLD_DEFAULT, "sendto$NOCANCEL");
112
+ sendmsg_fn nc_sendmsg = (sendmsg_fn)dlsym(RTLD_DEFAULT, "sendmsg$NOCANCEL");
113
+ if (!nc_connect || !nc_sendto || !nc_sendmsg) {
114
+ return 2;
115
+ }
116
+ struct sockaddr_in to = remote(443);
117
+ int failures = 0;
118
+ errno = 0;
119
+ if (nc_connect(-1, (struct sockaddr *)&to, sizeof to) != -1 || errno != ECONNREFUSED) {
120
+ printf("FAIL nocancel: connect$NOCANCEL not refused (errno %d)\n", errno);
121
+ failures++;
122
+ }
123
+ errno = 0;
124
+ if (nc_sendto(-1, "x", 1, 0, (struct sockaddr *)&to, sizeof to) != -1 || errno != ECONNREFUSED) {
125
+ printf("FAIL nocancel: sendto$NOCANCEL not refused (errno %d)\n", errno);
126
+ failures++;
127
+ }
128
+ char payload[] = "x";
129
+ struct iovec iov = {.iov_base = payload, .iov_len = 1};
130
+ struct msghdr msg = {.msg_name = &to, .msg_namelen = sizeof to, .msg_iov = &iov, .msg_iovlen = 1};
131
+ errno = 0;
132
+ if (nc_sendmsg(-1, &msg, 0) != -1 || errno != ECONNREFUSED) {
133
+ printf("FAIL nocancel: sendmsg$NOCANCEL not refused (errno %d)\n", errno);
134
+ failures++;
135
+ }
136
+ return failures ? 1 : 0;
137
+ }
138
+
139
+ // The shapes the kernel accepts as IPv4/IPv6 despite sa_family == AF_UNSPEC.
140
+ static int unspec(void) {
141
+ int failures = 0;
142
+ struct sockaddr_in to = remote(443);
143
+ to.sin_family = AF_UNSPEC;
144
+ int tcp = socket(AF_INET, SOCK_STREAM, 0);
145
+ errno = 0;
146
+ if (connect(tcp, (struct sockaddr *)&to, sizeof to) != -1 || errno != ECONNREFUSED) {
147
+ printf("FAIL unspec: TCP connect with AF_UNSPEC not refused (errno %d)\n", errno);
148
+ failures++;
149
+ }
150
+ close(tcp);
151
+ struct sockaddr_in6 to6 = remote6(443);
152
+ to6.sin6_family = AF_UNSPEC;
153
+ int tcp6 = socket(AF_INET6, SOCK_STREAM, 0);
154
+ errno = 0;
155
+ if (connect(tcp6, (struct sockaddr *)&to6, sizeof to6) != -1 || errno != ECONNREFUSED) {
156
+ printf("FAIL unspec: TCP6 connect with AF_UNSPEC not refused (errno %d)\n", errno);
157
+ failures++;
158
+ }
159
+ close(tcp6);
160
+ errno = 0;
161
+ if (sendto(-1, "x", 1, 0, (struct sockaddr *)&to, sizeof to) != -1 || errno != ECONNREFUSED) {
162
+ printf("FAIL unspec: sendto with AF_UNSPEC not refused (errno %d)\n", errno);
163
+ failures++;
164
+ }
165
+ char payload[] = "x";
166
+ struct iovec iov = {.iov_base = payload, .iov_len = 1};
167
+ struct msghdr msg = {.msg_name = &to, .msg_namelen = sizeof to, .msg_iov = &iov, .msg_iovlen = 1};
168
+ errno = 0;
169
+ if (sendmsg(-1, &msg, 0) != -1 || errno != ECONNREFUSED) {
170
+ printf("FAIL unspec: sendmsg with AF_UNSPEC not refused (errno %d)\n", errno);
171
+ failures++;
172
+ }
173
+ return failures ? 1 : 0;
174
+ }
175
+
176
+ // A UDP socket associated with loopback dissolves the association through
177
+ // connect(AF_UNSPEC), whatever address bytes follow the family. The guard
178
+ // must let the kernel do that.
179
+ static int udp_disconnect(void) {
180
+ int fd = socket(AF_INET, SOCK_DGRAM, 0);
181
+ struct sockaddr_in loopback;
182
+ memset(&loopback, 0, sizeof loopback);
183
+ loopback.sin_family = AF_INET;
184
+ loopback.sin_len = sizeof loopback;
185
+ loopback.sin_port = htons(9);
186
+ loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
187
+ if (connect(fd, (struct sockaddr *)&loopback, sizeof loopback) != 0) {
188
+ return 2;
189
+ }
190
+ struct sockaddr_in peer;
191
+ socklen_t peer_len = sizeof peer;
192
+ if (getpeername(fd, (struct sockaddr *)&peer, &peer_len) != 0) {
193
+ return 2;
194
+ }
195
+ int failures = 0;
196
+ for (int stale = 0; stale < 2; stale++) {
197
+ struct sockaddr_in dissolve = stale ? remote(443) : loopback;
198
+ if (!stale) {
199
+ memset(&dissolve, 0, sizeof dissolve);
200
+ dissolve.sin_len = sizeof dissolve;
201
+ }
202
+ dissolve.sin_family = AF_UNSPEC;
203
+ if (connect(fd, (struct sockaddr *)&loopback, sizeof loopback) != 0) {
204
+ return 2;
205
+ }
206
+ errno = 0;
207
+ int rc = connect(fd, (struct sockaddr *)&dissolve, sizeof dissolve);
208
+ int err = errno;
209
+ peer_len = sizeof peer;
210
+ int still_connected = getpeername(fd, (struct sockaddr *)&peer, &peer_len) == 0;
211
+ if (rc != -1 || err != EAFNOSUPPORT || still_connected) {
212
+ printf("FAIL udp-disconnect (%s address): rc=%d errno=%d still_connected=%d\n",
213
+ stale ? "stale remote" : "zeroed", rc, err, still_connected);
214
+ failures++;
215
+ }
216
+ }
217
+ close(fd);
218
+ return failures ? 1 : 0;
219
+ }
220
+
221
+ // Past 128 distinct destinations a process writes one overflow line and
222
+ // nothing more; refusal continues.
223
+ static int overflow(const char *log_path) {
224
+ for (int port = 1; port <= 140; port++) {
225
+ if (!refused_sendto(port)) {
226
+ return 2;
227
+ }
228
+ }
229
+ int listed = count_lines(log_path, "\tsendto\tblocked\t192.0.2.1:");
230
+ int overflow_lines = count_lines(log_path, "\toverflow\tblocked\t128 distinct destinations\t");
231
+ if (listed != 128 || overflow_lines != 1) {
232
+ printf("FAIL overflow: %d destinations listed, %d overflow line(s)\n", listed, overflow_lines);
233
+ return 1;
234
+ }
235
+ return 0;
236
+ }
237
+
238
+ // The event names the images above the guard, not the guard itself.
239
+ static int callers(const char *log_path) {
240
+ if (!refused_sendto(9)) {
241
+ return 2;
242
+ }
243
+ FILE *f = fopen(log_path, "r");
244
+ if (f == NULL) {
245
+ return 2;
246
+ }
247
+ char line[1024];
248
+ int ok = 0;
249
+ while (fgets(line, sizeof line, f)) {
250
+ if (strstr(line, "\tsendto\tblocked\t192.0.2.1:9\t")) {
251
+ const char *callers_field = strrchr(line, '\t') + 1;
252
+ ok = strstr(callers_field, "guard-insert-test") != NULL &&
253
+ strstr(callers_field, "egress-guard") == NULL;
254
+ if (!ok) {
255
+ printf("FAIL callers: %s", line);
256
+ }
257
+ }
258
+ }
259
+ fclose(f);
260
+ return ok ? 0 : 1;
261
+ }
262
+
263
+ int main(int argc, char **argv) {
264
+ if (argc < 3) {
265
+ return 2;
266
+ }
267
+ const char *name = argv[1];
268
+ const char *log_path = argv[2];
269
+ if (strcmp(name, "fd-reuse") == 0) {
270
+ return argc == 4 ? fd_reuse(log_path, argv[3]) : 2;
271
+ }
272
+ if (strcmp(name, "low-fd") == 0) {
273
+ return low_fd(log_path);
274
+ }
275
+ if (strcmp(name, "nocancel") == 0) {
276
+ return nocancel();
277
+ }
278
+ if (strcmp(name, "unspec") == 0) {
279
+ return unspec();
280
+ }
281
+ if (strcmp(name, "udp-disconnect") == 0) {
282
+ return udp_disconnect();
283
+ }
284
+ if (strcmp(name, "callers") == 0) {
285
+ return callers(log_path);
286
+ }
287
+ if (strcmp(name, "overflow") == 0) {
288
+ return overflow(log_path);
289
+ }
290
+ return 2;
291
+ }
@@ -0,0 +1,177 @@
1
+ // White-box host tests of guard.c's dedupe lock: the schedules that would
2
+ // abort the process are forced deterministically. Every case runs in its own
3
+ // process; sockets are never opened (fd -1), so nothing can leave the host.
4
+ // Build and run with tests/run-guard-tests.sh.
5
+ #include "../policy.h"
6
+
7
+ #include <pthread.h>
8
+ #include <signal.h>
9
+ #include <sys/wait.h>
10
+
11
+ static int scheduled_seen_insert(eg_seen_t *seen, const char *key);
12
+ #define eg_seen_insert scheduled_seen_insert
13
+ #include "../guard.c"
14
+ #undef eg_seen_insert
15
+
16
+ #include <arpa/inet.h>
17
+
18
+ #define CONTENTION_THREADS 8
19
+ #define CONTENTION_KEYS 12
20
+ #define CONTENTION_ROUNDS 25
21
+
22
+ static int interrupt_insert;
23
+ static int ready_pipe[2];
24
+ static int release_pipe[2];
25
+
26
+ static struct sockaddr_in remote(int port) {
27
+ struct sockaddr_in a;
28
+ memset(&a, 0, sizeof a);
29
+ a.sin_family = AF_INET;
30
+ a.sin_len = sizeof a;
31
+ a.sin_port = htons(port);
32
+ inet_pton(AF_INET, "192.0.2.1", &a.sin_addr);
33
+ return a;
34
+ }
35
+
36
+ // Block mode must refuse before touching the kernel, even on an invalid fd.
37
+ static void send_remote(int port) {
38
+ struct sockaddr_in to = remote(port);
39
+ if (eg_sendto(-1, "x", 1, 0, (struct sockaddr *)&to, sizeof to) != -1 || errno != ECONNREFUSED) {
40
+ _exit(2);
41
+ }
42
+ }
43
+
44
+ static void on_signal(int sig) {
45
+ (void)sig;
46
+ send_remote(9);
47
+ }
48
+
49
+ static int scheduled_seen_insert(eg_seen_t *seen, const char *key) {
50
+ if (interrupt_insert) {
51
+ interrupt_insert = 0;
52
+ raise(SIGUSR1);
53
+ }
54
+ return eg_seen_insert(seen, key);
55
+ }
56
+
57
+ static int signal_reentry(void) {
58
+ signal(SIGUSR1, on_signal);
59
+ interrupt_insert = 1;
60
+ send_remote(9);
61
+ send_remote(10);
62
+ return 0;
63
+ }
64
+
65
+ static void *hold_lock(void *unused) {
66
+ (void)unused;
67
+ char byte = 'x';
68
+ os_unfair_lock_lock(&eg_lock);
69
+ (void)write(ready_pipe[1], &byte, 1);
70
+ (void)read(release_pipe[0], &byte, 1);
71
+ os_unfair_lock_unlock(&eg_lock);
72
+ return NULL;
73
+ }
74
+
75
+ static int fork_while_locked(void) {
76
+ if (pipe(ready_pipe) || pipe(release_pipe)) {
77
+ return 2;
78
+ }
79
+ pthread_t thread;
80
+ if (pthread_create(&thread, NULL, hold_lock, NULL)) {
81
+ return 2;
82
+ }
83
+ char byte;
84
+ if (read(ready_pipe[0], &byte, 1) != 1) {
85
+ return 2;
86
+ }
87
+ pid_t child = fork();
88
+ if (child < 0) {
89
+ return 2;
90
+ }
91
+ if (child == 0) {
92
+ alarm(5);
93
+ send_remote(9);
94
+ send_remote(10);
95
+ _exit(0);
96
+ }
97
+ (void)write(release_pipe[1], "x", 1);
98
+ pthread_join(thread, NULL);
99
+ int status;
100
+ waitpid(child, &status, 0);
101
+ if (WIFSIGNALED(status)) {
102
+ printf("FAIL fork: child terminated by signal %d\n", WTERMSIG(status));
103
+ return 1;
104
+ }
105
+ return WIFEXITED(status) ? WEXITSTATUS(status) : 2;
106
+ }
107
+
108
+ static void *hammer(void *unused) {
109
+ (void)unused;
110
+ for (int round = 0; round < CONTENTION_ROUNDS; round++) {
111
+ for (int key = 0; key < CONTENTION_KEYS; key++) {
112
+ send_remote(1000 + key);
113
+ }
114
+ }
115
+ return NULL;
116
+ }
117
+
118
+ // Under contention a thread may give up its insert; the next attempt on the
119
+ // same key must then record it, so every key ends up logged exactly once.
120
+ static int contention(const char *log_path) {
121
+ pthread_t threads[CONTENTION_THREADS];
122
+ for (int i = 0; i < CONTENTION_THREADS; i++) {
123
+ if (pthread_create(&threads[i], NULL, hammer, NULL)) {
124
+ return 2;
125
+ }
126
+ }
127
+ for (int i = 0; i < CONTENTION_THREADS; i++) {
128
+ pthread_join(threads[i], NULL);
129
+ }
130
+ FILE *log = fopen(log_path, "r");
131
+ if (log == NULL) {
132
+ return 2;
133
+ }
134
+ int counts[CONTENTION_KEYS] = {0};
135
+ int total = 0;
136
+ char line[1024];
137
+ while (fgets(line, sizeof line, log)) {
138
+ total++;
139
+ for (int key = 0; key < CONTENTION_KEYS; key++) {
140
+ char needle[64];
141
+ snprintf(needle, sizeof needle, "\tsendto\tblocked\t192.0.2.1:%d\t", 1000 + key);
142
+ if (strstr(line, needle)) {
143
+ counts[key]++;
144
+ }
145
+ }
146
+ }
147
+ fclose(log);
148
+ int failures = 0;
149
+ for (int key = 0; key < CONTENTION_KEYS; key++) {
150
+ if (counts[key] != 1) {
151
+ printf("FAIL contention: 192.0.2.1:%d logged %d times\n", 1000 + key, counts[key]);
152
+ failures++;
153
+ }
154
+ }
155
+ if (total != CONTENTION_KEYS) {
156
+ printf("FAIL contention: %d lines for %d keys\n", total, CONTENTION_KEYS);
157
+ failures++;
158
+ }
159
+ return failures ? 1 : 0;
160
+ }
161
+
162
+ int main(int argc, char **argv) {
163
+ if (argc != 2) {
164
+ return 2;
165
+ }
166
+ eg_mode = EG_MODE_BLOCK;
167
+ if (strcmp(argv[1], "signal") == 0) {
168
+ return signal_reentry();
169
+ }
170
+ if (strcmp(argv[1], "fork") == 0) {
171
+ return fork_while_locked();
172
+ }
173
+ if (strcmp(argv[1], "contention") == 0) {
174
+ return contention(eg_log_path);
175
+ }
176
+ return 2;
177
+ }
@@ -0,0 +1,172 @@
1
+ // Probe run inside a simulator by the egress guard end-to-end test. Exercises
2
+ // every outbound path the guard must cover and prints one JSON object.
3
+ //
4
+ // nettest --proxy-port <port> --udp-port <port>
5
+ //
6
+ // proxy-port: a CONNECT proxy on the host's loopback, standing in for the
7
+ // egress proxy. udp-port: a UDP echo on loopback.
8
+ import Foundation
9
+ import Network
10
+
11
+ var results: [String: String] = [:]
12
+ let args = CommandLine.arguments
13
+ func arg(_ name: String) -> Int {
14
+ guard let i = args.firstIndex(of: name), i + 1 < args.count else { return 0 }
15
+ return Int(args[i + 1]) ?? 0
16
+ }
17
+ let proxyPort = arg("--proxy-port")
18
+ let udpPort = arg("--udp-port")
19
+ let remoteHost = "example.com"
20
+ let remotePort = 443
21
+
22
+ func classify(_ error: Error?) -> String {
23
+ guard let e = error as NSError? else { return "ok" }
24
+ if e.domain == NSURLErrorDomain && e.code == NSURLErrorCannotConnectToHost { return "refused" }
25
+ if e.domain == NSPOSIXErrorDomain && e.code == Int(ECONNREFUSED) { return "refused" }
26
+ return "error(\(e.domain):\(e.code))"
27
+ }
28
+
29
+ // 1. URLSession straight to the internet. Blocked by the guard.
30
+ func urlSessionDirect() {
31
+ let g = DispatchGroup(); g.enter()
32
+ let c = URLSessionConfiguration.ephemeral; c.timeoutIntervalForRequest = 10
33
+ c.connectionProxyDictionary = [:] // explicitly no proxy, even if the host has one
34
+ URLSession(configuration: c).dataTask(with: URL(string: "https://\(remoteHost)/")!) { _, r, e in
35
+ results["urlsessionDirect"] = e == nil ? "ok(\((r as? HTTPURLResponse)?.statusCode ?? 0))" : classify(e); g.leave()
36
+ }.resume(); g.wait()
37
+ }
38
+
39
+ // 2. URLSession through a CONNECT proxy on loopback. Allowed: the guard only
40
+ // sees a loopback connect.
41
+ func urlSessionProxied() {
42
+ let g = DispatchGroup(); g.enter()
43
+ let c = URLSessionConfiguration.ephemeral; c.timeoutIntervalForRequest = 15
44
+ // The kCFNetworkProxies* constants are not exposed to Swift on iOS; these are their values.
45
+ c.connectionProxyDictionary = ["HTTPSEnable": 1, "HTTPSProxy": "127.0.0.1", "HTTPSPort": proxyPort]
46
+ URLSession(configuration: c).dataTask(with: URL(string: "https://\(remoteHost)/")!) { _, r, e in
47
+ results["urlsessionProxied"] = e == nil ? "ok(\((r as? HTTPURLResponse)?.statusCode ?? 0))" : classify(e); g.leave()
48
+ }.resume(); g.wait()
49
+ }
50
+
51
+ // 3. Network.framework connection straight out. Blocked.
52
+ func nwConnectionDirect() {
53
+ let g = DispatchGroup(); g.enter()
54
+ let p = NWParameters.tls; p.preferNoProxies = true
55
+ let c = NWConnection(host: NWEndpoint.Host(remoteHost), port: NWEndpoint.Port(integerLiteral: UInt16(remotePort)), using: p)
56
+ var done = false
57
+ c.stateUpdateHandler = { s in
58
+ guard !done else { return }
59
+ switch s {
60
+ case .ready: done = true; results["nwconnectionDirect"] = "ok"; c.cancel(); g.leave()
61
+ case .failed(let e): done = true; results["nwconnectionDirect"] = classify(e); g.leave()
62
+ case .waiting(let e): done = true; results["nwconnectionDirect"] = "waiting(" + classify(e) + ")"; c.cancel(); g.leave()
63
+ default: break
64
+ }
65
+ }
66
+ c.start(queue: .global())
67
+ _ = g.wait(timeout: .now() + 15)
68
+ if results["nwconnectionDirect"] == nil { results["nwconnectionDirect"] = "timeout"; c.cancel() }
69
+ }
70
+
71
+ // 4. Plain BSD TCP connect to a resolved address. Blocked.
72
+ func bsdConnectDirect() {
73
+ var hints = addrinfo(); hints.ai_socktype = SOCK_STREAM; hints.ai_family = AF_INET
74
+ var info: UnsafeMutablePointer<addrinfo>? = nil
75
+ guard getaddrinfo(remoteHost, "443", &hints, &info) == 0, let ai = info else { results["bsdConnectDirect"] = "dns-failed"; return }
76
+ results["dns"] = "ok"
77
+ let fd = socket(ai.pointee.ai_family, ai.pointee.ai_socktype, ai.pointee.ai_protocol)
78
+ let rc = connect(fd, ai.pointee.ai_addr, ai.pointee.ai_addrlen)
79
+ results["bsdConnectDirect"] = rc == 0 ? "ok" : (errno == ECONNREFUSED ? "refused" : "errno(\(errno))")
80
+ close(fd); freeaddrinfo(info)
81
+ }
82
+
83
+ // 5. UDP datagram straight out. Blocked. 6. UDP datagram to loopback echo. Allowed.
84
+ func udp(_ key: String, _ host: String, _ port: Int, expectEcho: Bool) {
85
+ let fd = socket(AF_INET, SOCK_DGRAM, 0)
86
+ var to = sockaddr_in(); to.sin_family = sa_family_t(AF_INET); to.sin_port = in_port_t(UInt16(port)).bigEndian
87
+ inet_pton(AF_INET, host, &to.sin_addr)
88
+ var payload: [UInt8] = Array("ping".utf8)
89
+ let sent = withUnsafePointer(to: &to) { p in p.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
90
+ sendto(fd, &payload, payload.count, 0, sa, socklen_t(MemoryLayout<sockaddr_in>.size)) } }
91
+ if sent < 0 { results[key] = errno == ECONNREFUSED ? "refused" : "errno(\(errno))"; close(fd); return }
92
+ if expectEcho {
93
+ var tv = timeval(tv_sec: 3, tv_usec: 0); setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, socklen_t(MemoryLayout<timeval>.size))
94
+ var buf = [UInt8](repeating: 0, count: 16)
95
+ let n = recv(fd, &buf, buf.count, 0)
96
+ results[key] = n > 0 ? "ok" : "no-echo"
97
+ } else { results[key] = "sent" }
98
+ close(fd)
99
+ }
100
+
101
+ urlSessionDirect()
102
+ urlSessionProxied()
103
+ nwConnectionDirect()
104
+ bsdConnectDirect()
105
+ udp("udpDirect", "8.8.8.8", 53, expectEcho: false)
106
+ udp("udpLoopback", "127.0.0.1", udpPort, expectEcho: true)
107
+
108
+ // 7. The same literal destination twice: the guard must log it once per process.
109
+ func bsdConnectLiteral(_ key: String) {
110
+ var to = sockaddr_in(); to.sin_family = sa_family_t(AF_INET); to.sin_port = in_port_t(UInt16(443)).bigEndian
111
+ inet_pton(AF_INET, "1.1.1.1", &to.sin_addr)
112
+ let fd = socket(AF_INET, SOCK_STREAM, 0)
113
+ let rc = withUnsafePointer(to: &to) { p in p.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
114
+ connect(fd, sa, socklen_t(MemoryLayout<sockaddr_in>.size)) } }
115
+ results[key] = rc == 0 ? "ok" : (errno == ECONNREFUSED ? "refused" : "errno(\(errno))")
116
+ close(fd)
117
+ }
118
+ bsdConnectLiteral("literalFirst")
119
+ bsdConnectLiteral("literalSecond")
120
+
121
+ // 8. A sockaddr_in whose family was left AF_UNSPEC: the kernel connects it as
122
+ // IPv4, so the guard must refuse it. 9. connect(AF_UNSPEC) on a UDP socket
123
+ // dissolves the association; the guard must leave that to the kernel.
124
+ func unspecConnect() {
125
+ var to = sockaddr_in(); to.sin_family = sa_family_t(AF_UNSPEC); to.sin_port = in_port_t(UInt16(443)).bigEndian
126
+ inet_pton(AF_INET, "1.0.0.1", &to.sin_addr)
127
+ let fd = socket(AF_INET, SOCK_STREAM, 0)
128
+ let rc = withUnsafePointer(to: &to) { p in p.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
129
+ connect(fd, sa, socklen_t(MemoryLayout<sockaddr_in>.size)) } }
130
+ results["unspecConnect"] = rc == 0 ? "ok" : (errno == ECONNREFUSED ? "refused" : "errno(\(errno))")
131
+ close(fd)
132
+ }
133
+ func udpDisconnect() {
134
+ let fd = socket(AF_INET, SOCK_DGRAM, 0)
135
+ var to = sockaddr_in(); to.sin_family = sa_family_t(AF_INET); to.sin_port = in_port_t(UInt16(udpPort)).bigEndian
136
+ inet_pton(AF_INET, "127.0.0.1", &to.sin_addr)
137
+ let associated = withUnsafePointer(to: &to) { p in p.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
138
+ connect(fd, sa, socklen_t(MemoryLayout<sockaddr_in>.size)) } }
139
+ var dissolve = sockaddr_in(); dissolve.sin_family = sa_family_t(AF_UNSPEC); dissolve.sin_port = in_port_t(UInt16(443)).bigEndian
140
+ inet_pton(AF_INET, "1.1.1.1", &dissolve.sin_addr)
141
+ let rc = withUnsafePointer(to: &dissolve) { p in p.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
142
+ connect(fd, sa, socklen_t(MemoryLayout<sockaddr_in>.size)) } }
143
+ let err = errno
144
+ var peer = sockaddr_in(); var peerLength = socklen_t(MemoryLayout<sockaddr_in>.size)
145
+ let stillConnected = withUnsafeMutablePointer(to: &peer) { p in p.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
146
+ getpeername(fd, sa, &peerLength) } } == 0
147
+ results["udpDisconnect"] = associated != 0 ? "associate-failed"
148
+ : (rc == -1 && err == EAFNOSUPPORT && !stillConnected) ? "ok" : "errno(\(err)),connected=\(stillConnected)"
149
+ close(fd)
150
+ }
151
+ // 10. The non-cancelable variant of sendto, as libsystem exports it. Blocked.
152
+ func nocancelSendto() {
153
+ typealias SendtoFn = @convention(c) (Int32, UnsafeRawPointer?, Int, Int32, UnsafePointer<sockaddr>?, socklen_t) -> Int
154
+ guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "sendto$NOCANCEL") else {
155
+ results["nocancelSendto"] = "no-symbol"; return
156
+ }
157
+ let nocancelSendto = unsafeBitCast(symbol, to: SendtoFn.self)
158
+ let fd = socket(AF_INET, SOCK_DGRAM, 0)
159
+ var to = sockaddr_in(); to.sin_family = sa_family_t(AF_INET); to.sin_port = in_port_t(UInt16(53)).bigEndian
160
+ inet_pton(AF_INET, "8.8.8.8", &to.sin_addr)
161
+ var payload: [UInt8] = Array("ping".utf8)
162
+ let sent = withUnsafePointer(to: &to) { p in p.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
163
+ nocancelSendto(fd, &payload, payload.count, 0, sa, socklen_t(MemoryLayout<sockaddr_in>.size)) } }
164
+ results["nocancelSendto"] = sent < 0 ? (errno == ECONNREFUSED ? "refused" : "errno(\(errno))") : "sent"
165
+ close(fd)
166
+ }
167
+ unspecConnect()
168
+ udpDisconnect()
169
+ nocancelSendto()
170
+
171
+ let json = try! JSONSerialization.data(withJSONObject: results, options: [.sortedKeys])
172
+ print(String(data: json, encoding: .utf8)!)