@expo/build-tools 24.2.0 → 24.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/projectSources.js +3 -0
- package/dist/steps/easFunctions.js +2 -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/functions/startSandbox.d.ts +4 -0
- package/dist/steps/functions/startSandbox.js +93 -0
- 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/steps/utils/sandboxDaemon.d.ts +13 -0
- package/dist/steps/utils/sandboxDaemon.js +90 -0
- package/dist/utils/IosSimulatorUtils.d.ts +34 -0
- package/dist/utils/IosSimulatorUtils.js +62 -0
- package/package.json +8 -4
- 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
|
@@ -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)!)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Host-runnable tests for the guard's policy and formatting. Build and run
|
|
2
|
+
// with tests/run-policy-tests.sh; every failure prints the failing line.
|
|
3
|
+
#include "../policy.h"
|
|
4
|
+
|
|
5
|
+
#include <arpa/inet.h>
|
|
6
|
+
#include <netinet/in.h>
|
|
7
|
+
#include <stdio.h>
|
|
8
|
+
#include <string.h>
|
|
9
|
+
#include <sys/un.h>
|
|
10
|
+
|
|
11
|
+
static int failures = 0;
|
|
12
|
+
#define CHECK(cond) \
|
|
13
|
+
do { \
|
|
14
|
+
if (!(cond)) { \
|
|
15
|
+
failures++; \
|
|
16
|
+
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
|
17
|
+
} \
|
|
18
|
+
} while (0)
|
|
19
|
+
|
|
20
|
+
static struct sockaddr_in v4(const char *ip, int port) {
|
|
21
|
+
struct sockaddr_in a; memset(&a, 0, sizeof a);
|
|
22
|
+
a.sin_family = AF_INET; a.sin_len = sizeof a; a.sin_port = htons(port);
|
|
23
|
+
inet_pton(AF_INET, ip, &a.sin_addr);
|
|
24
|
+
return a;
|
|
25
|
+
}
|
|
26
|
+
static struct sockaddr_in6 v6(const char *ip, int port) {
|
|
27
|
+
struct sockaddr_in6 a; memset(&a, 0, sizeof a);
|
|
28
|
+
a.sin6_family = AF_INET6; a.sin6_len = sizeof a; a.sin6_port = htons(port);
|
|
29
|
+
inet_pton(AF_INET6, ip, &a.sin6_addr);
|
|
30
|
+
return a;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static void test_classify(void) {
|
|
34
|
+
struct sockaddr_in a4; struct sockaddr_in6 a6; struct sockaddr_un un;
|
|
35
|
+
|
|
36
|
+
a4 = v4("127.0.0.1", 8899); CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_LOOPBACK);
|
|
37
|
+
a4 = v4("127.42.0.9", 80); CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_LOOPBACK);
|
|
38
|
+
a4 = v4("0.0.0.0", 80); CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_LOOPBACK);
|
|
39
|
+
a4 = v4("93.184.216.34", 443); CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_REMOTE);
|
|
40
|
+
a4 = v4("192.168.1.10", 8081); CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_REMOTE);
|
|
41
|
+
a4 = v4("169.254.1.1", 80); CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_REMOTE);
|
|
42
|
+
a4 = v4("224.0.0.251", 5353); CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_REMOTE);
|
|
43
|
+
|
|
44
|
+
a6 = v6("::1", 8899); CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_LOOPBACK);
|
|
45
|
+
a6 = v6("::", 80); CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_LOOPBACK);
|
|
46
|
+
a6 = v6("::ffff:127.0.0.1", 8899); CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_LOOPBACK);
|
|
47
|
+
a6 = v6("::ffff:93.184.216.34", 443); CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_REMOTE);
|
|
48
|
+
a6 = v6("2606:4700::1", 443); CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_REMOTE);
|
|
49
|
+
a6 = v6("fe80::1", 443); CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_REMOTE);
|
|
50
|
+
|
|
51
|
+
memset(&un, 0, sizeof un); un.sun_family = AF_UNIX; strcpy(un.sun_path, "/var/run/mDNSResponder");
|
|
52
|
+
CHECK(eg_classify((struct sockaddr *)&un, sizeof un) == EG_PASSTHROUGH);
|
|
53
|
+
CHECK(eg_classify(NULL, 0) == EG_PASSTHROUGH);
|
|
54
|
+
a4 = v4("93.184.216.34", 443); CHECK(eg_classify((struct sockaddr *)&a4, 4) == EG_PASSTHROUGH); // too short to read
|
|
55
|
+
|
|
56
|
+
// AF_UNSPEC is what the kernel makes of it: the family the exact length implies.
|
|
57
|
+
a4 = v4("93.184.216.34", 443); a4.sin_family = AF_UNSPEC;
|
|
58
|
+
CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_REMOTE);
|
|
59
|
+
CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4 + 8) == EG_PASSTHROUGH); // kernel: EINVAL
|
|
60
|
+
a4 = v4("127.0.0.1", 8899); a4.sin_family = AF_UNSPEC;
|
|
61
|
+
CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_LOOPBACK);
|
|
62
|
+
a4 = v4("0.0.0.0", 0); a4.sin_family = AF_UNSPEC;
|
|
63
|
+
CHECK(eg_classify((struct sockaddr *)&a4, sizeof a4) == EG_LOOPBACK); // UDP disconnect shape
|
|
64
|
+
a6 = v6("2606:4700::1", 443); a6.sin6_family = AF_UNSPEC;
|
|
65
|
+
CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_REMOTE);
|
|
66
|
+
a6 = v6("::ffff:93.184.216.34", 443); a6.sin6_family = AF_UNSPEC;
|
|
67
|
+
CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_REMOTE);
|
|
68
|
+
a6 = v6("::1", 443); a6.sin6_family = AF_UNSPEC;
|
|
69
|
+
CHECK(eg_classify((struct sockaddr *)&a6, sizeof a6) == EG_LOOPBACK);
|
|
70
|
+
memset(&un, 0, sizeof un); un.sun_family = AF_UNSPEC;
|
|
71
|
+
CHECK(eg_classify((struct sockaddr *)&un, sizeof un) == EG_PASSTHROUGH);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static void test_mode(void) {
|
|
75
|
+
CHECK(eg_parse_mode(NULL) == EG_MODE_BLOCK);
|
|
76
|
+
CHECK(eg_parse_mode("") == EG_MODE_BLOCK);
|
|
77
|
+
CHECK(eg_parse_mode("block") == EG_MODE_BLOCK);
|
|
78
|
+
CHECK(eg_parse_mode("log") == EG_MODE_LOG);
|
|
79
|
+
CHECK(eg_parse_mode("LOG") == EG_MODE_BLOCK);
|
|
80
|
+
CHECK(eg_parse_mode("permissive") == EG_MODE_BLOCK);
|
|
81
|
+
|
|
82
|
+
CHECK(eg_should_deny(EG_MODE_BLOCK, EG_REMOTE) == 1);
|
|
83
|
+
CHECK(eg_should_deny(EG_MODE_BLOCK, EG_LOOPBACK) == 0);
|
|
84
|
+
CHECK(eg_should_deny(EG_MODE_BLOCK, EG_PASSTHROUGH) == 0);
|
|
85
|
+
CHECK(eg_should_deny(EG_MODE_LOG, EG_REMOTE) == 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static void test_format_peer(void) {
|
|
89
|
+
char out[64];
|
|
90
|
+
struct sockaddr_in a4 = v4("93.184.216.34", 443);
|
|
91
|
+
CHECK(eg_format_peer((struct sockaddr *)&a4, sizeof a4, out, sizeof out) == 0);
|
|
92
|
+
CHECK(strcmp(out, "93.184.216.34:443") == 0);
|
|
93
|
+
struct sockaddr_in6 a6 = v6("2606:4700::1", 443);
|
|
94
|
+
CHECK(eg_format_peer((struct sockaddr *)&a6, sizeof a6, out, sizeof out) == 0);
|
|
95
|
+
CHECK(strcmp(out, "[2606:4700::1]:443") == 0);
|
|
96
|
+
CHECK(eg_format_peer((struct sockaddr *)&a4, sizeof a4, out, 8) != 0);
|
|
97
|
+
struct sockaddr_un un; memset(&un, 0, sizeof un); un.sun_family = AF_UNIX;
|
|
98
|
+
CHECK(eg_format_peer((struct sockaddr *)&un, sizeof un, out, sizeof out) != 0);
|
|
99
|
+
a4.sin_family = AF_UNSPEC;
|
|
100
|
+
CHECK(eg_format_peer((struct sockaddr *)&a4, sizeof a4, out, sizeof out) == 0);
|
|
101
|
+
CHECK(strcmp(out, "93.184.216.34:443") == 0);
|
|
102
|
+
a6.sin6_family = AF_UNSPEC;
|
|
103
|
+
CHECK(eg_format_peer((struct sockaddr *)&a6, sizeof a6, out, sizeof out) == 0);
|
|
104
|
+
CHECK(strcmp(out, "[2606:4700::1]:443") == 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static void test_seen(void) {
|
|
108
|
+
static eg_seen_t seen; memset(&seen, 0, sizeof seen);
|
|
109
|
+
CHECK(eg_seen_insert(&seen, "connect 1.1.1.1:443") == 1);
|
|
110
|
+
CHECK(eg_seen_insert(&seen, "connect 1.1.1.1:443") == 0);
|
|
111
|
+
CHECK(eg_seen_insert(&seen, "sendto 1.1.1.1:443") == 1);
|
|
112
|
+
CHECK(seen.count == 2);
|
|
113
|
+
char key[64];
|
|
114
|
+
for (int i = 0; i < EG_SEEN_CAPACITY + 5; i++) { snprintf(key, sizeof key, "k%d", i); eg_seen_insert(&seen, key); }
|
|
115
|
+
CHECK(seen.count == EG_SEEN_CAPACITY);
|
|
116
|
+
CHECK(seen.overflow == 7); // 2 + 128 + 5 attempts, 128 fit
|
|
117
|
+
CHECK(eg_seen_insert(&seen, "k0") == 0); // still remembered
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static void test_format_event(void) {
|
|
121
|
+
char out[512];
|
|
122
|
+
const char *callers[] = {"Network", "CFNetwork", "MyApp"};
|
|
123
|
+
int n = eg_format_event(out, sizeof out, "MyApp", 4242, "connect", "blocked", "93.184.216.34:443", callers, 3);
|
|
124
|
+
CHECK(n > 0);
|
|
125
|
+
CHECK(strcmp(out, "eas-egress-guard\tMyApp\t4242\tconnect\tblocked\t93.184.216.34:443\tNetwork,CFNetwork,MyApp\n") == 0);
|
|
126
|
+
|
|
127
|
+
const char *tabbed[] = {"a\tb"};
|
|
128
|
+
n = eg_format_event(out, sizeof out, "we\nird\tname", 1, "sendto", "logged", "1.2.3.4:53", tabbed, 1);
|
|
129
|
+
CHECK(n > 0);
|
|
130
|
+
CHECK(strcmp(out, "eas-egress-guard\twe ird name\t1\tsendto\tlogged\t1.2.3.4:53\ta b\n") == 0);
|
|
131
|
+
|
|
132
|
+
n = eg_format_event(out, sizeof out, "NoCallers", 7, "connectx", "blocked", "[::2]:80", NULL, 0);
|
|
133
|
+
CHECK(n > 0 && strcmp(out, "eas-egress-guard\tNoCallers\t7\tconnectx\tblocked\t[::2]:80\t\n") == 0);
|
|
134
|
+
|
|
135
|
+
CHECK(eg_format_event(out, 16, "MyApp", 4242, "connect", "blocked", "93.184.216.34:443", callers, 3) == -1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
int main(void) {
|
|
139
|
+
test_classify(); test_mode(); test_format_peer(); test_seen(); test_format_event();
|
|
140
|
+
if (failures) { fprintf(stderr, "%d failure(s)\n", failures); return 1; }
|
|
141
|
+
printf("egress-guard policy tests: all passed\n");
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Builds the guard for the host (macOS) and runs the guard tests against it:
|
|
3
|
+
# white-box lock schedules with guard.c compiled in, and black-box cases with
|
|
4
|
+
# the built library inserted the way the simulator inserts it. No socket is
|
|
5
|
+
# ever opened to a non-loopback address.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
cd "$(dirname "$0")/.."
|
|
8
|
+
out=$(mktemp -d)
|
|
9
|
+
trap 'rm -rf "$out"' EXIT
|
|
10
|
+
|
|
11
|
+
clang -std=c11 -Wall -Wextra -Werror -O2 -dynamiclib policy.c guard.c -o "$out/egress-guard.dylib"
|
|
12
|
+
clang -std=c11 -Wall -Wextra -Werror -O2 -pthread policy.c tests/guard_test.c -o "$out/guard-test"
|
|
13
|
+
clang -std=c11 -Wall -Wextra -Werror -O2 tests/guard_insert_test.c -o "$out/guard-insert-test"
|
|
14
|
+
|
|
15
|
+
failures=0
|
|
16
|
+
report() {
|
|
17
|
+
if [[ $2 -eq 0 ]]; then
|
|
18
|
+
echo "ok $1"
|
|
19
|
+
else
|
|
20
|
+
echo "FAIL $1 (exit $2)"
|
|
21
|
+
failures=$((failures + 1))
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# White-box: guard.c compiled into the test, lock schedules forced.
|
|
26
|
+
for name in signal fork contention; do
|
|
27
|
+
status=0
|
|
28
|
+
: > "$out/$name.log"
|
|
29
|
+
EAS_EGRESS_GUARD_MODE=block EAS_EGRESS_GUARD_LOG="$out/$name.log" \
|
|
30
|
+
"$out/guard-test" "$name" || status=$?
|
|
31
|
+
report "$name" "$status"
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
# Black-box: the built library inserted into a plain process.
|
|
35
|
+
export DYLD_INSERT_LIBRARIES="$out/egress-guard.dylib"
|
|
36
|
+
export EAS_EGRESS_GUARD_MODE=block
|
|
37
|
+
|
|
38
|
+
status=0
|
|
39
|
+
: > "$out/fd-reuse.log"
|
|
40
|
+
EAS_EGRESS_GUARD_LOG="$out/fd-reuse.log" \
|
|
41
|
+
"$out/guard-insert-test" fd-reuse "$out/fd-reuse.log" "$out/application-file" || status=$?
|
|
42
|
+
report fd-reuse "$status"
|
|
43
|
+
|
|
44
|
+
status=0
|
|
45
|
+
: > "$out/low-fd.log"
|
|
46
|
+
EAS_EGRESS_GUARD_LOG="$out/low-fd.log" \
|
|
47
|
+
"$out/guard-insert-test" low-fd "$out/low-fd.log" 1>&- || status=$?
|
|
48
|
+
report low-fd "$status"
|
|
49
|
+
|
|
50
|
+
for name in nocancel unspec udp-disconnect callers overflow; do
|
|
51
|
+
status=0
|
|
52
|
+
: > "$out/$name.log"
|
|
53
|
+
EAS_EGRESS_GUARD_LOG="$out/$name.log" \
|
|
54
|
+
"$out/guard-insert-test" "$name" "$out/$name.log" || status=$?
|
|
55
|
+
report "$name" "$status"
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [[ $failures -ne 0 ]]; then
|
|
59
|
+
echo "egress-guard guard tests: $failures failure(s)"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
echo "egress-guard guard tests: all passed"
|