Adds IPv6 port forwarding. (#1029)

- Closes #1006.
This commit is contained in:
J Logan
2026-01-07 18:23:31 -08:00
committed by GitHub
parent 9d06475aaf
commit 98410fdb57
9 changed files with 197 additions and 95 deletions
@@ -14,6 +14,8 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationExtras
/// The network protocols available for port forwarding.
public enum PublishProtocol: String, Sendable, Codable {
case tcp = "tcp"
@@ -37,7 +39,7 @@ public enum PublishProtocol: String, Sendable, Codable {
/// Specifies internet port forwarding from host to container.
public struct PublishPort: Sendable, Codable {
/// The IP address of the proxy listener on the host
public let hostAddress: String
public let hostAddress: IPAddress
/// The port number of the proxy listener on the host
public let hostPort: UInt16
@@ -52,7 +54,7 @@ public struct PublishPort: Sendable, Codable {
public let count: UInt16
/// Creates a new port forwarding specification.
public init(hostAddress: String, hostPort: UInt16, containerPort: UInt16, proto: PublishProtocol, count: UInt16) {
public init(hostAddress: IPAddress, hostPort: UInt16, containerPort: UInt16, proto: PublishProtocol, count: UInt16) {
self.hostAddress = hostAddress
self.hostPort = hostPort
self.containerPort = containerPort
@@ -65,7 +67,7 @@ public struct PublishPort: Sendable, Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
hostAddress = try container.decode(String.self, forKey: .hostAddress)
hostAddress = try container.decode(IPAddress.self, forKey: .hostAddress)
hostPort = try container.decode(UInt16.self, forKey: .hostPort)
containerPort = try container.decode(UInt16.self, forKey: .containerPort)
proto = try container.decode(PublishProtocol.self, forKey: .proto)
@@ -17,6 +17,7 @@
import ContainerResource
import Containerization
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import ContainerizationOS
import Foundation
@@ -576,41 +577,39 @@ public struct Parser {
// Parse a single `--publish-port` argument into a `PublishPort`.
public static func publishPort(_ portText: String) throws -> PublishPort {
let protoSplit = portText.split(separator: "/")
let proto: PublishProtocol
let addressAndPortText: String
switch protoSplit.count {
case 1:
addressAndPortText = String(protoSplit[0])
proto = .tcp
case 2:
addressAndPortText = String(protoSplit[0])
let protoText = String(protoSplit[1])
guard let parsedProto = PublishProtocol(protoText) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish protocol: \(protoText)")
}
proto = parsedProto
default:
let publishPortRegex = #/((\[(?<ipv6>[^\]]*)\]|(?<ipv4>[^:].*)):)?(?<hostPort>[^:].*):(?<containerPort>[^:/]*)(/(?<proto>.*))?/#
guard let match = try publishPortRegex.wholeMatch(in: portText) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish value: \(portText)")
}
let hostAddress: String
let hostPortText: String
let containerPortText: String
let parts = addressAndPortText.split(separator: ":")
switch parts.count {
case 2:
hostAddress = "0.0.0.0"
hostPortText = String(parts[0])
containerPortText = String(parts[1])
case 3:
hostAddress = String(parts[0])
hostPortText = String(parts[1])
containerPortText = String(parts[2])
let proto: PublishProtocol
let protoText = match.proto?.lowercased() ?? "tcp"
switch protoText {
case "tcp":
proto = .tcp
case "udp":
proto = .udp
default:
throw ContainerizationError(.invalidArgument, message: "invalid publish address: \(portText)")
throw ContainerizationError(.invalidArgument, message: "invalid publish protocol: \(protoText)")
}
let hostAddress: IPAddress
if let ipv6 = match.ipv6, !ipv6.isEmpty {
guard let address = try? IPAddress(String(ipv6)), case .v6 = address else {
throw ContainerizationError(.invalidArgument, message: "invalid publish IPv6 address: \(portText)")
}
hostAddress = address
} else if let ipv4 = match.ipv4, !ipv4.isEmpty {
guard let address = try? IPAddress(String(ipv4)), case .v4 = address else {
throw ContainerizationError(.invalidArgument, message: "invalid publish IPv4 address: \(portText)")
}
hostAddress = address
} else {
hostAddress = try IPAddress("0.0.0.0")
}
let hostPortText = match.hostPort
let containerPortText = match.containerPort
let hostPortRangeStart: UInt16
let hostPortRangeEnd: UInt16
let containerPortRangeStart: UInt16
@@ -679,7 +678,7 @@ public struct Parser {
let containerCount = containerPortRangeEnd - containerPortRangeStart + 1
guard hostCount == containerCount else {
throw ContainerizationError(.invalidArgument, message: "publish host and container port counts are not equal: \(addressAndPortText)")
throw ContainerizationError(.invalidArgument, message: "publish host and container port counts are not equal: \(hostPortText):\(containerPortText)")
}
return PublishPort(
@@ -218,9 +218,7 @@ public actor SandboxService {
try await container.create()
try await self.monitor.registerProcess(id: config.id, onExit: self.onContainerExit)
if !container.interfaces.isEmpty {
let firstCidr = container.interfaces[0].ipv4Address
let ipAddress = firstCidr.address.description
try await self.startSocketForwarders(containerIpAddress: ipAddress, publishedPorts: config.publishedPorts)
try await self.startSocketForwarders(attachment: attachments[0], publishedPorts: config.publishedPorts)
}
await self.setState(.booted)
} catch {
@@ -704,7 +702,7 @@ public actor SandboxService {
try await self.monitor.track(id: id, waitingOn: waitFunc)
}
private func startSocketForwarders(containerIpAddress: String, publishedPorts: [PublishPort]) async throws {
private func startSocketForwarders(attachment: Attachment, publishedPorts: [PublishPort]) async throws {
var forwarders: [SocketForwarderResult] = []
guard !publishedPorts.hasOverlaps() else {
throw ContainerizationError(.invalidArgument, message: "host ports for different publish port specs may not overlap")
@@ -713,8 +711,18 @@ public actor SandboxService {
try await withThrowingTaskGroup(of: SocketForwarderResult.self) { group in
for publishedPort in publishedPorts {
for index in 0..<publishedPort.count {
let proxyAddress = try SocketAddress(ipAddress: publishedPort.hostAddress, port: Int(publishedPort.hostPort + index))
let serverAddress = try SocketAddress(ipAddress: containerIpAddress, port: Int(publishedPort.containerPort + index))
let proxyAddress = try SocketAddress(ipAddress: publishedPort.hostAddress.description, port: Int(publishedPort.hostPort + index))
let containerIPAddress: String
switch publishedPort.hostAddress {
case .v4(_):
containerIPAddress = attachment.ipv4Address.address.description
case .v6(_):
guard let ipv6Address = attachment.ipv6Address else {
throw ContainerizationError(.invalidState, message: "cannot configure IPv6 port forwarding for container with unknown IPv6 address")
}
containerIPAddress = ipv6Address.address.description
}
let serverAddress = try SocketAddress(ipAddress: containerIPAddress, port: Int(publishedPort.containerPort + index))
log.info(
"creating forwarder for",
metadata: [