Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Sources/ContainerCommands/Network/NetworkCreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ extension Application {
@Option(name: .customLong("label"), help: "Set metadata for a network")
var labels: [String] = []

@Flag(name: .customLong("internal"), help: "Restrict external access to the network")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be prohibiting external access by containers on the network?

This reads like things outside the network cannot access containers on the network, but the host is outside the network and it can access it, right?

Let's consider how the "network mode" (NAT or host-only today, but maybe someday we'll have access to "host mode" too) fits in with the changes on #1081.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the help message.

I think not much conflict with #1081?

var hostOnly: Bool = false

@Option(
name: .customLong("subnet"), help: "Set subnet for a network",
transform: {
Expand All @@ -55,9 +58,10 @@ extension Application {

public func run() async throws {
let parsedLabels = Utility.parseKeyValuePairs(labels)
let mode: NetworkMode = hostOnly ? .hostOnly : .nat
let config = try NetworkConfiguration(
id: self.name,
mode: .nat,
mode: mode,
ipv4Subnet: ipv4Subnet,
ipv6Subnet: ipv6Subnet,
labels: parsedLabels
Expand Down
5 changes: 5 additions & 0 deletions Sources/ContainerResource/Network/NetworkMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public enum NetworkMode: String, Codable, Sendable {
/// Containers do not have routable IPs, and the host performs network
/// address translation to allow containers to reach external services.
case nat = "nat"

/// Host only networking mode
/// Containers can talk with each other in the same subnet only.
case hostOnly = "hostOnly"
}

extension NetworkMode {
Expand All @@ -30,6 +34,7 @@ extension NetworkMode {
public init?(_ value: String) {
switch value.lowercased() {
case "nat": self = .nat
case "hostOnly": self = .hostOnly
default: return nil
}
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/Helpers/NetworkVmnet/NetworkVmnetHelper+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ extension NetworkVmnetHelper {
@Option(name: .shortAndLong, help: "Network identifier")
var id: String

@Flag(name: .long, help: "Restrict external access to the network")
var hostOnly: Bool = false

@Option(name: .customLong("subnet"), help: "CIDR address for the IPv4 subnet")
var ipv4Subnet: String?

Expand All @@ -57,9 +60,10 @@ extension NetworkVmnetHelper {
log.info("configuring XPC server")
let ipv4Subnet = try self.ipv4Subnet.map { try CIDRv4($0) }
let ipv6Subnet = try self.ipv6Subnet.map { try CIDRv6($0) }
let mode: NetworkMode = self.hostOnly ? .hostOnly : .nat
let configuration = try NetworkConfiguration(
id: id,
mode: .nat,
mode: mode,
ipv4Subnet: ipv4Subnet,
ipv6Subnet: ipv6Subnet,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ public actor NetworksService {
}

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

Expand All @@ -284,6 +284,10 @@ public actor NetworksService {
serviceIdentifier,
]

if case .hostOnly = configuration.mode {
args += ["--host-only"]
}

if let ipv4Subnet = configuration.ipv4Subnet {
var existingCidrs: [CIDRv4] = []
for networkState in networkStates.values {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public actor AllocationOnlyVmnetNetwork: Network {
configuration: NetworkConfiguration,
log: Logger
) throws {
guard configuration.mode == .nat else {
guard configuration.mode == .nat || configuration.mode == .hostOnly else {
throw ContainerizationError(.unsupported, message: "invalid network mode \(configuration.mode)")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public final class ReservedVmnetNetwork: Network {
configuration: NetworkConfiguration,
log: Logger
) throws {
guard configuration.mode == .nat else {
guard configuration.mode == .nat || configuration.mode == .hostOnly else {
throw ContainerizationError(.unsupported, message: "invalid network mode \(configuration.mode)")
}

Expand Down Expand Up @@ -110,7 +110,8 @@ public final class ReservedVmnetNetwork: Network {

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

Expand Down
57 changes: 57 additions & 0 deletions Tests/CLITests/Subcommands/Networks/TestCLINetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,61 @@ class TestCLINetwork: CLITest {
return
}
}

@available(macOS 26, *)
@Test func testIsolatedNetwork() async throws {
do {
let name = getLowercasedTestName()
let networkDeleteArgs = ["network", "delete", name]
_ = try? run(arguments: networkDeleteArgs)

let networkCreateArgs = ["network", "create", "--internal", name]
let result = try run(arguments: networkCreateArgs)
if result.status != 0 {
throw CLIError.executionFailed("command failed: \(result.error)")
}
defer {
_ = try? run(arguments: networkDeleteArgs)
}
let port = UInt16.random(in: 50000..<60000)
try doLongRun(
name: name,
image: "docker.io/library/python:alpine",
args: ["--network", name],
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(port)"]
)
defer {
try? doStop(name: name)
}

let container = try inspectContainer(name)
#expect(container.networks.count > 0)
let curlImage = "docker.io/curlimages/curl:8.6.0"
let cidrAddress = container.networks[0].ipv4Address
let url = "http://\(cidrAddress.address):\(port)"
let (_, _, _, succeed) = try run(arguments: [
"run",
"--rm",
"--network",
name,
curlImage,
"curl",
url,
])

#expect(succeed == 0, "internal connection should succeed")

let (_, _, _, failed) = try run(arguments: [
"run",
"--rm",
"--network",
name,
curlImage,
"curl",
"http://google.com",
])

#expect(failed == 6, "external connection should fail")
}
}
}