Non-routed GUA behind NPTv6
The idea in one line: number your internal network out of global-unicast (GUA) space that is never routed, and translate it to whatever prefix your ISP currently gives you at the boundary, with NPTv6 — stateless 1:1 prefix translation (RFC 6296). Internally, your addressing is stable and first-class. Externally, only the ISP’s current prefix ever appears. The rotation is absorbed at the edge.
Why GUA and not ULA
The one property you actually need internally is first-class address selection. RFC 6724’s default
policy table ranks ULA (fc00::/7) below IPv4, so a dual-stack host prefers IPv4 when the alternative
is ULA. GUA-shaped space sits at the top of the table, so hosts prefer IPv6 with no per-host policy override. Because
NPTv6 hides the internal prefix behind the ISP’s, the internal GUA never needs to be globally routable — it
only needs to be GUA-shaped. That is exactly what a non-routed GUA block gives you.
The two planes
Making this safe means getting two independent things right:
- Data plane — the block is a sink. A blackhole route ensures any stray packet addressed to the dark block that reaches your router is dropped rather than forwarded.
- Control plane — the block is un-originatable. An AS0 ROA (RFC 7607) in RPKI declares that no autonomous system is authorized to originate the prefix, so any leaked announcement of it is RPKI-invalid and dropped by validating networks.
The blackhole protects you; the AS0 ROA protects everyone else from an accidental leak out of your edge. You want both.
Carve the block
Give each site or purpose a sub-prefix out of the /48. Two non-overlapping /60s, for example, are plenty
for most single-site deployments and leave room to grow:
# edit-required values as variables (adjust to taste) DARK48=2602:f925:ad::/48 SITE_A=${DARK48%::/48}::/60 # e.g. 2602:f925:ad::/60 SITE_B=${DARK48%::/48}:bb00::/60 # e.g. 2602:f925:ad:bb00::/60
NPTv6 at the boundary (VyOS example)
Translate the internal /60 to the low /60 of your current ISP PD on the way out, and the
reverse on the way in. NPTv6 is stateless and checksum-neutral — the host bits pass through unchanged, only the
prefix is rewritten:
# edit-required values as variables set INTERNAL = 2602:f925:ad::/48 # your dark /48 (or a /60 out of it) set ISP_PD60 = 2001:db8:abcd:e000::/60 # the /60 you carve from the ISP's PD set WAN_IF = eth0 # ISP-facing interface # source (egress): internal -> ISP set nat66 source rule 100 outbound-interface name '$WAN_IF' set nat66 source rule 100 source prefix '$INTERNAL' set nat66 source rule 100 translation address '$ISP_PD60' # destination (ingress): ISP -> internal set nat66 destination rule 100 inbound-interface name '$WAN_IF' set nat66 destination rule 100 destination address '$ISP_PD60' set nat66 destination rule 100 translation address '$INTERNAL'
If your ISP rotates the PD, a short reconciler that reads the current delegation and rewrites the two translation addresses in place keeps egress live across rotations — edit the rules, don’t delete-and-recreate them, so active sessions aren’t dropped.
Blackhole the dark block (data plane)
# pin it hard so nothing accidentally overrides it
set protocols static route6 '2602:f925:ad::/48' blackhole distance '1'
Publish the AS0 ROA (control plane)
In your RIR’s hosted RPKI, create a Route Origin Authorization with origin AS0 for the dark
block. Because AS0 means “no AS may originate this,” any real-AS announcement of the prefix becomes
RPKI-invalid. Set the ROA’s max length wide (e.g. /128) — for a block that is never
routed at any granularity, a wide max length simply extends the “do-not-route” assertion to cover every
possible more-specific leak, with no downside.
Origin AS: AS0 Prefix: 2602:f925:ad::/48Max length: 128 # wide = covers every sub-prefix leak; dark space has no downside Auto-renew: yes # so it never silently expires
Note: AS0 is an RPKI/ROA concept. IRR route objects reject AS0 as an origin, so the
do-not-route assertion lives in RPKI, not in an IRR route6 object.
If you are using the shared block
If you don’t have your own allocation, you can number out of the published example block, 2602:f925:ad::/48,
which is already held dark with an AS0 ROA and origin blackhole by its maintainer. In that case you skip the
“publish the ROA” and “blackhole” steps — they’re already done — and you only
need the carve + NPTv6 steps on your own edge. Read the block’s benefits and its limits on
The Block before relying on it.
The honest edge
The AS0 ROA makes the block invalid on any RPKI-validating path, and the global return table plus the maintainer’s origin blackhole cover essentially everything else. The one residual gap is traffic that both originates and terminates inside a single network that doesn’t validate RPKI — and that gap exists for all address space, not just this technique. Your NPTv6 boundary is the backstop: never route the dark block; always translate at the edge. Treat the AS0 ROA as a belt over those suspenders, not a replacement for them.