RFDs/0004

IP address management

State
committed
Authors
Darwin D Wu
Discussion
renga-dcim/renga#110
Labels
rengaipaminventorynetworksoftware
Implemented
100%Checklist

Goal

Add organization-scoped IP address management that places managed network intent next to Renga’s observed interface assignments. Operators should be able to model routing domains and prefixes, reserve or assign addresses, understand utilization, and see where observed reality disagrees with managed intent.

The first release is inventory-assisted IPAM, not a DHCP server or a fully automatic allocator. Observation ingestion must not silently rewrite managed network intent.

Existing foundation

RFD 7 and RFD 8 shipped parts of IPAM before this RFD was settled. This RFD builds on them rather than replacing them.

  • Observed interface assignments (Address, AddressEvidence) are stored as PostgreSQL inet values with immutable observations, source evidence, presence history, and stale handling. Canonical identity is interface plus the complete inet, so the same host with two masks on one interface is two rows.

  • Prefix is a typed projection of a prefix resource envelope. It stores a PostgreSQL cidr with a GiST index, a status of active, reserved, or deprecated, and a free-text vrf string. Nothing enforces CIDR uniqueness, and the free text means default and a blank value name different tables.

  • RFD 8's prefix views live at /network/prefixes and /network/prefixes/:id. They derive hierarchy in memory per routing table and address family, choose an address map, child-space map, or address table by prefix size, present IPv6 addresses with their assignment method, and switch between the global table and each distinct vrf string.

  • RFD 8's Adopt action stores a ManagedAddress: one host per organization (mask forced to /32 or /128), an optional interface, and the adopting user. It has no routing domain, allocation state, management mode, or role.

  • RFD 7's optional prefix-to-VLAN relationship pairs IPv4 and IPv6 prefixes through the VLAN they serve.

  • Resource kinds already reserve prefix and vrf; the interface relationship kind vrf_member exists but nothing consumes it.

The shipped pieces have gaps this RFD closes:

  • Prefix creation checks the organization but not the owner/admin policy, and there is no update, delete, or create UI.

  • An exact duplicate CIDR in one table is accepted, and the in-memory tree nests it under its twin.

  • Adopt and release run outside a transaction, re-check roles only in memory, and write no change event, so they never appear in Activity.

  • Large-prefix utilization counts address rows rather than distinct hosts, and every count ignores routing domain.

NetBox reference

Renga adopts several useful NetBox IPAM semantics without copying its complete model:

  • A prefix or IP address may omit its VRF and thereby belong to the global routing table. A VRF is needed only to model another routing namespace.

  • Prefix hierarchy is derived from address containment rather than a mutable parent foreign key.

  • IP addresses retain their submitted prefix length, while duplicate detection and utilization compare distinct host values.

  • Prefix status, prefix function, address allocation state, and shared-address role are separate concerns.

  • Intentional shared identities use explicit roles such as VIP, anycast, or VRRP rather than disabling duplicate checks for all addresses.

The relevant upstream descriptions are NetBox’s VRF model and IPAM hierarchy and utilization. Renga differs by retaining immutable observations and source evidence, by treating observed-only addresses as normal unless an operator asks otherwise, and by raising unexpected duplicates as Inbox findings.

Domain boundary

Managed intent and observed evidence remain separate:

  • A null vrf_id identifies the global routing table. It is a real namespace, not an unknown or invalid value.

  • Vrf optionally defines an additional isolated routing namespace.

  • Prefix defines managed CIDR space and status in the global table or one VRF.

  • IpAddress defines one managed host address, allocation state, management mode, and role in the global table or one VRF. It replaces ManagedAddress.

  • IpAddressAssignment associates that managed address with canonical interfaces.

  • Existing Address and AddressEvidence records remain observed interface assignments reported by collectors.

  • Correlation and findings compare managed addresses with active observed assignments without erasing either side.

VRFs, prefixes, and managed IP addresses use resource envelopes because they are independently addressable inventory objects that need desired state, lifecycle, revisions, and change history. Activity requires explicitly written change events; envelopes provide their stable subjects but do not emit them automatically. Typed-field and assignment-only mutations also write events. Add ip_address to the accepted resource kinds and add prefix, vrf, and ip_address to the owner/admin-managed kinds. An IP-address envelope receives a server-generated stable name rather than using address text, because the same host address may validly exist in several VRFs. Typed projections retain their constrained, queryable fields.

VRFs

VRF objects are optional. A null vrf_id means the global routing table, and an organization can use prefixes, managed addresses, assignments, utilization, and findings without creating any VRFs. Objects in the global table are assumed to share one routing namespace. If two isolated sites reuse 10.0.0.0/8 without modeling their isolation, Renga is expected to report conflicts.

Creating a VRF introduces another organization-scoped namespace. The same CIDR or host address may exist independently in different VRFs and in the global table. There is no synthetic "default" VRF row.

A VRF initially has a name, optional route distinguisher, description, status, and metadata. Names, compared case-insensitively, and non-null route distinguishers are unique within an organization, and default is reserved for the global table. Import targets and route targets are deferred until a collector or operator workflow needs them.

The existing free-text prefix VRF migration starts with a namespace preflight. Values are trimmed and compared case-insensitively. Blank values and default are proposed to become null and therefore global. Every other distinct normalized value proposes one organization-scoped VRF envelope and its foreign key. The preflight reports every many-to-one mapping of legacy tables, including blank/default aliases. Different spellings are not assumed to be aliases: an operator approves a merge by renaming the legacy tables to one spelling (blank for global) in the prefix edit form and running the migration again. For example, Blue containing 10.0.0.0/24 and blue containing 10.0.0.0/25 must be reported even though no exact CIDR collides. Resulting exact-CIDR conflicts must be remediated before any changes are applied; unapproved merges fail with a remediation report. The routing-table switch in the prefix views then lists VRF records instead of distinct strings.

Prefixes

A prefix has a canonical CIDR, nullable VRF, status, description, optional role, an unmanaged-address policy, and resource envelope. Exact duplicate managed CIDRs are rejected within the same namespace, including the global table. The same CIDR is valid in different VRFs. Contained prefixes are hierarchy rather than overlap errors.

Parentage is derived using PostgreSQL containment and longest-prefix matching; it is not stored as an independently mutable parent ID. This avoids a second source of truth when prefixes are moved or resized. The GiST index remains the primary lookup path, and in-memory tree building uses strict containment so a prefix is never its own parent.

Statuses are container, active, reserved, and deprecated; container is added to the shipped three. Container utilization is based on the union of child-prefix space; other prefix statuses use occupied host values and ranges. Utilization follows status, while presentation follows children: a prefix with children is shown as a child-space map whatever its status, and a prefix without children as a host view. An empty container therefore shows its hosts but reports nothing allocated, counted one octet deeper until an addressing plan sets its planning level, and an active prefix with children keeps its map while its utilization counts hosts. Prefix roles are bounded or user-managed functional categories introduced only for immediate workflows, rather than an unvalidated metadata convention. Site, VLAN, tenant, and aggregate relationships may be added independently without making them prerequisites for prefix use.

Observed-only addresses are normal, as RFD 8 established: they are listed, counted, and can be adopted. A prefix may opt in to strict address management, for example a static server range, where every observed address is expected to have a managed record. Only inside a strict prefix does an observed address with no managed record become a finding. The policy is a boolean on the prefix, defaults to off, and applies to the most specific strict prefix containing the address.

Owners and admins create, edit, and delete prefixes. Deleting a prefix never deletes the managed addresses it contained; they simply fall under the next containing prefix or none.

Managed addresses and assignments

IpAddress evolves the shipped ManagedAddress table rather than adding a parallel one. IpAddress.address uses PostgreSQL inet and retains its intended prefix length. Managed uniqueness, matching, and utilization use the host portion, so 192.0.2.10/24 and 192.0.2.10/32 identify the same host in one namespace. The containing prefix is derived by longest-prefix match in that namespace rather than stored as an independently mutable association.

The resource envelope owns lifecycle. The typed address projection keeps orthogonal fields:

  • allocation_state: allocated or reserved;

  • management_mode: static, dhcp, or slaac;

  • role: ordinary, loopback, secondary, VIP, anycast, or a supported first-hop-redundancy protocol; and

  • optional DNS name, description, and metadata.

One canonical IpAddress exists for each managed host value and namespace. Assignments are separate rows so an address can be reserved or intentionally unassigned, normally assigned to one interface, or attached to several interfaces when its role explicitly permits sharing. The database prevents a duplicate address-to-interface pair. Assignment transactions lock the address row; ordinary roles reject a second assignment, and changing a shared role to an ordinary role is rejected while multiple assignments exist.

Adopt remains the quickest way to establish a managed address. It runs in one transaction that re-checks the owner/admin membership in the database, creates or reactivates the envelope and an allocated address in the observed address’s namespace with the observed mask, sets the management mode from the assignment method where it is known, and assigns it to the interface it was seen on. Release becomes lifecycle retirement of the envelope rather than a hard delete, so the history stays in Activity.

Release ends all current assignments and retires the whole managed address, including a shared address; removing just one assignment is a separate audited operation. Retired addresses are not current managed intent: they do not count as managed utilization, produce stale-managed findings, or suppress findings through their former roles. Surviving observations remain observed-only and still occupy space. Re-adoption, reservation, and allocation lock and reactivate the existing canonical record for that namespace and host instead of inserting a second record, retaining its audit history and establishing new current intent.

The migration from ManagedAddress gives each existing row an envelope, the global namespace, allocated state, an ordinary role, and an assignment for its remembered interface. The intended mask is recovered from the observed address on that interface when one is still present and otherwise stays at the host length.

Observed duplicate assignments are always accepted as evidence. An unexpected duplicate produces a finding rather than failing observation ingestion.

Observation correlation

Canonical observed-address identity becomes interface plus host value rather than interface plus complete inet. The current reported mask remains on the canonical row, and every original address and mask remains in AddressEvidence. A changed mask updates one observed assignment instead of creating a second host assignment on the same interface. The migration merges existing same-host rows on one interface, keeping the most recently observed mask on the canonical row and every evidence row.

A mask moves under the same source precedence as presence, so an older replay or a lower-priority source adds evidence without changing the canonical mask. When one observation reports several masks of the host, the shortest is canonical regardless of report order: it describes the connected network, and a choice independent of payload order keeps replays from flapping. The migration applies the same rule to rows observed together, and prefers a row still present over one an authoritative report has withdrawn.

Before reparenting evidence, change its uniqueness and ingestion idempotency contract to preserve distinct reported masks within the same observation and canonical address. Migration and replay tests include /24 and /32 reports for the same interface and host in one observation, retaining both evidence rows and presence history without duplicating them on replay.

An observation with no routing-domain claim belongs to the global table. This matches the product’s no-isolation default and may intentionally produce a duplicate finding when the operator has not modeled real isolation. Until collectors report routing domains, every observed address is global.

Sources that can report routing domains submit a source-local domain key and interface membership as observation-linked evidence. A source capability marks whether it is authoritative for that membership. Source-local keys map to a managed VRF through a stable external identifier, route distinguisher, or an explicit organization-scoped source mapping. An explicit but unmapped key produces unmapped_routing_domain and is not silently treated as global.

A claim is an interface’s routing_domain: an object with the source-local key and an optional route_distinguisher. null withdraws the source’s claim, and an absent field says nothing, so earlier claims stand. Each source has at most one active claim per interface: a newer report supersedes or withdraws it, and a late replay of an older report is stored already stale. Reports with the same timestamp are ordered by observation ID, matching inventory freshness, so reconciliation order cannot undo a withdrawal. The current claim is the newest active one, preferring authoritative sources. Its key resolves, first match wins, through an explicit mapping for that source and key (to a VRF or the global table), a VRF with the same route distinguisher, a VRF with the same name ignoring case, and the reserved key default for the global table; anything else is unmapped. Host agents and switch pollers read a device’s own configuration, so their claims are authoritative unless an owner or admin says otherwise; other sources are not.

Correlation first resolves the namespace, then compares normalized host values. The reported prefix length remains evidence and is compared with the most-specific managed prefix in that namespace. A wrong_vrf finding requires an explicit managed assignment to the observed interface plus authoritative evidence that the interface is currently in another mapped namespace; the mere existence of the same host in another VRF is not a conflict.

Findings

Address findings join the Inbox as an address domain alongside component, hardware match, placement, and topology. They use the shared finding workflow: assignment, snoozing, accepted exceptions with a reason and optional expiry, and member requests where a member cannot act directly. The reconciliation layer produces queryable findings for:

  • an observed address with no managed record inside a strict prefix;

  • an observed address outside every managed prefix in its namespace;

  • a managed assignment not currently observed;

  • an unexpected duplicate active address across interfaces or resources;

  • an observed prefix length that disagrees with managed containment;

  • an explicit source routing-domain key with no managed mapping; and

  • a managed address observed in the wrong VRF when explicit VRF evidence exists.

Outside-prefix findings apply only once an organization has at least one prefix in that namespace, so an organization that has not started modeling addresses sees no findings. Intentional shared-address roles suppress only the corresponding duplicate finding. They do not suppress stale, wrong-VRF, or outside-prefix findings. Findings retain involved records and source evidence and resolve when subsequent state agrees; immutable observations and change events preserve history. Duplicate findings group active evidence by namespace and distinct interface or resource, independent of reported mask. Unmapped-domain findings take precedence over namespace-dependent address findings because no safe namespace match exists.

The namespace-independent findings follow these rules. Only routable observed addresses are compared: loopback, link-local, and multicast addresses are unique by nobody’s design. An address inside any strict prefix is in strict scope, and the finding names the most specific strict prefix; a non-strict prefix inside a strict one cannot opt out, because the policy is a boolean. Outside-prefix findings start once the namespace has a prefix of the address’s family, so modeling only IPv4 never flags every IPv6 address. A prefix-length finding compares the canonical observed mask with the most specific non-container prefix containing the host, skipping containers, which are not subnets, and host-length reports, which usually mean the source did not know the mask. A stale assignment is an allocated address assigned to an interface that no longer reports it, on a resource some collector reports addresses for; documented assignments on unobserved equipment are not stale. Each observed address is compared with prefixes and managed addresses in its resolved namespace, and an address without a claim is global.

Precedence follows from what each finding can safely claim. An interface whose current claim is unmapped has no namespace, so its addresses take part in no namespace-dependent finding (strict, outside-prefix, prefix-length, duplicate, stale, or wrong-VRF) and count in no prefix; the interface carries the unmapped-domain finding instead. A wrong-VRF finding on an assignment replaces the stale finding the same assignment would otherwise raise, since the address cannot be observed in its namespace on an interface that is in another one.

Utilization

Prefix utilization is namespace-local and calculated from distinct host values and unions of address ranges or child prefixes, never from row counts. Duplicate observations and intentionally shared assignments therefore consume one host.

Utilization categories are mutually exclusive in this order: reserved managed hosts, assigned managed hosts, managed-but-unassigned hosts, observed-but-unmanaged hosts, then available space. Management mode is a filter or sub-count and does not compete with allocation categories. Child prefixes are reported separately so their space is not presented as individually available in the parent.

Neither IPv4 nor IPv6 utilization enumerates every possible address. Queries use counts and address arithmetic; RFD 8's views keep their presentation, showing IPv6 capacities as counts or qualitative occupancy where a percentage would be meaningless. The per-address map continues to draw at most 1,024 cells. Network, broadcast, and other non-assignable address policy is explicit and family-aware.

Addressing plan and allocation

An organization may define an addressing plan: an ordered set of planning levels per address family, for example /48 per site, /56 per hall, and /64 per VLAN for IPv6. The plan drives RFD 8's child-space map instead of the current guess from the most common child length, and it offers "next free" at each level: the first unallocated child prefix of the planned length, or the first unallocated host in a leaf prefix.

The plan belongs to the organization and applies in every routing table. A level is a child length, so neither a whole-space /0 nor a host length is one, and a family holds each length once. A container counts and maps its space in the first planned length of its family longer than its own, whatever children it has; past the plan’s deepest level, or with no plan, the level is still guessed from its children. Owners and admins edit the plan on the Prefixes page, and everyone sees it there and in each container’s usage, such as "5 of 256 hall /56s".

The initial allocator and all managed writes affecting allocation availability share the organization’s serialization lock, acquired in the existing topology order: organization, membership, then domain rows. This includes manual prefix creation, resizing and namespace moves, reservations, address assignments, release, and allocation. Availability is re-checked and the allocation durably recorded in the same transaction; locking only the selected parent is not enough when overlapping parents or manual writes can compete.

Free space excludes reservations, current managed hosts, active observed-only hosts, child-prefix space, and family-specific non-assignable addresses. The guarantee concerns known inventory, not unseen devices that later claim space. An observed host whose routing domain is unmapped could be in any namespace, so it occupies space in all of them rather than being handed out twice. Non-assignable addresses are the IPv4 network and broadcast addresses up to /30 (RFC 3021 leaves /31 and /32 whole), and for IPv6 the Subnet-Router anycast address up to /126 plus the reserved subnet-anycast IIDs fdff:ffff:ffff:ff80 through fdff:ffff:ffff:ffff in each /64, including narrower leaves that intersect that range (RFC 4291, RFC 2526). These repeating ranges are skipped arithmetically, not enumerated. A host is only allocated in a leaf; a prefix with children hands out space as child prefixes. An allocation is an ordinary prefix or managed address, written to Activity, and reuses a released address’s record for its host. Tests cover allocator-versus-allocator, allocator-versus-manual-write, and prefix-resize races before exposure. It is available to owners and admins in the browser; a public allocation API remains excluded.

User interaction

IPAM lives in RFD 8's Network area beside topology, VLANs, and cables. The browser surface defaults to the global table and provides optional VRF selection, the hierarchical prefix list, prefix detail and utilization, prefix create and edit forms, managed-address search, reservations and assignments, and "next free" once the addressing plan exists. Address findings appear in the Inbox and on the affected prefix, address, and resource pages. No VRF setup is required before creating global prefixes or addresses. As RFD 8 states, prefixes are readable but not editable on a phone.

The VRFs page also lists the routing domains collectors report, per source and key, with what each resolved to and how many interfaces claim it. Owners and admins map a key to a VRF or the global table, or leave it to automatic matching, and set which sources' claims are authoritative there. A resource’s interfaces show their current routing domain, and an unmapped-domain finding links to its mapping.

A prefix’s page offers owners and admins "Next free": a side panel that previews the first free child prefix of a chosen length (the planning level by default) or, in a leaf, the first free host, and allocates it with a status or allocation state, DNS name, and description. The preview follows the form, but the allocation re-reads the space when it is made. Like the other Network edits, it is hidden on a phone.

Routes belong inside the existing live_session :require_authenticated_user block and [:browser, :require_authenticated_user] pipeline because IPAM contains organization inventory and every view requires login. The LiveViews use current_scope for all context calls and derive the user from current_scope.user.

Routes are:

  • /network/prefixes (shipped)

  • /network/prefixes/:id (shipped)

  • /network/vrfs

  • /network/addresses

The earlier /ipam/…​ names are not used; the shipped /ipam/vlans and /ipam/vlan-groups redirects remain.

Mutations require an active selected organization and the same owner/admin policy used for other managed inventory changes. Read access follows existing organization membership policy. Stable DOM IDs and outcome-focused LiveView tests cover key lists, forms, filters, findings, and empty states.

Concurrency and authorization

Context functions accept the current organization scope first and apply tenant filters in every query. Cross-organization VRFs, prefixes, addresses, interfaces, and assignments cannot be linked, with composite foreign keys enforcing the boundary where practical. Mutations re-check the owner/admin membership in the database inside their transaction, as topology mutations already do, rather than trusting the roles loaded into the scope.

Reservation, assignment, adoption, and allocation mutations run transactionally and write change events through their envelopes.

Managed prefix and host uniqueness is enforced inside a namespace. Global-table rows require null-safe uniqueness, while VRF rows use their vrf_id. Observed rows deliberately do not use this managed uniqueness constraint.

Initial exclusions

This RFD does not include DHCP or DNS writes, RIR workflows, NAT modeling, route-policy management, automatic renumbering, bulk automatic allocation, public allocation APIs, or bidirectional NetBox synchronization. CSV import/export and read-only external IPAM sync can follow once the canonical model and conflict semantics are established.

VLANs are adjacent to IPAM but not required to deliver VRFs, prefixes, managed addresses, and observation findings. RFD 7 defines VLAN groups, scope, VID uniqueness, interface membership, switch evidence, and the optional prefix-to-VLAN relationship already in use.

Rollout

  1. Harden the shipped prefixes: owner/admin authorization, create, edit, and delete in the browser, the container status, per-namespace CIDR uniqueness, strict containment in the tree, and distinct-host utilization.

  2. Add optional VRFs and migrate legacy routing-domain strings without manufacturing a default VRF.

  3. Evolve managed addresses into ip_address envelopes with allocation state, management mode, role, retained masks, and role-constrained assignments; move Adopt and release onto them with change events.

  4. Change observed assignment identity to host-based matching while retaining submitted masks in evidence.

  5. Add the address Inbox domain with strict prefixes and the namespace-independent findings.

  6. Add optional routing-domain claims and mappings, then the namespace-dependent findings.

  7. Add the addressing plan and the serialized next-free allocator.

Implementation

Progress against this proposal is tracked in the RFD 4 implementation checklist.