批量添加IP/IP段,检查IP地址是否属于CIDR范围的前端TS解决方案

2,604 阅读6分钟

如何确定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 位无符号整数,详细含义请参考:

    what-is-the-javascript-operator-and-how-do-you-use-it

正则用法

判断 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 范围,详细用法请参考:

    Array.prototype.some()

以上原文链接。作者: 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

添加后报错信息:

小结

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