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

IPv4IPv6
Address size32 bits (about 4.3 billion addresses)128 bits (about 3.4 x 10^38 addresses)
NotationDotted decimal: 203.0.113.10Hexadecimal groups: 2001:db8::10
Typical allocation to a serverOne address, sometimes a fewA /64 subnet or larger
Address configurationStatic or DHCPStatic, SLAAC or DHCPv6
Neighbor discoveryARP (broadcast)NDP over ICMPv6 (multicast)
NATCommon, often requiredNot needed, every host can have a global address
Header20 to 60 bytes, variable, with checksumFixed 40 bytes, no checksum, extension headers
FragmentationRouters and sendersSender only, relies on Path MTU Discovery
Minimum MTU68 bytes (576 must be reassembled)1280 bytes
DNS recordAAAAA
Reverse DNS zonein-addr.arpaip6.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 ::1 or 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=yes is 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 sudo privileges.
  • A domain name if you want to publish A and AAAA records.

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:

TypeNameValue
Ayour_domain203.0.113.10
AAAAyour_domain2001: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]/.

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.