mirror of https://github.com/cirruslabs/tart.git
Retrieve IP from DHCPD leases file instead of ARP cache (#141)
* Retrieve IP from DHCPD leases file instead of ARP cache * Reference PLCache_read() from the retrieveRawLeases() parsing function
This commit is contained in:
parent
384abcd0bd
commit
85429cea0a
|
|
@ -1,119 +0,0 @@
|
|||
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])
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ struct IP: AsyncParsableCommand {
|
|||
let vmMacAddress = MACAddress(fromString: config.macAddress.string)!
|
||||
|
||||
repeat {
|
||||
if let ip = try ARPCache.ResolveMACAddress(macAddress: vmMacAddress) {
|
||||
if let ip = try Leases().resolveMACAddress(macAddress: vmMacAddress) {
|
||||
return ip
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import Network
|
||||
|
||||
struct Lease {
|
||||
var mac: MACAddress
|
||||
var ip: IPv4Address
|
||||
|
||||
init?(fromRawLease: [String : String]) {
|
||||
// Retrieve the required fields
|
||||
guard let hwAddress = fromRawLease["hw_address"] else { return nil }
|
||||
guard let ipAddress = fromRawLease["ip_address"] else { return nil }
|
||||
|
||||
// Parse MAC address
|
||||
let hwAddressSplits = hwAddress.split(separator: ",")
|
||||
if hwAddressSplits.count != 2 {
|
||||
return nil
|
||||
}
|
||||
if let hwAddressProto = Int(hwAddressSplits[0]), hwAddressProto != ARPHRD_ETHER {
|
||||
return nil
|
||||
}
|
||||
guard let mac = MACAddress(fromString: String(hwAddressSplits[1])) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse IP address
|
||||
guard let ip = IPv4Address(ipAddress) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.ip = ip
|
||||
self.mac = mac
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import Foundation
|
||||
import Network
|
||||
|
||||
enum LeasesError: Error {
|
||||
case UnexpectedFormat(name: String = "unexpected DHCPD leases file format", message: String, line: Int)
|
||||
case Truncated(name: String = "truncated DHCPD leases file")
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
|
||||
case .UnexpectedFormat(name: let name, message: let message, line: let line):
|
||||
return "\(name) on line \(line): \(message)"
|
||||
case .Truncated(name: let name):
|
||||
return "\(name)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Leases {
|
||||
private let leases: [MACAddress : Lease]
|
||||
|
||||
convenience init() throws {
|
||||
try self.init(URL(fileURLWithPath: "/var/db/dhcpd_leases"))
|
||||
}
|
||||
|
||||
convenience init(_ fromURL: URL) throws {
|
||||
let fileContents = try String(contentsOf: fromURL, encoding: .utf8)
|
||||
|
||||
try self.init(fileContents)
|
||||
}
|
||||
|
||||
init(_ fromString: String) throws {
|
||||
var leases: [MACAddress : Lease] = Dictionary()
|
||||
|
||||
for lease in try Self.retrieveRawLeases(fromString).compactMap({ Lease(fromRawLease: $0) }) {
|
||||
leases[lease.mac] = lease
|
||||
}
|
||||
|
||||
self.leases = leases
|
||||
}
|
||||
|
||||
/// Parse leases from the host cache similarly to the PLCache_read() function found in Apple's Open Source releases.
|
||||
///
|
||||
/// [1]: https://github.com/apple-opensource/bootp/blob/master/bootplib/NICache.c#L285-L391
|
||||
private static func retrieveRawLeases(_ dhcpdLeasesContents: String) throws -> [[String : String]] {
|
||||
var rawLeases: [[String : String]] = Array()
|
||||
|
||||
enum State {
|
||||
case Nowhere
|
||||
case Start
|
||||
case Body
|
||||
case End
|
||||
}
|
||||
var state = State.Nowhere
|
||||
|
||||
var currentRawLease: [String : String] = Dictionary()
|
||||
|
||||
for (lineNumber, line) in dhcpdLeasesContents.split(separator: "\n").enumerated().map({ ($0 + 1, $1) }) {
|
||||
if line == "{" {
|
||||
// Handle lease block start
|
||||
if state != .Nowhere && state != .End {
|
||||
throw LeasesError.UnexpectedFormat(message: "unexpected lease block start ({)", line: lineNumber)
|
||||
}
|
||||
|
||||
state = .Start
|
||||
} else if line == "}" {
|
||||
// Handle lease block end
|
||||
if state != .Body {
|
||||
throw LeasesError.UnexpectedFormat(message: "unexpected lease block end (})", line: lineNumber)
|
||||
}
|
||||
|
||||
rawLeases.append(currentRawLease)
|
||||
currentRawLease = Dictionary()
|
||||
|
||||
state = .End
|
||||
} else {
|
||||
// Handle lease block contents
|
||||
let lineWithoutTabs = String(line.drop { $0 == " " || $0 == "\t"})
|
||||
|
||||
if lineWithoutTabs.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
let splits = lineWithoutTabs.split(separator: "=", maxSplits: 1)
|
||||
if splits.count != 2 {
|
||||
throw LeasesError.UnexpectedFormat(message: "key-value pair with only a key", line: lineNumber)
|
||||
}
|
||||
let (key, value) = (String(splits[0]), String(splits[1]))
|
||||
|
||||
currentRawLease[key] = value
|
||||
|
||||
state = .Body
|
||||
}
|
||||
}
|
||||
|
||||
if state == .Start || state == .Body {
|
||||
throw LeasesError.Truncated()
|
||||
}
|
||||
|
||||
return rawLeases
|
||||
}
|
||||
|
||||
func resolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
||||
leases[macAddress]?.ip
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
struct MACAddress: Equatable, CustomStringConvertible {
|
||||
struct MACAddress: Equatable, Hashable, CustomStringConvertible {
|
||||
var mac: [UInt8] = Array(repeating: 0, count: 6)
|
||||
|
||||
init?(fromString: String) {
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import XCTest
|
||||
import Network
|
||||
@testable import tart
|
||||
|
||||
final class MACAddressResolverTests: XCTestCase {
|
||||
func testSingleEntry() throws {
|
||||
let leases = try Leases("""
|
||||
{
|
||||
ip_address=1.2.3.4
|
||||
hw_address=1,00:11:22:33:44:55
|
||||
}
|
||||
""")
|
||||
|
||||
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
||||
}
|
||||
|
||||
func testMultipleEntries() throws {
|
||||
let leases = try Leases("""
|
||||
{
|
||||
ip_address=1.2.3.4
|
||||
hw_address=1,00:11:22:33:44:55
|
||||
}
|
||||
{
|
||||
ip_address=5.6.7.8
|
||||
hw_address=1,AA:BB:CC:DD:EE:FF
|
||||
}
|
||||
""")
|
||||
|
||||
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
||||
XCTAssertEqual(IPv4Address("5.6.7.8"),
|
||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "AA:BB:CC:DD:EE:FF")!))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue