Requirements for IP address and port APIs

Hello!

One of the first tasks the Networking Workgroup is taking on is defining standard Swift APIs for networking currency types, specifically around IP addresses and ports. IP addresses and ports are foundational to many networking libraries, but don't yet have a common, standard implementation. The goal of this post is to request community input on the requirements for standard APIs in this space, and to gather examples of existing implementations that can be used as starting points and inspiration.

IP addresses and ports might end up being packaged with other currency types (hostnames, network endpoints, etc.), but we'd like to start with requirements for the basic address and port types.

As currency types, IP addresses and ports are needed in many contexts and many layers: clients, servers, middleware, high-performance datapaths, configuration, metrics, logging, and more. Please share the requirements you see for these APIs, and different ways you would expect to interact with IP addresses and ports.

For example, some use cases might need to have memory ownership of an IP address, while other use cases might only need to interact with IP address as views into memory. IP addresses might need to be instantiated from or converted into different types, and there are a number of properties that can be queried about addresses and ports.

One of the goals of this effort is to ensure that the common API for these currency types is highly performant, and works cleanly with modern Swift concepts like Span.

There's a lot of existing work in this space, so please also share examples of APIs you're aware of or have worked on that provide definitions of IP addresses and ports in Swift. We want to learn what aspects of those APIs are most useful, what has worked well, and what you might like to change.

16 Likes

It's great to see the Networking Workgroup starting to address the requirements in this area. In this context, I’d like to highly recommend @MahdiBM’s work:

However, neither this library nor similar libraries by other authors have deep integration with Swift's native networking stacks (such as URLSession and SwiftNIO). As a result, I currently have to write custom high-level wrappers to bridge them together and make them work seamlessly.

Once the Networking Workgroup takes on the development of standard APIs in this area, it will significantly boost Swift's overall competitiveness and ecosystem cohesion. Looking forward to seeing how this evolves!

4 Likes

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

Requirements for IP address and port APIs

Thanks for opening this requirements discussion.

One requirement I would make explicit here is platform scope. The accepted Vision for Networking in Swift names Apple platforms, Linux, Android, FreeBSD, Windows, WebAssembly, and embedded Swift, and says networking improvements should work everywhere Swift does.

The IP address and port work should inherit that requirement explicitly. These should be platform-independent currency types whose semantics are not shaped by Darwin, POSIX, Network.framework, URLSession, or SwiftNIO. Platform and framework integration should be provided through adapters around the common types.

“Excellent everywhere” should mean both platform coverage and semantic correctness. An API can compile everywhere and still be incomplete for network-infrastructure work if it models only end-host endpoints.

My main requirements are:

  1. The types must be explicitly cross-platform.
  2. IPAddress and Port must remain independent of any particular networking stack.
  3. Their design must support first-class PrefixLength, IPNetwork, and IPEndpoint types without requiring a second foundation.
  4. Existing implementations such as swift-cidr should be studied as working prior art.

The Vision for Networking in Swift is also what inspired me to start building swift-cidr. Its call for shared, modular cross-platform currency types highlighted a need for foundational IP and CIDR types that could serve not only clients and servers, but network infrastructure and control-plane software. swift-cidr is my attempt to explore that part of the vision in working code.

Disclosure: I am the author of RouteObjects/swift-cidr. In addition to the implementations already mentioned in this thread, I would like to offer it as substantial prior art—particularly for the semantic model and layering boundaries.

Put more simply, an IP address and an IP network answer different questions. 192.0.2.77 identifies an individual address. 192.0.2.0/24 identifies a canonical block containing 256 addresses—something that can be allocated, subdivided, routed, filtered, summarized, or referenced by policy. A port identifies a transport-layer number, and an endpoint combines that port with an address.

If the standard model stops at individual addresses and ports, every routing, firewall, IPAM, policy, and infrastructure library will still need to invent its own incompatible representation of networks and prefixes. That would solve endpoint interoperability while leaving the network-infrastructure problem unresolved.

A small network-types manifesto

Although this effort is beginning with IPAddress and Port, those types should not be designed in isolation from the layers that will be built on them.

For Swift on Server, and for Swift as a network-infrastructure language, the same types need to travel from the network core to the edge: routing and control-plane systems, policy and validation, configuration and IPAM, high-performance datapaths, servers and middleware, observability, tools, and applications.

This is where the full meaning of CIDR matters.

RFC 4632 is not merely a specification for parsing text containing a slash. Its title is Classless Inter-domain Routing: The Internet Address Assignment and Aggregation Plan:

  • Classless means that a network boundary is represented by an explicit prefix length rather than inferred from the former IPv4 Class A/B/C divisions. For IPv4, this is the variable-length prefix model associated with VLSM. IPv6 was designed around prefixes from the outset: its routing architecture supports prefix lengths from /0 through /128, although /64 is conventional for subnets using SLAAC. In both families, prefixes provide boundaries for routing and aggregation.
  • Inter-domain places these values in the context of independently administered networks, commonly Autonomous Systems, with different operational and policy boundaries.
  • Routing makes prefixes and their aggregation Layer 3 control-plane concepts, not merely presentation formats for host addresses.

A type that can parse 192.0.2.0/24 has not necessarily modeled CIDR. The important part is whether the resulting types preserve the relevant invariants and enable network operations.

At minimum, the broader type family should be able to express:

  • IPAddress, with explicit IPv4 and IPv6 family semantics.
  • PrefixLength, representing a family-valid number of contiguous leading prefix bits rather than an unconstrained integer.
  • IPNetwork, as a first-class canonical prefix boundary with host bits normalized.
  • AddressFamily, carrying the storage width and family identity needed by generic algorithms.
  • Port, as a 16-bit transport-layer number, independent of service-name registries or a particular transport implementation.
  • IPEndpoint, composing an IP address and port without baking TCP, UDP, a socket API, or an I/O framework into the value.

swift-cidr implements this as a family-bound model. Its IPAddress<Family> is an address-shaped CIDR value, IPNetwork<Family> is a canonical aligned prefix, and PrefixLength<Family> prevents an IPv4 prefix length from being used as an IPv6 prefix length. IPNetwork then supports containment, subnet traversal, and summarization as operations on the type—not as string-processing utilities.

Its Port and IPEndpoint types demonstrate another useful boundary: an endpoint is IPAddress + Port; choosing TCP, UDP, QUIC, a socket API, or an I/O backend belongs above that currency type.

The package keeps its core CIDR module pure Swift. POSIX interoperability is isolated in CIDRPOSIX, and SwiftNIO interoperability is isolated in CIDRNIO. That is the architecture I believe standard currency types need: one semantic model shared across platforms and networking stacks, with conversions at the edges. The same separation will remain important as work such as swift-network-evolution develops concrete protocol and transport implementations.

The current public swift-cidr validation covers Apple platforms and Linux, while the core is intentionally designed to be platform-neutral. The standardization effort should target the complete platform list in the accepted networking vision.

There are already small examples in the RouteObjects organization:

  • cidrwalk demonstrates the semantic distinction between address-range and whole-network summarization.
  • swift-cidr-admission uses the same address and network values for framework-neutral allow/deny policy, with adapters and examples for SwiftNIO-based servers.

More infrastructure-oriented examples and packages are in development and will be published there.

I am not suggesting that swift-cidr must be adopted wholesale, or that it already answers every owned-memory, view, and Span requirement raised in the original post. Those are important requirements for a standardized implementation. I am suggesting that it be studied as concrete prior art for the semantic model, type invariants, network mathematics, and separation between core currency types and platform adapters.

Getting IPAddress and Port right is the immediate task. Getting them right also means ensuring that first-class networks, prefixes, endpoints, routing policy, and infrastructure layers can be built on them without inventing a second incompatible foundation.

5 Likes

As input from the Apple client side, I’ll share some of the prior art in this space, the functionality we need to keep, and some of the future directions we’d like to see from addresses and ports.

The original Swift API for addresses and ports in Network.framework came with iOS 12 in 2018:
Port: NWEndpoint.Port | Apple Developer Documentation
IPv4: IPv4Address | Apple Developer Documentation
IPv6: IPv6Address | Apple Developer Documentation

swift-network-evolution has some similar approaches, but different details. The intent here is to replace these with the standard types we’ll come up with as a group.
IPv4: swift-network-evolution/Sources/SwiftNetwork/Endpoint/IPv4Address.swift at main · apple/swift-network-evolution · GitHub
IPv6: swift-network-evolution/Sources/SwiftNetwork/Endpoint/IPv6Address.swift at main · apple/swift-network-evolution · GitHub

In addition to IP addresses, we have coverage for things like MAC/Ethernet addresses. Sharing this as an example of a slightly different approach.

While we definitely want to move to use a standard type, we do want to ensure that the standard type still allows for easy access to the properties we use today in the stack. (1/2)

Following up on the last post, here are the requirements we've identified.

Addresses

Basic type requirements:
We assume that the basic address types will be structs that areHashableandSendable, and own their backing storage. See below for discussion on view-only access.

Initialization requirements:

  • Create addresses from buffer ([UInt8] ), or create from a Span derived from that buffer. Creating from Span is likely preferred. It is assumed that these forms are in network byte order.
  • Create addresses from the raw integer types that are used in the C sockaddr structs — UInt32 for IPv4 and (UInt32, UInt32, UInt32, UInt32) for IPv6. Note that using these types can raise some byte ordering concerns.
  • Create from a String , to parse an address from a string. Note that parsing needs to handle subnets as well (see below).
  • Also specify interface scope during initialization (for IPv6 link local, etc)

Interface-scoped addressing is one slightly tricky point that I haven’t seen brought up in the other examples. Supporting this requires being able to associate the address with a particular interface index/name. Supporting this implies that there is at least a basic type for Interface in the currency types. This also impacts string parsing and generation (interface scopes use%at the end of the address).

As mentioned above, we also have anEthernetAddresstype, which isn’t strictly an IP address, but has many similar implementation considerations. Having an 6-byte long type is a useful thing to consider in the generalized shape of these objects.

Queryable property requirements:

String description needs to be queryable.

IPv4: isBroadcast, isLoopback, isLinkLocal, isSiteLocal, isLocalGroup, isZeroNet, isMulticast, isInLoopbackRange, isDSLite, is6to4RelayAnycast, isPrivateUse, isSharedAddressSpace

IPv6: isLoopback, is6to4, isIPv4Mapped, isIPv4Mapped, isScopeLinkLocal, isMulticastLinkLocal, isMulticastInterfaceLocal, isScopeEmbedded, isSiteLocal, isUniqueLocal, isUnspecified, multicastScope, multicastFlags, isMulticast

Additionally, there should be some type that indicates an address family, or an enum that holds either v4 or v6, etc.

Static addresses:

As conveniences, provide static instances for theany,broadcast, andloopbackaddresses.

CIDR -Like in swift-network-evolution ( swift-network-evolution/Sources/SwiftNetwork/Utilities/IPAddress+CIDR.swift at main · apple/swift-network-evolution · GitHub ), we’d want to include CIDR parsing support for both IPv4 and IPv6 addresses that can take a CIDR string and produce a network address paired with its subnet mask. Additionally, we’d want the ability to check whether a given address is contained within that subnet, and falls back to domain pattern matching when the pattern isn’t a CIDR string.

NAT64 -Similar to what is done in swift-network-evolution ( swift-network-evolution/Sources/SwiftNetwork/Utilities/NAT64.swift at main · apple/swift-network-evolution · GitHub ), we’d like to include a NAT64 Prefix type that holds an IPv6 address paired with its length, and have the ability to embed an IPv4 address into an IPv6 address and extract it back out using the specified NAT64Prefix. We’d utilize the queryable property requirements above to make sure we don’t synthesize addresses that aren’t meant to be synthesized.

Ports

We assume that the port type will be struct that isHashableandSendable, and own its backing storage. The expected storage would be aUInt16. Ports have more complex byte-ordering considerations than addresses, since they need to be handled in packets in network byte order, but are usually dealt with in host byte order in applications.

Initialization requirements:

  • Create from a UInt16, with clear indication of byte ordering
  • Create from a String of the port, like "443" ; this has the nice side effect of avoiding byte ordering confusion.

Similarly, access to the port raw value needs to be clear about byte ordering. It also must be possible to access the string representation of the port.

It’s also useful to have a way to access static well-known ports, like done here: NWEndpoint.Port | Apple Developer Documentation . There are various ways this could be spelled, but can include enums, mapping from URL scheme strings into ports, etc.

Design Questions

What is the backing storage for addresses?

In the past, we’ve seen usage ofUInt32, tuples ofUInt32, orUInt128for the IPv4 and IPv6 addresses. However, we can also consider modeling addresses as being backed byInlineArray[4 of UInt8], or[16 of UInt8](and[6 of UInt8]for Ethernet addresses). Having an array of bytes with easy access to particular octets seems more correct conceptually. However, there are pros and cons.

Integer types are automaticallyHashable, available far back in Swift, and match C struct types. However, they are clumsy/inefficient forSpanaccess, introduce potential byte ordering confusion, and are harder to access on a per-octet level.

Inline arrays automatically supportSpanaccess, provide per-octet access, and more correctly model the concept of an address. However, they aren’t available in older Swift versions, and don’t natively supportHashableyet.

Hashableconformance for inline arrays is being discussed here ( Conform InlineArray to Hashable ) so with that added, we’d like to propose using inline arrays for addresses.

Should address properties and hashes be accessible without ownership?

Most of the properties and hashability of addresses are typically modeled as functions or computed variables on the owning struct. However, it might be useful to define most of these functions on a type that is just aviewof an IP address. This would allow querying properties on a span of bytes within a packet that represent an IP address.

For example, consider a model where there is anIPv6Addressstruct that owns the bytes for the address. There could be another non-escapable lifetime-constrained type,IPv6AddressVieworIPv6AddressPropertiesthat provides computed variables for properties about the address, and also offersHashableconformance. TheIPv6AddressViewcould be accessed on an ownedIPv6Address, or initialized from aSpanof 16 bytes.

It’s not clear if this model is necessary, but if we think it will be useful, it might be nice to put it in from the start.

How should interface scopes be represented?

(See discussion above) (2/2)

3 Likes

Is there a possibility of by order confusion in .init(0x1234) ?

To compare these requirements to swift-endpoint:

Makes sense, and is what swift-endpoint is already doing.

  • Create addresses from buffer ([UInt8] ), or create from a Span derived from that buffer. Creating from Span is likely preferred. It is assumed that these forms are in network byte order.

swift-endpoint provides init?(parsing span: Span<UInt8>) and func serialize(into span: inout OutputSpan<UInt8>) -> Bool /*success or failure of serialization*/ on IPAddress types (For clarity, I'm intentionally showing the func definitions instead of just the func signatures).
These work with network byte-order bytes. To be clear, these are NOT for "presentation" parsing. Just for "network" parsing, (where "presentation" and "network" refer to what C APIs like inet_pton refer to, where "pton" is short for "presentation to network").
API naming bikesheddings are welcome and will likely happen later if/when the library is proposed in a formal pitch / proposal.

  • Create addresses from the raw integer types that are used in the C sockaddr structs — UInt32 for IPv4 and (UInt32, UInt32, UInt32, UInt32) for IPv6. Note that using these types can raise some byte ordering concerns.

For reference, sockaddr is defined as:

struct sockaddr {
    sa_family_t     sa_family;      /* Address family */
    char            sa_data[];      /* Socket address */
};

Currently swift-endpoint doesn't expose any actual C types in APIs (or otherwise), which is by design, although we could make exemptions for certain widely-used types such as sockaddr.
This means there is no API in swift-endpoint to retrieve a sa_family_t for a given IP address type.
However, there are func withCString<Result>(_ body: (Span<CChar>) throws -> Result) rethrows -> Result and init?(cString: UnsafePointer<CChar>) to work with cStrings.
While this might look contradictory to what I just said above about "doesn't expose any actual C types", note that still no C types are exposed.

(UInt32, UInt32, UInt32, UInt32) initializer for IPv6 is not provided in swift-endpoint, as I saw no significance in having them. They could be provided though for completeness.
UInt8 (byte) and UInt16 (segment) initializers are provided, as well as UInt128 for the whole value.

  • Create from a String , to parse an address from a string. Note that parsing needs to handle subnets as well (see below).

swift-endpoint not only includes such parsing/encoding impls, it also provides those with what all the AI companies would like to call "frontier" performance.
For parsing subnets, you can use the parsing methods of the CIDR type:

The current CIDR impl behavior is as follows:

  • Parses [ip] into [ip]/32.
  • Parses [ip]/22 into the same [ip]/22 block.
  • Parses 192.168.0.77/24 into stored properties of prefix == 192.168.00.77 and mask == 255.255.255.0. Notice the insignificant bits (.77) remain.
  • For Hashable/Equatable the insignificant bits of prefix are assumed as so.
  • In CustomStringConvertible (var description: String), prefix is printed as-is.
  • For parsing, in init?(textualRepresentation span: Span<UInt8>), prefix is taken as-is.
  • You can use myCIDR.networkAddress if you need the host bits masked off.
  • You can use myCIDR.prefixLength to retrieve the prefix length as an integer.
  • Contains func contains(_ other: IPAddressType) -> Bool.
  • Also specify interface scope during initialization (for IPv6 link local, etc)

I have some stashed WIP work but this is currently not implemented in swift-endpoint as I forgot about it. Will be implemented later.

an EthernetAddress type

Layer 2 types are not included in swift-endpoint. Perhaps they could be included in a separate module, incase someone is working at layers below IP and would like to not pull in the IP types.

String description needs to be queryable.

String description of IPv4Address and CIDR<IPv4Address> are easily queryable.
String description of IPv6Address and CIDR<IPv6Address> is more complicated and one could call it not queryable for their usecase.
The IPv6 description follows RFC 5952 - A Recommendation for IPv6 Address Text Representation, which means [2001:db8::1] is printed as opposed to a queryable 2001:0db8:0000:0000:0000:0000:0000:0001.

IPv4: isBroadcast, isLoopback, isLinkLocal, isSiteLocal, isLocalGroup, isZeroNet, isMulticast, isInLoopbackRange, isDSLite, is6to4RelayAnycast, isPrivateUse, isSharedAddressSpace

IPv6: isLoopback, is6to4, isIPv4Mapped, isIPv4Mapped, isScopeLinkLocal, isMulticastLinkLocal, isMulticastInterfaceLocal, isScopeEmbedded, isSiteLocal, isUniqueLocal, isUnspecified, multicastScope, multicastFlags, isMulticast

Some of these are already included:

IPv4:

    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 }

IPv6:

    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 }

I'm not sure how far we should go and what should be our reference in terms of what CIDR blocks (and their corresponding IP properties) to add.

Additionally, there should be some type that indicates an address family, or an enum that holds either v4 or v6, etc.

AnyIPAddress sounds like would match this requirement. With the addition that we'll have to prepare for a possible v8 IP type as well.

As conveniences, provide static instances for theany,broadcast, andloopbackaddresses.

CIDR static methods are provided, but no such static methods in IP address types.
I'm open to adding such static properties, and defaulting to the first non-network address in the block, or the network address if block is only 1 IP (so 127.0.0.1 in 127.0.0.0/8, or ::1 in ::1/128).

CIDR -Like in swift-network-evolution ( swift-network-evolution/Sources/SwiftNetwork/Utilities/IPAddress+CIDR.swift at main · apple/swift-network-evolution · GitHub ), we’d want to include CIDR parsing support for both IPv4 and IPv6 addresses that can take a CIDR string and produce a network address paired with its subnet mask. Additionally, we’d want the ability to check whether a given address is contained within that subnet, and falls back to domain pattern matching when the pattern isn’t a CIDR string.

These all exist in swift-endpoint, and while I like the relative simplicity of the code in swift-network-evolution, I'd like to mention swift-endpoint implementations will certainly be more performant.

NAT64 -Similar to what is done in swift-network-evolution ( swift-network-evolution/Sources/SwiftNetwork/Utilities/NAT64.swift at main · apple/swift-network-evolution · GitHub ), we’d like to include a NAT64 Prefix type that holds an IPv6 address paired with its length, and have the ability to embed an IPv4 address into an IPv6 address and extract it back out using the specified NAT64Prefix. We’d utilize the queryable property requirements above to make sure we don’t synthesize addresses that aren’t meant to be synthesized.

NAT64 is not explicitly implemented in swift-endpoint. That could change.
However, swift-endpoint does work and parse ipv4-embedded ipv6 addresses, and already provides some conversion methods: IPv6Address.init(ipv4: IPv4Address), IPv4Address.init?(ipv6: IPv6Address).
Currently both assume "IPv4-Mapped IPv6 Address" where the trailing 32 bits of the IPv6 address is the value of the IPv4 address, and the 16 bits behind it are all set to on. Per RFC 4291 - IP Version 6 Addressing Architecture.
Perhaps the assumption for an exact "IPv4-mapped IPv6 address" should not be made and the library should be able to recognize more types of embedded IPv4 addresses in IPv6.

We assume that the port type will be struct that isHashableandSendable, and own its backing storage. The expected storage would be aUInt16. Ports have more complex byte-ordering considerations than addresses, since they need to be handled in packets in network byte order, but are usually dealt with in host byte order in applications.

Apart from API bikesheddings that one could have, this is the case in swift-endpoint (see Port.swift).

  • Create from a UInt16, with clear indication of byte ordering
  • Create from a String of the port, like "443" ; this has the nice side effect of avoiding byte ordering confusion.

Everything is assumed network-byte-order by default in swift-endpoint. Perhaps there should be more efforts to come with some kind of clear and explicit policy for byte-orderings to make sure users make as few mistakes as possible when holding the APIs.

I notice swift-endpoint's Port misses String conversion methods but they will be easy to implement. I'll take note to implement such methods.

What is the backing storage for addresses?

in swift-endpoint, the backing storages are UInt32 and UInt128 (UnsignedInteger128 to be exact) types. This allows for easy generic operations. Such as bitwise operations in CIDR blocks.

they are clumsy/inefficient forSpanaccess, introduce potential byte ordering confusion, and are harder to access on a per-octet level.

I agree with "clumsy", "harder" and "introduce potential byte ordering confusion". However, getting a span out of and integer is not an issue apart from the need to use closures.
For example:

withUnsafeBytes(of: myInteger.bigEndian) { pointer in
    let span = pointer.span
}

swift-endpoint provides methods to get byte tuples or segments (UInt16s) out of an address.
Perhaps swift-endpoint should also provide withSpan functions as well.

Inline arrays automatically supportSpanaccess, provide per-octet access, and more correctly model the concept of an address. However, they aren’t available in older Swift versions, and don’t natively supportHashableyet.

Agree. But requiring a minimum Apple platforms of 26 would simply block adoption of this package in a lot of existing packages that strictly follow SemVer and currently support Apple platforms lower than 26 (very common on server side), if they don't want to release a new major version.
This will have to be decided on in the workgroup I'd assume and see which way we'd want to take.

swift-endpoint used to use InlineArray for the backing storage before I noticed that makes the library platforms requirements too high to adopt in other libraries.

For example, consider a model where there is anIPv6Addressstruct that owns the bytes for the address. There could be another non-escapable lifetime-constrained type,IPv6AddressVieworIPv6AddressPropertiesthat provides computed variables for properties about the address, and also offersHashableconformance. TheIPv6AddressViewcould be accessed on an ownedIPv6Address, or initialized from aSpanof 16 bytes.

It’s not clear if this model is necessary, but if we think it will be useful, it might be nice to put it in from the start.

If the address types use an storage of an integer type or an InlineArray, I'd say there will be no concern of needing view types. The types will be easily and cheaply copyable. As opposed to an impl that e.g. uses Data, where there is CoW and ARC concerns with the performance.

How should interface scopes be represented?

As mentioned, scopes are not currently implemented in swift-endpoint so I have no remarks regarding this right now.

By itself, no, but in networking context and C APIs, yes.
Some C APIs require network byte-order (big endian) while integers are stored in platform byte order (basically little endian). So if you just pass a port number integer to a networking related C API, it's likely you passed a value with the wrong byte order. Or if you want to store a C API value in a Swift struct.

See swift-nio for example where they call a bunch of "bigEndian" Swift APIs where needed, to interop with C: swift-nio/Sources/NIOCore/SocketAddresses.swift at 72973283d7780ba94d69caff08c5c3c72a2dc6f7 · apple/swift-nio · GitHub

The requirements discussion so far is strong on host-facing needs (string parse, scope, classification predicates, client stacks). I want to add one framing requirement: that scope is only part of the job.

The thread’s property and “subnet/CIDR” discussion is mostly host and interface thinking. Prefix and address values show up far wider, and this is where a single CIDR-style wrapper around an address usually falls short. Concise inventory:

Host / link

  • Interface address assignment
  • Link-local + zone/scope
  • On-link subnet / point-to-point / host route
  • Loopback and router-id style addresses

Routing / control plane

  • Connected, static, IGP, and BGP destinations
  • Aggregates, defaults, more-specifics
  • RIB/FIB keys and next-hop addresses
  • FlowSpec-style prefix match components

Policy / filtering

  • Prefix-lists (exact / length-bounded / more-specific)
  • Route policy match/set
  • ACL / security-group CIDR operands
  • Source validation (e.g. expected source prefixes)

Registries / authorization

  • RIR allocations and assignments
  • IRR route / route6 objects
  • RPKI ROAs (and related resource bindings)
  • Working example: asroutes — IRR lookup by origin AS, results as canonical networks

Planning / isolation / ops

  • IPAM pools and delegations
  • VRF / tenant / VPC route entries
  • VPN NLRI (prefix plus routing-instance context)
  • Telemetry, collectors, and logging keys

That is why the focus should stay on the object model, and why Swift on Server (and multiplatform currency generally) must be in scope—not only app and interface APIs.

What that implies for standard types

  1. IPAddress and Port are necessary but not sufficient if every routing, policy, IRR, and IPAM library must invent incompatible network types on top.

  2. A canonical network / prefix type is first-class currency, not an afterthought wrapper around an address. Control-plane data (routes, IRR objects, many filters) is prefix-shaped, not “interface with a mask.”

  3. Classification helpers (isLoopback, isBroadcast, isPrivate, …) are useful on address-shaped values (host identities—including interface addresses and host match keys). They are the wrong center of design for network/route-shaped values—e.g. a route or prefix-list entry is not a broadcast-domain object, so isBroadcast on a hybrid “CIDR” type is a meaning collision.

  4. Parse and equality must respect form

    • bare a.b.c.d → address with implied host-length context (/32 / /128)
    • 192.0.2.77/24 → address + prefix context (host bits identity-bearing)
    • 192.0.2.0/24 → canonical network
    • host+prefix → network is an explicit lossy projection; not silent ==
  5. Performance, Span, and platform coverage matter—they do not replace a clear model of what the value is.

Concrete server-side implementation: asroutes is a small multiplatform CLI/library that queries an IRRd service for route/route6 objects by origin AS and returns canonical network values—not interface assignments, and not a hybrid “print as host, equal as network” blob. That is normal Swift on Server control-plane work. If the standard model only optimizes host predicates and a fuzzy subnet helper, tools like this must invent a second foundation on day one (or abuse address-shaped types as routes).

I’m not asking v1 to ship every algorithm above. I am asking that requirements treat address and network as distinct, multiplatform currency for apps and control plane, with interface classification as one consumer among many—not the only one.

2 Likes

Thanks @camunro. Like I've mentioned a few times and it's worth mentioning once again, considering I know you have a deep routing/networking background, I value your feedback and I'd like to ensure that the final standardized library at worst does not exempt routing usecases by incompatible API designs, although off-hand I think that's not what most people here will use it for.

While I have experience with home networking (and a relatively complex one at that) and I might know some concepts you mention, and might have seen/heard and vaguely had an idea of some other concepts, I'd like to mention that some of them are not too familiar to me, even if when I do some digging I can understand what issue they are supposed to fix and what holes to fill.

So if you think some APIs are blocking some usecases and can explicitly point them out, I'd love to double check and study more about that usecase to ensure routing usecases of such a standardized package are made as easy as practically possible.

Not exactly sure what exact types you mean, but with your previous post in mind, I can mention some stuff where swift-endpoint does fall short:

  • I think IPEndpoint would be a valuable addition to swift-endpoint.
  • I think we could also have IPNetwork since it does have some differences compared to CIDR. Not decided on that.
  • Looking at PrefixLength in PrefixLength.swift, I can see its value. I generally like "tagged" types for code clarity and correctness, although PrefixLength does a bit more as well (ensures length is within valid bounds).
    • I'm undecided on necessity of PrefixLength though to be included in the final standardized library, but I would still be happy with its inclusion if the work-group comes to conclusion that it does pass the value threshold to be included.
  1. Classification helpers (isLoopback, isBroadcast, isPrivate, …) are useful on address-shaped values...

Right. I think there has been a misunderstanding.
Example: CIDR<IPv4Address> has .broadcast. It's IPv4Address that has .isBroadcast.

  1. Parse and equality must respect form

Hmm. I can't find a standard definition to follow, but frankly, considering other libraries behaviors, it looks like equality of CIDR should also consider the whole prefix instead of the masked-off-prefix.

So assuming the addition of a IPNetwork type, it would be completely fine for CIDR to have that equality behavior. Perhaps it'll be less surprising as well, generally, although some other people might end up getting surprised at why the insignificant bits are considered significant.

@kpodosin — two quick notes on points you raised: scoped addressing, and an address-family type.

1. Interface-scoped IPv6

Thank you for calling out interface-scoped addressing. I agree it belongs in the requirements discussion and that it affects both parsing/generation (addr%zone) and identity (same link-local bits on different interfaces are not the same operational address).

I’d frame the requirement this way:

Scoped IPv6 (RFC 4007) is address + zone/interface identity, not part of core classless prefix mathematics. The 128-bit value alone is insufficient for link-local use; the zone (interface index and/or name) disambiguates which link the address is on. That often implies a small Interface / zone currency type, as you said, and it is distinct from MAC-address types or from how some interface identifiers are generated (e.g. EUI-64).

For layering, I treat that the same way I treat endpoints:

Layer Examples Role
Address / prefix currency IP address, prefix length, canonical network/prefix Classless address-space math: boundaries, containment, routes, many policy/registry prefixes
Next layer (composition + host/OS attachment) IPEndpoint = address + port; scoped IPv6 = address + zone/interface Transport binding and link/path selection

Port and zone are both extra identity beside the address. They matter enormously for sockets and link-local communication, but they are not what “is this a /24?” or “does this block contain that host?” need. Zone identifiers are also often host-local (name ↔ index mapping), which fits OS/adapter boundaries better than a pure multiplatform prefix-math core.

In prior art I’m building (swift-cidr), the core module is aimed at that address/prefix layer. I currently have IPEndpoint in that package; I plan to move it out of the CIDR math library so the boundary stays coherent: core stays classless address-space currency, and IPEndpoint and scoped IPv6 (addr%zone) live in the next layer (with interface/zone types and platform adapters). Core IPv6 values today are bits-only; that is intentional for the math layer, not a claim that scope is unimportant.

For the workgroup requirements matrix, I’d record something like:

  1. IPAddress / family / prefix length / canonical network — foundation currency (CIDR plan–shaped networks included).
  2. Port + IPEndpoint — adjacent composition for transport.
  3. Zone / Interface + scoped IPv6 — adjacent composition for link-local and similar; parse/format and equality must include zone; not a substitute for network/prefix types.

That keeps scoped addressing first-class without forcing every consumer of prefix math to take on host-local interface tables—and without treating scope as a footnote to classification predicates alone.

2. Address family (v4 / v6 — and room for adjacent families)

You also noted there should be a type that indicates address family, or an enum that holds either v4 or v6.

I agree that family identity belongs in the currency model. In prior art (swift-cidr), that is not only a runtime v4/v6 tag on a sum type:

  • AddressFamily — compile-time family trait: storage width, parse/format hooks, and IANA address-family metadata for selected registry families.
  • IPAddressFamily — narrows that to IP only, so IP address / network / prefix-length types stay IP-specific.
  • Concrete markers such as AF.V4 and AF.V6, with mixed-family boundary types (e.g. AnyIPAddress / AnyIPNetwork) when an API must accept either.
  • The same AddressFamily pattern also covers adjacent non-IP currency that shows up in real stacks: e.g. 48- and 64-bit MAC families, and Autonomous System number as inter-domain routing currency—without pretending those are IPv4/IPv6 values or overloading the IP network types.

Live control-plane data makes mixed-family collections concrete: public validated ROA feeds such as [Cloudflare’s `rpki.json`](https://rpki.cloudflare.com/rpki.json) list many origin ASes with **both IPv4 and IPv6** prefixes (and `maxLength`). Ingesting that class of RPKI data is ordinary infrastructure work—it needs family discrimination and mixed-family **network** collections, not only a host/interface “v4 or v6” enum for sockets.

So the requirement I’d put on the matrix is slightly broader than “enum of v4 or v6”:

  • IP family discrimination for standard IP types (and/or a sum type for mixed-family IP APIs).
  • A clear place for family metadata (width, naming) so the model can grow or coexist with L2 (MAC) and control-plane (ASN) currency without a second ad hoc design.
  • Keep IP prefix/network math on the IP refinement only—MAC and ASN share the family pattern, not the IP CIDR operations.

That pairs with the Ethernet/MAC interest already on this thread: family is the extension point; scoped IPv6 remains a separate composition (address + zone), not a new address family.

Happy to help phrase matrix rows for family, endpoint/scope layering, and IP vs adjacent families when the group captures the type-family boundary.

Following yesterday's work group meeting, here are my thoughts:

macOS version Requirements

InlineArray or UInt_ as the underlying storage?

  • As mentioned, we don't need to expose the underlying storage of the IP types.
    • If we can't easily come to a conclusion, we can postpone the decision to when the other parts of the library are clear, so we have an easier type making a decision.
    • Currently this is the case. We expose the storage via ip.address which would yield UInt32/UInt128.
      • However, this is not "unfixable" in a future version, even if we commit to just simply exposing the underlying storage.
      • In Swift you can simply replace the var address: UInt_ (stored storage/property) with var address: UInt_ { get { /*get from new storage*/ } set { /*set in the new storage*/ } } and it would be pretty fine.
      • Since we already know this can be an issue, I'd say we can plan to not having to expose the underlying storage so in the future we don't need to work around it at all.
      • Instead, I'll likely experiment with protocol IP[v4/v6]AddressConvertible then we can have functions such as myIP.as(UInt_.self) /*returns UInt_*/ where UInt_ conforms to IP[v4/v6]AddressConvertible. Or perhaps just with simple asUInt_() and asInlineArray() funcs. I'll have to see.

What macOS version then to require?

I realize not every single library will have to have the same availability, specifically to be able to be used in swift-nio or such.

A library like swift-endpoint will need to be available on macOS 12 so it can possibly replace swift-nio's SocketAddress guts whenever Swift 6.6 comes out (swift-nio supports 3 minor versions back, so at 6.6, it can drop support for 6.3 which defaults to macOS 10.13, which would be incompatible with swift-endpoint or any library primarily built on Spans).

For other networking libraries, we can also decide on a 1-by-1 basis based on what is available and what is not. I'd say requiring macOS 13 should be the default as that comes with Clock/Duration types. See [Networking Workgroup] Public meeting — Monday, August 3, 2026 — Agenda and call for topics - #3 by MahdiBM for more info.

What about DomainName?

swift-endpoint implements DomainName and "DomainName+IP Integration" in 2 extra separate modules. This means that they are both completely deduplicated and DomainName can somewhat easily be moved to another package or such, if needs be.

However, that needs answering a question.
What is the point of having IP address and port APIs? How much modularity we want to have?
Basically, what should be the scope of APIs that should be included in the same package as the one containing these IP address / Port APIs?

If the answer to these questions is too hard, we could simply go the safer route, which is to say to spread the implementation across multiple repos. Meaning DomainName ends up in its own repo (alongside its swift-idna dependency), then a possible third repo for ConnectionTarget which is an enum of where to connect to, which needs DomainName so users can name a domain for resolution (this is what other scattered types also contain right now).

The choices are basically:

IP address + DomainName + idna dependencies + Port/CIDR + "ConnectionTarget" in 1 package
vs IP address + Port/CIDR in 1, DomainName + idna dependencies in another, and "ConnectionTarget" in a third.

Having them in different repos does add overheard but it could be acceptable.

Little endian / big endian issues?

I'll try to experiment and see if we can find some kind of clear way of making sure users can choose any endianness on demand perhaps? For example via endianness function arguments? I'll have to see.

I think this is only a negligible performance issue, and that's not what I'd be looking solve with this experiment, as I don't see it as a big issue.
myInt.bigEndian which is currently required, should ultimately compile down to 1 instruction, which has a reasonable throughput on most systems and takes 1 cycle, sometimes 2 depending on the CPU. withUnsafeByte(of:) function should also do almost no work if any, other than acting as an interface to access the underlying bytes. One could confirm via godbolt (or via swiftc -O -emit-assembly $my_file) if needs be; I haven't tried.

I think this endianness dance is largely an artifact of the sockets API's unfortunate design. It is a leaky abstraction: instead of accepting a port number as an ordinary value and handling its representation internally, the API requires you to populate a struct field that is already encoded in network byte order. Higher-level modern APIs would normally hide this detail. In Swift, for example, I would expect to work with something like Port(rawValue: 1234) or Port("1234"), leaving any byte-order conversion to the implementation. The issue can still surface when using the imported POSIX socket structures directly, but it should not ordinarily appear at a higher-level API boundary.

3 Likes

+1

Higher-level modern APIs would normally hide this detail.

This is supposed to be able to be used as lower-level APIs as well.
I would say the swift-endpoint API is cleaner that C socket APIs[1], I wouldn't say it's necessarily "higher level". This is going to be used in lower level networking as well, up until we give up the values to C APIs or write them manually.

In Swift, for example, I would expect to work with something like Port(rawValue: 1234) or Port("1234")

Right. The APIs should work "Swifty" by design IMO unless otherwise necessary, but we'll have see exactly how.

The issue can still surface when using the imported POSIX socket structures directly, but it should not ordinarily appear at a higher-level API boundary.

Again, I don't necessarily see these APIs as "high-level".
I generally agree with you in trying to not expose a whole lot of endianness questions to the face of a user who simply just wants to use the APIs in their Swift code, but I'd also like the library to make it easy for those who are understandably talking to some C APIs.
I think keeping it "Swifty" takes precedence as that's what most users will want, which are not interacting directly with the C APIs, but I don't think these 2 will be at odds too much, in API designs.


  1. Decades after the C APIs, in their defense. ↩︎

Sharing here that this topic was discussed on the August 3rd public meeting of the Networking Workgroup (full notes here).

Some of the topics we covered were:

  • Discussions of platform dependencies, and how far back we need to support. This discussion used, as one motivating example, being able to use InlineArray as the backing storage for addresses. In that particular case, the group believed it should be possible to deal with compatibility for the implementation, but there may be some changes we'd like to see in Swift Package Manager. There was a general desire to make these packages forward looking and use modern language features where applicable.
  • We discussed some of the scope that gets pulled in along with addresses — subnets, CIDR, interface scoping — and talked about making sure we support more advanced cases (not just simple clients, but servers and network routers, etc), while being able to have "progressive disclosure" in the APIs and keeping the basic types simple.

Going forward, we'll want to see more experimentation and examples putting together some of the requirements and suggestions on this thread. We can use this thread, as well as the #networking channel in Slack for more real-time coordination.

3 Likes

I've addressed some of the concerns. Bold texts are the TL;DRs.


Modified CIDR Equatable/Hashable Behavior

The CIDR concerns from @camunro were addressed. CIDR now always behaves based on the full prefix, unless of course for containment checks where it doesn't matter if prefix contains host bits or not.

Now:

  • For Hashable/Equatable the insignificant bits of prefix are taken into account.
    • Therefore, now, 127.0.0.1/24 != 127.0.0.199/24.

Added IPv6Address Description Options

IPv6Address now contains:

public func description(
    options: /*IPv6Address.*/DescriptionOptions = .standardOptions
) -> String

and AnyIPAddress contains:

func description(
    ipv6Options: IPv6Address.DescriptionOptions = .standardOptions
) -> String

IPv6Address.DescriptionOptions is defined as:

/// Options for adjusting the textual representation of an IPv6 address.
public struct DescriptionOptions: Sendable, OptionSet {
    /// The raw value of the description options.
    public let rawValue: Int

    /// Initialize a description options with a raw value.
    public init(rawValue: Int)

    /// Enclose the description in square brackets.
    /// Useful for when the description is used in different contexts such as when followed by a port number.
    /// Example: `[2001:db8::1]` instead of `2001:db8::1`.
    public static var encloseInSquareBrackets: Self

    /// For IPv4-mapped addresses, print the last 32 bits in the mixed notation of
    /// [RFC 4291, Section 2.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2).
    ///
    /// That is, for the well-known IPv4-embedding subnet
    /// `::ffff:0:0/96` of [RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2).
    ///
    /// Use `ipv6.isIPv4Mapped` to check if this option will apply to an IPv6 address.
    ///
    /// Example: `::ffff:204.152.189.116` instead of `::ffff:cc98:bd74`.
    public static var useMixedNotationForIPv4MappedAddresses: Self

    /// For NAT64 well-known IPv4-embedded addresses, print the last 32 bits in the mixed
    /// notation of [RFC 4291, Section 2.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2).
    ///
    /// That is, for the well-known IPv4-embedding subnet
    /// `64:ff9b::/96` of [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052#section-2.4).
    ///
    /// Use `ipv6.isNAT64WellKnownIPv4Embedded` to check if this option will apply to an
    /// IPv6 address.
    ///
    /// Example: `64:ff9b::192.0.2.33` instead of `64:ff9b::c000:221`.
    public static var useMixedNotationForNAT64WellKnownIPv4EmbeddedAddresses: Self

    /// Print the last 32 bits of every well-known IPv4-embedded address in the mixed notation of
    /// [RFC 4291, Section 2.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2).
    /// Consists of `useMixedNotationForIPv4MappedAddresses` and `useMixedNotationForNAT64WellKnownIPv4EmbeddedAddresses`.
    ///
    /// Use `ipv6.isWellKnownIPv4Embedded` to check if this option will apply to an IPv6 address.
    public static var useMixedNotation: Self

    /// Options for compliance with [RFC 5952, A Recommendation for IPv6 Address Text Representation, August 2010](https://datatracker.ietf.org/doc/html/rfc5952).
    /// Consists of `useMixedNotationForIPv4MappedAddresses` and `useMixedNotationForNAT64WellKnownIPv4EmbeddedAddresses`.
    public static var standardOptions: Self
}

One note is that I've noticed RFC 5952 only recommends brackets for IPv6 only when there is a chance for ambiguity. For example in [ipv6]:port where :port might be indistinguishable from the IPv6's own segments.
As such, swift-endpoint now defaults to simply not enclosing the IPv6 description in brackets.
This is also in line with pretty much all other libraries (I appointed Claude to check other libs/ecosystems, perhaps I'll add more results later).

We could discuss having more options to adjust the behaviors mentioned below, but for now, all the followings remain true at all times, per RFC 5952:

  • All letters are in lowercase.
  • Compression sign (::) is always used where possible.
  • Segments are written in compact form where possible (::0021 :cross_mark:, ::21 :white_check_mark:).

Each one of these could have an option to turn on or off.
None of them are hard to implement. Some could be trivial, even.

Do we even need those options?! Generally, yes. Some more, some less.
For example an option for inserting brackets in a performant way is helpful for IP+Port descriptions.
For mixed notations, such options can also be helpful in different use-cases.

The only big behavior divergence that swift-endpoint has compared to other libraries, is that by default it prints IPv4-embedded form for NAT64 well-known IPv4-embedded addresses. This is not a big deal, and swift-endpoint is the one that is more closely implementing RFC recommendations (RFCs 6052 + 5952). Furthermore, that's why an option exists for disabling it.


Implemented Missing APIs For Port

Port now has a similar interface compared to the IP addresses.
Specially when it comes to String decoding / encodings.

I've added a script (+CI) which generates a static accessor in Port for all IANA-registered port numbers where there is a backing RFC. There are 192 of these, including familiar ones such as Port.http and .https, and less-familiar ones such as

Port.`dns-llq`

The names are currently kept as-is, and if needed are surrounded in backticks where they contain a character which Swift would otherwise reject as an identifier (here, -, in dns-llq).


Made Benchmark Comparisons More Straightforward

This was more of a me-concern that anybody else mentioning it.

To simplify benchmarks comparisons and remove a bunch of explanations from README, I've made it so swift-endpoint functions are no longer at a disadvantage of going through String's heap allocation and other internals, in benchmarks.
This has the side effect of exposing how much faster swift-endpoint actually is.
The README Performance tables are now as follows:

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 8.6 176.6 20.53x (previously 10.93x)
IPv4 Parsing 14.5 46.0 3.17x
IPv6 Serializing 31.0 236.2 7.62x (previously 2.82x)
IPv6 Parsing 29.5 95.9 3.25x

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 120.0 6.00x (previously 5.00x)
IPv4 Parsing 17.0 26.7 1.57x
IPv6 Serializing 37.5 180.0 4.80x (previously 2.29x)
IPv6 Parsing 37.5 46.7 1.25x

The "previously" ratios are from 21ceb0b, which is also the same as in the initial message I posted in this discussion. As mentioned in README, these results are easily verifiable by running the benchmarks yourselves (ask your LLM to do it).

Note that the base Linux (glibc) benchmarking machine has changed so the numbers have been raised a bit compared to running on more modern hardware.
What you can trust though, are the ratio changes and the fact that inet implementations have stayed the same.

Again, to be clear, the internals haven't really changed much. swift-endpoint hasn't gotten actually any faster, just that the benchmarks now do not put swift-endpoint at a disadvantage to make the README claims more straightforward.


Remaining concerns

  • 1. The backing storage, InlineArray or UInt_?
    • The solution that I'll experiment with is to simply hide the backing storage.
    • There is little to no need to actually tie an IPAddress to some specific kind of storage.
    • Then likely use UInt_ even if we are using it as a byte-storage (stored in big-endian), not a number (stored in little-endian), like how String's SmallString works (uses UInt_ as stack-allocated byte-storage for up to 15 utf8 bytes).
    • Then provide APIs to get the bytes out of the address. For example withSpan(), asUInt_(), asInlineArray() etc...
  • 2. Little-endian / big-endian: should make it clear what methods are using which.
    • I haven't experimented with this much yet, so I don't have much to add compared to above discussions.
    • 1 thing is clear though: I'll add clear documentation (like currently there is) to each function.
      • Including clear examples that are easy to understand even for new-comers.
  • 3. API surface / scope?
    • Generally, the main APIs in other ecosystems are quite limited in scope (e.g. rust std::net).
    • For example no DomainName, CIDR (usually, python has IPNetwork which is similar but not the same), NAT64 etc....
    • This doesn't mean we won't have those types, just that maybe they should not be in the same target/package.
    • I'm doing a survey of other ecosystems' APIs. I'll add detailed results later.
  • 4. Add UInt32-chunked inits/variables to work with IPv6Address.
    • Mostly forgot about this one. Trivial to add.
  • 5. An IPEndpoint type or similar. Likely implemented as IP+port+scope.
    • Perhaps there will be different types that contain scope/zone/interface if necessary; I haven't planned yet.
  • 6. Hardware / mac / ethernet address types.
    • Have not investigated yet. Pending scoping discussions.

Thanks @MahdiBM for the updates and for walking through the equality change.

One clarification up front, so nothing is misread:

My posts on this thread (and in the Networking Workgroup) are about workgroup requirements and deliverables—not personal concerns about swift-endpoint as a product.
I’m glad when any prior-art package experiments with equality, printing, or ports; that helps the group see tradeoffs. I am not asking anyone to treat “address @camunro’s package issues” as the agenda. The agenda is shared currency types and constraints the NWG can stand behind.

On CIDR / forms specifically: improving hybrid-wrapper Equatable so host bits participate in identity is a useful step toward the parse/equality requirements I listed earlier (address-with-context vs canonical network, lossy projection). That does not by itself close the broader requirements around distinct forms, canonical network currency, and prefix length as currency—those remain workgroup design questions, not a single-package changelog.

Storage (InlineArray vs integers)

I’ve written up a more careful walkthrough on the #networking Slack (thought experiment from a classless math library angle: bits + explicit length, hot-path mask/containment, adapters for octets). Summary for the Forums record:

  • Classless work is bit strings + prefix length, not “an open bag of octets” as the primary model.
  • Fixed-width unsigned integers (UInt32 / UInt128) make mask/containment natural; the payload is already inline (part of the value).
  • A fixed byte array (or InlineArray) still needs a layout definition and usually reintroduces integers for every prefix operation—or reimplements shifts across bytes.
  • Network byte order belongs at I/O / adapter boundaries; forcing NBO interpretation on every in-process containment check is the wrong default for large control-plane sets.
  • So: InlineArray (or raw octets) as an adapter / projection API is reasonable; as the primary model of address currency without a math story, it is feature-led rather than domain-led.

Hiding the storage type behind the API is fine as an implementation strategy. Hiding it does not remove the need to choose a domain model for equality, masking, and generic algorithms. “Use UInt_ as big-endian byte storage” still needs a clear story for host arithmetic vs wire octets—that’s the same endian discussion, not a free pass.

I’m not asking the Forums to re-litigate every Slack message. I am asking that storage not be treated as settled solely by one package’s experiment list, and that NWG notes capture the math-vs-host/context split.

If anyone wants to dig into the storage write-up, the detailed walkthrough is on the Networking Slack; I welcome more comments and questions there.

DomainName / DNS

Domain names are a separate system (resolution, caching, encoding, operational policy). They are valuable software—and out of scope for the IP address / port currency deliverable the workgroup has been scoping (progressive disclosure: address and port first; richer topology later). Folding DomainName into the same early surface confuses two problem spaces.

What I’d like the workgroup to keep on the board

  1. Forms (at least: address / address+context / canonical network) and explicit lossy projections.
  2. PrefixLength (or equivalent) as currency, not only an Int tag.
  3. Progressive disclosure without erasing control-plane uses of prefix math.
  4. Storage/representation as a follow-on design choice informed by (1)–(3), not the other way around.

Happy to keep iterating on those as requirements, regardless of which package anyone is dogfooding this week.

1 Like

I've prepared a proposal for the API shape of this core IP-address+port library.

The proposal first goes through prior arts. 10 languages and even more libraries were surveyed:

  • C: glibc (Posix / BSD)
  • Swift: SwiftNIO, Network.framework
  • Rust: std::net
  • Zig: std.net, std.Io.net
  • C++: Boost ASIO
  • Go: net, netip
  • JavaScript: nodejs
  • Python: ipaddress, socket
  • Java: java.net
  • C#: .NET

I'd like everyone to read it and give feedback.

I hope this survey will make everything much more clear and enable us to choose a direction easier.

Read the proposal here: Swift IP-address and Port API Proposal

Thanks @MahdiBM for the long prior-art survey and the draft API shape. Cross-language inventories are useful. They do not, by themselves, define the requirements boundary for a Swift currency surface that claims to support classless addressing.

I want to pin a few definitional points so the workgroup does not settle on the lowest common denominator of host-stack APIs and call that “CIDR.”

1. What CIDR means (again), and what that forces in the type system

Internet networking, at Layer 3, is built on routing currency: prefixes.
Without a first-class canonical network / prefix value—call it IPNetwork, IPv4Prefix, or YANG’s ip-prefix—there is no shared way to name what is assigned, aggregated, filtered, or installed. Host addresses and ports matter enormously for endpoints; they do not replace prefix-shaped currency. That is not a niche ops preference. It is how the Internet is structured.

I have already put the CIDR definition on this thread; restating it again so the workgroup keeps one load-bearing why for any “CIDR-compliant” or classless story:

The native unit of that plan is prefix-shaped address space: a canonical network / prefix (host bits cleared for identity). That is the unit of assignment and aggregation under CIDR. It is required currency for any design that claims classless / CIDR grounding.

So:

Any design that claims CIDR compliance must treat a first-class canonical network/prefix type as required currency—not as an afterthought, not as “maybe later,” and not as something you only get by calling .networkAddress on a host-shaped hybrid.

That is compatible with progressive disclosure (many apps start at address + port). Progressive disclosure must not erase the plan’s native unit—or we design a “networking” surface that cannot name a route.

2. RFC 9911 as vocabulary ally—not the foundational “why”

RFC 9911 YANG types are useful labels for interchange. They do not replace the architectural grounding of RFC 4632 (and IPv6 prefix construction in RFC 4291). YANG describes how modules name strings; CIDR explains why prefix-shaped currency exists.

Used properly:

Grounding Role
RFC 4632 / classless plan Why canonical prefixes exist (assignment, aggregation, inter-domain routing math)
RFC 9911 ip-prefix Same form as a canonical network type (IPNetwork / family-specific prefix)—host bits not part of identity in canonical form
RFC 9911 ip-address-and-prefix Different form: address + associated length (host bits may matter)—interface/config-shaped

So: YANG ip-prefix ≈ the IPNetwork / canonical network currency we need.
YANG ip-address-and-prefix ≈ address-with-prefix-context—necessary, not a substitute for ip-prefix.

Your table already states that 192.168.1.98/24 is not a canonical ip-prefix. That is correct.

What must not happen next is the reverse error: elevating ip-address-and-prefix as the primary public model because it is “lossless” and “just works” for a general audience, while ip-prefix / network currency remains second-class or optional.

For control-plane and Internet-scale use (RIB keys, IRR route/route6, RPKI ROA base prefixes, many filters, aggregation):

  • 192.168.1.98/24 is not a valid canonical network value.
  • Accepting host bits on input is fine only if the result is an explicit, lossy projection to another type—the canonical network/prefix type (e.g. address-with-context → IPNetwork / ip-prefix representing 192.168.1.0/24 as a value, not a string round-trip)—or the network type strictly rejects non-canonical input.
  • Equality and hashing of network currency must respect form—no silent host-bit identity, no silent drop without a named typed projection.

That list is not arbitrary product vocabulary. It is the everyday object set of Internet operations and control-plane protocols. A standards-grounded currency surface should be checked against the RFCs and registries those systems cite—not only against host std::net-style inventories. Filter and policy practice (prefix-lists, more-specifics, vendor ACL/prefix objects) is where operators learn that canonical prefix identity is non-negotiable; that experience is hard to extract from a language survey alone.

Naming IPv4AddressAndPrefix / IPv6AddressAndPrefix can track YANG for the context form, but that naming obfuscates the main point if it becomes the centerpiece of the proposal while the required ip-prefix / IPNetwork form stays secondary or optional. Context form is real; network form is not optional.

3. “The library should not concern itself with routing” is backwards for prefix currency

The proposal suggests (paraphrasing) that CIDR is a poor name because the library would “likely want to not concern itself with routing.”

Networking currency is routing-shaped at Layer 3. The library need not implement BGP, OSPF, or a RIB. It must provide the prefix/network values those systems exchange—or every higher library invents incompatible ones. I made the same requirements point earlier; it still holds:

Classless addressing is not a “core-only” specialty. The same plan and math run from core to edge—backbone and IX policy, enterprise routing and ACLs, host and interface configuration, automation and diagnostics. When that is taken seriously, several currency types fall out naturally; they are not a grab bag of extras. I sketched that direction last May:

That list is progressive—not “ship everything on day one”—but it is also coherent: once canonical network currency exists, length, address-with-context, blocks, and multicast stop looking like unrelated niches and start looking like one architecture.

Currency types are not a full control plane. They are the shared numbers and forms the control plane and host stack use. A surface that only optimizes sockets while treating canonical prefixes as optional is not a complete foundation for Internet software—on server or on end hosts that still speak the same address architecture.

4. Prior-art survey ≠ requirements minimum

A survey of std::net, POSIX, Go netip, etc. correctly shows many ecosystems ship a host-shaped core (address, sometimes port, sometimes a single prefix-ish type).

That is the status quo of host APIs. It is not automatically the right ceiling for Swift if the workgroup also cares about:

  • multiplatform Server-Side Swift infrastructure,
  • control-plane and policy consumers,
  • and not forcing every IRR/RPKI/IPAM tool to invent incompatible network types.

“Other languages don’t have X” is useful data. It is not a proof that X is out of scope when Internet standards and operational practice use X daily.

5. Progressive disclosure of forms (requirements-oriented type list)

Without arguing for any one package, here is a disclosure-ordered currency surface the workgroup can reason about. Day-one apps need only the top rows; infrastructure needs the middle; library authors need the bottom.

Host / common path

  1. IPv4Address / IPv6Address (and mixed-family boundary if needed)
  2. Port
  3. Endpoint composition (IP + port; scope/zone as host/context—later)

Classless math (required for any honest “CIDR” claim)

  1. PrefixLength (family-valid length currency—not only a field on one struct)
  2. Canonical network / prefix (IPNetwork / ip-prefix form)—required
  3. Address-with-prefix-context (ip-address-and-prefix form)—required as a distinct form, not as a substitute for (5)
  4. Explicit lossy projection to another type: address-with-context → canonical network (value types, not strings)

Infrastructure depth (progressive, not day-one for every app)

  1. Neutral allocation-shaped blocks (set math without “LAN gateway” ceremony)
  2. Prefix selectors / length ranges (RPSL-style more-specifics; related to ROA maxLength ideas)
  3. Multicast group and group-range identity (not unicast subnet semantics)
  4. Autonomous System number as numeric currency (with routes, ROAs, IRR—not BGP-only)
  5. Mixed-family collections at API boundaries

Protocols (later disclosure for generic algorithms)

  1. Shared structure for “storage + length” across forms (protocol, not one hybrid value type)
  2. Aligned-prefix operations (containment, subnets, summarize) as a refinement for network forms

This is the same spirit as progressive disclosure the NWG has discussed: start small, but do not design the small surface so the large surface is impossible.

Working CLI tools already exercise several of these forms in real workflows (IRR origin → canonical networks, coverage/merge of prefix sets, admission-style checks, walks). Those are good acceptance scenarios for any proposed currency library—independent of branding.

Context inventory (where the same slash string is not the same form):

6. Specific gaps / mis-scopes in the proposal

PrefixLength “undecided.”
Please treat length currency as required, not optional. Family bounds (0…32 / 0…128) matter. Length also appears as a standalone control-plane field—for example ROA maxLength in RFC 9582—not only glued to a host address. That point has been discussed on the Forums and in more detail on the networking Slack storage/math walkthrough. Ignoring length-as-currency is ignoring a real control-plane requirement.

Multicast.
Unicast “network” ceremony is the wrong model for multicast group destinations and ranges. If the surface only thinks in hybrid CIDR + socket address, multicast identity is easy to mishandle. A progressive path should at least reserve group / range forms (or explicitly defer them without pretending unicast types cover them).

NAT64.
Agree it need not live in the core IP+port package. Clarify taxonomy: NAT64 is address translation / transition, not a routing protocol. Scoping it “to routing” confuses categories. Translation helpers can live elsewhere; they should not drive the prefix form model.

Domain / hostname.
Agree with pulling DNS/IDN out of the early IP+port currency deliverable. Domain names are a separate system. Progressive disclosure: address + port first; name resolution beside or above—not inside—the currency core.

Socket-shaped expansion.
UnixDomainSocketAddress, rich *SocketAddress with flow-info, etc., may be valuable adapter / host types. They should not crowd out or delay canonical network/prefix currency. The workgroup’s networking vision has been moving toward clearer layering (currency and composition vs historical socket bags)—not toward recreating sockaddr as the only ontology.

Storage / InlineArray.
Representation experiments (hide storage, expose withSpan / byte views) are fine after the domain model is fixed. They do not replace the integer bit-string + length story for classless math. Detail remains on Slack for those who want the full walkthrough; the requirements point is domain-led, not feature-led.

7. What I am asking the workgroup to treat as non-negotiable

  1. Internet L3 currency includes canonical prefixes. Without ip-prefix / IPNetwork (or equivalent), there is no shared name for what is assigned, aggregated, or installed.
  2. Ground the “why” in RFC 4632 (and RFC 4291 for IPv6 construction); use RFC 9911 as vocabulary (ip-prefix ≈ network, ip-address-and-prefix ≈ address+context)—not as a substitute for the plan.
  3. Two forms, both first-class, with explicit lossy projection; equality/hashing respect form.
  4. PrefixLength (or equivalent) as currency, including standalone control-plane uses (e.g. ROA maxLength).
  5. Progressive disclosure that starts at address/port without deleting infrastructure forms.
  6. Prior-art LCD is not the requirements ceiling for Server-Side Swift + multiplatform currency.
  7. DNS, NAT64 translation, and full socket ceremony are adjacent scopes—not substitutes for prefix math.

I appreciate the survey effort. I will keep pushing on definitions and forms, because getting those wrong is far more expensive than adding a type later.

Happy to iterate on naming (IPNetwork vs IPv4Prefix vs YANG-style names) once the NWG firms direction on the requirements above—as shared workgroup requirements, not as optional package preferences.

1 Like