阿里云前缀列表 API 实战:如何精准更新指定的 CIDR 地址块
一、问题场景
在实际运维中,我们经常会遇到这样一个需求:服务器的公网 IP 地址是动态变化的(如家庭宽带、云服务器的弹性 IP 变更等),每次 IP 变化后都需要手动更新安全组规则,将新的 IP 地址加入白名单。
手动操作不仅繁琐,还容易遗漏,导致服务访问中断。那么,有没有办法让这个过程自动化呢?
答案是:使用阿里云前缀列表 + API 自动更新。
二、什么是前缀列表?
前缀列表(Prefix List)是阿里云提供的一项网络管理功能,可以将一个或多个 CIDR 地址块打包成一个逻辑单元。之后在配置安全组规则或路由表时,可以直接引用这个前缀列表,而不是逐个填写 IP 地址。
核心优势:当前缀列表中的条目发生变化时,所有引用了该列表的规则会自动生效,无需手动修改每一条规则。
三、解决思路
我们的目标是:当服务器 IP 变化时,自动更新前缀列表中对应的 CIDR 地址块。
解决方案分为三步:
- 固定标识:为每个需要动态更新的条目设置一个固定的描述(Description),作为唯一标识
- 查询定位:调用 API 查询前缀列表,根据描述找到当前的 CIDR 地址块
- 精准更新:删除旧的 CIDR,添加新的 CIDR(格式:
新IP/32),保留原描述
整个流程可以用下图表示:
获取公网IP → 对比上次IP → 查询当前CIDR → 删除旧CIDR → 添加新CIDR → 保存新IP
四、涉及的 API 接口
前缀列表的管理涉及两个 ECS API:
4.1 DescribePrefixListAttributes
作用:查询前缀列表的详细信息,包括所有条目的 CIDR 和描述。
请求示例:
Action=DescribePrefixListAttributes
RegionId=cn-hangzhou
PrefixListId=pl-xxxxx
Version=2014-05-26
响应结构:
{
"Entries": {
"Entry": [
{
"Cidr": "192.168.1.0/24",
"Description": "my-server"
}
]
}
}
4.2 ModifyPrefixList
作用:修改前缀列表,支持添加或删除条目。
重要说明:该接口没有直接的“修改条目”功能。更新一个条目需要组合使用删除和添加:
| 操作 | 参数 |
|---|---|
| 删除旧 CIDR | RemoveEntry.1.Cidr=192.168.1.0/24 |
| 添加新 CIDR | AddEntry.1.Cidr=10.0.0.1/32 |
| 保留描述 | AddEntry.1.Description=my-server |
五、API 调用要点
5.1 公共参数
所有阿里云 API 请求都需要携带以下公共参数:
| 参数 | 说明 |
|---|---|
| AccessKeyId | 访问密钥 ID |
| Format | 固定 JSON |
| SignatureMethod | 固定 HMAC-SHA1 |
| SignatureVersion | 固定 1.0 |
| SignatureNonce | 唯一随机数 |
| Timestamp | UTC 时间戳 |
5.2 签名算法
签名是调用阿里云 API 的核心步骤,流程如下:
- 对所有请求参数按参数名升序排序
- 构造规范化查询字符串
- 构造待签名字符串:
GET&%2F&+ URLEncode(规范化字符串) - 使用
AccessKeySecret&作为密钥,计算 HMAC-SHA1 - 将结果 Base64 编码,得到 Signature
5.3 Endpoint 地址
前缀列表属于 ECS 产品,请求地址为:
https://ecs.{RegionId}.aliyuncs.com/
例如杭州地域:https://ecs.cn-hangzhou.aliyuncs.com/
5.4 地域配置
前缀列表是地域级资源,RegionId 必须与创建前缀列表时选择的地域一致。
| 地域 | RegionId |
|---|---|
| 华东1(杭州) | cn-hangzhou |
| 华东2(上海) | cn-shanghai |
| 华北2(北京) | cn-beijing |
| 华南1(深圳) | cn-shenzhen |
六、C++ 实现代码
以下是完整的 C++ 实现,基于阿里云签名算法和 libcurl。
6.1 配置文件 Config.h
#ifndef CONFIG_H
#define CONFIG_H
#include <string>
// 阿里云账号配置
#define ACCESS_KEY_ID "your-access-key-id"
#define ACCESS_KEY_SECRET "your-access-key-secret"
// 前缀列表配置
#define PREFIX_LIST_REGION_ID "cn-hangzhou"
#define PREFIX_LIST_ID "pl-xxxxxxxxxxxx"
#define PREFIX_LIST_CHECK_INTERVAL 60
#define PREFIX_LIST_RETRY_TIMES 3
#define PREFIX_LIST_RETRY_DELAY_SEC 5
// 路径配置
#define STATE_FILE_DIR "/tmp/prefix_updater/"
#define LOG_FILE_PATH "/tmp/prefix_updater/updater.log"
namespace Config {
inline std::string getAccessKeyId() { return ACCESS_KEY_ID; }
inline std::string getAccessKeySecret() { return ACCESS_KEY_SECRET; }
inline std::string getRegionId() { return PREFIX_LIST_REGION_ID; }
inline std::string getPrefixListId() { return PREFIX_LIST_ID; }
inline int getCheckInterval() { return PREFIX_LIST_CHECK_INTERVAL; }
inline int getRetryTimes() { return PREFIX_LIST_RETRY_TIMES; }
inline int getRetryDelaySec() { return PREFIX_LIST_RETRY_DELAY_SEC; }
inline std::string getStateFileDir() { return STATE_FILE_DIR; }
inline std::string getLogFilePath() { return LOG_FILE_PATH; }
}
#endif
6.2 核心更新逻辑
bool PrefixListUpdater::updateOnce() {
// 1. 获取当前公网IP
std::string currentIP = getPublicIP();
if (currentIP.empty()) return false;
// 2. 检查IP是否变化
if (!isIPChanged(currentIP)) return true;
// 3. 查询当前CIDR(根据描述)
std::string oldCidr = getCurrentCidrByDescription();
if (oldCidr.empty()) return false;
// 4. 更新:删除旧CIDR,添加新CIDR
std::string newCidr = currentIP + "/32";
bool success = updateCidrBlock(oldCidr, newCidr);
if (success) {
saveLastIP(currentIP);
}
return success;
}
6.3 查询当前 CIDR
std::string PrefixListUpdater::getCurrentCidrByDescription() {
std::map<std::string, std::string> params;
params["Action"] = "DescribePrefixListAttributes";
params["RegionId"] = m_config.regionId;
params["PrefixListId"] = m_config.prefixListId;
std::string resp = sendRequest(params);
// 解析JSON,根据Description找到对应的Cidr
return findCidrByDescription(resp, m_config.description);
}
6.4 更新 CIDR 地址块
bool PrefixListUpdater::updateCidrBlock(const std::string& oldCidr,
const std::string& newCidr) {
std::map<std::string, std::string> params;
params["Action"] = "ModifyPrefixList";
params["RegionId"] = m_config.regionId;
params["PrefixListId"] = m_config.prefixListId;
// 删除旧的
params["RemoveEntry.1.Cidr"] = oldCidr;
// 添加新的(保留原描述)
params["AddEntry.1.Cidr"] = newCidr;
params["AddEntry.1.Description"] = m_config.description;
std::string resp = sendRequest(params);
return resp.find("\"RequestId\"") != std::string::npos;
}
七、使用示例
7.1 编译
chmod +x build.sh
./build.sh
7.2 运行
# 单次更新(测试用)
./prefix_updater my-server-ip -once
# 持续监控(每60秒检查一次)
./prefix_updater my-server-ip
# 指定检查间隔(300秒)
./prefix_updater my-server-ip -i 300
7.3 命令行参数
| 参数 | 说明 |
|---|---|
<description> | 必需,要更新的条目描述 |
-once | 只运行一次,不进入守护模式 |
-h | 显示帮助信息 |
八、注意事项
| 限制项 | 说明 |
|---|---|
| CIDR 格式 | 单个 IP 必须写为 x.x.x.x/32 |
| 条目唯一性 | 同一前缀列表中 CIDR 不能重复 |
| 容量限制 | 单个前缀列表最多 200 个条目 |
| 描述长度 | 2-32 个字符 |
| 原子操作 | 同一请求中不能同时删除和添加同一个 CIDR |
| 地域匹配 | RegionId 必须与前缀列表创建地域一致 |
九、总结
通过阿里云前缀列表 API,我们可以实现 CIDR 地址块的精准自动更新。核心要点如下:
- 固定描述作为标识:为每个条目设置不变的 Description,用于查询定位
- 先查后改:先调用
DescribePrefixListAttributes获取当前 CIDR,再执行更新 - 删加组合:通过
RemoveEntry+AddEntry实现原子性的条目更新 - IP 格式规范:动态 IP 必须转换为
/32格式
这套方案已在实际生产环境中验证,可稳定运行,有效解决了动态 IP 场景下的网络访问控制问题。