From 5793935317366608e30e134d6410a87d74f0b8f6 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Thu, 19 Jun 2025 11:07:06 +0200 Subject: [PATCH] tart ip: implement --resolver=agent (#1095) * tart ip: implement --resolver=agent * CI: fix GoReleaser installation --- .cirrus.yml | 6 ++- Package.resolved | 14 +++---- Package.swift | 2 +- Sources/tart/Commands/IP.swift | 30 ++++++++----- .../MACAddressResolver/AgentResolver.swift | 42 +++++++++++++++++++ 5 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 Sources/tart/MACAddressResolver/AgentResolver.swift diff --git a/.cirrus.yml b/.cirrus.yml index c472ebd..a9f6025 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -83,8 +83,9 @@ task: - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password101 build.keychain - xcrun notarytool store-credentials "notarytool" --apple-id "hello@cirruslabs.org" --team-id "9M2P8L4D89" --password $AC_PASSWORD install_script: - - brew install go goreleaser/tap/goreleaser-pro + - brew install go - brew install mitchellh/gon/gon + - brew install --cask goreleaser/tap/goreleaser-pro info_script: - security find-identity -v - xcodebuild -version @@ -121,8 +122,9 @@ task: - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password101 build.keychain - xcrun notarytool store-credentials "notarytool" --apple-id "hello@cirruslabs.org" --team-id "9M2P8L4D89" --password $AC_PASSWORD install_script: - - brew install go goreleaser/tap/goreleaser-pro getsentry/tools/sentry-cli + - brew install go getsentry/tools/sentry-cli - brew install mitchellh/gon/gon + - brew install --cask goreleaser/tap/goreleaser-pro info_script: - security find-identity -v - xcodebuild -version diff --git a/Package.resolved b/Package.resolved index 2296106..cc72fd6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "90af6efa7ed1bbb0cd6985eb109c1e265a223a697b7fbaae2453206a892180e7", + "originHash" : "c5371137580239f6928cf64425e754e77680bf24430b50f02dad5558c23f68b0", "pins" : [ { "identity" : "antlr4", @@ -13,19 +13,19 @@ { "identity" : "cirruslabs_tart-guest-agent_apple_swift", "kind" : "remoteSourceControl", - "location" : "https://buf.build/gen/swift/git/1.28.2-00000000000000-dfeb75ad2b39.1/cirruslabs_tart-guest-agent_apple_swift.git", + "location" : "https://buf.build/gen/swift/git/1.28.2-00000000000000-17d7dedafb88.1/cirruslabs_tart-guest-agent_apple_swift.git", "state" : { - "revision" : "3e13bec2dd36788e80a2e5a2022d44d4a1f373cf", - "version" : "1.28.2-00000000000000-dfeb75ad2b39.1" + "revision" : "ccfae5de1917cdb0d7c5000008fa5ed0bad032bf", + "version" : "1.28.2-00000000000000-17d7dedafb88.1" } }, { "identity" : "cirruslabs_tart-guest-agent_grpc_swift", "kind" : "remoteSourceControl", - "location" : "https://buf.build/gen/swift/git/1.24.2-00000000000000-dfeb75ad2b39.1/cirruslabs_tart-guest-agent_grpc_swift.git", + "location" : "https://buf.build/gen/swift/git/1.24.2-00000000000000-17d7dedafb88.1/cirruslabs_tart-guest-agent_grpc_swift.git", "state" : { - "branch" : "1.24.2-00000000000000-dfeb75ad2b39.1", - "revision" : "5b6ff43b580fe435f0a174e137e2b197759a7170" + "branch" : "1.24.2-00000000000000-17d7dedafb88.1", + "revision" : "b8421f137325fe8de737ff5b61238f6f2131b2a8" } }, { diff --git a/Package.swift b/Package.swift index 65ebd16..2df2560 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/fumoboy007/swift-retry", from: "0.2.3"), .package(url: "https://github.com/jozefizso/swift-xattr", from: "3.0.0"), .package(url: "https://github.com/grpc/grpc-swift.git", .upToNextMajor(from: "1.24.2")), - .package(url: "https://buf.build/gen/swift/git/1.24.2-00000000000000-dfeb75ad2b39.1/cirruslabs_tart-guest-agent_grpc_swift.git", revision: "1.24.2-00000000000000-dfeb75ad2b39.1"), + .package(url: "https://buf.build/gen/swift/git/1.24.2-00000000000000-17d7dedafb88.1/cirruslabs_tart-guest-agent_grpc_swift.git", revision: "1.24.2-00000000000000-17d7dedafb88.1"), ], targets: [ .executableTarget(name: "tart", dependencies: [ diff --git a/Sources/tart/Commands/IP.swift b/Sources/tart/Commands/IP.swift index f271c6d..51e74ab 100644 --- a/Sources/tart/Commands/IP.swift +++ b/Sources/tart/Commands/IP.swift @@ -5,9 +5,9 @@ import SystemConfiguration import Sentry enum IPResolutionStrategy: String, ExpressibleByArgument, CaseIterable { - case dhcp, arp + case dhcp, arp, agent - private(set) static var allValueStrings: [String] = Format.allCases.map { "\($0)"} + private(set) static var allValueStrings: [String] = Self.allCases.map { "\($0)"} } struct IP: AsyncParsableCommand { @@ -19,13 +19,11 @@ struct IP: AsyncParsableCommand { @Option(help: "Number of seconds to wait for a potential VM booting") var wait: UInt16 = 0 - @Option(help: ArgumentHelp("Strategy for resolving IP address: dhcp or arp", + @Option(help: ArgumentHelp("Strategy for resolving IP address", discussion: """ - By default, Tart is looking up and parsing DHCP lease file to determine the IP of the VM.\n - This method is fast and the most reliable but only returns local IP adresses.\n - Alternatively, Tart can call external `arp` executable and parse it's output.\n - In case of enabled Bridged Networking this method will return VM's IP address on the network interface used for Bridged Networking.\n - Note that `arp` strategy won't work for VMs using `--net-softnet`. + 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 + 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. """)) var resolver: IPResolutionStrategy = .dhcp @@ -34,14 +32,16 @@ struct IP: AsyncParsableCommand { let vmConfig = try VMConfig.init(fromURL: vmDir.configURL) let vmMACAddress = MACAddress(fromString: vmConfig.macAddress.string)! - guard let ip = try await IP.resolveIP(vmMACAddress, resolutionStrategy: resolver, secondsToWait: wait) else { + guard let ip = try await IP.resolveIP(vmMACAddress, resolutionStrategy: resolver, secondsToWait: wait, controlSocketURL: vmDir.controlSocketURL) else { var message = "no IP address found" if try !vmDir.running() { message += ", is your VM running?" } - if (vmConfig.os == .linux && resolver == .arp) { + if (resolver == .agent) { + message += " (also make sure that Guest agent for Tart is running inside of a VM)" + } else if (vmConfig.os == .linux && resolver == .arp) { message += " (not all Linux distributions are compatible with the ARP resolver)" } @@ -51,7 +51,7 @@ struct IP: AsyncParsableCommand { print(ip) } - static public func resolveIP(_ vmMACAddress: MACAddress, resolutionStrategy: IPResolutionStrategy = .dhcp, secondsToWait: UInt16 = 0) async throws -> IPv4Address? { + static public func resolveIP(_ vmMACAddress: MACAddress, resolutionStrategy: IPResolutionStrategy = .dhcp, secondsToWait: UInt16 = 0, controlSocketURL: URL? = nil) async throws -> IPv4Address? { let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)! repeat { @@ -64,6 +64,14 @@ struct IP: AsyncParsableCommand { if let leases = try Leases(), let ip = leases.ResolveMACAddress(macAddress: vmMACAddress) { return ip } + case .agent: + guard let controlSocketURL = controlSocketURL else { + throw RuntimeError.Generic("Cannot perform IP resolution via Tart Guest Agent when control socket URL is not set") + } + + if let ip = try await AgentResolver.ResolveIP(controlSocketURL) { + return ip + } } // wait a second diff --git a/Sources/tart/MACAddressResolver/AgentResolver.swift b/Sources/tart/MACAddressResolver/AgentResolver.swift new file mode 100644 index 0000000..856af3e --- /dev/null +++ b/Sources/tart/MACAddressResolver/AgentResolver.swift @@ -0,0 +1,42 @@ +import Foundation +import Network +import NIOPosix +import GRPC +import Cirruslabs_TartGuestAgent_Apple_Swift +import Cirruslabs_TartGuestAgent_Grpc_Swift + +class AgentResolver { + static func ResolveIP(_ controlSocketURL: URL) async throws -> IPv4Address? { + do { + return try await resolveIP(controlSocketURL) + } catch let error as GRPCConnectionPoolError { + return nil + } + } + + private static func resolveIP(_ controlSocketURL: URL) async throws -> IPv4Address? { + // Create a gRPC channel connected to the VM's control socket + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! group.syncShutdownGracefully() + } + + let channel = try GRPCChannelPool.with( + target: .unixDomainSocket(controlSocketURL.path()), + transportSecurity: .plaintext, + eventLoopGroup: group, + ) + defer { + try! channel.close().wait() + } + + // Invoke ResolveIP() gRPC method + let callOptions = CallOptions(timeLimit: .timeout(.seconds(1))) + let agentAsyncClient = AgentAsyncClient(channel: channel) + let resolveIPCall = agentAsyncClient.makeResolveIpCall(ResolveIPRequest(), callOptions: callOptions) + + let response = try await resolveIPCall.response + + return IPv4Address(response.ip) + } +}