這里使用Google Cloud和Cloudflare來實現,解決海外服務器被誤封IP,訪問不到的問題。
這段腳本的核心目的,是自動監測你在 Cloudflare 上管理的 VPS 域名是否可達,一旦發現域名無法 Ping 通,就會幫你更換IP:
-
重置該實例的外部 IP(通過 gcloud compute instances delete-access-config +
add-access-config
), -
把新分配到的 IP 同步到 Cloudflare DNS,
-
刷新本地 DNS 緩存,
-
并且全程將操作日志保存到按天分文件的 info_YYYYMMDD.log 中,方便你審計和排查。
—— 具體是這樣解決問題的 ——
問題場景 | 代碼模塊 | 解決辦法 |
---|---|---|
VPS 外網 IP 被 GCP 回收或被 ISP 屏蔽 | is_ping_reachable() | 用系統 ping 命令檢測域名連通性,如果返回碼≠0,就判定“被屏蔽/不可達”。 |
需要把 VPS 的新外網 IP 同步到用戶的域名 | update_external_ip() | 調用 gcloud compute instances delete-access-config + add-access-config,強制給實例分配一個新的外網 IP;再用 gcloud compute instances list 把這個 IP 取出來。 |
要讓域名馬上解析到新的 IP | update_dns() | 通過 Cloudflare REST API,查詢并更新(或新建)對應 A 記錄,把最新 IP 寫入 DNS。 |
本地可能還在緩存舊的 DNS 記錄 | flush_local_dns() | 在 Windows 下執行 ipconfig /flushdns,Linux 下嘗試刷新 systemd-resolve/nscd/dnsmasq 的緩存,確保下一次訪問能拿到新 IP。 |
整個流程要長期自動運行 | while True / time.sleep(300) | 把上面所有邏輯放到一個無限循環里,每輪處理完等待 5 分鐘(300 秒)再重新「讀配置 → 逐條處理」。 |
運維過程中出現任何錯誤也不影響后續 | try/except + 日志記錄 | 每次對單個 VPS 的操作都包在 try…except 中,捕獲異常后用 logging.error 把錯誤寫日志,但不拋出,保證后面的 VPS 還能繼續被處理。 |
總結:
-
監控(Ping)→ 修復(重置 IP + 更新 DNS)→ 驗證(本地 DNS 刷新),
-
全程自動化、可重試、有日志,從根本上解決了 VPS 外網 IP 不可用時,域名無法訪問的問題。
import os
import sys
import requests
import json
import platform
import subprocess
import re
import shutil
import time
from datetime import datetime
from pathlib import Path
import logging"""
腳本功能:
1. 輪詢 vps.config.json 中的 VPS 列表;
2. 若域名無法 ping 通,則重置實例外網 IP(gcloud)并同步到 Cloudflare;
3. 成功后刷新本地主機 DNS 緩存(Windows / Linux 通用);
4. 所有日志按日期寫入當前目錄的 info_YYYYMMDD.log,并保留 print 直觀輸出;
5. 出現任何異常僅記錄日志,不影響繼續處理下一條 VPS;
6. 每完成一輪后暫停 5 分鐘,重新加載配置再開始下一輪。
"""# === Cloudflare 相關參數 ===
API_TOKEN = "HGccH7pI34n65ZAz3MkBM1QGAQB7xo69_40J1" # 具有 DNS 編輯權限
ZONE_NAME = "askdfjsdd5.xyz" # 頂級域名
PROXIED = False # 關閉橙云(CDN)
TTL_SECONDS = 60 # TTL 最小值 60 秒CF_API = "https://api.cloudflare.com/client/v4"
HEADERS = {"Authorization": f"Bearer {API_TOKEN}","Content-Type": "application/json"
}# ================ 日志配置 ================
log_file = Path(__file__).with_name(f"info_{datetime.now():%Y%m%d}.log")
logging.basicConfig(level=logging.INFO,format="[%(asctime)s] %(message)s",datefmt="%Y-%m-%d %H:%M:%S",handlers=[logging.FileHandler(log_file, encoding="utf-8")]
)# ================ 數據結構 ================
class VPSInfo:"""保存單個 VPS/域名 的相關信息"""def __init__(self, proId: str, area: str, vpsId: str, dn: str):self.proId = proId # GCP 項目 IDself.area = area # GCP 可用區self.vpsId = vpsId # 實例名稱self.dn = dn # 對應域名def __repr__(self) -> str:return f"VPSInfo(proId={self.proId}, area={self.area}, vpsId={self.vpsId}, dn={self.dn})"# ================ 工具函數 ================
def load_vps_list(path: str = "vps.config.json"):"""加載配置文件并返回 VPSInfo 列表"""with open(path, "r", encoding="utf-8") as f:data = json.load(f)return [VPSInfo(item["proId"], item["area"], item["vpsId"], item["dn"]) for item in data]def is_ping_reachable(domain: str) -> bool:"""ping 判斷域名可達性"""count_param = "-n" if platform.system().lower() == "windows" else "-c"try:res = subprocess.run(["ping", count_param, "1", domain],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)return res.returncode == 0except Exception:return Falsedef cf(method: str, path: str, **kw):"""調用 Cloudflare API,成功返回 result"""url = CF_API + pathresp = requests.request(method, url, headers=HEADERS, timeout=30, **kw)resp.raise_for_status()data = resp.json()if not data.get("success"):raise RuntimeError(f"Cloudflare API 調用失敗: {data.get('errors')}")return data["result"]def update_dns(record_name: str, new_ip: str):"""創建或更新 A 記錄"""zone = cf("GET", f"/zones?name={ZONE_NAME}&status=active&per_page=1")if not zone:raise RuntimeError(f"未找到域 {ZONE_NAME} 對應的 Zone")zone_id = zone[0]["id"]body = {"type": "A", "name": record_name, "content": new_ip,"ttl": TTL_SECONDS, "proxied": PROXIED}recs = cf("GET", f"/zones/{zone_id}/dns_records?type=A&name={record_name}&per_page=1")if recs:rec_id = recs[0]["id"]cf("PUT", f"/zones/{zone_id}/dns_records/{rec_id}", json=body)print(f"已更新 DNS:{record_name} → {new_ip}")logging.info(f"已更新 DNS:{record_name} → {new_ip}")else:rec = cf("POST", f"/zones/{zone_id}/dns_records", json=body)print(f"已創建 DNS:{record_name} → {new_ip} (id={rec['id']})")logging.info(f"已創建 DNS:{record_name} → {new_ip} (id={rec['id']})")def flush_local_dns():"""刷新本機 DNS 緩存(Windows / Linux)"""system = platform.system().lower()cmds = [["ipconfig", "/flushdns"]] if system == "windows" else [["systemd-resolve", "--flush-caches"],["resolvectl", "flush-caches"],["service", "nscd", "restart"],["service", "dnsmasq", "restart"],]for argv in cmds:if shutil.which(argv[0]):res = subprocess.run(argv, stdout=subprocess.PIPE,stderr=subprocess.PIPE, text=True)if res.returncode == 0:print(f"已刷新本地 DNS:{' '.join(argv)}")logging.info(f"已刷新本地 DNS:{' '.join(argv)}")returnprint("刷新本地 DNS 失敗:未找到可用命令")logging.warning("刷新本地 DNS 失敗:未找到可用命令")def update_external_ip(vps: VPSInfo) -> str:"""重置外網 IP 并返回新的 IP"""gcloud = shutil.which("gcloud") or shutil.which("gcloud.cmd")if not gcloud:raise RuntimeError("找不到 gcloud,可執行文件不在 PATH 中")cmds = [[gcloud, "compute", "instances", "describe", vps.vpsId,f"--project={vps.proId}", f"--zone={vps.area}","--format=get(networkInterfaces[0].accessConfigs[0].name)"],[gcloud, "compute", "instances", "delete-access-config", vps.vpsId,f"--project={vps.proId}", f"--zone={vps.area}","--access-config-name=External NAT"],[gcloud, "compute", "instances", "add-access-config", vps.vpsId,f"--project={vps.proId}", f"--zone={vps.area}","--access-config-name=External NAT"],[gcloud, "compute", "instances", "list",f"--filter=name={vps.vpsId}"]]last_output = ""for idx, argv in enumerate(cmds, 1):proc = subprocess.run(argv, stdout=subprocess.PIPE,stderr=subprocess.PIPE, text=True)if proc.returncode != 0:raise RuntimeError(f"第 {idx} 步命令失敗: {' '.join(argv)}\n{proc.stderr.strip()}")last_output = proc.stdoutprint(f"第 {idx} 步命令執行成功")logging.info(f"第 {idx} 步命令執行成功")ips = re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", last_output)if not ips:raise RuntimeError("未在實例列表輸出中找到 IP")new_ip = ips[-1]print(f"獲得新外部 IP: {new_ip}")logging.info(f"獲得新外部 IP: {new_ip}")return new_ip# ================ 主流程 ================
def process_once():"""處理一輪:讀取配置并遍歷所有 VPS"""vps_list = load_vps_list()for vps in vps_list:print(f"開始處理: {vps}")logging.info(f"開始處理: {vps}")try:if is_ping_reachable(vps.dn):print(f"{vps.dn} 可 ping,跳過")logging.info(f"{vps.dn} 可 ping,跳過")else:print(f"{vps.dn} 不可 ping,準備重置 IP")logging.info(f"{vps.dn} 不可 ping,準備重置 IP")new_ip = update_external_ip(vps)update_dns(vps.dn, new_ip)flush_local_dns()print("等待 60 秒以確保解析生效")logging.info("等待 60 秒以確保解析生效")time.sleep(60)except Exception as e:print(f"處理 {vps.dn} 時發生異常: {e}")logging.error(f"處理 {vps.dn} 時發生異常: {e}")finally:print(f"完成處理: {vps}\n")logging.info(f"完成處理: {vps}\n")if __name__ == "__main__":while True: # 無限循環,每輪結束休眠 5 分鐘try:process_once()except Exception as e:print(f"腳本發生致命錯誤: {e}")logging.critical(f"腳本發生致命錯誤: {e}")print("=== 本輪結束,暫停 5 分鐘 ===\n")logging.info("=== 本輪結束,暫停 5 分鐘 ===\n")time.sleep(300) # 300 秒 = 5 分鐘