platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
633
selective-vpn-gui/main_window/singbox/editor_mixin.py
Normal file
633
selective-vpn-gui/main_window/singbox/editor_mixin.py
Normal file
@@ -0,0 +1,633 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from main_window.constants import SINGBOX_EDITOR_PROTOCOL_IDS, SINGBOX_PROTOCOL_SEED_SPEC
|
||||
|
||||
|
||||
class SingBoxEditorMixin:
|
||||
def _selected_singbox_profile_id(self) -> str:
|
||||
selected = self._selected_transport_client()
|
||||
if selected is not None:
|
||||
selected_cid = str(getattr(selected, "id", "") or "").strip()
|
||||
if (
|
||||
selected_cid
|
||||
and self._singbox_editor_profile_id
|
||||
and selected_cid == str(self._singbox_editor_profile_client_id or "").strip()
|
||||
):
|
||||
return str(self._singbox_editor_profile_id).strip()
|
||||
if selected_cid:
|
||||
# Desktop SingBox tab keeps one deterministic profile per engine card.
|
||||
return selected_cid
|
||||
return self._selected_transport_engine_id()
|
||||
|
||||
def _set_singbox_editor_enabled(self, enabled: bool) -> None:
|
||||
widgets = [
|
||||
self.ent_singbox_proto_name,
|
||||
self.chk_singbox_proto_enabled,
|
||||
self.cmb_singbox_proto_protocol,
|
||||
self.ent_singbox_vless_server,
|
||||
self.spn_singbox_vless_port,
|
||||
self.ent_singbox_vless_uuid,
|
||||
self.ent_singbox_proto_password,
|
||||
self.cmb_singbox_vless_flow,
|
||||
self.cmb_singbox_vless_packet_encoding,
|
||||
self.cmb_singbox_ss_method,
|
||||
self.ent_singbox_ss_plugin,
|
||||
self.spn_singbox_hy2_up_mbps,
|
||||
self.spn_singbox_hy2_down_mbps,
|
||||
self.ent_singbox_hy2_obfs,
|
||||
self.ent_singbox_hy2_obfs_password,
|
||||
self.cmb_singbox_tuic_congestion,
|
||||
self.cmb_singbox_tuic_udp_mode,
|
||||
self.chk_singbox_tuic_zero_rtt,
|
||||
self.ent_singbox_wg_private_key,
|
||||
self.ent_singbox_wg_peer_public_key,
|
||||
self.ent_singbox_wg_psk,
|
||||
self.ent_singbox_wg_local_address,
|
||||
self.ent_singbox_wg_reserved,
|
||||
self.spn_singbox_wg_mtu,
|
||||
self.btn_singbox_wg_paste_private,
|
||||
self.btn_singbox_wg_copy_private,
|
||||
self.btn_singbox_wg_paste_peer,
|
||||
self.btn_singbox_wg_copy_peer,
|
||||
self.btn_singbox_wg_paste_psk,
|
||||
self.btn_singbox_wg_copy_psk,
|
||||
self.cmb_singbox_vless_transport,
|
||||
self.ent_singbox_vless_path,
|
||||
self.ent_singbox_vless_grpc_service,
|
||||
self.cmb_singbox_vless_security,
|
||||
self.ent_singbox_vless_sni,
|
||||
self.ent_singbox_tls_alpn,
|
||||
self.cmb_singbox_vless_utls_fp,
|
||||
self.ent_singbox_vless_reality_pk,
|
||||
self.ent_singbox_vless_reality_sid,
|
||||
self.chk_singbox_vless_insecure,
|
||||
self.chk_singbox_vless_sniff,
|
||||
]
|
||||
for w in widgets:
|
||||
w.setEnabled(bool(enabled))
|
||||
|
||||
def _clear_singbox_editor(self) -> None:
|
||||
self._singbox_editor_loading = True
|
||||
try:
|
||||
self._singbox_editor_profile_id = ""
|
||||
self._singbox_editor_profile_client_id = ""
|
||||
self._singbox_editor_protocol = "vless"
|
||||
self._singbox_editor_source_raw = {}
|
||||
self.ent_singbox_proto_name.setText("")
|
||||
self.chk_singbox_proto_enabled.setChecked(True)
|
||||
self.cmb_singbox_proto_protocol.setCurrentIndex(0)
|
||||
self.ent_singbox_vless_server.setText("")
|
||||
self.spn_singbox_vless_port.setValue(443)
|
||||
self.ent_singbox_vless_uuid.setText("")
|
||||
self.ent_singbox_proto_password.setText("")
|
||||
self.cmb_singbox_vless_flow.setCurrentIndex(0)
|
||||
self.cmb_singbox_vless_flow.setEditText("")
|
||||
self.cmb_singbox_vless_packet_encoding.setCurrentIndex(0)
|
||||
self.cmb_singbox_ss_method.setCurrentIndex(0)
|
||||
self.ent_singbox_ss_plugin.setText("")
|
||||
self.spn_singbox_hy2_up_mbps.setValue(0)
|
||||
self.spn_singbox_hy2_down_mbps.setValue(0)
|
||||
self.ent_singbox_hy2_obfs.setText("")
|
||||
self.ent_singbox_hy2_obfs_password.setText("")
|
||||
self.cmb_singbox_tuic_congestion.setCurrentIndex(0)
|
||||
self.cmb_singbox_tuic_udp_mode.setCurrentIndex(0)
|
||||
self.chk_singbox_tuic_zero_rtt.setChecked(False)
|
||||
self.ent_singbox_wg_private_key.setText("")
|
||||
self.ent_singbox_wg_peer_public_key.setText("")
|
||||
self.ent_singbox_wg_psk.setText("")
|
||||
self.ent_singbox_wg_local_address.setText("")
|
||||
self.ent_singbox_wg_reserved.setText("")
|
||||
self.spn_singbox_wg_mtu.setValue(0)
|
||||
self.cmb_singbox_vless_transport.setCurrentIndex(0)
|
||||
self.ent_singbox_vless_path.setText("")
|
||||
self.ent_singbox_vless_grpc_service.setText("")
|
||||
self.cmb_singbox_vless_security.setCurrentIndex(0)
|
||||
self.ent_singbox_vless_sni.setText("")
|
||||
self.ent_singbox_tls_alpn.setText("")
|
||||
self.cmb_singbox_vless_utls_fp.setCurrentIndex(0)
|
||||
self.ent_singbox_vless_reality_pk.setText("")
|
||||
self.ent_singbox_vless_reality_sid.setText("")
|
||||
self.chk_singbox_vless_insecure.setChecked(False)
|
||||
self.chk_singbox_vless_sniff.setChecked(True)
|
||||
finally:
|
||||
self._singbox_editor_loading = False
|
||||
self.on_singbox_vless_editor_changed()
|
||||
|
||||
def _load_singbox_editor_for_selected(self, *, silent: bool = True) -> None:
|
||||
client = self._selected_transport_client()
|
||||
if client is None:
|
||||
self._clear_singbox_editor()
|
||||
self._set_singbox_editor_enabled(False)
|
||||
return
|
||||
try:
|
||||
cid = str(getattr(client, "id", "") or "").strip()
|
||||
profile = self.ctrl.singbox_profile_get_for_client(
|
||||
client,
|
||||
profile_id=self._selected_singbox_profile_id(),
|
||||
)
|
||||
self._apply_singbox_editor_profile(profile, fallback_name=str(getattr(client, "name", "") or "").strip())
|
||||
self._singbox_editor_profile_client_id = cid
|
||||
self._set_singbox_editor_enabled(True)
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
raise
|
||||
self._append_transport_log(f"[profile] editor load failed: {e}")
|
||||
self._clear_singbox_editor()
|
||||
self._set_singbox_editor_enabled(False)
|
||||
|
||||
def _find_editor_proxy_outbound(self, outbounds: list[Any]) -> dict[str, Any]:
|
||||
proxy = {}
|
||||
for row in outbounds:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
t = str(row.get("type") or "").strip().lower()
|
||||
tag = str(row.get("tag") or "").strip().lower()
|
||||
if self._is_supported_editor_protocol(t):
|
||||
proxy = row
|
||||
break
|
||||
if tag == "proxy":
|
||||
proxy = row
|
||||
return dict(proxy) if isinstance(proxy, dict) else {}
|
||||
|
||||
def _find_editor_sniff_inbound(self, inbounds: list[Any]) -> dict[str, Any]:
|
||||
inbound = {}
|
||||
for row in inbounds:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
tag = str(row.get("tag") or "").strip().lower()
|
||||
t = str(row.get("type") or "").strip().lower()
|
||||
if tag == "socks-in" or t == "socks":
|
||||
inbound = row
|
||||
break
|
||||
return dict(inbound) if isinstance(inbound, dict) else {}
|
||||
|
||||
def _apply_singbox_editor_profile(self, profile, *, fallback_name: str = "") -> None:
|
||||
raw = getattr(profile, "raw_config", {}) or {}
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
protocol = str(getattr(profile, "protocol", "") or "").strip().lower() or "vless"
|
||||
outbounds = raw.get("outbounds") or []
|
||||
if not isinstance(outbounds, list):
|
||||
outbounds = []
|
||||
inbounds = raw.get("inbounds") or []
|
||||
if not isinstance(inbounds, list):
|
||||
inbounds = []
|
||||
|
||||
proxy = self._find_editor_proxy_outbound(outbounds)
|
||||
inbound = self._find_editor_sniff_inbound(inbounds)
|
||||
proxy_type = str(proxy.get("type") or "").strip().lower()
|
||||
if self._is_supported_editor_protocol(proxy_type):
|
||||
protocol = proxy_type
|
||||
|
||||
tls = proxy.get("tls") if isinstance(proxy.get("tls"), dict) else {}
|
||||
reality = tls.get("reality") if isinstance(tls.get("reality"), dict) else {}
|
||||
utls = tls.get("utls") if isinstance(tls.get("utls"), dict) else {}
|
||||
transport = proxy.get("transport") if isinstance(proxy.get("transport"), dict) else {}
|
||||
|
||||
security = "none"
|
||||
if bool(tls.get("enabled", False)):
|
||||
security = "tls"
|
||||
if bool(reality.get("enabled", False)):
|
||||
security = "reality"
|
||||
|
||||
transport_type = str(transport.get("type") or "").strip().lower() or "tcp"
|
||||
path = str(transport.get("path") or "").strip()
|
||||
grpc_service = str(transport.get("service_name") or "").strip()
|
||||
alpn_vals = tls.get("alpn") or []
|
||||
if not isinstance(alpn_vals, list):
|
||||
alpn_vals = []
|
||||
alpn_text = ",".join([str(x).strip() for x in alpn_vals if str(x).strip()])
|
||||
|
||||
self._singbox_editor_loading = True
|
||||
try:
|
||||
self._singbox_editor_profile_id = str(getattr(profile, "id", "") or "").strip()
|
||||
self._singbox_editor_protocol = protocol
|
||||
self._singbox_editor_source_raw = json.loads(json.dumps(raw))
|
||||
self.ent_singbox_proto_name.setText(
|
||||
str(getattr(profile, "name", "") or "").strip() or fallback_name or self._singbox_editor_profile_id
|
||||
)
|
||||
self.chk_singbox_proto_enabled.setChecked(bool(getattr(profile, "enabled", True)))
|
||||
pidx = self.cmb_singbox_proto_protocol.findData(protocol)
|
||||
self.cmb_singbox_proto_protocol.setCurrentIndex(pidx if pidx >= 0 else 0)
|
||||
self.ent_singbox_vless_server.setText(str(proxy.get("server") or "").strip())
|
||||
try:
|
||||
self.spn_singbox_vless_port.setValue(int(proxy.get("server_port") or 443))
|
||||
except Exception:
|
||||
self.spn_singbox_vless_port.setValue(443)
|
||||
self.ent_singbox_vless_uuid.setText(str(proxy.get("uuid") or "").strip())
|
||||
self.ent_singbox_proto_password.setText(str(proxy.get("password") or "").strip())
|
||||
|
||||
flow_value = str(proxy.get("flow") or "").strip()
|
||||
idx = self.cmb_singbox_vless_flow.findData(flow_value)
|
||||
if idx >= 0:
|
||||
self.cmb_singbox_vless_flow.setCurrentIndex(idx)
|
||||
else:
|
||||
self.cmb_singbox_vless_flow.setEditText(flow_value)
|
||||
|
||||
pe = str(proxy.get("packet_encoding") or "").strip().lower()
|
||||
if pe in ("none", "off", "false"):
|
||||
pe = ""
|
||||
idx = self.cmb_singbox_vless_packet_encoding.findData(pe)
|
||||
self.cmb_singbox_vless_packet_encoding.setCurrentIndex(idx if idx >= 0 else 0)
|
||||
|
||||
ss_method = str(proxy.get("method") or "").strip().lower()
|
||||
idx = self.cmb_singbox_ss_method.findData(ss_method)
|
||||
if idx >= 0:
|
||||
self.cmb_singbox_ss_method.setCurrentIndex(idx)
|
||||
else:
|
||||
self.cmb_singbox_ss_method.setEditText(ss_method)
|
||||
self.ent_singbox_ss_plugin.setText(str(proxy.get("plugin") or "").strip())
|
||||
|
||||
try:
|
||||
self.spn_singbox_hy2_up_mbps.setValue(int(proxy.get("up_mbps") or 0))
|
||||
except Exception:
|
||||
self.spn_singbox_hy2_up_mbps.setValue(0)
|
||||
try:
|
||||
self.spn_singbox_hy2_down_mbps.setValue(int(proxy.get("down_mbps") or 0))
|
||||
except Exception:
|
||||
self.spn_singbox_hy2_down_mbps.setValue(0)
|
||||
obfs = proxy.get("obfs") if isinstance(proxy.get("obfs"), dict) else {}
|
||||
self.ent_singbox_hy2_obfs.setText(str(obfs.get("type") or "").strip())
|
||||
self.ent_singbox_hy2_obfs_password.setText(str(obfs.get("password") or "").strip())
|
||||
|
||||
cc = str(proxy.get("congestion_control") or "").strip()
|
||||
idx = self.cmb_singbox_tuic_congestion.findData(cc)
|
||||
if idx >= 0:
|
||||
self.cmb_singbox_tuic_congestion.setCurrentIndex(idx)
|
||||
else:
|
||||
self.cmb_singbox_tuic_congestion.setCurrentIndex(0)
|
||||
udp_mode = str(proxy.get("udp_relay_mode") or "").strip()
|
||||
idx = self.cmb_singbox_tuic_udp_mode.findData(udp_mode)
|
||||
self.cmb_singbox_tuic_udp_mode.setCurrentIndex(idx if idx >= 0 else 0)
|
||||
self.chk_singbox_tuic_zero_rtt.setChecked(bool(proxy.get("zero_rtt_handshake", False)))
|
||||
|
||||
self.ent_singbox_wg_private_key.setText(str(proxy.get("private_key") or "").strip())
|
||||
self.ent_singbox_wg_peer_public_key.setText(str(proxy.get("peer_public_key") or "").strip())
|
||||
self.ent_singbox_wg_psk.setText(str(proxy.get("pre_shared_key") or "").strip())
|
||||
local_addr = proxy.get("local_address") or []
|
||||
if not isinstance(local_addr, list):
|
||||
if str(local_addr or "").strip():
|
||||
local_addr = [str(local_addr).strip()]
|
||||
else:
|
||||
local_addr = []
|
||||
self.ent_singbox_wg_local_address.setText(
|
||||
",".join([str(x).strip() for x in local_addr if str(x).strip()])
|
||||
)
|
||||
reserved = proxy.get("reserved") or []
|
||||
if not isinstance(reserved, list):
|
||||
if str(reserved or "").strip():
|
||||
reserved = [str(reserved).strip()]
|
||||
else:
|
||||
reserved = []
|
||||
self.ent_singbox_wg_reserved.setText(
|
||||
",".join([str(x).strip() for x in reserved if str(x).strip()])
|
||||
)
|
||||
try:
|
||||
self.spn_singbox_wg_mtu.setValue(int(proxy.get("mtu") or 0))
|
||||
except Exception:
|
||||
self.spn_singbox_wg_mtu.setValue(0)
|
||||
|
||||
idx = self.cmb_singbox_vless_transport.findData(transport_type)
|
||||
self.cmb_singbox_vless_transport.setCurrentIndex(idx if idx >= 0 else 0)
|
||||
self.ent_singbox_vless_path.setText(path)
|
||||
self.ent_singbox_vless_grpc_service.setText(grpc_service)
|
||||
|
||||
idx = self.cmb_singbox_vless_security.findData(security)
|
||||
self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 0)
|
||||
self.ent_singbox_vless_sni.setText(str(tls.get("server_name") or "").strip())
|
||||
self.ent_singbox_tls_alpn.setText(alpn_text)
|
||||
idx = self.cmb_singbox_vless_utls_fp.findData(str(utls.get("fingerprint") or "").strip())
|
||||
self.cmb_singbox_vless_utls_fp.setCurrentIndex(idx if idx >= 0 else 0)
|
||||
self.ent_singbox_vless_reality_pk.setText(str(reality.get("public_key") or "").strip())
|
||||
self.ent_singbox_vless_reality_sid.setText(str(reality.get("short_id") or "").strip())
|
||||
self.chk_singbox_vless_insecure.setChecked(bool(tls.get("insecure", False)))
|
||||
self.chk_singbox_vless_sniff.setChecked(bool(inbound.get("sniff", True)))
|
||||
finally:
|
||||
self._singbox_editor_loading = False
|
||||
self.on_singbox_vless_editor_changed()
|
||||
|
||||
def _validate_singbox_editor_form(self) -> None:
|
||||
protocol = self._current_editor_protocol()
|
||||
addr = self.ent_singbox_vless_server.text().strip()
|
||||
if not addr:
|
||||
raise RuntimeError("Address is required")
|
||||
security = str(self.cmb_singbox_vless_security.currentData() or "none").strip().lower()
|
||||
transport = str(self.cmb_singbox_vless_transport.currentData() or "tcp").strip().lower()
|
||||
if protocol == "vless":
|
||||
if not self.ent_singbox_vless_uuid.text().strip():
|
||||
raise RuntimeError("UUID is required for VLESS")
|
||||
if security == "reality" and not self.ent_singbox_vless_reality_pk.text().strip():
|
||||
raise RuntimeError("Reality public key is required for Reality security mode")
|
||||
elif protocol == "trojan":
|
||||
if not self.ent_singbox_proto_password.text().strip():
|
||||
raise RuntimeError("Password is required for Trojan")
|
||||
if security == "reality":
|
||||
raise RuntimeError("Reality security is not supported for Trojan in this editor")
|
||||
elif protocol == "shadowsocks":
|
||||
method = str(self.cmb_singbox_ss_method.currentData() or "").strip()
|
||||
if not method:
|
||||
method = self.cmb_singbox_ss_method.currentText().strip()
|
||||
if not method:
|
||||
raise RuntimeError("SS method is required for Shadowsocks")
|
||||
if not self.ent_singbox_proto_password.text().strip():
|
||||
raise RuntimeError("Password is required for Shadowsocks")
|
||||
elif protocol == "hysteria2":
|
||||
if not self.ent_singbox_proto_password.text().strip():
|
||||
raise RuntimeError("Password is required for Hysteria2")
|
||||
elif protocol == "tuic":
|
||||
if not self.ent_singbox_vless_uuid.text().strip():
|
||||
raise RuntimeError("UUID is required for TUIC")
|
||||
if not self.ent_singbox_proto_password.text().strip():
|
||||
raise RuntimeError("Password is required for TUIC")
|
||||
elif protocol == "wireguard":
|
||||
if not self.ent_singbox_wg_private_key.text().strip():
|
||||
raise RuntimeError("WireGuard private key is required")
|
||||
if not self.ent_singbox_wg_peer_public_key.text().strip():
|
||||
raise RuntimeError("WireGuard peer public key is required")
|
||||
local_addr = [str(x).strip() for x in self.ent_singbox_wg_local_address.text().split(",") if str(x).strip()]
|
||||
if not local_addr:
|
||||
raise RuntimeError("WireGuard local address is required (CIDR list)")
|
||||
self._parse_wg_reserved_values(
|
||||
[str(x).strip() for x in self.ent_singbox_wg_reserved.text().split(",") if str(x).strip()],
|
||||
strict=True,
|
||||
)
|
||||
|
||||
if protocol in ("vless", "trojan"):
|
||||
if transport == "grpc" and not self.ent_singbox_vless_grpc_service.text().strip():
|
||||
raise RuntimeError("gRPC service is required for gRPC transport")
|
||||
if transport in ("ws", "http", "httpupgrade") and not self.ent_singbox_vless_path.text().strip():
|
||||
raise RuntimeError("Transport path is required for selected transport")
|
||||
|
||||
def _build_singbox_editor_raw_config(self) -> dict[str, Any]:
|
||||
base = self._singbox_editor_source_raw
|
||||
if not isinstance(base, dict):
|
||||
base = {}
|
||||
raw: dict[str, Any] = json.loads(json.dumps(base))
|
||||
protocol = self._current_editor_protocol()
|
||||
|
||||
outbounds = raw.get("outbounds") or []
|
||||
if not isinstance(outbounds, list):
|
||||
outbounds = []
|
||||
|
||||
proxy_idx = -1
|
||||
for i, row in enumerate(outbounds):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
t = str(row.get("type") or "").strip().lower()
|
||||
tag = str(row.get("tag") or "").strip().lower()
|
||||
if self._is_supported_editor_protocol(t) or tag == "proxy":
|
||||
proxy_idx = i
|
||||
break
|
||||
|
||||
proxy: dict[str, Any]
|
||||
if proxy_idx >= 0:
|
||||
proxy = dict(outbounds[proxy_idx]) if isinstance(outbounds[proxy_idx], dict) else {}
|
||||
else:
|
||||
proxy = {}
|
||||
|
||||
proxy["type"] = protocol
|
||||
proxy["tag"] = str(proxy.get("tag") or "proxy")
|
||||
proxy["server"] = self.ent_singbox_vless_server.text().strip()
|
||||
proxy["server_port"] = int(self.spn_singbox_vless_port.value())
|
||||
# clear protocol-specific keys before repopulating
|
||||
for key in (
|
||||
"uuid",
|
||||
"password",
|
||||
"method",
|
||||
"plugin",
|
||||
"flow",
|
||||
"packet_encoding",
|
||||
"up_mbps",
|
||||
"down_mbps",
|
||||
"obfs",
|
||||
"congestion_control",
|
||||
"udp_relay_mode",
|
||||
"zero_rtt_handshake",
|
||||
"private_key",
|
||||
"peer_public_key",
|
||||
"pre_shared_key",
|
||||
"local_address",
|
||||
"reserved",
|
||||
"mtu",
|
||||
):
|
||||
proxy.pop(key, None)
|
||||
|
||||
if protocol == "vless":
|
||||
proxy["uuid"] = self.ent_singbox_vless_uuid.text().strip()
|
||||
flow = str(self.cmb_singbox_vless_flow.currentData() or "").strip()
|
||||
if not flow:
|
||||
flow = self.cmb_singbox_vless_flow.currentText().strip()
|
||||
if flow:
|
||||
proxy["flow"] = flow
|
||||
packet_encoding = str(self.cmb_singbox_vless_packet_encoding.currentData() or "").strip().lower()
|
||||
if packet_encoding and packet_encoding != "none":
|
||||
proxy["packet_encoding"] = packet_encoding
|
||||
elif protocol == "trojan":
|
||||
proxy["password"] = self.ent_singbox_proto_password.text().strip()
|
||||
elif protocol == "shadowsocks":
|
||||
method = str(self.cmb_singbox_ss_method.currentData() or "").strip()
|
||||
if not method:
|
||||
method = self.cmb_singbox_ss_method.currentText().strip()
|
||||
proxy["method"] = method
|
||||
proxy["password"] = self.ent_singbox_proto_password.text().strip()
|
||||
plugin = self.ent_singbox_ss_plugin.text().strip()
|
||||
if plugin:
|
||||
proxy["plugin"] = plugin
|
||||
elif protocol == "hysteria2":
|
||||
proxy["password"] = self.ent_singbox_proto_password.text().strip()
|
||||
up = int(self.spn_singbox_hy2_up_mbps.value())
|
||||
down = int(self.spn_singbox_hy2_down_mbps.value())
|
||||
if up > 0:
|
||||
proxy["up_mbps"] = up
|
||||
if down > 0:
|
||||
proxy["down_mbps"] = down
|
||||
obfs_type = self.ent_singbox_hy2_obfs.text().strip()
|
||||
if obfs_type:
|
||||
obfs: dict[str, Any] = {"type": obfs_type}
|
||||
obfs_password = self.ent_singbox_hy2_obfs_password.text().strip()
|
||||
if obfs_password:
|
||||
obfs["password"] = obfs_password
|
||||
proxy["obfs"] = obfs
|
||||
elif protocol == "tuic":
|
||||
proxy["uuid"] = self.ent_singbox_vless_uuid.text().strip()
|
||||
proxy["password"] = self.ent_singbox_proto_password.text().strip()
|
||||
cc = str(self.cmb_singbox_tuic_congestion.currentData() or "").strip()
|
||||
if not cc:
|
||||
cc = self.cmb_singbox_tuic_congestion.currentText().strip()
|
||||
if cc:
|
||||
proxy["congestion_control"] = cc
|
||||
udp_mode = str(self.cmb_singbox_tuic_udp_mode.currentData() or "").strip()
|
||||
if udp_mode:
|
||||
proxy["udp_relay_mode"] = udp_mode
|
||||
if self.chk_singbox_tuic_zero_rtt.isChecked():
|
||||
proxy["zero_rtt_handshake"] = True
|
||||
elif protocol == "wireguard":
|
||||
proxy["private_key"] = self.ent_singbox_wg_private_key.text().strip()
|
||||
proxy["peer_public_key"] = self.ent_singbox_wg_peer_public_key.text().strip()
|
||||
psk = self.ent_singbox_wg_psk.text().strip()
|
||||
if psk:
|
||||
proxy["pre_shared_key"] = psk
|
||||
local_addr = [str(x).strip() for x in self.ent_singbox_wg_local_address.text().split(",") if str(x).strip()]
|
||||
if local_addr:
|
||||
proxy["local_address"] = local_addr
|
||||
reserved_vals = self._parse_wg_reserved_values(
|
||||
[str(x).strip() for x in self.ent_singbox_wg_reserved.text().split(",") if str(x).strip()],
|
||||
strict=True,
|
||||
)
|
||||
if reserved_vals:
|
||||
proxy["reserved"] = reserved_vals
|
||||
mtu = int(self.spn_singbox_wg_mtu.value())
|
||||
if mtu > 0:
|
||||
proxy["mtu"] = mtu
|
||||
|
||||
transport_type = str(self.cmb_singbox_vless_transport.currentData() or "tcp").strip().lower()
|
||||
if protocol in ("vless", "trojan"):
|
||||
self._apply_proxy_transport(
|
||||
proxy,
|
||||
transport=transport_type,
|
||||
path=self.ent_singbox_vless_path.text().strip(),
|
||||
grpc_service=self.ent_singbox_vless_grpc_service.text().strip(),
|
||||
)
|
||||
else:
|
||||
proxy.pop("transport", None)
|
||||
|
||||
security = str(self.cmb_singbox_vless_security.currentData() or "none").strip().lower()
|
||||
if protocol == "vless":
|
||||
pass
|
||||
elif protocol == "trojan":
|
||||
if security == "reality":
|
||||
security = "tls"
|
||||
elif protocol in ("hysteria2", "tuic"):
|
||||
security = "tls"
|
||||
else:
|
||||
security = "none"
|
||||
|
||||
alpn = []
|
||||
for p in self.ent_singbox_tls_alpn.text().split(","):
|
||||
v = str(p or "").strip()
|
||||
if v:
|
||||
alpn.append(v)
|
||||
self._apply_proxy_tls(
|
||||
proxy,
|
||||
security=security,
|
||||
sni=self.ent_singbox_vless_sni.text().strip(),
|
||||
utls_fp=str(self.cmb_singbox_vless_utls_fp.currentData() or "").strip(),
|
||||
tls_insecure=bool(self.chk_singbox_vless_insecure.isChecked()),
|
||||
reality_public_key=self.ent_singbox_vless_reality_pk.text().strip(),
|
||||
reality_short_id=self.ent_singbox_vless_reality_sid.text().strip(),
|
||||
alpn=alpn,
|
||||
)
|
||||
|
||||
if proxy_idx >= 0:
|
||||
outbounds[proxy_idx] = proxy
|
||||
else:
|
||||
outbounds.insert(0, proxy)
|
||||
|
||||
has_direct = any(
|
||||
isinstance(row, dict)
|
||||
and str(row.get("type") or "").strip().lower() == "direct"
|
||||
and str(row.get("tag") or "").strip().lower() == "direct"
|
||||
for row in outbounds
|
||||
)
|
||||
if not has_direct:
|
||||
outbounds.append({"type": "direct", "tag": "direct"})
|
||||
raw["outbounds"] = outbounds
|
||||
|
||||
inbounds = raw.get("inbounds") or []
|
||||
if not isinstance(inbounds, list):
|
||||
inbounds = []
|
||||
inbound_idx = -1
|
||||
for i, row in enumerate(inbounds):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
tag = str(row.get("tag") or "").strip().lower()
|
||||
t = str(row.get("type") or "").strip().lower()
|
||||
if tag == "socks-in" or t == "socks":
|
||||
inbound_idx = i
|
||||
break
|
||||
inbound = (
|
||||
dict(inbounds[inbound_idx]) if inbound_idx >= 0 and isinstance(inbounds[inbound_idx], dict) else {}
|
||||
)
|
||||
inbound["type"] = str(inbound.get("type") or "socks")
|
||||
inbound["tag"] = str(inbound.get("tag") or "socks-in")
|
||||
inbound["listen"] = str(inbound.get("listen") or "127.0.0.1")
|
||||
inbound["listen_port"] = int(inbound.get("listen_port") or 10808)
|
||||
sniff = bool(self.chk_singbox_vless_sniff.isChecked())
|
||||
inbound["sniff"] = sniff
|
||||
inbound["sniff_override_destination"] = sniff
|
||||
if inbound_idx >= 0:
|
||||
inbounds[inbound_idx] = inbound
|
||||
else:
|
||||
inbounds.insert(0, inbound)
|
||||
raw["inbounds"] = inbounds
|
||||
|
||||
route = raw.get("route") if isinstance(raw.get("route"), dict) else {}
|
||||
route["final"] = str(route.get("final") or "direct")
|
||||
rules = route.get("rules") or []
|
||||
if not isinstance(rules, list):
|
||||
rules = []
|
||||
has_proxy_rule = False
|
||||
for row in rules:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
outbound = str(row.get("outbound") or "").strip().lower()
|
||||
inbound_list = row.get("inbound") or []
|
||||
if not isinstance(inbound_list, list):
|
||||
inbound_list = []
|
||||
inbound_norm = [str(x).strip().lower() for x in inbound_list if str(x).strip()]
|
||||
if outbound == "proxy" and "socks-in" in inbound_norm:
|
||||
has_proxy_rule = True
|
||||
break
|
||||
if not has_proxy_rule:
|
||||
rules.insert(0, {"inbound": ["socks-in"], "outbound": "proxy"})
|
||||
route["rules"] = rules
|
||||
raw["route"] = route
|
||||
return raw
|
||||
|
||||
def _save_singbox_editor_draft(self, client, *, profile_id: str = ""):
|
||||
protocol = self._current_editor_protocol()
|
||||
self._validate_singbox_editor_form()
|
||||
raw_cfg = self._build_singbox_editor_raw_config()
|
||||
name = self.ent_singbox_proto_name.text().strip()
|
||||
enabled = bool(self.chk_singbox_proto_enabled.isChecked())
|
||||
res = self.ctrl.singbox_profile_save_raw_for_client(
|
||||
client,
|
||||
profile_id=profile_id,
|
||||
name=name,
|
||||
enabled=enabled,
|
||||
protocol=protocol,
|
||||
raw_config=raw_cfg,
|
||||
)
|
||||
profile = self.ctrl.singbox_profile_get_for_client(client, profile_id=profile_id)
|
||||
self._singbox_editor_profile_id = str(getattr(profile, "id", "") or "").strip()
|
||||
self._singbox_editor_profile_client_id = str(getattr(client, "id", "") or "").strip()
|
||||
self._singbox_editor_protocol = str(getattr(profile, "protocol", "") or protocol).strip().lower() or protocol
|
||||
self._singbox_editor_source_raw = json.loads(json.dumps(getattr(profile, "raw_config", {}) or {}))
|
||||
return res
|
||||
|
||||
def _sync_selected_singbox_profile_link(self, *, silent: bool = True) -> None:
|
||||
client = self._selected_transport_client()
|
||||
if client is None:
|
||||
return
|
||||
try:
|
||||
preferred_pid = str(getattr(client, "id", "") or "").strip()
|
||||
res = self.ctrl.singbox_profile_ensure_linked(
|
||||
client,
|
||||
preferred_profile_id=preferred_pid,
|
||||
)
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
raise
|
||||
self._append_transport_log(f"[profile] auto-link skipped: {e}")
|
||||
return
|
||||
line = (res.pretty_text or "").strip()
|
||||
if not line:
|
||||
return
|
||||
# Keep noisy "already linked" messages out of normal flow.
|
||||
if "already linked" in line.lower() and silent:
|
||||
return
|
||||
self._append_transport_log(f"[profile] {line}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {line}")
|
||||
Reference in New Issue
Block a user