为博客增加 IPv6 支持

最近,我家的杭州电信宽带能够自动分配到 IPv6 地址了,于是折腾一下自己的博客,使 Docker 中运行的 WordPress 也能够通过 IPv6 访问。

到我的博客 IPv6 地址的 Traceroute 结果

获取 IPv6 地址

通过 IPv6 隧道设置获得 IPv6 地址

我的博客搭建在 BandwagonHost (aff) 的 VPS 上。BandwagonHost 提供有 OpenVZKVM 两种虚拟化架构的 VPS,其中我选择了 KVM 架构,因为 KVM 实现了更加完整的虚拟化,能够自定义内核参数、使用 TCP BBR、安装 Docker 等。

但由于技术原因,BandwagonHost 的 IPv6 仅支持 OpenVZ 架构。对于 KVM VPS,就需要通过隧道等方式自行解决。

我使用的是 Hurricane Electrictunnelbroker.net 免费 IPv6 隧道。使用方法比较简单,直接输入 VPS 的公网 IP,即可创建隧道,并能自动生成各个平台的配置脚本/配置文件。

he.net IPv6 Tunnel 示例配置

添加防火墙规则

设置完 IPv6 地址,第二天早上,发现似乎无法通过 IPv6 地址访问 VPS 了。但在 VPS 中,对应的 IPv6 隧道接口仍然是 UP 状态,而且在 VPS 中执行 ping6 ipv6.google.com 后,又重新恢复正常。

这时候首先想到的是通过 crontab,定时 ping 一个 IPv6 地址,来实现对隧道的 keep-alive 操作,如下:

*/2 * * * *   ping6 -c 3 ipv6.google.com > /dev/null

不过经过查找资料,其根本原因防火墙屏蔽了协议号 41 的入站流量,导致 IPv6 隧道的服务器无法访问到 VPS,所以根本的解决方法是添加对应的防火墙规则。如果使用 iptables 做为防火墙,可按照这个帖子中的方法来设置。而我使用的是 UFW,可通过如下命令添加规则:

ufw allow from <IPv6 隧道服务器地址> proto ipv6

IPv6 + Docker

如果不使用 Docker,直接在 VPS 上搭建 WordPress 博客,在获取到 IP 地址后,只需要经过简单设置 Web 服务器,并在 DNS 中添加 AAAA 记录,即可使博客支持 IPv6.

但我的博客基于 Docker 搭建,使用了 PHP、数据库、Nginx 三个 Docker 容器。想在 Docker 环境里面支持 IPv6,事情变得麻烦起来……

方案一:直接使用 Docker 自带的 IPv6 支持

Docker 官方文档中有如下两篇文章:

通过指定 --ipv6 参数,即可打开 Docker 自带的 IPv6 支持。但是,通过这种方式,每个容器都会获得一个独立的公网 IPv6 地址,所有端口暴露在公网,安全性较低。

方案二:使用 Docker 的 userland proxy

Docker 默认打开了 userland-proxy,在用户态实现了一个代理服务器,同时监听 IPv4 和 IPv6 端口,并将流量转发至对应的 Docker 容器。这种方式有一定的局限性,例如:

  • 在用户态实现代理服务器,而不是通过 iptables 实现流量转发,性能相对较低
  • 用户可通过 IPv6 地址访问 Docker 容器提供的服务,但 Docker 容器内没有 IPv6 地址,容器内的进程无法直接通过 IPv6 与外界通信
  • 通过代理服务器之后,报文源地址发生了改变。导致 WordPress 无法获得用户的真实地址。而 WordPress 评论系统,以及部分防止恶意登录的插件,都需要用到用户的真实 IP 地址。

如下图所示,在使用 Docker userland proxy 的情况下,使用 IPv6 地址发表评论,WordPress 中显示的是本地 IPv4 地址:

通过 IPv6 地址在博客中发表评论,无法显示出用户源地址
通过 IPv6 地址在博客中发表评论,无法显示出用户源地址

方案三:使用 nftables 手动实现 IPv6 NAT

这篇文章介绍了 Angry Bytes 在生产环境中使用 Docker IPv6 的方式:

该方案的基本思路,是为 Docker 容器指定本地 IPv6 地址,而不是公网 IPv6 地址。然后通过手动添加 nftables 规则,实现 IPv6 NAT.

该方案使用 nftables 代替 iptables. nftables 是一种可以取代 iptables 的 Linux 防火墙框架,与 iptables 相比拥有更多新特性。但在我的 VPS 上,使用 nftables 代替 iptables 和 ufw,需要破坏原有环境。而且这种方案需要在创建容器时手动指定容器的 IPv6 地址,并在创建容器后手动添加 nftables 规则,操作较为繁琐。

最终方案:使用 robbertkl/docker-ipv6nat 自动实现 IPv6 NAT

robbertkl/docker-ipv6nat 项目,实现了 Docker 环境下的 IPv6 NAT,而且无需用户进行更加复杂的配置。所以,我最终选择了这种方案。

由于我使用 jwilder/nginx-proxy 做为反向代理服务器,而该镜像默认没有打开 IPv6 支持,所以需要删除原有的容器,然后添加 IPv6 参数,重新创建一个支持 IPv6 的反向代理。

完整的操作步骤如下:

# 创建 IPv6 NAT 容器
docker run -d --restart=always \
    -v /var/run/docker.sock:/var/run/docker.sock:ro \
    --privileged \
    --net=host \
    robbertkl/ipv6nat

# 创建一个 IPv6 Docker 网络
docker network create --ipv6 --subnet=fd00:dead:beef::/48 ipv6_bridge

# 创建支持 IPv6 的 nginx 反向代理
docker run -d --name nginx-proxy \
    --restart=always \
    -p 80:80 \
    -p 443:443 \
    -e ENABLE_IPV6=true \
    -v /var/run/docker.sock:/tmp/docker.sock:ro \
    -v /srv/docker/certs:/etc/nginx/certs:ro \
    -v /srv/docker/nginx/vhost.d:/etc/nginx/vhost.d \
    -v /srv/docker/nginx/html:/usr/share/nginx/html \
    --label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy \
    jwilder/nginx-proxy

# 将 nginx 反向代理连接到 IPv6 Docker 网络
docker network connect ipv6_bridge nginx-proxy

# 通过 Let's Encrypt 实现 SSL
docker run -d --name nginx-proxy-ssl-support \
    --restart=always \
    -v /srv/docker/certs:/etc/nginx/certs:rw \
    -v /var/run/docker.sock:/var/run/docker.sock:ro \
    --volumes-from nginx-proxy \
    jrcs/letsencrypt-nginx-proxy-companion

此时,使用 IPv6 地址在博客上发表评论,已经能够显示出用户的真实 IP 地址了:

通过 IPv6 地址在博客中发表评论,已经能够显示出用户的真实 IP 地址
通过 IPv6 地址在博客中发表评论,已经能够显示出用户的真实 IP 地址

速度测试

经过上面的一系列配置,我的博客已经能够正常支持 IPv6 了。顺便在杭州电信 100M 宽带的环境下进行了一次测速,结果如下:

wget 单线程下载:

IPv6 wget 单线程下载测试,速度约为 1.3MB/s

aria2 多线程下载(10 个线程):

IPv6 wget 单线程下载测试,速度约为 2.2MB/s

ping 延迟:

通过连续 Ping 博客的 IPv6 地址,延迟约在 270ms

发表评论

电子邮件地址不会被公开。 必填项已用*标注