Add umedbazarov/ruh-vpn: VPN/proxy manager for sing-box (#304)
* 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>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, Optional
|
||||
|
||||
from backend.models.server import RoutingRule, Server, Settings, StatusInfo
|
||||
|
||||
|
||||
LogEntry = tuple[float, str, str] # (timestamp, level, message)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppState:
|
||||
servers: list[Server] = field(default_factory=list)
|
||||
rules: list[RoutingRule] = field(default_factory=list)
|
||||
settings: Settings = field(default_factory=Settings)
|
||||
status: StatusInfo = field(default_factory=StatusInfo)
|
||||
pids: dict[str, int] = field(default_factory=dict)
|
||||
logs: deque[LogEntry] = field(default_factory=lambda: deque(maxlen=500))
|
||||
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
|
||||
status_listeners: list[Callable[[StatusInfo], None]] = field(default_factory=list)
|
||||
server_list_listeners: list[Callable[[], None]] = field(default_factory=list)
|
||||
log_listeners: list[Callable[[str, str], None]] = field(default_factory=list)
|
||||
|
||||
def get_server(self, server_id: str) -> Optional[Server]:
|
||||
for s in self.servers:
|
||||
if s.id == server_id:
|
||||
return s
|
||||
return None
|
||||
|
||||
def emit_status(self) -> None:
|
||||
for cb in list(self.status_listeners):
|
||||
try:
|
||||
cb(self.status)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def emit_server_list(self) -> None:
|
||||
for cb in list(self.server_list_listeners):
|
||||
try:
|
||||
cb()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def emit_log(self, level: str, message: str) -> None:
|
||||
import time
|
||||
self.logs.append((time.time(), level, message))
|
||||
for cb in list(self.log_listeners):
|
||||
try:
|
||||
cb(level, message)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user