mirror of https://github.com/cirruslabs/tart.git
Merge 15754bcc8d into be272d8abd
This commit is contained in:
commit
8c8737144c
|
|
@ -243,6 +243,13 @@ struct Run: AsyncParsableCommand {
|
|||
@Flag(help: ArgumentHelp("Restrict network access to the host-only network"))
|
||||
var netHost: Bool = false
|
||||
|
||||
@Option(help: ArgumentHelp("Use externally managed connected datagram socket file descriptor for VM networking (e.g. --net-fd=3)", discussion: """
|
||||
This option allows integrating Tart with externally launched networking helpers.
|
||||
|
||||
The provided file descriptor must reference a connected datagram socket.
|
||||
""", valueName: "fd", visibility: .hidden))
|
||||
var netFd: Int32?
|
||||
|
||||
@Option(help: ArgumentHelp("Set the root disk options (e.g. --root-disk-opts=\"ro\" or --root-disk-opts=\"caching=cached,sync=none\")",
|
||||
discussion: """
|
||||
Options are comma-separated and are as follows:
|
||||
|
|
@ -295,14 +302,19 @@ struct Run: AsyncParsableCommand {
|
|||
netSoftnet = true
|
||||
}
|
||||
|
||||
if let netFd = netFd, netFd < 0 {
|
||||
throw ValidationError("--net-fd must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
// Check that no more than one network option is specified
|
||||
var netFlags = 0
|
||||
if netBridged.count > 0 { netFlags += 1 }
|
||||
if netSoftnet { netFlags += 1 }
|
||||
if netHost { netFlags += 1 }
|
||||
if netFd != nil { netFlags += 1 }
|
||||
|
||||
if netFlags > 1 {
|
||||
throw ValidationError("--net-bridged, --net-softnet and --net-host are mutually exclusive")
|
||||
throw ValidationError("--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
}
|
||||
|
||||
if graphics && noGraphics {
|
||||
|
|
@ -620,6 +632,10 @@ struct Run: AsyncParsableCommand {
|
|||
}
|
||||
|
||||
func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? {
|
||||
if let netFd = netFd {
|
||||
return try NetworkFD(fd: netFd)
|
||||
}
|
||||
|
||||
var softnetExtraArguments: [String] = []
|
||||
|
||||
if let netSoftnetAllow = netSoftnetAllow {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import Darwin
|
||||
import Foundation
|
||||
import Semaphore
|
||||
import Virtualization
|
||||
|
||||
class NetworkFD: Network {
|
||||
private let fd: Int32
|
||||
|
||||
init(fd: Int32) throws {
|
||||
self.fd = fd
|
||||
|
||||
try Self.validateFD(fd)
|
||||
try Self.validateSocketType(fd)
|
||||
try Self.validateConnected(fd)
|
||||
}
|
||||
|
||||
func attachments() -> [VZNetworkDeviceAttachment] {
|
||||
[VZFileHandleNetworkDeviceAttachment(fileHandle: FileHandle(fileDescriptor: fd))]
|
||||
}
|
||||
|
||||
func run(_ sema: AsyncSemaphore) throws {
|
||||
// no-op, only used for Softnet
|
||||
}
|
||||
|
||||
func stop() async throws {
|
||||
// no-op, only used for Softnet
|
||||
}
|
||||
|
||||
private static func validateFD(_ fd: Int32) throws {
|
||||
if fcntl(fd, F_GETFD) == -1 {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): file descriptor is not open (\(errnoDescription(errno)))"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func validateSocketType(_ fd: Int32) throws {
|
||||
var socketType: Int32 = 0
|
||||
var optionLength = socklen_t(MemoryLayout<Int32>.size)
|
||||
|
||||
if getsockopt(fd, SOL_SOCKET, SO_TYPE, &socketType, &optionLength) == -1 {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): file descriptor must reference a socket (\(errnoDescription(errno)))"
|
||||
)
|
||||
}
|
||||
|
||||
if socketType != SOCK_DGRAM {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): expected SOCK_DGRAM socket, got \(socketType)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func validateConnected(_ fd: Int32) throws {
|
||||
var address = sockaddr_storage()
|
||||
var addressLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
|
||||
|
||||
let result = withUnsafeMutablePointer(to: &address) { pointer in
|
||||
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
|
||||
getpeername(fd, sockaddrPointer, &addressLength)
|
||||
}
|
||||
}
|
||||
|
||||
if result == -1 {
|
||||
throw RuntimeError.VMConfigurationError(
|
||||
"invalid --net-fd \(fd): socket must be connected (\(errnoDescription(errno)))"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func errnoDescription(_ code: CInt) -> String {
|
||||
String(cString: strerror(code))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import Darwin
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import tart
|
||||
|
||||
final class NetworkFDTests: XCTestCase {
|
||||
func testAcceptsConnectedDatagramSocket() throws {
|
||||
let (fdLeft, fdRight) = try makeDatagramSocketPair()
|
||||
defer {
|
||||
_ = close(fdLeft)
|
||||
_ = close(fdRight)
|
||||
}
|
||||
|
||||
let network = try NetworkFD(fd: fdLeft)
|
||||
|
||||
XCTAssertEqual(network.attachments().count, 1)
|
||||
}
|
||||
|
||||
func testRejectsClosedFileDescriptor() throws {
|
||||
let (fdLeft, fdRight) = try makeDatagramSocketPair()
|
||||
defer { _ = close(fdRight) }
|
||||
|
||||
_ = close(fdLeft)
|
||||
|
||||
XCTAssertThrowsError(try NetworkFD(fd: fdLeft)) { error in
|
||||
self.assertVMConfigurationError(error, contains: "file descriptor is not open")
|
||||
}
|
||||
}
|
||||
|
||||
func testRejectsNonSocketFileDescriptor() throws {
|
||||
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
XCTAssertTrue(FileManager.default.createFile(atPath: fileURL.path, contents: Data()))
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
|
||||
let fd = open(fileURL.path, O_RDONLY)
|
||||
XCTAssertGreaterThanOrEqual(fd, 0)
|
||||
defer { _ = close(fd) }
|
||||
|
||||
XCTAssertThrowsError(try NetworkFD(fd: fd)) { error in
|
||||
self.assertVMConfigurationError(error, contains: "must reference a socket")
|
||||
}
|
||||
}
|
||||
|
||||
func testRejectsUnconnectedDatagramSocket() throws {
|
||||
let fd = socket(AF_UNIX, SOCK_DGRAM, 0)
|
||||
XCTAssertGreaterThanOrEqual(fd, 0)
|
||||
defer { _ = close(fd) }
|
||||
|
||||
XCTAssertThrowsError(try NetworkFD(fd: fd)) { error in
|
||||
self.assertVMConfigurationError(error, contains: "socket must be connected")
|
||||
}
|
||||
}
|
||||
|
||||
private func makeDatagramSocketPair() throws -> (Int32, Int32) {
|
||||
var fds: [Int32] = [-1, -1]
|
||||
let result = socketpair(AF_UNIX, SOCK_DGRAM, 0, &fds)
|
||||
|
||||
if result == -1 {
|
||||
throw RuntimeError.VMConfigurationError("failed to create a datagram socketpair for tests")
|
||||
}
|
||||
|
||||
return (fds[0], fds[1])
|
||||
}
|
||||
|
||||
private func assertVMConfigurationError(
|
||||
_ error: Error,
|
||||
contains expectedSubstring: String,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
guard case RuntimeError.VMConfigurationError(let message) = error else {
|
||||
XCTFail("Expected RuntimeError.VMConfigurationError, got \(error)", file: file, line: line)
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(
|
||||
message.contains(expectedSubstring),
|
||||
"Expected message to contain \"\(expectedSubstring)\", got \"\(message)\"",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import XCTest
|
||||
@testable import tart
|
||||
|
||||
final class RunNetworkValidationTests: XCTestCase {
|
||||
func testNetFdRejectsNegativeValue() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd=-1"])) { error in
|
||||
self.assertError(error, contains: "--net-fd must be greater than or equal to 0")
|
||||
}
|
||||
}
|
||||
|
||||
func testNetFdConflictsWithNetBridged() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd", "3", "--net-bridged=en0"])) { error in
|
||||
self.assertError(error, contains: "--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
}
|
||||
}
|
||||
|
||||
func testNetFdConflictsWithNetSoftnet() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd", "3", "--net-softnet"])) { error in
|
||||
self.assertError(error, contains: "--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
}
|
||||
}
|
||||
|
||||
func testNetFdConflictsWithNetHost() throws {
|
||||
XCTAssertThrowsError(try Run.parseAsRoot(["unused", "--net-fd", "3", "--net-host"])) { error in
|
||||
self.assertError(error, contains: "--net-bridged, --net-softnet, --net-host and --net-fd are mutually exclusive")
|
||||
}
|
||||
}
|
||||
|
||||
private func assertError(
|
||||
_ error: Error,
|
||||
contains expectedSubstring: String,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) {
|
||||
XCTAssertTrue(
|
||||
String(describing: error).contains(expectedSubstring),
|
||||
"Expected error to contain \"\(expectedSubstring)\", got \"\(error)\"",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
14
docs/faq.md
14
docs/faq.md
|
|
@ -77,6 +77,20 @@ Note: that accessing host is only possible with the default NAT network. If you
|
|||
[Softnet](https://github.com/cirruslabs/softnet) (via `tart run --net-softnet <VM NAME>)`, then the network isolation
|
||||
is stricter and it's not possible to access the host.
|
||||
|
||||
## Using externally managed networking (`--net-fd`)
|
||||
|
||||
For advanced integrations, `tart run` can consume a pre-opened connected datagram socket via `--net-fd`.
|
||||
|
||||
Unlike `--net-softnet`, Tart will not launch Softnet or configure Softnet permissions in this mode.
|
||||
|
||||
External launcher is responsible for:
|
||||
|
||||
* creating a connected datagram socketpair (for example, `socketpair(AF_UNIX, SOCK_DGRAM, ...)`)
|
||||
* starting `softnet --vm-fd ...` (or another networking helper) with one end of that socketpair
|
||||
* starting `tart run --net-fd <FD> <VM NAME>` with the other end inherited into Tart
|
||||
|
||||
If the file descriptor is invalid, not a datagram socket, or not connected, `tart run` fails fast.
|
||||
|
||||
## Changing the default NAT subnet
|
||||
|
||||
To change the default network to `192.168.77.1`:
|
||||
|
|
|
|||
Loading…
Reference in New Issue