@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.
- package/README.md +126 -1
- package/backend.d.ts +32 -7
- package/backend.d.ts.map +1 -1
- package/backend.js +107 -524
- package/backend.js.map +1 -1
- package/host.d.ts +27 -1
- package/host.d.ts.map +1 -1
- package/host.js +47 -19
- package/host.js.map +1 -1
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/machine.d.ts +154 -0
- package/machine.d.ts.map +1 -0
- package/machine.js +573 -0
- package/machine.js.map +1 -0
- package/package.json +2 -2
- package/plan.d.ts +192 -31
- package/plan.d.ts.map +1 -1
- package/plan.js +298 -12
- package/plan.js.map +1 -1
- package/port-relay.d.ts +13 -0
- package/port-relay.d.ts.map +1 -0
- package/port-relay.js +56 -0
- package/port-relay.js.map +1 -0
- package/virtualization.d.ts +16 -0
- package/virtualization.d.ts.map +1 -0
- package/virtualization.js +156 -0
- package/virtualization.js.map +1 -0
- package/vz/Package.swift +23 -0
- package/vz/Sources/DeskNet/Addresses.swift +106 -0
- package/vz/Sources/DeskNet/DHCP.swift +111 -0
- package/vz/Sources/DeskNet/Fence.swift +57 -0
- package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
- package/vz/Sources/DeskNet/Packets.swift +355 -0
- package/vz/Sources/DeskNet/Plumbing.swift +312 -0
- package/vz/Sources/DeskNet/Sockets.swift +221 -0
- package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
- package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
- package/vz/Sources/appkit-desk-vz/Helper.swift +550 -0
- package/vz/Sources/appkit-desk-vz/main.swift +38 -0
- package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
- package/vz/appkit-desk-vz.entitlements +8 -0
- package/vz/build.sh +23 -0
- 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
|
+
}
|