Understanding Why HTTPD Listens on IPv6 but Remains Accessible via IPv4: A Deep Dive into Dual-Stack Socket Behavior


2 views

When examining HTTPD server behavior on Linux systems, you might encounter an interesting phenomenon where netstat or ss shows listening sockets bound only to IPv6 (tcp6), yet the service remains fully accessible via IPv4. This occurs due to Linux's implementation of dual-stack sockets and has important implications for server administration.

// Example of checking listening ports
$ netstat -ltn | grep :443
tcp6       0      0 :::443                 :::*                    LISTEN

Modern Linux kernels implement a special behavior for IPv6 sockets when the IPV6_V6ONLY socket option is not set (which is the default in most distributions). In this case:

  • An IPv6 socket listening on :: (equivalent to 0.0.0.0 in IPv4) will automatically accept both IPv4 and IPv6 connections
  • The kernel handles IPv4 connections by mapping them to IPv6-mapped IPv4 addresses (::ffff:x.y.z.w)
  • This behavior is controlled by the /proc/sys/net/ipv6/bindv6only parameter

To confirm whether your HTTPD is truly IPv6-only or actually dual-stack:

# Check the global dual-stack setting
$ cat /proc/sys/net/ipv6/bindv6only
0  # 0 means dual-stack is enabled

# Check Apache's listen directives
$ apachectl -S | grep listening
*:443                  # This wildcard listen will create dual-stack behavior

In some cases, you might explicitly want separate listeners. Here's how to configure Apache for this:

# In httpd.conf
Listen 0.0.0.0:443
Listen [::]:443

# And set IPV6_V6ONLY for the IPv6 socket
SetEnvIf Listener "[::]:443" ipv6only=1

The dual-stack approach actually has some advantages:

  • Single socket handling reduces kernel resource usage
  • Simplified connection management in the web server
  • Consistent logging format (all connections appear as IPv6)

However, if you need to distinguish between IPv4 and IPv6 connections for logging or access control, you'll need to configure separate listeners as shown above.

To see the actual connection types being handled:

$ ss -tnp sport = :443
State    Recv-Q   Send-Q     Local Address:Port      Peer Address:Port
ESTAB    0        0          192.168.1.100:443       192.168.1.50:54321
ESTAB    0        0          [2001:db8::1]:443       [2001:db8::2]:12345

When examining HTTPD server behavior with netstat, you might notice an interesting scenario where the service appears to be listening only on IPv6 (tcp6), yet remains fully accessible via IPv4 connections. This occurs due to Linux's dual-stack socket implementation.

Modern Linux kernels implement a special behavior for IPv6 sockets: when an application binds to :: (IPv6 wildcard address), the kernel automatically makes the service available on both IPv4 and IPv6 addresses through a single socket. This is controlled by the net.ipv6.bindv6only sysctl parameter (defaulting to 0).


# Check current dual-stack setting
sysctl net.ipv6.bindv6only
# Temporary change (for testing)
sysctl -w net.ipv6.bindv6only=1
# Permanent change (add to /etc/sysctl.conf)
echo "net.ipv6.bindv6only = 1" >> /etc/sysctl.conf

This behavior explains why you see:

  • netstat shows only IPv6 listening socket
  • Both IPv4 and IPv6 clients can connect
  • Apache logs will show IPv4 addresses for IPv4 clients

You can confirm this behavior with these commands:


# Check listening ports with ss (modern alternative to netstat)
ss -tuln | grep 443
# Test IPv4 connectivity
curl -4 https://your-server
# Test IPv6 connectivity
curl -6 https://your-server

While the default behavior works well, you can explicitly configure Apache for specific IP versions:


# Listen on IPv4 only
Listen 0.0.0.0:443
# Listen on IPv6 only (requires bindv6only=1)
Listen [::]:443
# Listen on both explicitly (creates two sockets)
Listen 0.0.0.0:443
Listen [::]:443

The dual-stack approach actually provides benefits:

  • Single socket management reduces kernel resource usage
  • Simplified configuration
  • Better performance for mixed environments

If you encounter issues:

  1. Verify net.ipv6.bindv6only setting
  2. Check firewall rules for both IP versions
  3. Ensure proper DNS records (A and AAAA)
  4. Test with both curl -4 and curl -6