|
14 | 14 | // limitations under the License. |
15 | 15 | //===----------------------------------------------------------------------===// |
16 | 16 |
|
| 17 | +import ContainerizationError |
17 | 18 | import ContainerizationExtras |
| 19 | +import ContainerizationOCI |
| 20 | +import Foundation |
18 | 21 | import Testing |
19 | 22 |
|
20 | 23 | @testable import Containerization |
@@ -56,4 +59,125 @@ struct InterfaceTests { |
56 | 59 | #expect(nat.ipv6Address == nil) |
57 | 60 | #expect(nat.ipv6Gateway == nil) |
58 | 61 | } |
| 62 | + |
| 63 | + /// Records the routing calls `setupInterface` makes so the routing decisions can be |
| 64 | + /// asserted without booting a sandbox. Everything outside the networking surface is |
| 65 | + /// unsupported, which is what the protocol asks unimplemented operations to report. |
| 66 | + private actor RecordingAgent: VirtualMachineAgent { |
| 67 | + private(set) var addresses: [InterfaceAddress] = [] |
| 68 | + private(set) var linkRoutes: [LinkRoute] = [] |
| 69 | + private(set) var defaultRoutes: [DefaultRoute] = [] |
| 70 | + private(set) var linksBroughtUp: [String] = [] |
| 71 | + |
| 72 | + func addressAdd(name: String, address: InterfaceAddress) async throws { |
| 73 | + addresses.append(address) |
| 74 | + } |
| 75 | + |
| 76 | + func up(name: String, mtu: UInt32?) async throws { |
| 77 | + linksBroughtUp.append(name) |
| 78 | + } |
| 79 | + |
| 80 | + func routeAddLink(name: String, route: LinkRoute) async throws { |
| 81 | + linkRoutes.append(route) |
| 82 | + } |
| 83 | + |
| 84 | + func routeAddDefault(name: String, route: DefaultRoute) async throws { |
| 85 | + defaultRoutes.append(route) |
| 86 | + } |
| 87 | + |
| 88 | + private func unsupported(_ operation: String) -> ContainerizationError { |
| 89 | + ContainerizationError(.unsupported, message: operation) |
| 90 | + } |
| 91 | + |
| 92 | + func standardSetup() async throws { throw unsupported("standardSetup") } |
| 93 | + func close() async throws { throw unsupported("close") } |
| 94 | + func filesystemOperation(operation: FilesystemOperation, path: String) async throws { throw unsupported("filesystemOperation") } |
| 95 | + func getenv(key: String) async throws -> String { throw unsupported("getenv") } |
| 96 | + func setenv(key: String, value: String) async throws { throw unsupported("setenv") } |
| 97 | + func mount(_ mount: ContainerizationOCI.Mount) async throws { throw unsupported("mount") } |
| 98 | + func umount(path: String, flags: Int32) async throws { throw unsupported("umount") } |
| 99 | + func mkdir(path: String, all: Bool, perms: UInt32) async throws { throw unsupported("mkdir") } |
| 100 | + func kill(pid: Int32, signal: Int32) async throws -> Int32 { throw unsupported("kill") } |
| 101 | + func down(name: String) async throws { throw unsupported("down") } |
| 102 | + func configureDNS(config: DNS, location: String) async throws { throw unsupported("configureDNS") } |
| 103 | + |
| 104 | + func createProcess( |
| 105 | + id: String, |
| 106 | + containerID: String?, |
| 107 | + stdinPort: UInt32?, |
| 108 | + stdoutPort: UInt32?, |
| 109 | + stderrPort: UInt32?, |
| 110 | + ociRuntimePath: String?, |
| 111 | + configuration: ContainerizationOCI.Spec, |
| 112 | + options: Data? |
| 113 | + ) async throws { throw unsupported("createProcess") } |
| 114 | + func startProcess(id: String, containerID: String?) async throws -> Int32 { throw unsupported("startProcess") } |
| 115 | + func signalProcess(id: String, containerID: String?, signal: Int32) async throws { throw unsupported("signalProcess") } |
| 116 | + func resizeProcess(id: String, containerID: String?, columns: UInt32, rows: UInt32) async throws { throw unsupported("resizeProcess") } |
| 117 | + func waitProcess(id: String, containerID: String?, timeoutInSeconds: Int64?) async throws -> Containerization.ExitStatus { throw unsupported("waitProcess") } |
| 118 | + func deleteProcess(id: String, containerID: String?) async throws { throw unsupported("deleteProcess") } |
| 119 | + } |
| 120 | + |
| 121 | + /// `Interface` documents a nil gateway as "no default route", so an interface with |
| 122 | + /// neither a v4 nor a v6 gateway must not get one. Installing it anyway leaves the |
| 123 | + /// guest with an on-link default route that makes every destination look local. |
| 124 | + @Test func noDefaultRouteWhenBothGatewaysAreNil() async throws { |
| 125 | + let agent = RecordingAgent() |
| 126 | + let interface = V4OnlyInterface( |
| 127 | + ipv4Address: try CIDRv4("172.16.0.3/24"), |
| 128 | + ipv4Gateway: nil, |
| 129 | + macAddress: nil) |
| 130 | + |
| 131 | + try await agent.setupInterface(interface, name: "eth0", setDefaultRoute: true, logger: nil) |
| 132 | + |
| 133 | + #expect(await agent.linksBroughtUp == ["eth0"]) |
| 134 | + #expect(await agent.addresses.count == 1) |
| 135 | + #expect(await agent.defaultRoutes.isEmpty) |
| 136 | + #expect(await agent.linkRoutes.isEmpty) |
| 137 | + } |
| 138 | + |
| 139 | + @Test func defaultRouteInstalledForIPv4Gateway() async throws { |
| 140 | + let agent = RecordingAgent() |
| 141 | + let interface = V4OnlyInterface( |
| 142 | + ipv4Address: try CIDRv4("172.16.0.3/24"), |
| 143 | + ipv4Gateway: try IPv4Address("172.16.0.1"), |
| 144 | + macAddress: nil) |
| 145 | + |
| 146 | + try await agent.setupInterface(interface, name: "eth0", setDefaultRoute: true, logger: nil) |
| 147 | + |
| 148 | + let routes = await agent.defaultRoutes |
| 149 | + #expect(routes.count == 1) |
| 150 | + #expect(routes.first?.ipv4Gateway == (try IPv4Address("172.16.0.1"))) |
| 151 | + #expect(routes.first?.ipv6Gateway == nil) |
| 152 | + } |
| 153 | + |
| 154 | + /// A v6-only gateway still needs the default route, so the nil v4 gateway alone must |
| 155 | + /// not suppress it. |
| 156 | + @Test func defaultRouteInstalledForIPv6OnlyGateway() async throws { |
| 157 | + let agent = RecordingAgent() |
| 158 | + let interface = NATInterface( |
| 159 | + ipv4Address: try CIDRv4("192.0.2.2/24"), |
| 160 | + ipv4Gateway: nil, |
| 161 | + ipv6Address: try CIDRv6("fd00::2/64"), |
| 162 | + ipv6Gateway: try IPv6Address("fd00::1")) |
| 163 | + |
| 164 | + try await agent.setupInterface(interface, name: "eth0", setDefaultRoute: true, logger: nil) |
| 165 | + |
| 166 | + let routes = await agent.defaultRoutes |
| 167 | + #expect(routes.count == 1) |
| 168 | + #expect(routes.first?.ipv4Gateway == nil) |
| 169 | + #expect(routes.first?.ipv6Gateway == (try IPv6Address("fd00::1"))) |
| 170 | + } |
| 171 | + |
| 172 | + @Test func noDefaultRouteWhenNotRequested() async throws { |
| 173 | + let agent = RecordingAgent() |
| 174 | + let interface = V4OnlyInterface( |
| 175 | + ipv4Address: try CIDRv4("172.16.0.3/24"), |
| 176 | + ipv4Gateway: try IPv4Address("172.16.0.1"), |
| 177 | + macAddress: nil) |
| 178 | + |
| 179 | + try await agent.setupInterface(interface, name: "eth0", setDefaultRoute: false, logger: nil) |
| 180 | + |
| 181 | + #expect(await agent.defaultRoutes.isEmpty) |
| 182 | + } |
59 | 183 | } |
0 commit comments