mirror of https://github.com/cirruslabs/tart.git
Pass Softnet policy control FD through Tart (#1287)
* Pass Softnet policy control FD through Tart * Update Softnet control test for policy set
This commit is contained in:
parent
b9ed1a98f0
commit
cbc160a592
|
|
@ -224,6 +224,13 @@ struct Run: AsyncParsableCommand {
|
||||||
""", valueName: "comma-separated CIDRs"))
|
""", valueName: "comma-separated CIDRs"))
|
||||||
var netSoftnetBlock: String?
|
var netSoftnetBlock: String?
|
||||||
|
|
||||||
|
@Option(help: ArgumentHelp("Connected Unix stream socket file descriptor to use for the Softnet control channel (e.g. --net-softnet-control-fd=3)", discussion: """
|
||||||
|
This option enables the Softnet control channel on an inherited Unix stream socket. It can be used to dynamically replace Softnet allow and block lists while the VM is running.
|
||||||
|
|
||||||
|
The file descriptor must be greater than 2. Implies --net-softnet.
|
||||||
|
""", valueName: "file descriptor"))
|
||||||
|
var netSoftnetControlFd: Int32?
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("Comma-separated list of TCP ports to expose (e.g. --net-softnet-expose 2222:22,8080:80)", discussion: """
|
@Option(help: ArgumentHelp("Comma-separated list of TCP ports to expose (e.g. --net-softnet-expose 2222:22,8080:80)", discussion: """
|
||||||
Options are comma-separated and are as follows:
|
Options are comma-separated and are as follows:
|
||||||
|
|
||||||
|
|
@ -313,7 +320,7 @@ struct Run: AsyncParsableCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically enable --net-softnet when any of its related options are specified
|
// Automatically enable --net-softnet when any of its related options are specified
|
||||||
if netSoftnetAllow != nil || netSoftnetBlock != nil || netSoftnetExpose != nil {
|
if netSoftnetAllow != nil || netSoftnetBlock != nil || netSoftnetExpose != nil || netSoftnetControlFd != nil {
|
||||||
netSoftnet = true
|
netSoftnet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,13 +688,13 @@ struct Run: AsyncParsableCommand {
|
||||||
if netSoftnet {
|
if netSoftnet {
|
||||||
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
|
|
||||||
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: softnetExtraArguments)
|
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: softnetExtraArguments, controlFD: netSoftnetControlFd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if netHost {
|
if netHost {
|
||||||
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
|
|
||||||
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: ["--vm-net-type", "host"] + softnetExtraArguments)
|
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: ["--vm-net-type", "host"] + softnetExtraArguments, controlFD: netSoftnetControlFd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if netBridged.count > 0 {
|
if netBridged.count > 0 {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,18 @@ class Softnet: Network {
|
||||||
|
|
||||||
let vmFD: Int32
|
let vmFD: Int32
|
||||||
|
|
||||||
init(vmMACAddress: String, extraArguments: [String] = []) throws {
|
init(vmMACAddress: String, extraArguments: [String] = [], controlFD: Int32? = nil) throws {
|
||||||
|
var controlFileHandle: FileHandle?
|
||||||
|
|
||||||
|
if let controlFD = controlFD {
|
||||||
|
guard controlFD > STDERR_FILENO else {
|
||||||
|
throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be greater than 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
controlFileHandle = FileHandle(fileDescriptor: controlFD, closeOnDealloc: true)
|
||||||
|
try Self.validateControlFD(controlFD)
|
||||||
|
}
|
||||||
|
|
||||||
let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)
|
let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)
|
||||||
|
|
||||||
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
|
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
|
||||||
|
|
@ -33,6 +44,44 @@ class Softnet: Network {
|
||||||
process.executableURL = try Self.softnetExecutableURL()
|
process.executableURL = try Self.softnetExecutableURL()
|
||||||
process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress] + extraArguments
|
process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress] + extraArguments
|
||||||
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
|
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
|
||||||
|
|
||||||
|
if let controlFileHandle = controlFileHandle {
|
||||||
|
process.arguments! += ["--control-fd", String(STDOUT_FILENO)]
|
||||||
|
process.standardOutput = controlFileHandle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func validateControlFD(_ fd: Int32) throws {
|
||||||
|
guard fd > STDERR_FILENO else {
|
||||||
|
throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be greater than 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
var socketType: Int32 = 0
|
||||||
|
var socketTypeLength = socklen_t(MemoryLayout<Int32>.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)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard socketType == SOCK_STREAM else {
|
||||||
|
throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor must be a Unix stream socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
var peerAddress = sockaddr_storage()
|
||||||
|
var peerAddressLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
|
||||||
|
let result = withUnsafeMutablePointer(to: &peerAddress) { pointer in
|
||||||
|
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||||
|
getpeername(fd, $0, &peerAddressLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard result == 0 else {
|
||||||
|
let details = Errno(rawValue: CInt(errno))
|
||||||
|
throw SoftnetError.InitializationFailed(why: "Softnet control file descriptor 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func softnetExecutableURL() throws -> URL {
|
static func softnetExecutableURL() throws -> URL {
|
||||||
|
|
@ -46,6 +95,8 @@ class Softnet: Network {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(_ sema: AsyncSemaphore) throws {
|
func run(_ sema: AsyncSemaphore) throws {
|
||||||
|
defer { try? (process.standardOutput as? FileHandle)?.close() }
|
||||||
|
|
||||||
try process.run()
|
try process.run()
|
||||||
|
|
||||||
monitorTask = Task {
|
monitorTask = Task {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import tart
|
||||||
|
|
||||||
|
import Semaphore
|
||||||
|
|
||||||
|
final class SoftnetControlFDTests: XCTestCase {
|
||||||
|
func testConnectedUnixStreamSocketIsAccepted() throws {
|
||||||
|
var fds: [Int32] = [-1, -1]
|
||||||
|
XCTAssertEqual(socketpair(AF_UNIX, SOCK_STREAM, 0, &fds), 0)
|
||||||
|
defer {
|
||||||
|
close(fds[0])
|
||||||
|
close(fds[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertNoThrow(try Softnet.validateControlFD(fds[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnixDatagramSocketIsRejected() throws {
|
||||||
|
var fds: [Int32] = [-1, -1]
|
||||||
|
XCTAssertEqual(socketpair(AF_UNIX, SOCK_DGRAM, 0, &fds), 0)
|
||||||
|
defer {
|
||||||
|
close(fds[0])
|
||||||
|
close(fds[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try Softnet.validateControlFD(fds[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnconnectedUnixStreamSocketIsRejected() throws {
|
||||||
|
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
|
||||||
|
XCTAssertGreaterThan(fd, STDERR_FILENO)
|
||||||
|
defer { close(fd) }
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try Softnet.validateControlFD(fd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPipeIsRejected() throws {
|
||||||
|
var fds: [Int32] = [-1, -1]
|
||||||
|
XCTAssertEqual(pipe(&fds), 0)
|
||||||
|
defer {
|
||||||
|
close(fds[0])
|
||||||
|
close(fds[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try Softnet.validateControlFD(fds[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStandardDescriptorsAreRejected() throws {
|
||||||
|
XCTAssertThrowsError(try Softnet.validateControlFD(STDIN_FILENO))
|
||||||
|
XCTAssertThrowsError(try Softnet.validateControlFD(STDOUT_FILENO))
|
||||||
|
XCTAssertThrowsError(try Softnet.validateControlFD(STDERR_FILENO))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStandardDescriptorsRemainOpenWhenInitializationFails() throws {
|
||||||
|
for fd in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO] {
|
||||||
|
let flags = fcntl(fd, F_GETFD)
|
||||||
|
XCTAssertNotEqual(flags, -1)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try Softnet(vmMACAddress: "02:00:00:00:00:01", controlFD: fd))
|
||||||
|
XCTAssertEqual(fcntl(fd, F_GETFD), flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testControlChannelIsPassedToSoftnetAndVMFDRemainsDatagram() async throws {
|
||||||
|
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
|
||||||
|
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: false)
|
||||||
|
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
|
||||||
|
|
||||||
|
let executable = temporaryDirectory.appendingPathComponent("softnet")
|
||||||
|
let script = """
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
assert sys.argv[1:] == ["--vm-fd", "0", "--vm-mac-address", "02:00:00:00:00:01", "--control-fd", "1"]
|
||||||
|
vm = socket.socket(fileno=0)
|
||||||
|
control = socket.socket(fileno=1)
|
||||||
|
assert vm.family == socket.AF_UNIX and vm.type == socket.SOCK_DGRAM
|
||||||
|
assert control.family == socket.AF_UNIX and control.type == socket.SOCK_STREAM
|
||||||
|
assert control.recv(4096) == b"softnet.policy.set\\n"
|
||||||
|
control.sendall(b"ok\\n")
|
||||||
|
"""
|
||||||
|
try script.write(to: executable, atomically: true, encoding: .utf8)
|
||||||
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
|
||||||
|
|
||||||
|
let previousPath = ProcessInfo.processInfo.environment["PATH"] ?? ""
|
||||||
|
setenv("PATH", "\(temporaryDirectory.path):\(previousPath)", 1)
|
||||||
|
defer { setenv("PATH", previousPath, 1) }
|
||||||
|
|
||||||
|
var fds: [Int32] = [-1, -1]
|
||||||
|
XCTAssertEqual(socketpair(AF_UNIX, SOCK_STREAM, 0, &fds), 0)
|
||||||
|
defer { close(fds[1]) }
|
||||||
|
|
||||||
|
var timeout = timeval(tv_sec: 5, tv_usec: 0)
|
||||||
|
XCTAssertEqual(setsockopt(fds[1], SOL_SOCKET, SO_RCVTIMEO, &timeout, socklen_t(MemoryLayout<timeval>.size)), 0)
|
||||||
|
|
||||||
|
let semaphore = AsyncSemaphore(value: 0)
|
||||||
|
let softnet = try Softnet(vmMACAddress: "02:00:00:00:00:01", controlFD: fds[0])
|
||||||
|
try softnet.run(semaphore)
|
||||||
|
|
||||||
|
XCTAssertEqual(fcntl(fds[0], F_GETFD), -1)
|
||||||
|
XCTAssertEqual(errno, EBADF)
|
||||||
|
|
||||||
|
let request = Array("softnet.policy.set\n".utf8)
|
||||||
|
XCTAssertEqual(request.withUnsafeBytes { send(fds[1], $0.baseAddress, $0.count, 0) }, request.count)
|
||||||
|
|
||||||
|
var response = [UInt8](repeating: 0, count: 128)
|
||||||
|
let received = recv(fds[1], &response, response.count, 0)
|
||||||
|
XCTAssertGreaterThan(received, 0)
|
||||||
|
XCTAssertEqual(String(decoding: response.prefix(Int(max(received, 0))), as: UTF8.self), "ok\n")
|
||||||
|
|
||||||
|
await semaphore.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testControlFDIsClosedWhenSoftnetInitializationFails() throws {
|
||||||
|
let previousPath = ProcessInfo.processInfo.environment["PATH"] ?? ""
|
||||||
|
setenv("PATH", "/this/path/does/not/exist", 1)
|
||||||
|
defer { setenv("PATH", previousPath, 1) }
|
||||||
|
|
||||||
|
var fds: [Int32] = [-1, -1]
|
||||||
|
XCTAssertEqual(socketpair(AF_UNIX, SOCK_STREAM, 0, &fds), 0)
|
||||||
|
defer { close(fds[1]) }
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try Softnet(vmMACAddress: "02:00:00:00:00:01", controlFD: fds[0]))
|
||||||
|
XCTAssertEqual(fcntl(fds[0], F_GETFD), -1)
|
||||||
|
XCTAssertEqual(errno, EBADF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testControlFDIsClosedWhenSoftnetValidationFails() throws {
|
||||||
|
var fds: [Int32] = [-1, -1]
|
||||||
|
XCTAssertEqual(socketpair(AF_UNIX, SOCK_DGRAM, 0, &fds), 0)
|
||||||
|
defer { close(fds[1]) }
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try Softnet(vmMACAddress: "02:00:00:00:00:01", controlFD: fds[0]))
|
||||||
|
XCTAssertEqual(fcntl(fds[0], F_GETFD), -1)
|
||||||
|
XCTAssertEqual(errno, EBADF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testControlFDImpliesSoftnet() throws {
|
||||||
|
let temporaryHome = try createTemporaryTartHome()
|
||||||
|
defer { try? FileManager.default.removeItem(at: temporaryHome) }
|
||||||
|
let previousHome = ProcessInfo.processInfo.environment["TART_HOME"]
|
||||||
|
setenv("TART_HOME", temporaryHome.path, 1)
|
||||||
|
defer { restoreEnvironment("TART_HOME", value: previousHome) }
|
||||||
|
|
||||||
|
let command = try Run.parse(["vm", "--net-softnet-control-fd", "3"])
|
||||||
|
|
||||||
|
XCTAssertTrue(command.netSoftnet)
|
||||||
|
XCTAssertEqual(command.netSoftnetControlFd, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testControlFDIsRejectedWithHostNetworking() throws {
|
||||||
|
let temporaryHome = try createTemporaryTartHome()
|
||||||
|
defer { try? FileManager.default.removeItem(at: temporaryHome) }
|
||||||
|
let previousHome = ProcessInfo.processInfo.environment["TART_HOME"]
|
||||||
|
setenv("TART_HOME", temporaryHome.path, 1)
|
||||||
|
defer { restoreEnvironment("TART_HOME", value: previousHome) }
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try Run.parse(["vm", "--net-host", "--net-softnet-control-fd", "3"])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createTemporaryTartHome() throws -> URL {
|
||||||
|
let temporaryHome = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
|
||||||
|
let vm = temporaryHome.appendingPathComponent("vms/vm")
|
||||||
|
try FileManager.default.createDirectory(at: vm, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
for name in ["config.json", "disk.img", "nvram.bin"] {
|
||||||
|
XCTAssertTrue(FileManager.default.createFile(atPath: vm.appendingPathComponent(name).path, contents: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
return temporaryHome
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restoreEnvironment(_ name: String, value: String?) {
|
||||||
|
if let value = value {
|
||||||
|
setenv(name, value, 1)
|
||||||
|
} else {
|
||||||
|
unsetenv(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue