mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
Format the tunnel network, hierarchy and patcher sources
swiftformat output only: wrapped single-line bodies, sorted imports, dropped redundant self, throws and async, and plain numeric literals. No behavior change; VPhoneCoreKitTests and the touched FirmwarePatcherTests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,8 @@ import IcliKit
|
||||
import VphonedNative
|
||||
|
||||
extension GuestAPI {
|
||||
// AX enable/restore is process-wide. Interface operations share this lock,
|
||||
// so a flat query cannot race this opt-in native snapshot's restoration.
|
||||
/// AX enable/restore is process-wide. Interface operations share this lock,
|
||||
/// so a flat query cannot race this opt-in native snapshot's restoration.
|
||||
static let interfaceLock = NSRecursiveLock()
|
||||
|
||||
private static func hierarchyInteger(
|
||||
|
||||
@@ -77,7 +77,9 @@ extension GuestAPI {
|
||||
throw GuestAPIError.invalidRequest("nested must be a boolean")
|
||||
}
|
||||
}
|
||||
if bool(params, "nested") { return try nestedHierarchy(params) }
|
||||
if bool(params, "nested") {
|
||||
return try nestedHierarchy(params)
|
||||
}
|
||||
return try uiElements(
|
||||
maxElements: (params["max_elements"] as? NSNumber)?.intValue ?? 500,
|
||||
visibleOnly: bool(params, "visible_only", default: true),
|
||||
|
||||
+3
-1
@@ -133,7 +133,9 @@ public enum DyldSharedCacheMISTrustAuthPatcher {
|
||||
static let baseError: UInt32 = 0xE800_8001
|
||||
|
||||
/// The high half both seeds share, and the only part that is actually stable.
|
||||
static var errorHighHalf: Int64 { Int64((seededError >> 16) & 0xFFFF) }
|
||||
static var errorHighHalf: Int64 {
|
||||
Int64((seededError >> 16) & 0xFFFF)
|
||||
}
|
||||
|
||||
/// How far back from the string reference the function start may sit. The
|
||||
/// reference is in the last third of the function; 1024 instructions is
|
||||
|
||||
+6
-2
@@ -69,7 +69,9 @@ extension FirmwarePipeline {
|
||||
func pristineInput(for fileURL: URL) throws -> (url: URL, created: Bool) {
|
||||
guard let stashURL = originalURL(for: fileURL) else { return (fileURL, false) }
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: stashURL.path) { return (stashURL, false) }
|
||||
if fm.fileExists(atPath: stashURL.path) {
|
||||
return (stashURL, false)
|
||||
}
|
||||
try fm.createDirectory(at: stashURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try fm.copyItem(at: fileURL, to: stashURL)
|
||||
return (stashURL, true)
|
||||
@@ -88,7 +90,9 @@ extension FirmwarePipeline {
|
||||
guard let stashURL = originalURL(for: fileURL),
|
||||
fm.fileExists(atPath: stashURL.path)
|
||||
else { return false }
|
||||
if try filesMatch(stashURL, fileURL) { return false }
|
||||
if try filesMatch(stashURL, fileURL) {
|
||||
return false
|
||||
}
|
||||
if fm.fileExists(atPath: fileURL.path) {
|
||||
try fm.removeItem(at: fileURL)
|
||||
}
|
||||
|
||||
+1
@@ -598,6 +598,7 @@ struct DyldSharedCacheMISTrustAuthShapeDetectorTests {
|
||||
}
|
||||
|
||||
// MARK: - The 24A435 shape: a seeded base plus a derivation
|
||||
|
||||
//
|
||||
// Measured on a pristine `iPhone17,3_27.0_24A435` SystemOS cryptex, decrypted
|
||||
// and mounted read-only. `libmis` there does not materialise `0xE8008026`
|
||||
|
||||
+20
-7
@@ -29,7 +29,9 @@ private final class OnceOnlyBytePatcher: Patcher {
|
||||
let verbose = false
|
||||
let data: Data
|
||||
|
||||
init(data: Data) { self.data = data }
|
||||
init(data: Data) {
|
||||
self.data = data
|
||||
}
|
||||
|
||||
func findAll() throws -> [PatchRecord] {
|
||||
guard data.first == 0x00 else { return [] }
|
||||
@@ -45,14 +47,21 @@ private final class OnceOnlyBytePatcher: Patcher {
|
||||
]
|
||||
}
|
||||
|
||||
func apply() throws -> Int { 1 }
|
||||
func apply() throws -> Int {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes in, bytes out. The shipped loader repackages IM4P containers, which these
|
||||
/// synthetic files are not.
|
||||
private struct RawFirmwareLoader: FirmwarePipeline.FirmwareLoader {
|
||||
func load(from url: URL) throws -> Data { try Data(contentsOf: url) }
|
||||
func save(_ data: Data, to url: URL) throws { try data.write(to: url) }
|
||||
func load(from url: URL) throws -> Data {
|
||||
try Data(contentsOf: url)
|
||||
}
|
||||
|
||||
func save(_ data: Data, to url: URL) throws {
|
||||
try data.write(to: url)
|
||||
}
|
||||
}
|
||||
|
||||
/// A VM directory holding one restore tree with one patchable file in it.
|
||||
@@ -72,9 +81,13 @@ private struct FakeVM {
|
||||
try Data([0x00, 0x11, 0x22]).write(to: componentURL)
|
||||
}
|
||||
|
||||
func remove() { try? FileManager.default.removeItem(at: root) }
|
||||
func remove() {
|
||||
try? FileManager.default.removeItem(at: root)
|
||||
}
|
||||
|
||||
var componentBytes: Data { (try? Data(contentsOf: componentURL)) ?? Data() }
|
||||
var componentBytes: Data {
|
||||
(try? Data(contentsOf: componentURL)) ?? Data()
|
||||
}
|
||||
|
||||
var stashURL: URL {
|
||||
root.appendingPathComponent(FirmwarePipeline.originalsDirectoryName)
|
||||
@@ -209,7 +222,7 @@ struct FirmwarePipelineOriginalsTests {
|
||||
#expect(pipeline.originalURL(for: vm.stashURL) == nil)
|
||||
}
|
||||
|
||||
@Test func `only the two whole-tree components opt out of keeping an original`() throws {
|
||||
@Test func `only the two whole-tree components opt out of keeping an original`() {
|
||||
let root = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
let pipeline = FirmwarePipeline(vmDirectory: root, variant: .less, verbose: false)
|
||||
let components = pipeline.buildComponentList(restoreDir: root, iOSBase: VPhoneVersion("26.6.2"))
|
||||
|
||||
+3
-1
@@ -144,7 +144,9 @@ class VPhoneKeychainBrowserModel {
|
||||
let item: VPhoneKeychainItem
|
||||
var value: String
|
||||
|
||||
var id: VPhoneKeychainItem.ID { item.id }
|
||||
var id: VPhoneKeychainItem.ID {
|
||||
item.id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@@ -7,7 +7,9 @@ import Foundation
|
||||
public struct VPhoneIPv4Address: Sendable, Equatable, Hashable, CustomStringConvertible {
|
||||
public var raw: UInt32
|
||||
|
||||
public init(_ raw: UInt32) { self.raw = raw }
|
||||
public init(_ raw: UInt32) {
|
||||
self.raw = raw
|
||||
}
|
||||
|
||||
public init(_ a: UInt8, _ b: UInt8, _ c: UInt8, _ d: UInt8) {
|
||||
raw = UInt32(a) << 24 | UInt32(b) << 16 | UInt32(c) << 8 | UInt32(d)
|
||||
@@ -18,7 +20,9 @@ public struct VPhoneIPv4Address: Sendable, Equatable, Hashable, CustomStringConv
|
||||
UInt8(truncatingIfNeeded: raw >> 8), UInt8(truncatingIfNeeded: raw)]
|
||||
}
|
||||
|
||||
public var description: String { bytes.map(String.init).joined(separator: ".") }
|
||||
public var description: String {
|
||||
bytes.map(String.init).joined(separator: ".")
|
||||
}
|
||||
|
||||
/// `255.255.255.255`
|
||||
public static let broadcast = VPhoneIPv4Address(255, 255, 255, 255)
|
||||
@@ -72,13 +76,17 @@ public struct VPhoneUserspaceNetworkConfiguration: Sendable, Equatable {
|
||||
public struct VPhoneMACAddress: Sendable, Equatable, Hashable {
|
||||
public var bytes: [UInt8]
|
||||
|
||||
public init(_ bytes: [UInt8]) { self.bytes = bytes }
|
||||
public init(_ bytes: [UInt8]) {
|
||||
self.bytes = bytes
|
||||
}
|
||||
|
||||
/// The gateway's address. Locally administered, unicast, and unlikely to
|
||||
/// collide with anything.
|
||||
public static let gateway = VPhoneMACAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
|
||||
|
||||
var hexString: String { bytes.map { String(format: "%02x", $0) }.joined(separator: ":") }
|
||||
var hexString: String {
|
||||
bytes.map { String(format: "%02x", $0) }.joined(separator: ":")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Checksums
|
||||
@@ -92,8 +100,12 @@ enum VPhoneInternetChecksum {
|
||||
sum += UInt32(bytes[index]) << 8 | UInt32(bytes[index + 1])
|
||||
index += 2
|
||||
}
|
||||
if index < bytes.count { sum += UInt32(bytes[index]) << 8 }
|
||||
while sum >> 16 != 0 { sum = (sum & 0xFFFF) + (sum >> 16) }
|
||||
if index < bytes.count {
|
||||
sum += UInt32(bytes[index]) << 8
|
||||
}
|
||||
while sum >> 16 != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16)
|
||||
}
|
||||
return UInt16(~sum & 0xFFFF)
|
||||
}
|
||||
}
|
||||
@@ -204,12 +216,16 @@ struct VPhoneIPv4Packet {
|
||||
return fragments
|
||||
}
|
||||
|
||||
var totalLength: Int { 20 + payload.count }
|
||||
var totalLength: Int {
|
||||
20 + payload.count
|
||||
}
|
||||
|
||||
/// True when this is one piece of a larger datagram. Nothing here reassembles,
|
||||
/// so such a packet cannot be handled and has to be dropped rather than
|
||||
/// misread: only the first fragment even carries the transport header.
|
||||
var isFragment: Bool { moreFragments || fragmentOffset != 0 }
|
||||
var isFragment: Bool {
|
||||
moreFragments || fragmentOffset != 0
|
||||
}
|
||||
|
||||
/// Bit 0x2000 marks a fragment that is not the last; bits 0..12 hold the
|
||||
/// offset in eight-byte units. A single unfragmented packet leaves both zero.
|
||||
@@ -222,7 +238,7 @@ struct VPhoneIPv4Packet {
|
||||
0x45, 0x00,
|
||||
UInt8(truncatingIfNeeded: totalLength >> 8), UInt8(truncatingIfNeeded: totalLength),
|
||||
UInt8(truncatingIfNeeded: identification >> 8), UInt8(truncatingIfNeeded: identification),
|
||||
UInt8((flagsAndFragmentOffset) >> 8), UInt8(truncatingIfNeeded: flagsAndFragmentOffset),
|
||||
UInt8(flagsAndFragmentOffset >> 8), UInt8(truncatingIfNeeded: flagsAndFragmentOffset),
|
||||
ttl, proto,
|
||||
]
|
||||
header += [0, 0] // checksum placeholder
|
||||
@@ -387,10 +403,21 @@ struct VPhoneTCPSegment {
|
||||
self.advertisedWindowScale = advertisedWindowScale
|
||||
}
|
||||
|
||||
var hasSYN: Bool { flags & VPhoneTCPFlags.syn != 0 }
|
||||
var hasACK: Bool { flags & VPhoneTCPFlags.ack != 0 }
|
||||
var hasFIN: Bool { flags & VPhoneTCPFlags.fin != 0 }
|
||||
var hasRST: Bool { flags & VPhoneTCPFlags.rst != 0 }
|
||||
var hasSYN: Bool {
|
||||
flags & VPhoneTCPFlags.syn != 0
|
||||
}
|
||||
|
||||
var hasACK: Bool {
|
||||
flags & VPhoneTCPFlags.ack != 0
|
||||
}
|
||||
|
||||
var hasFIN: Bool {
|
||||
flags & VPhoneTCPFlags.fin != 0
|
||||
}
|
||||
|
||||
var hasRST: Bool {
|
||||
flags & VPhoneTCPFlags.rst != 0
|
||||
}
|
||||
|
||||
/// Sequence space this segment occupies. A SYN or FIN each cost one, which
|
||||
/// matters when acknowledging them.
|
||||
@@ -408,7 +435,9 @@ struct VPhoneTCPSegment {
|
||||
}
|
||||
if let advertisedWindowScale {
|
||||
options += [3, 3, UInt8(truncatingIfNeeded: advertisedWindowScale)]
|
||||
while options.count % 4 != 0 { options.append(1) } // NOP padding
|
||||
while options.count % 4 != 0 {
|
||||
options.append(1)
|
||||
} // NOP padding
|
||||
}
|
||||
let headerLength = 20 + options.count
|
||||
var header: [UInt8] = [
|
||||
@@ -465,8 +494,12 @@ struct VPhoneTCPSegment {
|
||||
var index = 0
|
||||
while index < options.count {
|
||||
let kind = options[index]
|
||||
if kind == 0 { break } // end of options
|
||||
if kind == 1 { index += 1; continue } // no-op padding
|
||||
if kind == 0 {
|
||||
break
|
||||
} // end of options
|
||||
if kind == 1 {
|
||||
index += 1; continue
|
||||
} // no-op padding
|
||||
guard index + 1 < options.count else { break }
|
||||
let length = Int(options[index + 1])
|
||||
guard length >= 2, index + length <= options.count else { break }
|
||||
|
||||
@@ -139,6 +139,7 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
var windowScaleNegotiated = false
|
||||
|
||||
// MARK: Sending toward the guest
|
||||
|
||||
//
|
||||
// The link to the guest is local and does not lose packets, so the only
|
||||
// way data can go missing is by sending more than the guest has buffer
|
||||
@@ -159,6 +160,7 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
var unacknowledgedBytes: Int {
|
||||
Int(Int32(bitPattern: localSequence &- sendUna))
|
||||
}
|
||||
|
||||
/// When the guest's acknowledgment last moved, so a stalled connection
|
||||
/// can be told from a slow one.
|
||||
var lastAckAdvance = Date()
|
||||
@@ -182,6 +184,7 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
var readSuspended = false
|
||||
|
||||
// MARK: Sending toward the host
|
||||
|
||||
//
|
||||
// The mirror of the problem above: the host's send buffer can fill too,
|
||||
// when the guest uploads faster than the server accepts. Waiting for
|
||||
@@ -210,8 +213,8 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
self.remoteSequence = remoteSequence
|
||||
self.peerMSS = peerMSS
|
||||
self.peerWindowScale = peerWindowScale
|
||||
self.sendWindowRight = localSequence
|
||||
self.sendUna = localSequence
|
||||
sendWindowRight = localSequence
|
||||
sendUna = localSequence
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +225,7 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
private var isStopped = false
|
||||
/// Source of initial sequence numbers. Only has to be unpredictable enough
|
||||
/// that two connections to the same peer do not look alike.
|
||||
private var sequenceCounter: UInt32 = UInt32.random(in: 0 ... UInt32.max)
|
||||
private var sequenceCounter: UInt32 = .random(in: 0 ... UInt32.max)
|
||||
|
||||
/// Marks `queue` as ours, so `connectionCount` can tell whether it is already
|
||||
/// on it rather than deadlocking against itself.
|
||||
@@ -240,7 +243,9 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
/// queue it reads directly. Hopping unconditionally is what deadlocked
|
||||
/// `start()` in the UDP forwarder.
|
||||
var connectionCount: Int {
|
||||
if DispatchQueue.getSpecific(key: Self.queueKey) != nil { return connections.count }
|
||||
if DispatchQueue.getSpecific(key: Self.queueKey) != nil {
|
||||
return connections.count
|
||||
}
|
||||
return queue.sync { connections.count }
|
||||
}
|
||||
|
||||
@@ -260,7 +265,9 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
isStopped = true
|
||||
reaper?.cancel()
|
||||
reaper = nil
|
||||
for connection in connections.values { close(connection) }
|
||||
for connection in connections.values {
|
||||
close(connection)
|
||||
}
|
||||
connections.removeAll()
|
||||
}
|
||||
|
||||
@@ -514,7 +521,9 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
windowSize: advertisedWindowField(connection),
|
||||
), connection: connection)
|
||||
finishIfBothClosed(connection)
|
||||
if connection.state == .closing, connection.pendingToGuest.isEmpty { return }
|
||||
if connection.state == .closing, connection.pendingToGuest.isEmpty {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Room again: resume a source that was paused for backpressure.
|
||||
@@ -622,7 +631,9 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
connection.lastActivity = Date()
|
||||
connection.pendingToGuest += buffer[0 ..< received]
|
||||
flushToGuest(connection)
|
||||
if connection.pendingToGuest.count >= Self.maxPendingToGuest { break }
|
||||
if connection.pendingToGuest.count >= Self.maxPendingToGuest {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if received == 0 {
|
||||
@@ -675,12 +686,16 @@ final class VPhoneTCPForwarder: @unchecked Sendable {
|
||||
|
||||
private func send(_ segment: VPhoneTCPSegment, connection: Connection) {
|
||||
if !segment.payload.isEmpty {
|
||||
if connection.sentNotAcked.isEmpty { connection.sentNotAckedSequence = segment.sequenceNumber }
|
||||
if connection.sentNotAcked.isEmpty {
|
||||
connection.sentNotAckedSequence = segment.sequenceNumber
|
||||
}
|
||||
connection.sentNotAcked += segment.payload
|
||||
connection.localSequence &+= UInt32(segment.payload.count)
|
||||
armRetransmission(connection)
|
||||
}
|
||||
if segment.hasSYN || segment.hasFIN { connection.localSequence &+= 1 }
|
||||
if segment.hasSYN || segment.hasFIN {
|
||||
connection.localSequence &+= 1
|
||||
}
|
||||
deliver(connection.flow, segment)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import SystemConfiguration
|
||||
import os
|
||||
import SystemConfiguration
|
||||
|
||||
// MARK: - Host resolver lookup
|
||||
|
||||
@@ -113,7 +113,9 @@ final class VPhoneUDPForwarder: @unchecked Sendable {
|
||||
/// queue it reads directly. Hopping unconditionally would deadlock, which is
|
||||
/// exactly the bug `start()` had.
|
||||
var sessionCount: Int {
|
||||
if DispatchQueue.getSpecific(key: Self.queueKey) != nil { return sessions.count }
|
||||
if DispatchQueue.getSpecific(key: Self.queueKey) != nil {
|
||||
return sessions.count
|
||||
}
|
||||
return queue.sync { sessions.count }
|
||||
}
|
||||
|
||||
@@ -133,7 +135,9 @@ final class VPhoneUDPForwarder: @unchecked Sendable {
|
||||
isStopped = true
|
||||
reaper?.cancel()
|
||||
reaper = nil
|
||||
for session in sessions.values { closeSession(session) }
|
||||
for session in sessions.values {
|
||||
closeSession(session)
|
||||
}
|
||||
sessions.removeAll()
|
||||
}
|
||||
|
||||
@@ -242,7 +246,9 @@ final class VPhoneUDPForwarder: @unchecked Sendable {
|
||||
let received = readBuffer.withUnsafeMutableBytes { raw in
|
||||
recv(session.socket, raw.baseAddress, raw.count, 0)
|
||||
}
|
||||
if received <= 0 { return } // EAGAIN, or an ICMP error on the flow
|
||||
if received <= 0 {
|
||||
return
|
||||
} // EAGAIN, or an ICMP error on the flow
|
||||
session.lastActivity = Date()
|
||||
deliver(session.flow, Array(readBuffer[0 ..< received]))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import Virtualization
|
||||
import os
|
||||
import Virtualization
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
@@ -21,7 +21,9 @@ extension VPhoneUserspaceNetworkError: CustomStringConvertible, LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
public var errorDescription: String? { description }
|
||||
public var errorDescription: String? {
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The device
|
||||
@@ -130,7 +132,9 @@ public final class VPhoneUserspaceNetwork: @unchecked Sendable {
|
||||
}
|
||||
|
||||
/// The object to hand to `VZVirtioNetworkDeviceConfiguration.attachment`.
|
||||
public var networkAttachment: VZNetworkDeviceAttachment { attachment }
|
||||
public var networkAttachment: VZNetworkDeviceAttachment {
|
||||
attachment
|
||||
}
|
||||
|
||||
/// Begin draining the guest's frames. Idempotent, and a no-op after `stop()`.
|
||||
public func start() {
|
||||
@@ -178,7 +182,9 @@ public final class VPhoneUserspaceNetwork: @unchecked Sendable {
|
||||
let received = frameBuffer.withUnsafeMutableBytes { raw in
|
||||
recv(socket, raw.baseAddress, raw.count, 0)
|
||||
}
|
||||
if received <= 0 { return } // EAGAIN once the queue is empty
|
||||
if received <= 0 {
|
||||
return
|
||||
} // EAGAIN once the queue is empty
|
||||
let frame = Array(frameBuffer[0 ..< received])
|
||||
switch responder.handle(frame) {
|
||||
case .drop:
|
||||
@@ -266,5 +272,4 @@ public final class VPhoneUserspaceNetwork: @unchecked Sendable {
|
||||
write(frame)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ struct VPhoneARPMessage {
|
||||
}
|
||||
|
||||
init(operation: UInt16, senderHardware: VPhoneMACAddress, senderProtocol: VPhoneIPv4Address,
|
||||
targetHardware: VPhoneMACAddress, targetProtocol: VPhoneIPv4Address) {
|
||||
targetHardware: VPhoneMACAddress, targetProtocol: VPhoneIPv4Address)
|
||||
{
|
||||
self.operation = operation
|
||||
self.senderHardware = senderHardware
|
||||
self.senderProtocol = senderProtocol
|
||||
@@ -83,18 +84,23 @@ struct VPhoneDHCPMessage {
|
||||
var index = 0
|
||||
while index < options.count {
|
||||
let code = options[index]
|
||||
if code == 255 { return nil }
|
||||
if code == 255 {
|
||||
return nil
|
||||
}
|
||||
guard index + 1 < options.count else { return nil }
|
||||
let length = Int(options[index + 1])
|
||||
guard index + 2 + length <= options.count else { return nil }
|
||||
if code == 53, length == 1 { return MessageType(rawValue: options[index + 2]) }
|
||||
if code == 53, length == 1 {
|
||||
return MessageType(rawValue: options[index + 2])
|
||||
}
|
||||
index += 2 + length
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
init(operation: UInt8, transactionID: UInt32, clientHardware: VPhoneMACAddress,
|
||||
broadcastFlag: Bool, options: [UInt8]) {
|
||||
broadcastFlag: Bool, options: [UInt8])
|
||||
{
|
||||
self.operation = operation
|
||||
self.transactionID = transactionID
|
||||
self.clientHardware = clientHardware
|
||||
@@ -119,7 +125,7 @@ struct VPhoneDHCPMessage {
|
||||
server: VPhoneIPv4Address,
|
||||
netmask: VPhoneIPv4Address,
|
||||
mtu: Int,
|
||||
leaseSeconds: UInt32 = 86_400,
|
||||
leaseSeconds: UInt32 = 86400,
|
||||
) -> [UInt8] {
|
||||
var out = [UInt8](repeating: 0, count: 236)
|
||||
out[0] = 2 // BOOTREPLY
|
||||
@@ -142,15 +148,17 @@ struct VPhoneDHCPMessage {
|
||||
options += [code, UInt8(values.count)] + values
|
||||
}
|
||||
append(53, [type.rawValue])
|
||||
append(54, server.bytes) // server identifier
|
||||
append(54, server.bytes) // server identifier
|
||||
append(51, withUnsafeBytes(of: leaseSeconds.bigEndian, Array.init)) // lease time
|
||||
append(1, netmask.bytes) // subnet mask
|
||||
append(3, server.bytes) // router
|
||||
append(6, server.bytes) // DNS
|
||||
append(26, [UInt8(mtu >> 8), UInt8(mtu & 0xFF)]) // interface MTU
|
||||
append(1, netmask.bytes) // subnet mask
|
||||
append(3, server.bytes) // router
|
||||
append(6, server.bytes) // DNS
|
||||
append(26, [UInt8(mtu >> 8), UInt8(mtu & 0xFF)]) // interface MTU
|
||||
options.append(255)
|
||||
// Pad to the minimum BOOTP payload so short replies stay well-formed.
|
||||
while (out.count + options.count) < 300 { options.append(0) }
|
||||
while (out.count + options.count) < 300 {
|
||||
options.append(0)
|
||||
}
|
||||
return out + options
|
||||
}
|
||||
}
|
||||
@@ -211,7 +219,9 @@ final class VPhoneUserspaceNetworkResponder {
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
var netmask: VPhoneIPv4Address { VPhoneIPv4Address(255, 255, 255, 0) }
|
||||
var netmask: VPhoneIPv4Address {
|
||||
VPhoneIPv4Address(255, 255, 255, 0)
|
||||
}
|
||||
|
||||
func handle(_ frame: [UInt8]) -> VPhoneUserspaceNetworkOutcome {
|
||||
guard let ethernet = VPhoneEthernetFrame(bytes: frame) else { return .drop }
|
||||
@@ -302,7 +312,9 @@ final class VPhoneUserspaceNetworkResponder {
|
||||
|
||||
// DHCP is the one UDP exchange this side finishes itself: the guest is
|
||||
// asking us, by definition.
|
||||
if let reply = respondToDHCP(packet, datagram) { return .reply(reply) }
|
||||
if let reply = respondToDHCP(packet, datagram) {
|
||||
return .reply(reply)
|
||||
}
|
||||
|
||||
// Everything else is egress. The answer has to reach the guest, so we
|
||||
// need its MAC — and until it has sent something we do not have it.
|
||||
@@ -320,7 +332,7 @@ final class VPhoneUserspaceNetworkResponder {
|
||||
}
|
||||
|
||||
/// Our own DHCP server, or nil when this is not a request we answer.
|
||||
private func respondToDHCP(_ packet: VPhoneIPv4Packet, _ datagram: VPhoneUDPDatagram) -> [UInt8]? {
|
||||
private func respondToDHCP(_: VPhoneIPv4Packet, _ datagram: VPhoneUDPDatagram) -> [UInt8]? {
|
||||
guard datagram.destinationPort == VPhoneDHCPMessage.serverPort,
|
||||
let request = VPhoneDHCPMessage(bytes: datagram.payload)
|
||||
else { return nil }
|
||||
|
||||
@@ -82,12 +82,12 @@ struct VPhoneInternetProtocolTests {
|
||||
let source = VPhoneIPv4Address(192, 168, 127, 3)
|
||||
let destination = VPhoneIPv4Address(142, 250, 1, 1)
|
||||
let segment = VPhoneTCPSegment(
|
||||
sourcePort: 51_000,
|
||||
sourcePort: 51000,
|
||||
destinationPort: 443,
|
||||
sequenceNumber: 1234,
|
||||
acknowledgmentNumber: 5678,
|
||||
flags: VPhoneTCPFlags.ack | VPhoneTCPFlags.psh,
|
||||
windowSize: 65_535,
|
||||
windowSize: 65535,
|
||||
payload: Array("hello".utf8),
|
||||
)
|
||||
let bytes = segment.bytes(source: source, destination: destination)
|
||||
@@ -109,7 +109,7 @@ struct VPhoneInternetProtocolTests {
|
||||
sequenceNumber: 0,
|
||||
acknowledgmentNumber: 0,
|
||||
flags: VPhoneTCPFlags.syn,
|
||||
windowSize: 65_535,
|
||||
windowSize: 65535,
|
||||
advertisedMSS: 1460,
|
||||
advertisedWindowScale: 7,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,9 @@ struct VPhoneTCPForwarderTests {
|
||||
private let lock = NSLock()
|
||||
private var value: Value
|
||||
|
||||
init(_ value: Value) { self.value = value }
|
||||
init(_ value: Value) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
func withLock<Result>(_ body: (inout Value) -> Result) -> Result {
|
||||
lock.lock()
|
||||
@@ -48,8 +50,8 @@ struct VPhoneTCPForwarderTests {
|
||||
init(guestPort: UInt16, destinationPort: UInt16, mss: Int = 1460) {
|
||||
self.guestPort = guestPort
|
||||
let queue = DispatchQueue(label: "forwarder-tests.\(guestPort)")
|
||||
let recorded = self.recorded
|
||||
let acknowledgesImmediately = self.acknowledgesImmediately
|
||||
let recorded = recorded
|
||||
let acknowledgesImmediately = acknowledgesImmediately
|
||||
let flow = VPhoneTCPFlow(
|
||||
sourceAddress: VPhoneUserspaceNetworkConfiguration.default.guestAddress,
|
||||
sourcePort: guestPort,
|
||||
@@ -72,7 +74,7 @@ struct VPhoneTCPForwarderTests {
|
||||
sequenceNumber: segment.sequenceNumber,
|
||||
acknowledgmentNumber: segment.sequenceNumber &+ UInt32(segment.payload.count),
|
||||
flags: VPhoneTCPFlags.ack,
|
||||
windowSize: 65_535,
|
||||
windowSize: 65535,
|
||||
)
|
||||
// Async rather than direct: `receive` re-enters the same object,
|
||||
// and this way the recursion depth stays bounded.
|
||||
@@ -82,13 +84,19 @@ struct VPhoneTCPForwarderTests {
|
||||
_ = mss
|
||||
}
|
||||
|
||||
func acknowledgeImmediately() { acknowledgesImmediately.withLock { $0 = true } }
|
||||
func acknowledgeImmediately() {
|
||||
acknowledgesImmediately.withLock { $0 = true }
|
||||
}
|
||||
|
||||
/// What the forwarder has put on the wire, oldest first.
|
||||
var sent: [VPhoneTCPSegment] { recorded.withLock { $0 } }
|
||||
var sent: [VPhoneTCPSegment] {
|
||||
recorded.withLock { $0 }
|
||||
}
|
||||
|
||||
/// Our ISN, read off the SYN-ACK the only way any peer could.
|
||||
var ourISN: UInt32? { sent.first { $0.hasSYN && $0.hasACK }?.sequenceNumber }
|
||||
var ourISN: UInt32? {
|
||||
sent.first { $0.hasSYN && $0.hasACK }?.sequenceNumber
|
||||
}
|
||||
|
||||
/// Bytes of *distinct* sequence numbers. A retransmission repeats a range
|
||||
/// rather than extending it, so counting transmissions would overcount.
|
||||
@@ -109,9 +117,17 @@ struct VPhoneTCPForwarderTests {
|
||||
return unique.sorted { $0.key < $1.key }.map { (sequence: $0.key, count: $0.value) }
|
||||
}
|
||||
|
||||
func feed(_ segment: VPhoneTCPSegment) { queue.sync { forwarder.receive(segment, for: flow) } }
|
||||
func start() { queue.sync { forwarder.start() } }
|
||||
func stop() { queue.sync { forwarder.stop() } }
|
||||
func feed(_ segment: VPhoneTCPSegment) {
|
||||
queue.sync { forwarder.receive(segment, for: flow) }
|
||||
}
|
||||
|
||||
func start() {
|
||||
queue.sync { forwarder.start() }
|
||||
}
|
||||
|
||||
func stop() {
|
||||
queue.sync { forwarder.stop() }
|
||||
}
|
||||
|
||||
/// Wait for something a background socket is responsible for.
|
||||
///
|
||||
@@ -121,8 +137,10 @@ struct VPhoneTCPForwarderTests {
|
||||
func waitUntil(_ what: String, timeout: TimeInterval = 8, _ predicate: () -> Bool) -> Bool {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
if predicate() { return true }
|
||||
usleep(10_000)
|
||||
if predicate() {
|
||||
return true
|
||||
}
|
||||
usleep(10000)
|
||||
}
|
||||
Issue.record("timed out waiting for \(what)")
|
||||
return false
|
||||
@@ -180,31 +198,42 @@ struct VPhoneTCPForwarderTests {
|
||||
}
|
||||
|
||||
/// Bytes pulled out of the socket by the session.
|
||||
var bytesReceived: Int { received.withLock { $0 } }
|
||||
var bytesReceived: Int {
|
||||
received.withLock { $0 }
|
||||
}
|
||||
|
||||
/// Whether the session has returned.
|
||||
var isFinished: Bool { finished.withLock { $0 } }
|
||||
var isFinished: Bool {
|
||||
finished.withLock { $0 }
|
||||
}
|
||||
|
||||
/// Read and count everything the guest sends.
|
||||
func drain(_ client: Int32) {
|
||||
var buffer = [UInt8](repeating: 0, count: 65_536)
|
||||
var buffer = [UInt8](repeating: 0, count: 65536)
|
||||
while true {
|
||||
let read = recv(client, &buffer, buffer.count, 0)
|
||||
if read <= 0 { return }
|
||||
if read <= 0 {
|
||||
return
|
||||
}
|
||||
received.withLock { $0 += read }
|
||||
}
|
||||
}
|
||||
|
||||
/// Read and count, stopping early once `target` bytes have arrived.
|
||||
func drain(_ client: Int32, until target: Int) {
|
||||
var buffer = [UInt8](repeating: 0, count: 65_536)
|
||||
var buffer = [UInt8](repeating: 0, count: 65536)
|
||||
while bytesReceived < target {
|
||||
let read = recv(client, &buffer, buffer.count, 0)
|
||||
if read <= 0 { return }
|
||||
if read <= 0 {
|
||||
return
|
||||
}
|
||||
received.withLock { $0 += read }
|
||||
}
|
||||
}
|
||||
|
||||
func stop() { close(listener) }
|
||||
func stop() {
|
||||
close(listener)
|
||||
}
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case socketFailed
|
||||
@@ -218,7 +247,7 @@ struct VPhoneTCPForwarderTests {
|
||||
private func handshake(
|
||||
_ harness: Harness,
|
||||
guestSequence: UInt32 = 1000,
|
||||
window: UInt16 = 65_535,
|
||||
window: UInt16 = 65535,
|
||||
mss: Int = 1460,
|
||||
windowScale: Int? = nil,
|
||||
) throws -> UInt32 {
|
||||
@@ -246,7 +275,7 @@ struct VPhoneTCPForwarderTests {
|
||||
}
|
||||
|
||||
/// Ask the host for data: a push with the peek byte.
|
||||
private func request(_ harness: Harness, acknowledged: UInt32, window: UInt16 = 65_535) {
|
||||
private func request(_ harness: Harness, acknowledged: UInt32, window: UInt16 = 65535) {
|
||||
harness.feed(VPhoneTCPSegment(
|
||||
sourcePort: harness.guestPort,
|
||||
destinationPort: harness.flow.destinationPort,
|
||||
@@ -272,14 +301,16 @@ struct VPhoneTCPForwarderTests {
|
||||
var sent = 0
|
||||
while sent < payload.count {
|
||||
let written = payload.withUnsafeBytes { send(client, $0.baseAddress, $0.count, 0) }
|
||||
if written <= 0 { break }
|
||||
if written <= 0 {
|
||||
break
|
||||
}
|
||||
sent += written
|
||||
}
|
||||
_ = server.drain(client)
|
||||
}
|
||||
defer { server.stop() }
|
||||
|
||||
let harness = Harness(guestPort: 51_001, destinationPort: server.port)
|
||||
let harness = Harness(guestPort: 51001, destinationPort: server.port)
|
||||
harness.start()
|
||||
defer { harness.stop() }
|
||||
|
||||
@@ -312,7 +343,9 @@ struct VPhoneTCPForwarderTests {
|
||||
))
|
||||
harness.waitUntil("more data", timeout: 3) { harness.sentDataBytes > before }
|
||||
acknowledged = isn &+ 1 &+ UInt32(harness.sentDataBytes)
|
||||
if harness.sentDataBytes >= total { break }
|
||||
if harness.sentDataBytes >= total {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
#expect(harness.sentDataBytes >= total, "stalled at \(harness.sentDataBytes)B of \(total)B")
|
||||
@@ -338,7 +371,7 @@ struct VPhoneTCPForwarderTests {
|
||||
}
|
||||
defer { server.stop() }
|
||||
|
||||
let harness = Harness(guestPort: 51_002, destinationPort: server.port)
|
||||
let harness = Harness(guestPort: 51002, destinationPort: server.port)
|
||||
harness.start()
|
||||
defer { harness.stop() }
|
||||
|
||||
@@ -356,7 +389,7 @@ struct VPhoneTCPForwarderTests {
|
||||
sequenceNumber: sequence,
|
||||
acknowledgmentNumber: isn &+ 1,
|
||||
flags: VPhoneTCPFlags.ack | VPhoneTCPFlags.psh,
|
||||
windowSize: 65_535,
|
||||
windowSize: 65535,
|
||||
payload: Array(chunk.prefix(take)),
|
||||
))
|
||||
sequence &+= UInt32(take)
|
||||
@@ -384,13 +417,13 @@ struct VPhoneTCPForwarderTests {
|
||||
/// option 3 in our SYN-ACK must keep its window under 64 KiB however much
|
||||
/// buffer it has. That ceiling, divided by the round-trip time, is the most
|
||||
/// the connection can ever carry.
|
||||
@Test func `window scaling is negotiated and the guest's field read at its true size`() async throws {
|
||||
@Test func `window scaling is negotiated and the guest's field read at its true size`() throws {
|
||||
// The option survives a round trip before anything else is meaningful.
|
||||
let offered = VPhoneTCPSegment(
|
||||
sourcePort: 1, destinationPort: 2,
|
||||
sequenceNumber: 0, acknowledgmentNumber: 0,
|
||||
flags: VPhoneTCPFlags.syn,
|
||||
windowSize: 65_535,
|
||||
windowSize: 65535,
|
||||
advertisedMSS: 1460,
|
||||
advertisedWindowScale: 7,
|
||||
)
|
||||
@@ -404,7 +437,7 @@ struct VPhoneTCPForwarderTests {
|
||||
#expect(encoded[12] >> 4 == 7, "the option list must pad to a 32-bit boundary")
|
||||
|
||||
let total = 400_000
|
||||
let blob = [UInt8](repeating: 0x21, count: 65_536)
|
||||
let blob = [UInt8](repeating: 0x21, count: 65536)
|
||||
let server = try LoopbackServer { client, server in
|
||||
var buffer = [UInt8](repeating: 0, count: 4096)
|
||||
_ = recv(client, &buffer, buffer.count, 0)
|
||||
@@ -413,14 +446,16 @@ struct VPhoneTCPForwarderTests {
|
||||
let written = blob.withUnsafeBytes { raw in
|
||||
send(client, raw.baseAddress, min(raw.count, total - sent), 0)
|
||||
}
|
||||
if written <= 0 { break }
|
||||
if written <= 0 {
|
||||
break
|
||||
}
|
||||
sent += written
|
||||
}
|
||||
_ = server.drain(client)
|
||||
}
|
||||
defer { server.stop() }
|
||||
|
||||
let harness = Harness(guestPort: 51_003, destinationPort: server.port)
|
||||
let harness = Harness(guestPort: 51003, destinationPort: server.port)
|
||||
harness.start()
|
||||
defer { harness.stop() }
|
||||
|
||||
@@ -454,7 +489,7 @@ struct VPhoneTCPForwarderTests {
|
||||
/// stands. The link to the guest drops nothing by itself, but that is not the
|
||||
/// only way a segment goes unacknowledged, and with no copy kept there is
|
||||
/// nothing to send again -- the peer just waits for a timeout.
|
||||
@Test func `data the guest does not acknowledge is sent again`() async throws {
|
||||
@Test func `data the guest does not acknowledge is sent again`() throws {
|
||||
let payload = [UInt8](repeating: 0x77, count: 4000)
|
||||
let server = try LoopbackServer { client, server in
|
||||
var buffer = [UInt8](repeating: 0, count: 4096)
|
||||
@@ -464,7 +499,7 @@ struct VPhoneTCPForwarderTests {
|
||||
}
|
||||
defer { server.stop() }
|
||||
|
||||
let harness = Harness(guestPort: 51_004, destinationPort: server.port)
|
||||
let harness = Harness(guestPort: 51004, destinationPort: server.port)
|
||||
harness.start()
|
||||
defer { harness.stop() }
|
||||
|
||||
@@ -488,7 +523,7 @@ struct VPhoneTCPForwarderTests {
|
||||
/// change to windowing or retransmission has not made it one.
|
||||
@Test func `a promptly acknowledging guest gets the payload fast`() throws {
|
||||
let total = 4 << 20
|
||||
let blob = [UInt8](repeating: 0x33, count: 65_536)
|
||||
let blob = [UInt8](repeating: 0x33, count: 65536)
|
||||
let server = try LoopbackServer { client, server in
|
||||
var buffer = [UInt8](repeating: 0, count: 4096)
|
||||
_ = recv(client, &buffer, buffer.count, 0)
|
||||
@@ -497,14 +532,16 @@ struct VPhoneTCPForwarderTests {
|
||||
let written = blob.withUnsafeBytes { raw in
|
||||
send(client, raw.baseAddress, min(raw.count, total - sent), 0)
|
||||
}
|
||||
if written <= 0 { break }
|
||||
if written <= 0 {
|
||||
break
|
||||
}
|
||||
sent += written
|
||||
}
|
||||
_ = server.drain(client, until: total)
|
||||
}
|
||||
defer { server.stop() }
|
||||
|
||||
let harness = Harness(guestPort: 51_005, destinationPort: server.port)
|
||||
let harness = Harness(guestPort: 51005, destinationPort: server.port)
|
||||
harness.acknowledgeImmediately()
|
||||
harness.start()
|
||||
defer { harness.stop() }
|
||||
|
||||
@@ -40,8 +40,13 @@ struct VPhoneUserspaceNetworkIntegrationTests {
|
||||
)
|
||||
}
|
||||
|
||||
func start() { network.start() }
|
||||
func stop() { network.stop() }
|
||||
func start() {
|
||||
network.start()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
network.stop()
|
||||
}
|
||||
|
||||
/// Put a frame on the wire the way the attachment would.
|
||||
func write(_ frame: [UInt8]) {
|
||||
@@ -56,8 +61,10 @@ struct VPhoneUserspaceNetworkIntegrationTests {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
let count = recv(descriptor, &buffer, buffer.count, 0)
|
||||
if count > 0 { return Array(buffer[0 ..< count]) }
|
||||
usleep(5_000)
|
||||
if count > 0 {
|
||||
return Array(buffer[0 ..< count])
|
||||
}
|
||||
usleep(5000)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -70,7 +77,9 @@ struct VPhoneUserspaceNetworkIntegrationTests {
|
||||
private let lock = NSLock()
|
||||
private var value: Value
|
||||
|
||||
init(_ value: Value) { self.value = value }
|
||||
init(_ value: Value) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
func withLock<Result>(_ body: (inout Value) -> Result) -> Result {
|
||||
lock.lock()
|
||||
@@ -231,7 +240,7 @@ struct VPhoneUserspaceNetworkIntegrationTests {
|
||||
|
||||
let deadline = Date().addingTimeInterval(5)
|
||||
while Date() < deadline, !returned.withLock({ $0 }) {
|
||||
usleep(10_000)
|
||||
usleep(10000)
|
||||
}
|
||||
#expect(
|
||||
returned.withLock { $0 },
|
||||
|
||||
@@ -139,7 +139,9 @@ struct VPhoneUserspaceNetworkTests {
|
||||
while index + 1 < options.count, options[index] != 255 {
|
||||
let length = Int(options[index + 1])
|
||||
guard index + 2 + length <= options.count else { return nil }
|
||||
if options[index] == code { return Array(options[(index + 2) ..< (index + 2 + length)]) }
|
||||
if options[index] == code {
|
||||
return Array(options[(index + 2) ..< (index + 2 + length)])
|
||||
}
|
||||
index += 2 + length
|
||||
}
|
||||
return nil
|
||||
@@ -280,7 +282,7 @@ struct VPhoneUserspaceNetworkTests {
|
||||
/// UDP that is not DHCP is egress rather than something this side answers.
|
||||
/// The flow it names has to carry both ends and the guest's MAC, because the
|
||||
/// answer is built later, by the forwarder, with no access to this type.
|
||||
@Test func `UDP for somewhere else becomes a forward`() throws {
|
||||
@Test func `UDP for somewhere else becomes a forward`() {
|
||||
let responder = responder()
|
||||
// Learn the MAC first, the way a real guest's traffic would.
|
||||
_ = responder.handle(arpFrame(targeting: configuration.hostAddress))
|
||||
@@ -325,7 +327,7 @@ struct VPhoneUserspaceNetworkTests {
|
||||
|
||||
/// TCP is terminated, not relayed, so the whole segment goes to the
|
||||
/// forwarder along with the flow it belongs to.
|
||||
@Test func `TCP segment becomes a forwardTCP`() throws {
|
||||
@Test func `TCP segment becomes a forwardTCP`() {
|
||||
let responder = responder()
|
||||
let syn = VPhoneTCPSegment(
|
||||
sourcePort: 51000, destinationPort: 80, sequenceNumber: 1000,
|
||||
@@ -416,7 +418,9 @@ private extension VPhoneUserspaceNetworkResponder {
|
||||
/// Added when `respond(to:)` became `handle(_:)` returning an outcome, so
|
||||
/// the frame-level tests above kept reading the same way.
|
||||
func respond(to frame: [UInt8]) -> [UInt8]? {
|
||||
if case let .reply(reply) = handle(frame) { return reply }
|
||||
if case let .reply(reply) = handle(frame) {
|
||||
return reply
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user