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). |
|
| `sid` | `string` | Conditional | Short ID for VLESS Reality (hex string). |
|
||||||
| `fingerprint` | `string` | No | TLS fingerprint: `chrome`, `firefox`, `safari`, `randomized`. |
|
| `fingerprint` | `string` | No | TLS fingerprint: `chrome`, `firefox`, `safari`, `randomized`. |
|
||||||
| `method` | `string` | Conditional | Cipher method for Shadowsocks (e.g. `2022-blake3-aes-128-gcm`). |
|
| `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);
|
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(
|
o = section.option(
|
||||||
form.TextValue,
|
form.TextValue,
|
||||||
"inbound_json",
|
"inbound_json",
|
||||||
|
|||||||
Binary file not shown.
@@ -5146,3 +5146,16 @@ msgstr "Не удалось автоматически применить стр
|
|||||||
|
|
||||||
msgid "History cleared"
|
msgid "History cleared"
|
||||||
msgstr "История очищена"
|
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
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
msgid "History cleared"
|
msgid "History cleared"
|
||||||
msgstr ""
|
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_short_id '5a'
|
||||||
# option reality_max_time_difference '1m'
|
# option reality_max_time_difference '1m'
|
||||||
# option transport 'tcp'
|
# 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:
|
# # sing-box-extended only:
|
||||||
# #option transport 'xhttp'
|
# #option transport 'xhttp'
|
||||||
# #option transport_path '/xhttp'
|
# #option transport_path '/xhttp'
|
||||||
|
|||||||
@@ -1548,6 +1548,40 @@ function add_server_routes(config, servers, sections) {
|
|||||||
runtime_servers.add_sniff_rule(config, server);
|
runtime_servers.add_sniff_rule(config, server);
|
||||||
|
|
||||||
let inbound = runtime_constants.server_inbound_tag(server[".name"]);
|
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");
|
let routing_mode = option(server, "routing_mode", "rules");
|
||||||
if (routing_mode == "rules") {
|
if (routing_mode == "rules") {
|
||||||
runtime_servers.clone_rules_for_inbound(
|
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