Files
community-plugins/ruh-vpn/backend/storage/subscriptions.py
T
0733efd186 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>
2026-08-09 21:03:02 -04:00

38 lines
883 B
Python

"""Persistence for subscription metadata."""
from __future__ import annotations
import json
import os
import aiofiles
from backend.paths import DATA_DIR, ensure_private_dir, protect_file
SUBS_FILE = DATA_DIR / "subscriptions.json"
def ensure_dirs() -> None:
ensure_private_dir(DATA_DIR)
async def load_subscriptions() -> list[dict]:
ensure_dirs()
if not SUBS_FILE.exists():
return []
try:
async with aiofiles.open(SUBS_FILE, "r") as f:
raw = await f.read()
return json.loads(raw or "[]")
except (json.JSONDecodeError, ValueError):
return []
async def save_subscriptions(subs: list[dict]) -> None:
ensure_dirs()
tmp = SUBS_FILE.with_suffix(".json.tmp")
async with aiofiles.open(tmp, "w") as f:
await f.write(json.dumps(subs, indent=2))
protect_file(tmp)
os.replace(tmp, SUBS_FILE)