Skip to content

Commit 8883cd7

Browse files
committed
fix: enforce TCP isolation on --internal networks
Drops the default gateway allocation for hostOnly networks to prevent TCP egress, and updates the isolation integration test to verify against a raw IP.
1 parent ddaf2ca commit 8883cd7

5 files changed

Lines changed: 15 additions & 16 deletions

File tree

Sources/ContainerResource/Network/Attachment.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ public struct Attachment: Codable, Sendable {
2525
/// The CIDR address describing the interface IPv4 address, with the prefix length of the subnet.
2626
public let ipv4Address: CIDRv4
2727
/// The IPv4 gateway address.
28-
public let ipv4Gateway: IPv4Address
29-
/// The CIDR address describing the interface IPv6 address, with the prefix length of the subnet.
28+
public let ipv4Gateway: IPv4Address? /// The CIDR address describing the interface IPv6 address, with the prefix length of the subnet.
3029
/// The address is nil if the IPv6 subnet could not be determined at network creation time.
3130
public let ipv6Address: CIDRv6?
3231
/// The MAC address associated with the attachment (optional).
@@ -40,7 +39,7 @@ public struct Attachment: Codable, Sendable {
4039
network: String,
4140
hostname: String,
4241
ipv4Address: CIDRv4,
43-
ipv4Gateway: IPv4Address,
42+
ipv4Gateway: IPv4Address?,
4443
ipv6Address: CIDRv6?,
4544
macAddress: MACAddress?,
4645
mtu: UInt32? = nil,

Sources/ContainerResource/Network/NetworkStatus.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ public struct NetworkStatus: Codable, Sendable {
2323
public let ipv4Subnet: CIDRv4
2424

2525
/// The IPv4 gateway address.
26-
public let ipv4Gateway: IPv4Address
26+
public let ipv4Gateway: IPv4Address?
2727

2828
/// The IPv6 subnet assigned to the network, if IPv6 is enabled.
2929
public let ipv6Subnet: CIDRv6?
3030

3131
public init(
3232
ipv4Subnet: CIDRv4,
33-
ipv4Gateway: IPv4Address,
33+
ipv4Gateway: IPv4Address?,
3434
ipv6Subnet: CIDRv6?
3535
) {
3636
self.ipv4Subnet = ipv4Subnet

Sources/Services/NetworkVmnet/Server/ReservedVmnetNetwork.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
3636
private struct NetworkInfo {
3737
let network: vmnet_network_ref
3838
let ipv4Subnet: CIDRv4
39-
let ipv4Gateway: IPv4Address
39+
let ipv4Gateway: IPv4Address?
4040
let ipv6Subnet: CIDRv6
4141
}
4242

@@ -168,7 +168,7 @@ public final class ReservedVmnetNetwork: ContainerNetworkServer.Network {
168168
let lower = IPv4Address(subnetValue & maskValue)
169169
let upper = IPv4Address(lower.value + ~maskValue)
170170
let runningSubnet = try CIDRv4(lower: lower, upper: upper)
171-
let runningGateway = IPv4Address(runningSubnet.lower.value + 1)
171+
let runningGateway: IPv4Address? = configuration.mode == .hostOnly ? nil : IPv4Address(runningSubnet.lower.value + 1)
172172

173173
var prefixAddr = in6_addr()
174174
var prefixLength = UInt8(0)

Sources/Services/RuntimeLinux/Server/RuntimeService.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1149,7 +1149,9 @@ public actor RuntimeService {
11491149

11501150
private nonisolated func getDefaultNameservers(from attachments: [Attachment]) -> [String] {
11511151
for attachment in attachments {
1152-
return [attachment.ipv4Gateway.description]
1152+
if let gateway = attachment.ipv4Gateway {
1153+
return [gateway.description]
1154+
}
11531155
}
11541156
return []
11551157
}

Tests/IntegrationTests/Network/TestCLINetwork.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,7 @@ struct TestCLINetwork {
124124
}
125125

126126
@available(macOS 26, *)
127-
@Test func testIsolatedNetwork() async {
128-
await withKnownIssue("curl error 7 despite retries", isIntermittent: true) {
127+
@Test func testIsolatedNetwork() async throws {
129128
try await ContainerFixture.with { f in
130129
let net = "\(f.testID)-net"
131130
let server = "\(f.testID)-server"
@@ -163,15 +162,15 @@ struct TestCLINetwork {
163162
return result.status == 0
164163
}
165164

166-
// External connection should be blocked — the isolated network has no gateway.
165+
// Reaching a literal IP proves egress is open regardless of DNS state.
167166
let externalResult = try f.run([
168167
"run", "--rm", "--network", net, curlImage,
169-
"curl", "--connect-timeout", "5", "http://google.com",
168+
"curl", "-sSk", "--connect-timeout", "5", "https://1.1.1.1/",
170169
])
171-
let hostOnlyBlockedCodes: Set<Int32> = [6, 7, 28]
172170
#expect(
173-
hostOnlyBlockedCodes.contains(externalResult.status),
174-
"external connection from isolated network should be blocked, got exit \(externalResult.status)")
171+
externalResult.status != 0,
172+
"hostOnly network must not reach external IPs. curl exited with \(externalResult.status)"
173+
)
175174
}
176175
}
177176
}
@@ -210,4 +209,3 @@ struct TestCLINetwork {
210209
#expect(result.error.contains("network not found"))
211210
}
212211
}
213-
}

0 commit comments

Comments
 (0)