feat:自动更换ip+流量监控

This commit is contained in:
2026-01-07 17:19:53 +08:00
commit 035da64084
27 changed files with 6182 additions and 0 deletions

152
services/ip_change.py Normal file
View File

@@ -0,0 +1,152 @@
"""IP 更换核心逻辑"""
from __future__ import annotations
import logging
from datetime import datetime, timezone
from database import Config, ProxyMachine, get_session, ensure_singleton_config
from services.aws_eip import create_ec2_client, is_valid_instance_id, rotate_elastic_ip
from services.lightsail_static_ip import (
create_lightsail_client,
is_valid_lightsail_instance_name,
rotate_lightsail_static_ip,
)
from services.cloudflare_dns import CloudflareAuth, upsert_a_record
logger = logging.getLogger(__name__)
def _get_cf_auth(config: Config) -> CloudflareAuth:
auth_type = (config.cloudflare_auth_type or "").strip() or "api_token"
if auth_type == "api_token":
return CloudflareAuth(auth_type="api_token", api_token=config.cf_api_token)
return CloudflareAuth(
auth_type="global_key",
email=config.cf_email,
api_key=config.cf_api_key,
)
def change_ip_for_machine(machine: ProxyMachine, config: Config) -> dict[str, str]:
"""为指定机器更换 IP 并更新 DNS"""
if not machine.enabled:
raise ValueError(f"机器 {machine.name} 已被禁用")
aws_service = (machine.aws_service or "ec2").strip().lower()
# 执行 IP 轮换
if aws_service == "lightsail":
if not is_valid_lightsail_instance_name(machine.aws_instance_id):
raise ValueError(
f"Lightsail 实例名格式不正确:{machine.aws_instance_id}"
)
lightsail = create_lightsail_client(
region=machine.aws_region,
aws_access_key=config.aws_access_key,
aws_secret_key=config.aws_secret_key,
)
logger.info(
"Rotating Lightsail Static IP for instance %s (%s)",
machine.aws_instance_id,
machine.name,
)
aws_result = rotate_lightsail_static_ip(
lightsail,
instance_name=machine.aws_instance_id,
release_old=bool(config.release_old_eip),
)
else:
if not is_valid_instance_id(machine.aws_instance_id):
raise ValueError(
f"EC2 Instance ID 格式不正确:{machine.aws_instance_id}(应类似 i-xxxxxxxxxxxxxxxxx"
)
ec2 = create_ec2_client(
region=machine.aws_region,
aws_access_key=config.aws_access_key,
aws_secret_key=config.aws_secret_key,
)
logger.info(
"Rotating Elastic IP for instance %s (%s)",
machine.aws_instance_id,
machine.name,
)
aws_result = rotate_elastic_ip(
ec2,
instance_id=machine.aws_instance_id,
release_old=bool(config.release_old_eip),
)
public_ip = aws_result.get("public_ip")
if not public_ip:
raise RuntimeError("AWS 未返回新的 Public IP")
message = f"IP 已更换为 {public_ip}"
# 如果机器配置了域名,更新 DNS
if machine.cf_zone_id and machine.cf_record_name:
logger.info("Updating Cloudflare A record %s -> %s", machine.cf_record_name, public_ip)
record = upsert_a_record(
zone_id=machine.cf_zone_id,
record_name=machine.cf_record_name,
ip=public_ip,
proxied=bool(machine.cf_proxied),
record_id=machine.cf_record_id,
auth=_get_cf_auth(config),
)
record_id = record.get("id")
if record_id:
machine.cf_record_id = record_id
message = f"已更新 {machine.cf_record_name} -> {public_ip}"
return {
"public_ip": public_ip,
"message": message,
}
def run_ip_change_for_machine(machine_id: int) -> dict:
"""为指定机器执行一次 IP 更换"""
session = get_session()
try:
machine = session.query(ProxyMachine).filter_by(id=machine_id).first()
if not machine:
return {"ok": False, "message": "机器不存在"}
config = ensure_singleton_config(session)
started_at = datetime.now(timezone.utc)
try:
result = change_ip_for_machine(machine, config)
machine.last_run_at = started_at
machine.last_success = True
machine.current_ip = result.get("public_ip")
machine.last_message = result.get("message") or "OK"
session.add(machine)
session.commit()
logger.info("IP change success for %s: %s", machine.name, machine.current_ip)
return {"ok": True, "machine_name": machine.name, **result}
except Exception as exc:
logger.exception("IP change failed for %s", machine.name)
machine.last_run_at = started_at
machine.last_success = False
machine.last_message = str(exc)
session.add(machine)
session.commit()
return {"ok": False, "machine_name": machine.name, "message": str(exc)}
finally:
session.close()
# 兼容旧接口
def run_ip_change() -> dict:
"""执行一次 IP 更换(兼容旧接口)"""
session = get_session()
try:
# 获取第一台启用的机器
machine = session.query(ProxyMachine).filter_by(enabled=True).first()
if not machine:
return {"ok": False, "message": "没有可用的机器"}
return run_ip_change_for_machine(machine.id)
finally:
session.close()