let bind_addr = self.get_rand_ip();
let mut http = HttpConnector::new();
http.set_local_address(Some(bind_addr));
let client = Client::builder()
.http1_title_case_headers(true)
.http1_preserve_header_case(true)
.build(http);
let res = client.request(req).await?;
每一条连接用了哪个地址,都会打印一行出来,比如 example.com:443 via [2a0e:aa07:e0a0:...]:...,可以通过输出确认地址的切换和使用。
自从有了自己的 ASN 之后,我手上就一直有一段 /44 的 IPv6,后来又多了一段 /24 的 IPv4。这些地址通过 BIRD 广播出去,全世界都能路由到我这里,可是平时用到的,也就是服务器上配的那么几个,剩下的绝大部分都闲着。
/24 有 256 个 IPv4 地址,/44 则是
2^{84}个 IPv6 地址,这个数量大到基本可以当作用不完。而我有时候恰好又需要一个地址池,让不同的请求从不同的地址出去。所以我就写了 fluxy,一个 HTTP 代理,每一条经过它的连接,都会从我的地址池里随机挑一个地址作为源地址发出去。前提
fluxy 的工作原理是,在发起连接之前,把 socket 绑定到地址池里的某个地址上。
所以我们首先要让这些地址能路由回来。也就是说,前缀得先用 BIRD 广播出去,这样从这些地址发出去的包,对方的回包才能找到我。广播的配置之前已经介绍过了,在 protocol static 里写上前缀,再从 BGP session 里导出去。
我们还需要让本机接收发往这些地址的回包,并允许 socket 绑定到它们上面,可是我总不能把几百个、甚至
2^{84}个地址一个个配到网卡上。先往 lo 上加两条 local 类型的路由,让内核把发往整个前缀的包都交给本机处理。这两条路由负责接收回包,但 IPv4 和 IPv6 的绑定检查还有一点区别。IPv4 可以直接绑定到这条 local 路由覆盖的地址;IPv6 则还需要允许绑定未逐个配置到网卡上的地址。
这条设置只在当前运行时生效;需要重启后保留的话,可以把 net.ipv6.ip_nonlocal_bind=1 写进 /etc/sysctl.d/ 下的配置文件。
IPv4 的话,直接 ip addr add 44.31.223.0/24 dev lo 也能让整段前缀成为本地路由,这是回环网卡的特殊处理。如果想用 dummy 网卡承载这段地址,也需要为整个前缀显式配置 local 路由,不能只靠 ip addr add。
这样一来,整条路径就通了。
发出去的时候,包还是走默认路由,从上游出去,只是源地址换成了地址池里的某一个。回来的时候,因为前缀是我广播的,回包会沿着 BGP 的路由回到这台机器,再被 local 路由交给对应的 socket。
随机挑一个地址
随机地址的思路很简单,前缀的网络位保持不变,主机位随机生成,然后两者按位或起来。IPv6 地址本身就是一个 u128,所以直接当整数来算就行。
先右移再左移,把前缀的主机位清零,得到网络部分。随机数则反过来,先左移再右移,把高位清零,只留下主机位。最后两部分一合,就是前缀里的一个随机地址。/44 的前缀有 84 位主机位,随机出来的地址几乎不可能重复,相当于每一条连接都有一个独一无二的源地址。IPv4 也是一样的写法,只是换成了 u32。
HTTP 代理
代理部分是用 hyper 写的,基于 tokio。由于 HTTP 代理的请求分两种,所以有两种处理方式:
一种是 CONNECT。访问 HTTPS 网站的时候,客户端会先发一个 CONNECT 请求,让代理去连目标的 443 端口,连上之后代理就只负责在两边之间搬数据。所以 fluxy 收到 CONNECT 之后,自己新建一个 socket,绑定到一个随机地址,再去连接目标,最后把客户端和目标两边的流量对接起来。
另一种是普通的 HTTP 请求,代理需要替客户端把请求发出去,再把响应原样返回。这里直接用 hyper 自带的客户端,它的 HttpConnector 可以设置一个本地地址,所以每个请求都新建一个 connector,把随机地址设进去就行。
每一条连接用了哪个地址,都会打印一行出来,比如 example.com:443 via [2a0e:aa07:e0a0:...]:...,可以通过输出确认地址的切换和使用。
发布和使用
fluxy 已经发到了 crates.io,可以直接用 cargo 安装。
启动的时候指定监听地址,以及两个地址池。
-b 是监听地址,不写的话默认是 0.0.0.0:51080。-4 和 -6 分别是 IPv4 和 IPv6 的地址池,两个都是可选的,只配一个也能跑。地址池也不一定要用整个前缀,像这里 IPv6 只给了 /44 里的一个 /48,fluxy 就只会在这个 /48 里随机。
然后用 curl 通过代理访问一个查询 IP 的网站,连续请求几次,就能看到每次返回的地址都不一样。