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
31 changes: 29 additions & 2 deletions Sources/Containerization/DNSConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ public struct DNS: Sendable {
/// The set of default nameservers to use if none are provided
/// in the constructor.
public static let defaultNameservers = ["1.1.1.1"]
// Redirect-target ports must be unprivileged (>= 1024).
private static let minimumRedirectPort: UInt16 = 1024

/// The nameservers a container should use.
public var nameservers: [String]
/// Optional nftables DNS redirect targets: each (IPv4 address, port) pair
/// consumed by the guest alongside the corresponding nameserver.
public var redirectTargets: [(ip: String, port: UInt16)]
/// The DNS domain to use.
public var domain: String?
/// The DNS search domains to use.
Expand All @@ -35,23 +40,27 @@ public struct DNS: Sendable {

public init(
nameservers: [String] = defaultNameservers,
redirectTargets: [(ip: String, port: UInt16)] = [],
domain: String? = nil,
searchDomains: [String] = [],
options: [String] = []
) {
self.nameservers = nameservers
self.redirectTargets = redirectTargets
self.domain = domain
self.searchDomains = searchDomains
self.options = options
}

/// Validates the DNS configuration.
///
/// Ensures that all nameserver entries are valid IPv4 or IPv6 addresses.
/// Ensures that all nameserver entries are valid IPv4 or IPv6 addresses,
/// each redirect target is a valid IPv4 address with a port >= 1024.
/// Arbitrary hostnames are not permitted as nameservers.
///
/// - Throws: ``ContainerizationError`` with code `.invalidArgument` if
/// any nameserver is not a valid IP address.
/// any nameserver is not a valid IP address or any redirect target is
/// not a valid `ipv4:port` (>= 1024).
public func validate() throws {
for nameserver in nameservers {
let isValidIPv4 = (try? IPv4Address(nameserver)) != nil
Expand All @@ -63,13 +72,31 @@ public struct DNS: Sendable {
)
}
}
for (ip, port) in redirectTargets {
guard (try? IPv4Address(ip)) != nil else {
throw ContainerizationError(
.invalidArgument,
message: "DNS redirect target '\(ip):\(port)' is not a valid IPv4 address"
)
}
guard port >= Self.minimumRedirectPort else {
throw ContainerizationError(
.invalidArgument,
message: "DNS redirect target '\(ip):\(port)' specifies port \(port), which must be >= 1024"
)
}
}
}
}

extension DNS {
public var resolvConf: String {
var text = ""

if !redirectTargets.isEmpty {
text += "# DNS traffic may be redirected by an nftables rule; verify with: nft list ruleset\n"
}

if !nameservers.isEmpty {
text += nameservers.map { "nameserver \($0)" }.joined(separator: "\n") + "\n"
}
Expand Down
58 changes: 57 additions & 1 deletion Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1376,13 +1376,29 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequ

public var options: [String] = []

public var redirectTargets: [Com_Apple_Containerization_Sandbox_V3_DnsRedirectTarget] = []

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}

fileprivate var _domain: String? = nil
}

public nonisolated struct Com_Apple_Containerization_Sandbox_V3_DnsRedirectTarget: 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 ip: String = String()

public var port: UInt32 = 0

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
}

public nonisolated struct Com_Apple_Containerization_Sandbox_V3_ConfigureDnsResponse: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
Expand Down Expand Up @@ -3866,7 +3882,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRes

nonisolated extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".ConfigureDnsRequest"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}location\0\u{1}nameservers\0\u{1}domain\0\u{1}searchDomains\0\u{1}options\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}location\0\u{1}nameservers\0\u{1}domain\0\u{1}searchDomains\0\u{1}options\0\u{1}redirectTargets\0")

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
Expand All @@ -3879,6 +3895,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest:
case 3: try { try decoder.decodeSingularStringField(value: &self._domain) }()
case 4: try { try decoder.decodeRepeatedStringField(value: &self.searchDomains) }()
case 5: try { try decoder.decodeRepeatedStringField(value: &self.options) }()
case 6: try { try decoder.decodeRepeatedMessageField(value: &self.redirectTargets) }()
default: break
}
}
Expand All @@ -3904,6 +3921,9 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest:
if !self.options.isEmpty {
try visitor.visitRepeatedStringField(value: self.options, fieldNumber: 5)
}
if !self.redirectTargets.isEmpty {
try visitor.visitRepeatedMessageField(value: self.redirectTargets, fieldNumber: 6)
}
try unknownFields.traverse(visitor: &visitor)
}

Expand All @@ -3913,6 +3933,42 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ConfigureDnsRequest:
if lhs._domain != rhs._domain {return false}
if lhs.searchDomains != rhs.searchDomains {return false}
if lhs.options != rhs.options {return false}
if lhs.redirectTargets != rhs.redirectTargets {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

nonisolated extension Com_Apple_Containerization_Sandbox_V3_DnsRedirectTarget: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".DnsRedirectTarget"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}ip\0\u{1}port\0")

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.ip) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.port) }()
default: break
}
}
}

public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.ip.isEmpty {
try visitor.visitSingularStringField(value: self.ip, fieldNumber: 1)
}
if self.port != 0 {
try visitor.visitSingularUInt32Field(value: self.port, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}

public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_DnsRedirectTarget, rhs: Com_Apple_Containerization_Sandbox_V3_DnsRedirectTarget) -> Bool {
if lhs.ip != rhs.ip {return false}
if lhs.port != rhs.port {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/Containerization/SandboxContext/SandboxContext.proto
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,12 @@ message ConfigureDnsRequest {
optional string domain = 3;
repeated string searchDomains = 4;
repeated string options = 5;
repeated DnsRedirectTarget redirectTargets = 6;
}

message DnsRedirectTarget {
string ip = 1;
uint32 port = 2;
}

message ConfigureDnsResponse {}
Expand Down
6 changes: 6 additions & 0 deletions Sources/Containerization/Vminitd.swift
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ extension Vminitd {
.with {
$0.location = location
$0.nameservers = config.nameservers
$0.redirectTargets = config.redirectTargets.map { entry in
.with {
$0.ip = entry.ip
$0.port = UInt32(entry.port)
}
}
if let domain = config.domain {
$0.domain = domain
}
Expand Down
8 changes: 5 additions & 3 deletions Sources/ContainerizationNetlink/NetlinkSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ public class DefaultNetlinkSocket: NetlinkSocket {
public let pid: UInt32

/// Creates a new instance.
public init() throws {
/// - Parameter socketProtocol: The netlink protocol to use (default
/// `NetlinkProtocol.NETLINK_ROUTE`).
public init(socketProtocol: Int32 = NetlinkProtocol.NETLINK_ROUTE) throws {
pid = UInt32(getpid())
sockfd = osSocket(Int32(AddressFamily.AF_NETLINK), SocketType.SOCK_RAW, NetlinkProtocol.NETLINK_ROUTE)
sockfd = osSocket(Int32(AddressFamily.AF_NETLINK), SocketType.SOCK_RAW, socketProtocol)
guard sockfd >= 0 else {
throw NetlinkSocketError.socketFailure(rc: errno)
}
Expand Down Expand Up @@ -128,7 +130,7 @@ public class DefaultNetlinkSocket: NetlinkSocket {
public class DefaultNetlinkSocket: NetlinkSocket {
public var pid: UInt32 { 0 }

public init() throws {}
public init(socketProtocol: Int32 = NetlinkProtocol.NETLINK_ROUTE) throws {}

public func send(buf: UnsafeRawPointer!, len: Int, flags: Int32) throws -> Int {
throw NetlinkSocketError.notImplemented
Expand Down
Loading