feat(launcher): требовать интерфейс для проверок MikroTik
Docker / build (push) Failing after 22s

Все пробы и ingest идут через src-address выбранного WAN, scheduler сохраняет vtIface.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-08-26 00:22:25 +07:00
co-authored by Cursor
parent c844874b0a
commit a9e3cce113
6 changed files with 97 additions and 27 deletions
+4 -4
View File
@@ -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)
+41 -8
View File
@@ -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 . ")")
+42 -9
View File
@@ -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 . ")")
+6 -2
View File
@@ -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__')
+2 -2
View File
@@ -59,9 +59,9 @@ export const CENSORCHECK_STATUS_LABELS: Record<string, string> = {
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`
+2 -2
View File
@@ -58,9 +58,9 @@ export const IPREGION_STATUS_LABELS: Record<string, string> = {
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`