LinuxContainer: Add /etc/hosts writing functionality (#207)

Closes #206 

Much like we have support for supplying DNS configurations and writing
out /etc/resolv.conf, this adds a way to write out /etc/hosts for a
given container.
This commit is contained in:
Danny Canter
2025-07-18 00:54:18 -07:00
committed by GitHub
parent 3cc638624f
commit c4b6b68ce7
11 changed files with 562 additions and 0 deletions
@@ -309,6 +309,11 @@ extension Vminitd {
})
}
/// Configure /etc/hosts within the sandbox's environment.
public func configureHosts(config: Hosts, location: String) async throws {
_ = try await client.configureHosts(config.toAgentHostsRequest(location: location))
}
/// Perform a sync call.
public func sync() async throws {
_ = try await client.sync(.init())
@@ -337,6 +342,27 @@ extension Vminitd {
}
}
extension Hosts {
func toAgentHostsRequest(location: String) -> Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest {
Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest.with {
$0.location = location
if let comment {
$0.comment = comment
}
$0.entries = entries.map {
let entry = $0
return Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest.HostsEntry.with {
if let comment = entry.comment {
$0.comment = comment
}
$0.ipAddress = entry.ipAddress
$0.hostnames = entry.hostnames
}
}
}
}
}
extension Vminitd.Client {
public init(socket: String, group: MultiThreadedEventLoopGroup) {
var config = ClientConnection.Configuration.default(
@@ -0,0 +1,140 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization 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.
//===----------------------------------------------------------------------===//
/// Static table lookups for a container. The values will be used to
/// construct /etc/hosts for a given container.
public struct Hosts: Sendable {
/// Represents one entry in an /etc/hosts file.
public struct Entry: Sendable {
/// The IPV4 or IPV6 address in String form.
public var ipAddress: String
/// The hostname(s) for the entry.
public var hostnames: [String]
/// An optional comment to be placed to the right side of the entry.
public var comment: String?
public init(ipAddress: String, hostnames: [String], comment: String? = nil) {
self.comment = comment
self.hostnames = hostnames
self.ipAddress = ipAddress
}
/// The information in the structure rendered to a String representation
/// that matches the format /etc/hosts expects.
public var rendered: String {
var line = ipAddress
if !hostnames.isEmpty {
line += " " + hostnames.joined(separator: " ")
}
if let comment {
line += " # \(comment) "
}
return line
}
public static func localHostIPV4(comment: String? = nil) -> Self {
Self(
ipAddress: "127.0.0.1",
hostnames: ["localhost"],
comment: comment
)
}
public static func localHostIPV6(comment: String? = nil) -> Self {
Self(
ipAddress: "::1",
hostnames: ["localhost", "ip6-localhost", "ip6-loopback"],
comment: comment
)
}
public static func ipv6LocalNet(comment: String? = nil) -> Self {
Self(
ipAddress: "fe00::",
hostnames: ["ip6-localnet"],
comment: comment
)
}
public static func ipv6MulticastPrefix(comment: String? = nil) -> Self {
Self(
ipAddress: "ff00::",
hostnames: ["ip6-mcastprefix"],
comment: comment
)
}
public static func ipv6AllNodes(comment: String? = nil) -> Self {
Self(
ipAddress: "ff02::1",
hostnames: ["ip6-allnodes"],
comment: comment
)
}
public static func ipv6AllRouters(comment: String? = nil) -> Self {
Self(
ipAddress: "ff02::2",
hostnames: ["ip6-allrouters"],
comment: comment
)
}
}
/// The entries to be written to /etc/hosts.
public var entries: [Entry]
/// A comment to render at the top of the file.
public var comment: String?
public init(
entries: [Entry],
comment: String? = nil
) {
self.entries = entries
self.comment = comment
}
}
extension Hosts {
/// A default entry that can be used for convenience. It contains a IPV4
/// and IPV6 localhost entry, as well as ipv6 localnet, ipv6 mcastprefix,
/// ipv6 allnodes, and ipv6 allrouters.
public static let `default` = Hosts(entries: [
Entry.localHostIPV4(),
Entry.localHostIPV6(),
Entry.ipv6LocalNet(),
Entry.ipv6MulticastPrefix(),
Entry.ipv6AllNodes(),
Entry.ipv6AllRouters(),
])
/// Returns a string variant of the data that can be written to
/// /etc/hosts directly.
public var hostsFile: String {
var lines: [String] = []
if let comment {
lines.append("# \(comment)")
}
for entry in entries {
lines.append(entry.rendered)
}
return lines.joined(separator: "\n") + "\n"
}
}
@@ -52,6 +52,7 @@ public final class LinuxContainer: Container, Sendable {
var ioHandlers: LinuxProcess.IOHandler = .nullIO()
var mounts: [Mount]
var dns: DNS? = nil
var hosts: Hosts? = nil
}
@SendablePropertyUnchecked
@@ -324,6 +325,12 @@ extension LinuxContainer {
}
}
/// Hostname mapping configurations for the container.
public var hosts: Hosts? {
get { config.hosts }
set { config.hosts = newValue }
}
/// Unix sockets to share into or out of the container.
///
/// The VirtualMachineAgent used to launch the container
@@ -545,9 +552,14 @@ extension LinuxContainer {
try await agent.routeAddDefault(name: name, gateway: gateway)
}
}
// Setup /etc/resolv.conf and /etc/hosts if asked for.
if let dns = self.dns {
try await agent.configureDNS(config: dns, location: rootfs.destination)
}
if let hosts = self.hosts {
try await agent.configureHosts(config: hosts, location: rootfs.destination)
}
try state.setCreated(vm: vm, relayManager: relayManager)
}
@@ -144,6 +144,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtoc
callOptions: CallOptions?
) -> UnaryCall<Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse>
func configureHosts(
_ request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
callOptions: CallOptions?
) -> UnaryCall<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>
func sync(
_ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest,
callOptions: CallOptions?
@@ -557,6 +562,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtocol {
)
}
/// Configure /etc/hosts.
///
/// - Parameters:
/// - request: Request to send to ConfigureHosts.
/// - callOptions: Call options.
/// - Returns: A `UnaryCall` with futures for the metadata, status and response.
public func configureHosts(
_ request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
callOptions: CallOptions? = nil
) -> UnaryCall<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse> {
return self.makeUnaryCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.configureHosts.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeConfigureHostsInterceptors() ?? []
)
}
/// Perform the sync syscall.
///
/// - Parameters:
@@ -767,6 +790,11 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientP
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse>
func makeConfigureHostsCall(
_ request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>
func makeSyncCall(
_ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest,
callOptions: CallOptions?
@@ -1052,6 +1080,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco
)
}
public func makeConfigureHostsCall(
_ request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
callOptions: CallOptions? = nil
) -> GRPCAsyncUnaryCall<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse> {
return self.makeAsyncUnaryCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.configureHosts.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeConfigureHostsInterceptors() ?? []
)
}
public func makeSyncCall(
_ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest,
callOptions: CallOptions? = nil
@@ -1343,6 +1383,18 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco
)
}
public func configureHosts(
_ request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
callOptions: CallOptions? = nil
) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse {
return try await self.performAsyncUnaryCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.configureHosts.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeConfigureHostsInterceptors() ?? []
)
}
public func sync(
_ request: Com_Apple_Containerization_Sandbox_V3_SyncRequest,
callOptions: CallOptions? = nil
@@ -1453,6 +1505,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientInterc
/// - Returns: Interceptors to use when invoking 'configureDns'.
func makeConfigureDnsInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse>]
/// - Returns: Interceptors to use when invoking 'configureHosts'.
func makeConfigureHostsInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>]
/// - Returns: Interceptors to use when invoking 'sync'.
func makeSyncInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_SyncRequest, Com_Apple_Containerization_Sandbox_V3_SyncResponse>]
@@ -1487,6 +1542,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata {
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.ipRouteAddLink,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.ipRouteAddDefault,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.configureDns,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.configureHosts,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.sync,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.kill,
]
@@ -1625,6 +1681,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata {
type: GRPCCallType.unary
)
public static let configureHosts = GRPCMethodDescriptor(
name: "ConfigureHosts",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/ConfigureHosts",
type: GRPCCallType.unary
)
public static let sync = GRPCMethodDescriptor(
name: "Sync",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/Sync",
@@ -1712,6 +1774,9 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider: Ca
/// Configure DNS resolver.
func configureDns(request: Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse>
/// Configure /etc/hosts.
func configureHosts(request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>
/// Perform the sync syscall.
func sync(request: Com_Apple_Containerization_Sandbox_V3_SyncRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_SyncResponse>
@@ -1929,6 +1994,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider {
userFunction: self.configureDns(request:context:)
)
case "ConfigureHosts":
return UnaryServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>(),
interceptors: self.interceptors?.makeConfigureHostsInterceptors() ?? [],
userFunction: self.configureHosts(request:context:)
)
case "Sync":
return UnaryServerHandler(
context: context,
@@ -2094,6 +2168,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvide
context: GRPCAsyncServerCallContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse
/// Configure /etc/hosts.
func configureHosts(
request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
context: GRPCAsyncServerCallContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse
/// Perform the sync syscall.
func sync(
request: Com_Apple_Containerization_Sandbox_V3_SyncRequest,
@@ -2324,6 +2404,15 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvider {
wrapping: { try await self.configureDns(request: $0, context: $1) }
)
case "ConfigureHosts":
return GRPCAsyncServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>(),
interceptors: self.interceptors?.makeConfigureHostsInterceptors() ?? [],
wrapping: { try await self.configureHosts(request: $0, context: $1) }
)
case "Sync":
return GRPCAsyncServerHandler(
context: context,
@@ -2438,6 +2527,10 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextServerInterc
/// Defaults to calling `self.makeInterceptors()`.
func makeConfigureDnsInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse>]
/// - Returns: Interceptors to use when handling 'configureHosts'.
/// Defaults to calling `self.makeInterceptors()`.
func makeConfigureHostsInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse>]
/// - Returns: Interceptors to use when handling 'sync'.
/// Defaults to calling `self.makeInterceptors()`.
func makeSyncInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_SyncRequest, Com_Apple_Containerization_Sandbox_V3_SyncResponse>]
@@ -2474,6 +2567,7 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata {
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.ipRouteAddLink,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.ipRouteAddDefault,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.configureDns,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.configureHosts,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.sync,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.kill,
]
@@ -2612,6 +2706,12 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata {
type: GRPCCallType.unary
)
public static let configureHosts = GRPCMethodDescriptor(
name: "ConfigureHosts",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/ConfigureHosts",
type: GRPCCallType.unary
)
public static let sync = GRPCMethodDescriptor(
name: "Sync",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/Sync",
@@ -830,6 +830,66 @@ public struct Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse: Sendab
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
public var location: String = String()
public var entries: [Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest.HostsEntry] = []
public var comment: String {
get {return _comment ?? String()}
set {_comment = newValue}
}
/// Returns true if `comment` has been explicitly set.
public var hasComment: Bool {return self._comment != nil}
/// Clears the value of `comment`. Subsequent reads from it will return its default value.
public mutating func clearComment() {self._comment = nil}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public struct HostsEntry: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
public var ipAddress: String = String()
public var hostnames: [String] = []
public var comment: String {
get {return _comment ?? String()}
set {_comment = newValue}
}
/// Returns true if `comment` has been explicitly set.
public var hasComment: Bool {return self._comment != nil}
/// Clears the value of `comment`. Subsequent reads from it will return its default value.
public mutating func clearComment() {self._comment = nil}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
fileprivate var _comment: String? = nil
}
public init() {}
fileprivate var _comment: String? = nil
}
public struct Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_SyncRequest: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@@ -2415,6 +2475,121 @@ extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse: SwiftProto
}
}
extension Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".ConfigureHostsRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "location"),
2: .same(proto: "entries"),
3: .same(proto: "comment"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.location) }()
case 2: try { try decoder.decodeRepeatedMessageField(value: &self.entries) }()
case 3: try { try decoder.decodeSingularStringField(value: &self._comment) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.location.isEmpty {
try visitor.visitSingularStringField(value: self.location, fieldNumber: 1)
}
if !self.entries.isEmpty {
try visitor.visitRepeatedMessageField(value: self.entries, fieldNumber: 2)
}
try { if let v = self._comment {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
} }()
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest, rhs: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest) -> Bool {
if lhs.location != rhs.location {return false}
if lhs.entries != rhs.entries {return false}
if lhs._comment != rhs._comment {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest.HostsEntry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest.protoMessageName + ".HostsEntry"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "ipAddress"),
2: .same(proto: "hostnames"),
3: .same(proto: "comment"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.ipAddress) }()
case 2: try { try decoder.decodeRepeatedStringField(value: &self.hostnames) }()
case 3: try { try decoder.decodeSingularStringField(value: &self._comment) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
if !self.ipAddress.isEmpty {
try visitor.visitSingularStringField(value: self.ipAddress, fieldNumber: 1)
}
if !self.hostnames.isEmpty {
try visitor.visitRepeatedStringField(value: self.hostnames, fieldNumber: 2)
}
try { if let v = self._comment {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
} }()
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest.HostsEntry, rhs: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest.HostsEntry) -> Bool {
if lhs.ipAddress != rhs.ipAddress {return false}
if lhs.hostnames != rhs.hostnames {return false}
if lhs._comment != rhs._comment {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".ConfigureHostsResponse"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
// Load everything into unknown fields
while try decoder.nextFieldNumber() != nil {}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse, rhs: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse) -> Bool {
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_SyncRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".SyncRequest"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
@@ -52,6 +52,8 @@ service SandboxContext {
rpc IpRouteAddDefault(IpRouteAddDefaultRequest) returns (IpRouteAddDefaultResponse);
// Configure DNS resolver.
rpc ConfigureDns(ConfigureDnsRequest) returns (ConfigureDnsResponse);
// Configure /etc/hosts.
rpc ConfigureHosts(ConfigureHostsRequest) returns (ConfigureHostsResponse);
// Perform the sync syscall.
rpc Sync(SyncRequest) returns (SyncResponse);
// Send a signal to a process via the PID.
@@ -239,6 +241,19 @@ message ConfigureDnsRequest {
message ConfigureDnsResponse {}
message ConfigureHostsRequest {
message HostsEntry {
string ipAddress = 1;
repeated string hostnames = 2;
optional string comment = 3;
}
string location = 1;
repeated HostsEntry entries = 2;
optional string comment = 3;
}
message ConfigureHostsResponse {}
message SyncRequest {}
message SyncResponse {}
@@ -60,10 +60,15 @@ public protocol VirtualMachineAgent: Sendable {
func addressAdd(name: String, address: String) async throws
func routeAddDefault(name: String, gateway: String) async throws
func configureDNS(config: DNS, location: String) async throws
func configureHosts(config: Hosts, location: String) async throws
}
extension VirtualMachineAgent {
public func closeProcessStdin(id: String, containerID: String?) async throws {
throw ContainerizationError(.unsupported, message: "closeProcessStdin")
}
public func configureHosts(config: Hosts, location: String) async throws {
throw ContainerizationError(.unsupported, message: "configureHosts")
}
}
+33
View File
@@ -418,6 +418,39 @@ extension IntegrationSuite {
}
}
func testHostsFile() async throws {
let id = "test-container-hosts-file"
let bs = try await bootstrap()
let container = LinuxContainer(
id,
rootfs: bs.rootfs,
vmm: bs.vmm
)
container.arguments = ["cat", "/etc/hosts"]
let entry = Hosts.Entry.localHostIPV4(comment: "Testaroo")
container.hosts = Hosts(entries: [entry])
let buffer = BufferWriter()
container.stdout = buffer
try await container.create()
try await container.start()
let status = try await container.wait()
try await container.stop()
guard status == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}
let expected = entry.rendered
guard String(data: buffer.data, encoding: .utf8) == "\(expected)\n" else {
throw IntegrationError.assert(
msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'")
}
}
func testProcessStdin() async throws {
let id = "test-container-stdin"
+1
View File
@@ -206,6 +206,7 @@ struct IntegrationSuite: AsyncParsableCommand {
"multiple concurrent processes": testMultipleConcurrentProcesses,
"multiple concurrent processes with output stress": testMultipleConcurrentProcessesOutputStress,
"container hostname": testHostname,
"container hosts": testHostsFile,
"container mount": testMounts,
"nested virt": testNestedVirtualizationEnabled,
]
+7
View File
@@ -119,6 +119,7 @@ extension Application {
container.mounts.append(czMount)
}
var hosts = Hosts.default
if let ip {
guard let gateway else {
throw ContainerizationError(.invalidArgument, message: "gateway must be specified")
@@ -128,7 +129,13 @@ extension Application {
if nameservers.count > 0 {
container.dns = .init(nameservers: nameservers)
}
hosts.entries.append(
Hosts.Entry(
ipAddress: ip,
hostnames: [id]
))
}
container.hosts = hosts
try await container.create()
try await container.start()
+48
View File
@@ -794,6 +794,38 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
return .init()
}
func configureHosts(
request: Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest,
context: GRPC.GRPCAsyncServerCallContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_ConfigureHostsResponse {
log.debug(
"configureHosts",
metadata: [
"location": "\(request.location)"
])
do {
let etc = URL(fileURLWithPath: request.location).appendingPathComponent("etc")
try FileManager.default.createDirectory(atPath: etc.path, withIntermediateDirectories: true)
let hostsPath = etc.appendingPathComponent("hosts")
let config = request.toCZHosts()
let text = config.hostsFile
try text.write(toFile: hostsPath.path, atomically: true, encoding: .utf8)
log.debug("wrote /etc/hosts configuration", metadata: ["path": "\(hostsPath.path)"])
} catch {
log.error(
"configureHosts",
metadata: [
"error": "\(error)"
])
throw GRPCStatus(code: .internalError, message: "configureHosts: \(error)")
}
return .init()
}
private func swiftErrno(_ msg: Logger.Message) -> POSIXError {
let error = POSIXError(.init(rawValue: errno)!)
log.error(
@@ -832,6 +864,22 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
}
}
extension Com_Apple_Containerization_Sandbox_V3_ConfigureHostsRequest {
func toCZHosts() -> Hosts {
let entries = self.entries.map {
Hosts.Entry(
ipAddress: $0.ipAddress,
hostnames: $0.hostnames,
comment: $0.hasComment ? $0.comment : nil
)
}
return Hosts(
entries: entries,
comment: self.hasComment ? self.comment : nil
)
}
}
extension Initd {
func ociAlterations(ociSpec: inout ContainerizationOCI.Spec) throws {
guard var process = ociSpec.process else {