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.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) } } }