Skip to content

Commit 9d4cded

Browse files
authored
Isolated network (apple#1079)
- Closes apple#1037. - Adds a `--mode` flag that has `nat` and `hostOnly` options. The host-only option selects the vmnet host-only mode, where containers attached to the network can reach each other and the host, but not external systems.
1 parent 033c999 commit 9d4cded

6 files changed

Lines changed: 80 additions & 6 deletions

File tree

Sources/ContainerCommands/Network/NetworkCreate.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ extension Application {
3131
@Option(name: .customLong("label"), help: "Set metadata for a network")
3232
var labels: [String] = []
3333

34+
@Flag(name: .customLong("internal"), help: "Restrict to host-only network")
35+
var hostOnly: Bool = false
36+
3437
@Option(
3538
name: .customLong("subnet"), help: "Set subnet for a network",
3639
transform: {
@@ -55,9 +58,10 @@ extension Application {
5558

5659
public func run() async throws {
5760
let parsedLabels = Utility.parseKeyValuePairs(labels)
61+
let mode: NetworkMode = hostOnly ? .hostOnly : .nat
5862
let config = try NetworkConfiguration(
5963
id: self.name,
60-
mode: .nat,
64+
mode: mode,
6165
ipv4Subnet: ipv4Subnet,
6266
ipv6Subnet: ipv6Subnet,
6367
labels: parsedLabels

Sources/ContainerResource/Network/NetworkMode.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17+
import ArgumentParser
18+
1719
/// Networking mode that applies to client containers.
18-
public enum NetworkMode: String, Codable, Sendable {
20+
public enum NetworkMode: String, Codable, Sendable, ExpressibleByArgument {
1921
/// NAT networking mode.
2022
/// Containers do not have routable IPs, and the host performs network
2123
/// address translation to allow containers to reach external services.
2224
case nat = "nat"
25+
26+
/// Host only networking mode
27+
/// Containers can talk with each other in the same subnet only.
28+
case hostOnly = "hostOnly"
2329
}
2430

2531
extension NetworkMode {
@@ -30,6 +36,7 @@ extension NetworkMode {
3036
public init?(_ value: String) {
3137
switch value.lowercased() {
3238
case "nat": self = .nat
39+
case "hostOnly": self = .hostOnly
3340
default: return nil
3441
}
3542
}

Sources/Helpers/NetworkVmnet/NetworkVmnetHelper+Start.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ extension NetworkVmnetHelper {
4545
@Option(name: .shortAndLong, help: "Network identifier")
4646
var id: String
4747

48+
@Option(name: .long, help: "Network mode")
49+
var mode: NetworkMode = .nat
50+
4851
@Option(name: .customLong("subnet"), help: "CIDR address for the IPv4 subnet")
4952
var ipv4Subnet: String?
5053

@@ -73,7 +76,7 @@ extension NetworkVmnetHelper {
7376
let ipv6Subnet = try self.ipv6Subnet.map { try CIDRv6($0) }
7477
let configuration = try NetworkConfiguration(
7578
id: id,
76-
mode: .nat,
79+
mode: mode,
7780
ipv4Subnet: ipv4Subnet,
7881
ipv6Subnet: ipv6Subnet,
7982
)

Sources/Services/ContainerAPIService/Server/Networks/NetworksService.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public actor NetworksService {
269269
}
270270

271271
private func registerService(configuration: NetworkConfiguration) async throws {
272-
guard configuration.mode == .nat else {
272+
guard configuration.mode == .nat || configuration.mode == .hostOnly else {
273273
throw ContainerizationError(.invalidArgument, message: "unsupported network mode \(configuration.mode.rawValue)")
274274
}
275275

@@ -282,6 +282,8 @@ public actor NetworksService {
282282
configuration.id,
283283
"--service-identifier",
284284
serviceIdentifier,
285+
"--mode",
286+
configuration.mode.rawValue,
285287
]
286288

287289
if let ipv4Subnet = configuration.ipv4Subnet {

Sources/Services/ContainerNetworkService/Server/ReservedVmnetNetwork.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public final class ReservedVmnetNetwork: Network {
5252
configuration: NetworkConfiguration,
5353
log: Logger
5454
) throws {
55-
guard configuration.mode == .nat else {
55+
guard configuration.mode == .nat || configuration.mode == .hostOnly else {
5656
throw ContainerizationError(.unsupported, message: "invalid network mode \(configuration.mode)")
5757
}
5858

@@ -110,7 +110,8 @@ public final class ReservedVmnetNetwork: Network {
110110

111111
// set up the vmnet configuration
112112
var status: vmnet_return_t = .VMNET_SUCCESS
113-
guard let vmnetConfiguration = vmnet_network_configuration_create(vmnet.operating_modes_t.VMNET_SHARED_MODE, &status), status == .VMNET_SUCCESS else {
113+
let mode: vmnet.operating_modes_t = configuration.mode == .hostOnly ? .VMNET_HOST_MODE : .VMNET_SHARED_MODE
114+
guard let vmnetConfiguration = vmnet_network_configuration_create(mode, &status), status == .VMNET_SUCCESS else {
114115
throw ContainerizationError(.unsupported, message: "failed to create vmnet config with status \(status)")
115116
}
116117

Tests/CLITests/Subcommands/Networks/TestCLINetwork.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,61 @@ class TestCLINetwork: CLITest {
190190
return
191191
}
192192
}
193+
194+
@available(macOS 26, *)
195+
@Test func testIsolatedNetwork() async throws {
196+
do {
197+
let name = getLowercasedTestName()
198+
let networkDeleteArgs = ["network", "delete", name]
199+
_ = try? run(arguments: networkDeleteArgs)
200+
201+
let networkCreateArgs = ["network", "create", "--internal", name]
202+
let result = try run(arguments: networkCreateArgs)
203+
if result.status != 0 {
204+
throw CLIError.executionFailed("command failed: \(result.error)")
205+
}
206+
defer {
207+
_ = try? run(arguments: networkDeleteArgs)
208+
}
209+
let port = UInt16.random(in: 50000..<60000)
210+
try doLongRun(
211+
name: name,
212+
image: "docker.io/library/python:alpine",
213+
args: ["--network", name],
214+
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(port)"]
215+
)
216+
defer {
217+
try? doStop(name: name)
218+
}
219+
220+
let container = try inspectContainer(name)
221+
#expect(container.networks.count > 0)
222+
let curlImage = "docker.io/curlimages/curl:8.6.0"
223+
let cidrAddress = container.networks[0].ipv4Address
224+
let url = "http://\(cidrAddress.address):\(port)"
225+
let (_, _, _, succeed) = try run(arguments: [
226+
"run",
227+
"--rm",
228+
"--network",
229+
name,
230+
curlImage,
231+
"curl",
232+
url,
233+
])
234+
235+
#expect(succeed == 0, "internal connection should succeed")
236+
237+
let (_, _, _, failed) = try run(arguments: [
238+
"run",
239+
"--rm",
240+
"--network",
241+
name,
242+
curlImage,
243+
"curl",
244+
"http://google.com",
245+
])
246+
247+
#expect(failed == 6, "external connection should fail")
248+
}
249+
}
193250
}

0 commit comments

Comments
 (0)