L3 路由器支持 NDP 代理

Launchpad Bug: https://bugs.launchpad.net/neutron/+bug/1877301

IPv6 的 NDP(邻居发现协议)在功能上类似于 IPv4 的 ARP(地址解析协议),但 NDP 在 L3 层工作。NDP 代理 [1] 也类似于 ARP 代理 [2]。但是,NDP 代理仅适用于 IPv6,它可以代理特定的 IPv6 地址的 NA(邻居通告),以响应来自上游路由器的 NS(邻居请求)。Linux 内核已经实现了 NDP 代理。有了这些功能,我们可以实现一些 API,以便用户/租户可以通告特定的 IPv6 地址。这有点像 IPv4 的浮动 IP,但此解决方案不执行任何 NAT 操作。

问题描述

随着 IPv6 的日益普及,我们应该提供一种简单的方法,使 IPv6 VM 更容易、更灵活地连接到外部网络。

  • 在某些用例中,例如拥有一些 DB 服务、MQ 服务和门户服务等的网站,它们在具有相同子网的 IPv6 VM 上工作。网站管理员只想将门户服务发布到外部。因此,管理员需要一些方法来仅通告一部分地址到外部。

  • 目前 Neutron 支持的发布 IPv6 地址的解决方案,例如 BGP(边界网关协议) [3]、PD(前缀委派) [4],更加复杂,它们需要在上游路由器上执行一些复杂的配置。这可能不适合某些公司,因为他们的裸机托管在第三方 IDC 中,他们没有上游路由器的权限。因此,我们应该提供一种不需要上游路由器深度合作的解决方案。

提议的变更

概述

服务器端

  • 实现一个名为 ndp_proxy 的服务插件,以支持此规范描述中的功能。管理员可以通过将插件附加到 Neutron 配置文件中的 service_plugin 来启用该功能。

  • 实现一个 API 扩展,在扩展中,我们应该定义一个名为 ndp_proxy 的资源及其 API。

  • 在路由器中添加一个新的参数 enable_ndp_proxy。如果将其设置为 True,则 NDP 代理将在路由器命名空间中启用。

代理端

实现 Neutron L3 代理的扩展,以设置 NDP 代理的关系规则。扩展行为如下

  • 如果路由器的 enable_ndp_proxy 字段为 true,则路由器的命名空间应该具有默认的 ip6tables 规则,以丢弃所有从路由器的外部网关设备输入的包。通过这种方式,我们可以消除上游路由器中额外路由和邻居的影响。因此,如果路由器的 enable_ndp_proxy 设置为 True,则前缀委派 [4] 和 BGP 动态路由 [3] 解决方案将失效,我们应该在用户文档中解释这一点。

    注意

    如果路由器的 enable_ndp_proxy 设置为 false,而路由器具有 ndp 代理,则这些 ndp 代理将失效,前缀委派 [4] 和 BGP 动态路由 [3] 解决方案将恢复。特别是,DVR 路由器将忽略 enable_ndp_proxy

  • 对于每个 ndp 代理对象,扩展应在路由器的外部网关设备上设置一个邻居代理条目,并设置一个 ip6tables 规则以允许关系数据包通过。通过这种方式,路由器仅允许启用了 ndp_proxy 的 IPv6 地址与外部网络通信。这使得 ndp 代理的行为更类似于 IPv4 的浮动 IP。

数据模型影响

作为 NDP 代理功能的一部分,将添加以下新表。

对于 ndp 代理资源

CREATE TABLE ndp_proxies (
    id CHAR(36) NOT NULL PRI KEY,
    name VARCHAR(255),
    router_id VARCHAR(36) NOT NULL FOREIGN KEY,
    port_id VARCHAR(36) NOT NULL FOREIGN KEY,
    ip_address VARCHAR(64) NOT NULL,
    project_id VARCHAR(255),
    standard_attr_id bigint(20) NOT NULL,
);

router_id 列将存储 ndp 代理所属的路由器的 ID。 port_id 列将存储 ip_address 所在的 Neutron 内部端口的 ID。 ip_address 列将存储我们需要代理的 IPv6 地址。

对于路由器的 enable_ndp_proxy 参数

CREATE TABLE router_ndp_proxy_state (
    router_id VARCHAR(36) NOT NULL FOREIGN KEY,
    enable_ndp_proxy BOOL NOT NULL,
);

新的资源扩展

将添加新的资源扩展 ndp_proxy。它将定义 ndp_proxy 条目的 CURD API。

对于此新功能,将引入一个新的服务插件,并将添加以下方法

  • ‘create_ndp_proxy()’

  • ‘delete_ndp_proxy()’

  • ‘update_ndp_proxy()’

  • ‘get_ndp_proxy()’

  • ‘get_ndp_proxies()’

因此,新资源的属性映射如下

RESOURCE_ATTRIBUTE_MAP = {
    'ndp_proxy': {
        'id': {'allow_post': False,
               'allow_put': False,
               'validate': {'type:uuid': None},
               'is_visible': True,
               'primary_key': True},
        'name': {'allow_post': True,
                 'allow_put': True,
                 'validate': {'type:string': 255},
                 'is_filter': True,
                 'is_sort_key': True,
                 'is_visible': True, 'default': ''},
        'project_id': {'allow_post': True,
                       'allow_put': False,
                       'required_by_policy': True,
                       'validate': {'type:uuid': None},
                       'is_visible': True},
        'router_id': {'allow_post': True,
                      'allow_put': False,
                      'validate': {'type:uuid': None},
                      'is_visible': True},
        'port_id': {'allow_post': True,
                    'allow_put': False,
                    'validate': {'type:uuid': None},
                    'is_visible': True},
        'ip_address': {'allow_post': True,
                       'allow_put': False,
                       'default': None,
                       'validate': {
                           'type:ip_address_or_none': None},
                       'is_visible': True}
        'description': {'allow_post': True,
                        'allow_put': True,
                        'default': '',
                        'validate': {'type:string': 1024},
                        'is_visible': True}
    }
}

注意

ip_address 参数是可选的,如果用户发送 post 请求时未设置,则新的服务插件将从 port_id 表示的端口中选择一个 IPv6 地址。

REST API 影响

扩展路由器 API

想法是使用以下定义的属性扩展 router Rest API,添加一个新的扩展 router_ndp_proxy

路由器扩展

属性名称

类型

CRUD

默认值

描述

enable_ndp_proxy

布尔值

CRU

False

路由器是否启用 ndp 代理功能。

router 扩展定义将扩展为

RESOURCE_ATTRIBUTE_MAP = {
     'routers': {
         'enable_ndp_proxy': {
             'allow_post': True, 'allow_put': True,
             'convert_to': converters.convert_to_boolean_if_not_none,
             'is_visible': True,
             'is_filter': True,
         }
     }
}

默认情况下,只有管理员用户可以更新 enable_ndp_proxy 参数。并且,将引入一个新的配置选项 enable_ndp_proxy_by_default。如果将其设置为 True,则 enable_ndp_proxy 将默认设置为 True

例如,GET 一个 router

GET /v2.0/routers/<router-uuid>

{
    "router": {
        "admin_state_up": true,
        "availability_zone_hints": [],
        "availability_zones": [
            "nova"
        ],
        "created_at": "2018-03-19T19:17:04Z",
        "description": "",
        "distributed": false,
        "enable_ndp_proxy": false,
        "external_gateway_info": {
            "enable_snat": true,
            "external_fixed_ips": [
                {
                    "ip_address": "172.24.4.6",
                    "subnet_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf"
                },
                {
                    "ip_address": "2001:db8::9",
                    "subnet_id": "0c56df5d-ace5-46c8-8f4c-45fa4e334d18"
                }
            ],
            "network_id": "ae34051f-aa6c-4c75-abf5-50dc9ac99ef3"
        },
        "flavor_id": "f7b14d9a-b0dc-4fbe-bb14-a0f4970a69e0",
        "ha": false,
        "id": "f8a44de0-fc8e-45df-93c7-f79bf3b01c95",
        "name": "router1",
        "revision_number": 1,
        "routes": [
            {
                "destination": "179.24.1.0/24",
                "nexthop": "172.24.3.99"
            }
        ],
        "status": "ACTIVE",
        "updated_at": "2018-03-19T19:17:22Z",
        "project_id": "0bd18306d801447bb457a46252d82d13",
        "tenant_id": "0bd18306d801447bb457a46252d82d13",
        "service_type_id": null,
        "tags": ["tag1,tag2"],
        "conntrack_helpers": []
    }
}

将路由器的 enable_ndp_proxy 参数设置为 True

Post /v2.0/routers/<router-uuid>

请求主体

{
    "router": {
        "enable_ndp_proxy": true
    }
}

对于新的资源 ndp_proxy,将引入一些新的 URL

列出 NDP 代理

GET /v2.0/ndp_proxies

响应体

{
    "ndp_proxies": [
         {
             "project_id": "ad239fa5-ceb7-8b81-abf5-b930d7f6ceb7",
             "id": "ae34051f-aa6c-4c75-abf5-50dc9ac99ef3",
             "name": "test01",
             "port_id": "fa450s1f-aa6c-4c75-abf5-50dc9ac9df67",
             "ip_address": "2001:217::19",
             "created_at":"2020-05-21T11:33:21Z",
             "updated_at":"2020-06-18T08:31:48Z",
             "description": ""
         },
         {
             "project_id": "ad239fa5-ceb7-8b81-abf5-b930d7f6ceb7",
             "id": "915a14a6-867b-4af7-83d1-70efceb146f9",
             "name": "test02",
             "port_id": "4323401f-aa6c-4c75-abf5-50dc9ac99ef3",
             "ip_address": "2001:218::12"
             "created_at":"2020-05-21T11:33:21Z",
             "updated_at":"2020-06-18T08:31:48Z",
             "description": ""
         }
    ]
}

显示 NDP 代理

GET /v2.0/ndp_proxies/<ndp-proxy-id>

响应体

{
    "ndp_proxy": {
         "project_id": "ad239fa5-ceb7-8b81-abf5-b930d7f6ceb7",
         "id": "ae34051f-aa6c-4c75-abf5-50dc9ac99ef3",
         "name": "test01",
         "port_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf"
         "ip_address": "2001:217::19",
         "created_at":"2020-05-21T11:33:21Z",
         "updated_at":"2020-06-18T08:31:48Z",
         "description": "Some descriptions"
     }
}

创建 NDP 代理

POST /v2.0/ndp_proxies

请求体

{
    "ndp_proxy": {
         "name": "test01",
         "router_id": "5823deb7-8b81-ceb7-40a0-b930d7f6ceb7",
         "port_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf",
         "ip_address": "2001:217::19",
         "description": "Some descriptions"
     }
}

这里有一些约束

  • 路由器的 enable_ndp_proxy 参数必须设置为 True。

  • ip_address 分配到的子网必须添加到路由器。

  • port_id 表示的端口的网络必须与路由器外部网关的网络具有相同的 ipv6_address_scope [5]

响应体

{
    "ndp_proxy": {
         "id": "ad239fv5-aa6c-4c75-abf5-50dc9ac99ef3",
         "name": "test01",
         "project_id": "ad239fa5-ceb7-8b81-abf5-b930d7f6ceb7",
         "router_id": "5823deb7-8b81-ceb7-40a0-b930d7f6ceb7",
         "port_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf",
         "ip_address": "2001:217::19",
         "created_at":"2020-05-21T11:33:21Z",
         "updated_at":"2020-06-18T08:31:48Z",
         "description": "Some descriptions"
     }
}

更新 NDP 代理

PUT /v2.0/ndp_proxies/<ndp-proxy-id>

请求体

{
    "ndp_proxy": {
         "name": "test02",
         "description": "New descriptions"
     }
}

响应体

{
    "ndp_proxy": {
         "id": "ad239fv5-aa6c-4c75-abf5-50dc9ac99ef3",
         "name": "test02",
         "project_id": "ad239fa5-ceb7-8b81-abf5-b930d7f6ceb7",
         "router_id": "5823deb7-8b81-ceb7-40a0-b930d7f6ceb7",
         "port_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf"
         "ip_address": "2001:217::56",
         "created_at":"2020-05-21T11:33:21Z",
         "updated_at":"2020-06-18T08:31:48Z",
         "description": "New descriptions"
     }
}

删除 NDP 代理

DELETE /v2.0/ndp_proxies/<ndp-proxy-id>

此操作不接受请求体,也不返回响应体。

此外,如果路由器或端口将被删除,与它们相关的 ndp 代理将被级联删除。

对现有路由器 API 的影响

  • 在从路由器中删除子网之前,Neutron 应该检查是否有任何与该子网相关的 ndp 代理(无论 ndp 代理的 ip_address 是否从该子网分配)。如果有任何与该子网相关的 ndp 代理,则无法删除该子网。

L3 代理影响

Neutron 需要实现一个新的 l3 代理扩展,与服务器 API 配合设置路由器命名空间中的关系规则(neigh 代理和 ip6tables,分布式路由器需要设置一些额外的路由规则)。

我们假设用户有一个如下场景,然后分别描述有关 DVR 路由器和 Legacy 路由器的功能实现

  • 一个 CIDR 为 2001::1:0/112 的子网

  • 一个属于该子网的 VM,其 IPv6 地址为 2001::1:8

  • 一个配置了外部网关并连接到该子网的分布式/Legacy 路由器。

Legacy 路由器影响

假设路由器的外部网关设备是 qg-733bd76b-62

  • 如果路由器的参数 enable_ndp_proxy 为 true,则扩展需要在路由器的 qrouter-namespace 中将内核参数 proxy_ndp 设置为 1,创建一个名为 neutron-l3-agent-NDP 的自定义链,并在其中设置一个默认的 iptables 规则,以丢弃所有从路由器的外部网关设备输入的包。执行的命令如下

    sysctl -w net.ipv6.conf.qg-733bd76b-62.proxy_ndp=1
    ip6tables -N neutron-l3-agent-NDP
    ip6tables -A neutron-l3-agent-NDP -i qg-733bd76b-62 -j DROP
    

注意

如果路由器没有外部网关,则将忽略参数 enable_ndp_proxy

  • 当将 IPv6 子网添加到路由器时(路由器的 enable_ndp_proxy 已经设置为 true),在用户使用 ndp 代理通告子网的地址之前,子网应该丢弃所有外部流量。因此,应该执行以下命令

    ip6tables -I neutron-l3-agent-FORWARD -i qg-733bd76b-62 --destination 2001::1:0/112 -j neutron-l3-agent-NDP
    

    通过这种方式,我们可以消除上游路由器中额外路由和邻居条目的影响。

  • 对于每个 ndp 代理,扩展应将一个邻居代理条目添加到路由器的外部网关设备,并添加 ip6tables 规则以允许关系数据包通过。执行的命令如下

    ip -6 neigh add proxy 2001::1:8 dev qg-733bd76b-62
    ip6tables -I neutron-l3-agent-NDP -i qg-733bd76b-62 --destination 2001::1:8 -j ACCEPT
    
  • 删除 ndp 代理时,扩展应从路由器的外部网关设备中删除相关的邻居代理条目,并删除相关的 ip6tables 规则以重新禁止关系数据包通过。执行的命令如下

    ip -6 neigh del proxy 2001::1:8 dev qg-733bd76b-62
    ip6tables -D neutron-l3-agent-NDP -i qg-733bd76b-62 --destination 2001::1:8
    
  • 当路由器的参数 enable_ndp_proxy 设置为 false 时,扩展需要在路由器的 qrouter-namespace 命名空间中将内核参数 proxy_ndp 设置为 0,删除名为 neutron-l3-agent-NDP 的自定义链。执行的命令如下

    sysctl -w net.ipv6.conf.qg-733bd76b-62.proxy_ndp=0
    ip6tables -X neutron-l3-agent-NDP
    

HA 路由器影响

HA 路由器的功能实现与 Legacy 路由器相同,除了故障转移。当 HA 路由器的状态发生变化时,扩展应刷新 ndp 代理规则,因为 ndp 代理规则在多次故障转移后可能会丢失。

注意

当 HA 路由器的状态发生变化时,keepalived 将自动发送未经请求的邻居通告。因此,我们不需要编写额外的代码来执行此操作。在故障转移期间,流量将被中断,但如果故障转移完成,它将很快恢复。

DVR 路由器影响

对于 DVR [6] 路由器,它与 legacy 和 HA 路由器略有不同。我们需要考虑两种场景:如果计算节点中的 neutron-l3-agent 设置为 dvr 模式,则 neutron-l3-agent 将创建一个 fip-namespace 来处理南北流量,因此我们需要在 qrouter-namespace 和 fip-namespace 中应用相关的规则。如果计算节点中的 neutron-l3-agent 设置为 dvr_no_external 模式,则南北流量将由网络节点中的 snat-namespace 处理,因此我们需要在 qrouter-namespace 和 snat-namespace 中应用相关的规则。

对于 dvr 模式

假设 fip-namespace 的 fg-dev 端口是 fg-84920cf6-5e;连接 fip-namespace 到 qrouter-namespace 的端口是 fpr-ea902fe0-9,其 IPv6 地址是 fe80::8493:5bff:fe9b:8d93;连接 qrouter-namespace 到 fip-namespace 的端口是 rfp-ea902fe0-9,其 IPv6 地址是 fe80::a0a7:c5ff:fe2c:bade。拓扑结构如下

      +---------------+
      |               |
      |upstream router|
      |               |
      +-------+-------+
              |
+----------------------------+
|             | compute node |
|             |              |
|     +-------+--------+     |
|     | fg-84920cf6-5e |     |
|     |                |     |
|     |  fip-namespace |     |
|     |                |     |
|     | fpr-ea902fe0-9 |     |
|     +--------+-------+     |
|              |             |
|              |             |
|     +--------+--------+    |
|     |  rfp-ea902fe0-9 |    |
|     |                 |    |
|     |qrouter-namespace|    |
|     |                 |    |
|     +-----------------+    |
|                            |
+----------------------------+

注意

我们不需要为 dvr 路由器设置 ip6tables 规则。因为只需在 fip-namespace 中添加或删除相关的路由(如下所述),就可以启用/禁用 IPv6 流量。

  • 由于当前的 Neutron 不支持带有 IPv6 的 dvr,qrouter-namespace 没有关于 IPv6 的默认路由。如果路由器设置了外部网关,我们应该首先添加默认路由,默认路由的下一跳应该是 fpr-dev 设备的 IPv6 地址。执行的命令如下(在 qrouter-namespace 中执行)

    ip route add default via fe80::8493:5bff:fe9b:8d93 dev rfp-ea902fe0-9
    
  • 由于直接连接到上游路由器的设备位于 fip-namespace 中,因此代理条目应该在 fip-namespace 中设置。因此,以下命令应该在所有 fip-namespace 中执行

    sysctl -w net.ipv6.conf.fg-84920cf6-5e.proxy_ndp=1
    
  • 对于每个 ndp 代理,扩展会在 fip-namespace 中的 fg-dev 中添加一个代理条目,代理条目仅添加到托管 ndp 代理对象端口所属节点的命名空间中,并在命名空间中添加一个路由,以便可以将目标地址为 ndp 代理的 ip_address 的数据包转发到 qrouter-namespace。这些命令如下(在 fip-namespace 中执行)

    ip -6 neigh add proxy 2001::1:8 dev fg-84920cf6-5e
    ip route add 2001::1:8 via fe80::a0a7:c5ff:fe2c:bade dev fpr-ea902fe0-9
    
  • 删除 ndp 代理时,扩展应从 fg-dev 中删除相关的邻居代理条目,并删除相关的路由。这些命令如下(在 fip-namespace 中执行)

    ip -6 neigh del proxy 2001::1:8 dev fg-84920cf6-5e
    ip route del 2001::1:8 via fe80::a0a7:c5ff:fe2c:bade dev fpr-ea902fe0-9
    
  • 由于不需要设置 ip6tables 规则,因此对于 enable_ndp_proxy 的更改,代理扩展无需执行任何操作。

  • 如果实例迁移并且其端口与 ndp 代理条目相关联。扩展应删除旧主机中的相关规则并在新主机中创建它们。此外,扩展应发送一个 NA(邻居通告)来刷新上游路由器的邻居条目,以便外部流量可以立即转发到新主机 fip-namespace。

对于 dvr_no_external 模式

假设 snat-namespace 的 qg-dev 是 qg-87059c6c-a9,sg-dev 是 sg-68bcdb7b-a2;qrouter-namespace 的 qr-dev 是 qr-50457f9b-98。拓扑结构如下

    +---------------+
    |               |
    |upstream router|
    |               |
    +-------+-------+
            |
+-----------------------+
|network    |           |
| node      |           |
|   +-------+------+    |
|   |qg-87059c6c-a9|    |
|   |              |    |
|   |snat-namespace|    |
|   |              |    |
|   |sg-68bcdb7b-a2|    |
|   +-------+------+    |
|           |           |
+-----------------------+
            |
+-----------------------+
|compute    |           |
| node      |           |
|  +--------+--------+  |
|  | qr-50457f9b-98  |  |
|  |                 |  |
|  |qrouter-namespace|  |
|  |                 |  |
|  +-----------------+  |
|                       |
+-----------------------+

对于此模式,在 qrouter-namespace 中,我们只需要添加一个默认路由,以便可以将南北流量重定向到 snat-namespace(当前的 neutron 代码已经完成了此需求)。对于 snat-namespace,我们将其视为传统路由器的 qrouter-namepsace,关于其规则处理,我们可以参考 传统路由器影响

OVN 后端影响

此提案未涵盖 OVN L3 后端的 ndp_proxy。如果证明其可行,我们应该在未来基于 OVN 后端实现此功能。但目前,我们将仅基于 Neutron L3 代理实现此功能。

实现

负责人

yangjianfeng

工作项

  1. API 实现

  2. DB 实现

  3. 参考实现

  4. 测试

  5. 文档

测试

Tempest 测试

规范中提出的功能需要外部硬件路由器的支持(需要在上游路由器上添加一个直接路由条目)。这对于 tempest 来说很难做到。因此,我们可以首先跳过场景测试。

功能测试

需要添加功能测试

API 测试

需要添加 API 测试

Fullstack 测试

需要添加全栈测试。

文档影响

用户文档

需要用户文档

开发人员文档

需要 devref 文档

API 参考

需要 API 参考文档

参考资料