From a9e3cce113519d86052c2f0c4b1add678541d4fa Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 26 Aug 2026 00:22:25 +0700 Subject: [PATCH] =?UTF-8?q?feat(launcher):=20=D1=82=D1=80=D0=B5=D0=B1?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D1=84=D0=B5=D0=B9=D1=81=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BE=D0=BA=20MikroTik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Все пробы и ingest идут через src-address выбранного WAN, scheduler сохраняет vtIface. Co-authored-by: Cursor --- AGENTS.md | 8 +-- apps/api/scripts/censorcheck/launcher.rsc | 49 ++++++++++++++++--- apps/api/scripts/ipregion/launcher.rsc | 51 ++++++++++++++++---- apps/api/src/routes/launcher.test.ts | 8 ++- apps/web/src/components/censorcheck/types.ts | 4 +- apps/web/src/components/ipregion/types.ts | 4 +- 6 files changed, 97 insertions(+), 27 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8d58d8b..f5d9ed3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,11 +94,11 @@ vps-tracker/ Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/cc | bash` (тот же контейнер, Traefik dual Host). Раз в сутки: `curl -fsSL https://vt.shnt.top/cc | bash -s -- --daily` (cron, свежий HMAC-токен на каждый запуск). -MikroTik 7.22+: `/tool fetch url="https://vt.shnt.top/cc.rsc" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc` (ежедневно: `?daily=1`, scheduler качает `.rsc` заново). +MikroTik 7.22+: `:global vtIface "ether1"; /tool fetch url="https://vt.shnt.top/cc.rsc" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc` (ежедневно: `?daily=1`; все пробы через `src-address` интерфейса). - **Vendor:** `apps/api/scripts/censorcheck/censorcheck.sh` (pin SHA `12c5839`, MIT) - **Launcher:** `GET /cc` минтит HMAC ingest-токен (TTL 20 мин); по `/etc/os-release` ставит `jq`/`dig`/`column` без prompt; прогресс-бар в stderr; `GET /cc/vendor` — скрипт (LF); `--daily` / `--remove-daily` -- **MikroTik:** `GET /cc.rsc` — RouterOS-скрипт (HTTPS GET, без DPI); `?daily=1` / `?remove=daily`; scheduler `vt-cc` +- **MikroTik:** `GET /cc.rsc` — RouterOS-скрипт (HTTPS GET, без DPI); обязателен `:global vtIface`; `?daily=1` / `?remove=daily`; scheduler `vt-cc` - **Ingest:** `POST /api/integrations/censorcheck/runs` (без portal JWT) - **UI:** `/blocking` — текущие прогоны и история, группировка VPS / сервис - Env: `CENSORCHECK_INGEST_SECRET`, `CENSORCHECK_PUBLIC_URL`, `VPS_LAUNCHER_DOMAIN` @@ -107,11 +107,11 @@ MikroTik 7.22+: `/tool fetch url="https://vt.shnt.top/cc.rsc" dst-path=vt-cc.rsc Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/ic | bash`. Раз в сутки: `curl -fsSL https://vt.shnt.top/ic | bash -s -- --daily`. -MikroTik 7.22+: `/tool fetch url="https://vt.shnt.top/ic.rsc" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc` (`?daily=1`). +MikroTik 7.22+: `:global vtIface "ether1"; /tool fetch url="https://vt.shnt.top/ic.rsc" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc` (`?daily=1`; пробы через тот же `vtIface`). - **Vendor:** `apps/api/scripts/ipregion/ipregion.sh` (pin SHA `7d1c25c`, MIT, [vernette/ipregion](https://github.com/vernette/ipregion)) - **Launcher:** `GET /ic` минтит HMAC ingest-токен (тот же `CENSORCHECK_INGEST_SECRET`); `GET /ic/vendor` — pinned скрипт (LF); `--daily` / `--remove-daily` -- **MikroTik:** `GET /ic.rsc` — primary GeoIP JSON + Cloudflare CDN; scheduler `vt-ic` +- **MikroTik:** `GET /ic.rsc` — primary GeoIP JSON + Cloudflare CDN; обязателен `:global vtIface`; scheduler `vt-ic` - **Ingest:** `POST /api/integrations/ipregion/runs` (без portal JWT) - **UI:** `/geo` — матрица ISO-стран VPS × сервисы (primary / custom / cdn) diff --git a/apps/api/scripts/censorcheck/launcher.rsc b/apps/api/scripts/censorcheck/launcher.rsc index 6e8e9ec..afabe9f 100644 --- a/apps/api/scripts/censorcheck/launcher.rsc +++ b/apps/api/scripts/censorcheck/launcher.rsc @@ -1,12 +1,12 @@ # VPS Tracker — blocking launcher for RouterOS 7.22+ -# /tool fetch url="__VT_API_URL__/cc.rsc" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc +# First run: :global vtIface "ether1"; /tool fetch url="__VT_API_URL__/cc.rsc" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc # HTTPS GET only (no DPI/SNI). Ingest: POST /api/integrations/censorcheck/runs :local vtApi "__VT_API_URL__" :local vtToken "__VT_INGEST_TOKEN__" :local vtDaily "__VT_DAILY__" :local vtRemove "__VT_REMOVE_DAILY__" -:local launcherVer "ros-1" +:local launcherVer "ros-2" :local schedName "vt-cc" :local dstFile "vt-cc.rsc" @@ -34,10 +34,43 @@ :put ("censorcheck launcher " . $launcherVer . " (RouterOS)") +:global vtIface +:if (([:typeof $vtIface] != "str") or ([:len $vtIface] = 0)) do={ + :put "Задайте интерфейс и повторите импорт:" + :put ":global vtIface \"ether1\"" + :put "Интерфейсы:" + /interface print + :error "vtIface не задан" +} +:if ([:len [/interface find where name=$vtIface]] = 0) do={ + :put "Интерфейсы:" + /interface print + :error ("Нет интерфейса " . $vtIface) +} + +:local srcIp "" +:local addrRaw +:local slash +:foreach a in=[/ip address find where interface=$vtIface] do={ + :if ([:len $srcIp] = 0) do={ + :set addrRaw [/ip address get $a address] + :set slash [:find $addrRaw "/"] + :if ([:typeof $slash] != "nil") do={ + :set srcIp [:pick $addrRaw 0 $slash] + } else={ + :set srcIp $addrRaw + } + } +} +:if ([:len $srcIp] = 0) do={ + :error ("Нет IPv4 на интерфейсе " . $vtIface) +} +:put ("интерфейс: " . $vtIface . " src=" . $srcIp) + :local r :local publicIp "" :do { - :set r [/tool fetch url="https://api.ipify.org" output=user as-value] + :set r [/tool fetch url="https://api.ipify.org" src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ :set publicIp ($r->"data") } @@ -48,7 +81,7 @@ :if ([:typeof $lf] != "nil") do={ :set publicIp [:pick $publicIp 0 $lf] } :if ([:len $publicIp] = 0) do={ :do { - :set r [/tool fetch url="https://ifconfig.me/ip" output=user as-value] + :set r [/tool fetch url="https://ifconfig.me/ip" src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ :set publicIp ($r->"data") } @@ -64,7 +97,7 @@ :local hoster "" :do { - :set r [/tool fetch url=("https://ipwho.is/" . $publicIp) output=user as-value] + :set r [/tool fetch url=("https://ipwho.is/" . $publicIp) src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ :local j [:deserialize from=json value=($r->"data")] :local isp ($j->"connection"->"isp") @@ -105,7 +138,7 @@ :foreach host in=$hosts do={ :set code 0 :do { - :set r [/tool fetch url=("https://" . $host . "/") output=user-with-headers as-value] + :set r [/tool fetch url=("https://" . $host . "/") src-address=$srcIp output=user-with-headers as-value] :if (($r->"status") != "finished") do={ :set code -1 } else={ @@ -150,7 +183,7 @@ :local hdrs ("Content-Type: application/json,Authorization: Bearer " . $vtToken) :put "Отправляю ingest..." :do { - :set r [/tool fetch url=($vtApi . "/api/integrations/censorcheck/runs") http-method=post http-header-field=$hdrs http-data=$json output=user as-value] + :set r [/tool fetch url=($vtApi . "/api/integrations/censorcheck/runs") src-address=$srcIp http-method=post http-header-field=$hdrs http-data=$json output=user as-value] :put ($r->"data") } on-error={ :put "API недоступен (fetch error)" @@ -166,7 +199,7 @@ :if ($hour < 10) do={ :set hh ("0" . $hour) } :if ($minute < 10) do={ :set mm ("0" . $minute) } :local startTime ($hh . ":" . $mm . ":00") - :local ev ("/tool fetch url=" . $vtApi . "/cc.rsc dst-path=" . $dstFile . "; /import file-name=" . $dstFile) + :local ev (":global vtIface \"" . $vtIface . "\"; /tool fetch url=" . $vtApi . "/cc.rsc dst-path=" . $dstFile . "; /import file-name=" . $dstFile) :do { /system scheduler remove [find name=$schedName] } on-error={} /system scheduler add name=$schedName interval=1d start-time=$startTime on-event=$ev policy=read,write,test,policy comment="vps-tracker vt-cc" :put ("Ежедневная проверка: каждый день в " . $startTime . " (" . $schedName . ")") diff --git a/apps/api/scripts/ipregion/launcher.rsc b/apps/api/scripts/ipregion/launcher.rsc index a713c60..9868285 100644 --- a/apps/api/scripts/ipregion/launcher.rsc +++ b/apps/api/scripts/ipregion/launcher.rsc @@ -1,12 +1,12 @@ # VPS Tracker — GeoIP launcher for RouterOS 7.22+ -# /tool fetch url="__VT_API_URL__/ic.rsc" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc +# First run: :global vtIface "ether1"; /tool fetch url="__VT_API_URL__/ic.rsc" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc # Primary GeoIP JSON + Cloudflare CDN. Ingest: POST /api/integrations/ipregion/runs :local vtApi "__VT_API_URL__" :local vtToken "__VT_INGEST_TOKEN__" :local vtDaily "__VT_DAILY__" :local vtRemove "__VT_REMOVE_DAILY__" -:local launcherVer "ros-1" +:local launcherVer "ros-2" :local schedName "vt-ic" :local dstFile "vt-ic.rsc" @@ -34,13 +34,46 @@ :put ("ipregion launcher " . $launcherVer . " (RouterOS)") +:global vtIface +:if (([:typeof $vtIface] != "str") or ([:len $vtIface] = 0)) do={ + :put "Задайте интерфейс и повторите импорт:" + :put ":global vtIface \"ether1\"" + :put "Интерфейсы:" + /interface print + :error "vtIface не задан" +} +:if ([:len [/interface find where name=$vtIface]] = 0) do={ + :put "Интерфейсы:" + /interface print + :error ("Нет интерфейса " . $vtIface) +} + +:local srcIp "" +:local addrRaw +:local slash +:foreach a in=[/ip address find where interface=$vtIface] do={ + :if ([:len $srcIp] = 0) do={ + :set addrRaw [/ip address get $a address] + :set slash [:find $addrRaw "/"] + :if ([:typeof $slash] != "nil") do={ + :set srcIp [:pick $addrRaw 0 $slash] + } else={ + :set srcIp $addrRaw + } + } +} +:if ([:len $srcIp] = 0) do={ + :error ("Нет IPv4 на интерфейсе " . $vtIface) +} +:put ("интерфейс: " . $vtIface . " src=" . $srcIp) + :local r :local j :local isp :local org :local publicIp "" :do { - :set r [/tool fetch url="https://api.ipify.org" output=user as-value] + :set r [/tool fetch url="https://api.ipify.org" src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ :set publicIp ($r->"data") } @@ -51,7 +84,7 @@ :if ([:typeof $lf] != "nil") do={ :set publicIp [:pick $publicIp 0 $lf] } :if ([:len $publicIp] = 0) do={ :do { - :set r [/tool fetch url="https://ifconfig.me/ip" output=user as-value] + :set r [/tool fetch url="https://ifconfig.me/ip" src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ :set publicIp ($r->"data") } @@ -67,7 +100,7 @@ :local hoster "" :do { - :set r [/tool fetch url=("https://ipwho.is/" . $publicIp) output=user as-value] + :set r [/tool fetch url=("https://ipwho.is/" . $publicIp) src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ :set j [:deserialize from=json value=($r->"data")] :set isp ($j->"connection"->"isp") @@ -150,9 +183,9 @@ :set body "" :do { :if ($method = "post") do={ - :set r [/tool fetch url=$url http-method=post http-data=$postData http-header-field="Content-Type: application/x-www-form-urlencoded" output=user-with-headers as-value] + :set r [/tool fetch url=$url http-method=post http-data=$postData http-header-field="Content-Type: application/x-www-form-urlencoded" src-address=$srcIp output=user-with-headers as-value] } else={ - :set r [/tool fetch url=$url output=user-with-headers as-value] + :set r [/tool fetch url=$url src-address=$srcIp output=user-with-headers as-value] } :if (($r->"status") != "finished") do={ :set httpCode -1 @@ -267,7 +300,7 @@ :local hdrs ("Content-Type: application/json,Authorization: Bearer " . $vtToken) :put "Отправляю ingest..." :do { - :set r [/tool fetch url=($vtApi . "/api/integrations/ipregion/runs") http-method=post http-header-field=$hdrs http-data=$json output=user as-value] + :set r [/tool fetch url=($vtApi . "/api/integrations/ipregion/runs") src-address=$srcIp http-method=post http-header-field=$hdrs http-data=$json output=user as-value] :put ($r->"data") } on-error={ :put "API недоступен (fetch error)" @@ -283,7 +316,7 @@ :if ($hour < 10) do={ :set hh ("0" . $hour) } :if ($minute < 10) do={ :set mm ("0" . $minute) } :local startTime ($hh . ":" . $mm . ":00") - :local ev ("/tool fetch url=" . $vtApi . "/ic.rsc dst-path=" . $dstFile . "; /import file-name=" . $dstFile) + :local ev (":global vtIface \"" . $vtIface . "\"; /tool fetch url=" . $vtApi . "/ic.rsc dst-path=" . $dstFile . "; /import file-name=" . $dstFile) :do { /system scheduler remove [find name=$schedName] } on-error={} /system scheduler add name=$schedName interval=1d start-time=$startTime on-event=$ev policy=read,write,test,policy comment="vps-tracker vt-ic" :put ("Ежедневная проверка: каждый день в " . $startTime . " (" . $schedName . ")") diff --git a/apps/api/src/routes/launcher.test.ts b/apps/api/src/routes/launcher.test.ts index 2b6f7ee..c8064b5 100644 --- a/apps/api/src/routes/launcher.test.ts +++ b/apps/api/src/routes/launcher.test.ts @@ -128,11 +128,13 @@ describe('GET /cc.rsc RouterOS launcher', () => { expect(res.body).toContain('https://vt.shnt.top') expect(res.body).toContain('/tool fetch') expect(res.body).toContain('/api/integrations/censorcheck/runs') - expect(res.body).toContain('ros-1') + expect(res.body).toContain('ros-2') expect(res.body).toContain(':local vtDaily "no"') expect(res.body).toContain(':local vtRemove "no"') expect(res.body).toContain('/system scheduler') expect(res.body).toContain('youtube.com') + expect(res.body).toContain(':global vtIface') + expect(res.body).toContain('src-address=$srcIp') expect(res.body).not.toContain('\r') expect(res.body).not.toContain('__VT_API_URL__') expect(res.body).not.toContain('__VT_INGEST_TOKEN__') @@ -181,10 +183,12 @@ describe('GET /ic.rsc RouterOS launcher', () => { expect(res.body).toContain('https://vt.shnt.top') expect(res.body).toContain('/tool fetch') expect(res.body).toContain('/api/integrations/ipregion/runs') - expect(res.body).toContain('ros-1') + expect(res.body).toContain('ros-2') expect(res.body).toContain(':local vtDaily "no"') expect(res.body).toContain('ipinfo.io') expect(res.body).toContain('cloudflare cdn') + expect(res.body).toContain(':global vtIface') + expect(res.body).toContain('src-address=$srcIp') expect(res.body).not.toContain('\r') expect(res.body).not.toContain('__VT_API_URL__') expect(res.body).not.toContain('__VT_INGEST_TOKEN__') diff --git a/apps/web/src/components/censorcheck/types.ts b/apps/web/src/components/censorcheck/types.ts index d86b7b8..06a3255 100644 --- a/apps/web/src/components/censorcheck/types.ts +++ b/apps/web/src/components/censorcheck/types.ts @@ -59,9 +59,9 @@ export const CENSORCHECK_STATUS_LABELS: Record = { export const LAUNCHER_CMD = 'curl -fsSL https://vt.shnt.top/cc | bash' export const LAUNCHER_CMD_DAILY = 'curl -fsSL https://vt.shnt.top/cc | bash -s -- --daily' export const LAUNCHER_CMD_ROS = - '/tool fetch url="https://vt.shnt.top/cc.rsc" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc' + ':global vtIface "ether1"; /tool fetch url="https://vt.shnt.top/cc.rsc" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc' export const LAUNCHER_CMD_ROS_DAILY = - '/tool fetch url="https://vt.shnt.top/cc.rsc?daily=1" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc' + ':global vtIface "ether1"; /tool fetch url="https://vt.shnt.top/cc.rsc?daily=1" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc' export function formatVpsResources(vcpu: number, ramGb: number, diskGb: number): string { return `${vcpu} vCPU / ${ramGb} GB / ${diskGb} GB` diff --git a/apps/web/src/components/ipregion/types.ts b/apps/web/src/components/ipregion/types.ts index ec0b7cf..30ce924 100644 --- a/apps/web/src/components/ipregion/types.ts +++ b/apps/web/src/components/ipregion/types.ts @@ -58,9 +58,9 @@ export const IPREGION_STATUS_LABELS: Record = { export const LAUNCHER_CMD = 'curl -fsSL https://vt.shnt.top/ic | bash' export const LAUNCHER_CMD_DAILY = 'curl -fsSL https://vt.shnt.top/ic | bash -s -- --daily' export const LAUNCHER_CMD_ROS = - '/tool fetch url="https://vt.shnt.top/ic.rsc" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc' + ':global vtIface "ether1"; /tool fetch url="https://vt.shnt.top/ic.rsc" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc' export const LAUNCHER_CMD_ROS_DAILY = - '/tool fetch url="https://vt.shnt.top/ic.rsc?daily=1" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc' + ':global vtIface "ether1"; /tool fetch url="https://vt.shnt.top/ic.rsc?daily=1" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc' export function formatVpsResources(vcpu: number, ramGb: number, diskGb: number): string { return `${vcpu} vCPU / ${ramGb} GB / ${diskGb} GB`