From 699bbde41cd3d94c14961cc317a76c1e94d9aef1 Mon Sep 17 00:00:00 2001 From: John Logan Date: Fri, 18 Jul 2025 14:18:24 -0700 Subject: [PATCH 1/2] Adds `--publish` flag for forwarding traffic to container ports. --- .../Core/ContainerConfiguration.swift | 4 + .../ContainerClient/Core/PublishPort.swift | 58 ++++++++ Sources/ContainerClient/Flags.swift | 13 +- Sources/ContainerClient/Parser.swift | 93 +++++++++++-- Sources/ContainerClient/Utility.swift | 2 + .../SandboxService.swift | 63 +++++++++ .../Subcommands/Networks/TestCLINetwork.swift | 16 --- .../Subcommands/Run/TestCLIRunOptions.swift | 45 +++++++ Tests/CLITests/Utilities/CLITest.swift | 17 +++ Tests/ContainerClientTests/ParserTest.swift | 125 ++++++++++++++++++ docs/how-to.md | 51 +++++++ 11 files changed, 455 insertions(+), 32 deletions(-) create mode 100644 Sources/ContainerClient/Core/PublishPort.swift create mode 100644 Tests/ContainerClientTests/ParserTest.swift diff --git a/Sources/ContainerClient/Core/ContainerConfiguration.swift b/Sources/ContainerClient/Core/ContainerConfiguration.swift index bb73d8f4c..47dfa0373 100644 --- a/Sources/ContainerClient/Core/ContainerConfiguration.swift +++ b/Sources/ContainerClient/Core/ContainerConfiguration.swift @@ -23,6 +23,8 @@ public struct ContainerConfiguration: Sendable, Codable { public var image: ImageDescription /// External mounts to add to the container. public var mounts: [Filesystem] = [] + /// Ports to publish from container to host. + public var publishedPorts: [PublishPort] = [] /// Sockets to publish from container to host. public var publishedSockets: [PublishSocket] = [] /// Key/Value labels for the container. @@ -50,6 +52,7 @@ public struct ContainerConfiguration: Sendable, Codable { case id case image case mounts + case publishedPorts case publishedSockets case labels case sysctls @@ -71,6 +74,7 @@ public struct ContainerConfiguration: Sendable, Codable { id = try container.decode(String.self, forKey: .id) image = try container.decode(ImageDescription.self, forKey: .image) mounts = try container.decodeIfPresent([Filesystem].self, forKey: .mounts) ?? [] + publishedPorts = try container.decodeIfPresent([PublishPort].self, forKey: .publishedPorts) ?? [] publishedSockets = try container.decodeIfPresent([PublishSocket].self, forKey: .publishedSockets) ?? [] labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:] sysctls = try container.decodeIfPresent([String: String].self, forKey: .sysctls) ?? [:] diff --git a/Sources/ContainerClient/Core/PublishPort.swift b/Sources/ContainerClient/Core/PublishPort.swift new file mode 100644 index 000000000..0c543558f --- /dev/null +++ b/Sources/ContainerClient/Core/PublishPort.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +/// The network protocols available for port forwarding. +public enum PublishProtocol: String, Sendable, Codable { + case tcp = "tcp" + case udp = "udp" + + /// Initialize a protocol with to default value, `.tcp`. + public init() { + self = .tcp + } + + /// Initialize a protocol value from the provided string. + public init?(_ value: String) { + switch value.lowercased() { + case "tcp": self = .tcp + case "udp": self = .udp + default: return nil + } + } +} + +/// 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 + + /// The port number of the proxy listener on the host + public let hostPort: Int + + /// The port number of the container listener + public let containerPort: Int + + /// The network protocol for the proxy + public let proto: PublishProtocol + + /// Creates a new port forwarding specification. + public init(hostAddress: String, hostPort: Int, containerPort: Int, proto: PublishProtocol) { + self.hostAddress = hostAddress + self.hostPort = hostPort + self.containerPort = containerPort + self.proto = proto + } +} diff --git a/Sources/ContainerClient/Flags.swift b/Sources/ContainerClient/Flags.swift index 087cff4c2..42e24cfa6 100644 --- a/Sources/ContainerClient/Flags.swift +++ b/Sources/ContainerClient/Flags.swift @@ -84,7 +84,7 @@ public struct Flags { public struct Management: ParsableArguments { public init() {} - @Flag(name: [.customLong("detach"), .customShort("d")], help: "Run the container and detach from the process") + @Flag(name: [.customLong("detach"), .short], help: "Run the container and detach from the process") public var detach = false @Option(name: .customLong("entrypoint"), help: "Override the entrypoint of the image") @@ -93,6 +93,9 @@ public struct Flags { @Option(name: .customLong("mount"), help: "Add a mount to the container (type=<>,source=<>,target=<>,readonly)") public var mounts: [String] = [] + @Option(name: [.customLong("publish"), .short], help: "Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])") + public var publishPorts: [String] = [] + @Option(name: .customLong("publish-socket"), help: "Publish a socket from container to host (format: host_path:container_path)") public var publishSockets: [String] = [] @@ -109,14 +112,14 @@ public struct Flags { public var os = "linux" @Option( - name: [.customLong("arch"), .customShort("a")], help: "Set arch if image can target multiple architectures") + name: [.customLong("arch"), .short], help: "Set arch if image can target multiple architectures") public var arch: String = Arch.hostArchitecture().rawValue - @Option(name: [.customLong("volume"), .customShort("v")], help: "Bind mount a volume into the container") + @Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container") public var volumes: [String] = [] @Option( - name: [.customLong("kernel"), .customShort("k")], help: "Set a custom kernel path", completion: .file(), + name: [.customLong("kernel"), .short], help: "Set a custom kernel path", completion: .file(), transform: { str in URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false) }) @@ -143,7 +146,7 @@ public struct Flags { @Option(name: .customLong("dns-option"), help: "DNS options") public var dnsOptions: [String] = [] - @Option(name: [.customLong("label"), .customShort("l")], help: "Add a key=value label to the container") + @Option(name: [.customLong("label"), .short], help: "Add a key=value label to the container") public var labels: [String] = [] } diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index b79fdad56..f3be89d36 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -397,12 +397,84 @@ public struct Parser { } } - // Parse --publish-socket arguments into PublishSocket objects - // Format: "host_path:container_path" (e.g., "/tmp/docker.sock:/var/run/docker.sock") - // - // - Parameter rawPublishSockets: Array of socket specifications - // - Returns: Array of PublishSocket objects - // - Throws: ContainerizationError if parsing fails + /// Parse --publish-port arguments into PublishPort objects + /// The format of each argument is `[host-ip:]host-port:container-port[/protocol]` + /// (e.g., "127.0.0.1:8080:80/tcp") + /// + /// - Parameter rawPublishPorts: Array of port arguments + /// - Returns: Array of PublishPort objects + /// - Throws: ContainerizationError if parsing fails + static func publishPorts(_ rawPublishPorts: [String]) throws -> [PublishPort] { + var sockets: [PublishPort] = [] + + // Process each raw port string + for socket in rawPublishPorts { + let parsedSocket = try Parser.publishPort(socket) + sockets.append(parsedSocket) + } + return sockets + } + + // Parse a single `--publish-port` argument into a `PublishPort`. + private 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: + 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]) + default: + throw ContainerizationError(.invalidArgument, message: "invalid publish address: \(portText)") + } + + guard let hostPort = Int(hostPortText) else { + throw ContainerizationError(.invalidArgument, message: "invalid publish host port: \(hostPortText)") + } + + guard let containerPort = Int(containerPortText) else { + throw ContainerizationError(.invalidArgument, message: "invalid publish container port: \(containerPortText)") + } + + return PublishPort( + hostAddress: hostAddress, + hostPort: hostPort, + containerPort: containerPort, + proto: proto + ) + } + + /// Parse --publish-socket arguments into PublishSocket objects + /// The format of each argument is `host_path:container_path` + /// (e.g., "/tmp/docker.sock:/var/run/docker.sock") + /// + /// - Parameter rawPublishSockets: Array of socket arguments + /// - Returns: Array of PublishSocket objects + /// - Throws: ContainerizationError if parsing fails or a path is invalid static func publishSockets(_ rawPublishSockets: [String]) throws -> [PublishSocket] { var sockets: [PublishSocket] = [] @@ -414,11 +486,10 @@ public struct Parser { return sockets } - // Parse a single --publish-socket argument and validate paths - // Format: "host_path:container_path" -> PublishSocket - private static func publishSocket(_ socket: String) throws -> PublishSocket { + // Parse a single `--publish-socket`` argument into a `PublishSocket`. + private static func publishSocket(_ socketText: String) throws -> PublishSocket { // Split by colon to two parts: [host_path, container_path] - let parts = socket.split(separator: ":") + let parts = socketText.split(separator: ":") switch parts.count { case 2: @@ -483,7 +554,7 @@ public struct Parser { throw ContainerizationError( .invalidArgument, message: - "invalid publish-socket format \(socket). Expected: host_path:container_path") + "invalid publish-socket format \(socketText). Expected: host_path:container_path") } } } diff --git a/Sources/ContainerClient/Utility.swift b/Sources/ContainerClient/Utility.swift index 70361e052..62dba4e2e 100644 --- a/Sources/ContainerClient/Utility.swift +++ b/Sources/ContainerClient/Utility.swift @@ -187,6 +187,8 @@ public struct Utility { config.labels = try Parser.labels(management.labels) + config.publishedPorts = try Parser.publishPorts(management.publishPorts) + // Parse --publish-socket arguments and add to container configuration // to enable socket forwarding from container to host. config.publishedSockets = try Parser.publishSockets(management.publishSockets) diff --git a/Sources/Services/ContainerSandboxService/SandboxService.swift b/Sources/Services/ContainerSandboxService/SandboxService.swift index 99fead6e2..b61953fc1 100644 --- a/Sources/Services/ContainerSandboxService/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/SandboxService.swift @@ -26,6 +26,9 @@ import ContainerizationOCI import ContainerizationOS import Foundation import Logging +import NIO +import NIOFoundationCompat +import SocketForwarder import struct ContainerizationOCI.Mount import struct ContainerizationOCI.Process @@ -36,11 +39,13 @@ public actor SandboxService { private let interfaceStrategy: InterfaceStrategy private var container: ContainerInfo? private let monitor: ExitMonitor + private let eventLoopGroup: MultiThreadedEventLoopGroup private var waiters: [String: [CheckedContinuation]] = [:] private let lock: AsyncLock = AsyncLock() private let log: Logging.Logger private var state: State = .created private var processes: [String: ProcessInfo] = [:] + private var socketForwarders: [SocketForwarderResult] = [] /// Create an instance with a bundle that describes the container. /// @@ -54,6 +59,7 @@ public actor SandboxService { self.interfaceStrategy = interfaceStrategy self.log = log self.monitor = ExitMonitor(log: log) + self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) } /// Start the VM and the guest agent process for a container. @@ -142,6 +148,11 @@ public actor SandboxService { do { try await container.create() try await self.monitor.registerProcess(id: config.id, onExit: self.onContainerExit) + if !container.interfaces.isEmpty { + let firstCidr = try CIDRAddress(container.interfaces[0].address) + let ipAddress = firstCidr.address.description + try await self.startSocketForwarders(containerIpAddress: ipAddress, publishedPorts: config.publishedPorts) + } await self.setState(.booted) } catch { do { @@ -274,6 +285,57 @@ public actor SandboxService { try await self.monitor.track(id: id, waitingOn: waitFunc) } + private func startSocketForwarders(containerIpAddress: String, publishedPorts: [PublishPort]) async throws { + var forwarders: [SocketForwarderResult] = [] + 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 + ) + } + return try await forwarder.run().get() + } + } + for try await result in group { + forwarders.append(result) + } + } + + self.socketForwarders = forwarders + } + + private func stopSocketForwarders() async { + log.info("closing forwarders") + for forwarder in self.socketForwarders { + forwarder.close() + try? await forwarder.wait() + } + log.info("closed forwarders") + } + /// Create a process inside the virtual machine for the container. /// /// Use this procedure to run ad hoc processes in the virtual @@ -776,6 +838,7 @@ public actor SandboxService { private func cleanupContainer() async throws { // Give back our lovely IP(s) + await self.stopSocketForwarders() let containerInfo = try self.getContainer() for attachment in containerInfo.attachments { let client = NetworkClient(id: attachment.network) diff --git a/Tests/CLITests/Subcommands/Networks/TestCLINetwork.swift b/Tests/CLITests/Subcommands/Networks/TestCLINetwork.swift index 726b25b5c..3cc7d28b9 100644 --- a/Tests/CLITests/Subcommands/Networks/TestCLINetwork.swift +++ b/Tests/CLITests/Subcommands/Networks/TestCLINetwork.swift @@ -80,20 +80,4 @@ class TestCLINetwork: CLITest { return } } - - private func getClient() -> HTTPClient { - var httpConfiguration = HTTPClient.Configuration() - let proxyConfig: HTTPClient.Configuration.Proxy? = { - let proxyEnv = ProcessInfo.processInfo.environment["HTTP_PROXY"] - guard let proxyEnv else { - return nil - } - guard let url = URL(string: proxyEnv), let host = url.host(), let port = url.port else { - return nil - } - return .server(host: host, port: port) - }() - httpConfiguration.proxy = proxyConfig - return HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration) - } } diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift index a8849c0c9..bbf37bdda 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift @@ -16,7 +16,9 @@ // +import AsyncHTTPClient import ContainerClient +import ContainerizationExtras import ContainerizationOS import Foundation import Testing @@ -426,4 +428,47 @@ class TestCLIRunCommand: CLITest { return } } + + @Test func testForwardTCP() async throws { + let retries = 10 + let retryDelaySeconds = Int64(3) + do { + let name = Test.current!.name.trimmingCharacters(in: ["(", ")"]) + let proxyIp = "127.0.0.1" + let proxyPort = UInt16.random(in: 50000..<55000) + let serverPort = UInt16.random(in: 55000..<60000) + try doLongRun( + name: name, + image: "docker.io/library/python:alpine", + args: ["--publish", "\(proxyIp):\(proxyPort):\(serverPort)/tcp"], + containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(serverPort)"]) + defer { + try? doStop(name: name) + } + + let url = "http://\(proxyIp):\(proxyPort)" + var request = HTTPClientRequest(url: url) + request.method = .GET + let client = getClient() + defer { _ = client.shutdown() } + var retriesRemaining = retries + var success = false + while !success && retriesRemaining > 0 { + do { + let response = try await client.execute(request, timeout: .seconds(retryDelaySeconds)) + try #require(response.status == .ok) + success = true + } catch { + print("request to \(url) failed, error \(error)") + try await Task.sleep(for: .seconds(retryDelaySeconds)) + } + retriesRemaining -= 1 + } + #expect(success, "Request to \(url) failed after \(retries - retriesRemaining) retries") + try doStop(name: name) + } catch { + Issue.record("failed to run container \(error)") + return + } + } } diff --git a/Tests/CLITests/Utilities/CLITest.swift b/Tests/CLITests/Utilities/CLITest.swift index 542183d77..249e41248 100644 --- a/Tests/CLITests/Utilities/CLITest.swift +++ b/Tests/CLITests/Utilities/CLITest.swift @@ -16,6 +16,7 @@ // +import AsyncHTTPClient import ContainerClient import ContainerNetworkService import Containerization @@ -382,4 +383,20 @@ class CLITest { throw CLIError.executionFailed("command failed: \(error)") } } + + func getClient() -> HTTPClient { + var httpConfiguration = HTTPClient.Configuration() + let proxyConfig: HTTPClient.Configuration.Proxy? = { + let proxyEnv = ProcessInfo.processInfo.environment["HTTP_PROXY"] + guard let proxyEnv else { + return nil + } + guard let url = URL(string: proxyEnv), let host = url.host(), let port = url.port else { + return nil + } + return .server(host: host, port: port) + }() + httpConfiguration.proxy = proxyConfig + return HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration) + } } diff --git a/Tests/ContainerClientTests/ParserTest.swift b/Tests/ContainerClientTests/ParserTest.swift new file mode 100644 index 000000000..53f9c0c2b --- /dev/null +++ b/Tests/ContainerClientTests/ParserTest.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// + +import ContainerizationError +import Foundation +import Testing + +@testable import ContainerClient + +struct ParserTest { + @Test + func testPublishPortParserTcp() throws { + let result = try Parser.publishPorts(["127.0.0.1:8080:8000/tcp"]) + #expect(result.count == 1) + #expect(result[0].hostAddress == "127.0.0.1") + #expect(result[0].hostPort == UInt16(8080)) + #expect(result[0].containerPort == UInt16(8000)) + #expect(result[0].proto == .tcp) + } + + @Test + func testPublishPortParserUdp() throws { + let result = try Parser.publishPorts(["192.168.32.36:8000:8080/UDP"]) + #expect(result.count == 1) + #expect(result[0].hostAddress == "192.168.32.36") + #expect(result[0].hostPort == UInt16(8000)) + #expect(result[0].containerPort == UInt16(8080)) + #expect(result[0].proto == .udp) + } + + @Test + func testPublishPortNoHostAddress() throws { + let result = try Parser.publishPorts(["8080:8000/tcp"]) + #expect(result.count == 1) + #expect(result[0].hostAddress == "0.0.0.0") + #expect(result[0].hostPort == UInt16(8080)) + #expect(result[0].containerPort == UInt16(8000)) + #expect(result[0].proto == .tcp) + } + + @Test + func testPublishPortNoProtocol() throws { + let result = try Parser.publishPorts(["8080:8000"]) + #expect(result.count == 1) + #expect(result[0].hostAddress == "0.0.0.0") + #expect(result[0].hostPort == UInt16(8080)) + #expect(result[0].containerPort == UInt16(8000)) + #expect(result[0].proto == .tcp) + } + + @Test + func testPublishPortInvalidProtocol() throws { + #expect { + _ = try Parser.publishPorts(["8080:8000/sctp"]) + } throws: { error in + guard let error = error as? ContainerizationError else { + return false + } + return error.description.contains("invalid publish protocol") + } + } + + @Test + func testPublishPortInvalidValue() throws { + #expect { + _ = try Parser.publishPorts([""]) + } throws: { error in + guard let error = error as? ContainerizationError else { + return false + } + return error.description.contains("invalid publish value") + } + } + + @Test + func testPublishPortInvalidAddress() throws { + #expect { + _ = try Parser.publishPorts(["1234"]) + } throws: { error in + guard let error = error as? ContainerizationError else { + return false + } + return error.description.contains("invalid publish address") + } + } + + @Test + func testPublishPortInvalidHostPort() throws { + #expect { + _ = try Parser.publishPorts(["foo:1234"]) + } throws: { error in + guard let error = error as? ContainerizationError else { + return false + } + return error.description.contains("invalid publish host port") + } + } + + @Test + func testPublishPortInvalidContainerPort() throws { + #expect { + _ = try Parser.publishPorts(["1234:foo"]) + } throws: { error in + guard let error = error as? ContainerizationError else { + return false + } + return error.description.contains("invalid publish container port") + } + } +} diff --git a/docs/how-to.md b/docs/how-to.md index bf26394c4..11ec96049 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -145,6 +145,57 @@ Use the `list` command with the `--format` option to display information for all ] +## Forward traffic from `localhost` to your container + +Use the `--publish` option to forward TCP or UDP traffic from your loopback IP to the container you run. The option value has the form `[host-ip:]host-port:container-port[/protocol]`, where protocol may be `tcp` or `udp`, case insensitive. + +If your container attaches to multiple networks, the ports you publish forward to the IP address of the interface attached to the first network. + +To forward requests from `localhost:8080` to a Python webserver on container port 8000, run: + +```bash +container run -d --rm -p 127.0.0.1:8080:8000 python:slim python3 -m http.server --bind 0.0.0.0 8000 +``` + +A `curl` to `localhost:8000` outputs: + +```console +% curl http://localhost:8080 + + + + +Directory listing for / + + +

Directory listing for /

+
+ +
+ + +``` + ## Create and use a separate isolated network > [!NOTE] From 27bbe63ec40fc8756ba5d86a9f2f63f125f2c9c4 Mon Sep 17 00:00:00 2001 From: John Logan Date: Fri, 18 Jul 2025 16:45:29 -0700 Subject: [PATCH 2/2] No proxy for TCP forwarding test. --- Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift index bbf37bdda..20af7cc55 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift @@ -449,7 +449,8 @@ class TestCLIRunCommand: CLITest { let url = "http://\(proxyIp):\(proxyPort)" var request = HTTPClientRequest(url: url) request.method = .GET - let client = getClient() + let config = HTTPClient.Configuration(proxy: nil) + let client = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config) defer { _ = client.shutdown() } var retriesRemaining = retries var success = false