634 lines
29 KiB
Python
634 lines
29 KiB
Python
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}")
|