tart ip: show a warning when DHCP lease and ARP cache entries mismatch (#186)

This commit is contained in:
Nikolay Edigaryev 2022-08-18 22:25:02 +04:00 committed by GitHub
parent 1a1f19e169
commit e4ac2275b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 7 deletions

View File

@ -16,14 +16,21 @@ struct IP: AsyncParsableCommand {
do {
let vmDir = try VMStorageLocal().open(name)
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
let vmMACAddress = MACAddress(fromString: vmConfig.macAddress.string)!
guard let ip = try await IP.resolveIP(vmConfig, secondsToWait: wait) else {
guard let ipViaDHCP = try await IP.resolveIP(vmMACAddress, secondsToWait: wait) else {
print("no IP address found, is your VM running?")
Foundation.exit(1)
}
print(ip)
if let ipViaARP = try ARPCache.ResolveMACAddress(macAddress: vmMACAddress), ipViaARP != ipViaDHCP {
fputs("WARNING: DHCP lease and ARP cache entries for MAC address \(vmMACAddress) differ: "
+ "got \(ipViaDHCP) and \(ipViaARP) respectively, consider reporting this case to"
+ " https://github.com/cirruslabs/tart/issues/172\n", stderr)
}
print(ipViaDHCP)
Foundation.exit(0)
} catch {
@ -33,12 +40,11 @@ struct IP: AsyncParsableCommand {
}
}
static public func resolveIP(_ config: VMConfig, secondsToWait: UInt16) async throws -> IPv4Address? {
static public func resolveIP(_ vmMACAddress: MACAddress, secondsToWait: UInt16) async throws -> IPv4Address? {
let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)!
let vmMacAddress = MACAddress(fromString: config.macAddress.string)!
repeat {
if let ip = try Leases().resolveMACAddress(macAddress: vmMacAddress) {
if let ip = try Leases().resolveMACAddress(macAddress: vmMACAddress) {
return ip
}

View File

@ -0,0 +1,119 @@
import Foundation
import Network
import Virtualization
struct ARPCommandFailedError: Error, CustomStringConvertible {
var terminationReason: Process.TerminationReason
var terminationStatus: Int32
var description: String {
var reason: String
switch terminationReason {
case .exit:
reason = "exit code \(terminationStatus)"
case .uncaughtSignal:
reason = "uncaught signal"
default:
reason = "unknown reason"
}
return "arp command failed: \(reason)"
}
}
struct ARPCommandYieldedInvalidOutputError: Error, CustomStringConvertible {
var explanation: String
var description: String {
"arp command yielded invalid output: \(explanation)"
}
}
struct ARPCacheInternalError: Error, CustomStringConvertible {
var explanation: String
var description: String {
"ARPCache internal error: \(explanation)"
}
}
struct ARPCache {
static func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
let process = Process.init()
process.executableURL = URL.init(fileURLWithPath: "/usr/sbin/arp")
process.arguments = ["-an"]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
process.standardInput = FileHandle.nullDevice
try process.run()
process.waitUntilExit()
if !(process.terminationReason == .exit && process.terminationStatus == 0) {
throw ARPCommandFailedError(
terminationReason: process.terminationReason,
terminationStatus: process.terminationStatus)
}
guard let rawLines = try pipe.fileHandleForReading.readToEnd() else {
throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
}
let lines = String(decoding: rawLines, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "\n")
// Based on https://opensource.apple.com/source/network_cmds/network_cmds-606.40.2/arp.tproj/arp.c.auto.html
let regex = try NSRegularExpression(pattern: #"^.* \((?<ip>.*)\) at (?<mac>.*) on (?<interface>.*) .*$"#)
for line in lines {
let nsLineRange = NSRange(line.startIndex..<line.endIndex, in: line)
guard let match = regex.firstMatch(in: line, range: nsLineRange) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "unparseable entry \"\(line)\"")
}
let rawIP = try match.getCaptureGroup(name: "ip", for: line)
guard let ip = IPv4Address(rawIP) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse IPv4 address \(rawIP)")
}
let rawMAC = try match.getCaptureGroup(name: "mac", for: line)
if rawMAC == "(incomplete)" {
continue
}
guard let mac = MACAddress(fromString: rawMAC) else {
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
}
let interface = try match.getCaptureGroup(name: "interface", for: line)
if bridgeOnly && !interface.starts(with: "bridge") {
continue
}
if macAddress == mac {
return ip
}
}
return nil
}
}
extension NSTextCheckingResult {
func getCaptureGroup(name: String, for string: String) throws -> String {
let nsRange = self.range(withName: name)
if nsRange.location == NSNotFound {
throw ARPCacheInternalError(explanation: "attempted to retrieve non-existent named capture group \(name)")
}
guard let range = Range.init(nsRange, in: string) else {
throw ARPCacheInternalError(explanation: "failed to convert NSRange to Range")
}
return String(string[range])
}
}

View File

@ -16,6 +16,6 @@ struct MACAddress: Equatable, Hashable, CustomStringConvertible {
}
var description: String {
return String(format: "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
String(format: "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
}
}

View File

@ -10,7 +10,8 @@ class ScreenSharingVNC: VNC {
}
func waitForURL() async throws -> URL {
let ip = try await IP.resolveIP(vmConfig, secondsToWait: 60)
let vmMACAddress = MACAddress(fromString: vmConfig.macAddress.string)!
let ip = try await IP.resolveIP(vmMACAddress, secondsToWait: 60)
if let ip = ip {
return URL(string: "vnc://\(ip)")!