From dd0b519137ec21760440abac7e9e5e93a750fa64 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Tue, 13 Oct 2026 15:04:41 +0100 Subject: [PATCH] tart run: introduce "--net-fd" for better network composability --- Sources/tart/Commands/Run.swift | 18 +++++++++++++- Sources/tart/Network/NetworkFD.swift | 37 ++++++++++++++++++++++++++++ Sources/tart/Network/Softnet.swift | 20 +++++++++------ 3 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 Sources/tart/Network/NetworkFD.swift diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index c241ddb..1c965e1 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -187,6 +187,17 @@ struct Run: AsyncParsableCommand { """, valueName: "interface name")) var netBridged: [String] = [] + @Option(help: ArgumentHelp("Use a connected datagram Unix domain socket inherited from the parent process for networking (e.g. --net-fd=3)", + discussion: """ + The socket carries raw Ethernet frames, one per datagram. + + The simplest use-case is composing Tart and Softnet invocations that are orchestrated by another program, such as Orchard. + + Another way to use this argument is to connect a VM to any other networking implementation such as gVisor or software-defined networking implementation. + """, + valueName: "file descriptor")) + var netFd: Int32? + @Flag(help: ArgumentHelp("Use software networking provided by Softnet instead of the default shared (NAT) networking", discussion: """ Softnet provides better network isolation and alleviates DHCP shortage on production systems. Tart invokes Softnet when this option is specified as a sub-process and communicates with it over socketpair(2). @@ -332,9 +343,10 @@ struct Run: AsyncParsableCommand { 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 { @@ -725,6 +737,10 @@ struct Run: AsyncParsableCommand { return NetworkBridged(interfaces: try netBridged.map { try findBridgedInterface($0) }) } + if let netFd = netFd { + return try NetworkFD(fd: netFd) + } + return nil } diff --git a/Sources/tart/Network/NetworkFD.swift b/Sources/tart/Network/NetworkFD.swift new file mode 100644 index 0000000..a4f1980 --- /dev/null +++ b/Sources/tart/Network/NetworkFD.swift @@ -0,0 +1,37 @@ +import Darwin +import Foundation +import Semaphore +import System +import Virtualization + +class NetworkFD: Network { + private let attachment: VZFileHandleNetworkDeviceAttachment + + init(fd: Int32) throws { + do { + try Softnet.validateControlFD(fd, type: SOCK_DGRAM) + } catch { + throw RuntimeError.Generic("--net-fd \(error)") + } + + let flags = fcntl(fd, F_GETFD) + guard flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1 else { + throw RuntimeError.Generic("failed to set close-on-exec on --net-fd: \(Errno(rawValue: errno))") + } + + let fileHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: true) + attachment = VZFileHandleNetworkDeviceAttachment(fileHandle: fileHandle) + } + + func attachments() -> [VZNetworkDeviceAttachment] { + [attachment] + } + + func run(_ sema: AsyncSemaphore) throws { + // no-op, only used for Softnet + } + + func stop() async throws { + // no-op, only used for Softnet + } +} diff --git a/Sources/tart/Network/Softnet.swift b/Sources/tart/Network/Softnet.swift index b23dcfe..9684e53 100644 --- a/Sources/tart/Network/Softnet.swift +++ b/Sources/tart/Network/Softnet.swift @@ -24,7 +24,11 @@ class Softnet: Network { } controlFileHandle = FileHandle(fileDescriptor: controlFD, closeOnDealloc: true) - try Self.validateControlFD(controlFD) + do { + try Self.validateControlFD(controlFD) + } catch { + throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor \(error)") + } } let fds = UnsafeMutablePointer.allocate(capacity: MemoryLayout.stride * 2) @@ -50,20 +54,20 @@ class Softnet: Network { } } - static func validateControlFD(_ fd: Int32) throws { + static func validateControlFD(_ fd: Int32, type: Int32 = SOCK_STREAM) throws { guard fd > STDERR_FILENO else { - throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be greater than 2") + throw RuntimeError.Generic("must be greater than 2") } var socketType: Int32 = 0 var socketTypeLength = socklen_t(MemoryLayout.size) guard getsockopt(fd, SOL_SOCKET, SO_TYPE, &socketType, &socketTypeLength) == 0 else { let details = Errno(rawValue: CInt(errno)) - throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor is not a socket: \(details)") + throw RuntimeError.Generic("is not a socket: \(details)") } - guard socketType == SOCK_STREAM else { - throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be a Unix stream socket") + guard socketType == type else { + throw RuntimeError.Generic("must be a \(type == SOCK_STREAM ? "stream" : "datagram") socket") } var peerAddress = sockaddr_storage() @@ -75,11 +79,11 @@ class Softnet: Network { } guard result == 0 else { let details = Errno(rawValue: CInt(errno)) - throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor is not connected: \(details)") + throw RuntimeError.Generic("is not connected: \(details)") } guard peerAddress.ss_family == sa_family_t(AF_UNIX) else { - throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be a Unix stream socket") + throw RuntimeError.Generic("must be a Unix socket") } }