How to Configure IPv6 on FreeBSD: Complete Guide
IPv6 is no longer optional. Major cloud providers, hosting companies, and ISPs allocate IPv6 by default. Google reports over 45% of its traffic arrives over IPv6. FreeBSD has had production-quality IPv6 support since the KAME project was merged into the base system over two decades ago. The implementation is mature, well-tested, and fully integrated into the networking stack, PF firewall, and routing subsystems.
This guide covers every IPv6 configuration method on FreeBSD: static addressing, SLAAC (stateless autoconfiguration), DHCPv6, dual-stack operation, PF firewall rules, running radvd as a router advertisement daemon, and troubleshooting connectivity.
IPv6 Fundamentals for FreeBSD Admins
Before configuring, understand the address types you will encounter.
Link-local addresses (fe80::/10): Automatically assigned to every IPv6-enabled interface. Used for neighbor discovery, router advertisements, and communication within a single network segment. Every interface has one. You cannot remove it without disabling IPv6.
Global unicast addresses (2000::/3): Routable on the public internet. Equivalent to public IPv4 addresses. Assigned statically, via SLAAC, or via DHCPv6.
Unique local addresses (fd00::/8): Private addresses, similar to RFC 1918 IPv4 space. Use these for internal networks that should not be routed to the internet.
Loopback (::1): The IPv6 equivalent of 127.0.0.1.
Prerequisites
- FreeBSD 13.0 or later
- Root access
- An IPv6 allocation from your hosting provider or ISP (for global addresses)
Checking Current IPv6 Status
Before making changes, check what is already configured:
sh# List all IPv6 addresses ifconfig -L inet6 # Check if IPv6 is enabled in rc.conf sysrc -a | grep ipv6 # Verify the kernel accepts IPv6 sysctl net.inet6.ip6.accept_rtadv sysctl net.inet6.ip6.forwarding
Static IPv6 Configuration
Static configuration is the most common method for servers. Your hosting provider assigns a /64 or /128 prefix.
Single Static Address
sh# Enable IPv6 on the interface sysrc ifconfig_em0_ipv6="inet6 2001:db8:1::10 prefixlen 64" sysrc ipv6_defaultrouter="2001:db8:1::1" # Apply immediately without reboot ifconfig em0 inet6 2001:db8:1::10 prefixlen 64 route -6 add default 2001:db8:1::1
Replace 2001:db8:1::10 with your assigned address, 64 with your prefix length, and 2001:db8:1::1 with your gateway.
Multiple IPv6 Addresses on One Interface
FreeBSD supports multiple IPv6 addresses per interface using aliases:
shsysrc ifconfig_em0_ipv6="inet6 2001:db8:1::10 prefixlen 64" sysrc ifconfig_em0_alias0="inet6 2001:db8:1::11 prefixlen 64" sysrc ifconfig_em0_alias1="inet6 2001:db8:1::12 prefixlen 64"
Apply without reboot:
shifconfig em0 inet6 2001:db8:1::11 prefixlen 64 alias ifconfig em0 inet6 2001:db8:1::12 prefixlen 64 alias
Entire /64 Subnet
If you have a full /64, you can assign any address within it. There is no need to register individual addresses. Simply configure the addresses you need as aliases.
SLAAC Configuration (Stateless Address Autoconfiguration)
SLAAC allows FreeBSD to automatically configure its IPv6 address from router advertisements. This is the standard method for networks with a router running radvd or similar.
sh# Enable SLAAC sysrc ifconfig_em0_ipv6="inet6 accept_rtadv" sysrc rtsold_enable="YES" sysrc rtsold_flags="-aF"
Apply immediately:
shifconfig em0 inet6 accept_rtadv service rtsold start
The -a flag tells rtsold to monitor all interfaces. The -F flag forces it to run in the foreground briefly before daemonizing.
Verify the address was assigned:
shifconfig em0 inet6
You should see a global unicast address derived from the prefix advertised by the router and your interface's MAC address (or a random identifier if privacy extensions are enabled).
Privacy Extensions
SLAAC by default derives the interface identifier from the MAC address, making it trackable. Enable privacy extensions to generate random interface identifiers:
sh# Enable privacy extensions sysrc ifconfig_em0_ipv6="inet6 accept_rtadv autoconf privacy" # Or via sysctl for all interfaces sysctl net.inet6.ip6.use_tempaddr=1 sysctl net.inet6.ip6.prefer_tempaddr=1
Make persistent:
shcat >> /etc/sysctl.conf << 'EOF' net.inet6.ip6.use_tempaddr=1 net.inet6.ip6.prefer_tempaddr=1 EOF
Privacy addresses rotate periodically. For servers, static addresses are generally preferred because you need predictable addressing for DNS records and firewall rules.
DHCPv6 Configuration
DHCPv6 provides stateful address assignment, allowing centralized management of IPv6 addresses, DNS servers, and other options.
DHCPv6 Client
Install the DHCPv6 client:
shpkg install dhcp6
Configure /usr/local/etc/dhcp6c.conf:
shcat > /usr/local/etc/dhcp6c.conf << 'EOF' interface em0 { send ia-na 0; request domain-name-servers; request domain-name; script "/usr/local/etc/dhcp6c-script"; }; id-assoc na 0 { }; EOF
Enable and start the client:
shsysrc dhcp6c_enable="YES" sysrc dhcp6c_interfaces="em0" service dhcp6c start
DHCPv6 with Prefix Delegation
If your ISP provides a prefix delegation (common for routers receiving a /48 or /56):
shcat > /usr/local/etc/dhcp6c.conf << 'EOF' interface em0 { send ia-pd 0; send ia-na 0; request domain-name-servers; }; id-assoc pd 0 { prefix-interface em1 { sla-id 1; sla-len 8; }; }; id-assoc na 0 { }; EOF
This requests a delegated prefix from the upstream router and assigns a /64 from that prefix to the em1 (LAN) interface.
Dual-Stack Configuration
Most production servers run dual-stack, serving both IPv4 and IPv6 simultaneously.
sh# IPv4 configuration sysrc ifconfig_em0="inet 192.168.1.10 netmask 255.255.255.0" sysrc defaultrouter="192.168.1.1" # IPv6 configuration sysrc ifconfig_em0_ipv6="inet6 2001:db8:1::10 prefixlen 64" sysrc ipv6_defaultrouter="2001:db8:1::1"
Verify both stacks:
sh# IPv4 connectivity ping -c 3 8.8.8.8 # IPv6 connectivity ping6 -c 3 2600::
DNS Resolution for Dual-Stack
Configure /etc/resolv.conf with both IPv4 and IPv6 nameservers:
shcat > /etc/resolv.conf << 'EOF' nameserver 2001:4860:4860::8888 nameserver 2001:4860:4860::8844 nameserver 8.8.8.8 nameserver 8.8.4.4 EOF
IPv6 nameservers are listed first. If they are unreachable, the resolver falls back to IPv4.
PF Firewall Rules for IPv6
PF on FreeBSD handles IPv4 and IPv6 in the same ruleset. Use the inet6 keyword for IPv6-specific rules.
Basic IPv6 Firewall
shcat > /etc/pf.conf << 'PFEOF' # Macros ext_if = "em0" icmp6_types = "{ unreach, toobig, timex, paramprob, echoreq, echorep, \ routersol, routeradv, neighbrsol, neighbradv }" # Options set skip on lo0 set block-policy drop # Scrub scrub in all # NAT (IPv4 only -- IPv6 does not use NAT) nat on $ext_if inet from 10.0.0.0/24 to any -> ($ext_if) # Default block block all # Allow loopback pass quick on lo0 all # ICMPv6 -- MUST allow for IPv6 to function pass inet6 proto icmp6 icmp6-type $icmp6_types # Allow established connections pass out all keep state # IPv4 services pass in on $ext_if inet proto tcp to port { 22, 80, 443 } keep state # IPv6 services (same ports) pass in on $ext_if inet6 proto tcp to port { 22, 80, 443 } keep state # Allow DHCPv6 client pass in on $ext_if inet6 proto udp from fe80::/10 to fe80::/10 port 546 PFEOF pfctl -f /etc/pf.conf
Critical: you must allow ICMPv6. Unlike IPv4 where blocking ICMP is common (if misguided), IPv6 requires ICMPv6 for neighbor discovery, path MTU discovery, and router advertisements. Blocking ICMPv6 breaks IPv6 connectivity.
Rate Limiting ICMPv6
If you want to limit ICMPv6 to prevent abuse while keeping it functional:
sh# Allow essential ICMPv6 but rate-limit echo requests pass inet6 proto icmp6 icmp6-type { unreach, toobig, timex, paramprob, \ routersol, routeradv, neighbrsol, neighbradv } pass inet6 proto icmp6 icmp6-type { echoreq } max-pkt-rate 100/10
Per-Address Filtering
Block traffic to specific IPv6 addresses:
sh# Block all traffic to a decommissioned address block in quick on $ext_if inet6 to 2001:db8:1::dead
Running radvd (Router Advertisement Daemon)
If your FreeBSD server acts as a router for an IPv6 network, install radvd to send router advertisements:
shpkg install radvd
Enable IPv6 Forwarding
shsysrc ipv6_gateway_enable="YES" sysctl net.inet6.ip6.forwarding=1
Configure radvd
shcat > /usr/local/etc/radvd.conf << 'EOF' interface em1 { AdvSendAdvert on; MinRtrAdvInterval 30; MaxRtrAdvInterval 100; # Advertise the /64 prefix for SLAAC prefix 2001:db8:1:1::/64 { AdvOnLink on; AdvAutonomous on; AdvPreferredLifetime 14400; AdvValidLifetime 86400; }; # Advertise DNS servers RDNSS 2001:4860:4860::8888 2001:4860:4860::8844 { AdvRDNSSLifetime 3600; }; # Advertise DNS search domain DNSSL example.com { AdvDNSSLLifetime 3600; }; }; EOF
Enable and start radvd:
shsysrc radvd_enable="YES" service radvd start
Verify advertisements are being sent:
shradvdump
This listens for router advertisements and displays their contents. Clients on the em1 network should now autoconfigure IPv6 addresses.
IPv6-Specific DNS (AAAA Records)
Once IPv6 is configured, add AAAA records to your DNS zone:
shell; Forward zone server.example.com. IN A 192.168.1.10 server.example.com. IN AAAA 2001:db8:1::10 ; Reverse zone (ip6.arpa) 0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. IN PTR server.example.com.
If using Unbound as a local resolver, it handles IPv6 automatically.
Testing and Verification
Basic Connectivity
sh# Ping the gateway ping6 -c 3 2001:db8:1::1 # Ping a public IPv6 host ping6 -c 3 2600:: # Ping Google's IPv6 DNS ping6 -c 3 2001:4860:4860::8888
Path MTU Discovery
sh# Test path MTU to a remote host traceroute6 -I 2001:4860:4860::8888
Verify Dual-Stack Services
sh# Check that NGINX listens on both protocols sockstat -6 -l | grep nginx # Test HTTP over IPv6 fetch -o /dev/null http://[2001:db8:1::10]/
NDP (Neighbor Discovery Protocol)
sh# View the IPv6 neighbor cache (equivalent of ARP for IPv6) ndp -a # View router list ndp -r # View prefix list ndp -p
Configuring Services for IPv6
NGINX
NGINX listens on IPv6 by default on FreeBSD. To be explicit:
nginxserver { listen 80; listen [::]:80; listen 443 ssl; listen [::]:443 ssl; server_name example.com; # ... }
SSH
OpenSSH on FreeBSD listens on both IPv4 and IPv6 by default. To restrict:
sh# Listen on specific IPv6 address only # In /etc/ssh/sshd_config: ListenAddress 2001:db8:1::10 ListenAddress 192.168.1.10
PostgreSQL / MariaDB
For PostgreSQL, in postgresql.conf:
shelllisten_addresses = 'localhost,::1,2001:db8:1::10'
For MariaDB, in server.cnf:
shellbind-address = ::
Setting bind-address to :: makes MariaDB listen on all IPv4 and IPv6 addresses.
Disabling IPv6
If you genuinely need to disable IPv6 (rare, but some compliance requirements demand it):
sh# Disable IPv6 on a specific interface sysrc ifconfig_em0_ipv6="inet6 -ifdisabled" sysrc ipv6_activate_all_interfaces="NO" # Disable IPv6 system-wide cat >> /etc/sysctl.conf << 'EOF' net.inet6.ip6.accept_rtadv=0 EOF # Remove IPv6 from rc.conf sysrc -x ifconfig_em0_ipv6 sysrc -x ipv6_defaultrouter
Do not disable IPv6 unless you have a specific, documented reason. Many modern services depend on IPv6 loopback (::1), and disabling it can cause unexpected failures.
Troubleshooting
No Global IPv6 Address with SLAAC
Check that router advertisements are reaching the interface:
sh# Listen for router advertisements rtsol -d em0
If no advertisements arrive, verify the upstream router is sending them. Check that PF is not blocking ICMPv6.
IPv6 Address Assigned but No Connectivity
sh# Verify the default route exists netstat -rn -f inet6 | grep default # Check PF is not blocking pfctl -sr | grep inet6 # Test with PF temporarily disabled pfctl -d ping6 -c 3 2001:4860:4860::8888 pfctl -e
Duplicate Address Detection (DAD) Failure
If an address shows "tentative" in ifconfig output, DAD detected a conflict:
shifconfig em0 inet6 | grep tentative
Another device on the network has the same address. Change the address or investigate the conflict with:
shndp -a | grep <conflicting-address>
DNS Resolution Over IPv6 Fails
sh# Test DNS resolution explicitly over IPv6 drill -6 example.com @2001:4860:4860::8888 # Check if the resolver is reachable ping6 -c 3 2001:4860:4860::8888
If ping works but DNS does not, your firewall may be blocking UDP port 53 over IPv6.
FAQ
Do I need NAT for IPv6?
No. IPv6 was designed to eliminate NAT. Every device gets a globally routable address. Use PF firewall rules to control access instead of hiding behind NAT. If you need address privacy, use IPv6 privacy extensions instead.
Is IPv6 slower than IPv4?
No. In many cases IPv6 is faster because it avoids NAT translation overhead and many ISPs provide more direct IPv6 routing. Some CDNs (including Cloudflare and Google) prioritize IPv6 paths.
What prefix length should I use?
/64 is the standard for a single subnet. Never use anything smaller than /64 on a LAN segment; SLAAC requires exactly /64. For point-to-point links, /127 is acceptable (RFC 6164). For loopback addresses, /128 is standard.
Can I run IPv6-only on FreeBSD?
Yes, FreeBSD supports IPv6-only operation. DNS64 and NAT64 (via tayga or jool) allow IPv6-only hosts to reach IPv4 services. This is increasingly common in mobile networks and modern data centers.
How do I test if my server is reachable over IPv6 from the internet?
Use an external testing service like test-ipv6.com, or run ping6 from a different network. From a Linux/macOS workstation: curl -6 https://your-server.example.com/.
What happens if I assign an IPv4-mapped IPv6 address (::ffff:192.168.1.1)?
FreeBSD supports IPv4-mapped IPv6 addresses, but they are not routed over the IPv6 network. They exist only as a compatibility mechanism for dual-stack sockets. Do not use them for interface configuration.
How do I monitor IPv6 traffic separately?
Use netstat -s -f inet6 for protocol statistics, systat -ifstat for per-interface traffic rates, or tcpdump with the ip6 filter: tcpdump -i em0 ip6.