Files
elmprodvpn/selective-vpn-gui/main_window/ui_tabs_singbox_editor_mixin.py

421 lines
20 KiB
Python

from __future__ import annotations
from PySide6.QtCore import QSize
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QRadioButton,
QSpinBox,
QToolButton,
QVBoxLayout,
QWidget,
)
from main_window.constants import SINGBOX_EDITOR_PROTOCOL_IDS, SINGBOX_EDITOR_PROTOCOL_OPTIONS
class UITabsSingBoxEditorMixin:
def _build_singbox_vless_editor(self, parent_layout: QVBoxLayout) -> None:
grp = QGroupBox("Protocol editor (client)")
self.grp_singbox_proto_editor = grp
lay = QVBoxLayout(grp)
self.lbl_singbox_proto_editor_info = QLabel(
"Client-side fields only. Server billing/traffic/expiry fields are excluded."
)
self.lbl_singbox_proto_editor_info.setStyleSheet("color: gray;")
lay.addWidget(self.lbl_singbox_proto_editor_info)
form = QFormLayout()
self.frm_singbox_proto_form = form
self.ent_singbox_proto_name = QLineEdit()
self.ent_singbox_proto_name.setPlaceholderText("Profile name")
form.addRow("Profile name:", self.ent_singbox_proto_name)
self.chk_singbox_proto_enabled = QCheckBox("Enabled")
self.chk_singbox_proto_enabled.setChecked(True)
form.addRow("Enabled:", self.chk_singbox_proto_enabled)
self.cmb_singbox_proto_protocol = QComboBox()
for label, pid in SINGBOX_EDITOR_PROTOCOL_OPTIONS:
self.cmb_singbox_proto_protocol.addItem(label, pid)
self.cmb_singbox_proto_protocol.currentIndexChanged.connect(
self.on_singbox_vless_editor_changed
)
form.addRow("Protocol:", self.cmb_singbox_proto_protocol)
self.ent_singbox_vless_server = QLineEdit()
self.ent_singbox_vless_server.setPlaceholderText("example.com")
form.addRow("Address:", self.ent_singbox_vless_server)
self.spn_singbox_vless_port = QSpinBox()
self.spn_singbox_vless_port.setRange(1, 65535)
self.spn_singbox_vless_port.setValue(443)
form.addRow("Port:", self.spn_singbox_vless_port)
self.ent_singbox_vless_uuid = QLineEdit()
self.ent_singbox_vless_uuid.setPlaceholderText("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
form.addRow("UUID:", self.ent_singbox_vless_uuid)
self.ent_singbox_proto_password = QLineEdit()
self.ent_singbox_proto_password.setPlaceholderText("password")
form.addRow("Password:", self.ent_singbox_proto_password)
self.cmb_singbox_vless_flow = QComboBox()
self.cmb_singbox_vless_flow.addItem("None", "")
# sing-box v1.12/v1.13 VLESS flow preset; field remains editable for custom/raw values.
self.cmb_singbox_vless_flow.addItem("xtls-rprx-vision", "xtls-rprx-vision")
self.cmb_singbox_vless_flow.setEditable(True)
self.cmb_singbox_vless_flow.setInsertPolicy(QComboBox.NoInsert)
form.addRow("Flow:", self.cmb_singbox_vless_flow)
self.cmb_singbox_vless_packet_encoding = QComboBox()
self.cmb_singbox_vless_packet_encoding.addItem("auto", "")
self.cmb_singbox_vless_packet_encoding.addItem("xudp", "xudp")
form.addRow("Packet encoding:", self.cmb_singbox_vless_packet_encoding)
self.cmb_singbox_ss_method = QComboBox()
self.cmb_singbox_ss_method.setEditable(True)
self.cmb_singbox_ss_method.setInsertPolicy(QComboBox.NoInsert)
for method in (
"aes-128-gcm",
"aes-256-gcm",
"chacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"none",
):
self.cmb_singbox_ss_method.addItem(method, method)
form.addRow("SS method:", self.cmb_singbox_ss_method)
self.ent_singbox_ss_plugin = QLineEdit()
self.ent_singbox_ss_plugin.setPlaceholderText("obfs-local;obfs=http;obfs-host=example.com")
form.addRow("SS plugin:", self.ent_singbox_ss_plugin)
self.spn_singbox_hy2_up_mbps = QSpinBox()
self.spn_singbox_hy2_up_mbps.setRange(0, 100000)
form.addRow("HY2 up mbps:", self.spn_singbox_hy2_up_mbps)
self.spn_singbox_hy2_down_mbps = QSpinBox()
self.spn_singbox_hy2_down_mbps.setRange(0, 100000)
form.addRow("HY2 down mbps:", self.spn_singbox_hy2_down_mbps)
self.ent_singbox_hy2_obfs = QLineEdit()
self.ent_singbox_hy2_obfs.setPlaceholderText("salamander")
form.addRow("HY2 obfs type:", self.ent_singbox_hy2_obfs)
self.ent_singbox_hy2_obfs_password = QLineEdit()
self.ent_singbox_hy2_obfs_password.setPlaceholderText("obfs password")
form.addRow("HY2 obfs password:", self.ent_singbox_hy2_obfs_password)
self.cmb_singbox_tuic_congestion = QComboBox()
self.cmb_singbox_tuic_congestion.setEditable(True)
self.cmb_singbox_tuic_congestion.setInsertPolicy(QComboBox.NoInsert)
self.cmb_singbox_tuic_congestion.addItem("Default", "")
self.cmb_singbox_tuic_congestion.addItem("bbr", "bbr")
self.cmb_singbox_tuic_congestion.addItem("cubic", "cubic")
self.cmb_singbox_tuic_congestion.addItem("new_reno", "new_reno")
form.addRow("TUIC congestion:", self.cmb_singbox_tuic_congestion)
self.cmb_singbox_tuic_udp_mode = QComboBox()
self.cmb_singbox_tuic_udp_mode.addItem("Default", "")
self.cmb_singbox_tuic_udp_mode.addItem("native", "native")
self.cmb_singbox_tuic_udp_mode.addItem("quic", "quic")
form.addRow("TUIC UDP relay:", self.cmb_singbox_tuic_udp_mode)
self.chk_singbox_tuic_zero_rtt = QCheckBox("Enable zero RTT handshake")
form.addRow("TUIC zero RTT:", self.chk_singbox_tuic_zero_rtt)
self.ent_singbox_wg_private_key = QLineEdit()
self.ent_singbox_wg_private_key.setPlaceholderText("wireguard private key")
self.ent_singbox_wg_private_key.setEchoMode(QLineEdit.PasswordEchoOnEdit)
form.addRow("WG private key:", self.ent_singbox_wg_private_key)
self.ent_singbox_wg_peer_public_key = QLineEdit()
self.ent_singbox_wg_peer_public_key.setPlaceholderText("peer public key")
self.ent_singbox_wg_peer_public_key.setEchoMode(QLineEdit.PasswordEchoOnEdit)
form.addRow("WG peer public key:", self.ent_singbox_wg_peer_public_key)
self.ent_singbox_wg_psk = QLineEdit()
self.ent_singbox_wg_psk.setPlaceholderText("pre-shared key (optional)")
self.ent_singbox_wg_psk.setEchoMode(QLineEdit.PasswordEchoOnEdit)
form.addRow("WG pre-shared key:", self.ent_singbox_wg_psk)
self.ent_singbox_wg_local_address = QLineEdit()
self.ent_singbox_wg_local_address.setPlaceholderText("10.0.0.2/32,fd00::2/128")
form.addRow("WG local address:", self.ent_singbox_wg_local_address)
self.ent_singbox_wg_reserved = QLineEdit()
self.ent_singbox_wg_reserved.setPlaceholderText("0,0,0 (optional)")
form.addRow("WG reserved:", self.ent_singbox_wg_reserved)
self.spn_singbox_wg_mtu = QSpinBox()
self.spn_singbox_wg_mtu.setRange(0, 9200)
form.addRow("WG MTU:", self.spn_singbox_wg_mtu)
self.cmb_singbox_vless_transport = QComboBox()
self.cmb_singbox_vless_transport.addItem("TCP (RAW)", "tcp")
self.cmb_singbox_vless_transport.addItem("WebSocket", "ws")
self.cmb_singbox_vless_transport.addItem("gRPC", "grpc")
self.cmb_singbox_vless_transport.addItem("HTTP", "http")
self.cmb_singbox_vless_transport.addItem("HTTP Upgrade", "httpupgrade")
self.cmb_singbox_vless_transport.addItem("QUIC", "quic")
self.cmb_singbox_vless_transport.currentIndexChanged.connect(
self.on_singbox_vless_editor_changed
)
form.addRow("Transport:", self.cmb_singbox_vless_transport)
self.ent_singbox_vless_path = QLineEdit()
self.ent_singbox_vless_path.setPlaceholderText("/")
form.addRow("Transport path:", self.ent_singbox_vless_path)
self.ent_singbox_vless_grpc_service = QLineEdit()
self.ent_singbox_vless_grpc_service.setPlaceholderText("service-name")
form.addRow("gRPC service:", self.ent_singbox_vless_grpc_service)
self.cmb_singbox_vless_security = QComboBox()
self.cmb_singbox_vless_security.addItem("None", "none")
self.cmb_singbox_vless_security.addItem("TLS", "tls")
self.cmb_singbox_vless_security.addItem("Reality", "reality")
self.cmb_singbox_vless_security.currentIndexChanged.connect(
self.on_singbox_vless_editor_changed
)
form.addRow("Security:", self.cmb_singbox_vless_security)
self.ent_singbox_vless_sni = QLineEdit()
self.ent_singbox_vless_sni.setPlaceholderText("www.example.com")
form.addRow("SNI:", self.ent_singbox_vless_sni)
self.ent_singbox_tls_alpn = QLineEdit()
self.ent_singbox_tls_alpn.setPlaceholderText("h2,http/1.1")
form.addRow("TLS ALPN:", self.ent_singbox_tls_alpn)
self.cmb_singbox_vless_utls_fp = QComboBox()
self.cmb_singbox_vless_utls_fp.addItem("Default", "")
self.cmb_singbox_vless_utls_fp.addItem("chrome", "chrome")
self.cmb_singbox_vless_utls_fp.addItem("firefox", "firefox")
self.cmb_singbox_vless_utls_fp.addItem("safari", "safari")
self.cmb_singbox_vless_utls_fp.addItem("edge", "edge")
form.addRow("uTLS fingerprint:", self.cmb_singbox_vless_utls_fp)
self.ent_singbox_vless_reality_pk = QLineEdit()
self.ent_singbox_vless_reality_pk.setPlaceholderText("Reality public key")
form.addRow("Reality public key:", self.ent_singbox_vless_reality_pk)
self.ent_singbox_vless_reality_sid = QLineEdit()
self.ent_singbox_vless_reality_sid.setPlaceholderText("short_id")
form.addRow("Reality short id:", self.ent_singbox_vless_reality_sid)
self.chk_singbox_vless_insecure = QCheckBox("Allow insecure TLS")
form.addRow("TLS insecure:", self.chk_singbox_vless_insecure)
self.chk_singbox_vless_sniff = QCheckBox("Enable sniffing for local inbound")
self.chk_singbox_vless_sniff.setChecked(True)
form.addRow("Sniffing:", self.chk_singbox_vless_sniff)
lay.addLayout(form)
wg_helpers = QHBoxLayout()
self.btn_singbox_wg_paste_private = QToolButton()
self.btn_singbox_wg_paste_private.setText("Paste private")
self.btn_singbox_wg_paste_private.clicked.connect(
lambda: self._paste_line_edit_from_clipboard(self.ent_singbox_wg_private_key)
)
wg_helpers.addWidget(self.btn_singbox_wg_paste_private)
self.btn_singbox_wg_copy_private = QToolButton()
self.btn_singbox_wg_copy_private.setText("Copy private")
self.btn_singbox_wg_copy_private.clicked.connect(
lambda: self._copy_line_edit_to_clipboard(self.ent_singbox_wg_private_key)
)
wg_helpers.addWidget(self.btn_singbox_wg_copy_private)
self.btn_singbox_wg_paste_peer = QToolButton()
self.btn_singbox_wg_paste_peer.setText("Paste peer")
self.btn_singbox_wg_paste_peer.clicked.connect(
lambda: self._paste_line_edit_from_clipboard(self.ent_singbox_wg_peer_public_key)
)
wg_helpers.addWidget(self.btn_singbox_wg_paste_peer)
self.btn_singbox_wg_copy_peer = QToolButton()
self.btn_singbox_wg_copy_peer.setText("Copy peer")
self.btn_singbox_wg_copy_peer.clicked.connect(
lambda: self._copy_line_edit_to_clipboard(self.ent_singbox_wg_peer_public_key)
)
wg_helpers.addWidget(self.btn_singbox_wg_copy_peer)
self.btn_singbox_wg_paste_psk = QToolButton()
self.btn_singbox_wg_paste_psk.setText("Paste PSK")
self.btn_singbox_wg_paste_psk.clicked.connect(
lambda: self._paste_line_edit_from_clipboard(self.ent_singbox_wg_psk)
)
wg_helpers.addWidget(self.btn_singbox_wg_paste_psk)
self.btn_singbox_wg_copy_psk = QToolButton()
self.btn_singbox_wg_copy_psk.setText("Copy PSK")
self.btn_singbox_wg_copy_psk.clicked.connect(
lambda: self._copy_line_edit_to_clipboard(self.ent_singbox_wg_psk)
)
wg_helpers.addWidget(self.btn_singbox_wg_copy_psk)
wg_helpers.addStretch(1)
self.wdg_singbox_wg_key_helpers = QWidget()
self.wdg_singbox_wg_key_helpers.setLayout(wg_helpers)
lay.addWidget(self.wdg_singbox_wg_key_helpers)
self.lbl_singbox_proto_guardrails = QLabel("Guardrails: address/port/uuid required")
self.lbl_singbox_proto_guardrails.setStyleSheet("color: gray;")
lay.addWidget(self.lbl_singbox_proto_guardrails)
parent_layout.addWidget(grp)
self.on_singbox_vless_editor_changed()
def _set_proto_form_row_visible(self, field: QWidget, visible: bool) -> None:
field.setVisible(visible)
label = None
form = getattr(self, "frm_singbox_proto_form", None)
if form is not None:
try:
label = form.labelForField(field)
except Exception:
label = None
if label is not None:
label.setVisible(visible)
def _copy_line_edit_to_clipboard(self, field: QLineEdit) -> None:
txt = str(field.text() or "").strip()
if txt:
QApplication.clipboard().setText(txt)
def _paste_line_edit_from_clipboard(self, field: QLineEdit) -> None:
txt = str(QApplication.clipboard().text() or "").strip()
field.setText(txt)
def _current_editor_protocol(self) -> str:
return str(self.cmb_singbox_proto_protocol.currentData() or "vless").strip().lower() or "vless"
def _is_supported_editor_protocol(self, protocol: str) -> bool:
return str(protocol or "").strip().lower() in SINGBOX_EDITOR_PROTOCOL_IDS
def on_singbox_vless_editor_changed(self, _index: int = 0) -> None:
protocol = self._current_editor_protocol()
self._singbox_editor_protocol = protocol
transport = str(self.cmb_singbox_vless_transport.currentData() or "tcp").strip().lower()
security = str(self.cmb_singbox_vless_security.currentData() or "none").strip().lower()
if protocol == "vless":
self.cmb_singbox_vless_security.setEnabled(True)
elif protocol == "trojan":
if security == "reality":
idx = self.cmb_singbox_vless_security.findData("tls")
self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 1)
security = "tls"
self.cmb_singbox_vless_security.setEnabled(True)
elif protocol in ("hysteria2", "tuic"):
idx = self.cmb_singbox_vless_security.findData("tls")
self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 1)
security = "tls"
self.cmb_singbox_vless_security.setEnabled(False)
elif protocol == "wireguard":
idx = self.cmb_singbox_vless_security.findData("none")
self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 0)
security = "none"
self.cmb_singbox_vless_security.setEnabled(False)
else:
idx = self.cmb_singbox_vless_security.findData("none")
self.cmb_singbox_vless_security.setCurrentIndex(idx if idx >= 0 else 0)
security = "none"
self.cmb_singbox_vless_security.setEnabled(False)
path_needed = transport in ("ws", "http", "httpupgrade")
grpc_needed = transport == "grpc"
transport_supported = protocol in ("vless", "trojan")
self.cmb_singbox_vless_transport.setEnabled(transport_supported)
self.ent_singbox_vless_path.setEnabled(transport_supported and path_needed)
self.ent_singbox_vless_grpc_service.setEnabled(transport_supported and grpc_needed)
tls_like = security in ("tls", "reality")
reality = security == "reality"
self.ent_singbox_vless_sni.setEnabled(tls_like)
self.ent_singbox_tls_alpn.setEnabled(tls_like)
self.cmb_singbox_vless_utls_fp.setEnabled(tls_like)
self.chk_singbox_vless_insecure.setEnabled(tls_like)
self.ent_singbox_vless_reality_pk.setEnabled(reality)
self.ent_singbox_vless_reality_sid.setEnabled(reality)
show_vless_auth = protocol == "vless"
show_password = protocol in ("trojan", "shadowsocks", "hysteria2", "tuic")
show_ss = protocol == "shadowsocks"
show_hy2 = protocol == "hysteria2"
show_tuic = protocol == "tuic"
show_wg = protocol == "wireguard"
self._set_proto_form_row_visible(self.ent_singbox_vless_uuid, show_vless_auth or show_tuic)
self._set_proto_form_row_visible(self.ent_singbox_proto_password, show_password)
self._set_proto_form_row_visible(self.cmb_singbox_vless_flow, show_vless_auth)
self._set_proto_form_row_visible(self.cmb_singbox_vless_packet_encoding, show_vless_auth)
self._set_proto_form_row_visible(self.cmb_singbox_ss_method, show_ss)
self._set_proto_form_row_visible(self.ent_singbox_ss_plugin, show_ss)
self._set_proto_form_row_visible(self.spn_singbox_hy2_up_mbps, show_hy2)
self._set_proto_form_row_visible(self.spn_singbox_hy2_down_mbps, show_hy2)
self._set_proto_form_row_visible(self.ent_singbox_hy2_obfs, show_hy2)
self._set_proto_form_row_visible(self.ent_singbox_hy2_obfs_password, show_hy2)
self._set_proto_form_row_visible(self.cmb_singbox_tuic_congestion, show_tuic)
self._set_proto_form_row_visible(self.cmb_singbox_tuic_udp_mode, show_tuic)
self._set_proto_form_row_visible(self.chk_singbox_tuic_zero_rtt, show_tuic)
self._set_proto_form_row_visible(self.ent_singbox_wg_private_key, show_wg)
self._set_proto_form_row_visible(self.ent_singbox_wg_peer_public_key, show_wg)
self._set_proto_form_row_visible(self.ent_singbox_wg_psk, show_wg)
self._set_proto_form_row_visible(self.ent_singbox_wg_local_address, show_wg)
self._set_proto_form_row_visible(self.ent_singbox_wg_reserved, show_wg)
self._set_proto_form_row_visible(self.spn_singbox_wg_mtu, show_wg)
self.wdg_singbox_wg_key_helpers.setVisible(show_wg)
self._set_proto_form_row_visible(self.cmb_singbox_vless_transport, transport_supported)
self._set_proto_form_row_visible(self.ent_singbox_vless_path, transport_supported)
self._set_proto_form_row_visible(self.ent_singbox_vless_grpc_service, transport_supported)
self._set_proto_form_row_visible(self.cmb_singbox_vless_security, protocol not in ("shadowsocks", "wireguard"))
self._set_proto_form_row_visible(self.ent_singbox_vless_sni, tls_like)
self._set_proto_form_row_visible(self.ent_singbox_tls_alpn, tls_like)
self._set_proto_form_row_visible(self.cmb_singbox_vless_utls_fp, tls_like)
self._set_proto_form_row_visible(self.chk_singbox_vless_insecure, tls_like)
self._set_proto_form_row_visible(self.ent_singbox_vless_reality_pk, reality)
self._set_proto_form_row_visible(self.ent_singbox_vless_reality_sid, reality)
tips = ["Guardrails:"]
if protocol == "vless":
tips.append("address/port/uuid required")
elif protocol == "trojan":
tips.append("address/port/password required")
elif protocol == "shadowsocks":
tips.append("address/port/SS method/password required")
elif protocol == "hysteria2":
tips.append("address/port/password required")
elif protocol == "tuic":
tips.append("address/port/uuid/password required")
elif protocol == "wireguard":
tips.append("address/port/private_key/peer_public_key/local_address required")
if reality:
tips.append("reality.public_key is required")
if transport_supported and grpc_needed:
tips.append("gRPC service is required")
if transport_supported and path_needed:
tips.append("transport path is required")
self.lbl_singbox_proto_guardrails.setText(" | ".join(tips))