河汉清且浅,牵牛敦而纯

KVM VPS 的 IPv6 邻居发现响应器

我想要 Docker 的 IPv6

这些天我正在使用 Docker,我希望在我的 Docker 容器中使用 IPv6。在 Docker 中启用 IPv6 的最佳指南是 如何在 Ubuntu 18.04 上为 Docker 容器启用 IPv6。该文章中的第一种方法将私有 IPv6 地址分配给容器,并使用 IPv6 NAT,类似于 Docker 处理 IPv4 NAT 的方式。我很快就让它工作了,但我注意到一个不良行为:网络地址转换 (NAT) 会更改传出 UDP 数据报的源端口号,即使存在入站流量的端口转发规则;因此,具有相同源端口和目标端口的 UDP 流被识别为两个单独的流。

<code class="hljs shell">
 
$ docker exec nfd nfdc face show 262
    faceid=262
    remote=udp6://[2001:db8:f440:2:eb26:f0a9:4dc3:1]:6363
     local=udp6://[fd00:2001:db8:4d55:0:242:ac11:4]:6363
congestion={base-marking-interval=100ms default-threshold=65536B}
       mtu=1337
  counters={in={25i 4603d 2n 1179907B} out={11921i 14d 0n 1506905B}}
     flags={non-local permanent point-to-point congestion-marking}
$ docker exec nfd nfdc face show 270
    faceid=270
    remote=udp6://[2001:db8:f440:2:eb26:f0a9:4dc3:1]:1024
     local=udp6://[fd00:2001:db8:4d55:0:242:ac11:4]:6363
   expires=0s
congestion={base-marking-interval=100ms default-threshold=65536B}
       mtu=1337
  counters={in={11880i 0d 0n 1498032B} out={0i 4594d 0n 1175786B}}
     flags={non-local on-demand point-to-point congestion-marking}</code>

该文章中的第二种方法允许每个容器都有一个公共 IPv6 地址。它避免了 NAT 及其带来的问题,但要求主机具有 路由的IPv6 子网。然而, 路由IPv6在KVM服务器上很难实现,因为 Virtualizor等虚拟化平台不支持路由IPv6子网,而只能提供on-link IPv6。

链路 IPv6 与路由 IPv6

那么,链路 IPv6 和路由 IPv6 之间有什么区别呢?其不同之处在于如何配置前一跳的路由器以到达目标 IP 地址。

我先用IPv4术语解释一下:

<code class="hljs text">
|--------| 192.0.2.1/24       |--------| 198.51.100.1/24    |-----------|
| router |--------------------| server |--------------------| container |
|--------|       192.0.2.2/24 |--------|    198.51.100.2/24 |-----------|
            (192.0.2.16-23/24)    |
                                  | 192.0.2.17/28           |-----------|
                                  \-------------------------| container |
                                              192.0.2.18/28 |-----------|</code>
  • 服务器的在线 IP 地址为 192.0.2.2。

    • 路由器知道该 IP 地址处于链路状态,因为它位于路由器接口上配置的 192.0.2.0/24 子网中。
    • 为了将数据包发送到 192.0.2.2,路由器会发送 192.0.2.2 的 ARP 查询来了解服务器的 MAC 地址,服务器应响应该查询。
  • 服务器已路由 IP 子网 198.51.100.0/24。

    • 路由器必须配置为知道:198.51.100.0/24 可通过 192.0.2.2 访问。
    • 为了将数据包传递到 198.51.100.2,路由器首先查询其路由表并找到上述条目,然后发送 ARP 查询以获知服务器应响应的 MAC 地址 192.0.2.2,最后将数据包传递到学习到的MAC地址。
  • 主要区别在于 ARP 查询中包含的 IP 地址:

    • 如果目的IP地址是链路上的IP地址,则ARP查询包含目的IP地址本身。
    • 如果目标 IP 地址位于路由子网中,则 ARP 查询包含由路由表确定的下一跳 IP 地址。
  • 如果我想为容器分配一个链路上的 IPv4 地址(例如 192.0.2.18/28),应该让服务器回答该 IP 地址的 ARP 查询,以便路由器将数据包传递到服务器,然后转发这些数据包到容器。

    • 这种技术称为 ARP 代理,其中服务器代表容器响应 ARP 查询。

IPv6 中的情况稍微复杂一些,因为每个网络接口可以有多个 IPv6 地址,但概念相同。 IPv6 使用 属于 ICMPv6 一部分的邻居发现协议,而不是地址解析协议 (ARP)。一些术语有所不同:

IPv4IPv6
ARP 邻居发现协议 (NDP)
ARP查询 ICMPv6 邻居请求
ARP回复 ICMPv6 邻居通告
ARP代理 新民主党代理

如果我想为容器分配一个链路上的 IPv6 地址,服务器应该响应该 IP 地址的邻居请求,以便路由器将数据包传送到服务器。之后,服务器的 Linux 内核可以将数据包路由到容器的网桥,就好像目标 IPv6 地址位于路由子网中一样。

我希望 NDP 代理守护程序能够提供救援?

ndppd或 NDP 代理守护程序是一个程序,用于侦听网络接口上的邻居请求并以邻居通告进行响应。通常建议用于处理服务器只有链接 IPv6 但我们需要路由 IPv6 子网的情况。

我在我的一台服务器上安装了 ndppd,并且它按照以下配置按预期工作:

<code class="hljs nginx">
proxy uplink {
  rule 2001:db8:fbc0:2:646f:636b:6572::/112 {
    auto
  }
}</code>

我可以使用公共 IPv6 地址启动 Docker 容器。它可以访问 IPv6 Internet,并且可以从外部 ping 通。

<code class="hljs shell">
 
$ docker network create --ipv6 --subnet=172.26.0.0/16 \
  --subnet=2001:db8:fbc0:2:646f:636b:6572::/112 ipv6exposed
118c3a9e00595262e41b8cb839a55d1bc7bc54979a1ff76b5993273d82eea1f4
 
$ docker run -it --rm --network ipv6exposed \
  --ip6 2001:db8:fbc0:2:646f:636b:6572:d002 alpine
 
# wget -q -O- https://www.cloudflare.com/cdn-cgi/trace | grep ip
ip=2001:db8:fbc0:2:646f:636b:6572:d002</code>

然而,当我在另一台 KVM 服务器上重复相同的设置时,情况并不顺利:容器根本无法访问 IPv6 Internet。

<code class="hljs shell">
 
$ docker run -it --rm --network ipv6exposed \
  --ip6 2001:db8:f440:2:646f:636b:6572:d003 alpine
/ # ping -c 4 ipv6.google.com
PING ipv6.google.com (2607:f8b0:400a:809::200e): 56 data bytes
--- ipv6.google.com ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss</code>

ndppd有什么问题吗 

为什么 ndppd在第一台服务器上工作,但在第二台服务器上不起作用?有什么不同?我们需要更深入,所以我转向 tcpdump

在第一台服务器上,我看到:

<code class="hljs text">
$ sudo tcpdump -pi uplink icmp6
19:13:17.958191 IP6 2001:db8:fbc0::1 > ff02::1:ff72:d002:
    ICMP6, neighbor solicitation, who has 2001:db8:fbc0:2:646f:636b:6572:d002, length 32
19:13:17.958472 IP6 2001:db8:fbc0:2::2 > 2001:db8:fbc0::1:
    ICMP6, neighbor advertisement, tgt is 2001:db8:fbc0:2:646f:636b:6572:d002, length 32</code>
  • 来自路由器的邻居请求来自 全局IPv6 地址。
  • 服务器使用来自其 全局IPv6 地址的邻居通告进行响应。请注意,该地址与容器的地址不同。
  • IPv6 在容器中工作。

在第二台服务器上,我看到:

<code class="hljs text">
$ sudo tcpdump -pi uplink icmp6
00:07:53.617438 IP6 fe80::669d:99ff:feb1:55b8 > ff02::1:ff72:d003:
    ICMP6, neighbor solicitation, who has 2001:db8:f440:2:646f:636b:6572:d003, length 32
00:07:53.617714 IP6 fe80::216:3eff:fedd:7c83 > fe80::669d:99ff:feb1:55b8:
    ICMP6, neighbor advertisement, tgt is 2001:db8:f440:2:646f:636b:6572:d003, length 32</code>
  • 来自路由器的邻居请求来自 链路本地IPv6 地址。
  • 服务器使用来自其 链路本地IPv6 地址的邻居通告进行响应。
  • IPv6 在容器中不起作用。

由于 IPv6 一直在第二台服务器上工作,以获取分配给该服务器本身的 IPv6 地址,因此我添加了一个新的 IPv6 地址并捕获了其 NDP 交换:

<code class="hljs text">
$ sudo tcpdump -pi uplink icmp6
00:29:39.378544 IP6 fe80::669d:99ff:feb1:55b8 > ff02::1:ff00:a006:
    ICMP6, neighbor solicitation, who has 2001:db8:f440:2::a006, length 32
00:29:39.378581 IP6 2001:db8:f440:2::a006 > fe80::669d:99ff:feb1:55b8:
    ICMP6, neighbor advertisement, tgt is 2001:db8:f440:2::a006, length 32</code>
  • 来自路由器的邻居请求来自 链路本地IPv6 地址,与上面相同。
  • 服务器使用来自目标 全局IPv6 地址的邻居通告进行响应。
  • IPv6 通过该地址在服务器上运行。

在 IPv6 中,每个网络接口可以有多个 IPv6 地址。当 Linux 内核响应邻居请求(其中目标地址被分配给同一网络接口)时,它 会使用该特定地址作为源地址。另一方面, ndppd通过PF_INET6 套接字传输邻居通告 ,并且 不指定源地址。在这种情况下,一些复杂的 默认地址选择规则就开始发挥作用。

这些规则之一是优先选择 与目标地址(即路由器)具有相同范围的源地址。在我的第一台服务器上,路由器使用 全局地址,并且服务器选择 全局地址作为其邻居通告的源地址。在我的第二台服务器上,路由器使用 链路本地地址,服务器也选择 链路本地地址。

在未经过滤的网络中,路由器不会关心邻居通告的来源。然而,当涉及 Virtualizor 上的 KVM 服务器时,虚拟机管理程序会将此类数据包视为尝试的 IP 欺骗攻击,并通过 ebtables 规则丢弃它们。因此,邻居通告永远不会到达路由器,并且路由器无法知道如何到达容器的 IPv6 地址。

ndpresponder:KVM VPS 的 NDP 响应程序

我尝试了一些技巧,例如 弃用链接本地地址,但没有一个起作用。因此,我制作了自己的 NDP 响应程序,从目标地址发送邻居通告。

ndpresponder是一个使用GoPacket库的Go 程序 

  1. 该程序打开一个 AF_PACKET 套接字,其中包含用于 ICMPv6 邻居请求消息的 BPF 过滤器。
  2. 当邻居请求到达时,它会根据用户提供的 IP 范围检查目标地址。
  3. 如果目标地址在 Docker 容器使用的范围内,则程序会构造 ICMPv6 邻居通告消息并通过相同的 AF_PACKET 套接字传输它。

与ndppd的主要区别 在于,邻居通告消息上的源 IPv6 地址始终设置为与邻居请求的目标地址相同的值,以便管理程序不会丢弃该消息。这是可能的,因为我通过 AF_PACKET 套接字发送消息,而不是 ndppd使用的 AF_INET6 套接字。

ndpresponder 的操作方式与“静态”模式下的ndppd类似 。它不会像 ndppd在“自动”模式下那样将邻居通告转发到目标子网,但此功能在 KVM 服务器上并不重要。

如果 ndppd似乎无法在您的 KVM VPS 上运行,请 尝试ndpresponder !前往我的 GitHub 存储库获取安装和使用说明: https: //github.com/yoursunny/ndpresponder

加入讨论: LowEndSpirit LowEndTalk

标签: Docker Go IPv6 Linux 托管


https://yoursunny.com/t/2021/ndpresponder/
posted on 2024-01-31 15:57  伊索  阅读(38)  评论(0编辑  收藏  举报