mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-21 09:42:13 +02:00
tart ip: read /etc/bootptab in addition to /var/db/dhcpd_leases (#1355)
This commit is contained in:
@@ -20,7 +20,7 @@ struct IP: AsyncParsableCommand {
|
||||
|
||||
@Option(help: ArgumentHelp("Strategy for resolving IP address",
|
||||
discussion: """
|
||||
By default, Tart is using a "dhcp" resolver which parses the DHCP lease file on host and tries to find an entry containing the VM's MAC address. This method is fast and the most reliable, but only works for VMs are not using the bridged networking.\n
|
||||
By default, Tart is using a "dhcp" resolver which parses the DHCP reservation file, then the lease file on host and tries to find an entry containing the VM's MAC address. This method is fast and the most reliable, but only works for VMs not using the bridged networking.\n
|
||||
Alternatively, Tart has an "arp" resolver which calls an external "arp" executable and parses it's output. This works for VMs using bridged networking and returns their IP, but when they generate enough network activity to populate the host's ARP table. Note that "arp" strategy won't work for VMs using the Softnet networking.\n
|
||||
A third strategy, "agent" works in all cases reliably, but requires Guest agent for Tart VMs (https://github.com/cirruslabs/tart-guest-agent) to be installed inside of a VM.
|
||||
"""))
|
||||
@@ -60,6 +60,9 @@ struct IP: AsyncParsableCommand {
|
||||
return ip
|
||||
}
|
||||
case .dhcp:
|
||||
if let bootptab = try Bootptab(), let ip = try bootptab.ResolveMACAddress(macAddress: vmMACAddress) {
|
||||
return ip
|
||||
}
|
||||
if let leases = try Leases(), let ip = leases.ResolveMACAddress(macAddress: vmMACAddress) {
|
||||
return ip
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
struct Bootptab {
|
||||
private var reservations: [MACAddress: Swift.Set<IPv4Address>] = [:]
|
||||
|
||||
init?(_ fromURL: URL = URL(fileURLWithPath: "/etc/bootptab")) throws {
|
||||
let contents: String
|
||||
|
||||
do {
|
||||
contents = try String(contentsOf: fromURL, encoding: .utf8)
|
||||
} catch {
|
||||
if error.isFileNotFound() {
|
||||
return nil
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
for line in contents.split(whereSeparator: \.isNewline) {
|
||||
let fields = line.split(whereSeparator: \.isWhitespace)
|
||||
|
||||
// Skip lines that don't look like reservation fields
|
||||
guard fields.count >= 4 else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Assign reservation fields
|
||||
let hardwareType = fields[1]
|
||||
let hardwareAddress = fields[2]
|
||||
let ipAddress = fields[3]
|
||||
|
||||
// Skip non-Ethernet reservations
|
||||
guard hardwareType == "1" else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip malformed MAC addresses
|
||||
guard let mac = MACAddress(fromString: String(hardwareAddress)) else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip malformed IPv4 addresses
|
||||
guard let ip = IPv4Address(String(ipAddress)) else {
|
||||
continue
|
||||
}
|
||||
|
||||
reservations[mac, default: []].insert(ip)
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
||||
guard let addresses = reservations[macAddress] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if addresses.count > 1 {
|
||||
let addresses = addresses.map { $0.debugDescription }.sorted().joined(separator: ", ")
|
||||
|
||||
throw RuntimeError.Generic("multiple DHCP reservations in /etc/bootptab for \(macAddress): \(addresses)")
|
||||
}
|
||||
|
||||
return addresses.first
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,11 @@ struct MACAddress: Equatable, Hashable, CustomStringConvertible {
|
||||
}
|
||||
|
||||
for (index, component) in components.enumerated() {
|
||||
mac[index] = UInt8(component, radix: 16)!
|
||||
guard let byte = UInt8(component, radix: 16) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
mac[index] = byte
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import XCTest
|
||||
import Network
|
||||
@testable import tart
|
||||
|
||||
final class BootptabTests: XCTestCase {
|
||||
func testResolveMACAddress() throws {
|
||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
// A missing file produces no Bootptab
|
||||
XCTAssertNil(try Bootptab(url))
|
||||
|
||||
// Write reservations with duplicates, conflicts, and a malformed MAC address
|
||||
let contents = """
|
||||
# DHCP reservations
|
||||
%
|
||||
client1 1 02:ab:00:01:02:03 192.168.64.2
|
||||
client2 1 02:ab:00:01:02:03 192.168.64.2
|
||||
client3 1 02:ab:00:01:02:04 192.168.64.3
|
||||
client4 1 02:ab:00:01:02:04 192.168.65.3
|
||||
malformed 1 02:gg:00:01:02:03 192.168.64.9
|
||||
"""
|
||||
try contents.write(to: url, atomically: true, encoding: .utf8)
|
||||
defer { try? FileManager.default.removeItem(at: url) }
|
||||
|
||||
// Parse the reservation file
|
||||
let bootptab = try XCTUnwrap(Bootptab(url))
|
||||
|
||||
// Identical reservations resolve to one address
|
||||
XCTAssertEqual(try bootptab.ResolveMACAddress(
|
||||
macAddress: MACAddress(fromString: "02:ab:00:01:02:03")!), IPv4Address("192.168.64.2"))
|
||||
|
||||
// An unknown MAC address has no reservation
|
||||
XCTAssertNil(try bootptab.ResolveMACAddress(
|
||||
macAddress: MACAddress(fromString: "02:ab:00:01:02:05")!))
|
||||
|
||||
// Conflicting reservations produce an error
|
||||
XCTAssertThrowsError(try bootptab.ResolveMACAddress(
|
||||
macAddress: MACAddress(fromString: "02:ab:00:01:02:04")!))
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -132,7 +132,7 @@ And no worries, this file will be re-created on the next `tart run`.
|
||||
|
||||
Due to the limitations of the macOS built-in DHCP server, `tart ip` is unable to correctly report the IP addresses for VMs using DHCP client identifiers that are not based on VMs link-layer addresses (MAC addresses).
|
||||
|
||||
By default, when [no `--resolver=arp` is specified](#resolving-the-vms-ip-when-using-bridged-networking), `tart ip` reads the `/var/db/dhcpd_leases` file and tries to find the freshest entry that matches the VM's MAC address (based on the `hw_address` field).
|
||||
By default, when [no `--resolver=arp` is specified](#resolving-the-vms-ip-when-using-bridged-networking), `tart ip` first checks the `/etc/bootptab` file for a matching DHCP reservation. Otherwise, it reads the `/var/db/dhcpd_leases` file and tries to find the freshest entry that matches the VM's MAC address (based on the `hw_address` field).
|
||||
|
||||
However, things starts to break when the VM uses a [DUID-EN](https://metebalci.com/blog/a-note-on-dhcpv6-duid-and-prefix-delegation#duid-types) identifier, for example. One of the notorious examples of this being Ubuntu, using this type of identifier by default on latest versions. This results in the `/var/db/dhcpd_leases` entry for Ubuntu appearing as follows:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user