@braedonsaunders/appkit-desk 0.6.0 → 0.8.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 (53) hide show
  1. package/README.md +83 -1
  2. package/backend.d.ts +28 -7
  3. package/backend.d.ts.map +1 -1
  4. package/backend.js +18 -521
  5. package/backend.js.map +1 -1
  6. package/events.d.ts +3 -0
  7. package/events.d.ts.map +1 -1
  8. package/events.js.map +1 -1
  9. package/host.d.ts +32 -1
  10. package/host.d.ts.map +1 -1
  11. package/host.js +141 -23
  12. package/host.js.map +1 -1
  13. package/index.d.ts +3 -0
  14. package/index.d.ts.map +1 -1
  15. package/index.js +3 -0
  16. package/index.js.map +1 -1
  17. package/machine.d.ts +145 -0
  18. package/machine.d.ts.map +1 -0
  19. package/machine.js +518 -0
  20. package/machine.js.map +1 -0
  21. package/network.d.ts +37 -0
  22. package/network.d.ts.map +1 -0
  23. package/network.js +173 -0
  24. package/network.js.map +1 -0
  25. package/package.json +2 -2
  26. package/plan.d.ts +129 -31
  27. package/plan.d.ts.map +1 -1
  28. package/plan.js +165 -9
  29. package/plan.js.map +1 -1
  30. package/port-relay.d.ts +13 -0
  31. package/port-relay.d.ts.map +1 -0
  32. package/port-relay.js +56 -0
  33. package/port-relay.js.map +1 -0
  34. package/virtualization.d.ts +16 -0
  35. package/virtualization.d.ts.map +1 -0
  36. package/virtualization.js +148 -0
  37. package/virtualization.js.map +1 -0
  38. package/vz/Package.swift +23 -0
  39. package/vz/Sources/DeskNet/Addresses.swift +106 -0
  40. package/vz/Sources/DeskNet/DHCP.swift +111 -0
  41. package/vz/Sources/DeskNet/Fence.swift +57 -0
  42. package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
  43. package/vz/Sources/DeskNet/Packets.swift +355 -0
  44. package/vz/Sources/DeskNet/Plumbing.swift +312 -0
  45. package/vz/Sources/DeskNet/Sockets.swift +221 -0
  46. package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
  47. package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
  48. package/vz/Sources/appkit-desk-vz/Helper.swift +505 -0
  49. package/vz/Sources/appkit-desk-vz/main.swift +38 -0
  50. package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
  51. package/vz/appkit-desk-vz.entitlements +8 -0
  52. package/vz/build.sh +23 -0
  53. package/vz/check/extract-boot.py +221 -0
@@ -0,0 +1,547 @@
1
+ import Darwin
2
+ import Dispatch
3
+
4
+ public struct NetworkConfiguration: Sendable {
5
+ public var guestAddress: IPv4
6
+ public var gatewayAddress: IPv4
7
+ public var prefixLength: Int
8
+ /// Resolvers handed to the guest over DHCP. The first also answers DNS sent
9
+ /// to the gateway address, for guests configured to use it.
10
+ public var dns: [IPv4]
11
+ /// The guest NIC's address, when known up front; otherwise learned.
12
+ public var guestMAC: MAC?
13
+ public var allowLan: Bool
14
+
15
+ public init(
16
+ guestAddress: IPv4 = IPv4(10, 0, 2, 15), gatewayAddress: IPv4 = IPv4(10, 0, 2, 2),
17
+ prefixLength: Int = 24, dns: [IPv4] = [IPv4(1, 1, 1, 1), IPv4(9, 9, 9, 9)], guestMAC: MAC? = nil,
18
+ allowLan: Bool = false
19
+ ) {
20
+ self.guestAddress = guestAddress
21
+ self.gatewayAddress = gatewayAddress
22
+ self.prefixLength = prefixLength
23
+ self.dns = dns
24
+ self.guestMAC = guestMAC
25
+ self.allowLan = allowLan
26
+ }
27
+ }
28
+
29
+ public func monotonicMilliseconds() -> UInt64 {
30
+ DispatchTime.now().uptimeNanoseconds / 1_000_000
31
+ }
32
+
33
+ /// A NAT between one guest and the Mac's own sockets.
34
+ ///
35
+ /// Frames from the guest arrive through `receiveFrame`; frames for the guest
36
+ /// leave through `sendFrame`. In between, the gateway answers ARP and DHCP on
37
+ /// the link, and every TCP connection, UDP exchange and ICMP echo the guest
38
+ /// starts is re-originated from a socket on the Mac — after the fence has
39
+ /// approved its destination. Nothing the guest sends is ever forwarded as a
40
+ /// packet, so nothing it sends can reach a destination the fence refused.
41
+ ///
42
+ /// The Mac reaches guest TCP ports through `connectToGuest`, which opens a
43
+ /// connection from the gateway address to the guest.
44
+ @MainActor
45
+ public final class NetworkStack {
46
+ public let configuration: NetworkConfiguration
47
+ public var onGuestAddress: ((IPv4) -> Void)?
48
+ public let now: () -> UInt64
49
+
50
+ private let dialer: Dialer
51
+ private let fence: EgressFence
52
+ private let hostAddresses: () -> Set<IPv4>
53
+ private let sendFrame: ([UInt8]) -> Void
54
+ private let dhcp: DHCPServer
55
+ private var guestMAC: MAC?
56
+ private var announcedGuest = false
57
+ private var tcpFlows: [FlowKey: TCPFlow] = [:]
58
+ private var udpFlows: [FlowKey: UDPFlow] = [:]
59
+ private var echoFlows: [EchoKey: EchoFlow] = [:]
60
+ private var nextEphemeralPort: UInt16 = 49_152
61
+ private var timer: DispatchSourceTimer?
62
+ private var identification: UInt16 = 0
63
+
64
+ static let maximumTCPFlows = 4_096
65
+ static let maximumUDPFlows = 1_024
66
+ static let maximumEchoFlows = 64
67
+
68
+ public init(
69
+ configuration: NetworkConfiguration, dialer: Dialer = SystemDialer(),
70
+ now: @escaping () -> UInt64 = monotonicMilliseconds,
71
+ hostAddresses: @escaping () -> Set<IPv4> = Socket.hostAddresses,
72
+ sendFrame: @escaping ([UInt8]) -> Void
73
+ ) {
74
+ self.configuration = configuration
75
+ self.dialer = dialer
76
+ self.now = now
77
+ self.hostAddresses = hostAddresses
78
+ self.sendFrame = sendFrame
79
+ fence = EgressFence(allowLan: configuration.allowLan)
80
+ guestMAC = configuration.guestMAC
81
+ dhcp = DHCPServer(
82
+ guest: configuration.guestAddress, gateway: configuration.gatewayAddress,
83
+ prefixLength: configuration.prefixLength, dns: configuration.dns)
84
+ }
85
+
86
+ public var flowCount: Int { tcpFlows.count + udpFlows.count + echoFlows.count }
87
+
88
+ // MARK: Inbound frames
89
+
90
+ public func receiveFrame(_ bytes: [UInt8]) {
91
+ guard let frame = EthernetFrame(parsing: bytes) else { return }
92
+ guard frame.destination == .gateway || frame.destination.isBroadcast else { return }
93
+ if guestMAC == nil { guestMAC = frame.source }
94
+ switch frame.type {
95
+ case EtherType.arp:
96
+ if let arp = ARPPacket(parsing: frame.payload) { receiveARP(arp, from: frame.source) }
97
+ case EtherType.ipv4:
98
+ if let packet = IPv4Packet(parsing: frame.payload) { receiveIPv4(packet, from: frame.source) }
99
+ default:
100
+ // IPv6 and everything else stop here: the guest gets no route for it.
101
+ break
102
+ }
103
+ }
104
+
105
+ private func receiveARP(_ arp: ARPPacket, from source: MAC) {
106
+ guard arp.operation == ARPPacket.request else { return }
107
+ // Answer for every address but the guest's own. The guest then sends all
108
+ // its traffic to the gateway whether it thinks a destination is on-link
109
+ // or not, and the fence judges it there. Probes from 0.0.0.0 are the
110
+ // guest checking its own address is free, and must go unanswered.
111
+ guard arp.senderIP != .zero, arp.targetIP != arp.senderIP,
112
+ arp.targetIP != configuration.guestAddress
113
+ else { return }
114
+ let reply = ARPPacket(
115
+ operation: ARPPacket.reply, senderMAC: .gateway, senderIP: arp.targetIP,
116
+ targetMAC: arp.senderMAC, targetIP: arp.senderIP)
117
+ sendFrame(EthernetFrame(destination: source, source: .gateway, type: EtherType.arp, payload: reply.encoded).encoded)
118
+ }
119
+
120
+ private func receiveIPv4(_ packet: IPv4Packet, from source: MAC) {
121
+ if packet.proto == IPProtocol.udp, let udp = UDPDatagram(parsing: packet.payload),
122
+ udp.destinationPort == 67, udp.sourcePort == 68
123
+ {
124
+ receiveDHCP(udp, from: source)
125
+ return
126
+ }
127
+ // One guest, one address: anything else is spoofed or misconfigured.
128
+ guard packet.source == configuration.guestAddress, !packet.isFragment else { return }
129
+ announceGuest()
130
+ let destination = packet.destination
131
+ if destination == configuration.gatewayAddress {
132
+ receiveForGateway(packet)
133
+ return
134
+ }
135
+ switch fence.verdict(for: destination, hostAddresses: hostAddresses()) {
136
+ case .allowed:
137
+ break
138
+ case .invalid:
139
+ return
140
+ case .fenced:
141
+ sendUnreachable(ICMPMessage.administrativelyProhibited, quoting: packet)
142
+ return
143
+ }
144
+ switch packet.proto {
145
+ case IPProtocol.tcp:
146
+ if let segment = TCPSegment(parsing: packet.payload) { receiveTCP(segment, packet: packet) }
147
+ case IPProtocol.udp:
148
+ if let datagram = UDPDatagram(parsing: packet.payload) { receiveUDP(datagram, packet: packet, upstream: destination) }
149
+ case IPProtocol.icmp:
150
+ if let message = ICMPMessage(parsing: packet.payload), message.type == ICMPMessage.echoRequest {
151
+ receiveEcho(message, destination: destination)
152
+ }
153
+ default:
154
+ sendUnreachable(ICMPMessage.administrativelyProhibited, quoting: packet)
155
+ }
156
+ }
157
+
158
+ private func announceGuest() {
159
+ guard !announcedGuest else { return }
160
+ announcedGuest = true
161
+ onGuestAddress?(configuration.guestAddress)
162
+ }
163
+
164
+ private func receiveDHCP(_ datagram: UDPDatagram, from source: MAC) {
165
+ guard let reply = dhcp.respond(to: datagram.payload) else { return }
166
+ let udp = UDPDatagram(sourcePort: 67, destinationPort: 68, payload: reply.payload)
167
+ let ip = IPv4Packet.encode(
168
+ source: configuration.gatewayAddress, destination: .broadcast, proto: IPProtocol.udp,
169
+ payload: udp.encoded(source: configuration.gatewayAddress, destination: .broadcast),
170
+ identification: nextIdentification())
171
+ sendFrame(EthernetFrame(destination: reply.clientMAC, source: .gateway, type: EtherType.ipv4, payload: ip).encoded)
172
+ if reply.assigned { announceGuest() }
173
+ }
174
+
175
+ /// The gateway is the Mac's face on the link, so it offers nothing but a
176
+ /// ping reply and a DNS forwarder: no port on it reaches a Mac service.
177
+ private func receiveForGateway(_ packet: IPv4Packet) {
178
+ switch packet.proto {
179
+ case IPProtocol.icmp:
180
+ guard let message = ICMPMessage(parsing: packet.payload), message.type == ICMPMessage.echoRequest else { return }
181
+ let reply = ICMPMessage(type: ICMPMessage.echoReply, code: 0, rest: message.rest, payload: message.payload)
182
+ sendIPv4(reply.encoded, proto: IPProtocol.icmp, from: configuration.gatewayAddress)
183
+ case IPProtocol.tcp:
184
+ if let segment = TCPSegment(parsing: packet.payload) {
185
+ let key = FlowKey(guestPort: segment.sourcePort, remote: packet.destination, remotePort: segment.destinationPort)
186
+ if let flow = tcpFlows[key] {
187
+ flow.receive(segment)
188
+ } else {
189
+ sendReset(answering: segment, key: key)
190
+ }
191
+ }
192
+ case IPProtocol.udp:
193
+ guard let datagram = UDPDatagram(parsing: packet.payload) else { return }
194
+ if datagram.destinationPort == 53, let resolver = configuration.dns.first {
195
+ guard fence.verdict(for: resolver, hostAddresses: hostAddresses()) == .allowed else {
196
+ sendUnreachable(ICMPMessage.administrativelyProhibited, quoting: packet)
197
+ return
198
+ }
199
+ receiveUDP(datagram, packet: packet, upstream: resolver)
200
+ } else {
201
+ sendUnreachable(ICMPMessage.portUnreachable, quoting: packet)
202
+ }
203
+ default:
204
+ return
205
+ }
206
+ }
207
+
208
+ // MARK: TCP
209
+
210
+ private func receiveTCP(_ segment: TCPSegment, packet: IPv4Packet) {
211
+ let key = FlowKey(guestPort: segment.sourcePort, remote: packet.destination, remotePort: segment.destinationPort)
212
+ if let flow = tcpFlows[key] {
213
+ flow.receive(segment)
214
+ return
215
+ }
216
+ guard segment.flags.contains(.syn), !segment.flags.contains(.ack), !segment.flags.contains(.rst) else {
217
+ sendReset(answering: segment, key: key)
218
+ return
219
+ }
220
+ guard tcpFlows.count < Self.maximumTCPFlows else {
221
+ sendReset(answering: segment, key: key)
222
+ return
223
+ }
224
+ let fd: Int32
225
+ do {
226
+ fd = try dialer.tcp(to: packet.destination, port: segment.destinationPort)
227
+ } catch let failure as POSIXFailure {
228
+ reportUpstreamFailure(failure.code, key: key, quoting: packet, segment: segment)
229
+ return
230
+ } catch {
231
+ sendUnreachable(ICMPMessage.hostUnreachable, quoting: packet)
232
+ return
233
+ }
234
+ let flow = TCPFlow(stack: self, key: key, guest: configuration.guestAddress, syn: segment, hostFD: fd)
235
+ pendingSYNs[ObjectIdentifier(flow)] = packet
236
+ tcpFlows[key] = flow
237
+ startTimer()
238
+ }
239
+
240
+ /// The SYN each connecting flow came from, for quoting in an ICMP error.
241
+ private var pendingSYNs: [ObjectIdentifier: IPv4Packet] = [:]
242
+
243
+ func upstreamFailed(_ flow: TCPFlow, errno code: Int32) {
244
+ guard let packet = pendingSYNs.removeValue(forKey: ObjectIdentifier(flow)),
245
+ let segment = TCPSegment(parsing: packet.payload)
246
+ else { return }
247
+ reportUpstreamFailure(code, key: flow.key, quoting: packet, segment: segment)
248
+ }
249
+
250
+ private func reportUpstreamFailure(_ code: Int32, key: FlowKey, quoting packet: IPv4Packet, segment: TCPSegment) {
251
+ if code == ECONNREFUSED {
252
+ sendReset(answering: segment, key: key)
253
+ } else {
254
+ sendUnreachable(ICMPMessage.hostUnreachable, quoting: packet)
255
+ }
256
+ }
257
+
258
+ func sendReset(answering segment: TCPSegment, key: FlowKey) {
259
+ guard !segment.flags.contains(.rst) else { return }
260
+ let reset: TCPSegment
261
+ if segment.flags.contains(.ack) {
262
+ reset = TCPSegment(
263
+ sourcePort: key.remotePort, destinationPort: key.guestPort, sequence: segment.acknowledgement,
264
+ acknowledgement: 0, flags: [.rst], window: 0)
265
+ } else {
266
+ reset = TCPSegment(
267
+ sourcePort: key.remotePort, destinationPort: key.guestPort, sequence: 0,
268
+ acknowledgement: segment.sequence &+ segment.sequenceLength, flags: [.rst, .ack], window: 0)
269
+ }
270
+ sendTCP(reset, from: key.remote, to: configuration.guestAddress)
271
+ }
272
+
273
+ func sendTCP(_ segment: TCPSegment, from source: IPv4, to destination: IPv4) {
274
+ sendIPv4(segment.encoded(source: source, destination: destination), proto: IPProtocol.tcp, from: source)
275
+ }
276
+
277
+ func flowClosed(_ flow: TCPFlow) {
278
+ pendingSYNs.removeValue(forKey: ObjectIdentifier(flow))
279
+ if tcpFlows[flow.key] === flow { tcpFlows.removeValue(forKey: flow.key) }
280
+ }
281
+
282
+ /// Open a TCP connection from the gateway to `port` on the guest, relayed to
283
+ /// `clientFD`. Once the guest accepts, `OK <local port>\n` is written to the
284
+ /// client and the descriptor carries the connection; a refusal or a guest
285
+ /// that never answers closes it with nothing written. `early` is client data
286
+ /// that arrived with the request and is delivered first.
287
+ public func connectToGuest(port: UInt16, clientFD: Int32, early: [UInt8] = []) {
288
+ guard tcpFlows.count < Self.maximumTCPFlows, let localPort = allocateEphemeralPort() else {
289
+ Darwin.close(clientFD)
290
+ return
291
+ }
292
+ let key = FlowKey(guestPort: port, remote: configuration.gatewayAddress, remotePort: localPort)
293
+ Socket.setNonBlocking(clientFD)
294
+ Socket.setNoSigPipe(clientFD)
295
+ let flow = TCPFlow(
296
+ stack: self, key: key, guest: configuration.guestAddress, clientFD: clientFD,
297
+ ready: Array("OK \(localPort)\n".utf8), early: early)
298
+ if flow.state != .closed { tcpFlows[key] = flow }
299
+ startTimer()
300
+ }
301
+
302
+ private func allocateEphemeralPort() -> UInt16? {
303
+ for _ in 0..<16_384 {
304
+ let port = nextEphemeralPort
305
+ nextEphemeralPort = nextEphemeralPort == 65_535 ? 49_152 : nextEphemeralPort + 1
306
+ let inUse = tcpFlows.keys.contains { $0.remote == configuration.gatewayAddress && $0.remotePort == port }
307
+ if !inUse { return port }
308
+ }
309
+ return nil
310
+ }
311
+
312
+ // MARK: UDP
313
+
314
+ private func receiveUDP(_ datagram: UDPDatagram, packet: IPv4Packet, upstream: IPv4) {
315
+ let key = FlowKey(guestPort: datagram.sourcePort, remote: packet.destination, remotePort: datagram.destinationPort)
316
+ let flow: UDPFlow
317
+ if let existing = udpFlows[key] {
318
+ flow = existing
319
+ } else {
320
+ if udpFlows.count >= Self.maximumUDPFlows, let oldest = udpFlows.values.min(by: { $0.lastActive < $1.lastActive }) {
321
+ oldest.close()
322
+ }
323
+ let fd: Int32
324
+ do {
325
+ fd = try dialer.udp(to: upstream, port: datagram.destinationPort)
326
+ } catch {
327
+ sendUnreachable(ICMPMessage.hostUnreachable, quoting: packet)
328
+ return
329
+ }
330
+ flow = UDPFlow(stack: self, key: key, fd: fd)
331
+ udpFlows[key] = flow
332
+ startTimer()
333
+ }
334
+ flow.send(datagram.payload, quoting: packet)
335
+ }
336
+
337
+ func sendUDP(_ payload: [UInt8], key: FlowKey) {
338
+ let datagram = UDPDatagram(sourcePort: key.remotePort, destinationPort: key.guestPort, payload: payload)
339
+ sendIPv4(
340
+ datagram.encoded(source: key.remote, destination: configuration.guestAddress), proto: IPProtocol.udp,
341
+ from: key.remote)
342
+ }
343
+
344
+ func udpFlowClosed(_ flow: UDPFlow) {
345
+ if udpFlows[flow.key] === flow { udpFlows.removeValue(forKey: flow.key) }
346
+ }
347
+
348
+ // MARK: ICMP
349
+
350
+ private func receiveEcho(_ message: ICMPMessage, destination: IPv4) {
351
+ let key = EchoKey(remote: destination, identifier: message.identifier)
352
+ let flow: EchoFlow
353
+ if let existing = echoFlows[key] {
354
+ flow = existing
355
+ } else {
356
+ guard echoFlows.count < Self.maximumEchoFlows else { return }
357
+ let fd: Int32
358
+ do {
359
+ fd = try dialer.icmp()
360
+ } catch {
361
+ return
362
+ }
363
+ flow = EchoFlow(stack: self, key: key, fd: fd)
364
+ echoFlows[key] = flow
365
+ startTimer()
366
+ }
367
+ flow.send(message)
368
+ }
369
+
370
+ func sendEchoReply(_ message: ICMPMessage, key: EchoKey) {
371
+ sendIPv4(message.encoded, proto: IPProtocol.icmp, from: key.remote)
372
+ }
373
+
374
+ func echoFlowClosed(_ flow: EchoFlow) {
375
+ if echoFlows[flow.key] === flow { echoFlows.removeValue(forKey: flow.key) }
376
+ }
377
+
378
+ func sendUnreachable(_ code: UInt8, quoting packet: IPv4Packet) {
379
+ let quoted = packet.header + Array(packet.payload.prefix(8))
380
+ let message = ICMPMessage(type: ICMPMessage.destinationUnreachable, code: code, rest: 0, payload: quoted)
381
+ sendIPv4(message.encoded, proto: IPProtocol.icmp, from: configuration.gatewayAddress)
382
+ }
383
+
384
+ // MARK: Output
385
+
386
+ private func nextIdentification() -> UInt16 {
387
+ identification &+= 1
388
+ return identification
389
+ }
390
+
391
+ private func sendIPv4(_ payload: [UInt8], proto: UInt8, from source: IPv4) {
392
+ let packet = IPv4Packet.encode(
393
+ source: source, destination: configuration.guestAddress, proto: proto, payload: payload,
394
+ identification: nextIdentification())
395
+ let destination = guestMAC ?? .broadcast
396
+ sendFrame(EthernetFrame(destination: destination, source: .gateway, type: EtherType.ipv4, payload: packet).encoded)
397
+ }
398
+
399
+ // MARK: Timers
400
+
401
+ /// Retransmissions and idle expiry. Runs only while there are flows, so an
402
+ /// idle guest costs the Mac no wakeups.
403
+ public func tick() {
404
+ let current = now()
405
+ for flow in Array(tcpFlows.values) { flow.tick(now: current) }
406
+ for flow in Array(udpFlows.values) where flow.expired(now: current) { flow.close() }
407
+ for flow in Array(echoFlows.values) where flow.expired(now: current) { flow.close() }
408
+ if flowCount == 0 { stopTimer() }
409
+ }
410
+
411
+ private func startTimer() {
412
+ guard timer == nil else { return }
413
+ let source = DispatchSource.makeTimerSource(queue: .main)
414
+ source.schedule(deadline: .now() + .milliseconds(50), repeating: .milliseconds(50), leeway: .milliseconds(10))
415
+ source.setEventHandler { @Sendable [weak self] in Task { @MainActor in self?.tick() } }
416
+ source.resume()
417
+ timer = source
418
+ }
419
+
420
+ private func stopTimer() {
421
+ timer?.cancel()
422
+ timer = nil
423
+ }
424
+
425
+ /// Drop every connection, resetting the host side of each.
426
+ public func shutdown() {
427
+ for flow in Array(tcpFlows.values) { flow.abort() }
428
+ for flow in Array(udpFlows.values) { flow.close() }
429
+ for flow in Array(echoFlows.values) { flow.close() }
430
+ stopTimer()
431
+ }
432
+ }
433
+
434
+ // MARK: - UDP and ICMP flows
435
+
436
+ @MainActor
437
+ final class UDPFlow {
438
+ let key: FlowKey
439
+ private(set) var lastActive: UInt64
440
+ private unowned let stack: NetworkStack
441
+ private let socket: FDWatcher
442
+ private var lastQuote: IPv4Packet?
443
+
444
+ init(stack: NetworkStack, key: FlowKey, fd: Int32) {
445
+ self.stack = stack
446
+ self.key = key
447
+ socket = FDWatcher(fd: fd)
448
+ lastActive = stack.now()
449
+ socket.onReadable = { [weak self] in self?.readable() }
450
+ socket.wantRead(true)
451
+ }
452
+
453
+ /// DNS answers come back at once or not at all; anything else may be a
454
+ /// long-lived exchange such as QUIC.
455
+ private var idleLimit: UInt64 { key.remotePort == 53 ? 15_000 : 120_000 }
456
+
457
+ func expired(now: UInt64) -> Bool { now - lastActive > idleLimit }
458
+
459
+ func send(_ payload: [UInt8], quoting packet: IPv4Packet) {
460
+ lastActive = stack.now()
461
+ lastQuote = packet
462
+ _ = payload.withUnsafeBytes { Darwin.send(socket.fd, $0.baseAddress, payload.count, 0) }
463
+ }
464
+
465
+ private func readable() {
466
+ var buffer = [UInt8](repeating: 0, count: 65_536)
467
+ while true {
468
+ let count = buffer.withUnsafeMutableBytes { Darwin.recv(socket.fd, $0.baseAddress, $0.count, 0) }
469
+ if count < 0 {
470
+ if errno == ECONNREFUSED, let quote = lastQuote {
471
+ stack.sendUnreachable(ICMPMessage.portUnreachable, quoting: quote)
472
+ continue
473
+ }
474
+ return
475
+ }
476
+ lastActive = stack.now()
477
+ stack.sendUDP(Array(buffer[0..<count]), key: key)
478
+ }
479
+ }
480
+
481
+ func close() {
482
+ socket.close()
483
+ stack.udpFlowClosed(self)
484
+ }
485
+ }
486
+
487
+ struct EchoKey: Hashable {
488
+ let remote: IPv4
489
+ let identifier: UInt16
490
+ }
491
+
492
+ @MainActor
493
+ final class EchoFlow {
494
+ let key: EchoKey
495
+ private var lastActive: UInt64
496
+ private unowned let stack: NetworkStack
497
+ private let socket: FDWatcher
498
+
499
+ init(stack: NetworkStack, key: EchoKey, fd: Int32) {
500
+ self.stack = stack
501
+ self.key = key
502
+ socket = FDWatcher(fd: fd)
503
+ lastActive = stack.now()
504
+ socket.onReadable = { [weak self] in self?.readable() }
505
+ socket.wantRead(true)
506
+ }
507
+
508
+ func expired(now: UInt64) -> Bool { now - lastActive > 10_000 }
509
+
510
+ func send(_ message: ICMPMessage) {
511
+ lastActive = stack.now()
512
+ let bytes = message.encoded
513
+ var address = Socket.address(key.remote, 0)
514
+ _ = withUnsafePointer(to: &address) { pointer in
515
+ pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
516
+ bytes.withUnsafeBytes {
517
+ Darwin.sendto(socket.fd, $0.baseAddress, bytes.count, 0, sockaddrPointer, socklen_t(MemoryLayout<sockaddr_in>.size))
518
+ }
519
+ }
520
+ }
521
+ }
522
+
523
+ private func readable() {
524
+ var buffer = [UInt8](repeating: 0, count: 65_536)
525
+ while true {
526
+ let count = buffer.withUnsafeMutableBytes { Darwin.recv(socket.fd, $0.baseAddress, $0.count, 0) }
527
+ if count <= 0 { return }
528
+ var bytes = Array(buffer[0..<count])
529
+ // Darwin hands datagram ICMP sockets the IP header too.
530
+ if bytes.count >= 20, bytes[0] >> 4 == 4 {
531
+ let headerLength = Int(bytes[0] & 0x0f) * 4
532
+ guard bytes.count >= headerLength + 8 else { continue }
533
+ bytes = Array(bytes[headerLength...])
534
+ }
535
+ guard let reply = ICMPMessage(parsing: bytes), reply.type == ICMPMessage.echoReply else { continue }
536
+ lastActive = stack.now()
537
+ // The kernel may have chosen its own identifier; the guest gets its own back.
538
+ let rest = UInt32(key.identifier) << 16 | UInt32(reply.sequenceNumber)
539
+ stack.sendEchoReply(ICMPMessage(type: ICMPMessage.echoReply, code: 0, rest: rest, payload: reply.payload), key: key)
540
+ }
541
+ }
542
+
543
+ func close() {
544
+ socket.close()
545
+ stack.echoFlowClosed(self)
546
+ }
547
+ }