← Back to Knowledge Base
Web Tutorials · July 10, 2026 · 16 min read

IPv6-Only Hosting: A Practical Guide to NAT64, DNS64, and Modern Address Planning

IPv6-Only Hosting: A Practical Guide to NAT64, DNS64, and Modern Address Planning

A decade ago, standing up a server without a public IPv4 address would have gotten you funny looks. Today it’s a legitimate design decision—and increasingly a budgetary one. Cloud providers now bill separately for IPv4 addresses, the RIRs blew through their free pools years ago, and the transfer market has turned IPv4 into a line item worth engineering away.

But IPv6-only isn’t purely a cost play. Done right, it flattens your addressing plan, kills off an entire category of NAT pain inside your network, and leaves your infrastructure ready for the next decade. Done wrong, it hands you mysterious hangs, silently rejected mail, and package mirrors that time out for reasons nobody can immediately explain.

What follows is the practical mechanics of making IPv6-only actually work: NAT64 and DNS64 for reaching the legacy IPv4 internet, 464XLAT for the applications that refuse to play nice, sensible address planning, and the firewall rules that will absolutely bite you if you copy your IPv4 ruleset and assume you’re finished.

Introduction to IPv6-Only Hosting

Why Providers Are Moving Toward IPv6-Only

The main driver is scarcity. There are no large IPv4 blocks left to hand out, so every new customer either shares addresses behind carrier-grade NAT or gets a dedicated address the provider had to buy on the transfer market. IPv6 allocations, by contrast, are effectively free and staggeringly large—a single /32 from your RIR holds 65,536 /48s.

The operational payoff is just as real. When your backend fleet lives entirely in IPv6, you stop fighting overlapping RFC 1918 ranges across VPCs, you stop building elaborate NAT gateways solely so VMs can reach a package repo, and every host gets a globally unique address you can route directly to.

IPv4 Exhaustion and Cost Pressures

The major clouds now charge per hour for every public IPv4 address you hold, whether it’s attached to anything or not. Across a few thousand instances that adds up to a meaningful monthly bill. IPv6-only instances dodge the charge entirely, and NAT64 at the edge still lets them reach the parts of the internet that haven’t modernized. That’s the bargain this article is about: you save on addresses, but you take on the job of translation.

Understanding the IPv6-Only Challenge

Reaching IPv4-Only External Services

The moment a host has no IPv4 address, it cannot open a socket to an IPv4 destination. That’s not policy—it’s plumbing. And a large chunk of the internet is still IPv4-only: legacy APIs, plenty of package mirrors, payment gateways, GitHub for a long time, and no end of third-party endpoints your application leans on.

The answer is a translation layer. Your IPv6-only host believes it’s talking to an IPv6 destination, and a translator at the network edge rewrites those packets into IPv4 on the way out. That’s the job of NAT64, paired with DNS64 to keep the whole thing invisible to applications.

Legacy Application and Library Compatibility

Before you flip anything to IPv6-only, audit your software. The failures rarely live in the OS or the kernel—they live in application code and libraries. The usual suspects:

  • Hardcoded IPv4 literals in config files or code (127.0.0.1 is fine; a baked-in public IPv4 is not).
  • Older database connectors and ORMs that resolve or bind to IPv4 by default.
  • Health checks and monitoring agents that assume an IPv4 loopback or metadata endpoint.
  • Third-party SDKs that phone home over IPv4 with no AAAA record and no notion of NAT64.

Clients that implement Happy Eyeballs (RFC 8305) help enormously—they race IPv6 and IPv4 connection attempts and take whichever finishes first, masking latency and avoiding long timeouts on broken paths. Modern browsers, curl, and most current language runtimes do this. Old ones don’t, which is one more reason to check versions before you commit.

NAT64 and DNS64 Fundamentals

How NAT64 Address Translation Works

NAT64 maps IPv6 packets to IPv4 and back. The trick is embedding the IPv4 destination inside an IPv6 address using a well-known prefix. RFC 6052 reserves 64:ff9b::/96 for this, so the IPv4 address 192.0.2.25 becomes 64:ff9b::192.0.2.25 (which renders as 64:ff9b::c000:219).

When your host sends a packet to any address inside that /96, the NAT64 translator strips the prefix, pulls out the embedded IPv4 address, and forwards the packet over IPv4 using its own IPv4 address (or a pool) as the source—much like traditional NAPT. Return traffic gets translated back. Your host never needs an IPv4 address of its own; the translator owns that side of the conversation.

Synthesizing AAAA Records with DNS64

Now the missing piece: your application does a DNS lookup for api.example.com, which has only an A record. An IPv6-only host can’t do anything with an A record. DNS64 fixes this by synthesizing a fake AAAA on the fly—it takes the A record’s IPv4 address, prepends the NAT64 prefix, and returns 64:ff9b::c000:219 to the client.

The client connects to that IPv6 address, which routes to your NAT64 translator, which translates back to the real IPv4 host. If a domain already publishes a genuine AAAA record, DNS64 leaves it alone and native IPv6 is used. This is precisely why the two have to be deployed together: DNS64 without NAT64 hands out addresses that route nowhere, and NAT64 without DNS64 forces apps to build the synthesized addresses themselves.

Deploying a Translator with Jool or Tayga

On Linux the two workhorses are Jool (a kernel module—stateful, high performance) and Tayga (a userspace, stateless TUN-based translator that’s simpler but slower). For a production edge, Jool is the usual pick.

A minimal stateful Jool setup, once the kernel modules are installed:

# Load the NAT64 instance
modprobe jool
jool instance add "edge" --netfilter --pool6 64:ff9b::/96

# Give Jool an IPv4 address pool to source from
jool -i "edge" pool4 add 198.51.100.10 --tcp 61001-65535
jool -i "edge" pool4 add 198.51.100.10 --udp 61001-65535
jool -i "edge" pool4 add 198.51.100.10 --icmp

# Verify
jool -i "edge" global display

Enable forwarding and make sure traffic destined for the NAT64 prefix actually reaches the translator box:

sysctl -w net.ipv6.conf.all.forwarding=1
sysctl -w net.ipv4.conf.all.forwarding=1
# On upstream routers, route 64:ff9b::/96 toward the Jool host

Tayga, by contrast, runs as a daemon writing to a nat64 TUN interface and relies on your ordinary IPv4 NAT to masquerade the translated traffic. It’s a fine choice for low-traffic edges or lab environments where you’d rather not load a kernel module.

Configuring DNS64 in BIND or Unbound

Both major recursive resolvers support DNS64 natively. In BIND, drop it into the options block:

options {
    // ... existing config ...
    dns64 64:ff9b::/96 {
        clients { any; };
        exclude { ::ffff:0:0/96; };
        // suppress synthesis for RFC 1918-mapped junk, etc.
    };
};

In Unbound the equivalent lives in the module chain:

server:
    module-config: "dns64 validator iterator"
    dns64-prefix: 64:ff9b::/96
    # dns64-synthall: no   # only synthesize when no AAAA exists

Point your IPv6-only hosts at this resolver and the entire chain vanishes from the application’s point of view. One caution: DNS64 and DNSSEC interact awkwardly, because synthesizing an AAAA from a signed A record breaks the signature. Validating resolvers deal with this by validating the A record before synthesis, but understand that the returned AAAA is unsigned by design.

464XLAT and CLAT for Client Compatibility

CLAT on Clients, NAT64 at the Edge

NAT64 plus DNS64 covers the common case, but it has a blind spot: applications that skip DNS entirely and connect straight to IPv4 literals. A hardcoded connect("203.0.113.5", ...) never triggers DNS64, so there’s no synthesized AAAA and the app dies on an IPv6-only host.

464XLAT closes that gap. The client runs a CLAT (customer-side translator) that presents a local IPv4 interface to applications and translates their IPv4 packets into IPv6 using the NAT64 prefix, which the edge PLAT (the NAT64 translator) then handles as usual. To the application, IPv4 still exists locally; on the wire, it’s IPv6 all the way.

On Linux you can implement CLAT with Jool’s SIIT mode (jool_siit) or the clatd daemon. This is exactly the mechanism mobile carriers use to run IPv6-only networks while apps keep believing they have IPv4. If your workload includes software you can’t fix that insists on IPv4 literals, CLAT is your escape hatch.

When to Use Dual-Stack Instead

Don’t get religious about pure IPv6-only. For public-facing services—your website, your API, your mail server—dual-stack is still the right answer. A huge fraction of the world’s clients only have IPv4, and you have to be reachable by them. Run dual-stack on the front door.

Reserve IPv6-only for backend fleets: application servers, workers, build agents, internal databases. There, an IPv6-only design with NAT64 for outbound legacy access gives you the cost and simplicity wins without cutting off a single real user. The load balancer or reverse proxy at the edge bridges IPv4 clients through to your IPv6-only backends.

IPv6 Address Planning and Allocation

The /64 Minimum and Why It Matters

The single most important rule in IPv6 planning: never subnet smaller than a /64. SLAAC needs a /64 to autoconfigure addresses, and a range of protocols and implementations assume 64 bits of interface identifier. Hand out a /112 or /120 to “save space” and you’ll break autoconfiguration and stumble into odd corner cases. There’s no space to save anyway—a /64 holds 18 quintillion addresses, and you have effectively unlimited /64s to give away.

Treat the /64 as the atomic unit of an IPv6 segment. One /64 per link, per VLAN, per broadcast domain.

Delegating /56 and /48 Blocks to Customers and VMs

When a customer or tenant needs multiple networks, delegate a block of /64s, not individual addresses. The conventional sizes:

  • /48 — a full site allocation; 65,536 /64 subnets. Generous, and standard for a customer running their own segmented network.
  • /56 — 256 /64 subnets; a common compromise for smaller tenants or per-VM delegation where a full /48 is overkill.
  • /64 — a single segment, right for one VM or one interface that won’t sub-delegate.

A clean plan might look like this: the RIR assigns you a /32, you carve /48s per data center or per major customer, then /56s or /64s within. Leave room and align on nibble boundaries (/48, /52, /56, /60, /64) so your reverse DNS delegation stays sane.

Per-Host and Per-Interface Subnetting

In a virtualized or container environment, it’s common to route a /64 to each hypervisor and let the guests draw addresses from it, or to route a /64 per guest for full isolation. Routing (rather than bridging) a dedicated prefix to each host sidesteps Neighbor Discovery scaling problems on large shared segments and makes per-tenant firewalling trivial—you filter on prefix.

Firewall Configuration for IPv6

ip6tables vs nftables inet Family

Here’s the mistake that catches almost everyone: your IPv4 firewall rules do not apply to IPv6 traffic. A pristine iptables ruleset with a default-drop policy provides exactly zero protection for IPv6. If you enabled IPv6 without touching ip6tables, your hosts may be wide open on their global addresses right now.

On modern systems, prefer nftables with the inet family, which handles IPv4 and IPv6 in one ruleset and eliminates the duplication that causes drift:

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        ct state established,related accept
        iif "lo" accept

        # ICMPv6 — required, see below
        ip6 nexthdr icmpv6 icmpv6 type {
            echo-request, echo-reply,
            nd-neighbor-solicit, nd-neighbor-advert,
            nd-router-solicit, nd-router-advert,
            packet-too-big, time-exceeded, parameter-problem,
            destination-unreachable
        } accept

        # Services
        tcp dport { 22, 80, 443 } accept
    }
    chain forward { type filter hook forward priority 0; policy drop; }
    chain output  { type filter hook output priority 0; policy accept; }
}

Default-Deny Policies for IPv6 Traffic

Same philosophy as IPv4: drop by default, permit explicitly. The catch with IPv6 is that “drop everything, then allow my services” will silently break the network layer itself unless you carve out ICMPv6—because IPv6 depends on ICMPv6 far more heavily than IPv4 depends on ICMP.

Permitting Required ICMPv6

Never blanket-drop ICMPv6. IPv6 has no in-flight fragmentation by routers—Path MTU Discovery relies entirely on Packet Too Big messages. Filter those and you get the classic failure: small requests work, large responses hang forever. Neighbor Discovery (IPv6’s replacement for ARP) also runs over ICMPv6; block it and hosts can’t even resolve their on-link neighbors.

At a minimum permit: packet-too-big, destination-unreachable, time-exceeded, parameter-problem, the four Neighbor Discovery types, and echo request/reply. RFC 4890 is the reference for exactly which ICMPv6 types to allow through firewalls and which to filter.

DNS and Reverse Records

Forward DNS is only half the story. Configure PTR records under ip6.arpa for any service that gets reverse-DNS-validated—mail servers above all else. A missing IPv6 PTR is one of the most common reasons a perfectly configured mail server sees its messages rejected or spam-scored when sending over IPv6.

The ip6.arpa format is tedious: every nibble reversed, dot-separated. For 2001:db8::25 the PTR zone entry expands to a nibble sequence ending in ...8.b.d.0.1.0.0.2.ip6.arpa. Use a generator or your DNS provider’s tooling rather than hand-writing these—it’s an easy place to fat-finger a nibble. And make forward and reverse agree: a mail server’s PTR should resolve to a name whose AAAA points back to the same address.

MTU and Path MTU Discovery

Get MTU right, or expect intermittent, maddening hangs. Native IPv6 links typically run 1500 bytes, but tunneled or translated paths may be smaller. Since routers can’t fragment IPv6 packets, the source has to learn the smallest MTU on the path via Packet Too Big messages—which only works if you allowed that ICMPv6 type through every firewall along the way.

If you can’t guarantee ICMPv6 delivery end to end (some upstreams still filter it), clamp TCP MSS as a defensive measure:

# nftables MSS clamping to path MTU
table inet mangle {
    chain forward {
        type filter hook forward priority mangle;
        tcp flags syn tcp option maxseg size set rt mtu
    }
}

Best Practices

  • Run dual-stack on public-facing edges and reserve IPv6-only for internal backend fleets.
  • Deploy NAT64 and DNS64 together; neither is useful alone. Use the well-known 64:ff9b::/96 prefix unless you have a reason not to.
  • Add 464XLAT/CLAT for workloads that use IPv4 literals and can’t be fixed.
  • Never subnet below a /64; delegate /56 or /48 to tenants and align allocations on nibble boundaries.
  • Write an explicit IPv6 firewall—prefer the nftables inet family—with default-drop and a deliberate ICMPv6 allowlist per RFC 4890.
  • Always permit Packet Too Big and Neighbor Discovery ICMPv6 types; clamp MSS where ICMPv6 delivery is uncertain.
  • Configure ip6.arpa PTR records for mail and any rDNS-validated service, and keep forward/reverse consistent.
  • Audit applications and libraries for hardcoded IPv4 and old connectors before cutover; prefer Happy Eyeballs–aware clients.
  • Monitor the NAT64 translator’s IPv4 source pool and port utilization—it’s a shared, exhaustible resource.

Troubleshooting

SymptomLikely CauseFix
Small requests work, large transfers hangICMPv6 Packet Too Big filtered; broken PMTUDAllow ICMPv6 type 2 end to end; clamp TCP MSS to path MTU as backup
Host can’t reach IPv4-only site by hostnameDNS64 not configured or resolver returning A onlyPoint host at a DNS64 resolver; verify a synthesized AAAA is returned
Synthesized AAAA returned but connection times outNAT64 prefix not routed to translator, or Jool pool4 exhaustedRoute 64:ff9b::/96 to the translator; widen the IPv4 port pool
App connects to IPv4 literals but not hostnamesApp bypasses DNS; no DNS64 triggerDeploy CLAT (464XLAT) on the host, or fix the app to resolve names
Outbound mail rejected or spam-scored over IPv6Missing or mismatched ip6.arpa PTR recordCreate PTR under ip6.arpa; ensure forward/reverse names match
IPv6 traffic reaching services you meant to blockip6tables/nftables inet rules never writtenApply an explicit default-drop IPv6 ruleset; don’t rely on IPv4 rules
Hosts can’t reach on-link neighbors or gatewayNeighbor Discovery ICMPv6 dropped by firewallPermit ND solicit/advert and router solicit/advert ICMPv6 types
DNSSEC validation errors for translated namesSynthesized AAAA breaks the A record signatureEnsure resolver validates the A before synthesis; expect unsigned AAAA

Conclusion

IPv6-only hosting has matured from an experiment into a sound, cost-effective architecture—provided you respect its two hard requirements: a working translation layer for the legacy IPv4 internet, and an IPv6-native firewall that treats ICMPv6 as essential rather than optional. NAT64 and DNS64 handle the common case transparently; 464XLAT with CLAT rescues the applications that flatly refuse to cooperate.

Plan your address space generously with /64s as the floor, delegate in /56 or /48 blocks, and don’t skip the unglamorous details—PTR records and MTU—that quietly break production the moment they’re overlooked. Keep dual-stack on the parts of your infrastructure that face the public, and let your backends live comfortably in an IPv6-only world.

Start with a pilot fleet, put a NAT64/DNS64 pair in front of it, audit what breaks, and expand from there. The address economics will only tilt further in your favor, and the operational simplicity of a single, globally routable address per host is well worth the migration effort.

Frequently Asked Questions

How does an IPv6-only server reach IPv4-only external services?

You deploy a NAT64 translator such as Jool or Tayga at the network edge alongside a DNS64-capable resolver like BIND or Unbound. DNS64 synthesizes AAAA records from IPv4-only A records, mapping them into the NAT64 prefix (commonly 64:ff9b::/96). Traffic to those synthesized addresses is then translated to real IPv4 by the NAT64 gateway.

Why should I never subnet smaller than a /64 in IPv6?

A /64 is the smallest recommended subnet because SLAAC (Stateless Address Autoconfiguration) and many IPv6 features rely on a full 64-bit interface identifier. Allocating smaller blocks breaks address autoconfiguration and Neighbor Discovery behavior. Reserve at least a /64 per interface or host, and delegate /56 or /48 blocks to customers and VMs.

Do my existing IPv4 firewall rules protect IPv6 traffic?

No. IPv4 rules in iptables do not apply to IPv6 traffic at all, leaving hosts exposed if you don’t add explicit IPv6 rules. Use ip6tables or the nftables inet family to enforce a default-deny policy for IPv6, and remember to permit required ICMPv6 types for Neighbor Discovery and PMTUD.

Why does filtering ICMPv6 break connectivity?

Unlike IPv4, IPv6 routers do not fragment packets, so Path MTU Discovery depends entirely on ICMPv6 ‘Packet Too Big’ messages. Blocking all ICMPv6 also disrupts Neighbor Discovery, which replaces ARP in IPv6. The result is hung connections and silent failures, so you must explicitly allow the necessary ICMPv6 types.

Why do my IPv6 mail servers get rejected by other SMTP hosts?

Many mail servers require valid reverse DNS (PTR) records for the sending IP, and missing IPv6 rDNS is a common cause of SMTP rejections. You must configure PTR records under the ip6.arpa zone for every mail-sending IPv6 address. Ensure the forward and reverse records match to protect deliverability and reputation.

Should I go fully IPv6-only or keep dual-stack?

For public-facing services, dual-stack remains the safest choice for maximum client compatibility. True IPv6-only backends can work if you combine NAT64 at the edge with 464XLAT/CLAT on clients to preserve access to legacy IPv4 endpoints. Always audit your applications, libraries, and third-party APIs for hardcoded IPv4 literals before committing.