如何高效地将IP地址与大量路由条目匹配?

5

假设有一堵防火墙,系统管理员已经封锁了许多子网,甚至可能封锁了某个特定国家的所有子网。

例如:

192.168.2.0 / 255.255.255.0
223.201.0.0 / 255.255.0.0
223.202.0.0 / 255.254.0.0
223.208.0.0 / 255.252.0.0
....

为了判断一个IP地址是否被阻止,防火墙可能会使用以下算法。

func blocked(ip)
    foreach subnet in blocked_subnets
        if in_subnet(subnet, ip)
            return true
    return false

但是,该算法需要运行太长的时间,时间复杂度为O(n)。如果路由表包含太多条目,则网络几乎将不可用。

有没有更有效的方法将IP地址与大量路由条目匹配?我猜想它基于某种树/图(Trie?)。我已经阅读了一些关于最长前缀匹配和Trie的内容,但并没有理解其中的要点。

5个回答

14

你所需要的只是一个四层的 trie。每个非叶子节点包含高达256个子节点的数组。每个节点还包含一个子网掩码。所以,以你的例子为例:

192.168.2.0 / 255.255.255.0
223.201.0.0 / 255.255.0.0
223.202.0.0 / 255.254.0.0
223.208.0.0 / 255.252.0.0

您的树将类似于下面的样子。每个节点的两个数字分别是IP段和子网掩码。

             root
         /           \
     192,255             223,255
       |           -------------------------
     168,255       |           |           |
       |          201,255    202,255    208,255
      2,255

获取 IP 地址后,需要将其分段。首先在根级别上搜索第一个段。为了提高速度,你可能会想要在根级别使用数组进行直接查找。

假设 IP 地址的第一个段是 223。你可以从 root[223] 中获取节点,然后你就只处理一个子树了。除非数据非常密集,否则在其他级别上你可能不想要完整的数组,而是需要一些字典之类的东西来处理后续层级。如果下一个段是 201,则在 223 节点的字典中查找 201,这样你的候选列表中可能只有 64K 个项目(即所有以 223、201 开头的 IP 地址)。你可以在其他两个级别上执行相同的操作。结果是你只需进行四次查找就能解析一个 IP 地址:一次在数组中的查找和三次在字典中的查找。

这种结构也很容易维护。插入新地址或范围最多需要进行四次查找和添加。删除也是如此。更新可以就地完成,无需重建整个树。只需确保在更新时不要尝试读取,并且不要尝试进行并发更新。但任意数量的读者可以同时访问该项。


这个答案似乎不正确。对于第二级别,掩码并非总是255。例如:223.208.0.0 / 255.252.0.0 - shawn

6
使用哈希映射或trie数据结构处理CIDR IP地址范围(例如,掩码不一定是基于8的,如192.168.1.0/28)会让你感到很困难。
一种高效的方法是利用二分查找,前提是这些IP地址范围不重叠:
1. 将范围A.B.C.D/X转换为32位整数表示起始IP地址,以及一个表示该范围中有多少个IP地址的整数。例如,192.168.1.0/24转换为3232235776, 256。
2. 将这些范围添加到列表或数组中,并按照起始IP地址号码排序。
3. 要将传入的IP地址与列表中的任何范围匹配,只需进行二分查找即可。

太棒了!但是即使范围重叠,这也能正常工作吗?例如,假设我们有三个相同的/8、/16、/24范围,表示为(64M,16777216)、(64M,65536)、(64M,256)。在二分搜索数组中,只需按第二个成员对相同的元组进行排序,从而在从左到右遍历重叠范围时获得最长前缀匹配效果。在二进制数组中定位重叠范围的“起点”可能需要一些额外的时间,但查找应该仍然很快。 - Liviu Chircu
请忽略上面的内容:您是正确的,该算法不适用于重叠范围。例如:10.0.0.0/8和10.1.0.0/16:两者都匹配10.1.200.200,但我们无法准确地表示它们在二进制搜索数组中,以使/16始终优先于/8。 - Liviu Chircu

1
使用红黑树或AVL树来存储不同子网的已阻止IP。由于您正在处理基本上是4个数字集合的IP,因此可以在所需的编程语言中使用自定义比较器,并将其存储在红黑树或AVL树中。
比较器:使用4/6个IP部分来比较两个IP,无论它们是大还是小,都使用第一个不匹配的部分。例如:10.0.1.1和10.0.0.1。这里ip1>ip2,因为其中一个的第3个不匹配条目更大。
时间复杂度:由于红黑树是平衡BST,因此插入、删除和搜索需要O(logn)。对于k个子网的每个子网,总共需要O(log(n)*k)来搜索IP。
优化:如果子网数量很大,则使用具有类似比较的不同键,但只使用一个红黑树。
Key = (subnet_no,ip)

You can compare them similar to above and would get O(log(S)) where S is total number of ip entries in all subnets.


1
这可能是一个简单的问题,但由于没有人提到内存限制,因此您可以使用查找表。即使在实践中拥有2^32个项目的LUT也不是不可能的,然后问题就被减少为单个表查找,而不管规则如何。(路由也可以使用相同的方法。)如果您想要快速执行,它需要2^32个八位字节(4 GiB),如果您有更多时间,位表需要2^32个位,即512 MiB。即使在这种情况下,它也可以变得很快,但是使用高级编程语言可能会产生次优结果。
当然,“快速”问题总是有点棘手。您想在实践中还是在理论上快速?如果在实践中,在哪个平台上?即使是LUT方法,如果系统将表交换到HDD中,并且根据缓存结构,更复杂的方法可能比基于RAM的LUT更快,因为它们适合处理器高速缓存。缓存未命中可能需要数百个CPU周期,在这些周期内可以进行相当复杂的操作。
LUT方法存在的问题(除了记忆使用)是规则删除的成本。由于表格结果来自所有规则的按位或,因此没有简单的方法可以删除规则。因此,在这种情况下,必须确定哪些区域与要删除的规则没有重叠,然后将这些区域清零。最好使用其他答案中概述的结构逐位进行操作。

-2

请注意,IP地址基本上是一个32位数字。

您可以将每个子网规范化为其正常形式,并将所有正常形式存储在哈希表中。

在运行时,将给定的地址规范化(易于执行),并检查哈希表是否包含此条目 - 如果是,则阻止。否则-允许。

例如,假设您想要阻止子网5.*.*.*,这实际上是具有前导位00000101的网络。因此,将地址5.0.0.000000101-00000000-00000000-00000000添加到哈希表中。
一旦到达特定地址-例如5.1.2.3,将其规范化回5.0.0.0,并检查它是否在表中。

使用哈希表,平均查询时间为O(1)


谢谢。但是对于使用非255 / 非8位基础网络掩码的子网怎么办?例如255.254.0.0(/15)。 - 比尔盖子
@比尔盖子 在这种情况下,将255.254.0.0放入您的表中,一旦出现特定的IP地址(例如255.254.5.6)- 从最不重要的16位中删除它,然后您将获得255.254.0.0 - 您可以在哈希表中进行检查。请注意,使用IP结构,您始终知道需要剥离哪些位。 - amit
3
知道规范化地址需要多少位比特是会给这个解决方案增加过多的复杂性。使用 Trie 数据结构可以获得更简单的解决方案。 - Chuck Wolber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接