mirror of
https://github.com/apple/container.git
synced 2026-08-24 10:05:43 -05:00
@@ -335,6 +335,7 @@ let package = Package(
|
||||
name: "ContainerResourceTests",
|
||||
dependencies: [
|
||||
.product(name: "Containerization", package: "containerization"),
|
||||
.product(name: "ContainerizationExtras", package: "containerization"),
|
||||
"ContainerResource",
|
||||
]
|
||||
),
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -577,6 +577,52 @@ class TestCLIRunCommand: CLITest {
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 26, *)
|
||||
@Test func testForwardTCPv6() async throws {
|
||||
let retries = 10
|
||||
let retryDelaySeconds = Int64(3)
|
||||
do {
|
||||
let name = getLowercasedTestName()
|
||||
let proxyIp = "[::1]"
|
||||
let proxyPort = UInt16.random(in: 50000..<55000)
|
||||
let serverPort = UInt16.random(in: 55000..<60000)
|
||||
try doLongRun(
|
||||
name: name,
|
||||
image: "docker.io/library/node:alpine",
|
||||
args: ["--publish", "\(proxyIp):\(proxyPort):\(serverPort)/tcp"],
|
||||
containerArgs: ["npx", "http-server", "-a", "::", "-p", "\(serverPort)"])
|
||||
defer {
|
||||
try? doStop(name: name)
|
||||
}
|
||||
|
||||
let url = "http://\(proxyIp):\(proxyPort)"
|
||||
var request = HTTPClientRequest(url: url)
|
||||
request.method = .GET
|
||||
let config = HTTPClient.Configuration(proxy: nil)
|
||||
let client = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config)
|
||||
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
|
||||
print("request to \(url) succeeded")
|
||||
} catch {
|
||||
print("request to \(url) failed, error \(error)")
|
||||
try await Task.sleep(for: .seconds(retryDelaySeconds))
|
||||
}
|
||||
retriesRemaining -= 1
|
||||
}
|
||||
try #require(success, "Request to \(url) failed after \(retries - retriesRemaining) retries")
|
||||
try doStop(name: name)
|
||||
} catch {
|
||||
Issue.record("failed to run container \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRunCommandEnvFileFromNamedPipe() throws {
|
||||
do {
|
||||
let name = getTestName()
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@@ -25,7 +26,8 @@ struct ParserTest {
|
||||
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")
|
||||
let expectedAddress = try IPAddress("127.0.0.1")
|
||||
#expect(result[0].hostAddress == expectedAddress)
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(8000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
@@ -36,7 +38,8 @@ struct ParserTest {
|
||||
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")
|
||||
let expectedAddress = try IPAddress("192.168.32.36")
|
||||
#expect(result[0].hostAddress == expectedAddress)
|
||||
#expect(result[0].hostPort == UInt16(8000))
|
||||
#expect(result[0].containerPort == UInt16(8080))
|
||||
#expect(result[0].proto == .udp)
|
||||
@@ -47,7 +50,8 @@ struct ParserTest {
|
||||
func testPublishPortRange() throws {
|
||||
let result = try Parser.publishPorts(["127.0.0.1:8080-8179:9000-9099/tcp"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "127.0.0.1")
|
||||
let expectedAddress = try IPAddress("127.0.0.1")
|
||||
#expect(result[0].hostAddress == expectedAddress)
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(9000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
@@ -58,7 +62,8 @@ struct ParserTest {
|
||||
func testPublishPortRangeSingle() throws {
|
||||
let result = try Parser.publishPorts(["127.0.0.1:8080-8080:9000-9000/tcp"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "127.0.0.1")
|
||||
let expectedAddress = try IPAddress("127.0.0.1")
|
||||
#expect(result[0].hostAddress == expectedAddress)
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(9000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
@@ -69,7 +74,8 @@ struct ParserTest {
|
||||
func testPublishPortNoHostAddress() throws {
|
||||
let result = try Parser.publishPorts(["8080:8000/tcp"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "0.0.0.0")
|
||||
let expectedAddress = try IPAddress("0.0.0.0")
|
||||
#expect(result[0].hostAddress == expectedAddress)
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(8000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
@@ -80,7 +86,20 @@ struct ParserTest {
|
||||
func testPublishPortNoProtocol() throws {
|
||||
let result = try Parser.publishPorts(["8080:8000"])
|
||||
#expect(result.count == 1)
|
||||
#expect(result[0].hostAddress == "0.0.0.0")
|
||||
let expectedAddress = try IPAddress("0.0.0.0")
|
||||
#expect(result[0].hostAddress == expectedAddress)
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(8000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
#expect(result[0].count == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortParserIPv6() throws {
|
||||
let result = try Parser.publishPorts(["[fe80::36f3:5e50:ed71:1bb]:8080:8000/tcp"])
|
||||
#expect(result.count == 1)
|
||||
let expectedAddress = try IPAddress("fe80::36f3:5e50:ed71:1bb")
|
||||
#expect(result[0].hostAddress == expectedAddress)
|
||||
#expect(result[0].hostPort == UInt16(8080))
|
||||
#expect(result[0].containerPort == UInt16(8000))
|
||||
#expect(result[0].proto == .tcp)
|
||||
@@ -112,14 +131,43 @@ struct ParserTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishPortInvalidAddress() throws {
|
||||
func testPublishPortMissingPort() 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")
|
||||
return error.description.contains("invalid publish value")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishInvalidIPv4Address() throws {
|
||||
#expect {
|
||||
_ = try Parser.publishPorts(["1234:8080:8000"])
|
||||
} throws: { error in
|
||||
guard let error = error as? ContainerizationError else {
|
||||
return false
|
||||
}
|
||||
return error.description.contains("invalid publish IPv4 address")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPublishInvalidIPv6Address() throws {
|
||||
#expect {
|
||||
_ = try Parser.publishPorts([
|
||||
"[1234:5678]:8080:8000",
|
||||
"[2001::db8::1]:8080:8080",
|
||||
"[2001:db8:85a3::8a2e:370g:7334]:8080:8080",
|
||||
"[2001:db8:85a3::][8a2e::7334]:8080:8080",
|
||||
])
|
||||
} throws: { error in
|
||||
guard let error = error as? ContainerizationError else {
|
||||
return false
|
||||
}
|
||||
return error.description.contains("invalid publish IPv6 address")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ struct UtilityTests {
|
||||
"8080-8179:9000-9099/udp",
|
||||
])
|
||||
#expect(ports.count == 2)
|
||||
#expect(ports[0].hostAddress == "127.0.0.1")
|
||||
#expect(ports[0].hostAddress.description == "127.0.0.1")
|
||||
#expect(ports[0].hostPort == 8000)
|
||||
#expect(ports[0].containerPort == 9080)
|
||||
#expect(ports[0].proto == .tcp)
|
||||
#expect(ports[0].count == 1)
|
||||
#expect(ports[1].hostAddress == "0.0.0.0")
|
||||
#expect(ports[1].hostAddress.description == "0.0.0.0")
|
||||
#expect(ports[1].hostPort == 8080)
|
||||
#expect(ports[1].containerPort == 9000)
|
||||
#expect(ports[1].proto == .udp)
|
||||
|
||||
@@ -14,17 +14,18 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerResource
|
||||
|
||||
struct PublshPortTests {
|
||||
struct PublishPortTests {
|
||||
@Test
|
||||
func testPublishPortsNonOverlapping() throws {
|
||||
let ports = [
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 9000, containerPort: 8080, proto: .tcp, count: 100),
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9000, containerPort: 8080, proto: .tcp, count: 100),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
|
||||
]
|
||||
#expect(!ports.hasOverlaps())
|
||||
}
|
||||
@@ -32,8 +33,8 @@ struct PublshPortTests {
|
||||
@Test
|
||||
func testPublishPortsOverlapping() throws {
|
||||
let ports = [
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 9000, containerPort: 8080, proto: .tcp, count: 101),
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9000, containerPort: 8080, proto: .tcp, count: 101),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 9100, containerPort: 8180, proto: .tcp, count: 100),
|
||||
]
|
||||
#expect(ports.hasOverlaps())
|
||||
}
|
||||
@@ -41,10 +42,10 @@ struct PublshPortTests {
|
||||
@Test
|
||||
func testPublishPortsSamePortDifferentProtocols() throws {
|
||||
let ports = [
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 8080, containerPort: 8080, proto: .tcp, count: 1),
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 8080, containerPort: 8080, proto: .udp, count: 1),
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 1024, containerPort: 1024, proto: .tcp, count: 1025),
|
||||
PublishPort(hostAddress: "0.0.0.0", hostPort: 1024, containerPort: 1024, proto: .udp, count: 1025),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 8080, proto: .tcp, count: 1),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 8080, containerPort: 8080, proto: .udp, count: 1),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 1024, containerPort: 1024, proto: .tcp, count: 1025),
|
||||
PublishPort(hostAddress: try IPAddress("0.0.0.0"), hostPort: 1024, containerPort: 1024, proto: .udp, count: 1025),
|
||||
]
|
||||
#expect(!ports.hasOverlaps())
|
||||
}
|
||||
|
||||
+34
-37
@@ -156,49 +156,46 @@ Use the `--publish` option to forward TCP or UDP traffic from your loopback IP t
|
||||
|
||||
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:
|
||||
To forward requests from port 8080 on the IPv4 loopback IP to a NodeJS 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
|
||||
container run -d --rm -p 127.0.0.1:8080:8000 node:latest npx http-server -a :: -p 8000
|
||||
```
|
||||
|
||||
A `curl` to `localhost:8000` outputs:
|
||||
Test access using `curl`:
|
||||
|
||||
```console
|
||||
% curl http://localhost:8080
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Directory listing for /</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Directory listing for /</h1>
|
||||
<hr>
|
||||
<ul>
|
||||
<li><a href="bin/">bin@</a></li>
|
||||
<li><a href="boot/">boot/</a></li>
|
||||
<li><a href="dev/">dev/</a></li>
|
||||
<li><a href="etc/">etc/</a></li>
|
||||
<li><a href="home/">home/</a></li>
|
||||
<li><a href="lib/">lib@</a></li>
|
||||
<li><a href="lost%2Bfound/">lost+found/</a></li>
|
||||
<li><a href="media/">media/</a></li>
|
||||
<li><a href="mnt/">mnt/</a></li>
|
||||
<li><a href="opt/">opt/</a></li>
|
||||
<li><a href="proc/">proc/</a></li>
|
||||
<li><a href="root/">root/</a></li>
|
||||
<li><a href="run/">run/</a></li>
|
||||
<li><a href="sbin/">sbin@</a></li>
|
||||
<li><a href="srv/">srv/</a></li>
|
||||
<li><a href="sys/">sys/</a></li>
|
||||
<li><a href="tmp/">tmp/</a></li>
|
||||
<li><a href="usr/">usr/</a></li>
|
||||
<li><a href="var/">var/</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
</body>
|
||||
</html>
|
||||
% curl http://127.0.0.1:8080
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Index of /</title>
|
||||
...
|
||||
<br><address>Node.js v25.2.1/ <a href="https://github.com/http-party/http-server">http-server</a> server running @ 127.0.0.1:8080</address>
|
||||
</body></html>
|
||||
```
|
||||
|
||||
To forward requests from port 8080 on the IPv6 loopback IP to a NodeJS webserver on container port 8000, run:
|
||||
|
||||
```bash
|
||||
container run -d --rm -p '[::1]:8080:8000' node:latest npx http-server -a :: -p 8000
|
||||
```
|
||||
|
||||
Test access using `curl`:
|
||||
|
||||
```console
|
||||
% curl -6 'http://[::1]:8080'
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Index of /</title>
|
||||
...
|
||||
<br><address>Node.js v25.2.1/ <a href="https://github.com/http-party/http-server">http-server</a> server running @ [::1]:8080</address>
|
||||
</body></html>
|
||||
```
|
||||
|
||||
## Set a custom MAC address for your container
|
||||
|
||||
Reference in New Issue
Block a user