@braedonsaunders/appkit-desk 0.7.0 → 0.9.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 (46) hide show
  1. package/README.md +126 -1
  2. package/backend.d.ts +32 -7
  3. package/backend.d.ts.map +1 -1
  4. package/backend.js +107 -524
  5. package/backend.js.map +1 -1
  6. package/host.d.ts +27 -1
  7. package/host.d.ts.map +1 -1
  8. package/host.js +47 -19
  9. package/host.js.map +1 -1
  10. package/index.d.ts +2 -0
  11. package/index.d.ts.map +1 -1
  12. package/index.js +2 -0
  13. package/index.js.map +1 -1
  14. package/machine.d.ts +154 -0
  15. package/machine.d.ts.map +1 -0
  16. package/machine.js +573 -0
  17. package/machine.js.map +1 -0
  18. package/package.json +2 -2
  19. package/plan.d.ts +192 -31
  20. package/plan.d.ts.map +1 -1
  21. package/plan.js +298 -12
  22. package/plan.js.map +1 -1
  23. package/port-relay.d.ts +13 -0
  24. package/port-relay.d.ts.map +1 -0
  25. package/port-relay.js +56 -0
  26. package/port-relay.js.map +1 -0
  27. package/virtualization.d.ts +16 -0
  28. package/virtualization.d.ts.map +1 -0
  29. package/virtualization.js +156 -0
  30. package/virtualization.js.map +1 -0
  31. package/vz/Package.swift +23 -0
  32. package/vz/Sources/DeskNet/Addresses.swift +106 -0
  33. package/vz/Sources/DeskNet/DHCP.swift +111 -0
  34. package/vz/Sources/DeskNet/Fence.swift +57 -0
  35. package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
  36. package/vz/Sources/DeskNet/Packets.swift +355 -0
  37. package/vz/Sources/DeskNet/Plumbing.swift +312 -0
  38. package/vz/Sources/DeskNet/Sockets.swift +221 -0
  39. package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
  40. package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
  41. package/vz/Sources/appkit-desk-vz/Helper.swift +550 -0
  42. package/vz/Sources/appkit-desk-vz/main.swift +38 -0
  43. package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
  44. package/vz/appkit-desk-vz.entitlements +8 -0
  45. package/vz/build.sh +23 -0
  46. package/vz/check/extract-boot.py +221 -0
@@ -0,0 +1,38 @@
1
+ import Foundation
2
+
3
+ /// appkit-desk-vz: one Linux microVM under Apple's Virtualization.framework,
4
+ /// driven over newline-delimited JSON on stdin and stdout. See README.md.
5
+
6
+ // A peer that hangs up must surface as EPIPE on that socket, not end the VM.
7
+ signal(SIGPIPE, SIG_IGN)
8
+
9
+ MainActor.assumeIsolated {
10
+ let control = ControlChannel()
11
+ let helper = Helper(control: control)
12
+ control.onRequest = { id, op, body in helper.handle(id: id, op: op, body: body) }
13
+ control.onClose = { helper.stop(nil) }
14
+
15
+ // SIGTERM and SIGINT stop the VM before the process goes, exactly as a
16
+ // `stop` request does, so a host that kills the helper never strands a VM.
17
+ var signalSources: [DispatchSourceSignal] = []
18
+ for number in [SIGTERM, SIGINT, SIGHUP] {
19
+ signal(number, SIG_IGN)
20
+ let source = DispatchSource.makeSignalSource(signal: number, queue: .main)
21
+ source.setEventHandler { @Sendable in Task { @MainActor in helper.stop(nil) } }
22
+ source.resume()
23
+ signalSources.append(source)
24
+ }
25
+ withExtendedLifetime(signalSources) {}
26
+ KeepAlive.objects = [control, helper, signalSources]
27
+
28
+ control.start()
29
+ control.emit("ready", ["protocol": 1, "pid": Int(getpid())])
30
+ }
31
+
32
+ /// Holds the long-lived objects for the life of the process.
33
+ @MainActor
34
+ enum KeepAlive {
35
+ static var objects: [Any] = []
36
+ }
37
+
38
+ dispatchMain()
@@ -0,0 +1,426 @@
1
+ import Darwin
2
+ import Testing
3
+
4
+ @testable import DeskNet
5
+
6
+ // MARK: - Pure codecs and policy
7
+
8
+ @Test func parsesAddressesStrictly() {
9
+ #expect(IPv4("10.0.2.15") == IPv4(10, 0, 2, 15))
10
+ #expect(IPv4("256.0.0.1") == nil)
11
+ #expect(IPv4("1.2.3") == nil)
12
+ #expect(IPv4("1.2.3.4 ") == nil)
13
+ #expect(IPv4("01.2.3.4.5") == nil)
14
+ #expect(MAC("06:00:00:00:00:64")?.description == "06:00:00:00:00:64")
15
+ #expect(MAC("06:00:00:00:00") == nil)
16
+ #expect(CIDR("100.64.0.0/10")?.contains(IPv4(100, 127, 255, 255)) == true)
17
+ #expect(CIDR("100.64.0.0/10")?.contains(IPv4(100, 128, 0, 0)) == false)
18
+ }
19
+
20
+ @Test func fencesLocalSpaceAndTheMacUnlessTheLanIsAllowed() {
21
+ let fence = EgressFence(allowLan: false)
22
+ let mac: Set<IPv4> = [IPv4(151, 101, 1, 69)]
23
+ for fenced in ["10.1.2.3", "172.16.0.1", "172.31.255.255", "192.168.1.1", "169.254.169.254", "100.64.0.1", "100.100.100.100", "151.101.1.69"] {
24
+ #expect(fence.verdict(for: IPv4(fenced)!, hostAddresses: mac) == .fenced, "\(fenced)")
25
+ }
26
+ for open in ["1.1.1.1", "9.9.9.9", "172.32.0.1", "100.128.0.1"] {
27
+ #expect(fence.verdict(for: IPv4(open)!, hostAddresses: mac) == .allowed, "\(open)")
28
+ }
29
+ for invalid in ["127.0.0.1", "0.1.2.3", "224.0.0.251", "255.255.255.255", "240.0.0.1"] {
30
+ #expect(fence.verdict(for: IPv4(invalid)!, hostAddresses: mac) == .invalid, "\(invalid)")
31
+ }
32
+ let lan = EgressFence(allowLan: true)
33
+ #expect(lan.verdict(for: IPv4(192, 168, 1, 1), hostAddresses: mac) == .allowed)
34
+ #expect(lan.verdict(for: IPv4(151, 101, 1, 69), hostAddresses: mac) == .allowed)
35
+ #expect(lan.verdict(for: IPv4(127, 0, 0, 1), hostAddresses: mac) == .invalid)
36
+ }
37
+
38
+ @Test func documentationNetworksRemainBlockedWhenLanIsAllowed() {
39
+ for allowLan in [false, true] {
40
+ let fence = EgressFence(allowLan: allowLan)
41
+ for address in ["192.0.2.0", "192.0.2.1", "192.0.2.255", "198.51.100.0", "198.51.100.255", "203.0.113.0", "203.0.113.255"] {
42
+ #expect(fence.verdict(for: IPv4(address)!, hostAddresses: []) == .invalid, "\(address)")
43
+ }
44
+ for address in ["192.0.1.255", "192.0.3.0", "198.51.99.255", "198.51.101.0", "203.0.112.255", "203.0.114.0"] {
45
+ #expect(fence.verdict(for: IPv4(address)!, hostAddresses: []) == .allowed, "\(address)")
46
+ }
47
+ }
48
+ }
49
+
50
+ @Test func computesTheInternetChecksum() {
51
+ // The worked example from RFC 1071-style references: a real IPv4 header.
52
+ let header: [UInt8] = [
53
+ 0x45, 0x00, 0x00, 0x73, 0x00, 0x00, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x01,
54
+ 0xc0, 0xa8, 0x00, 0xc7,
55
+ ]
56
+ #expect(internetChecksum(header) == 0xb861)
57
+ }
58
+
59
+ @Test func roundTripsTCPWithAValidChecksum() throws {
60
+ let source = IPv4(10, 0, 2, 15)
61
+ let destination = IPv4(1, 1, 1, 1)
62
+ let segment = TCPSegment(
63
+ sourcePort: 40_000, destinationPort: 443, sequence: 0xffff_fff0, acknowledgement: 7,
64
+ flags: [.syn], window: 64_240, mss: 1460, payload: [])
65
+ let bytes = segment.encoded(source: source, destination: destination)
66
+ let sum = pseudoHeaderSum(source: source, destination: destination, protocol: IPProtocol.tcp, length: bytes.count)
67
+ #expect(internetChecksum(bytes, initial: sum) == 0)
68
+ let parsed = try #require(TCPSegment(parsing: bytes))
69
+ #expect(parsed.mss == 1460)
70
+ #expect(parsed.sequence == 0xffff_fff0)
71
+ #expect(parsed.flags == [.syn])
72
+ #expect(parsed.sequenceLength == 1)
73
+ #expect(seqLT(0xffff_fff0, 5))
74
+ }
75
+
76
+ @Test func parsesConnectLines() {
77
+ #expect(PendingHandshake.parse("CONNECT 5252") == 5252)
78
+ #expect(PendingHandshake.parse("CONNECT 14500\r") == 14500)
79
+ #expect(PendingHandshake.parse("CONNECT") == nil)
80
+ #expect(PendingHandshake.parse("CONNECT 1") == nil)
81
+ #expect(PendingHandshake.parse("connect 1") == nil)
82
+ #expect(PendingHandshake.parse("CONNECT -1") == nil)
83
+ }
84
+
85
+ // MARK: - A guest on the other end of the stack
86
+
87
+ let guestIP = IPv4(10, 0, 2, 15)
88
+ let gatewayIP = IPv4(10, 0, 2, 2)
89
+ let guestMAC = MAC([0x06, 0, 0, 0, 0, 0x64])
90
+
91
+ /// Dials socket pairs instead of the internet and keeps the far ends.
92
+ final class PairDialer: Dialer, @unchecked Sendable {
93
+ var tcpPeers: [Int32] = []
94
+ var udpPeers: [Int32] = []
95
+ var dialed: [(IPv4, UInt16)] = []
96
+ var refuse = false
97
+
98
+ func tcp(to ip: IPv4, port: UInt16) throws -> Int32 {
99
+ dialed.append((ip, port))
100
+ if refuse { throw POSIXFailure("connect", ECONNREFUSED) }
101
+ let (mine, theirs) = try Self.pair(SOCK_STREAM)
102
+ tcpPeers.append(theirs)
103
+ return mine
104
+ }
105
+
106
+ func udp(to ip: IPv4, port: UInt16) throws -> Int32 {
107
+ dialed.append((ip, port))
108
+ let (mine, theirs) = try Self.pair(SOCK_DGRAM)
109
+ udpPeers.append(theirs)
110
+ return mine
111
+ }
112
+
113
+ func icmp() throws -> Int32 { throw POSIXFailure("socket", EPERM) }
114
+
115
+ static func pair(_ type: Int32) throws -> (Int32, Int32) {
116
+ var fds: [Int32] = [0, 0]
117
+ guard socketpair(AF_UNIX, type, 0, &fds) == 0 else { throw POSIXFailure("socketpair") }
118
+ Socket.setNonBlocking(fds[0])
119
+ Socket.setNonBlocking(fds[1])
120
+ Socket.setNoSigPipe(fds[0])
121
+ Socket.setNoSigPipe(fds[1])
122
+ return (fds[0], fds[1])
123
+ }
124
+ }
125
+
126
+ @MainActor
127
+ final class Harness {
128
+ var frames: [[UInt8]] = []
129
+ let dialer = PairDialer()
130
+ var stack: NetworkStack!
131
+ var announced: [IPv4] = []
132
+
133
+ init(allowLan: Bool = false, hostAddresses: Set<IPv4> = [], dns: [IPv4] = [IPv4(1, 1, 1, 1), IPv4(9, 9, 9, 9)]) {
134
+ stack = NetworkStack(
135
+ configuration: NetworkConfiguration(dns: dns, guestMAC: guestMAC, allowLan: allowLan), dialer: dialer,
136
+ hostAddresses: { hostAddresses }, sendFrame: { [unowned self] in self.frames.append($0) })
137
+ stack.onGuestAddress = { [unowned self] in self.announced.append($0) }
138
+ }
139
+
140
+ func send(ip payload: [UInt8], proto: UInt8, to destination: IPv4, from source: IPv4 = guestIP) {
141
+ let packet = IPv4Packet.encode(source: source, destination: destination, proto: proto, payload: payload)
142
+ stack.receiveFrame(EthernetFrame(destination: .gateway, source: guestMAC, type: EtherType.ipv4, payload: packet).encoded)
143
+ }
144
+
145
+ func send(tcp segment: TCPSegment, to destination: IPv4) {
146
+ send(ip: segment.encoded(source: guestIP, destination: destination), proto: IPProtocol.tcp, to: destination)
147
+ }
148
+
149
+ /// Every IPv4 packet sent to the guest since the last call.
150
+ func takePackets() -> [IPv4Packet] {
151
+ defer { frames.removeAll() }
152
+ return frames.compactMap { EthernetFrame(parsing: $0) }.filter { $0.type == EtherType.ipv4 }
153
+ .compactMap { IPv4Packet(parsing: $0.payload) }
154
+ }
155
+
156
+ func takeSegments() -> [TCPSegment] {
157
+ takePackets().filter { $0.proto == IPProtocol.tcp }.compactMap { TCPSegment(parsing: $0.payload) }
158
+ }
159
+
160
+ func waitFor(_ condition: () -> Bool) async {
161
+ for _ in 0..<200 where !condition() {
162
+ try? await Task.sleep(for: .milliseconds(10))
163
+ }
164
+ }
165
+ }
166
+
167
+ func readAll(_ fd: Int32) -> [UInt8]? {
168
+ var buffer = [UInt8](repeating: 0, count: 65_536)
169
+ let count = buffer.withUnsafeMutableBytes { read(fd, $0.baseAddress, $0.count) }
170
+ if count < 0 { return nil }
171
+ return Array(buffer[0..<count])
172
+ }
173
+
174
+ func writeAll(_ fd: Int32, _ bytes: [UInt8]) {
175
+ _ = bytes.withUnsafeBytes { write(fd, $0.baseAddress, bytes.count) }
176
+ }
177
+
178
+ @MainActor @Test func answersARPForEveryAddressButTheGuests() throws {
179
+ let harness = Harness()
180
+ let request = ARPPacket(
181
+ operation: ARPPacket.request, senderMAC: guestMAC, senderIP: guestIP, targetMAC: MAC([0, 0, 0, 0, 0, 0]),
182
+ targetIP: gatewayIP)
183
+ harness.stack.receiveFrame(EthernetFrame(destination: .broadcast, source: guestMAC, type: EtherType.arp, payload: request.encoded).encoded)
184
+ let first = try #require(harness.frames.first)
185
+ let frame = try #require(EthernetFrame(parsing: first))
186
+ let reply = try #require(ARPPacket(parsing: frame.payload))
187
+ #expect(reply.operation == ARPPacket.reply)
188
+ #expect(reply.senderMAC == .gateway)
189
+ #expect(reply.senderIP == gatewayIP)
190
+ harness.frames.removeAll()
191
+ // A duplicate-address probe for the guest's own address goes unanswered.
192
+ let probe = ARPPacket(
193
+ operation: ARPPacket.request, senderMAC: guestMAC, senderIP: .zero, targetMAC: MAC([0, 0, 0, 0, 0, 0]),
194
+ targetIP: guestIP)
195
+ harness.stack.receiveFrame(EthernetFrame(destination: .broadcast, source: guestMAC, type: EtherType.arp, payload: probe.encoded).encoded)
196
+ #expect(harness.frames.isEmpty)
197
+ }
198
+
199
+ @MainActor @Test func leasesTheGuestItsAddressOverDHCP() throws {
200
+ let harness = Harness()
201
+ func dhcp(_ type: UInt8) -> [UInt8] {
202
+ var bootp = [UInt8](repeating: 0, count: 240)
203
+ bootp[0] = 1
204
+ bootp[1] = 1
205
+ bootp[2] = 6
206
+ bootp[4...7] = [1, 2, 3, 4]
207
+ bootp[28..<34] = guestMAC.bytes[0..<6]
208
+ bootp[236..<240] = [99, 130, 83, 99]
209
+ return bootp + [53, 1, type, 255]
210
+ }
211
+ for (type, expected) in [(UInt8(1), UInt8(2)), (UInt8(3), UInt8(5))] {
212
+ let udp = UDPDatagram(sourcePort: 68, destinationPort: 67, payload: dhcp(type))
213
+ harness.send(ip: udp.encoded(source: .zero, destination: .broadcast), proto: IPProtocol.udp, to: .broadcast, from: .zero)
214
+ let packet = try #require(harness.takePackets().first)
215
+ let reply = try #require(UDPDatagram(parsing: packet.payload))
216
+ #expect(reply.destinationPort == 68)
217
+ #expect(IPv4(bytes: reply.payload, at: 16) == guestIP)
218
+ let options = DHCPServer.options(reply.payload)
219
+ #expect(options[53] == [expected])
220
+ #expect(options[3] == gatewayIP.bytes)
221
+ #expect(options[6] == [1, 1, 1, 1, 9, 9, 9, 9])
222
+ }
223
+ #expect(harness.announced == [guestIP])
224
+ }
225
+
226
+ @MainActor @Test func refusesFencedDestinationsWithAdministrativelyProhibited() throws {
227
+ let harness = Harness(hostAddresses: [IPv4(151, 101, 1, 69)])
228
+ for target in ["192.168.1.1", "10.0.0.1", "169.254.169.254", "100.64.0.1", "151.101.1.69"] {
229
+ let syn = TCPSegment(sourcePort: 40_000, destinationPort: 80, sequence: 1, acknowledgement: 0, flags: [.syn], window: 64_240)
230
+ harness.send(tcp: syn, to: IPv4(target)!)
231
+ let packet = try #require(harness.takePackets().first, "\(target)")
232
+ let icmp = try #require(ICMPMessage(parsing: packet.payload))
233
+ #expect(icmp.type == ICMPMessage.destinationUnreachable)
234
+ #expect(icmp.code == ICMPMessage.administrativelyProhibited)
235
+ // The quote carries the original header, so the guest maps it to its socket.
236
+ #expect(IPv4(bytes: icmp.payload, at: 16) == IPv4(target)!)
237
+ }
238
+ #expect(harness.dialer.dialed.isEmpty)
239
+ }
240
+
241
+ @MainActor @Test func lanAccessLiftsTheFence() async throws {
242
+ let harness = Harness(allowLan: true)
243
+ let syn = TCPSegment(sourcePort: 40_000, destinationPort: 80, sequence: 1, acknowledgement: 0, flags: [.syn], window: 64_240)
244
+ harness.send(tcp: syn, to: IPv4(192, 168, 1, 1))
245
+ #expect(harness.dialer.dialed.count == 1)
246
+ await harness.waitFor { !harness.frames.isEmpty }
247
+ #expect(harness.takeSegments().first?.flags == [.syn, .ack])
248
+ harness.stack.shutdown()
249
+ }
250
+
251
+ @MainActor @Test func resetsAGuestWhoseUpstreamRefuses() throws {
252
+ let harness = Harness()
253
+ harness.dialer.refuse = true
254
+ let syn = TCPSegment(sourcePort: 40_000, destinationPort: 81, sequence: 99, acknowledgement: 0, flags: [.syn], window: 64_240)
255
+ harness.send(tcp: syn, to: IPv4(1, 1, 1, 1))
256
+ let reset = try #require(harness.takeSegments().first)
257
+ #expect(reset.flags == [.rst, .ack])
258
+ #expect(reset.acknowledgement == 100)
259
+ }
260
+
261
+ @MainActor @Test func relaysAGuestTCPConnectionBothWaysWithHalfClose() async throws {
262
+ let harness = Harness()
263
+ let remote = IPv4(1, 1, 1, 1)
264
+ var seq: UInt32 = 1_000
265
+ harness.send(tcp: TCPSegment(sourcePort: 40_000, destinationPort: 443, sequence: seq, acknowledgement: 0, flags: [.syn], window: 64_240, mss: 1460), to: remote)
266
+ await harness.waitFor { !harness.frames.isEmpty }
267
+ let synAck = try #require(harness.takeSegments().first)
268
+ #expect(synAck.flags == [.syn, .ack])
269
+ #expect(synAck.acknowledgement == seq + 1)
270
+ #expect(synAck.mss == 1460)
271
+ seq += 1
272
+ var theirs = synAck.sequence &+ 1
273
+ let peer = try #require(harness.dialer.tcpPeers.first)
274
+
275
+ // Guest -> upstream.
276
+ harness.send(tcp: TCPSegment(sourcePort: 40_000, destinationPort: 443, sequence: seq, acknowledgement: theirs, flags: [.ack, .psh], window: 64_240, payload: Array("hello".utf8)), to: remote)
277
+ seq += 5
278
+ #expect(harness.takeSegments().last?.acknowledgement == seq)
279
+ #expect(readAll(peer) == Array("hello".utf8))
280
+
281
+ // Upstream -> guest, segmented to the guest's MSS.
282
+ let big = [UInt8](repeating: 0x61, count: 4_000)
283
+ writeAll(peer, big)
284
+ var received: [UInt8] = []
285
+ await harness.waitFor {
286
+ for segment in harness.takeSegments() where !segment.payload.isEmpty {
287
+ #expect(segment.sequence == theirs)
288
+ #expect(segment.payload.count <= 1460)
289
+ received += segment.payload
290
+ theirs &+= UInt32(segment.payload.count)
291
+ }
292
+ return received.count == big.count
293
+ }
294
+ #expect(received == big)
295
+ harness.send(tcp: TCPSegment(sourcePort: 40_000, destinationPort: 443, sequence: seq, acknowledgement: theirs, flags: [.ack], window: 64_240), to: remote)
296
+
297
+ // Guest half-closes: the upstream reads end of stream but can still write.
298
+ harness.send(tcp: TCPSegment(sourcePort: 40_000, destinationPort: 443, sequence: seq, acknowledgement: theirs, flags: [.ack, .fin], window: 64_240), to: remote)
299
+ seq += 1
300
+ #expect(harness.takeSegments().last?.acknowledgement == seq)
301
+ #expect(readAll(peer) == [])
302
+ writeAll(peer, Array("bye".utf8))
303
+ await harness.waitFor { harness.frames.count >= 1 }
304
+ let tail = try #require(harness.takeSegments().first)
305
+ #expect(tail.payload == Array("bye".utf8))
306
+ theirs &+= 3
307
+ harness.send(tcp: TCPSegment(sourcePort: 40_000, destinationPort: 443, sequence: seq, acknowledgement: theirs, flags: [.ack], window: 64_240), to: remote)
308
+
309
+ // Upstream closes: the guest gets a FIN, acknowledges it, and the flow ends.
310
+ close(peer)
311
+ await harness.waitFor { !harness.frames.isEmpty }
312
+ let fin = try #require(harness.takeSegments().first)
313
+ #expect(fin.flags.contains(.fin))
314
+ #expect(fin.sequence == theirs)
315
+ harness.send(tcp: TCPSegment(sourcePort: 40_000, destinationPort: 443, sequence: seq, acknowledgement: theirs &+ 1, flags: [.ack], window: 64_240), to: remote)
316
+ #expect(harness.stack.flowCount == 0)
317
+ }
318
+
319
+ @MainActor @Test func retransmitsWhatTheGuestDidNotAcknowledge() async throws {
320
+ var clock: UInt64 = 0
321
+ var frames: [[UInt8]] = []
322
+ let dialer = PairDialer()
323
+ let stack = NetworkStack(
324
+ configuration: NetworkConfiguration(guestMAC: guestMAC), dialer: dialer, now: { clock },
325
+ hostAddresses: { [] }, sendFrame: { frames.append($0) })
326
+ let remote = IPv4(1, 1, 1, 1)
327
+ let syn = TCPSegment(sourcePort: 40_001, destinationPort: 80, sequence: 1, acknowledgement: 0, flags: [.syn], window: 64_240)
328
+ func deliver(_ segment: TCPSegment) {
329
+ let packet = IPv4Packet.encode(source: guestIP, destination: remote, proto: IPProtocol.tcp, payload: segment.encoded(source: guestIP, destination: remote))
330
+ stack.receiveFrame(EthernetFrame(destination: .gateway, source: guestMAC, type: EtherType.ipv4, payload: packet).encoded)
331
+ }
332
+ func segments() -> [TCPSegment] {
333
+ defer { frames.removeAll() }
334
+ return frames.compactMap { EthernetFrame(parsing: $0) }.compactMap { IPv4Packet(parsing: $0.payload) }
335
+ .compactMap { TCPSegment(parsing: $0.payload) }
336
+ }
337
+ deliver(syn)
338
+ for _ in 0..<200 where frames.isEmpty { try? await Task.sleep(for: .milliseconds(10)) }
339
+ let synAck = try #require(segments().first)
340
+ deliver(TCPSegment(sourcePort: 40_001, destinationPort: 80, sequence: 2, acknowledgement: synAck.sequence &+ 1, flags: [.ack], window: 64_240))
341
+ writeAll(try #require(dialer.tcpPeers.first), Array("payload".utf8))
342
+ for _ in 0..<200 where frames.isEmpty { try? await Task.sleep(for: .milliseconds(10)) }
343
+ let first = try #require(segments().first)
344
+ #expect(first.payload == Array("payload".utf8))
345
+ // No acknowledgement arrives: the timer resends the same bytes.
346
+ clock += 300
347
+ stack.tick()
348
+ let again = try #require(segments().first)
349
+ #expect(again.sequence == first.sequence)
350
+ #expect(again.payload == first.payload)
351
+ stack.shutdown()
352
+ }
353
+
354
+ @MainActor @Test func connectsTheMacToAGuestPort() async throws {
355
+ let harness = Harness()
356
+ let (client, mine) = try PairDialer.pair(SOCK_STREAM)
357
+ writeAll(client, [])
358
+ harness.stack.connectToGuest(port: 14_500, clientFD: mine, early: Array("GET".utf8))
359
+ let syn = try #require(harness.takeSegments().first)
360
+ #expect(syn.flags == [.syn])
361
+ #expect(syn.destinationPort == 14_500)
362
+ // Nothing reaches the client before the guest accepts.
363
+ #expect(readAll(client) == nil)
364
+ harness.send(tcp: TCPSegment(sourcePort: 14_500, destinationPort: syn.sourcePort, sequence: 5_000, acknowledgement: syn.sequence &+ 1, flags: [.syn, .ack], window: 64_240, mss: 1460), to: gatewayIP)
365
+ await harness.waitFor { harness.frames.count >= 2 }
366
+ let segments = harness.takeSegments()
367
+ #expect(segments.first?.flags == [.ack])
368
+ #expect(segments.contains { $0.payload == Array("GET".utf8) })
369
+ try? await Task.sleep(for: .milliseconds(50))
370
+ let banner = try #require(readAll(client))
371
+ #expect(String(decoding: banner, as: UTF8.self) == "OK \(syn.sourcePort)\n")
372
+ harness.send(tcp: TCPSegment(sourcePort: 14_500, destinationPort: syn.sourcePort, sequence: 5_001, acknowledgement: syn.sequence &+ 4, flags: [.ack, .psh], window: 64_240, payload: Array("200".utf8)), to: gatewayIP)
373
+ try? await Task.sleep(for: .milliseconds(50))
374
+ #expect(readAll(client) == Array("200".utf8))
375
+ close(client)
376
+ harness.stack.shutdown()
377
+ }
378
+
379
+ @MainActor @Test func closesTheClientWhenTheGuestRefusesThePort() async throws {
380
+ let harness = Harness()
381
+ let (client, mine) = try PairDialer.pair(SOCK_STREAM)
382
+ harness.stack.connectToGuest(port: 9, clientFD: mine)
383
+ let syn = try #require(harness.takeSegments().first)
384
+ harness.send(tcp: TCPSegment(sourcePort: 9, destinationPort: syn.sourcePort, sequence: 0, acknowledgement: syn.sequence &+ 1, flags: [.rst, .ack], window: 0), to: gatewayIP)
385
+ await harness.waitFor { readAll(client) != nil }
386
+ // End of stream with no reply line: a refusal, as a refused vsock port reads.
387
+ #expect(readAll(client) == [])
388
+ #expect(harness.stack.flowCount == 0)
389
+ close(client)
390
+ }
391
+
392
+ @MainActor @Test func natsUDPAndForwardsGatewayDNS() async throws {
393
+ let harness = Harness()
394
+ let query = UDPDatagram(sourcePort: 5_353, destinationPort: 53, payload: [1, 2, 3])
395
+ harness.send(ip: query.encoded(source: guestIP, destination: gatewayIP), proto: IPProtocol.udp, to: gatewayIP)
396
+ #expect(harness.dialer.dialed.first?.0 == IPv4(1, 1, 1, 1))
397
+ let peer = try #require(harness.dialer.udpPeers.first)
398
+ #expect(readAll(peer) == [1, 2, 3])
399
+ writeAll(peer, [4, 5])
400
+ await harness.waitFor { !harness.frames.isEmpty }
401
+ let packet = try #require(harness.takePackets().first)
402
+ #expect(packet.source == gatewayIP)
403
+ let answer = try #require(UDPDatagram(parsing: packet.payload))
404
+ #expect(answer.sourcePort == 53)
405
+ #expect(answer.destinationPort == 5_353)
406
+ #expect(answer.payload == [4, 5])
407
+ harness.stack.shutdown()
408
+ }
409
+
410
+ @MainActor @Test func dropsTrafficFromAnyAddressButTheGuests() {
411
+ let harness = Harness()
412
+ let syn = TCPSegment(sourcePort: 1, destinationPort: 80, sequence: 1, acknowledgement: 0, flags: [.syn], window: 1)
413
+ harness.send(ip: syn.encoded(source: IPv4(10, 0, 2, 99), destination: IPv4(1, 1, 1, 1)), proto: IPProtocol.tcp, to: IPv4(1, 1, 1, 1), from: IPv4(10, 0, 2, 99))
414
+ #expect(harness.frames.isEmpty)
415
+ #expect(harness.dialer.dialed.isEmpty)
416
+ }
417
+
418
+ @MainActor @Test func gatewayDNSCannotBypassTheLanFence() {
419
+ let harness = Harness(dns: [IPv4(192, 168, 1, 1)])
420
+ let query = UDPDatagram(sourcePort: 5353, destinationPort: 53, payload: [1, 2, 3])
421
+ harness.send(ip: query.encoded(source: guestIP, destination: gatewayIP), proto: IPProtocol.udp, to: gatewayIP)
422
+ #expect(harness.dialer.dialed.isEmpty)
423
+ let packets = harness.takePackets()
424
+ #expect(packets.contains { $0.proto == IPProtocol.icmp })
425
+ harness.stack.shutdown()
426
+ }
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.virtualization</key>
6
+ <true/>
7
+ </dict>
8
+ </plist>
package/vz/build.sh ADDED
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+ # Build appkit-desk-vz for release and sign it with the virtualization
3
+ # entitlement, without which Virtualization.framework refuses to start a VM.
4
+ #
5
+ # packages/desk/vz/build.sh ad hoc signature (local use)
6
+ # APPKIT_CODESIGN_IDENTITY="Developer ID Application: …" packages/desk/vz/build.sh
7
+ #
8
+ # Prints the path of the signed binary on stdout.
9
+ set -eu
10
+ here=$(cd "$(dirname "$0")" && pwd)
11
+ cd "$here"
12
+ swift build -c release -j "${APPKIT_SWIFT_JOBS:-2}" --product appkit-desk-vz >&2
13
+ binary="$(swift build -c release --show-bin-path)/appkit-desk-vz"
14
+ identity=${APPKIT_CODESIGN_IDENTITY:--}
15
+ if [ "$identity" = "-" ]; then
16
+ codesign --force --sign - --entitlements appkit-desk-vz.entitlements "$binary" >&2
17
+ else
18
+ # A Developer ID build is hardened and timestamped so it can be notarized.
19
+ codesign --force --sign "$identity" --options runtime --timestamp \
20
+ --entitlements appkit-desk-vz.entitlements "$binary" >&2
21
+ fi
22
+ codesign --verify --strict "$binary" >&2
23
+ echo "$binary"