从构建WARP端口扫描工具中学习WireGuard协议
在之前的博客中曾经介绍过将 WireGuard 的配置从 WARP 中提取出来,再导入到使用官方的客户端或者第三方的代理工具中使用。配置文件导出的endpoint engage.cloudflareclient.com:2408
因为某种不可抗力因素下是无法正确连接的,需要手动获取直连 IP+端口。当时使用了一个来自于互联网上的闭源工具包,因为是闭源,难免对其安全性存疑。因此,花一些时间亲自动手,开发了一个工具,以更安全可控的方式实现这个功能。
如何构建端口扫描工具
那么第一步从何开始呢? 按照传统的思路,如果你想进行端口扫描别人的服务,第一步 打开扫描工具,输入IP范围,输入端口范围,启动!然后等待一段时间,便能得到扫描的端口。那么接下来只需要解决两个问题:
- 获得IP和端口范围
- 进行全量扫描
对于第一个问题,很好解决。在经过简单的搜索之后发现,Cloudflare在它的官方文档中记录了WARP的CIDR和一些常用的端口:
WARP ingress IP
These are the IP addresses that the WARP client will connect to. All traffic from your device to the Cloudflare edge will go through these IP addresses.
- IPv4 Range:
162.159.193.0/24
- IPv6 Range:
2606:4700:100::/48
WARP UDP ports
WARP utilizes UDP for all of its communications. By default, the UDP port required for WARP is
UDP 2408
. WARP can fallback toUDP 500
,UDP 1701
, orUDP 4500
.
从上面的文档可以得到一些WARP的ip范围和它常用的一些端口号。
IPv4的Ingress IP来说看起来只有256个,但是IPv6的地址范围是有2^80
个,只能从中间抽取部分IP做测试了。除了consumer only的WARP服务,zero trust的tunnel也能够基于WARP中继构建虚拟子网实现内网穿透,官方应该是预留了一些ip网段专门用于提供zero trust 的服务使用。继续深入挖掘,得到了IPv4其他的一些CIDR:
162.159.192.0/24
162.159.193.0/24
162.159.195.0/24
162.159.204.0/24
188.114.96.0/24
188.114.97.0/24
188.114.98.0/24
188.114.99.0/24
同时WARP能够使用的端口号也不只是官方文档中提到的2408/500/1701/4500, 而是:
500, 854, 859, 864, 878, 880, 890, 891, 894, 903,
908, 928, 934, 939, 942, 943, 945, 946, 955, 968,
987, 988, 1002, 1010, 1014, 1018, 1070, 1074, 1180, 1387,
1701, 1843, 2371, 2408, 2506, 3138, 3476, 3581, 3854, 4177,
4198, 4233, 4500, 5279, 5956, 7103, 7152, 7156, 7281, 7559, 8319, 8742, 8854, 8886
ip+端口现在已经有了,接下来就是最大的问题所在:如何知道在这个端口是否运行了WARP的服务呢?这里就需要一些WireGuard协议知识的补充了。
WireGuard简介
WireGuard是一个相对较新的项目,旨在用简单、快速、安全的协议取代老旧的VPN协议。与传统的VPN不同,WireGuard是建立在噪声协议框架周围的,并且仅依赖于一些精选的现代加密原语:X25519用于公钥操作,ChaCha20-Poly1305用于认证加密,以及Blake2s用于消息认证。
与QUIC类似,WireGuard在UDP上运行,但它唯一的目标是安全地封装IP数据包。因此,它不保证数据包的传递,或者数据包按照发送顺序的到达。
协议的简单性意味着它比旧的、难以维护的代码库更加稳健,并且也可以相对快速地实现。尽管它相对年轻,但WireGuard正在迅速赢得人们的青睐。
WireGuard协议
与UDP类似的是,WireGuard协议也是无状态的。在大部分场景下,握手仅需要一个RTT后即可开始传输数据:
因为仅需1-RTT的轻量设计所以WireGuard的性能也是得到了极大的提升,当然简单也意味着需要数据一致性、防范攻击的逻辑控制需要移动到应用层,这也是目前HTTP3以及未来的协议正在做的事情(比如QUIC)
Handshake Initiation
一个握手初始化的数据包结构如下:
需要注意的是图上static
和timestamp
的上方有一个小箭头,意为n+16,16 是 Poly1305 authentication tag 的长度。
通过借助WireShark,可以更直观地观察握手包的结构:
下面是对于握手包字段的解释:
- type:标识数据包类型,从1-4对应:
Initiation
,Response
,CookieReply
,Transport
。 - reserved:保留字段。无任何实际含义,可由使用者自由定义,目前Warp将其用于存放
client_id
以实现用户设备绑定和验证。 - sender:随机生成的长度为 32 的字节数组,用于唯一标识该包的发送者。
- ephemeral:临时公钥,发送方为这次握手临时生成的公钥(未加密)
- static:永久公钥,用对端公钥和临时生成的私钥 ECDH 出的临时密钥 k1 对称加密对方的公钥
- timestamp:时间戳,用对端公钥和自己的私钥 ECDH 出 k2,k2 混淆进 k1,来加密当前的时间戳
- mac1:消息验证签名: 对端公钥加上整个报文内容后的哈希
- mac2:消息验证签名: 报文内容+mac1的哈希,使用逻辑略有不同,仅在握手消息间隔小于120s时生成。
以下是完整的握手包算法生成过程:
以及mac1 & mac2 的生成算法:
编写端口检测工具
学习了大致的协议内容后,那问题就变得简单了:
只要构建握手包,然后把它通过UDP协议发出去,如果收到了回包,那就证明在这个端口上是可以使用WARP服务的。那现在的问题就变成如何构建出一个握手包,看了上面的伪代码算法实属有点复杂。
好在WireGuard协议支持跨平台的用户态实现(Cross-platform Userspace Implementation),通常情况下WireGuard运行在内核态以获取最佳的性能,但是它也允许以用户态的应用出现,前提是符合其制定的规范,以保证在相同配置下具有相同的行为。在这个大背景下,以下用户态的库便出现了,使用Go编写的wireguard-go, Rust编写的BoringTun等等。
有了第三方的库,构建握手包简单到仅需几行代码:
func buildHandshakePacket(pri device.NoisePrivateKey, pub device.NoisePublicKey) []byte {
d, _, _ := netstack.CreateNetTUN([]netip.Addr{}, []netip.Addr{}, 1480)
dev := device.NewDevice(d, conn.NewDefaultBind(), device.NewLogger(0, ""))
dev.SetPrivateKey(pri)
peer, _ := dev.NewPeer(pub)
msg, _ := dev.CreateMessageInitiation(peer)
var buf [device.MessageInitiationSize]byte
writer := bytes.NewBuffer(buf[:0])
binary.Write(writer, binary.LittleEndian, msg)
packet := writer.Bytes()
generator := device.CookieGenerator{}
generator.Init(pub)
generator.AddMacs(packet)
return packet
}
然后再把构建出来的数据包通过UDP发送出去,成功接收到回包并且记录1-RTT的时间即可:
func handshake(conn net.Conn) (bool, time.Duration) {
startTime := time.Now()
conn, _ := net.DialTimeout("udp", fullAddress, udpConnectTimeout)
_, _ = conn.Write(warpHandshakePacket)
revBuff := make([]byte, 1024)
_ = conn.SetDeadline(time.Now().Add(udpConnectTimeout))
n, err := conn.Read(revBuff)
if err != nil {
return false, 0
}
if n != wireguardHandshakeRespBytes {
return false, 0
}
duration := time.Since(startTime)
return true, duration
}
然后再根据所有的ip+端口,并发去处理即可。
代码仓库可供参考:https://github.com/peanut996/CloudflareWarpSpeedTest
总结
作为一款现代化的VPN,WireGuard以其内容简洁和优雅的实现脱颖而出。相较于之前OpenVPN的庞杂代码,WireGuard仅需4k行代码,展现了简约而高效的特性。然而,并非万事万物都是完美的,WireGuard也并非适用于所有场景。目前,WireGuard在网络拓扑结构和P2P连接中广泛应用。在云原生环境中,它可用于容器间通信,并在Kubernetes中作为加密的overlay网络使用,展现了其多样化的应用场景。