Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,21 @@ 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.description, port: Int(publishedPort.hostPort + index))
let hostPort = Int(publishedPort.hostPort + index)
let containerPort = Int(publishedPort.containerPort + index)

var proxyAddresses = [SocketAddress]()
proxyAddresses.append(
try SocketAddress(ipAddress: publishedPort.hostAddress.description, port: hostPort)
)

if case .v4(let ipv4) = publishedPort.hostAddress,
ipv4.isUnspecified || ipv4.isLoopback
{
// Bind IPv6 loopback for localhost resolution (macOS resolves localhost to ::1 first).
proxyAddresses.append(try SocketAddress(ipAddress: "::1", port: hostPort))
}

let containerIPAddress: String
switch publishedPort.hostAddress {
case .v4(_):
Expand All @@ -726,42 +740,45 @@ public actor SandboxService {
}
containerIPAddress = ipv6Address.address.description
}
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
)
}
do {
return try await forwarder.run().get()
} catch let error as IOError where error.errnoCode == EACCES {
if let port = proxyAddress.port, port < 1024 {
throw ContainerizationError(
.invalidArgument,
message: "Permission denied while binding to host port \(port). Binding to ports below 1024 requires root privileges."
let serverAddress = try SocketAddress(ipAddress: containerIPAddress, port: containerPort)

for proxyAddress in proxyAddresses {
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
)
}
do {
return try await forwarder.run().get()
} catch let error as IOError where error.errnoCode == EACCES {
if let port = proxyAddress.port, port < 1024 {
throw ContainerizationError(
.invalidArgument,
message: "Permission denied while binding to host port \(port). Binding to ports below 1024 requires root privileges."
)
}
throw error
}
throw error
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,29 @@ 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.

On macOS, `localhost` resolves to IPv6 (`::1`) first. When you publish without an explicit host IP (for example, `-p 8080:8000`, which defaults to `0.0.0.0`) or use an IPv4 loopback address, `container` also binds the IPv6 loopback so `localhost` works. If you want IPv4-only or IPv6-only binding, specify the host IP explicitly (for example, `-p 127.0.0.1:8080:8000` or `-p '[::1]:8080:8000'`).

To publish without an explicit host IP and access via `localhost`, run:

```bash
container run -d --rm -p 8080:8000 node:latest npx http-server -a :: -p 8000
```

Test access using `curl`:

```console
% curl http://localhost: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 @ localhost:8080</address>
</body></html>
```

To forward requests from port 8080 on the IPv4 loopback IP to a NodeJS webserver on container port 8000, run:

```bash
Expand Down