From e182a7526e31872534e58b8d920cf3170580d929 Mon Sep 17 00:00:00 2001 From: Victor Muryn Date: Thu, 5 Mar 2026 12:35:51 +0200 Subject: [PATCH 1/2] added ability to get ip for AppleVirt machines --- .../UTMScriptingVirtualMachineImpl.swift | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/Scripting/UTMScriptingVirtualMachineImpl.swift b/Scripting/UTMScriptingVirtualMachineImpl.swift index 328a6e6ea..13ada2060 100644 --- a/Scripting/UTMScriptingVirtualMachineImpl.swift +++ b/Scripting/UTMScriptingVirtualMachineImpl.swift @@ -278,9 +278,25 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable { } } + @objc func queryIp(_ command: NSScriptCommand) { withScriptCommand(command) { [self] in - try await withGuestAgent { guestAgent in + // Apple Virtualization backend: no guest agent available, + // so mirror Tart's host-side DHCP lease resolver instead. + if let appleVM = vm as? UTMAppleVirtualMachine { + guard appleVM.state == .started else { + throw ScriptingError.notRunning + } + + guard let network = appleVM.config.networks.first else { + return [] + } + let macAddress = network.macAddress.lowercased() + return Self.ipFromARP(macAddress: macAddress) + } + + // Non-Apple backend (QEMU): use guest agent (existing code). + return try await withGuestAgent { guestAgent in let interfaces = try await guestAgent.guestNetworkGetInterfaces() var ipv4: [String] = [] var ipv6: [String] = [] @@ -303,6 +319,75 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable { } } +extension UTMScriptingVirtualMachineImpl { + + /// Normalizes a colon-separated MAC address by stripping leading zeros from each + /// octet so that `%x`-formatted bytes compare equal to stored representations. + /// + /// Example: `"ce:09:f1:ce:7f:f2"` → `"ce:9:f1:ce:7f:f2"` + private static func normalizeMac(_ mac: String) -> String { + mac.split(separator: ":").map { octet in + let stripped = octet.drop(while: { $0 == "0" }) + return stripped.isEmpty ? "0" : String(stripped) + }.joined(separator: ":") + } + + /// Find the IP address for the given MAC by querying the kernel ARP cache via + /// `sysctl(CTL_NET, PF_ROUTE, …, NET_RT_FLAGS, RTF_LLINFO)`. + /// + /// This avoids reading `/var/db/dhcpd_leases`, which macOS 15+ blocks from + /// sandboxed apps even when the relevant entitlements are present. + /// + /// - Parameter macAddress: Lowercase colon-separated MAC, e.g. `"ce:09:f1:ce:7f:f2"`. + /// - Returns: A single-element array with the IP, or empty if not found. + static func ipFromARP(macAddress: String) -> [String] { + var mib: [Int32] = [CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, RTF_LLINFO] + var needed = 0 + guard sysctl(&mib, 6, nil, &needed, nil, 0) == 0, needed > 0 else { return [] } + + var buf = [UInt8](repeating: 0, count: needed) + guard sysctl(&mib, 6, &buf, &needed, nil, 0) == 0 else { return [] } + + let normalizedTarget = normalizeMac(macAddress) + var offset = 0 + + while offset + MemoryLayout.stride <= needed { + let msglen = Int(buf.withUnsafeBytes { + $0.load(fromByteOffset: offset, as: rt_msghdr.self).rtm_msglen + }) + guard msglen > 0, offset + msglen <= needed else { break } + defer { offset += msglen } + + // Sockaddrs start immediately after rt_msghdr. + // First: sockaddr_in (destination IP). Layout: len(1) family(1) port(2) addr(4) … + let sinStart = offset + MemoryLayout.stride + guard sinStart + 8 <= needed else { continue } + let sinLen = Int(buf[sinStart]) + let sinFamily = buf[sinStart + 1] + guard sinFamily == UInt8(AF_INET), sinLen >= 8 else { continue } + let ipStr = buf[(sinStart + 4)..<(sinStart + 8)].map { String($0) }.joined(separator: ".") + + // Second: sockaddr_dl (link-layer MAC). Padded to sizeof(long) = 8. + // Layout: len(1) family(1) index(2) type(1) nlen(1) alen(1) slen(1) data[nlen+alen…] + let sdlStart = sinStart + ((sinLen + 7) & ~7) + guard sdlStart + 8 <= needed else { continue } + let sdlFamily = buf[sdlStart + 1] + let sdlNlen = Int(buf[sdlStart + 5]) + let sdlAlen = Int(buf[sdlStart + 6]) + guard sdlFamily == UInt8(AF_LINK), sdlAlen == 6 else { continue } + + let macStart = sdlStart + 8 + sdlNlen + guard macStart + 6 <= needed else { continue } + let mac = buf[macStart..<(macStart + 6)].map { String(format: "%x", $0) }.joined(separator: ":") + if normalizeMac(mac) == normalizedTarget { + return [ipStr] + } + } + return [] + } +} + + // MARK: - Errors extension UTMScriptingVirtualMachineImpl { enum ScriptingError: Error, LocalizedError { @@ -325,3 +410,4 @@ extension UTMScriptingVirtualMachineImpl { } } } + \ No newline at end of file From 89ae8356a742102c95e85f6b75b6f8340417bae4 Mon Sep 17 00:00:00 2001 From: Victor Muryn Date: Fri, 6 Mar 2026 14:27:13 +0200 Subject: [PATCH 2/2] fixed comments --- Scripting/UTMScriptingVirtualMachineImpl.swift | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Scripting/UTMScriptingVirtualMachineImpl.swift b/Scripting/UTMScriptingVirtualMachineImpl.swift index 13ada2060..2e5c775e9 100644 --- a/Scripting/UTMScriptingVirtualMachineImpl.swift +++ b/Scripting/UTMScriptingVirtualMachineImpl.swift @@ -278,11 +278,9 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable { } } - @objc func queryIp(_ command: NSScriptCommand) { withScriptCommand(command) { [self] in - // Apple Virtualization backend: no guest agent available, - // so mirror Tart's host-side DHCP lease resolver instead. + // Apple Virtualization backend: no guest agent available if let appleVM = vm as? UTMAppleVirtualMachine { guard appleVM.state == .started else { throw ScriptingError.notRunning @@ -295,7 +293,7 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable { return Self.ipFromARP(macAddress: macAddress) } - // Non-Apple backend (QEMU): use guest agent (existing code). + // Non-Apple backend (QEMU): use guest agent return try await withGuestAgent { guestAgent in let interfaces = try await guestAgent.guestNetworkGetInterfaces() var ipv4: [String] = [] @@ -335,9 +333,6 @@ extension UTMScriptingVirtualMachineImpl { /// Find the IP address for the given MAC by querying the kernel ARP cache via /// `sysctl(CTL_NET, PF_ROUTE, …, NET_RT_FLAGS, RTF_LLINFO)`. /// - /// This avoids reading `/var/db/dhcpd_leases`, which macOS 15+ blocks from - /// sandboxed apps even when the relevant entitlements are present. - /// /// - Parameter macAddress: Lowercase colon-separated MAC, e.g. `"ce:09:f1:ce:7f:f2"`. /// - Returns: A single-element array with the IP, or empty if not found. static func ipFromARP(macAddress: String) -> [String] { @@ -410,4 +405,3 @@ extension UTMScriptingVirtualMachineImpl { } } } - \ No newline at end of file