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,以及性能对比数据。
服务器同时监听IPv4和IPv6,应用层不做特殊处理。适合大多数场景。
服务器只监听IPv4,通过DNS64将IPv6请求转换为IPv4,NAT64做地址转换。
前端用HAProxy或Nginx做双栈代理,后端服务只监听IPv4。
服务器:Ubuntu 22.04 LTS,内核5.15.0-91-generic
PHP:8.3.6
Nginx:1.24.0
MySQL:8.0.35
云厂商:阿里云ECS(已开通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
# /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;
}
}
}
# /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
-- 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';
#!/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 "防火墙规则已应用"
# 在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
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;
?>
工具: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
| 指标 | 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% |
| 方案 | 平均延迟 (ms) | 吞吐量 (req/s) | 配置复杂度 | 兼容性 |
|---|---|---|---|---|
| 方案A:纯双栈 | 12.8 | 8,012 | 低 | 高 |
| 方案B:DNS64+NAT64 | 15.6 | 6,543 | 中 | 中 |
| 方案C:反向代理 | 14.2 | 7,234 | 高 | 高 |
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;
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
阿里云ECS默认IPv6公网带宽为0,需要额外购买。腾讯云类似。华为云IPv6需要单独申请。
检查云厂商IPv6支持:
# 检查是否分配了公网IPv6
ip -6 addr show | grep global
# 如果没有输出,说明没有公网IPv6
# 检查IPv6网关
ip -6 route show | grep default
# 如果没有默认路由,说明IPv6网络未配置
很多人只配了iptables,忘了ip6tables。IPv6的防火墙规则必须单独配置。
检查IPv6防火墙状态:
# 查看IPv6规则
ip6tables -L -n -v
# 如果没有任何规则,说明IPv6防火墙未配置
# 攻击者可以通过IPv6绕过IPv4防火墙
IPv6依赖ICMPv6进行邻居发现和路径MTU发现。如果防火墙禁止ICMPv6,会导致连接超时。
# 必须放行ICMPv6
ip6tables -A INPUT -p ipv6-icmp -j ACCEPT
# 错误做法:禁止所有ICMP
ip6tables -A INPUT -p ipv6-icmp -j DROP # 会导致IPv6不通
如果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
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 = '::';
IPv6双栈配置的核心要点:
性能方面,IPv6比IPv4延迟高约4%,吞吐量低约3%,但这是可以接受的。推荐使用方案A(纯双栈),配置简单,兼容性好。
专注技术分享与实战