Configuring IPv6 on Linux Servers: Static Addresses, Dual-Stack, and Real-World ip6tables

Network tutorial - IT technology blog
Network tutorial - IT technology blog

Get It Done in 5 Minutes: Enable IPv6 and Test Connectivity

If you just need to know whether your server has IPv6 and want to enable it as quickly as possible, follow these 3 steps:

# Check if IPv6 is enabled
cat /proc/sys/net/ipv6/conf/all/disable_ipv6
# Output: 0 = enabled, 1 = disabled

# Enable IPv6 immediately (no reboot required)
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0

# Check assigned IPv6 addresses
ip -6 addr show

Seeing fe80:: in the output? Your server has received a link-local IPv6 address — but you’re not done yet. Link-local addresses only work within the same subnet; pings from the internet will still fail. You need a few more steps before the server is reachable from outside.

A Quick IPv6 Overview Before You Configure

The thing that trips people up when first working with IPv6: a single interface typically has 3–4 IPv6 addresses simultaneously, each serving a different purpose.

  • Link-local (fe80::/10): Auto-generated, only usable within the same subnet. Not routable outside.
  • Global unicast (2000::/3): Public, globally routable address — the IPv6 equivalent of a public IPv4 address.
  • Loopback (::1): Equivalent to 127.0.0.1.
  • ULA (fd00::/8): Internal private address, like 192.168.x.x but for IPv6.

When first deploying a VPS with IPv6, it’s easy to see fe80:: and think you’re done — but pings from outside still fail because that’s just a link-local address. You need a global unicast address to be reachable from the internet.

Configuring a Static IPv6 Address on Ubuntu/Debian

Ubuntu 20.04 and later manage networking through Netplan — configuration files live in /etc/netplan/, and /etc/network/interfaces is no longer used.

# View the current Netplan file
ls /etc/netplan/
cat /etc/netplan/00-installer-config.yaml

Edit the file to add a static IPv6 address:

# /etc/netplan/00-installer-config.yaml
network:
  version: 2
  ethernets:
    eth0:
      addresses:
        - 192.168.1.100/24          # Static IPv4 (keep as-is)
        - 2001:db8:1::100/64        # Static IPv6 (replace with your actual address)
      routes:
        - to: default
          via: 192.168.1.1          # IPv4 gateway
        - to: ::/0
          via: 2001:db8:1::1        # IPv6 gateway
      nameservers:
        addresses:
          - 8.8.8.8
          - 2001:4860:4860::8888    # Google DNS IPv6
# Apply the configuration
sudo netplan apply

# Verify the result
ip -6 addr show eth0
ip -6 route show

Configuring a Static IPv6 Address on CentOS/RHEL/Rocky Linux

On CentOS/Rocky systems, interface configuration files are located in /etc/sysconfig/network-scripts/:

# Open the interface configuration file
sudo vi /etc/sysconfig/network-scripts/ifcfg-eth0

Add the IPv6 lines to the file:

TYPE=Ethernet
BOOTPROTO=none
NAME=eth0
DEVICE=eth0
ONBOOT=yes

# IPv4
IPADDR=192.168.1.100
PREFIX=24
GATEWAY=192.168.1.1

# IPv6
IPV6INIT=yes
IPV6_AUTOCONF=no
IPV6ADDR=2001:db8:1::100/64
IPV6_DEFAULTGW=2001:db8:1::1
DNS1=8.8.8.8
DNS2=2001:4860:4860::8888
# Restart network interface
sudo nmcli connection reload
sudo nmcli connection up eth0

# Or on older systems
sudo systemctl restart NetworkManager

Advanced Configuration: Dual-Stack and SLAAC

Dual-Stack: Running IPv4 and IPv6 in Parallel

This is the setup I run on all my production servers. IPv4 is kept for compatibility with legacy systems; IPv6 for direct connections without NAT — lower latency and none of the headaches that come with CGNAT.

One thing that’s easy to overlook: Nginx listening on :: (all IPv6) typically covers IPv4 as well through IPv4-mapped addresses — but only typically. The actual behavior depends on this kernel setting:

# Check whether IPv4-mapped addresses are enabled
cat /proc/sys/net/ipv6/bindv6only
# 0 = automatic dual-stack, 1 = must bind each protocol separately

SLAAC — Automatically Receiving an IPv6 Address from the Router

If your router supports RA (Router Advertisement), the server can automatically receive a global IPv6 address without any static configuration:

# Enable SLAAC for interface eth0
sudo sysctl -w net.ipv6.conf.eth0.autoconf=1
sudo sysctl -w net.ipv6.conf.eth0.accept_ra=1

# Write to /etc/sysctl.conf to make it persistent
echo "net.ipv6.conf.eth0.autoconf=1" | sudo tee -a /etc/sysctl.conf
echo "net.ipv6.conf.eth0.accept_ra=1" | sudo tee -a /etc/sysctl.conf

SLAAC is convenient for office workstations — no extra configuration needed. For servers, always assign a static IP. A fixed address makes writing firewall rules much simpler and you don’t have to worry about the IP changing after every reboot.

Configuring ip6tables — Firewall for IPv6

This is a mistake I’ve seen repeatedly when reviewing other people’s servers: iptables is locked down tight, but ip6tables is completely empty — IPv6 traffic flows straight through, unfiltered.

# View current ip6tables rules
sudo ip6tables -L -n -v

# Set up basic IPv6 rules
# Allow loopback
sudo ip6tables -A INPUT -i lo -j ACCEPT

# Allow established traffic
sudo ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow ICMPv6 (REQUIRED — without this, IPv6 will not work correctly)
sudo ip6tables -A INPUT -p icmpv6 -j ACCEPT

# Allow SSH
sudo ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT

# Allow HTTP/HTTPS
sudo ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT

# Drop everything else
sudo ip6tables -A INPUT -j DROP

# Save rules
sudo ip6tables-save | sudo tee /etc/ip6tables.rules

Never block ICMPv6 — this is a critical difference from IPv4. Partially blocking ICMPv4 and the server still runs fine. But ICMPv6 is the backbone of the entire protocol: neighbor discovery, router advertisement, and path MTU discovery all run through it. Block ICMPv6 and IPv6 breaks immediately.

Practical Tips from Real Network Management Experience

I manage networking for a 50-person office and a small datacenter — everything below comes from real mistakes, not theory:

1. Testing Connectivity the Right Way

# Ping IPv6 (use -6 to force IPv6)
ping6 google.com
# Or
ping -6 2001:4860:4860::8888

# IPv6 traceroute
traceroute6 google.com

# Curl over IPv6
curl -6 https://ipv6.google.com

# Check if DNS returns AAAA records
dig AAAA google.com
nslookup -type=AAAA google.com

2. Persistent sysctl — Keep IPv6 Enabled After Reboot

# Add to /etc/sysctl.conf or create a separate file
cat << 'EOF' | sudo tee /etc/sysctl.d/99-ipv6.conf
net.ipv6.conf.all.disable_ipv6=0
net.ipv6.conf.default.disable_ipv6=0
net.ipv6.conf.lo.disable_ipv6=0
EOF

# Apply immediately
sudo sysctl --system

3. Writing IPv6 Addresses in Nginx/Apache Configs

# Nginx — listen on both IPv4 and IPv6
server {
    listen 80;
    listen [::]:80;        # IPv6 — square brackets required
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;
}
# SSH to a server over IPv6 (square brackets required)
ssh user@[2001:db8:1::100]
ssh -6 [email protected]

4. Troubleshooting When Your VPS Provider Assigns IPv6 but It Won't Connect

I ran into this exact situation on Vultr: IPv6 was showing in the dashboard, but pings to the outside were still timing out. The cause was that their IPv6 gateway required a different route configuration:

# Check IPv6 routes
ip -6 route show

# If the default route is missing, add it manually
sudo ip -6 route add default via 2001:db8:1::1 dev eth0

# Check neighbor cache (the IPv6 equivalent of IPv4's ARP)
ip -6 neigh show

If it's still not working, check the MTU. IPv6 requires a minimum of 1280 bytes — anything lower and packets are dropped silently, making it very difficult to debug:

ip link show eth0 | grep mtu
# If MTU < 1280, increase it
sudo ip link set eth0 mtu 1500

Share: