WireGuard
是由 Jason A. Donenfeld 等人建立的下一代开源 *** 协议,旨在解决许多困扰 IPSec/IKEv2
、Open***
或 L2TP
等其余 *** 协议的问题。2020 年 1 月 29 日,WireGuard 正式合并进入 Linux 5.6
内核主线。html
利用 WireGuard 咱们能够实现不少很是奇妙的功能,好比跨公有云组建 Kubernetes 集群,本地直接访问公有云 Kubernetes
集群中的 Pod IP 和 Service IP,在家中没有公网 IP 的状况下直连家中的设备,等等。linux
若是你是第一次据说 WireGuard,建议你花点时间看看我以前写的 WireGuard 工做原理。git
本文将探讨 WireGuard 使用过程当中遇到的一个重大难题:如何使两个位于 NAT 后面(且没有指定公网出口)的客户端之间直接创建链接。github
WireGuard 不区分服务端和客户端,你们都是客户端,与本身链接的全部客户端都被称之为 Peer
。缓存
WireGuard 的核心部分是加密密钥路由(Cryptokey Routing),它的工做原理是将公钥和 IP 地址列表(AllowedIPs
)关联起来。每个网络接口都有一个私钥和一个 Peer 列表,每个 Peer 都有一个公钥和 IP 地址列表。发送数据时,能够把 IP 地址列表当作路由表;接收数据时,能够把 IP 地址列表当作访问控制列表。安全
公钥和 IP 地址列表的关联组成了 Peer 的必要配置,从隧道验证的角度看,根本不须要 Peer 具有静态 IP 地址。理论上,若是 Peer 的 IP 地址不一样时发生变化,WireGuard 是能够实现 IP 漫游的。bash
如今回到最初的问题:假设两个 Peer 都在 NAT 后面,且这个 NAT 不受咱们控制,没法配置 UDP 端口转发,即没法指定公网出口,要想创建链接,不只要动态发现 Peer 的 IP 地址,还要发现 Peer 的端口。服务器
找了一圈下来,现有的工具根本没法实现这个需求,本文将致力于不对 WireGuard 源码作任何改动的状况下实现上述需求。网络
你可能会问我为何不使用中心辐射型(hub-and-spoke)网络拓扑?中心辐射型网络有一个 *** 网关,这个网关一般都有一个静态 IP 地址,其余全部的客户端都须要链接这个 *** 网关,再由网关将流量转发到其余的客户端。假设 Alice
和 Bob
都位于 NAT 后面,那么 Alice
和 Bob
都要和网关创建隧道,而后 Alice
和 Bob
之间就能够经过 *** 网关转发流量来实现相互通讯。app
其实这个方法是现在你们都在用的方法,已经没什么可说的了,缺点至关明显:
本文想探讨的是 Alice
和 Bob
之间直接创建隧道,中心辐射型(hub-and-spoke)网络拓扑是没法作到的。
要想在 Alice
和 Bob
之间直接创建一个 WireGuard 隧道,就须要它们可以穿过挡在它们面前的 NAT。因为 WireGuard 是经过 UDP
来相互通讯的,因此理论上 UDP 打洞(UDP hole punching) 是最佳选择。
UDP 打洞(UDP hole punching)利用了这样一个事实:大多数 NAT 在将入站数据包与现有的链接进行匹配时都很宽松。这样就能够重复使用端口状态来打洞,由于 NAT 路由器不会限制只接收来自原始目的地址(信使服务器)的流量,其余客户端的流量也能够接收。
举个例子,假设 Alice
向新主机 Carol
发送一个 UDP 数据包,而 Bob
此时经过某种方法获取到了 Alice
的 NAT 在地址转换过程当中使用的出站源 IP:Port
,Bob
就能够向这个 IP:Port
(2.2.2.2:7777) 发送 UDP 数据包来和 Alice
创建联系。
其实上面讨论的就是彻底圆锥型 NAT(Full cone NAT),即一对一(one-to-one)NAT。它具备如下特色:
大部分的 NAT 都是这种 NAT,对于其余少数不常见的 NAT,这种打洞方法有必定的局限性,没法顺利使用。
回到上面的例子,UDP 打洞过程当中有几个问题相当重要:
IP:Port
?RFC5389 关于 STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)的详细描述中定义了一个协议回答了上面的一部分问题,这是一篇内容很长的 RFC,因此我将尽我所能对其进行总结。先提醒一下,STUN
并不能直接解决上面的问题,它只是个扳手,你还得拿他去打造一个称手的工具:
STUN 自己并非 NAT 穿透问题的解决方案,它只是定义了一个机制,你能够用这个机制来组建实际的解决方案。
— RFC5389
STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它容许位于NAT(或多重NAT)后的客户端找出本身的公网地址,查出本身位于哪一种类型的 NAT 以后以及 NAT 为某一个本地端口所绑定的公网端口。这些信息被用来在两个同时处于 NAT 路由器以后的主机之间创建 UDP 通讯。该协议由 RFC 5389 定义。
STUN 是一个客户端-服务端协议,在上图的例子中,Alice
是客户端,Carol
是服务端。Alice
向 Carol
发送一个 STUN Binding
请求,当 Binding 请求经过 Alice
的 NAT 时,源 IP:Port
会被重写。当 Carol
收到 Binding 请求后,会将三层和四层的源 IP:Port
复制到 Binding 响应的有效载荷中,并将其发送给 Alice
。Binding 响应经过 Alice 的 NAT 转发到内网的 Alice
,此时的目标 IP:Port 被重写成了内网地址,但有效载荷保持不变。Alice
收到 Binding 响应后,就会意识到这个 Socket 的公网 IP:Port 是 2.2.2.2:7777
。
然而,STUN
并非一个完整的解决方案,它只是提供了这么一种机制,让应用程序获取到它的公网 IP:Port
,但 STUN 并无提供具体的方法来向相关方向发出信号。若是要重头编写一个具备 NAT 穿透功能的应用,确定要利用 STUN 来实现。固然,明智的作法是不修改 WireGuard 的源码,最好是借鉴 STUN 的概念来实现。总之,无论如何,都须要一个拥有静态公网地址的主机来充当信使服务器。
早在 2016 年 8 月份,WireGuard 的建立者就在 WireGuard 邮件列表上分享了一个 NAT 穿透示例。Jason 的示例包含了客户端应用和服务端应用,其中客户端应用于 WireGuard 一块儿运行,服务端运行在拥有静态地址的主机上用来发现各个 Peer 的 IP:Port
,客户端使用原始套接字(raw socket)与服务端进行通讯。
/* We use raw sockets so that the WireGuard interface can actually own the real socket. */ sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); if (sock < 0) { perror("socket"); return errno; }
正如评论中指出的,WireGuard 拥有“真正的套接字”。经过使用原始套接字(raw socket),客户端可以向服务端假装本地 WireGuard 的源端口,这样就确保了在服务端返回响应通过 NAT 时目标 IP:Port
会被映射到 WireGuard 套接字上。
客户端在其原始套接字上使用一个经典的 BPF 过滤器来过滤服务端发往 WireGuard 端口的回复。
static void apply_bpf(int sock, uint16_t port, uint32_t ip) { struct sock_filter filter[] = { BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 12 /* src ip */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ip, 0, 5), BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 20 /* src port */), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, PORT, 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); } }
客户端与服务端的通讯数据都被定义在 packet
和 reply
这两个结构体中:
struct { struct udphdr udp; uint8_t my_pubkey[32]; uint8_t their_pubkey[32]; } __attribute__((packed)) packet = { .udp = { .len = htons(sizeof(packet)), .dest = htons(PORT) } }; struct { struct iphdr iphdr; struct udphdr udp; uint32_t ip; uint16_t port; } __attribute__((packed)) reply;
客户端会遍历配置好的 WireGuard Peer(wg show <interface> peers
),并为每个 Peer 发送一个数据包给服务端,其中 my_pubkey
和 their_pubkey
字段会被适当填充。当服务端收到来自客户端的数据包时,它会向以公钥为密钥的 Peer 内存表中插入或更新一个 pubkey=my_pubkey
的 entry
,而后再从该表中查找 pubkey=their_pubkey
的 entry
,一但发现 entry
存在,就会将其中的 IP:Port
发送给客户端。当客户端收到回复时,会将 IP 和端口从数据包中解包,并配置 Peer 的 endpoint 地址(wg set <interface> peer <key> <options...> endpoint <ip>:<port>
)。
entry
结构体源码:
struct entry { uint8_t pubkey[32]; uint32_t ip; uint16_t port; };
entry
结构体中的 ip
和 port
字段是从客户端收到的数据包中提取的 IP 和 UDP 头部,每次客户端请求 Peer 的 IP 和端口信息时,都会在 Peer 列表中刷新本身的 IP 和端口信息。
上面的例子展现了 WireGuard 如何实现 UDP 打洞,但仍是太复杂了,由于并非全部的 Peer 端都能打开原始套接字(raw socket),也并非全部的 Peer 端都能利用 BPF 过滤器。并且这里还用到了自定义的 wire protocol,代码层面的数据(链表、队列、二叉树)都是结构化的,但网络层看到的都是二进制流,所谓 wire protocol
就是把结构化的数据序列化为二进制流发送出去,而且对方也能以一样的格式反序列化出来。这种方式是很难调试的,因此咱们须要另辟蹊径,利用现有的成熟工具来达到目的。
其实彻底不必这么麻烦,咱们能够直接利用 WireGuard 自己的特性来实现 UDP 打洞,直接看图:
你可能会认为这是个中心辐射型(hub-and-spoke)网络拓扑,但实际上仍是有些区别的,这里的 Registry Peer 不会充当网关的角色,由于它没有相应的路由,不会转发流量。Registry 的 WireGuard 接口地址为 10.0.0.254/32
,Alice 和 Bob 的 AllowedIPs
中只包含了 10.0.0.254/32
,表示只接收来自 Registry
的流量,因此 Alice 和 Bob 之间没法经过 Registry 来进行通讯。
这里有一点相当重要,Registry
分别和 Alice 与 Bob 创建了两个隧道,这就会在 Alice 和 Bob 的 NAT 上打开一个洞,咱们须要找到一种方法来从 Registry Peer 中查询这些洞的 IP:Port
,天然而然就想到了 DNS
协议。DNS 的优点很明显,它比较简单、成熟,还跨平台。有一种 DNS 记录类型叫 SRV记录(Service Record,服务定位记录),它用来记录服务器提供的服务,即识别服务的 IP 和端口,RFC6763 用具体的结构和查询模式对这种记录类型进行了扩展,用于发现给定域下的服务,咱们能够直接利用这些扩展语义。
选好了服务发现协议后,还须要一种方法来将其与 WireGuard 对接。CoreDNS 是 Golang 编写的一个插件式 DNS 服务器,是目前 Kubernetes 内置的默认 DNS 服务器,而且已从 CNCF 毕业。咱们能够直接写一个 CoreDNS 插件,用来接受 DNS-SD
(DNS-based Service Discovery)查询并返回相关 WireGuard Peer 的信息,其中公钥做为记录名称,fuckcloudnative.io 做为域。若是你熟悉 bind 风格的域文件,能够想象一个相似这样的域数据:
_wireguard._udp IN PTR alice._wireguard._udp.fuckcloudnative.io. _wireguard._udp IN PTR bob._wireguard._udp.fuckcloudnative.io. alice._wireguard._udp IN SRV 0 1 7777 alice.fuckcloudnative.io. alice IN A 2.2.2.2 bob._wireguard._udp IN SRV 0 1 8888 bob.fuckcloudnative.io. bob IN A 3.3.3.3
到目前为止,咱们一直使用别名 Alice 和 Bob 来替代其对应的 WireGuard 公钥。WireGuard 公钥是 Base64
编码的,长度为 44
字节:
$ wg genkey | wg pubkey UlVJVmPSwuG4U9BwyVILFDNlM+Gk9nQ7444HimPPgQg=
Base 64 编码的设计是为了以一种容许使用大写字母和小写字母的形式来表示任意的八位字节序列。
— RFC4648
不幸的是,DNS 的 SRV 记录的服务名称是不区分大小写的:
DNS 树中的每一个节点都有一个由零个或多个标签组成的名称 [STD13, RFC1591, RFC2606],这些标签不区分大小写。
— RFC4343
Base32
虽然产生了一个稍长的字符串(56
字节),但它的表现形式容许咱们在 DNS 内部表示 WireGuard 公钥:
Base32 编码的目的是为了表示任意八位字节序列,其形式必须不区分大小写。
咱们可使用 base64
和 base32
命令来回转换编码格式,例如:
$ 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 O9rAAiO5qTejOEtFbsQhCl745ovoM9coTGiprFTaHUE=
咱们能够直接使用 base32
这种不区分大小写的公钥编码,来使其与 DNS 兼容。
CoreDNS 提供了编写插件的文档,插件必需要实现 plugin.Handler
接口:
type Handler interface { ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) Name() string }
我本身已经写好了插件,经过 DNS-SD
(DNS-based Service Discovery)语义来提供 WireGuard 的 Peer 信息,该插件名就叫 wgsd。本身编写的插件不属于官方内置插件,从 CoreDNS 官方下载页下载的可执行程序并不包括这两个插件,因此须要本身编译 CoreDNS。
编译 CoreDNS 并不复杂,在没有外部插件的状况下能够这么编译:
$ git clone https://github.com/coredns/coredns.git $ cd coredns $ make
若是要加上 wgsd 插件,则在 make
前,要修改 plugin.cfg
文件,加入如下一行:
wgsd:github.com/jwhited/wgsd
而后开始编译:
$ go generate $ go build
查看编译好的二进制文件是否包含该插件:
$ ./coredns -plugins | grep wgsd dns.wgsd
编译完成后,就能够在配置文件中启用 wgsd
插件了:
.:53 { wgsd <zone> <wg device> }
能够来测试一下,配置文件以下:
$ cat Corefile .:53 { debug wgsd fuckcloudnative.io. wg0 }
运行 CoreDNS:
$ ./coredns -conf Corefile .:53 CoreDNS-1.8.1 linux/amd64, go1.15,
当前节点的 WireGuard 信息:
$ sudo wg show interface: wg0 listening port: 52022 peer: mvplwow3agnGM8G78+BiJ3tmlPf9gDtbJ2NdxqV44D8= endpoint: 3.3.3.3:8888 allowed ips: 10.0.0.2/32
下面就是见证奇迹的时候,列出全部 Peer:
$ dig @127.0.0.1 _wireguard._udp.fuckcloudnative.io. PTR +noall +answer +additional ; <<>> DiG 9.10.6 <<>> @127.0.0.1 _wireguard._udp.fuckcloudnative.io. PTR +noall +answer +additional ; (1 server found) ;; global options: +cmd _wireguard._udp.fuckcloudnative.io. 0 IN PTR TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====._wireguard._udp.fuckcloudnative.io.
查询每一个 Peer 的 IP 和端口:
$ dig @127.0.0.1 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====._wireguard._udp.fuckcloudnative.io. SRV +noall +answer +additional ; <<>> DiG 9.10.6 <<>> @127.0.0.1 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====._wireguard._udp.fuckcloudnative.io. SRV +noall +answer +additional ; (1 server found) ;; global options: +cmd tl5glqumg5vatrrtyg57hydce55wnfhx7wadwwzhmno4njly4a7q====._wireguard._udp.fuckcloudnative.io. 0 IN SRV 0 0 8888 TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====.fuckcloudnative.io. TL5GLQUMG5VATRRTYG57HYDCE55WNFHX7WADWWZHMNO4NJLY4A7Q====.fuckcloudnative.io. 0 IN A 3.3.3.3
???? ???? ???? 完美!???? ???? ????
验证公钥是否匹配:
$ wg show wg0 peers mvplwow3agnGM8G78+BiJ3tmlPf9gDtbJ2NdxqV44D8= $ dig @127.0.0.1 _wireguard._udp.fuckcloudnative.io. PTR +short | cut -d. -f1 | base32 -d | base64 mvplwow3agnGM8G78+BiJ3tmlPf9gDtbJ2NdxqV44D8=
???? ???? ????
最终实现的通讯流程以下:
一开始,Alice 和 Bob 分别与 Registry 创建了隧道;接下来,Alice 上的 wgsd-client
向 Registry 节点上运行的 CoreDNS插件(wgsd
)发起查询请求,该插件从 WireGuard 信息中检索 Bob
的 endpoint 信息,并将其返回给 wgsd-client
;而后 wgsd-client
开始设置 Bob 的 endpoint;最后 Alice 和 Bob 之间直接创建了一条隧道。
任何说起 "创建隧道 "的地方都只是意味着发生了握手,数据包能够在 Peer 之间传输。虽然 WireGuard 确实有一个握手机制,但它比你想象的更像是一个无链接的协议。
任何安全协议都须要保持一些状态,因此最初的握手是很是简单的,只是创建用于数据传输的对称密钥。这种握手每隔几分钟就会发生一次,以提供轮换密钥来实现完美的前向保密。它是根据时间来完成的,而不是根据以前数据包的内容来完成的,由于它的设计是为了优雅地处理数据包丢失的问题。
如今万事俱备,只欠东风,只须要实现 wgsd-client
就完事了。
wgsd-client
负责使 Peer 的 endpoint 配置保持最新状态,它会检索配置中的 Peer 列表,查询 CoreDNS 中与之匹配的公钥,而后在须要时为相应的 Peer 更新 endpoint 的值。最初的实现方式是以定时任务或者相似的调度机制运行,以序列化的方式检查全部 Peer,设置 endpoint,而后退出。目前它还不是一个守护进程,后续会继续改进优化。
wgsd-client
的源码位于 wgsd 仓库中的 cmd/wgsd-client 目录。
下面开始进行最终的测试。
Alice 和 Bob 都在 NAT 后面,Registry 没有 NAT,且有固定的公网地址。这三个 Peer 的信息以下:
Peer | Public Key | Tunnel Address |
---|---|---|
Alice | xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= | 10.0.0.1 |
Bob | syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= | 10.0.0.2 |
Registry | JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= | 10.0.0.254 |
它们各自的初始配置:
$ cat /etc/wireguard/wg0.conf [Interface] Address = 10.0.0.1/32 PrivateKey = 0CtieMOYKa2RduPbJss/Um9BiQPSjgvHW+B7Mor5OnE= ListenPort = 51820 # Registry [Peer] PublicKey = JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= Endpoint = 4.4.4.4:51820 PersistentKeepalive = 5 AllowedIPs = 10.0.0.254/32 # Bob [Peer] PublicKey = syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= PersistentKeepalive = 5 AllowedIPs = 10.0.0.2/32 $ wg show interface: wg0 public key: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= private key: (hidden) listening port: 51820 peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 48 seconds ago transfer: 1.67 KiB received, 11.99 KiB sent persistent keepalive: every 5 seconds peer: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= allowed ips: 10.0.0.2/32 persistent keepalive: every 5 seconds
$ cat /etc/wireguard/wg0.conf [Interface] Address = 10.0.0.2/32 PrivateKey = cIN5NqeWcbreXoaIhR/4wgrrQJGym/E7WrTttMtK8Gc= ListenPort = 51820 # Registry [Peer] PublicKey = JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= Endpoint = 4.4.4.4:51820 PersistentKeepalive = 5 AllowedIPs = 10.0.0.254/32 # Alice [Peer] PublicKey = xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= PersistentKeepalive = 5 AllowedIPs = 10.0.0.1/32 $ wg show interface: wg0 public key: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= private key: (hidden) listening port: 51820 peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 26 seconds ago transfer: 1.54 KiB received, 11.75 KiB sent persistent keepalive: every 5 seconds peer: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= allowed ips: 10.0.0.1/32 persistent keepalive: every 5 seconds
$ 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 $ wg show interface: wg0 public key: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= private key: (hidden) listening port: 51820 peer: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= endpoint: 2.2.2.2:41424 allowed ips: 10.0.0.1/32 latest handshake: 6 seconds ago transfer: 510.29 KiB received, 52.11 KiB sent peer: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= endpoint: 3.3.3.3:51820 allowed ips: 10.0.0.2/32 latest handshake: 1 minute, 46 seconds ago transfer: 498.04 KiB received, 50.59 KiB sent
Registry 与 Alice 和 Bob 都创建了链接,能够直接查询它们的 endpoint 信息:
$ dig @4.4.4.4 -p 53 _wireguard._udp.fuckcloudnative.io. PTR +noall +answer +additional ; <<>> DiG 9.10.6 <<>> @4.4.4.4 -p 53 _wireguard._udp.fuckcloudnative.io. PTR +noall +answer +additional ; (1 server found) ;; global options: +cmd _wireguard._udp.fuckcloudnative.io. 0 IN PTR YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====._wireguard._udp.fuckcloudnative.io. _wireguard._udp.fuckcloudnative.io. 0 IN PTR WMRID55V4ENHXQX2JSTYOYVKICJ5PIHKB2TR7R42SMIU3T5L4I5Q====._wireguard._udp.fuckcloudnative.io. $ dig @4.4.4.4 -p 53 YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====._wireguard._udp.fuckcloudnative.io. SRV +noall +answer +additional ; <<>> DiG 9.10.6 <<>> @4.4.4.4 -p 53 YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====._wireguard._udp.fuckcloudnative.io. SRV +noall +answer +additional ; (1 server found) ;; global options: +cmd yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.fuckcloudnative.io. 0 IN SRV 0 0 41424 YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====.fuckcloudnative.io. YUTRLED535IGKL7BDLERL6M4VJXSXM3UQQPL4NMSN27MT56AD4HA====.fuckcloudnative.io. 0 IN A 2.2.2.2
完美,下面分别在 Alice 和 Bob 上启动 wgsd-client
试试:
# Alice $ ./wgsd-client -device=wg0 -dns=4.4.4.4:53 -zone=fuckcloudnative.io. 2020/05/20 13:24:02 [JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=] no SRV records found jwhited@Alice:~$ ping 10.0.0.2 PING 10.0.0.2 (10.0.0.2): 56 data bytes 64 bytes from 10.0.0.2: icmp_seq=0 ttl=64 time=173.260 ms ^C jwhited@Alice:~$ wg show interface: wg0 public key: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= private key: (hidden) listening port: 51820 peer: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= endpoint: 3.3.3.3:51820 allowed ips: 10.0.0.2/32 latest handshake: 2 seconds ago transfer: 252 B received, 264 B sent persistent keepalive: every 5 seconds peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 1 minute, 19 seconds ago transfer: 184 B received, 1.57 KiB sent persistent keepalive: every 5 seconds
# Bob $ ./wgsd-client -device=wg0 -dns=4.4.4.4:53 -zone=fuckcloudnative.io. 2020/05/20 13:24:04 [JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=] no SRV records found jwhited@Bob:~$ wg show interface: wg0 public key: syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= private key: (hidden) listening port: 51820 peer: xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= endpoint: 2.2.2.2:41424 allowed ips: 10.0.0.1/32 latest handshake: 22 seconds ago transfer: 392 B received, 9.73 KiB sent persistent keepalive: every 5 seconds peer: JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY= endpoint: 4.4.4.4:51820 allowed ips: 10.0.0.254/32 latest handshake: 1 minute, 14 seconds ago transfer: 2.08 KiB received, 17.59 KiB sent persistent keepalive: every 5 seconds
wgsd-client
成功发现了 Peer 的 endpoint 地址并更新了 WireGuard 的配置,最终 Alice 和 Bob 之间直接创建了一条隧道!
本文探讨了如何在受 NAT 限制的两个 Peer 之间直接创建一条 WireGuard 隧道。本文提供的解决方案都是使用现有的协议和服务发现技术,以及本身写了个可插拔的插件,你能够直接使用 dig
或 nslookup
来进行调试,不须要干扰或修改 WireGuard 自己。
固然,这个 CoreDNS 插件确定还能够优化,wgsd-client
也须要继续优化。好比,CoreDNS 服务器是否应该限制只在 Registry 的隧道中可用?是否应该对域进行签名?每次查询 DNS 时是否都须要查询一次 WireGuard 的 Peer 信息,仍是说能够用缓存来解决?这些都是值得思考的问题。
wgsd 插件的代码是开源的,欢迎你们踊跃贡献。