Your server responds to IPv6, you can even ping it from inside with ping6, but a user with IPv6 internet says the site won't open. Or worse: the site works for most people, and only a percentage of mobile users see a blank page. This almost always means one link in the IPv6 chain is half-done. The chain is: address on the interface, default route, AAAA record in DNS, service listening, and firewall. If any of these five links is missing, the result is exactly this contradictory behavior.
The first thing you should do is figure out whether the problem is on the server side or in the network path. On the server itself:
ip -6 addr show dev eth0
ip -6 route show default
curl -6 -I https://example.com
ss -6 -tlnp | grep :443
If ip -6 addr shows an address but ip -6 route show default is empty, the server has an address but no way out. This is very common on VPSes where you've added the IPv6 address manually. If the route exists but ss -6 shows nothing, it means Nginx or Apache is only listening on IPv4.
Why Nginx doesn't listen on IPv6
The default in many old configs is:
listen 443 ssl;
listen [::]:443 ssl;
You have to add the second line manually. If it's only listen 443 ssl;, the socket opens on 0.0.0.0 and IPv6 has nothing to do with it. After adding it, run nginx -t and then systemctl reload nginx. Note that if you mistakenly set ipv6only=on on a socket that also takes IPv4, the port may fail to bind again and you'll get an address already in use error.
With Apache the story is different. With just Listen 80, Apache doesn't listen on IPv6; you must write Listen [::]:80 separately. In newer versions, if you set Listen 80 and the system has net.ipv6.bindv6only=0, a single socket takes both. This difference between distributions means a config copied from one server behaves differently on another.
Where and how to set the AAAA record
The AAAA record is the IPv6 equivalent of the A record and only takes an IPv6 address. If you set a wrong or stale address, the browser tries to connect to it and falls back to IPv4 after a timeout. This fallback exists in modern browsers, but it adds a few seconds of delay, and in some clients (like old HTTP libraries) it doesn't exist at all.
example.com. 300 IN AAAA 2001:db8:1::10
www.example.com. 300 IN AAAA 2001:db8:1::10
The number 300 is the TTL. If you're testing, set the TTL low so changes propagate quickly; once things are stable you can go back to 3600. One practical tip: if your server has IPv6 but the service isn't listening on it, don't set the AAAA record. Not having an AAAA record is better than having a wrong one, because the client quickly goes to IPv4.
To check that the record has propagated correctly, use network tools like ServerNet network tools or dig AAAA example.com @1.1.1.1. If the answer differs across resolvers, the old cache hasn't been cleared yet.
The firewall you forget
This is where people make mistakes. Someone runs ufw allow 443, IPv4 opens up, they test, it works, done. Then the site won't open for IPv6 users and they spend hours looking for the problem in Nginx. The ufw rule is that IPv4 and IPv6 rules are separate and must be specified with ipv6:
ufw allow proto tcp from any to any port 443
ufw allow proto tcp from any to any port 443
In fact, by default, if IPV6=yes is in /etc/default/ufw, ufw writes both together. But if that line is no, only IPv4 is written and you see no error at all. The sign is this: ufw status verbose shows the rule, but ip6tables -L -n is empty.
The same story applies to raw iptables. The INPUT chain in iptables and ip6tables are two completely separate things. If you have a firewall script that only calls iptables, your IPv6 is open and unprotected. This is the worst case: the service works, the test passes, but management ports are open to the entire internet over IPv6.
Dual-stack in practice: the cost nobody accounts for
Dual-stack means the service listens on IPv4 and IPv6 at the same time. The benefit is obvious. The cost is that the attack surface doubles and logs have two kinds of addresses. If you have fail2ban or rate-limiting, you must make sure it sees IPv6 addresses too. A single IPv6 address can be an entire /64; if you rate-limit on one address, the attacker simply continues from the next address in the same subnet. In Nginx you should carefully configure limit_req_zone on $binary_remote_addr and know that this variable is sixteen bytes for IPv6, not four.
Second point: if you're behind a CDN, you must parse the X-Forwarded-For header correctly for IPv6 as well. Many ready-made configs only have IPv4 ranges in set_real_ip_from, and the real IPv6 user address gets logged as the CDN address.
Which should I choose?
If you have a public site and traffic comes from mobile and large networks, enable dual-stack. If it's an internal service and all clients are on IPv4, enabling IPv6 only adds attack surface with no benefit. In practice, for public web servers I choose dual-stack, but only after I've prepared the IPv6 firewall and monitoring before the AAAA record. Order matters: firewall first, then service, then DNS. If you go the other way, you have a window where the service is open on IPv6 and nobody is protecting it.
For heavier workloads where you want full control over the network stack, a dedicated server is a more sensible choice than a shared VPS, because you can configure routing and firewall yourself without platform limitations. If the workload is lighter and you just want to get going quickly, a cloud server with a dedicated IPv6 address does the same with less setup. For projects that need a stable machine with an Iran address, an Iran VPS is a conventional starting point.
Final test before you publish the AAAA record
- On the server:
ss -6 -tlnpshould show port 443 on[::]. - From outside:
curl -6 -I https://example.comshould return headers, not a timeout. - Firewall:
ip6tables -L -nshould have your rules, not an empty chain. - DNS:
dig AAAA example.com +shortshould be the same address you have on the interface. - SSL: the certificate should be valid for that domain; if you use Let's Encrypt, the HTTP-01 challenge must work over IPv6 too.
If any of these five fails, don't publish the AAAA record. We also have a supplementary guide on IPv6 and its importance in modern infrastructure that covers the transition and compatibility discussion. For configuration details of specific services, the documentation and knowledge base is a more precise reference.
Frequently asked questions
Why doesn't the site open over IPv6 but works over IPv4?
Usually one of three things: the service isn't listening on IPv6, the IPv6 firewall has no rules, or the AAAA record points to the wrong address. With ss -6 -tlnp and ip6tables -L -n and dig AAAA, you can distinguish all three in a few minutes.
Does removing the AAAA record fix the problem?
Temporarily, yes. If you remove the AAAA record, clients quickly go to IPv4 and the site opens. But this is a workaround, not a cure; if the goal is to enable IPv6, you need to fix the service and firewall and then put the record back.
Does ufw block IPv6 by default too?
Only if IPV6=yes is in /etc/default/ufw. Otherwise your rules only apply to IPv4 and ip6tables -L -n stays empty, while ufw status shows everything as normal.
How do I know users are actually connecting over IPv6?
Look for addresses with colons in the web server log. If you're behind a CDN, you need to add the X-Forwarded-For header to the log format, otherwise you only see the CDN's own IPv4 or IPv6 address and lose the real user statistics.
Comments 0
No comments yet — be the first!