WireGuard基本原理

WireGuard:下一代内核网络隧道

摘要:

WireGuard是一个安全的网络隧道,在第3层运行,作为Linux的内核虚拟网络接口实现,其目标是在大多数情况下取代IPsec,以及流行的用户空间和/或基于tls的解决方案,如OpenVPN,同时更安全、性能更高且更易于使用。虚拟隧道接口基于安全隧道的基本原则:对等公钥和隧道源IP地址之间的关联。它使用基于NoiseIK的单次往返密钥交换,并使用新的计时器状态机机制对用户透明地处理所有会话创建。短的预共享静态密钥(Curve25519点)用于OpenSSH风格的相互认证。该协议除了提供高度的身份隐藏之外,还提供了强大的完美前向保密性。使用ChaCha20Poly1305身份验证加密将数据包封装在UDP中可以实现传输速度。 IP绑定cookie的改进形式用于缓解拒绝服务攻击,从而大大改进了IKEv2和DTLS的cookie机制以添加加密和身份验证。总体设计不允许为接收到的数据包分配任何资源,并且从系统的角度来看,有多种有趣的Linux实现技术用于队列和并行性。最后,WireGuard可以轻松地用不到4,000行代码在Linux上实现,从而易于审核和验证。

WireGuard的优势

  • 更轻便:以Linux内核模块的形式运行,资源占用小。
  • 更高效:相比目前主流的IPSec、OpenVPN等协议,WireGuard的效率要更高。
  • 更快速:比目前主流的VPN协议,连接速度要更快。
  • 更安全:使用了更先进的加密技术。
  • 更易搭建:部署难度相对更低。
  • 更隐蔽:以UDP协议进行数据传输,比TCP协议更低调。
  • 不易被封锁:TCP阻断对WireGuard无效,IP被墙的情况下仍然可用。
  • 更省电:不使用时不进行数据传输,移动端更省电。

简介与动机

在Linux中,加密隧道的标准解决方案是IPsec,它使用Linux转换(“xfrm”)层。用户填充一个内核结构,以确定哪个密码套件和密钥,或其他转换(如压缩)用于遍历子系统的哪个选择器。通常,用户空间守护进程负责根据密钥交换的结果更新这些数据结构,通常使用IKEv2来完成,而IKEv2本身是一个复杂的协议,具有很多选择和可扩展性。这种解决方案的复杂性以及代码的数量是相当可观的。管理员有一套完全独立的防火墙语义和IPsec包的安全标签。从语义的观点来看,将密钥交换层与传输加密或转换层分开是明智的选择,并且从网络的角度来看,相似的是将转换层与接口层分开是正确的,但是这种严格正确的分层方法增加了复杂性,难以正确实施和部署。

WireGuard消除了这些分层分隔。WireGuard没有提供复杂的IPsec和xfrm层,而是提供了一个虚拟接口(例如wg0),然后可以使用标准的ip和ifconfig实用程序对其进行管理。在使用私钥(以及可选的预共享对称密钥和它将安全通信的对等点的各种公钥配置接口之后,隧道就简单地工作了。密钥交换、连接、断开、重连接、发现等在幕后透明而可靠地发生,管理员不需要担心这些细节。换句话说,从管理的角度来看,WireGuard接口似乎是无状态的。然后可以使用普通的基础结构为防火墙接口配置防火墙规则,并确保对来自WireGuard接口的数据包进行身份验证和加密。与IPsec相比,WireGuard简单明了,不容易发生灾难性故障和配置错误。需要强调的是,IPsec的分层是正确无误的。使用IPsec,一切都在正确的地方,以达到学术上的完美。但是,正如抽象的正确性经常发生的那样,它严重缺乏可用性,并且很难实现可验证的安全实现。相反,WireGuard从有缺陷的分层违规开始,然后试图纠正这种合并产生的问题,使用实用的工程解决方案和解决现实世界问题的密码技术。

另一个极端是OpenVPN,它是一种基于用户空间TUN/TAP的解决方案,使用TLS。由于它位于用户空间,它的性能非常差——因为包必须在内核空间和用户空间之间复制多次——而且需要一个长期存在的守护进程;对于管理员来说,OpenVPN并不是无状态的。虽然TUN/TAP接口(比如tun0)具有上述类似于wg0的优点,但OpenVPN也非常复杂,它支持大量的TLS功能,这将相当一部分代码暴露给了潜在的漏洞。OpenVPN可以在用户空间中实现,因为内核中的ASN.1和x509解析器历来都有很大的问题(CVE-2008-1673、CVE-2016-2053),而添加一个TLS堆栈只会使问题变得更糟。TLS还带来了一个巨大的状态机,以及源IP地址和公钥之间不太清晰的关联。

对于密钥分发,WireGuard从OpenSSH汲取灵感,其常见用法包括一种非常简单的密钥管理方法。通过一组不同的带外机制,两个对等点通常交换其静态公钥。有时,它很简单,就像PGP签名的电子邮件一样,而有时,它是使用LDAP和证书颁发机构的复杂密钥分发机制。重要的是,大多数情况下,OpenSSH密钥分发完全不可知。 WireGuard也效仿。两个WireGuard对等方通过某种未指定的机制交换其公钥,之后便可以进行通信。换句话说,WireGuard对待密钥分发的态度是,这是解决特定问题的错误层,因此界面足够简单,因此可以与任何密钥分发解决方案一起使用。另外一个优点是,公钥只有32个字节长,并且可以轻松地用44个字符的Base64编码表示,这对于通过各种不同的介质传输密钥非常有用。

最后,WireGuard具有密码学依据。 它故意缺乏密码和协议敏捷性。如果在基础原语中发现漏洞,则需要更新所有端点。正如不断涌现的SSL/TLS漏洞所显示的,密码的敏捷性极大地增加了复杂性。 WireGuard使用Trevor Perrin的Noise的变体(在其开发过程中收到了来自本文作者的大量输入,目的是为了在WireGuard中使用)与Curve25519进行1-RTT密钥交换[5]。 对于ECDH,HKDF [15]用于扩展ECDH结果,RFC7539 [17]的ChaCha20 [3]和Poly1305 [8]的结构用于认证加密,而BLAKE2s [2]的哈希处理。 它使用新的crypto-cookie机制来实现IP地址可归属性,从而内置了防止拒绝服务攻击的保护功能。

同样,WireGuard仅适用于第3层。这是确保数据包真实性和可归属性的最干净的方法。 作者认为,layer 3是桥接多个IP网络的正确方法,将其应用到WireGuard上可以实现许多简化,从而产生一个更清晰、更容易实现的协议。它支持IPv4和IPv6的第3层,可以封装v4-in-v6和v6-in-v4

WireGuard将这些原则组合在一起,专注于简单性和可审核的代码库,同时仍然保持极高的速度,并适合于少数环境。 通过将密钥交换和第3层传输加密组合为一种机制,并使用虚拟接口而不是转换层,WireGuard确实打破了传统的分层原则,从而寻求了一种既实用又安全的可靠工程解决方案。 一路走来,它采用了几种新颖的密码和系统解决方案来实现其目标。

密钥路由Cryptokey Routing

安全VPN的基本原理是对等节点(peer)之间的关联,每个节点的IP地址都允许用作源IP。在WireGuard中,对等点(peer)严格由它们的公钥(32字节Curve25519点)识别。这意味着在公钥和一组允许的IP地址之间存在一个简单的关联映射。检查下面的密钥路由表:

Configuration 1a

Interface Public Key Interface Private Key Listening UDP Port
HIgo…8ykw yAnz…fBmk 41414
Peer Public Key Allowed Source IPs
xTIB…p8Dg 10.192.122.3/32, 10.192.124.0/24
TrMv…WXX0 10.192.122.4/32, 192.168.0.0/16
gN65…z6EA 10.10.10.230/32

接口本身有一个私有密钥和一个UDP端口,它在上面侦听,后面是对等点列表。每个对等点(peer)由其公钥标识。每个ip都有一个允许源ip的列表。

当外发包在WireGuard接口wg0上传输时,要参考此表来确定使用哪个公钥进行加密。例如,目的地IP为10.192.122.4的数据包将使用来自公钥TrMv…WXX0的安全会话进行加密。相反,当wg0收到一个加密的包时,在对它进行解密和认证之后,只有当它的源IP在表中解析为安全会话中用于解密它的公钥时,它才会接受它。例如,如果一个包解密从xTIB…qp8D,只有在解密包的源IP为10.192.122.3或在10.192.124.0到10.192.124.255之间才允许解密;否则它将被丢弃(dropped)。

有了这个非常简单的原则,管理员可以依赖于简单的防火墙规则。例如,在接口wg0上的一个源IP为10.10.10.230的数据包可以被认为是来自具有gN65…Bz6E公钥的对等方。更一般的情况是,到达WireGuard接口的任何数据包都将具有可靠的可信源IP(当然,这是为了保证传输的完美前向保密)。请注意,这是唯一可能的,因为WireGuard是严格基于第3层。与一些常见的VPN协议(如L2TP/IPsec)不同,在第3层使用经过身份验证的对等点标识可以实现更明确的网络设计。

如果一个WireGuard对等点希望路由所有流量通过另一个WireGuard对等点,则密钥路由表可以更简单地配置为:

Configuration 2a

Interface Public Key Interface Private Key Listening UDP Port
gN65…z6EA gI6E…fWGE 21841
Peer Public Key HIgo…8ykw Allowed Source IPs 0.0.0.0/0

在这里,对等体授权HIgo…f8yk将包与任何源IP放到wg0上,wg0上发出的所有包将使用与该公钥关联的安全会话进行加密并发送到该对等点的端点(peer’s endpoint.)。

端点和漫游(Endpoints & Roaming)

当然,对等点能够在特定的互联网端点向彼此发送加密的WireGuard UDP数据包是很重要的。密钥路由表中的每个对等点可以选择性地预先指定该对等点端点的已知外部IP地址和UDP端口。它是可选的原因是,如果它没有被指定并且WireGuard从一个对等点接收到一个正确的经过身份验证的包,它将使用外部源IP地址来确定端点。

由于公钥唯一地标识一个对等点,加密的WireGuard包的外部源IP被用来标识对等点的远程端点,使对等点能够在不同的外部IP(例如在移动网络之间)之间自由漫游,类似于Mosh[25]所允许的内容。例如,可以扩充之前的加密密钥路由表,使其具有一个对等点的初始端点:

Configuration 2b

Interface Public Key Interface Private Key Listening UDP Port
gN65…z6EA gI6E…fWGE 21841
Peer Public Key Allowed Source IPs Internet Endpoint
HIgo…8ykw 0.0.0.0/0 192.95.5.69:41414

然后,这个主机,gN65…z6EA,发送加密包HIgo…f8yk给192.95.5.69:41414。后HIgo……f8yk收到一个包,它更新它的表,以学习发送应答包的端点,例如,192.95.5.64:21841:

Configuration 1b

Interface Public Key Interface Private Key Listening UDP Port
HIgo…8ykw yAnz…fBmk 41414
Peer Public Key Allowed Source IPs Internet Endpoint
xTIB…p8Dg 10.192.122.3/32, 10.192.124.0/24
TrMv…WXX0 10.192.122.4/32, 192.168.0.0/16
gN65…z6EA 10.10.10.230/32 192.95.5.64:21841

请注意,对等方的侦听端口和发送的数据包的源端口始终相同,从而大大简化了操作,同时还确保了在NAT之后的可靠遍历。 而且,由于此漫游属性确保对等方将拥有最新的外部源IP和UDP端口,因此NAT无需使会话长时间保持打开状态。 (对于必须无限期地打开NAT会话或有状态防火墙的用例,可以选择将接口配置为定期发送经过身份验证的持久性Keepalive。)

这种设计带来了极大的便利和最小的配置。 虽然拥有活跃中间人的攻击者当然可以修改这些未经身份验证的外部源IP,但是攻击者将无法解密或修改任何有效载荷,这仅相当于拒绝服务攻击, 只需从假定的中间人位置丢弃原始数据包,这已经很容易实现。 而且,无法解密并随后回复数据包的主机将很快被遗忘。

发送和接收的数据流

漫游的设计,结合密钥路由表,在wg0接口上使用上节“Configuration 1”的配置,对数据包的发送和接收流程如下:

本地生成(或转发)数据包,并准备在出方向接口wg0上传输:

  1. 明文包到达WireGuard接口wg0。
  2. 数据包的目的IP地址192.168.87.21被检查,它与对等节点TrMv…WXX0匹配。(如果它没有匹配的对等点,它被丢弃,并且发送者被一个标准的ICMP“no route to host”数据包通知,并且返回-ENOKEY到用户空间。)
  3. 与对等方关联的安全会话的发送对称加密密钥和 nonce随机数计数器,
    TrMv … WXX0用于使用ChaCha20Poly1305算法加密明文数据包。
  4. 包含在第后续章节中说明的各个字段的标头被放在现在加密的数据包之前。
  5. 此标头和加密的数据包一起作为UDP数据包发送到与对等TrMv … WXX0关联的Internet UDP / IP端点,从而导致外部UDP / IP数据包包含标头和加密的内部- 包。 对等端点是预先配置的,或者是从接收到的最新经过正确身份验证的数据包的外部外部源IP标头字段中获悉的。 (否则,如果无法确定端点,则将数据包丢弃,发送ICMP消息,并将-EHOSTUNREACH返回到用户空间。)

一个UDP/IP数据包到达主机的UDP端口41414,该端口为wg0接口监听UDP端口:

  1. 在正确的端口(在本例中,端口41414)上接收到一个UDP/IP数据包,其中包含一个特定的报头和一个加密的有效负载。
  2. 使用WireGuard报文头,WireGuard确定它与peer TrMv…WXX0的关联安全会话,检查消息计数器的有效性,并尝试使用安全会话中接收对称密钥对其进行身份验证和解密。如果它不能确定一个对等点或身份验证失败,数据包将被丢弃。
  3. 从数据包被正确验证以后,外部UDP/IP数据包的源IP被用于更新对等TrMv…WXX0的端点。
  4. 一旦数据包有效负载被解密,接口就有一个明文数据包。如果这不是一个IP包,它将被丢弃。否则,WireGuard检查明文包内的源IP地址是否在密钥路由表中有相应的路由。例如,如果解密的明文包的源IP是192.168.31.28,则这个数据包会被进行相应路由。但是,如果源IP是10.192.122.3,数据包就不会为这个对等体进行相应路由,而被丢弃。
  5. 如果明文包没有被丢弃,它将被插入到wg0接口的接收队列中。

可以将允许的IP列表分为两个列表—一个用于检查传入数据包的源地址,另一个用于根据目标地址选择对等方。但是,通过将它们保留在同一列表中,可以实现类似于反向路径过滤的功能。发送数据包时,将根据目标IP查询该列表。当接收到数据包时,将使用相同的列表来确定是否允许源IP。但是,与其询问接收数据包的发送对等方是否具有该源IP作为其允许的IP列表的一部分,它还可以询问一个更全局的问题-在表中为该源IP选择哪个对等方,并且这样做将从匹配的对等方接收数据包。这将强制执行发送和接收IP地址的一对一映射,因此,如果从特定对等方接收到数据包,则将确保对该IP的答复到达该同一个对等方。

基本用法(Basic Usage)

在深入了解密码学和实现细节之前,看看一个使用WireGuard的简单命令行接口可能会很有用,从而将目前所介绍的概念具体化。

考虑一个具有单个物理网络接口eth0的Linux环境,它使用192.95.5.69的公共IP将其连接到Internet。可以添加WireGuard接口wg0并使用标准ip实用程序进行配置在/ 24子网中具有10.192.122.3的隧道IP地址,如左图所示。然后可以使用wg(8)工具以多种方式配置密钥路由表,包括从配置文件中读取信息,如右图所示:

image-20201020084837820

此时,向该系统上的10.10.10.230发送数据包将通过wg0接口发送数据,该接口将使用与公钥gN65…z6EA相关联的安全会话加密数据包和发送加密和经过UDP封装的数据包到192.95.5.70:54421。当在wg0上收到来自10.10.10.230的数据包时,管理员可以确信它是来自gN65…z6EA。

协议和加密学(Protocol&Cryptography)

如前所述,为了开始发送加密的封装数据包,必须先进行1-RTT密钥交换握手。发起方将消息发送到响应方,响应方将消息发送回发起方。在此握手之后,发起者可以使用共享的对称密钥对将加密消息发送给响应者,一个用于发送,一个用于接收,并且在第一个加密消息从发起方发送到响应方之后,响应方可以开始向发起方发送加密的消息。此顺序限制要求要求进行确认,如针对KEA + C [18]所述,并允许握手消息被异步处理以传输数据消息。这些消息除了使用新颖的cookie结构来减轻拒绝服务攻击之外,还使用Noise [23]中的“ IK”模式。该协议的最终结果是一个非常健壮的安全系统,该系统达到了认证密钥交换(AKE)安全性的要求[18],避免了密钥泄露,避免了重播攻击,提供了完美的前向保密性,提供了静态公共身份的隐藏密钥类似于SIGMA [16],并且具有拒绝服务攻击的能力。

Silence is a Virtue:

WireGuard的一个设计目标是避免在身份验证之前存储任何状态,并且不对未经身份验证的数据包发送任何响应。由于没有存储未经身份验证的数据包的状态,也没有生成响应,因此,WireGuard对非法对等端和网络扫描程序是不可见的。通过不允许未经身份验证的数据包影响任何状态,可以避免几类攻击。而且更普遍的是,有可能以一种根本不需要动态内存分配的方式来实现WireGuard,即使对于经过身份验证的数据包也是如此。但是,此属性要求响应者收到的第一条消息就对发起者进行身份验证。 像这样在第一个数据包中进行身份验证可能会使响应者面临重放攻击的风险。攻击者可以重播初始握手消息,以诱使响应者重新生成其临时密钥,从而使合法发起者的会话无效(尽管不会影响任何消息的保密性或真实性)。为防止这种情况,在第一个消息中包含12字节的TAI64N [7]时间戳,并对其进行加密和身份验证。响应者跟踪每个对等方收到的最大时间戳,并丢弃包含小于或等于该时间戳的数据包。 (实际上,它甚至不必是准确的时间戳;它仅必须是按对等方单调递增的96位数字。)如果响应方重新启动并丢失了此状态,那不是问题:即使初始可以重播较早的数据包,它不可能中断任何正在进行的安全会话,因为响应者刚刚重新启动,因此没有活动的安全会话要中断。一旦发起方在重新发起后与响应方重新建立了安全会话,发起方将使用更大的时间戳,从而使前一个时间戳无效。此时间戳可确保攻击者不会通过重播攻击中断发起者和响应者之间的当前会话。 (这也意味着两个不同的对等点不应该共享私钥,因为在这种情况下,发送给一个对等点的数据包可能会被重放给另一个对等点,随后产生的响应将导致发起者不由自主地从一个对等点漫游到另一个。但无论如何,一开始就不应该共享私钥。)从实现的角度来看,TAI64N[7]非常方便,因为它是big-endian,允许使用标准memcmp()对两个12字节的时间戳进行比较。由于WireGuard不使用签名,为了获得一定程度的可否认性,第一条消息仅依赖于两个对等点静态密钥的Diffie-Hellman结果进行身份验证。这意味着,如果它们中的任何一个静态密钥被破坏,攻击者将能够伪造包含最大时间戳值的初始消息(尽管它无法完成完整的handshap),从而阻止所有未来的连接成功。虽然这看起来类似于传统的密钥泄露模拟漏洞(WireGuard并不是它的弱点),但它实际上非常不同。因为,如果一个密钥泄露使得攻击者能够阻止其他节点再次使用他们泄露的密钥,那么攻击者实际上已经帮助了对这种泄露的正确响应。如果TIA64N时间戳的精度造成了不合适的信息泄漏,实现可能会截断时间戳的24位纳秒部分。

可选的预共享对称密钥模式

WireGuard依赖于先与对等方(peer)彼此交换静态公钥,作为它们的静态身份。所有数据发送的保密性依赖于Curve25519 ECDH函数的安全性。为了减少量子计算未来的进步,WireGuard还支持一种模式,在这种模式中,任何一对对等点可以在它们之间额外预先共享一个256位的对称加密密钥,以增加一层对称加密。这里的攻击模式是,对手可能会长期记录加密的流量,希望有一天能够打破Curve25519并解密过去的流量。尽管从密钥管理的角度来看,预共享对称加密密钥通常很麻烦,而且更有可能被窃取,但这种想法是,当量子计算进步到突破Curve25519时,这种预共享对称密钥早已被遗忘。更重要的是,从短期来看,如果预先共享的对称密钥被破坏,Curve25519密钥仍然提供足够的保护。与使用完全的后量子密码系统(到编写时还不适合在这里使用)不同,这种预共享对称密钥的可选混合方法为极端偏执的人提供了一种合理的和可接受的折衷方案。此外,它还允许在WireGuard上构建复杂的键旋转方案,以实现各种类型的折衷后安全性。

WireGuard依赖于先与对等方(peer)彼此交换静态公钥,作为它们的静态身份。发送的所有数据的保密性取决于Curve25519 ECDH功能的安全性。为了减轻将来在量子计算方面的任何进步,WireGuard还支持一种模式,在该模式下,任何对对端都可以在彼此之间预共享一个256位对称加密密钥,以便添加一层对称加密。这里的攻击模型是,攻击者可能会长期记录加密的流量,以期有一天能够打破Curve25519并解密过去的流量。虽然预共享对称加密密钥通常从密钥管理的角度来看很麻烦,并且可能更容易被盗,但是这种想法是,随着量子计算的发展打破了Curve25519,这种长共享对称密钥早已被人们遗忘。而且,更重要的是,在短期内,如果预共享的对称密钥遭到破坏,Curve25519密钥仍然可以提供足够的保护。替代使用完整的后量子密码系统(在撰写本文时不适合在此处使用),此预共享对称密钥的可选混合方法可补充椭圆曲线密码,为用户提供了合理的折衷方案。极度偏执。此外,它还允许在WireGuard上构建复杂的键旋转方案,以实现各种类型的折衷后安全性。

缓解拒绝服务攻击和Cookies

即使Curve25519在大多数处理器上是非常快的曲线,计算Curve25519点乘法也是CPU密集型的。 为了确定握手消息的真实性,必须计算Curve25519乘法,这意味着可能会出现拒绝服务攻击。为了防止cpu耗尽攻击,如果响应方——消息的接收方——处于负载状态,它可以选择不处理握手消息(启动或响应握手消息),而是使用包含cookie的cookie应答消息进行响应。然后发起者使用这个cookie来重新发送消息,并让它在接下来的时间被响应者接受。

响应方维护一个秘密的随机值,该值每两分钟更改一次。cookie只是使用这个变化的密钥作为MAC密钥计算发起者源IP地址的MAC的结果。发起方在重新发送它的消息时,使用这个cookie作为MAC密钥发送它的消息的MAC。当responder接收到消息时,如果它处于负载状态,它可以根据是否有正确的MAC使用cookie作为密钥来选择是否接受和处理消息。这种机制将从发起者发送的消息绑定到它的IP地址,提供IP所有权的证明,允许使用经典的IP速率限制算法来限制速率(令牌桶等)。

这或多或少是DTLS [24]和IKEv2 [13]使用的方案。 但是,它具有三个主要缺陷。 首先,我们希望保持silent,不要对未认证的消息发送任何回复; 在负载下不加选择地发送cookie回复消息会破坏此属性。 其次,cookie不应以明文形式发送,因为中间人可能会使用它来发送经过处理的欺诈性消息。 第三,发起者本人可能会被发送欺诈性cookie,从而遭到拒绝服务攻击,然后,它就无法成功使用它来计算其消息的MAC。 WireGuard的cookie机制使用两个MAC(msg.mac1和msg.mac2)解决了这些问题。

对于第一个问题,为了响应方保持沉默,即使在负载下,所有消息都有一个使用响应方公钥的第一个MAC (msg.mac1)。这意味着,为了引出任何响应,发送消息的对等方至少必须知道它在与谁交谈(通过知道它的公钥)。无论是否加载,第一个MAC (msg.mac1)都必须存在且有效。虽然响应方的公钥本身不是秘密的,但在这个攻击模型中它是足够秘密的,其目标是确保服务的私密性,因此知道响应方的公钥就足以证明已经知道它的存在。(值得注意的是,第一个MAC允许被动攻击者猜测数据包的目的是哪个公钥,这略微削弱了身份隐藏属性,尽管正确的猜测不会构成密码证明,因为在生成MAC时没有使用私有材料。)

同样地,为了解决第二个问题——用明文发送mac电脑——我们对传输中的cookie应用了一个扩展的随机现时的AEAD,同样使用响应方的公钥作为对称加密密钥。同样,这里的大多数公共价值对于我们在拒绝服务攻击威胁模型中的目的已经足够了。

最后,为了解决第三个问题,我们使用AEAD的“附加数据”字段对传输中的cookie进行加密,对引发cookie回复消息的初始消息的第一个MAC (msg.mac1)进行附加认证。这确保了没有中间人的攻击者不能发送无效的cookie的种子到发起者,以防止他们验证一个正确的cookie。(攻击者一个中间人的位置可以简单地把cookie应答消息无论如何防止连接,所以这种情况下是不相关的,但攻击者,只是被动的中间人的地位的确可能伪造这些数据包,这不是明显不同于一个拒绝服务攻击的TCP)。换句话说,我们使用AD字段将cookie响应绑定到启动消息。

解决了这些问题后,我们就可以使用安全传输的cookie作为MAC密钥添加前面提到的第二个MAC (msg.mac2)。当响应器处于负载状态时,它将只接受另外拥有第二个MAC的消息。

总之,响应方在计算了这些mac并将其与收到的消息进行比较后,必须总是拒绝带有无效msg的消息。当负载不足时,可能会拒绝带有无效msg.mac2的消息。如果响应者收到一个有效的消息。mac1,但有一个无效的mac2,并在负载下,它可能会响应一个cookie回复消息。这大大改进了DTLS和IKEv2使用的cookie方案。

HIPv2[20]通过使用2-RTT密钥交换和复杂性谜题解决了这个问题,与之相反,WireGuard避开了解谜结构,因为前者需要存储状态,而后者使发起者和响应者之间的关系不对称。在WireGuard中,任何节点上的任何一方都可能被激发开始握手。这意味着要求发起者提供一个复杂的难题是不可行的,因为发起者和响应者可能很快就会改变角色,从而将此缓解机制转变为拒绝服务漏洞本身。相反,我们上面的cookie解决方案支持在1-RTT协议上缓解拒绝服务攻击,同时保持发起者和响应者角色对称。

消息交互(Messages)

有四种类型的消息,每一种都以一个单字节消息类型标识符作为前缀,标记为msg.type如下:

  1. 开始建立安全会话的握手过程的握手启动消息。
  2. 对结束握手的初始化消息的握手响应,在此之后可以建立安全会话
  3. 对握手启动消息或握手响应消息的响应,它通信一个加密的cookie值,用于重新发送被拒绝的握手启动消息或握手响应消息。
  4. 一种封装和加密的IP包,它使用通过握手协商的安全会话。

协议概述:

具体报文字段细节:https://www.wireguard.com/protocol/

在大多数情况下,握手在1-RTT将完成,之后传输数据如下:

image-20201104085400411

如果有一个对等端负载不足,则在握手时添加一条cookie回复消息,以防止拒绝服务攻击:

image-20201104085453001

Routing & Network Namespace Integration

与所有Linux网络接口一样,WireGuard集成到网络名称空间基础结构中。这意味着管理员可以拥有几个完全不同的网络子系统,并选择每个子系统中都存在哪些接口。

WireGuard做了一些非常有趣的事情。创建WireGuard接口时(with ip link add wg0 type wireguard),它会记住创建它的命名空间。我是在命名空间A中创建的。稍后,WireGuard可以移动到新的命名空间(我要移动到命名空间B),但它仍然会记得它起源于命名空间A。

WireGuard使用UDP套接字发送和接收加密数据包。此套接字始终位于命名空间A-原始出生地命名空间中。这允许一些非常酷的属性。也就是说,您可以在一个命名空间(A)中创建WireGuard接口,将其移动到另一个命名空间(B),并通过命名空间A中的UDP套接字。

(请注意,通过在一个名称空间中创建套接字文件描述符,然后再更改为另一个名称空间,并使先前名称空间中的文件描述符保持打开状态,这种相同的技术可用于基于用户空间TUN的接口。)

这提供了一些非常好的可能性。

普通容器:

最明显的用法是为容器(例如Docker容器)提供WireGuard接口作为其唯一接口。

1
2
3
4
5
6
7
8
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN group default qlen 1
link/none
inet 192.168.4.33/32 scope global wg0
valid_lft forever preferred_lft forever

在这里,唯一可能访问网络的方法是通过WireGuard接口wg0。

Container Network Namespace Diagram

完成这样的设置的方法如下:

  1. 首先,我们创建一个称为“container”的网络名称空间:

    # ip netns add container

  2. 接下来,我们在“ init”(原始)名称空间中创建一个WireGuard接口:

    # ip link add wg0 type wireguard

  3. 最后,我们将该接口移到新的名称空间中:

    # ip link set wg0 netns container

  4. 现在,我们可以像往常一样配置wg0,除了我们这样做时指定它的新名称空间:

    1
    2
    3
    4
    # ip -n container addr add 192.168.4.33/32 dev wg0
    # ip netns exec container wg setconf wg0 /etc/wireguard/wg0.conf
    # ip -n container link set wg0 up
    # ip -n container route add default dev wg0
  5. 现在访问“container”的任何网络资源的唯一方法就是通过WireGuard接口。

    请注意,Docker用户可以指定Docker进程的PID而不是网络名称空间名称,使用Docker已经为其容器创建的网络名称空间:

    # ip link set wg0 netns 879

路由所有流量:

一种不太明显的用法,但功能极其强大,是使用WireGuard的此特性通过WireGuard重定向所有普通Internet流量。但是首先,让我们回顾一下执行此操作的旧的常用解决方案:

经典解决方案:

经典的解决方案依赖于不同类型的路由表配置。对于所有这些,我们需要为实际的WireGuard端点设置一些显式路由。对于这些示例,让我们假设WireGuard端点是demo.wireguard.com,在编写时,它解析为163.172.161.0。此外,让我们假设我们通常使用eth0和192.168.1.1的经典网关连接到互联网。

替换默认路由

最简单的技术是仅替换默认路由,为WireGuard端点添加一个明确的规则:

1
2
3
# ip route del default
# ip route add default dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

这很有效并且相对简单,但是不幸的是,DHCP守护程序等都希望撤消我们刚才所做的事情。

覆盖默认路由

因此,除了替换默认路由外,我们还可以使用另外两个特定的规则来覆盖它,这些规则的总和为默认值,但在默认值之前匹配:

1
2
3
# ip route add 0.0.0.0/1 dev wg0
# ip route add 128.0.0.0/1 dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

这样,我们就不会破坏默认路由。这也很好用,但是不幸的是,当eth0 up/down时,demo.wireguard.com的显式路由将被忘记,这很烦人。

基于规则的路由

有些人更喜欢使用基于规则的路由和多个路由表。工作方式是我们为WireGuard路由创建一个路由表,为明文Internet路由创建一个路由表,然后添加规则来确定使用哪个路由表:

1
2
3
# ip rule add to 163.172.161.0 lookup main pref 30
# ip rule add to all lookup 80 pref 40
# ip route add default dev wg0 table 80

现在,我们能够将路由表分开。不幸的是,缺点是仍然需要添加显式端点规则,并且在删除接口时没有清理,需要更复杂的路由规则重复。

改进的基于规则的路由

先前的解决方案依赖于我们知道应该从tunnel中豁免的显式端点IP,但是WireGuard端点可以漫游,这意味着该规则可能会过时。幸运的是,我们能够对所有从WireGuard的UDP套接字发出的数据包设置fwmark,然后将其从tunnel中豁免:

1
2
3
4
# wg set wg0 fwmark 1234
# ip route add default dev wg0 table 2468
# ip rule add not fwmark 1234 table 2468
# ip rule add table main suppress_prefixlength 0

我们首先在接口上设置fwmark,并在替代路由表上设置默认路由。然后我们指示没有fwmark的数据包应该转到这个替代路由表。最后,我们为仍然访问本地网络添加了一个方便的功能,通过这个功能,我们允许没有fwmark的数据包使用主路由表,而不是WireGuard接口的路由表,如果它匹配其中前缀长度大于零的任何路由,例如非默认本地路由。这是wg-Quick(8)工具使用的技术。

改进经典解决方案

WireGuard的作者有兴趣在内核中添加一个名为notoif的特性来覆盖隧道用例。这将允许接口说“不要用我自己作为接口路由,以避免路由环路。”WireGuard将能够添加像.flowi4_not_oif = wg0_idx这样的行,基于用户空间tun的接口将能够在他们的输出套接字上设置一个选项,比如set so kopt(fd,so_NOTOIF, Tun0_idx);

新的命名空间解决方案

事实证明,我们可以使用网络名称空间通过WireGuard路由所有互联网流量,而不是经典的路由表。这种方法的工作原理是,我们将连接到互联网的接口,如eth0或wlan0,移动到命名空间(我们称之为“physical”),然后有一个WireGuard接口是init命名空间中唯一的接口。

Physical Network Namespace Diagram

  1. 首先,我们创建“physical”网络名称空间:

    # ip netns add physical

  2. 现在,将eth0和wlan0移到“physical”名称空间中:

    1
    2
    # ip link set eth0 netns physical
    # iw phy phy0 set netns name physical

    (请注意,必须使用iw并通过指定物理设备phy0来移动无线设备。)

  3. 现在,我们在“physical”名称空间中具有这些接口,而在“init”名称空间中没有接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # ip -n physical link
    1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000
    link/ether ab:cd:ef:g1:23:45 brd ff:ff:ff:ff:ff:ff
    3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT group default qlen 1000
    link/ether 01:23:45:67:89:ab brd ff:ff:ff:ff:ff:ff

    # ip link
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  4. 现在,我们将WireGuard接口直接添加到“physical”名称空间:

    # ip -n physical link add wg0 type wireguard

    wg0的出生地命名空间现在是physical命名空间,这意味着密文UDP套接字将分配给eth0和wlan0等设备。我们现在可以将wg0移动到init命名空间中;然而,它仍然会记得套接字的出生地。

  5. 我们将“ 1”指定为“ init”命名空间,因为这是系统上第一个进程的PID。现在,“ init”名称空间具有wg0设备:

    1
    2
    3
    4
    5
    6
    # ip -n physical link set wg0 netns 1
    # ip link
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/none
  6. 现在,我们可以使用普通工具配置物理设备,但是我们在“physical”网络名称空间内启动它们:

    1
    2
    3
    # ip netns exec physical dhcpd wlan0
    # ip netns exec physical wpa_supplicant -iwlan0 -c/etc/wpa_supplicant/wpa_supplicant.conf
    # ip -n physical addr add 192.168.12.52/24 dev eth0
  7. 依此类推。最后,我们可以像平常一样配置wg0接口,并将其设置为默认路由:

    1
    2
    3
    4
    # wg setconf wg0 /etc/wireguard/wg0.conf
    # ip addr add 10.2.4.5/32 dev wg0
    # ip link set wg0 up
    # ip route add default dev wg0
  8. 完成了!在这一点上,系统上的所有普通进程都将通过“init”命名空间路由它们的数据包,该命名空间只包含wg0接口和wg0路由。然而,wg0的UDP套接字位于physical命名空间中,这意味着它将把流量从eth0或wlan0发送出去。正常的进程甚至不会意识到eth0或wlan0,除了dhcpcd和wpa_Request icant,它们是在“physical”命名空间中生成的。

  9. 然而,有时您可能希望打开一个网页或使用physical命名空间快速执行某些操作。例如,也许你计划像往常一样通过WireGuard路由所有的流量,但是你所在的咖啡店要求你在它之前使用网站进行身份验证会给你一个真正的互联网链接。因此,可以使用物理接口执行选择进程(作为您的本地用户):

    1
    $ sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) chromium

    当然,可以将它做成.bashrc的一个不错的函数:

    1
    g) "$@"; }physexec() { sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) "$@"; }
  10. 现在,您可以编写以下代码在“physical”名称空间中打开chromium。

    1
    $ physexec chromium
  11. 当您完成登录咖啡店网络的操作后,像往常一样生成一个浏览器,然后平静地冲浪,知道您的所有流量都受到WireGuard的保护:

    $ chromium

示例脚本

以下示例脚本可以另存为/usr/local/bin/ wgphys,并用于诸如wgphys up,wgphys down和wgphys exec之类的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/bin/bash
set -ex

[[ $UID != 0 ]] && exec sudo -E "$(readlink -f "$0")" "$@"

up() {
killall wpa_supplicant dhcpcd || true
ip netns add physical
ip -n physical link add wgvpn0 type wireguard
ip -n physical link set wgvpn0 netns 1
wg setconf wgvpn0 /etc/wireguard/wgvpn0.conf
ip addr add 192.168.4.33/32 dev wgvpn0
ip link set eth0 down
ip link set wlan0 down
ip link set eth0 netns physical
iw phy phy0 set netns name physical
ip netns exec physical dhcpcd -b eth0
ip netns exec physical dhcpcd -b wlan0
ip netns exec physical wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
ip link set wgvpn0 up
ip route add default dev wgvpn0
}

down() {
killall wpa_supplicant dhcpcd || true
ip -n physical link set eth0 down
ip -n physical link set wlan0 down
ip -n physical link set eth0 netns 1
ip netns exec physical iw phy phy0 set netns 1
ip link del wgvpn0
ip netns del physical
dhcpcd -b eth0
dhcpcd -b wlan0
wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
}

execi() {
exec ip netns exec physical sudo -E -u \#${SUDO_UID:-$(id -u)} -g \#${SUDO_GID:-$(id -g)} -- "$@"
}

command="$1"
shift

case "$command" in
up) up "$@" ;;
down) down "$@" ;;
exec) execi "$@" ;;
*) echo "Usage: $0 up|down|exec" >&2; exit 1 ;;
esac

Wireguard安装配置:

wireguard支持各平台。安装教程可参考官网教程。

https://www.wireguard.com/install/

https://www.wogong.net/blog/2019/01/how-to-configure-wireguard

https://www.w3xue.com/exp/article/201811/9320.html

https://lijiangwei.github.io/2019/06/17/wireguard/

服务端配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@centos7 wireguard]# more wg0.conf  
[Interface]
PrivateKey = xxxx
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; i
ptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; i
ptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
Address = 192.168.2.1/32
ListenPort = 51820
DNS = 8.8.8.8
MTU = 1420


[Peer]
PublicKey = xxx
AllowedIPs = 192.168.2.2/32

如果需要服务端转发流量才需要配置PostUp,PostDown,只配置点对点通信的话不需要。

客户端配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost wireguard]# more wg0.conf 
[Interface]
Address = 192.168.2.2/32
PrivateKey = XXX
ListenPort = 21841

[Peer]
PublicKey = XXX
Endpoint = 140.143.154.78:51820
AllowedIPs = 192.168.2.1/32, 0.0.0.0/0

# This is for if you're behind a NAT and
# want the connection to be kept alive.
PersistentKeepalive = 25

要想服务端转发客户端所有流量,在配置文件添加0.0.0.0/0即可。

各配置项说明:https://fuckcloudnative.io/posts/wireguard-docs-practice/

其他链接推荐:

https://itigic.com/zh-CN/wireguard-vpn-installation-and-configuration/


参考资料:

https://www.wireguard.com/papers/wireguard.pdf

https://www.wireguard.com/


坚持原创技术分享,您的支持将鼓励我继续创作!