PEANUT996

从构建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 to UDP 500, UDP 1701, or UDP 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

一个握手初始化的数据包结构如下:

需要注意的是图上statictimestamp的上方有一个小箭头,意为n+16,16 是 Poly1305 authentication tag 的长度。

通过借助WireShark,可以更直观地观察握手包的结构:

下面是对于握手包字段的解释:

  • type:标识数据包类型,从1-4对应:InitiationResponseCookieReplyTransport
  • 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网络使用,展现了其多样化的应用场景。

参考文档

  1. WireGuard白皮书
  2. WireGuard基本原理
  3. Wireguard:简约之美