Fix LAN proxy binding in routing setup
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 13s
Some checks failed
Build and Deploy Gateway / build-and-deploy (push) Failing after 13s
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
PORT=3456
|
PORT=3456
|
||||||
PROXY_PORT=8080
|
PROXY_PORT=8080
|
||||||
|
PROXY_BIND_IP=0.0.0.0
|
||||||
TPROXY_PORT=7895
|
TPROXY_PORT=7895
|
||||||
TPROXY_MARK=1
|
TPROXY_MARK=1
|
||||||
TPROXY_TABLE=100
|
TPROXY_TABLE=100
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ RUN chmod +x /entrypoint.sh \
|
|||||||
|
|
||||||
ENV PORT=3456 \
|
ENV PORT=3456 \
|
||||||
PROXY_PORT=8080 \
|
PROXY_PORT=8080 \
|
||||||
|
PROXY_BIND_IP=0.0.0.0 \
|
||||||
TPROXY_PORT=7895 \
|
TPROXY_PORT=7895 \
|
||||||
DIRECT_BYPASS_CACHE=false \
|
DIRECT_BYPASS_CACHE=false \
|
||||||
|
RULE_SET_DOWNLOAD_DETOUR=vpn \
|
||||||
DATA_DIR=/var/lib/vpn-proxy \
|
DATA_DIR=/var/lib/vpn-proxy \
|
||||||
SING_BOX_CONFIG=/etc/sing-box/config.json \
|
SING_BOX_CONFIG=/etc/sing-box/config.json \
|
||||||
SING_BOX_CACHE=/var/lib/sing-box/cache.db
|
SING_BOX_CACHE=/var/lib/sing-box/cache.db
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ ipset-кэш намеренно **не** очищается — записи и
|
|||||||
| 3 | `rule_set: [geoip-ru, geosite-category-ru]` | `direct` |
|
| 3 | `rule_set: [geoip-ru, geosite-category-ru]` | `direct` |
|
||||||
| 4 | Device defaults для `tproxy-in` | `direct` / VPN / `block` |
|
| 4 | Device defaults для `tproxy-in` | `direct` / VPN / `block` |
|
||||||
| 5 | Proxy default для `mixed-in` | по умолчанию VPN |
|
| 5 | Proxy default для `mixed-in` | по умолчанию VPN |
|
||||||
| 6 | Transparent default для unknown devices | по умолчанию `direct` |
|
| 6 | Transparent default для unknown devices | по умолчанию VPN |
|
||||||
| 7 | Всё остальное (`final`) | `direct` |
|
| 7 | Всё остальное (`final`) | `direct` |
|
||||||
|
|
||||||
Конфиг генерируется динамически через `buildGatewayConfig()` из подписки + сохранённых правил. Перед применением выполняется `sing-box check`.
|
Конфиг генерируется динамически через `buildGatewayConfig()` из подписки + сохранённых правил. Перед применением выполняется `sing-box check`.
|
||||||
@@ -142,7 +142,7 @@ DIRECT_BYPASS_TTL=3600 # TTL в секундах
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"defaultTransparentMode": "direct",
|
"defaultTransparentMode": "vpn",
|
||||||
"proxyDefaultMode": "vpn",
|
"proxyDefaultMode": "vpn",
|
||||||
"devices": [
|
"devices": [
|
||||||
{
|
{
|
||||||
@@ -290,7 +290,8 @@ UI доступен на `http://<gateway-ip>:3456`.
|
|||||||
| `DIRECT_BYPASS_CACHE` | `false` | Включить dst-IP bypass cache; по умолчанию выключен |
|
| `DIRECT_BYPASS_CACHE` | `false` | Включить dst-IP bypass cache; по умолчанию выключен |
|
||||||
| `DIRECT_BYPASS_SET` | `vpn_direct_bypass` | Имя ipset bypass-кэша |
|
| `DIRECT_BYPASS_SET` | `vpn_direct_bypass` | Имя ipset bypass-кэша |
|
||||||
| `DIRECT_BYPASS_TTL` | `3600` | TTL записей (секунды) |
|
| `DIRECT_BYPASS_TTL` | `3600` | TTL записей (секунды) |
|
||||||
| `PROXY_BIND_IP` | `127.0.0.1` | Bind для HTTP/SOCKS; `0.0.0.0` для LAN |
|
| `RULE_SET_DOWNLOAD_DETOUR` | `vpn` | Через какой outbound sing-box скачивает remote rule-set; `vpn` = выбранный сервер |
|
||||||
|
| `PROXY_BIND_IP` | `0.0.0.0` | Bind для HTTP/SOCKS в LAN; можно сузить до IP gateway |
|
||||||
| `PROXY_FIREWALL` | `true` | Закрыть `PROXY_PORT` не из allowed CIDR |
|
| `PROXY_FIREWALL` | `true` | Закрыть `PROXY_PORT` не из allowed CIDR |
|
||||||
| `PROXY_ALLOWED_CIDRS` | `10.0.0.0/8 172.16.0.0/12 192.168.0.0/16` | Кто может подключаться к mixed proxy |
|
| `PROXY_ALLOWED_CIDRS` | `10.0.0.0/8 172.16.0.0/12 192.168.0.0/16` | Кто может подключаться к mixed proxy |
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ TPROXY_MARK="${TPROXY_MARK:-1}"
|
|||||||
TPROXY_TABLE="${TPROXY_TABLE:-100}"
|
TPROXY_TABLE="${TPROXY_TABLE:-100}"
|
||||||
TPROXY_CHAIN="${TPROXY_CHAIN:-VPN_PROXY_TPROXY}"
|
TPROXY_CHAIN="${TPROXY_CHAIN:-VPN_PROXY_TPROXY}"
|
||||||
PROXY_PORT="${PROXY_PORT:-8080}"
|
PROXY_PORT="${PROXY_PORT:-8080}"
|
||||||
PROXY_BIND_IP="${PROXY_BIND_IP:-127.0.0.1}"
|
PROXY_BIND_IP="${PROXY_BIND_IP:-0.0.0.0}"
|
||||||
PROXY_INPUT_CHAIN="${PROXY_INPUT_CHAIN:-VPN_PROXY_INPUT}"
|
PROXY_INPUT_CHAIN="${PROXY_INPUT_CHAIN:-VPN_PROXY_INPUT}"
|
||||||
PROXY_FIREWALL="${PROXY_FIREWALL:-true}"
|
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}"
|
PROXY_ALLOWED_CIDRS="${PROXY_ALLOWED_CIDRS:-10.0.0.0/8 172.16.0.0/12 192.168.0.0/16}"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ if [ ! -f "${DEPLOY_PATH}/.env" ]; then
|
|||||||
cat > "${DEPLOY_PATH}/.env" <<'EOF'
|
cat > "${DEPLOY_PATH}/.env" <<'EOF'
|
||||||
PORT=3456
|
PORT=3456
|
||||||
PROXY_PORT=8080
|
PROXY_PORT=8080
|
||||||
|
PROXY_BIND_IP=0.0.0.0
|
||||||
TPROXY_PORT=7895
|
TPROXY_PORT=7895
|
||||||
TPROXY_MARK=1
|
TPROXY_MARK=1
|
||||||
TPROXY_TABLE=100
|
TPROXY_TABLE=100
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const settings = {
|
|||||||
port: Number(process.env.PORT || 3456),
|
port: Number(process.env.PORT || 3456),
|
||||||
proxyPort: Number(process.env.PROXY_PORT || 8080),
|
proxyPort: Number(process.env.PROXY_PORT || 8080),
|
||||||
tproxyPort: Number(process.env.TPROXY_PORT || 7895),
|
tproxyPort: Number(process.env.TPROXY_PORT || 7895),
|
||||||
bindIp: process.env.PROXY_BIND_IP || "127.0.0.1",
|
bindIp: process.env.PROXY_BIND_IP || "0.0.0.0",
|
||||||
dataDir,
|
dataDir,
|
||||||
distDir: process.env.DIST_DIR || "/app/dist",
|
distDir: process.env.DIST_DIR || "/app/dist",
|
||||||
configPath:
|
configPath:
|
||||||
@@ -20,6 +20,7 @@ export const settings = {
|
|||||||
subscriptionCachePath: path.join(dataDir, "subscription-cache.json"),
|
subscriptionCachePath: path.join(dataDir, "subscription-cache.json"),
|
||||||
hwidPath: path.join(dataDir, "hwid"),
|
hwidPath: path.join(dataDir, "hwid"),
|
||||||
routingRuDirect: String(process.env.ROUTING_RU_DIRECT || "true") !== "false",
|
routingRuDirect: String(process.env.ROUTING_RU_DIRECT || "true") !== "false",
|
||||||
|
ruleSetDownloadDetour: process.env.RULE_SET_DOWNLOAD_DETOUR || "vpn",
|
||||||
logLevel: process.env.LOG_LEVEL || "info",
|
logLevel: process.env.LOG_LEVEL || "info",
|
||||||
appName: "VPN Proxy Gateway",
|
appName: "VPN Proxy Gateway",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { settings } from "./config.js";
|
|||||||
|
|
||||||
export const DEVICE_MODES = new Set(["direct", "vpn", "rules", "block"]);
|
export const DEVICE_MODES = new Set(["direct", "vpn", "rules", "block"]);
|
||||||
export const DEFAULT_DEVICE_MODES = new Set(["direct", "vpn", "block"]);
|
export const DEFAULT_DEVICE_MODES = new Set(["direct", "vpn", "block"]);
|
||||||
export const DEFAULT_DEVICE_MODE = "direct";
|
export const DEFAULT_DEVICE_MODE = "vpn";
|
||||||
export const DEFAULT_PROXY_MODE = "vpn";
|
export const DEFAULT_PROXY_MODE = "vpn";
|
||||||
export const TPROXY_INBOUND = "tproxy-in";
|
export const TPROXY_INBOUND = "tproxy-in";
|
||||||
export const MIXED_INBOUND = "mixed-in";
|
export const MIXED_INBOUND = "mixed-in";
|
||||||
|
|||||||
@@ -21,6 +21,30 @@ import { matchRoute, detectRuleConflicts } from "./routeMatcher.js";
|
|||||||
import { tcpPing, resolveHost } from "./ping.js";
|
import { tcpPing, resolveHost } from "./ping.js";
|
||||||
|
|
||||||
const APPLY_HISTORY_LIMIT = 10;
|
const APPLY_HISTORY_LIMIT = 10;
|
||||||
|
const FALLBACK_RULE_SET_CATALOG = {
|
||||||
|
geosite: [
|
||||||
|
"geosite-category-ru",
|
||||||
|
"geosite-google",
|
||||||
|
"geosite-youtube",
|
||||||
|
"geosite-telegram",
|
||||||
|
"geosite-openai",
|
||||||
|
"geosite-apple",
|
||||||
|
"geosite-github",
|
||||||
|
"geosite-steam",
|
||||||
|
"geosite-discord",
|
||||||
|
"geosite-netflix",
|
||||||
|
"geosite-cloudflare",
|
||||||
|
"geosite-category-ads-all",
|
||||||
|
],
|
||||||
|
geoip: [
|
||||||
|
"geoip-ru",
|
||||||
|
"geoip-cloudflare",
|
||||||
|
"geoip-telegram",
|
||||||
|
"geoip-google",
|
||||||
|
"geoip-netflix",
|
||||||
|
"geoip-private",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
fs.mkdirSync(settings.dataDir, { recursive: true });
|
fs.mkdirSync(settings.dataDir, { recursive: true });
|
||||||
|
|
||||||
@@ -402,6 +426,88 @@ function sendJson(res, statusCode, payload) {
|
|||||||
res.end(body);
|
res.end(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRuleSetUrl(url) {
|
||||||
|
const value = String(url || "").trim();
|
||||||
|
if (!value) return [];
|
||||||
|
|
||||||
|
const urls = [value];
|
||||||
|
|
||||||
|
const jsdelivrMatch = value.match(
|
||||||
|
/^https:\/\/cdn\.jsdelivr\.net\/gh\/([^/]+)\/([^@/]+)@([^/]+)\/(.+)$/i,
|
||||||
|
);
|
||||||
|
if (jsdelivrMatch) {
|
||||||
|
const [, owner, repo, ref, filePath] = jsdelivrMatch;
|
||||||
|
urls.push(
|
||||||
|
`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawMatch = value.match(
|
||||||
|
/^https:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/i,
|
||||||
|
);
|
||||||
|
if (rawMatch) {
|
||||||
|
const [, owner, repo, ref, filePath] = rawMatch;
|
||||||
|
urls.push(`https://cdn.jsdelivr.net/gh/${owner}/${repo}@${ref}/${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Set(urls));
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(urlOrUrls, outputPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const candidates = Array.isArray(urlOrUrls)
|
||||||
|
? Array.from(new Set(urlOrUrls.flatMap((url) => normalizeRuleSetUrl(url))))
|
||||||
|
: normalizeRuleSetUrl(urlOrUrls);
|
||||||
|
let index = 0;
|
||||||
|
let lastError = "";
|
||||||
|
|
||||||
|
function tryNext() {
|
||||||
|
const url = candidates[index];
|
||||||
|
if (!url) {
|
||||||
|
reject(new Error(lastError || "Не удалось скачать файл"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stderr = "";
|
||||||
|
const dl = spawn("curl", [
|
||||||
|
"-fsSL",
|
||||||
|
"--retry",
|
||||||
|
"2",
|
||||||
|
"--retry-delay",
|
||||||
|
"1",
|
||||||
|
"--connect-timeout",
|
||||||
|
"10",
|
||||||
|
"--max-time",
|
||||||
|
"45",
|
||||||
|
"-A",
|
||||||
|
"vpn-proxy-app",
|
||||||
|
url,
|
||||||
|
"-o",
|
||||||
|
outputPath,
|
||||||
|
]);
|
||||||
|
dl.stderr.on("data", (d) => {
|
||||||
|
stderr += d;
|
||||||
|
});
|
||||||
|
dl.on("error", (err) => {
|
||||||
|
lastError = err.message;
|
||||||
|
index += 1;
|
||||||
|
tryNext();
|
||||||
|
});
|
||||||
|
dl.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastError = `curl ${url} завершился с кодом ${code}: ${stderr}`;
|
||||||
|
index += 1;
|
||||||
|
tryNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tryNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function readBody(req) {
|
function readBody(req) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@@ -490,6 +596,7 @@ function publicState() {
|
|||||||
mode: "gateway",
|
mode: "gateway",
|
||||||
port: settings.port,
|
port: settings.port,
|
||||||
proxyPort: settings.proxyPort,
|
proxyPort: settings.proxyPort,
|
||||||
|
proxyBindIp: settings.bindIp,
|
||||||
tproxyPort: settings.tproxyPort,
|
tproxyPort: settings.tproxyPort,
|
||||||
routingRuDirect: settings.routingRuDirect,
|
routingRuDirect: settings.routingRuDirect,
|
||||||
configExists: fs.existsSync(settings.configPath),
|
configExists: fs.existsSync(settings.configPath),
|
||||||
@@ -772,7 +879,7 @@ async function handleApi(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const profiles = writeDeviceProfiles({
|
const profiles = writeDeviceProfiles({
|
||||||
defaultTransparentMode: "direct",
|
defaultTransparentMode: "vpn",
|
||||||
proxyDefaultMode: "vpn",
|
proxyDefaultMode: "vpn",
|
||||||
devices,
|
devices,
|
||||||
});
|
});
|
||||||
@@ -865,26 +972,7 @@ async function handleApi(req, res) {
|
|||||||
const tmpSrs = path.join(os.tmpdir(), `singbox-rs-${Date.now()}.srs`);
|
const tmpSrs = path.join(os.tmpdir(), `singbox-rs-${Date.now()}.srs`);
|
||||||
const tmpJson = tmpSrs.replace(".srs", ".json");
|
const tmpJson = tmpSrs.replace(".srs", ".json");
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
const downloadedFrom = await downloadFile(url, tmpSrs);
|
||||||
let stderr = "";
|
|
||||||
// Скачиваем через curl (доступен в контейнере)
|
|
||||||
const dl = spawn("curl", [
|
|
||||||
"-fsSL",
|
|
||||||
"--max-time",
|
|
||||||
"30",
|
|
||||||
url,
|
|
||||||
"-o",
|
|
||||||
tmpSrs,
|
|
||||||
]);
|
|
||||||
dl.stderr.on("data", (d) => {
|
|
||||||
stderr += d;
|
|
||||||
});
|
|
||||||
dl.on("close", (code) => {
|
|
||||||
if (code !== 0)
|
|
||||||
reject(new Error(`curl завершился с кодом ${code}: ${stderr}`));
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Декомпилировать через sing-box rule-set decompile
|
// Декомпилировать через sing-box rule-set decompile
|
||||||
const dec = spawnSync(
|
const dec = spawnSync(
|
||||||
@@ -896,6 +984,13 @@ async function handleApi(req, res) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (dec.error) {
|
||||||
|
return sendJson(res, 200, {
|
||||||
|
success: false,
|
||||||
|
error: `sing-box не найден или не запустился: ${dec.error.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (dec.status !== 0) {
|
if (dec.status !== 0) {
|
||||||
return sendJson(res, 200, {
|
return sendJson(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -942,6 +1037,7 @@ async function handleApi(req, res) {
|
|||||||
const result = {
|
const result = {
|
||||||
tag,
|
tag,
|
||||||
url,
|
url,
|
||||||
|
downloadedFrom,
|
||||||
entries,
|
entries,
|
||||||
stats,
|
stats,
|
||||||
cachedAt: new Date().toISOString(),
|
cachedAt: new Date().toISOString(),
|
||||||
@@ -987,6 +1083,11 @@ async function handleApi(req, res) {
|
|||||||
{ headers },
|
{ headers },
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
if (!gsRes.ok || !giRes.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API недоступен: geosite=${gsRes.status}, geoip=${giRes.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const gsData = await gsRes.json();
|
const gsData = await gsRes.json();
|
||||||
const giData = await giRes.json();
|
const giData = await giRes.json();
|
||||||
|
|
||||||
@@ -999,11 +1100,21 @@ async function handleApi(req, res) {
|
|||||||
.map((f) => f.path.replace(".srs", ""))
|
.map((f) => f.path.replace(".srs", ""))
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
|
if (!geosite.length && !geoip.length) {
|
||||||
|
throw new Error("GitHub API вернул пустой каталог rule-set");
|
||||||
|
}
|
||||||
|
|
||||||
const result = { geosite, geoip, cachedAt: new Date().toISOString() };
|
const result = { geosite, geoip, cachedAt: new Date().toISOString() };
|
||||||
writeJson(cacheFile, result);
|
writeJson(cacheFile, result);
|
||||||
return sendJson(res, 200, { success: true, ...result });
|
return sendJson(res, 200, { success: true, ...result });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return sendJson(res, 500, { success: false, error: err.message });
|
const result = {
|
||||||
|
...FALLBACK_RULE_SET_CATALOG,
|
||||||
|
cachedAt: new Date().toISOString(),
|
||||||
|
fallback: true,
|
||||||
|
warning: `GitHub каталог не загрузился, показан встроенный список: ${err.message}`,
|
||||||
|
};
|
||||||
|
return sendJson(res, 200, { success: true, ...result });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function matchRoute(target, customRules, options = {}) {
|
|||||||
routingRuDirect = true,
|
routingRuDirect = true,
|
||||||
vpnTag = "vpn-out",
|
vpnTag = "vpn-out",
|
||||||
deviceProfiles = {
|
deviceProfiles = {
|
||||||
defaultTransparentMode: "direct",
|
defaultTransparentMode: "vpn",
|
||||||
proxyDefaultMode: "vpn",
|
proxyDefaultMode: "vpn",
|
||||||
devices: [],
|
devices: [],
|
||||||
},
|
},
|
||||||
@@ -227,7 +227,7 @@ export function matchRoute(target, customRules, options = {}) {
|
|||||||
|
|
||||||
// 6. unknown transparent device default.
|
// 6. unknown transparent device default.
|
||||||
if (inbound === TPROXY_INBOUND) {
|
if (inbound === TPROXY_INBOUND) {
|
||||||
const mode = deviceProfiles.defaultTransparentMode || "direct";
|
const mode = deviceProfiles.defaultTransparentMode || "vpn";
|
||||||
return {
|
return {
|
||||||
matched: "transparent-default",
|
matched: "transparent-default",
|
||||||
ruleIndex: -1,
|
ruleIndex: -1,
|
||||||
|
|||||||
@@ -51,7 +51,14 @@ function readCustomRuleSets() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ruleSets(customRuleSets = []) {
|
function ruleSetDownloadDetour(vpnTag) {
|
||||||
|
const detour = String(settings.ruleSetDownloadDetour || "vpn").trim();
|
||||||
|
if (!detour || detour === "vpn") return vpnTag;
|
||||||
|
return detour;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ruleSets(customRuleSets = [], vpnTag = "direct") {
|
||||||
|
const downloadDetour = ruleSetDownloadDetour(vpnTag);
|
||||||
const builtIn = settings.routingRuDirect
|
const builtIn = settings.routingRuDirect
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -59,14 +66,14 @@ function ruleSets(customRuleSets = []) {
|
|||||||
tag: "geoip-ru",
|
tag: "geoip-ru",
|
||||||
format: "binary",
|
format: "binary",
|
||||||
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs",
|
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs",
|
||||||
download_detour: "direct",
|
download_detour: downloadDetour,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "remote",
|
type: "remote",
|
||||||
tag: "geosite-category-ru",
|
tag: "geosite-category-ru",
|
||||||
format: "binary",
|
format: "binary",
|
||||||
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-ru.srs",
|
url: "https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-ru.srs",
|
||||||
download_detour: "direct",
|
download_detour: downloadDetour,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
@@ -78,7 +85,7 @@ function ruleSets(customRuleSets = []) {
|
|||||||
tag: String(rs.tag).trim(),
|
tag: String(rs.tag).trim(),
|
||||||
format: rs.format || "binary",
|
format: rs.format || "binary",
|
||||||
url: String(rs.url).trim(),
|
url: String(rs.url).trim(),
|
||||||
download_detour: "direct",
|
download_detour: downloadDetour,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Пользовательские rule-sets не должны дублировать встроенные
|
// Пользовательские rule-sets не должны дублировать встроенные
|
||||||
@@ -286,7 +293,7 @@ export function buildGatewayConfig(
|
|||||||
{ type: "block", tag: "block" },
|
{ type: "block", tag: "block" },
|
||||||
],
|
],
|
||||||
route: {
|
route: {
|
||||||
rule_set: bypassAll ? [] : ruleSets(customRuleSets),
|
rule_set: bypassAll ? [] : ruleSets(customRuleSets, vpnOutbound.tag),
|
||||||
rules: bypassAll
|
rules: bypassAll
|
||||||
? [{ ip_is_private: true, outbound: "direct" }]
|
? [{ ip_is_private: true, outbound: "direct" }]
|
||||||
: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
|
: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function App() {
|
|||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [customRules, setCustomRules] = useState([]);
|
const [customRules, setCustomRules] = useState([]);
|
||||||
const [devicesConfig, setDevicesConfig] = useState({
|
const [devicesConfig, setDevicesConfig] = useState({
|
||||||
defaultTransparentMode: 'direct',
|
defaultTransparentMode: 'vpn',
|
||||||
proxyDefaultMode: 'vpn',
|
proxyDefaultMode: 'vpn',
|
||||||
devices: [],
|
devices: [],
|
||||||
});
|
});
|
||||||
@@ -73,7 +73,7 @@ function App() {
|
|||||||
setServers(data.servers || []);
|
setServers(data.servers || []);
|
||||||
if (!rulesDirtyRef.current) setCustomRules(data.customRules || []);
|
if (!rulesDirtyRef.current) setCustomRules(data.customRules || []);
|
||||||
setDevicesConfig(data.devicesConfig || {
|
setDevicesConfig(data.devicesConfig || {
|
||||||
defaultTransparentMode: 'direct',
|
defaultTransparentMode: 'vpn',
|
||||||
proxyDefaultMode: 'vpn',
|
proxyDefaultMode: 'vpn',
|
||||||
devices: data.devices || [],
|
devices: data.devices || [],
|
||||||
});
|
});
|
||||||
@@ -208,7 +208,7 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
const data = await api.devices.save(nextConfig);
|
const data = await api.devices.save(nextConfig);
|
||||||
setDevicesConfig({
|
setDevicesConfig({
|
||||||
defaultTransparentMode: data.defaultTransparentMode || data.defaultMode || 'direct',
|
defaultTransparentMode: data.defaultTransparentMode || data.defaultMode || 'vpn',
|
||||||
proxyDefaultMode: data.proxyDefaultMode || 'vpn',
|
proxyDefaultMode: data.proxyDefaultMode || 'vpn',
|
||||||
devices: data.devices || [],
|
devices: data.devices || [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ function RoutingSummary({ state, onNav, onFlushDirectCache }) {
|
|||||||
const enabled = rules.filter((r) => r.enabled).length;
|
const enabled = rules.filter((r) => r.enabled).length;
|
||||||
const cacheCount = state?.directBypassCount || 0;
|
const cacheCount = state?.directBypassCount || 0;
|
||||||
const cacheAvailable = state?.directBypassAvailable && state?.directBypassEnabled;
|
const cacheAvailable = state?.directBypassAvailable && state?.directBypassEnabled;
|
||||||
const transparentDefault = state?.devicesConfig?.defaultTransparentMode || 'direct';
|
const transparentDefault = state?.devicesConfig?.defaultTransparentMode || 'vpn';
|
||||||
const proxyDefault = state?.devicesConfig?.proxyDefaultMode || 'vpn';
|
const proxyDefault = state?.devicesConfig?.proxyDefaultMode || 'vpn';
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function DeviceModeSelect({ value, onChange }) {
|
|||||||
|
|
||||||
function DevicesCard({ devicesConfig, onDefaultsChange, onAdd, onUpdate, onRemove }) {
|
function DevicesCard({ devicesConfig, onDefaultsChange, onAdd, onUpdate, onRemove }) {
|
||||||
const devices = devicesConfig?.devices || [];
|
const devices = devicesConfig?.devices || [];
|
||||||
const defaultTransparentMode = devicesConfig?.defaultTransparentMode || devicesConfig?.defaultMode || 'direct';
|
const defaultTransparentMode = devicesConfig?.defaultTransparentMode || devicesConfig?.defaultMode || 'vpn';
|
||||||
const proxyDefaultMode = devicesConfig?.proxyDefaultMode || 'vpn';
|
const proxyDefaultMode = devicesConfig?.proxyDefaultMode || 'vpn';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -496,6 +496,11 @@ function SagerNetSearchCard({ ruleSets, onAdd, busy }) {
|
|||||||
Ищите по имени: <code>steam</code>, <code>gaming</code>, <code>netflix</code>, <code>apple</code> и т.д.
|
Ищите по имени: <code>steam</code>, <code>gaming</code>, <code>netflix</code>, <code>apple</code> и т.д.
|
||||||
Кеш обновляется раз в 24 ч.
|
Кеш обновляется раз в 24 ч.
|
||||||
</small>
|
</small>
|
||||||
|
{catalog.fallback && (
|
||||||
|
<div className="conflict-banner warning" style={{ marginBottom: 12 }}>
|
||||||
|
<span>!</span><div>{catalog.warning || 'Показан встроенный fallback-каталог.'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex" style={{ gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
<div className="flex" style={{ gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
@@ -741,7 +746,7 @@ function PortsCard({ state }) {
|
|||||||
<div className="card-header"><h2>Порты и маршруты</h2></div>
|
<div className="card-header"><h2>Порты и маршруты</h2></div>
|
||||||
<div className="kv-list">
|
<div className="kv-list">
|
||||||
<div className="row"><span className="key">UI</span><span className="val text-mono">:{state?.port || 3456}</span></div>
|
<div className="row"><span className="key">UI</span><span className="val text-mono">:{state?.port || 3456}</span></div>
|
||||||
<div className="row"><span className="key">Mixed proxy (http+socks5)</span><span className="val text-mono">:{state?.proxyPort || 8080}</span></div>
|
<div className="row"><span className="key">Mixed proxy (http+socks5)</span><span className="val text-mono">{state?.proxyBindIp || '0.0.0.0'}:{state?.proxyPort || 8080}</span></div>
|
||||||
<div className="row"><span className="key">TProxy</span><span className="val text-mono">:{state?.tproxyPort || 7895}</span></div>
|
<div className="row"><span className="key">TProxy</span><span className="val text-mono">:{state?.tproxyPort || 7895}</span></div>
|
||||||
<div className="row"><span className="key">RU direct (geoip-ru)</span><span className="val">{state?.routingRuDirect ? 'включено' : 'выключено'}</span></div>
|
<div className="row"><span className="key">RU direct (geoip-ru)</span><span className="val">{state?.routingRuDirect ? 'включено' : 'выключено'}</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function StatusPane({ state, busy, onStop, onRestart, onShowConfig }) {
|
|||||||
<div className="kv-list" style={{ marginTop: 12 }}>
|
<div className="kv-list" style={{ marginTop: 12 }}>
|
||||||
<StatusRow label="Статус" value={singboxStatus} kind={singboxKind} />
|
<StatusRow label="Статус" value={singboxStatus} kind={singboxKind} />
|
||||||
<StatusRow label="UI порт" value={`:${state?.port || 3456}`} />
|
<StatusRow label="UI порт" value={`:${state?.port || 3456}`} />
|
||||||
<StatusRow label="Mixed proxy" value={`:${state?.proxyPort || 8080}`} />
|
<StatusRow label="Mixed proxy" value={`${state?.proxyBindIp || '0.0.0.0'}:${state?.proxyPort || 8080}`} />
|
||||||
<StatusRow label="TProxy" value={`:${state?.tproxyPort || 7895}`} />
|
<StatusRow label="TProxy" value={`:${state?.tproxyPort || 7895}`} />
|
||||||
<StatusRow label="RU direct" value={state?.routingRuDirect ? 'включено' : 'выключено'} />
|
<StatusRow label="RU direct" value={state?.routingRuDirect ? 'включено' : 'выключено'} />
|
||||||
<StatusRow label="Трафик" value={traffic} />
|
<StatusRow label="Трафик" value={traffic} />
|
||||||
|
|||||||
Reference in New Issue
Block a user