IPv4 and IPv6 are the two versions of the Internet Protocol in use today. IPv4 still carries most traffic, but its address pool ran out years ago, and a large and growing share of users now reach the Internet over IPv6, especially on mobile networks. This guide explains how the two protocols differ, what that means for a server, and how to configure and test a dual-stack Ubuntu 24.04 server so it answers on both.
At a glance
| IPv4 | IPv6 | |
|---|---|---|
| Address size | 32 bits (about 4.3 billion addresses) | 128 bits (about 3.4 x 10^38 addresses) |
| Notation | Dotted decimal: 203.0.113.10 | Hexadecimal groups: 2001:db8::10 |
| Typical allocation to a server | One address, sometimes a few | A /64 subnet or larger |
| Address configuration | Static or DHCP | Static, SLAAC or DHCPv6 |
| Neighbor discovery | ARP (broadcast) | NDP over ICMPv6 (multicast) |
| NAT | Common, often required | Not needed, every host can have a global address |
| Header | 20 to 60 bytes, variable, with checksum | Fixed 40 bytes, no checksum, extension headers |
| Fragmentation | Routers and senders | Sender only, relies on Path MTU Discovery |
| Minimum MTU | 68 bytes (576 must be reassembled) | 1280 bytes |
| DNS record | A | AAAA |
| Reverse DNS zone | in-addr.arpa | ip6.arpa |
The addresses used in this guide come from the ranges reserved for documentation (203.0.113.0/24 and 2001:db8::/32). Replace them with your own.
Key differences that matter on a server
Address space and NAT
With IPv4, addresses are scarce and cost money, so providers hand out one public address per server and private networks sit behind NAT. NAT breaks end-to-end connectivity: inbound connections need port forwarding, and protocols that embed addresses (SIP, FTP active mode, some games) need helpers. Carrier-grade NAT on ISP networks also means many clients share a single public IPv4, which makes rate limiting and abuse blocking by IP less precise.
With IPv6, a server usually gets a whole /64 or more. Every service or container can have its own global address, and no translation happens on the path. That simplifies logging and firewall rules, but it also means a firewall is now the only thing standing between the Internet and your hosts, not NAT.
Header and performance
The IPv6 header has a fixed size and drops the checksum, which makes forwarding simpler for routers. In practice the performance difference between the two protocols is small and depends far more on the route each one takes: IPv4 and IPv6 traffic can follow different paths through different providers. Measure both from where your users are instead of assuming one is faster.
Security
Neither protocol is secure by itself. IPsec was mandatory in the original IPv6 specification but is now optional in both, so it is not a practical difference. What does change:
- ICMPv6 is required. IPv6 uses ICMPv6 for neighbor discovery, router advertisements and Path MTU Discovery. Blocking all ICMPv6 breaks connectivity in subtle ways, such as connections that hang when packets get larger.
- Scanning is harder but not impossible. A /64 is too large to sweep, but addresses like
::1or ones derived from the MAC address are easy to guess. - Firewalls must cover both. A common mistake is to write strict IPv4 rules and leave IPv6 wide open. On Ubuntu, UFW applies each rule to both protocols as long as
IPV6=yesis set in/etc/default/ufw, which is the default.
Which one should you use?
Run dual-stack (recommended for public services). Serve both IPv4 and IPv6. IPv4 keeps you reachable from every network, and IPv6 gives a direct path to the many clients that prefer it, avoids carrier-grade NAT on mobile networks and prepares you for IPv6-only networks. Modern clients use Happy Eyeballs (RFC 8305) to try both and pick the one that connects first, so a working dual-stack server has no downside for users.
IPv4 only is acceptable for internal services on a private network, legacy software that cannot bind to IPv6, or when your upstream provider does not offer IPv6. Even then, keep IPv6 enabled in the kernel: many tools expect the loopback ::1 to exist.
IPv6 only fits internal infrastructure you fully control (Kubernetes clusters, backend networks, IoT fleets) where the address space makes planning easier. For anything that must be reachable by the public, you still need IPv4 or a translation service (NAT64/DNS64) in front of it, because a part of the Internet still has no IPv6.
The rest of this guide sets up and verifies a dual-stack server.
Prerequisites
- A server running Ubuntu 24.04 LTS with both a public IPv4 and a public IPv6 address, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - A domain name if you want to publish
AandAAAArecords.
Step 1 - Checking your IPv4 and IPv6 configuration
List the addresses on the server:
ip -brief address
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0 UP 203.0.113.10/24 2001:db8:1234::10/64 fe80::5054:ff:fe12:3456/64
The address starting with fe80:: is link-local: every IPv6 interface has one, and it only works on the local link. You need a global address (here 2001:db8:1234::10) for Internet traffic.
Check that both protocols have a default route:
ip -4 route show default
ip -6 route show default
default via 203.0.113.1 dev eth0 proto static
default via 2001:db8:1234::1 dev eth0 proto static metric 1024 pref medium
On Ubuntu 24.04 addresses are managed by Netplan. If the IPv6 address or route is missing, check the files in /etc/netplan/: a static IPv6 configuration lists the address under addresses: and the gateway in a routes: entry with to: "::/0". Apply changes with sudo netplan try, which rolls back automatically if you lose connectivity.
Step 2 - Testing connectivity over each protocol
Test outbound connectivity on each protocol separately:
ping -4 -c 3 one.one.one.one
ping -6 -c 3 one.one.one.one
Then check which public address the Internet sees for each protocol:
curl -4 https://icanhazip.com
curl -6 https://icanhazip.com
203.0.113.10
2001:db8:1234::10
If the IPv6 command fails with Network is unreachable, there is no IPv6 default route. If it times out, something upstream or a firewall is dropping IPv6 traffic.
To see the path each protocol takes, install mtr, which works for both:
sudo apt install mtr-tiny
mtr -4 -rwc 10 one.one.one.one
mtr -6 -rwc 10 one.one.one.one
Step 3 - Making services listen on both protocols
Services must be told to listen on IPv6. The details depend on the software.
Nginx
In the server block, add an IPv6 listen directive next to the IPv4 one:
sudo nano /etc/nginx/sites-available/your_domain
server {
listen 80;
listen [::]:80;
server_name your_domain;
root /var/www/your_domain;
index index.html;
}
The default site on Ubuntu already includes listen [::]:80 default_server;. Test and reload:
sudo nginx -t
sudo systemctl reload nginx
MySQL 8
MySQL 8 accepts a comma-separated list of addresses in bind-address, or * for all IPv4 and IPv6 addresses:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 127.0.0.1,::1
Listening only on loopback addresses is the safe default. Add a specific private or public address only if other servers must connect, and restrict access with the firewall.
PostgreSQL
In postgresql.conf, listen_addresses takes a comma-separated list, and '*' means all IPv4 and IPv6 addresses:
listen_addresses = 'localhost'
localhost resolves to both 127.0.0.1 and ::1. Clients connecting over IPv6 must also be allowed in pg_hba.conf with an IPv6 CIDR, for example 2001:db8:1234::/64.
Verifying the listening sockets
sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=1234,fd=7))
A line with 0.0.0.0 accepts IPv4, a line with [::] accepts IPv6.
Step 4 - Applying firewall rules to both protocols
Confirm that UFW manages IPv6:
grep IPV6 /etc/default/ufw
IPV6=yes
Rules without an address apply to both protocols automatically:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)
Rules that name a source address apply only to that protocol, so an IPv4 allowlist for a database port needs a matching IPv6 rule if clients connect over IPv6:
sudo ufw allow from 2001:db8:5678::/64 to any port 5432 proto tcp
UFW's default rules already allow the ICMPv6 messages IPv6 needs. Do not add rules that drop all ICMPv6.
Step 5 - Publishing DNS records and testing from outside
Point both records at the server in your DNS provider:
| Type | Name | Value |
|---|---|---|
| A | your_domain | 203.0.113.10 |
| AAAA | your_domain | 2001:db8:1234::10 |
Check them:
dig +short A your_domain
dig +short AAAA your_domain
Then test the site over each protocol from another machine that has IPv6:
curl -4 -I http://your_domain
curl -6 -I http://your_domain
Both should return HTTP/1.1 200 OK. To test an IPv6 address directly, put it in brackets: curl -I http://[2001:db8:1234::10]/.
Importantonly publish an
AAAArecord once the service really works over IPv6. Clients with IPv6 try it first, and a broken IPv6 path makes the site slow or unreachable for them even though IPv4 works.
Troubleshooting
The server has an IPv6 address but no outbound IPv6 connectivity. Check ip -6 route show default. If the route is missing and your provider uses router advertisements, make sure the interface accepts them (sysctl net.ipv6.conf.eth0.accept_ra should not be 0); otherwise add the gateway to Netplan as described in Step 1.
IPv6 is disabled in the kernel. Check:
sysctl net.ipv6.conf.all.disable_ipv6
A value of 1 means it was disabled, usually by a file in /etc/sysctl.d/ or by ipv6.disable=1 on the kernel command line. Remove that setting and reboot.
Connections work for small requests but hang on large responses. This is typically a Path MTU problem caused by a firewall dropping ICMPv6 "Packet Too Big" messages. Allow ICMPv6 along the path.
A client prefers IPv6 but you need it to use IPv4. On Linux clients, uncomment this line in /etc/gai.conf to make the system resolver prefer IPv4 addresses:
precedence ::ffff:0:0/96 100
This is a client-side workaround. The real fix is making the IPv6 path work.
Conclusion
IPv6 is not a replacement you switch to on a fixed date: for the foreseeable future public services need both. Running dual-stack costs little on a modern Linux server, as long as every service listens on both protocols, the firewall covers both, and you only publish AAAA records after testing. As next steps, add HTTPS with Let's Encrypt for both records, set up reverse DNS (PTR) for your IPv6 address if the server sends email, and monitor your site over IPv4 and IPv6 separately so a failure on one does not go unnoticed.
