使用DNS-SD的WireGuard端点发现和NAT穿越

2020-05-22 15:47:46

Wireguard是Jason A.Donenfeld创建的下一代跨平台VPN技术,它迅速成为多年来使用的强大、复杂的IPSec和SSL VPN解决方案的流行替代方案。作为其成功的证明,它最近被合并到了v5.6的Linux内核中。它还可以作为内核模块使用,也可以作为用Go或Rust编写的用户空间应用程序使用。如果您是Wireguard的新手,我建议您查看概念性概述,因为这将有助于您阅读本文的其余部分。

Wireguard的天才部分之一是它的密钥路由概念。加密密钥路由定义了公钥和IP地址列表(允许的IP)之间的关联。IP地址列表允许(入站)和路由(出站)Wireguard隧道内的数据包。此关联包含对等项的总最低配置,从隧道验证的角度来看,不需要静态对等项端点IP地址。这允许在两端进行内置IP漫游,前提是对等地址不会同时更改,或者在更改时有可靠的信令方式。

DNS可用于支持动态寻址的对等体,因为各种Wireguard实用程序在配置对等体时将解析DNS名称,并且存在可用于定期重新解析对等体地址的支持脚本。太棒了!这听起来很有希望,…。但是:

如果两个对等点都位于我们无法控制的NAT之后,会发生什么情况?即没有静态端口转发。

我们如何不仅发现IP地址,而且发现端口?现有的实用程序不支持此功能。

在这篇文章中,我们将着手在两个都位于NAT后面的动态寻址对等体之间建立Wireguard隧道。实现这一目标的主要目标之一是坚持使用Wireguard最纯粹的形式,也就是现在Linux内核附带的代码。我们不想以任何方式折衷它来实现我们的目标,尽管我们可以通过它的用户空间实现变得非常有创意。

你们中的一些人可能会想,为什么不使用中心辐射型呢?当然,我们只需创建从Alice和Bob到静态寻址、无NAT的中央集线器的隧道即可。爱丽丝和鲍勃可以通过集线器进行路由。

这是一种完全有效的方法,也是今天广泛使用的方法。但是,对于我们的用例,我们不感兴趣,原因如下:

对于许多对等点,集线器成为垂直扩展瓶颈。(想想物联网、互联汽车、机器人等)。

既然我们已经概述了问题,是时候开始着手了。如果我们要在Alice和Bob之间直接建立Wireguard隧道,我们需要能够穿越他们前面的NAT。由于Wireguard在UDP上工作,因此UDP打孔是我们实现这一点的最佳选择。

UDP打洞利用了这样一个事实,即大多数NAT在将入站数据包与现有“连接”进行匹配时都是宽松的。这使我们可以重用端口状态来重新进入。如果Alice向新主机Carol发送UDP数据包,并且Bob知道转换期间使用的出站源IP和Alice的NAT端口,则Bob可以通过向此IP:端口对(下图中的2.2.2.2:7777)发送UDP数据包到达Alice。

*我们的打孔示例描述的是全锥NAT。其他不太常见的NAT类型存在此方法不起作用的限制。

现在我们知道了UDP打孔是如何工作的。很好,但这仍然给我们留下了悬而未决的问题。

RFC5389 NAT会话穿越实用程序(STUN)定义了一个协议来回答其中一些问题。这是一个很长的RFC,所以我会尽我最大的努力来总结。需要注意的是,STUN不是我们试图解决的问题的临时解决方案:

STUN本身并不能解决NAT穿越问题。相反,STUN定义了一种可以在更大的解决方案中使用的工具。术语“STUN用法”用于任何使用STUN作为组件的解决方案。

STUN是一种客户端/服务器协议。在上面的示例中,Alice充当客户端,Carol充当服务器。Alice向Carol发送STUN绑定请求。当绑定请求通过Alice的NAT时,源IP:port将被重写。一旦Carol收到绑定请求,她就会将第3层和第4层报头中的源ip:port复制到绑定响应的有效负载中,并将其发送给Alice。绑定响应通过Alice的NAT传回,此时目标ip:port将被重写,但有效负载保持不变。Alice收到绑定响应,并知道此套接字的外部IP:Port为2.2.2.2:7777。

正如前面指出的,STUN不是一个完整的解决方案。STUN为应用程序提供了一种在NAT之后理解其外部IP:端口的机制,但STUN不提供向相关方发送信号的方法。如果我们从头开始编写需要NAT穿越功能的应用程序,STUN是我们应该考虑的组件。我们不是在编写Wireguard,它已经存在了,我们不能修改它(参见关于保持其源代码不变的目标)。那我们该怎么办呢?我们当然可以从STUN中吸取一些概念,并用它们来实现我们的目标。我们显然需要一台外部的静态寻址主机来发现我们可以穿透的UDP漏洞。

早在2016年8月,Wireguard的创建者在Wireguard邮件列表上分享了一个NAT打洞PoC/Example。Jason的示例包含一个客户端和服务器应用程序。客户机打算与Wireguard一起运行,而服务器运行在静态寻址的主机上,用于IP:端口发现。客户端使用原始套接字与服务器通信:

/*我们使用原始套接字,因此WireGuard接口可以实际拥有实际套接字。*/SOCK=SOCKET(AF_INET,SOCK_RAW,IPPROTO_UDP);IF(SOCK<;0){perror(";socket";);return errno;}。

正如评论中指出的,Wireguard拥有“真正的插座”。通过使用原始套接字,客户端能够在与服务器通信时欺骗Wireguard使用的源端口。这确保了在服务器上看到的源IP:端口在打回时将映射回NAT上的Wireguard套接字。

客户端在其原始套接字上使用传统的BPF过滤器来过滤从服务器发往Wireguard端口的回复:

static void Apply_bpf(int sock,uint16_t port,uint32_t ip){struct sock_filter[]={bpf_stmt(bpf_ld+bpf_W+bpf_ABS,12/*src ip*/),bpf_jpp(bpf_jmp+bpf_jeq+bpf_K,ip,0,5),bpf_stmt(bpf_ld+bpf_H+bpf_ABS,0,。3),BPF_STMT(BPF_LD+BPF_H+BPF_ABS,22/*DST port*/),BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K,port,0,1),BPF_STMT(BPF_RET+BPF_K,-1),BPF_STMT(BPF_RET+BPF_K,0)};struct sock_fprog filter_prog={.len=sizeof(Filter)/sizeof(filter[0]),.filter=filter};if(setsockopt(sock,SOL_SOCKET,SO_ATTACH_FILTER,&;filter_prog,sizeof(Filter_Prog))<;0){perror(";setsockopt(Bpf)";);exit(Errno);}}。

客户端和服务器之间通信的数据在数据包和回复结构中定义:

struct{struct udphdr udp;uint8_t my_pubkey[32];uint8_t They_pubkey[32];}__attribute__((Pack))Packet={.udp={.len=htons(sizeof(Packet)),.est=htons(Port)}};struct{struct iphdr iphdr;struct udphdr udp;uint32_t ip;uint16_t port

客户端的工作方式是遍历已配置的Wireguard对等点(WG show<;interface>;对等点),并为每个对等点向服务器发送一个数据包。my_pubkey和its_pubkey字段被适当填充。当服务器接收到来自客户端的数据包时,它使用my_pubkey字段针对由公钥键控的对等体的内存中的表执行条目的向上插入。然后,它使用thems_pubkey字段对同一个表执行查找。如果在第二次查找中找到条目,则对等体的IP:PORT将在回复中发送给客户端。当客户端收到回复时,IP和端口字段将被解包,并配置对等方的端点地址(WG设置<;interface>;对等方<;密钥>;选项.>;端点<;IP>;:<;端口>;)。

条目结构中的IP和端口字段由从客户端接收的数据包中的IP和UDP报头填充。每次客户端请求对等设备的IP和端口信息时,都会刷新对等设备表中自己的IP和端口信息。

示例客户端和服务器应用程序很好地说明了Wireguard如何成为UDP打孔解决方案的一部分。我们知道这是可能的,所以让我们在这一知识的基础上,向更适合现实世界的东西靠拢。具体地说,让我们的目标是以一种跨平台且不需要用于IP:端口发现的自定义有线协议的方式来实现这一点。我们的解决方案应该尽可能简单。不是所有的对等点都可以打开原始套接字,我们也可能无法利用BPF筛选器。如果没有自定义调试工具,自定义连接协议很难调试,所以让我们使用现有的、广泛使用的成熟工具。

在上一节中,我们探索了一个示例客户机,该客户机使用特定于Linux的网络特性从Wireguard拥有的套接字欺骗数据包。我们不是围绕Wireguard构建定制应用程序来打开和观察NAT上的IP:端口映射,而是只使用Wireguard。Wireguard隧道是极其轻量级的,我们可以简单地为IP:端口发现构建到我们的静态寻址对等体的隧道。

上图可能会让您想起轴辐图。这里的主要区别在于,我们不打算通过注册表对等体进行路由。注册表对等项有一个IPv4地址为10.0.0.254/32的Wireguard接口。爱丽丝和鲍勃的配置已更新以反映这一点。它们将只接受来自10.0.0.254/32的数据包,并通过此对等体将数据包路由到10.0.0.254/32。

与注册表对等体的Wireguard隧道在Alice和Bob的NAT上打开了一个洞,以便它们彼此连接。现在,我们需要一种方法来从注册表对等体查询这些洞的IP:端口。为此,我们将使用DNS协议。DNS相对简单、成熟(大约1987年)、跨平台,并且碰巧定义了一种记录类型,即SRV(服务)记录,用于定位服务,即识别其IP地址和端口。基于DNS的服务发现RFC6763使用具体的结构和查询模式对此记录类型进行了扩展,用于发现给定域下的服务。我们可以在我们的用例中利用这些语义。

既然我们已经选定了服务发现协议,我们就需要一种方法将其与Wireguard连接起来。CoreDNS是一个用Go编写的基于插件的DNS服务器。这是一个毕业的CNCF项目,恰好是Kubernetes的DNS服务器。让我们编写一个接受DNS-SD查询并返回相关Wireguard对等点信息的CoreDNS插件。记录名称将使用公钥(在我们的示例中为Alice&;Bob),而jordanWhited.net将用作区域。对于那些熟悉BIND样式区域文件的人来说,您可能会看到类似以下内容的区域数据:

_wireguard._udp in Ptr alice._wireguard._udp.jordanwhited.net._wireguard._udp in Ptr bob._wireguard._udp.jordanwhited.net.alice._wireguard._udp in SRV 0 1 7777 alice.jordanWhited.net.alice in A 2.2.2.2 bob._wireguard._udp in SRV 0 1 8888 bob.jordanWhited.net.bob in A 3.3.3.3

到目前为止,我们一直使用假名Alice和Bob来代替Wireguard公钥。Wireguard公钥在需要文本表示(配置文件、实用程序输出等…)的地方采用base64编码。。因此,我们将有一个44字节长的字符串,而不是Alice:

Base64编码被设计为以允许使用大写和小写字母但不需要人类可读的形式表示任意八位字节序列。

DNS树中的每个节点具有由以不区分大小写的方式处理的零个或多个标签[STD13、RFC1591、RFC2606]组成的名称。

另一方面,Base32在生成稍长一些的字符串(56字节)的同时,将允许我们在DNS内表示Wireguard公钥:

Base32编码被设计为以需要不区分大小写但不需要人类可读的形式表示任意八位字节序列。

您可以在命令行中使用base32和base64实用程序在编码格式之间来回转换。例如:

$WG genkey|WG pubkey>;pub.txt$cat pub.txt O9rAAiO5qTejOEtFbsQhCl745ovoM9coTGiprFTaHUE=$cat pub.txt|base64-D|base32 HPNMAARDXGUTPIZYJNCW5RBBBJPPRZUL5AZ5OKCMNCU2YVG2DVAQ=$cat pub.txt|base64-D|base32|base32-d|base64 O9rAiO5。

CoreDNS有关于编写插件的文档。除了设置和配置解析之外,插件还必须实现plugin.Handler接口:

我已经实现了一个CoreDNS插件,通过名为wgsd的DNS-SD语义提供Wireguard对等点信息。wgsd是一个“外部”插件,需要在编译时启用。这里介绍了两种加载外部插件的方法。我们将使用编译时构建配置文件方法。

$diff-u plugin.cfg.orig plugin.cfg-plugin.cfg.orig 2020-05-13 20:32:56.000000000-0700+plugin.cfg2020-05-13 12:24:22.000000000-0700@@-54,6+54,7@@K8S_EXTERNAL:K8S_EXTERNAL Kubernetes:Kubernetes file:file+wgsd:github.com/jwhited。

在我们的测试中,我们使用端点3.3.3.3:8888配置了单个Wireguard对等设备:

[email protected]_wireguard._udp.jordanWhited.net。ptr+Noall+Answer+Additional;<;<;>;dig 9.10.6;<;>;>;@127.0.0.1_wireguard._udp.jordanWhited.net。ptr+Noall+Answer+Additional;(找到1台服务器);;全局选项:+cmd_wireguard._udp.jordanWhited.net。0 in ptr TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q=._wireguard._udp.jordanWhited.net.。

[email protected] TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q=._wireguard._udp.jordanWhited.net.。srv+Noall+Answer+Additional;<;<;>;dig 9.10.6;<;>;>;@127.0.0.1 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q=._wireguard._udp.jordanWhited.net。srv+noall+Answer+Additional;(找到1台服务器);;全局选项:+cmd tl5glqumg5vatrrtyg57hydce55wnfhx7wadwwzhmno4njly4a7q=._wireguard._udp.jordanWhited.net。0在srv 0 0 8888 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q=.jordanWhited.net.。TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q=.jordanWhited.net.。A 3.3.3.3中的0。

$sudo WG show utun4 [email protected]_wireguard._udp.jordanWhited.net.。Ptr+Short|Cut-d。-f1|base32-d|base64mvplwow3agnGM8G78+BiJ3tmlPf9gDtbJ2NdxqV44D8=。

Alice首先与注册表建立隧道。鲍勃同时做着同样的事情。接下来,Alice上的wgsd-client(仍有待实现)查询在注册表上运行的CoreDNS插件(Wgsd)。该插件从Wireguard检索Bob的端点信息,并将其返回给wgsd-client。然后,wgsd-client设置Bob的端点值。最后,在Alice和Bob之间直接建立Wireguard隧道。

对“已建立的隧道”的任何引用仅仅意味着发生了握手,并且分组可以在对等体之间流动。请注意,虽然Wireguard确实有握手机制,但它比您想象的更像是一种连接较少的协议:

任何安全协议都需要保持某种状态,因此最初只需非常简单的握手即可建立用于数据传输的对称密钥。这种握手每隔几分钟进行一次,以便为完美的前向保密提供旋转密钥。它是基于时间完成的,而不是基于先前数据包的内容,因为它旨在优雅地处理数据包丢失。

WGSD-Client负责使对等端点配置保持最新。它检索已配置对等项的列表,查询CoreDNS以查找匹配的公钥,然后根据需要设置每个对等项的端点值。我们最初的实现打算通过cron或类似的调度机制定期运行。它以序列化的方式检查所有对等点一次,然后退出,它不是守护进程。我们稍后可以改进这一点,但现在让我们从简单的事情开始。

我们已经准备好测试我们的解决方案了。在我们的测试中,我们将在NAT后面有Alice和Bob,而没有NAT的注册表对等体。Alice连接到LTE提供商,Bob连接到住宅ISP,Registry是EC2实例。以下是所有三个对等点的公钥:

[电子邮件受保护]:~$sudo cat/etc/wireguard/utun4.conf[接口]地址=10.0.0.1/32 PrivateKey=0CtieMOYKa2RduPbJss/Um9BiQPSjgvHW+B7Mor5OnE=Listenport=51820#注册表[对等]公钥=JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDP。)侦听端口:51820Peer:JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=ENDPOINT:4.4.4.4:51820允许IPS:10.0.0.254/32最新握手:48秒前传输:1.67KiB已收到,11.99 KiB发送持久保持连接:每5秒:syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js=允许的IPS:10.0.0.2/32持久保持连接:每5秒。

[电子邮件受保护]:~$sudo cat/etc/wireguard/wg0.conf[sudo]jwhited的密码:[interface]address=10.0.0.2/32 PrivateKey=cIN5NqeWcbreXoaIhR/4wgrrQJGym/E7WrTtMtK8Gc=listenport=51820#注册表[对等]公钥=JeZlz14G8tg1Bqhhr。syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js=私钥:(隐藏)侦听端口:51820对等:JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=端点:4.4.4.4:51820允许的IPS:10.0.0.254/32最新握手:26秒前传输:1.54KiB已接收。11.75 KiB发送持久保持连接:每5秒:xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4=允许的IP:10.0.0.1/32持久保持连接:每5秒。

[email protected]:~$sudo cat/etc/wireguard/wg0.conf[Interface]Address=10.0.0.254/32 PrivateKey=wLw2ja5AapryT+3SsBiyYVNVDYABJiWfPxLzyuiy5nE=ListenPort=51820#Alice[Peer]PublicKey=xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4=AllowedIPs=10.0.0.1/32#Bob[Peer]PublicKey=syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js=AllowedIPs=10.0.0.2/32[email protected]:~$sudo wg showinterface:wg0 public key:JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=private key:(hidden)listening port:51820peer:xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4=endpoint:2.2.2.2。:41424允许的IPS:10.0.0.1/32最近一次握手:6秒前传输:收到510.29 KiB,52.11KiB发送方:syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js=端点:3.3.3.3:51820允许IPS:10.0.0.2/32最新握手:1分46秒前传输:498.04 KiB已收到,50.59KiB已发送。

使用Alice-Registry和Bob-Registry之间的活动隧道,我们应该能够查询端点信息:

[邮箱受保护]:[email protected] 5353_wireguard._udp.jordanWhited.net。PTR+Noall+Answer+Additional;<;&

..