MySQL 8.0+ 的 bind_address 不支持 IPv6 字面量(如 ::1),需用 bind_address = * 配合系统级 IPv6 限定或 systemd socket 激活(ListenStream = [::1]:3306)并配合防火墙拦截 IPv4 流量,客户端连接 IPv6 地址必须加方括号。
MySQL 8.0+ 中 bind_address 不支持 IPv6 地址字面量
直接在 my.cnf 里写 bind_address = ::1 或 bind_address = 2001:db8::1 会启动失败,报错:invalid argument 或 failed to bind to address。这是因为 mysql 8.0 之前只接受 ipv4 地址或 localhost;8.0+ 虽然增加了 ipv6 支持,但 bind_address 仍不解析 ipv6 字面量——它只认 *、0.0.0.0、127.0.0.1 这类字符串。
真正起作用的是 bind_address = *,配合系统级网络栈和防火墙限制,才能实现“仅 IPv6 可达”。
- MySQL 本身不区分 IPv4/IPv6 连接,
bind_address = *表示监听所有协议族(AF_INET + AF_INET6)的所有本地地址 - 若只想让 IPv6 客户端连上,必须靠操作系统层面禁用 IPv4 监听,或用防火墙拦截 IPv4 入站流量
-
skip_networking = ON会彻底关闭 TCP/IP,不可取;它不满足“允许连接”的前提
用 systemd socket 激活 + 网络命名空间隔离 IPv4 流量
最干净的做法不是改 MySQL 配置,而是让系统根本不给 IPv4 分配监听端口。Linux 的 socket 激活机制可以做到:只创建 IPv6 socket,不创建 IPv4 的。
- 停用默认的
mysqld.service,启用自定义 socket 单元mysqld6.socket - 在
[Socket]段中指定ListenStream = [::1]:3306(带方括号表示强制 IPv6) - 确保
BindTo = mysqld6.socket,且mysqld启动时不设bind_address(留空或注释掉) - 这样 systemd 只会为 IPv6 创建 socket,IPv4 的
0.0.0.0:3306根本不会出现
验证命令:ss -tln | grep :3306 —— 输出应只有 tcp6 行,没有 tcp 行。
iptables/nftables 必须封掉 IPv4 的 3306 入向连接
即使 MySQL 只监听 IPv6,如果服务器同时启用了 IPv4 协议栈,且防火墙放行了 0.0.0.0:3306,外部 IPv4 包仍可能被内核转发到 MySQL(取决于 net.ipv4.ip_forward 和路由表)。不能依赖“没监听就安全”。
- IPv4 规则示例(iptables):
iptables -A INPUT -p tcp --dport 3306 -j DROP - nftables 更明确:
nft add rule ip filter input tcp dport 3306 drop - 注意:规则顺序很重要,要放在所有 ACCEPT 之前;否则可能被更宽泛的规则绕过
- 别忘了检查
iptables -t nat -L,避免 DNAT 把 IPv4 流量重定向到 MySQL
客户端连接时的 IPv6 地址格式容易出错
MySQL 客户端(包括 mysql 命令、JDBC、Python 的 pymysql)对 IPv6 地址的解析很敏感。漏掉方括号、混用冒号分隔符都会导致连接失败。
- 正确写法:
mysql -h '[2001:db8::1]' -P 3306 -u user或mysql -h '::1' -P 3306 - 错误写法:
mysql -h '2001:db8::1'(报错:Unknown MySQL server host '2001:db8::1' (2)) - JDBC URL 示例:
jdbc:mysql://[2001:db8::1]:3306/db?useSSL=false - Python
pymysql.connect(host='[::1]')—— 方括号不能少,否则解析成域名
IPv6 地址里的双冒号 :: 是压缩写法,但客户端库不一定都支持;生产环境建议用完整格式或 ::1 这类标准缩写。











