Use typesafe IP/CIDR parameters everywhere. (#448)

- Closes #445.
- Adopts refined IPv4 and IPv6 types developed
  by Agam Dua <agamdua@users.noreply.github.com>.
- For type safety and clarity, use IP and CIDR types
  where we were previously using String.

Co-authored-by: Agam Dua <agamdua@users.noreply.github.com>
This commit is contained in:
J LoganandAgam Dua authored and GitHub committed 2025-12-16 09:58:44 -03:00
1 parent 68694942ec
commit 9ba8267afb
36 files changed
+3687 -666

No files matched your search

+29 -28
View File
@@ -49,19 +49,19 @@ public struct ContainerManager: Sendable {
nonisolated(unsafe) private let reference: vmnet_network_ref
/// The IPv4 subnet of this network.
public let subnet: CIDRAddress
public let subnet: CIDRv4
/// The gateway address of this network.
public var gateway: IPv4Address {
/// The IPv4 gateway address of this network.
public var ipv4Gateway: IPv4Address {
subnet.gateway
}
struct Allocator: Sendable {
private let addressAllocator: any AddressAllocator<UInt32>
private let cidr: CIDRAddress
private let cidr: CIDRv4
private var allocations: [String: UInt32]
init(cidr: CIDRAddress) throws {
init(cidr: CIDRv4) throws {
self.cidr = cidr
self.allocations = .init()
let size = Int(cidr.upper.value - cidr.lower.value - 3)
@@ -71,14 +71,14 @@ public struct ContainerManager: Sendable {
)
}
mutating func allocate(_ id: String) throws -> String {
mutating func allocate(_ id: String) throws -> CIDRv4 {
if allocations[id] != nil {
throw ContainerizationError(.exists, message: "allocation with id \(id) already exists")
}
let index = try addressAllocator.allocate()
allocations[id] = index
let ip = IPv4Address(fromValue: index)
return try CIDRAddress(ip, prefixLength: cidr.prefixLength).description
let ip = IPv4Address(index)
return try CIDRv4(ip, prefix: cidr.prefix)
}
mutating func release(_ id: String) throws {
@@ -91,8 +91,8 @@ public struct ContainerManager: Sendable {
/// A network interface supporting the vmnet_network_ref.
public struct Interface: Containerization.Interface, VZInterface, Sendable {
public let address: String
public let gateway: String?
public let ipv4Address: CIDRv4
public let ipv4Gateway: IPv4Address?
public let macAddress: String?
// `reference` isn't used concurrently.
@@ -100,12 +100,12 @@ public struct ContainerManager: Sendable {
public init(
reference: vmnet_network_ref,
address: String,
gateway: String,
ipv4Address: CIDRv4,
ipv4Gateway: IPv4Address,
macAddress: String? = nil
) {
self.address = address
self.gateway = gateway
self.ipv4Address = ipv4Address
self.ipv4Gateway = ipv4Gateway
self.macAddress = macAddress
self.reference = reference
}
@@ -126,7 +126,7 @@ public struct ContainerManager: Sendable {
/// Creates a new network.
/// - Parameter subnet: The subnet to use for this network.
public init(subnet: String? = nil) throws {
public init(subnet: CIDRv4? = nil) throws {
var status: vmnet_return_t = .VMNET_FAILURE
guard let config = vmnet_network_configuration_create(.VMNET_SHARED_MODE, &status) else {
throw ContainerizationError(.unsupported, message: "failed to create vmnet config with status \(status)")
@@ -135,7 +135,7 @@ public struct ContainerManager: Sendable {
vmnet_network_configuration_disable_dhcp(config)
if let subnet {
try Self.configureSubnet(config, subnet: try CIDRAddress(subnet))
try Self.configureSubnet(config, subnet: subnet)
}
guard let ref = vmnet_network_create(config, &status), status == .VMNET_SUCCESS else {
@@ -152,11 +152,11 @@ public struct ContainerManager: Sendable {
/// Returns a new interface for use with a container.
/// - Parameter id: The container ID.
public mutating func create(_ id: String) throws -> Containerization.Interface? {
let address = try allocator.allocate(id)
let ipv4Address = try allocator.allocate(id)
return Self.Interface(
reference: self.reference,
address: address,
gateway: self.gateway.description,
ipv4Address: ipv4Address,
ipv4Gateway: self.ipv4Gateway,
)
}
@@ -166,7 +166,7 @@ public struct ContainerManager: Sendable {
try allocator.release(id)
}
private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRAddress {
private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRv4 {
var subnet = in_addr()
var mask = in_addr()
vmnet_network_get_ipv4_subnet(ref, &subnet, &mask)
@@ -174,19 +174,19 @@ public struct ContainerManager: Sendable {
let sa = UInt32(bigEndian: subnet.s_addr)
let mv = UInt32(bigEndian: mask.s_addr)
let lower = IPv4Address(fromValue: sa & mv)
let upper = IPv4Address(fromValue: lower.value + ~mv)
let lower = IPv4Address(sa & mv)
let upper = IPv4Address(lower.value + ~mv)
return try CIDRAddress(lower: lower, upper: upper)
return try CIDRv4(lower: lower, upper: upper)
}
private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRAddress) throws {
private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRv4) throws {
let gateway = subnet.gateway
var ga = in_addr()
inet_pton(AF_INET, gateway.description, &ga)
let mask = IPv4Address(fromValue: subnet.prefixLength.prefixMask32)
let mask = IPv4Address(subnet.prefix.prefixMask32)
var ma = in_addr()
inet_pton(AF_INET, mask.description, &ma)
@@ -415,7 +415,8 @@ public struct ContainerManager: Sendable {
}
if let interface = try self.network?.create(id) {
config.interfaces = [interface]
config.dns = .init(nameservers: [interface.gateway!])
// FIXME: throw instead of crash here if we can't unwrap?
config.dns = .init(nameservers: [interface.ipv4Gateway!.description])
}
config.bootLog = BootLog.file(path: self.containerRoot.appendingPathComponent(id).appendingPathComponent("bootlog.log"))
try configuration(&config)
@@ -454,10 +455,10 @@ public struct ContainerManager: Sendable {
}
}
extension CIDRAddress {
extension CIDRv4 {
/// The gateway address of the network.
public var gateway: IPv4Address {
IPv4Address(fromValue: self.lower.value + 1)
IPv4Address(self.lower.value + 1)
}
}
+4 -2
View File
@@ -14,14 +14,16 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationExtras
/// A network interface.
public protocol Interface: Sendable {
/// The interface IPv4 address and subnet prefix length, as a CIDR address.
/// Example: `192.168.64.3/24`
var address: String { get }
var ipv4Address: CIDRv4 { get }
/// The IP address for the default route, or nil for no default route.
var gateway: String? { get }
var ipv4Gateway: IPv4Address? { get }
/// The interface MAC address, or nil to auto-configure the address.
var macAddress: String? { get }
@@ -434,10 +434,10 @@ extension LinuxContainer {
// 3. If a gateway IP address is present, add the default route.
for (index, i) in self.interfaces.enumerated() {
let name = "eth\(index)"
try await agent.addressAdd(name: name, address: i.address)
try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address)
try await agent.up(name: name, mtu: 1280)
if let gateway = i.gateway {
try await agent.routeAddDefault(name: name, gateway: gateway)
if let ipv4Gateway = i.ipv4Gateway {
try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway)
}
}
+3 -3
View File
@@ -393,10 +393,10 @@ extension LinuxPod {
// 3. If a gateway IP address is present, add the default route.
for (index, i) in self.interfaces.enumerated() {
let name = "eth\(index)"
try await agent.addressAdd(name: name, address: i.address)
try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address)
try await agent.up(name: name, mtu: 1280)
if let gateway = i.gateway {
try await agent.routeAddDefault(name: name, gateway: gateway)
if let ipv4Gateway = i.ipv4Gateway {
try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway)
}
}
+7 -5
View File
@@ -14,14 +14,16 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationExtras
public struct NATInterface: Interface {
public var address: String
public var gateway: String?
public var ipv4Address: CIDRv4
public var ipv4Gateway: IPv4Address?
public var macAddress: String?
public init(address: String, gateway: String?, macAddress: String? = nil) {
self.address = address
self.gateway = gateway
public init(ipv4Address: CIDRv4, ipv4Gateway: IPv4Address?, macAddress: String? = nil) {
self.ipv4Address = ipv4Address
self.ipv4Gateway = ipv4Gateway
self.macAddress = macAddress
}
}
@@ -19,6 +19,7 @@
import vmnet
import Virtualization
import ContainerizationError
import ContainerizationExtras
import Foundation
import Synchronization
@@ -26,8 +27,8 @@ import Synchronization
/// container/virtual machine.
@available(macOS 26, *)
public final class NATNetworkInterface: Interface, Sendable {
public let address: String
public let gateway: String?
public let ipv4Address: CIDRv4
public let ipv4Gateway: IPv4Address?
public let macAddress: String?
@available(macOS 26, *)
@@ -36,25 +37,25 @@ public final class NATNetworkInterface: Interface, Sendable {
@available(macOS 26, *)
public init(
address: String,
gateway: String?,
ipv4Address: CIDRv4,
ipv4Gateway: IPv4Address?,
reference: sending vmnet_network_ref,
macAddress: String? = nil
) {
self.address = address
self.gateway = gateway
self.ipv4Address = ipv4Address
self.ipv4Gateway = ipv4Gateway
self.macAddress = macAddress
self.reference = reference
}
@available(macOS, obsoleted: 26, message: "Use init(address:gateway:reference:macAddress:) instead")
@available(macOS, obsoleted: 26, message: "Use init(ipv4Address:ipv4Gateway:reference:macAddress:) instead")
public init(
address: String,
gateway: String?,
ipv4Address: CIDRv4,
ipv4Gateway: IPv4Address?,
macAddress: String? = nil
) {
self.address = address
self.gateway = gateway
self.ipv4Address = ipv4Address
self.ipv4Gateway = ipv4Gateway
self.macAddress = macAddress
self.reference = nil
}
@@ -798,7 +798,7 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: Sendable {
public var interface: String = String()
public var address: String = String()
public var ipv4Address: String = String()
public var unknownFields = SwiftProtobuf.UnknownStorage()
@@ -822,9 +822,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: Senda
public var interface: String = String()
public var address: String = String()
public var dstIpv4Addr: String = String()
public var srcAddr: String = String()
public var srcIpv4Addr: String = String()
public var unknownFields = SwiftProtobuf.UnknownStorage()
@@ -848,7 +848,7 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: Se
public var interface: String = String()
public var gateway: String = String()
public var ipv4Gateway: String = String()
public var unknownFields = SwiftProtobuf.UnknownStorage()
@@ -2636,7 +2636,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf.
public static let protoMessageName: String = _protobuf_package + ".IpAddrAddRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "interface"),
2: .same(proto: "address"),
2: .same(proto: "ipv4Address"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@@ -2646,7 +2646,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf.
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.address) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.ipv4Address) }()
default: break
}
}
@@ -2656,15 +2656,15 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf.
if !self.interface.isEmpty {
try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1)
}
if !self.address.isEmpty {
try visitor.visitSingularStringField(value: self.address, fieldNumber: 2)
if !self.ipv4Address.isEmpty {
try visitor.visitSingularStringField(value: self.ipv4Address, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest) -> Bool {
if lhs.interface != rhs.interface {return false}
if lhs.address != rhs.address {return false}
if lhs.ipv4Address != rhs.ipv4Address {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@@ -2693,8 +2693,8 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt
public static let protoMessageName: String = _protobuf_package + ".IpRouteAddLinkRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "interface"),
2: .same(proto: "address"),
3: .same(proto: "srcAddr"),
2: .same(proto: "dstIpv4Addr"),
3: .same(proto: "srcIpv4Addr"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@@ -2704,8 +2704,8 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.address) }()
case 3: try { try decoder.decodeSingularStringField(value: &self.srcAddr) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.dstIpv4Addr) }()
case 3: try { try decoder.decodeSingularStringField(value: &self.srcIpv4Addr) }()
default: break
}
}
@@ -2715,19 +2715,19 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt
if !self.interface.isEmpty {
try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1)
}
if !self.address.isEmpty {
try visitor.visitSingularStringField(value: self.address, fieldNumber: 2)
if !self.dstIpv4Addr.isEmpty {
try visitor.visitSingularStringField(value: self.dstIpv4Addr, fieldNumber: 2)
}
if !self.srcAddr.isEmpty {
try visitor.visitSingularStringField(value: self.srcAddr, fieldNumber: 3)
if !self.srcIpv4Addr.isEmpty {
try visitor.visitSingularStringField(value: self.srcIpv4Addr, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest) -> Bool {
if lhs.interface != rhs.interface {return false}
if lhs.address != rhs.address {return false}
if lhs.srcAddr != rhs.srcAddr {return false}
if lhs.dstIpv4Addr != rhs.dstIpv4Addr {return false}
if lhs.srcIpv4Addr != rhs.srcIpv4Addr {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@@ -2756,7 +2756,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftP
public static let protoMessageName: String = _protobuf_package + ".IpRouteAddDefaultRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "interface"),
2: .same(proto: "gateway"),
2: .same(proto: "ipv4Gateway"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@@ -2766,7 +2766,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftP
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.gateway) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.ipv4Gateway) }()
default: break
}
}
@@ -2776,15 +2776,15 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftP
if !self.interface.isEmpty {
try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1)
}
if !self.gateway.isEmpty {
try visitor.visitSingularStringField(value: self.gateway, fieldNumber: 2)
if !self.ipv4Gateway.isEmpty {
try visitor.visitSingularStringField(value: self.ipv4Gateway, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest) -> Bool {
if lhs.interface != rhs.interface {return false}
if lhs.gateway != rhs.gateway {return false}
if lhs.ipv4Gateway != rhs.ipv4Gateway {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@@ -235,22 +235,22 @@ message IpLinkSetResponse {}
message IpAddrAddRequest {
string interface = 1;
string address = 2;
string ipv4Address = 2;
}
message IpAddrAddResponse {}
message IpRouteAddLinkRequest {
string interface = 1;
string address = 2;
string srcAddr = 3;
string dstIpv4Addr = 2;
string srcIpv4Addr = 3;
}
message IpRouteAddLinkResponse {}
message IpRouteAddDefaultRequest {
string interface = 1;
string gateway = 2;
string ipv4Gateway = 2;
}
message IpRouteAddDefaultResponse {}
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import Foundation
@@ -66,8 +67,8 @@ public protocol VirtualMachineAgent: Sendable {
// Networking
func up(name: String, mtu: UInt32?) async throws
func down(name: String) async throws
func addressAdd(name: String, address: String) async throws
func routeAddDefault(name: String, gateway: String) async throws
func addressAdd(name: String, ipv4Address: CIDRv4) async throws
func routeAddDefault(name: String, ipv4Gateway: IPv4Address) async throws
func configureDNS(config: DNS, location: String) async throws
func configureHosts(config: Hosts, location: String) async throws
+5 -4
View File
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import ContainerizationOS
import Foundation
@@ -359,20 +360,20 @@ extension Vminitd {
}
/// Add an IP address to the sandbox's network interfaces.
public func addressAdd(name: String, address: String) async throws {
public func addressAdd(name: String, ipv4Address: CIDRv4) async throws {
_ = try await client.ipAddrAdd(
.with {
$0.interface = name
$0.address = address
$0.ipv4Address = ipv4Address.description
})
}
/// Set the default route in the sandbox's environment.
public func routeAddDefault(name: String, gateway: String) async throws {
public func routeAddDefault(name: String, ipv4Gateway: IPv4Address) async throws {
_ = try await client.ipRouteAddDefault(
.with {
$0.interface = name
$0.gateway = gateway
$0.ipv4Gateway = ipv4Gateway.description
})
}
+135
View File
@@ -0,0 +1,135 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// Describes an IPv4 or IPv6 CIDR address block.
@frozen
public enum CIDR: CustomStringConvertible, Equatable, Sendable, Hashable {
case v4(IPv4Address, Prefix)
case v6(IPv6Address, Prefix)
/// Create a CIDR address block.
public init(_ cidr: String) throws {
if let cidrV4 = try? CIDRv4(cidr) {
self = .v4(cidrV4.address, cidrV4.prefix)
} else if let cidrV6 = try? CIDRv6(cidr) {
self = .v6(cidrV6.address, cidrV6.prefix)
} else {
throw Error.invalidCIDR(cidr: cidr)
}
}
/// Create a CIDR address from a member IP and a prefix length.
public init(_ address: IPAddress, prefix: Prefix) throws {
switch address {
case .v4(let addr):
guard prefix.length <= 32 else {
throw Error.invalidCIDR(cidr: "\(address)/\(prefix)")
}
self = .v4(addr, prefix)
case .v6(let addr):
guard prefix.length <= 128 else {
throw Error.invalidCIDR(cidr: "\(address)/\(prefix)")
}
self = .v6(addr, prefix)
}
}
/// Create the smallest CIDR block that includes the lower and upper bounds.
///
/// For type-safe construction, prefer `v4Range(lower:upper:)` or `v6Range(lower:upper:)`.
public init(lower: IPAddress, upper: IPAddress) throws {
switch (lower, upper) {
case (.v4(let lowerAddr), .v4(let upperAddr)):
let cidr = try CIDRv4(lower: lowerAddr, upper: upperAddr)
self = .v4(cidr.address, cidr.prefix)
case (.v6(let lowerAddr), .v6(let upperAddr)):
let cidr = try CIDRv6(lower: lowerAddr, upper: upperAddr)
self = .v6(cidr.address, cidr.prefix)
default:
throw Error.invalidAddressRange(lower: lower.description, upper: upper.description)
}
}
/// The IP component of this CIDR address.
@inlinable
public var address: IPAddress {
switch self {
case .v4(let addr, _):
return .v4(addr)
case .v6(let addr, _):
return .v6(addr)
}
}
/// The prefix length of this CIDR address.
@inlinable
public var prefix: Prefix {
switch self {
case .v4(_, let prefix), .v6(_, let prefix):
return prefix
}
}
/// The lowest address in this CIDR block
@inlinable
public var lower: IPAddress {
switch self {
case (.v4(let addr, let prefix)):
return .v4(IPv4Address(addr.value & prefix.prefixMask32))
case (.v6(let addr, let prefix)):
return .v6(IPv6Address(addr.value & prefix.prefixMask128))
}
}
/// The highest address in this CIDR block (broadcast address).
@inlinable
public var upper: IPAddress {
switch self {
case .v4(let addr, let prefix):
return .v4(IPv4Address(addr.value | prefix.suffixMask32))
case .v6(let addr, let prefix):
return .v6(IPv6Address(addr.value | prefix.suffixMask128, zone: addr.zone))
}
}
/// Return true if the CIDR block contains the specified address.
///
/// Compares network portion of the given IP address.
@inlinable
public func contains(_ ip: IPAddress) -> Bool {
switch (self, ip) {
case (.v4(let network, let prefix), .v4(let ip)):
return network.value == (ip.value & prefix.prefixMask32)
case (.v6(let network, let prefix), .v6(let ip)):
return (network.zone == ip.zone) && (network.value == (ip.value & prefix.prefixMask128))
default:
return false
}
}
/// Retrieve the text representation of the CIDR block.
public var description: String {
"\(address)/\(prefix)"
}
}
extension CIDR {
public enum Error: Swift.Error {
case invalidCIDR(cidr: String)
case invalidAddressRange(lower: String, upper: String)
}
}
@@ -1,132 +0,0 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// Describes an IPv4 CIDR address block.
public struct CIDRAddress: CustomStringConvertible, Equatable, Sendable {
/// The base IPv4 address of the CIDR block.
public let lower: IPv4Address
/// The last IPv4 address of the CIDR block
public let upper: IPv4Address
/// The IPv4 address component of the CIDR block.
public let address: IPv4Address
/// The address prefix length for the CIDR block, which determines its size.
public let prefixLength: PrefixLength
/// Create an CIDR address block from its text representation.
public init(_ cidr: String) throws {
let split = cidr.components(separatedBy: "/")
guard split.count == 2 else {
throw NetworkAddressError.invalidCIDR(cidr: cidr)
}
address = try IPv4Address(split[0])
guard let prefixLength = PrefixLength(split[1]) else {
throw NetworkAddressError.invalidCIDR(cidr: cidr)
}
guard prefixLength >= 0 && prefixLength <= 32 else {
throw NetworkAddressError.invalidCIDR(cidr: cidr)
}
self.prefixLength = prefixLength
lower = address.prefix(prefixLength: prefixLength)
upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32)
}
/// Create a CIDR address from a member IP and a prefix length.
public init(_ address: IPv4Address, prefixLength: PrefixLength) throws {
guard prefixLength >= 0 && prefixLength <= 32 else {
throw NetworkAddressError.invalidCIDR(cidr: "\(address)/\(prefixLength)")
}
self.prefixLength = prefixLength
self.address = address
lower = address.prefix(prefixLength: prefixLength)
upper = IPv4Address(fromValue: lower.value + prefixLength.suffixMask32)
}
/// Create the smallest CIDR block that includes the lower and upper bounds.
public init(lower: IPv4Address, upper: IPv4Address) throws {
guard lower.value <= upper.value else {
throw NetworkAddressError.invalidAddressRange(lower: lower.description, upper: upper.description)
}
address = lower
for prefixLength: PrefixLength in 1...32 {
// find the first case where a subnet mask would put lower and upper in different CIDR block
let mask = prefixLength.prefixMask32
if (lower.value & mask) != (upper.value & mask) {
self.prefixLength = prefixLength - 1
self.lower = address.prefix(prefixLength: self.prefixLength)
self.upper = IPv4Address(fromValue: self.lower.value + self.prefixLength.suffixMask32)
return
}
}
// if lower and upper are same, create a /32 block
self.prefixLength = 32
self.lower = lower
self.upper = upper
}
/// Get the offset of the specified address, relative to the
/// base address for the CIDR block, returning nil if the block
/// does not contain the address.
public func getIndex(_ address: IPv4Address) -> UInt32? {
guard address.value >= lower.value && address.value <= upper.value else {
return nil
}
return address.value - lower.value
}
/// Return true if the CIDR block contains the specified address.
public func contains(ipv4: IPv4Address) -> Bool {
lower.value <= ipv4.value && ipv4.value <= upper.value
}
/// Return true if the CIDR block contains all addresses of another CIDR block.
public func contains(cidr: CIDRAddress) -> Bool {
lower.value <= cidr.lower.value && cidr.upper.value <= upper.value
}
/// Return true if the CIDR block shares any addresses with another CIDR block.
public func overlaps(cidr: CIDRAddress) -> Bool {
(lower.value <= cidr.lower.value && upper.value >= cidr.lower.value)
|| (upper.value >= cidr.upper.value && lower.value <= cidr.upper.value)
}
/// Retrieve the text representation of the CIDR block.
public var description: String {
"\(address)/\(prefixLength)"
}
}
extension CIDRAddress: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let text = try container.decode(String.self)
try self.init(text)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.description)
}
}
+101
View File
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// Describes an IPv4 CIDR address block.
@frozen
public struct CIDRv4: CustomStringConvertible, Equatable, Sendable, Hashable {
/// The IP component of this CIDR address.
public let address: IPv4Address
/// The prefix length of this CIDR address.
public let prefix: Prefix
/// Create a CIDR address block.
public init(_ cidr: String) throws {
let split = cidr.split(separator: "/")
guard split.count == 2 else {
throw CIDR.Error.invalidCIDR(cidr: cidr)
}
guard let prefixLength = UInt8(split[1]), let prefix = Prefix(length: prefixLength) else {
throw CIDR.Error.invalidCIDR(cidr: cidr)
}
let address = try IPv4Address(String(split[0]))
try self.init(address, prefix: prefix)
}
/// Create a CIDR address from a member IP and a prefix length.
public init(_ address: IPv4Address, prefix: Prefix) throws {
guard prefix.length <= 32 else {
throw CIDR.Error.invalidCIDR(cidr: "\(address)/\(prefix)")
}
self.address = address
self.prefix = prefix
}
/// Create the smallest IPv4 CIDR block that includes the lower and upper bounds.
///
/// - Parameters:
/// - lower: The lower bound IPv4 address
/// - upper: The upper bound IPv4 address
/// - Returns: The smallest CIDR block containing both addresses
/// - Throws: If lower > upper
public init(lower: IPv4Address, upper: IPv4Address) throws {
guard lower.value <= upper.value else {
throw CIDR.Error.invalidAddressRange(lower: lower.description, upper: upper.description)
}
for length in 1...32 {
let prefixLength = Prefix(unchecked: UInt8(length))
let mask = prefixLength.prefixMask32
if (lower.value & mask) != (upper.value & mask) {
let prefix = Prefix(unchecked: UInt8(length - 1))
let networkAddr = IPv4Address(lower.value & prefix.prefixMask32)
try self.init(networkAddr, prefix: prefix)
return
}
}
// Same address - /32 block
let prefix = Prefix(unchecked: 32)
let networkAddr = IPv4Address(lower.value & prefix.prefixMask32)
try self.init(networkAddr, prefix: prefix)
}
/// The lowest address in this CIDR block
@inlinable
public var lower: IPv4Address {
IPv4Address(address.value & prefix.prefixMask32)
}
/// The highest address in this CIDR block (broadcast address).
@inlinable
public var upper: IPv4Address {
IPv4Address(address.value | prefix.suffixMask32)
}
/// Return true if the CIDR block contains the specified address.
///
/// Compares network portion of the given IP address.
@inlinable
public func contains(_ ip: IPv4Address) -> Bool {
(address.value & prefix.prefixMask32) == (ip.value & prefix.prefixMask32)
}
/// Retrieve the text representation of the CIDR block.
public var description: String {
"\(address)/\(prefix)"
}
}
+102
View File
@@ -0,0 +1,102 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// Describes an IPv4 or IPv6 CIDR address block.
@frozen
public struct CIDRv6: CustomStringConvertible, Equatable, Sendable, Hashable {
/// The IP component of this CIDR address.
public let address: IPv6Address
/// The prefix length of this CIDR address.
public let prefix: Prefix
/// Create a CIDR address block.
public init(_ cidr: String) throws {
let split = cidr.split(separator: "/")
guard split.count == 2 else {
throw CIDR.Error.invalidCIDR(cidr: cidr)
}
guard let prefixLength = UInt8(split[1]), let prefix = Prefix(length: prefixLength) else {
throw CIDR.Error.invalidCIDR(cidr: cidr)
}
let address = try IPv6Address(String(split[0]))
try self.init(address, prefix: prefix)
}
/// Create a CIDR address from a member IP and a prefix length.
public init(_ address: IPv6Address, prefix: Prefix) throws {
guard prefix.length <= 128 else {
throw CIDR.Error.invalidCIDR(cidr: "\(address)/\(prefix)")
}
self.address = address
self.prefix = prefix
}
/// Create the smallest IPv6 CIDR block that includes the lower and upper bounds.
///
/// - Parameters:
/// - lower: The lower bound IPv6 address
/// - upper: The upper bound IPv6 address
/// - Returns: The smallest CIDR block containing both addresses
/// - Throws: If lower > upper or zones don't match
public init(lower: IPv6Address, upper: IPv6Address) throws {
guard lower.value <= upper.value && lower.zone == upper.zone else {
throw CIDR.Error.invalidAddressRange(lower: lower.description, upper: upper.description)
}
for length in 1...128 {
let prefixLength = Prefix(unchecked: UInt8(length))
let mask = prefixLength.prefixMask128
if (lower.value & mask) != (upper.value & mask) {
let prefix = Prefix(unchecked: UInt8(length - 1))
let networkAddr = IPv6Address(lower.value & prefix.prefixMask128, zone: lower.zone)
try self.init(networkAddr, prefix: prefix)
return
}
}
// Same address - /128 block
let prefix = Prefix(unchecked: 128)
let networkAddr = IPv6Address(lower.value & prefix.prefixMask128, zone: lower.zone)
try self.init(networkAddr, prefix: prefix)
}
/// The lowest address in this CIDR block
@inlinable
public var lower: IPv6Address {
IPv6Address(address.value & prefix.prefixMask128)
}
/// The highest address in this CIDR block (broadcast address).
@inlinable
public var upper: IPv6Address {
IPv6Address(address.value | prefix.suffixMask128, zone: address.zone)
}
/// Return true if the CIDR block contains the specified address.
///
/// Compares network portion of the given IP address.
@inlinable
public func contains(_ ip: IPv6Address) -> Bool {
(address.zone == ip.zone) && ((address.value & prefix.prefixMask128) == (ip.value & prefix.prefixMask128))
}
/// Retrieve the text representation of the CIDR block.
public var description: String {
"\(address)/\(prefix)"
}
}
+102 -59
View File
@@ -14,79 +14,122 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
/// Facilitates conversion between IPv4 address representations.
public struct IPv4Address: Codable, CustomStringConvertible, Equatable, Sendable {
/// The address as a 32-bit integer.
public let value: UInt32
/// Represents an IP address that can be either IPv4 or IPv6.
@frozen
public enum IPAddress: Sendable, Hashable, CustomStringConvertible, Equatable {
/// An IPv4 address
case v4(IPv4Address)
/// Create an address from a dotted-decimal string, such as "192.168.64.10".
public init(_ fromString: String) throws {
let split = fromString.components(separatedBy: ".")
if split.count != 4 {
throw NetworkAddressError.invalidStringAddress(address: fromString)
}
/// An IPv6 address
case v6(IPv6Address)
var parsedValue: UInt32 = 0
for index in 0..<4 {
guard let octet = UInt8(split[index]) else {
throw NetworkAddressError.invalidStringAddress(address: fromString)
/// Parses an IP address string, automatically detecting IPv4 or IPv6 format.
///
/// - Parameter string: IP address string to parse
/// - Returns: An `IPAddress` containing either an IPv4 or IPv6 address
/// - Throws: `IPAddressError.unableToParse` if invalid
public init(_ string: String) throws {
let utf8 = string.utf8
var hasColon = false
var hasDot = false
for byte in utf8 {
if byte == 58 { // ASCII ':'
hasColon = true
break
}
if byte == 46 { // ASCII '.'
hasDot = true
}
parsedValue |= UInt32(octet) << ((3 - index) * 8)
}
value = parsedValue
}
/// Create an address from an array of four bytes in network order (big-endian),
/// such as [192, 168, 64, 10].
public init(fromNetworkBytes: [UInt8]) throws {
guard fromNetworkBytes.count == 4 else {
throw NetworkAddressError.invalidNetworkByteAddress(address: fromNetworkBytes)
if hasColon {
let ipv6 = try IPv6Address.parse(string)
self = .v6(ipv6)
} else if hasDot {
let ipv4 = try IPv4Address(string)
self = .v4(ipv4)
} else {
throw IPAddressError.unableToParse
}
value =
(UInt32(fromNetworkBytes[0]) << 24)
| (UInt32(fromNetworkBytes[1]) << 16)
| (UInt32(fromNetworkBytes[2]) << 8)
| UInt32(fromNetworkBytes[3])
}
/// Create an address from a 32-bit integer, such as 0xc0a8_400a.
public init(fromValue: UInt32) {
value = fromValue
}
/// Retrieve the address as an array of bytes in network byte order.
public var networkBytes: [UInt8] {
[
UInt8((value >> 24) & 0xff),
UInt8((value >> 16) & 0xff),
UInt8((value >> 8) & 0xff),
UInt8(value & 0xff),
]
}
/// Retrieve the address as a dotted decimal string.
/// String representation of the IP address.
public var description: String {
networkBytes.map(String.init).joined(separator: ".")
switch self {
case .v4(let addr):
return addr.description
case .v6(let addr):
return addr.description
}
}
/// Create the base IPv4 address for a network that contains this
/// address and uses the specified subnet mask length.
public func prefix(prefixLength: PrefixLength) -> IPv4Address {
IPv4Address(fromValue: value & prefixLength.prefixMask32)
/// Returns `true` if this is an IPv4 address.
@inlinable
public var isV4: Bool {
if case .v4 = self {
return true
}
return false
}
}
extension IPv4Address {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let text = try container.decode(String.self)
try self.init(text)
/// Returns `true` if this is an IPv6 address.
@inlinable
public var isV6: Bool {
if case .v6 = self {
return true
}
return false
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.description)
/// Returns the underlying IPv4 address if this is an IPv4 address, otherwise `nil`.
@inlinable
public var ipv4: IPv4Address? {
if case .v4(let addr) = self {
return addr
}
return nil
}
/// Returns the underlying IPv6 address if this is an IPv6 address, otherwise `nil`.
@inlinable
public var ipv6: IPv6Address? {
if case .v6(let addr) = self {
return addr
}
return nil
}
/// Returns `true` if this is a loopback address (127.0.0.0/8 or ::1).
@inlinable
public var isLoopback: Bool {
switch self {
case .v4(let addr):
return addr.isLoopback
case .v6(let addr):
return addr.isLoopback
}
}
/// Returns `true` if this is a multicast address.
@inlinable
public var isMulticast: Bool {
switch self {
case .v4(let addr):
return addr.isMulticast
case .v6(let addr):
return addr.isMulticast
}
}
/// Returns `true` if this is an unspecified address (0.0.0.0 or ::).
@inlinable
public var isUnspecified: Bool {
switch self {
case .v4(let addr):
return addr.isUnspecified
case .v6(let addr):
return addr.isUnspecified
}
}
}
@@ -0,0 +1,66 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
public struct IPAddressError: Error, Equatable, Hashable, CustomStringConvertible {
public var description: String {
String(describing: self.base)
}
@usableFromInline
enum Base: Equatable, Hashable, Sendable {
case unableToParse
case invalidZoneIdentifier
case invalidIPv4Suffix
case multipleEllipsis
case invalidHexGroup
case malformedAddress
case incompleteAddress
}
@usableFromInline
let base: Base
@inlinable
init(_ base: Base) { self.base = base }
public static var unableToParse: Self {
Self(.unableToParse)
}
public static var invalidZoneIdentifier: Self {
Self(.invalidZoneIdentifier)
}
public static var invalidIPv4SuffixInIPv6Address: Self {
Self(.invalidIPv4Suffix)
}
public static var multipleEllipsis: Self {
Self(.multipleEllipsis)
}
public static var invalidHexGroup: Self {
Self(.invalidHexGroup)
}
public static var malformedAddress: Self {
Self(.malformedAddress)
}
public static var incompleteAddress: Self {
Self(.incompleteAddress)
}
}
@@ -0,0 +1,217 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
@frozen
public struct IPv4Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
public let value: UInt32
@inlinable
public init(_ value: UInt32) {
self.value = value
}
/// Creates an IPv4Address from a string representation.
///
/// - Parameter string: The IPv4 address string in dotted decimal notation (e.g., "192.168.1.1")
/// - Throws: `IPAddressError.unableToParse` if the string is not a valid IPv4 address
@inlinable
public init(_ string: String) throws {
self.value = try Self.parse(string)
}
@inlinable
public var bytes: [UInt8] {
Self.bytes(value)
}
@usableFromInline
static func bytes(_ value: UInt32) -> [UInt8] {
var result = [UInt8](repeating: 0, count: 4)
result[0] = UInt8((value >> 24) & 0xff)
result[1] = UInt8((value >> 16) & 0xff)
result[2] = UInt8((value >> 8) & 0xff)
result[3] = UInt8(value & 0xff)
return result
}
// TODO: spans?
@available(macOS 26.0, *)
@usableFromInline
static func bytes(_ value: UInt32) -> InlineArray<4, UInt8> {
let result: InlineArray<4, UInt8> = [
UInt8((value >> 24) & 0xff),
UInt8((value >> 16) & 0xff),
UInt8((value >> 8) & 0xff),
UInt8(value & 0xff),
]
return result
}
@inlinable
public var description: String {
"\(bytes[0]).\(bytes[1]).\(bytes[2]).\(bytes[3])"
}
/// Parses an IPv4 address string in dotted decimal notation into a UInt32 representation.
///
/// ## Validation Rules
/// - Exactly 4 octets separated by dots
/// - Each octet must be 0-255
/// - No leading zeros (except for "0" itself)
/// - No whitespace characters
/// - Only digits and dots allowed
///
/// ## Examples
/// ```swift
/// IPv4Address.parse("192.168.1.1") // Returns: 3232235777
/// IPv4Address.parse("127.0.0.1") // Returns: 2130706433
/// IPv4Address.parse("0.0.0.0") // Returns: 0
/// IPv4Address.parse("255.255.255.255") // Returns: 4294967295
///
/// // Invalid examples:
/// IPv4Address.parse("192.168.1") // Wrong number of octets
/// IPv4Address.parse("192.168.1.256") // Octet out of range
/// IPv4Address.parse("192.168.001.1") // Leading zeros
/// IPv4Address.parse(" 192.168.1.1 ") // Whitespace
/// ```
///
/// - Parameter s: The IPv4 address string to parse
/// - Returns: The 32-bit representation of the IP address, or `nil` if parsing fails
/// - Note: The returned value is in network byte order (big-endian)
@usableFromInline
internal static func parse(_ s: String) throws -> UInt32 {
guard !s.isEmpty, s.count >= 7, s.count <= 15 else {
throw IPAddressError.unableToParse
}
// IP addresses should only contain ASCII digits and dots
let utf8 = s.utf8
for byte in utf8 {
// ASCII whitespace: space(32), tab(9), newline(10), return(13)
if byte == 32 || byte == 9 || byte == 10 || byte == 13 {
throw IPAddressError.unableToParse
}
}
// accumulator for the 32bit representation of the IPv4 address
var result: UInt32 = 0
// tracking octet count, max 4 allowed
var octetCount = 0
var currentOctet = 0
// number of digits in the string representation of the octet, max 3
var digitCount = 0
for byte in utf8 {
if byte == 46 { // ASCII '.'
// Validate octet before processing
guard octetCount < 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
throw IPAddressError.unableToParse
}
// Shift result and add current octet
result = (result << 8) | UInt32(currentOctet)
// Reset for next octet
octetCount += 1
currentOctet = 0
digitCount = 0
} else if byte >= 48 && byte <= 57 { // ASCII '0'-'9'
let digit = Int(byte - 48)
digitCount += 1
// Check for invalid leading zeros: "01", "001", etc.
// Allow single "0" but reject multi-digit numbers starting with 0
if digitCount == 1 && digit == 0 {
// First digit is 0 - this is only valid if it's the only digit
currentOctet = 0
} else if digitCount > 1 && currentOctet == 0 {
// We had a leading zero and now have more digits - invalid
throw IPAddressError.unableToParse
} else {
// Normal case: build the octet value
currentOctet = currentOctet * 10 + digit
}
// Early termination if octet becomes too large
guard currentOctet <= 255, digitCount <= 3 else {
throw IPAddressError.unableToParse
}
} else {
throw IPAddressError.unableToParse
}
}
// Validate final octet
guard octetCount == 3, digitCount > 0, digitCount <= 3, currentOctet <= 255 else {
throw IPAddressError.unableToParse
}
return (result << 8) | UInt32(currentOctet)
}
// MARK: - Address Classification Methods
/// Returns `true` if this is the unspecified address (0.0.0.0).
///
/// Per RFC 791, 0.0.0.0 is the "this network" address.
@inlinable
public var isUnspecified: Bool {
value == 0
}
/// Returns `true` if this is a loopback address (127.0.0.0/8).
///
/// Per RFC 1122 Section 3.2.1.3, the entire 127.0.0.0/8 block is reserved for loopback.
@inlinable
public var isLoopback: Bool {
(value & 0xFF00_0000) == 0x7F00_0000
}
/// Returns `true` if this is a multicast address (224.0.0.0/4).
///
/// Per RFC 1112, addresses in the range 224.0.0.0 to 239.255.255.255 are multicast addresses.
@inlinable
public var isMulticast: Bool {
(value & 0xF000_0000) == 0xE000_0000
}
/// Returns `true` if this is a link-local address (169.254.0.0/16).
///
/// Per RFC 3927, 169.254.0.0/16 is reserved for link-local addresses (APIPA/Auto-IP).
@inlinable
public var isLinkLocal: Bool {
(value & 0xFFFF_0000) == 0xA9FE_0000
}
/// Returns `true` if this is the limited broadcast address (255.255.255.255).
///
/// Per RFC 919/922, 255.255.255.255 is the limited broadcast address.
@inlinable
public var isBroadcast: Bool {
value == 0xFFFF_FFFF
}
/// Compares two IPv4 addresses numerically.
@inlinable
public static func < (lhs: IPv4Address, rhs: IPv4Address) -> Bool {
lhs.value < rhs.value
}
}
@@ -0,0 +1,333 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
extension IPv6Address {
/// Parses an IPv6 address string into an IPv6Address instance.
///
/// Follows RFC 4291 and RFC 5952.
///
/// This function supports standard IPv6 notation including:
/// - Full addresses: `2001:0db8:0000:0042:0000:8a2e:0370:7334`
/// - Zero compression: `2001:db8::8a2e:370:7334`
/// - Leading zero omission: `2001:db8:0:42:0:8a2e:370:7334`
/// - Unspecified address: `::`
/// - Zone identifiers: `fe80::1%eth0`
///
/// - Parameter input: IPv6 address string (with optional zone identifier after %)
/// - Returns: An `IPv6Address` instance representing the parsed address
///
/// ## Example Usage
/// ```swift
/// let addr1 = try IPv6Address.parse("2001:db8::1")
/// let addr2 = try IPv6Address.parse("::") // Unspecified address
/// let addr3 = try IPv6Address.parse("fe80::1%eth0") // With zone identifier
/// ```
static func parse(_ input: String) throws -> IPv6Address {
var ipBytes = [UInt8](repeating: 0, count: 16)
var ellipsisPosition: Int?
// Extract zone identifier
let (address, zone) = try extractZoneIdentifier(from: input)
// RFC 4291 Section 2.2.3: IPv4 suffix must be at the end (last 32 bits)
var remainingAddress = address
var ipv6ByteLimit = 16 // Maximum bytes available for IPv6 hex groups
var hasIPv4Suffix = false
// check if the IPv6 address has IPv4 address in it.
if let (ipv6Part, ipv4Bytes) = try extractIPv4Suffix(from: address) {
// If IPv4 present, save directly to last 4 bytes.
ipBytes[12] = ipv4Bytes[0]
ipBytes[13] = ipv4Bytes[1]
ipBytes[14] = ipv4Bytes[2]
ipBytes[15] = ipv4Bytes[3]
// Update address and limit IPv6 parsing to first 12 bytes (6 groups max)
remainingAddress = ipv6Part
ipv6ByteLimit = 12
hasIPv4Suffix = true
}
// Handle leading ellipsis in the IPv6 part
if remainingAddress.utf8.starts(with: [58, 58]) { // "::"
ellipsisPosition = 0
remainingAddress = String(remainingAddress.dropFirst(2))
// Special case: "::" represents the unspecified address (all zeros)
// But if we have IPv4 suffix, the IPv4 bytes are already set correctly
if remainingAddress.isEmpty {
// If we have IPv4 suffix, ipBytes already has the IPv4 data, just return
if hasIPv4Suffix {
return Self(ipBytes, zone: zone)
}
// Pure "::" - Return the unspecified address, handling zone identifiers
if let zone = zone, !zone.isEmpty {
return Self(ipBytes, zone: zone)
}
return .unspecified
}
}
// Parse IPv6 hex groups up to the byte limit
var byteIndex = 0
let utf8 = remainingAddress.utf8
var currentPosition = utf8.startIndex
while byteIndex < ipv6ByteLimit && currentPosition < utf8.endIndex {
let (hexValue, nextPosition) = try parseHexadecimal(
from: utf8,
startingAt: currentPosition
)
// Store the UInt16 in network-byte order
ipBytes[byteIndex] = UInt8(hexValue >> 8)
ipBytes[byteIndex + 1] = UInt8(hexValue & 0xFF)
byteIndex += 2
currentPosition = nextPosition
// Terminate early if we have consumed the whole string
if currentPosition == utf8.endIndex {
break
}
// Parse separator and handle ellipsis detection
currentPosition = try skipColonSeparator(
from: utf8,
at: currentPosition,
currentByteIndex: byteIndex,
ellipsisPosition: &ellipsisPosition
)
}
// Validate complete consumption of input
guard currentPosition >= utf8.endIndex else {
throw IPAddressError.malformedAddress
}
// Apply ellipsis expansion for the IPv6 portion
try expandEllipsis(
in: &ipBytes,
parsedBytes: byteIndex,
ellipsisPosition: ellipsisPosition,
byteLimit: ipv6ByteLimit
)
let value = ipBytes.reduce(UInt128(0)) { ($0 << 8) | UInt128($1) }
return Self(value, zone: zone)
}
// MARK: - Helper Functions
/// Extracts IPv4 suffix if present at the end of the address
///
/// Follows: RFC 4291 Section 2.2.3: Alternative form x:x:x:x:x:x:d.d.d.d
/// IPv4 must be the last 32 bits and preceded by a colon
///
/// - Parameter input: The IPv6 address string to check
/// - Returns: Optional tuple of (IPv6 part without IPv4, IPv4 bytes array) if IPv4 found, nil otherwise
/// - Throws: `IPAddressError.invalidIPv4Suffix` for invalid IPv4 addresses
internal static func extractIPv4Suffix(from input: String) throws -> (String, [UInt8])? {
// must contain a dot to be IPv4
guard input.utf8.contains(46) else { // ASCII '.'
return nil
}
// IPv4 address must be present after last colon
guard let lastColonIndex = input.lastIndex(of: ":") else {
return nil
}
// TODO: maybe refactor for performance
let afterColon = input.index(after: lastColonIndex)
guard afterColon < input.endIndex else {
return nil
}
let possibleIPv4 = String(input[afterColon...])
guard let ipv4Value = try? IPv4Address.parse(possibleIPv4) else {
throw IPAddressError.invalidIPv4SuffixInIPv6Address
}
// Check if lastColonIndex is the second ':' of '::'. If so, ensure to include it.
let isDoubleColon = lastColonIndex > input.startIndex && input[input.index(before: lastColonIndex)] == ":"
let ipv6Part = isDoubleColon ? String(input[...lastColonIndex]) : String(input[..<lastColonIndex])
return (ipv6Part, IPv4Address.bytes(ipv4Value))
}
/// Extracts zone identifier
///
/// - Parameter input: The full IPv6 address string with potential zone identifier
/// - Returns: Tuple of (address part, optional zone identifier)
/// - Throws: `IPAddressError.invalidZoneIdentifier` for malformed zone identifiers
private static func extractZoneIdentifier(from input: String) throws -> (String, String?) {
guard let percentIndex = input.lastIndex(of: "%") else {
return (input, nil)
}
let zoneStartIndex = input.index(after: percentIndex)
guard zoneStartIndex < input.endIndex else {
throw IPAddressError.invalidZoneIdentifier
}
let addressPart = String(input[..<percentIndex])
let zoneIdentifier = String(input[zoneStartIndex...])
return (addressPart, zoneIdentifier)
}
/// Parses a hexadecimal group from an IPv6 address component.
///
/// - Parameters:
/// - utf8: The UTF-8 view to parse from
/// - startIndex: Starting position in the UTF-8 view
/// - Returns: Tuple of (parsed hex value as UInt16, next position after parsed digits)
/// - Throws: `IPAddressError.invalidHexGroup` if no valid hex digits are found
///
/// ## Example
/// ```swift
/// let utf8 = "2001:db8::1".utf8
/// let (value, nextPos) = try parseHexadecimal(from: utf8, startingAt: utf8.startIndex)
/// // value = 0x2001, nextPos points to ':'
/// ```
@inlinable
internal static func parseHexadecimal(
from group: String.UTF8View,
startingAt startIndex: String.UTF8View.Index
) throws -> (UInt16, String.UTF8View.Index) {
var accumulator: UInt16 = 0
var digitCount = 0
var currentIndex = startIndex
while currentIndex < group.endIndex && digitCount < 4 {
let byte = group[currentIndex]
// Fast hex digit parsing using ASCII values
let hexValue: UInt16
if byte >= 48 && byte <= 57 { // '0'-'9'
hexValue = UInt16(byte - 48)
} else if byte >= 65 && byte <= 70 { // 'A'-'F'
hexValue = UInt16(byte - 65 + 10)
} else if byte >= 97 && byte <= 102 { // 'a'-'f'
hexValue = UInt16(byte - 97 + 10)
} else {
break // Not a hex digit
}
accumulator = (accumulator << 4) + hexValue
digitCount += 1
currentIndex = group.index(after: currentIndex)
}
guard digitCount > 0 else {
// No hex digits found
throw IPAddressError.invalidHexGroup
}
return (accumulator, currentIndex)
}
/// Parses a colon separator between IPv6 groups and detects ellipsis notation (::).
///
/// - Parameters:
/// - utf8: The UTF-8 view being parsed
/// - position: Current position in the UTF-8 view (must point to a colon)
/// - currentByteIndex: Current byte index in the IP array
/// - ellipsisPosition: Inout parameter tracking ellipsis position
/// - Returns: Next position in the UTF-8 view after parsing separator
///
/// ## Example
/// ```swift
/// let utf8 = "2001:db8::1".utf8
/// var ellipsisPos: Int? = nil
/// // After parsing "2001", position points to first ':'
/// let nextPos = try skipColonSeparator(from: utf8, at: position,
/// currentByteIndex: 2,
/// ellipsisPosition: &ellipsisPos)
/// // For single colon: nextPos points to 'd' in 'db8'
/// // For double colon (::): ellipsisPos = 2, nextPos points to '1'
/// ```
private static func skipColonSeparator(
from group: String.UTF8View,
at position: String.UTF8View.Index,
currentByteIndex: Int,
ellipsisPosition: inout Int?
) throws -> String.UTF8View.Index {
// Expect colon separator
guard group[position] == 58 else { // ASCII ':'
throw IPAddressError.malformedAddress
}
let afterFirstColon = group.index(after: position)
guard afterFirstColon < group.endIndex else {
// Trailing colon not allowed
throw IPAddressError.malformedAddress
}
// Check for double colon, return position after that
if group[afterFirstColon] == 58 { // ASCII ':'
guard ellipsisPosition == nil else {
// Multiple :: not allowed
throw IPAddressError.multipleEllipsis
}
ellipsisPosition = currentByteIndex
let afterSecondColon = group.index(after: afterFirstColon)
return afterSecondColon
}
return afterFirstColon
}
/// Expands ellipsis for IPv6 addresses
///
/// - Parameters:
/// - ipBytes: Inout array of IP bytes to modify
/// - parsedBytes: Number of bytes already parsed for IPv6 groups
/// - ellipsisPosition: Optional position where ellipsis was found
/// - byteLimit: Maximum bytes available for IPv6 (16 for pure IPv6, 12 if IPv4 suffix present)
/// - Throws: `IPAddressError.incompleteAddress` for invalid address lengths
private static func expandEllipsis(
in ipBytes: inout [UInt8],
parsedBytes: Int,
ellipsisPosition: Int?,
byteLimit: Int = 16
) throws {
guard let ellipsisPosition = ellipsisPosition else {
// No ellipsis - validate we have exactly filled the available bytes
guard parsedBytes == byteLimit else {
throw IPAddressError.incompleteAddress // Incomplete address without ellipsis
}
return
}
// Calculate expansion within the byte limit
let bytesToExpand = byteLimit - parsedBytes
guard bytesToExpand > 0 else {
throw IPAddressError.malformedAddress // No room for ellipsis expansion
}
let suffixBytes = Array(ipBytes[ellipsisPosition..<parsedBytes])
let targetStartIndex = byteLimit - suffixBytes.count
// Clear the expansion area using Swift's range-based assignment
for i in ellipsisPosition..<targetStartIndex {
ipBytes[i] = 0
}
// Place suffix at the end of the IPv6 section using Swift's collection assignment
for (offset, byte) in suffixBytes.enumerated() {
ipBytes[targetStartIndex + offset] = byte
}
}
}
@@ -0,0 +1,256 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// Represents an IPv6 network address conforming to RFC 5952 and RFC 4291.
public struct IPv6Address: Sendable, Hashable, CustomStringConvertible, Equatable, Comparable {
@usableFromInline
internal let value: UInt128
@usableFromInline
internal let zone: String?
/// Creates an IPv6Address by parsing a string representation.
///
/// Supports standard IPv6 formats including compressed notation (::), mixed IPv4 notation, and zone identifiers.
///
/// - Parameter address: String representation of an IPv6 address
/// - Throws: `IPAddressError` if the string is not a valid IPv6 address
public init(_ address: String) throws {
self = try Self.parse(address)
}
/// Creates an IPv6Address from 16 bytes.
///
/// - Parameters:
/// - bytes: 16-byte array representing the IPv6 address
/// - zone: Optional zone identifier (e.g., "eth0")
@inlinable
public init(_ bytes: [UInt8], zone: String? = nil) {
precondition(bytes.count == 16, "IPv6 address must be exactly 16 bytes")
// Build UInt128 value in chunks to avoid compiler complexity
let hh =
(UInt128(bytes[0]) << 120) | (UInt128(bytes[1]) << 112) | (UInt128(bytes[2]) << 104)
| (UInt128(bytes[3]) << 96)
let hl =
(UInt128(bytes[4]) << 88) | (UInt128(bytes[5]) << 80) | (UInt128(bytes[6]) << 72)
| (UInt128(bytes[7]) << 64)
let lh =
(UInt128(bytes[8]) << 56) | (UInt128(bytes[9]) << 48) | (UInt128(bytes[10]) << 40)
| (UInt128(bytes[11]) << 32)
let ll =
(UInt128(bytes[12]) << 24) | (UInt128(bytes[13]) << 16) | (UInt128(bytes[14]) << 8) | UInt128(bytes[15])
self.value = hh | hl | lh | ll
self.zone = zone
}
@inlinable
public init(_ value: UInt128, zone: String? = nil) {
self.value = value
self.zone = zone
}
/// Canonical string representation following RFC 5952.
public var description: String {
// Convert UInt128 value to 16-bit groups
let groups: [UInt16] = [
UInt16((value >> 112) & 0xFFFF),
UInt16((value >> 96) & 0xFFFF),
UInt16((value >> 80) & 0xFFFF),
UInt16((value >> 64) & 0xFFFF),
UInt16((value >> 48) & 0xFFFF),
UInt16((value >> 32) & 0xFFFF),
UInt16((value >> 16) & 0xFFFF),
UInt16(value & 0xFFFF),
]
// Find the longest run of consecutive zeros for :: compression
var longestZeroStart = -1
var longestZeroLength = 0
var currentZeroStart = -1
var currentZeroLength = 0
for (index, group) in groups.enumerated() {
if group == 0 {
if currentZeroStart == -1 {
currentZeroStart = index
currentZeroLength = 1
} else {
currentZeroLength += 1
}
} else {
if currentZeroLength > longestZeroLength {
longestZeroStart = currentZeroStart
longestZeroLength = currentZeroLength
}
currentZeroStart = -1
currentZeroLength = 0
}
}
if currentZeroLength > longestZeroLength {
longestZeroStart = currentZeroStart
longestZeroLength = currentZeroLength
}
let useCompression = longestZeroLength >= 2
var result = ""
var index = 0
while index < 8 {
if useCompression && index == longestZeroStart {
if index == 0 {
result += "::"
} else {
result += ":"
}
// Skip the compressed zeros
index += longestZeroLength
// If we compressed to the end, we're done
if index >= 8 {
break
}
} else {
// Add the group in lowercase hex without leading zeros
result += String(groups[index], radix: 16, uppercase: false)
index += 1
// Add colon if not at the end
if index < 8 {
result += ":"
}
}
}
if let zone = zone {
result += "%" + zone
}
return result
}
@inlinable
public var bytes: [UInt8] {
Self.bytes(self.value)
}
@usableFromInline
internal static func bytes(_ value: UInt128) -> [UInt8] {
var result = [UInt8](repeating: 0, count: 16)
result[0] = UInt8((value >> 120) & 0xff)
result[1] = UInt8((value >> 112) & 0xff)
result[2] = UInt8((value >> 104) & 0xff)
result[3] = UInt8((value >> 96) & 0xff)
result[4] = UInt8((value >> 88) & 0xff)
result[5] = UInt8((value >> 80) & 0xff)
result[6] = UInt8((value >> 72) & 0xff)
result[7] = UInt8((value >> 64) & 0xff)
result[8] = UInt8((value >> 56) & 0xff)
result[9] = UInt8((value >> 48) & 0xff)
result[10] = UInt8((value >> 40) & 0xff)
result[11] = UInt8((value >> 32) & 0xff)
result[12] = UInt8((value >> 24) & 0xff)
result[13] = UInt8((value >> 16) & 0xff)
result[14] = UInt8((value >> 8) & 0xff)
result[15] = UInt8(value & 0xff)
return result
}
@available(macOS 26.0, *)
@usableFromInline
internal static func bytes(_ value: UInt128) -> InlineArray<16, UInt8> {
let result: InlineArray<16, UInt8> = [
UInt8((value >> 120) & 0xff),
UInt8((value >> 112) & 0xff),
UInt8((value >> 104) & 0xff),
UInt8((value >> 96) & 0xff),
UInt8((value >> 88) & 0xff),
UInt8((value >> 80) & 0xff),
UInt8((value >> 72) & 0xff),
UInt8((value >> 64) & 0xff),
UInt8((value >> 56) & 0xff),
UInt8((value >> 48) & 0xff),
UInt8((value >> 40) & 0xff),
UInt8((value >> 32) & 0xff),
UInt8((value >> 24) & 0xff),
UInt8((value >> 16) & 0xff),
UInt8((value >> 8) & 0xff),
UInt8(value & 0xff),
]
return result
}
/// The unspecified IPv6 address (::)
public static let unspecified = IPv6Address(0)
/// The loopback IPv6 address (::1)
public static let loopback = IPv6Address(1)
// MARK: - Address Classification Methods
/// Returns `true` if this is the unspecified address (::).
@inlinable
public var isUnspecified: Bool {
value == 0
}
/// Returns `true` if this is the loopback address (::1).
@inlinable
public var isLoopback: Bool {
value == 1
}
/// Returns `true` if this is a multicast address (ff00::/8).
@inlinable
public var isMulticast: Bool {
(value >> 120) == 0xFF
}
/// Returns `true` if this is a link-local unicast address (fe80::/10).
@inlinable
public var isLinkLocal: Bool {
(value >> 118) == 0x3FA // fe80::/10 = top 10 bits are 1111111010
}
/// Returns `true` if this is a unique local address (fc00::/7).
@inlinable
public var isUniqueLocal: Bool {
(value >> 121) == 0x7E // fc00::/7 = top 7 bits are 1111110
}
/// Returns `true` if this is a global unicast address.
@inlinable
public var isGlobalUnicast: Bool {
!isUnspecified && !isLoopback && !isMulticast && !isLinkLocal && !isUniqueLocal
}
/// Returns `true` if this is a documentation address (2001:db8::/32).
@inlinable
public var isDocumentation: Bool {
(value >> 96) == 0x2001_0DB8 // 2001:db8::/32
}
/// Compares two IPv6 addresses numerically, then by zone if values are equal.
@inlinable
public static func < (lhs: IPv6Address, rhs: IPv6Address) -> Bool {
if lhs.value != rhs.value {
return lhs.value < rhs.value
}
// Same value, compare zones lexicographically
return (lhs.zone ?? "") < (rhs.zone ?? "")
}
}
@@ -29,7 +29,7 @@ extension IPv4Address {
}
return Int(address.value - lower)
},
indexToAddress: { IPv4Address(fromValue: lower + UInt32($0)) }
indexToAddress: { IPv4Address(lower + UInt32($0)) }
)
}
}
@@ -1,69 +0,0 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// Errors related to IP and CIDR addresses.
public enum NetworkAddressError: Swift.Error, Equatable, CustomStringConvertible {
case invalidStringAddress(address: String)
case invalidNetworkByteAddress(address: [UInt8])
case invalidCIDR(cidr: String)
case invalidAddressForSubnet(address: String, cidr: String)
case invalidAddressRange(lower: String, upper: String)
public var description: String {
switch self {
case .invalidStringAddress(let address):
return "invalid IP address string \(address)"
case .invalidNetworkByteAddress(let address):
return "invalid IP address bytes \(address)"
case .invalidCIDR(let cidr):
return "invalid CIDR block: \(cidr)"
case .invalidAddressForSubnet(let address, let cidr):
return "invalid address \(address) for subnet \(cidr)"
case .invalidAddressRange(let lower, let upper):
return "invalid range for addresses \(lower) and \(upper)"
}
}
}
public typealias PrefixLength = UInt8
extension PrefixLength {
/// Compute a bit mask that passes the suffix bits, given the network prefix mask length.
public var suffixMask32: UInt32 {
if self <= 0 {
return 0xffff_ffff
}
return self >= 32 ? 0x0000_0000 : (1 << (32 - self)) - 1
}
/// Compute a bit mask that passes the prefix bits, given the network prefix mask length.
public var prefixMask32: UInt32 {
~self.suffixMask32
}
/// Compute a bit mask that passes the suffix bits, given the network prefix mask length.
public var suffixMask48: UInt64 {
if self <= 0 {
return 0x0000_ffff_ffff_ffff
}
return self >= 48 ? 0x0000_0000_0000_0000 : (1 << (48 - self)) - 1
}
/// Compute a bit mask that passes the prefix bits, given the network prefix mask length.
public var prefixMask48: UInt64 {
~self.suffixMask48 & 0x0000_ffff_ffff_ffff
}
}
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
/// CIDR prefix length (e.g., `/24` for a 24-bit network mask).
@frozen
public struct Prefix: Sendable, CustomStringConvertible, Hashable {
public let length: UInt8
/// Create a prefix (0-128). Use `ipv4(_:)` or `ipv6(_:)` for version-specific validation.
public init?(length: UInt8) {
guard length <= 128 else { return nil }
self.length = length
}
/// Create an IPv4 prefix (0-32). Returns `nil` if length > 32.
public static func ipv4(_ length: UInt8) -> Prefix? {
guard length <= 32 else { return nil }
return Prefix(unchecked: length)
}
/// Create an IPv6 prefix (0-128). Returns `nil` if length > 128.
public static func ipv6(_ length: UInt8) -> Prefix? {
guard length <= 128 else { return nil }
return Prefix(unchecked: length)
}
/// Internal unchecked initializer for known-valid values.
internal init(unchecked length: UInt8) {
self.length = length
}
public var description: String {
"\(length)"
}
}
extension Prefix {
/// Computes a 32-bit mask for the suffix (host) portion of an IPv4 address.
///
/// Example: Prefix `/24` → `0x0000_00FF` (255 host addresses)
@inlinable
public var suffixMask32: UInt32 {
if self.length <= 0 {
return 0xffff_ffff
}
return self.length >= 32 ? 0x0000_0000 : (1 << (32 - self.length)) - 1
}
/// Network portion mask (high-order bits) for IPv4.
///
/// Example: Prefix `/24` → `0xFFFF_FF00` (255.255.255.0)
@inlinable
public var prefixMask32: UInt32 {
~self.suffixMask32
}
/// Computes a 128-bit mask for the suffix (host) portion of an IPv6 address.
///
/// Example: Prefix `/64` → `0x0000_0000_0000_0000_FFFF_FFFF_FFFF_FFFF`
@inlinable
public var suffixMask128: UInt128 {
if self.length <= 0 {
return UInt128.max
}
return self.length >= 128 ? 0 : (1 << (128 - self.length)) - 1
}
/// Network portion mask (high-order bits) for IPv6.
///
/// Example: Prefix `/64` → `0xFFFF_FFFF_FFFF_FFFF_0000_0000_0000_0000`
@inlinable
public var prefixMask128: UInt128 {
~self.suffixMask128
}
}
@@ -193,8 +193,8 @@ public struct NetlinkSession {
/// Adds an IPv4 address to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - address: The IPv4 address to add.
public func addressAdd(interface: String, address: String) throws {
/// - ipv4Address: The CIDRv4 address describing the interface IP and subnet prefix length.
public func addressAdd(interface: String, ipv4Address: CIDRv4) throws {
// ip addr add [addr] dev [interface]
// ip address {add|change|replace} IFADDR dev IFNAME [ LIFETIME ] [ CONFFLAG-LIST ]
// IFADDR := PREFIX | ADDR peer PREFIX
@@ -205,9 +205,9 @@ public struct NetlinkSession {
// CONFFLAG := [ home | nodad | mngtmpaddr | noprefixroute | autojoin ]
// LIFETIME := [ valid_lft LFT ] [ preferred_lft LFT ]
// LFT := forever | SECONDS
let parsed = try parseCIDR(cidr: address)
let interfaceIndex = try getInterfaceIndex(interface)
let ipAddressBytes = try IPv4Address(parsed.address).networkBytes
let ipAddressBytes = ipv4Address.address.bytes
let addressAttrSize = RTAttribute.size + MemoryLayout<UInt8>.size * ipAddressBytes.count
let requestSize = NetlinkMessageHeader.size + AddressInfo.size + 2 * addressAttrSize
var requestBuffer = [UInt8](repeating: 0, count: requestSize)
@@ -224,7 +224,7 @@ public struct NetlinkSession {
let requestInfo = AddressInfo(
family: UInt8(AddressFamily.AF_INET),
prefixLength: parsed.prefix,
prefixLength: ipv4Address.prefix.length,
flags: 0,
scope: NetlinkScope.RT_SCOPE_UNIVERSE,
index: UInt32(interfaceIndex))
@@ -253,37 +253,22 @@ public struct NetlinkSession {
}
}
private func parseCIDR(cidr: String) throws -> (address: String, prefix: UInt8) {
let split = cidr.components(separatedBy: "/")
guard split.count == 2 else {
throw NetworkAddressError.invalidCIDR(cidr: cidr)
}
let address = split[0]
guard let prefixLength = PrefixLength(split[1]) else {
throw NetworkAddressError.invalidCIDR(cidr: cidr)
}
guard prefixLength >= 0 && prefixLength <= 32 else {
throw NetworkAddressError.invalidCIDR(cidr: cidr)
}
return (address, prefixLength)
}
/// Adds a route to an interface.
/// Adds an IPv4 route to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - destinationAddress: The destination address to route to.
/// - srcAddr: The source address to route from.
/// - dstIpv4Addr: The CIDRv4 address describing the gateway IP and subnet prefix length.
/// - srcIpv4Addr: The source IPv4 address to route from.
public func routeAdd(
interface: String,
destinationAddress: String,
srcAddr: String
dstIpv4Addr: CIDRv4,
srcIpv4Addr: IPv4Address
) throws {
// ip route add [dest-cidr] dev [interface] src [src-addr] proto kernel
let parsed = try parseCIDR(cidr: destinationAddress)
let interfaceIndex = try getInterfaceIndex(interface)
let dstAddrBytes = try IPv4Address(parsed.address).networkBytes
let dstAddrBytes = dstIpv4Addr.address.bytes
let dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count
let srcAddrBytes = try IPv4Address(srcAddr).networkBytes
let srcAddrBytes = srcIpv4Addr.bytes
let srcAddrAttrSize = RTAttribute.size + srcAddrBytes.count
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
let requestSize =
@@ -301,7 +286,7 @@ public struct NetlinkSession {
let requestInfo = RouteInfo(
family: UInt8(AddressFamily.AF_INET),
dstLen: parsed.prefix,
dstLen: dstIpv4Addr.prefix.length,
srcLen: 0,
tos: 0,
table: RouteTable.MAIN,
@@ -345,16 +330,16 @@ public struct NetlinkSession {
}
}
/// Adds a default route to an interface.
/// Adds a default IPv4 route to an interface.
/// - Parameters:
/// - interface: The name of the interface.
/// - gateway: The gateway address.
/// - ipv4Gateway: The gateway address.
public func routeAddDefault(
interface: String,
gateway: String
ipv4Gateway: IPv4Address
) throws {
// ip route add default via [dst-address] src [src-address]
let dstAddrBytes = try IPv4Address(gateway).networkBytes
let dstAddrBytes = ipv4Gateway.bytes
let dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count
let interfaceAttrSize = RTAttribute.size + MemoryLayout<UInt32>.size
+4 -1
View File
@@ -17,6 +17,7 @@
import ArgumentParser
import Containerization
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import ContainerizationOS
import Foundation
@@ -124,7 +125,9 @@ extension Application {
guard let gateway else {
throw ContainerizationError(.invalidArgument, message: "gateway must be specified")
}
config.interfaces.append(NATInterface(address: ip, gateway: gateway))
let ipv4Address = try CIDRv4(ip)
let ipv4Gateway = try IPv4Address(gateway)
config.interfaces.append(NATInterface(ipv4Address: ipv4Address, ipv4Gateway: ipv4Gateway))
config.dns = .init(nameservers: [gateway])
if nameservers.count > 0 {
config.dns = .init(nameservers: nameservers)
@@ -0,0 +1,311 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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 Testing
@testable import ContainerizationExtras
struct TestCIDR {
// MARK: - Normalization Tests
struct Normalization {
let input: String
let expectedNetwork: String
let expectedLength: UInt8
}
@Test(arguments: [
Normalization(
input: "192.168.1.100/24",
expectedNetwork: "192.168.1.0",
expectedLength: 24
),
Normalization(
input: "10.1.2.3/16",
expectedNetwork: "10.1.0.0",
expectedLength: 16
),
Normalization(
input: "2001:db8::1234/64",
expectedNetwork: "2001:db8::",
expectedLength: 64
),
Normalization(
input: "172.16.0.1/12",
expectedNetwork: "172.16.0.0",
expectedLength: 12
),
])
func testNormalization(testCase: Normalization) throws {
let cidr = try CIDR(testCase.input)
#expect(cidr.lower.description == testCase.expectedNetwork)
#expect(cidr.prefix.length == testCase.expectedLength)
}
struct ParsePreservation {
let input: String
let expectedIP: String
let expectedLength: UInt8
}
// MARK: - Bounds Tests
struct Bounds {
let cidr: String
let lower: String
let upper: String
}
@Test(arguments: [
Bounds(
cidr: "192.168.1.0/24",
lower: "192.168.1.0",
upper: "192.168.1.255"
),
Bounds(
cidr: "10.0.0.0/8",
lower: "10.0.0.0",
upper: "10.255.255.255"
),
Bounds(
cidr: "2001:db8::/64",
lower: "2001:db8::",
upper: "2001:db8::ffff:ffff:ffff:ffff"
),
Bounds(
cidr: "192.168.1.0/32",
lower: "192.168.1.0",
upper: "192.168.1.0"
),
])
func testBounds(testCase: Bounds) throws {
let block = try CIDR(testCase.cidr)
#expect(block.lower.description == testCase.lower)
#expect(block.upper.description == testCase.upper)
}
// MARK: - Containment Tests
struct IPContainment {
let cidr: String
let ip: String
let shouldContain: Bool
}
@Test(arguments: [
IPContainment(
cidr: "192.168.1.0/24",
ip: "192.168.1.100",
shouldContain: true
),
IPContainment(
cidr: "192.168.1.0/24",
ip: "192.168.2.1",
shouldContain: false
),
IPContainment(
cidr: "10.0.0.0/8",
ip: "10.255.255.255",
shouldContain: true
),
IPContainment(
cidr: "10.0.0.0/8",
ip: "11.0.0.1",
shouldContain: false
),
IPContainment(
cidr: "2001:db8::/32",
ip: "2001:db8::1",
shouldContain: true
),
])
func testContainsIP(testCase: IPContainment) throws {
let block = try CIDR(testCase.cidr)
let address = try IPAddress(testCase.ip)
#expect(block.contains(address) == testCase.shouldContain)
}
@Test func testDoesNotContainDifferentIPv6Zone() throws {
let cidr = try CIDR("fe80::1/64")
let ip = try IPv6Address("fe80::2%eth1")
#expect(!cidr.contains(.v6(ip)))
}
// MARK: - Range Constructor
@Test func testRangeConstructorFindsSmallestBlock() throws {
let lower = try IPAddress("192.168.1.0")
let upper = try IPAddress("192.168.1.255")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 24)
#expect(cidr.address.description == "192.168.1.0")
}
@Test func testRangeConstructorSingleIPv4() throws {
let ip = try IPAddress("192.168.1.100")
let cidr = try CIDR(lower: ip, upper: ip)
#expect(cidr.prefix.length == 32)
#expect(cidr.address.description == "192.168.1.100")
}
@Test func testRangeConstructorIPv6() throws {
let lower = try IPAddress("2001:db8::")
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 64)
#expect(cidr.address.description == "2001:db8::")
}
@Test func testRangeConstructorSingleIPv6() throws {
let ip = try IPAddress("2001:db8::1")
let cidr = try CIDR(lower: ip, upper: ip)
#expect(cidr.prefix.length == 128)
#expect(cidr.address.description == "2001:db8::1")
}
@Test func testRangeConstructorRejectsMixedVersions() throws {
#expect(throws: CIDR.Error.self) {
let v4 = try IPAddress("192.168.1.0")
let v6 = try IPAddress("2001:db8::1")
_ = try CIDR(lower: v4, upper: v6)
}
}
@Test func testRangeConstructorRejectsDifferentZones() throws {
#expect(throws: CIDR.Error.self) {
let lower = IPAddress.v6(try IPv6Address("fe80::1%eth0"))
let upper = IPAddress.v6(try IPv6Address("fe80::2%eth1"))
_ = try CIDR(lower: lower, upper: upper)
}
}
// MARK: - Validation Tests
struct InvalidInput {
let input: String
}
@Test(arguments: [
InvalidInput(input: "192.168.1.0/33"), // IPv4 prefix too large
InvalidInput(input: "2001:db8::/129"), // IPv6 prefix too large
InvalidInput(input: "192.168.1.0"), // Missing prefix
InvalidInput(input: "192.168.1.0/"), // Empty prefix
InvalidInput(input: "192.168.1.0/abc"), // Invalid prefix
])
func testRejectsInvalidInput(testCase: InvalidInput) throws {
#expect(throws: CIDR.Error.self) {
try CIDR(testCase.input)
}
}
@Test func testRejectsInvalidRangeOrder() throws {
#expect(throws: CIDR.Error.self) {
let lower = try IPAddress("192.168.1.255")
let upper = try IPAddress("192.168.1.0")
_ = try CIDR(lower: lower, upper: upper)
}
}
// MARK: - Range Constructor Validation Tests
@Test func testRangeConstructorValidatesContainment() throws {
// This should work: 192.168.1.64 to 192.168.1.127 -> /26
let lower1 = try IPAddress("192.168.1.64")
let upper1 = try IPAddress("192.168.1.127")
let cidr1 = try CIDR(lower: lower1, upper: upper1)
#expect(cidr1.prefix.length == 26)
#expect(cidr1.contains(lower1))
#expect(cidr1.contains(upper1))
}
@Test func testRangeConstructorValidatesIPv6Containment() throws {
// Test IPv6 range containment validation
let lower = try IPAddress("2001:db8::1000")
let upper = try IPAddress("2001:db8::1fff")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.contains(lower))
#expect(cidr.contains(upper))
}
// MARK: - Version-Specific Range Constructors
@Test func testV4RangeConstructor() throws {
let lower = try IPAddress("192.168.1.0")
let upper = try IPAddress("192.168.1.255")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 24)
#expect(cidr.address.description == "192.168.1.0")
}
@Test func testV4RangeSingleAddress() throws {
let addr = try IPAddress("192.168.1.100")
let cidr = try CIDR(lower: addr, upper: addr)
#expect(cidr.prefix.length == 32)
#expect(cidr.address.description == "192.168.1.100")
}
@Test func testV6RangeConstructor() throws {
let lower = try IPAddress("2001:db8::")
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
let cidr = try CIDR(lower: lower, upper: upper)
#expect(cidr.prefix.length == 64)
#expect(cidr.address.description == "2001:db8::")
}
@Test func testV6RangeSingleAddress() throws {
let addr = try IPAddress("2001:db8::1")
let cidr = try CIDR(lower: addr, upper: addr)
#expect(cidr.prefix.length == 128)
#expect(cidr.address.description == "2001:db8::1")
}
@Test func testV4RangeRejectsInvalidOrder() throws {
let lower = try IPAddress("192.168.1.255")
let upper = try IPAddress("192.168.1.0")
#expect(throws: CIDR.Error.self) {
_ = try CIDR(lower: lower, upper: upper)
}
}
@Test func testV6RangeRejectsInvalidOrder() throws {
let lower = try IPAddress("2001:db8::ffff")
let upper = try IPAddress("2001:db8::1")
#expect(throws: CIDR.Error.self) {
_ = try CIDR(lower: lower, upper: upper)
}
}
@Test func testV6RangeRejectsDifferentZones() throws {
let lower = try IPAddress("fe80::1%eth0")
let upper = try IPAddress("fe80::2%eth1")
#expect(throws: CIDR.Error.self) {
_ = try CIDR(lower: lower, upper: upper)
}
}
// MARK: - Description Tests
@Test func testDescriptionFormat() throws {
let cidr = try CIDR("10.0.0.0/8")
#expect(cidr.description == "10.0.0.0/8")
}
@Test func testPreservesAddress() throws {
let cidr = try CIDR("192.168.1.100/24")
#expect(cidr.description == "192.168.1.100/24")
}
}
@@ -1,211 +0,0 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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.
//===----------------------------------------------------------------------===//
//
// swiftlint:disable force_try
//
import Foundation
import Testing
@testable import ContainerizationExtras
final class TestCIDRAddress {
@Test
func testMissingSplitError() {
let cidr = "192.168.64.0"
do {
_ = try CIDRAddress(cidr)
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
} catch {
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
}
}
@Test
func testInvalidIPError() {
let cidr = "192.168.256.1/24"
do {
_ = try CIDRAddress(cidr)
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
} catch {
#expect(
error as? NetworkAddressError == .invalidStringAddress(address: "192.168.256.1"),
"Unexpected error thrown: \(error)")
}
}
@Test
func testInvalidSubnetTypeError() {
let cidr = "192.168.64.0/foo"
do {
_ = try CIDRAddress(cidr)
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
} catch {
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
}
}
@Test
func testInvalidPrefixTooLargeError() {
let cidr = "192.168.64.0/33"
do {
_ = try CIDRAddress(cidr)
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
} catch {
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
}
}
@Test
func testInvalidPrefixTooSmallError() {
let cidr = "192.168.64.0/-1"
do {
_ = try CIDRAddress(cidr)
#expect(Bool(false), "Expected AddressError.invalidCIDR to be thrown")
} catch {
#expect(error as? NetworkAddressError == .invalidCIDR(cidr: cidr), "Unexpected error thrown: \(error)")
}
}
@Test
func testComparison() async throws {
let cidr64_1 = try! CIDRAddress("192.168.64.1/24")
let cidr64_1a = try! CIDRAddress("192.168.64.1/24")
#expect(cidr64_1 == cidr64_1a)
#expect(cidr64_1.contains(cidr: cidr64_1a))
#expect(cidr64_1a.contains(cidr: cidr64_1))
#expect(cidr64_1.overlaps(cidr: cidr64_1a))
#expect(cidr64_1a.overlaps(cidr: cidr64_1))
let cidr64_2 = try! CIDRAddress("192.168.64.2/24")
#expect(cidr64_1 != cidr64_2)
let addr63_255 = try! IPv4Address("192.168.63.255")
#expect(!cidr64_1.contains(ipv4: addr63_255))
let addr64_0 = try! IPv4Address("192.168.64.0")
#expect(cidr64_1.contains(ipv4: addr64_0))
let addr64_255 = try! IPv4Address("192.168.64.255")
#expect(cidr64_1.contains(ipv4: addr64_255))
let addr65_0 = try! IPv4Address("192.168.65.0")
#expect(!cidr64_1.contains(ipv4: addr65_0))
let cidr64_prefix25 = try! CIDRAddress("192.168.64.0/25")
#expect(cidr64_1.contains(cidr: cidr64_prefix25))
#expect(!cidr64_prefix25.contains(cidr: cidr64_1))
#expect(cidr64_1.overlaps(cidr: cidr64_prefix25))
#expect(cidr64_prefix25.overlaps(cidr: cidr64_1))
let cidr64_prefix25a = try! CIDRAddress("192.168.64.128/25")
#expect(cidr64_1.contains(cidr: cidr64_prefix25a))
#expect(!cidr64_prefix25a.contains(cidr: cidr64_1))
#expect(cidr64_1.overlaps(cidr: cidr64_prefix25a))
#expect(cidr64_prefix25a.overlaps(cidr: cidr64_1))
let cidr63_prefix24 = try! CIDRAddress("192.168.63.0/24")
#expect(!cidr64_1.contains(cidr: cidr63_prefix24))
#expect(!cidr63_prefix24.contains(cidr: cidr64_1))
#expect(!cidr64_1.overlaps(cidr: cidr63_prefix24))
#expect(!cidr63_prefix24.overlaps(cidr: cidr64_1))
let cidr65_prefix24 = try! CIDRAddress("192.168.65.0/24")
#expect(!cidr64_1.contains(cidr: cidr65_prefix24))
#expect(!cidr65_prefix24.contains(cidr: cidr64_1))
#expect(!cidr64_1.overlaps(cidr: cidr65_prefix24))
#expect(!cidr65_prefix24.overlaps(cidr: cidr64_1))
}
@Test
func testBiggestSubnet() throws {
let cidr = "1.2.3.4/0"
let subnet = try CIDRAddress(cidr)
#expect(try! IPv4Address("0.0.0.0") == subnet.lower)
#expect(try! IPv4Address("1.2.3.4") == subnet.address)
#expect(try! IPv4Address("255.255.255.255") == subnet.upper)
#expect(0 == subnet.prefixLength)
#expect(cidr == subnet.description)
}
@Test
func testSmallestSubnet() throws {
let cidr = "255.255.255.255/32"
let subnet = try CIDRAddress(cidr)
#expect(try! IPv4Address("255.255.255.255") == subnet.lower)
#expect(try! IPv4Address("255.255.255.255") == subnet.address)
#expect(try! IPv4Address("255.255.255.255") == subnet.upper)
#expect(32 == subnet.prefixLength)
#expect(cidr == subnet.description)
}
@Test
func testJustRightSubnet() throws {
let cidr = "192.168.64.10/24"
let subnet = try CIDRAddress(cidr)
#expect(try! IPv4Address("192.168.64.0") == subnet.lower)
#expect(try! IPv4Address("192.168.64.10") == subnet.address)
#expect(try! IPv4Address("192.168.64.255") == subnet.upper)
#expect(24 == subnet.prefixLength)
#expect(cidr == subnet.description)
}
@Test
func testBiggestRangedSubnet() throws {
let lower = try IPv4Address("127.255.255.255")
let upper = try IPv4Address("128.0.0.0")
let subnet = try CIDRAddress(lower: lower, upper: upper)
#expect(try! IPv4Address("0.0.0.0") == subnet.lower)
#expect(try! IPv4Address("127.255.255.255") == subnet.address)
#expect(try! IPv4Address("255.255.255.255") == subnet.upper)
#expect(0 == subnet.prefixLength)
#expect("\(lower)/0" == subnet.description)
}
@Test
func testSmallestRangedSubnet() throws {
let lower = try IPv4Address("255.255.255.255")
let subnet = try CIDRAddress(lower: lower, upper: lower)
#expect(lower == subnet.lower)
#expect(lower == subnet.address)
#expect(lower == subnet.upper)
#expect(32 == subnet.prefixLength)
#expect("\(lower)/32" == subnet.description)
}
@Test
func testJustRightRangedSubnet() throws {
let lower = try IPv4Address("192.168.64.10")
let upper = try IPv4Address("192.168.64.254")
let subnet = try CIDRAddress(lower: lower, upper: upper)
#expect(try! IPv4Address("192.168.64.0") == subnet.lower)
#expect(try! IPv4Address("192.168.64.10") == subnet.address)
#expect(try! IPv4Address("192.168.64.255") == subnet.upper)
#expect(24 == subnet.prefixLength)
#expect("\(lower)/24" == subnet.description)
_ = 16 >> PrefixLength(2)
}
@Test
func testCoding() async throws {
let text = "200.100.50.25/12"
let expected: CIDRAddress = try CIDRAddress(text)
let data = try JSONEncoder().encode(expected)
let actual = try JSONDecoder().decode(CIDRAddress.self, from: data)
#expect(expected == actual)
}
}
@@ -14,77 +14,162 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import Foundation
import Testing
@testable import ContainerizationExtras
final class TestIPv4Address {
@Test
func testUintMask() {
#expect(0xffff_ffff == PrefixLength(0).suffixMask32)
#expect(0x0000_7fff == PrefixLength(17).suffixMask32)
#expect(0x0000_0000 == PrefixLength(32).suffixMask32)
#expect(0x0000_0000 == PrefixLength(33).suffixMask32)
#expect(0x0000_0000 == PrefixLength(0).prefixMask32)
#expect(0xfffe_0000 == PrefixLength(15).prefixMask32)
#expect(0xffff_ffff == PrefixLength(32).prefixMask32)
#expect(0xffff_ffff == PrefixLength(33).prefixMask32)
@Suite("Unified IPAddress Tests")
struct IPAddressTests {
@Test(
"Parse IPv4 addresses",
arguments: [
"192.168.1.1",
"127.0.0.1",
"0.0.0.0",
"255.255.255.255",
]
)
func testParseIPv4(input: String) throws {
let ip = try IPAddress(input)
#expect(ip.isV4, "Should be IPv4: \(input)")
#expect(!ip.isV6, "Should not be IPv6: \(input)")
#expect(ip.ipv4 != nil, "Should have IPv4 value: \(input)")
#expect(ip.ipv6 == nil, "Should not have IPv6 value: \(input)")
}
@Test
func testOctetCountError() {
let ipAddressValue = "192.168.64"
do {
_ = try IPv4Address(ipAddressValue)
#expect(Bool(false), "Expected AddressError.invalidStringAddress to be thrown")
} catch {
#expect(
error as? NetworkAddressError == .invalidStringAddress(address: ipAddressValue),
"Unexpected error thrown: \(error)")
@Test(
"Parse IPv6 addresses",
arguments: [
"2001:db8::1",
"::1",
"::",
"fe80::1",
"2001:db8:0:0:0:0:0:1",
]
)
func testParseIPv6(input: String) throws {
let ip = try IPAddress(input)
#expect(ip.isV6, "Should be IPv6: \(input)")
#expect(!ip.isV4, "Should not be IPv4: \(input)")
#expect(ip.ipv6 != nil, "Should have IPv6 value: \(input)")
#expect(ip.ipv4 == nil, "Should not have IPv4 value: \(input)")
}
@Test(
"Loopback detection",
arguments: [
// Loopback addresses
("127.0.0.1", true, "IPv4 loopback"),
("127.0.0.255", true, "IPv4 loopback variant"),
("127.255.255.255", true, "Any 127.x.x.x"),
("::1", true, "IPv6 loopback"),
// Non-loopback addresses
("192.168.1.1", false, "Private IPv4"),
("2001:db8::1", false, "IPv6 documentation"),
("0.0.0.0", false, "IPv4 unspecified"),
("::", false, "IPv6 unspecified"),
]
)
func testLoopback(input: String, expected: Bool, description: String) throws {
let ip = try IPAddress(input)
#expect(ip.isLoopback == expected, "\(description): \(input) should\(expected ? "" : " not") be loopback")
}
@Test(
"Multicast detection",
arguments: [
// Multicast addresses
("224.0.0.1", true, "IPv4 multicast start"),
("239.255.255.255", true, "IPv4 multicast end (224.0.0.0/4)"),
("ff02::1", true, "IPv6 link-local multicast"),
("ff00::1", true, "IPv6 multicast"),
// Non-multicast addresses
("192.168.1.1", false, "Private IPv4"),
("2001:db8::1", false, "IPv6 documentation"),
("223.255.255.255", false, "Just before multicast range"),
]
)
func testMulticast(input: String, expected: Bool, description: String) throws {
let ip = try IPAddress(input)
#expect(ip.isMulticast == expected, "\(description): \(input) should\(expected ? "" : " not") be multicast")
}
@Test(
"Unspecified detection",
arguments: [
// Unspecified addresses
("0.0.0.0", true, "IPv4 unspecified"),
("::", true, "IPv6 unspecified"),
// Specified addresses
("0.0.0.1", false, "Not unspecified IPv4"),
("192.168.1.1", false, "Private IPv4"),
("::1", false, "IPv6 loopback"),
("2001:db8::1", false, "IPv6 documentation"),
]
)
func testUnspecified(input: String, expected: Bool, description: String) throws {
let ip = try IPAddress(input)
#expect(ip.isUnspecified == expected, "\(description): \(input) should\(expected ? "" : " not") be unspecified")
}
@Test("Comparable - IPv4 ordering")
func testIPv4Ordering() throws {
let ip1 = try IPv4Address("192.168.1.1")
let ip2 = try IPv4Address("192.168.1.2")
let ip3 = try IPv4Address("192.168.2.1")
#expect(ip1 < ip2)
#expect(ip2 < ip3)
#expect(ip1 < ip3)
#expect(!(ip2 < ip1))
}
@Test("Comparable - IPv6 ordering")
func testIPv6Ordering() throws {
let ip1 = try IPv6Address("2001:db8::1")
let ip2 = try IPv6Address("2001:db8::2")
let ip3 = try IPv6Address("2001:db9::1")
#expect(ip1 < ip2)
#expect(ip2 < ip3)
#expect(ip1 < ip3)
#expect(!(ip2 < ip1))
}
@Test(
"Equality",
arguments: [
("192.168.1.1", "192.168.1.1", true, "Same IPv4"),
("192.168.1.1", "192.168.1.2", false, "Different IPv4"),
("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0001", true, "Same IPv6, different format"),
("2001:db8::1", "2001:db8::2", false, "Different IPv6"),
]
)
func testEquality(addr1: String, addr2: String, shouldBeEqual: Bool, description: String) throws {
let ip1 = try IPAddress(addr1)
let ip2 = try IPAddress(addr2)
if shouldBeEqual {
#expect(ip1 == ip2, "\(description): \(addr1) should equal \(addr2)")
} else {
#expect(ip1 != ip2, "\(description): \(addr1) should not equal \(addr2)")
}
}
@Test
func testOctetInvalidError() {
let ipAddressValue = "192.168.256.255"
do {
_ = try IPv4Address(ipAddressValue)
#expect(Bool(false), "Expected AddressError.invalidStringAddress to be thrown")
} catch {
#expect(
error as? NetworkAddressError == .invalidStringAddress(address: ipAddressValue),
"Unexpected error thrown: \(error)")
}
}
@Test("Hashable")
func testHashable() throws {
var dict: [IPAddress: String] = [:]
@Test
func testAddressFromString() throws {
let ipAddressValue = "192.168.64.25"
let ipAddress = try IPv4Address(ipAddressValue)
#expect(ipAddressValue == ipAddress.description)
#expect(UInt32(0xc0a8_4019) == ipAddress.value)
#expect([192, 168, 64, 25] == ipAddress.networkBytes)
}
let ip1 = try IPAddress("192.168.1.1")
let ip2 = try IPAddress("2001:db8::1")
@Test
func testAddressPrefix() throws {
let ipAddressValue = "172.18.204.85"
let ipAddress = try IPv4Address(ipAddressValue)
#expect(0x0000_0000 == ipAddress.prefix(prefixLength: 0).value)
#expect(0xac12_cc55 == ipAddress.prefix(prefixLength: 32).value)
#expect(0xac12_cc55 == ipAddress.prefix(prefixLength: 33).value)
#expect(0xac10_0000 == ipAddress.prefix(prefixLength: 14).value)
}
dict[ip1] = "IPv4"
dict[ip2] = "IPv6"
@Test
func testCoding() async throws {
let text = "200.100.50.25"
let expected = try IPv4Address(text)
let data = try JSONEncoder().encode(expected)
let actual = try JSONDecoder().decode(IPv4Address.self, from: data)
#expect(expected == actual)
#expect(dict[ip1] == "IPv4")
#expect(dict[ip2] == "IPv6")
#expect(dict.count == 2)
}
}
@@ -0,0 +1,431 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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 Foundation
import Testing
@testable import ContainerizationExtras
@Suite("IPv4Address Tests")
struct IPv4AddressTests {
// MARK: - Initializer Tests
@Suite("Initializers")
struct InitializerTests {
@Test(
"UInt32 initializer",
arguments: [
(0x7F00_0001, "127.0.0.1"), // localhost
(0x0000_0000, "0.0.0.0"), // zero address
(0xFFFF_FFFF, "255.255.255.255"), // max address
(0xC0A8_0101, "192.168.1.1"), // private network
(0x0808_0808, "8.8.8.8"), // Google DNS
]
)
func testUInt32Initializer(inputValue: UInt32, description: String) {
let address = IPv4Address(inputValue)
#expect(address.value == inputValue)
}
@Test(
"String initializer - valid addresses",
arguments: [
("127.0.0.1", 0x7F00_0001), // localhost
("0.0.0.0", 0x0000_0000), // zero address
("255.255.255.255", 0xFFFF_FFFF), // broadcast
("10.0.0.1", 0x0A00_0001), // private network 10.x
("192.168.1.1", 0xC0A8_0101), // private network 192.168.x
("172.16.0.1", 0xAC10_0001), // private network 172.16.x
("1.2.3.4", 0x0102_0304), // single digits
("192.168.100.254", 0xC0A8_64FE), // mixed digits
]
)
func testStringInitializerValid(addressString: String, expectedValue: UInt32) throws {
let address = try IPv4Address(addressString)
#expect(address.value == expectedValue)
}
@Test(
"String initializer - invalid addresses",
arguments: [
"", // empty string
"1.2.3", // too short
"1.2.3.4.5", // too many octets
"192.168.1.256", // octet out of range
"192.168.001.1", // leading zeros
"01.2.3.4", // leading zero first octet
" 192.168.1.1", // leading whitespace
"192.168.1.1 ", // trailing whitespace
"192. 168.1.1", // internal whitespace
"192.168.1.a", // invalid character
"192.168.1.-1", // negative number
"192..1.1", // missing octet
".168.1.1", // missing first octet
"192.168.1.", // missing last octet
"192.168.1.1.extra", // too long
]
)
func testStringInitializerInvalid(invalidAddress: String) {
#expect(throws: IPAddressError.self) {
try IPv4Address(invalidAddress)
}
}
}
// MARK: - Property Tests
@Suite("Properties")
struct PropertyTests {
@Test(
"bytes property",
arguments: [
(UInt32(0x7F00_0001), [UInt8(127), UInt8(0), UInt8(0), UInt8(1)]), // localhost
(UInt32(0x0000_0000), [UInt8(0), UInt8(0), UInt8(0), UInt8(0)]), // zero
(UInt32(0xFFFF_FFFF), [UInt8(255), UInt8(255), UInt8(255), UInt8(255)]), // broadcast
(UInt32(0xC0A8_0101), [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]), // private network
(UInt32(0x1234_5678), [UInt8(0x12), UInt8(0x34), UInt8(0x56), UInt8(0x78)]), // byte order test
]
)
func testBytesProperty(inputValue: UInt32, expectedBytes: [UInt8]) {
let address = IPv4Address(inputValue)
#expect(address.bytes == expectedBytes)
}
@Test(
"description property",
arguments: [
(0x7F00_0001, "127.0.0.1"), // localhost
(0x0000_0000, "0.0.0.0"), // zero
(0xFFFF_FFFF, "255.255.255.255"), // broadcast
(0xC0A8_0101, "192.168.1.1"), // private network
(0x0102_0304, "1.2.3.4"), // single digits
]
)
func testDescriptionProperty(inputValue: UInt32, expectedDescription: String) {
let address = IPv4Address(inputValue)
#expect(address.description == expectedDescription)
}
@Test(
"round-trip string conversion",
arguments: [
"0.0.0.0",
"127.0.0.1",
"192.168.1.1",
"10.0.0.1",
"172.16.0.1",
"255.255.255.255",
"1.2.3.4",
"8.8.8.8",
"1.1.1.1",
]
)
func testRoundTripStringConversion(addressString: String) throws {
let address = try IPv4Address(addressString)
#expect(address.description == addressString)
}
}
// MARK: - Protocol Conformance Tests
@Suite("Protocol Conformances")
struct ProtocolConformanceTests {
@Test("Equatable conformance")
func testEquatableConformance() {
let addr1 = IPv4Address(0x7F00_0001)
let addr2 = IPv4Address(0x7F00_0001)
let addr3 = IPv4Address(0xC0A8_0101)
#expect(addr1 == addr2)
#expect(addr1 != addr3)
#expect(addr2 != addr3)
}
@Test("Hashable conformance")
func testHashableConformance() {
let addr1 = IPv4Address(0x7F00_0001)
let addr2 = IPv4Address(0x7F00_0001)
let addr3 = IPv4Address(0xC0A8_0101)
// Equal objects should have equal hash values
#expect(addr1.hashValue == addr2.hashValue)
// Different objects should ideally have different hash values
// (though this is not guaranteed, it's very likely for these values)
#expect(addr1.hashValue != addr3.hashValue)
// Test that addresses can be used in Sets and Dictionaries
let addressSet: Set<IPv4Address> = [addr1, addr2, addr3]
#expect(addressSet.count == 2) // addr1 and addr2 are equal
let addressDict = [addr1: "localhost", addr3: "private"]
#expect(addressDict[addr2] == "localhost") // addr2 equals addr1
}
@Test("CustomStringConvertible conformance")
func testCustomStringConvertibleConformance() {
let address = IPv4Address(0x7F00_0001)
let stringRepresentation = String(describing: address)
#expect(stringRepresentation == "127.0.0.1")
}
@Test("Sendable conformance")
func testSendableConformance() {
// This test verifies that IPv4Address can be safely passed across concurrency boundaries
let address = IPv4Address(0x7F00_0001)
Task {
let taskAddress = address
#expect(taskAddress.value == 0x7F00_0001)
}
}
}
// MARK: - Edge Cases and Error Conditions
@Suite("Edge Cases")
struct EdgeCaseTests {
@Test(
"boundary values",
arguments: [
("0.0.0.0", 0x0000_0000), // minimum
("255.255.255.255", 0xFFFF_FFFF), // maximum
("255.0.0.0", 0xFF00_0000), // max first octet
("0.255.0.0", 0x00FF_0000), // max second octet
("0.0.255.0", 0x0000_FF00), // max third octet
("0.0.0.255", 0x0000_00FF), // max fourth octet
]
)
func testBoundaryValues(addressString: String, expectedValue: UInt32) throws {
let address = try IPv4Address(addressString)
#expect(address.value == expectedValue)
}
@Test(
"special addresses",
arguments: [
"127.0.0.1", // loopback
"255.255.255.255", // broadcast
"0.0.0.0", // network address
"8.8.8.8", // Google DNS
"1.1.1.1", // Cloudflare DNS
]
)
func testSpecialAddresses(addressString: String) throws {
let address = try IPv4Address(addressString)
#expect(address.description == addressString)
}
@Test(
"leading zero validation - invalid",
arguments: [
"01.0.0.0",
"0.01.0.0",
"0.0.01.0",
"0.0.0.01",
"192.168.001.1",
"010.0.0.1",
"00.0.0.1",
]
)
func testLeadingZeroValidationInvalid(invalidAddress: String) {
#expect(throws: IPAddressError.self) {
try IPv4Address(invalidAddress)
}
}
@Test("leading zero validation - valid single zeros")
func testLeadingZeroValidationValid() {
// Single "0" should be valid
#expect(throws: Never.self) {
try IPv4Address("0.0.0.0")
}
}
@Test(
"string length validation - too short",
arguments: [
"", "1", "1.2", "1.2.3", "1.2.3.",
]
)
func testStringLengthValidationTooShort(shortString: String) {
#expect(throws: IPAddressError.self) {
try IPv4Address(shortString)
}
}
@Test(
"string length validation - too long",
arguments: [
"255.255.255.255.1",
"1234.168.1.1",
"192.1234.1.1",
"192.168.1234.1",
"192.168.1.1234",
]
)
func testStringLengthValidationTooLong(longString: String) {
#expect(throws: IPAddressError.self) {
try IPv4Address(longString)
}
}
}
// MARK: - Performance Tests
@Suite("Performance")
struct PerformanceTests {
@Test("parsing performance")
func testParsingPerformance() throws {
let testAddresses = [
"192.168.1.1",
"10.0.0.1",
"172.16.0.1",
"127.0.0.1",
"8.8.8.8",
"1.1.1.1",
"255.255.255.255",
"0.0.0.0",
]
// Warm up
for _ in 0..<100 {
for address in testAddresses {
_ = try IPv4Address(address)
}
}
// Measure performance
let iterations = 10000
let startTime = Date()
for _ in 0..<iterations {
for address in testAddresses {
_ = try IPv4Address(address)
}
}
let endTime = Date()
let totalTime = endTime.timeIntervalSince(startTime)
let averageTime = totalTime / Double(iterations * testAddresses.count)
// Should be very fast - less than 1ms per parse on average
#expect(averageTime < 0.001, "Parsing should be fast: \(averageTime)s per address")
}
@Test("bytes property performance")
func testBytesPropertyPerformance() {
let address = IPv4Address(0xC0A8_0101)
let iterations = 100000
let startTime = Date()
for _ in 0..<iterations {
_ = address.bytes
}
let endTime = Date()
let totalTime = endTime.timeIntervalSince(startTime)
let averageTime = totalTime / Double(iterations)
// Should be very fast - less than 0.1ms per call on average
#expect(averageTime < 0.0001, "Bytes property should be fast: \(averageTime)s per call")
}
@Test("description property performance")
func testDescriptionPropertyPerformance() {
let address = IPv4Address(0xC0A8_0101)
let iterations = 10000
let startTime = Date()
for _ in 0..<iterations {
_ = address.description
}
let endTime = Date()
let totalTime = endTime.timeIntervalSince(startTime)
let averageTime = totalTime / Double(iterations)
// Should be reasonably fast - less than 1ms per call on average
#expect(averageTime < 0.001, "Description property should be fast: \(averageTime)s per call")
}
}
// MARK: - Integration Tests
@Suite("Integration")
struct IntegrationTests {
@Test(
"comprehensive round-trip test",
arguments: [
(0x0000_0000, "0.0.0.0"),
(0x7F00_0001, "127.0.0.1"),
(0xC0A8_0101, "192.168.1.1"),
(0x0A00_0001, "10.0.0.1"),
(0xAC10_0001, "172.16.0.1"),
(0xFFFF_FFFF, "255.255.255.255"),
(0x0808_0808, "8.8.8.8"),
(0x0101_0101, "1.1.1.1"),
(0x1234_5678, "18.52.86.120"),
(0xDEAD_BEEF, "222.173.190.239"),
]
)
func testComprehensiveRoundTrip(expectedValue: UInt32, expectedString: String) throws {
// Test UInt32 -> String
let addressFromUInt32 = IPv4Address(expectedValue)
#expect(addressFromUInt32.description == expectedString)
// Test String -> UInt32
let addressFromString = try IPv4Address(expectedString)
#expect(addressFromString.value == expectedValue)
// Test equality
#expect(addressFromUInt32 == addressFromString)
}
@Test(
"error message consistency",
arguments: [
"",
"256.1.1.1",
"1.2.3",
"1.2.3.4.5",
"192.168.001.1",
" 192.168.1.1",
"192.168.1.1 ",
"192.168.1.a",
]
)
func testErrorMessageConsistency(invalidInput: String) {
do {
_ = try IPv4Address(invalidInput)
#expect(Bool(false), "Should have thrown for input: \(invalidInput)")
} catch let error as IPAddressError {
#expect(error == IPAddressError.unableToParse)
} catch {
#expect(Bool(false), "Should have thrown IPAddressError, got: \(error)")
}
}
}
}
@@ -0,0 +1,734 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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 Foundation
import Testing
@testable import ContainerizationExtras
@Suite("IPv6Address Parsing Tests")
struct IPv6AddressParseTests {
// MARK: - Valid Hexadecimal Group Tests
@Test(
"Parsing valid hexadecimal groups",
arguments: [
("0", 0x0),
("1", 0x1),
("F", 0xF),
("FF", 0xFF),
("FFF", 0xFFF),
("FFFF", 0xFFFF),
("1234", 0x1234),
("abcd", 0xABCD),
("ABCD", 0xABCD),
("0000", 0x0000),
]
)
func testParseValidHexadecimalGroups(input: String, expectedValue: UInt16) throws {
let utf8 = input.utf8
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
from: utf8,
startingAt: utf8.startIndex
)
#expect(
parsedValue == expectedValue,
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
)
#expect(nextIndex == input.endIndex, "Parser should consume entire input")
}
@Test(
"Parsing hexadecimal groups with trailing characters",
arguments: [
("FF:1234", 0xFF, ":1234"),
("1234G", 0x1234, "G"),
("AB::CD", 0xAB, "::CD"),
("0Z", 0x0, "Z"),
]
)
func testParseHexadecimalGroupWithTrailingCharacters(
input: String,
expectedValue: UInt16,
expectedRemainder: String
) throws {
let utf8 = input.utf8
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
from: utf8,
startingAt: utf8.startIndex
)
let remainder = String(input[String.Index(nextIndex, within: input)!...])
#expect(
parsedValue == expectedValue,
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
)
#expect(
remainder == expectedRemainder,
"For input '\(input)': expected remainder '\(expectedRemainder)' but got '\(remainder)'"
)
}
// MARK: - Error Handling Tests
@Test(
"Parsing invalid hexadecimal groups should throw",
arguments: [
"", // Empty string - no hex digits found
"G", // Invalid hex character - no hex digits found
"GGGG", // All invalid hex characters - no hex digits found
]
)
func testParseInvalidHexadecimalGroup(invalidInput: String) {
#expect(throws: IPAddressError.self) {
let utf8 = invalidInput.utf8
_ = try IPv6Address.parseHexadecimal(
from: utf8,
startingAt: utf8.startIndex
)
}
}
@Test(
"Parsing hexadecimal groups with overflow behavior",
arguments: [
("12345", 0x1234, "5"), // 5 digits -> takes first 4
("10000", 0x1000, "0"), // 5 digits -> takes first 4
("FFFFF", 0xFFFF, "F"), // 5 digits -> takes first 4
("123456789", 0x1234, "56789"), // Many digits -> takes first 4
]
)
func testParseHexadecimalGroupOverflow(
input: String,
expectedValue: UInt16,
expectedRemainder: String
) throws {
let utf8 = input.utf8
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
from: utf8,
startingAt: utf8.startIndex
)
let remainder = String(input[String.Index(nextIndex, within: input)!...])
#expect(
parsedValue == expectedValue,
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
)
#expect(
remainder == expectedRemainder,
"For input '\(input)': expected remainder '\(expectedRemainder)' but got '\(remainder)'"
)
}
@Test("Parsing from middle of string")
func testParseHexadecimalGroupFromMiddle() throws {
let input = "prefix1234suffix"
let utf8 = input.utf8
let startIndex = utf8.index(utf8.startIndex, offsetBy: 6) // Start at "1234"
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
from: utf8,
startingAt: startIndex
)
#expect(parsedValue == 0x1234)
let remainder = String(input[String.Index(nextIndex, within: input)!...])
#expect(remainder == "suffix")
}
// MARK: - Performance Tests
@Test("Performance with maximum length hex groups")
func testParsePerformance() throws {
let testInput = "FFFF"
// Measure performance of parsing operation
let startTime = CFAbsoluteTimeGetCurrent()
let count = 10000
for _ in 0..<count {
let utf8 = testInput.utf8
_ = try IPv6Address.parseHexadecimal(
from: utf8,
startingAt: utf8.startIndex
)
}
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
// Expect parsing to be reasonably fast (less than 1ms per operation on average)
print("Parsed \(count) IPv6 addresses in \(timeElapsed)s")
#expect(timeElapsed < 0.1, "Parsing should be performant: \(timeElapsed)s for 10000 operations")
}
// MARK: - RFC 4291 Section 2.3 Text Representation of Address Prefixes Tests
@Test("RFC 4291 Section 2.3 - Valid address prefix representations")
func testRFC4291Section23ValidPrefixRepresentations() throws {
// Examples of valid 60-bit prefix 20010DB80000CD3 (hexadecimal)
let validPrefixCases = [
"2001:0DB8:0000:CD30:0000:0000:0000:0000/60",
"2001:0DB8::CD30:0:0:0:0/60",
"2001:0DB8:0:CD30::/60",
]
let parsed = try validPrefixCases.map { testCase in
let addressPart = String(testCase.prefix(while: { $0 != "/" }))
return try IPv6Address.parse(addressPart).bytes
}
#expect(
parsed.allSatisfy { $0 == parsed.first },
"All valid prefix representations should parse to identical byte arrays"
)
}
@Test("RFC 4291 Section 2.3 - Invalid address prefix representations")
func testRFC4291Section23InvalidPrefixRepresentations() throws {
// RFC 4291 Section 2.3 examples of invalid representations
let invalidPrefixCases = [
"2001:0DB8:0:CD3/60", // ex 1: may drop leading zeros, but not trailing zeros"
"2001:0DB8::CD30/60", // ex 2: expands to wrong address
"2001:0DB8::CD3/60", // ex 3: expands to wrong address
]
let validAddress = "2001:0DB8:0000:CD30:0000:0000:0000:0000"
let parsedValidAddress: [UInt8] = [32, 1, 13, 184, 0, 0, 205, 48, 0, 0, 0, 0, 0, 0, 0, 0]
let parsedLibValidAddress = try IPv6Address.parse(validAddress).bytes
#expect(parsedLibValidAddress == parsedValidAddress)
for testCase in invalidPrefixCases {
let addressPart = String(testCase.prefix(while: { $0 != "/" }))
do {
let actualBytes = try IPv6Address.parse(addressPart).bytes
#expect(actualBytes != parsedValidAddress, "\(testCase) should not match valid prefix")
} catch {
// If parsing fails, that's also acceptable for invalid representations
#expect(error is IPAddressError, "\(testCase) should throw IPAddressError if it fails to parse")
}
}
}
@Test(
"RFC 4291 Section 2.3 - Leading zero rules in prefixes",
arguments: [
("2001:DB8:0:0CD3::", "2001:DB8:0:CD3::", "Can drop leading zeros 0CD3 -> CD3"),
("2001:0DB8::", "2001:DB8::", "Can drop leading zeros 0DB8 -> DB8"),
("0001:0002:0003::", "1:2:3::", "Can drop leading zeros in multiple groups"),
]
)
func testRFC4291Section23LeadingZeroRules(form1: String, form2: String, description: String) throws {
let bytes1 = try IPv6Address.parse(form1).bytes
let bytes2 = try IPv6Address.parse(form2).bytes
#expect(bytes1 == bytes2, "\(description)")
}
@Test(
"RFC 4291 Section 2.3 - cannot drop trailing zeros",
arguments: [
("2001:DB8:0:CD30::", "2001:DB8:0:CD3::", "Cannot drop trailing zeros in CD30 -> CD3"),
("2001:DB80::", "2001:DB8::", "Cannot drop trailing zero DB80 -> DB8"),
("ABCD:EF00::", "ABCD:EF::", "Cannot drop trailing zeros EF00 -> EF"),
]
)
func testRFC4291Section23CannotDropTrailingZeros(full: String, truncated: String, description: String) throws {
let fullBytes = try IPv6Address.parse(full).bytes
let truncatedBytes = try IPv6Address.parse(truncated).bytes
#expect(fullBytes != truncatedBytes, "\(description)")
}
@Test("RFC 4291 Section 2.3 - Node address and subnet prefix combination")
func testRFC4291Section23NodeAddressSubnetCombination() throws {
// RFC example: node address 2001:0DB8:0:CD30:123:4567:89AB:CDEF
// and its subnet number 2001:0DB8:0:CD30::/60
// can be abbreviated as 2001:0DB8:0:CD30:123:4567:89AB:CDEF/60
let nodeAddress = "2001:0DB8:0:CD30:123:4567:89AB:CDEF"
let subnetPrefix = "2001:0DB8:0:CD30::"
// Both should parse successfully
#expect(throws: Never.self, "Node address should parse successfully") {
_ = try IPv6Address.parse(nodeAddress)
}
#expect(throws: Never.self, "Subnet prefix should parse successfully") {
_ = try IPv6Address.parse(subnetPrefix)
}
// Verify that the subnet prefix is indeed a prefix of the node address
let nodeBytes = try IPv6Address.parse(nodeAddress).bytes
let subnetBytes = try IPv6Address.parse(subnetPrefix).bytes
// Validate that node address has the subnet as a prefix (60-bit prefix = 7.5 bytes)
let prefixLength = 60
let hasValidPrefix = nodeBytes.hasPrefix(subnetBytes, upToBits: prefixLength)
#expect(
hasValidPrefix,
"Node address should have subnet as a \(prefixLength)-bit prefix"
)
}
// MARK: - RFC 4291 Section 2.2 Comprehensive Text Representation Tests
@Test(
"RFC 4291 Section 2.2 - Preferred form with all groups",
arguments: [
"2001:0db8:0000:0042:0000:8a2e:0370:7334",
"ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
"0000:0000:0000:0000:0000:0000:0000:0000",
"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF",
]
)
func testRFC4291Section22PreferredForm(testCase: String) throws {
#expect(throws: Never.self, "Should parse preferred form: \(testCase)") {
_ = try IPv6Address.parse(testCase)
}
}
@Test(
"RFC 4291 Section 2.2 - Leading zero omission in all positions",
arguments: [
("2001:0db8:0000:0042:0000:8a2e:0370:7334", "2001:db8:0:42:0:8a2e:370:7334"),
("0001:0002:0003:0004:0005:0006:0007:0008", "1:2:3:4:5:6:7:8"),
("0000:0001:0002:0003:0004:0005:0006:0007", "0:1:2:3:4:5:6:7"),
("1000:0100:0010:0001:1000:0100:0010:0001", "1000:100:10:1:1000:100:10:1"),
]
)
func testRFC4291Section22LeadingZeroOmission(full: String, compressed: String) throws {
let fullBytes = try IPv6Address.parse(full).bytes
let compressedBytes = try IPv6Address.parse(compressed).bytes
#expect(fullBytes == compressedBytes, "'\(full)' should equal '\(compressed)'")
}
@Test(
"RFC 4291 Section 2.2 - Zero compression at beginning",
arguments: [
("::", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
("::1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
("::8a2e:370:7334", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34]),
]
)
func testRFC4291Section22ZeroCompressionAtBeginning(compressed: String, expected: [UInt8]) throws {
let parsed = try IPv6Address.parse(compressed)
#expect(parsed.bytes == expected, "'\(compressed)' should parse correctly")
}
@Test(
"RFC 4291 Section 2.2 - Zero compression in middle",
arguments: [
("2001:db8::8a2e:370:7334", "2001:db8:0:0:0:8a2e:370:7334"),
("2001:db8::1", "2001:db8:0:0:0:0:0:1"),
("fe80::1", "fe80:0:0:0:0:0:0:1"),
("2001:0db8:0:0::1", "2001:db8:0:0:0:0:0:1"),
]
)
func testRFC4291Section22ZeroCompressionInMiddle(compressed: String, full: String) throws {
let compressedBytes = try IPv6Address.parse(compressed).bytes
let fullBytes = try IPv6Address.parse(full).bytes
#expect(compressedBytes == fullBytes, "'\(compressed)' should equal '\(full)'")
}
@Test(
"RFC 4291 Section 2.2 - Zero compression at end",
arguments: [
("2001:db8::", "2001:db8:0:0:0:0:0:0"),
("2001:db8:0:0:1::", "2001:db8:0:0:1:0:0:0"),
("fe80::", "fe80:0:0:0:0:0:0:0"),
("1::", "1:0:0:0:0:0:0:0"),
]
)
func testRFC4291Section22ZeroCompressionAtEnd(compressed: String, full: String) throws {
let compressedBytes = try IPv6Address.parse(compressed).bytes
let fullBytes = try IPv6Address.parse(full).bytes
#expect(compressedBytes == fullBytes, "'\(compressed)' should equal '\(full)'")
}
@Test(
"RFC 4291 Section 2.2 - Multiple :: should fail",
arguments: [
"2001::db8::1",
"::1::2",
"fe80::1::2::3",
"::1::",
]
)
func testRFC4291Section22MultipleDoubleColonsShouldFail(invalid: String) {
#expect(throws: IPAddressError.self, "Multiple '::' should fail: \(invalid)") {
_ = try IPv6Address.parse(invalid)
}
}
@Test(
"RFC 4291 Section 2.2 - Case insensitivity",
arguments: [
("2001:db8::1", "2001:DB8::1", "2001:Db8::1"),
("dead:beef::cafe", "DEAD:BEEF::CAFE", "DeAd:BeEf::CaFe"),
("fe80::1", "FE80::1", "Fe80::1"),
("abcd:ef01:2345:6789::1", "ABCD:EF01:2345:6789::1", "AbCd:Ef01:2345:6789::1"),
]
)
func testRFC4291Section22CaseInsensitivity(lower: String, upper: String, mixed: String) throws {
let lowerBytes = try IPv6Address.parse(lower).bytes
let upperBytes = try IPv6Address.parse(upper).bytes
let mixedBytes = try IPv6Address.parse(mixed).bytes
#expect(lowerBytes == upperBytes, "Case should not matter: '\(lower)' vs '\(upper)'")
#expect(lowerBytes == mixedBytes, "Case should not matter: '\(lower)' vs '\(mixed)'")
}
@Test("RFC 4291 Section 2.2 - Special addresses")
func testRFC4291Section22SpecialAddresses() throws {
// Unspecified address
let unspecified = try IPv6Address.parse("::")
#expect(unspecified.bytes.allSatisfy { $0 == 0 }, ":: should be all zeros")
// Loopback address
let loopback = try IPv6Address.parse("::1")
let expectedLoopback: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
#expect(loopback.bytes == expectedLoopback, "::1 should be loopback address")
// IPv4-compatible (deprecated but valid syntax)
let ipv4Compat = try IPv6Address.parse("::c000:0201")
let expectedCompat: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc0, 0x00, 0x02, 0x01]
#expect(ipv4Compat.bytes == expectedCompat, "::c000:0201 should parse correctly")
}
@Test(
"RFC 4291 Section 2.2 - Invalid formats should fail",
arguments: [
"2001:db8", // Too few groups without ::
"2001:db8:1:2:3:4:5:6:7", // Too many groups
"2001:db8:1:2:3:4:5:6:7:8:9", // Too many groups
"2001:db8:::1", // Triple colon
"2001:db8::1::2", // Multiple ::
"gggg::1", // Invalid hex character
"2001:db8:xyz::1", // Invalid hex character
"::ffff:", // Trailing colon
":2001:db8::1", // Leading single colon
"2001:db8::1:", // Trailing single colon
]
)
func testRFC4291Section22InvalidFormatsShouldFail(invalid: String) {
#expect(throws: IPAddressError.self, "Invalid format should fail: \(invalid)") {
_ = try IPv6Address.parse(invalid)
}
}
@Test("RFC 4291 Section 2.2 - Maximum values")
func testRFC4291Section22MaximumValues() throws {
// All FFs - maximum value
let maxAddress = try IPv6Address.parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
#expect(maxAddress.bytes.allSatisfy { $0 == 0xFF }, "All groups should be 0xFFFF")
// Mix of max and min values
let mixedMax = try IPv6Address.parse("ffff:0:ffff:0:ffff:0:ffff:0")
let expectedMixed: [UInt8] = [0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0]
#expect(mixedMax.bytes == expectedMixed, "Should alternate between max and zero")
}
@Test(
"RFC 4291 Section 2.2 - Single zero groups",
arguments: [
("2001:db8:0:1:2:3:4:5", "2001:db8:0:1:2:3:4:5"), // Single 0, no compression needed
("2001:db8:0:0:1:2:3:4", "2001:db8::1:2:3:4"), // Two zeros can be compressed
("0:0:0:0:0:0:0:1", "::1"), // All zeros except last
]
)
func testRFC4291Section22SingleZeroGroups(withZero: String, withCompression: String) throws {
let zeroBytes = try IPv6Address.parse(withZero).bytes
let compressedBytes = try IPv6Address.parse(withCompression).bytes
#expect(zeroBytes == compressedBytes, "'\(withZero)' should equal '\(withCompression)'")
}
@Test("RFC 4291 Section 2.2 - Boundary conditions")
func testRFC4291Section22BoundaryConditions() throws {
// Single non-zero value in each position
for position in 0..<8 {
var groups = [String](repeating: "0", count: 8)
groups[position] = "1"
let address = groups.joined(separator: ":")
#expect(throws: Never.self, "Single non-zero at position \(position) should parse") {
_ = try IPv6Address.parse(address)
}
}
// Verify the bytes are correct
let firstPosition = try IPv6Address.parse("1:0:0:0:0:0:0:0")
#expect(firstPosition.bytes[0] == 0 && firstPosition.bytes[1] == 1, "First group should be 0x0001")
let lastPosition = try IPv6Address.parse("0:0:0:0:0:0:0:1")
#expect(lastPosition.bytes[14] == 0 && lastPosition.bytes[15] == 1, "Last group should be 0x0001")
}
@Test(
"RFC 4291 Section 2.2 - Hex digit limits",
arguments: [
("1:2:3:4:5:6:7:8", "1-digit groups"),
("12:34:56:78:9a:bc:de:f0", "2-digit groups"),
("123:456:789:abc:def:123:456:789", "3-digit groups"),
("1234:5678:9abc:def0:1234:5678:9abc:def0", "4-digit groups"),
("1:12:123:1234:1:12:123:1234", "Mixed digit counts"),
]
)
func testRFC4291Section22HexDigitLimits(address: String, description: String) throws {
#expect(throws: Never.self, "Should parse \(description): \(address)") {
_ = try IPv6Address.parse(address)
}
}
@Test("RFC 4291 Section 2.2 - Equivalence of different representations")
func testRFC4291Section22EquivalenceOfRepresentations() throws {
let equivalentGroups: [[String]] = [
// Same address, different representations
[
"2001:0db8:0000:0000:0000:0000:0000:0001",
"2001:db8:0:0:0:0:0:1",
"2001:db8::1",
"2001:0DB8::1",
"2001:0DB8:0000:0000:0000:0000:0000:0001",
],
[
"fe80:0000:0000:0000:0000:0000:0000:0001",
"fe80::1",
"FE80::1",
"fe80:0:0:0:0:0:0:1",
],
[
"0000:0000:0000:0000:0000:0000:0000:0000",
"::",
"0:0:0:0:0:0:0:0",
],
]
for group in equivalentGroups {
let bytesArray = try group.map { try IPv6Address.parse($0).bytes }
let firstBytes = bytesArray[0]
for (index, bytes) in bytesArray.enumerated() {
#expect(bytes == firstBytes, "All forms should be equivalent: \(group[index])")
}
}
}
@Test(
"RFC 4291 Section 2.2 - Zero compression selection (longest run)",
arguments: [
("2001:db8:0:0:1:0:0:1", "Two runs of 2 zeros each"),
("2001:0:0:0:db8:0:0:1", "Run of 3 and run of 2"),
("2001:db8:0:0:0:0:1:1", "Run of 4 zeros in middle"),
]
)
func testRFC4291Section22ZeroCompressionLongestRun(address: String, description: String) throws {
#expect(throws: Never.self, "Should parse \(description): \(address)") {
_ = try IPv6Address.parse(address)
}
}
@Test(
"RFC 4291 Section 2.2 - Edge case with :: at different positions",
arguments: [
("::1", "0:0:0:0:0:0:0:1"),
("1::", "1:0:0:0:0:0:0:0"),
("1::1", "1:0:0:0:0:0:0:1"),
("1:2::1", "1:2:0:0:0:0:0:1"),
("1::2:3", "1:0:0:0:0:0:2:3"),
("1:2:3::4:5:6", "1:2:3:0:0:4:5:6"),
]
)
func testRFC4291Section22DoubleColonAtDifferentPositions(compressed: String, expanded: String) throws {
let compressedBytes = try IPv6Address.parse(compressed).bytes
let expandedBytes = try IPv6Address.parse(expanded).bytes
#expect(compressedBytes == expandedBytes, "'\(compressed)' should equal '\(expanded)'")
}
// MARK: - RFC 4291 Section 2.2 IPv4 Mixed Notation Tests
@Test(
"RFC 4291 Section 2.2 - IPv4 mixed notation basic formats",
arguments: [
// RFC 4291 examples
("0:0:0:0:0:0:13.1.68.3", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 1, 68, 3]),
("::13.1.68.3", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 1, 68, 3]),
// IPv4-mapped IPv6 address (::ffff:x.x.x.x)
("::ffff:129.144.52.38", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 129, 144, 52, 38]),
("0:0:0:0:0:ffff:129.144.52.38", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 129, 144, 52, 38]),
// IPv4-compatible IPv6 address (deprecated but valid syntax)
("::192.168.1.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
("::0.0.0.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
// Various valid positions
("2001:db8::192.0.2.1", [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 1]),
("fe80::192.168.1.1", [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
]
)
func testRFC4291Section22IPv4MixedNotation(address: String, expected: [UInt8]) throws {
let parsed = try IPv6Address.parse(address)
#expect(parsed.bytes == expected, "IPv4 mixed notation '\(address)' should parse correctly")
}
@Test(
"RFC 4291 Section 2.2 - IPv4 mixed notation with full IPv6 prefix",
arguments: [
("0:0:0:0:0:0:192.168.1.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
("2001:db8:0:0:0:0:192.0.2.1", [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 1]),
("64:ff9b::192.0.2.33", [0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 33]),
]
)
func testRFC4291Section22IPv4MixedNotationFullPrefix(address: String, expected: [UInt8]) throws {
let parsed = try IPv6Address.parse(address)
#expect(parsed.bytes == expected, "Full prefix with IPv4 '\(address)' should parse correctly")
}
@Test("RFC 4291 Section 2.2 - IPv4 mixed notation edge cases")
func testRFC4291Section22IPv4MixedNotationEdgeCases() throws {
// Maximum values
let maxIPv4 = try IPv6Address.parse("::255.255.255.255")
let expectedMax: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255]
#expect(maxIPv4.bytes == expectedMax, "Maximum IPv4 values should work")
// Minimum values
let minIPv4 = try IPv6Address.parse("::0.0.0.0")
let expectedMin: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
#expect(minIPv4.bytes == expectedMin, "Minimum IPv4 values should work")
// With non-zero prefix
let withPrefix = try IPv6Address.parse("2001:db8:85a3::8a2e:255.255.255.255")
let expectedPrefix: [UInt8] = [0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0, 0, 0, 0, 0x8a, 0x2e, 255, 255, 255, 255]
#expect(withPrefix.bytes == expectedPrefix, "IPv4 with hex prefix should work")
}
@Test(
"RFC 4291 Section 2.2 - IPv4 mixed notation invalid formats",
arguments: [
"::192.168.1", // Incomplete IPv4
"::192.168.1.1.1", // Too many IPv4 octets
"::256.1.1.1", // IPv4 octet out of range
"::192.168.001.1", // Leading zeros in IPv4
"::192.168.-1.1", // Negative in IPv4
"::192.168.1.a", // Non-numeric in IPv4
"2001:db8:1:2:3:4:5:192.168.1.1", // Too many hex groups before IPv4
"::192.168.1.1:1234", // Extra hex after IPv4
]
)
func testRFC4291Section22IPv4MixedNotationInvalid(invalid: String) {
#expect(throws: IPAddressError.self, "Invalid IPv4 mixed notation should fail: \(invalid)") {
_ = try IPv6Address.parse(invalid)
}
}
@Test("RFC 4291 Section 2.2 - IPv4 mixed notation equivalence")
func testRFC4291Section22IPv4MixedNotationEquivalence() throws {
// These should all represent the same address
let equivalentForms = [
"0:0:0:0:0:0:192.0.2.1",
"::192.0.2.1",
"::c000:201", // Same as 192.0.2.1 in hex
]
let bytesArray = try equivalentForms.map { try IPv6Address.parse($0).bytes }
let firstBytes = bytesArray[0]
for (index, bytes) in bytesArray.enumerated() {
#expect(bytes == firstBytes, "All forms should be equivalent: \(equivalentForms[index])")
}
}
@Test(
"RFC 4291 Section 2.2 - IPv4-mapped IPv6 addresses",
arguments: [
("127.0.0.1", "::ffff:127.0.0.1"),
("192.168.1.1", "::ffff:192.168.1.1"),
("8.8.8.8", "::ffff:8.8.8.8"),
("0.0.0.0", "::ffff:0.0.0.0"),
("255.255.255.255", "::ffff:255.255.255.255"),
]
)
func testRFC4291Section22IPv4MappedAddresses(ipv4: String, ipv6: String) throws {
let parsed = try IPv6Address.parse(ipv6)
// First 10 bytes should be 0
#expect(parsed.bytes[0..<10].allSatisfy { $0 == 0 }, "First 10 bytes should be zero")
// Next 2 bytes should be 0xff
#expect(parsed.bytes[10] == 0xff && parsed.bytes[11] == 0xff, "Bytes 10-11 should be 0xffff")
// Last 4 bytes should match IPv4 address
let ipv4Parsed = try IPv4Address.parse(ipv4)
let ipv4Bytes = [
UInt8((ipv4Parsed >> 24) & 0xFF),
UInt8((ipv4Parsed >> 16) & 0xFF),
UInt8((ipv4Parsed >> 8) & 0xFF),
UInt8(ipv4Parsed & 0xFF),
]
#expect(Array(parsed.bytes[12..<16]) == ipv4Bytes, "Last 4 bytes should match IPv4")
}
@Test(
"RFC 4291 Section 2.2 - IPv4 mixed notation with zone identifier",
arguments: [
"::ffff:192.168.1.1%eth0",
"fe80::192.168.1.1%lo0",
]
)
func testRFC4291Section22IPv4MixedNotationWithZone(testCase: String) throws {
#expect(throws: Never.self, "IPv4 mixed notation with zone should parse: \(testCase)") {
let parsed = try IPv6Address.parse(testCase)
#expect(parsed.zone != nil, "Zone should be preserved")
}
}
}
// MARK: - Array Extension for Prefix Validation
extension Array where Element == UInt8 {
/// Checks if this byte array has another array as a prefix up to the specified number of bits
/// - Parameters:
/// - prefix: The potential prefix array
/// - bits: Number of bits to compare (0-128 for IPv6)
/// - Returns: true if the prefix matches for the specified number of bits
func hasPrefix(_ prefix: [UInt8], upToBits bits: Int) -> Bool {
guard self.count >= 16 && prefix.count >= 16 else { return false }
guard bits >= 0 && bits <= 128 else { return false }
let fullBytes = bits / 8
let remainingBits = bits % 8
// Compare full bytes
for i in 0..<fullBytes {
if self[i] != prefix[i] {
return false
}
}
// Compare partial byte if needed
if remainingBits > 0 && fullBytes < 16 {
let shiftAmount = 8 - remainingBits
let mask: UInt8 = shiftAmount >= 8 ? 0 : (0xFF << shiftAmount)
return (self[fullBytes] & mask) == (prefix[fullBytes] & mask)
}
return true
}
}
@@ -0,0 +1,243 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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 Foundation
import Testing
@testable import ContainerizationExtras
@Suite("IPv6 Address Tests")
struct IPv6AddressTests {
// MARK: - String Representation Tests (RFC 5952)
@Test(
"IPv6 address string representation - RFC 5952",
arguments: [
// Zero compression algorithm tests
("0:0:0:0:0:0:0:0", "::", "all zeros - unspecified address"),
("0:0:0:0:0:0:0:1", "::1", "leading zeros - loopback"),
("2001:0db8:0:0:0:0:0:0", "2001:db8::", "trailing zeros"),
("2001:0:0:0:0:0:0db8:1", "2001::db8:1", "middle zeros - longest run"),
("2001:0:0:0:0:0db8:0:1", "2001::db8:0:1", "multiple runs - prefer longest"),
("2001:0:0db8:0:1:0:0:2", "2001:0:db8:0:1::2", "tie-breaking - first occurrence wins"),
("2001:0:0db8:1:2:3:4:5", "2001:0:db8:1:2:3:4:5", "single zero - no compression (min 2 required)"),
("2001:0db8:0:0:1:2:3:4", "2001:db8::1:2:3:4", "exactly 2 zeros - minimum for compression"),
("0:0:0:0:1234:0:0:0", "::1234:0:0:0", "tie-breaking - first run wins (4 vs 3 zeros)"),
// RFC 5952 formatting rules
(
"ABCD:EF01:2345:6789:9ABC:DEF0:1122:3344", "abcd:ef01:2345:6789:9abc:def0:1122:3344",
"lowercase hex (Section 4.3)"
),
("0001:0002:0003:0004:0005:0006:0007:0008", "1:2:3:4:5:6:7:8", "no leading zeros (Section 4.1)"),
// Edge cases
("2001:a:a:a:0:0db8:0:1", "2001:a:a:a:0:db8:0:1", "only single zeros scattered - no compression"),
]
)
func testIPv6StringRepresentation(input: String, expected: String, description: String) throws {
let addr = try IPv6Address.parse(input)
#expect(
addr.description == expected,
"Expected '\(expected)' but got '\(addr.description)' for input: '\(input)' (\(description))"
)
}
// MARK: - isUnspecified Tests
@Test(
"isUnspecified - RFC 4291 Section 2.5.2",
arguments: [
("::", true, "unspecified address (short form)"),
("0:0:0:0:0:0:0:0", true, "unspecified address (full form)"),
(IPv6Address.unspecified.description, true, "unspecified"),
("::1", false, "loopback"),
("0:0:0:0:0:0:0:1", false, "loopback (full form)"),
("fe80::1", false, "link-local"),
("2001:db8::1", false, "global unicast"),
]
)
func testIsUnspecified(address: String, expected: Bool, description: String) throws {
let addr = try IPv6Address.parse(address)
#expect(
addr.isUnspecified == expected,
"Address \(address) (\(description)) should\(expected ? "" : " not") be unspecified"
)
}
// MARK: - isLoopback Tests
@Test(
"isLoopback - RFC 4291 Section 2.5.3",
arguments: [
("::1", true, "loopback (short form)"),
("0:0:0:0:0:0:0:1", true, "loopback (full form)"),
(IPv6Address.loopback.description, true, "loopback var"),
("::", false, "unspecified"),
("::2", false, "not loopback"),
("0:0:0:0:0:0:0:2", false, "not loopback (full form)"),
("fe80::1", false, "link-local"),
("2001:db8::1", false, "global unicast"),
]
)
func testIsLoopback(addressString: String, expected: Bool, description: String) throws {
let addr = try IPv6Address.parse(addressString)
#expect(
addr.isLoopback == expected,
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be loopback"
)
}
// MARK: - isMulticast Tests
@Test(
"isMulticast - RFC 4291 Section 2.7",
arguments: [
// Positive cases - all multicast addresses start with ff
("ff00::1", true, "Reserved multicast"),
("ff01::1", true, "Interface-local multicast"),
("ff02::1", true, "Link-local multicast (all nodes)"),
("ff02::2", true, "Link-local multicast (all routers)"),
("ff05::1", true, "Site-local multicast"),
("ff0e::1", true, "Global multicast"),
("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "Max multicast"),
// Negative cases
("::", false, "unspecified"),
("::1", false, "loopback"),
("fe80::1", false, "link-local unicast"),
("2001:db8::1", false, "global unicast"),
("fd00::1", false, "unique local"),
]
)
func testIsMulticast(addressString: String, expected: Bool, description: String) throws {
let addr = try IPv6Address.parse(addressString)
#expect(
addr.isMulticast == expected,
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be multicast"
)
}
// MARK: - isLinkLocal Tests
@Test(
"isLinkLocal - RFC 4291 Section 2.5.6",
arguments: [
// Positive cases - fe80::/10
("fe80::1", true, "basic link-local"),
("fe80::dead:beef", true, "link-local with hex"),
("fe80:0:0:0:0:0:0:1", true, "link-local (full form)"),
("fe80::1234:5678:90ab:cdef", true, "link-local with interface ID"),
("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "Last address in fe80::/10"),
// Negative cases
("::", false, "unspecified"),
("::1", false, "loopback"),
("fec0::1", false, "site-local (deprecated)"),
("ff02::1", false, "multicast"),
("2001:db8::1", false, "global unicast"),
("fd00::1", false, "unique local"),
]
)
func testIsLinkLocal(addressString: String, expected: Bool, description: String) throws {
let addr = try IPv6Address.parse(addressString)
#expect(
addr.isLinkLocal == expected,
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be link-local"
)
}
// MARK: - isUniqueLocal Tests
@Test(
"isUniqueLocal - RFC 4193",
arguments: [
// Positive cases - fc00::/7 (fc00::/8 and fd00::/8)
("fc00::1", true, "fc00 unique local"),
("fc00:dead:beef::1", true, "fc00 with hex"),
("fd00::1", true, "fd00 unique local"),
("fd12:3456:789a::1", true, "fd00 with prefix"),
("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "max unique local"),
// Negative cases
("::", false, "unspecified"),
("::1", false, "loopback"),
("fe80::1", false, "link-local"),
("ff02::1", false, "multicast"),
("2001:db8::1", false, "global unicast"),
]
)
func testIsUniqueLocal(addressString: String, expected: Bool, description: String) throws {
let addr = try IPv6Address.parse(addressString)
#expect(
addr.isUniqueLocal == expected,
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be unique local"
)
}
// MARK: - isGlobalUnicast Tests
@Test(
"isGlobalUnicast - RFC 4291 Section 2.5.4",
arguments: [
// Positive cases - routable on the global internet
("2001:db8::1", true, "Documentation (but still global unicast format)"),
("2001:4860:4860::8888", true, "Google DNS"),
("2606:4700:4700::1111", true, "Cloudflare DNS"),
("2001:500::1", true, "Root DNS server"),
("2a00:1450:4001::1", true, "Google"),
// Negative cases - special addresses
("::", false, "unspecified"),
("::1", false, "loopback"),
("fe80::1", false, "link-local"),
("ff02::1", false, "multicast"),
("fc00::1", false, "unique local"),
("fd00::1", false, "unique local"),
]
)
func testIsGlobalUnicast(addressString: String, expected: Bool, description: String) throws {
let addr = try IPv6Address.parse(addressString)
#expect(
addr.isGlobalUnicast == expected,
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be global unicast"
)
}
// MARK: - isDocumentation Tests
@Test(
"isDocumentation - RFC 3849",
arguments: [
// Positive cases - 2001:db8::/32
("2001:db8::1", true, "basic documentation address"),
("2001:db8::", true, "documentation prefix"),
("2001:db8:0:0:0:0:0:1", true, "documentation (full form)"),
("2001:db8:1234:5678:90ab:cdef:1234:5678", true, "documentation with all fields"),
("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", true, "max documentation address"),
// Negative cases
("2001:db7::1", false, "Just before documentation range"),
("2001:db9::1", false, "Just after documentation range"),
("2001:4860:4860::8888", false, "Google DNS"),
("::", false, "unspecified"),
("::1", false, "loopback"),
]
)
func testIsDocumentation(addressString: String, expected: Bool, description: String) throws {
let addr = try IPv6Address.parse(addressString)
#expect(
addr.isDocumentation == expected,
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be documentation"
)
}
}
@@ -0,0 +1,131 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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 Foundation
import Testing
@testable import ContainerizationExtras
@Suite("IPv6 Mixed IPv4 Notation Tests")
struct IPv6IPv4ParsingTests {
@Test(
"Extract IPv4 suffix from various IPv6 formats",
arguments: [
("::192.168.1.1", "::", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
("::ffff:192.0.2.1", "::ffff", [UInt8(192), UInt8(0), UInt8(2), UInt8(1)]),
("fe80::192.168.1.1", "fe80::", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
("2001:db8::192.0.2.1", "2001:db8::", [UInt8(192), UInt8(0), UInt8(2), UInt8(1)]),
("0:0:0:0:0:0:192.168.1.1", "0:0:0:0:0:0", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
]
)
func testIPv4SuffixExtraction(input: String, expectedIPv6: String, expectedIPv4: [UInt8]) throws {
let result = try #require(try IPv6Address.extractIPv4Suffix(from: input))
#expect(result.0 == expectedIPv6)
#expect(result.1 == expectedIPv4)
}
@Test(
"No IPv4 suffix for pure IPv6 addresses",
arguments: [
"2001:db8::1",
"fe80::1",
"::",
"::1",
]
)
func testPureIPv6ReturnsNil(address: String) throws {
#expect(try IPv6Address.extractIPv4Suffix(from: address) == nil)
}
@Test(
"Invalid IPv4 suffix throws error",
arguments: [
"::256.1.1.1",
"::192.168.1",
"::192.168.001.1",
]
)
func testInvalidIPv4Throws(invalid: String) {
#expect(throws: IPAddressError.self) {
_ = try IPv6Address.extractIPv4Suffix(from: invalid)
}
}
@Test(
"IPv4 bytes always at positions 12-15",
arguments: [
"::192.168.1.1",
"::ffff:127.0.0.1",
"fe80::10.0.0.1",
]
)
func testIPv4BytePlacement(address: String) throws {
let parsed = try IPv6Address.parse(address)
let ipv4String = String(address.split(separator: ":").last!)
let ipv4 = try IPv4Address.parse(ipv4String)
#expect(parsed.bytes[12] == UInt8((ipv4 >> 24) & 0xFF))
#expect(parsed.bytes[13] == UInt8((ipv4 >> 16) & 0xFF))
#expect(parsed.bytes[14] == UInt8((ipv4 >> 8) & 0xFF))
#expect(parsed.bytes[15] == UInt8(ipv4 & 0xFF))
}
@Test(
"Unspecified address with IPv4 suffix",
arguments: [
("::192.168.1.1", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
("::0.0.0.1", [UInt8(0), UInt8(0), UInt8(0), UInt8(1)]),
("::255.255.255.255", [UInt8(255), UInt8(255), UInt8(255), UInt8(255)]),
]
)
func testUnspecifiedWithIPv4(address: String, ipv4: [UInt8]) throws {
let parsed = try IPv6Address.parse(address)
#expect(parsed.bytes[0..<12].allSatisfy { $0 == 0 })
#expect(Array(parsed.bytes[12..<16]) == ipv4)
}
@Test("IPv4 with zone identifier")
func testIPv4WithZone() throws {
let parsed = try IPv6Address.parse("::192.168.1.1%lo0")
#expect(parsed.zone == "lo0")
#expect(Array(parsed.bytes[12..<16]) == [192, 168, 1, 1])
}
@Test(
"IPv4-mapped addresses (::ffff:x.x.x.x)",
arguments: [
"::ffff:127.0.0.1",
"::ffff:192.168.1.1",
]
)
func testIPv4MappedAddresses(address: String) throws {
let parsed = try IPv6Address.parse(address)
#expect(parsed.bytes[0..<10].allSatisfy { $0 == 0 })
#expect(parsed.bytes[10] == 0xff && parsed.bytes[11] == 0xff)
}
@Test("Complex ellipsis with IPv4")
func testComplexEllipsisWithIPv4() throws {
let address = "2001:db8:85a3::8a2e:192.168.1.1"
let parsed = try IPv6Address.parse(address)
#expect(parsed.bytes[10] == 0x8a && parsed.bytes[11] == 0x2e)
#expect(Array(parsed.bytes[12..<16]) == [192, 168, 1, 1])
}
}
@@ -94,7 +94,7 @@ final class TestAddressAllocators {
@Test
func testipv4AddressAllocator() throws {
var allocations = Set<UInt32>()
let lower = try IPv4Address("192.168.64.1").prefix(prefixLength: 24).value
let lower = try IPv4Address("192.168.64.1").value & Prefix(length: 24)!.prefixMask32
let allocator = try IPv4Address.allocator(
lower: lower, size: 3)
allocations.insert(try allocator.allocate().value)
@@ -117,7 +117,7 @@ final class TestAddressAllocators {
@Test
func testHighestIPv4AddressAllocator() throws {
var allocations = Set<UInt32>()
let lower = try IPv4Address("255.255.255.255").prefix(prefixLength: 32).value
let lower = try IPv4Address("255.255.255.255").value & Prefix(length: 32)!.prefixMask32
let allocator = try IPv4Address.allocator(
lower: lower, size: 1)
allocations.insert(try allocator.allocate().value)
@@ -0,0 +1,155 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
//
// 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 Testing
@testable import ContainerizationExtras
struct TestPrefix {
// MARK: - IPv4 Mask Tests
struct IPv4Mask {
let length: UInt8
let expectedPrefix: UInt32
let expectedSuffix: UInt32
}
@Test(arguments: [
IPv4Mask(
length: 0,
expectedPrefix: 0x0000_0000,
expectedSuffix: 0xFFFF_FFFF
),
IPv4Mask(
length: 8,
expectedPrefix: 0xFF00_0000,
expectedSuffix: 0x00FF_FFFF
),
IPv4Mask(
length: 16,
expectedPrefix: 0xFFFF_0000,
expectedSuffix: 0x0000_FFFF
),
IPv4Mask(
length: 24,
expectedPrefix: 0xFFFF_FF00,
expectedSuffix: 0x0000_00FF
),
IPv4Mask(
length: 32,
expectedPrefix: 0xFFFF_FFFF,
expectedSuffix: 0x0000_0000
),
])
func testIPv4Masks(testCase: IPv4Mask) {
let prefix = Prefix(length: testCase.length)!
#expect(prefix.prefixMask32 == testCase.expectedPrefix)
#expect(prefix.suffixMask32 == testCase.expectedSuffix)
}
@Test func testMasksAreInverses32() {
for length in 0...32 {
let prefix = Prefix(length: UInt8(length))!
#expect(~prefix.prefixMask32 == prefix.suffixMask32)
}
}
// MARK: - IPv6 Mask Tests
struct IPv6Mask {
let length: UInt8
let expectedPrefix: UInt128
let expectedSuffix: UInt128
}
@Test func testIPv6Masks() {
let cases = [
IPv6Mask(
length: 0,
expectedPrefix: UInt128(0),
expectedSuffix: UInt128.max
),
IPv6Mask(
length: 64,
expectedPrefix: 0xFFFF_FFFF_FFFF_FFFF_0000_0000_0000_0000,
expectedSuffix: 0x0000_0000_0000_0000_FFFF_FFFF_FFFF_FFFF
),
IPv6Mask(
length: 128,
expectedPrefix: UInt128.max,
expectedSuffix: UInt128(0)
),
]
for testCase in cases {
let prefix = Prefix(length: testCase.length)!
#expect(prefix.prefixMask128 == testCase.expectedPrefix)
#expect(prefix.suffixMask128 == testCase.expectedSuffix)
}
}
@Test func testMasksAreInverses128() {
for length in stride(from: 0, through: 128, by: 8) {
let prefix = Prefix(length: UInt8(length))!
#expect(~prefix.prefixMask128 == prefix.suffixMask128)
}
}
// MARK: - Description Tests
@Test func testDescription() {
#expect(Prefix(length: 24)!.description == "24")
#expect(Prefix(length: 0)!.description == "0")
#expect(Prefix(length: 128)!.description == "128")
}
// MARK: - Validation Tests
@Test func testValidationRejectsInvalidLengths() {
// Valid ranges
#expect(Prefix(length: 0) != nil)
#expect(Prefix(length: 32) != nil)
#expect(Prefix(length: 128) != nil)
// Invalid ranges
#expect(Prefix(length: 129) == nil)
#expect(Prefix(length: 200) == nil)
#expect(Prefix(length: 255) == nil)
}
@Test func testIPv4SpecificValidation() {
// Valid IPv4 prefixes
#expect(Prefix.ipv4(0) != nil)
#expect(Prefix.ipv4(16) != nil)
#expect(Prefix.ipv4(32) != nil)
// Invalid IPv4 prefixes
#expect(Prefix.ipv4(33) == nil)
#expect(Prefix.ipv4(64) == nil)
#expect(Prefix.ipv4(128) == nil)
}
@Test func testIPv6SpecificValidation() {
// Valid IPv6 prefixes
#expect(Prefix.ipv6(0) != nil)
#expect(Prefix.ipv6(64) != nil)
#expect(Prefix.ipv6(128) != nil)
// Invalid IPv6 prefixes
#expect(Prefix.ipv6(129) == nil)
#expect(Prefix.ipv6(255) == nil)
}
}
@@ -14,6 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationExtras
import ContainerizationOS
import Testing
@@ -274,7 +275,7 @@ struct NetlinkSessionTest {
)
let session = NetlinkSession(socket: mockSocket)
try session.addressAdd(interface: "eth0", address: "192.168.64.250/24")
try session.addressAdd(interface: "eth0", ipv4Address: try CIDRv4("192.168.64.250/24"))
#expect(mockSocket.requests.count == 2)
#expect(mockSocket.responseIndex == 2)
@@ -321,8 +322,8 @@ struct NetlinkSessionTest {
let session = NetlinkSession(socket: mockSocket)
try session.routeAdd(
interface: "eth0",
destinationAddress: "192.168.64.0/24",
srcAddr: "192.168.64.3"
dstIpv4Addr: try CIDRv4("192.168.64.0/24"),
srcIpv4Addr: try IPv4Address("192.168.64.3")
)
#expect(mockSocket.requests.count == 2)
+2 -2
View File
@@ -17,8 +17,8 @@ WARNINGS_AS_ERRORS ?= true
SWIFT_WARNING_CONFIG := $(if $(filter-out false,$(WARNINGS_AS_ERRORS)),-Xswiftc -warnings-as-errors)
SWIFT_CONFIGURATION := --swift-sdk aarch64-swift-linux-musl $(SWIFT_WARNING_CONFIG)
SWIFT_VERSION := 6.2
SWIFT_SDK_URL := https://download.swift.org/swift-6.2-release/static-sdk/swift-6.2-RELEASE/swift-6.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz
SWIFT_VERSION := 6.2.3
SWIFT_SDK_URL := https://download.swift.org/swift-6.2.3-release/static-sdk/swift-6.2.3-RELEASE/swift-6.2.3-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz
SWIFT_SDK_PATH := /tmp/$(notdir $(SWIFT_SDK_URL))
SWIFTLY_URL := https://download.swift.org/swiftly/darwin/swiftly.pkg
+13 -8
View File
@@ -17,6 +17,7 @@
import Cgroup
import Containerization
import ContainerizationError
import ContainerizationExtras
import ContainerizationNetlink
import ContainerizationOCI
import ContainerizationOS
@@ -774,13 +775,14 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
"ipAddrAdd",
metadata: [
"interface": "\(request.interface)",
"address": "\(request.address)",
"ipv4Address": "\(request.ipv4Address)",
])
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
try session.addressAdd(interface: request.interface, address: request.address)
let ipv4Address = try CIDRv4(request.ipv4Address)
try session.addressAdd(interface: request.interface, ipv4Address: ipv4Address)
} catch {
log.error(
"ipAddrAdd",
@@ -800,17 +802,19 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
"ipRouteAddLink",
metadata: [
"interface": "\(request.interface)",
"address": "\(request.address)",
"srcAddr": "\(request.srcAddr)",
"dstIpv4Addr": "\(request.dstIpv4Addr)",
"srcIpv4Addr": "\(request.srcIpv4Addr)",
])
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
let dstIpv4Addr = try CIDRv4(request.dstIpv4Addr)
let srcIpv4Addr = try IPv4Address(request.srcIpv4Addr)
try session.routeAdd(
interface: request.interface,
destinationAddress: request.address,
srcAddr: request.srcAddr
dstIpv4Addr: dstIpv4Addr,
srcIpv4Addr: srcIpv4Addr
)
} catch {
log.error(
@@ -832,13 +836,14 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
"ipRouteAddDefault",
metadata: [
"interface": "\(request.interface)",
"gateway": "\(request.gateway)",
"ipv4Gateway": "\(request.ipv4Gateway)",
])
do {
let socket = try DefaultNetlinkSocket()
let session = NetlinkSession(socket: socket, log: log)
try session.routeAddDefault(interface: request.interface, gateway: request.gateway)
let ipv4Gateway = try IPv4Address(request.ipv4Gateway)
try session.routeAddDefault(interface: request.interface, ipv4Gateway: ipv4Gateway)
} catch {
log.error(
"ipRouteAddDefault",