阿里云自动配置安全组IP白名单
阿里云用户凭证
xxx
xxx
安装 Python 环境
python3 --version
https://www.python.org/?spm=5176.28103460.0.0.51ee6308BjLGXI
安装必要的 Python 库
win
python -m pip install aliyun-python-sdk-ecs requests
linux
pip3 install aliyun-python-sdk-ecs requests
验证安装是否成功
python -c "import aliyunsdkcore; import requests; print('环境检查通过!')"
创建脚本文件sync_ip.py
sql
import os
import sys
import json
import logging
import requests
from datetime import datetime
# 引入阿里云 SDK
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException, ClientException
from aliyunsdkecs.request.v20140526 import AuthorizeSecurityGroupRequest, RevokeSecurityGroupRequest, DescribeSecurityGroupAttributeRequest
# ================= 配置区域 =================
ACCESS_KEY_ID = 'xxx'
ACCESS_KEY_SECRET = 'xxx'
REGION_ID = 'cn-shanghai'
SECURITY_GROUP_ID = 'sg-uf6bisuipjjdnb5rmugl'
PORT_RANGE = '80/80'
PROTOCOL = 'TCP'
TARGET_DESCRIPTION = 'nangtong-ca-servertest'
STATE_FILE = 'last_sync_ip.json'
LOG_FILE = 'sync_ip.log' # 日志文件名
# ===========================================
# --- 日志配置 ---
def setup_logger():
logger = logging.getLogger('IPSync')
logger.setLevel(logging.DEBUG) # 设置最低日志级别为 DEBUG,记录所有细节
# 创建 formatter (包含时间、级别、消息)
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 1. 控制台处理器 (Console Handler)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO) # 控制台只显示 INFO 及以上,避免太吵,调试时可改为 DEBUG
ch.setFormatter(formatter)
# 2. 文件处理器 (File Handler)
fh = logging.FileHandler(LOG_FILE, encoding='utf-8')
fh.setLevel(logging.DEBUG) # 文件记录所有 DEBUG 细节
fh.setFormatter(formatter)
logger.addHandler(ch)
logger.addHandler(fh)
return logger
logger = setup_logger()
# --- 辅助函数 ---
def mask_secret(secret):
"""脱敏显示 AccessKey Secret"""
if len(secret) > 4:
return secret[:2] + "****" + secret[-2:]
return "****"
def get_public_ip():
"""获取当前出口公网IP"""
logger.debug("Starting to fetch public IP...")
urls = [
'https://ifconfig.me/ip',
'https://api.ipify.org',
'https://icanhazip.com'
]
for url in urls:
try:
logger.debug(f"Trying URL: {url}")
resp = requests.get(url, timeout=5)
resp.raise_for_status()
ip = resp.text.strip()
if all(part.isdigit() and 0 <= int(part) <= 255 for part in ip.split('.')):
logger.info(f"Successfully retrieved public IP: {ip} from {url}")
return ip
else:
logger.warning(f"Invalid IP format received from {url}: {ip}")
except Exception as e:
logger.debug(f"Failed to fetch from {url}: {e}")
continue
logger.error("Failed to retrieve public IP from all sources.")
return None
def load_state():
"""加载本地状态"""
if os.path.exists(STATE_FILE):
try:
with open(STATE_FILE, 'r') as f:
data = json.load(f)
ip = data.get('ip')
ts = data.get('timestamp')
logger.info(f"Loaded state file: Last IP={ip}, Time={ts}")
return ip, ts
except Exception as e:
logger.error(f"Failed to parse state file: {e}")
else:
logger.info("State file not found. This might be the first run.")
return None, None
def save_state(ip):
"""保存状态"""
try:
with open(STATE_FILE, 'w') as f:
json.dump({'ip': ip, 'timestamp': datetime.now().isoformat()}, f)
logger.info(f"State saved successfully: {ip}")
except Exception as e:
logger.error(f"Failed to save state file: {e}")
def update_security_group(client, old_ip):
"""核心更新逻辑"""
logger.info("--- Starting Security Group Update Process ---")
# 1. 获取当前 IP
current_ip = get_public_ip()
if not current_ip:
logger.error("Aborting: Cannot determine current public IP.")
return False
if old_ip and old_ip == current_ip:
logger.info(f"IP unchanged ({current_ip}). No API calls needed. Exiting.")
return True
logger.info(f"IP detected change: [{old_ip}] -> [{current_ip}]")
# 2. 添加新规则
logger.info(f"Step 1: Attempting to ADD rule for {current_ip}...")
try:
auth_req = AuthorizeSecurityGroupRequest.AuthorizeSecurityGroupRequest()
auth_req.set_SecurityGroupId(SECURITY_GROUP_ID)
auth_req.set_SourceCidrIp(f"{current_ip}/32")
auth_req.set_PortRange(PORT_RANGE)
auth_req.set_IpProtocol(PROTOCOL)
auth_req.set_Description(TARGET_DESCRIPTION)
# 记录请求详情(脱敏)
logger.debug(f"Request Params: SG={SECURITY_GROUP_ID}, CIDR={current_ip}/32, Desc={TARGET_DESCRIPTION}")
resp = client.do_action_with_exception(auth_req)
logger.info(f"SUCCESS: Added rule for {current_ip}. Response: {resp[:100]}...") # 只打印部分响应
except Exception as e:
err_msg = str(e)
if "Duplicate" in err_msg or "already exists" in err_msg.lower():
logger.warning(f"Rule for {current_ip} already exists. Proceeding to cleanup logic.")
else:
logger.error(f"CRITICAL: Failed to add new rule. Error: {e}")
logger.error("Aborting deletion to prevent lockout.")
return False
# 3. 查找并删除旧规则
logger.info(f"Step 2: Scanning security group rules to find old entries with desc='{TARGET_DESCRIPTION}'...")
if old_ip and old_ip != current_ip:
try:
req_desc = DescribeSecurityGroupAttributeRequest.DescribeSecurityGroupAttributeRequest()
req_desc.set_SecurityGroupId(SECURITY_GROUP_ID)
req_desc.set_Direction('ingress')
resp = client.do_action_with_exception(req_desc)
resp_json = json.loads(resp)
permissions = resp_json.get('Permissions', {}).get('Permission', [])
logger.debug(f"Total ingress rules found: {len(permissions)}")
deleted_count = 0
for idx, perm in enumerate(permissions):
p_desc = perm.get('Description', '')
p_cidr = perm.get('SourceCidrIp', '')
p_port = perm.get('PortRange', '')
p_proto = perm.get('IpProtocol', '')
p_ip_only = p_cidr.split('/')[0]
# 调试日志:打印每条规则的关键信息
logger.debug(f" Rule[{idx}]: IP={p_ip_only}, Port={p_port}, Proto={p_proto}, Desc='{p_desc}'")
# 匹配逻辑
if (p_desc == TARGET_DESCRIPTION and
p_port == PORT_RANGE and
p_proto == PROTOCOL):
if p_ip_only != current_ip:
# 找到旧规则,执行删除
logger.info(f" -> MATCH FOUND (Old): {p_ip_only}. Sending DELETE request...")
try:
rev_req = RevokeSecurityGroupRequest.RevokeSecurityGroupRequest()
rev_req.set_SecurityGroupId(SECURITY_GROUP_ID)
rev_req.set_SourceCidrIp(p_cidr) # 带掩码
rev_req.set_PortRange(PORT_RANGE)
rev_req.set_IpProtocol(PROTOCOL)
client.do_action_with_exception(rev_req)
logger.info(f" -> SUCCESS: Deleted old rule {p_ip_only}")
deleted_count += 1
except Exception as del_e:
logger.error(f" -> FAILED to delete {p_ip_only}: {del_e}")
else:
logger.debug(f" -> SKIP: {p_ip_only} is the current/new IP, keeping it.")
else:
# 不匹配的规则
if p_desc != TARGET_DESCRIPTION:
logger.debug(f" -> SKIP: Description mismatch ('{p_desc}' != '{TARGET_DESCRIPTION}')")
elif p_port != PORT_RANGE or p_proto != PROTOCOL:
logger.debug(f" -> SKIP: Port/Protocol mismatch")
if deleted_count == 0 and old_ip:
logger.warning(f"Expected to delete old IP {old_ip}, but no matching rule found in the list.")
logger.warning("Possible reasons: Rule was manually deleted, or Description changed.")
elif deleted_count > 0:
logger.info(f"Cleanup complete. Removed {deleted_count} old rule(s).")
except Exception as e:
logger.error(f"Failed to describe security group rules: {e}")
logger.warning("Skipping deletion step. New rule is active, but old rules may remain.")
else:
logger.info("No old IP recorded or IP same. Skipping deletion scan.")
logger.info("--- Update Process Finished ---")
return True
def main():
logger.info(f"===== Script Started (PID: {os.getpid()}) =====")
logger.info(f"Config: Region={REGION_ID}, SG={SECURITY_GROUP_ID}, TargetDesc={TARGET_DESCRIPTION}")
logger.info(f"AK ID: {ACCESS_KEY_ID}, AK Secret: {mask_secret(ACCESS_KEY_SECRET)}")
last_ip, _ = load_state()
# 初始化客户端
try:
client = AcsClient(ACCESS_KEY_ID, ACCESS_KEY_SECRET, REGION_ID)
logger.debug("Aliyun SDK Client initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize Aliyun Client: {e}")
sys.exit(1)
success = update_security_group(client, last_ip)
if success:
# 重新获取一次确保保存的是最新的
final_ip = get_public_ip()
if final_ip:
save_state(final_ip)
logger.info("===== Script Completed Successfully =====")
else:
logger.error("===== Script Completed with Errors =====")
sys.exit(1)
if __name__ == '__main__':
main()
_创建重启和每小时定时触发的脚本_run_sync.bat
where python
sql
@echo off
cd /d "D:\file\a20260320"
"C:\Users\Administrator\AppData\Local\Programs\Python\Python310\python.exe" sync_ip.py
PowerShell 创建定时任务
schtasks /create /tn "SyncPublicIP" /tr "D:\file\a20260320\run_sync.bat" /sc ONSTART /delay 0001:00 /ru SYSTEM /rl HIGHEST /f
schtasks /create /tn "SyncPublicIP_Hourly" /tr "D:\file\a20260320\run_sync.bat" /sc HOURLY /mo 1 /ru SYSTEM /rl HIGHEST /f
测试是否输出日志
schtasks /run /tn "SyncPublicIP_Hourly"
禁用任务(保留配置,随时可以重新启用)
schtasks /change /tn "SyncPublicIP" /disable
schtasks /change /tn "SyncPublicIP_Hourly" /disable
再开启
schtasks /change /tn "SyncPublicIP" /enable
schtasks /change /tn "SyncPublicIP_Hourly" /enable
是否在启用
schtasks /query /tn "SyncPublicIP" /fo LIST /v | Select-String "上次运行|Last Run|状态|Status"
schtasks /query /tn "SyncPublicIP_Hourly" /fo LIST /v | Select-String "上次运行|Last Run|状态|Status"