如何通过阿里云 OpenAPI 获取 DCDN 节点 IP 白名单

前言

事情的起因还得追溯到给博客加了 全站加速

依靠全站加速,还减轻了一次网站攻击,详见:记一次网站被 DDoS(CC)攻击 的过程和应对方案

在设置了全站加速之后,通过域名默认会走阿里云的 DCDN 节点,只有在未缓存的时候会访问源站。

此时,源站就会遇到一个需求:最好仅允许阿里云的 DCDN 节点进行访问。

也就是说,需要获取到阿里云的 DCDN 节点的 IP 列表,以添加到白名单中。

就此,开始了折腾之路。

开始

由于之前已经用过阿里云 OpenAPI 进行 CDN 的管理(如何通过阿里云 OpenAPI 管理 CDN),所以这一次也采用了阿里云 OpenAP,从而实现自动化设置。

翻阅全站加速相关文档可以看到一个接口:DescribeDcdnL2Ips – 查询 L2 节点 IP 地址

image-20240919224443348

这正是我需要的。

由于文档里没有说前置要求,所以我想当然的认为只要提交工单就可以获得权限。

中道崩阻

结果:

image-20240919224751866

不是,大哥,你在 DCDN 的文档中又没有说明前置要求,没写就是没要求才对。

再者,CDN 节点的 IP 是什么需要藏着掖着的东西吗?你不公开出来我怎么设置白名单?

隔壁 Cloudflare 就是把 IP 地址范围全部公布出来的,例如 IP 地址范围

所以什么要设置个门槛?不是很能理解。

峰回路转

但很快啊,我又发现另一个接口:DescribeDcdnIpInfo – 验证 IP 节点

image-20240919225454340

虽然没法直接获取 DCDN 节点的 IP ,但是可以验证一个 IP 是不是阿里云 DCDN 节点。

解决问题

所以接下来解决方案就很简单了。

我手动把 IP 地址验证一遍不就完事了?

当然了,考虑到 IPv4 一共有 42 亿个 IP,显然是不可能穷举一遍的。

那么我就得找个地方获取访问 IP。

获取 IP 地址

而 NGINX 的日志,就是一个获取 IP 的好地方。

以下是一个从日志中获取 IP 地址的 TypeScript 函数:

// 定义提取 IP 地址的函数
async function extractUniqueIps(logFilePath: string) {
    // 使用 Set 来存储唯一的 IP 地址
    const uniqueIps = new Set<string>()

    // 使用流式读取日志文件
    const readStream = fs.createReadStream(logFilePath, { encoding: 'utf-8' })
    const rl = createInterface({ input: readStream })

    // 逐行处理日志文件
    rl.on('line', (line) => {
        // 使用正则表达式提取 IP 地址
        const ipPattern = /\b([0-9]{1,3}\.){3}[0-9]{1,3}\b/
        const match = line.match(ipPattern)
        if (match) {
            uniqueIps.add(match[0])
        }
    })

    // 返回一个 Promise,当读取结束时,解析为唯一的 IP 地址数组
    return new Promise<string[]>((resolve) => {
        rl.on('close', () => {
            resolve([...uniqueIps])
        })
    })
}

采用流式读取是因为日志文件可能会有点大,全部读取进来有点占内存。

经过这一通折腾,我拿到了一个约 28000 个 IP 地址。

有一说一,这个量还是有点大,50 次/秒的话要跑 560 秒,约 9.3 分钟,这个时间就有点久了。

所以,可以采用 IP 属地进行初次过滤。

获取 IP 属地

通过 lionsoul2014/ip2region 这个项目可以拿到所有 IP 的属地。

虽然数据上可能有点过时,但用来初步筛选已经够了。

以下是一个使用 JavaScript 实现的查询脚本(参考:ip2region nodejs 查询客户端实现

// 导入包
const Searcher = require('.')
// 指定ip2region数据文件路径
const dbPath = 'ip2region.xdb file path'

try {
  // 创建searcher对象
  const searcher = Searcher.newWithFileOnly(dbPath)
  // 查询
  const data = await searcher.search('218.4.167.70')
  // data: {region: '中国|0|江苏省|苏州市|电信', ioCount: 3, took: 1.342389}
} catch(e) {
  console.log(e)
}

然后从中筛选出 region 为 阿里云阿里巴巴 的 IP。

接下来,再判断是否为阿里云 DCDN 节点。

判断 DCDN 节点

接下来就是调用 DescribeDcdnIpInfo 接口来验证是否为阿里云 DCDN 节点了。

以下是一个用 TypeScript 实现的 Client 封装类。

// This file is auto-generated, don't edit it
// 依赖的模块可通过下载工程中的模块依赖文件或右上角的获取 SDK 依赖信息查看
import dcdn20180115, * as $dcdn20180115 from '@alicloud/dcdn20180115'
import OpenApi, * as $OpenApi from '@alicloud/openapi-client'
import Util, * as $Util from '@alicloud/tea-util'

export default class Client {

    /**
   * @remarks
   * 使用AK&SK初始化账号Client
   * @returns Client
   *
   * @throws Exception
   */
    static createClient(): dcdn20180115 {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378664.html。
        const config = new $OpenApi.Config({
            // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
            accessKeyId: process.env['ALIBABA_CLOUD_ACCESS_KEY_ID'],
            // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
            accessKeySecret: process.env['ALIBABA_CLOUD_ACCESS_KEY_SECRET'],
        })
        // Endpoint 请参考 https://api.aliyun.com/product/dcdn
        config.endpoint = 'dcdn.aliyuncs.com'
        return new (dcdn20180115 as any).default(config)
    }

    static async DescribeDcdnIpInfoRequest(IP: string) {
        const client = Client.createClient()
        const describeDcdnIpInfoRequest = new $dcdn20180115.DescribeDcdnIpInfoRequest({
            IP,
        })
        const runtime = new $Util.RuntimeOptions({})
        try {
            // 复制代码运行请自行打印 API 的返回值
            return (await client.describeDcdnIpInfoWithOptions(describeDcdnIpInfoRequest, runtime)).body
        } catch (error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            console.log(error.message)
            // 诊断地址
            console.log(error.data['Recommend'])

        }
    }

}

最后,把是 阿里云 DCDN 的 IP 写入到文件。

至此,就大功告成了!

生成网段

不过,单纯的用 IP 的话实际上容易误判,原因是阿里云 DCDN 的 IP 还是挺多的,靠从 NGINX 日志获取的方法效率低不说,还很滞后。

所以,将 IP 改为网段更加合理一些。

以下是一个从 IP 地址列表生成 IP 网段的 TypeScript 脚本。

/**
 *
 *
 * @author CaoMeiYouRen
 * @date 2024-09-16
 * @param ips IP地址
 * @param [netMask=24] 子网掩码位数
 */
function getSubnetCounts(ips: string[], netMask = 24) {
    // 创建 Map 来存储网段及其出现频率
    const subnetCounts = new Map<string, number>()

    // 遍历每个 IP 地址
    for (const ip of ips) {
        // 提取前 n 段作为网段
        const segments = ip.split('.')
        const subnetSegments = segments.slice(0, netMask / 8)
        const subnet = `${subnetSegments.join('.')}${'.0'.repeat(4 - subnetSegments.length)}/NULL`

        // 如果网段不存在于 subnetCounts 中,初始化为 0
        if (!subnetCounts.has(subnet)) {
            subnetCounts.set(subnet, 0)
        }

        // 增加网段的计数
        subnetCounts.set(subnet, subnetCounts.get(subnet) + 1)
    }
    return [...subnetCounts].map(([subnet, count]) => ({ subnet, count }))
}

然后就可以生成 NGINX 的白名单了,生成的结果如下:

allow 101.133.199.0/24; # count: 66
allow 101.200.20.0/24; # count: 58
allow 101.37.183.0/24; # count: 2
allow 106.11.37.0/24; # count: 23
allow 112.124.132.0/24; # count: 30
allow 114.215.0.0/16; # count: 108
allow 114.215.72.0/24; # count: 108
allow 118.190.214.0/24; # count: 22
allow 118.190.218.0/24; # count: 14
allow 119.23.123.0/24; # count: 39
allow 119.23.91.0/24; # count: 41
allow 120.27.78.0/24; # count: 43
allow 121.199.80.0/24; # count: 37
allow 121.89.252.0/24; # count: 6
allow 139.129.78.0/24; # count: 6
allow 139.196.128.0/24; # count: 46
allow 163.181.0.0/16; # count: 562
allow 163.181.1.0/24; # count: 10
allow 163.181.126.0/24; # count: 38
allow 163.181.128.0/24; # count: 2
allow 163.181.130.0/24; # count: 22
allow 163.181.131.0/24; # count: 12
allow 163.181.132.0/24; # count: 1
allow 163.181.140.0/24; # count: 14
allow 163.181.143.0/24; # count: 3
allow 163.181.145.0/24; # count: 19
allow 163.181.146.0/24; # count: 36
allow 163.181.154.0/24; # count: 11
allow 163.181.160.0/24; # count: 16
allow 163.181.164.0/24; # count: 10
allow 163.181.166.0/24; # count: 9
allow 163.181.18.0/24; # count: 2
allow 163.181.199.0/24; # count: 20
allow 163.181.201.0/24; # count: 15
allow 163.181.22.0/24; # count: 12
allow 163.181.23.0/24; # count: 5
allow 163.181.35.0/24; # count: 24
allow 163.181.37.0/24; # count: 5
allow 163.181.38.0/24; # count: 10
allow 163.181.39.0/24; # count: 10
allow 163.181.42.0/24; # count: 11
allow 163.181.49.0/24; # count: 5
allow 163.181.50.0/24; # count: 6
allow 163.181.52.0/24; # count: 2
allow 163.181.57.0/24; # count: 12
allow 163.181.66.0/24; # count: 17
allow 163.181.67.0/24; # count: 34
allow 163.181.70.0/24; # count: 2
allow 163.181.77.0/24; # count: 23
allow 163.181.78.0/24; # count: 10
allow 163.181.79.0/24; # count: 39
allow 163.181.81.0/24; # count: 24
allow 163.181.82.0/24; # count: 4
allow 163.181.85.0/24; # count: 31
allow 163.181.88.0/24; # count: 3
allow 163.181.89.0/24; # count: 1
allow 163.181.90.0/24; # count: 14
allow 163.181.92.0/24; # count: 14
allow 163.181.94.0/24; # count: 4
allow 39.100.171.0/24; # count: 62
allow 39.103.26.0/24; # count: 1
allow 39.108.196.0/24; # count: 9
allow 39.96.152.0/24; # count: 52
allow 47.102.25.0/24; # count: 45
allow 47.104.50.0/24; # count: 46
allow 47.117.201.0/24; # count: 56
allow 47.118.223.0/24; # count: 51
allow 47.120.0.0/16; # count: 129
allow 47.120.230.0/24; # count: 77
allow 47.120.92.0/24; # count: 52
allow 47.123.117.0/24; # count: 26
allow 47.246.0.0/16; # count: 189
allow 47.246.2.0/24; # count: 14
allow 47.246.20.0/24; # count: 14
allow 47.246.22.0/24; # count: 14
allow 47.246.23.0/24; # count: 15
allow 47.246.24.0/24; # count: 14
allow 47.246.36.0/24; # count: 4
allow 47.246.37.0/24; # count: 7
allow 47.246.38.0/24; # count: 4
allow 47.246.4.0/24; # count: 28
allow 47.246.42.0/24; # count: 13
allow 47.246.44.0/24; # count: 8
allow 47.246.45.0/24; # count: 3
allow 47.246.46.0/24; # count: 7
allow 47.246.48.0/24; # count: 14
allow 47.246.49.0/24; # count: 9
allow 47.246.50.0/24; # count: 21
allow 8.132.0.0/16; # count: 206
allow 8.132.18.0/24; # count: 132
allow 8.132.19.0/24; # count: 74
allow 8.141.176.0/24; # count: 50

注释里面的 count 是该网段有多少个阿里云 DCDN IP。

以上结果仅供参考,如有需要,可以参考这些 IP 网段。

可以看到 NGINX 的白名单中有部分还采用了 16 位子网掩码,将 IP 范围进一步扩大到 65536 个。

总结

以上就是如何通过阿里云 OpenAPI 获取 DCDN 节点 IP 白名单的具体方法。

如果你达到了调用 DescribeDcdnL2Ips - 查询L2节点IP地址接口的要求,那么实际上直接调用这个接口会更加方便。

如果你没达到要求的话,可以参考本文的实现方案,虽然不够优雅,但应该够用了。


喜欢的话就点个关注吧~

欢迎在评论区评论~


评论

发表回复