前段时间有个小币种挖矿项目,有几百台机器需要接入某矿池。这原本没有任何难度,直到我批量部署完以后,一看网站后台——怎么回事,竟然只有几十台在线?

一脸懵逼的我找了台不在线的机器登上去,一看日志,大片的 HTTP 429 Too Many Requests。稍加思索后我明白了,一定是因为这个出口下面的机器太多了,触发了 API 限速。明白这一切后我更无语了,你一个矿池还搞 API 限速……

这咋办呢,跟机房说再来一打 IP?那不太现实,还是试试能不能搭代理吧。

说起代理,前段时间看了一篇利用 IPv6 获得 $2^{64}$ 个 IP 的方法[^1],刚好可以试一下,能行的话就省事儿了,一台机就搞定。

首先使用 curl -6 确认这个矿池的 API 是支持 IPv6 的,然后找一台支持 IPv6 的 vps,在上面配置一下路由和 ndp,方法见前面提到的文章,精简一下大概就像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ ip route add local 2001:19f0:6001:48e4::/64 dev enp1s0
$ sysctl net.ipv6.ip_nonlocal_bind=1
$ cat >> /etc/ndppd.conf <<EOF
route-ttl 30000
proxy enp1s0 {
    router no
    timeout 500
    ttl 30000
    rule 2001:19f0:6001:48e4::/64 {
        static
    }
}
EOF
$ curl --interface 2001:19f0:6001:48e4::1 ipv6.ip.sb
2001:19f0:6001:48e4::1

接下来,就只需要寻找一个支持绑定 IP 的反向代理工具了。

考虑到这台服务器上原本就部署了一个 caddy,我首先想的是能不能直接用 caddy 实现,可惜的是不能。于是我不得不切换回 nginx,因为 nginx 可以使用 proxy_bind 来绑定地址。

已经有好几次因为 caddy 缺乏某个功能而不得已切回 nginx 的经历了,累觉不爱了

但是,proxy_bind 只能绑定固定的地址,怎么增加一点随机性呢?刚好,原文评论区中有一位叫 @yllhwa 的同学给出了 openresty / lua module 的解法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
server {
    listen       80;
    server_name  localhost;

    location / {
        set_by_lua_block $bind_ip {
            return '2001:1111:1111:1111:1111:1111:1111:' .. string.format('%x', math.random(1, 1000))
        }
        proxy_bind $bind_ip;
        proxy_pass http://example.com;
        proxy_set_header Host example.com;
    }
}

使用这份配置随手测了下,能通,于是我决定把所有机器接入这个反向代理。

等到所有机器接入后,我却发现,上线的机器数量确实增加了,但只增加了一点。

咋回事儿啊,我查看了 nginx 日志,发现了大量类似下述报错:

1
2024/03/08 00:02:31 [crit] 1879023#1879023: *2498 bind(dead:beef:aaaa:bbbb::1) failed (97: Address family not supported by protocol) while connecting to upstream, client: 22.34.56.78, server: "reverse-proxy.com, request: "GET /url HTTP/1.1", upstream: "https://12.34.56.78:443/url", host: "reverse-proxy.com"

这个报错的意思是,我为这个请求绑定的是 IPv6 地址,但 nginx 尝试访问上游网站的 IPv4 地址,所以报错了。

怎么会这样呢?翻阅 nginx 文档 才发现,proxy_pass 会对上游域名解析出来的所有地址轮换请求:

If a domain name resolves to several addresses, all of them will be used in a round-robin fashion. In addition, an address can be specified as a server group.

嗯,这很负载均衡,唯一的问题是,你们不考虑协议的吗?

实际上,十年前就有人遇到了类似问题。但IPv6 在那时还是个麻烦制造者,大多数人都没有 IPv6 地址,于是 nginx 为 resolver 加入了 ipv6=off 来关掉对 IPv6 的解析。

可惜,十年之后的我,遇到的是相反的问题——我想关掉 IPv4。有没有 ipv4=off 呢?

答案是——Yes,and No.

如果你直接尝试使用这个功能,有很大概率,你会得到 host not found in resolver ipv4=off 的报错。

nginx 在文档中确实提到有 ipv4=off,不过是 1.23.1 才引入的。如果你和我一样对 nginx 的版本号不敏感,一定想不到,距 1.23.1 发布还不满两年,Debian 12 只有 1.22.1,Ubuntu 22.04 更是只有 1.18.0……

……我是真的无语,ipv4=offipv6=off 这两个看起来成对的功能,引入时间竟然相差近 10 年!

我原本想直接改用 docker 算了,但转念一想万一 docker 里面绑定 IPv6 地址出问题咋办,我不就又多了一个问题了?最终还是选择了使用 testing 源中的 nginx (还好,只更新了 nginx 相关的包,没有出现我预想中的把整个系统换一遍的情况)。

更新 nginx 后,这段配置终于成功跑起来了!!但是,报错还是在!!

为什么呢,如果你再次仔细阅读 nginx 的文档,你还会发现这样一段话:

Parameter value can contain variables. In this case, if an address is specified as a domain name, the name is searched among the described server groups, and, if not found, is determined using a resolver.

原来 resolver 竟然只有在 proxy_pass 参数包含变量的时候才会被使用,否则 nginx 只会在启动时解析一次。好吧,那就增加一个变量,于是最终的配置文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server {
    listen       80;
    server_name  localhost;

    location / {
        set_by_lua_block $bind_ip {
            return '2001:1111:1111:1111:1111:1111:1111:' .. string.format('%x', math.random(1, 1000))
        }
        proxy_bind $bind_ip;
        resolver 8.8.8.8 ipv4=off valid=60s;
        set $endpoint example.com;
        proxy_pass http://$endpoint;
        proxy_set_header Host example.com;
    }
}

这下终于可以工作了,浪费了我一个下午的时间……

参考