阿里云自动配置安全组IP白名单

阿里云自动配置安全组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"

相关推荐
梦醒过后说珍重1 小时前
PyTorch 工程实践:如何优雅地将 ViT 大模型封装为即插即用的感知损失(Perceptual Loss)
python·深度学习
爱钓鱼的程序员小郭1 小时前
腾讯云 vs 阿里云 视频直播费用分析报告
阿里云·音视频·腾讯云
灰阳阳1 小时前
Docker实践-阿里云上创建私有仓库
阿里云·docker·容器
超越自我肖1 小时前
python--函数返回值之None类型
python
Oueii1 小时前
构建一个基于命令行的待办事项应用
jvm·数据库·python
中科固源2 小时前
强制性国标在路上:预解读《民用无人驾驶航空器数据链路网络安全要求》(一)
安全·网络安全·模糊测试·低空经济·商业航天
在屏幕前出油2 小时前
04. FastAPI——响应类型
开发语言·后端·python·pycharm·fastapi
Ivanqhz2 小时前
寄存器分配的核心函数 allocate
java·开发语言·后端·python·rust
阿里云云原生2 小时前
让 OpenClaw 受控运行: SLS 一键接入与审计
阿里云·云原生·sls·openclaw