Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 19 additions & 3 deletions Sources/ContainerClient/Core/PublishPort.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,35 @@ public struct PublishPort: Sendable, Codable {
public let hostAddress: String

/// The port number of the proxy listener on the host
public let hostPort: Int
public let hostPort: UInt16

/// The port number of the container listener
public let containerPort: Int
public let containerPort: UInt16

/// The network protocol for the proxy
public let proto: PublishProtocol

/// The number of ports to publish
public let count: UInt16

/// Creates a new port forwarding specification.
public init(hostAddress: String, hostPort: Int, containerPort: Int, proto: PublishProtocol) {
public init(hostAddress: String, hostPort: UInt16, containerPort: UInt16, proto: PublishProtocol, count: UInt16) {
self.hostAddress = hostAddress
self.hostPort = hostPort
self.containerPort = containerPort
self.proto = proto
self.count = count
}

/// Create a configuration from the supplied Decoder, initializing missing
/// values where possible to reasonable defaults.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

hostAddress = try container.decode(String.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)
count = try container.decodeIfPresent(UInt16.self, forKey: .count) ?? 1
}
}
149 changes: 67 additions & 82 deletions Sources/ContainerClient/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -560,18 +560,18 @@ public struct Parser {
/// - Returns: Array of PublishPort objects
/// - Throws: ContainerizationError if parsing fails
public static func publishPorts(_ rawPublishPorts: [String]) throws -> [PublishPort] {
var sockets: [PublishPort] = []
var publishPorts: [PublishPort] = []

// Process each raw port string
for socket in rawPublishPorts {
let parsedSockets = try Parser.publishPort(socket)
sockets.append(contentsOf: parsedSockets)
let publishPort = try Parser.publishPort(socket)
publishPorts.append(publishPort)
}
return sockets
return publishPorts
}

// Parse a single `--publish-port` argument into a `[PublishPort]`.
public static func publishPort(_ portText: String) throws -> [PublishPort] {
// 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
Expand Down Expand Up @@ -607,99 +607,84 @@ public struct Parser {
throw ContainerizationError(.invalidArgument, message: "invalid publish address: \(portText)")
}

guard let hostPort = Int(hostPortText) else {
let hostPortRangeStart: Int
let hostPortRangeEnd: Int
let containerPortRangeStart: Int
let containerPortRangeEnd: Int
let hostPortRangeStart: UInt16
let hostPortRangeEnd: UInt16
let containerPortRangeStart: UInt16
let containerPortRangeEnd: UInt16

let hostPortParts = hostPortText.split(separator: "-")
switch hostPortParts.count {
case 2:
guard let start = Int(hostPortParts[0]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish host port \(hostPortText)")
}

guard let end = Int(hostPortParts[1]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish host port \(hostPortText)")
}

hostPortRangeStart = start
hostPortRangeEnd = end
default:
throw ContainerizationError(.invalidArgument, message: "invalid publish host port \(hostPortText)")
let hostPortParts = hostPortText.split(separator: "-")
switch hostPortParts.count {
case 1:
guard let start = UInt16(hostPortParts[0]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish host port: \(hostPortText)")
}

let containerPortParts = containerPortText.split(separator: "-")
switch containerPortParts.count {
case 2:
guard let start = Int(containerPortParts[0]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port \(containerPortText)")
}

guard let end = Int(containerPortParts[1]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port \(containerPortText)")
}

containerPortRangeStart = start
containerPortRangeEnd = end
default:
throw ContainerizationError(.invalidArgument, message: "invalid publish container port \(containerPortText)")
hostPortRangeStart = start
hostPortRangeEnd = start
case 2:
guard let start = UInt16(hostPortParts[0]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish host port: \(hostPortText)")
}

guard hostPortRangeStart > 1,
hostPortRangeEnd > 1,
hostPortRangeStart < hostPortRangeEnd,
hostPortRangeEnd > hostPortRangeStart
else {
throw ContainerizationError(.invalidArgument, message: "invalid publish host port range \(hostPortText)")
guard let end = UInt16(hostPortParts[1]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish host port: \(hostPortText)")
}

guard containerPortRangeStart > 1,
containerPortRangeEnd > 1,
containerPortRangeStart < containerPortRangeEnd,
containerPortRangeEnd > containerPortRangeStart
else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port range \(containerPortText)")
hostPortRangeStart = start
hostPortRangeEnd = end
default:
throw ContainerizationError(.invalidArgument, message: "invalid publish host port: \(hostPortText)")
}

let containerPortParts = containerPortText.split(separator: "-")
switch containerPortParts.count {
case 1:
guard let start = UInt16(containerPortParts[0]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port: \(containerPortText)")
}

let hostRange = hostPortRangeEnd - hostPortRangeStart
let containerRange = containerPortRangeEnd - containerPortRangeStart
containerPortRangeStart = start
containerPortRangeEnd = start
case 2:
guard let start = UInt16(containerPortParts[0]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port: \(containerPortText)")
}

guard hostRange == containerRange else {
throw ContainerizationError(.invalidArgument, message: "publish host and container port range are not equal \(addressAndPortText)")
guard let end = UInt16(containerPortParts[1]) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port: \(containerPortText)")
}

var publishPorts = [PublishPort]()
for i in 0..<hostPortRangeEnd - hostPortRangeStart + 1 {
let hostPort = hostPortRangeStart + i
let containerPort = containerPortRangeStart + i
containerPortRangeStart = start
containerPortRangeEnd = end
default:
throw ContainerizationError(.invalidArgument, message: "invalid publish container port: \(containerPortText)")
}

publishPorts.append(
PublishPort(
hostAddress: hostAddress,
hostPort: hostPort,
containerPort: containerPort,
proto: proto
)
)
}
guard hostPortRangeStart > 1,
hostPortRangeStart <= hostPortRangeEnd
else {
throw ContainerizationError(.invalidArgument, message: "invalid publish host port range: \(hostPortText)")
}

return publishPorts
guard containerPortRangeStart > 1,
containerPortRangeStart <= containerPortRangeEnd
else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port range: \(containerPortText)")
}

guard let containerPort = Int(containerPortText) else {
throw ContainerizationError(.invalidArgument, message: "invalid publish container port: \(containerPortText)")
let hostCount = hostPortRangeEnd - hostPortRangeStart + 1
let containerCount = containerPortRangeEnd - containerPortRangeStart + 1

guard hostCount == containerCount else {
throw ContainerizationError(.invalidArgument, message: "publish host and container port counts are not equal: \(addressAndPortText)")
}

return [
PublishPort(
hostAddress: hostAddress,
hostPort: hostPort,
containerPort: containerPort,
proto: proto
)
]
return PublishPort(
hostAddress: hostAddress,
hostPort: hostPortRangeStart,
containerPort: containerPortRangeStart,
proto: proto,
count: hostCount
)
}

/// Parse --publish-socket arguments into PublishSocket objects
Expand Down
19 changes: 19 additions & 0 deletions Sources/ContainerClient/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import Foundation
import TerminalProgress

public struct Utility {
static let publishedPortCountLimit = 64

private static let infraImages = [
DefaultsStore.get(key: .defaultBuilderImage),
DefaultsStore.get(key: .defaultInitImage),
Expand Down Expand Up @@ -70,6 +72,19 @@ public struct Utility {
}
}

public static func validPublishPorts(_ publishPorts: [PublishPort]) throws {
var hostPorts = Set<UInt16>()
for publishPort in publishPorts {
for index in 0..<publishPort.count {
let hostPort = publishPort.hostPort + index
guard !hostPorts.contains(hostPort) else {
throw ContainerizationError(.invalidArgument, message: "host ports for different publish port specs may not overlap")
}
hostPorts.insert(hostPort)
}
}
}

public static func containerConfigFromFlags(
id: String,
image: String,
Expand Down Expand Up @@ -222,6 +237,10 @@ public struct Utility {
config.labels = try Parser.labels(management.labels)

config.publishedPorts = try Parser.publishPorts(management.publishPorts)
guard config.publishedPorts.count <= publishedPortCountLimit else {
throw ContainerizationError(.invalidArgument, message: "cannot exceed more than \(publishedPortCountLimit) port publish descriptors")
}
try validPublishPorts(config.publishedPorts)

// Parse --publish-socket arguments and add to container configuration
// to enable socket forwarding from container to host.
Expand Down
57 changes: 30 additions & 27 deletions Sources/Services/ContainerSandboxService/SandboxService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -676,36 +676,39 @@ public actor SandboxService {

private func startSocketForwarders(containerIpAddress: String, publishedPorts: [PublishPort]) async throws {
var forwarders: [SocketForwarderResult] = []
try Utility.validPublishPorts(publishedPorts)
try await withThrowingTaskGroup(of: SocketForwarderResult.self) { group in
for publishedPort in publishedPorts {
let proxyAddress = try SocketAddress(ipAddress: publishedPort.hostAddress, port: Int(publishedPort.hostPort))
let serverAddress = try SocketAddress(ipAddress: containerIpAddress, port: Int(publishedPort.containerPort))
log.info(
"creating forwarder for",
metadata: [
"proxy": "\(proxyAddress)",
"server": "\(serverAddress)",
"protocol": "\(publishedPort.proto)",
])
group.addTask {
let forwarder: SocketForwarder
switch publishedPort.proto {
case .tcp:
forwarder = try TCPForwarder(
proxyAddress: proxyAddress,
serverAddress: serverAddress,
eventLoopGroup: self.eventLoopGroup,
log: self.log
)
case .udp:
forwarder = try UDPForwarder(
proxyAddress: proxyAddress,
serverAddress: serverAddress,
eventLoopGroup: self.eventLoopGroup,
log: self.log
)
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))
log.info(
"creating forwarder for",
metadata: [
"proxy": "\(proxyAddress)",
"server": "\(serverAddress)",
"protocol": "\(publishedPort.proto)",
])
group.addTask {
let forwarder: SocketForwarder
switch publishedPort.proto {
case .tcp:
forwarder = try TCPForwarder(
proxyAddress: proxyAddress,
serverAddress: serverAddress,
eventLoopGroup: self.eventLoopGroup,
log: self.log
)
case .udp:
forwarder = try UDPForwarder(
proxyAddress: proxyAddress,
serverAddress: serverAddress,
eventLoopGroup: self.eventLoopGroup,
log: self.log
)
}
return try await forwarder.run().get()
}
return try await forwarder.run().get()
}
}
for try await result in group {
Expand Down
47 changes: 47 additions & 0 deletions Tests/CLITests/Subcommands/Containers/TestCLICreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,51 @@ class TestCLICreateCommand: CLITest {
#expect(inspectResp.networks[0].macAddress == expectedMAC, "expected MAC address \(expectedMAC), got \(inspectResp.networks[0].macAddress ?? "nil")")
}
}

@Test func testPublishPortParserMaxPorts() throws {
let name = getTestName()
var args: [String] = ["create", "--name", name]

let portCount = 64
for i in 0..<portCount {
args.append("--publish")
args.append("127.0.0.1:\(8000 + i):\(9000 + i)")
}

args.append("ghcr.io/linuxcontainers/alpine:3.20")
args.append("echo")
args.append("\"hello world\"")

#expect(throws: Never.self, "expected container create maximum port publishes to succeed") {
let (_, error, status) = try run(arguments: args)
defer { try? doRemove(name: name) }
if status != 0 {
throw CLIError.executionFailed("command failed: \(error)")
}
}
}

@Test func testPublishPortParserTooManyPorts() throws {
let name = getTestName()
var args: [String] = ["create", "--name", name]

let portCount = 65
for i in 0..<portCount {
args.append("--publish")
args.append("127.0.0.1:\(8000 + i):\(9000 + i)")
}

args.append("ghcr.io/linuxcontainers/alpine:3.20")
args.append("echo")
args.append("\"hello world\"")

#expect(throws: CLIError.self, "expected container create more than maximum port publishes to fail") {
let (_, error, status) = try run(arguments: args)
defer { try? doRemove(name: name) }
if status != 0 {
throw CLIError.executionFailed("command failed: \(error)")
}
}
}

}
Loading