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,64 @@
|
||||
"""build_ruleset must never let untrusted text into the nft program.
|
||||
|
||||
The ruleset text is executed by nft with root privileges, and server
|
||||
addresses can come from untrusted subscriptions.
|
||||
"""
|
||||
|
||||
from backend.service.kill_switch import TABLE_NAME, build_ruleset
|
||||
|
||||
|
||||
def test_ipv4_with_port():
|
||||
rs = build_ruleset(["203.0.113.7"], 443)
|
||||
assert " ip daddr 203.0.113.7 tcp dport 443 accept\n" in rs
|
||||
assert f"table inet {TABLE_NAME}" in rs
|
||||
|
||||
|
||||
def test_ipv6_goes_to_ip6_rule():
|
||||
rs = build_ruleset(["2001:db8::1"], 8443)
|
||||
assert " ip6 daddr 2001:db8::1 tcp dport 8443 accept\n" in rs
|
||||
assert "ip daddr 2001:db8::1" not in rs
|
||||
|
||||
|
||||
def test_ip_without_port():
|
||||
rs = build_ruleset(["203.0.113.7"], None)
|
||||
assert " ip daddr 203.0.113.7 accept\n" in rs
|
||||
|
||||
|
||||
def test_multiple_ips():
|
||||
rs = build_ruleset(["203.0.113.7", "2001:db8::1"], 443)
|
||||
assert "ip daddr 203.0.113.7 tcp dport 443 accept" in rs
|
||||
assert "ip6 daddr 2001:db8::1 tcp dport 443 accept" in rs
|
||||
|
||||
|
||||
def test_domain_is_dropped():
|
||||
rs = build_ruleset(["evil.example.com"], 443)
|
||||
assert "evil.example.com" not in rs
|
||||
|
||||
|
||||
def test_newline_injection_is_dropped():
|
||||
payload = "1.2.3.4\ndelete table inet filter\n"
|
||||
rs = build_ruleset([payload], 443)
|
||||
assert "delete table inet filter" not in rs
|
||||
# and the payload as a whole must not appear either
|
||||
assert payload not in rs
|
||||
|
||||
|
||||
def test_non_canonical_ip_is_reemitted_canonically():
|
||||
rs = build_ruleset(["2001:0DB8:0000:0000:0000:0000:0000:0001"], None)
|
||||
assert "ip6 daddr 2001:db8::1 accept" in rs
|
||||
|
||||
|
||||
def test_ports_are_coerced_to_int():
|
||||
rs = build_ruleset(["1.2.3.4"], "443")
|
||||
assert "tcp dport 443 accept" in rs
|
||||
rs2 = build_ruleset(None, None, extra_allow_tcp=["11080", 11081])
|
||||
assert "tcp dport { 11080, 11081 } accept" in rs2
|
||||
|
||||
|
||||
def test_no_server_lines_without_ips():
|
||||
rs = build_ruleset(None, None)
|
||||
assert "daddr" not in rs.split("chain input")[0].replace(
|
||||
"ip daddr 192.168.0.0/16 accept", ""
|
||||
).replace("ip daddr 10.0.0.0/8 accept", "").replace(
|
||||
"ip daddr 172.16.0.0/12 accept", ""
|
||||
)
|
||||
Reference in New Issue
Block a user