Requirements for IP address and port APIs

Like @martinlau mentioned, I’d like to nominate swift-endpoint.

swift-endpoint API is fairly complete as-is, but it will end up needing some adjustments. There are some things we'll need to discuss and some decisions to be made. I can explain further if needed.

Click for discussion
  • Should there be a Domain module at all? Like mentioned in the original post.
  • Domain module depends on ByteBuffer.
    • [UInt8] can do fine, but it'll be a performance hit for swift-nio-dependant libraries which will end up copying a ByteBuffer slice into a new [UInt8] array.
    • There is a currently-unused trait to switch between [UInt8] and ByteBuffer. That's also one way to go.
  • DomainName comes with IDN support for non-ascii domain names. It costs a bit of binary size due to the vendored IDNA mappings table, and soon NFC tables (like 200KB I'd say off-hand, not sure). There could possibly be a trait to disable it since IDNs are extremely rare (top 50K Cloudflare domains only has 13 of them).
  • What availability we'd want this package to have?
    • Big question is macOS 15. The package currently contains an UnsignedInteger128 type consisting of 2 UInt64s because of UInt128's requirement of macOS 15.
    • Dropping macOS 15 might be a big hit on adoption in existing packages.
    • UnsignedInteger128 is almost identical to UInt128, API-wise and behavior wise (there are tests, as well as crash-tests aka exit-tests to ensure). It has a few hand-written impls that I know can be made more performant, but for now my policy has been to let's just discourage using UnsignedInt128; hence the relatively ugly name. Realistically, anyone can get the performance by simply doing UInt128(myUnsignedInt128) before operating on the value. The performance is not bad to be clear but yeah.
  • unixDomainSocketAddress is waiting for the FilePath type. Theoretically we could try to hide the impl and provide String APIs for now.
  • IPv8 looks to be imminent. Should we at least have stubs for it? since an enum like AnyIPAddress is a "frozen" enum (not in the ABI-stability meaning of it), not a non-exhaustive one, and we can't add a new case to it without a semver-major bump in the future.
  • I do recognize IPv8's adoption around the world will take a few years at best anyway, but considering a foundational package like swift-endpoint won't be able to do major releases easily, we should at least plan for it (a major bump might end up breaking too many user codes that breaking changes even in a major bump would be hard to justify, see swift-nio for example).

Furthermore:

  • I plan to add fuzz-testing to swift-endpoint as well soon.
    • The package is pretty complete on having unit tests, but in such a fundamental package we can't afford any risks.
    • The package does also use a fair bit of "unsafe" / "unchecked" constructs for performance reasons, so that's another reason. Although again as far as I can tell there are no security holes, but well, that doesn't mean there are actually no security holes, just that I can see none.
  • The package contains a pretty custom benchmark CI so we'll need to see about that if it is to be moved to swiftlang. Currently it uses a custom fork of ordo-one/benchmark as well.

Design

Module Contents
IPAddress IPv4Address, IPv6Address, AnyIPAddress, CIDR, UnsignedInteger128
Domain DomainName, DomainName.Label
DomainIPAddressCompat DomainName ⇄ IP address conversions
Endpoint ConnectionTarget, Port + exports all of the above

API surface

Here are all the public APIs (LLM generated, with my edits where needed).

Click to show

Assume big-endian order unless explicitly specified.

IPv4Address

A 32-bit IPv4 address stored as a UInt32.

public struct IPv4Address: Sendable, Hashable {
    public var address: UInt32
}
Click for IPv4Address full API
extension IPv4Address {
    public static var size: Int { get }        // 4

    /// big-endian order
    public var bytes: (UInt8, UInt8, UInt8, UInt8) { get }

    public init(_: UInt32)
    /// big-endian order
    public init(_: UInt8, _: UInt8, _: UInt8, _: UInt8)

    // RFC-classified predicates
    public var isLoopback: Bool { get }
    public var isMulticast: Bool { get }
    public var isLinkLocal: Bool { get }
    public var isUnspecified: Bool { get }
    public var isBroadcast: Bool { get }
    public var isPrivate: Bool { get }
    public var isShared: Bool { get }
    public var isDocumentation: Bool { get }
}

extension IPv4Address: ExpressibleByIntegerLiteral {
    public init(integerLiteral: UInt32)
}

extension IPv4Address: CustomStringConvertible, LosslessStringConvertible {
    public var description: String { get }      // "127.0.0.1"
    public init?(_: String)
    public init?(_: Substring)
}

extension IPv4Address {
    // Allocation-free span parsing/serialization
    public init?(parsing: Span<UInt8>)
    public func serialize(into: inout OutputSpan<UInt8>) -> Bool

    @available(SwiftStdlib 6.2, *)
    public init?(textualRepresentation: UTF8Span)
    public init?(textualRepresentation: Span<UInt8>)

    // C interop
    public init?(cString: UnsafePointer<CChar>)
    public func withCString<Result>(_: (Span<CChar>) throws -> Result) rethrows -> Result

    // Cross-type conversions
    public init?(exactly: AnyIPAddress)
    public init?(ipv6: IPv6Address)             // extracts IPv4-mapped IPv6
}

IPv6Address

A 128-bit IPv6 address. Serialization follows RFC 5952 canonical form; parsing accepts bracketed forms and RFC 4291 IPv4-mapped addresses.

public struct IPv6Address: Sendable, Hashable {
    public var address: UnsignedInteger128
}
Click for IPv6Address full API
extension IPv6Address {
    public static var size: Int { get }         // 16

    public var bytes: (UInt8, /* … 16 total … */ UInt8) { get }
    public var segments: (UInt16, /* … 8 total … */ UInt16) { get }

    public init(_: UnsignedInteger128)
    public init(_: UInt16, /* … 8 UInt16 segments … */ _: UInt16)
    public init(_: UInt8, /* … 16 UInt8 bytes … */ _: UInt8)

    @available(SwiftStdlib 6.0, *)
    @_disfavoredOverload public init(_: UInt128)

    // RFC-classified predicates
    public var isLoopback: Bool { get }
    public var isMulticast: Bool { get }
    public var isLinkLocalUnicast: Bool { get }
    public var isUnspecified: Bool { get }
    public var isUniqueLocal: Bool { get }
    public var isDocumentation: Bool { get }
}

@available(SwiftStdlib 6.0, *)
extension IPv6Address: ExpressibleByIntegerLiteral {
    public init(integerLiteral: UInt128)
}

extension IPv6Address: CustomStringConvertible, LosslessStringConvertible {
    public var description: String { get }       // "[2001:db8:85a3::100]"
    public init?(_: String)
    public init?(_: Substring)
}

extension IPv6Address {
    public init?(parsing: Span<UInt8>)
    public func serialize(into: inout OutputSpan<UInt8>) -> Bool

    @available(SwiftStdlib 6.2, *)
    public init?(textualRepresentation: UTF8Span)
    public init?(textualRepresentation: Span<UInt8>)

    public init?(cString: UnsafePointer<CChar>)
    public func withCString<Result>(_: (Span<CChar>) throws -> Result) rethrows -> Result

    public init?(exactly: AnyIPAddress)
    public init(ipv4: IPv4Address)               // IPv4-mapped IPv6
}

AnyIPAddress

A tagged union of the two address families that auto-detects on parse.

public enum AnyIPAddress: Sendable, Hashable {
    case v4(IPv4Address)
    case v6(IPv6Address)
}
Click for AnyIPAddress full API
extension AnyIPAddress {
    public var ipv4Value: IPv4Address? { get }
    public var ipv6Value: IPv6Address? { get }
    public var isIPv4: Bool { get }
    public var isIPv6: Bool { get }

    public var isLoopback: Bool { get }
    public var isMulticast: Bool { get }
    public var isContiguous: Bool { get }
}

extension AnyIPAddress: CustomStringConvertible, LosslessStringConvertible {
    public var description: String { get }
    public init?(_: String)
    public init?(_: Substring)
}

extension AnyIPAddress {
    @available(SwiftStdlib 6.2, *)
    public init?(textualRepresentation: UTF8Span)
    public init?(textualRepresentation: Span<UInt8>)

    public init?(cString: UnsafePointer<CChar>)
    public func withCString<Result>(_: (Span<CChar>) throws -> Result) rethrows -> Result
}

CIDR

A generic CIDR block over either address family, storing a prefix and derived mask.

public struct CIDR<IPAddressType: _IPAddressProtocol>: Sendable {
    public let prefix: IPAddressType
    public let mask: IPAddressType
}
Click for CIDR full API
extension CIDR {
    public typealias AddressValueType = IPAddressType.AddressValueType

    public var prefixLength: Int { get }
    public var networkAddress: IPAddressType { get }

    public init(prefix: IPAddressType, prefixLength: Int)
    public init?(prefix: IPAddressType, mask: IPAddressType)
    public init(prefix: IPAddressType, uncheckedMask: IPAddressType)

    public func contains(_: IPAddressType) -> Bool
    public func contains(_: AnyIPAddress) -> Bool
}

extension CIDR: Hashable {}

extension CIDR: CustomStringConvertible, LosslessStringConvertible {
    public var description: String { get }        // "127.0.0.0/8"
    public init?(_: String)
    public init?(_: Substring)
}

// Well-known blocks
extension CIDR<IPv4Address> {
    public static var loopback: Self { get }      // 127.0.0.0/8
    public static var multicast: Self { get }
    public static var linkLocal: Self { get }
    public static var broadcast: Self { get }
    public static var shared: Self { get }
    public static var unspecified: Self { get }
}

extension CIDR<IPv6Address> {
    public static var loopback: Self { get }      // ::1/128
    public static var multicast: Self { get }
    public static var linkLocalUnicast: Self { get }
    public static var ipv4Mapped: Self { get }
    public static var uniqueLocal: Self { get }
    public static var documentation: Self { get }
    public static var unspecified: Self { get }
}

UnsignedInteger128

A backport shim used as IPv6Address.address on toolchains without UInt128. It provides the full fixed-width-integer surface and interoperates with UInt128 where available.

public struct UnsignedInteger128: Sendable, Hashable, BitwiseCopyable {
    public init(_low: UInt64, _high: UInt64)
    // Conforms to: Comparable, AdditiveArithmetic, Numeric, BinaryInteger,
    //              FixedWidthInteger, UnsignedInteger, Strideable, Codable,
    //              CustomStringConvertible, ExpressibleByIntegerLiteral (SwiftStdlib 6.0+)
}

DomainName

A DNS-wire-format domain name with Unicode-17 IDNA support. Iterating yields labels.

public struct DomainName: Sendable, Hashable, Sequence,
                          CustomStringConvertible, CustomDebugStringConvertible {
    public var isFQDN: Bool
}
Click for DomainName full API
extension DomainName {
    public static var maxLength: UInt8 { get }        // 255
    public static var maxLabelLength: UInt8 { get }   // 63
    public static var root: Self { get }

    public var encodedLength: Int { get }
    public var labelsCount: Int { get }
    public var isRoot: Bool { get }
    public var isWildcard: Bool { get }

    @available(SwiftStdlib 5.1, *)
    public init(_: String, idnaConfiguration: IDNA.Configuration = .default) throws
    @available(SwiftStdlib 5.1, *)
    public init(_: Substring, idnaConfiguration: IDNA.Configuration = .default) throws
    @available(SwiftStdlib 6.2, *)
    public init(textualRepresentation: UTF8Span,
                idnaConfiguration: IDNA.Configuration = .default) throws

    public var description: String { get }            // Unicode, e.g. "新华网.中国"
    public var debugDescription: String { get }       // A-label, e.g. "xn--xkrr14bows.xn--fiqs8s"
    @available(SwiftStdlib 5.1, *)
    public func description(format: DescriptionFormat, options: DescriptionOptions = []) -> String

    // Containment
    public func isSubdomain(of: DomainName) -> Bool
    public func isStrictSubdomain(of: DomainName) -> Bool
    public func isSuperdomain(of: DomainName) -> Bool
    public func isStrictSuperdomain(of: DomainName) -> Bool

    public func isExactlyEqual(to: Self) -> Bool         // also compares isFQDN
    public func makeIterator() -> Iterator
}

extension DomainName {
    @nonexhaustive public enum DescriptionFormat: Sendable { case ascii, unicode }
    public struct DescriptionOptions: OptionSet, Sendable {
        public static var includeRootLabelIndicator: Self { get }
    }
    @nonexhaustive public enum ValidationError: Error { /* … */ }
    public struct Iterator: Sendable, IteratorProtocol { public mutating func next() -> Label? }

    public struct Label: Sendable, Hashable,
                         CustomStringConvertible, CustomDebugStringConvertible {
        public var description: String { get }
        public var debugDescription: String { get }
        @available(SwiftStdlib 5.1, *)
        public func description(format: DomainName.DescriptionFormat) -> String
    }
}

DomainName ⇄ IP address conversions (DomainIPAddressCompat)

Fast, string-free conversions between domain names and IP addresses (dotted-quad and .arpa reverse-DNS forms).

Click for conversion full API
@available(SwiftStdlib 5.1, *)
extension IPv4Address {
    public init?(domainName: DomainName)          // dotted-quad or .arpa
    public init?(arpaDomainName: DomainName)
}

@available(SwiftStdlib 5.1, *)
extension IPv6Address {
    public init?(domainName: DomainName)          // .arpa
    public init?(arpaDomainName: DomainName)
}

@available(SwiftStdlib 5.1, *)
extension AnyIPAddress {
    public init?(domainName: DomainName)
    public init?(arpaDomainName: DomainName)
}

extension DomainName {
    @nonexhaustive public enum IPv4AddressInDomainNameFormatting: Sendable { case dottedQuad, arpa }
    public init(ipv4: IPv4Address, format: IPv4AddressInDomainNameFormatting = .arpa)
    @available(SwiftStdlib 5.1, *) public init(ipv6: IPv6Address)   // .arpa
    @available(SwiftStdlib 5.1, *) public init(ip: AnyIPAddress)    // .arpa
}

Port

public struct Port: Sendable, Hashable {
    public let canonicalValue: UInt16
}
Click for Port full API
extension Port {
    public var value: Int { get }

    public init(canonicalValue: UInt16)
    public init(_: Int)
}

extension Port: ExpressibleByIntegerLiteral { public init(integerLiteral: UInt16) }
extension Port: CustomStringConvertible { public var description: String { get } }

ConnectionTarget

The top-level endpoint: an IP + port, a domain name + port, or a Unix domain socket path.

@available(SwiftStdlib 5.1, *)
public struct ConnectionTarget: Sendable, Hashable, CustomStringConvertible {
    public private(set) var target: Target

    @nonexhaustive public enum Target: Sendable, Hashable, CustomStringConvertible {
        case ipAddress(AnyIPAddress, port: Port)
        case domainName(DomainName, port: Port)
        case unixDomainSocketAddress(String)
    }
}
Click for ConnectionTarget full API
@available(SwiftStdlib 5.1, *)
extension ConnectionTarget {
    public static func ipAddress(_: String, port: Port) throws(Error) -> Self
    public static func ipAddress(_: AnyIPAddress, port: Port) -> Self
    public static func ipAddress(_: IPv4Address, port: Port) -> Self
    public static func ipAddress(_: IPv6Address, port: Port) -> Self

    public static func domainName(_: String, port: Port,
                                  idnaConfiguration: IDNA.Configuration = .default) throws -> Self
    public static func domainName(_: DomainName, port: Port) -> Self

    public static func unixDomainSocketAddress(_: String) -> Self

    public var description: String { get }

    @nonexhaustive public enum Error: Swift.Error, CustomStringConvertible {
        case invalidIPAddressString(String)
        case failedToParseDomainName(any Swift.Error)
    }
}

Performance

This is a copy of the current README performance section:

Click for Performance overview

Performance

  • Below are benchmarks of this library against inet C-library APIs of macOS's Darwin and Linux's glibc.
  • In all cases, swift-endpoint wins against the inet C APIs.
  • These benchmarks are meant to represent a slow-case scenario of real-world workloads.
  • The C API benchmarks represent a C language user's experience. Meaning that they don't contain any overhead coming from interfacing with Swift.
    • For example converting C character-strings to Strings for ip-address->string conversions. Specially if the character-string is over 15 bytes of length, which would force String to incur a heap allocation.
    • To give you an idea: in a simple usage of C APIs in Swift, C API results for serialization can be up to 3 times slower due to heap-allocated String. For parsing the overhead can be marginal, or up to 50% higher.
    • The swift-endpoint API benchmarks go through those overheads anyway, such as the String's heap-allocation, but they still manage to beat the C API benchmarks.
  • Each benchmark runs against 16 different IPs one by one in a random manner, via a constant seed to keep the benchmarks consistent across benchmark runs.
    • This means CPUs won't find a clear pattern to over-optimize for in any of the operations, which would make the benchmarks less realistic.

Against Darwin

These were performed on my M1 Pro MacBook, on macOS 27.

IP Type Operation Swift (ns/op) inet (ns/op) Speedup
IPv4 Serializing 16.1 176.0 10.93x
IPv4 Parsing 14.4 45.8 3.18x
IPv6 Serializing 84.0 237.0 2.82x
IPv6 Parsing 29.3 96.5 3.29x

Against glibc

These were performed on a dedicated-cpu-core machine from Hetzner, on Ubuntu 24.04.

IP Type Operation Swift (ns/op) inet (ns/op) Speedup
IPv4 Serializing 20.0 100.0 5.00x
IPv4 Parsing 17.0 25.0 1.47x
IPv6 Serializing 70.0 160.0 2.29x
IPv6 Parsing 32.5 40.0 1.23x

Additional Notes

Click for Additional Notes
  • To see up to date information about performance of this package, please go to this benchmarks list, and choose the most recent benchmark. You'll see a summary of the benchmark there.
  • The results above are all reproducible by simply running scripts/benchmark.sh on a machine of your own.
  • It's worth noting that swift-endpoint APIs win in pretty much any other situation as well, as visible in the benchmarks.
    • For example even if you run a benchmark over only 1 IP so CPUs can over-optimize for the specific IP's case and run it as fast as possible. This might even widen the speed gap and be advantageous to swift-endpoint APIs.
    • This is to say the above tables are not an over-representation of this library's capabilities.
3 Likes