feat(policy): именованные наборы правил с DNS и M:N привязкой к агентам
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m44s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

Правила живут в policy_sets; evaluate мержит назначенные наборы; источник list|CIDR|hostname с кэшем DNS.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-20 22:11:03 +07:00
co-authored by Cursor
parent cad05659b1
commit 0bed4367c7
18 changed files with 1524 additions and 312 deletions
+3
View File
@@ -166,9 +166,12 @@ export async function refreshIpList(db: Db, listId: string): Promise<void> {
}
}
import { refreshAllHostnameRules } from '../policy/resolve-hostname.js'
export async function refreshAllLists(db: Db): Promise<void> {
for (const list of repos.listIpLists(db)) {
if (list.type === 'static') continue
await refreshIpList(db, list.id)
}
await refreshAllHostnameRules(db)
}
+19 -9
View File
@@ -28,26 +28,36 @@ function expandList(db: Db, listId: string | null | undefined): string[] {
return repos.listIpListEntries(db, listId).map((e) => e.cidr)
}
/** Evaluate allow/deny sets for an agent. */
function expandRule(
db: Db,
rule: {
cidr: string | null
listId: string | null
hostname: string | null
id: string
},
): string[] {
if (rule.cidr?.trim()) return [rule.cidr.trim()]
if (rule.hostname?.trim()) {
return repos.listResolvedForRule(db, rule.id).map((r) => r.cidr)
}
return expandList(db, rule.listId)
}
/** Evaluate allow/deny sets for an agent from assigned policy sets. */
export function evaluateAgentPolicy(db: Db, agentId: string): EvaluatedPolicy {
const agent = repos.getAgent(db, agentId)
if (!agent) {
throw new Error(`agent not found: ${agentId}`)
}
const agentRules = repos.listPolicyRules(db, agentId)
const tenantRules = repos.listPolicyRules(db, null)
const ordered = [...agentRules, ...tenantRules].sort(
(a, b) => a.priority - b.priority,
)
const ordered = repos.listPolicyRulesForAgent(db, agentId)
const deny: string[] = []
const allow: string[] = []
for (const rule of ordered) {
const cidrs = rule.cidr
? [rule.cidr]
: expandList(db, rule.listId)
const cidrs = expandRule(db, rule)
if (rule.action === 'deny') deny.push(...cidrs)
else allow.push(...cidrs)
}
@@ -0,0 +1,79 @@
import { resolve4, resolve6 } from 'node:dns/promises'
import { createHash } from 'node:crypto'
import type { Db } from '@evofw/db'
import { repos } from '@evofw/db'
function uniq(cidrs: string[]): string[] {
return [...new Set(cidrs.map((c) => c.trim()).filter(Boolean))].sort()
}
function hashCidrs(cidrs: string[]): string {
return `sha256:${createHash('sha256').update(cidrs.join('\n')).digest('hex')}`
}
/** Resolve FQDN to /32 and /128 CIDRs. Throws if nothing resolved. */
export async function resolveHostnameToCidrs(hostname: string): Promise<string[]> {
const host = hostname.trim().replace(/\.$/, '').toLowerCase()
if (!host) throw new Error('hostname empty')
const out: string[] = []
try {
const a = await resolve4(host)
out.push(...a.map((ip) => `${ip}/32`))
} catch {
/* ignore A failures */
}
try {
const aaaa = await resolve6(host)
out.push(...aaaa.map((ip) => `${ip}/128`))
} catch {
/* ignore AAAA failures */
}
const cidrs = uniq(out)
if (cidrs.length === 0) {
throw new Error(`DNS resolve failed for ${host}: no A/AAAA records`)
}
return cidrs
}
export async function resolveAndStoreHostnameRule(
db: Db,
ruleId: string,
hostname: string,
): Promise<string[]> {
const cidrs = await resolveHostnameToCidrs(hostname)
const prev = repos
.listResolvedForRule(db, ruleId)
.map((r) => r.cidr)
.sort()
const nextHash = hashCidrs(cidrs)
const prevHash = hashCidrs(prev)
repos.replaceResolvedForRule(db, ruleId, cidrs)
return cidrs.length && nextHash !== prevHash ? cidrs : cidrs
}
/** Re-resolve all hostname rules; bump agents when cache changes. */
export async function refreshAllHostnameRules(db: Db): Promise<void> {
const rules = repos.listHostnameRules(db)
const changedSets = new Set<string>()
for (const rule of rules) {
if (!rule.hostname) continue
try {
const prev = hashCidrs(
repos.listResolvedForRule(db, rule.id).map((r) => r.cidr),
)
const cidrs = await resolveHostnameToCidrs(rule.hostname)
const next = hashCidrs(cidrs)
repos.replaceResolvedForRule(db, rule.id, cidrs)
if (prev !== next) changedSets.add(rule.setId)
} catch {
/* keep previous cache on transient DNS failure */
}
}
for (const setId of changedSets) {
repos.bumpAgentsForSet(db, setId)
}
}