feat(server): support per-user LAN isolation with auth_user and custom route rules
This commit is contained in:
@@ -51,6 +51,12 @@ Defines an individual manual proxy outbound node.
|
||||
| `sid` | `string` | Conditional | Short ID for VLESS Reality (hex string). |
|
||||
| `fingerprint` | `string` | No | TLS fingerprint: `chrome`, `firefox`, `safari`, `randomized`. |
|
||||
| `method` | `string` | Conditional | Cipher method for Shadowsocks (e.g. `2022-blake3-aes-128-gcm`). |
|
||||
| `routing_mode` | `string` | No | Routing mode: `rules` (default), `direct`, `section`. |
|
||||
| `routing_section` | `string` | No | Bound routing section when `routing_mode = 'section'`. |
|
||||
| `isolate_lan_for_users` | `boolean` | No | Restrict access to private/LAN IP addresses (`1` / `0`). |
|
||||
| `isolated_users` | `list string` | No | Specific usernames (`auth_user`) restricted from LAN. If empty, restricts all users of the inbound. |
|
||||
| `isolated_subnets` | `list string` | No | Optional custom subnets to block for isolated users (defaults to `ip_is_private: true`). |
|
||||
| `custom_route_rules` | `list string` | No | Raw JSON sing-box route rule objects injected before general routing rules. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2720,6 +2720,29 @@ function createServerContent(section, options = {}) {
|
||||
return loadRoutingSectionChoices(this);
|
||||
};
|
||||
|
||||
o = section.option(
|
||||
form.Flag,
|
||||
"isolate_lan_for_users",
|
||||
_("Restrict LAN access for specific users"),
|
||||
_(
|
||||
"Blocks access to private/LAN IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) for selected users before applying routing mode.",
|
||||
),
|
||||
);
|
||||
o.modalonly = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = section.option(
|
||||
form.DynamicList,
|
||||
"isolated_users",
|
||||
_("Restricted users (auth_user)"),
|
||||
_(
|
||||
"User names (e.g. from VLESS/Trojan/SS/JSON inbound users) that are forbidden from reaching LAN subnets. Leave empty to restrict all users on this inbound.",
|
||||
),
|
||||
);
|
||||
o.depends("isolate_lan_for_users", "1");
|
||||
o.modalonly = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = section.option(
|
||||
form.TextValue,
|
||||
"inbound_json",
|
||||
|
||||
Binary file not shown.
@@ -5146,3 +5146,16 @@ msgstr "Не удалось автоматически применить стр
|
||||
|
||||
msgid "History cleared"
|
||||
msgstr "История очищена"
|
||||
|
||||
msgid "Restrict LAN access for specific users"
|
||||
msgstr "Ограничить доступ к локальной сети для отдельных пользователей"
|
||||
|
||||
msgid "Blocks access to private/LAN IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) for selected users before applying routing mode."
|
||||
msgstr "Блокирует доступ к приватным/локальным IP-адресам (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 и др.) для указанных пользователей перед применением режима маршрутизации."
|
||||
|
||||
msgid "Restricted users (auth_user)"
|
||||
msgstr "Изолированные пользователи (auth_user)"
|
||||
|
||||
msgid "User names (e.g. from VLESS/Trojan/SS/JSON inbound users) that are forbidden from reaching LAN subnets. Leave empty to restrict all users on this inbound."
|
||||
msgstr "Имена пользователей (например, из пользователей VLESS/Trojan/SS/JSON inbound), которым запрещен доступ к подсетям LAN. Оставьте пустым, чтобы ограничить всех пользователей данного inbound."
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7686,3 +7686,20 @@ msgstr ""
|
||||
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||
msgid "History cleared"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/server.js
|
||||
msgid "Restrict LAN access for specific users"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/server.js
|
||||
msgid "Blocks access to private/LAN IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) for selected users before applying routing mode."
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/server.js
|
||||
msgid "Restricted users (auth_user)"
|
||||
msgstr ""
|
||||
|
||||
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/server.js
|
||||
msgid "User names (e.g. from VLESS/Trojan/SS/JSON inbound users) that are forbidden from reaching LAN subnets. Leave empty to restrict all users on this inbound."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -173,6 +173,10 @@ config settings 'settings'
|
||||
# option reality_short_id '5a'
|
||||
# option reality_max_time_difference '1m'
|
||||
# option transport 'tcp'
|
||||
# # Restrict LAN / private network access for specific guest users:
|
||||
# #option isolate_lan_for_users '1'
|
||||
# #list isolated_users 'friend-1'
|
||||
# #list isolated_users 'friend-2'
|
||||
# # sing-box-extended only:
|
||||
# #option transport 'xhttp'
|
||||
# #option transport_path '/xhttp'
|
||||
|
||||
@@ -1548,6 +1548,40 @@ function add_server_routes(config, servers, sections) {
|
||||
runtime_servers.add_sniff_rule(config, server);
|
||||
|
||||
let inbound = runtime_constants.server_inbound_tag(server[".name"]);
|
||||
|
||||
let isolate_lan = bool_option(server, "isolate_lan_for_users", false) || bool_option(server, "block_private_for_users", false);
|
||||
let isolated_users = list_option(server, "isolated_users");
|
||||
if (length(isolated_users) == 0)
|
||||
isolated_users = list_option(server, "blocked_users");
|
||||
|
||||
if (isolate_lan) {
|
||||
let isolate_rule = {
|
||||
action: "reject",
|
||||
inbound: inbound,
|
||||
ip_is_private: true
|
||||
};
|
||||
if (length(isolated_users) > 0)
|
||||
isolate_rule.auth_user = isolated_users;
|
||||
let isolated_subnets = list_option(server, "isolated_subnets");
|
||||
if (length(isolated_subnets) > 0)
|
||||
isolate_rule.ip_cidr = isolated_subnets;
|
||||
push(config.route.rules, isolate_rule);
|
||||
}
|
||||
|
||||
let custom_rules = list_option(server, "custom_route_rules");
|
||||
if (length(custom_rules) == 0)
|
||||
custom_rules = list_option(server, "custom_rules_json");
|
||||
for (let rule_str in custom_rules) {
|
||||
try {
|
||||
let r = json(rule_str);
|
||||
if (type(r) == "object") {
|
||||
if (!r.inbound)
|
||||
r.inbound = inbound;
|
||||
push(config.route.rules, r);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
let routing_mode = option(server, "routing_mode", "rules");
|
||||
if (routing_mode == "rules") {
|
||||
runtime_servers.clone_rules_for_inbound(
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="${TACHYON_LIB:-$ROOT_DIR/tachyon/files/usr/lib}"
|
||||
if [ ! -d "$TACHYON_LIB" ]; then
|
||||
TACHYON_LIB="/usr/lib/tachyon"
|
||||
fi
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
fixture_with_server() {
|
||||
cat >"$WORK_DIR/fixture.json" <<JSON
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"enabled": "1"
|
||||
},
|
||||
"server": [
|
||||
$1
|
||||
],
|
||||
"section": [
|
||||
{
|
||||
".name": "default_direct",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "bypass"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
generate() {
|
||||
mkdir -p "$WORK_DIR/out.section-cache"
|
||||
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/singbox/generator.uc" generate-config-fixture \
|
||||
"$WORK_DIR/fixture.json" "$WORK_DIR/singbox.json" "127.0.0.1" 0 0
|
||||
}
|
||||
|
||||
# 1. Test isolate_lan_for_users with specific isolated_users list
|
||||
fixture_with_server '{
|
||||
".name": "my_vless_server",
|
||||
".type": "server",
|
||||
"enabled": "1",
|
||||
"protocol": "vless",
|
||||
"listen_port": "443",
|
||||
"server_uuid": "00000000-0000-0000-0000-000000000000",
|
||||
"routing_mode": "direct",
|
||||
"isolate_lan_for_users": "1",
|
||||
"isolated_users": ["friend-1", "friend-2"]
|
||||
}'
|
||||
generate
|
||||
|
||||
# Verify reject rule exists with auth_user and ip_is_private
|
||||
grep -q '"action": "reject"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected action reject rule for isolated users"
|
||||
grep -q '"inbound": "server-my_vless_server-in"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected inbound tag on reject rule"
|
||||
grep -q '"ip_is_private": true' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected ip_is_private on reject rule"
|
||||
grep -q '"auth_user": \[' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected auth_user array on reject rule"
|
||||
grep -q '"friend-1"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected friend-1 in auth_user"
|
||||
grep -q '"friend-2"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected friend-2 in auth_user"
|
||||
|
||||
# 2. Test isolate_lan_for_users without specific users (blocks all users on that inbound)
|
||||
fixture_with_server '{
|
||||
".name": "guest_vless",
|
||||
".type": "server",
|
||||
"enabled": "1",
|
||||
"protocol": "vless",
|
||||
"listen_port": "443",
|
||||
"server_uuid": "00000000-0000-0000-0000-000000000000",
|
||||
"routing_mode": "direct",
|
||||
"isolate_lan_for_users": "1"
|
||||
}'
|
||||
generate
|
||||
|
||||
grep -q '"action": "reject"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected action reject rule for guest_vless"
|
||||
grep -q '"inbound": "server-guest_vless-in"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected inbound tag on reject rule for guest_vless"
|
||||
grep -q '"ip_is_private": true' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected ip_is_private on reject rule for guest_vless"
|
||||
|
||||
# 3. Test custom isolated_subnets and block_private_for_users alias
|
||||
fixture_with_server '{
|
||||
".name": "custom_subnet_server",
|
||||
".type": "server",
|
||||
"enabled": "1",
|
||||
"protocol": "vless",
|
||||
"listen_port": "443",
|
||||
"server_uuid": "00000000-0000-0000-0000-000000000000",
|
||||
"routing_mode": "direct",
|
||||
"block_private_for_users": "1",
|
||||
"blocked_users": ["guest"],
|
||||
"isolated_subnets": ["192.168.1.0/24", "10.10.0.0/16"]
|
||||
}'
|
||||
generate
|
||||
|
||||
grep -q '"action": "reject"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected reject rule for custom_subnet_server"
|
||||
grep -q '"192.168.1.0/24"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected custom subnet 192.168.1.0/24 in reject rule"
|
||||
grep -q '"guest"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected guest in auth_user"
|
||||
|
||||
# 4. Test custom_route_rules injection
|
||||
fixture_with_server '{
|
||||
".name": "custom_rules_server",
|
||||
".type": "server",
|
||||
"enabled": "1",
|
||||
"protocol": "vless",
|
||||
"listen_port": "443",
|
||||
"server_uuid": "00000000-0000-0000-0000-000000000000",
|
||||
"routing_mode": "direct",
|
||||
"custom_route_rules": ["{\"action\":\"reject\",\"domain\":[\"blocked.local\"]}"]
|
||||
}'
|
||||
generate
|
||||
|
||||
grep -q '"domain": \[' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected custom domain rule"
|
||||
grep -q '"blocked.local"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected blocked.local domain in rules"
|
||||
grep -q '"inbound": "server-custom_rules_server-in"' "$WORK_DIR/singbox.json" ||
|
||||
fail "expected auto-assigned inbound tag on custom route rule"
|
||||
|
||||
printf 'PASS: server isolated users and custom route rules tests passed\n'
|
||||
Reference in New Issue
Block a user