Add source CIDR bypass for TProxy

This commit is contained in:
2026-05-24 13:26:03 +03:00
parent 12ad0c8b78
commit 0092ec4cde
4 changed files with 97 additions and 0 deletions

View File

@@ -75,6 +75,7 @@ docker compose -f docker-compose.client.yml restart
iptables mangle PREROUTING → цепочка VPN_PROXY_TPROXY
├─ source bypass CIDR → RETURN ← устройства мимо sing-box
├─ ipset vpn_direct_bypass (dst IP) → RETURN ← опциональный bypass-кэш
├─ приватные CIDR (RFC1918, ...) → RETURN
└─ TCP/UDP → TPROXY :7895
@@ -128,6 +129,7 @@ ip route replace local 0.0.0.0/0 dev lo table 100
iptables -t mangle -N VPN_PROXY_TPROXY
-m addrtype --dst-type LOCAL → RETURN # ответы самого sing-box
-m mark --mark 1 → RETURN # уже помеченные пакеты
-s 192.168.50.25/32 → RETURN # TPROXY_BYPASS_SOURCE_CIDRS
-m set --match-set vpn_direct_bypass → RETURN # только если DIRECT_BYPASS_CACHE=true
-d 10.0.0.0/8, 192.168.0.0/16, ... → RETURN # приватные адреса
-p tcp → TPROXY :7895 mark 1
@@ -377,6 +379,7 @@ UI доступен на `http://<gateway-ip>:3456`.
| `INSTALL_SINGBOX` | `true` | Скачивать sing-box в Docker build; `false` для подготовленного runtime base |
| `PROXY_PORT` | `8080` | HTTP/SOCKS mixed inbound |
| `TPROXY_PORT` | `7895` | TProxy inbound sing-box |
| `TPROXY_BYPASS_SOURCE_CIDRS` | unset | Source CIDR устройств, которые должны идти напрямую мимо TProxy/sing-box, например `192.168.50.25/32` |
| `DATA_DIR` | `/var/lib/vpn-proxy` | Директория данных (volume) |
| `ROUTING_RU_DIRECT` | `true` | geoip-ru/geosite-ru → direct |
| `LOG_LEVEL` | `info` | Уровень логов sing-box |

View File

@@ -10,6 +10,7 @@ PROXY_BIND_IP="${PROXY_BIND_IP:-0.0.0.0}"
PROXY_INPUT_CHAIN="${PROXY_INPUT_CHAIN:-VPN_PROXY_INPUT}"
PROXY_FIREWALL="${PROXY_FIREWALL:-true}"
PROXY_ALLOWED_CIDRS="${PROXY_ALLOWED_CIDRS:-10.0.0.0/8 172.16.0.0/12 192.168.0.0/16}"
TPROXY_BYPASS_SOURCE_CIDRS="${TPROXY_BYPASS_SOURCE_CIDRS:-}"
BYPASS_CIDRS="${BYPASS_CIDRS:-0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4}"
# Имя ipset для IP-адресов, которые sing-box отправил напрямую (direct bypass cache)
DIRECT_BYPASS_SET="${DIRECT_BYPASS_SET:-vpn_direct_bypass}"
@@ -85,6 +86,10 @@ setup_tproxy() {
ipt -t mangle -A "$TPROXY_CHAIN" -m addrtype --dst-type LOCAL -j RETURN
ipt -t mangle -A "$TPROXY_CHAIN" -m mark --mark "$TPROXY_MARK" -j RETURN
for cidr in $TPROXY_BYPASS_SOURCE_CIDRS; do
ipt -t mangle -A "$TPROXY_CHAIN" -s "$cidr" -j RETURN
done
if [[ "$DIRECT_BYPASS_CACHE" == "true" ]]; then
# Direct bypass cache: IP-адреса из ipset идут напрямую, минуя sing-box.
# Включайте только если готовы к тому, что global rules для этих dst IP не будут проверяться.

View File

@@ -47,6 +47,7 @@ TPROXY_PORT=7895
TPROXY_MARK=1
TPROXY_TABLE=100
TPROXY_CHAIN=VPN_PROXY_TPROXY
TPROXY_BYPASS_SOURCE_CIDRS=
ROUTING_RU_DIRECT=true
LOG_LEVEL=info
EOF

View File

@@ -0,0 +1,88 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
const ROOT = path.resolve(import.meta.dirname, "../..");
const ENTRYPOINT = path.join(ROOT, "entrypoint.sh");
function writeExecutable(filePath, contents) {
fs.writeFileSync(filePath, contents, { mode: 0o755 });
}
test("entrypoint bypasses configured source CIDRs before TProxy interception", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "vpn-entrypoint-"));
const commandLog = path.join(tmp, "commands.log");
writeExecutable(
path.join(tmp, "iptables"),
`#!/usr/bin/env bash
printf 'iptables %s\\n' "$*" >> "$COMMAND_LOG"
exit 0
`,
);
writeExecutable(
path.join(tmp, "ip"),
`#!/usr/bin/env bash
printf 'ip %s\\n' "$*" >> "$COMMAND_LOG"
exit 0
`,
);
writeExecutable(
path.join(tmp, "ipset"),
`#!/usr/bin/env bash
printf 'ipset %s\\n' "$*" >> "$COMMAND_LOG"
exit 0
`,
);
writeExecutable(
path.join(tmp, "node"),
`#!/usr/bin/env bash
printf 'node %s\\n' "$*" >> "$COMMAND_LOG"
exit 0
`,
);
const result = spawnSync("bash", [ENTRYPOINT], {
cwd: ROOT,
env: {
...process.env,
PATH: `${tmp}${path.delimiter}${process.env.PATH}`,
COMMAND_LOG: commandLog,
TPROXY_BYPASS_SOURCE_CIDRS: "192.168.50.25/32 192.168.50.26/32",
DIRECT_BYPASS_CACHE: "true",
BYPASS_CIDRS: "10.0.0.0/8",
},
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr || result.stdout);
const commands = fs.readFileSync(commandLog, "utf8").trim().split("\n");
const sourceBypassIndex = commands.findIndex((line) =>
line.includes(
"iptables -w -t mangle -A VPN_PROXY_TPROXY -s 192.168.50.25/32 -j RETURN",
),
);
const secondSourceBypassIndex = commands.findIndex((line) =>
line.includes(
"iptables -w -t mangle -A VPN_PROXY_TPROXY -s 192.168.50.26/32 -j RETURN",
),
);
const directCacheIndex = commands.findIndex((line) =>
line.includes("-m set --match-set vpn_direct_bypass dst -j RETURN"),
);
const tproxyIndex = commands.findIndex((line) =>
line.includes("-p tcp -j TPROXY --on-port 7895"),
);
assert.notEqual(sourceBypassIndex, -1);
assert.notEqual(secondSourceBypassIndex, -1);
assert.notEqual(directCacheIndex, -1);
assert.notEqual(tproxyIndex, -1);
assert.ok(sourceBypassIndex < directCacheIndex);
assert.ok(secondSourceBypassIndex < directCacheIndex);
assert.ok(sourceBypassIndex < tproxyIndex);
});