From 0092ec4cde370ea12be0df700c3e9f514ed51126 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Sun, 24 May 2026 13:26:03 +0300 Subject: [PATCH] Add source CIDR bypass for TProxy --- README.md | 3 + entrypoint.sh | 5 ++ scripts/deploy-gateway.sh | 1 + test/server/entrypoint-tproxy-bypass.test.js | 88 ++++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 test/server/entrypoint-tproxy-bypass.test.js diff --git a/README.md b/README.md index 44f1577..348cea7 100644 --- a/README.md +++ b/README.md @@ -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://: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 | diff --git a/entrypoint.sh b/entrypoint.sh index 6563a8a..d88f831 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -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 не будут проверяться. diff --git a/scripts/deploy-gateway.sh b/scripts/deploy-gateway.sh index 0dd03be..8d39e14 100644 --- a/scripts/deploy-gateway.sh +++ b/scripts/deploy-gateway.sh @@ -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 diff --git a/test/server/entrypoint-tproxy-bypass.test.js b/test/server/entrypoint-tproxy-bypass.test.js new file mode 100644 index 0000000..8c72abe --- /dev/null +++ b/test/server/entrypoint-tproxy-bypass.test.js @@ -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); +});