From 72aad059bd9e7afdba5a614577dcc68497403039 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 26 Aug 2026 00:43:26 +0700 Subject: [PATCH] =?UTF-8?q?fix(launcher):=20=D0=B2=D1=8B=D0=BF=D1=83=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B1=D1=8B=20Mik?= =?UTF-8?q?roTik=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=B2=D1=8B=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=BD=D1=8B=D0=B9=20WAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src-address недостаточно при нескольких default route: добавляем policy routing и снимаем правила после прогона. Co-authored-by: Cursor --- AGENTS.md | 4 +- apps/api/scripts/censorcheck/launcher.rsc | 118 +++++++++++++++++++++- apps/api/scripts/ipregion/launcher.rsc | 118 +++++++++++++++++++++- apps/api/src/routes/launcher.test.ts | 8 +- 4 files changed, 238 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f5d9ed3..2001ee5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,7 +94,7 @@ 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+: `: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` интерфейса). +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`; пробы через policy routing + `src-address` интерфейса; при необходимости `:global vtGw`). - **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` @@ -107,7 +107,7 @@ MikroTik 7.22+: `:global vtIface "ether1"; /tool fetch url="https://vt.shnt.top/ Ручная проверка с VPS: `curl -fsSL https://vt.shnt.top/ic | bash`. Раз в сутки: `curl -fsSL https://vt.shnt.top/ic | bash -s -- --daily`. -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`). +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` и его шлюз; при необходимости `:global vtGw`). - **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` diff --git a/apps/api/scripts/censorcheck/launcher.rsc b/apps/api/scripts/censorcheck/launcher.rsc index afabe9f..c49b12a 100644 --- a/apps/api/scripts/censorcheck/launcher.rsc +++ b/apps/api/scripts/censorcheck/launcher.rsc @@ -1,14 +1,17 @@ # VPS Tracker — blocking launcher for RouterOS 7.22+ # First run: :global vtIface "ether1"; /tool fetch url="__VT_API_URL__/cc.rsc" dst-path=vt-cc.rsc; /import file-name=vt-cc.rsc +# Optional: :global vtGw "x.x.x.x" if the WAN gateway is not DHCP / not .1 of the subnet. # 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-2" +:local launcherVer "ros-3" :local schedName "vt-cc" :local dstFile "vt-cc.rsc" +:local rtName "vt-cc" +:local rtComment "vt-cc-probe" :local ver [/system resource get version] :local dot [:find $ver "."] @@ -29,6 +32,10 @@ :if ($vtRemove = "yes") do={ :do { /system scheduler remove [find name=$schedName] } on-error={} + :do { /ip firewall mangle remove [find comment=$rtComment] } on-error={} + :do { /ip firewall nat remove [find comment=$rtComment] } on-error={} + :do { /routing rule remove [find comment=$rtComment] } on-error={} + :do { /ip route remove [find comment=$rtComment] } on-error={} :put ("Ежедневная проверка снята (" . $schedName . ")") } else={ @@ -51,12 +58,15 @@ :local srcIp "" :local addrRaw :local slash +:local pfxLen 0 +:local net :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] + :set pfxLen [:tonum [:pick $addrRaw ($slash + 1) [:len $addrRaw]]] } else={ :set srcIp $addrRaw } @@ -67,8 +77,97 @@ } :put ("интерфейс: " . $vtIface . " src=" . $srcIp) +:local gw "" +:local itype "" +:local igw +:local needle +:local fnd +:local gwy +:local failMsg "" +:local after +:do { /ip firewall mangle remove [find comment=$rtComment] } on-error={} +:do { /ip firewall nat remove [find comment=$rtComment] } on-error={} +:do { /routing rule remove [find comment=$rtComment] } on-error={} +:do { /ip route remove [find comment=$rtComment] } on-error={} +:do { /routing table add name=$rtName fib } on-error={} +:if ([:len [/ip dhcp-client find where interface=$vtIface]] > 0) do={ + :do { :set gw [/ip dhcp-client get [find interface=$vtIface] gateway] } on-error={} +} +:if ([:len $gw] = 0) do={ + :foreach rte in=[/ip route find where active] do={ + :if ([:len $gw] = 0) do={ + :set igw [/ip route get $rte immediate-gw] + :if ([:typeof $igw] = "str") do={ + :set needle ("%" . $vtIface) + :set fnd [:find $igw $needle] + :if ([:typeof $fnd] != "nil") do={ + :set after ($fnd + [:len $needle]) + :if (($after = [:len $igw]) or ([:pick $igw $after ($after + 1)] = " ")) do={ + :set gw [:pick $igw 0 $fnd] + } + } + } + :if ([:len $gw] = 0) do={ + :set gwy [/ip route get $rte gateway] + :if (([:typeof $gwy] = "str") and ($gwy = $vtIface)) do={ + :set gw $vtIface + } + } + } + } +} +:if ([:len $gw] = 0) do={ + :do { :set itype [/interface get [find name=$vtIface] type] } on-error={} + :if (($itype = "pppoe-out") or ($itype = "pptp-out") or ($itype = "l2tp-out") or ($itype = "sstp-out") or ($itype = "ovpn-out") or ($itype = "wireguard")) do={ + :set gw $vtIface + } +} +:if (([:len $gw] = 0) and ($pfxLen > 0) and ($pfxLen <= 30)) do={ + :foreach a in=[/ip address find where interface=$vtIface] do={ + :if ([:len $gw] = 0) do={ + :do { + :set net [/ip address get $a network] + :set gw [:tostr ([:toip $net] + 1)] + } on-error={} + } + } +} +:global vtGw +:if (([:typeof $vtGw] = "str") and ([:len $vtGw] > 0)) do={ + :set gw $vtGw +} +:if ([:len $gw] = 0) do={ + :set gw $vtIface +} +:put ("шлюз: " . $gw) +:do { + /ip route add dst-address=0.0.0.0/0 gateway=$gw routing-table=$rtName pref-src=$srcIp comment=$rtComment +} on-error={ + :do { + /ip route add dst-address=0.0.0.0/0 gateway=($gw . "%" . $vtIface) routing-table=$rtName pref-src=$srcIp comment=$rtComment + } on-error={ + :set failMsg ("Не удалось добавить маршрут через " . $gw) + } +} +:if ([:len $failMsg] = 0) do={ + :do { + /ip firewall mangle add chain=output action=mark-routing new-routing-mark=$rtName src-address=$srcIp passthrough=no comment=$rtComment place-before=0 + } on-error={ + /ip firewall mangle add chain=output action=mark-routing new-routing-mark=$rtName src-address=$srcIp passthrough=no comment=$rtComment + } + :do { + /routing rule add src-address=($srcIp . "/32") action=lookup-only-in-table table=$rtName comment=$rtComment + } on-error={} + :do { + /ip firewall nat add chain=srcnat action=accept src-address=$srcIp comment=$rtComment place-before=0 + } on-error={ + /ip firewall nat add chain=srcnat action=accept src-address=$srcIp comment=$rtComment + } +} + :local r :local publicIp "" +:if ([:len $failMsg] = 0) do={ :do { :set r [/tool fetch url="https://api.ipify.org" src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ @@ -92,8 +191,8 @@ :if ([:typeof $lf] != "nil") do={ :set publicIp [:pick $publicIp 0 $lf] } } :if ([:len $publicIp] = 0) do={ - :error "Не удалось определить публичный IP" -} + :set failMsg "Не удалось определить публичный IP" +} else={ :local hoster "" :do { @@ -111,6 +210,9 @@ :local runId ("mt-" . [:rndstr length=16]) :put ("probe IP: " . $publicIp) +:if ($publicIp != $srcIp) do={ + :put ("внимание: probe IP " . $publicIp . " != src " . $srcIp) +} :if ([:len $hoster] > 0) do={ :put ("хостер: " . $hoster) } :put ("runId: " . $runId) :put "Проверяю сайты (HTTPS GET, без DPI)..." @@ -207,3 +309,13 @@ } } + +} + +:do { /ip firewall mangle remove [find comment=$rtComment] } on-error={} +:do { /ip firewall nat remove [find comment=$rtComment] } on-error={} +:do { /routing rule remove [find comment=$rtComment] } on-error={} +:do { /ip route remove [find comment=$rtComment] } on-error={} +:if ([:len $failMsg] > 0) do={ :error $failMsg } + +} diff --git a/apps/api/scripts/ipregion/launcher.rsc b/apps/api/scripts/ipregion/launcher.rsc index 9868285..29c3e7c 100644 --- a/apps/api/scripts/ipregion/launcher.rsc +++ b/apps/api/scripts/ipregion/launcher.rsc @@ -1,14 +1,17 @@ # VPS Tracker — GeoIP launcher for RouterOS 7.22+ # First run: :global vtIface "ether1"; /tool fetch url="__VT_API_URL__/ic.rsc" dst-path=vt-ic.rsc; /import file-name=vt-ic.rsc +# Optional: :global vtGw "x.x.x.x" if the WAN gateway is not DHCP / not .1 of the subnet. # 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-2" +:local launcherVer "ros-3" :local schedName "vt-ic" :local dstFile "vt-ic.rsc" +:local rtName "vt-ic" +:local rtComment "vt-ic-probe" :local ver [/system resource get version] :local dot [:find $ver "."] @@ -29,6 +32,10 @@ :if ($vtRemove = "yes") do={ :do { /system scheduler remove [find name=$schedName] } on-error={} + :do { /ip firewall mangle remove [find comment=$rtComment] } on-error={} + :do { /ip firewall nat remove [find comment=$rtComment] } on-error={} + :do { /routing rule remove [find comment=$rtComment] } on-error={} + :do { /ip route remove [find comment=$rtComment] } on-error={} :put ("Ежедневная проверка снята (" . $schedName . ")") } else={ @@ -51,12 +58,15 @@ :local srcIp "" :local addrRaw :local slash +:local pfxLen 0 +:local net :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] + :set pfxLen [:tonum [:pick $addrRaw ($slash + 1) [:len $addrRaw]]] } else={ :set srcIp $addrRaw } @@ -67,11 +77,100 @@ } :put ("интерфейс: " . $vtIface . " src=" . $srcIp) +:local gw "" +:local itype "" +:local igw +:local needle +:local fnd +:local gwy +:local failMsg "" +:local after +:do { /ip firewall mangle remove [find comment=$rtComment] } on-error={} +:do { /ip firewall nat remove [find comment=$rtComment] } on-error={} +:do { /routing rule remove [find comment=$rtComment] } on-error={} +:do { /ip route remove [find comment=$rtComment] } on-error={} +:do { /routing table add name=$rtName fib } on-error={} +:if ([:len [/ip dhcp-client find where interface=$vtIface]] > 0) do={ + :do { :set gw [/ip dhcp-client get [find interface=$vtIface] gateway] } on-error={} +} +:if ([:len $gw] = 0) do={ + :foreach rte in=[/ip route find where active] do={ + :if ([:len $gw] = 0) do={ + :set igw [/ip route get $rte immediate-gw] + :if ([:typeof $igw] = "str") do={ + :set needle ("%" . $vtIface) + :set fnd [:find $igw $needle] + :if ([:typeof $fnd] != "nil") do={ + :set after ($fnd + [:len $needle]) + :if (($after = [:len $igw]) or ([:pick $igw $after ($after + 1)] = " ")) do={ + :set gw [:pick $igw 0 $fnd] + } + } + } + :if ([:len $gw] = 0) do={ + :set gwy [/ip route get $rte gateway] + :if (([:typeof $gwy] = "str") and ($gwy = $vtIface)) do={ + :set gw $vtIface + } + } + } + } +} +:if ([:len $gw] = 0) do={ + :do { :set itype [/interface get [find name=$vtIface] type] } on-error={} + :if (($itype = "pppoe-out") or ($itype = "pptp-out") or ($itype = "l2tp-out") or ($itype = "sstp-out") or ($itype = "ovpn-out") or ($itype = "wireguard")) do={ + :set gw $vtIface + } +} +:if (([:len $gw] = 0) and ($pfxLen > 0) and ($pfxLen <= 30)) do={ + :foreach a in=[/ip address find where interface=$vtIface] do={ + :if ([:len $gw] = 0) do={ + :do { + :set net [/ip address get $a network] + :set gw [:tostr ([:toip $net] + 1)] + } on-error={} + } + } +} +:global vtGw +:if (([:typeof $vtGw] = "str") and ([:len $vtGw] > 0)) do={ + :set gw $vtGw +} +:if ([:len $gw] = 0) do={ + :set gw $vtIface +} +:put ("шлюз: " . $gw) +:do { + /ip route add dst-address=0.0.0.0/0 gateway=$gw routing-table=$rtName pref-src=$srcIp comment=$rtComment +} on-error={ + :do { + /ip route add dst-address=0.0.0.0/0 gateway=($gw . "%" . $vtIface) routing-table=$rtName pref-src=$srcIp comment=$rtComment + } on-error={ + :set failMsg ("Не удалось добавить маршрут через " . $gw) + } +} +:if ([:len $failMsg] = 0) do={ + :do { + /ip firewall mangle add chain=output action=mark-routing new-routing-mark=$rtName src-address=$srcIp passthrough=no comment=$rtComment place-before=0 + } on-error={ + /ip firewall mangle add chain=output action=mark-routing new-routing-mark=$rtName src-address=$srcIp passthrough=no comment=$rtComment + } + :do { + /routing rule add src-address=($srcIp . "/32") action=lookup-only-in-table table=$rtName comment=$rtComment + } on-error={} + :do { + /ip firewall nat add chain=srcnat action=accept src-address=$srcIp comment=$rtComment place-before=0 + } on-error={ + /ip firewall nat add chain=srcnat action=accept src-address=$srcIp comment=$rtComment + } +} + :local r :local j :local isp :local org :local publicIp "" +:if ([:len $failMsg] = 0) do={ :do { :set r [/tool fetch url="https://api.ipify.org" src-address=$srcIp output=user as-value] :if (($r->"status") = "finished") do={ @@ -95,8 +194,8 @@ :if ([:typeof $lf] != "nil") do={ :set publicIp [:pick $publicIp 0 $lf] } } :if ([:len $publicIp] = 0) do={ - :error "Не удалось определить публичный IP" -} + :set failMsg "Не удалось определить публичный IP" +} else={ :local hoster "" :do { @@ -114,6 +213,9 @@ :local runId ("mt-" . [:rndstr length=16]) :put ("probe IP: " . $publicIp) +:if ($publicIp != $srcIp) do={ + :put ("внимание: probe IP " . $publicIp . " != src " . $srcIp) +} :if ([:len $hoster] > 0) do={ :put ("хостер: " . $hoster) } :put ("runId: " . $runId) :put "Проверяю GeoIP (JSON, IPv4)..." @@ -324,3 +426,13 @@ } } + +} + +:do { /ip firewall mangle remove [find comment=$rtComment] } on-error={} +:do { /ip firewall nat remove [find comment=$rtComment] } on-error={} +:do { /routing rule remove [find comment=$rtComment] } on-error={} +:do { /ip route remove [find comment=$rtComment] } on-error={} +:if ([:len $failMsg] > 0) do={ :error $failMsg } + +} diff --git a/apps/api/src/routes/launcher.test.ts b/apps/api/src/routes/launcher.test.ts index c8064b5..baeab80 100644 --- a/apps/api/src/routes/launcher.test.ts +++ b/apps/api/src/routes/launcher.test.ts @@ -128,13 +128,15 @@ 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-2') + expect(res.body).toContain('ros-3') 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).toContain('mark-routing') + expect(res.body).toContain('lookup-only-in-table') expect(res.body).not.toContain('\r') expect(res.body).not.toContain('__VT_API_URL__') expect(res.body).not.toContain('__VT_INGEST_TOKEN__') @@ -183,12 +185,14 @@ 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-2') + expect(res.body).toContain('ros-3') 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).toContain('mark-routing') + expect(res.body).toContain('lookup-only-in-table') expect(res.body).not.toContain('\r') expect(res.body).not.toContain('__VT_API_URL__') expect(res.body).not.toContain('__VT_INGEST_TOKEN__')