Requirements for IP address and port APIs

Your examination here has been extensive, but I think it would be illustrative if you provided a few examples of the APIs you expect to exist as you'd use them. You've outlined a lot of feature requirements, but I'd like to see what you think is the most important developer experience for using these types. It doesn't have to be compiling Swift code, but some sample code would be appreciated.

Thanks @Jon_Shier — fair ask.

I’ve been focused on requirements and forms rather than a frozen API surface. The best written map of the type model and progressive disclosure I’m arguing for is here:

  • Documentation/DESIGN.md — why each form exists, type↔standards, progressive disclosure
  • Documentation/ — package docs index (including INTERNALS for non-public/helpers if useful)

Happy to follow up with a few short, illustrative usage sketches (address-with-context vs canonical network, PrefixLength as standalone currency, mixed-family networks)—names flexible, not a proposal that NWG adopt any package wholesale. I can put that together in the morning if that’s what would help most.

Generally speaking, I'm in favor of having such additional types as well, compared to what I've proposed based on prior art.

What I think should be done is though, to do this in layers.

Particularly, the proposed API shape in the API shape proposal I've shared, only contains types that prior arts agree that are needed for networking (although to varying degrees).

The additional APIs you (and Keely) proposed are all 100% valid. Likely we should start working on them in parallel. Just that I think they should not be included in the main core library.

What I think we should do is to have 1 or 2 packages, one of them with the core types and the other as a place to hold all other APIs. Or perhaps just 1 package but with different targets/modules.

The reason is, as there is precendent in Swift and other languages, such core types can ultimately end up in the stdlib.
Lots other languages already contain a "net" module in the stdlib, per the prior art survey I've shared in the document above.
So I'd like us to already draw a line between what types might have a chance at making it to the stdlib.
As such, the core types, as proposed in the proposal I shared above, should be distinctively decoupled from the other useful types.

The process for making it to stdlib, as I understand, is that a library/target can prove its worth in another package for a few years, and when the core team sees enough value in the types and is satisfied with the library's shape, and when the library has passed the test of time, they might consider adding it to the stdlib, even if with an API reshape/rename.
This has happened to Atomics (previously under swift-atomics) and UniqueArray (previously under swift-collections) for example.

Another reason is that if these types are to have a chance to make it to stdlib, they must be very widely used and hard to question. Stdlib, as I understand, acts defensively in adding new APIs and types because if they accept some APIs, it's hard to remove them.
They'll likely stay in the stdlib for years, even beyond major Swift releases even if they've been deprecated for a while. If some APIs cannot prove their absolute worth, it's fine by the stdlib. Afterall they can just be used by explicitly depending on the package.

Essentially the bar would be too high, and there will be harder questions other than "Are these types useful?". I'm not in a position to actually say what those qualities and qualifications are, but off-hand, I'd think for now only the API shape I've proposed will have such a chance in the first place.

So to recap, we can work on all the proposed APIs simultaneously. Just that the scope of the core "IP-address + port" library should be something more similar to what prior arts have proven useful.

One more reason IMO to have an IPPrefixAndAddress type in the core library instead of IPPrefix, is that IPPrefix can easily just wrap the IPPrefixAndAddress type if there is to be such a type in a target/package that consumes this "core" library.
The reverse is not true.

Thanks @Jon_Shier — happy to make the developer experience concrete.

Illustrative only. Names are flexible; this is not a proposal that the workgroup adopt any particular package or freeze an API. I’m showing form boundaries and how they show up in daily infrastructure work. Prior-art design notes remain in DESIGN.md and the package Documentation/ tree.

That prior art (swift-cidr and small CLIs around it) was designed deliberately for network engineering, operations, and architecture workflows—as well as end-host use—not only for client HTTP stacks. The DX goal is: the same currency types work when you are cleaning a prefix-list at 2 a.m. and when you are binding a port in an app.

What “good DX” means here

In the prior-art model I’m arguing from, classless length is always in the picture. A “bare” IPv4 dotted-quad is not outside CIDR—it is the host-length case: 192.0.2.10 means 192.0.2.10/32 (“exactly this address”). IPv6 likewise implies /128. Everything is classless currency; not every value is the same form.

For end-host apps, the happy path really is:

// Bare address text ⇒ implied /32 (exactly this host). Still classless currency.
let host = IPv4Address("192.0.2.10")!   // same idea as 192.0.2.10/32
let port = Port(443)
// later: endpoint = host + port (+ scope when we model it)

That path should stay short. Progressive disclosure is real.

Where experience gets expensive is when the same slash string is used for different jobs without different types. The important DX is not “more methods”—it is not lying about form.

Requirements for core currency (atomic forms): discrete address types and discrete canonical network / prefix types (v4 and v6), each with their own operations. The canonical network form is grounded in RFC 4632 (prefix-shaped assignment/aggregation), not in YANG. YANG’s ip-prefix (RFC 9911) maps cleanly to that RFC 4632 network/prefix form as interchange vocabulary; it is not a redefinition of CIDR. A bare address remains the implied /32 / /128 host-length case.

Not a core requirement: a combined ip-address-and-prefix / “address and prefix in one required hybrid” as the foundational type. That mixed form (host bits + a non-host length in one value) is useful for some contexts (e.g. interface config), but it is not a substitute for atomic IPAddress and IPNetwork / IPPrefix, and it does not add a third mathematical job those atoms (+ PrefixLength) and an explicit projection do not already cover. In routing tables, many filters, and ROA base prefixes, the unit of identity is the canonical network, not “whatever host bits happened to be in the string.”

// Address (host identity) — atomic; bare text implies /32
let host = IPv4Address("192.0.2.77")!          // ≡ 192.0.2.77/32

// Address-with-prefix *context* (shorter length; interface/config-shaped)
// Host bits matter for identity. Not the same as a route key.
let iface = IPv4Address("192.0.2.77/24")!

// Canonical network / prefix (atomic) — explicit lossy projection to *another type*
let net = iface.network   // → IPNetwork / IPv4Network value for 192.0.2.0/24

// net is what routing tables, many ACL/prefix-list entries, and ROA *prefixes* want
// iface and net must not silently share one hybrid equality story
// “Everything is classless” ≠ “one hybrid type for every job”
// Prefix length as its own currency (not only glued to a host)
let maxLen = IPv4PrefixLength(24)!   // family-checked 0...32
// RPKI ROA records are typically: origin ASN + base prefix + optional maxLength
// Public feeds (e.g. Cloudflare rpki.json) look like:
//   { "asn": 13335, "prefix": "1.0.0.0/24", "maxLength": 24, ... }
// → parse prefix as CanonicalNetwork; maxLength as PrefixLength—not Int soup
// Mixed-family prefix sets (ordinary for ROA / multi-family policy lists)
var roaPrefixes: [AnyIPNetwork] = []
roaPrefixes.append(.v4(IPv4Network("1.0.0.0/24")!))
roaPrefixes.append(.v6(IPv6Network("2001:db8::/32")!))

That is the DX I care about most: parse at the edge, keep values typed, project forms on purpose.

Why this is “daily work,” not opaque jargon

If you mostly build client HTTP stacks, a lot of this currency is easy to never see—but it is still what operators and architects touch when they protect routing and access policy. A few concrete jobs (no special vocabulary required):

1) Registry intent → filter / allowlist hygiene
Tools like bgpq4 are how many network engineers turn IRR (routing registry) data into prefix-lists and related filters for routers and policy. In the RouteObjects toolkit we deliberately do not reimplement all of bgpq4 in one binary yet; we split the job so each step uses the right form, and more CLI pieces are still under development toward that full capability:

  • asroutes — IRRd lookup by origin AS → canonical networks (not “print host, equal as network”).
  • cidrmerge — many hosts/CIDRs/ranges → minimal exact coverage (list hygiene before install).
  • swift-cidr-admission — load allow/deny prefix coverage for server-side source checks.
  • Still ahead on the roadmap: richer IRR set expansion and multi-vendor filter rendering (the rest of what operators use bgpq4 for).

Illustrative pipeline (conceptually):

asroutes AS701          →  list of IPNetwork
cidrmerge               →  minimal cover (still networks/ranges)
→ vendor prefix-list / admission policy file

2) Routing security authorizations (ROAs)
Industry programs such as MANRS treat routing security—including ROA correctness—as baseline good citizenship. Observatory-style ROA views (e.g. MANRS Observatory) and public ROA JSON feeds make the data shape obvious: prefix + maxLength + origin ASN. That is exactly “canonical network + PrefixLength + ASN currency,” not a single host-oriented hybrid type.

3) Large public prefix tables (optional later demos)
Public collectors such as RouteViews (and related APIs) are useful corpora when you want to stress “parse many prefixes as networks.” I would use them as fixtures for tools, not as the definition of the core type system. The type model has to be right before the bulk download.

Progressive disclosure (clarifying “core” vs “later”)

In the longer requirements post I listed forms in disclosure order: host path first; classless math next; deeper infrastructure later.

Important distinction:

  • Later in progressive disclosure does not mean “never in a shared currency library” or “only a third-party package forever.”
  • It means you do not need every type on day one of learning.
  • For any surface that claims classless / CIDR grounding, canonical network/prefix and family-valid prefix length are part of the core math currency, not optional garnish.
  • Context of use (full routing-policy languages, ROA crypto validation, complete IRR RPSL engines, socket I/O) belongs in layers above that consume the currency—not as a reason to omit the currency.

So again: required atomic forms are address (including implied host-length /32//128) and canonical network (plus length currency). A mixed address-and-prefix value with a non-host length can exist for specific contexts; it is not the requirement that replaces those atomic forms.

What I would not optimize the core DX for

  • One hybrid value that is both interface config and routing-table / filter key depending on the day.
  • Treating slash text as the interchange type between libraries.
  • Pulling DNS names, full socket bags, or NAT64 translation into the same early surface as address/port/prefix currency (those are real systems—beside or above the currency).

Happy to expand any one scenario (ROA ingest sketch, IRR→filter pipeline, or admission checks) with more detail, still illustrative. If a short inventory of which CLIs exercise which forms would help more than more snippets, I can do that too.

Brief orientation (for readers less deep in infrastructure ops)

A fair amount of this discussion sits next to end-host connection work (addresses, ports, sockets). That path matters. Internet infrastructure work also needs shared currency for prefixes—what gets listed in routing registries, authorized in routing security, and installed in filters—not only “where do I connect this app.”

If some of the ops vocabulary above is unfamiliar, these are short, neutral starting points (not homework, not a certification path):

  • RFC 4632 — Classless Inter-domain Routing (why prefix-shaped assignment/aggregation exists)
  • What is an Internet Routing Registry (IRR)? — ARIN overview of routing registries (the kind of data asroutes / bgpq4-style tools query)
  • MANRS — industry routing-security mutual expectations (context for why ROA prefix identity is serious)
  • RPKI / ROA (high level) — optional plain-language ROA primer; data shape is still origin + prefix + maxLength

Looking glasses (optional, hands-on): public web UIs that query live routing views. They make “control plane” tangible, and the query unit is almost always a canonical prefix (or address that is treated as host-length)—not an opaque host-only abstraction:

(Try a prefix such as 23.35.28.0/22 or a prefix you care about like your Internet upstream connection; notice the input is prefix-shaped.)

The type model is aimed at both progressive disclosure for app developers and not forcing every infrastructure library to reinvent incompatible network/prefix types.

1 Like

Thanks for all of the input and discussion here! I wanted to chime in and reply to a couple of the recent proposals and posts.

I see there's some discussion about what is in the package or not — I think we should talk about the progressive disclosure and the layers of what different developers should access without yet committing to the boundary of particular packages. I don't necessarily see a benefit of having a large number of different packages for very common behaviors, as I expect the code size for dealing with properties or conversions of addresses to be minimal enough. I would find it more annoying to need to import a different package just to be able to check if an address is multicast or not. That said, I think the discussion about layering is very useful (and can be represented even if we have one larger package).

While RFCs are clearly an important source of definitions for the work we're doing, the shape and boundaries of our APIs need to be focused on what is most useful and reusable for Swift developers — of clients, servers, intermediaries, routers, etc. RFCs generally steer clear of defining API surfaces, and our solution is going to end up being far more about the system as a whole and less about being bounded by a specific set of RFCs.

In response to @MahdiBM's proposal and the list of conclusions:

  • I certainly agree that domain names are separable from IP addresses. I believe they can end up being in the current types package (although they don't need to be), but they aren't part of this IP address exercise. I strongly think that getting the domain name type shouldn't require pulling in a whole DNS package (and separately we should talk more about the approach for DNS).
  • I agree that having the multicast scope defined is useful in the common types, at least where appropriate in the progressive disclosure that gets into multicast properties.
  • I share @camunro's concern around focusing too heavily on a YANG RFC's model for IP addresses. I'll comment separately below on the prefix approach, but I agree with @MahdiBM that we probably don't want the API names focused on the term "CIDR".
  • I agree on the approach for ports (being a cheap struct around an integer)
  • I disagree that NAT64 is out of scope, but think that kind of conversion is just another more advanced level of progressive disclosure. Conversions and parsing on basic address types belongs in a library that supports addresses, rather than making implementations rebuild this. Additionally, building connection establishment (happy eyeballs) correctly requires being able to do these conversions.
  • I'm ambivalent about where we represent Unix domain socket addresses. My current feeling is that they are either part of the higher level endpoint type (like hostnames), or an "address" endpoint type, but are not an "address" directly.
  • I'm pretty strongly against calling the basic type a "socket address", since there is no need for a networking library to be tied to sockets. We should keep the concepts being around IP addresses.
  • Regarding byte ordering, the main thing I want to see is that the byte ordering is clear in the API, particularly around ports. Ports are used both by applications (which will usually treat them in host order) and by the portions of networking libraries that write and parse packets (which will need to treat them in network byte order). The fact that many other libraries don't handle this clearly doesn't mean we shouldn't hold a higher bar.

On the topic of handling addresses and prefixes, I think @camunro's suggestion of having a strong type for prefix length makes sense. I also think we would benefit from having a single primary interpretation of what it means to store an address+prefix, and I currently think it makes more sense to have it default to zero-ing out the bits that are not part of the subnet mask. This is the "canonical" form of the prefix. If one needs to store a notion of a full address along with an associated prefix, that could be another custom type on top.

I am also wondering why we shouldn't just put the prefix length into the base IP address storage, so that all addresses are stored with a prefix length, that just is usually the full length of the address (/32 or /128). Addresses don't necessarily need to be a separate layer for the objects here.

The discussion above has not yet been focusing on the interface scoping for addresses, which is needed for correctly representing link-local addresses. I think this is also a fundamental requirement for storage in the basic address type.

As I step back and look at the scope here, I think we can end up with a basic set of types for addresses and ports that define storage of properties that are required to completely and uniquely identify the types (for addresses, one suggestion is that this is address+prefix+scope); and then a set of extensions that add functionality that is needed by various use cases: queryable properties, well-known static values, subnet math, NAT64 conversion, etc. I've drawn a diagram of this, attached.