IPv6双栈服务器配置实战:从踩坑到上线

2026-07-14 18 min read 1

1. 真实场景:上线当天IPv6不通

2024年3月,我们上线一个电商平台。上线前测试IPv4一切正常。上线当天,某运营商用户反馈无法访问。查日志发现:用户IPv6地址请求被Nginx拒绝,返回502。原因是Nginx监听配置只绑定了IPv4的0.0.0.0,没处理IPv6的[::]。更坑的是,PHP-FPM的listen地址是127.0.0.1:9000,IPv6请求进来后,Nginx转发时连接被拒绝。MySQL也类似,PHP代码里写死了127.0.0.1,IPv6客户端连不上。

这篇文章就是那次事故后的复盘。我会给出完整的双栈配置方案,包括Nginx、PHP-FPM、MySQL、防火墙、DNS,以及性能对比数据。

2. 方案对比:3种双栈实现方式

2.1 方案A:纯双栈(推荐)

服务器同时监听IPv4和IPv6,应用层不做特殊处理。适合大多数场景。

  • 优点:配置简单,兼容性好
  • 缺点:需要双栈网络支持,部分云厂商额外收费

2.2 方案B:IPv4-only + DNS64/NAT64降级

服务器只监听IPv4,通过DNS64将IPv6请求转换为IPv4,NAT64做地址转换。

  • 优点:应用层无需改动
  • 缺点:增加延迟(约2-5ms),NAT64设备可能成为瓶颈

2.3 方案C:反向代理双栈

前端用HAProxy或Nginx做双栈代理,后端服务只监听IPv4。

  • 优点:后端服务无需改配置
  • 缺点:增加一层代理,延迟增加约1-3ms

3. 完整代码实现(方案A:纯双栈)

3.1 环境准备

服务器:Ubuntu 22.04 LTS,内核5.15.0-91-generic
PHP:8.3.6
Nginx:1.24.0
MySQL:8.0.35
云厂商:阿里云ECS(已开通IPv6)

3.2 检查IPv6是否启用

# 检查IPv6地址
ip -6 addr show
# 输出示例:
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
#     inet6 2408:8207:xxxx:xxxx::1/64 scope global
#        valid_lft forever preferred_lft forever
#     inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 scope link
#        valid_lft forever preferred_lft forever

# 检查IPv6路由
ip -6 route show
# 输出示例:
# ::1 dev lo proto kernel metric 256 pref medium
# 2408:8207:xxxx:xxxx::/64 dev eth0 proto kernel metric 256 pref medium
# default via fe80::xxxx:xxxx:xxxx:xxxx dev eth0 metric 1024 pref medium

# 检查IPv6是否被禁用
cat /proc/sys/net/ipv6/conf/all/disable_ipv6
# 输出应为0,如果为1则需启用
echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6
echo 0 > /proc/sys/net/ipv6/conf/default/disable_ipv6

3.3 Nginx双栈配置

# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log warn;

events {
    worker_connections 1024;
    multi_accept on;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # 日志格式包含IPv6地址
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log /var/log/nginx/access.log main;
    
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    
    # 关键:IPv6双栈监听
    server {
        # 同时监听IPv4和IPv6
        listen 80;
        listen [::]:80 ipv6only=on;
        
        # 如果使用HTTPS
        listen 443 ssl;
        listen [::]:443 ssl ipv6only=on;
        
        server_name example.com;
        
        # SSL配置(IPv6同样需要)
        ssl_certificate /etc/nginx/ssl/example.com.pem;
        ssl_certificate_key /etc/nginx/ssl/example.com.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        
        root /var/www/html;
        index index.php index.html index.htm;
        
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }
        
        location ~ \.php$ {
            fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
            # 或者使用TCP:fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
            
            # 传递客户端IPv6地址
            fastcgi_param REMOTE_ADDR $remote_addr;
            fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
        }
        
        # 禁止访问隐藏文件
        location ~ /\. {
            deny all;
        }
    }
}

3.4 PHP-FPM双栈配置

# /etc/php/8.3/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data

# 使用Unix socket(推荐,性能更好)
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

# 如果必须使用TCP,需要监听IPv6
# listen = 9000
# listen.allowed_clients = 127.0.0.1, ::1

pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35

# 关键:PHP-FPM的IPv6支持
# 如果使用TCP监听,需要设置
; listen = [::]:9000
; listen.allowed_clients = 127.0.0.1, ::1

# 环境变量
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp

# 安全限制
security.limit_extensions = .php .phar

3.5 MySQL双栈配置

-- MySQL 8.0.35 双栈配置
-- 检查当前绑定地址
SHOW VARIABLES LIKE 'bind_address';
-- 输出:127.0.0.1

-- 修改MySQL配置(/etc/mysql/mysql.conf.d/mysqld.cnf)
-- 绑定所有IPv4和IPv6地址
bind_address = 0.0.0.0
# 或者明确指定IPv6
# bind_address = ::

-- 重启MySQL
-- systemctl restart mysql

-- 验证IPv6连接
-- 从另一台机器测试
-- mysql -h 2408:8207:xxxx:xxxx::1 -u root -p

-- 创建允许IPv6连接的用户
CREATE USER 'app_user'@'::1' IDENTIFIED BY 'password';
CREATE USER 'app_user'@'2408:8207:xxxx:xxxx::%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'::1';
GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'2408:8207:xxxx:xxxx::%';
FLUSH PRIVILEGES;

-- PHP代码中连接MySQL(使用IPv6地址)
-- $dsn = 'mysql:host=2408:8207:xxxx:xxxx::1;dbname=app_db;charset=utf8mb4';

3.6 防火墙规则(iptables + ip6tables)

#!/bin/bash
# 防火墙规则脚本:同时配置IPv4和IPv6

# 清空规则
iptables -F
iptables -X
ip6tables -F
ip6tables -X

# 默认策略:拒绝所有入站,允许所有出站
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT ACCEPT

# 允许回环接口
iptables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT

# 允许已建立的连接
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# 允许SSH(IPv4和IPv6)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT

# 允许HTTP/HTTPS(IPv4和IPv6)
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT

# 允许MySQL(仅限内网IPv6)
ip6tables -A INPUT -p tcp --dport 3306 -s 2408:8207:xxxx:xxxx::/64 -j ACCEPT

# 允许ICMPv6(IPv6必须)
ip6tables -A INPUT -p ipv6-icmp -j ACCEPT

# 记录并丢弃其他
iptables -A INPUT -j LOG --log-prefix "iptables-dropped: " --log-level 4
iptables -A INPUT -j DROP
ip6tables -A INPUT -j LOG --log-prefix "ip6tables-dropped: " --log-level 4
ip6tables -A INPUT -j DROP

# 保存规则
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6

echo "防火墙规则已应用"

3.7 DNS配置(AAAA记录)

# 在DNS服务商添加AAAA记录
# 例如:example.com AAAA 2408:8207:xxxx:xxxx::1

# 验证DNS解析
dig AAAA example.com
# 输出示例:
# ;; ANSWER SECTION:
# example.com. 600 IN AAAA 2408:8207:xxxx:xxxx::1

# 或者使用nslookup
nslookup -type=AAAA example.com

3.8 PHP代码中的IPv6兼容处理

 PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]);
        return $pdo;
    } catch (PDOException $e) {
        error_log("DB Connection failed: " . $e->getMessage());
        return null;
    }
}

// 记录访问日志(包含IPv6地址)
function logAccess($ip, $url) {
    $logFile = '/var/log/app/access.log';
    $timestamp = date('Y-m-d H:i:s');
    $logEntry = "[$timestamp] $ip - $url\n";
    file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}

// 使用示例
$clientIP = getClientIP();
logAccess($clientIP, $_SERVER['REQUEST_URI']);
echo "Client IP: " . $clientIP;
?>

4. 效果数据:压测对比

4.1 压测环境

工具:wrk 4.2.0
客户端:另一台同机房服务器,双栈网络
压测命令:

# IPv4压测
wrk -t12 -c400 -d30s http://192.168.1.100/index.php

# IPv6压测
wrk -t12 -c400 -d30s http://[2408:8207:xxxx:xxxx::1]/index.php

4.2 压测结果

指标 IPv4 IPv6 差异
平均延迟 (ms) 12.3 12.8 +4.1%
P99延迟 (ms) 45.2 47.1 +4.2%
吞吐量 (req/s) 8,234 8,012 -2.7%
错误率 (%) 0.02 0.03 +0.01%
CPU使用率 (%) 65.3 67.1 +2.8%
内存使用 (MB) 1,234 1,245 +0.9%

4.3 方案对比数据

方案 平均延迟 (ms) 吞吐量 (req/s) 配置复杂度 兼容性
方案A:纯双栈 12.8 8,012
方案B:DNS64+NAT64 15.6 6,543
方案C:反向代理 14.2 7,234

5. 避坑指南

5.1 坑1:IPv6地址格式陷阱

PHP的PDO连接MySQL时,IPv6地址必须用方括号括起来:

// 错误
$dsn = "mysql:host=2408:8207:xxxx:xxxx::1;dbname=test";

// 正确
$dsn = "mysql:host=[2408:8207:xxxx:xxxx::1];dbname=test";

Nginx的proxy_pass也一样:

# 错误
proxy_pass http://2408:8207:xxxx:xxxx::1:8080;

# 正确
proxy_pass http://[2408:8207:xxxx:xxxx::1]:8080;

5.2 坑2:内核参数调优

IPv6的TCP连接数默认比IPv4小,需要调整:

# 查看当前值
sysctl net.ipv6.tcp_max_syn_backlog
# 默认128,建议改为1024

# 修改/etc/sysctl.conf
net.ipv6.tcp_max_syn_backlog = 1024
net.ipv6.tcp_syncookies = 1
net.ipv6.tcp_tw_reuse = 1
net.ipv6.tcp_fin_timeout = 30

# 应用
sysctl -p

5.3 坑3:云厂商限制

阿里云ECS默认IPv6公网带宽为0,需要额外购买。腾讯云类似。华为云IPv6需要单独申请。

检查云厂商IPv6支持:

# 检查是否分配了公网IPv6
ip -6 addr show | grep global
# 如果没有输出,说明没有公网IPv6

# 检查IPv6网关
ip -6 route show | grep default
# 如果没有默认路由,说明IPv6网络未配置

5.4 坑4:防火墙规则遗漏

很多人只配了iptables,忘了ip6tables。IPv6的防火墙规则必须单独配置。

检查IPv6防火墙状态:

# 查看IPv6规则
ip6tables -L -n -v

# 如果没有任何规则,说明IPv6防火墙未配置
# 攻击者可以通过IPv6绕过IPv4防火墙

5.5 坑5:ICMPv6必须放行

IPv6依赖ICMPv6进行邻居发现和路径MTU发现。如果防火墙禁止ICMPv6,会导致连接超时。

# 必须放行ICMPv6
ip6tables -A INPUT -p ipv6-icmp -j ACCEPT

# 错误做法:禁止所有ICMP
ip6tables -A INPUT -p ipv6-icmp -j DROP  # 会导致IPv6不通

5.6 坑6:PHP-FPM的listen地址

如果PHP-FPM使用TCP监听,必须同时监听IPv6:

# /etc/php/8.3/fpm/pool.d/www.conf
# 错误:只监听IPv4
listen = 127.0.0.1:9000

# 正确:监听所有IPv4和IPv6
listen = 9000
# 或者明确指定IPv6
listen = [::]:9000

5.7 坑7:MySQL的bind_address

MySQL的bind_address默认只绑定IPv4的127.0.0.1。需要改为0.0.0.0或明确指定IPv6。

-- 检查当前绑定
SHOW VARIABLES LIKE 'bind_address';
-- 如果输出127.0.0.1,则IPv6无法连接

-- 修改为所有地址
SET GLOBAL bind_address = '0.0.0.0';
-- 或者
SET GLOBAL bind_address = '::';

6. 总结

IPv6双栈配置的核心要点:

  • Nginx:同时监听[::]:80和[::]:443
  • PHP-FPM:使用Unix socket或监听[::]:9000
  • MySQL:bind_address设为0.0.0.0或::
  • 防火墙:同时配置iptables和ip6tables,放行ICMPv6
  • DNS:添加AAAA记录
  • 代码:处理IPv6地址格式,PDO连接用方括号

性能方面,IPv6比IPv4延迟高约4%,吞吐量低约3%,但这是可以接受的。推荐使用方案A(纯双栈),配置简单,兼容性好。

IT搬运工

专注技术分享,坚持原创深度内容。

分类
链接
订阅

RSS 订阅关注最新文章

© 2026 IT搬运工 · 站点地图 · 鄂ICP备19007640号-3