如何确定IPv4地址是否在CIDR范围内?
什么是CIDR表示法?

CIDR(Classless Inter-Domain Routing,无类别域间路由)是一种 IP 地址归类方法,主要用于分配 IP 地址与有效地路由 IP 数据包等功能。
CIDR 的表示方法为 A.B.C.D/N,其中 A.B.C.D 是点分十进制的 IPv4 地址,N 是 0 至 32 之间的整数,代表 CIDR 的前缀长度,两者用斜线 / 分隔。
若一个 IP 地址的前 N 位与 一个 CIDR 范围的前缀相同,则说明此 IP 地址在此 CIDR 范围中。以 CIDR 范围 10.10.1.32/27 为例,可知 110.10.1.44 属于该 CIDR 范围,而 10.10.1.90 则不属于,如下图。

具体实现方法:
验证是否是IPv4
IPv4需要满足以下三个要求:
- 由三个 . 分隔的四个整数
- 每个整数范围是 0 至 255
- 每个整数不能有前导 0
/**
* 判断IPv4是否合法
*/
validateIp4(ip: string): boolean {
return /^((\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))(\.|$)){4}$/.test(ip) ? true : false;
}
validateIp4(192.168.1.1); // true
validateIp4(10.0.0.25); // true
validateIp4(10.0.0.1000); // false
其中正则解释:
- (\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5])) 限定每部分整数取值只能为 0 至 9、10 至 99、100 至 255
- (\.|$) 限定每部分结尾只能是 . 符号或行尾
验证是否是CIDR
IPv4需要满足以下三个要求:
- 长度范围是 0 至 32
- 不能有前导 0
/**
* 判断CIDR是否合法
* @param cidr ip段
*/
validateCidr(cidr: string): boolean {
return /^((\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\.){3}((\d|[1-9]\d|1\d\d|2([0-4]\d|5[0-5]))\/){1}(\d|[1-3]\d)$/.test(cidr) ? true : false;
}
validateCidr("192.168.1.1/24") // true
validateCidr("192.168.1.1") // false
validateCidr("192.168.1/24") // false
validateCidr("192.168.1.1/48") // false
validateCidr("192.168.1/024") // false
由于 IPv4 地址最后一部分不再匹配行尾,因此正则表达式拆分成两部分:
- IPv4 地址前三部分末尾匹配 . 符号,最后一部分末尾匹配 / 符号
- (\d|[1-3]\d) 限定前缀范围为 0 至 32。
计算 IPv4 地址每部分整数的移位累加和
ip4ToInt 用于计算 IPv4 地址每部分整数的移位累加和。可以理解为,在去除 . 符号后,得到从左至右排列的 32 位二进制整数,再将其转换为十进制整数。
/**
* 计算 IPv4 地址每部分整数的移位累加和
* @param ip i
*/
ip4ToInt(ip: string): number {
return ip.split('.').reduce((sum, part) => (sum << 8) + parseInt(part, 10), 0) >>> 0;
}
ip4ToInt("192.168.1.1") // 3232235777
ip4ToInt("192.168.1") // 12625921
ip4ToInt("192.168.1.1000") // 3232236776
ip4ToInt("192.168.1.01") // 3232235777
由测试结果可见,函数暂未做 IPv4 地址合法性判断,适用于任何点分数字字符串,其中:
- split('.') 将 IPv4 地址的每部分取出,例如 "192.168.1.1".split('.') 将得到 ["192", "168", "1", "1"]
- reduce((sum, part) => (sum << 8) + parseInt(part, 10), 0) 将计算每个数组元素的移位累加和,详细用法请参考:Array.prototype.reduce()
- sum 为累加和,作为 reduce 函数的结果返回,初始值设置为 0
- part 为每个数组元素,转换为十进制整数后,与左移 8 位后的 sum 相加
- >>> 0 将计算结果转换为 32 位无符号整数,详细含义请参考:
判断 IPv4 地址是否属于 CIDR 范围
/**
* 判断IPv4是否属于CIRD范围
* @param ip ip
* @param cidr ip段
*/
isIp4InCidr(ip: string, cidr: string): boolean {
if (this.validateIp4(ip) && this.validateCidr(cidr)) {
const [range, bits] = cidr.split('/');
const mask = ~(2 ** (32 - (bits as any)) - 1);
return (this.ip4ToInt(ip) & mask) === (this.ip4ToInt(range) & mask);
}
return false
};
isIp4InCidr("192.168.1.1")("192.168.1.1/24") // true
isIp4InCidr("192.168.1.1")("192.168.1.1/48") // false
isIp4InCidr("192.168.1")("192.168.1.1/24") // false
若 IPv4 地址与 CIDR 范围均合法,则继续进行判断,否则返回 false,其中:
- [range, bits] = cidr.split('/') 使用了 ES6 的新特性解构赋值,range 为 CIDR 范围的 IPv4 部分,bits 为前缀长度部分
- mask = ~(2 ** (32 - bits) - 1) 根据前缀长度计算掩码,掩码的前 bits 位为 1,后 32 - bits 位为 0
- 若 ip 与 range 的前 bits 位相同,则返回 true,否则返回 false
判断 IPv4 地址是否属于 CIDR 数组内的其中一个范围
/**
* 判断 IPv4 地址是否属于 CIDR 数组内的其中一个范围
* @param ip ip
* @param cidr ip段的数组
*/
isIp4InCidrs(ip: string, cidrs: string[]): boolean{
return cidrs.some(isIp4InCidr(ip));
}
isIp4InCidrs('192.168.10.1', ['10.10.0.0/16', '192.168.1.1/24']); // false
isIp4InCidrs('192.168.10.1', ['10.10.0.0/16', '192.168.1.1/16']); // true
isIp4InCidrs('10.10.10.10', ['10.10.0.0/48', '10.10.0.0/16']); // true
若 IPv4 地址属于CIDR 数组内的其中一个范围,则返回 true,结束判断,否则返回 false,其中:
- isIp4InCidr(ip)返回的是cird是否匹配上某个IPv4
- some(isIp4InCidr(ip)) 将判断 CIDR 数组中,只要有一个元素满足 isIp4InCidr(ip) 为真,即 IPv4 地址属于某个 CIDR 范围,详细用法请参考:
以上原文链接。作者: Cipher Saw
下面是批量添加IP/IP段并验证的具体实践
问题来源:
需求:
如下图:
批量添加IP或IP段,如果当前添加的IP存在与某一IP段内,则提示不能重复添加。

思路:
首先要把textarea中以回车换行的值转成数组:
<mat-form-field class="app-box-table-box-left-form-field" appearance="outline">
<textarea matInput
cdkTextareaAutosize
[formControl]="blackIpCtrl"
matTextareaAutosize="true"
class="textarea">
</textarea>
</mat-form-field>
我这里用balckNoteCtrl绑定这个表单,便于获取值和设置报错信息。
写一个方法allPassed判断是否全部通过验证,如通过则返回true,不通过直接setError提示错误信息。
全局开始:
import * as _ from 'lodash';
export interface SecurityTable {
id: number;
ip: string;
note: string;
}
// ip黑名单的控制器
blackIpCtrl: FormControl;
// ip黑名单数据
blackTableDatasource: Array<SecurityTable>;
// 当前已存在的黑名单ip段
existedBlackCidrs: Array<string>;
constructor(){
this.existedBlackCidrs = [];
this.blackIpCtrl = new FormControl();
this.blackTableDatasource = [];
}
全局结束.
/**
* 添加域名时检测
* @param filterArr 源数组
*/
allPassed(filterArr: any): boolean {
const passed = [];
let errors = [];
filterArr ?.length && filterArr.forEach((data: string, i: number, arr: string[]) => {
// 判断当前输入的是否重复或有关联关系开始
const copyArr = JSON.parse(JSON.stringify(arr));
copyArr.splice(i, 1);
copyArr.forEach((temp: string) => {
// 是否重复
if (temp === arr[i]) {
errors.unshift([`${data}重复,不能重复添加`]);
passed.unshift(false);
}
// 是否有关联关系
if (this.validateCidr(arr[i])) {
if (this.isIp4InCidr(temp, arr[i])) {
errors.unshift([`${temp}从属于${arr[i]},不能重复添加`]);
passed.unshift(false);
}
} else {
if (this.validateCidr(temp)) {
if (this.isIp4InCidr(arr[i], temp)) {
errors.unshift([`${arr[i]}从属于${temp},不能重复添加`]);
passed.unshift(false);
}
}
}
});
// 判断当前输入的是否重复或有关联关系结束
// 判断格式是否正确
passed.unshift(((!this.validateIp4(data)) && (!this.validateCidr(data))) ? false : true);
if ((!this.pattern.test(data)) && (!this.isCirl(data))) {
errors.unshift([`${data}格式不正确`]);
}
if (this.validateIp4(data)) {
this.existedBlackCidrs.forEach((cidr: any) => {
if (cidr) {
if (this.isIp4InCidr(data, cidr)) {
errors.unshift([`${data}从属于${cidr},不能重复添加`]);
passed.unshift(false);
}
}
});
if (this.blackTableDatasource.find(((black: any) => black.ip === data))) {
errors.unshift([`${data}已存在于您的IP黑名单中,不能重复添加`]);
passed.unshift(false);
}
}else{
if (this.validateCidr(data)) {
if (this.blackTableDatasource.find(((black: any) => black.ip === data))) {
errors.unshift([`${data}已存在于您的IP黑名单中,不能重复添加`]);
passed.unshift(false);
}
if (this.blackTableDatasource.find((black: any) => this.isIp4InCidr(black.ip, data))) {
errors.unshift([`${data}在您的IP黑名单中已存在从属IP,不能重复添加`]);
passed.unshift(false);
}
}
}
});
errors = _.uniqWith(errors, _.isEqual);
setTimeout(() => {
this.blackIpCtrl.setErrors(errors);
});
return passed.every((d) => d === true);
}
当点击保存按钮时,执行代码:
/**
* 黑名单添加时保存
*/
onBlackSaveClick(): void {
if (!this.blackIpCtrl.value) {
return;
}
// 以回车分割获取ip数组
const splitArr = this.blackIpCtrl.value.split(/[(\r\n)\r\n]+/);
// 过滤空值
const filterArr = splitArr ?.length && splitArr.filter(d => d);
// 传给allPassed进行判断
if (this.allPassed(filterArr)) {
filterArr ?.length && filterArr.forEach((data) => {
this.blackTableDatasource.unshift(
{
id: this.blackTableDatasource.length ? Number(this.blackTableDatasource.reduce((pre: any, curv: any) => {
return pre.value < curv.value ? curv : pre;
}).id) + 1 : this.blackTableDatasource.length + 1,
ip: data,
note: this.blackNoteCtrl.value
});
});
this.blackTableDatasource = [...this.blackTableDatasource];
// 操作状态
this.blackOperating = false;
// 更新已存的Cidr
this.refreshExistedCidr();
// 清空输入框
this.blackIpCtrl.reset();
}
}
已存在的ipv4

添加后报错信息:

小结
解决实际问题的时候来循序渐进,先把若干问题分成小问题,再逐个突破,最终聚合到一起,可以更加高效。