diff --git a/apps/api/src/agent-scripts/mikrotik-install.rsc b/apps/api/src/agent-scripts/mikrotik-install.rsc index a3733d8..1e5ffaa 100644 --- a/apps/api/src/agent-scripts/mikrotik-install.rsc +++ b/apps/api/src/agent-scripts/mikrotik-install.rsc @@ -4,14 +4,15 @@ # # Re-import on an already-enrolled router: skips enroll, refreshes sync + scheduler (token kept). # -# Blacklist: drop EVOFW_DENY on input+forward -# Whitelist: accept EVOFW_ALLOW + drop others on forward only (input stays open for Winbox/SSH) +# Sync uses GET /v1/agent/policy (JSON) + address-list rebuild — not /import of huge .rsc +# (large /import often fails silently while apply-report still looks "ok"). :global EvofwCpUrl :global EvofwSeed :global EvofwName :global EvofwInstallLinkId :global EvofwToken +:global EvofwLastHash :if ([:typeof $EvofwCpUrl] = "nothing" || [:len $EvofwCpUrl] = 0) do={ :error "EvofwCpUrl required" } :if ([:typeof $EvofwSeed] = "nothing" || [:len $EvofwSeed] = 0) do={ :error "EvofwSeed required" } @@ -30,7 +31,7 @@ :set token ("evofw_" . [:tostr [/system clock get time]] . [:tostr [/system resource get cpu-load]] . [:tostr [/system resource get free-memory]] . [:tostr [:rndnum from=100000 to=999999]]) } - :local body ("{\"name\":\"" . $EvofwName . "\",\"hostname\":\"" . [/system identity get name] . "\",\"platform\":\"mikrotik\",\"token\":\"" . $token . "\",\"client_version\":\"rsc/1\"") + :local body ("{\"name\":\"" . $EvofwName . "\",\"hostname\":\"" . [/system identity get name] . "\",\"platform\":\"mikrotik\",\"token\":\"" . $token . "\",\"client_version\":\"rsc/2\"") :if ([:typeof $EvofwInstallLinkId] != "nothing" && [:len $EvofwInstallLinkId] > 0) do={ :set body ($body . ",\"install_link_id\":\"" . $EvofwInstallLinkId . "\"") } @@ -43,15 +44,17 @@ } :do { /system script remove [find name="evofw-env"] } on-error={} - /system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $token . "\" ") + /system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $token . "\"; :global EvofwLastHash \"\" ") :set EvofwToken $token + :set EvofwLastHash "" } else={ - :put "evofw: already enrolled — updating sync script (token kept)" + :put "evofw: already enrolled — updating sync script (token kept); forcing full list rebuild" + :set EvofwLastHash "" :do { /system script remove [find name="evofw-env"] } on-error={} - /system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $EvofwToken . "\" ") + /system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $EvofwToken . "\"; :global EvofwLastHash \"\" ") } -# Filter rules (idempotent by comment) — names must match policy.rsc (mikrotik-rsc.ts) +# Filter rules (idempotent by comment) — names must match policy toggles below :do { /ip firewall filter remove [find comment~"^evofw-"] } on-error={} /ip firewall filter add chain=input action=drop src-address-list=EVOFW_DENY comment=evofw-deny-drop-input disabled=no @@ -59,11 +62,12 @@ /ip firewall filter add chain=forward action=accept src-address-list=EVOFW_ALLOW comment=evofw-allow-accept-forward disabled=no /ip firewall filter add chain=forward action=drop comment=evofw-default-drop-forward disabled=yes -# Sync: fetch policy.rsc → import address-lists + toggle mode +# Sync: JSON policy → rebuild address-lists (reliable for ~1k CIDRs; no nested /import) :do { /system script remove [find name="evofw-sync"] } on-error={} /system script add name=evofw-sync policy=read,write,policy,test source={ :global EvofwCpUrl :global EvofwToken + :global EvofwLastHash :if ([:typeof $EvofwCpUrl] = "nothing" || [:typeof $EvofwToken] = "nothing") do={ /system script run evofw-env } @@ -71,23 +75,67 @@ :log error "evofw: missing EvofwCpUrl/EvofwToken" :error "evofw env missing" } + :if ([:typeof $EvofwLastHash] = "nothing") do={ :set EvofwLastHash "" } + + :local syncOk 0 :do { - /tool fetch url=($EvofwCpUrl . "/v1/agent/policy.rsc") http-header-field=("Authorization: Bearer " . $EvofwToken) dst-path=evofw-policy.rsc - /import file-name=evofw-policy.rsc + :local result [/tool fetch url=($EvofwCpUrl . "/v1/agent/policy") http-header-field=("Authorization: Bearer " . $EvofwToken . ",Accept: application/json") output=user as-value] + :if (($result->"status") != "finished") do={ + :log warning ("evofw: policy fetch status=" . ($result->"status")) + :error "policy fetch failed" + } + :local raw ($result->"data") + :if ([:len $raw] < 10) do={ :error "empty policy body" } + :local p [:deserialize $raw from=json] + :local hash ($p->"hash") + :if ([:typeof $hash] = "nothing" || [:len $hash] = 0) do={ :error "policy missing hash (pending?)" } + + :if ($hash = $EvofwLastHash) do={ + :log info ("evofw: unchanged " . $hash) + :set syncOk 1 + } else={ + :local deny ($p->"deny_cidrs") + :local allow ($p->"allow_cidrs") + :do { /ip firewall address-list remove [find list=EVOFW_DENY] } on-error={} + :do { /ip firewall address-list remove [find list=EVOFW_ALLOW] } on-error={} + :foreach a in=$deny do={ + :do { /ip firewall address-list add list=EVOFW_DENY address=$a comment=evofw } on-error={} + } + :foreach a in=$allow do={ + :do { /ip firewall address-list add list=EVOFW_ALLOW address=$a comment=evofw } on-error={} + } + :local da ($p->"default_action") + :do { /ip firewall filter set [find comment=evofw-deny-drop-input] disabled=no } on-error={} + :do { /ip firewall filter set [find comment=evofw-deny-drop-forward] disabled=no } on-error={} + :do { /ip firewall filter set [find comment=evofw-allow-accept-forward] disabled=no } on-error={} + :if ($da = "drop") do={ + :do { /ip firewall filter set [find comment=evofw-default-drop-forward] disabled=no } on-error={} + } else={ + :do { /ip firewall filter set [find comment=evofw-default-drop-forward] disabled=yes } on-error={} + } + :set EvofwLastHash $hash + :do { /system script remove [find name="evofw-env"] } on-error={} + /system script add name=evofw-env policy=read,write,policy,test source=(" :global EvofwCpUrl \"" . $EvofwCpUrl . "\"; :global EvofwToken \"" . $EvofwToken . "\"; :global EvofwLastHash \"" . $hash . "\" ") + :log info ("evofw: applied " . $hash) + :set syncOk 1 + } } on-error={ - :log warning "evofw: policy sync failed (pending approval or network)" + :log warning "evofw: policy sync failed (pending approval, network, or JSON)" + } + + :if ($syncOk = 1) do={ + :local denyCnt [:len [/ip firewall address-list find list=EVOFW_DENY]] + :local allowCnt [:len [/ip firewall address-list find list=EVOFW_ALLOW]] + :local cnt ($denyCnt + $allowCnt) + :local report ("{\"status\":\"ok\",\"prefix_count\":" . $cnt . ",\"kernel_method\":\"address-list\",\"source\":\"mikrotik\"}") + :do { + /tool fetch url=($EvofwCpUrl . "/v1/agent/apply-report") http-method=post http-header-field=("Authorization: Bearer " . $EvofwToken . ",Content-Type: application/json") http-data=$report keep-result=no + } on-error={} + :do { + /tool fetch url=($EvofwCpUrl . "/v1/agent/heartbeat") http-method=post http-header-field=("Authorization: Bearer " . $EvofwToken . ",Content-Type: application/json") http-data="{\"source\":\"mikrotik\"}" keep-result=no + } on-error={} + :log info ("evofw: sync done deny=" . $denyCnt . " allow=" . $allowCnt) } - :local denyCnt [:len [/ip firewall address-list find list=EVOFW_DENY]] - :local allowCnt [:len [/ip firewall address-list find list=EVOFW_ALLOW]] - :local cnt ($denyCnt + $allowCnt) - :local report ("{\"status\":\"ok\",\"prefix_count\":" . $cnt . ",\"kernel_method\":\"address-list\",\"source\":\"mikrotik\"}") - :do { - /tool fetch url=($EvofwCpUrl . "/v1/agent/apply-report") http-method=post http-header-field=("Authorization: Bearer " . $EvofwToken . ",Content-Type: application/json") http-data=$report keep-result=no - } on-error={} - :do { - /tool fetch url=($EvofwCpUrl . "/v1/agent/heartbeat") http-method=post http-header-field=("Authorization: Bearer " . $EvofwToken . ",Content-Type: application/json") http-data="{\"source\":\"mikrotik\"}" keep-result=no - } on-error={} - :log info ("evofw: sync done deny=" . $denyCnt . " allow=" . $allowCnt) } :do { /system scheduler remove [find name="evofw-sync"] } on-error={} diff --git a/apps/api/src/services/install-links.test.ts b/apps/api/src/services/install-links.test.ts index 7b0aa46..47f90fd 100644 --- a/apps/api/src/services/install-links.test.ts +++ b/apps/api/src/services/install-links.test.ts @@ -141,7 +141,8 @@ describe('install-links', () => { expect(byId.body).toContain(':global EvofwCpUrl "https://fw.example.com"') expect(byId.body).toContain(`:global EvofwInstallLinkId "${body.id}"`) expect(byId.body).toContain('evofw-deny-drop-input') - expect(byId.body).toContain('/v1/agent/policy.rsc') + expect(byId.body).toContain('/v1/agent/policy') + expect(byId.body).toContain(':deserialize') }) it('approved agent can fetch empty policy without rule sets', async () => { diff --git a/docs/agents.md b/docs/agents.md index 465e2ea..212435c 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -67,7 +67,9 @@ Install RSC: 1. Enroll (с `install_link_id` → агент Invited → Pending). 2. Создаёт filter-правила `evofw-*` и address-list `EVOFW_DENY` / `EVOFW_ALLOW`. -3. Scheduler `evofw-sync` каждую минуту: `GET /v1/agent/policy.rsc` → `/import` (списки + default). +3. Scheduler `evofw-sync` каждую минуту: `GET /v1/agent/policy` (JSON) → rebuild address-list + toggle default. Не использует `/import` огромного `.rsc` (на больших списках часто падает молча). + +Лог: `/log print where message~"evofw"`. Ручной sync: `/system script run evofw-sync`. **Default action** задаётся на **агенте** (`default_action: accept | drop`):