* Add umedbazarov/ruh-vpn: VPN/proxy manager for sing-box New community plugin: bar widget, panel, service and control-center shortcut for managing SSH, VLESS, VMess, Shadowsocks and SOCKS5 connections through sing-box, with routing presets, custom rules, system-proxy/TUN modes and a kill switch. The bundled Python backend serves a loopback control API protected by a per-launch bearer token. * Address review: sanitize kill-switch ruleset, scope TUN capability, fix mux error path, disclose DNS - kill switch: only pre-resolved, canonicalized literal IPs enter the nft ruleset; domains are resolved first and anything unparseable is dropped, so subscription-supplied addresses can no longer inject nft syntax - TUN: CAP_NET_ADMIN is granted to a plugin-private copy of sing-box in a 0700 directory instead of the shared system binary; the copy is refreshed (clearing the cap) when the system binary changes, and the legacy grant on the shared binary is removed in the same polkit prompt - fix NameError in the mux startup failure path (undefined mux_name) that hid the log tail and skipped teardown - README: disclose plain-UDP DNS endpoints (8.8.8.8 via tunnel, 223.5.5.5 direct in rules mode) alongside the TUN DoH endpoint --------- Co-authored-by: Umedjon Bazarov <170195993+UmedjonBA@users.noreply.github.com>
46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
import os
|
|
from types import SimpleNamespace
|
|
|
|
from backend.http.control import ControlServer
|
|
|
|
|
|
def _control(token: str = "right-token", token_file=None) -> ControlServer:
|
|
state = SimpleNamespace(
|
|
status_listeners=[], server_list_listeners=[], log_listeners=[]
|
|
)
|
|
service = SimpleNamespace(state=state, add_traffic_listener=lambda cb: None)
|
|
return ControlServer(service, token=token, token_file=token_file)
|
|
|
|
|
|
def _request(header: str | None):
|
|
headers = {} if header is None else {"Authorization": header}
|
|
return SimpleNamespace(headers=headers)
|
|
|
|
|
|
def test_missing_header_rejected():
|
|
assert not _control()._authorized(_request(None))
|
|
|
|
|
|
def test_wrong_token_rejected():
|
|
assert not _control()._authorized(_request("Bearer wrong"))
|
|
|
|
|
|
def test_wrong_scheme_rejected():
|
|
assert not _control()._authorized(_request("Basic right-token"))
|
|
|
|
|
|
def test_correct_token_accepted():
|
|
assert _control()._authorized(_request("Bearer right-token"))
|
|
|
|
|
|
def test_empty_configured_token_rejects_everything():
|
|
# A backend that somehow starts without a token must fail closed.
|
|
assert not _control(token="")._authorized(_request("Bearer "))
|
|
|
|
|
|
def test_token_file_written_0600(tmp_path):
|
|
path = tmp_path / "control.token"
|
|
_control(token="secret", token_file=path)._publish_token()
|
|
assert path.read_text().strip() == "secret"
|
|
assert (os.stat(path).st_mode & 0o777) == 0o600
|