feat: implement routing_mode configuration to support auto, fixed_ip, and fixed_region selection

This commit is contained in:
baoweise
2026-05-29 14:09:28 +08:00
parent 06ca67306a
commit 224f7f2f48
+86 -29
View File
@@ -218,7 +218,7 @@ def get_state() -> dict[str, Any]:
state["username"] = ui_cfg.get("username", "admin")
state["port"] = ui_cfg.get("port", 8787)
state["secret_path"] = ui_cfg.get("secret_path", "EJsW2EeBo9lY")
state["enable_force_country"] = ui_cfg.get("enable_force_country", False)
state["routing_mode"] = ui_cfg.get("routing_mode", "auto")
state["force_country"] = ui_cfg.get("force_country", "")
return state
@@ -775,20 +775,33 @@ def auto_switch_node(attempt: int = 0) -> None:
print("[自动切换] 连续切换失败已达 3 次,停止切换以防止主线程死锁,将在后台重新加载节点...", flush=True)
return
ui_cfg = load_ui_config()
routing_mode = ui_cfg.get("routing_mode", "auto")
target_country = ui_cfg.get("force_country", "")
if routing_mode == "fixed_ip":
print("[自动切换] 当前处于固定 IP 模式,不进行自动切换。", flush=True)
if active_openvpn_node_id:
if not active_openvpn_running():
print(f"[自动切换] 固定 IP 模式检测到连接已断开,尝试重新连接原节点: {active_openvpn_node_id}", flush=True)
def reconnect_bg():
try:
connect_node(active_openvpn_node_id)
except Exception as e:
print(f"[自动切换] 重新连接固定节点失败: {e}", flush=True)
threading.Thread(target=reconnect_bg, daemon=True).start()
return
# Find the next best available node
with lock:
nodes = read_json(NODES_FILE, [])
ui_cfg = load_ui_config()
enable_force = ui_cfg.get("enable_force_country", False)
target_country = ui_cfg.get("force_country", "")
candidates = [
n for n in nodes
if n.get("probe_status") == "available"
and not n.get("active")
]
if enable_force and target_country:
if routing_mode == "fixed_region" and target_country:
candidates = [n for n in candidates if n.get("country") == target_country]
candidates.sort(key=lambda n: (parse_int(n.get("latency_ms")) or 999999, -parse_int(n.get("score"))))
@@ -807,6 +820,8 @@ def auto_switch_node(attempt: int = 0) -> None:
auto_switch_node(attempt + 1)
else:
msg = "没有可用的备选节点,将自动断开并清理当前连接状态,同时在后台异步获取新节点..."
if routing_mode == "fixed_region" and target_country:
msg = f"没有可用的【{target_country}】备选节点,已断开连接,将在后台持续尝试获取新节点..."
print(f"[自动切换] {msg}", flush=True)
log_to_json("WARNING", "VPN", msg)
stop_active_openvpn()
@@ -815,7 +830,7 @@ def auto_switch_node(attempt: int = 0) -> None:
for item in nodes:
item["active"] = False
write_json(NODES_FILE, nodes)
set_state(active_openvpn_node_id="", last_check_message="没有可用的备选节点,已断开")
set_state(active_openvpn_node_id="", last_check_message=msg)
def bg_fetch_and_switch():
try:
@@ -991,7 +1006,15 @@ def maintain_valid_nodes(force: bool = False) -> str:
# Test the first 10 non-active nodes from the new list
with lock:
current_nodes = read_json(NODES_FILE, [])
ui_cfg = load_ui_config()
routing_mode = ui_cfg.get("routing_mode", "auto")
target_country = ui_cfg.get("force_country", "")
if routing_mode == "fixed_region" and target_country:
to_test = [n for n in current_nodes if not n.get("active") and n.get("country") == target_country][:10]
else:
to_test = [n for n in current_nodes if not n.get("active")][:10]
to_test_ids = [n["id"] for n in to_test]
print(f"[维护线程] 正在检测新获取列表的前 10 个节点: {to_test_ids}", flush=True)
@@ -1004,11 +1027,15 @@ def maintain_valid_nodes(force: bool = False) -> str:
merged = read_json(NODES_FILE, [])
if not active_openvpn_running():
ui_cfg = load_ui_config()
enable_force = ui_cfg.get("enable_force_country", False)
routing_mode = ui_cfg.get("routing_mode", "auto")
target_country = ui_cfg.get("force_country", "")
if routing_mode == "fixed_ip":
if active_openvpn_node_id:
auto_switch_node()
else:
available_candidates = [n for n in merged if n.get("probe_status") == "available"]
if enable_force and target_country:
if routing_mode == "fixed_region" and target_country:
available_candidates = [n for n in available_candidates if n.get("country") == target_country]
if available_candidates:
@@ -2325,22 +2352,26 @@ INDEX_HTML = r"""<!doctype html>
</div>
<div style="border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 16px; margin-bottom: 16px;">
<div style="font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); font-weight: 600; margin-bottom: 12px;">出站IP路由设置 (锁定出站国家)</div>
<div class="form-group" style="margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="settings_enable_force" style="width: 16px; height: 16px; cursor: pointer; margin: 0;" onchange="toggleForceCountrySelect(this.checked)">
<label class="form-label" for="settings_enable_force" style="margin-bottom: 0; cursor: pointer; user-select: none;">启用强制锁定出站国家/地区</label>
</div>
<div style="font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); font-weight: 600; margin-bottom: 12px;">出站IP路由设置</div>
<div class="form-group" style="margin-bottom: 12px;">
<label class="form-label" for="settings_force_country">锁定国家</label>
<select id="settings_force_country" class="input-field" style="background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-color); color: var(--text-primary); outline: none; cursor: pointer; width: 100%; height: 40px; border-radius: 8px; padding: 0 12px;" disabled>
<label class="form-label" for="settings_routing_mode">IP 路由模式</label>
<select id="settings_routing_mode" class="input-field" style="background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-color); color: var(--text-primary); outline: none; cursor: pointer; width: 100%; height: 40px; border-radius: 8px; padding: 0 12px;" onchange="handleRoutingModeChange(this.value)">
<option value="auto">自动配置 (智能切换,最稳定)</option>
<option value="fixed_ip">固定 IP (永不自动换 IP)</option>
<option value="fixed_region">固定地区 (锁定特定国家节点)</option>
</select>
</div>
<div id="settings_force_country_group" class="form-group" style="margin-bottom: 12px; display: none;">
<label class="form-label" for="settings_force_country">锁定国家地区</label>
<select id="settings_force_country" class="input-field" style="background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-color); color: var(--text-primary); outline: none; cursor: pointer; width: 100%; height: 40px; border-radius: 8px; padding: 0 12px;">
<option value="">正在加载节点国家...</option>
</select>
</div>
<div style="font-size: 12px; color: var(--warning); line-height: 1.4; padding: 8px 12px; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.2); border-radius: 6px; margin-top: 8px;">
<strong>注意</strong>强制指向特定国家的出站IP在没有获取到该国最新可用节点的情况下可能导致代理连接失效
<div id="settings_routing_warning" style="font-size: 12px; color: var(--warning); line-height: 1.4; padding: 8px 12px; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.2); border-radius: 6px; margin-top: 8px;">
<strong>自动配置</strong>全自动测试并选择最佳IP。在使用过程中,如果当前连接节点没有失效,将不再更换IP;如果当前节点失效,系统将立刻秒级自动漂移到其他最快的可用节点
</div>
</div>
@@ -2979,8 +3010,29 @@ if (adminBtn && adminDropdown) {
});
}
function toggleForceCountrySelect(enabled) {
$("settings_force_country").disabled = !enabled;
function handleRoutingModeChange(mode) {
const countryGroup = $("settings_force_country_group");
const warningDiv = $("settings_routing_warning");
if (mode === "fixed_region") {
countryGroup.style.display = "block";
warningDiv.style.color = "var(--warning)";
warningDiv.style.background = "rgba(245, 158, 11, 0.1)";
warningDiv.style.border = "1px solid rgba(245, 158, 11, 0.2)";
warningDiv.innerHTML = `⚠️ <strong>固定地区</strong>:限制仅连接选定国家的节点,且后台仅并发测速该国家的节点。如果该国的所有可用节点都失效,会造成代理中断且<strong>绝不自动切换到其他国家</strong>的节点。`;
} else if (mode === "fixed_ip") {
countryGroup.style.display = "none";
warningDiv.style.color = "var(--warning)";
warningDiv.style.background = "rgba(245, 158, 11, 0.1)";
warningDiv.style.border = "1px solid rgba(245, 158, 11, 0.2)";
warningDiv.innerHTML = `⚠️ <strong>固定IP</strong>:锁定当前连接的节点。不管该节点是否失效,系统都绝不自动切换至其他IP;如果节点由于网络故障失效,会造成代理中断(但如果OpenVPN连接意外退出,脚本将尝试为您在后台重新拉起连接同一IP)。<br><strong>提示</strong>:您可以在主页的节点列表中直接点击“连接”按钮来选择并锁定不同的IP节点。`;
} else {
countryGroup.style.display = "none";
warningDiv.style.color = "var(--text-secondary)";
warningDiv.style.background = "rgba(255, 255, 255, 0.02)";
warningDiv.style.border = "1px solid rgba(255, 255, 255, 0.05)";
warningDiv.innerHTML = `️ <strong>自动配置</strong>:全自动测试并选择最佳IP。在使用过程中,如果当前连接节点没有失效,将不再更换IP;如果当前节点失效,系统将立刻秒级自动漂移到其他最快的可用节点。`;
}
}
function populateSettingsCountries() {
@@ -3000,10 +3052,10 @@ function populateSettingsCountries() {
select.innerHTML = html;
if (state) {
const enabled = state.enable_force_country || false;
$("settings_enable_force").checked = enabled;
const mode = state.routing_mode || "auto";
$("settings_routing_mode").value = mode;
select.value = state.force_country || "";
toggleForceCountrySelect(enabled);
handleRoutingModeChange(mode);
}
}
@@ -3042,7 +3094,7 @@ async function saveSettings(e) {
const newPassword = $("settings_new_password").value.trim();
const currUsername = $("settings_curr_username").value.trim();
const currPassword = $("settings_curr_password").value.trim();
const enableForce = $("settings_enable_force").checked;
const routingMode = $("settings_routing_mode").value;
const forceCountry = $("settings_force_country").value;
if (isNaN(port) || port < 1 || port > 65535) {
@@ -3057,7 +3109,7 @@ async function saveSettings(e) {
return;
}
if (enableForce && !forceCountry) {
if (routingMode === "fixed_region" && !forceCountry) {
errorDivEl.textContent = "请选择一个要锁定的目标国家";
errorDivEl.style.display = "block";
return;
@@ -3077,7 +3129,7 @@ async function saveSettings(e) {
new_password: newPassword,
curr_username: currUsername,
curr_password: currPassword,
enable_force_country: enableForce,
routing_mode: routingMode,
force_country: forceCountry
})
});
@@ -3497,9 +3549,13 @@ class Handler(BaseHTTPRequestHandler):
new_suffix = str(payload.get("secret_path") or "").strip()
new_username = str(payload.get("new_username") or "").strip()
new_password = str(payload.get("new_password") or "").strip()
enable_force_country = bool(payload.get("enable_force_country"))
routing_mode = str(payload.get("routing_mode") or "auto").strip()
force_country = str(payload.get("force_country") or "").strip()
if routing_mode not in ("auto", "fixed_ip", "fixed_region"):
self.send_json({"ok": False, "error": "无效的路由配置模式"}, HTTPStatus.BAD_REQUEST)
return
if not curr_username or not curr_password:
self.send_json({"ok": False, "error": "请输入当前账号和密码进行安全验证"}, HTTPStatus.FORBIDDEN)
return
@@ -3528,8 +3584,9 @@ class Handler(BaseHTTPRequestHandler):
ui_cfg["port"] = new_port_int
ui_cfg["secret_path"] = new_suffix
ui_cfg["enable_force_country"] = enable_force_country
ui_cfg["routing_mode"] = routing_mode
ui_cfg["force_country"] = force_country
ui_cfg.pop("enable_force_country", None)
if new_username:
ui_cfg["username"] = new_username
if new_password: