阅读: 1

1、引言

在高频业务:一种IP网段判断重叠的算法(其二)中,我们完善了利用数轴进行IP重叠判断的算法。

本篇博客中,笔者讨论拓展该算法的应用面。

2、应用面的拓展

首先思考,进行IP重叠的判断,本质上是拿一个一串IP序列(即一连串数轴上的段或点),在一个数轴上进行“碰撞”的过程。如果碰撞成功,那么说明存在重叠。

那么碰撞除了带给我们“重叠”的信息,还有什么额外的信息呢,经过思考可以得知,当我们得出重叠的段时,可以获取到:

  1. 重叠的IP所在的数轴区域
  2. 重叠的IP究竟被什么IP所覆盖(碰撞)

看上去好像并没有什么用,但是当我们给每个IP加上一些维度标签时,事情就发生了变化。想象存在以下场景:

已知一些攻击事件的源IP段,分布在数轴上。需要判断另一批源IP是否命中了其中的攻击事件。

这看似于计算重叠无关,但本质上仍然是在计算重叠!因为我们已将IP抽象与数组之上,任何针对IP维度的属性标签的碰撞需求,本质上都是在计算重叠。

那我们的算法就可以应用到以下场景了:

  1. 筛选存在与僵尸网络A的源IP相同的源IP
  2. 筛选曾经出现在美国,且处于攻击组织B的常用肉鸡的源IP
  3. ….

3、数据结构的改造

那么我们首先需要对数组进行改造,这里以ipv6为例,因为ipv4仍然是整数形成的数组,只是每个段增加了“属性”标签。

ipv6较为特殊,转化为整数时更大,要考虑溢出问题。它的长度达到128位。

使用

type u128 struct {

hi uint64

lo uint64

}

表示128位整数。其中hi表示高64位,lo表示低64位

这样可以:

1.避免使用 big.Int

2.避免动态内存分配

3.降低GC压力

4.提高比较速度

现在采用一种统一的方式,表达一个IP段。

通过一个三元组(start, end, length)表示一个ip段,length表示这个ip段是多长。

对于单IP,1.1.1.1,会转换为 start = end = 16843009,此时区间长度为1

start = 16843009

end = 16843009

length = 1

[16843009, 16843009]

对于IP段的情况,

1.1.1.0/24

得到

start = 16843008

end = 16843263

length = 256

那么以ipv6为例我们有以下表达:

至于属性,则是在字典里,可以任意进行定义。

4、需要解决的问题

但是当我们定义上后,新的问题出现了。

问题1:考虑以下情况:

显而易见,更小的段,行业应该继承大段(金融),再包括自身的小段(农业)。

问题2:在考虑一种更复杂的情况:

我们设计上显然要避免这种情况,否则就会出现多段交叉的情况。此时的属性继承既依赖前,又依赖后。无法进行高效的统计。

5、方案设计

因此我们有以下设计:

  1. 所有的输入均为IP段,IP段在数学上映射到数轴之后,有一个非常神奇的性质,就是小段始终被大段包裹,并且小段和小段之间不会重叠。这解决了问题2
  2. 我们通过扫描线算法,在建表时以O(N)的时间成本,读取<ip, 业务标签>建立数轴即可。

下面介绍扫描线算法:

笔者将该算法执行的过程,称之为维度聚合。在维度聚合过程中。线段上的所有单IP或IP段,他们的业务标签属性被逐步修正为正确值。

原始数据可能存在,同一个IP段,对应多条记录。

例如

1.1.1.0/24   [金融]

1.1.1.0/24   [信息技术]

需要聚合为

1.1.1.0/24   [金融, 信息技术]

采用如下结构

type triplet struct {

ip string

ci int16

val string

}

ip表示ip段,ci表示维度列编号,val表示维度值

例如(1.1.1.0/24, industry, 金融)

所有数据被展开为(ip, col, value)

例如

1.1.1.0/24   industry 金融

1.1.1.0/24   industry 信息技术

1.1.1.0/24   app     网银

使用sort.Slice(…)进行排序,排序后,相同IP,相同维度,相同值会连续排列。

随后通过线性扫描算法,完成去重,聚合。

下面详细介绍sweep line线性扫描的应用

该算法用来解决CIDR嵌套继承。

假设以下数据

1.0.0.0/8   云服务

1.1.0.0/16  金融

1.1.1.0/24  银行

这是一个典型的嵌套场景,1.1.1.0/24需要继承,云服务和金融。因为它的段更小。被大段所包括。

1.0.0.0/8

└── 1.1.0.0/16

└── 1.1.1.0/24

假设下列三条线

/8 : ───────────────────────────────

/16 : ───────────────

/24 : ─────

有一根扫描线,不断向右移动,当扫描线进入某个CIDR(线段),说明当前CIDR开始生效,当扫描线离开某个CIDR时,说明当前CIDR结束。

为了实现扫描,每个CIDR会被拆为两个事件,一个表示进入事件,一个表示离开事件。

例如1.1.0.0/16

对应[16842752, start], [16843007, end]

系统在扫描过程中,会维护一个active集合,他表示正在覆盖扫描位置的所有CIDR(因为CIDR会有嵌套,扫描线位于某一点时,可能属于多个CIDR段)

开始扫描时,系统会按照数轴从左到右扫描所有事件。例如

(10, START)

/8开始

(20, START)

/16开始

(30, START)

/24开始

(40, END)

/24结束

(50, END)

/16结束

(60, END)

/8结束

当扫描到start事件时,存在两种情况,一种是刚刚进入该扫描段,另一种是在进入它之前,已经进入过其他扫描段(但还没退出)。

第二种情况时,active集合不为空,此时系统立刻知道,该扫描段,被active集合中的扫描段所包含。

然后系统会把active的维度,复制到当前扫描的段内。完成维度聚合。

这里还有一个问题,如何保证父CIDR一定出现在active集合中?

答案是首先对事件进行特殊排序(pos, isEnd, length)

先按照位置从小到大扫描

同一位置,start优先于end

同一位置 同类型的事件,更大的CIDR优先处理

当扫描到end事件时,说明该CIDR不再覆盖后续区域,因此需要移除出active集合。

该方案的优势:

1.以O(N)的复杂度解决了地质正确性中讨论的问题

2.能够任意扩展业务维度标签,子段继承父段的操作几乎没有开销

3.速度非常快,即使在100万个/24段的情况下仍然能在60秒内完成整个扫描与结果输出。

通过这种方式,现在我们拓展了我们算法的应用面,让他能够被应用于业务维度的碰撞之中。笔者在下一篇博客中将介绍这种算法一种工程化实现。

最后修改日期: 2026-09-28

作者