Files
community-plugins/ruh-vpn/tests/test_kill_switch.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

65 lines
2.0 KiB
Python

"""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", ""
)